aleem1234 commited on
Commit
38c1a14
·
0 Parent(s):

Fix requirements.txt dependencies

Browse files
.gitattributes ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
.github/workflows/mlops_pipeline.yml ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: MLOps CI/CD & Continuous Training Pipeline
2
+
3
+ on:
4
+ push:
5
+ branches: [ main ]
6
+ pull_request:
7
+ branches: [ main ]
8
+
9
+ jobs:
10
+ lint-and-test:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - name: Checkout Repository
14
+ uses: actions/checkout@v4
15
+
16
+ - name: Set up Python
17
+ uses: actions/setup-python@v5
18
+ with:
19
+ python-version: '3.12'
20
+ cache: 'pip'
21
+
22
+ - name: Install Python Dependencies
23
+ run: |
24
+ python -m pip install --upgrade pip
25
+ pip install ruff pytest anyio httpx pandas numpy scikit-learn joblib xgboost fastapi uvicorn pydantic
26
+
27
+ - name: Run Ruff Linter
28
+ run: ruff check src/
29
+
30
+ - name: Run Unit Tests
31
+ run: pytest
32
+
33
+ continuous-training-validation:
34
+ needs: lint-and-test
35
+ runs-on: ubuntu-latest
36
+ steps:
37
+ - name: Checkout Repository
38
+ uses: actions/checkout@v4
39
+
40
+ - name: Set up Python
41
+ uses: actions/setup-python@v5
42
+ with:
43
+ python-version: '3.12'
44
+ cache: 'pip'
45
+
46
+ - name: Install Dependencies
47
+ run: |
48
+ python -m pip install --upgrade pip
49
+ pip install pandas numpy scikit-learn joblib xgboost fastapi uvicorn pydantic
50
+
51
+ - name: Validate Model Retraining Performance
52
+ run: python src/validate_retraining.py
53
+
54
+ docker-build:
55
+ needs: continuous-training-validation
56
+ runs-on: ubuntu-latest
57
+ steps:
58
+ - name: Checkout Repository
59
+ uses: actions/checkout@v4
60
+
61
+ - name: Set up Docker Buildx
62
+ uses: docker/setup-buildx-action@v3
63
+
64
+ - name: Build Backend Container
65
+ uses: docker/build-push-action@v5
66
+ with:
67
+ context: .
68
+ file: ./Dockerfile
69
+ push: false
70
+ tags: customer-segmentation-api:latest
.gitignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ venv/
2
+ node_modules/
3
+ __pycache__/
4
+ *.ipynb_checkpoints
5
+ data/raw/*.xlsx
6
+ .DS_Store
Dockerfile ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use official slim Python runtime as base image
2
+ FROM python:3.12-slim
3
+
4
+ # Set system environment variables
5
+ ENV PYTHONDONTWRITEBYTECODE=1
6
+ ENV PYTHONUNBUFFERED=1
7
+
8
+ # Set the container workspace directory
9
+ WORKDIR /app
10
+
11
+ # Install compilation tools needed for C-extensions
12
+ RUN apt-get update && apt-get install -y --no-install-recommends \
13
+ build-essential \
14
+ && rm -rf /var/lib/apt/lists/*
15
+
16
+ # Copy only requirements to leverage Docker cache layers
17
+ COPY requirements.txt .
18
+
19
+ # Install dependencies
20
+ RUN pip install --no-cache-dir -r requirements.txt
21
+
22
+ # Copy unified YAML configuration and source directory
23
+ COPY config.yaml .
24
+ COPY src/ src/
25
+
26
+ # Copy processed datasets and serialized models needed for server inference
27
+ COPY data/processed/ data/processed/
28
+ COPY models/ models/
29
+
30
+ # Expose port (7860 is default for Hugging Face Spaces, 8000 for local)
31
+ EXPOSE 7860
32
+ EXPOSE 8000
33
+
34
+ # Run FastAPI prediction service via Uvicorn with port override fallback
35
+ CMD ["sh", "-c", "uvicorn src.api:app --host 0.0.0.0 --port ${PORT:-8000}"]
README.md ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Churnflow API
3
+ emoji: 📈
4
+ colorFrom: green
5
+ colorTo: green
6
+ sdk: docker
7
+ app_port: 8000
8
+ pinned: false
9
+ ---
10
+
11
+ 🛒 Customer Segmentation & Churn Prediction
12
+ ![alt text](https://img.shields.io/badge/Python-3.9-blue)
13
+ ![alt text](https://img.shields.io/badge/Model-XGBoost-orange)
14
+ ![alt text](https://img.shields.io/badge/Analysis-RFM-green)
15
+ ![alt text](https://img.shields.io/badge/Status-Complete-brightgreen)
16
+ 📌 Executive Summary
17
+ Customer churn is one of the biggest revenue leaks in retail. This project analyzes transaction data to identify high-value customer segments and predict churn probability with 73% Recall.
18
+ By engineering features from raw purchase logs and utilizing an XGBoost classifier, this solution helps businesses transition from "reactive" retention (trying to save everyone) to "proactive" retention (focusing resources on high-value at-risk users).
19
+ Key Business Insight:
20
+ "Frequency" was identified as the #1 predictor of churn.
21
+ Strategy: Users who fail to make a second purchase within 45 days are 80% likely to churn. Marketing spend should prioritize "Second Purchase Habits" rather than generic discounts.
22
+ 📊 Project Architecture
23
+ The project follows a modular Data Science lifecycle:
24
+ 1. Data Cleaning & Preprocessing (01_Data_Cleaning)
25
+ Source: UCI Online Retail II Dataset.
26
+ Engineering: Converted raw transaction logs into a customer-level dataset.
27
+ Handling: Removed returns (negative quantities) and adjusted for inflation/outliers.
28
+ 2. Customer Segmentation (02_RFM_Segmentation)
29
+ Methodology: RFM (Recency, Frequency, Monetary) Analysis combined with K-Means Clustering.
30
+ Result: 3 Distinct Segments:
31
+ 🟢 Champions: High spend, frequent buyers (Upsell targets).
32
+ 🔵 Loyalists: Moderate frequency (Maintenance targets).
33
+ 🔴 Hibernating: High risk of churn (Win-back targets).
34
+ ![3D Plotly Cluster Graph](images/image-1.png)
35
+ 3. Predictive Modeling (03_Churn_Prediction)
36
+ Target Definition: Sliding window approach (Train on first 9 months, Predict on next 3 months).
37
+ Models Tested: Random Forest (Baseline) vs. XGBoost (Final).
38
+ Handling Imbalance: Used scale_pos_weight to penalize false negatives.
39
+ 📈 Model Performance
40
+ We optimized the model for Recall (Sensitivity) because missing a churning customer is more costly than a false alarm.
41
+ Metric Random Forest (Baseline) XGBoost (Tuned)
42
+ Accuracy 67% 64%
43
+ Recall (Churn Catch Rate) 58% 73% 🚀
44
+ Precision 60% 55%
45
+ Key Drivers (Feature Importance)
46
+ Unlike typical assumptions that "Big Spenders Stay," our model found that Consistency matters more than Value.
47
+ Frequency (Count of purchases): The strongest signal for retention.
48
+ Recency: Immediate drop-off after 60 days of silence.
49
+ Monetary: Surprisingly, a weaker predictor of churn than frequency.
50
+ ![XGBoost Feature Importance Bar Chart](images/image.png)
51
+ 🛠️ Tech Stack
52
+ Language: Python 3.9+
53
+ Data Manipulation: Pandas, NumPy
54
+ Machine Learning: Scikit-Learn (K-Means), XGBoost
55
+ Visualization: Plotly (3D Interactive), Matplotlib, Seaborn
56
+ Interpretability: Feature Importance Analysis
57
+ 🚀 Usage
58
+ 1. Clone the repository
59
+ code
60
+ Bash
61
+ git clone https://github.com/aleem1991/customer-segmentation.git
62
+ 2. Install dependencies
63
+ code
64
+ Bash
65
+ pip install -r requirements.txt
66
+ 3. Run the Notebooks
67
+ Open notebooks/01_Data_Cleaning.ipynb to prepare the data.
68
+ Run notebooks/03_Churn_Prediction.ipynb to train the model and generate the Churn Report.
69
+ 💼 Business Recommendations
70
+ Based on the analysis, the following actions are recommended to stakeholders:
71
+ Segment Risk Identified Behavior Recommended Action
72
+ High Risk (One-Time Buyers) Purchased once, no return in 45 days. Trigger: Send "Welcome Back" bonus for 2nd purchase immediately.
73
+ At-Risk Champions High historic spend but Low Recency. Trigger: Personal account manager outreach (No automated spam).
74
+ Safe Loyalists Buying regularly. Trigger: Do nothing. Exclude from aggressive discount lists to save margin.
75
+ 📬 Contact
76
+ Created by Aleem - Feel free to contact me for details on the feature engineering pipeline!
config.yaml ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ paths:
2
+ raw_data: "data/raw/online_retail_II.xlsx"
3
+ clean_data: "data/processed/online_retail_clean.csv"
4
+ model: "models/churn_xgb_model.pkl"
5
+ segments: "data/processed/rfm_segments.csv"
6
+ predictions_report: "data/processed/churn_predictions_report.csv"
7
+ high_value_report: "data/processed/high_value_at_risk_customers.csv"
8
+
9
+ parameters:
10
+ cutoff_offset_days: 90
11
+ recent_purchase_window_days: 60
12
+ single_order_imputation_days: 180.0
13
+ default_recent_orders_ratio: 0.3
14
+ default_is_uk: 1
15
+
16
+ model_hyperparameters:
17
+ n_estimators: 150
18
+ learning_rate: 0.05
19
+ max_depth: 4
20
+ subsample: 0.8
21
+ colsample_bytree: 0.8
22
+ scale_pos_weight: 2.0
23
+ random_state: 42
24
+
25
+ api:
26
+ host: "127.0.0.1"
27
+ port: 8000
28
+ reload: true
data/processed/churn_predictions_report.csv ADDED
The diff for this file is too large to render. See raw diff
 
data/processed/high_value_at_risk_customers.csv ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Customer ID,Recency,Frequency,Monetary,AvgBucketSize,AvgDaysBetween,Recency_to_AvgDaysRatio,Recent_Orders_Ratio,Is_UK,Churn_Probability,SHAP_Recency,SHAP_Frequency,SHAP_Monetary,SHAP_AvgBucketSize,SHAP_AvgDaysBetween,SHAP_Recency_to_AvgDaysRatio,SHAP_Recent_Orders_Ratio,SHAP_Is_UK,Risk_Tier,Segment,Actionable_Recommendation
2
+ 12918,261,1,10953.5,1.0,180.0,1.4499999194444488,0.0,1,0.8683201,-0.14584176,0.3756971,0.040458303,0.8840396,0.25739065,0.033985544,0.013004925,-0.013065498,High Risk,Potential Loyalists,At-Risk Loyalist: Dedicated customer showing signs of leaving. Offer a special loyalty reward or high-value incentive.
3
+ 15760,265,2,6958.17,1.0,0.0,26499999.999999996,0.0,0,0.7910413,-0.25016513,0.07113772,0.2324797,0.6669705,-0.11506628,0.14898624,0.023567816,0.11278458,High Risk,Potential Loyalists,At-Risk Loyalist: Dedicated customer showing signs of leaving. Offer a special loyalty reward or high-value incentive.
4
+ 15202,55,3,2000.4966666666667,1.0,0.0,5500000.0,1.0,1,0.8577937,-0.0120769255,-0.10333775,0.40868494,0.72960603,0.031659637,0.24801446,0.061697606,-0.0076822555,High Risk,Potential Loyalists,At-Risk Loyalist: Dedicated customer showing signs of leaving. Offer a special loyalty reward or high-value incentive.
5
+ 12737,133,2,1855.25,1.0,0.0,13299999.999999998,0.0,0,0.86656964,-0.068069756,0.14429699,0.42602763,0.6281888,-0.033610195,0.19079919,0.023821695,0.11898945,High Risk,Potential Loyalists,At-Risk Loyalist: Dedicated customer showing signs of leaving. Offer a special loyalty reward or high-value incentive.
6
+ 14028,185,3,1155.1666666666667,796.6666666666666,0.0,18500000.0,0.0,1,0.766893,0.15611957,-0.15823689,-0.10209136,0.26102847,0.18570681,0.40627936,0.0065838774,-0.0050586713,High Risk,Potential Loyalists,At-Risk Loyalist: Dedicated customer showing signs of leaving. Offer a special loyalty reward or high-value incentive.
7
+ 14255,181,1,1000.63,1.0,180.0,1.0055554996913612,0.0,1,0.91079277,0.12798013,0.41695756,0.28489372,0.900302,0.22334704,-0.06761861,0.013092393,-0.016118586,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
8
+ 17369,90,1,979.2,1152.0,180.0,0.49999997222222375,0.0,1,0.71803045,0.0103942,0.47507063,-0.2360715,0.1945713,0.21206254,-0.15888846,0.0033493883,-0.0062943185,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
9
+ 14802,73,2,751.49,1.0,0.0,7299999.999999999,0.0,1,0.88576806,0.040923417,0.1836641,0.48145267,0.6741837,0.007585065,0.22021893,0.010153994,-0.010476549,High Risk,Potential Loyalists,At-Risk Loyalist: Dedicated customer showing signs of leaving. Offer a special loyalty reward or high-value incentive.
10
+ 14091,289,2,733.0830769230769,255.07692307692307,44.0,6.568180325413562,0.0,1,0.7271479,0.091391824,0.17524946,-0.17669925,0.19313954,-0.08684244,0.34631354,0.0009169499,-0.0037881061,High Risk,Potential Loyalists,At-Risk Loyalist: Dedicated customer showing signs of leaving. Offer a special loyalty reward or high-value incentive.
11
+ 18052,199,1,679.82375,1110.375,180.0,1.105555494135806,0.0,1,0.8765103,0.35821477,0.48372772,0.22388105,0.18248336,0.27700907,-0.0032853936,0.0044742,-0.0072329985,High Risk,Potential Loyalists,At-Risk Loyalist: Dedicated customer showing signs of leaving. Offer a special loyalty reward or high-value incentive.
12
+ 13864,21,1,662.4000000000001,192.0,180.0,0.11666666018518554,1.0,1,0.87140644,-0.028443767,0.48841187,0.3369665,0.16094254,0.24510849,0.19651242,0.0738269,-0.00039153988,High Risk,Potential Loyalists,At-Risk Loyalist: Dedicated customer showing signs of leaving. Offer a special loyalty reward or high-value incentive.
13
+ 15929,279,1,594.0,120.0,180.0,1.5499999138888936,0.0,1,0.81515896,0.1256732,0.46727258,0.104192905,0.078614965,0.234024,0.032018322,0.006934156,-0.0053616036,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
14
+ 13270,1,1,590.0,200.0,180.0,0.005555555246913597,1.0,1,0.82054025,-0.15044011,0.48968485,0.11528971,0.35057056,0.26227272,-0.10368249,0.12256415,-0.006765737,High Risk,Potential Loyalists,At-Risk Loyalist: Dedicated customer showing signs of leaving. Offer a special loyalty reward or high-value incentive.
15
+ 14308,184,2,573.51,1.0,0.0,18400000.0,0.0,1,0.906499,0.19546686,0.12831813,0.71812356,0.6581632,-0.05771281,0.18535405,0.014247822,-0.010861011,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
16
+ 12503,219,1,563.0,280.5,180.0,1.2166665990740777,0.0,0,0.849484,0.27350044,0.4294449,0.2781778,0.024391534,0.25317106,-0.06344194,0.017178748,0.07761856,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
17
+ 17310,22,2,512.55,201.0,147.0,0.1496598537646358,0.5,1,0.8062422,-0.049092665,0.31561866,0.355356,0.16486068,0.3545853,-0.19349882,0.044131324,-0.0067038443,High Risk,Potential Loyalists,At-Risk Loyalist: Dedicated customer showing signs of leaving. Offer a special loyalty reward or high-value incentive.
18
+ 13559,147,2,495.0,180.0,30.0,4.899998366667211,0.0,1,0.8264181,0.20834415,0.12245606,0.25340852,0.30387664,-0.08544916,0.32287952,-0.001210685,-0.004372636,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
19
+ 14328,73,1,445.05,1.0,180.0,0.4055555330246926,0.0,1,0.93956363,-0.0031666749,0.49439386,0.73135823,0.86251944,0.28447154,-0.0673614,0.0144158965,-0.013325618,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
20
+ 14933,78,4,442.5,150.0,0.0,7799999.999999999,0.0,0,0.7752419,0.060348637,-0.24076572,0.122391224,0.20989162,0.20933269,0.3749227,0.010439365,0.051071145,High Risk,Potential Loyalists,At-Risk Loyalist: Dedicated customer showing signs of leaving. Offer a special loyalty reward or high-value incentive.
21
+ 13096,29,1,419.7000000000001,6.0,180.0,0.16111110216049432,1.0,1,0.83807516,-0.26944754,0.47000134,0.6362138,0.10482195,0.285685,-0.028517047,0.014973851,-0.01027515,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
22
+ 16737,212,1,417.6,288.0,180.0,1.1777777123456827,0.0,1,0.8591849,0.28989765,0.46507868,0.27312875,0.12854779,0.25804186,-0.046517033,0.0069905496,-0.0071508707,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
23
+ 14956,56,1,331.25,625.0,180.0,0.31111109382716146,1.0,1,0.8023891,-0.08622908,0.48221987,0.2930206,0.13310407,0.23756616,-0.1834748,0.088894166,-0.0043261293,High Risk,Potential Loyalists,At-Risk Loyalist: Dedicated customer showing signs of leaving. Offer a special loyalty reward or high-value incentive.
24
+ 14832,265,1,322.69,1.0,180.0,1.4722221404321032,0.0,1,0.9262868,-0.057054132,0.41229048,0.49762478,0.8607022,0.30551142,0.07147015,0.013004925,-0.013065498,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
25
+ 13942,169,1,320.0,1000.0,180.0,0.938888836728398,0.0,1,0.86956227,0.25778037,0.47029015,0.2386569,0.13503678,0.2795039,0.07809711,0.0038271693,-0.006617478,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
26
+ 13776,63,2,311.99,1.0,0.0,6299999.999999999,0.0,1,0.9061351,0.08695371,0.2076631,0.74798536,0.69797194,-0.07788167,0.16444331,0.010153994,-0.010476549,High Risk,Potential Loyalists,At-Risk Loyalist: Dedicated customer showing signs of leaving. Offer a special loyalty reward or high-value incentive.
27
+ 17077,213,1,306.0,36.0,180.0,1.1833332675925963,0.0,1,0.85449183,0.32563493,0.4737239,0.28877264,0.037688,0.2744987,-0.06548419,0.0069905496,-0.012069167,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
28
+ 15893,116,1,305.28000000000003,288.0,180.0,0.6444444086419773,0.0,1,0.8460431,0.027688151,0.5384277,0.3096512,0.26984128,0.28384647,-0.16075432,0.003313062,-0.008634467,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
29
+ 13383,178,1,305.28000000000003,288.0,180.0,0.9888888339506203,0.0,1,0.87058854,0.19314793,0.49404708,0.3014009,0.24192238,0.26358965,-0.026058193,0.004220874,-0.006617478,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
30
+ 18227,119,1,295.0,1.0,180.0,0.6611110743827181,0.0,1,0.90232384,-0.045089867,0.4882156,0.7754832,0.61632246,0.2863492,-0.3376014,0.014498922,-0.015379935,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
31
+ 16077,260,1,287.55,105.0,180.0,1.4444443641975353,0.0,1,0.73613423,-0.2621814,0.43871605,0.025004843,0.054327212,0.25595507,0.07323174,0.0068274788,-0.0064279893,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
32
+ 17638,282,2,280.8,144.0,74.0,3.8108102958364465,0.0,1,0.720453,0.047233086,0.21030648,0.14712171,0.21434663,-0.19297561,0.076287016,0.0054180957,-0.0015458524,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
33
+ 15413,326,5,271.9488,107.76,11.0,29.63633669423937,0.0,1,0.81940955,0.54656905,-0.36783624,0.09415762,0.18211961,-0.034905687,0.64035094,0.022861568,-0.011482323,High Risk,Potential Loyalists,At-Risk Loyalist: Dedicated customer showing signs of leaving. Offer a special loyalty reward or high-value incentive.
34
+ 13687,73,1,264.01866666666666,1937.0444444444445,180.0,0.4055555330246926,0.0,1,0.79651535,0.051139995,0.524901,0.24140647,0.07159849,0.26918164,-0.23056902,0.0033493883,-0.0068710186,High Risk,Potential Loyalists,At-Risk Loyalist: Dedicated customer showing signs of leaving. Offer a special loyalty reward or high-value incentive.
35
+ 13128,266,1,263.5,31.0,180.0,1.477777695679017,0.0,1,0.750212,-0.18384303,0.43728304,0.062268745,0.047677793,0.26860306,0.028813446,0.009643778,-0.0112220105,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
36
+ 15396,218,1,254.10000000000002,78.0,180.0,1.2111110438271642,0.0,1,0.85103226,0.30042621,0.4737511,0.3197179,0.0031427573,0.2586012,-0.05120978,0.0069905496,-0.009218815,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
37
+ 17353,126,2,250.33333333333334,161.33333333333334,132.0,0.9545453822314104,0.0,1,0.84467256,0.10606239,0.26392543,0.35501146,0.2813725,0.17308062,0.07372454,0.0037325455,-0.0040144715,High Risk,Potential Loyalists,At-Risk Loyalist: Dedicated customer showing signs of leaving. Offer a special loyalty reward or high-value incentive.
38
+ 17925,7,1,244.08,72.0,180.0,0.03888888672839518,1.0,1,0.75641066,-0.06098584,0.4947902,0.008607862,-0.02921516,0.22039022,-0.05360272,0.12026309,-0.007665579,High Risk,Potential Loyalists,At-Risk Loyalist: Dedicated customer showing signs of leaving. Offer a special loyalty reward or high-value incentive.
39
+ 14079,317,1,243.63,1.0,180.0,1.7611110132716103,0.0,1,0.9270312,0.08041994,0.42261404,0.5517617,0.8182054,0.28215742,-0.057272885,0.013247201,-0.009694921,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
40
+ 16943,174,2,241.38,1.0,84.0,2.071428324829961,0.0,1,0.8857018,0.21014504,0.18077694,0.7175967,0.88491416,-0.27503544,-0.1127453,0.012264338,-0.010866449,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
41
+ 15538,172,6,239.8857142857143,72.57142857142857,24.8,6.935481074402793,0.0,1,0.8352917,0.6161796,-0.5144201,0.22870167,0.14732449,0.034406506,0.6614082,0.022810183,-0.0133235995,High Risk,Potential Loyalists,At-Risk Loyalist: Dedicated customer showing signs of leaving. Offer a special loyalty reward or high-value incentive.
42
+ 16088,210,2,239.5716666666667,62.166666666666664,15.0,13.99999066667289,0.0,1,0.80810887,0.35759032,0.13327034,0.33125308,0.12198398,-0.19290407,0.2502246,0.0024329892,-0.0066020433,High Risk,Potential Loyalists,At-Risk Loyalist: Dedicated customer showing signs of leaving. Offer a special loyalty reward or high-value incentive.
43
+ 13734,143,4,239.34428571428575,10.821428571428571,31.0,4.612901737773633,0.0,1,0.778019,0.38236699,-0.07584708,0.4193908,-0.080341525,-0.09105603,0.27122387,0.00048107334,-0.012577674,High Risk,Potential Loyalists,At-Risk Loyalist: Dedicated customer showing signs of leaving. Offer a special loyalty reward or high-value incentive.
44
+ 14213,39,1,238.44,48.8,180.0,0.2166666546296303,1.0,1,0.7997152,-0.14744543,0.49471092,0.3505476,0.07216812,0.26155192,-0.16662256,0.085938975,-0.006852856,High Risk,Potential Loyalists,At-Risk Loyalist: Dedicated customer showing signs of leaving. Offer a special loyalty reward or high-value incentive.
45
+ 17305,283,1,237.27333333333334,111.77777777777777,180.0,1.572222134876548,0.0,1,0.7965884,0.045146357,0.44581264,0.12551177,0.07680425,0.22950946,8.705119e-05,0.0070697567,-0.005353445,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
46
+ 15273,234,1,228.96,216.0,180.0,1.2999999277777818,0.0,1,0.7636944,-0.08355361,0.441052,0.00889778,0.09994274,0.2723697,-0.00644185,0.0069905496,-0.0067343814,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
47
+ 15461,95,2,214.85,115.0,170.0,0.5588234965397944,0.0,1,0.8575641,0.056089323,0.29321954,0.28589597,0.103681564,0.5194643,0.09579922,0.0077854665,-0.0072499337,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
48
+ 14106,372,1,214.8,24.0,180.0,2.066666551851858,0.0,1,0.74580586,0.019533314,0.44525173,0.05051103,-0.028042816,0.23758788,-0.089100294,0.008592701,-0.008485308,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
49
+ 13290,73,1,208.63,1.0,180.0,0.4055555330246926,0.0,1,0.93800575,-0.007822571,0.506005,0.6617401,0.8550486,0.31759185,-0.05746439,0.0144158965,-0.013317459,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
50
+ 16716,265,1,208.08,132.0,180.0,1.4722221404321032,0.0,1,0.7671527,-0.114768855,0.45032713,-0.02761958,0.08510107,0.299645,0.058700155,0.0068274788,-0.0064279893,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
51
+ 17306,28,2,191.84,376.0,10.0,2.7999972000028,1.0,1,0.7140727,-0.25865832,0.29266587,0.09730331,0.36923125,-0.29378274,0.11768084,0.14942782,0.0008609069,High Risk,Potential Loyalists,At-Risk Loyalist: Dedicated customer showing signs of leaving. Offer a special loyalty reward or high-value incentive.
52
+ 15477,284,1,184.2,108.0,180.0,1.5777776901234617,0.0,1,0.78616935,0.04049046,0.45742372,0.03252366,0.07056018,0.27319947,-0.014444533,0.0070697567,-0.005353445,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
53
+ 17474,234,1,174.5,33.333333333333336,180.0,1.2999999277777818,0.0,1,0.74012333,-0.047250398,0.4558094,-0.026870375,-0.042866636,0.30344027,-0.031200213,0.0069905496,-0.011961846,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
54
+ 16663,364,1,165.0,100.0,180.0,2.0222221098765494,0.0,1,0.77520156,0.039883595,0.45725864,0.032774806,0.10054011,0.23216112,-0.06699358,0.007129067,-0.005353445,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
55
+ 16990,57,1,165.0,100.0,180.0,0.31666664907407505,1.0,1,0.82418245,-0.041655477,0.51727825,0.31604987,0.11387758,0.28001815,-0.165594,0.088894166,-0.0044422457,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
56
+ 12850,80,1,165.0,100.0,180.0,0.4444444197530878,0.0,1,0.8130627,0.054528892,0.55204886,0.25336605,0.06524957,0.31505975,-0.20673133,0.0033493883,-0.0073546115,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
57
+ 18017,66,1,165.0,100.0,180.0,0.3666666462962974,0.0,1,0.8170782,0.044613916,0.5336104,0.29319015,0.055722743,0.32247123,-0.18800537,0.0019083486,-0.0073546115,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
58
+ 17152,60,1,163.5,30.0,180.0,0.3333333148148158,0.0,1,0.85258406,0.057985757,0.52903056,0.28890547,0.2145412,0.31807044,-0.089694105,0.0071831476,-0.011527476,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
59
+ 17715,227,1,163.2,192.0,180.0,1.2611110410493866,0.0,1,0.81344557,0.085461125,0.45164868,0.20435496,0.14872381,0.25765806,-0.11601696,0.0069905496,-0.006783393,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
60
+ 16443,155,1,162.24,96.0,180.0,0.8611110632716076,0.0,1,0.8722512,0.2538715,0.501787,0.30852035,0.19683821,0.32780954,-0.10585323,0.0042527774,-0.0067335954,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
61
+ 18273,225,1,153.0,60.0,180.0,1.2499999305555594,0.0,1,0.76272017,0.040120125,0.47992492,0.14772294,-0.060303986,0.29489866,-0.1730021,0.0069905496,-0.009218815,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
62
+ 13551,301,1,152.39999999999998,24.0,180.0,1.6722221293209927,0.0,1,0.7581268,0.019602392,0.44497186,0.052180123,-0.036487423,0.29200095,-0.07039765,0.00853339,-0.008485308,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
63
+ 14845,234,1,150.0,600.0,180.0,1.2999999277777818,0.0,1,0.756506,-0.064797536,0.4106948,0.0018510458,0.08667919,0.26797357,-0.009557817,0.0069905496,-0.0067343814,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
64
+ 12664,149,2,148.52,36.5,212.0,0.7028301555268794,0.0,0,0.7827179,0.10495622,0.10439113,0.29573157,0.091725804,0.0680638,0.03063783,0.014195854,0.13135464,High Risk,Potential Loyalists,At-Risk Loyalist: Dedicated customer showing signs of leaving. Offer a special loyalty reward or high-value incentive.
65
+ 12403,156,1,147.91500000000002,41.0,180.0,0.8666666185185212,0.0,0,0.8557086,0.25329316,0.46825686,0.25347114,-0.08115564,0.33741352,-0.0059517953,0.014440974,0.099807695,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
66
+ 15588,312,1,144.9,24.0,180.0,1.7333332370370425,0.0,1,0.7581268,0.019602392,0.44497186,0.052180123,-0.036487423,0.29200095,-0.07039765,0.00853339,-0.008485308,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
67
+ 17923,162,2,144.75,45.0,16.0,10.124993671878956,0.0,1,0.71028626,0.26093584,0.10520434,0.11913107,-0.04583787,-0.2213832,0.2467466,-0.0016541027,-0.0068859933,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
68
+ 12487,365,1,142.65,87.0,180.0,2.027777665123463,0.0,0,0.76291114,0.019607082,0.42885676,0.036144786,0.014835881,0.23365141,-0.07959073,0.01729321,0.057389535,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
69
+ 12636,373,1,141.0,1.0,180.0,2.0722221070987716,0.0,0,0.9188975,0.056913346,0.42290902,0.44708997,0.80543834,0.24772145,-0.13147782,0.026332468,0.112016365,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
70
+ 13452,107,1,139.8,4.0,180.0,0.5944444114197549,0.0,1,0.8897794,0.022081388,0.55601597,0.57185715,0.15678401,0.37191868,-0.036505695,0.021693984,-0.015873926,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
71
+ 17440,184,1,137.16,36.0,180.0,1.022222165432102,0.0,1,0.8467202,0.17794903,0.5141411,0.25101814,0.12786518,0.28918335,-0.08151595,0.004220874,-0.014275097,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
72
+ 14366,160,1,136.0,16.0,180.0,0.8888888395061756,0.0,1,0.85542715,0.2512185,0.47472706,0.44635943,-0.11264087,0.3205786,-0.036703885,0.0063774344,-0.01261739,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
73
+ 13217,290,1,132.5,250.0,180.0,1.6111110216049431,0.0,1,0.8104051,0.028411692,0.42870918,0.116704464,0.23050293,0.23826113,-0.03261357,0.0070697567,-0.004920002,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
74
+ 17955,290,2,130.86666666666667,136.0,9.0,32.22218641979287,0.0,1,0.76554906,0.09143055,0.13352051,0.08824912,0.40954962,-0.21750519,0.23725858,0.0023541,-0.0020290106,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
75
+ 12362,373,1,130.0,1.0,180.0,2.0722221070987716,0.0,0,0.9188975,0.056913346,0.42290902,0.44708997,0.80543834,0.24772145,-0.13147782,0.026332468,0.112016365,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
76
+ 12555,317,1,129.23,1.0,180.0,1.7611110132716103,0.0,0,0.92567414,0.056162834,0.42548972,0.4477685,0.80945355,0.30605575,-0.10124805,0.025846109,0.112016365,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
77
+ 14463,134,1,128.475,15.5,180.0,0.744444403086422,0.0,1,0.807534,0.025637913,0.49459708,0.19671223,-0.009414781,0.3284372,-0.03647787,0.0062321844,-0.012177299,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
78
+ 16377,252,1,126.45599999999999,285.6,180.0,1.3999999222222266,0.0,1,0.71440095,-0.20710045,0.37690508,-0.13922226,0.19036289,0.26438522,-0.009972773,0.006973106,-0.005994546,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
79
+ 18026,135,2,122.00411764705883,31.176470588235293,4.0,33.74991562521094,0.0,1,0.75952536,0.17362782,0.07013777,-0.07189671,0.260672,0.06101594,0.22249378,0.0043840148,-0.010874458,High Risk,Potential Loyalists,At-Risk Loyalist: Dedicated customer showing signs of leaving. Offer a special loyalty reward or high-value incentive.
80
+ 12770,91,1,118.4375,68.75,180.0,0.5055555274691373,0.0,0,0.7898765,0.008696979,0.46372905,0.06239674,0.02670436,0.32215506,-0.110906675,0.013537589,0.09734964,High Risk,Potential Loyalists,At-Risk Loyalist: Dedicated customer showing signs of leaving. Offer a special loyalty reward or high-value incentive.
81
+ 16291,317,1,117.72,1.0,180.0,1.7611110132716103,0.0,0,0.9079962,0.07205189,0.40750706,0.21611315,0.8166895,0.30365312,-0.10498457,0.025846109,0.112016365,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
82
+ 13689,79,1,116.5,70.0,180.0,0.4388888645061742,0.0,1,0.7583982,0.004534104,0.5271085,0.033158135,0.01023579,0.31385162,-0.18015338,0.0033493883,-0.008685282,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
83
+ 12482,211,29,116.1343137254902,132.55882352941177,2.642857142857143,79.83753574986473,0.0,0,0.84622484,0.7961586,-1.0141556,0.015162547,0.37464514,0.122670256,0.7423003,0.13462669,0.09336659,High Risk,Champions,At-Risk Champion: High historical spend. Assign a personal account manager for direct outreach. Do not send automated discount spam.
84
+ 14033,190,1,109.44,576.0,180.0,1.0555554969135834,0.0,1,0.82589555,0.2743012,0.44985548,-0.14528352,0.40516403,0.2011673,-0.06413387,0.004220874,-0.008997483,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
85
+ 16047,317,1,107.14,1.0,180.0,1.7611110132716103,0.0,0,0.8836207,0.14292195,0.39505908,-0.052152354,0.8136765,0.25752944,-0.10824186,0.025846109,0.112016365,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
86
+ 15486,162,1,102.0,40.0,180.0,0.8999999500000028,0.0,1,0.7704043,0.27469337,0.4682121,-0.268406,-0.006926936,0.27524278,0.031800497,0.0041461005,-0.008685423,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
87
+ 17570,16,1,100.0,1.0,180.0,0.08888888395061756,1.0,1,0.90564007,-0.059992082,0.43089333,-0.0150554,0.96384317,0.18816493,0.25993294,0.05891501,-0.005693866,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
88
+ 14405,21,1,97.5,50.0,180.0,0.11666666018518554,1.0,1,0.70704037,-0.06208126,0.45313776,-0.45464393,0.016201207,0.18768324,0.22824828,0.0738269,-0.0018383281,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
89
+ 15473,79,1,91.60000000000001,34.666666666666664,180.0,0.4388888645061742,0.0,1,0.7023224,-0.012668301,0.5058844,-0.35742375,0.14846462,0.25152698,-0.10974328,0.0033493883,-0.011527476,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
90
+ 14436,188,1,91.32,84.0,180.0,1.0444443864197563,0.0,1,0.7559669,0.15942195,0.4632893,-0.2910039,0.21186559,0.25495398,-0.10309199,0.004220874,-0.009481076,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
91
+ 12934,371,1,90.825,15.5,180.0,2.0611109966049446,0.0,1,0.74671805,0.25627404,0.40513808,-0.15259355,-0.019050045,0.21447068,-0.0614618,0.006837304,-0.008949365,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
92
+ 15595,122,1,89.39999999999999,36.0,180.0,0.6777777401234588,0.0,1,0.77405286,0.08320883,0.51091033,-0.31873354,0.19429693,0.24377802,0.08409204,0.004107526,-0.010839812,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
93
+ 13222,212,1,88.5,30.0,180.0,1.1777777123456827,0.0,1,0.7867836,0.34685043,0.44091654,-0.26807287,0.13854462,0.27394763,-0.06480529,0.009806849,-0.012061008,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
94
+ 13091,364,2,82.33333333333333,36.375,0.0,36400000.0,0.0,1,0.7786999,0.25861734,0.07590865,-0.09289283,0.16430083,0.060402997,0.35446298,0.0048178704,-0.008030576,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
95
+ 15864,16,1,81.48,98.0,180.0,0.08888888395061756,1.0,1,0.7252866,-0.14857061,0.45528546,-0.2572866,0.04305798,0.21587747,0.14250846,0.08030854,-0.0008608361,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
96
+ 17336,107,1,81.12,48.0,180.0,0.5944444114197549,0.0,1,0.71907973,-0.051948063,0.51438844,-0.24020365,0.022857245,0.28482383,-0.027676715,0.007932247,-0.010790947,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
97
+ 14190,301,1,79.9,2.0,180.0,1.6722221293209927,0.0,1,0.83312607,0.28912613,0.42488518,0.08609699,0.14164144,0.30887416,-0.085104465,0.012009043,-0.010100378,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
98
+ 16165,140,1,79.9,2.0,180.0,0.7777777345679037,0.0,1,0.7800712,0.03300752,0.4249649,0.04978654,0.1406256,0.26942003,-0.09071067,0.011678684,-0.013210046,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
99
+ 14207,322,1,78.9,18.0,180.0,1.7888887895061782,0.0,1,0.7582457,0.28483862,0.41708556,-0.07926231,-0.13486601,0.24296807,-0.030216305,0.010969019,-0.008949365,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
100
+ 15782,65,1,77.74222222222221,54.44444444444444,180.0,0.3611110910493838,0.0,1,0.7132219,0.017856965,0.4874556,-0.23715499,0.070420705,0.2669529,-0.12818903,0.0019083486,-0.008685282,High Risk,Potential Loyalists,At-Risk Loyalist: Dedicated customer showing signs of leaving. Offer a special loyalty reward or high-value incentive.
101
+ 12570,317,1,77.52,1.0,180.0,1.7611110132716103,0.0,0,0.9200536,0.2723058,0.4004957,0.1844653,0.8153533,0.28692684,-0.09485146,0.025846109,0.112016365,High Risk,Hibernating,Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage.
data/processed/rfm_segments.csv ADDED
The diff for this file is too large to render. See raw diff
 
images/image-1.png ADDED
images/image.png ADDED
images/newplot.png ADDED
logs/customer_analytics.log ADDED
@@ -0,0 +1,485 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [2026-06-30 22:57:57] INFO [api.py:30] Attempting to load churn model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
2
+ [2026-06-30 22:58:00] INFO [api.py:34] Successfully loaded churn XGBoost model.
3
+ [2026-06-30 22:58:03] INFO [api.py:30] Attempting to load churn model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
4
+ [2026-06-30 22:58:05] INFO [train.py:27] Loading cleaned dataset from E:\project\customer_segmentation_project\data\processed\online_retail_clean.csv...
5
+ [2026-06-30 22:58:05] INFO [train.py:32] Computing cutoff date for prediction window...
6
+ [2026-06-30 22:58:05] INFO [train.py:35] Reference snapshot cutoff date: 2010-09-10
7
+ [2026-06-30 22:58:05] INFO [train.py:37] Splitting dataset into history vs target prediction window...
8
+ [2026-06-30 22:58:05] INFO [train.py:44] Engineering base RFM features...
9
+ [2026-06-30 22:58:06] INFO [train.py:61] Engineering advanced MLE features...
10
+ [2026-06-30 22:58:06] INFO [train.py:103] Target Label Generation complete. Churn class ratio: 42.49%
11
+ [2026-06-30 22:58:06] INFO [train.py:115] Initializing XGBoost classifier with config hyperparameters...
12
+ [2026-06-30 22:58:06] INFO [train.py:127] Fitting model on training set...
13
+ [2026-06-30 22:58:07] INFO [api.py:34] Successfully loaded churn XGBoost model.
14
+ [2026-06-30 22:58:07] INFO [train.py:130] Evaluating model on validation hold-out set:
15
+ [2026-06-30 22:58:07] INFO [train.py:133]
16
+ precision recall f1-score support
17
+
18
+ 0 0.78 0.47 0.59 397
19
+ 1 0.52 0.81 0.63 279
20
+
21
+ accuracy 0.61 676
22
+ macro avg 0.65 0.64 0.61 676
23
+ weighted avg 0.67 0.61 0.61 676
24
+
25
+ [2026-06-30 22:58:07] INFO [train.py:138] Saving serialized model to E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
26
+ [2026-06-30 22:58:07] INFO [train.py:140] Model training pipeline complete!
27
+ [2026-06-30 22:58:13] INFO [predict.py:35] Loading cleaned dataset...
28
+ [2026-06-30 22:58:14] INFO [predict.py:41] Engineering customer RFM features...
29
+ [2026-06-30 22:58:14] INFO [predict.py:43] Current snapshot date (Reference 'Today'): 2010-12-09
30
+ [2026-06-30 22:58:14] INFO [predict.py:62] Calculating average days between purchases...
31
+ [2026-06-30 22:58:15] INFO [predict.py:81] Calculating order frequency ratios in recent days...
32
+ [2026-06-30 22:58:15] INFO [predict.py:101] Loading trained XGBoost model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
33
+ [2026-06-30 22:58:19] INFO [predict.py:114] Running predictions...
34
+ [2026-06-30 22:58:19] INFO [predict.py:129] Merging predictions with offline segments...
35
+ [2026-06-30 22:58:19] INFO [predict.py:142] Generating targeted marketing recommendations...
36
+ [2026-06-30 22:58:19] INFO [predict.py:174] Exporting CSV reports...
37
+ [2026-06-30 22:58:19] INFO [predict.py:179] -> Complete Churn Report saved to: E:\project\customer_segmentation_project\data\processed\churn_predictions_report.csv
38
+ [2026-06-30 22:58:19] INFO [predict.py:186] -> Top 100 High-Value At-Risk Customers saved to: E:\project\customer_segmentation_project\data\processed\high_value_at_risk_customers.csv
39
+ [2026-06-30 22:58:19] INFO [predict.py:189]
40
+ ==================================================
41
+ [2026-06-30 22:58:19] INFO [predict.py:190] CHURN ANALYSIS SUMMARY
42
+ [2026-06-30 22:58:19] INFO [predict.py:191] ==================================================
43
+ [2026-06-30 22:58:19] INFO [predict.py:192] Total Customers Analyzed: 4312
44
+ [2026-06-30 22:58:19] INFO [predict.py:199] High Risk (Churn Prob >= 70%): 1340 (31.1%)
45
+ [2026-06-30 22:58:19] INFO [predict.py:200] Medium Risk (30% <= Prob < 70%): 1966 (45.6%)
46
+ [2026-06-30 22:58:19] INFO [predict.py:201] Low Risk (Churn Prob < 30%): 1006 (23.3%)
47
+ [2026-06-30 22:58:19] INFO [predict.py:202] --------------------------------------------------
48
+ [2026-06-30 22:58:19] INFO [predict.py:205] Total Revenue At Risk (High Risk): $108,704.41
49
+ [2026-06-30 22:58:19] INFO [predict.py:206] ==================================================
50
+
51
+ [2026-06-30 22:58:26] INFO [export_json.py:32] Reading prediction outputs for JSON export...
52
+ [2026-06-30 22:58:26] INFO [export_json.py:78] Saving summary statistics to E:\project\customer_segmentation_project\dashboard\public\data\summary.json...
53
+ [2026-06-30 22:58:26] INFO [export_json.py:82] Formatting individual customer records for frontend table...
54
+ [2026-06-30 22:58:26] INFO [export_json.py:97] Saving 4312 formatted records to E:\project\customer_segmentation_project\dashboard\public\data\customers.json...
55
+ [2026-06-30 22:58:26] INFO [export_json.py:101] JSON Data Export Completed successfully!
56
+ [2026-06-30 23:00:29] INFO [api.py:75] Executing health check request...
57
+ [2026-06-30 23:00:29] ERROR [api.py:85] Inference requested but model is not loaded.
58
+ [2026-06-30 23:00:29] ERROR [api.py:144] Inference batch requested but model is not loaded.
59
+ [2026-06-30 23:00:37] INFO [api.py:30] Attempting to load churn model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
60
+ [2026-06-30 23:00:39] INFO [api.py:34] Successfully loaded churn XGBoost model.
61
+ [2026-06-30 23:00:39] INFO [api.py:75] Executing health check request...
62
+ [2026-06-30 23:00:39] INFO [api.py:30] Attempting to load churn model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
63
+ [2026-06-30 23:00:39] INFO [api.py:34] Successfully loaded churn XGBoost model.
64
+ [2026-06-30 23:00:39] INFO [api.py:113] Running inference for customer profile: recency=10, freq=5
65
+ [2026-06-30 23:00:39] INFO [api.py:30] Attempting to load churn model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
66
+ [2026-06-30 23:00:39] INFO [api.py:34] Successfully loaded churn XGBoost model.
67
+ [2026-06-30 23:00:39] INFO [api.py:174] Running batch inference for 2 profiles...
68
+ [2026-06-30 23:00:42] INFO [api.py:30] Attempting to load churn model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
69
+ [2026-06-30 23:00:47] INFO [api.py:34] Successfully loaded churn XGBoost model.
70
+ [2026-06-30 23:00:51] INFO [api.py:30] Attempting to load churn model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
71
+ [2026-06-30 23:00:54] INFO [api.py:34] Successfully loaded churn XGBoost model.
72
+ [2026-06-30 23:04:46] INFO [api.py:30] Attempting to load churn model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
73
+ [2026-06-30 23:04:49] INFO [api.py:34] Successfully loaded churn XGBoost model.
74
+ [2026-06-30 23:07:13] INFO [api.py:40] Attempting to load churn model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
75
+ [2026-06-30 23:07:16] INFO [api.py:44] Successfully loaded churn XGBoost model.
76
+ [2026-06-30 23:08:12] INFO [api.py:40] Attempting to load churn model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
77
+ [2026-06-30 23:08:14] INFO [api.py:44] Successfully loaded churn XGBoost model.
78
+ [2026-06-30 23:09:09] INFO [api.py:123] Running inference for customer profile: recency=65, freq=3
79
+ [2026-06-30 23:11:22] INFO [api.py:41] Attempting to load churn model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
80
+ [2026-06-30 23:11:23] INFO [api.py:45] Successfully loaded churn XGBoost model.
81
+ [2026-06-30 23:11:39] INFO [api.py:41] Attempting to load churn model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
82
+ [2026-06-30 23:11:41] INFO [api.py:45] Successfully loaded churn XGBoost model.
83
+ [2026-06-30 23:11:48] INFO [api.py:41] Attempting to load churn model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
84
+ [2026-06-30 23:11:50] INFO [api.py:45] Successfully loaded churn XGBoost model.
85
+ [2026-06-30 23:11:52] INFO [predict.py:36] Loading cleaned dataset...
86
+ [2026-06-30 23:11:52] INFO [predict.py:42] Engineering customer RFM features...
87
+ [2026-06-30 23:11:52] INFO [predict.py:44] Current snapshot date (Reference 'Today'): 2010-12-09
88
+ [2026-06-30 23:11:53] INFO [predict.py:63] Calculating average days between purchases...
89
+ [2026-06-30 23:11:53] INFO [predict.py:82] Calculating order frequency ratios in recent days...
90
+ [2026-06-30 23:11:53] INFO [predict.py:102] Loading trained XGBoost model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
91
+ [2026-06-30 23:11:53] INFO [predict.py:115] Running predictions...
92
+ [2026-06-30 23:11:53] INFO [predict.py:118] Calculating TreeSHAP contributions...
93
+ [2026-06-30 23:11:54] INFO [predict.py:137] Merging predictions with offline segments...
94
+ [2026-06-30 23:11:54] INFO [predict.py:150] Generating targeted marketing recommendations...
95
+ [2026-06-30 23:11:54] INFO [predict.py:182] Exporting CSV reports...
96
+ [2026-06-30 23:11:54] INFO [predict.py:187] -> Complete Churn Report saved to: E:\project\customer_segmentation_project\data\processed\churn_predictions_report.csv
97
+ [2026-06-30 23:11:54] INFO [predict.py:194] -> Top 100 High-Value At-Risk Customers saved to: E:\project\customer_segmentation_project\data\processed\high_value_at_risk_customers.csv
98
+ [2026-06-30 23:11:54] INFO [predict.py:197]
99
+ ==================================================
100
+ [2026-06-30 23:11:54] INFO [predict.py:198] CHURN ANALYSIS SUMMARY
101
+ [2026-06-30 23:11:54] INFO [predict.py:199] ==================================================
102
+ [2026-06-30 23:11:54] INFO [predict.py:200] Total Customers Analyzed: 4312
103
+ [2026-06-30 23:11:54] INFO [predict.py:207] High Risk (Churn Prob >= 70%): 1340 (31.1%)
104
+ [2026-06-30 23:11:54] INFO [predict.py:208] Medium Risk (30% <= Prob < 70%): 1966 (45.6%)
105
+ [2026-06-30 23:11:54] INFO [predict.py:209] Low Risk (Churn Prob < 30%): 1006 (23.3%)
106
+ [2026-06-30 23:11:54] INFO [predict.py:210] --------------------------------------------------
107
+ [2026-06-30 23:11:54] INFO [predict.py:213] Total Revenue At Risk (High Risk): $108,704.41
108
+ [2026-06-30 23:11:54] INFO [predict.py:214] ==================================================
109
+
110
+ [2026-06-30 23:11:59] INFO [export_json.py:32] Reading prediction outputs for JSON export...
111
+ [2026-06-30 23:11:59] INFO [export_json.py:78] Saving summary statistics to E:\project\customer_segmentation_project\dashboard\public\data\summary.json...
112
+ [2026-06-30 23:11:59] INFO [export_json.py:82] Formatting individual customer records for frontend table...
113
+ [2026-06-30 23:12:00] INFO [export_json.py:107] Saving 4312 formatted records to E:\project\customer_segmentation_project\dashboard\public\data\customers.json...
114
+ [2026-06-30 23:12:00] INFO [export_json.py:111] JSON Data Export Completed successfully!
115
+ [2026-06-30 23:12:58] INFO [api.py:41] Attempting to load churn model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
116
+ [2026-06-30 23:12:59] INFO [api.py:45] Successfully loaded churn XGBoost model.
117
+ [2026-06-30 23:12:59] INFO [api.py:86] Executing health check request...
118
+ [2026-06-30 23:12:59] INFO [api.py:41] Attempting to load churn model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
119
+ [2026-06-30 23:12:59] INFO [api.py:45] Successfully loaded churn XGBoost model.
120
+ [2026-06-30 23:12:59] INFO [api.py:124] Running inference for customer profile: recency=10, freq=5
121
+ [2026-06-30 23:12:59] INFO [api.py:41] Attempting to load churn model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
122
+ [2026-06-30 23:12:59] INFO [api.py:45] Successfully loaded churn XGBoost model.
123
+ [2026-06-30 23:12:59] INFO [api.py:206] Running batch inference for 2 profiles...
124
+ [2026-06-30 23:13:38] INFO [api.py:42] Attempting to load churn model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
125
+ [2026-06-30 23:13:39] INFO [api.py:46] Successfully loaded churn XGBoost model.
126
+ [2026-06-30 23:13:39] INFO [api.py:58] Initialized inference history log at E:\project\customer_segmentation_project\logs\inference_history.csv
127
+ [2026-06-30 23:13:48] INFO [api.py:43] Attempting to load churn model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
128
+ [2026-06-30 23:13:49] INFO [api.py:47] Successfully loaded churn XGBoost model.
129
+ [2026-06-30 23:13:49] INFO [api.py:136] Running inference for customer profile: recency=65, freq=3
130
+ [2026-06-30 23:14:01] INFO [api.py:43] Attempting to load churn model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
131
+ [2026-06-30 23:14:02] INFO [api.py:47] Successfully loaded churn XGBoost model.
132
+ [2026-06-30 23:14:13] INFO [api.py:43] Attempting to load churn model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
133
+ [2026-06-30 23:14:15] INFO [api.py:47] Successfully loaded churn XGBoost model.
134
+ [2026-06-30 23:14:15] INFO [api.py:98] Executing health check request...
135
+ [2026-06-30 23:14:15] INFO [api.py:43] Attempting to load churn model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
136
+ [2026-06-30 23:14:15] INFO [api.py:47] Successfully loaded churn XGBoost model.
137
+ [2026-06-30 23:14:15] INFO [api.py:148] Running inference for customer profile: recency=10, freq=5
138
+ [2026-06-30 23:14:15] INFO [api.py:43] Attempting to load churn model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
139
+ [2026-06-30 23:14:15] INFO [api.py:47] Successfully loaded churn XGBoost model.
140
+ [2026-06-30 23:14:15] INFO [api.py:230] Running batch inference for 2 profiles...
141
+ [2026-06-30 23:14:15] INFO [api.py:43] Attempting to load churn model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
142
+ [2026-06-30 23:14:15] INFO [api.py:47] Successfully loaded churn XGBoost model.
143
+ [2026-06-30 23:14:21] INFO [api.py:43] Attempting to load churn model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
144
+ [2026-06-30 23:14:22] INFO [api.py:47] Successfully loaded churn XGBoost model.
145
+ [2026-06-30 23:15:12] INFO [api.py:43] Attempting to load churn model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
146
+ [2026-06-30 23:15:13] INFO [api.py:47] Successfully loaded churn XGBoost model.
147
+ [2026-06-30 23:15:52] INFO [train_challenger.py:25] Loading cleaned dataset from E:\project\customer_segmentation_project\data\processed\online_retail_clean.csv for Challenger training...
148
+ [2026-06-30 23:15:53] INFO [train_challenger.py:99] Initializing RandomForest challenger classifier...
149
+ [2026-06-30 23:15:53] INFO [train_challenger.py:107] Fitting Random Forest challenger on training set...
150
+ [2026-06-30 23:15:54] INFO [train_challenger.py:110] Evaluating Challenger model on hold-out validation:
151
+ [2026-06-30 23:15:54] INFO [train_challenger.py:113]
152
+ precision recall f1-score support
153
+
154
+ 0 0.75 0.62 0.68 397
155
+ 1 0.56 0.70 0.62 279
156
+
157
+ accuracy 0.65 676
158
+ macro avg 0.65 0.66 0.65 676
159
+ weighted avg 0.67 0.65 0.65 676
160
+
161
+ [2026-06-30 23:15:54] INFO [train_challenger.py:116] Saving serialized Random Forest challenger model to E:\project\customer_segmentation_project\models\churn_rf_model.pkl...
162
+ [2026-06-30 23:15:54] INFO [train_challenger.py:118] Challenger RF model training complete!
163
+ [2026-06-30 23:16:14] INFO [api.py:50] Attempting to load champion model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
164
+ [2026-06-30 23:16:15] INFO [api.py:54] Successfully loaded champion XGBoost model.
165
+ [2026-06-30 23:16:15] INFO [api.py:61] Attempting to load challenger model from E:\project\customer_segmentation_project\models\churn_rf_model.pkl...
166
+ [2026-06-30 23:16:15] INFO [api.py:65] Successfully loaded challenger Random Forest model.
167
+ [2026-06-30 23:16:15] INFO [api.py:100] Successfully initialized shadow prediction database at E:\project\customer_segmentation_project\logs\predictions.db
168
+ [2026-06-30 23:16:26] INFO [api.py:50] Attempting to load champion model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
169
+ [2026-06-30 23:16:28] INFO [api.py:54] Successfully loaded champion XGBoost model.
170
+ [2026-06-30 23:16:28] INFO [api.py:61] Attempting to load challenger model from E:\project\customer_segmentation_project\models\churn_rf_model.pkl...
171
+ [2026-06-30 23:16:28] INFO [api.py:65] Successfully loaded challenger Random Forest model.
172
+ [2026-06-30 23:16:28] INFO [api.py:100] Successfully initialized shadow prediction database at E:\project\customer_segmentation_project\logs\predictions.db
173
+ [2026-06-30 23:16:35] INFO [api.py:50] Attempting to load champion model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
174
+ [2026-06-30 23:16:36] INFO [api.py:54] Successfully loaded champion XGBoost model.
175
+ [2026-06-30 23:16:36] INFO [api.py:61] Attempting to load challenger model from E:\project\customer_segmentation_project\models\churn_rf_model.pkl...
176
+ [2026-06-30 23:16:37] INFO [api.py:65] Successfully loaded challenger Random Forest model.
177
+ [2026-06-30 23:16:37] INFO [api.py:100] Successfully initialized shadow prediction database at E:\project\customer_segmentation_project\logs\predictions.db
178
+ [2026-06-30 23:16:44] INFO [api.py:50] Attempting to load champion model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
179
+ [2026-06-30 23:16:45] INFO [api.py:54] Successfully loaded champion XGBoost model.
180
+ [2026-06-30 23:16:45] INFO [api.py:61] Attempting to load challenger model from E:\project\customer_segmentation_project\models\churn_rf_model.pkl...
181
+ [2026-06-30 23:16:45] INFO [api.py:65] Successfully loaded challenger Random Forest model.
182
+ [2026-06-30 23:16:45] INFO [api.py:100] Successfully initialized shadow prediction database at E:\project\customer_segmentation_project\logs\predictions.db
183
+ [2026-06-30 23:16:47] INFO [api.py:50] Attempting to load champion model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
184
+ [2026-06-30 23:16:47] INFO [api.py:54] Successfully loaded champion XGBoost model.
185
+ [2026-06-30 23:16:47] INFO [api.py:61] Attempting to load challenger model from E:\project\customer_segmentation_project\models\churn_rf_model.pkl...
186
+ [2026-06-30 23:16:48] INFO [api.py:65] Successfully loaded challenger Random Forest model.
187
+ [2026-06-30 23:16:48] INFO [api.py:100] Successfully initialized shadow prediction database at E:\project\customer_segmentation_project\logs\predictions.db
188
+ [2026-06-30 23:16:48] INFO [api.py:139] Executing health check request...
189
+ [2026-06-30 23:16:48] INFO [api.py:50] Attempting to load champion model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
190
+ [2026-06-30 23:16:48] INFO [api.py:54] Successfully loaded champion XGBoost model.
191
+ [2026-06-30 23:16:48] INFO [api.py:61] Attempting to load challenger model from E:\project\customer_segmentation_project\models\churn_rf_model.pkl...
192
+ [2026-06-30 23:16:48] INFO [api.py:65] Successfully loaded challenger Random Forest model.
193
+ [2026-06-30 23:16:48] INFO [api.py:100] Successfully initialized shadow prediction database at E:\project\customer_segmentation_project\logs\predictions.db
194
+ [2026-06-30 23:16:48] INFO [api.py:204] Running inference for customer profile: recency=10, freq=5
195
+ [2026-06-30 23:16:48] INFO [api.py:50] Attempting to load champion model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
196
+ [2026-06-30 23:16:48] INFO [api.py:54] Successfully loaded champion XGBoost model.
197
+ [2026-06-30 23:16:48] INFO [api.py:61] Attempting to load challenger model from E:\project\customer_segmentation_project\models\churn_rf_model.pkl...
198
+ [2026-06-30 23:16:48] INFO [api.py:65] Successfully loaded challenger Random Forest model.
199
+ [2026-06-30 23:16:48] INFO [api.py:100] Successfully initialized shadow prediction database at E:\project\customer_segmentation_project\logs\predictions.db
200
+ [2026-06-30 23:16:48] INFO [api.py:300] Running batch inference for 2 profiles...
201
+ [2026-06-30 23:16:48] INFO [api.py:50] Attempting to load champion model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
202
+ [2026-06-30 23:16:48] INFO [api.py:54] Successfully loaded champion XGBoost model.
203
+ [2026-06-30 23:16:48] INFO [api.py:61] Attempting to load challenger model from E:\project\customer_segmentation_project\models\churn_rf_model.pkl...
204
+ [2026-06-30 23:16:48] INFO [api.py:65] Successfully loaded challenger Random Forest model.
205
+ [2026-06-30 23:16:48] INFO [api.py:100] Successfully initialized shadow prediction database at E:\project\customer_segmentation_project\logs\predictions.db
206
+ [2026-06-30 23:16:48] INFO [api.py:50] Attempting to load champion model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
207
+ [2026-06-30 23:16:48] INFO [api.py:54] Successfully loaded champion XGBoost model.
208
+ [2026-06-30 23:16:48] INFO [api.py:61] Attempting to load challenger model from E:\project\customer_segmentation_project\models\churn_rf_model.pkl...
209
+ [2026-06-30 23:16:48] INFO [api.py:65] Successfully loaded challenger Random Forest model.
210
+ [2026-06-30 23:16:48] INFO [api.py:100] Successfully initialized shadow prediction database at E:\project\customer_segmentation_project\logs\predictions.db
211
+ [2026-06-30 23:17:04] INFO [api.py:50] Attempting to load champion model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
212
+ [2026-06-30 23:17:05] INFO [api.py:54] Successfully loaded champion XGBoost model.
213
+ [2026-06-30 23:17:05] INFO [api.py:61] Attempting to load challenger model from E:\project\customer_segmentation_project\models\churn_rf_model.pkl...
214
+ [2026-06-30 23:17:05] INFO [api.py:65] Successfully loaded challenger Random Forest model.
215
+ [2026-06-30 23:17:05] INFO [api.py:100] Successfully initialized shadow prediction database at E:\project\customer_segmentation_project\logs\predictions.db
216
+ [2026-06-30 23:17:05] INFO [api.py:139] Executing health check request...
217
+ [2026-06-30 23:17:05] INFO [api.py:50] Attempting to load champion model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
218
+ [2026-06-30 23:17:05] INFO [api.py:54] Successfully loaded champion XGBoost model.
219
+ [2026-06-30 23:17:05] INFO [api.py:61] Attempting to load challenger model from E:\project\customer_segmentation_project\models\churn_rf_model.pkl...
220
+ [2026-06-30 23:17:05] INFO [api.py:65] Successfully loaded challenger Random Forest model.
221
+ [2026-06-30 23:17:05] INFO [api.py:100] Successfully initialized shadow prediction database at E:\project\customer_segmentation_project\logs\predictions.db
222
+ [2026-06-30 23:17:05] INFO [api.py:204] Running inference for customer profile: recency=10, freq=5
223
+ [2026-06-30 23:17:05] INFO [api.py:50] Attempting to load champion model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
224
+ [2026-06-30 23:17:05] INFO [api.py:54] Successfully loaded champion XGBoost model.
225
+ [2026-06-30 23:17:05] INFO [api.py:61] Attempting to load challenger model from E:\project\customer_segmentation_project\models\churn_rf_model.pkl...
226
+ [2026-06-30 23:17:06] INFO [api.py:65] Successfully loaded challenger Random Forest model.
227
+ [2026-06-30 23:17:06] INFO [api.py:100] Successfully initialized shadow prediction database at E:\project\customer_segmentation_project\logs\predictions.db
228
+ [2026-06-30 23:17:06] INFO [api.py:305] Running batch inference for 2 profiles...
229
+ [2026-06-30 23:17:06] INFO [api.py:50] Attempting to load champion model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
230
+ [2026-06-30 23:17:06] INFO [api.py:54] Successfully loaded champion XGBoost model.
231
+ [2026-06-30 23:17:06] INFO [api.py:61] Attempting to load challenger model from E:\project\customer_segmentation_project\models\churn_rf_model.pkl...
232
+ [2026-06-30 23:17:06] INFO [api.py:65] Successfully loaded challenger Random Forest model.
233
+ [2026-06-30 23:17:06] INFO [api.py:100] Successfully initialized shadow prediction database at E:\project\customer_segmentation_project\logs\predictions.db
234
+ [2026-06-30 23:17:06] INFO [api.py:50] Attempting to load champion model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
235
+ [2026-06-30 23:17:06] INFO [api.py:54] Successfully loaded champion XGBoost model.
236
+ [2026-06-30 23:17:06] INFO [api.py:61] Attempting to load challenger model from E:\project\customer_segmentation_project\models\churn_rf_model.pkl...
237
+ [2026-06-30 23:17:06] INFO [api.py:65] Successfully loaded challenger Random Forest model.
238
+ [2026-06-30 23:17:06] INFO [api.py:100] Successfully initialized shadow prediction database at E:\project\customer_segmentation_project\logs\predictions.db
239
+ [2026-06-30 23:17:08] INFO [api.py:50] Attempting to load champion model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
240
+ [2026-06-30 23:17:09] INFO [api.py:54] Successfully loaded champion XGBoost model.
241
+ [2026-06-30 23:17:09] INFO [api.py:61] Attempting to load challenger model from E:\project\customer_segmentation_project\models\churn_rf_model.pkl...
242
+ [2026-06-30 23:17:09] INFO [api.py:65] Successfully loaded challenger Random Forest model.
243
+ [2026-06-30 23:17:09] INFO [api.py:100] Successfully initialized shadow prediction database at E:\project\customer_segmentation_project\logs\predictions.db
244
+ [2026-06-30 23:18:07] INFO [api.py:50] Attempting to load champion model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
245
+ [2026-06-30 23:18:09] INFO [api.py:54] Successfully loaded champion XGBoost model.
246
+ [2026-06-30 23:18:09] INFO [api.py:61] Attempting to load challenger model from E:\project\customer_segmentation_project\models\churn_rf_model.pkl...
247
+ [2026-06-30 23:18:09] INFO [api.py:65] Successfully loaded challenger Random Forest model.
248
+ [2026-06-30 23:18:09] INFO [api.py:100] Successfully initialized shadow prediction database at E:\project\customer_segmentation_project\logs\predictions.db
249
+ [2026-06-30 23:18:56] INFO [api.py:51] Attempting to load champion model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
250
+ [2026-06-30 23:18:57] INFO [api.py:55] Successfully loaded champion XGBoost model.
251
+ [2026-06-30 23:18:57] INFO [api.py:62] Attempting to load challenger model from E:\project\customer_segmentation_project\models\churn_rf_model.pkl...
252
+ [2026-06-30 23:18:58] INFO [api.py:66] Successfully loaded challenger Random Forest model.
253
+ [2026-06-30 23:18:58] INFO [api.py:101] Successfully initialized shadow prediction database at E:\project\customer_segmentation_project\logs\predictions.db
254
+ [2026-06-30 23:19:09] INFO [api.py:52] Attempting to load champion model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
255
+ [2026-06-30 23:19:11] INFO [api.py:56] Successfully loaded champion XGBoost model.
256
+ [2026-06-30 23:19:11] INFO [api.py:63] Attempting to load challenger model from E:\project\customer_segmentation_project\models\churn_rf_model.pkl...
257
+ [2026-06-30 23:19:12] INFO [api.py:67] Successfully loaded challenger Random Forest model.
258
+ [2026-06-30 23:19:12] INFO [api.py:102] Successfully initialized shadow prediction database at E:\project\customer_segmentation_project\logs\predictions.db
259
+ [2026-06-30 23:19:22] INFO [api.py:52] Attempting to load champion model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
260
+ [2026-06-30 23:19:24] INFO [api.py:56] Successfully loaded champion XGBoost model.
261
+ [2026-06-30 23:19:24] INFO [api.py:63] Attempting to load challenger model from E:\project\customer_segmentation_project\models\churn_rf_model.pkl...
262
+ [2026-06-30 23:19:24] INFO [api.py:67] Successfully loaded challenger Random Forest model.
263
+ [2026-06-30 23:19:24] INFO [api.py:102] Successfully initialized shadow prediction database at E:\project\customer_segmentation_project\logs\predictions.db
264
+ [2026-06-30 23:19:24] INFO [api.py:114] Successfully loaded 4312 customer records into memory for WebSocket streaming.
265
+ [2026-06-30 23:19:36] INFO [api.py:52] Attempting to load champion model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
266
+ [2026-06-30 23:19:38] INFO [api.py:56] Successfully loaded champion XGBoost model.
267
+ [2026-06-30 23:19:38] INFO [api.py:63] Attempting to load challenger model from E:\project\customer_segmentation_project\models\churn_rf_model.pkl...
268
+ [2026-06-30 23:19:38] INFO [api.py:67] Successfully loaded challenger Random Forest model.
269
+ [2026-06-30 23:19:38] INFO [api.py:102] Successfully initialized shadow prediction database at E:\project\customer_segmentation_project\logs\predictions.db
270
+ [2026-06-30 23:19:38] INFO [api.py:114] Successfully loaded 4312 customer records into memory for WebSocket streaming.
271
+ [2026-06-30 23:21:38] INFO [validate_retraining.py:28] Initializing model performance validation check...
272
+ [2026-06-30 23:21:41] INFO [validate_retraining.py:117] Retrained Model validation result: Recall = 81.36%
273
+ [2026-06-30 23:21:41] INFO [validate_retraining.py:118] Target Performance Threshold: Recall >= 80.00%
274
+ [2026-06-30 23:21:41] INFO [validate_retraining.py:119]
275
+ precision recall f1-score support
276
+
277
+ 0 0.78 0.47 0.59 397
278
+ 1 0.52 0.81 0.63 279
279
+
280
+ accuracy 0.61 676
281
+ macro avg 0.65 0.64 0.61 676
282
+ weighted avg 0.67 0.61 0.61 676
283
+
284
+ [2026-06-30 23:21:41] INFO [validate_retraining.py:122] Validation PASSED! Model is eligible for release.
285
+ [2026-06-30 23:22:17] INFO [api.py:52] Attempting to load champion model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
286
+ [2026-06-30 23:22:17] INFO [api.py:56] Successfully loaded champion XGBoost model.
287
+ [2026-06-30 23:22:17] INFO [api.py:63] Attempting to load challenger model from E:\project\customer_segmentation_project\models\churn_rf_model.pkl...
288
+ [2026-06-30 23:22:19] INFO [api.py:67] Successfully loaded challenger Random Forest model.
289
+ [2026-06-30 23:22:19] INFO [api.py:102] Successfully initialized shadow prediction database at E:\project\customer_segmentation_project\logs\predictions.db
290
+ [2026-06-30 23:22:20] INFO [api.py:114] Successfully loaded 4312 customer records into memory for WebSocket streaming.
291
+ [2026-06-30 23:25:03] INFO [api.py:61] Attempting to load champion model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
292
+ [2026-06-30 23:25:05] INFO [api.py:65] Successfully loaded champion XGBoost model.
293
+ [2026-06-30 23:25:05] INFO [api.py:72] Attempting to load challenger model from E:\project\customer_segmentation_project\models\churn_rf_model.pkl...
294
+ [2026-06-30 23:25:05] INFO [api.py:76] Successfully loaded challenger Random Forest model.
295
+ [2026-06-30 23:25:05] INFO [api.py:111] Successfully initialized shadow prediction database at E:\project\customer_segmentation_project\logs\predictions.db
296
+ [2026-06-30 23:25:05] INFO [api.py:123] Successfully loaded 4312 customer records into memory for WebSocket streaming.
297
+ [2026-06-30 23:25:05] INFO [api.py:164] Executing health check request...
298
+ [2026-06-30 23:25:05] INFO [api.py:61] Attempting to load champion model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
299
+ [2026-06-30 23:25:05] INFO [api.py:65] Successfully loaded champion XGBoost model.
300
+ [2026-06-30 23:25:05] INFO [api.py:72] Attempting to load challenger model from E:\project\customer_segmentation_project\models\churn_rf_model.pkl...
301
+ [2026-06-30 23:25:05] INFO [api.py:76] Successfully loaded challenger Random Forest model.
302
+ [2026-06-30 23:25:05] INFO [api.py:111] Successfully initialized shadow prediction database at E:\project\customer_segmentation_project\logs\predictions.db
303
+ [2026-06-30 23:25:05] INFO [api.py:123] Successfully loaded 4312 customer records into memory for WebSocket streaming.
304
+ [2026-06-30 23:25:05] INFO [api.py:229] Running inference for customer profile: recency=10, freq=5
305
+ [2026-06-30 23:25:05] INFO [api.py:61] Attempting to load champion model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
306
+ [2026-06-30 23:25:05] INFO [api.py:65] Successfully loaded champion XGBoost model.
307
+ [2026-06-30 23:25:05] INFO [api.py:72] Attempting to load challenger model from E:\project\customer_segmentation_project\models\churn_rf_model.pkl...
308
+ [2026-06-30 23:25:05] INFO [api.py:76] Successfully loaded challenger Random Forest model.
309
+ [2026-06-30 23:25:05] INFO [api.py:111] Successfully initialized shadow prediction database at E:\project\customer_segmentation_project\logs\predictions.db
310
+ [2026-06-30 23:25:05] INFO [api.py:123] Successfully loaded 4312 customer records into memory for WebSocket streaming.
311
+ [2026-06-30 23:25:05] INFO [api.py:330] Running batch inference for 2 profiles...
312
+ [2026-06-30 23:25:05] INFO [api.py:61] Attempting to load champion model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
313
+ [2026-06-30 23:25:05] INFO [api.py:65] Successfully loaded champion XGBoost model.
314
+ [2026-06-30 23:25:05] INFO [api.py:72] Attempting to load challenger model from E:\project\customer_segmentation_project\models\churn_rf_model.pkl...
315
+ [2026-06-30 23:25:05] INFO [api.py:76] Successfully loaded challenger Random Forest model.
316
+ [2026-06-30 23:25:05] INFO [api.py:111] Successfully initialized shadow prediction database at E:\project\customer_segmentation_project\logs\predictions.db
317
+ [2026-06-30 23:25:05] INFO [api.py:123] Successfully loaded 4312 customer records into memory for WebSocket streaming.
318
+ [2026-06-30 23:25:05] INFO [api.py:61] Attempting to load champion model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
319
+ [2026-06-30 23:25:05] INFO [api.py:65] Successfully loaded champion XGBoost model.
320
+ [2026-06-30 23:25:05] INFO [api.py:72] Attempting to load challenger model from E:\project\customer_segmentation_project\models\churn_rf_model.pkl...
321
+ [2026-06-30 23:25:05] INFO [api.py:76] Successfully loaded challenger Random Forest model.
322
+ [2026-06-30 23:25:05] INFO [api.py:111] Successfully initialized shadow prediction database at E:\project\customer_segmentation_project\logs\predictions.db
323
+ [2026-06-30 23:25:05] INFO [api.py:123] Successfully loaded 4312 customer records into memory for WebSocket streaming.
324
+ [2026-06-30 23:25:13] INFO [api.py:61] Attempting to load champion model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
325
+ [2026-06-30 23:25:14] INFO [api.py:65] Successfully loaded champion XGBoost model.
326
+ [2026-06-30 23:25:14] INFO [api.py:72] Attempting to load challenger model from E:\project\customer_segmentation_project\models\churn_rf_model.pkl...
327
+ [2026-06-30 23:25:15] INFO [api.py:76] Successfully loaded challenger Random Forest model.
328
+ [2026-06-30 23:25:15] INFO [api.py:111] Successfully initialized shadow prediction database at E:\project\customer_segmentation_project\logs\predictions.db
329
+ [2026-06-30 23:25:15] INFO [api.py:123] Successfully loaded 4312 customer records into memory for WebSocket streaming.
330
+ [2026-06-30 23:27:54] INFO [api.py:61] Attempting to load champion model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
331
+ [2026-06-30 23:27:55] INFO [api.py:65] Successfully loaded champion XGBoost model.
332
+ [2026-06-30 23:27:55] INFO [api.py:72] Attempting to load challenger model from E:\project\customer_segmentation_project\models\churn_rf_model.pkl...
333
+ [2026-06-30 23:27:55] INFO [api.py:76] Successfully loaded challenger Random Forest model.
334
+ [2026-06-30 23:27:55] INFO [api.py:111] Successfully initialized shadow prediction database at E:\project\customer_segmentation_project\logs\predictions.db
335
+ [2026-06-30 23:27:55] INFO [api.py:123] Successfully loaded 4312 customer records into memory for WebSocket streaming.
336
+ [2026-06-30 23:31:40] INFO [api.py:229] Running inference for customer profile: recency=5, freq=15
337
+ [2026-06-30 23:33:49] INFO [api.py:229] Running inference for customer profile: recency=4, freq=22
338
+ [2026-06-30 23:34:32] INFO [api.py:229] Running inference for customer profile: recency=240, freq=18
339
+ [2026-06-30 23:35:32] INFO [api.py:229] Running inference for customer profile: recency=65, freq=12
340
+ [2026-06-30 23:36:23] INFO [api.py:229] Running inference for customer profile: recency=2, freq=5
341
+ [2026-06-30 23:37:05] INFO [api.py:229] Running inference for customer profile: recency=12, freq=6
342
+ [2026-06-30 23:39:06] INFO [api.py:229] Running inference for customer profile: recency=320, freq=1
343
+ [2026-06-30 23:39:54] INFO [api.py:229] Running inference for customer profile: recency=8, freq=4
344
+ [2026-06-30 23:40:48] INFO [api.py:229] Running inference for customer profile: recency=110, freq=3
345
+ [2026-06-30 23:41:40] INFO [api.py:229] Running inference for customer profile: recency=18, freq=8
346
+ [2026-06-30 23:42:17] INFO [api.py:229] Running inference for customer profile: recency=45, freq=14
347
+ [2026-06-30 23:42:37] WARNING [api.py:427] Feature columns not found: Recency in base or Recency in prod.
348
+ [2026-06-30 23:42:37] WARNING [api.py:427] Feature columns not found: Frequency in base or Frequency in prod.
349
+ [2026-06-30 23:42:37] WARNING [api.py:427] Feature columns not found: Monetary in base or Monetary in prod.
350
+ [2026-06-30 23:42:37] WARNING [api.py:427] Feature columns not found: AvgBucketSize in base or BasketSize in prod.
351
+ [2026-06-30 23:45:23] INFO [api.py:61] Attempting to load champion model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
352
+ [2026-06-30 23:45:25] INFO [api.py:65] Successfully loaded champion XGBoost model.
353
+ [2026-06-30 23:45:25] INFO [api.py:72] Attempting to load challenger model from E:\project\customer_segmentation_project\models\churn_rf_model.pkl...
354
+ [2026-06-30 23:45:26] INFO [api.py:76] Successfully loaded challenger Random Forest model.
355
+ [2026-06-30 23:45:26] INFO [api.py:111] Successfully initialized shadow prediction database at E:\project\customer_segmentation_project\logs\predictions.db
356
+ [2026-06-30 23:45:26] INFO [api.py:123] Successfully loaded 4312 customer records into memory for WebSocket streaming.
357
+ [2026-06-30 23:48:00] INFO [api.py:61] Attempting to load champion model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
358
+ [2026-06-30 23:48:01] INFO [api.py:65] Successfully loaded champion XGBoost model.
359
+ [2026-06-30 23:48:01] INFO [api.py:72] Attempting to load challenger model from E:\project\customer_segmentation_project\models\churn_rf_model.pkl...
360
+ [2026-06-30 23:48:01] INFO [api.py:76] Successfully loaded challenger Random Forest model.
361
+ [2026-06-30 23:48:01] INFO [api.py:111] Successfully initialized shadow prediction database at E:\project\customer_segmentation_project\logs\predictions.db
362
+ [2026-06-30 23:48:01] INFO [api.py:123] Successfully loaded 4312 customer records into memory for WebSocket streaming.
363
+ [2026-06-30 23:49:12] WARNING [api.py:427] Feature columns not found: Recency in base or Recency in prod.
364
+ [2026-06-30 23:49:12] WARNING [api.py:427] Feature columns not found: Frequency in base or Frequency in prod.
365
+ [2026-06-30 23:49:12] WARNING [api.py:427] Feature columns not found: Monetary in base or Monetary in prod.
366
+ [2026-06-30 23:49:12] WARNING [api.py:427] Feature columns not found: AvgBucketSize in base or BasketSize in prod.
367
+ [2026-06-30 23:51:51] INFO [api.py:61] Attempting to load champion model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
368
+ [2026-06-30 23:51:52] INFO [api.py:65] Successfully loaded champion XGBoost model.
369
+ [2026-06-30 23:51:52] INFO [api.py:72] Attempting to load challenger model from E:\project\customer_segmentation_project\models\churn_rf_model.pkl...
370
+ [2026-06-30 23:51:53] INFO [api.py:76] Successfully loaded challenger Random Forest model.
371
+ [2026-06-30 23:51:53] INFO [api.py:111] Successfully initialized shadow prediction database at E:\project\customer_segmentation_project\logs\predictions.db
372
+ [2026-06-30 23:51:53] INFO [api.py:123] Successfully loaded 4312 customer records into memory for WebSocket streaming.
373
+ [2026-06-30 23:53:10] INFO [api.py:61] Attempting to load champion model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
374
+ [2026-06-30 23:53:11] INFO [api.py:65] Successfully loaded champion XGBoost model.
375
+ [2026-06-30 23:53:11] INFO [api.py:72] Attempting to load challenger model from E:\project\customer_segmentation_project\models\churn_rf_model.pkl...
376
+ [2026-06-30 23:53:11] INFO [api.py:76] Successfully loaded challenger Random Forest model.
377
+ [2026-06-30 23:53:11] INFO [api.py:111] Successfully initialized shadow prediction database at E:\project\customer_segmentation_project\logs\predictions.db
378
+ [2026-06-30 23:53:12] INFO [api.py:123] Successfully loaded 4312 customer records into memory for WebSocket streaming.
379
+ [2026-06-30 23:53:35] INFO [api.py:61] Attempting to load champion model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
380
+ [2026-06-30 23:53:36] INFO [api.py:65] Successfully loaded champion XGBoost model.
381
+ [2026-06-30 23:53:36] INFO [api.py:72] Attempting to load challenger model from E:\project\customer_segmentation_project\models\churn_rf_model.pkl...
382
+ [2026-06-30 23:53:37] INFO [api.py:76] Successfully loaded challenger Random Forest model.
383
+ [2026-06-30 23:53:37] INFO [api.py:111] Successfully initialized shadow prediction database at E:\project\customer_segmentation_project\logs\predictions.db
384
+ [2026-06-30 23:53:37] INFO [api.py:123] Successfully loaded 4312 customer records into memory for WebSocket streaming.
385
+ [2026-07-01 09:00:21] INFO [main.py:13] Starting up Customer Churn Prediction API...
386
+ [2026-07-01 09:00:21] INFO [database.py:37] Successfully initialized shadow prediction database at E:\project\customer_segmentation_project\logs\predictions.db
387
+ [2026-07-01 09:00:21] INFO [ml_services.py:21] Attempting to load champion model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
388
+ [2026-07-01 09:00:24] INFO [ml_services.py:25] Successfully loaded champion XGBoost model.
389
+ [2026-07-01 09:00:24] INFO [ml_services.py:32] Attempting to load challenger model from E:\project\customer_segmentation_project\models\churn_rf_model.pkl...
390
+ [2026-07-01 09:00:26] INFO [ml_services.py:36] Successfully loaded challenger Random Forest model.
391
+ [2026-07-01 09:00:26] INFO [ml_services.py:49] Successfully loaded 4312 customer records into memory for WebSocket streaming.
392
+ [2026-07-01 09:00:26] INFO [routes.py:26] Executing health check request...
393
+ [2026-07-01 09:00:26] INFO [main.py:18] Shutting down Customer Churn Prediction API...
394
+ [2026-07-01 09:00:26] INFO [main.py:13] Starting up Customer Churn Prediction API...
395
+ [2026-07-01 09:00:26] INFO [database.py:37] Successfully initialized shadow prediction database at E:\project\customer_segmentation_project\logs\predictions.db
396
+ [2026-07-01 09:00:26] INFO [ml_services.py:21] Attempting to load champion model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
397
+ [2026-07-01 09:00:27] INFO [ml_services.py:25] Successfully loaded champion XGBoost model.
398
+ [2026-07-01 09:00:27] INFO [ml_services.py:32] Attempting to load challenger model from E:\project\customer_segmentation_project\models\churn_rf_model.pkl...
399
+ [2026-07-01 09:00:27] INFO [ml_services.py:36] Successfully loaded challenger Random Forest model.
400
+ [2026-07-01 09:00:27] INFO [ml_services.py:49] Successfully loaded 4312 customer records into memory for WebSocket streaming.
401
+ [2026-07-01 09:00:27] ERROR [routes.py:39] Inference requested but model is not loaded.
402
+ [2026-07-01 09:00:27] INFO [main.py:18] Shutting down Customer Churn Prediction API...
403
+ [2026-07-01 09:00:28] INFO [main.py:13] Starting up Customer Churn Prediction API...
404
+ [2026-07-01 09:00:28] INFO [database.py:37] Successfully initialized shadow prediction database at E:\project\customer_segmentation_project\logs\predictions.db
405
+ [2026-07-01 09:00:28] INFO [ml_services.py:21] Attempting to load champion model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
406
+ [2026-07-01 09:00:28] INFO [ml_services.py:25] Successfully loaded champion XGBoost model.
407
+ [2026-07-01 09:00:28] INFO [ml_services.py:32] Attempting to load challenger model from E:\project\customer_segmentation_project\models\churn_rf_model.pkl...
408
+ [2026-07-01 09:00:28] INFO [ml_services.py:36] Successfully loaded challenger Random Forest model.
409
+ [2026-07-01 09:00:28] INFO [ml_services.py:49] Successfully loaded 4312 customer records into memory for WebSocket streaming.
410
+ [2026-07-01 09:00:28] ERROR [routes.py:90] Inference batch requested but model is not loaded.
411
+ [2026-07-01 09:00:28] INFO [main.py:18] Shutting down Customer Churn Prediction API...
412
+ [2026-07-01 09:00:29] INFO [main.py:13] Starting up Customer Churn Prediction API...
413
+ [2026-07-01 09:00:29] INFO [database.py:37] Successfully initialized shadow prediction database at E:\project\customer_segmentation_project\logs\predictions.db
414
+ [2026-07-01 09:00:29] INFO [ml_services.py:21] Attempting to load champion model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
415
+ [2026-07-01 09:00:29] INFO [ml_services.py:25] Successfully loaded champion XGBoost model.
416
+ [2026-07-01 09:00:29] INFO [ml_services.py:32] Attempting to load challenger model from E:\project\customer_segmentation_project\models\churn_rf_model.pkl...
417
+ [2026-07-01 09:00:29] INFO [ml_services.py:36] Successfully loaded challenger Random Forest model.
418
+ [2026-07-01 09:00:29] INFO [ml_services.py:49] Successfully loaded 4312 customer records into memory for WebSocket streaming.
419
+ [2026-07-01 09:00:31] WARNING [drift_service.py:69] Feature columns not found: Recency in base or Recency in prod.
420
+ [2026-07-01 09:00:31] WARNING [drift_service.py:69] Feature columns not found: Frequency in base or Frequency in prod.
421
+ [2026-07-01 09:00:31] WARNING [drift_service.py:69] Feature columns not found: Monetary in base or Monetary in prod.
422
+ [2026-07-01 09:00:31] WARNING [drift_service.py:69] Feature columns not found: AvgBucketSize in base or BasketSize in prod.
423
+ [2026-07-01 09:00:31] INFO [main.py:18] Shutting down Customer Churn Prediction API...
424
+ [2026-07-01 09:00:31] INFO [main.py:13] Starting up Customer Churn Prediction API...
425
+ [2026-07-01 09:00:31] INFO [database.py:37] Successfully initialized shadow prediction database at E:\project\customer_segmentation_project\logs\predictions.db
426
+ [2026-07-01 09:00:31] INFO [ml_services.py:21] Attempting to load champion model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
427
+ [2026-07-01 09:00:31] INFO [ml_services.py:25] Successfully loaded champion XGBoost model.
428
+ [2026-07-01 09:00:31] INFO [ml_services.py:32] Attempting to load challenger model from E:\project\customer_segmentation_project\models\churn_rf_model.pkl...
429
+ [2026-07-01 09:00:31] INFO [ml_services.py:36] Successfully loaded challenger Random Forest model.
430
+ [2026-07-01 09:00:31] INFO [ml_services.py:49] Successfully loaded 4312 customer records into memory for WebSocket streaming.
431
+ [2026-07-01 09:00:31] INFO [main.py:18] Shutting down Customer Churn Prediction API...
432
+ [2026-07-01 09:00:41] INFO [main.py:13] Starting up Customer Churn Prediction API...
433
+ [2026-07-01 09:00:41] INFO [database.py:37] Successfully initialized shadow prediction database at E:\project\customer_segmentation_project\logs\predictions.db
434
+ [2026-07-01 09:00:41] INFO [ml_services.py:21] Attempting to load champion model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
435
+ [2026-07-01 09:00:42] INFO [ml_services.py:25] Successfully loaded champion XGBoost model.
436
+ [2026-07-01 09:00:42] INFO [ml_services.py:32] Attempting to load challenger model from E:\project\customer_segmentation_project\models\churn_rf_model.pkl...
437
+ [2026-07-01 09:00:43] INFO [ml_services.py:36] Successfully loaded challenger Random Forest model.
438
+ [2026-07-01 09:00:43] INFO [ml_services.py:49] Successfully loaded 4312 customer records into memory for WebSocket streaming.
439
+ [2026-07-01 09:02:08] INFO [main.py:18] Shutting down Customer Churn Prediction API...
440
+ [2026-07-01 09:05:15] INFO [main.py:13] Starting up Customer Churn Prediction API...
441
+ [2026-07-01 09:05:15] INFO [database.py:37] Successfully initialized shadow prediction database at E:\project\customer_segmentation_project\logs\predictions.db
442
+ [2026-07-01 09:05:15] INFO [ml_services.py:21] Attempting to load champion model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
443
+ [2026-07-01 09:05:17] INFO [ml_services.py:25] Successfully loaded champion XGBoost model.
444
+ [2026-07-01 09:05:17] INFO [ml_services.py:32] Attempting to load challenger model from E:\project\customer_segmentation_project\models\churn_rf_model.pkl...
445
+ [2026-07-01 09:05:19] INFO [ml_services.py:36] Successfully loaded challenger Random Forest model.
446
+ [2026-07-01 09:05:19] INFO [ml_services.py:49] Successfully loaded 4312 customer records into memory for WebSocket streaming.
447
+ [2026-07-01 09:05:19] INFO [routes.py:19] Executing health check request...
448
+ [2026-07-01 09:05:19] INFO [main.py:18] Shutting down Customer Churn Prediction API...
449
+ [2026-07-01 09:05:19] INFO [main.py:13] Starting up Customer Churn Prediction API...
450
+ [2026-07-01 09:05:19] INFO [database.py:37] Successfully initialized shadow prediction database at E:\project\customer_segmentation_project\logs\predictions.db
451
+ [2026-07-01 09:05:19] INFO [ml_services.py:21] Attempting to load champion model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
452
+ [2026-07-01 09:05:19] INFO [ml_services.py:25] Successfully loaded champion XGBoost model.
453
+ [2026-07-01 09:05:19] INFO [ml_services.py:32] Attempting to load challenger model from E:\project\customer_segmentation_project\models\churn_rf_model.pkl...
454
+ [2026-07-01 09:05:20] INFO [ml_services.py:36] Successfully loaded challenger Random Forest model.
455
+ [2026-07-01 09:05:20] INFO [ml_services.py:49] Successfully loaded 4312 customer records into memory for WebSocket streaming.
456
+ [2026-07-01 09:05:20] INFO [main.py:18] Shutting down Customer Churn Prediction API...
457
+ [2026-07-01 09:05:20] INFO [main.py:13] Starting up Customer Churn Prediction API...
458
+ [2026-07-01 09:05:20] INFO [database.py:37] Successfully initialized shadow prediction database at E:\project\customer_segmentation_project\logs\predictions.db
459
+ [2026-07-01 09:05:20] INFO [ml_services.py:21] Attempting to load champion model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
460
+ [2026-07-01 09:05:20] INFO [ml_services.py:25] Successfully loaded champion XGBoost model.
461
+ [2026-07-01 09:05:20] INFO [ml_services.py:32] Attempting to load challenger model from E:\project\customer_segmentation_project\models\churn_rf_model.pkl...
462
+ [2026-07-01 09:05:20] INFO [ml_services.py:36] Successfully loaded challenger Random Forest model.
463
+ [2026-07-01 09:05:20] INFO [ml_services.py:49] Successfully loaded 4312 customer records into memory for WebSocket streaming.
464
+ [2026-07-01 09:05:20] INFO [routes.py:97] Running batch inference for 2 profiles...
465
+ [2026-07-01 09:05:20] INFO [main.py:18] Shutting down Customer Churn Prediction API...
466
+ [2026-07-01 09:05:20] INFO [main.py:13] Starting up Customer Churn Prediction API...
467
+ [2026-07-01 09:05:20] INFO [database.py:37] Successfully initialized shadow prediction database at E:\project\customer_segmentation_project\logs\predictions.db
468
+ [2026-07-01 09:05:20] INFO [ml_services.py:21] Attempting to load champion model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
469
+ [2026-07-01 09:05:20] INFO [ml_services.py:25] Successfully loaded champion XGBoost model.
470
+ [2026-07-01 09:05:20] INFO [ml_services.py:32] Attempting to load challenger model from E:\project\customer_segmentation_project\models\churn_rf_model.pkl...
471
+ [2026-07-01 09:05:20] INFO [ml_services.py:36] Successfully loaded challenger Random Forest model.
472
+ [2026-07-01 09:05:20] INFO [ml_services.py:49] Successfully loaded 4312 customer records into memory for WebSocket streaming.
473
+ [2026-07-01 09:05:21] WARNING [drift_service.py:69] Feature columns not found: Recency in base or Recency in prod.
474
+ [2026-07-01 09:05:21] WARNING [drift_service.py:69] Feature columns not found: Frequency in base or Frequency in prod.
475
+ [2026-07-01 09:05:21] WARNING [drift_service.py:69] Feature columns not found: Monetary in base or Monetary in prod.
476
+ [2026-07-01 09:05:21] WARNING [drift_service.py:69] Feature columns not found: AvgBucketSize in base or BasketSize in prod.
477
+ [2026-07-01 09:05:21] INFO [main.py:18] Shutting down Customer Churn Prediction API...
478
+ [2026-07-01 09:05:21] INFO [main.py:13] Starting up Customer Churn Prediction API...
479
+ [2026-07-01 09:05:21] INFO [database.py:37] Successfully initialized shadow prediction database at E:\project\customer_segmentation_project\logs\predictions.db
480
+ [2026-07-01 09:05:21] INFO [ml_services.py:21] Attempting to load champion model from E:\project\customer_segmentation_project\models\churn_xgb_model.pkl...
481
+ [2026-07-01 09:05:21] INFO [ml_services.py:25] Successfully loaded champion XGBoost model.
482
+ [2026-07-01 09:05:21] INFO [ml_services.py:32] Attempting to load challenger model from E:\project\customer_segmentation_project\models\churn_rf_model.pkl...
483
+ [2026-07-01 09:05:21] INFO [ml_services.py:36] Successfully loaded challenger Random Forest model.
484
+ [2026-07-01 09:05:21] INFO [ml_services.py:49] Successfully loaded 4312 customer records into memory for WebSocket streaming.
485
+ [2026-07-01 09:05:21] INFO [main.py:18] Shutting down Customer Churn Prediction API...
logs/inference_history.csv ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Timestamp,Recency,Frequency,Monetary,BasketSize
2
+ 2026-06-30 23:14:15,10,5,250.0,8.0
3
+ 2026-06-30 23:16:48,10,5,250.0,8.0
4
+ 2026-06-30 23:17:05,10,5,250.0,8.0
5
+ 2026-06-30 23:25:05,10,5,250.0,8.0
6
+ 2026-06-30 23:31:40,5,15,650.0,8.5
7
+ 2026-06-30 23:33:49,4,22,580.0,15.0
8
+ 2026-06-30 23:34:32,240,18,450.0,12.0
9
+ 2026-06-30 23:35:32,65,12,380.0,10.5
10
+ 2026-06-30 23:36:23,2,5,180.0,6.0
11
+ 2026-06-30 23:37:05,12,6,850.0,45.0
12
+ 2026-06-30 23:39:06,320,1,15.5,1.0
13
+ 2026-06-30 23:39:54,8,4,95.0,4.5
14
+ 2026-06-30 23:40:48,110,3,45.0,3.0
15
+ 2026-06-30 23:41:40,18,8,120.0,5.0
16
+ 2026-06-30 23:42:17,45,14,25.0,2.0
17
+ 2026-07-01 09:00:27,10,5,250.0,8.0
18
+ 2026-07-01 09:05:20,10,5,250.0,8.0
logs/predictions.db ADDED
Binary file (12.3 kB). View file
 
notebooks/01_Data_Loading.ipynb ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": 5,
6
+ "id": "18eacdbd",
7
+ "metadata": {},
8
+ "outputs": [],
9
+ "source": [
10
+ "import pandas as pd\n",
11
+ "import os\n"
12
+ ]
13
+ },
14
+ {
15
+ "cell_type": "code",
16
+ "execution_count": 6,
17
+ "id": "1429805a",
18
+ "metadata": {},
19
+ "outputs": [],
20
+ "source": [
21
+ "file_path = '../data/raw/online_retail_II.xlsx'\n"
22
+ ]
23
+ },
24
+ {
25
+ "cell_type": "code",
26
+ "execution_count": null,
27
+ "id": "907ab9ac",
28
+ "metadata": {},
29
+ "outputs": [
30
+ {
31
+ "name": "stdout",
32
+ "output_type": "stream",
33
+ "text": [
34
+ "Loading data...\n"
35
+ ]
36
+ }
37
+ ],
38
+ "source": [
39
+ "df = pd.read_excel(file_path, sheet_name='Year 2009-2010')"
40
+ ]
41
+ },
42
+ {
43
+ "cell_type": "markdown",
44
+ "id": "0386fe66",
45
+ "metadata": {},
46
+ "source": [
47
+ "# Basic Sanity Check\n"
48
+ ]
49
+ },
50
+ {
51
+ "cell_type": "code",
52
+ "execution_count": 8,
53
+ "id": "0c568d7f",
54
+ "metadata": {},
55
+ "outputs": [
56
+ {
57
+ "name": "stdout",
58
+ "output_type": "stream",
59
+ "text": [
60
+ "Data loaded successfully!\n",
61
+ "Total Rows: 525461\n",
62
+ "Total Columns: 8\n"
63
+ ]
64
+ }
65
+ ],
66
+ "source": [
67
+ "print(\"Data loaded successfully!\")\n",
68
+ "print(f\"Total Rows: {df.shape[0]}\")\n",
69
+ "print(f\"Total Columns: {df.shape[1]}\")\n",
70
+ "\n"
71
+ ]
72
+ },
73
+ {
74
+ "cell_type": "markdown",
75
+ "id": "be9a7f30",
76
+ "metadata": {},
77
+ "source": [
78
+ "# Preview data"
79
+ ]
80
+ },
81
+ {
82
+ "cell_type": "code",
83
+ "execution_count": 9,
84
+ "id": "b8a911f7",
85
+ "metadata": {},
86
+ "outputs": [
87
+ {
88
+ "data": {
89
+ "text/html": [
90
+ "<div>\n",
91
+ "<style scoped>\n",
92
+ " .dataframe tbody tr th:only-of-type {\n",
93
+ " vertical-align: middle;\n",
94
+ " }\n",
95
+ "\n",
96
+ " .dataframe tbody tr th {\n",
97
+ " vertical-align: top;\n",
98
+ " }\n",
99
+ "\n",
100
+ " .dataframe thead th {\n",
101
+ " text-align: right;\n",
102
+ " }\n",
103
+ "</style>\n",
104
+ "<table border=\"1\" class=\"dataframe\">\n",
105
+ " <thead>\n",
106
+ " <tr style=\"text-align: right;\">\n",
107
+ " <th></th>\n",
108
+ " <th>Invoice</th>\n",
109
+ " <th>StockCode</th>\n",
110
+ " <th>Description</th>\n",
111
+ " <th>Quantity</th>\n",
112
+ " <th>InvoiceDate</th>\n",
113
+ " <th>Price</th>\n",
114
+ " <th>Customer ID</th>\n",
115
+ " <th>Country</th>\n",
116
+ " </tr>\n",
117
+ " </thead>\n",
118
+ " <tbody>\n",
119
+ " <tr>\n",
120
+ " <th>0</th>\n",
121
+ " <td>489434</td>\n",
122
+ " <td>85048</td>\n",
123
+ " <td>15CM CHRISTMAS GLASS BALL 20 LIGHTS</td>\n",
124
+ " <td>12</td>\n",
125
+ " <td>2009-12-01 07:45:00</td>\n",
126
+ " <td>6.95</td>\n",
127
+ " <td>13085.0</td>\n",
128
+ " <td>United Kingdom</td>\n",
129
+ " </tr>\n",
130
+ " <tr>\n",
131
+ " <th>1</th>\n",
132
+ " <td>489434</td>\n",
133
+ " <td>79323P</td>\n",
134
+ " <td>PINK CHERRY LIGHTS</td>\n",
135
+ " <td>12</td>\n",
136
+ " <td>2009-12-01 07:45:00</td>\n",
137
+ " <td>6.75</td>\n",
138
+ " <td>13085.0</td>\n",
139
+ " <td>United Kingdom</td>\n",
140
+ " </tr>\n",
141
+ " <tr>\n",
142
+ " <th>2</th>\n",
143
+ " <td>489434</td>\n",
144
+ " <td>79323W</td>\n",
145
+ " <td>WHITE CHERRY LIGHTS</td>\n",
146
+ " <td>12</td>\n",
147
+ " <td>2009-12-01 07:45:00</td>\n",
148
+ " <td>6.75</td>\n",
149
+ " <td>13085.0</td>\n",
150
+ " <td>United Kingdom</td>\n",
151
+ " </tr>\n",
152
+ " <tr>\n",
153
+ " <th>3</th>\n",
154
+ " <td>489434</td>\n",
155
+ " <td>22041</td>\n",
156
+ " <td>RECORD FRAME 7\" SINGLE SIZE</td>\n",
157
+ " <td>48</td>\n",
158
+ " <td>2009-12-01 07:45:00</td>\n",
159
+ " <td>2.10</td>\n",
160
+ " <td>13085.0</td>\n",
161
+ " <td>United Kingdom</td>\n",
162
+ " </tr>\n",
163
+ " <tr>\n",
164
+ " <th>4</th>\n",
165
+ " <td>489434</td>\n",
166
+ " <td>21232</td>\n",
167
+ " <td>STRAWBERRY CERAMIC TRINKET BOX</td>\n",
168
+ " <td>24</td>\n",
169
+ " <td>2009-12-01 07:45:00</td>\n",
170
+ " <td>1.25</td>\n",
171
+ " <td>13085.0</td>\n",
172
+ " <td>United Kingdom</td>\n",
173
+ " </tr>\n",
174
+ " </tbody>\n",
175
+ "</table>\n",
176
+ "</div>"
177
+ ],
178
+ "text/plain": [
179
+ " Invoice StockCode Description Quantity \\\n",
180
+ "0 489434 85048 15CM CHRISTMAS GLASS BALL 20 LIGHTS 12 \n",
181
+ "1 489434 79323P PINK CHERRY LIGHTS 12 \n",
182
+ "2 489434 79323W WHITE CHERRY LIGHTS 12 \n",
183
+ "3 489434 22041 RECORD FRAME 7\" SINGLE SIZE 48 \n",
184
+ "4 489434 21232 STRAWBERRY CERAMIC TRINKET BOX 24 \n",
185
+ "\n",
186
+ " InvoiceDate Price Customer ID Country \n",
187
+ "0 2009-12-01 07:45:00 6.95 13085.0 United Kingdom \n",
188
+ "1 2009-12-01 07:45:00 6.75 13085.0 United Kingdom \n",
189
+ "2 2009-12-01 07:45:00 6.75 13085.0 United Kingdom \n",
190
+ "3 2009-12-01 07:45:00 2.10 13085.0 United Kingdom \n",
191
+ "4 2009-12-01 07:45:00 1.25 13085.0 United Kingdom "
192
+ ]
193
+ },
194
+ "metadata": {},
195
+ "output_type": "display_data"
196
+ }
197
+ ],
198
+ "source": [
199
+ "\n",
200
+ "display(df.head())"
201
+ ]
202
+ }
203
+ ],
204
+ "metadata": {
205
+ "kernelspec": {
206
+ "display_name": "base",
207
+ "language": "python",
208
+ "name": "python3"
209
+ },
210
+ "language_info": {
211
+ "codemirror_mode": {
212
+ "name": "ipython",
213
+ "version": 3
214
+ },
215
+ "file_extension": ".py",
216
+ "mimetype": "text/x-python",
217
+ "name": "python",
218
+ "nbconvert_exporter": "python",
219
+ "pygments_lexer": "ipython3",
220
+ "version": "3.11.5"
221
+ }
222
+ },
223
+ "nbformat": 4,
224
+ "nbformat_minor": 5
225
+ }
notebooks/01_EDA_and_Cleaning.ipynb ADDED
@@ -0,0 +1,553 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": 20,
6
+ "id": "74cd0921",
7
+ "metadata": {},
8
+ "outputs": [],
9
+ "source": [
10
+ "import pandas as pd\n",
11
+ "import os\n",
12
+ "\n"
13
+ ]
14
+ },
15
+ {
16
+ "cell_type": "markdown",
17
+ "id": "459667d1",
18
+ "metadata": {},
19
+ "source": [
20
+ "# Define file path (Professional relative path, works on any computer)\n"
21
+ ]
22
+ },
23
+ {
24
+ "cell_type": "code",
25
+ "execution_count": 21,
26
+ "id": "04746c48",
27
+ "metadata": {},
28
+ "outputs": [],
29
+ "source": [
30
+ "file_path = '../data/raw/online_retail_II.xlsx'\n"
31
+ ]
32
+ },
33
+ {
34
+ "cell_type": "markdown",
35
+ "id": "23b1f2e1",
36
+ "metadata": {},
37
+ "source": [
38
+ "\n",
39
+ "# Load the dataset (Sheet 1 - 2009-2010 data)\n"
40
+ ]
41
+ },
42
+ {
43
+ "cell_type": "code",
44
+ "execution_count": 22,
45
+ "id": "ae699594",
46
+ "metadata": {},
47
+ "outputs": [
48
+ {
49
+ "name": "stdout",
50
+ "output_type": "stream",
51
+ "text": [
52
+ "Loading data...\n"
53
+ ]
54
+ }
55
+ ],
56
+ "source": [
57
+ "\n",
58
+ "print(\"Loading data...\")\n",
59
+ "df = pd.read_excel(file_path, sheet_name='Year 2009-2010')\n",
60
+ "\n"
61
+ ]
62
+ },
63
+ {
64
+ "cell_type": "markdown",
65
+ "id": "0973e9e8",
66
+ "metadata": {},
67
+ "source": [
68
+ "# Basic Sanity Check"
69
+ ]
70
+ },
71
+ {
72
+ "cell_type": "code",
73
+ "execution_count": 23,
74
+ "id": "e095a2ca",
75
+ "metadata": {},
76
+ "outputs": [
77
+ {
78
+ "name": "stdout",
79
+ "output_type": "stream",
80
+ "text": [
81
+ "Data loaded successfully!\n",
82
+ "Total Rows: 525461\n",
83
+ "Total Columns: 8\n"
84
+ ]
85
+ }
86
+ ],
87
+ "source": [
88
+ "\n",
89
+ "print(\"Data loaded successfully!\")\n",
90
+ "print(f\"Total Rows: {df.shape[0]}\")\n",
91
+ "print(f\"Total Columns: {df.shape[1]}\")\n",
92
+ "\n"
93
+ ]
94
+ },
95
+ {
96
+ "cell_type": "markdown",
97
+ "id": "582961c9",
98
+ "metadata": {},
99
+ "source": [
100
+ "# Preview data\n"
101
+ ]
102
+ },
103
+ {
104
+ "cell_type": "code",
105
+ "execution_count": 24,
106
+ "id": "6242e17f",
107
+ "metadata": {},
108
+ "outputs": [
109
+ {
110
+ "data": {
111
+ "text/html": [
112
+ "<div>\n",
113
+ "<style scoped>\n",
114
+ " .dataframe tbody tr th:only-of-type {\n",
115
+ " vertical-align: middle;\n",
116
+ " }\n",
117
+ "\n",
118
+ " .dataframe tbody tr th {\n",
119
+ " vertical-align: top;\n",
120
+ " }\n",
121
+ "\n",
122
+ " .dataframe thead th {\n",
123
+ " text-align: right;\n",
124
+ " }\n",
125
+ "</style>\n",
126
+ "<table border=\"1\" class=\"dataframe\">\n",
127
+ " <thead>\n",
128
+ " <tr style=\"text-align: right;\">\n",
129
+ " <th></th>\n",
130
+ " <th>Invoice</th>\n",
131
+ " <th>StockCode</th>\n",
132
+ " <th>Description</th>\n",
133
+ " <th>Quantity</th>\n",
134
+ " <th>InvoiceDate</th>\n",
135
+ " <th>Price</th>\n",
136
+ " <th>Customer ID</th>\n",
137
+ " <th>Country</th>\n",
138
+ " </tr>\n",
139
+ " </thead>\n",
140
+ " <tbody>\n",
141
+ " <tr>\n",
142
+ " <th>0</th>\n",
143
+ " <td>489434</td>\n",
144
+ " <td>85048</td>\n",
145
+ " <td>15CM CHRISTMAS GLASS BALL 20 LIGHTS</td>\n",
146
+ " <td>12</td>\n",
147
+ " <td>2009-12-01 07:45:00</td>\n",
148
+ " <td>6.95</td>\n",
149
+ " <td>13085.0</td>\n",
150
+ " <td>United Kingdom</td>\n",
151
+ " </tr>\n",
152
+ " <tr>\n",
153
+ " <th>1</th>\n",
154
+ " <td>489434</td>\n",
155
+ " <td>79323P</td>\n",
156
+ " <td>PINK CHERRY LIGHTS</td>\n",
157
+ " <td>12</td>\n",
158
+ " <td>2009-12-01 07:45:00</td>\n",
159
+ " <td>6.75</td>\n",
160
+ " <td>13085.0</td>\n",
161
+ " <td>United Kingdom</td>\n",
162
+ " </tr>\n",
163
+ " <tr>\n",
164
+ " <th>2</th>\n",
165
+ " <td>489434</td>\n",
166
+ " <td>79323W</td>\n",
167
+ " <td>WHITE CHERRY LIGHTS</td>\n",
168
+ " <td>12</td>\n",
169
+ " <td>2009-12-01 07:45:00</td>\n",
170
+ " <td>6.75</td>\n",
171
+ " <td>13085.0</td>\n",
172
+ " <td>United Kingdom</td>\n",
173
+ " </tr>\n",
174
+ " <tr>\n",
175
+ " <th>3</th>\n",
176
+ " <td>489434</td>\n",
177
+ " <td>22041</td>\n",
178
+ " <td>RECORD FRAME 7\" SINGLE SIZE</td>\n",
179
+ " <td>48</td>\n",
180
+ " <td>2009-12-01 07:45:00</td>\n",
181
+ " <td>2.10</td>\n",
182
+ " <td>13085.0</td>\n",
183
+ " <td>United Kingdom</td>\n",
184
+ " </tr>\n",
185
+ " <tr>\n",
186
+ " <th>4</th>\n",
187
+ " <td>489434</td>\n",
188
+ " <td>21232</td>\n",
189
+ " <td>STRAWBERRY CERAMIC TRINKET BOX</td>\n",
190
+ " <td>24</td>\n",
191
+ " <td>2009-12-01 07:45:00</td>\n",
192
+ " <td>1.25</td>\n",
193
+ " <td>13085.0</td>\n",
194
+ " <td>United Kingdom</td>\n",
195
+ " </tr>\n",
196
+ " </tbody>\n",
197
+ "</table>\n",
198
+ "</div>"
199
+ ],
200
+ "text/plain": [
201
+ " Invoice StockCode Description Quantity \\\n",
202
+ "0 489434 85048 15CM CHRISTMAS GLASS BALL 20 LIGHTS 12 \n",
203
+ "1 489434 79323P PINK CHERRY LIGHTS 12 \n",
204
+ "2 489434 79323W WHITE CHERRY LIGHTS 12 \n",
205
+ "3 489434 22041 RECORD FRAME 7\" SINGLE SIZE 48 \n",
206
+ "4 489434 21232 STRAWBERRY CERAMIC TRINKET BOX 24 \n",
207
+ "\n",
208
+ " InvoiceDate Price Customer ID Country \n",
209
+ "0 2009-12-01 07:45:00 6.95 13085.0 United Kingdom \n",
210
+ "1 2009-12-01 07:45:00 6.75 13085.0 United Kingdom \n",
211
+ "2 2009-12-01 07:45:00 6.75 13085.0 United Kingdom \n",
212
+ "3 2009-12-01 07:45:00 2.10 13085.0 United Kingdom \n",
213
+ "4 2009-12-01 07:45:00 1.25 13085.0 United Kingdom "
214
+ ]
215
+ },
216
+ "metadata": {},
217
+ "output_type": "display_data"
218
+ }
219
+ ],
220
+ "source": [
221
+ "display(df.head())"
222
+ ]
223
+ },
224
+ {
225
+ "cell_type": "markdown",
226
+ "id": "b29ae0a1",
227
+ "metadata": {},
228
+ "source": [
229
+ "# 1. Cleaning\n"
230
+ ]
231
+ },
232
+ {
233
+ "cell_type": "code",
234
+ "execution_count": 25,
235
+ "id": "08e595c3",
236
+ "metadata": {},
237
+ "outputs": [],
238
+ "source": [
239
+ "df_clean = df.dropna(subset=['Customer ID']).copy()"
240
+ ]
241
+ },
242
+ {
243
+ "cell_type": "markdown",
244
+ "id": "75bc4bb9",
245
+ "metadata": {},
246
+ "source": [
247
+ "# Remove cancelled orders (Negative Quantity) & Free items (0 Price)\n"
248
+ ]
249
+ },
250
+ {
251
+ "cell_type": "code",
252
+ "execution_count": 26,
253
+ "id": "8e2bf7ca",
254
+ "metadata": {},
255
+ "outputs": [],
256
+ "source": [
257
+ "df_clean = df_clean[(df_clean['Quantity'] > 0) & (df_clean['Price'] > 0)]"
258
+ ]
259
+ },
260
+ {
261
+ "cell_type": "markdown",
262
+ "id": "ee3e4c59",
263
+ "metadata": {},
264
+ "source": [
265
+ "# Ensure types\n"
266
+ ]
267
+ },
268
+ {
269
+ "cell_type": "code",
270
+ "execution_count": 27,
271
+ "id": "8ac438e3",
272
+ "metadata": {},
273
+ "outputs": [],
274
+ "source": [
275
+ "df_clean['Customer ID'] = df_clean['Customer ID'].astype(int).astype(str)\n",
276
+ "df_clean['InvoiceDate'] = pd.to_datetime(df_clean['InvoiceDate'])\n"
277
+ ]
278
+ },
279
+ {
280
+ "cell_type": "markdown",
281
+ "id": "1a59080f",
282
+ "metadata": {},
283
+ "source": [
284
+ "# 2. Total Price Calculation (THE FIX)\n",
285
+ "# We multiply Clean Quantity by Clean Price"
286
+ ]
287
+ },
288
+ {
289
+ "cell_type": "code",
290
+ "execution_count": 28,
291
+ "id": "6eb4906e",
292
+ "metadata": {},
293
+ "outputs": [],
294
+ "source": [
295
+ "df_clean['Total Price'] = df_clean['Quantity'] * df_clean['Price']"
296
+ ]
297
+ },
298
+ {
299
+ "cell_type": "code",
300
+ "execution_count": 29,
301
+ "id": "1fb1d580",
302
+ "metadata": {},
303
+ "outputs": [
304
+ {
305
+ "name": "stdout",
306
+ "output_type": "stream",
307
+ "text": [
308
+ "Data Cleaned. Rows: 407664\n"
309
+ ]
310
+ },
311
+ {
312
+ "data": {
313
+ "text/html": [
314
+ "<div>\n",
315
+ "<style scoped>\n",
316
+ " .dataframe tbody tr th:only-of-type {\n",
317
+ " vertical-align: middle;\n",
318
+ " }\n",
319
+ "\n",
320
+ " .dataframe tbody tr th {\n",
321
+ " vertical-align: top;\n",
322
+ " }\n",
323
+ "\n",
324
+ " .dataframe thead th {\n",
325
+ " text-align: right;\n",
326
+ " }\n",
327
+ "</style>\n",
328
+ "<table border=\"1\" class=\"dataframe\">\n",
329
+ " <thead>\n",
330
+ " <tr style=\"text-align: right;\">\n",
331
+ " <th></th>\n",
332
+ " <th>Invoice</th>\n",
333
+ " <th>StockCode</th>\n",
334
+ " <th>Description</th>\n",
335
+ " <th>Quantity</th>\n",
336
+ " <th>InvoiceDate</th>\n",
337
+ " <th>Price</th>\n",
338
+ " <th>Customer ID</th>\n",
339
+ " <th>Country</th>\n",
340
+ " <th>Total Price</th>\n",
341
+ " </tr>\n",
342
+ " </thead>\n",
343
+ " <tbody>\n",
344
+ " <tr>\n",
345
+ " <th>0</th>\n",
346
+ " <td>489434</td>\n",
347
+ " <td>85048</td>\n",
348
+ " <td>15CM CHRISTMAS GLASS BALL 20 LIGHTS</td>\n",
349
+ " <td>12</td>\n",
350
+ " <td>2009-12-01 07:45:00</td>\n",
351
+ " <td>6.95</td>\n",
352
+ " <td>13085</td>\n",
353
+ " <td>United Kingdom</td>\n",
354
+ " <td>83.4</td>\n",
355
+ " </tr>\n",
356
+ " <tr>\n",
357
+ " <th>1</th>\n",
358
+ " <td>489434</td>\n",
359
+ " <td>79323P</td>\n",
360
+ " <td>PINK CHERRY LIGHTS</td>\n",
361
+ " <td>12</td>\n",
362
+ " <td>2009-12-01 07:45:00</td>\n",
363
+ " <td>6.75</td>\n",
364
+ " <td>13085</td>\n",
365
+ " <td>United Kingdom</td>\n",
366
+ " <td>81.0</td>\n",
367
+ " </tr>\n",
368
+ " <tr>\n",
369
+ " <th>2</th>\n",
370
+ " <td>489434</td>\n",
371
+ " <td>79323W</td>\n",
372
+ " <td>WHITE CHERRY LIGHTS</td>\n",
373
+ " <td>12</td>\n",
374
+ " <td>2009-12-01 07:45:00</td>\n",
375
+ " <td>6.75</td>\n",
376
+ " <td>13085</td>\n",
377
+ " <td>United Kingdom</td>\n",
378
+ " <td>81.0</td>\n",
379
+ " </tr>\n",
380
+ " <tr>\n",
381
+ " <th>3</th>\n",
382
+ " <td>489434</td>\n",
383
+ " <td>22041</td>\n",
384
+ " <td>RECORD FRAME 7\" SINGLE SIZE</td>\n",
385
+ " <td>48</td>\n",
386
+ " <td>2009-12-01 07:45:00</td>\n",
387
+ " <td>2.10</td>\n",
388
+ " <td>13085</td>\n",
389
+ " <td>United Kingdom</td>\n",
390
+ " <td>100.8</td>\n",
391
+ " </tr>\n",
392
+ " <tr>\n",
393
+ " <th>4</th>\n",
394
+ " <td>489434</td>\n",
395
+ " <td>21232</td>\n",
396
+ " <td>STRAWBERRY CERAMIC TRINKET BOX</td>\n",
397
+ " <td>24</td>\n",
398
+ " <td>2009-12-01 07:45:00</td>\n",
399
+ " <td>1.25</td>\n",
400
+ " <td>13085</td>\n",
401
+ " <td>United Kingdom</td>\n",
402
+ " <td>30.0</td>\n",
403
+ " </tr>\n",
404
+ " </tbody>\n",
405
+ "</table>\n",
406
+ "</div>"
407
+ ],
408
+ "text/plain": [
409
+ " Invoice StockCode Description Quantity \\\n",
410
+ "0 489434 85048 15CM CHRISTMAS GLASS BALL 20 LIGHTS 12 \n",
411
+ "1 489434 79323P PINK CHERRY LIGHTS 12 \n",
412
+ "2 489434 79323W WHITE CHERRY LIGHTS 12 \n",
413
+ "3 489434 22041 RECORD FRAME 7\" SINGLE SIZE 48 \n",
414
+ "4 489434 21232 STRAWBERRY CERAMIC TRINKET BOX 24 \n",
415
+ "\n",
416
+ " InvoiceDate Price Customer ID Country Total Price \n",
417
+ "0 2009-12-01 07:45:00 6.95 13085 United Kingdom 83.4 \n",
418
+ "1 2009-12-01 07:45:00 6.75 13085 United Kingdom 81.0 \n",
419
+ "2 2009-12-01 07:45:00 6.75 13085 United Kingdom 81.0 \n",
420
+ "3 2009-12-01 07:45:00 2.10 13085 United Kingdom 100.8 \n",
421
+ "4 2009-12-01 07:45:00 1.25 13085 United Kingdom 30.0 "
422
+ ]
423
+ },
424
+ "execution_count": 29,
425
+ "metadata": {},
426
+ "output_type": "execute_result"
427
+ }
428
+ ],
429
+ "source": [
430
+ "print(f\"Data Cleaned. Rows: {df_clean.shape[0]}\")\n",
431
+ "df_clean.head()"
432
+ ]
433
+ },
434
+ {
435
+ "cell_type": "code",
436
+ "execution_count": 30,
437
+ "id": "29ab34c0",
438
+ "metadata": {},
439
+ "outputs": [
440
+ {
441
+ "name": "stdout",
442
+ "output_type": "stream",
443
+ "text": [
444
+ "cleaned dataset \n"
445
+ ]
446
+ }
447
+ ],
448
+ "source": [
449
+ "save_path = '../data/processed/online_retail_clean.csv'\n",
450
+ "df_clean.to_csv(save_path,index=False)\n",
451
+ "print(f'cleaned dataset ')"
452
+ ]
453
+ },
454
+ {
455
+ "cell_type": "code",
456
+ "execution_count": 31,
457
+ "id": "569a4aec",
458
+ "metadata": {},
459
+ "outputs": [],
460
+ "source": [
461
+ "refrence_date = df_clean['InvoiceDate'].max() + pd.DateOffset(days=1)"
462
+ ]
463
+ },
464
+ {
465
+ "cell_type": "markdown",
466
+ "id": "04ae8651",
467
+ "metadata": {},
468
+ "source": [
469
+ "# Group by Customer ID\n"
470
+ ]
471
+ },
472
+ {
473
+ "cell_type": "code",
474
+ "execution_count": null,
475
+ "id": "1d03b214",
476
+ "metadata": {},
477
+ "outputs": [],
478
+ "source": [
479
+ "rfm = df_clean.groupby('Customer ID').agg({\n",
480
+ " 'InvoiceDate': lambda x: (refrence_date - x.max()).days, # Recency\n",
481
+ " 'Invoice': 'nunique', # Frequency\n",
482
+ " 'Total Price': 'sum' # Monetary (Summing the calculated Total Price)\n",
483
+ "}).reset_index()"
484
+ ]
485
+ },
486
+ {
487
+ "cell_type": "markdown",
488
+ "id": "9d87d402",
489
+ "metadata": {},
490
+ "source": [
491
+ "# Rename columns\n"
492
+ ]
493
+ },
494
+ {
495
+ "cell_type": "code",
496
+ "execution_count": 33,
497
+ "id": "778b4c3c",
498
+ "metadata": {},
499
+ "outputs": [],
500
+ "source": [
501
+ "rfm.rename(columns={\n",
502
+ " 'InvoiceDate': 'Recency',\n",
503
+ " 'Invoice': 'Frequency',\n",
504
+ " 'Total Price': 'Monetary'\n",
505
+ "}, inplace=True)"
506
+ ]
507
+ },
508
+ {
509
+ "cell_type": "code",
510
+ "execution_count": 34,
511
+ "id": "292626a1",
512
+ "metadata": {},
513
+ "outputs": [
514
+ {
515
+ "name": "stdout",
516
+ "output_type": "stream",
517
+ "text": [
518
+ " Customer ID Recency Frequency Monetary\n",
519
+ "0 12346 165 11 372.86\n",
520
+ "1 12347 3 2 1323.32\n",
521
+ "2 12348 74 1 222.16\n",
522
+ "3 12349 43 3 2671.14\n",
523
+ "4 12351 11 1 300.93\n"
524
+ ]
525
+ }
526
+ ],
527
+ "source": [
528
+ "print(rfm.head())"
529
+ ]
530
+ }
531
+ ],
532
+ "metadata": {
533
+ "kernelspec": {
534
+ "display_name": "Python 3",
535
+ "language": "python",
536
+ "name": "python3"
537
+ },
538
+ "language_info": {
539
+ "codemirror_mode": {
540
+ "name": "ipython",
541
+ "version": 3
542
+ },
543
+ "file_extension": ".py",
544
+ "mimetype": "text/x-python",
545
+ "name": "python",
546
+ "nbconvert_exporter": "python",
547
+ "pygments_lexer": "ipython3",
548
+ "version": "3.12.4"
549
+ }
550
+ },
551
+ "nbformat": 4,
552
+ "nbformat_minor": 5
553
+ }
notebooks/02_RFM_Segmentation.ipynb ADDED
The diff for this file is too large to render. See raw diff
 
notebooks/03_Churn_Prediction.ipynb ADDED
The diff for this file is too large to render. See raw diff
 
requirements.txt ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ pandas
2
+ numpy
3
+ matplotlib
4
+ seaborn
5
+ plotly>=5.0.0
6
+ scikit-learn
7
+ openpyxl
8
+ nbformat>=4.2.0
9
+ ipykernel
10
+ fastapi
11
+ uvicorn
12
+ pyyaml
13
+ pytest
14
+ httpx
15
+ xgboost
16
+ shap
17
+ scipy
18
+ websockets
src/api.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # Entry point wrapper for uvicorn execution and testing
2
+ from src.api.main import app
src/api/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # ChurnFlow Modular API Package
2
+ from src.api.main import app
src/api/config.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+
4
+ # Add project root to path if needed
5
+ project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
6
+ if project_root not in sys.path:
7
+ sys.path.append(project_root)
8
+
9
+ from src.config_loader import CONFIG
10
+ from src.logger_config import logger
11
+
12
+ MODEL_PATH = CONFIG["paths"]["model"]
13
+ CHALLENGER_PATH = os.path.join(project_root, "models", "churn_rf_model.pkl")
14
+ HISTORY_PATH = os.path.join(project_root, "logs", "inference_history.csv")
15
+ DB_PATH = os.path.join(project_root, "logs", "predictions.db")
16
+ CLEANED_DATA_PATH = os.path.join(project_root, "data", "processed", "cleaned_customer_data.csv")
17
+ CUSTOMERS_JSON_PATH = os.path.join(project_root, "dashboard", "public", "data", "customers.json")
src/api/database.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sqlite3
3
+ import datetime
4
+ from src.api.config import DB_PATH, HISTORY_PATH, logger
5
+
6
+ def init_db() -> None:
7
+ """Initializes the CSV inference log and SQLite shadow database schemas."""
8
+ # Initialize CSV file
9
+ try:
10
+ os.makedirs(os.path.dirname(HISTORY_PATH), exist_ok=True)
11
+ if not os.path.exists(HISTORY_PATH):
12
+ with open(HISTORY_PATH, "w") as f:
13
+ f.write("Timestamp,Recency,Frequency,Monetary,BasketSize\n")
14
+ logger.info(f"Initialized inference history log at {HISTORY_PATH}")
15
+ except Exception as e:
16
+ logger.error(f"Failed to initialize inference log file: {str(e)}")
17
+
18
+ # Initialize SQLite database
19
+ try:
20
+ os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
21
+ conn = sqlite3.connect(DB_PATH)
22
+ cursor = conn.cursor()
23
+ cursor.execute("""
24
+ CREATE TABLE IF NOT EXISTS shadow_predictions (
25
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
26
+ timestamp TEXT,
27
+ recency REAL,
28
+ frequency REAL,
29
+ monetary REAL,
30
+ basket_size REAL,
31
+ champion_prob REAL,
32
+ challenger_prob REAL
33
+ )
34
+ """)
35
+ conn.commit()
36
+ conn.close()
37
+ logger.info(f"Successfully initialized shadow prediction database at {DB_PATH}")
38
+ except Exception as e:
39
+ logger.error(f"Failed to initialize SQLite shadow DB: {str(e)}")
40
+
41
+ def log_inference(recency: float, frequency: float, monetary: float, basket_size: float) -> None:
42
+ """Appends live inference inputs to the CSV history log."""
43
+ try:
44
+ timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
45
+ with open(HISTORY_PATH, "a") as f:
46
+ f.write(f"{timestamp},{recency},{frequency},{monetary},{basket_size}\n")
47
+ except Exception as e:
48
+ logger.error(f"Failed to log inference request to CSV: {str(e)}")
49
+
50
+ def log_shadow_prediction(recency: float, frequency: float, monetary: float, basket_size: float, champion_prob: float, challenger_prob: float) -> None:
51
+ """Logs prediction inputs and outputs of Champion and Challenger models to SQLite."""
52
+ try:
53
+ timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
54
+ conn = sqlite3.connect(DB_PATH)
55
+ cursor = conn.cursor()
56
+ cursor.execute(
57
+ "INSERT INTO shadow_predictions (timestamp, recency, frequency, monetary, basket_size, champion_prob, challenger_prob) VALUES (?, ?, ?, ?, ?, ?, ?)",
58
+ (timestamp, recency, frequency, monetary, basket_size, champion_prob, challenger_prob)
59
+ )
60
+ conn.commit()
61
+ conn.close()
62
+ except Exception as e:
63
+ logger.error(f"Failed to log shadow prediction to SQLite: {str(e)}")
src/api/drift_service.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import pandas as pd
3
+ from scipy.stats import ks_2samp
4
+ from typing import Dict, Any
5
+ from src.api.config import HISTORY_PATH, CLEANED_DATA_PATH, CONFIG, logger
6
+
7
+ def run_drift_analysis() -> Dict[str, Any]:
8
+ """Runs a Kolmogorov-Smirnov test to detect data drift between baseline and production data."""
9
+ # Check production history file
10
+ if not os.path.exists(HISTORY_PATH):
11
+ return {
12
+ "drift_detected": False,
13
+ "drift_status": "Insufficient Data",
14
+ "message": "Production inference history file is missing."
15
+ }
16
+
17
+ try:
18
+ prod_df = pd.read_csv(HISTORY_PATH)
19
+ except Exception as e:
20
+ logger.error(f"Error reading inference history: {str(e)}")
21
+ return {
22
+ "drift_detected": False,
23
+ "drift_status": "Error",
24
+ "message": f"Could not load production logs: {str(e)}"
25
+ }
26
+
27
+ # We require a minimum of 10 samples to run statistical checks
28
+ min_samples = 10
29
+ prod_size = len(prod_df)
30
+ if prod_size < min_samples:
31
+ return {
32
+ "drift_detected": False,
33
+ "drift_status": "Insufficient Data",
34
+ "message": f"Awaiting production predictions. Need at least {min_samples} requests to run statistical test (current: {prod_size}).",
35
+ "sample_sizes": {
36
+ "baseline": 4312,
37
+ "production": prod_size
38
+ }
39
+ }
40
+
41
+ # Load baseline dataset
42
+ baseline_path = CLEANED_DATA_PATH
43
+ if not os.path.exists(baseline_path):
44
+ baseline_path = CONFIG["paths"]["clean_data"]
45
+
46
+ try:
47
+ base_df = pd.read_csv(baseline_path)
48
+ except Exception as e:
49
+ logger.error(f"Error loading baseline clean dataset: {str(e)}")
50
+ return {
51
+ "drift_detected": False,
52
+ "drift_status": "Error",
53
+ "message": f"Could not load baseline training data: {str(e)}"
54
+ }
55
+
56
+ # Mapping of column names: baseline vs production history
57
+ features_to_test = {
58
+ "Recency": "Recency",
59
+ "Frequency": "Frequency",
60
+ "Monetary": "Monetary",
61
+ "AvgBucketSize": "BasketSize"
62
+ }
63
+
64
+ drift_details = {}
65
+ drift_detected = False
66
+
67
+ for base_col, prod_col in features_to_test.items():
68
+ if base_col not in base_df.columns or prod_col not in prod_df.columns:
69
+ logger.warning(f"Feature columns not found: {base_col} in base or {prod_col} in prod.")
70
+ continue
71
+
72
+ base_arr = base_df[base_col].dropna().values
73
+ prod_arr = prod_df[prod_col].dropna().values
74
+
75
+ # Run Kolmogorov-Smirnov test (2-sample)
76
+ stat, pval = ks_2samp(base_arr, prod_arr)
77
+
78
+ # Standard 5% significance level
79
+ has_drifted = pval < 0.05
80
+ if has_drifted:
81
+ drift_detected = True
82
+
83
+ drift_details[base_col] = {
84
+ "p_value": round(float(pval), 5),
85
+ "drift_status": "Drifted" if has_drifted else "Stable",
86
+ "baseline_mean": round(float(base_arr.mean()), 2),
87
+ "production_mean": round(float(prod_arr.mean()), 2)
88
+ }
89
+
90
+ status = "Drift Detected" if drift_detected else "Stable"
91
+ message = "Production distribution has shifted statistically from baseline training distributions. Model performance may degrade." if drift_detected else "Incoming request distributions align with baseline training distributions."
92
+
93
+ return {
94
+ "drift_detected": drift_detected,
95
+ "drift_status": status,
96
+ "message": message,
97
+ "sample_sizes": {
98
+ "baseline": len(base_df),
99
+ "production": prod_size
100
+ },
101
+ "features": drift_details
102
+ }
src/api/main.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from contextlib import asynccontextmanager
4
+
5
+ from src.api.config import logger
6
+ from src.api.database import init_db
7
+ from src.api.ml_services import load_models_and_data
8
+ from src.api.routes import router
9
+
10
+ @asynccontextmanager
11
+ async def lifespan(app: FastAPI):
12
+ # Startup: Load databases and models
13
+ logger.info("Starting up Customer Churn Prediction API...")
14
+ init_db()
15
+ load_models_and_data()
16
+ yield
17
+ # Shutdown
18
+ logger.info("Shutting down Customer Churn Prediction API...")
19
+
20
+ app = FastAPI(
21
+ title="Customer Churn Prediction API",
22
+ description="Production-grade REST API serving real-time customer churn forecasts based on XGBoost & Random Forest features.",
23
+ version="2.0.0",
24
+ lifespan=lifespan
25
+ )
26
+
27
+ # Enable CORS for frontend dashboard access
28
+ app.add_middleware(
29
+ CORSMiddleware,
30
+ allow_origins=["*"], # Restrict origins in production environments
31
+ allow_credentials=True,
32
+ allow_methods=["*"],
33
+ allow_headers=["*"],
34
+ )
35
+
36
+ # Include routes APIRouter
37
+ app.include_router(router)
src/api/ml_services.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import joblib
3
+ import json
4
+ import numpy as np
5
+ import xgboost as xgb
6
+ import pandas as pd
7
+ from typing import Dict, Any, List
8
+ from src.api.config import MODEL_PATH, CHALLENGER_PATH, CUSTOMERS_JSON_PATH, CONFIG, logger
9
+ from src.api.models import CustomerInput
10
+
11
+ # Global variables for models and in-memory customer data
12
+ champion_model = None
13
+ challenger_model = None
14
+ memory_customers = {}
15
+
16
+ def load_models_and_data() -> None:
17
+ """Loads all models and loads customer record assets into memory."""
18
+ global champion_model, challenger_model, memory_customers
19
+
20
+ # Load Champion model
21
+ logger.info(f"Attempting to load champion model from {MODEL_PATH}...")
22
+ if os.path.exists(MODEL_PATH):
23
+ try:
24
+ champion_model = joblib.load(MODEL_PATH)
25
+ logger.info("Successfully loaded champion XGBoost model.")
26
+ except Exception as e:
27
+ logger.error(f"Error loading champion model: {str(e)}")
28
+ else:
29
+ logger.warning(f"Champion model file missing at {MODEL_PATH}.")
30
+
31
+ # Load Challenger model
32
+ logger.info(f"Attempting to load challenger model from {CHALLENGER_PATH}...")
33
+ if os.path.exists(CHALLENGER_PATH):
34
+ try:
35
+ challenger_model = joblib.load(CHALLENGER_PATH)
36
+ logger.info("Successfully loaded challenger Random Forest model.")
37
+ except Exception as e:
38
+ logger.error(f"Error loading challenger model: {str(e)}")
39
+ else:
40
+ logger.warning(f"Challenger model file missing at {CHALLENGER_PATH}.")
41
+
42
+ # Load customer records into memory for live WebSocket streaming
43
+ if os.path.exists(CUSTOMERS_JSON_PATH):
44
+ try:
45
+ with open(CUSTOMERS_JSON_PATH, "r") as f:
46
+ data = json.load(f)
47
+ for cust in data:
48
+ memory_customers[str(cust["id"])] = cust
49
+ logger.info(f"Successfully loaded {len(memory_customers)} customer records into memory for WebSocket streaming.")
50
+ except Exception as e:
51
+ logger.error(f"Failed to load customers.json into memory: {str(e)}")
52
+ else:
53
+ logger.warning(f"customers.json missing at {CUSTOMERS_JSON_PATH}. WebSocket streaming fallback mock data will be used.")
54
+
55
+ def get_realtime_recommendation(recency: int, frequency: int, monetary: float, churn_prob: float) -> str:
56
+ """Generates actionable retention strategy based on client segment classification."""
57
+ is_high_value = (frequency >= 3) or (monetary >= 300.0)
58
+
59
+ if churn_prob >= 0.70:
60
+ if is_high_value:
61
+ return "At-Risk VIP: High historic value. Route to customer relations manager for direct feedback outreach. Offer priority recovery benefits."
62
+ else:
63
+ return "Hibernating Win-back: Inactive standard customer. Target with automated email re-engagement flow offering aggressive discount vouchers."
64
+ elif churn_prob >= 0.30:
65
+ if is_high_value:
66
+ return "Proactive VIP Retention: High-value showing drop-off signs. Send customized recommendations based on past purchases. Avoid direct discount spam."
67
+ else:
68
+ return "Standard Retention: Nurture with standard newsletter promotions and seasonal discounts."
69
+ else:
70
+ if is_high_value:
71
+ return "Maintain & Upsell: Core loyal customer. Exclude from margin-diluting discount codes. Send early access and premium alerts."
72
+ else:
73
+ return "Nurture Campaign: Keep engaged with standard marketing updates."
74
+
75
+ def preprocess_features(customer: CustomerInput) -> np.ndarray:
76
+ """Preprocesses input parameters and fills in feature engineered attributes."""
77
+ single_buyer_impute = CONFIG["parameters"]["single_order_imputation_days"]
78
+ default_uk = CONFIG["parameters"]["default_is_uk"]
79
+
80
+ avg_days = customer.avg_days_between if customer.avg_days_between is not None else (single_buyer_impute if customer.frequency == 1 else 30.0)
81
+ recent_ratio = customer.recent_orders_ratio if customer.recent_orders_ratio is not None else (1.0 if customer.recency <= 60 else 0.0)
82
+ recency_ratio = customer.recency / (avg_days + 1e-5)
83
+ is_uk = customer.is_uk if customer.is_uk is not None else default_uk
84
+
85
+ return np.array([[
86
+ customer.recency,
87
+ customer.frequency,
88
+ customer.monetary,
89
+ customer.basket_size,
90
+ avg_days,
91
+ recency_ratio,
92
+ recent_ratio,
93
+ is_uk
94
+ ]])
95
+
96
+ def run_champion_inference(features: np.ndarray) -> float:
97
+ """Computes prediction probability using the Champion XGBoost model."""
98
+ if champion_model is None:
99
+ raise ValueError("Champion model is not loaded.")
100
+ return float(champion_model.predict_proba(features)[:, 1][0])
101
+
102
+ def run_challenger_inference(features: np.ndarray) -> float:
103
+ """Computes prediction probability using the Challenger Random Forest model."""
104
+ if challenger_model is None:
105
+ return 0.0
106
+ df_features = pd.DataFrame(features, columns=[
107
+ 'Recency', 'Frequency', 'Monetary', 'AvgBucketSize',
108
+ 'AvgDaysBetween', 'Recency_to_AvgDaysRatio', 'Recent_Orders_Ratio', 'Is_UK'
109
+ ])
110
+ return float(challenger_model.predict_proba(df_features)[:, 1][0])
111
+
112
+ def compute_shap_values(features: np.ndarray) -> Dict[str, float]:
113
+ """Calculates TreeSHAP feature attribution scores for explainability plots."""
114
+ if champion_model is None:
115
+ return {}
116
+ booster = champion_model.get_booster()
117
+ dmat = xgb.DMatrix(features, feature_names=[
118
+ 'Recency', 'Frequency', 'Monetary', 'AvgBucketSize',
119
+ 'AvgDaysBetween', 'Recency_to_AvgDaysRatio', 'Recent_Orders_Ratio', 'Is_UK'
120
+ ])
121
+ contribs = booster.predict(dmat, pred_contribs=True)[0]
122
+ return {
123
+ "Recency": float(contribs[0]),
124
+ "Frequency": float(contribs[1]),
125
+ "Monetary": float(contribs[2]),
126
+ "AvgBucketSize": float(contribs[3]),
127
+ "AvgDaysBetween": float(contribs[4]),
128
+ "Recency_to_AvgDaysRatio": float(contribs[5]),
129
+ "Recent_Orders_Ratio": float(contribs[6]),
130
+ "Is_UK": float(contribs[7])
131
+ }
src/api/models.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field
2
+ from typing import List, Dict, Any
3
+
4
+ class CustomerInput(BaseModel):
5
+ recency: int = Field(..., description="Days since last purchase", ge=0, json_schema_extra={"example": 65})
6
+ frequency: int = Field(..., description="Number of unique orders/invoices", ge=1, json_schema_extra={"example": 3})
7
+ monetary: float = Field(..., description="Average value spend per invoice", ge=0.0, json_schema_extra={"example": 350.50})
8
+ basket_size: float = Field(..., description="Average items count per basket size", ge=0.0, json_schema_extra={"example": 12.5})
9
+ avg_days_between: float | None = Field(None, description="Average days between purchases.", ge=0.0)
10
+ recent_orders_ratio: float | None = Field(None, description="Ratio of orders placed in last 60 days.", ge=0.0, le=1.0)
11
+ is_uk: int | None = Field(None, description="1 if customer is in the UK, else 0.", ge=0, le=1)
12
+
13
+ class BatchInput(BaseModel):
14
+ customers: List[CustomerInput]
src/api/routes.py ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sqlite3
3
+ import numpy as np
4
+ from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect
5
+ from typing import Dict, Any
6
+
7
+ from src.api.config import DB_PATH, HISTORY_PATH, logger
8
+ from src.api.models import CustomerInput, BatchInput
9
+ from src.api import ml_services
10
+ from src.api.database import log_inference, log_shadow_prediction
11
+ from src.api.drift_service import run_drift_analysis
12
+ from src.api.websocket_manager import ws_manager
13
+
14
+ router = APIRouter()
15
+
16
+ @router.get("/health", tags=["System"])
17
+ def health_check() -> Dict[str, Any]:
18
+ """Returns API health status and checks if the model is loaded."""
19
+ logger.info("Executing health check request...")
20
+ return {
21
+ "status": "healthy",
22
+ "model_loaded": ml_services.champion_model is not None
23
+ }
24
+
25
+ @router.post("/predict", tags=["Predictions"])
26
+ def predict_churn(customer: CustomerInput) -> Dict[str, Any]:
27
+ """Calculates churn probability and risk tier for a single customer."""
28
+ # Log incoming prediction inputs to CSV
29
+ log_inference(customer.recency, customer.frequency, customer.monetary, customer.basket_size)
30
+
31
+ if ml_services.champion_model is None:
32
+ logger.error("Inference requested but model is not loaded.")
33
+ raise HTTPException(
34
+ status_code=503,
35
+ detail="Machine learning model is not loaded. Please train the model first."
36
+ )
37
+
38
+ try:
39
+ # Preprocess features
40
+ features = ml_services.preprocess_features(customer)
41
+
42
+ # Calculate probabilities
43
+ prob = ml_services.run_champion_inference(features)
44
+ challenger_prob = ml_services.run_challenger_inference(features)
45
+
46
+ # Compute explainability values
47
+ shap_values = ml_services.compute_shap_values(features)
48
+
49
+ # Log to SQLite DB
50
+ log_shadow_prediction(
51
+ customer.recency, customer.frequency, customer.monetary, customer.basket_size,
52
+ prob, challenger_prob
53
+ )
54
+ except Exception as e:
55
+ logger.error(f"Inference prediction process failed: {str(e)}")
56
+ raise HTTPException(status_code=500, detail=f"Inference failed: {str(e)}")
57
+
58
+ if prob >= 0.70:
59
+ tier = "High Risk"
60
+ elif prob >= 0.30:
61
+ tier = "Medium Risk"
62
+ else:
63
+ tier = "Low Risk"
64
+
65
+ recommendation = ml_services.get_realtime_recommendation(
66
+ customer.recency,
67
+ customer.frequency,
68
+ customer.monetary,
69
+ prob
70
+ )
71
+
72
+ return {
73
+ "churn_probability": round(prob, 4),
74
+ "risk_tier": tier,
75
+ "recommendation": recommendation,
76
+ "shap_values": shap_values
77
+ }
78
+
79
+ @router.post("/predict_batch", tags=["Predictions"])
80
+ def predict_churn_batch(batch: BatchInput) -> Dict[str, Any]:
81
+ """Calculates churn predictions in batch for a list of customer records."""
82
+ if ml_services.champion_model is None:
83
+ logger.error("Inference batch requested but model is not loaded.")
84
+ raise HTTPException(
85
+ status_code=503,
86
+ detail="Machine learning model is not loaded. Please train the model first."
87
+ )
88
+
89
+ if not batch.customers:
90
+ return {"predictions": []}
91
+
92
+ features_list = []
93
+ for c in batch.customers:
94
+ features = ml_services.preprocess_features(c)
95
+ features_list.append(features[0])
96
+
97
+ logger.info(f"Running batch inference for {len(batch.customers)} profiles...")
98
+ try:
99
+ probs = ml_services.champion_model.predict_proba(np.array(features_list))[:, 1].tolist()
100
+ except Exception as e:
101
+ logger.error(f"XGBoost batch prediction failed: {str(e)}")
102
+ raise HTTPException(status_code=500, detail=f"Inference failed: {str(e)}")
103
+
104
+ results = []
105
+ for customer, prob in zip(batch.customers, probs):
106
+ if prob >= 0.70:
107
+ tier = "High Risk"
108
+ elif prob >= 0.30:
109
+ tier = "Medium Risk"
110
+ else:
111
+ tier = "Low Risk"
112
+
113
+ recommendation = ml_services.get_realtime_recommendation(
114
+ customer.recency,
115
+ customer.frequency,
116
+ customer.monetary,
117
+ prob
118
+ )
119
+
120
+ results.append({
121
+ "churn_probability": round(prob, 4),
122
+ "risk_tier": tier,
123
+ "recommendation": recommendation
124
+ })
125
+
126
+ return {"predictions": results}
127
+
128
+ @router.get("/monitor", tags=["System"])
129
+ def monitor_drift() -> Dict[str, Any]:
130
+ """Runs a Kolmogorov-Smirnov test to detect data drift between baseline and production data."""
131
+ return run_drift_analysis()
132
+
133
+ @router.get("/shadow_stats", tags=["System"])
134
+ def get_shadow_stats() -> Dict[str, Any]:
135
+ """Retrieves side-by-side performance metrics for Champion vs Challenger models in shadow deployment."""
136
+ if not os.path.exists(DB_PATH):
137
+ return {
138
+ "total_predictions": 0,
139
+ "champion_mean": 0.0,
140
+ "challenger_mean": 0.0,
141
+ "mean_absolute_deviation": 0.0,
142
+ "agreement_rate": 1.0,
143
+ "recent_logs": []
144
+ }
145
+
146
+ try:
147
+ conn = sqlite3.connect(DB_PATH)
148
+ conn.row_factory = sqlite3.Row
149
+ cursor = conn.cursor()
150
+
151
+ # Load all shadow logs
152
+ cursor.execute("SELECT * FROM shadow_predictions ORDER BY id DESC")
153
+ rows = cursor.fetchall()
154
+ conn.close()
155
+ except Exception as e:
156
+ logger.error(f"Error reading shadow predictions SQLite table: {str(e)}")
157
+ raise HTTPException(status_code=500, detail=f"Database query failed: {str(e)}")
158
+
159
+ total_preds = len(rows)
160
+ if total_preds == 0:
161
+ return {
162
+ "total_predictions": 0,
163
+ "champion_mean": 0.0,
164
+ "challenger_mean": 0.0,
165
+ "mean_absolute_deviation": 0.0,
166
+ "agreement_rate": 1.0,
167
+ "recent_logs": []
168
+ }
169
+
170
+ champion_probs = [r["champion_prob"] for r in rows]
171
+ challenger_probs = [r["challenger_prob"] for r in rows]
172
+
173
+ champion_mean = sum(champion_probs) / total_preds
174
+ challenger_mean = sum(challenger_probs) / total_preds
175
+
176
+ # Calculate Mean Absolute Deviation (MAD)
177
+ mad = sum(abs(champ - chall) for champ, chall in zip(champion_probs, challenger_probs)) / total_preds
178
+
179
+ # Calculate decision agreement (agreement on binary threshold 0.50 risk split)
180
+ agreements = 0
181
+ for champ, chall in zip(champion_probs, challenger_probs):
182
+ champ_class = 1 if champ >= 0.5 else 0
183
+ chall_class = 1 if chall >= 0.5 else 0
184
+ if champ_class == chall_class:
185
+ agreements += 1
186
+
187
+ agreement_rate = agreements / total_preds
188
+
189
+ # Extract last 5 logs for front-end rendering
190
+ recent_logs = []
191
+ for r in rows[:5]:
192
+ recent_logs.append({
193
+ "id": r["id"],
194
+ "timestamp": r["timestamp"],
195
+ "recency": r["recency"],
196
+ "frequency": r["frequency"],
197
+ "monetary": r["monetary"],
198
+ "basket_size": r["basket_size"],
199
+ "champion_prob": round(float(r["champion_prob"]), 4),
200
+ "challenger_prob": round(float(r["challenger_prob"]), 4)
201
+ })
202
+
203
+ return {
204
+ "total_predictions": total_preds,
205
+ "champion_mean": round(champion_mean, 4),
206
+ "challenger_mean": round(challenger_mean, 4),
207
+ "mean_absolute_deviation": round(mad, 4),
208
+ "agreement_rate": round(agreement_rate, 4),
209
+ "recent_logs": recent_logs
210
+ }
211
+
212
+ @router.websocket("/ws/transactions")
213
+ async def websocket_transactions(websocket: WebSocket):
214
+ """WebSocket connection that generates a live transaction stream and pushes real-time updates."""
215
+ await ws_manager.connect(websocket)
216
+ try:
217
+ await ws_manager.stream_live_transactions(websocket)
218
+ except WebSocketDisconnect:
219
+ ws_manager.disconnect(websocket)
220
+ except Exception as e:
221
+ logger.error(f"WebSocket execution error: {str(e)}")
222
+ ws_manager.disconnect(websocket)
src/api/websocket_manager.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import random
3
+ import numpy as np
4
+ import pandas as pd
5
+ from fastapi import WebSocket, WebSocketDisconnect
6
+ from src.api.config import logger
7
+ from src.api.ml_services import memory_customers, champion_model, challenger_model
8
+ from src.api.database import log_inference, log_shadow_prediction
9
+
10
+ class WebSocketManager:
11
+ def __init__(self):
12
+ self.active_connections: list[WebSocket] = []
13
+
14
+ async def connect(self, websocket: WebSocket):
15
+ await websocket.accept()
16
+ self.active_connections.append(websocket)
17
+ logger.info(f"WebSocket client connected. Total connections: {len(self.active_connections)}")
18
+
19
+ def disconnect(self, websocket: WebSocket):
20
+ if websocket in self.active_connections:
21
+ self.active_connections.remove(websocket)
22
+ logger.info(f"WebSocket client disconnected. Total connections: {len(self.active_connections)}")
23
+
24
+ async def stream_live_transactions(self, websocket: WebSocket):
25
+ """Generates mock transaction stream, executes predictions, and pushes live JSON socket updates."""
26
+ try:
27
+ while True:
28
+ # Select random customer
29
+ if not memory_customers:
30
+ cust_id = "19999"
31
+ cust = {
32
+ "id": cust_id,
33
+ "recency": 45,
34
+ "frequency": 3,
35
+ "monetary": 120.0,
36
+ "basketSize": 4.5,
37
+ "isUk": 1,
38
+ "avgDaysBetween": 30.0
39
+ }
40
+ else:
41
+ cust_id = random.choice(list(memory_customers.keys()))
42
+ cust = memory_customers[cust_id]
43
+
44
+ # Simulate transaction values
45
+ invoice_value = round(random.uniform(15.0, 250.0), 2)
46
+ quantity = random.randint(1, 10)
47
+
48
+ old_freq = cust.get("frequency", 3)
49
+ old_mon = cust.get("monetary", 100.0)
50
+ old_basket = cust.get("basketSize", cust.get("basket_size", 4.0))
51
+ is_uk = cust.get("isUk", cust.get("is_uk", 1))
52
+ avg_days = cust.get("avgDaysBetween", cust.get("avg_days_between", 30.0))
53
+
54
+ new_freq = old_freq + 1
55
+ new_mon = (old_mon * old_freq + invoice_value) / new_freq
56
+ new_basket = (old_basket * old_freq + quantity) / new_freq
57
+
58
+ # Update memory cache
59
+ cust["recency"] = 0
60
+ cust["frequency"] = new_freq
61
+ cust["monetary"] = new_mon
62
+ cust["basketSize"] = new_basket
63
+ memory_customers[cust_id] = cust
64
+
65
+ # Features ordering: Recency, Frequency, Monetary, AvgBucketSize, AvgDaysBetween, Recency_to_AvgDaysRatio, Recent_Orders_Ratio, Is_UK
66
+ features = np.array([[
67
+ 0.0,
68
+ new_freq,
69
+ new_mon,
70
+ new_basket,
71
+ avg_days,
72
+ 0.0,
73
+ 1.0,
74
+ is_uk
75
+ ]])
76
+
77
+ champion_prob = 0.15
78
+ challenger_prob = 0.20
79
+
80
+ # Model predicts
81
+ if champion_model is not None:
82
+ try:
83
+ champion_prob = float(champion_model.predict_proba(features)[:, 1][0])
84
+ except Exception as ex:
85
+ logger.error(f"XGBoost WS prediction failed: {str(ex)}")
86
+
87
+ if challenger_model is not None:
88
+ try:
89
+ df_features = pd.DataFrame(features, columns=[
90
+ 'Recency', 'Frequency', 'Monetary', 'AvgBucketSize',
91
+ 'AvgDaysBetween', 'Recency_to_AvgDaysRatio', 'Recent_Orders_Ratio', 'Is_UK'
92
+ ])
93
+ challenger_prob = float(challenger_model.predict_proba(df_features)[:, 1][0])
94
+ except Exception as ex:
95
+ logger.error(f"RF WS prediction failed: {str(ex)}")
96
+
97
+ if champion_prob >= 0.70:
98
+ new_risk_tier = "High Risk"
99
+ elif champion_prob >= 0.30:
100
+ new_risk_tier = "Medium Risk"
101
+ else:
102
+ new_risk_tier = "Low Risk"
103
+
104
+ # Log predictions
105
+ log_inference(0.0, new_freq, new_mon, new_basket)
106
+ log_shadow_prediction(0.0, new_freq, new_mon, new_basket, champion_prob, challenger_prob)
107
+
108
+ payload = {
109
+ "id": cust_id,
110
+ "type": "TRANSACTION",
111
+ "invoiceValue": invoice_value,
112
+ "quantity": quantity,
113
+ "newMetrics": {
114
+ "recency": 0,
115
+ "frequency": int(new_freq),
116
+ "monetary": round(float(new_mon), 2),
117
+ "basketSize": round(float(new_basket), 1),
118
+ "churnProb": round(float(champion_prob), 4),
119
+ "riskTier": new_risk_tier
120
+ }
121
+ }
122
+
123
+ await websocket.send_json(payload)
124
+ await asyncio.sleep(random.uniform(3.0, 6.0))
125
+
126
+ except WebSocketDisconnect:
127
+ pass
128
+ except Exception as e:
129
+ logger.error(f"Error streaming live transactions: {str(e)}")
130
+
131
+ ws_manager = WebSocketManager()
src/config_loader.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import yaml
3
+ from typing import Any, Dict
4
+
5
+ # Determine the absolute project root directory
6
+ PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
7
+
8
+ def load_config() -> Dict[str, Any]:
9
+ """Loads and parses the config.yaml file, resolving relative paths to absolute ones."""
10
+ config_path = os.path.join(PROJECT_ROOT, "config.yaml")
11
+ if not os.path.exists(config_path):
12
+ raise FileNotFoundError(f"Configuration file not found at {config_path}")
13
+
14
+ with open(config_path, 'r') as f:
15
+ config = yaml.safe_load(f)
16
+
17
+ # Dynamically resolve relative paths to absolute paths
18
+ if "paths" in config:
19
+ for key, rel_path in config["paths"].items():
20
+ config["paths"][key] = os.path.abspath(os.path.join(PROJECT_ROOT, rel_path))
21
+
22
+ return config
23
+
24
+ # Load configuration dynamically on import
25
+ CONFIG = load_config()
src/export_json.py ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import pandas as pd
4
+ import json
5
+ from typing import Dict, Any
6
+
7
+ # Ensure project root is in path
8
+ project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
9
+ if project_root not in sys.path:
10
+ sys.path.append(project_root)
11
+
12
+ from src.config_loader import CONFIG
13
+ from src.logger_config import logger
14
+
15
+ def export_data() -> None:
16
+ """Formats churn model prediction outcomes into browser-ready JSON assets for the dashboard."""
17
+ REPORT_PATH = CONFIG["paths"]["predictions_report"]
18
+
19
+ # Target folders inside React project
20
+ DATA_DIR = os.path.join(project_root, "dashboard", "public", "data")
21
+ os.makedirs(DATA_DIR, exist_ok=True)
22
+
23
+ SUMMARY_OUT = os.path.join(DATA_DIR, "summary.json")
24
+ CUSTOMERS_OUT = os.path.join(DATA_DIR, "customers.json")
25
+
26
+ # Fallback to run prediction script if missing
27
+ if not os.path.exists(REPORT_PATH):
28
+ logger.warning(f"Report file not found at {REPORT_PATH}. Running predict.py first...")
29
+ from src.predict import run_predictions
30
+ run_predictions()
31
+
32
+ logger.info("Reading prediction outputs for JSON export...")
33
+ try:
34
+ df = pd.read_csv(REPORT_PATH)
35
+
36
+ # Calculate stats
37
+ total_customers = len(df)
38
+
39
+ risk_counts = df['Risk_Tier'].value_counts()
40
+ high_risk_count = int(risk_counts.get('High Risk', 0))
41
+ med_risk_count = int(risk_counts.get('Medium Risk', 0))
42
+ low_risk_count = int(risk_counts.get('Low Risk', 0))
43
+
44
+ # Revenue at Risk (High Risk customers)
45
+ high_risk_df = df[df['Risk_Tier'] == 'High Risk']
46
+ high_risk_df_copy = high_risk_df.copy()
47
+ high_risk_df_copy['TotalValue'] = high_risk_df_copy['Monetary'] * high_risk_df_copy['Frequency']
48
+ revenue_at_risk = float(high_risk_df_copy['TotalValue'].sum())
49
+
50
+ # Calculate segments count
51
+ segment_counts = df['Segment'].value_counts().to_dict()
52
+
53
+ # Churn risk per segment
54
+ segment_churn: Dict[str, Dict[str, Any]] = {}
55
+ for seg in df['Segment'].unique():
56
+ seg_df = df[df['Segment'] == seg]
57
+ total_seg = len(seg_df)
58
+ if total_seg > 0:
59
+ high_risk_seg = len(seg_df[seg_df['Risk_Tier'] == 'High Risk'])
60
+ segment_churn[seg] = {
61
+ "total": total_seg,
62
+ "high_risk": high_risk_seg,
63
+ "high_risk_pct": float(high_risk_seg / total_seg)
64
+ }
65
+
66
+ summary_data = {
67
+ "total_customers": total_customers,
68
+ "revenue_at_risk": round(revenue_at_risk, 2),
69
+ "risk_tiers": {
70
+ "High Risk": high_risk_count,
71
+ "Medium Risk": med_risk_count,
72
+ "Low Risk": low_risk_count
73
+ },
74
+ "segment_distribution": {str(k): int(v) for k, v in segment_counts.items()},
75
+ "segment_churn": segment_churn
76
+ }
77
+
78
+ logger.info(f"Saving summary statistics to {SUMMARY_OUT}...")
79
+ with open(SUMMARY_OUT, 'w', encoding='utf-8') as f:
80
+ json.dump(summary_data, f, indent=2)
81
+
82
+ logger.info("Formatting individual customer records for frontend table...")
83
+ customers_list = []
84
+ for _, row in df.iterrows():
85
+ customers_list.append({
86
+ "id": str(row['Customer ID']),
87
+ "recency": int(row['Recency']),
88
+ "frequency": int(row['Frequency']),
89
+ "monetary": round(float(row['Monetary']), 2),
90
+ "basketSize": round(float(row['AvgBucketSize']), 2),
91
+ "churnProb": round(float(row['Churn_Probability']), 4),
92
+ "riskTier": str(row['Risk_Tier']),
93
+ "segment": str(row['Segment']),
94
+ "recommendation": str(row['Actionable_Recommendation']),
95
+ "shapValues": {
96
+ "Recency": round(float(row['SHAP_Recency']), 4),
97
+ "Frequency": round(float(row['SHAP_Frequency']), 4),
98
+ "Monetary": round(float(row['SHAP_Monetary']), 4),
99
+ "AvgBucketSize": round(float(row['SHAP_AvgBucketSize']), 4),
100
+ "AvgDaysBetween": round(float(row['SHAP_AvgDaysBetween']), 4),
101
+ "Recency_to_AvgDaysRatio": round(float(row['SHAP_Recency_to_AvgDaysRatio']), 4),
102
+ "Recent_Orders_Ratio": round(float(row['SHAP_Recent_Orders_Ratio']), 4),
103
+ "Is_UK": round(float(row['SHAP_Is_UK']), 4)
104
+ }
105
+ })
106
+
107
+ logger.info(f"Saving {len(customers_list)} formatted records to {CUSTOMERS_OUT}...")
108
+ with open(CUSTOMERS_OUT, 'w', encoding='utf-8') as f:
109
+ json.dump(customers_list, f) # No indent to minimize payload size
110
+
111
+ logger.info("JSON Data Export Completed successfully!")
112
+
113
+ except Exception as e:
114
+ logger.error(f"JSON export failed with error: {str(e)}")
115
+ raise e
116
+
117
+ if __name__ == "__main__":
118
+ export_data()
src/logger_config.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import logging
3
+ from typing import Any
4
+
5
+ PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
6
+
7
+ def setup_logger() -> logging.Logger:
8
+ """Configures structured logging to both standard output and log file."""
9
+ log_dir = os.path.join(PROJECT_ROOT, "logs")
10
+ os.makedirs(log_dir, exist_ok=True)
11
+
12
+ log_file = os.path.join(log_dir, "customer_analytics.log")
13
+
14
+ logger = logging.getLogger("customer_analytics")
15
+ logger.setLevel(logging.INFO)
16
+
17
+ # Avoid duplicate handlers if imported multiple times
18
+ if not logger.handlers:
19
+ formatter = logging.Formatter(
20
+ '[%(asctime)s] %(levelname)s [%(filename)s:%(lineno)d] %(message)s',
21
+ datefmt='%Y-%m-%d %H:%M:%S'
22
+ )
23
+
24
+ # File Handler
25
+ file_handler = logging.FileHandler(log_file, encoding='utf-8')
26
+ file_handler.setFormatter(formatter)
27
+ logger.addHandler(file_handler)
28
+
29
+ # Console Handler
30
+ console_handler = logging.StreamHandler()
31
+ console_handler.setFormatter(formatter)
32
+ logger.addHandler(console_handler)
33
+
34
+ return logger
35
+
36
+ # Globally available logger instance
37
+ logger = setup_logger()
src/make_dataset.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import pandas as pd
4
+ from typing import NoReturn
5
+
6
+ # Ensure project root is in path
7
+ project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
8
+ if project_root not in sys.path:
9
+ sys.path.append(project_root)
10
+
11
+ from src.config_loader import CONFIG
12
+ from src.logger_config import logger
13
+
14
+ def load_and_clean_data(raw_file_path: str, output_path: str) -> None:
15
+ """Loads raw Excel transaction logs, applies data cleaning pipeline, and saves processed CSV.
16
+
17
+ Args:
18
+ raw_file_path: Path to the raw Excel dataset.
19
+ output_path: Path to save the cleaned CSV dataset.
20
+ """
21
+ logger.info("Loading Raw Excel Data...")
22
+ if not os.path.exists(raw_file_path):
23
+ logger.error(f"Raw file not found: {raw_file_path}")
24
+ raise FileNotFoundError(f"File not found at {raw_file_path}")
25
+
26
+ try:
27
+ df = pd.read_excel(raw_file_path, sheet_name='Year 2009-2010')
28
+ logger.info(f"Successfully loaded raw data. Row count: {len(df)}")
29
+
30
+ logger.info("Starting cleaning pipeline: dropping missing customer IDs...")
31
+ df_clean = df.dropna(subset=['Customer ID']).copy()
32
+
33
+ logger.info("Filtering out refunds and returns (Quantity/Price <= 0)...")
34
+ df_clean = df_clean[(df_clean['Quantity'] > 0) & (df_clean['Price'] > 0)]
35
+
36
+ logger.info("Formatting customer IDs and datetimes...")
37
+ df_clean['Customer ID'] = df_clean['Customer ID'].astype(int).astype(str)
38
+ df_clean['InvoiceDate'] = pd.to_datetime(df_clean['InvoiceDate'])
39
+
40
+ logger.info("Calculating total transactional revenue per row...")
41
+ df_clean['Total Price'] = df_clean['Quantity'] * df_clean['Price']
42
+
43
+ # Ensure parent directory exists
44
+ os.makedirs(os.path.dirname(output_path), exist_ok=True)
45
+
46
+ logger.info(f"Saving cleaned dataset to {output_path}...")
47
+ df_clean.to_csv(output_path, index=False)
48
+ logger.info("Data loading and cleaning pipeline complete!")
49
+
50
+ except Exception as e:
51
+ logger.error(f"Data cleaning failed with error: {str(e)}")
52
+ raise e
53
+
54
+ if __name__ == "__main__":
55
+ RAW_PATH = CONFIG["paths"]["raw_data"]
56
+ PROCESSED_PATH = CONFIG["paths"]["clean_data"]
57
+
58
+ try:
59
+ load_and_clean_data(RAW_PATH, PROCESSED_PATH)
60
+ except Exception:
61
+ sys.exit(1)
src/predict.py ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import pandas as pd
4
+ import numpy as np
5
+ import joblib
6
+ import xgboost as xgb
7
+ from typing import Optional
8
+
9
+ # Ensure project root is in the path
10
+ project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
11
+ if project_root not in sys.path:
12
+ sys.path.append(project_root)
13
+
14
+ from src.config_loader import CONFIG
15
+ from src.logger_config import logger
16
+ from src.make_dataset import load_and_clean_data
17
+
18
+ def run_predictions() -> None:
19
+ """Executes customer churn forecasts, merges segments, and exports marketing reports."""
20
+ RAW_PATH = CONFIG["paths"]["raw_data"]
21
+ PROCESSED_PATH = CONFIG["paths"]["clean_data"]
22
+ MODEL_PATH = CONFIG["paths"]["model"]
23
+ SEGMENTS_PATH = CONFIG["paths"]["segments"]
24
+
25
+ OUTPUT_REPORT_PATH = CONFIG["paths"]["predictions_report"]
26
+ OUTPUT_TARGET_PATH = CONFIG["paths"]["high_value_report"]
27
+
28
+ # Step 1: Ensure Clean Data Exists
29
+ if not os.path.exists(PROCESSED_PATH):
30
+ logger.warning(f"Cleaned dataset not found at {PROCESSED_PATH}. Running loader...")
31
+ if not os.path.exists(RAW_PATH):
32
+ logger.error(f"Raw data file not found at {RAW_PATH}. Cannot proceed.")
33
+ raise FileNotFoundError(f"Raw data file not found at {RAW_PATH}")
34
+ load_and_clean_data(RAW_PATH, PROCESSED_PATH)
35
+
36
+ logger.info("Loading cleaned dataset...")
37
+ try:
38
+ df = pd.read_csv(PROCESSED_PATH)
39
+ df['InvoiceDate'] = pd.to_datetime(df['InvoiceDate'])
40
+
41
+ # Step 2: Feature Engineering (Full History Snapshot)
42
+ logger.info("Engineering customer RFM features...")
43
+ today = df['InvoiceDate'].max()
44
+ logger.info(f"Current snapshot date (Reference 'Today'): {today.strftime('%Y-%m-%d')}")
45
+
46
+ features = df.groupby('Customer ID').agg({
47
+ 'InvoiceDate': lambda x: (today - x.max()).days, # Recency
48
+ 'Invoice': 'nunique', # Frequency
49
+ 'Total Price': 'mean', # Avg Monetary Spend
50
+ 'Quantity': 'mean' # Avg Basket Size
51
+ }).reset_index()
52
+
53
+ features.rename(columns={
54
+ 'InvoiceDate': 'Recency',
55
+ 'Invoice': 'Frequency',
56
+ 'Total Price': 'Monetary',
57
+ 'Quantity': 'AvgBucketSize'
58
+ }, inplace=True)
59
+
60
+ features['Customer ID'] = features['Customer ID'].astype(str)
61
+
62
+ # 1. Inter-purchase Time (AvgDaysBetween)
63
+ logger.info("Calculating average days between purchases...")
64
+ df_sorted = df.sort_values(['Customer ID', 'InvoiceDate'])
65
+ invoices = df_sorted.drop_duplicates(subset=['Customer ID', 'Invoice']).copy()
66
+ invoices['PrevInvoiceDate'] = invoices.groupby('Customer ID')['InvoiceDate'].shift(1)
67
+ invoices['DaysBetween'] = (invoices['InvoiceDate'] - invoices['PrevInvoiceDate']).dt.days
68
+
69
+ avg_days_between = invoices.groupby('Customer ID')['DaysBetween'].mean().reset_index()
70
+ avg_days_between.rename(columns={'DaysBetween': 'AvgDaysBetween'}, inplace=True)
71
+ avg_days_between['Customer ID'] = avg_days_between['Customer ID'].astype(str)
72
+
73
+ features = pd.merge(features, avg_days_between, on='Customer ID', how='left')
74
+
75
+ single_buyer_impute = CONFIG["parameters"]["single_order_imputation_days"]
76
+ features['AvgDaysBetween'] = features['AvgDaysBetween'].fillna(single_buyer_impute)
77
+
78
+ # 2. Recency to AvgDaysBetween Ratio
79
+ features['Recency_to_AvgDaysRatio'] = features['Recency'] / (features['AvgDaysBetween'] + 1e-5)
80
+
81
+ # 3. Recent Orders Ratio (last 60 days)
82
+ logger.info("Calculating order frequency ratios in recent days...")
83
+ recent_window = CONFIG["parameters"]["recent_purchase_window_days"]
84
+ recent_cutoff = today - pd.DateOffset(days=recent_window)
85
+ recent_invoices = df[df['InvoiceDate'] >= recent_cutoff].groupby('Customer ID')['Invoice'].nunique().reset_index()
86
+ recent_invoices.rename(columns={'Invoice': 'RecentInvoices'}, inplace=True)
87
+ recent_invoices['Customer ID'] = recent_invoices['Customer ID'].astype(str)
88
+
89
+ features = pd.merge(features, recent_invoices, on='Customer ID', how='left')
90
+ features['RecentInvoices'] = features['RecentInvoices'].fillna(0)
91
+ features['Recent_Orders_Ratio'] = features['RecentInvoices'] / features['Frequency']
92
+ features.drop(columns=['RecentInvoices'], inplace=True)
93
+
94
+ # 4. Is UK Customer
95
+ customer_country = df.groupby('Customer ID')['Country'].first().reset_index()
96
+ customer_country['Customer ID'] = customer_country['Customer ID'].astype(str)
97
+ customer_country['Is_UK'] = (customer_country['Country'] == 'United Kingdom').astype(int)
98
+
99
+ features = pd.merge(features, customer_country[['Customer ID', 'Is_UK']], on='Customer ID', how='left')
100
+
101
+ # Step 3: Loading Model and Predicting
102
+ logger.info(f"Loading trained XGBoost model from {MODEL_PATH}...")
103
+ if not os.path.exists(MODEL_PATH):
104
+ logger.error("Serialized model file missing.")
105
+ raise FileNotFoundError(f"Model file not found at {MODEL_PATH}. Please train the model first.")
106
+
107
+ model = joblib.load(MODEL_PATH)
108
+
109
+ feature_cols = [
110
+ 'Recency', 'Frequency', 'Monetary', 'AvgBucketSize',
111
+ 'AvgDaysBetween', 'Recency_to_AvgDaysRatio', 'Recent_Orders_Ratio', 'Is_UK'
112
+ ]
113
+ X = features[feature_cols]
114
+
115
+ logger.info("Running predictions...")
116
+ features['Churn_Probability'] = model.predict_proba(X)[:, 1]
117
+
118
+ logger.info("Calculating TreeSHAP contributions...")
119
+ booster = model.get_booster()
120
+ dmat = xgb.DMatrix(X, feature_names=feature_cols)
121
+ contribs = booster.predict(dmat, pred_contribs=True)
122
+ for i, col in enumerate(feature_cols):
123
+ features[f'SHAP_{col}'] = contribs[:, i]
124
+
125
+ # Define Risk Tiers
126
+ def get_risk_tier(prob: float) -> str:
127
+ if prob >= 0.70:
128
+ return "High Risk"
129
+ elif prob >= 0.30:
130
+ return "Medium Risk"
131
+ else:
132
+ return "Low Risk"
133
+
134
+ features['Risk_Tier'] = features['Churn_Probability'].apply(get_risk_tier)
135
+
136
+ # Step 4: Merging with Customer Segments
137
+ logger.info("Merging predictions with offline segments...")
138
+ if os.path.exists(SEGMENTS_PATH):
139
+ segments_df = pd.read_csv(SEGMENTS_PATH)[['Customer ID', 'Segment']]
140
+ segments_df['Customer ID'] = segments_df['Customer ID'].astype(str)
141
+
142
+ merged_df = pd.merge(features, segments_df, on='Customer ID', how='left')
143
+ merged_df['Segment'] = merged_df['Segment'].fillna('New / Unclassified')
144
+ else:
145
+ logger.warning(f"Segments database missing at {SEGMENTS_PATH}.")
146
+ merged_df = features.copy()
147
+ merged_df['Segment'] = 'Unclassified'
148
+
149
+ # Step 5: Assign Business Recommendations
150
+ logger.info("Generating targeted marketing recommendations...")
151
+ def get_recommendation(row: pd.Series) -> str:
152
+ segment = row['Segment']
153
+ tier = row['Risk_Tier']
154
+
155
+ if tier == "High Risk":
156
+ if "Champion" in segment:
157
+ return "At-Risk Champion: High historical spend. Assign a personal account manager for direct outreach. Do not send automated discount spam."
158
+ elif "Loyalist" in segment or "Loyal" in segment:
159
+ return "At-Risk Loyalist: Dedicated customer showing signs of leaving. Offer a special loyalty reward or high-value incentive."
160
+ elif "Hibernating" in segment or "About to Sleep" in segment:
161
+ return "Hibernating Win-back: Aggressive discount offer or 'We Miss You' promotion with limited validity to re-engage."
162
+ elif "New" in segment or "Promising" in segment:
163
+ return "Immediate Activation: One-time buyer showing low activity. Trigger welcome sequence or first-repeat-purchase incentive."
164
+ else:
165
+ return "Standard Re-engagement: Target with standard product updates and a mild discount."
166
+ elif tier == "Medium Risk":
167
+ if "Champion" in segment or "Loyal" in segment:
168
+ return "Proactive VIP Retention: High-value showing drop-off signs. Send customized recommendations based on past purchases. Avoid direct discount spam."
169
+ elif "Hibernating" in segment or "About to Sleep" in segment:
170
+ return "Nurture Campaign: Include in standard promotional newsletters and generic sale announcements."
171
+ else:
172
+ return "Standard Retention: Monitor activity. Send standard seasonal discount codes."
173
+ else: # Low Risk
174
+ if "Champion" in segment or "Loyal" in segment:
175
+ return "Maintain & Protect: Do nothing. Keep regular service quality high. Exclude from aggressive discount lists to preserve margin."
176
+ else:
177
+ return "Standard Relationship Management: Keep engaged with standard updates."
178
+
179
+ merged_df['Actionable_Recommendation'] = merged_df.apply(get_recommendation, axis=1)
180
+
181
+ # Step 6: Exporting Reports
182
+ logger.info("Exporting CSV reports...")
183
+ merged_df = merged_df.sort_values(by='Churn_Probability', ascending=False)
184
+
185
+ os.makedirs(os.path.dirname(OUTPUT_REPORT_PATH), exist_ok=True)
186
+ merged_df.to_csv(OUTPUT_REPORT_PATH, index=False)
187
+ logger.info(f" -> Complete Churn Report saved to: {OUTPUT_REPORT_PATH}")
188
+
189
+ # Filter and export top 100 high-value high-risk customers
190
+ high_risk_df = merged_df[merged_df['Risk_Tier'] == 'High Risk']
191
+ high_value_at_risk = high_risk_df.sort_values(by='Monetary', ascending=False).head(100)
192
+
193
+ high_value_at_risk.to_csv(OUTPUT_TARGET_PATH, index=False)
194
+ logger.info(f" -> Top 100 High-Value At-Risk Customers saved to: {OUTPUT_TARGET_PATH}")
195
+
196
+ # Summary logging
197
+ logger.info("\n" + "="*50)
198
+ logger.info(" CHURN ANALYSIS SUMMARY")
199
+ logger.info("="*50)
200
+ logger.info(f"Total Customers Analyzed: {len(merged_df)}")
201
+
202
+ risk_counts = merged_df['Risk_Tier'].value_counts()
203
+ high_count = risk_counts.get('High Risk', 0)
204
+ med_count = risk_counts.get('Medium Risk', 0)
205
+ low_count = risk_counts.get('Low Risk', 0)
206
+
207
+ logger.info(f"High Risk (Churn Prob >= 70%): {high_count} ({high_count/len(merged_df):.1%})")
208
+ logger.info(f"Medium Risk (30% <= Prob < 70%): {med_count} ({med_count/len(merged_df):.1%})")
209
+ logger.info(f"Low Risk (Churn Prob < 30%): {low_count} ({low_count/len(merged_df):.1%})")
210
+ logger.info("-"*50)
211
+
212
+ high_risk_revenue = (high_risk_df['Monetary'] * high_risk_df['Frequency']).sum()
213
+ logger.info(f"Total Revenue At Risk (High Risk): ${high_risk_revenue:,.2f}")
214
+ logger.info("="*50 + "\n")
215
+
216
+ except Exception as e:
217
+ logger.error(f"Prediction flow failed with error: {str(e)}")
218
+ raise e
219
+
220
+ if __name__ == "__main__":
221
+ try:
222
+ run_predictions()
223
+ except Exception:
224
+ sys.exit(1)
src/train.py ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import pandas as pd
4
+ import numpy as np
5
+ import joblib
6
+ from sklearn.model_selection import train_test_split
7
+ from xgboost import XGBClassifier
8
+ from sklearn.metrics import classification_report
9
+
10
+ # Ensure project root is in path
11
+ project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
12
+ if project_root not in sys.path:
13
+ sys.path.append(project_root)
14
+
15
+ from src.config_loader import CONFIG
16
+ from src.logger_config import logger
17
+
18
+ def train_churn_model(data_path: str, model_output_path: str) -> None:
19
+ """Performs feature engineering on clean customer history, trains an XGBoost model,
20
+
21
+ saves the trained model `.pkl` to disk.
22
+
23
+ Args:
24
+ data_path: Path to the clean CSV dataset.
25
+ model_output_path: Output path for the serialized XGBoost model.
26
+ """
27
+ logger.info(f"Loading cleaned dataset from {data_path}...")
28
+ try:
29
+ df = pd.read_csv(data_path)
30
+ df['InvoiceDate'] = pd.to_datetime(df['InvoiceDate'])
31
+
32
+ logger.info("Computing cutoff date for prediction window...")
33
+ cutoff_days = CONFIG["parameters"]["cutoff_offset_days"]
34
+ cutoff_date = df['InvoiceDate'].max() - pd.DateOffset(days=cutoff_days)
35
+ logger.info(f"Reference snapshot cutoff date: {cutoff_date.strftime('%Y-%m-%d')}")
36
+
37
+ logger.info("Splitting dataset into history vs target prediction window...")
38
+ train_data = df[df['InvoiceDate'] < cutoff_date].copy()
39
+ test_target_data = df[df['InvoiceDate'] >= cutoff_date].copy()
40
+
41
+ # Identify active customers (members of active list are non-churners)
42
+ active_customers = [str(int(x)) for x in test_target_data['Customer ID'].dropna().unique()]
43
+
44
+ logger.info("Engineering base RFM features...")
45
+ features = train_data.groupby('Customer ID').agg({
46
+ 'InvoiceDate': lambda x: (cutoff_date - x.max()).days, # Recency
47
+ 'Invoice': 'nunique', # Frequency
48
+ 'Total Price': 'mean', # Avg Monetary
49
+ 'Quantity': 'mean' # Avg Basket Size
50
+ }).reset_index()
51
+
52
+ features.rename(columns={
53
+ 'InvoiceDate': 'Recency',
54
+ 'Invoice': 'Frequency',
55
+ 'Total Price': 'Monetary',
56
+ 'Quantity': 'AvgBucketSize'
57
+ }, inplace=True)
58
+
59
+ features['Customer ID'] = features['Customer ID'].astype(str)
60
+
61
+ logger.info("Engineering advanced MLE features...")
62
+
63
+ # 1. Calculate Inter-purchase Time (AvgDaysBetween)
64
+ df_sorted = train_data.sort_values(['Customer ID', 'InvoiceDate'])
65
+ invoices = df_sorted.drop_duplicates(subset=['Customer ID', 'Invoice']).copy()
66
+ invoices['PrevInvoiceDate'] = invoices.groupby('Customer ID')['InvoiceDate'].shift(1)
67
+ invoices['DaysBetween'] = (invoices['InvoiceDate'] - invoices['PrevInvoiceDate']).dt.days
68
+
69
+ avg_days_between = invoices.groupby('Customer ID')['DaysBetween'].mean().reset_index()
70
+ avg_days_between.rename(columns={'DaysBetween': 'AvgDaysBetween'}, inplace=True)
71
+ avg_days_between['Customer ID'] = avg_days_between['Customer ID'].astype(str)
72
+
73
+ features = pd.merge(features, avg_days_between, on='Customer ID', how='left')
74
+
75
+ # Impute single purchase buyers with config default
76
+ single_buyer_impute = CONFIG["parameters"]["single_order_imputation_days"]
77
+ features['AvgDaysBetween'] = features['AvgDaysBetween'].fillna(single_buyer_impute)
78
+
79
+ # 2. Recency to AvgDaysBetween Ratio
80
+ features['Recency_to_AvgDaysRatio'] = features['Recency'] / (features['AvgDaysBetween'] + 1e-5)
81
+
82
+ # 3. Recent Orders Ratio (last 60 days of training window)
83
+ recent_window = CONFIG["parameters"]["recent_purchase_window_days"]
84
+ recent_cutoff = cutoff_date - pd.DateOffset(days=recent_window)
85
+ recent_invoices = train_data[train_data['InvoiceDate'] >= recent_cutoff].groupby('Customer ID')['Invoice'].nunique().reset_index()
86
+ recent_invoices.rename(columns={'Invoice': 'RecentInvoices'}, inplace=True)
87
+ recent_invoices['Customer ID'] = recent_invoices['Customer ID'].astype(str)
88
+
89
+ features = pd.merge(features, recent_invoices, on='Customer ID', how='left')
90
+ features['RecentInvoices'] = features['RecentInvoices'].fillna(0)
91
+ features['Recent_Orders_Ratio'] = features['RecentInvoices'] / features['Frequency']
92
+ features.drop(columns=['RecentInvoices'], inplace=True)
93
+
94
+ # 4. Is UK Customer
95
+ customer_country = train_data.groupby('Customer ID')['Country'].first().reset_index()
96
+ customer_country['Customer ID'] = customer_country['Customer ID'].astype(str)
97
+ customer_country['Is_UK'] = (customer_country['Country'] == 'United Kingdom').astype(int)
98
+
99
+ features = pd.merge(features, customer_country[['Customer ID', 'Is_UK']], on='Customer ID', how='left')
100
+
101
+ # Generate target labels
102
+ features['Is_Churn'] = features['Customer ID'].apply(lambda x: 0 if x in active_customers else 1)
103
+ logger.info(f"Target Label Generation complete. Churn class ratio: {features['Is_Churn'].mean():.2%}")
104
+
105
+ # Split features and labels
106
+ feature_cols = [
107
+ 'Recency', 'Frequency', 'Monetary', 'AvgBucketSize',
108
+ 'AvgDaysBetween', 'Recency_to_AvgDaysRatio', 'Recent_Orders_Ratio', 'Is_UK'
109
+ ]
110
+ X = features[feature_cols]
111
+ y = features['Is_Churn']
112
+
113
+ X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
114
+
115
+ logger.info("Initializing XGBoost classifier with config hyperparameters...")
116
+ hyperparams = CONFIG["model_hyperparameters"]
117
+ xgb = XGBClassifier(
118
+ n_estimators=hyperparams["n_estimators"],
119
+ learning_rate=hyperparams["learning_rate"],
120
+ max_depth=hyperparams["max_depth"],
121
+ subsample=hyperparams["subsample"],
122
+ colsample_bytree=hyperparams["colsample_bytree"],
123
+ scale_pos_weight=hyperparams["scale_pos_weight"],
124
+ random_state=hyperparams["random_state"]
125
+ )
126
+
127
+ logger.info("Fitting model on training set...")
128
+ xgb.fit(X_train, y_train)
129
+
130
+ logger.info("Evaluating model on validation hold-out set:")
131
+ y_pred = xgb.predict(X_test)
132
+ report = classification_report(y_test, y_pred)
133
+ logger.info(f"\n{report}")
134
+
135
+ # Ensure parent folder exists
136
+ os.makedirs(os.path.dirname(model_output_path), exist_ok=True)
137
+
138
+ logger.info(f"Saving serialized model to {model_output_path}...")
139
+ joblib.dump(xgb, model_output_path)
140
+ logger.info("Model training pipeline complete!")
141
+
142
+ except Exception as e:
143
+ logger.error(f"Model training failed with error: {str(e)}")
144
+ raise e
145
+
146
+ if __name__ == "__main__":
147
+ DATA_PATH = CONFIG["paths"]["clean_data"]
148
+ MODEL_PATH = CONFIG["paths"]["model"]
149
+
150
+ try:
151
+ train_churn_model(DATA_PATH, MODEL_PATH)
152
+ except Exception:
153
+ sys.exit(1)
src/train_challenger.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import pandas as pd
4
+ import numpy as np
5
+ import joblib
6
+ from sklearn.model_selection import train_test_split
7
+ from sklearn.ensemble import RandomForestClassifier
8
+ from sklearn.metrics import classification_report
9
+
10
+ # Ensure project root is in path
11
+ project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
12
+ if project_root not in sys.path:
13
+ sys.path.append(project_root)
14
+
15
+ from src.config_loader import CONFIG
16
+ from src.logger_config import logger
17
+
18
+ def train_challenger_rf(data_path: str, model_output_path: str) -> None:
19
+ """Trains a Random Forest classifier as a Challenger model shadow candidate.
20
+
21
+ Args:
22
+ data_path: Path to the clean CSV dataset.
23
+ model_output_path: Output path for the serialized RF model.
24
+ """
25
+ logger.info(f"Loading cleaned dataset from {data_path} for Challenger training...")
26
+ try:
27
+ df = pd.read_csv(data_path)
28
+ df['InvoiceDate'] = pd.to_datetime(df['InvoiceDate'])
29
+
30
+ cutoff_days = CONFIG["parameters"]["cutoff_offset_days"]
31
+ cutoff_date = df['InvoiceDate'].max() - pd.DateOffset(days=cutoff_days)
32
+
33
+ train_data = df[df['InvoiceDate'] < cutoff_date].copy()
34
+ test_target_data = df[df['InvoiceDate'] >= cutoff_date].copy()
35
+
36
+ active_customers = [str(int(x)) for x in test_target_data['Customer ID'].dropna().unique()]
37
+
38
+ # Aggregate features
39
+ features = train_data.groupby('Customer ID').agg({
40
+ 'InvoiceDate': lambda x: (cutoff_date - x.max()).days,
41
+ 'Invoice': 'nunique',
42
+ 'Total Price': 'mean',
43
+ 'Quantity': 'mean'
44
+ }).reset_index()
45
+
46
+ features.rename(columns={
47
+ 'InvoiceDate': 'Recency',
48
+ 'Invoice': 'Frequency',
49
+ 'Total Price': 'Monetary',
50
+ 'Quantity': 'AvgBucketSize'
51
+ }, inplace=True)
52
+
53
+ features['Customer ID'] = features['Customer ID'].astype(str)
54
+
55
+ # Advanced features
56
+ df_sorted = train_data.sort_values(['Customer ID', 'InvoiceDate'])
57
+ invoices = df_sorted.drop_duplicates(subset=['Customer ID', 'Invoice']).copy()
58
+ invoices['PrevInvoiceDate'] = invoices.groupby('Customer ID')['InvoiceDate'].shift(1)
59
+ invoices['DaysBetween'] = (invoices['InvoiceDate'] - invoices['PrevInvoiceDate']).dt.days
60
+
61
+ avg_days_between = invoices.groupby('Customer ID')['DaysBetween'].mean().reset_index()
62
+ avg_days_between.rename(columns={'DaysBetween': 'AvgDaysBetween'}, inplace=True)
63
+ avg_days_between['Customer ID'] = avg_days_between['Customer ID'].astype(str)
64
+
65
+ features = pd.merge(features, avg_days_between, on='Customer ID', how='left')
66
+ single_buyer_impute = CONFIG["parameters"]["single_order_imputation_days"]
67
+ features['AvgDaysBetween'] = features['AvgDaysBetween'].fillna(single_buyer_impute)
68
+
69
+ features['Recency_to_AvgDaysRatio'] = features['Recency'] / (features['AvgDaysBetween'] + 1e-5)
70
+
71
+ recent_window = CONFIG["parameters"]["recent_purchase_window_days"]
72
+ recent_cutoff = cutoff_date - pd.DateOffset(days=recent_window)
73
+ recent_invoices = train_data[train_data['InvoiceDate'] >= recent_cutoff].groupby('Customer ID')['Invoice'].nunique().reset_index()
74
+ recent_invoices.rename(columns={'Invoice': 'RecentInvoices'}, inplace=True)
75
+ recent_invoices['Customer ID'] = recent_invoices['Customer ID'].astype(str)
76
+
77
+ features = pd.merge(features, recent_invoices, on='Customer ID', how='left')
78
+ features['RecentInvoices'] = features['RecentInvoices'].fillna(0)
79
+ features['Recent_Orders_Ratio'] = features['RecentInvoices'] / features['Frequency']
80
+ features.drop(columns=['RecentInvoices'], inplace=True)
81
+
82
+ customer_country = train_data.groupby('Customer ID')['Country'].first().reset_index()
83
+ customer_country['Customer ID'] = customer_country['Customer ID'].astype(str)
84
+ customer_country['Is_UK'] = (customer_country['Country'] == 'United Kingdom').astype(int)
85
+
86
+ features = pd.merge(features, customer_country[['Customer ID', 'Is_UK']], on='Customer ID', how='left')
87
+
88
+ features['Is_Churn'] = features['Customer ID'].apply(lambda x: 0 if x in active_customers else 1)
89
+
90
+ feature_cols = [
91
+ 'Recency', 'Frequency', 'Monetary', 'AvgBucketSize',
92
+ 'AvgDaysBetween', 'Recency_to_AvgDaysRatio', 'Recent_Orders_Ratio', 'Is_UK'
93
+ ]
94
+ X = features[feature_cols]
95
+ y = features['Is_Churn']
96
+
97
+ X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
98
+
99
+ logger.info("Initializing RandomForest challenger classifier...")
100
+ rf = RandomForestClassifier(
101
+ n_estimators=100,
102
+ max_depth=6,
103
+ class_weight="balanced",
104
+ random_state=42
105
+ )
106
+
107
+ logger.info("Fitting Random Forest challenger on training set...")
108
+ rf.fit(X_train, y_train)
109
+
110
+ logger.info("Evaluating Challenger model on hold-out validation:")
111
+ y_pred = rf.predict(X_test)
112
+ report = classification_report(y_test, y_pred)
113
+ logger.info(f"\n{report}")
114
+
115
+ os.makedirs(os.path.dirname(model_output_path), exist_ok=True)
116
+ logger.info(f"Saving serialized Random Forest challenger model to {model_output_path}...")
117
+ joblib.dump(rf, model_output_path)
118
+ logger.info("Challenger RF model training complete!")
119
+
120
+ except Exception as e:
121
+ logger.error(f"Challenger training failed: {str(e)}")
122
+ raise e
123
+
124
+ if __name__ == "__main__":
125
+ DATA_PATH = CONFIG["paths"]["clean_data"]
126
+ MODEL_OUT = os.path.join(project_root, "models", "churn_rf_model.pkl")
127
+
128
+ try:
129
+ train_challenger_rf(DATA_PATH, MODEL_OUT)
130
+ except Exception:
131
+ sys.exit(1)
src/validate_retraining.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import pandas as pd
4
+ import numpy as np
5
+ import joblib
6
+ from sklearn.model_selection import train_test_split
7
+ from sklearn.metrics import recall_score, classification_report
8
+
9
+ # Ensure project root is in path
10
+ project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
11
+ if project_root not in sys.path:
12
+ sys.path.append(project_root)
13
+
14
+ from src.config_loader import CONFIG
15
+ from src.logger_config import logger
16
+
17
+ def validate_model_performance(data_path: str, model_path: str, recall_threshold: float = 0.80) -> bool:
18
+ """Evaluates the trained model recall performance on holdout test set to gate deployment.
19
+
20
+ Args:
21
+ data_path: Path to the clean CSV dataset.
22
+ model_path: Path to the serialized XGBoost model.
23
+ recall_threshold: Minimum acceptable recall score on the churn class.
24
+
25
+ Returns:
26
+ bool: True if the model performance meets or exceeds the threshold, False otherwise.
27
+ """
28
+ logger.info("Initializing model performance validation check...")
29
+
30
+ if not os.path.exists(model_path):
31
+ logger.error(f"Model binary not found at {model_path}. Cannot validate.")
32
+ return False
33
+
34
+ try:
35
+ df = pd.read_csv(data_path)
36
+ df['InvoiceDate'] = pd.to_datetime(df['InvoiceDate'])
37
+
38
+ cutoff_days = CONFIG["parameters"]["cutoff_offset_days"]
39
+ cutoff_date = df['InvoiceDate'].max() - pd.DateOffset(days=cutoff_days)
40
+
41
+ train_data = df[df['InvoiceDate'] < cutoff_date].copy()
42
+ test_target_data = df[df['InvoiceDate'] >= cutoff_date].copy()
43
+
44
+ active_customers = [str(int(x)) for x in test_target_data['Customer ID'].dropna().unique()]
45
+
46
+ # Aggregations
47
+ features = train_data.groupby('Customer ID').agg({
48
+ 'InvoiceDate': lambda x: (cutoff_date - x.max()).days,
49
+ 'Invoice': 'nunique',
50
+ 'Total Price': 'mean',
51
+ 'Quantity': 'mean'
52
+ }).reset_index()
53
+
54
+ features.rename(columns={
55
+ 'InvoiceDate': 'Recency',
56
+ 'Invoice': 'Frequency',
57
+ 'Total Price': 'Monetary',
58
+ 'Quantity': 'AvgBucketSize'
59
+ }, inplace=True)
60
+
61
+ features['Customer ID'] = features['Customer ID'].astype(str)
62
+
63
+ # Advanced Features
64
+ df_sorted = train_data.sort_values(['Customer ID', 'InvoiceDate'])
65
+ invoices = df_sorted.drop_duplicates(subset=['Customer ID', 'Invoice']).copy()
66
+ invoices['PrevInvoiceDate'] = invoices.groupby('Customer ID')['InvoiceDate'].shift(1)
67
+ invoices['DaysBetween'] = (invoices['InvoiceDate'] - invoices['PrevInvoiceDate']).dt.days
68
+
69
+ avg_days_between = invoices.groupby('Customer ID')['DaysBetween'].mean().reset_index()
70
+ avg_days_between.rename(columns={'DaysBetween': 'AvgDaysBetween'}, inplace=True)
71
+ avg_days_between['Customer ID'] = avg_days_between['Customer ID'].astype(str)
72
+
73
+ features = pd.merge(features, avg_days_between, on='Customer ID', how='left')
74
+ single_buyer_impute = CONFIG["parameters"]["single_order_imputation_days"]
75
+ features['AvgDaysBetween'] = features['AvgDaysBetween'].fillna(single_buyer_impute)
76
+
77
+ features['Recency_to_AvgDaysRatio'] = features['Recency'] / (features['AvgDaysBetween'] + 1e-5)
78
+
79
+ recent_window = CONFIG["parameters"]["recent_purchase_window_days"]
80
+ recent_cutoff = cutoff_date - pd.DateOffset(days=recent_window)
81
+ recent_invoices = train_data[train_data['InvoiceDate'] >= recent_cutoff].groupby('Customer ID')['Invoice'].nunique().reset_index()
82
+ recent_invoices.rename(columns={'Invoice': 'RecentInvoices'}, inplace=True)
83
+ recent_invoices['Customer ID'] = recent_invoices['Customer ID'].astype(str)
84
+
85
+ features = pd.merge(features, recent_invoices, on='Customer ID', how='left')
86
+ features['RecentInvoices'] = features['RecentInvoices'].fillna(0)
87
+ features['Recent_Orders_Ratio'] = features['RecentInvoices'] / features['Frequency']
88
+ features.drop(columns=['RecentInvoices'], inplace=True)
89
+
90
+ customer_country = train_data.groupby('Customer ID')['Country'].first().reset_index()
91
+ customer_country['Customer ID'] = customer_country['Customer ID'].astype(str)
92
+ customer_country['Is_UK'] = (customer_country['Country'] == 'United Kingdom').astype(int)
93
+
94
+ features = pd.merge(features, customer_country[['Customer ID', 'Is_UK']], on='Customer ID', how='left')
95
+ features['Is_Churn'] = features['Customer ID'].apply(lambda x: 0 if x in active_customers else 1)
96
+
97
+ feature_cols = [
98
+ 'Recency', 'Frequency', 'Monetary', 'AvgBucketSize',
99
+ 'AvgDaysBetween', 'Recency_to_AvgDaysRatio', 'Recent_Orders_Ratio', 'Is_UK'
100
+ ]
101
+
102
+ X = features[feature_cols]
103
+ y = features['Is_Churn']
104
+
105
+ # Validation Hold-out Split
106
+ _, X_test, _, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
107
+
108
+ # Load active model
109
+ model = joblib.load(model_path)
110
+
111
+ # Predict on holdout
112
+ y_pred = model.predict(X_test)
113
+
114
+ # Compute Recall for Churn (label=1)
115
+ recall = recall_score(y_test, y_pred)
116
+
117
+ logger.info(f"Retrained Model validation result: Recall = {recall:.2%}")
118
+ logger.info(f"Target Performance Threshold: Recall >= {recall_threshold:.2%}")
119
+ logger.info(f"\n{classification_report(y_test, y_pred)}")
120
+
121
+ if recall >= recall_threshold:
122
+ logger.info("Validation PASSED! Model is eligible for release.")
123
+ return True
124
+ else:
125
+ logger.warning("Validation FAILED! Recall performance does not meet threshold.")
126
+ return False
127
+
128
+ except Exception as e:
129
+ logger.error(f"Error validating model performance: {str(e)}")
130
+ return False
131
+
132
+ if __name__ == "__main__":
133
+ DATA_PATH = CONFIG["paths"]["clean_data"]
134
+ MODEL_PATH = CONFIG["paths"]["model"]
135
+
136
+ success = validate_model_performance(DATA_PATH, MODEL_PATH, recall_threshold=0.80)
137
+ if success:
138
+ sys.exit(0)
139
+ else:
140
+ sys.exit(1)
tests/test_api.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ from fastapi.testclient import TestClient
4
+
5
+ # Ensure project root is in path
6
+ project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
7
+ if project_root not in sys.path:
8
+ sys.path.append(project_root)
9
+
10
+ from src.api import app
11
+
12
+ def test_health_check() -> None:
13
+ """Verifies that the /health API endpoint responds with success status."""
14
+ with TestClient(app) as client:
15
+ response = client.get("/health")
16
+ assert response.status_code == 200
17
+ data = response.json()
18
+ assert data["status"] == "healthy"
19
+ assert "model_loaded" in data
20
+
21
+ def test_predict_endpoint() -> None:
22
+ """Verifies that a valid customer prediction payload yields correct structure and values."""
23
+ payload = {
24
+ "recency": 10,
25
+ "frequency": 5,
26
+ "monetary": 250.0,
27
+ "basket_size": 8.0,
28
+ "avg_days_between": 20.0,
29
+ "recent_orders_ratio": 0.8,
30
+ "is_uk": 1
31
+ }
32
+ with TestClient(app) as client:
33
+ response = client.post("/predict", json=payload)
34
+ assert response.status_code == 200
35
+ data = response.json()
36
+ assert "churn_probability" in data
37
+ assert "risk_tier" in data
38
+ assert "recommendation" in data
39
+ assert isinstance(data["churn_probability"], float)
40
+
41
+ def test_predict_batch_endpoint() -> None:
42
+ """Verifies that a valid list of customer profiles returns batched prediction arrays."""
43
+ payload = {
44
+ "customers": [
45
+ {
46
+ "recency": 10,
47
+ "frequency": 5,
48
+ "monetary": 250.0,
49
+ "basket_size": 8.0
50
+ },
51
+ {
52
+ "recency": 200,
53
+ "frequency": 1,
54
+ "monetary": 50.0,
55
+ "basket_size": 2.0
56
+ }
57
+ ]
58
+ }
59
+ with TestClient(app) as client:
60
+ response = client.post("/predict_batch", json=payload)
61
+ assert response.status_code == 200
62
+ data = response.json()
63
+ assert "predictions" in data
64
+ assert len(data["predictions"]) == 2
65
+ for pred in data["predictions"]:
66
+ assert "churn_probability" in pred
67
+ assert "risk_tier" in pred
68
+ assert "recommendation" in pred
69
+
70
+ def test_monitor_drift_endpoint() -> None:
71
+ """Verifies that the /monitor endpoint returns the statistical drift analysis report."""
72
+ with TestClient(app) as client:
73
+ response = client.get("/monitor")
74
+ assert response.status_code == 200
75
+ data = response.json()
76
+ assert "drift_detected" in data
77
+ assert "drift_status" in data
78
+ assert "message" in data
79
+
80
+ def test_shadow_stats_endpoint() -> None:
81
+ """Verifies that the /shadow_stats endpoint returns Champion vs Challenger metrics."""
82
+ with TestClient(app) as client:
83
+ response = client.get("/shadow_stats")
84
+ assert response.status_code == 200
85
+ data = response.json()
86
+ assert "total_predictions" in data
87
+ assert "champion_mean" in data
88
+ assert "challenger_mean" in data
89
+ assert "mean_absolute_deviation" in data
90
+ assert "agreement_rate" in data
91
+ assert "recent_logs" in data