Project 08
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/ndarray

Project 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!");311

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