Project 03
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/plot

Project 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));658

Console 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.