| """ |
| Utility functions for building and displaying taxonomic trees. |
| """ |
|
|
| import pandas as pd |
| from typing import Dict, List, Any, Optional |
| from collections import defaultdict |
|
|
|
|
| def build_taxonomic_tree(df: pd.DataFrame) -> Dict[str, Any]: |
| """ |
| Build a hierarchical taxonomic tree from a dataframe. |
| |
| Args: |
| df: DataFrame containing taxonomic columns |
| |
| Returns: |
| Nested dictionary representing the taxonomic tree with counts |
| """ |
| taxonomic_levels = ['kingdom', 'phylum', 'class', 'order', 'family', 'genus', 'species'] |
|
|
| |
| df_clean = df[df['kingdom'].notna()].copy() |
|
|
| tree = defaultdict(lambda: defaultdict(lambda: defaultdict(lambda: defaultdict( |
| lambda: defaultdict(lambda: defaultdict(lambda: defaultdict(int))))))) |
|
|
| def _val(row, col): |
| """Get column value, replacing NaN/None/empty with 'Unknown'.""" |
| v = row.get(col, 'Unknown') |
| if pd.isna(v) or v == '': |
| return 'Unknown' |
| return v |
|
|
| for _, row in df_clean.iterrows(): |
| |
| kingdom = _val(row, 'kingdom') |
| phylum = _val(row, 'phylum') |
| class_name = _val(row, 'class') |
| order = _val(row, 'order') |
| family = _val(row, 'family') |
| genus = _val(row, 'genus') |
| species = _val(row, 'species') |
|
|
| |
| tree[kingdom][phylum][class_name][order][family][genus][species] += 1 |
|
|
| return dict(tree) |
|
|
|
|
| def format_tree_string(tree: Dict[str, Any], max_depth: int = 7, min_count: int = 1) -> str: |
| """ |
| Format the taxonomic tree as a string similar to the 'tree' command output. |
| |
| Args: |
| tree: Taxonomic tree dictionary |
| max_depth: Maximum depth to display |
| min_count: Minimum count to include in the tree |
| |
| Returns: |
| Formatted tree string |
| """ |
| lines = [] |
|
|
| def format_level(node, level=0, prefix="", is_last=True, path=""): |
| if level >= max_depth: |
| return |
|
|
| if isinstance(node, dict): |
| items = list(node.items()) |
| |
| if level == 6: |
| items = sorted(items, key=lambda x: x[1] if isinstance(x[1], int) else 0, reverse=True) |
| else: |
| |
| items = sorted(items, key=lambda x: x[0]) |
|
|
| |
| items = [(k, v) for k, v in items if ( |
| isinstance(v, int) and v >= min_count) or ( |
| isinstance(v, dict) and any( |
| get_total_count(subv) >= min_count for subv in v.values() |
| ) |
| )] |
|
|
| for i, (key, value) in enumerate(items): |
| is_last_item = (i == len(items) - 1) |
|
|
| |
| if level == 0: |
| connector = "" |
| new_prefix = "" |
| else: |
| connector = "βββ " if is_last_item else "βββ " |
| new_prefix = prefix + (" " if is_last_item else "β ") |
|
|
| |
| if isinstance(value, int): |
| count = value |
| count_str = f" ({count})" |
| else: |
| count = get_total_count(value) |
| count_str = f" ({count})" if count > 0 else "" |
|
|
| |
| lines.append(f"{prefix}{connector}{key}{count_str}") |
|
|
| |
| if isinstance(value, dict): |
| format_level(value, level + 1, new_prefix, is_last_item, f"{path}/{key}") |
|
|
| format_level(tree) |
| return "\n".join(lines) |
|
|
|
|
| def get_total_count(node: Any) -> int: |
| """ |
| Get the total count for a tree node. |
| |
| Args: |
| node: Tree node (dict or int) |
| |
| Returns: |
| Total count for this node and all children |
| """ |
| if isinstance(node, int): |
| return node |
| elif isinstance(node, dict): |
| return sum(get_total_count(child) for child in node.values()) |
| else: |
| return 0 |
|
|
|
|
| def get_tree_statistics(tree: Dict[str, Any]) -> Dict[str, int]: |
| """ |
| Get statistics about the taxonomic tree. |
| |
| Args: |
| tree: Taxonomic tree dictionary |
| |
| Returns: |
| Dictionary with statistics |
| """ |
| stats = { |
| 'total_records': get_total_count(tree), |
| 'kingdoms': len(tree), |
| 'phyla': 0, |
| 'classes': 0, |
| 'orders': 0, |
| 'families': 0, |
| 'genera': 0, |
| 'species': 0 |
| } |
|
|
| for kingdom, phyla in tree.items(): |
| stats['phyla'] += len(phyla) |
| for phylum, classes in phyla.items(): |
| stats['classes'] += len(classes) |
| for class_name, orders in classes.items(): |
| stats['orders'] += len(orders) |
| for order, families in orders.items(): |
| stats['families'] += len(families) |
| for family, genera in families.items(): |
| stats['genera'] += len(genera) |
| for genus, species in genera.items(): |
| stats['species'] += len(species) |
|
|
| return stats |
|
|