2424
2525
2626import logging
27+ import pandas as pd
2728
2829from ssvc .decision_tables .base import (
2930 DecisionTable ,
@@ -215,7 +216,7 @@ def mapping2mermaid(rows: list[dict[str:str]], title: str = None) -> str:
215216
216217
217218def dt2df_md (
218- dt : " DecisionTable" ,
219+ dt : DecisionTable ,
219220 longform : bool = True ,
220221) -> str :
221222 """
@@ -224,7 +225,7 @@ def dt2df_md(
224225 decision_table (DecisionTable): The decision table to convert.
225226 longform (bool): Whether to return the longform or shortform DataFrame.
226227 Returns:
227- str: A string representation of the DataFrame in CSV format.
228+ str: A string representation of the DataFrame in text/markdown format.
228229 """
229230 if longform :
230231 df = decision_table_to_longform_df (dt )
@@ -234,6 +235,161 @@ def dt2df_md(
234235 df .index .rename ("Row" , inplace = True )
235236 return df .to_markdown (index = True )
236237
238+ def dt2df_html (
239+ dt : DecisionTable ,
240+ longform : bool = True ) -> str :
241+ """
242+ Converts a Decision Tree and represent it in friendly HTML Code
243+ Args:
244+ decision_table (DecisionTable): The decision table to convert.
245+ longform (bool): Whether to return the longform or shortform DataFram, defaults to true
246+ Returns:
247+ str: A string representation of the DataFrame in HTML format.
248+ """
249+
250+ if longform :
251+ df = decision_table_to_longform_df (dt )
252+ else :
253+ df = decision_table_to_shortform_df (dt )
254+
255+ df = decision_table_to_longform_df (dt )
256+ ncols = len (df .columns )
257+ nrows = len (df )
258+
259+ # Precompute rowspan info for every cell
260+ # rowspan[i][j] = number of rows this cell should span; 0 means skip (because merged above)
261+ rowspan = [[1 ]* ncols for _ in range (nrows )]
262+
263+ for col in range (ncols ):
264+ r = 0
265+ while r < nrows :
266+ start = r
267+ val = df .iat [r , col ] #data_rows[r][col]
268+ # Count how many subsequent rows have same value
269+ while r + 1 < nrows and df .iat [r + 1 , col ] == val :#data_rows[r + 1][col] == val:
270+ r += 1
271+ span = r - start + 1
272+ if span > 1 :
273+ # Assign span to first, mark rest as 0 (skip)
274+ rowspan [start ][col ] = span
275+ for k in range (start + 1 , start + span ):
276+ rowspan [k ][col ] = 0
277+ r += 1
278+
279+ # Build HTML
280+ html = ["""<style>
281+ table,th,td,tr { border-spacing: 0px; border: 1px solid cyan; padding: 0px; font-family: verdana,courier }
282+ .decision_table th { font-weight: bold; }
283+ td.decision_point { vertical-align: middle}
284+ td.outcome { font-style: italic; font-weight: bold}
285+ </style>""" ]
286+ html .append ("<table class=\" decision_table\" >" )
287+ html .append (" <tr>" + "" .join (f"<th>{ h } </th>" for h in df .columns ) + "</tr>" )
288+
289+ for i , row in df .iterrows (): #for i, row in enumerate(df):
290+ cells = []
291+ j = 0
292+ for _ , val in row .items (): #enumerate(row):
293+ tdtype = "decision_point"
294+ if j == len (row ) - 1 :
295+ tdtype = "outcome"
296+ if rowspan [i ][j ] > 0 :
297+ span = rowspan [i ][j ]
298+ if span > 1 :
299+ cells .append (f'<td rowspan="{ span } " class="{ tdtype } ">{ val } </td>' )
300+ else :
301+ cells .append (f'<td class="{ tdtype } ">{ val } </td>' )
302+ j = j + 1
303+ html .append (" <tr>" + "" .join (cells ) + "</tr>" )
304+
305+ html .append ("</table>" )
306+ return "" .join (html )
307+
308+ def build_tree (df : pd .DataFrame , columns : pd .Index | list [str ]) -> dict [str , dict [str , str ] | list [str ]] | list [str ]:
309+ """
310+ Helper function recursively build a nested dict:
311+ {feature_value: subtree_or_list_of_outcomes}
312+ This tree should preserve the original row order of the DataFrame.
313+ """
314+ # Base case: if only one column is left, it's the outcome.
315+ if len (columns ) == 1 :
316+ # Last column: return a list of outcomes.
317+ return df [columns [0 ]].astype (str ).tolist ()
318+
319+ # Get the first feature column and the rest of the columns.
320+ first , rest = columns [0 ], columns [1 :]
321+ tree = {}
322+
323+ # Iterate through the unique values of the first column in the order they appear.
324+ # This is the key change to preserve the original CSV order.
325+ for val in df [first ].unique ():
326+ # Filter the DataFrame to get the group for the current value.
327+ group = df [df [first ] == val ]
328+ # Recursively build the subtree for this group.
329+ tree [str (val )] = build_tree (group , rest )
330+
331+ return tree
332+
333+ def draw_tree (node : dict | list , prefix : str = "" , lines : list | None = None ) -> list :
334+ """
335+ Pretty-print nested dict/list as a tree.
336+ """
337+ if lines is None :
338+ lines = []
339+
340+ if isinstance (node , dict ):
341+ items = list (node .items ())
342+ for i , (k , v ) in enumerate (items ):
343+ # Determine the branch characters for the tree.
344+ branch = "└── " if i == len (items ) - 1 else "├── "
345+ lines .append (prefix + branch + k + " " * 4 )
346+
347+ # Calculate the prefix for the next level of the tree.
348+ next_prefix = prefix + (" " * 16 if i == len (items ) - 1 else "│" + " " * 15 )
349+ # Recursively draw the subtree.
350+ draw_tree (v , next_prefix , lines )
351+ else : # list of outcomes
352+ for i , leaf in enumerate (node ):
353+ # Determine the branch characters for the leaves.
354+ branch = "└── " if i == len (node ) - 1 else "├── "
355+ lines .append (prefix + branch + f"[{ leaf } ]" )
356+
357+ return lines
358+
359+ def ascii_tree (dt : DecisionTable , df : pd .DataFrame | None = None ) -> str :
360+ """
361+ Reads a Pandas data frame, builds a decision tree, and returns its ASCII representation.
362+ """
363+ # Check for the optional 'row' column and drop it if it exists.
364+ if df == None :
365+ df = decision_table_to_longform_df (dt )
366+
367+ if 'row' in df .columns :
368+ df .drop (columns = 'row' , inplace = True )
369+
370+ # Separate feature columns from the outcome column.
371+ feature_cols = list (df .columns [:- 1 ])
372+ outcome_col = df .columns [- 1 ]
373+
374+ # Build the tree structure.
375+ tree = build_tree (df , feature_cols + [outcome_col ])
376+ # Draw the tree into a list of strings.
377+ lines = draw_tree (tree )
378+
379+ # Generate the header line.
380+ header = ""
381+ for item in df .columns :
382+ if len (item ) > 14 :
383+ header = header + item [0 :12 ] + ".." + " | "
384+ else :
385+ header = header + item + " " * (14 - len (item )) + " | "
386+
387+ # Generate the separator line.
388+ sep = "-" * len (header )
389+
390+ # Combine the header, separator, and tree lines into a single string.
391+ return "\n " .join ([header , sep ] + lines )
392+
237393
238394def main ():
239395 pass
0 commit comments