bci-mvp / src /explainability.py
WilliamK112
feat: add explainability pipeline + CI + Docker + Makefile for reproducibility
09da960
Raw
History Blame Contribute Delete
2.19 kB
"""
Model explainability for RF baseline.
- Computes feature importances
- Aggregates by frequency band and channel
- Saves csv/json summaries
"""
from pathlib import Path
import json
import numpy as np
import pandas as pd
import joblib
BANDS = ["delta", "theta", "alpha", "beta"]
def main(model_path="outputs/model_rf_real.joblib", max_channels=8):
model = joblib.load(model_path)
rf = model.named_steps.get("rf") or model.named_steps.get("clf")
if rf is None or not hasattr(rf, "feature_importances_"):
raise RuntimeError("Loaded model does not expose feature_importances_")
imp = rf.feature_importances_
n_features = len(imp)
# expected features = channels * 4 bands
if n_features % 4 != 0:
raise RuntimeError(f"Unexpected feature dim {n_features}, not divisible by 4")
n_channels = n_features // 4
rows = []
for ch in range(n_channels):
for bi, b in enumerate(BANDS):
idx = ch * 4 + bi
rows.append({"channel": ch, "band": b, "importance": float(imp[idx])})
df = pd.DataFrame(rows)
by_band = df.groupby("band", as_index=False)["importance"].sum().sort_values("importance", ascending=False)
by_channel = df.groupby("channel", as_index=False)["importance"].sum().sort_values("importance", ascending=False)
out = Path("outputs")
out.mkdir(exist_ok=True)
df.to_csv(out / "feature_importance_detailed.csv", index=False)
by_band.to_csv(out / "feature_importance_by_band.csv", index=False)
by_channel.to_csv(out / "feature_importance_by_channel.csv", index=False)
summary = {
"n_features": int(n_features),
"n_channels": int(n_channels),
"top_band": by_band.iloc[0].to_dict() if len(by_band) else None,
"top_channel": by_channel.iloc[0].to_dict() if len(by_channel) else None,
}
(out / "explainability_summary.json").write_text(json.dumps(summary, indent=2), encoding="utf-8")
print("Top bands:")
print(by_band.to_string(index=False))
print("\nTop channels:")
print(by_channel.head(10).to_string(index=False))
print("\nSaved explainability artifacts in outputs/")
if __name__ == "__main__":
main()