Preprocessing
ML
Metrics
DataFrame
Visualization
Support Ticket Triage
A production-style text operations pipeline for support routing, using the v1.0.0 text preprocessing stack, model selection helpers, and reporting outputs. It combines deepbox/preprocess, deepbox/ml, deepbox/metrics, deepbox/dataframe, deepbox/plot, deepbox/ndarray to deliver a larger production-style Deepbox workflow with reproducible outputs and documented architecture.
Features
- Synthetic ticket stream with priority, channel, team, and free-text issue descriptions
- Text vectorization with `CountVectorizer`, `TfidfVectorizer`, and `HashingVectorizer`
- Model selection with `GridSearchCV`
- Classifier comparison across logistic regression and Naive Bayes baselines
- Operational reporting via DataFrame summaries and exported JSON artifacts
- Visualization with a labeled confusion matrix
Deepbox Modules Used
deepbox/preprocessdeepbox/mldeepbox/metricsdeepbox/dataframedeepbox/plotdeepbox/ndarrayProject Architecture
- 08-support-ticket-triage/
- ├── index.ts
- ├── README.md
- └── output/
Source Files
index.ts
1/**2 * Support Ticket Triage3 *4 * A production-style NLP operations project built with Deepbox text5 * preprocessing, classical ML, model selection, and reporting utilities.6 */78import { mkdir } from "node:fs/promises";9import { DataFrame } from "deepbox/dataframe";10import { accuracy, confusionMatrix, f1Score } from "deepbox/metrics";11import { cross_validate, GridSearchCV, LogisticRegression, MultinomialNB } from "deepbox/ml";12import { tensor } from "deepbox/ndarray";13import { figure, plotConfusionMatrix, saveFig } from "deepbox/plot";14import { CountVectorizer, HashingVectorizer, TfidfVectorizer } from "deepbox/preprocess";1516const OUTPUT_DIR = "docs/projects/08-support-ticket-triage/output";17const RANDOM_SEED = 42;18const NUM_TICKETS = 640;1920type TicketLabel = 0 | 1 | 2 | 3;2122type TicketRecord = {23 readonly ticketId: string;24 readonly channel: string;25 readonly priority: string;26 readonly team: string;27 readonly text: string;28 readonly label: TicketLabel;29};3031const LABEL_NAMES = ["billing", "account_access", "outage", "bug_report"];3233function createRng(seed: number): () => number {34 let state = seed >>> 0;35 return () => {36 state = (1664525 * state + 1013904223) >>> 0;37 return state / 0x100000000;38 };39}4041function randomChoice<T>(rng: () => number, values: readonly T[]): T {42 return values[Math.floor(rng() * values.length)] ?? values[0]!;43}4445function generateTicket(label: TicketLabel, index: number, rng: () => number): TicketRecord {46 const billingPhrases = [47 "invoice charged twice after renewal",48 "refund not reflected on my card statement",49 "subscription amount is different from quote",50 "billing cycle changed without approval",51 ];52 const accessPhrases = [53 "cannot sign in after password reset",54 "mfa code keeps failing on mobile",55 "admin locked out of workspace",56 "sso login redirects to blank page",57 ];58 const outagePhrases = [59 "dashboard returns 503 for all users",60 "api latency is above ten seconds",61 "production sync jobs are stalled",62 "incident bridge opened for regional outage",63 ];64 const bugPhrases = [65 "export csv button freezes the browser",66 "search filters ignore the selected workspace",67 "notification settings save the wrong value",68 "audit log view crashes on pagination",69 ];70 const urgencyPhrases = [71 "urgent customer escalation",72 "sev1 impact to executives",73 "please prioritize this today",74 "blocked for go-live launch",75 ];76 const contexts = [77 "enterprise contract renewal",78 "migration cutover",79 "month-end finance close",80 "on-call support shift",81 "new customer onboarding",82 ];83 const channels = ["email", "chat", "portal"];84 const priorities = ["p1", "p2", "p3"];8586 const baseText =87 label === 088 ? randomChoice(rng, billingPhrases)89 : label === 190 ? randomChoice(rng, accessPhrases)91 : label === 292 ? randomChoice(rng, outagePhrases)93 : randomChoice(rng, bugPhrases);9495 const urgent = rng() < (label === 2 ? 0.55 : 0.18);96 const priority = urgent ? "p1" : randomChoice(rng, priorities);97 const channel = urgent ? "chat" : randomChoice(rng, channels);98 const team =99 label === 0100 ? "revenue-ops"101 : label === 1102 ? "identity"103 : label === 2104 ? "sre"105 : "product-engineering";106107 const text = [108 baseText,109 randomChoice(rng, contexts),110 urgent ? randomChoice(rng, urgencyPhrases) : "customer requested an update by end of day",111 ].join(". ");112113 return {114 ticketId: `T-${String(index + 1).padStart(4, "0")}`,115 channel,116 priority,117 team,118 text,119 label,120 };121}122123function generateTickets(n: number, seed: number): TicketRecord[] {124 const rng = createRng(seed);125 const records: TicketRecord[] = [];126 const labelWeights: TicketLabel[] = [0, 0, 0, 1, 1, 2, 2, 3, 3, 3];127128 for (let i = 0; i < n; i++) {129 const label = randomChoice(rng, labelWeights);130 records.push(generateTicket(label, i, rng));131 }132133 return records;134}135136function splitArray<T>(values: readonly T[], testFraction: number): [T[], T[]] {137 const splitIndex = Math.floor(values.length * (1 - testFraction));138 return [values.slice(0, splitIndex), values.slice(splitIndex)];139}140141console.log("═".repeat(72));142console.log(" SUPPORT TICKET TRIAGE");143console.log(" Deepbox v1.0.0 production example");144console.log("═".repeat(72));145146await mkdir(OUTPUT_DIR, { recursive: true });147148const tickets = generateTickets(NUM_TICKETS, RANDOM_SEED);149const ticketFrame = new DataFrame({150 ticketId: tickets.map((row) => row.ticketId),151 channel: tickets.map((row) => row.channel),152 priority: tickets.map((row) => row.priority),153 team: tickets.map((row) => row.team),154 text: tickets.map((row) => row.text),155 label: tickets.map((row) => LABEL_NAMES[row.label]),156});157158// ============================================================================159// Step 1: Operations view160// ============================================================================161console.log("\n📨 STEP 1: Ticket Operations View");162console.log("─".repeat(72));163164const urgentMask = ticketFrame.get("text").str.contains("urgent|sev1|prioritize|blocked");165let urgentCount = 0;166for (const value of urgentMask.data) {167 if (value === true) {168 urgentCount++;169 }170}171172console.log(`Tickets generated: ${ticketFrame.shape[0]}`);173console.log(`Urgent/escalated tickets: ${urgentCount}`);174console.log("Average priority mix by team:");175console.log(176 new DataFrame({177 team: tickets.map((row) => row.team),178 priorityCode: tickets.map((row) => (row.priority === "p1" ? 3 : row.priority === "p2" ? 2 : 1)),179 })180 .groupBy("team")181 .mean()182 .toString()183);184185// ============================================================================186// Step 2: Train/test text split187// ============================================================================188console.log("\n🧰 STEP 2: Vectorization + Model Selection");189console.log("─".repeat(72));190191const texts = tickets.map((row) => row.text);192const labels = tickets.map((row) => row.label);193194const [trainTexts, testTexts] = splitArray(texts, 0.2);195const [trainLabels, testLabels] = splitArray(labels, 0.2);196197const yTrain = tensor(trainLabels);198const yTest = tensor(testLabels);199200const countVectorizer = new CountVectorizer({201 ngramRange: [1, 2],202 maxFeatures: 400,203 minDf: 2,204});205const tfidfVectorizer = new TfidfVectorizer({206 ngramRange: [1, 2],207 maxFeatures: 400,208 minDf: 2,209});210const hashingVectorizer = new HashingVectorizer({211 nFeatures: 256,212 ngramRange: [1, 2],213 alternateSign: false,214});215216const XTrainCount = countVectorizer.fitTransformText(trainTexts);217const XTestCount = countVectorizer.transformText(testTexts);218const XTrainTfidf = tfidfVectorizer.fitTransformText(trainTexts);219const XTestTfidf = tfidfVectorizer.transformText(testTexts);220const XTrainHash = hashingVectorizer.fitTransformText(trainTexts);221const XTestHash = hashingVectorizer.transformText(testTexts);222223const ticketSearch = new GridSearchCV(224 new LogisticRegression({ maxIter: 300 }),225 {226 C: [0.5, 1.0, 2.0, 4.0],227 maxIter: [250, 350],228 },229 { cv: 4 }230);231ticketSearch.fit(XTrainTfidf, yTrain);232233const nbModel = new MultinomialNB();234nbModel.fit(XTrainCount, yTrain);235236const hashModel = new LogisticRegression({ C: 1.0, maxIter: 300 });237hashModel.fit(XTrainHash, yTrain);238239const bestLogistic = ticketSearch.bestEstimator as LogisticRegression;240const logisticPred = bestLogistic.predict(XTestTfidf);241const nbPred = nbModel.predict(XTestCount);242const hashPred = hashModel.predict(XTestHash);243244const comparisonRows = [245 {246 pipeline: "TF-IDF + GridSearchCV(LogReg)",247 accuracy: Number(accuracy(yTest, logisticPred)),248 weightedF1: Number(f1Score(yTest, logisticPred, "weighted")),249 predictions: logisticPred,250 },251 {252 pipeline: "Count + MultinomialNB",253 accuracy: Number(accuracy(yTest, nbPred)),254 weightedF1: Number(f1Score(yTest, nbPred, "weighted")),255 predictions: nbPred,256 },257 {258 pipeline: "Hashing + LogisticRegression",259 accuracy: Number(accuracy(yTest, hashPred)),260 weightedF1: Number(f1Score(yTest, hashPred, "weighted")),261 predictions: hashPred,262 },263];264265comparisonRows.sort((left, right) => right.weightedF1 - left.weightedF1);266267const bestPipeline = comparisonRows[0]!;268269const results = new DataFrame({270 pipeline: comparisonRows.map((row) => row.pipeline),271 accuracy: comparisonRows.map((row) => row.accuracy.toFixed(4)),272 weightedF1: comparisonRows.map((row) => row.weightedF1.toFixed(4)),273});274275console.log(`Best logistic params: ${JSON.stringify(ticketSearch.bestParams)}`);276console.log(`Best deployable pipeline: ${bestPipeline.pipeline}`);277console.log(results.toString());278279const cv = cross_validate(bestLogistic, XTrainTfidf, yTrain, { cv: 4 });280const cvScores = cv.testScores["score"] ?? [];281console.log(`Cross-validation scores: ${cvScores.map((score) => score.toFixed(4)).join(", ")}`);282283// ============================================================================284// Step 3: Confusion matrix and artifacts285// ============================================================================286console.log("\n📈 STEP 3: Reporting Outputs");287console.log("─".repeat(72));288289const confusion = confusionMatrix(yTest, bestPipeline.predictions);290const confusionFigure = figure({ width: 720, height: 520 });291plotConfusionMatrix(confusion, LABEL_NAMES, {292 color: "#1d4ed8",293});294await saveFig(`${OUTPUT_DIR}/best-model-confusion-matrix.svg`, {295 figure: confusionFigure,296 format: "svg",297});298299await results.toJson(`${OUTPUT_DIR}/model-comparison.json`);300301const vocabularyPreview = new DataFrame({302 topTerms: tfidfVectorizer.getFeatureNames().slice(0, 20),303});304await vocabularyPreview.toJson(`${OUTPUT_DIR}/tfidf-vocabulary-preview.json`);305306console.log(`Saved confusion matrix: ${OUTPUT_DIR}/best-model-confusion-matrix.svg`);307console.log(`Saved model summary: ${OUTPUT_DIR}/model-comparison.json`);308console.log(`Saved vocabulary dump: ${OUTPUT_DIR}/tfidf-vocabulary-preview.json`);309310console.log("\n✅ Support Ticket Triage Complete!");311Console Output
$ npx tsx 08-support-ticket-triage/index.ts
`output/best-model-confusion-matrix.svg`
`output/model-comparison.json`
`output/tfidf-vocabulary-preview.json`Key Takeaways
- Synthetic ticket stream with priority, channel, team, and free-text issue descriptions
- Text vectorization with `CountVectorizer`, `TfidfVectorizer`, and `HashingVectorizer`
- Model selection with `GridSearchCV`
- Classifier comparison across logistic regression and Naive Bayes baselines
- Use deepbox/preprocess for CountVectorizer, TfidfVectorizer, HashingVectorizer.
- Use deepbox/ml for LogisticRegression, MultinomialNB, GridSearchCV, cross_validate.