Source code for mmm_framework.eda.profiling

"""
Dataset profiling: per-variable summary statistics, missingness, and
spend share / concentration.
"""

from __future__ import annotations

import numpy as np
import pandas as pd
from scipy import stats as sps

from .loading import EDAPanel


[docs] def profile_panel(panel: EDAPanel) -> pd.DataFrame: """Per-variable summary statistics (one row per variable).""" rows = [] for var in panel.variables: col = panel.df_wide[var].astype(float) values = col.dropna().to_numpy() role = ( "kpi" if var == panel.kpi else ( "media" if var in panel.media else "control" if var in panel.controls else "unassigned" ) ) row = { "variable": var, "role": role, "n": int(col.size), "missing_pct": float(col.isna().mean() * 100.0), "zero_pct": float((col == 0).mean() * 100.0), } if values.size: q1, med, q3 = np.percentile(values, [25, 50, 75]) row.update( mean=float(values.mean()), std=float(values.std()), min=float(values.min()), q1=float(q1), median=float(med), q3=float(q3), max=float(values.max()), skew=float(sps.skew(values)) if values.size > 2 else np.nan, kurtosis=float(sps.kurtosis(values)) if values.size > 3 else np.nan, ) rows.append(row) return pd.DataFrame(rows)
[docs] def missingness_matrix(panel: EDAPanel) -> pd.DataFrame: """Availability per (period, variable): 1 observed, 0 missing. For panel data, a cell counts as observed when ANY slice has a value; the fraction observed is returned instead of a binary flag. """ wide = panel.df_wide if not panel.dims: return wide.notna().astype(float) return wide.notna().groupby(level=panel.date_col).mean()
[docs] def spend_share(panel: EDAPanel) -> dict[str, object]: """Total + over-time spend shares and the HHI concentration index.""" media = [m for m in panel.media if m in panel.df_wide.columns] if not media: return {"totals": {}, "shares": {}, "hhi": None, "share_over_time": None} spend = panel.df_wide[media].clip(lower=0) if panel.dims: spend = spend.groupby(level=panel.date_col).sum() totals = spend.sum() grand = float(totals.sum()) shares = (totals / grand) if grand > 0 else totals * 0.0 # Herfindahl–Hirschman index: 1/n (even split) .. 1.0 (single channel). hhi = float((shares**2).sum()) if grand > 0 else None row_total = spend.sum(axis=1) share_over_time = spend.div(row_total.where(row_total > 0), axis=0) return { "totals": {c: float(totals[c]) for c in media}, "shares": {c: float(shares[c]) for c in media}, "hhi": hhi, "share_over_time": share_over_time, }
__all__ = ["profile_panel", "missingness_matrix", "spend_share"]