"""GROUP BY + agregaciones sobre datos tabulares list[dict].""" from collections import defaultdict def aggregate_by_group( rows: list[dict], group_by: list[str], aggs: dict[str, str], ) -> list[dict]: """Agrupa filas por una o varias columnas y aplica agregaciones. Equivalente a SQL GROUP BY con funciones de agregacion. La funcion 'collect' acumula todos los valores en una lista. Los valores None se ignoran en agregaciones numericas (sum, mean, min, max). Args: rows: Lista de dicts con los datos. group_by: Lista de columnas por las que agrupar. aggs: Dict de {columna: funcion}. Funciones: sum, mean, count, min, max, first, last, collect. Returns: Lista de dicts con las columnas de group_by mas los campos agregados. El orden de las filas sigue el orden de primera aparicion del grupo. """ # Mantener orden de grupos con lista de claves group_keys: list[tuple] = [] seen_groups: set[tuple] = set() buckets: dict[tuple, dict[str, list]] = defaultdict(lambda: defaultdict(list)) for row in rows: gk = tuple(row.get(col) for col in group_by) if gk not in seen_groups: seen_groups.add(gk) group_keys.append(gk) for col in aggs: val = row.get(col) buckets[gk][col].append(val) def _aggregate(vals: list, func: str): if func == "collect": return vals if func == "count": return len(vals) if func == "first": return vals[0] if vals else None if func == "last": return vals[-1] if vals else None # Para sum, mean, min, max: ignorar None numeric = [v for v in vals if v is not None] if not numeric: return None if func == "sum": return sum(numeric) if func == "mean": return sum(numeric) / len(numeric) if func == "min": return min(numeric) if func == "max": return max(numeric) raise ValueError(f"Funcion de agregacion no soportada: {func}") result = [] for gk in group_keys: record: dict = dict(zip(group_by, gk)) for col, func in aggs.items(): record[col] = _aggregate(buckets[gk][col], func) result.append(record) return result