Text Analytics
Text preprocessing, feature representation (one-hot, count, TF-IDF, word embeddings, N-grams, hashing), similarity, POS tagging, entity extraction, topic extraction, spam-ham classification, sentiment analysis, and Transformers in NLP.
Topics in this chapter
- Tokenization, Lower Casing, Stop Word Removal, Stemming and Lemmatization
- One Hot Encoding, Count Vectorization, TF-IDF Vectorization
- Word Embeddings, Generating N-Grams, Hash Vectorization
- Finding Text Similarity
- POS Tagging, Entity Extraction, Topic Extraction
- Spam-Ham Classification
- Sentiment Analysis
- Transformer in NLP
- Programs using NLTK
Text Preprocessing and Feature Representation
The Necessity of Numerical Representations
Raw text is unstructured, variable in length, and rich with linguistic nuance. For any predictive model — be it a classifier, a clustering algorithm, or a neural network — the first and most crucial step is to transform this amorphous sequence of characters into a fixed-size numerical vector that preserves as much relevant information as possible. The quality of this transformation often overshadows the choice of the downstream algorithm: a well-engineered feature representation can make a linear model competitive, while a poor one will cripple even the most sophisticated architecture.
Text Preprocessing: Cleaning and Normalizing
Before any numerical encoding, the raw string must be broken into atomic units — typically words — and stripped of noise that would inflate the feature space or obscure meaningful patterns. Using libraries such as NLTK (Natural Language Toolkit), we execute the following sequential operations:
Tokenization splits a document into a sequence of tokens, usually individual words or punctuation. NLTK's word_tokenize uses a rule-based approach that handles contractions, possessives, and sentence boundaries. For example:
"The students' projects, which aren't trivial, cost $50 each."
becomes ['The', 'students', "'", 'projects', ',', 'which', 'are', "n't", 'trivial', ',', 'cost', '$', '50', 'each', '.'].
While regular-expression tokenizers (RegexpTokenizer) offer finer control, word_tokenize is a reliable default.
from nltk.tokenize import word_tokenize
text = "The students' projects, which aren't trivial, cost $50 each."
tokens = word_tokenize(text)
print(tokens)
Normalization typically includes lowercasing all tokens. This not only reduces the vocabulary size by collapsing case variations ("Book" vs. "book") but also aligns with the fact that most downstream tasks are case-insensitive. Exceptions exist for named-entity recognition, where capitalization carries signal.
Stopword removal eliminates high-frequency function words (e.g., "the", "is", "and") that contribute little semantic content. NLTK provides a list of stopwords for many languages. Removing them compacts the representation and often improves signal-to-noise ratio, although modern contextual embeddings may retain them to preserve syntactic structure.
from nltk.corpus import stopwords
stop_words = set(stopwords.words('english'))
filtered_tokens = [w for w in tokens if w.lower() not in stop_words]
Stemming and lemmatization reduce inflectional forms to a common base. Stemming (e.g., the Porter stemmer in NLTK) applies crude heuristic rules ("running" → "run", "berries" → "berri") and is fast, but it can produce non-words. Lemmatization, using a vocabulary and morphological analysis ("am", "are", "is" → "be"), yields valid dictionary forms. For many applications, a lemmatizer like NLTK's WordNetLemmatizer is preferred because it preserves semantic consistency.
from nltk.stem import PorterStemmer, WordNetLemmatizer
stemmer = PorterStemmer()
lemmatizer = WordNetLemmatizer()
print(stemmer.stem('running')) # 'run'
print(lemmatizer.lemmatize('running', pos='v')) # 'run'
print(lemmatizer.lemmatize('better', pos='a')) # 'good'
Additional cleaning steps — removing punctuation, numeric digits, HTML tags, and expanding contractions — are tailored to the corpus. After preprocessing, each document is reduced to a sequence of normalized tokens ready for vectorization.
One-Hot Encoding and Count Vectorization
The simplest representation is the bag-of-words (BoW) model, which treats a document as an unordered multiset of its tokens. Given a vocabulary constructed from the entire corpus, each document is mapped to a vector .
In one-hot encoding (strictly, a binary BoW), the -th component is if appears in and otherwise:
While extremely sparse, one-hot vectors are suitable as input to naive Bayes classifiers that model the presence or absence of words.
Count vectorization extends the idea by recording the raw frequency of each term:
This representation, often implemented via scikit-learn's CountVectorizer, captures term intensity: a document that mentions "data" ten times likely values the concept more than one that mentions it once. However, raw counts inflate the influence of common words that appear frequently in every document, obscuring truly discriminative terms.
from sklearn.feature_extraction.text import CountVectorizer
corpus = ["The cat sat on the mat", "The dog sat on the log"]
vectorizer = CountVectorizer()
X = vectorizer.fit_transform(corpus)
print(vectorizer.get_feature_names_out())
print(X.toarray())
TF-IDF Vectorization: Weighting Term Importance
Term Frequency–Inverse Document Frequency (TF-IDF) adjusts raw counts by penalizing words that appear in many documents and boosting rare, informative ones. It combines two statistics:
-
Term Frequency : how often term occurs in document . A common formulation (sublinear scaling) is
The logarithm dampens the effect of very high counts, acknowledging that the 100th occurrence adds less information than the first. - **Inverse Document Frequency** $IDF(t, D)$: $$IDF(t,D) = \log\frac{N}{1 + n_t},$$ where $N$ is the total number of documents and $n_t = |\{d \in D : t \in d\}|$ is the document frequency of $t$. The "$+1$" in the denominator prevents division by zero and is analogous to Laplace smoothing. A term that appears in every document yields $IDF \approx \log 1 = 0$, effectively nullifying its weight. The **TF-IDF score** for term $t$ in document $d$ is the product
TFIDF(t,d,D) = TF(t,d) \times IDF(t,D).
The resulting document vectors are often normalized to unit Euclidean length ($L_2$ normalization):x_j^{(d)} = \frac{TFIDF(w_j,d,D)}{\sqrt{\sum_{k} TFIDF(w_k,d,D)^2}}.
*Economic and Analytical Intuition:* We can view TF-IDF through the lens of information economics. The TF component represents the *local supply* or *local utility* of a term within a specific document. The IDF component acts as a *scarcity premium* or *information rent*. Ubiquitous terms have high supply across the corpus, driving their marginal information value (IDF) to zero. Conversely, domain-specific or rare terms (e.g., "quantitative easing" or "EBITDA") command a high scarcity premium. Thus, TF-IDF effectively prices words based on their marginal contribution to distinguishing one document from another. ```python from sklearn.feature_extraction.text import TfidfVectorizer tfidf = TfidfVectorizer(sublinear_tf=True, stop_words='english') X_tfidf = tfidf.fit_transform(corpus) ``` ### N-Gram Expansion and Hash Vectorization The bag-of-words assumption discards word order, losing phrases like "New York" or negations like "not good". **N-gram features** capture contiguous sequences of $n$ tokens. For instance, adding bigrams ($n=2$) to a unigram vocabulary allows the model to distinguish "not good" from "good" alone. Scikit-learn's `CountVectorizer` accepts `ngram_range=(1,2)` to include both unigrams and bigrams. The vocabulary size grows quickly with $n$; careful pruning by minimum document frequency or feature selection is required. When the vocabulary becomes prohibitively large — for example, with character n-grams or streaming data — **hash vectorization** (the "hashing trick") maps each feature directly to a fixed-size vector of dimension $M$ using a hash function $h: \text{string} \to \{1,\dots,M\}$. Each occurrence of a token $t$ increments the component at index $h(t)$, possibly with a second hash function $h_2$ that determines the sign (±1) to mitigate collision bias:x_{k}^{(d)} = \sum_{t \in d} \sum_{\text{occurrences}} \operatorname{sign}(h_2(t)) \cdot \mathbb{I}[h(t) = k].
Hash vectorization never needs a vocabulary dictionary, making it memory-efficient and suitable for online learning. The cost is the loss of interpretability (indices no longer correspond to known words) and occasional collisions, which become negligible when $M$ is large enough (typically $2^{18}$ to $2^{24}$). ```python from sklearn.feature_extraction.text import HashingVectorizer hasher = HashingVectorizer(n_features=2**18, alternate_sign=True) X_hashed = hasher.fit_transform(corpus) ``` ### Dense Word Embeddings: Capturing Semantic Relationships Count-based representations treat each word as an atomic, independent symbol — the vector for "dog" is as close to "cat" as it is to "car". **Word embeddings** overcome this by mapping every word to a low-dimensional dense vector $\mathbf{v}_w \in \mathbb{R}^d$ (e.g., $d=300$) such that semantically similar words lie close to one another in the vector space. These embeddings are learned from large corpora by exploiting the **distributional hypothesis** (you shall know a word by the company it keeps). The most influential framework, **Word2Vec**, offers two architectures: - **Skip-gram**: Given a center word $w_t$, predict the surrounding context words $w_{t+j}$ for $-c \le j \le c, j \neq 0$. The objective function maximizes: $$\max \sum_{t=1}^{T} \sum_{-c \le j \le c, j \neq 0} \log p(w_{t+j} | w_t).$$ - **Continuous Bag-of-Words (CBOW)**: Given context words, predict the center word. The Skip-gram model with negative sampling maximizes the probability of observing a context word $w_O$ given a target word $w_I$, while pushing apart the representations of randomly sampled negative words:\mathcal{L} = \log \sigma(v_{w_O}^{\top} v_{w_I}) + \sum_{k=1}^{K} \mathbb{E}{w_k \sim P_n(w)} \left[ \log \sigma(-v{w_k}^{\top} v_{w_I}) \right],
where $\sigma$ is the logistic function and $P_n$ is a noise distribution (often the unigram distribution raised to the $3/4$ power). After training, each word is represented by a vector $v_w$. **GloVe** (Global Vectors for Word Representation) combines global matrix factorization with local context window methods. It factorizes the log-count matrix of word co-occurrences, producing embeddings where vector differences capture semantic relationships (e.g., $\vec{\text{king}} - \vec{\text{man}} + \vec{\text{woman}} \approx \vec{\text{queen}}$). ```python from gensim.models import Word2Vec sentences = [["the", "cat", "sat"], ["the", "dog", "barked"]] model = Word2Vec(sentences, vector_size=100, window=5, min_count=1, workers=4) vector = model.wv['cat'] similar = model.wv.most_similar('cat', topn=5) ``` *Analytical Intuition:* Word embeddings operationalize the distributional hypothesis — the linguistic premise that words occurring in similar contexts tend to have similar meanings. In a business analytics context, this allows models to understand that "revenue" and "sales" occupy similar regions in the vector space. This geometric representation enables robust sentiment analysis, topic extraction, and text classification, allowing algorithms to generalize across synonymous terminology even when exact keyword matches fail.Core Text Analysis: Similarity, Tagging, and Topic Extraction
Document Similarity and the Vector Space Model
Quantifying the semantic relatedness of documents is a foundational task in text analytics, underpinning clustering, information retrieval, and recommendation. The most established approach builds on the Vector Space Model (VSM), which represents each document as a weighted vector of the terms in a corpus.
Let be the vocabulary of size . A document is mapped to a vector . The most robust weighting scheme is TF-IDF. For a term in document within corpus , the weight is defined as:
where and .
Measuring similarity between two documents and then reduces to comparing their TF-IDF vectors. The most common choice is the cosine similarity, which normalizes for document length:
Geometrically, cosine similarity is the cosine of the angle between the two vectors, ranging from (orthogonal, no shared terms) to (identical direction). In practice, documents with high cosine similarity often share thematic content, but the measure is purely lexical and fails to capture synonymy or polysemy.
To overcome this lexical gap, document-level representations can be obtained by simple averaging of word vectors (weighted by TF-IDF) or by more sophisticated models such as Doc2Vec. Cosine similarity computed on these dense vectors yields semantically richer document similarities.
from sklearn.metrics.pairwise import cosine_similarity
sim_matrix = cosine_similarity(X_tfidf)
Economic Intuition: In spatial models of product differentiation and industrial organization, textual similarity maps directly to product substitutability. If we treat product descriptions or patent abstracts as vectors in the VSM, a high cosine similarity between two firms' product portfolios implies a high Cross-Price Elasticity of Demand. Analysts use these similarity matrices to map the Product Space, identifying direct competitors and visualizing strategic clustering within an industry.
Part-of-Speech Tagging and Named Entity Recognition
Part-of-speech (POS) tagging and named entity recognition (NER) are sequence labeling problems: given a sequence of words , the goal is to predict the most probable sequence of tags . For POS tagging, the tags are grammatical categories (noun, verb, adjective, etc.); for NER, tags indicate entity types (person, organization, location, or null) and are often encoded with a Beginning-Inside-Outside (BIO) scheme.
A classical generative model for POS tagging is the Hidden Markov Model (HMM). It assumes that the probability of observing a sentence and its tag sequence factorizes as:
with special start and end states. The transition probabilities capture grammatical ordering constraints, while the emission probabilities model the likelihood of a word being emitted by a given tag. Inference — finding — is performed by the Viterbi algorithm, which uses dynamic programming with the recursion:
HMMs are simple and efficient, but they suffer from label bias. To address this, Conditional Random Fields (CRFs) model the conditional probability directly with a log-linear model:
where is a global partition function. The feature functions can capture rich dependencies on the entire input sequence.
import nltk
nltk.download('averaged_perceptron_tagger')
nltk.download('maxent_ne_chunker')
nltk.download('words')
pos_tags = nltk.pos_tag(tokens)
named_entities = nltk.ne_chunk(pos_tags)
Economic Intuition: NER is the foundational step for constructing Economic Networks from textual sources. By extracting firm names, geographic locations, and commodity types from earnings call transcripts or global news feeds, analysts can build bipartite graphs linking supply chain nodes. Tracking the co-occurrence of specific entities allows researchers to measure Supply Chain Contagion or gauge market exposure to localized geopolitical shocks.
Topic Extraction: Latent Dirichlet Allocation (LDA)
When a corpus consists of thousands of unstructured documents, manually labeling their themes is infeasible. Topic modeling automates the discovery of latent thematic structures. The most influential technique is Latent Dirichlet Allocation (LDA).
LDA is a generative probabilistic model that posits the following process for creating a corpus of documents:
- For each topic , draw a word distribution .
- For each document : a. Choose topic proportions . b. For each word position : i. Choose a topic assignment . ii. Choose a word .
The symmetric Dirichlet hyperparameters and control the sparsity of topic mixtures and topic-word distributions, respectively. The posterior distribution over the latent variables given the observed words is intractable, so approximate inference is carried out via collapsed Gibbs sampling that integrates out and . For a given word token in document , the conditional probability of its topic assignment given all other assignments is:
where is the number of words in document assigned to topic excluding the current token, is the number of times word is assigned to topic across all documents, and is the total number of words assigned to topic excluding the current token.
Economic Intuition: In information economics and behavioral finance, LDA topics represent Latent Consumer Preferences or Macroeconomic Regimes. For example, applying LDA to decades of central bank communications (e.g., FOMC minutes) allows economists to quantify shifts in monetary policy stance. A rising probability weight on a topic characterized by words like "inflation", "supply", and "commodity" signals a hawkish regime shift. Visualizing the temporal evolution of via Streamgraphs or Ridgeline Plots provides a powerful visual narrative of thematic momentum.
from sklearn.decomposition import LatentDirichletAllocation
from sklearn.feature_extraction.text import CountVectorizer
vectorizer = CountVectorizer(max_df=0.95, min_df=2, stop_words='english')
dtm = vectorizer.fit_transform(docs)
lda = LatentDirichletAllocation(n_components=5, random_state=42)
lda.fit(dtm)
# Display top words per topic
for i, topic in enumerate(lda.components_):
top_words = [vectorizer.get_feature_names_out()[idx] for idx in topic.argsort()[-10:]]
print(f"Topic {i}: {', '.join(top_words)}")
Text Classification, Sentiment Analysis, and Transformers in NLP
Spam-Ham Classification with Naive Bayes
For binary classification tasks like spam–ham filtering, the Multinomial Naive Bayes classifier is a generative probabilistic model that assumes features (word counts) are conditionally independent given the class. Let be the class variable with two outcomes: spam () and ham (). By Bayes' theorem, the posterior probability for a document represented by word count vector is
The likelihood under the multinomial model is
where is the probability of generating word in class , with . The parameters are estimated via maximum likelihood with Laplace smoothing (additive smoothing) to avoid zero probabilities:
where is the count of word in class , is the total number of words in class , and is a smoothing hyperparameter (typically ). The prior is the fraction of documents in class .
The decision rule assigns the class with the highest posterior. Working in log space avoids underflow:
Economic Intuition and Utility Maximization: In practical analytics, classification is rarely a purely statistical exercise; it is a decision-making process under uncertainty. In spam-ham filtering, the cost matrix is highly asymmetric. A false positive (classifying ham as spam) incurs a severe utility loss, such as a missed critical business contract. Conversely, a false negative (allowing spam into the inbox) incurs a lower, albeit cumulative, cognitive cost. Therefore, an optimal analytics system does not merely predict the class with the highest probability; it selects a decision threshold that maximizes the expected economic utility . If represents the utility of predicting when the true class is , the optimal decision rule is .
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.naive_bayes import MultinomialNB
from sklearn.pipeline import make_pipeline
corpus = [
"Win a free iPhone now! Click the link to claim your prize.",
"Hey, are we still on for the budget meeting at 3 PM?",
"URGENT: Your bank account has been compromised. Verify immediately.",
"Please review the attached Q3 financial projections before tomorrow."
]
labels = ['spam', 'ham', 'spam', 'ham']
model = make_pipeline(
TfidfVectorizer(sublinear_tf=True, stop_words='english'),
MultinomialNB(alpha=1.0)
)
model.fit(corpus, labels)
new_messages = ["Meeting moved to 4 PM.", "Congratulations! You won a $1000 gift card."]
predictions = model.predict(new_messages)
probabilities = model.predict_proba(new_messages)
for msg, pred, prob in zip(new_messages, predictions, probabilities):
print(f"Message: '{msg}'\nPredicted: {pred} | P(spam): {prob[1]:.4f}\n")
Discriminative Models: Logistic Regression and SVMs
A more flexible approach models the conditional probability directly. Logistic regression models the probability of the positive class as
The parameters and are learned by minimizing the negative log-likelihood (cross-entropy loss) with L2 regularization:
The resulting model yields interpretable coefficients: a high positive means that the presence of word increases the log-odds of spam. Support Vector Machines (SVMs) find a maximum-margin hyperplane in a high-dimensional feature space. For text data, linear SVMs with TF-IDF weighted BoW vectors are highly effective due to the high dimensionality and sparsity of the feature space.
Sentiment Analysis
Sentiment analysis (or opinion mining) is the text classification task of determining the affective stance (positive, negative, neutral) expressed in a piece of text. It extends binary classification to multi-class or ordinal regression.
Lexicon-Based Approaches use a predefined sentiment lexicon — a dictionary mapping words to sentiment scores. For instance, the AFINN lexicon assigns integer scores from –5 (negative) to +5 (positive), and VADER (Valence Aware Dictionary and sEntiment Reasoner) handles negations, intensifiers, and emojis. The sentiment of a document is computed as the sum of its word scores:
Lexicon methods are transparent and require no training data, but they ignore context, sarcasm, and domain-specific jargon.
Supervised Learning for Sentiment learns from labeled documents. The BoW (or TF-IDF) representation is fed into any classifier — Naive Bayes, logistic regression, or SVMs. Because sentiment is often expressed through word combinations, n-gram features (e.g., bigrams like "not good") are essential. Feature engineering includes handling negation by appending a "NOT_" prefix to words following a negation term within a window.
Economic Intuition: From a macroeconomic and strategic perspective, consumer sentiment acts as a leading indicator for market dynamics. The marginal value of information derived from real-time sentiment analysis allows firms to dynamically adjust pricing strategies, optimize inventory allocation, and hedge against reputational shocks. An unexpected spike in negative sentiment regarding a product's battery life on social media provides an early signal that allows a firm to reallocate supply chain resources before the issue manifests in lagging financial metrics.
Transformers in NLP
The limitations of recurrent neural networks (RNNs) — namely, their inability to parallelize training and their struggle with long-range dependencies due to vanishing gradients — were fundamentally resolved by the introduction of Transformer Models. Transformers discard recurrence and convolution entirely, relying instead on the Self-Attention Mechanism to model global dependencies.
The core mathematical operation is the Scaled Dot-Product Attention:
The scaling factor prevents the softmax from saturating. Multi-Head Attention runs multiple attention operations in parallel:
This architectural paradigm shift has had a transformative impact on NLP, enabling the pre-training of massive Large Language Models (LLMs) on vast corpora, which can then be fine-tuned for downstream tasks with minimal task-specific data. Pre-trained models like BERT (Bidirectional Encoder Representations from Transformers) and DistilBERT use the Transformer encoder to produce deeply contextualized word representations. The pipeline API from Hugging Face's transformers library enables zero-shot classification and sentiment analysis:
from transformers import pipeline
# Sentiment analysis with a pre-trained Transformer
sentiment_analyzer = pipeline(
"sentiment-analysis",
model="distilbert-base-uncased-finetuned-sst-2-english"
)
reviews = [
"The company's earnings call was incredibly disappointing, missing all revenue targets.",
"This product exceeded all my expectations. Highly recommended!"
]
for review in reviews:
result = sentiment_analyzer(review)[0]
print(f"Review: '{review}'\nSentiment: {result['label']} (score: {result['score']:.4f})\n")
# Zero-shot text classification
classifier = pipeline("zero-shot-classification", model="facebook/bart-large-mnli")
text = "The new fiscal policy aims to reduce inflation while maintaining employment levels."
candidate_labels = ["economics", "politics", "sports", "technology"]
result = classifier(text, candidate_labels)
for label, score in zip(result['labels'], result['scores']):
print(f"{label}: {score:.4f}")
The Transformer architecture revolutionizes text analytics by enabling transfer learning: models pre-trained on massive corpora capture general linguistic knowledge, which is then fine-tuned on domain-specific tasks with relatively small labeled datasets. This paradigm has set new state-of-the-art benchmarks across virtually all NLP tasks, from classification and sentiment analysis to question answering and text generation.