"""Tipos de columna y resumen de análisis.""" from __future__ import annotations from dataclasses import dataclass, field from typing import Optional TYPE_INTEGER = "integer" TYPE_DECIMAL = "decimal" TYPE_DATE = "date" TYPE_BOOLEAN = "boolean" TYPE_CATEGORICAL = "categorical" TYPE_TEXT = "text" TYPE_MIXED = "mixed" TYPE_EMPTY = "empty" @dataclass class ColumnSummary: """Resumen inteligente para una columna del DataFrame.""" name: str inferred_type: str example: str = "" samples: list[str] = field(default_factory=list) null_count: int = 0 null_pct: float = 0.0 unique_count: int = 0 unique_pct: float = 0.0 min: Optional[float] = None max: Optional[float] = None mean: Optional[float] = None avg_length: Optional[float] = None top_values: list[tuple[str, int]] = field(default_factory=list) detected_date_format: Optional[str] = None applicable_rules: list[str] = field(default_factory=list) def info_text(self) -> str: """Texto humano con información adicional (para subtítulo de la UI).""" parts: list[str] = [] parts.append(f"nulos: {self.null_count} ({self.null_pct:.0f}%)") parts.append(f"únicos: {self.unique_count}") if self.inferred_type in (TYPE_INTEGER, TYPE_DECIMAL): if self.min is not None: parts.append(f"min: {_fmt_num(self.min)}") if self.max is not None: parts.append(f"max: {_fmt_num(self.max)}") if self.mean is not None: parts.append(f"media: {_fmt_num(self.mean)}") elif self.inferred_type == TYPE_DATE: if self.detected_date_format: parts.append(f"formato: {self.detected_date_format}") elif self.inferred_type in (TYPE_TEXT, TYPE_CATEGORICAL): if self.avg_length is not None: parts.append(f"long. prom: {self.avg_length:.1f}") if self.top_values: tops = ", ".join(f'"{v}" ({c})' for v, c in self.top_values[:3]) parts.append(f"top: {tops}") return " · ".join(parts) def _fmt_num(x: float) -> str: if x is None: return "" if isinstance(x, float) and x.is_integer(): return str(int(x)) if isinstance(x, float): return f"{x:.2f}" return str(x)