Files
IPSExtractor/services/cleaning/domain/csv_cleaning/analyze.py
T

94 lines
2.9 KiB
Python

"""Análisis de DataFrames: resumen por columna y muestras."""
from __future__ import annotations
import json
import random
from typing import Any
import pandas as pd
from .models import (
TYPE_CATEGORICAL,
TYPE_DECIMAL,
TYPE_INTEGER,
TYPE_TEXT,
ColumnSummary,
)
from .numbers import _to_numeric_lenient
from .type_inference import _applicable_rules_for_type, _infer_column_type
def analyze_dataframe(df: pd.DataFrame) -> list[ColumnSummary]:
"""Genera un resumen por columna del DataFrame."""
summaries: list[ColumnSummary] = []
total_rows = len(df)
rng = random.Random(42)
for col in df.columns:
series = df[col]
null_count = int(series.isna().sum() + (series.astype(str).str.strip() == "").sum())
# Evitar doble conteo: considerar vacío solo si no-NaN
null_count = int((series.isna() | (series.astype(str).str.strip() == "")).sum())
null_pct = (null_count / total_rows * 100) if total_rows else 0.0
non_null = series.dropna().astype(str)
non_null = non_null[non_null.str.strip() != ""]
example = ""
samples: list[str] = []
if not non_null.empty:
values = non_null.tolist()
k = min(5, len(values))
picked = rng.sample(values, k) if len(values) >= k else list(values)
for raw in picked:
text = str(raw)
if len(text) > 80:
text = text[:77] + "..."
samples.append(text)
example = samples[0] if samples else ""
unique_count = int(non_null.nunique())
unique_pct = (unique_count / total_rows * 100) if total_rows else 0.0
inferred_type, date_fmt = _infer_column_type(series, str(col))
summary = ColumnSummary(
name=str(col),
inferred_type=inferred_type,
example=example,
samples=samples,
null_count=null_count,
null_pct=null_pct,
unique_count=unique_count,
unique_pct=unique_pct,
detected_date_format=date_fmt,
)
if inferred_type in (TYPE_INTEGER, TYPE_DECIMAL):
numeric = _to_numeric_lenient(non_null)
if numeric.notna().any():
summary.min = float(numeric.min())
summary.max = float(numeric.max())
summary.mean = float(numeric.mean())
elif inferred_type in (TYPE_TEXT, TYPE_CATEGORICAL):
if not non_null.empty:
summary.avg_length = float(non_null.str.len().mean())
counts = non_null.value_counts().head(3)
summary.top_values = [(str(v), int(c)) for v, c in counts.items()]
summary.applicable_rules = _applicable_rules_for_type(inferred_type)
summaries.append(summary)
return summaries
def sample_dataframe_rows(df: pd.DataFrame, n: int = 8) -> list[dict[str, Any]]:
"""Devuelve hasta `n` filas aleatorias como dicts JSON-serializables."""
if df.empty or n <= 0:
return []
take = min(n, len(df))
rng = random.Random(42)
indices = rng.sample(list(range(len(df))), take)
subset = df.iloc[indices]
return json.loads(subset.to_json(orient="records", date_format="iso"))