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/ndarrayProject 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!");395Console 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.