ML
Preprocessing
Metrics
DataFrame
Visualization
Customer Churn Prediction System
A production-grade customer churn prediction system demonstrating classical machine learning with Deepbox. It combines deepbox/ml, deepbox/preprocess, deepbox/metrics, deepbox/dataframe, deepbox/plot to deliver a larger production-style Deepbox workflow with reproducible outputs and documented architecture.
Features
- Multiple Models: Logistic Regression, Decision Tree, Random Forest, Gradient Boosting, KNN, Gaussian Naive Bayes
- Feature Engineering: Synthetic customer data generation
- Cross-Validation: K-Fold validation for robust evaluation
- Model Comparison: Comprehensive metrics comparison
- Interpretability notes: Domain-oriented discussion of drivers (model-native feature importance is not exposed in this walkthrough)
Deepbox Modules Used
deepbox/mldeepbox/preprocessdeepbox/metricsdeepbox/dataframedeepbox/plotProject Architecture
- 03-customer-churn-prediction/
- ├── index.ts # Main entry: data generation, training, CV, plots
- ├── README.md # This file
- └── output/ # Generated SVGs (model comparison, CV scores)
Source Files
index.ts
1/**2 * Customer Churn Prediction System3 *4 * A comprehensive ML pipeline for predicting customer churn using5 * classical machine learning algorithms.6 *7 * Deepbox Modules Used:8 * - deepbox/ml: Classical ML models9 * - deepbox/preprocess: Data preprocessing, train/test split, cross-validation10 * - deepbox/metrics: Classification metrics11 * - deepbox/dataframe: Data manipulation12 * - deepbox/stats: Statistical analysis13 * - deepbox/plot: Visualization14 */1516import { existsSync, mkdirSync, writeFileSync } from "node:fs";17import { isNumericTypedArray, isTypedArray } from "deepbox/core";18import { DataFrame } from "deepbox/dataframe";19import { accuracy, confusionMatrix, f1Score, precision, recall } from "deepbox/metrics";20import {21 DecisionTreeClassifier,22 GaussianNB,23 GradientBoostingClassifier,24 KNeighborsClassifier,25 LogisticRegression,26 RandomForestClassifier,27} from "deepbox/ml";28import { type Tensor, tensor } from "deepbox/ndarray";29import { Figure } from "deepbox/plot";30import { KFold, StandardScaler, trainTestSplit } from "deepbox/preprocess";3132// ============================================================================33// Configuration34// ============================================================================3536const OUTPUT_DIR = "docs/projects/03-customer-churn-prediction/output";37const NUM_SAMPLES = 1000;38const NUM_FEATURES = 10;39const TEST_SIZE = 0.2;40const RANDOM_STATE = 42;4142const expectNumericTypedArray = (43 value: unknown44): Float32Array | Float64Array | Int32Array | Uint8Array => {45 if (!isTypedArray(value) || !isNumericTypedArray(value)) {46 throw new Error("Expected numeric typed array");47 }48 return value;49};5051// ============================================================================52// Data Generation53// ============================================================================5455/**56 * Generate synthetic customer churn dataset57 */58function generateChurnData(59 numSamples: number,60 seed = 4261): {62 X: Tensor;63 y: Tensor;64 featureNames: string[];65} {66 // Seeded random for reproducibility67 let randomSeed = seed;68 const seededRandom = () => {69 randomSeed = (randomSeed * 1103515245 + 12345) & 0x7fffffff;70 return randomSeed / 0x7fffffff;71 };7273 const randomNormal = (mean: number, std: number) => {74 const u1 = seededRandom();75 const u2 = seededRandom();76 const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);77 return mean + std * z;78 };7980 const featureNames = [81 "tenure_months",82 "monthly_charges",83 "total_charges",84 "num_products",85 "has_contract",86 "support_calls",87 "payment_delay_days",88 "age",89 "satisfaction_score",90 "usage_frequency",91 ];9293 const X: number[][] = [];94 const y: number[] = [];9596 for (let i = 0; i < numSamples; i++) {97 // Generate features98 const tenure = Math.max(1, Math.round(randomNormal(24, 18))); // months99 const monthlyCharges = Math.max(20, randomNormal(65, 30));100 const totalCharges = tenure * monthlyCharges * (0.8 + seededRandom() * 0.4);101 const numProducts = Math.round(Math.max(1, Math.min(5, randomNormal(2, 1))));102 const hasContract = seededRandom() > 0.4 ? 1 : 0;103 const supportCalls = Math.round(Math.max(0, randomNormal(2, 3)));104 const paymentDelay = Math.max(0, Math.round(randomNormal(5, 10)));105 const age = Math.round(Math.max(18, Math.min(80, randomNormal(42, 15))));106 const satisfaction = Math.max(1, Math.min(10, randomNormal(6, 2)));107 const usageFreq = Math.max(0, randomNormal(15, 8)); // days per month108109 X.push([110 tenure,111 monthlyCharges,112 totalCharges,113 numProducts,114 hasContract,115 supportCalls,116 paymentDelay,117 age,118 satisfaction,119 usageFreq,120 ]);121122 // Churn probability based on features123 let churnProb = 0.2; // base probability124125 // Higher charges increase churn126 if (monthlyCharges > 80) churnProb += 0.15;127 // Short tenure increases churn128 if (tenure < 12) churnProb += 0.2;129 // No contract increases churn130 if (hasContract === 0) churnProb += 0.15;131 // Many support calls increase churn132 if (supportCalls > 4) churnProb += 0.2;133 // Payment delays increase churn134 if (paymentDelay > 10) churnProb += 0.15;135 // Low satisfaction increases churn136 if (satisfaction < 4) churnProb += 0.25;137 // Low usage increases churn138 if (usageFreq < 5) churnProb += 0.1;139140 // Cap probability141 churnProb = Math.min(0.9, Math.max(0.1, churnProb));142143 // Generate label144 y.push(seededRandom() < churnProb ? 1 : 0);145 }146147 return {148 X: tensor(X),149 y: tensor(y),150 featureNames,151 };152}153154// ============================================================================155// Main Execution156// ============================================================================157158console.log("═".repeat(70));159console.log(" CUSTOMER CHURN PREDICTION SYSTEM");160console.log(" Built with Deepbox — TypeScript toolkit for AI & numerical computing");161console.log("═".repeat(70));162163// Create output directory164if (!existsSync(OUTPUT_DIR)) {165 mkdirSync(OUTPUT_DIR, { recursive: true });166}167168// ============================================================================169// Step 1: Generate and Explore Data170// ============================================================================171172console.log("\n📊 STEP 1: Data Generation and Exploration");173console.log("─".repeat(70));174175const { X, y, featureNames } = generateChurnData(NUM_SAMPLES, RANDOM_STATE);176177console.log(`\n✓ Generated synthetic customer data`);178console.log(` Samples: ${NUM_SAMPLES}`);179console.log(` Features: ${NUM_FEATURES}`);180181// Class distribution182const yData = expectNumericTypedArray(y.data);183const numChurned = Array.from(yData).filter((v) => v === 1).length;184const numRetained = NUM_SAMPLES - numChurned;185186console.log(`\nClass Distribution:`);187console.log(` Churned (1): ${numChurned} (${((numChurned / NUM_SAMPLES) * 100).toFixed(1)}%)`);188console.log(` Retained (0): ${numRetained} (${((numRetained / NUM_SAMPLES) * 100).toFixed(1)}%)`);189190// Feature statistics191console.log(`\nFeature Statistics:`);192const XData = expectNumericTypedArray(X.data);193194const statsDF = new DataFrame({195 Feature: featureNames,196 Mean: featureNames.map((_, i) => {197 let sum = 0;198 for (let j = 0; j < NUM_SAMPLES; j++) {199 sum += XData[j * NUM_FEATURES + i];200 }201 return (sum / NUM_SAMPLES).toFixed(2);202 }),203 Std: featureNames.map((_, i) => {204 let sum = 0;205 let sumSq = 0;206 for (let j = 0; j < NUM_SAMPLES; j++) {207 const val = XData[j * NUM_FEATURES + i];208 sum += val;209 sumSq += val * val;210 }211 const mean = sum / NUM_SAMPLES;212 const variance = sumSq / NUM_SAMPLES - mean * mean;213 return Math.sqrt(variance).toFixed(2);214 }),215 Min: featureNames.map((_, i) => {216 let min = Infinity;217 for (let j = 0; j < NUM_SAMPLES; j++) {218 min = Math.min(min, XData[j * NUM_FEATURES + i]);219 }220 return min.toFixed(2);221 }),222 Max: featureNames.map((_, i) => {223 let max = -Infinity;224 for (let j = 0; j < NUM_SAMPLES; j++) {225 max = Math.max(max, XData[j * NUM_FEATURES + i]);226 }227 return max.toFixed(2);228 }),229});230231console.log(statsDF.toString());232233// ============================================================================234// Step 2: Data Preprocessing235// ============================================================================236237console.log("\n🔄 STEP 2: Data Preprocessing");238console.log("─".repeat(70));239240// Train/test split241const [XTrain, XTest, yTrain, yTest] = trainTestSplit(X, y, {242 testSize: TEST_SIZE,243 randomState: RANDOM_STATE,244 shuffle: true,245});246247console.log(`\n✓ Train/Test Split:`);248console.log(` Training samples: ${XTrain.shape[0]}`);249console.log(` Test samples: ${XTest.shape[0]}`);250251// Feature scaling252const scaler = new StandardScaler();253scaler.fit(XTrain);254const XTrainScaled = scaler.transform(XTrain);255const XTestScaled = scaler.transform(XTest);256257console.log(`✓ Applied StandardScaler`);258259// ============================================================================260// Step 3: Model Training and Evaluation261// ============================================================================262263console.log("\n🤖 STEP 3: Model Training and Evaluation");264console.log("─".repeat(70));265266// Define models to compare267const models: {268 name: string;269 model:270 | LogisticRegression271 | DecisionTreeClassifier272 | RandomForestClassifier273 | GradientBoostingClassifier274 | KNeighborsClassifier275 | GaussianNB;276 params: string;277}[] = [278 {279 name: "Logistic Regression",280 model: new LogisticRegression({ maxIter: 100, learningRate: 0.1 }),281 params: "maxIter=100, lr=0.1",282 },283 {284 name: "Decision Tree",285 model: new DecisionTreeClassifier({ maxDepth: 5 }),286 params: "maxDepth=5",287 },288 {289 name: "Random Forest",290 model: new RandomForestClassifier({291 nEstimators: 50,292 maxDepth: 5,293 randomState: RANDOM_STATE,294 }),295 params: "nEstimators=50, maxDepth=5",296 },297 {298 name: "Gradient Boosting",299 model: new GradientBoostingClassifier({300 nEstimators: 50,301 maxDepth: 3,302 learningRate: 0.1,303 }),304 params: "nEstimators=50, maxDepth=3, lr=0.1",305 },306 {307 name: "KNN",308 model: new KNeighborsClassifier({ nNeighbors: 5 }),309 params: "k=5",310 },311 {312 name: "Naive Bayes",313 model: new GaussianNB(),314 params: "default",315 },316];317318const results: {319 name: string;320 accuracy: number;321 precision: number;322 recall: number;323 f1: number;324 trainTime: number;325}[] = [];326327console.log("\nTraining models...\n");328329for (const { name, model, params: _params } of models) {330 const startTime = Date.now();331332 try {333 // Train model334 model.fit(XTrainScaled, yTrain);335336 // Predict337 const yPred = model.predict(XTestScaled);338339 // Calculate metrics340 const acc = accuracy(yTest, yPred);341 const prec = precision(yTest, yPred, "binary");342 const rec = recall(yTest, yPred, "binary");343 const f1 = f1Score(yTest, yPred, "binary");344345 const trainTime = Date.now() - startTime;346347 results.push({348 name,349 accuracy: Number(acc),350 precision: Number(prec),351 recall: Number(rec),352 f1: Number(f1),353 trainTime,354 });355356 console.log(357 ` ✓ ${name.padEnd(20)} - Accuracy: ${(Number(acc) * 100).toFixed(2)}% (${trainTime}ms)`358 );359 } catch (error) {360 console.log(` ✗ ${name.padEnd(20)} - Error: ${error}`);361 }362}363364// ============================================================================365// Step 4: Model Comparison366// ============================================================================367368console.log("\n📈 STEP 4: Model Comparison");369console.log("─".repeat(70));370371// Sort by F1 score372results.sort((a, b) => b.f1 - a.f1);373374const comparisonDF = new DataFrame({375 Model: results.map((r) => r.name),376 "Accuracy (%)": results.map((r) => (r.accuracy * 100).toFixed(2)),377 "Precision (%)": results.map((r) => (r.precision * 100).toFixed(2)),378 "Recall (%)": results.map((r) => (r.recall * 100).toFixed(2)),379 "F1 Score (%)": results.map((r) => (r.f1 * 100).toFixed(2)),380 "Time (ms)": results.map((r) => r.trainTime.toString()),381});382383console.log("\nModel Performance Comparison (sorted by F1 Score):\n");384console.log(comparisonDF.toString());385386// Best model387const bestModel = results[0];388console.log(`\n🏆 Best Model: ${bestModel.name}`);389console.log(` F1 Score: ${(bestModel.f1 * 100).toFixed(2)}%`);390391// ============================================================================392// Step 5: Cross-Validation393// ============================================================================394395console.log("\n🔄 STEP 5: Cross-Validation (Best Model)");396console.log("─".repeat(70));397398// Re-train best model type for cross-validation399const bestModelType = results[0].name;400console.log(`\nPerforming 5-Fold Cross-Validation on ${bestModelType}...`);401402const kfold = new KFold({403 nSplits: 5,404 shuffle: true,405 randomState: RANDOM_STATE,406});407const cvScores: number[] = [];408409let foldNum = 1;410for (const { trainIndex: trainIdx, testIndex: valIdx } of kfold.split(X)) {411 // Extract fold data412 const XTrainFold: number[][] = [];413 const yTrainFold: number[] = [];414 const XValFold: number[][] = [];415 const yValFold: number[] = [];416417 for (const idx of trainIdx) {418 const row: number[] = [];419 for (let j = 0; j < NUM_FEATURES; j++) {420 row.push(XData[idx * NUM_FEATURES + j]);421 }422 XTrainFold.push(row);423 yTrainFold.push(yData[idx]);424 }425426 for (const idx of valIdx) {427 const row: number[] = [];428 for (let j = 0; j < NUM_FEATURES; j++) {429 row.push(XData[idx * NUM_FEATURES + j]);430 }431 XValFold.push(row);432 yValFold.push(yData[idx]);433 }434435 // Scale436 const foldScaler = new StandardScaler();437 foldScaler.fit(tensor(XTrainFold));438 const XTrainFoldScaled = foldScaler.transform(tensor(XTrainFold));439 const XValFoldScaled = foldScaler.transform(tensor(XValFold));440441 // Train and evaluate442 let cvModel:443 | LogisticRegression444 | DecisionTreeClassifier445 | RandomForestClassifier446 | GradientBoostingClassifier447 | KNeighborsClassifier448 | GaussianNB;449 if (bestModelType === "Random Forest") {450 cvModel = new RandomForestClassifier({451 nEstimators: 50,452 maxDepth: 5,453 randomState: RANDOM_STATE,454 });455 } else if (bestModelType === "Gradient Boosting") {456 cvModel = new GradientBoostingClassifier({457 nEstimators: 50,458 maxDepth: 3,459 learningRate: 0.1,460 });461 } else if (bestModelType === "Logistic Regression") {462 cvModel = new LogisticRegression({ maxIter: 100, learningRate: 0.1 });463 } else {464 cvModel = new DecisionTreeClassifier({ maxDepth: 5 });465 }466467 cvModel.fit(XTrainFoldScaled, tensor(yTrainFold));468 const yPredFold = cvModel.predict(XValFoldScaled);469 const foldAcc = Number(accuracy(tensor(yValFold), yPredFold));470471 cvScores.push(foldAcc);472 console.log(` Fold ${foldNum}: Accuracy = ${(foldAcc * 100).toFixed(2)}%`);473 foldNum++;474}475476const cvMean = cvScores.reduce((a, b) => a + b, 0) / cvScores.length;477const cvStd = Math.sqrt(cvScores.reduce((sum, s) => sum + (s - cvMean) ** 2, 0) / cvScores.length);478479console.log(`\n CV Mean Accuracy: ${(cvMean * 100).toFixed(2)}% ± ${(cvStd * 100).toFixed(2)}%`);480481// ============================================================================482// Step 6: Confusion Matrix Analysis483// ============================================================================484485console.log("\n📊 STEP 6: Confusion Matrix Analysis");486console.log("─".repeat(70));487488// Re-train best model for detailed analysis489let analysisModel:490 | LogisticRegression491 | DecisionTreeClassifier492 | RandomForestClassifier493 | GradientBoostingClassifier494 | KNeighborsClassifier495 | GaussianNB;496if (bestModelType === "Random Forest") {497 analysisModel = new RandomForestClassifier({498 nEstimators: 50,499 maxDepth: 5,500 randomState: RANDOM_STATE,501 });502} else if (bestModelType === "Gradient Boosting") {503 analysisModel = new GradientBoostingClassifier({504 nEstimators: 50,505 maxDepth: 3,506 learningRate: 0.1,507 });508} else {509 analysisModel = new LogisticRegression({ maxIter: 100, learningRate: 0.1 });510}511512analysisModel.fit(XTrainScaled, yTrain);513const yPredFinal = analysisModel.predict(XTestScaled);514515const cm = confusionMatrix(yTest, yPredFinal);516const cmData = expectNumericTypedArray(cm.data);517518console.log("\nConfusion Matrix:");519console.log(" Predicted");520console.log(" Retained Churned");521console.log(522 ` Actual Retained ${String(cmData[0]).padStart(4)} ${String(cmData[1]).padStart(4)}`523);524console.log(525 ` Actual Churned ${String(cmData[2]).padStart(4)} ${String(cmData[3]).padStart(4)}`526);527528const tn = cmData[0];529const fp = cmData[1];530const fn = cmData[2];531const tp = cmData[3];532533console.log(`\n True Negatives: ${tn} (correctly predicted retained)`);534console.log(` False Positives: ${fp} (incorrectly predicted churned)`);535console.log(` False Negatives: ${fn} (missed churns)`);536console.log(` True Positives: ${tp} (correctly predicted churned)`);537538// Business metrics539const detectionRate = tp / (tp + fn);540const falseAlarmRate = fp / (fp + tn);541542console.log(`\nBusiness Metrics:`);543console.log(` Churn Detection Rate: ${(detectionRate * 100).toFixed(1)}%`);544console.log(` False Alarm Rate: ${(falseAlarmRate * 100).toFixed(1)}%`);545546// ============================================================================547// Step 7: Feature Importance (for tree-based models)548// ============================================================================549550console.log("\n🔍 STEP 7: Feature Importance Analysis");551console.log("─".repeat(70));552553// Train Random Forest for feature importance554const rfForImportance = new RandomForestClassifier({555 nEstimators: 100,556});557rfForImportance.fit(XTrainScaled, yTrain);558559const featureImportanceTensor = rfForImportance.featureImportances;560const featureImportanceData = expectNumericTypedArray(featureImportanceTensor.data);561const rankedFeatures = featureNames562 .map((name, index) => ({563 name,564 importance: Number(featureImportanceData[featureImportanceTensor.offset + index]),565 }))566 .sort((left, right) => right.importance - left.importance);567568console.log("\n Random Forest feature importances:");569for (const [index, feature] of rankedFeatures.slice(0, 5).entries()) {570 console.log(` ${index + 1}. ${feature.name} - importance=${feature.importance.toFixed(4)}`);571}572573// ============================================================================574// Step 8: Visualizations575// ============================================================================576577console.log("\n📊 STEP 8: Generating Visualizations");578console.log("─".repeat(70));579580// Model comparison bar chart581try {582 const fig = new Figure({ width: 800, height: 500 });583 const ax = fig.addAxes();584585 const modelNames = results.map((_, i) => i);586 const f1Scores = results.map((r) => r.f1 * 100);587588 ax.bar(tensor(modelNames), tensor(f1Scores), { color: "#4CAF50" });589 ax.setTitle("Model Comparison (F1 Score)");590 ax.setXLabel("Model");591 ax.setYLabel("F1 Score (%)");592593 const svg = fig.renderSVG();594 writeFileSync(`${OUTPUT_DIR}/model-comparison.svg`, svg.svg);595 console.log(` ✓ Saved: ${OUTPUT_DIR}/model-comparison.svg`);596} catch (e) {597 console.log(` ⚠ Could not generate model comparison plot: ${e}`);598}599600// Cross-validation scores plot601try {602 const fig = new Figure({ width: 800, height: 400 });603 const ax = fig.addAxes();604605 const folds = cvScores.map((_, i) => i + 1);606 ax.bar(tensor(folds), tensor(cvScores.map((s) => s * 100)), {607 color: "#2196F3",608 });609 ax.setTitle("Cross-Validation Scores");610 ax.setXLabel("Fold");611 ax.setYLabel("Accuracy (%)");612613 const svg = fig.renderSVG();614 writeFileSync(`${OUTPUT_DIR}/cv-scores.svg`, svg.svg);615 console.log(` ✓ Saved: ${OUTPUT_DIR}/cv-scores.svg`);616} catch (e) {617 console.log(` ⚠ Could not generate CV scores plot: ${e}`);618}619620// ============================================================================621// Step 9: Summary and Recommendations622// ============================================================================623624console.log(`\n${"═".repeat(70)}`);625console.log(" ANALYSIS COMPLETE - SUMMARY");626console.log("═".repeat(70));627628console.log("\n📌 Key Findings:\n");629console.log(" 1. Dataset Overview:");630console.log(` • ${NUM_SAMPLES} customers analyzed`);631console.log(` • ${((numChurned / NUM_SAMPLES) * 100).toFixed(1)}% churn rate`);632633console.log("\n 2. Best Performing Model:");634console.log(` • ${bestModel.name}`);635console.log(` • Accuracy: ${(bestModel.accuracy * 100).toFixed(2)}%`);636console.log(` • F1 Score: ${(bestModel.f1 * 100).toFixed(2)}%`);637console.log(` • CV Score: ${(cvMean * 100).toFixed(2)}% ± ${(cvStd * 100).toFixed(2)}%`);638639console.log("\n 3. Business Impact:");640console.log(` • Can detect ${(detectionRate * 100).toFixed(1)}% of churning customers`);641console.log(` • False alarm rate: ${(falseAlarmRate * 100).toFixed(1)}%`);642643console.log("\n💡 Recommendations:");644console.log(" • Focus retention efforts on customers with:");645console.log(" - Low satisfaction scores");646console.log(" - No contract");647console.log(" - High support call frequency");648console.log(" • Consider ensemble methods for production deployment");649console.log(" • Implement model monitoring for drift detection");650651console.log("\n📁 Output Files:");652console.log(` • ${OUTPUT_DIR}/model-comparison.svg`);653console.log(` • ${OUTPUT_DIR}/cv-scores.svg`);654655console.log(`\n${"═".repeat(70)}`);656console.log(" ✅ Customer Churn Prediction Complete!");657console.log("═".repeat(70));658Console Output
$ npx tsx 03-customer-churn-prediction/index.ts
Model comparison table
Confusion matrix summaries
Interpretability discussion (domain notes, not model-derived importances)Key Takeaways
- Multiple Models: Logistic Regression, Decision Tree, Random Forest, Gradient Boosting, KNN, Gaussian Naive Bayes
- Feature Engineering: Synthetic customer data generation
- Cross-Validation: K-Fold validation for robust evaluation
- Model Comparison: Comprehensive metrics comparison
- Use deepbox/ml for LogisticRegression, DecisionTreeClassifier, RandomForestClassifier, GradientBoostingClassifier, KNeighborsClassifier, GaussianNB.
- Use deepbox/preprocess for StandardScaler, trainTestSplit, KFold.