| """ |
| Explainability stability check across repeated permutation runs. |
| Outputs: |
| - outputs/explainability_stability.json |
| - docs/EXPLAINABILITY_STABILITY.md |
| """ |
| from pathlib import Path |
| import json |
| import subprocess |
| import pandas as pd |
|
|
|
|
| def run_once(tag: str): |
| subprocess.run(['python', 'src/permutation_explain.py'], check=True) |
| src = Path('outputs/permutation_importance_detailed.csv') |
| df = pd.read_csv(src).sort_values('importance_mean', ascending=False) |
| top5 = df.head(5)['feature_idx'].astype(int).tolist() |
| band = None |
| bpath = Path('outputs/permutation_importance_by_band.csv') |
| if bpath.exists(): |
| bdf = pd.read_csv(bpath).sort_values('importance_mean', ascending=False) |
| if len(bdf): |
| band = str(bdf.iloc[0]['band']) |
| return {'tag': tag, 'top5': top5, 'top_band': band} |
|
|
|
|
| def jaccard(a, b): |
| sa, sb = set(a), set(b) |
| if not sa and not sb: |
| return 1.0 |
| return len(sa & sb) / max(1, len(sa | sb)) |
|
|
|
|
| def main(): |
| r1 = run_once('run1') |
| r2 = run_once('run2') |
|
|
| jac = jaccard(r1['top5'], r2['top5']) |
| band_same = (r1['top_band'] == r2['top_band']) and (r1['top_band'] is not None) |
|
|
| out = { |
| 'run1': r1, |
| 'run2': r2, |
| 'top5_jaccard': jac, |
| 'top_band_consistent': band_same, |
| 'pass': bool(jac >= 0.5 and band_same), |
| } |
|
|
| op = Path('outputs/explainability_stability.json') |
| op.write_text(json.dumps(out, indent=2), encoding='utf-8') |
|
|
| lines = [ |
| '# Explainability Stability', |
| '', |
| f"- Top-5 feature Jaccard: **{jac:.3f}**", |
| f"- Top band consistency: **{'YES' if band_same else 'NO'}**", |
| f"- Gate (`Jaccard >= 0.5` and top band consistent): **{'PASS' if out['pass'] else 'FAIL'}**", |
| '', |
| f"- Run1 top5: `{r1['top5']}`", |
| f"- Run2 top5: `{r2['top5']}`", |
| f"- Run1 top band: `{r1['top_band']}`", |
| f"- Run2 top band: `{r2['top_band']}`", |
| ] |
|
|
| dp = Path('docs/EXPLAINABILITY_STABILITY.md') |
| dp.write_text('\n'.join(lines) + '\n', encoding='utf-8') |
| print(f'Generated {op} and {dp}') |
|
|
| if not out['pass']: |
| raise SystemExit(2) |
|
|
|
|
| if __name__ == '__main__': |
| main() |
|
|