Project 07
ML
Preprocessing
Metrics
DataFrame
Visualization

Fraud Detection Platform

A production-style transaction monitoring workflow built around the biggest new v1.0.0 additions: calibrated classification, anomaly detection, feature inspection, and operations-ready reporting. It combines deepbox/ml, deepbox/preprocess, deepbox/metrics, deepbox/dataframe, deepbox/plot, deepbox/ndarray to deliver a larger production-style Deepbox workflow with reproducible outputs and documented architecture.

Features

  • Synthetic transaction stream with merchant, channel, country, velocity, and device-risk signals
  • Supervised fraud scoring using `LogisticRegression`
  • Probability calibration with `CalibratedClassifierCV`
  • Unsupervised anomaly detection with `IsolationForest`, `LocalOutlierFactor`, and `OneClassSVM`
  • Inspection via `permutationImportance`
  • Operational reporting with DataFrame querying, grouping, and JSON export

Deepbox Modules Used

deepbox/mldeepbox/preprocessdeepbox/metricsdeepbox/dataframedeepbox/plotdeepbox/ndarray

Project Architecture

  • 07-fraud-detection-platform/
  • ├── index.ts
  • ├── README.md
  • └── output/

Source Files

index.ts
1/**2 * Fraud Detection Platform3 *4 * A production-style risk scoring workflow that combines supervised fraud5 * classification, probability calibration, anomaly detection, feature6 * inspection, and operational reporting in one Deepbox project.7 */89import { mkdir } from "node:fs/promises";10import { DataFrame, to_datetime } from "deepbox/dataframe";11import { accuracy, f1Score, precision, recall } from "deepbox/metrics";12import {13  CalibratedClassifierCV,14  calibrationCurve,15  IsolationForest,16  LocalOutlierFactor,17  LogisticRegression,18  OneClassSVM,19  permutationImportance,20} from "deepbox/ml";21import { type Tensor, tensor } from "deepbox/ndarray";22import { figure, plotCalibrationCurve, plotFeatureImportance, saveFig } from "deepbox/plot";23import { StandardScaler, trainTestSplit } from "deepbox/preprocess";2425const OUTPUT_DIR = "docs/projects/07-fraud-detection-platform/output";26const RANDOM_SEED = 42;27const NUM_TRANSACTIONS = 1_500;2829type TransactionRecord = {30  readonly timestamp: string;31  readonly merchantCategory: string;32  readonly channel: string;33  readonly country: string;34  readonly amount: number;35  readonly hourOfDay: number;36  readonly velocity30m: number;37  readonly geoRisk: number;38  readonly deviceRisk: number;39  readonly merchantRisk: number;40  readonly chargebackHistory: number;41  readonly accountAgeDays: number;42  readonly isFraud: number;43};4445function createRng(seed: number): () => number {46  let state = seed >>> 0;47  return () => {48    state = (1664525 * state + 1013904223) >>> 0;49    return state / 0x100000000;50  };51}5253function randomNormal(rng: () => number, mean: number, std: number): number {54  const u1 = Math.max(rng(), 1e-12);55  const u2 = rng();56  const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);57  return mean + std * z;58}5960function generateTransactions(n: number, seed: number): TransactionRecord[] {61  const rng = createRng(seed);62  const merchantCategories = ["electronics", "travel", "luxury", "groceries", "gaming"];63  const channels = ["web", "mobile", "call-center"];64  const countries = ["SA", "AE", "EG", "GB", "US"];65  const baseDate = new Date("2026-02-01T00:00:00.000Z").getTime();6667  const records: TransactionRecord[] = [];6869  for (let i = 0; i < n; i++) {70    const merchantCategory =71      merchantCategories[Math.floor(rng() * merchantCategories.length)] ?? "groceries";72    const channel = channels[Math.floor(rng() * channels.length)] ?? "web";73    const country = countries[Math.floor(rng() * countries.length)] ?? "SA";74    const hourOfDay = Math.floor(rng() * 24);7576    let amount = Math.max(8, randomNormal(rng, 180, 120));77    let velocity30m = Math.max(1, Math.round(randomNormal(rng, 2.5, 1.8)));78    let geoRisk = Math.max(0, Math.min(1, rng() * 0.7));79    let deviceRisk = Math.max(0, Math.min(1, rng() * 0.6));80    const merchantRisk =81      merchantCategory === "travel" || merchantCategory === "luxury"82        ? 0.45 + rng() * 0.4583        : 0.05 + rng() * 0.45;84    const chargebackHistory = Math.max(0, Math.round(randomNormal(rng, 0.5, 1.2)));85    let accountAgeDays = Math.max(2, Math.round(randomNormal(rng, 420, 260)));8687    let fraudScore = 0.015;88    fraudScore += amount > 650 ? 0.18 : 0;89    fraudScore += hourOfDay < 5 ? 0.12 : 0;90    fraudScore += velocity30m > 5 ? 0.18 : 0;91    fraudScore += geoRisk > 0.65 ? 0.16 : 0;92    fraudScore += deviceRisk > 0.55 ? 0.12 : 0;93    fraudScore += merchantRisk > 0.65 ? 0.09 : 0;94    fraudScore += chargebackHistory > 1 ? 0.14 : 0;95    fraudScore += accountAgeDays < 45 ? 0.2 : 0;9697    if (rng() < 0.08) {98      amount *= 2.5;99      velocity30m += 4;100      geoRisk = Math.min(1, geoRisk + 0.35);101      deviceRisk = Math.min(1, deviceRisk + 0.35);102      accountAgeDays = Math.max(2, Math.round(accountAgeDays * 0.12));103      fraudScore += 0.28;104    }105106    const isFraud = rng() < Math.min(fraudScore, 0.94) ? 1 : 0;107    const timestamp = new Date(108      baseDate + i * 45 * 60 * 1000 + hourOfDay * 60 * 60 * 1000109    ).toISOString();110111    records.push({112      timestamp,113      merchantCategory,114      channel,115      country,116      amount: Number(amount.toFixed(2)),117      hourOfDay,118      velocity30m,119      geoRisk: Number(geoRisk.toFixed(3)),120      deviceRisk: Number(deviceRisk.toFixed(3)),121      merchantRisk: Number(merchantRisk.toFixed(3)),122      chargebackHistory,123      accountAgeDays,124      isFraud,125    });126  }127128  return records;129}130131function buildFeatureTensor(records: readonly TransactionRecord[]): Tensor {132  return tensor(133    records.map((row) => [134      row.amount,135      row.hourOfDay,136      row.velocity30m,137      row.geoRisk,138      row.deviceRisk,139      row.merchantRisk,140      row.chargebackHistory,141      row.accountAgeDays,142    ])143  );144}145146function buildLabelTensor(records: readonly TransactionRecord[]): Tensor {147  return tensor(records.map((row) => row.isFraud));148}149150function selectRows(X: Tensor, y: Tensor, label: number): Tensor {151  const rows: number[][] = [];152  for (let i = 0; i < (y.shape[0] ?? 0); i++) {153    if (Number(y.at(i)) !== label) {154      continue;155    }156    rows.push(Array.from({ length: X.shape[1] ?? 0 }, (_, j) => Number(X.at(i, j))));157  }158  return tensor(rows);159}160161function positiveProbabilities(probabilities: Tensor): Tensor {162  return tensor(163    Array.from({ length: probabilities.shape[0] ?? 0 }, (_, i) => Number(probabilities.at(i, 1)))164  );165}166167function thresholdPredictions(probabilities: Tensor, threshold: number): Tensor {168  return tensor(169    Array.from({ length: probabilities.shape[0] ?? 0 }, (_, i) =>170      Number(probabilities.at(i)) >= threshold ? 1 : 0171    )172  );173}174175function anomalyRecall(yTrue: Tensor, predictedLabels: Tensor): number {176  let fraudCount = 0;177  let flaggedFraudCount = 0;178179  for (let i = 0; i < (yTrue.shape[0] ?? 0); i++) {180    const actual = Number(yTrue.at(i));181    const predicted = Number(predictedLabels.at(i));182    if (actual === 1) {183      fraudCount++;184      if (predicted === -1) {185        flaggedFraudCount++;186      }187    }188  }189190  return fraudCount === 0 ? 0 : flaggedFraudCount / fraudCount;191}192193function meanProbabilityByLabel(yTrue: Tensor, probabilities: Tensor, label: number): number {194  let sum = 0;195  let count = 0;196  for (let i = 0; i < (yTrue.shape[0] ?? 0); i++) {197    if (Number(yTrue.at(i)) !== label) {198      continue;199    }200    sum += Number(probabilities.at(i));201    count++;202  }203  return count === 0 ? 0 : sum / count;204}205206console.log("═".repeat(72));207console.log("  FRAUD DETECTION PLATFORM");208console.log("  Deepbox v1.0.0 production example");209console.log("═".repeat(72));210211await mkdir(OUTPUT_DIR, { recursive: true });212213const records = generateTransactions(NUM_TRANSACTIONS, RANDOM_SEED);214const transactions = new DataFrame({215  timestamp: records.map((row) => row.timestamp),216  merchantCategory: records.map((row) => row.merchantCategory),217  channel: records.map((row) => row.channel),218  country: records.map((row) => row.country),219  amount: records.map((row) => row.amount),220  hourOfDay: records.map((row) => row.hourOfDay),221  velocity30m: records.map((row) => row.velocity30m),222  geoRisk: records.map((row) => row.geoRisk),223  deviceRisk: records.map((row) => row.deviceRisk),224  merchantRisk: records.map((row) => row.merchantRisk),225  chargebackHistory: records.map((row) => row.chargebackHistory),226  accountAgeDays: records.map((row) => row.accountAgeDays),227  isFraud: records.map((row) => row.isFraud),228});229230// ============================================================================231// Step 1: Operations reporting232// ============================================================================233console.log("\n📊 STEP 1: Operational Reporting");234console.log("─".repeat(72));235236const parsedTimestamps = to_datetime(records.map((row) => row.timestamp));237const nightTransactions = transactions.query("hourOfDay < 5 and amount > 700");238const fraudShare = records.reduce((sum, row) => sum + row.isFraud, 0) / records.length;239240console.log(`Transactions generated: ${records.length}`);241console.log(`Fraud rate: ${(fraudShare * 100).toFixed(2)}%`);242console.log(`Parsed timestamps: ${parsedTimestamps.data.length}`);243console.log(`High-value night transactions: ${nightTransactions.shape[0]}`);244console.log("Merchant category means for frauds only:");245console.log(transactions.query("isFraud == 1").groupBy("merchantCategory").mean().toString());246247// ============================================================================248// Step 2: Supervised classifier + calibration249// ============================================================================250console.log("\n🛡️  STEP 2: Supervised Fraud Scoring");251console.log("─".repeat(72));252253const featureNames = [254  "amount",255  "hourOfDay",256  "velocity30m",257  "geoRisk",258  "deviceRisk",259  "merchantRisk",260  "chargebackHistory",261  "accountAgeDays",262];263264const X = buildFeatureTensor(records);265const y = buildLabelTensor(records);266267const [XTrain, XTest, yTrain, yTest] = trainTestSplit(X, y, {268  testSize: 0.25,269  randomState: RANDOM_SEED,270});271272const scaler = new StandardScaler();273const XTrainScaled = scaler.fitTransform(XTrain);274const XTestScaled = scaler.transform(XTest);275276const baseModel = new LogisticRegression({277  C: 3.5,278  maxIter: 450,279});280baseModel.fit(XTrainScaled, yTrain);281282const calibratedModel = new CalibratedClassifierCV({283  estimator: new LogisticRegression({284    C: 3.5,285    maxIter: 450,286  }),287  method: "sigmoid",288  cv: 4,289});290calibratedModel.fit(XTrainScaled, yTrain);291292const reviewThreshold = 0.18;293const baseProbabilities = positiveProbabilities(baseModel.predictProba(XTestScaled));294const calibratedProba = positiveProbabilities(calibratedModel.predictProba(XTestScaled));295const basePred = thresholdPredictions(baseProbabilities, reviewThreshold);296297console.log(`Decision threshold: ${reviewThreshold.toFixed(2)} (tuned for fraud review queues)`);298console.log(299  `Base LogisticRegression -> accuracy=${(Number(accuracy(yTest, basePred)) * 100).toFixed(2)}%, precision=${Number(precision(yTest, basePred)).toFixed(4)}, recall=${Number(recall(yTest, basePred)).toFixed(4)}, f1=${Number(f1Score(yTest, basePred)).toFixed(4)}`300);301console.log(302  `Calibrated probabilities -> mean(fraud)=${meanProbabilityByLabel(yTest, calibratedProba, 1).toFixed(4)}, mean(clean)=${meanProbabilityByLabel(yTest, calibratedProba, 0).toFixed(4)}`303);304305// ============================================================================306// Step 3: Unsupervised anomaly models307// ============================================================================308console.log("\n🕵️  STEP 3: Unsupervised Anomaly Detectors");309console.log("─".repeat(72));310311const normalTrain = selectRows(XTrainScaled, yTrain, 0);312313const isolationForest = new IsolationForest({314  nEstimators: 120,315  contamination: fraudShare,316  randomState: RANDOM_SEED,317});318isolationForest.fit(normalTrain);319320const lof = new LocalOutlierFactor({321  nNeighbors: 12,322  contamination: fraudShare,323});324lof.fit(normalTrain);325326const oneClass = new OneClassSVM({327  nu: Math.min(Math.max(fraudShare * 1.2, 0.05), 0.35),328  kernel: "rbf",329  gamma: "scale",330});331oneClass.fit(normalTrain);332333const iforestPred = isolationForest.predict(XTestScaled);334const lofPred = lof.predict(XTestScaled);335const oneClassPred = oneClass.predict(XTestScaled);336337console.log(338  `IsolationForest fraud recall: ${(anomalyRecall(yTest, iforestPred) * 100).toFixed(2)}%`339);340console.log(`LocalOutlierFactor recall:    ${(anomalyRecall(yTest, lofPred) * 100).toFixed(2)}%`);341console.log(342  `OneClassSVM recall:           ${(anomalyRecall(yTest, oneClassPred) * 100).toFixed(2)}%`343);344345// ============================================================================346// Step 4: Inspection + visualization outputs347// ============================================================================348console.log("\n📈 STEP 4: Inspection & Outputs");349console.log("─".repeat(72));350351const reliability = calibrationCurve(yTest, calibratedProba, { nBins: 8 });352const importance = permutationImportance(baseModel, XTestScaled, yTest, {353  nRepeats: 6,354  randomState: RANDOM_SEED,355});356357const calibrationFigure = figure({ width: 760, height: 520 });358plotCalibrationCurve(tensor(reliability.fractionPositives), tensor(reliability.meanPredicted), {359  color: "#0f766e",360  label: "Fraud model",361});362await saveFig(`${OUTPUT_DIR}/calibration-curve.svg`, {363  figure: calibrationFigure,364  format: "svg",365});366367const importanceFigure = figure({ width: 860, height: 520 });368plotFeatureImportance(importance.importancesMean, featureNames, {369  color: "#1d4ed8",370});371await saveFig(`${OUTPUT_DIR}/feature-importance.svg`, {372  figure: importanceFigure,373  format: "svg",374});375376const scoreReport = new DataFrame({377  model: ["LogisticRegression", "IsolationForest", "LocalOutlierFactor", "OneClassSVM"],378  primaryMetric: [379    Number(f1Score(yTest, basePred)).toFixed(4),380    anomalyRecall(yTest, iforestPred).toFixed(4),381    anomalyRecall(yTest, lofPred).toFixed(4),382    anomalyRecall(yTest, oneClassPred).toFixed(4),383  ],384  metricName: ["F1", "Fraud recall", "Fraud recall", "Fraud recall"],385});386387const reportPath = `${OUTPUT_DIR}/model-report.json`;388await scoreReport.toJson(reportPath);389390console.log(`Saved calibration curve: ${OUTPUT_DIR}/calibration-curve.svg`);391console.log(`Saved feature chart:     ${OUTPUT_DIR}/feature-importance.svg`);392console.log(`Saved JSON report:       ${reportPath}`);393394console.log("\n✅ Fraud Detection Platform Complete!");395

Console Output

$ npx tsx 07-fraud-detection-platform/index.ts
`output/calibration-curve.svg`
`output/feature-importance.svg`
`output/model-report.json`

Key Takeaways

  • Synthetic transaction stream with merchant, channel, country, velocity, and device-risk signals
  • Supervised fraud scoring using `LogisticRegression`
  • Probability calibration with `CalibratedClassifierCV`
  • Unsupervised anomaly detection with `IsolationForest`, `LocalOutlierFactor`, and `OneClassSVM`
  • Use deepbox/ml for LogisticRegression, CalibratedClassifierCV, IsolationForest, LocalOutlierFactor, OneClassSVM, calibrationCurve, permutationImportance.
  • Use deepbox/preprocess for trainTestSplit, StandardScaler.