| """Batch churn probability prediction for all active customers. |
| |
| Run as a module: |
| uv run python -m src.prediction.batch_predict |
| """ |
|
|
| import pandas as pd |
|
|
| from src.config import ( |
| CHURN_PERIOD_DAYS, |
| DATA_DIR, |
| PREDICTIONS_DIR, |
| RISK_HIGH, |
| RISK_MEDIUM, |
| ) |
| from src.feature_engineering.pipeline import build_feature_matrix |
| from src.models.train import get_feature_cols, load_model |
| from src.preprocessing.clean import clean_customers, clean_orders |
| from src.target.churn_target import compute_churn_target |
| from src.utils.logger import get_logger |
|
|
| logger = get_logger(__name__) |
|
|
|
|
| def assign_risk_bucket(prob: float) -> str: |
| """Map a churn probability to a risk segment label. |
| |
| Args: |
| prob: Predicted churn probability in [0, 1]. |
| |
| Returns: |
| One of ``"high"``, ``"medium"``, or ``"low"``. |
| """ |
| if prob >= RISK_HIGH: |
| return "high" |
| if prob >= RISK_MEDIUM: |
| return "medium" |
| return "low" |
|
|
|
|
| def run_batch_prediction( |
| orders: pd.DataFrame | None = None, |
| customers: pd.DataFrame | None = None, |
| ) -> pd.DataFrame: |
| """Generate batch churn predictions for all eligible customers. |
| |
| The model is trained on the historical window ending at |
| ``T_max − CHURN_PERIOD_DAYS``. Predictions represent estimated churn |
| probability over the *next* ``CHURN_PERIOD_DAYS`` days. |
| |
| Args: |
| orders: Pre-loaded orders DataFrame (reads from disk if None). |
| customers: Pre-loaded customers DataFrame (reads from disk if None). |
| |
| Returns: |
| DataFrame with columns [customer_id, churn_probability, risk_bucket]. |
| Also persisted to ``predictions/churn_predictions.csv``. |
| """ |
| if orders is None: |
| orders = pd.read_csv(DATA_DIR / "orders.csv", parse_dates=["order_date", "contract_date"]) |
| if customers is None: |
| customers = pd.read_csv( |
| DATA_DIR / "customers.csv", |
| parse_dates=["registration_date", "birth_date", "last_profile_update"], |
| ) |
|
|
| orders = clean_orders(orders) |
| customers = clean_customers(customers) |
|
|
| _, feature_end_date, t_max = compute_churn_target(orders, customers, CHURN_PERIOD_DAYS) |
| logger.info(f"Building features up to {feature_end_date.date()} (T_max={t_max.date()})") |
|
|
| features = build_feature_matrix(orders, customers, feature_end_date) |
|
|
| model = load_model() |
| feature_cols = get_feature_cols(features) |
| X = features[feature_cols] |
|
|
| probs = model.predict_proba(X)[:, 1] |
|
|
| predictions = pd.DataFrame( |
| { |
| "customer_id": features["customer_id"].values, |
| "churn_probability": probs.round(6), |
| "risk_bucket": [assign_risk_bucket(p) for p in probs], |
| } |
| ) |
|
|
| PREDICTIONS_DIR.mkdir(parents=True, exist_ok=True) |
| output_path = PREDICTIONS_DIR / "churn_predictions.csv" |
| predictions.to_csv(output_path, index=False) |
|
|
| dist = predictions["risk_bucket"].value_counts().to_dict() |
| logger.info( |
| f"Predictions saved → {output_path} " |
| f"high={dist.get('high', 0)}, medium={dist.get('medium', 0)}, low={dist.get('low', 0)}" |
| ) |
| return predictions |
|
|
|
|
| if __name__ == "__main__": |
| logger.info("=== Batch Prediction ===") |
| preds = run_batch_prediction() |
| logger.info(f"Generated {len(preds):,} predictions") |
| logger.info("=== Done ===") |
|
|