94 lines
2.9 KiB
Python
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"))
|