Project 06
ML
Preprocessing
Metrics
DataFrame

Sentiment Analysis System

A text classification system for sentiment analysis demonstrating NLP preprocessing and classification. It combines deepbox/ml, deepbox/preprocess, deepbox/metrics, deepbox/dataframe to deliver a larger production-style Deepbox workflow with reproducible outputs and documented architecture.

Features

  • Text Preprocessing: Tokenization, TF-IDF vectorization
  • Classification: Logistic Regression, Naive Bayes
  • Evaluation: Classification metrics, confusion matrix

Deepbox Modules Used

deepbox/mldeepbox/preprocessdeepbox/metricsdeepbox/dataframe

Project Architecture

    Source Files

    index.ts
    1/**2 * Sentiment Analysis System3 *4 * Text classification for sentiment analysis using bag-of-words and ML classifiers.5 *6 * Deepbox Modules Used:7 * - deepbox/ml: LogisticRegression, GaussianNB8 * - deepbox/preprocess: StandardScaler, trainTestSplit9 * - deepbox/metrics: Classification metrics10 * - deepbox/dataframe: Data manipulation11 * - deepbox/ndarray: Tensor operations12 */1314import { existsSync, mkdirSync, writeFileSync } from "node:fs";15import { isNumericTypedArray, isTypedArray } from "deepbox/core";16import { DataFrame } from "deepbox/dataframe";17import { accuracy, confusionMatrix, f1Score, precision, recall } from "deepbox/metrics";18import { GaussianNB, LogisticRegression } from "deepbox/ml";19import { tensor } from "deepbox/ndarray";20import { Figure } from "deepbox/plot";21import { StandardScaler, trainTestSplit } from "deepbox/preprocess";2223const expectNumericTypedArray = (24  value: unknown25): Float32Array | Float64Array | Int32Array | Uint8Array => {26  if (!isTypedArray(value) || !isNumericTypedArray(value)) {27    throw new Error("Expected numeric typed array");28  }29  return value;30};3132// ============================================================================33// Configuration34// ============================================================================3536const OUTPUT_DIR = "docs/projects/06-sentiment-analysis/output";37const NUM_SAMPLES = 500;38const VOCAB_SIZE = 100;3940// ============================================================================41// Data Generation & Text Processing42// ============================================================================4344/**45 * Sample positive and negative word lists for sentiment46 */47const POSITIVE_WORDS = [48  "good",49  "great",50  "excellent",51  "amazing",52  "wonderful",53  "fantastic",54  "awesome",55  "love",56  "like",57  "enjoy",58  "happy",59  "pleased",60  "satisfied",61  "recommend",62  "best",63  "perfect",64  "brilliant",65  "outstanding",66  "superb",67  "delightful",68  "impressive",69  "beautiful",70  "incredible",71  "exceptional",72  "remarkable",73  "terrific",74  "fabulous",75];7677const NEGATIVE_WORDS = [78  "bad",79  "terrible",80  "awful",81  "horrible",82  "poor",83  "worst",84  "hate",85  "dislike",86  "disappointed",87  "frustrating",88  "annoying",89  "waste",90  "boring",91  "slow",92  "broken",93  "useless",94  "pathetic",95  "dreadful",96  "disgusting",97  "miserable",98  "unpleasant",99  "disappointing",100  "inadequate",101  "inferior",102  "subpar",103  "mediocre",104  "defective",105];106107const NEUTRAL_WORDS = [108  "the",109  "a",110  "an",111  "is",112  "was",113  "it",114  "this",115  "that",116  "with",117  "for",118  "on",119  "product",120  "service",121  "item",122  "order",123  "delivery",124  "quality",125  "price",126  "time",127  "experience",128  "customer",129  "support",130  "would",131  "could",132  "should",133  "have",134  "been",135];136137/**138 * Generate synthetic review text139 */140function generateReview(isPositive: boolean, seed: number): { text: string; words: string[] } {141  let randomSeed = seed;142  const seededRandom = () => {143    randomSeed = (randomSeed * 1103515245 + 12345) & 0x7fffffff;144    return randomSeed / 0x7fffffff;145  };146147  const numWords = Math.floor(seededRandom() * 15) + 10; // 10-25 words148  const words: string[] = [];149150  const primaryWords = isPositive ? POSITIVE_WORDS : NEGATIVE_WORDS;151  const secondaryWords = isPositive ? NEGATIVE_WORDS : POSITIVE_WORDS;152153  for (let i = 0; i < numWords; i++) {154    const r = seededRandom();155    if (r < 0.3) {156      // 30% sentiment words (mostly matching sentiment)157      if (seededRandom() < 0.85) {158        words.push(primaryWords[Math.floor(seededRandom() * primaryWords.length)]);159      } else {160        words.push(secondaryWords[Math.floor(seededRandom() * secondaryWords.length)]);161      }162    } else {163      // 70% neutral words164      words.push(NEUTRAL_WORDS[Math.floor(seededRandom() * NEUTRAL_WORDS.length)]);165    }166  }167168  return { text: words.join(" "), words };169}170171/**172 * Build vocabulary from reviews173 */174function buildVocabulary(reviews: string[][], maxSize: number): string[] {175  const wordCounts: Map<string, number> = new Map();176177  for (const words of reviews) {178    for (const word of words) {179      wordCounts.set(word, (wordCounts.get(word) || 0) + 1);180    }181  }182183  // Sort by frequency and take top words184  const sorted = Array.from(wordCounts.entries())185    .sort((a, b) => b[1] - a[1])186    .slice(0, maxSize);187188  return sorted.map(([word]) => word);189}190191/**192 * Convert text to bag-of-words vector193 */194function textToVector(words: string[], vocabulary: string[]): number[] {195  const vector = new Array(vocabulary.length).fill(0);196197  for (const word of words) {198    const idx = vocabulary.indexOf(word);199    if (idx >= 0) {200      vector[idx]++;201    }202  }203204  return vector;205}206207/**208 * Calculate TF-IDF weights209 */210function calculateTFIDF(documents: number[][], vocabulary: string[]): number[][] {211  const numDocs = documents.length;212  const vocabSize = vocabulary.length;213214  // Calculate document frequency for each term215  const df = new Array(vocabSize).fill(0);216  for (const doc of documents) {217    for (let i = 0; i < vocabSize; i++) {218      if (doc[i] > 0) {219        df[i]++;220      }221    }222  }223224  // Calculate TF-IDF225  const tfidf: number[][] = [];226  for (const doc of documents) {227    const docLength = doc.reduce((a, b) => a + b, 0);228    const tfidfDoc: number[] = [];229230    for (let i = 0; i < vocabSize; i++) {231      const tf = docLength > 0 ? doc[i] / docLength : 0;232      const idf = df[i] > 0 ? Math.log(numDocs / df[i]) : 0;233      tfidfDoc.push(tf * idf);234    }235236    tfidf.push(tfidfDoc);237  }238239  return tfidf;240}241242// ============================================================================243// Main Execution244// ============================================================================245246console.log("═".repeat(70));247console.log("  SENTIMENT ANALYSIS SYSTEM");248console.log("  Built with Deepbox — TypeScript toolkit for AI & numerical computing");249console.log("═".repeat(70));250251// Create output directory252if (!existsSync(OUTPUT_DIR)) {253  mkdirSync(OUTPUT_DIR, { recursive: true });254}255256// ============================================================================257// Step 1: Generate Data258// ============================================================================259260console.log("\n📊 STEP 1: Generating Sentiment Data");261console.log("─".repeat(70));262263const reviews: { text: string; words: string[]; label: number }[] = [];264265for (let i = 0; i < NUM_SAMPLES; i++) {266  const isPositive = i < NUM_SAMPLES / 2;267  const { text, words } = generateReview(isPositive, i + 42);268  reviews.push({ text, words, label: isPositive ? 1 : 0 });269}270271// Shuffle reviews272for (let i = reviews.length - 1; i > 0; i--) {273  const j = Math.floor(Math.random() * (i + 1));274  [reviews[i], reviews[j]] = [reviews[j], reviews[i]];275}276277const numPositive = reviews.filter((r) => r.label === 1).length;278const numNegative = reviews.filter((r) => r.label === 0).length;279280console.log(`\n✓ Generated ${NUM_SAMPLES} reviews`);281console.log(`  Positive: ${numPositive} (${((numPositive / NUM_SAMPLES) * 100).toFixed(1)}%)`);282console.log(`  Negative: ${numNegative} (${((numNegative / NUM_SAMPLES) * 100).toFixed(1)}%)`);283284// Sample reviews285console.log("\nSample Reviews:");286console.log(`  [Positive] "${reviews.find((r) => r.label === 1)?.text.slice(0, 60)}..."`);287console.log(`  [Negative] "${reviews.find((r) => r.label === 0)?.text.slice(0, 60)}..."`);288289// ============================================================================290// Step 2: Build Vocabulary & Features291// ============================================================================292293console.log("\n📝 STEP 2: Building Vocabulary & Features");294console.log("─".repeat(70));295296// Build vocabulary297const allWords = reviews.map((r) => r.words);298const vocabulary = buildVocabulary(allWords, VOCAB_SIZE);299300console.log(`\n✓ Built vocabulary with ${vocabulary.length} terms`);301console.log(`  Top terms: ${vocabulary.slice(0, 10).join(", ")}...`);302303// Convert to bag-of-words304const bowVectors = reviews.map((r) => textToVector(r.words, vocabulary));305306// Calculate TF-IDF307const tfidfVectors = calculateTFIDF(bowVectors, vocabulary);308309console.log(`✓ Created TF-IDF vectors`);310console.log(`  Vector dimension: ${tfidfVectors[0].length}`);311312// Prepare data313const X = tensor(tfidfVectors);314const y = tensor(reviews.map((r) => r.label));315316// ============================================================================317// Step 3: Train/Test Split318// ============================================================================319320console.log("\n📦 STEP 3: Train/Test Split");321console.log("─".repeat(70));322323const [XTrain, XTest, yTrain, yTest] = trainTestSplit(X, y, {324  testSize: 0.2,325  randomState: 42,326  shuffle: true,327});328329console.log(`\n✓ Split data`);330console.log(`  Training: ${XTrain.shape[0]} samples`);331console.log(`  Testing: ${XTest.shape[0]} samples`);332333// Scale features334const scaler = new StandardScaler();335scaler.fit(XTrain);336const XTrainScaled = scaler.transform(XTrain);337const XTestScaled = scaler.transform(XTest);338339console.log(`✓ Applied StandardScaler`);340341// ============================================================================342// Step 4: Model Training343// ============================================================================344345console.log("\n🤖 STEP 4: Model Training");346console.log("─".repeat(70));347348// Logistic Regression349console.log("\nTraining Logistic Regression...");350const lr = new LogisticRegression({ maxIter: 200, learningRate: 0.1 });351const lrStart = Date.now();352lr.fit(XTrainScaled, yTrain);353const lrTime = Date.now() - lrStart;354console.log(`  ✓ Trained in ${lrTime}ms`);355356// Naive Bayes357console.log("Training Gaussian Naive Bayes...");358const nb = new GaussianNB();359const nbStart = Date.now();360nb.fit(XTrainScaled, yTrain);361const nbTime = Date.now() - nbStart;362console.log(`  ✓ Trained in ${nbTime}ms`);363364// ============================================================================365// Step 5: Model Evaluation366// ============================================================================367368console.log("\n📈 STEP 5: Model Evaluation");369console.log("─".repeat(70));370371// Get predictions372const yPredLR = lr.predict(XTestScaled);373const yPredNB = nb.predict(XTestScaled);374375// Calculate metrics376const results = [377  {378    name: "Logistic Regression",379    accuracy: accuracy(yTest, yPredLR),380    precision: precision(yTest, yPredLR, "binary"),381    recall: recall(yTest, yPredLR, "binary"),382    f1: f1Score(yTest, yPredLR, "binary"),383    predictions: yPredLR,384    time: lrTime,385  },386  {387    name: "Gaussian Naive Bayes",388    accuracy: accuracy(yTest, yPredNB),389    precision: precision(yTest, yPredNB, "binary"),390    recall: recall(yTest, yPredNB, "binary"),391    f1: f1Score(yTest, yPredNB, "binary"),392    predictions: yPredNB,393    time: nbTime,394  },395];396397console.log("\nModel Comparison:\n");398const metricsDF = new DataFrame({399  Model: results.map((r) => r.name),400  "Accuracy (%)": results.map((r) => (Number(r.accuracy) * 100).toFixed(2)),401  "Precision (%)": results.map((r) => (Number(r.precision) * 100).toFixed(2)),402  "Recall (%)": results.map((r) => (Number(r.recall) * 100).toFixed(2)),403  "F1 Score (%)": results.map((r) => (Number(r.f1) * 100).toFixed(2)),404  "Time (ms)": results.map((r) => r.time.toString()),405});406console.log(metricsDF.toString());407408// Best model409const bestModel = results.reduce((best, r) => (Number(r.f1) > Number(best.f1) ? r : best));410console.log(`\n🏆 Best Model: ${bestModel.name} (F1: ${(Number(bestModel.f1) * 100).toFixed(2)}%)`);411412// ============================================================================413// Step 6: Confusion Matrix414// ============================================================================415416console.log("\n📊 STEP 6: Confusion Matrix Analysis");417console.log("─".repeat(70));418419const cm = confusionMatrix(yTest, bestModel.predictions);420const cmData = expectNumericTypedArray(cm.data);421422console.log(`\nConfusion Matrix (${bestModel.name}):`);423console.log("                  Predicted");424console.log("                  Negative  Positive");425console.log(426  `  Actual Negative    ${String(cmData[0]).padStart(4)}      ${String(cmData[1]).padStart(4)}`427);428console.log(429  `  Actual Positive    ${String(cmData[2]).padStart(4)}      ${String(cmData[3]).padStart(4)}`430);431432const tn = cmData[0];433const fp = cmData[1];434const fn = cmData[2];435const tp = cmData[3];436437console.log(`\n  True Negatives:  ${tn}`);438console.log(`  False Positives: ${fp}`);439console.log(`  False Negatives: ${fn}`);440console.log(`  True Positives:  ${tp}`);441442// ============================================================================443// Step 7: Feature Analysis444// ============================================================================445446console.log("\n🔍 STEP 7: Feature Analysis");447console.log("─".repeat(70));448449// Analyze most predictive words (simple frequency analysis)450const posWordCounts: Map<string, number> = new Map();451const negWordCounts: Map<string, number> = new Map();452453for (const review of reviews) {454  const countMap = review.label === 1 ? posWordCounts : negWordCounts;455  for (const word of review.words) {456    countMap.set(word, (countMap.get(word) || 0) + 1);457  }458}459460// Find words with highest positive/negative ratio461const wordRatios: {462  word: string;463  ratio: number;464  posCount: number;465  negCount: number;466}[] = [];467for (const word of vocabulary) {468  const posCount = posWordCounts.get(word) || 0;469  const negCount = negWordCounts.get(word) || 0;470  if (posCount + negCount >= 5) {471    // Minimum frequency472    const ratio = (posCount + 1) / (negCount + 1); // Laplace smoothing473    wordRatios.push({ word, ratio, posCount, negCount });474  }475}476477// Most positive words478const mostPositive = wordRatios.sort((a, b) => b.ratio - a.ratio).slice(0, 5);479// Most negative words480const mostNegative = wordRatios.sort((a, b) => a.ratio - b.ratio).slice(0, 5);481482console.log("\nMost Positive Words:");483for (const { word, ratio, posCount, negCount } of mostPositive) {484  console.log(`  "${word}": ratio=${ratio.toFixed(2)} (pos=${posCount}, neg=${negCount})`);485}486487console.log("\nMost Negative Words:");488for (const { word, ratio, posCount, negCount } of mostNegative) {489  console.log(`  "${word}": ratio=${ratio.toFixed(2)} (pos=${posCount}, neg=${negCount})`);490}491492// ============================================================================493// Step 8: Sample Predictions494// ============================================================================495496console.log("\n🎯 STEP 8: Sample Predictions");497console.log("─".repeat(70));498499// Show some predictions500const testLabels = yTest.data;501const predLabels = bestModel.predictions.data;502503console.log("\nSample Test Predictions:\n");504const sampleCount = Math.min(10, XTest.shape[0]);505let sampleCorrect = 0;506let totalCorrect = 0;507let shown = 0;508509for (let i = 0; i < XTest.shape[0]; i++) {510  const actual = testLabels[i] === 1 ? "Positive" : "Negative";511  const predicted = predLabels[i] === 1 ? "Positive" : "Negative";512  const status = testLabels[i] === predLabels[i] ? "✓" : "✗";513514  if (i < sampleCount && shown < 5) {515    console.log(`  ${status} Actual: ${actual.padEnd(10)} Predicted: ${predicted}`);516    shown++;517  }518519  if (testLabels[i] === predLabels[i]) {520    totalCorrect++;521    if (i < sampleCount) {522      sampleCorrect++;523    }524  }525}526527console.log(`\n  Sample subset correct: ${sampleCorrect}/${sampleCount}`);528console.log(`  Full test correct:    ${totalCorrect}/${XTest.shape[0]}`);529530// ============================================================================531// Step 9: Visualizations532// ============================================================================533534console.log("\n📊 STEP 9: Generating Visualizations");535console.log("─".repeat(70));536537// Model comparison chart538try {539  const fig = new Figure({ width: 800, height: 400 });540  const ax = fig.addAxes();541542  const modelIndices = [0, 1];543  const f1Scores = results.map((r) => Number(r.f1) * 100);544545  ax.bar(tensor(modelIndices), tensor(f1Scores), { color: "#4CAF50" });546  ax.setTitle("Model Comparison (F1 Score)");547  ax.setXLabel("Model");548  ax.setYLabel("F1 Score (%)");549550  const svg = fig.renderSVG();551  writeFileSync(`${OUTPUT_DIR}/model-comparison.svg`, svg.svg);552  console.log(`  ✓ Saved: ${OUTPUT_DIR}/model-comparison.svg`);553} catch (e) {554  console.log(`  ⚠ Could not generate model comparison: ${e}`);555}556557// ============================================================================558// Summary559// ============================================================================560561console.log(`\n${"═".repeat(70)}`);562console.log("  SENTIMENT ANALYSIS COMPLETE - SUMMARY");563console.log("═".repeat(70));564565console.log("\n📌 Key Findings:\n");566console.log("  1. Data Overview:");567console.log(`     • ${NUM_SAMPLES} reviews analyzed`);568console.log(`     • Vocabulary size: ${vocabulary.length} terms`);569console.log(`     • Balanced classes (50/50)`);570571console.log("\n  2. Best Model:");572console.log(`     • ${bestModel.name}`);573console.log(`     • Accuracy: ${(Number(bestModel.accuracy) * 100).toFixed(2)}%`);574console.log(`     • F1 Score: ${(Number(bestModel.f1) * 100).toFixed(2)}%`);575console.log(`     • Training time: ${bestModel.time}ms`);576577console.log("\n  3. Observations:");578console.log("     • TF-IDF features capture sentiment effectively");579console.log("     • Both models achieve good performance");580console.log("     • Sentiment words are strong predictors");581582console.log("\n📁 Output Files:");583console.log(`   • ${OUTPUT_DIR}/model-comparison.svg`);584585console.log(`\n${"═".repeat(70)}`);586console.log("  ✅ Sentiment Analysis Complete!");587console.log("═".repeat(70));588

    Console Output

    $ npx tsx 06-sentiment-analysis/index.ts
    See the example README for the expected console and artifact output.

    Key Takeaways

    • Text Preprocessing: Tokenization, TF-IDF vectorization
    • Classification: Logistic Regression, Naive Bayes
    • Evaluation: Classification metrics, confusion matrix
    • Use deepbox/ml for LogisticRegression, GaussianNB.
    • Use deepbox/preprocess for StandardScaler, trainTestSplit.
    • Use deepbox/metrics for accuracy, precision, recall, f1Score.