EDA & Data Quality

Pre-fit data validation, profiling, collinearity analysis, and outlier detection/treatment.

Pre-fit data quality for MMM: validation, outlier detection + remediation, and exploratory data analysis (EDA) with plotly charts.

This package is the pre-fit sibling of mmm_framework.validation (which diagnoses a fitted model). Everything here runs on the raw dataset before BayesianMMM sees it.

class mmm_framework.eda.OutlierConfig(methods=('robust_z', 'iqr', 'stl_residual', 'level_shift', 'spend_spike', 'spend_drop'), robust_z_threshold=3.5, iqr_multiplier=3.0, stl_residual_threshold=3.5, stl_period=None, rolling_window=13, level_shift_window=8, level_shift_threshold=3.25, level_shift_min_run=4, spike_ratio=5.0, spike_neighborhood=4, normalization_damage_ratio=3.0, drop_context_ratio=0.25, consensus_threshold=0.3, heavy_tail_kurtosis=2.0, heavy_tail_min_flags=5, heavy_tail_top_k=5)[source]

Bases: object

Thresholds for the outlier detectors.

Defaults are deliberately conservative: in MMM data, seasonal peaks and promo flights are real signal, so only points that survive the seasonally-adjusted (STL) view or an isolated-spike test should be flagged.

methods: tuple[str, ...] = ('robust_z', 'iqr', 'stl_residual', 'level_shift', 'spend_spike', 'spend_drop')
robust_z_threshold: float = 3.5
iqr_multiplier: float = 3.0
stl_residual_threshold: float = 3.5
stl_period: int | None = None
rolling_window: int = 13
level_shift_window: int = 8
level_shift_threshold: float = 3.25
level_shift_min_run: int = 4
spike_ratio: float = 5.0
spike_neighborhood: int = 4
normalization_damage_ratio: float = 3.0
drop_context_ratio: float = 0.25
consensus_threshold: float = 0.3
heavy_tail_kurtosis: float = 2.0
heavy_tail_min_flags: int = 5
heavy_tail_top_k: int = 5
classmethod for_sensitivity(sensitivity='default')[source]

Preset configs: ‘low’ flags less, ‘high’ flags more.

Return type:

OutlierConfig

__init__(methods=('robust_z', 'iqr', 'stl_residual', 'level_shift', 'spend_spike', 'spend_drop'), robust_z_threshold=3.5, iqr_multiplier=3.0, stl_residual_threshold=3.5, stl_period=None, rolling_window=13, level_shift_window=8, level_shift_threshold=3.25, level_shift_min_run=4, spike_ratio=5.0, spike_neighborhood=4, normalization_damage_ratio=3.0, drop_context_ratio=0.25, consensus_threshold=0.3, heavy_tail_kurtosis=2.0, heavy_tail_min_flags=5, heavy_tail_top_k=5)
class mmm_framework.eda.DataValidationConfig(missing_warn_pct=5.0, missing_error_pct=30.0, zero_inflation_warn_pct=60.0, near_constant_cv=1e-06, scale_ratio_threshold=10000.0, min_obs_per_param=4.0)[source]

Bases: object

Thresholds for pre-fit dataset validation.

missing_warn_pct: float = 5.0
missing_error_pct: float = 30.0
zero_inflation_warn_pct: float = 60.0
near_constant_cv: float = 1e-06
scale_ratio_threshold: float = 10000.0
min_obs_per_param: float = 4.0
__init__(missing_warn_pct=5.0, missing_error_pct=30.0, zero_inflation_warn_pct=60.0, near_constant_cv=1e-06, scale_ratio_threshold=10000.0, min_obs_per_param=4.0)
class mmm_framework.eda.EDAConfig(correlation_threshold=0.8, vif_threshold=10.0, top_correlations=10, stl_robust=False, significance_level=0.05, max_series_per_chart=12)[source]

Bases: object

Knobs for the EDA analyses.

correlation_threshold: float = 0.8
vif_threshold: float = 10.0
top_correlations: int = 10
stl_robust: bool = False
significance_level: float = 0.05
max_series_per_chart: int = 12
__init__(correlation_threshold=0.8, vif_threshold=10.0, top_correlations=10, stl_robust=False, significance_level=0.05, max_series_per_chart=12)
class mmm_framework.eda.EDAPanel(df_wide, df_long, kpi, media, controls, unassigned, dims, date_col, freq, roles_source, duplicate_rows=0, source_path=None)[source]

Bases: object

A wide panel plus role metadata, ready for EDA/validation/outliers.

df_wide: DataFrame
df_long: DataFrame | None
kpi: str | None
media: list[str]
controls: list[str]
unassigned: list[str]
dims: list[str]
date_col: str
freq: str | None
roles_source: str
duplicate_rows: int = 0
source_path: str | None = None
property variables: list[str]
property is_panel: bool
series(variable, dim_values=None)[source]

One variable as a Period-indexed series (a single panel slice).

Return type:

Series

slices(variable)[source]

Yield (dim_values, series) per panel slice (one slice if national).

__init__(df_wide, df_long, kpi, media, controls, unassigned, dims, date_col, freq, roles_source, duplicate_rows=0, source_path=None)
mmm_framework.eda.load_eda_panel(dataset_path, spec=None)[source]

Load a dataset (MFF long CSV, or wide CSV fallback) for EDA.

Duplicate (VariableName, Period, dims) rows are counted and resolved by keeping the FIRST occurrence — the count is surfaced on the panel so the validator reports it instead of silently aggregating.

Return type:

EDAPanel

mmm_framework.eda.load_eda_panel_from_df(df, spec=None, source_path=None)[source]

Build an EDAPanel from an in-memory DataFrame (the Data Studio’s staged/transformed frame) — the path-free sibling of load_eda_panel().

Same MFF-long vs wide dispatch; no disk IO so the studio can re-run EDA on every pipeline edit without writing a temp file.

Return type:

EDAPanel

mmm_framework.eda.seasonal_period_for_freq(freq)[source]

Observations per year for STL, by frequency.

Return type:

int | None

class mmm_framework.eda.OutlierFlag(variable, period, value, expected, methods, score, kind, dims=<factory>)[source]

Bases: object

One flagged observation in one series.

variable: str
period: str
value: float
expected: float
methods: list[str]
score: float
kind: str
dims: dict[str, str]
property flag_id: str
__init__(variable, period, value, expected, methods, score, kind, dims=<factory>)
class mmm_framework.eda.RemediationAction(action_id, flag_ids, strategy, params, rationale, spec_change=None)[source]

Bases: object

A concrete, applyable treatment for one or more flags.

action_id: str
flag_ids: list[str]
strategy: str
params: dict[str, Any]
rationale: str
spec_change: dict[str, Any] | None = None
__init__(action_id, flag_ids, strategy, params, rationale, spec_change=None)
class mmm_framework.eda.OutlierReport(flags, actions, config, per_variable=<factory>, dataset_path=None, dataset_mtime=None)[source]

Bases: object

Full outlier-detection output: flags + recommended treatments.

flags: list[OutlierFlag]
actions: list[RemediationAction]
config: OutlierConfig
per_variable: dict[str, dict[str, Any]]
dataset_path: str | None = None
dataset_mtime: float | None = None
flags_frame()[source]
Return type:

DataFrame

summary()[source]
Return type:

str

to_dict()[source]
Return type:

dict

classmethod from_dict(d)[source]
Return type:

OutlierReport

__init__(flags, actions, config, per_variable=<factory>, dataset_path=None, dataset_mtime=None)
class mmm_framework.eda.ValidationIssue(check, severity, message, variable=None, affected=<factory>)[source]

Bases: object

One finding from a single validation check.

check: str
severity: str
message: str
variable: str | None = None
affected: list[Any]
__init__(check, severity, message, variable=None, affected=<factory>)
class mmm_framework.eda.DataValidationReport(issues, n_periods, n_variables, config)[source]

Bases: object

All validation findings for a dataset.

issues: list[ValidationIssue]
n_periods: int
n_variables: int
config: DataValidationConfig
property passed: bool
by_severity(severity)[source]
Return type:

list[ValidationIssue]

summary()[source]
Return type:

str

to_dict()[source]
Return type:

dict

__init__(issues, n_periods, n_variables, config)
class mmm_framework.eda.DecompositionResult(variable, method, period, trend, seasonal, resid, trend_strength, seasonal_strength)[source]

Bases: object

STL (or fallback) decomposition of one series.

variable: str
method: str
period: int | None
trend: Series
seasonal: Series | None
resid: Series
trend_strength: float
seasonal_strength: float | None
to_dict()[source]
Return type:

dict

__init__(variable, method, period, trend, seasonal, resid, trend_strength, seasonal_strength)
class mmm_framework.eda.EDAReport(sections=<factory>, figures=<factory>)[source]

Bases: object

Aggregated EDA output keyed by analysis name.

sections holds JSON-safe dicts (one per analysis run); figures holds the plotly figures keyed by a descriptive name — callers decide how to render/store them.

sections: dict[str, Any]
figures: dict[str, Any]
to_dict()[source]
Return type:

dict

__init__(sections=<factory>, figures=<factory>)
class mmm_framework.eda.OutlierDetector(panel, config=None)[source]

Bases: object

Run all configured detectors over a panel’s KPI + media (+ controls).

__init__(panel, config=None)[source]
default_variables()[source]
Return type:

list[str]

run(variables=None)[source]
Return type:

OutlierReport

mmm_framework.eda.detect_outliers(panel, config=None, variables=None)[source]

Functional wrapper around OutlierDetector.

Return type:

OutlierReport

mmm_framework.eda.recommend_treatments(panel, flags, config=None)[source]

Build remediation recommendations for a set of flags.

Return type:

list[RemediationAction]

mmm_framework.eda.apply_treatments(df_long, actions, flags, *, date_col='Period', kpi=None)[source]

Apply winsorize / dummy / exclude actions to an MFF long frame.

Returns a NEW frame; the input is not mutated. flags provides the (variable, period, dims) targeting for each action’s flag_ids.

Return type:

DataFrame

mmm_framework.eda.decompose_series(series, period=None, *, robust=False, rolling_window=13, variable=None)[source]

Decompose one series into trend / seasonal / residual.

Return type:

DecompositionResult

Parameters

seriespd.Series

Period-indexed values (NaNs are interpolated for decomposition only).

periodint, optional

Seasonal period (e.g. 52 for weekly). When None or when fewer than two full cycles are available, falls back to a rolling-median detrend.

robustbool

Passed to STL. Default False: with only a few seasonal cycles, statsmodels’ robust mode degenerates to near-interpolation (residual MAD ~ 0), which breaks residual-based screening. Outlier resistance comes from MAD-based z-scores on the residual instead.

mmm_framework.eda.decomposition_summary(panel_series, period, config=None)[source]

Decompose several series (e.g. KPI + media) with shared settings.

Return type:

list[DecompositionResult]

mmm_framework.eda.stationarity_tests(series, *, significance=0.05)[source]

ADF + KPSS stationarity tests with a combined verdict.

ADF null: unit root (non-stationary). KPSS null: stationary.

Return type:

dict[str, object]

class mmm_framework.eda.DataValidator(panel, config=None, spec=None)[source]

Bases: object

Run every check_* method over a panel and collect issues.

__init__(panel, config=None, spec=None)[source]
run()[source]
Return type:

DataValidationReport

check_date_gaps()[source]
Return type:

list[ValidationIssue]

check_duplicate_rows()[source]
Return type:

list[ValidationIssue]

check_constant_series()[source]
Return type:

list[ValidationIssue]

check_missingness()[source]
Return type:

list[ValidationIssue]

check_negative_spend()[source]
Return type:

list[ValidationIssue]

check_panel_consistency()[source]
Return type:

list[ValidationIssue]

check_scale_pathology()[source]
Return type:

list[ValidationIssue]

check_short_history()[source]
Return type:

list[ValidationIssue]

check_zero_inflation()[source]
Return type:

list[ValidationIssue]

mmm_framework.eda.validate_dataset(panel, config=None, spec=None)[source]

Functional wrapper around DataValidator.

Return type:

DataValidationReport

mmm_framework.eda.profile_panel(panel)[source]

Per-variable summary statistics (one row per variable).

Return type:

DataFrame

mmm_framework.eda.missingness_matrix(panel)[source]

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.

Return type:

DataFrame

mmm_framework.eda.spend_share(panel)[source]

Total + over-time spend shares and the HHI concentration index.

Return type:

dict[str, object]

mmm_framework.eda.collinearity_analysis(panel, config=None, variables=None)[source]

Correlation matrix + VIF + collinear clusters for media (+ controls).

Returns a dict with keys: variables, correlation (DataFrame), vif (dict), high_vif (list), clusters (list of dicts), condition_number, top_pairs (list of dicts).

Return type:

dict[str, object]