"""Fit the FeatureBuilder on the training split and persist all artifacts. Outputs: artifacts/feature_pipeline.pkl - fitted FeatureBuilder artifacts/feature_list.json - selected feature names artifacts/train_holdout.parquet - raw training rows (+target) artifacts/test_holdout.parquet - raw test rows (+target), used by eval & simulator reports/feature_selection.md - what was selected and why """ from __future__ import annotations import json import joblib import pandas as pd from src import config from src.data.load import load_raw, train_test from src.features.builder import ANOMALY_COL, FeatureBuilder def main() -> None: config.ensure_dirs() df = load_raw() X_tr, X_te, y_tr, y_te = train_test(df) # Persist raw holdouts so training / eval / simulator share identical splits. train_raw = X_tr.copy(); train_raw[config.TARGET] = y_tr.values test_raw = X_te.copy(); test_raw[config.TARGET] = y_te.values train_raw.to_parquet(config.ARTIFACTS_DIR / "train_holdout.parquet") test_raw.to_parquet(config.TEST_SPLIT_PATH) builder = FeatureBuilder() builder.fit(X_tr, y_tr) joblib.dump(builder, config.PIPELINE_PATH) config.FEATURE_LIST_PATH.write_text(json.dumps(builder.selected_features_, indent=2)) # ---- selection report -------------------------------------------------- freq = pd.Series(builder.selection_freq_).sort_values(ascending=False) priors = set(config.KNOWN_IMPORTANT) | {"F3888_age_days", "F3889_recency_ord"} lines = ["# Feature Selection — MuleGuard\n"] lines.append(f"- Full feature space after preprocessing: **{len(builder.feature_names_full_)}** columns") lines.append(f"- Selected for modeling: **{len(builder.selected_features_)}**") lines.append(f"- Includes the fused **{ANOMALY_COL}** (Isolation Forest) and retained domain priors.\n") lines.append("## Top selected features by CV selection frequency\n") lines.append("| Feature | Selection freq | Domain prior? |") lines.append("|---|---|---|") for feat in builder.selected_features_: f = freq.get(feat, 0.0) base = feat.split("_")[0] prior = "✅" if (feat in priors or base in priors or feat == ANOMALY_COL) else "" lines.append(f"| {feat} | {f:.2f} | {prior} |") (config.REPORTS_DIR / "feature_selection.md").write_text("\n".join(lines)) print(f"Selected {len(builder.selected_features_)} features " f"(of {len(builder.feature_names_full_)}). Saved pipeline + reports.") print("Anomaly score included:", ANOMALY_COL in builder.selected_features_) if __name__ == "__main__": main()