"""Churn, final step: Extra trees (picked on validation in churn_race.py) refit on every labelled cut-off, isotonic-calibrated, scoring every outlet active in the 13 weeks to 20 Oct 2026 for the 8 weeks after it. Also the LightGBM version for comparison.""" import sys, pickle, warnings import numpy as np, pandas as pd warnings.filterwarnings('ignore') sys.path.insert(0, '/home/claude/binzagr/pred'); sys.path.insert(0, '/home/claude/binzagr/boost') import p04_churn as C from churn_race import num_X from sklearn.ensemble import ExtraTreesClassifier from sklearn.calibration import CalibratedClassifierCV cuts = ['2026-06-02', '2026-06-16', '2026-06-30', '2026-07-14', '2026-07-28', '2026-08-11', '2026-08-25'] fin = C.labelled(cuts); now = C.features(pd.Timestamp('2026-10-20')) et = ExtraTreesClassifier(n_estimators=500, min_samples_leaf=20, max_features=0.5, n_jobs=2, random_state=7) cal = CalibratedClassifierCV(et, method='isotonic', cv=5).fit(num_X(fin).fillna(-1), fin.y) now['churn_risk_8w_v2'] = cal.predict_proba(num_X(now).fillna(-1))[:, 1] old = pickle.load(open('/home/claude/binzagr/pred/work/churn.pkl', 'rb'))['now'][['outlet_id', 'p']].rename(columns={'p': 'churn_risk_8w_v1'}) out = now[['outlet_id', 'recency_days', 'churn_risk_8w_v2']].merge(old, on='outlet_id', how='left') tier = lambda p: np.select([p >= 0.25, p >= 0.10], ['High', 'Medium'], 'Low') out['tier_v2'] = tier(out.churn_risk_8w_v2); out['tier_v1'] = tier(out.churn_risk_8w_v1.fillna(0)) out.to_parquet('/home/claude/binzagr/boost/work/churn_v2.parquet', index=False) print(len(out), 'outlets | tiers v2', out.tier_v2.value_counts().to_dict(), '| tiers v1', out.tier_v1.value_counts().to_dict()) print('rank correlation v1 vs v2', round(out[['churn_risk_8w_v1', 'churn_risk_8w_v2']].corr(method='spearman').iloc[0, 1], 3))