MuleGuard / src /features /build.py
MuleGuard
MuleGuard: end-to-end mule-account detection + HF Space deploy
af879c2
Raw
History Blame Contribute Delete
2.65 kB
"""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()