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/dataframeProject 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));588Console 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.