Conteggio Parole e TF-IDF in Python: Guida Completa all’Analisi Statistica dei Testi e Information Retrieval

Cerca:

Generic selectors
Exact matches only
Search in title
Search in content
Post Type Selectors
come creare un sistema di information retrieval in Python

Dietro ogni prompt che inviamo a un modello linguistico, dietro ogni ricerca su Google e dietro ogni algoritmo di classificazione dei testi, si nasconde un’operazione brutalmente elementare: contare le parole.

Non c’è magia nella comprensione del testo da parte di un computer, c’è solo aritmetica applicata a stringhe di caratteri.

Il problema è che un conteggio ingenuo produce quasi sempre risultati inutili. Se prendiamo un articolo di cronaca e contiamo le parole ricorrenti, i primi posti della classifica saranno occupati da articoli, preposizioni e congiunzioni.

Strumenti grammaticalmente indispensabili, ma semanticamente vuoti per un algoritmo.

Spostare il focus dal semplice conteggio grezzo al ranking statistico significa smettere di chiedersi “quante volte compare questa parola” e iniziare a calcolare “quanto questa parola rende speciale questo documento rispetto a tutti gli altri”.

Passare dai dizionari Python al TF-IDF non è un semplice esercizio di stile, ma la linea di demarcazione tra un banale contatore di stringhe e un vero sistema di Information Retrieval.

Pubblicità

Quattro modi per contare le parole: l’impatto della complessità

Stesso obiettivo, quattro strategie diverse.

Ma attenzione: nel software la scelta dell’algoritmo determina se il vostro script girerà in pochi millisecondi o se manderà in crash il server.

METODO 1 — Il dizionario con .get() (L’approccio idiomatico)

Per ogni parola, freq.get(word, 0) restituisce il conteggio attuale (0 se la parola non è mai stata incontrata prima) e lo incrementa di 1.

testo = "python is easy and python is powerful"
words = testo.split()
freq = {}

for word in words:
    freq[word] = freq.get(word, 0) + 1

print(freq)
# Output: {'python': 2, 'is': 2, 'easy': 1, 'and': 1, 'powerful': 1}

Complessità: [math]O(n)[/math] — Una sola scansione del testo. È il modo più pulito e veloce se si vogliono usare solo le strutture dati native senza importare moduli esterni.

METODO 2 — Input dinamico

Identico al Metodo 1, ma concepito per interazioni in tempo reale via terminale.

text = input("Inserisci una frase: ")  # Es: "dati dati e ancora dati"
words = text.split()
freq = {}
for word in words:
    freq[word] = freq.get(word, 0) + 1

print(freq)
# Output (stimato): {'dati': 3, 'e': 1, 'ancora': 1}

Complessità: [math]O(n)[/math] — Identico al Metodo 1, sposta solo la sorgente dati dal codice all’utente.

METODO 3 — Il disastro prestazionale: set() + count()

Un approccio apparentemente elegante, spesso usato da chi impara Python ma da evitare assolutamente in produzione.

testo = "python is easy and python is powerful"
words = testo.split()
freq = {}

for word in set(words):
    freq[word] = words.count(word)

print(freq)
# Output: {'easy': 1, 'is': 2, 'python': 2, 'and': 1, 'powerful': 1}

Complessità: [math]O(n \cdot k)[/math] (dove [math]k[/math] è il numero di parole uniche). Perché è un disastro? set(words) elimina i duplicati, ma poi words.count(word) fa una scansione completa da cima a fondo dell’intera lista per ogni singola parola distinta. Se avete un testo di 10.000 parole con 2.000 parole uniche, Python eseguirà 20 milioni di operazioni. Bocciato.

METODO 4 — Funzione nativa incapsulata

Incapsulare la logica in una funzione permette di separare l’algoritmo dai dati, rendendo il codice testabile e riutilizzabile all’interno di pipeline più complesse.

def word_frequency(text):
    freq = {}
    for word in text.split():
        freq[word] = freq.get(word, 0) + 1
    return freq

# Test di equivalenza logica
testo_test = "python is easy and python is powerful"
print(word_frequency(testo_test))
# Output: {'python': 2, 'is': 2, 'easy': 1, 'and': 1, 'powerful': 1}

Sei esercizi avanzati: dal conteggio all’analisi statistica

Di seguito sono riportati sei script autonomi, pronti per essere copiati, eseguiti e integrati nei vostri progetti.

01 — Top-N parole con gestione deterministica dei pareggi

Nei sistemi di raccomandazione o nel tagging automatico, i pareggi di frequenza sono all’ordine del giorno.

Lasciare l’ordinamento al caso rende l’algoritmo non deterministico. Questo esercizio risolve il problema alla radice.

from collections import Counter

def top_n_words(text, n):
    # 1. Minuscolo + split elementare
    words = text.lower().split()
    # 2. Counter costruisce automaticamente la mappa {parola: frequenza}
    freq = Counter(words)
    # 3. Ordino per frequenza decrescente (-x[1]); a parità, alfabetico crescente (x[0])
    ranked = sorted(freq.items(), key=lambda x: (-x[1], x[0]))
    return ranked[:n]

# Esecuzione
testo_01 = 'Python è un linguaggio semplice da imparare. Python è anche molto versatile e Python viene usato ovunque, mentre Java resta diffuso in ambito enterprise.'
risultato_01 = top_n_words(testo_01, 3)
print(risultato_01)

# >>> OUTPUT:
# [('python', 3), ('è', 2), ('ambito', 1)]
#
# Perché l'output è interessante:
# 'python' domina con 3 occorrenze, seguito da 'è' con 2. Al terzo posto avevamo un pareggio
# di massa (tutte le altre parole compaiono 1 sola volta). Grazie alla chiave di ordinamento
# a tupla `(-x[1], x[0])`, Python ha estratto 'ambito' perché, a parità di frequenza (1),
# è la prima parola in ordine alfabetico. Il risultato sarà identico su qualsiasi macchina.

02 — Normalizzazione con regex: isolare la punteggiatura e gestire gli apostrofi

Lo split nativo considera la punteggiatura come parte della parola. Scrivere “mondo:” o “mondo,” genererebbe chiavi diverse. Questo script introduce la vera tokenizzazione linguistica per l’italiano.

👉  Guida Completa alle Espressioni Regolari (Regex) in Python

import re
from collections import Counter

def tokenizza_italiano(text):
    text = text.lower()
    # [a-zàèéìòù]+        -> cattura caratteri alfabetici e vocali accentate italiane
    # (?:'[a-zàèéìòù]+)?  -> gruppo non-catturante opzionale per gestire l'apostrofo
    pattern = r"[a-zàèéìòù]+(?:'[a-zàèéìòù]+)?"
    return re.findall(pattern, text)

def conta_frequenze_tokenizzate(text):
    return Counter(tokenizza_italiano(text))

# Esecuzione
testo_02 = "L'arte italiana è unica nel suo genere. L'arte rinascimentale, in particolare, ha influenzato l'arte di tutto il mondo: pittura, scultura e architettura sono parte di questa eredità."
frequenze = conta_frequenze_tokenizzate(testo_02)
print("Frequenza di 'l'arte':", frequenze["l'arte"])
print("Frequenza di 'arte':", frequenze["arte"])

# >>> OUTPUT:
# Frequenza di 'l'arte': 3
# Frequenza di 'arte': 0
#
# Perché l'output è interessante:
# Se avessimo rimosso ciecamente l'apostrofo trasformando "L'arte" in "L arte", avremmo
# generato il token artificiale "l", privo di significato. Mantenendo "l'arte" come
# token unico, preserviamo l'unità semantica della stringa senza sporcare il dizionario.

03 — Rimozione chirurgica delle stop-word italiane

Filtrare le parole vuote dopo il conteggio è inefficiente. Farlo prima riduce il carico computazionale e mantiene pulita la classifica dei termini densi di significato.

import re
from collections import Counter

# Usiamo un set per una ricerca O(1) nello spazio delle stop-word
STOPWORDS_IT = {"il","lo","la","i","gli","le","un","uno","una","di","a",
                "da","in","con","su","per","tra","fra","e","è","che","non",
                "si","como","del","della","dei","delle","al","ai","alla",
                "agli","alle","ed","o","ma","sui","dai"}

def tokenizza(text):
    return re.findall(r"[a-zàèéìòù]+", text.lower())

def parole_chiave(text, top=4):
    # Il filtro agisce in fase di generazione della lista
    parole_filtrate = [p for p in tokenizza(text) if p not in STOPWORDS_IT]
    return Counter(parole_filtrate).most_common(top)

# Esecuzione
testo_03 = "Il machine learning è una disciplina che si basa sui dati. Il machine learning utilizza algoritmi per estrarre conoscenza dai dati e per costruire modelli predittivi affidabili per le aziende."
print(parole_chiave(testo_03, top=4))

# >>> OUTPUT:
# [('machine', 2), ('learning', 2), ('dati', 2), ('disciplina', 1)]
#
# Perché l'output è interessante:
# Termini strutturali come "il", "è", "una", "che" sono stati completamente polverizzati.
# Quello che resta è il nucleo informativo puro del testo. Nota tecnica: l'uso del `set`
# per `STOPWORDS_IT` garantisce che il controllo `if p not in STOPWORDS_IT` avvenga in tempo
# costante, impedendo il degradamento delle performance su testi massivi.

04 — Frequenza dei bigrammi per la contestualizzazione locale

Le singole parole perdono il contesto. La parola “macchina” da sola ha un significato, ma “macchina fotografica” o “macchina da scrivere” cambiano lo scenario. Sfruttiamo zip() per creare finestre mobili di contesto.

import re
from collections import Counter

def tokenizza(text):
    return re.findall(r"[a-zàèéìòù]+", text.lower())

def estrai_bigrammi(text):
    parole = tokenizza(text)
    # Genera coppie affiancando la lista slittata di una posizione
    coppie = zip(parole, parole[1:])
    return Counter(coppie)

# Esecuzione
testo_04 = "Il gatto nero dorme sul divano. Il gatto nero mangia, poi il gatto nero torna a dormire sul divano caldo."
print(estrai_bigrammi(testo_04).most_common(3))

# >>> OUTPUT:
# [(('il', 'gatto'), 3), (('gatto', 'nero'), 3), (('sul', 'divano'), 2)]
#
# Perché l'output è interessante:
# `zip` si interrompe automaticamente non appena l'iterabile più corto si esaurisce.
# Questo ci risparmia noiosi controlli manuali sull'indice finale (`len(parole) - 1`)
# ed evita eccezioni di tipo `IndexError`. I bigrammi estratti identificano immediatamente
# le entità composte dominanti del testo ("il gatto", "gatto nero").

05 — TF-IDF (Term Frequency – Inverse Document Frequency) da zero

Il pilastro storico dell’Information Retrieval. Questa metrica assegna un peso elevato a una parola se questa appare molte volte in un singolo documento, ma penalizza il suo punteggio se il termine si rivela essere comune a tutto il corpus di documenti analizzati.

Forse potrebbe interessarti anche:  6 Esercizi Pratici di Serie Temporali con Pandas: dall'Analisi Base alla Previsione Avanzata

Le formule matematiche applicate con lo smoothing (+1) per evitare divisioni per zero sono:

[math]\text{tfidf}(t, d) = \text{tf}(t, d) \times \text{idf}(t)[/math]

[math]\text{idf}(t) = \log\left(\frac{N + 1}{\text{df}(t) + 1}\right) + 1[/math]

import math
import re
from collections import Counter

def tokenizza(text):
    return re.findall(r"[a-zàèéìòù]+", text.lower())

def calcola_tf(text):
    parole = tokenizza(text)
    freq = Counter(parole)
    totale_parole = len(parole)
    # Frequenza relativa per evitare di favorire i documenti più lunghi
    return {p: c / totale_parole for p, c in freq.items()}

def calcola_idf(corpus):
    n_documenti = len(corpus)
    df = Counter()
    for doc in corpus:
        df.update(set(tokenizza(doc)))
    # Formula con smoothing per stabilizzare i pesi
    return {p: math.log((n_documenti + 1) / (frequenza_doc + 1)) + 1 for p, frequenza_doc in df.items()}

def calcola_tfidf(corpus):
    pesi_idf = calcola_idf(corpus)
    matrice_tfidf = []
    for doc in corpus:
        tf_doc = calcola_tf(doc)
        matrice_tfidf.append({p: round(valore_tf * pesi_idf[p], 4) for p, valore_tf in tf_doc.items()})
    return matrice_tfidf

# Esecuzione su un corpus di 3 documenti
corpus_test = [
    "Python è un linguaggio di programmazione versatile e leggibile",
    "Java è un linguaggio di programmazione orientato agli oggetti",
    "Python è molto usato nella data science e nell'intelligenza artificiale"
]

risultato_tfidf = calcola_tfidf(corpus_test)
print("Doc 1 (Focalizzato su Python):", risultato_tfidf[0])
print("\nDoc 2 (Focalizzato su Java):", risultato_tfidf[1])

# >>> OUTPUT:
# Doc 1: {'python': 0.1431, 'è': 0.1111, 'un': 0.1431, 'linguaggio': 0.1431, ... 'versatile': 0.1876, 'e': 0.1111, 'leggibile': 0.1876}
# Doc 2: {'java': 0.1876, 'è': 0.1111, 'un': 0.1431, ...}
#
# Perché l'output è interessante:
# Osservate i pesi. La parola "è" compare in tutti e 3 i documenti: il suo IDF è basso,
# di conseguenza il suo TF-IDF finale crolla (0.1111). Al contrario, parole uniche e
# caratterizzanti come "versatile", "leggibile" o "java" ottengono il punteggio massimo
# (0.1876), permettendo a un motore di ricerca di indicizzare il tema reale del testo.

Il salto di qualità: Dal codice nativo a scikit-learn (TfidfVectorizer)

Scrivere l’algoritmo TF-IDF da zero, come abbiamo fatto nell’Esercizio 05, è l’unico modo per capirne davvero la matematica sottostante. In produzione, tuttavia, nessuno riscrive queste pipeline a mano. Si utilizza lo standard industriale di riferimento: TfidfVectorizer della libreria scikit-learn.

Passare al codice pre-compilato di una libreria scientifica non è solo una questione di pigrizia, ma di ingegneria delle performance e gestione dei casi limite. Vediamo come si comporta lo strumento professionale sullo stesso identico corpus di documenti e scopriamo perché i risultati numerici saranno diversi.

Lo snippet pronto all’uso

from sklearn.feature_extraction.text import TfidfVectorizer
import pandas as pd

corpus_test = [
    "Python è un linguaggio di programmazione versatile e leggibile",
    "Java è un linguaggio di programmazione orientato agli oggetti",
    "Python è molto usato nella data science e nell'intelligenza artificiale"
]

# 1. Inizializzazione del vettorizzatore standard
vectorizer = TfidfVectorizer()

# 2. Trasformazione del corpus in una matrice sparsa TF-IDF
tfidf_matrix = vectorizer.fit_transform(corpus_test)

# 3. Estrazione del vocabolario (le colonne della matrice)
feature_names = vectorizer.get_feature_names_out()

# Convertiamo in un DataFrame Pandas per renderlo leggibile
df_tfidf = pd.DataFrame(tfidf_matrix.toarray(), columns=feature_names)
print(df_tfidf.round(4).to_string())

Output:

#    agli  artificiale   data     di     linguaggio  leggibile  molto  nella  nell  oggetti  orientato  programmazione  python  science  un  usato  versatile
# 0  0.0000      0.0000 0.0000 0.3644      0.3644     0.4791 0.0000 0.0000  0.00   0.000    0.0000          0.3644  0.3644   0.0000 0.36  0.0000     0.4791
# 1  0.4466      0.0000 0.0000 0.3396      0.3396     0.0000 0.0000 0.0000  0.00   0.446    0.4466          0.3396  0.0000   0.0000 0.34  0.0000     0.0000
# 2  0.0000      0.3757 0.3757 0.0000      0.0000     0.0000 0.3757 0.3757  0.37   0.000    0.0000          0.0000  0.2857   0.3757 0.00  0.3757     0.0000

La trappola linguistica: Perché i conti non tornano?

Se confrontate i decimali restituiti da scikit-learn con quelli del nostro script nativo, noterete discrepanze evidenti.

Forse potrebbe interessarti anche:  Teorema del Limite Centrale (TLC): Guida Completa all'Analisi Dati con Spiegazione, Esercizi Python e i Casi in cui Fallisce

Non si tratta di un errore, ma di tre scelte ingegneristiche fondamentali compiute dalla libreria:

1. Il bug delle parole monofonemiche italiane (La trappola nascosta)

Se osservate attentamente le colonne del DataFrame generato da scikit-learn, noterete che le parole "è", "e", "o", "a" sono completamente scomparse.

Il motivo risiede nella regex di tokenizzazione nativa di scikit-learn: token_pattern=r'(?u)\b\w\w+\b'. Questa espressione seleziona solo parole composte da almeno due caratteri. Se per la lingua inglese questo elimina solo rumore (come la “a” indefinita), in italiano taglia fuori il verbo essere (“è”) e le congiunzioni (“e”), alterando la metrica se non viene configurata esplicitamente una regex personalizzata.

2. La normalizzazione euclidea [math]L_2[/math]

Il nostro script artigianale divideva il conteggio per il totale delle parole del singolo documento. Scikit-learn fa un passo statistico successivo: applica la normalizzazione euclidea [math]L_2[/math] al vettore finale del documento. Il punteggio di ogni parola viene diviso per la radice quadrata della somma dei quadrati di tutti i punteggi TF-IDF del documento:

[math]\displaystyle v_{\text{norm}} = \frac{v}{\sqrt{\sum_{i=1}^{n} v_i^2}}[/math]

Questo assicura che il vettore risultante abbia una lunghezza pari a 1, eliminando matematicamente qualsiasi distorsione legata alla lunghezza del testo quando si calcola la somiglianza tra documenti (Cosinus Similarity).

3. Algoritmo di Smoothing modificato

Mentre noi abbiamo implementato una formula scolastica, scikit-learn modifica il logaritmo naturale per evitare che i termini con frequenza di documento pari a zero restituiscano un valore indefinito, sommando 1 costante sia al numeratore che al denominatore della frazione prima di calcolare il logaritmo, e aggiungendo un ulteriore 1 al risultato finale.


Tabella comparativa: Implementazione manuale vs Scikit-learn

Caratteristica Implementazione Manuale Scikit-learn
Tokenizzazione Regex personalizzata, cattura anche le parola singole e apostrofi Regex standard inglese: ignora parole di una sola lettera
Normalizzazione TF grezzo (conteggio / parole totali del documento) Normalizzazione [math]L_2[/math] del vettore TF-IDF finale
Smoothing IDF Formula base: log((N+1)/(df+1)) + 1 Parametro smooth_idf=True (default): somma 1 al conteggio di documenti
Gestione delle stop-word Set manuale personalizzabile Parametro stop_words per filtrare liste predefinite
Memoria Dizionario Python (occupa molta RAM) Matrice sparsa scipy.sparse (ottimizzata per memoria)
Velocità Lenta su grandi volumi (Python puro) Molto veloce (backend in C / Cython)

In Sintesi:

  • Sviluppare l’algoritmo a mano serve a capire il concetto ed è utile quando vi trovate in ambienti embedded o AWS Lambda dove non potete (o non volete) importare pacchetti pesanti come scikit-learn.
  • Utilizzare TfidfVectorizer è d’obbligo quando dovete processare matrici di migliaia di documenti, sfruttando la velocità computazionale del codice scritto in C sottostante la libreria e l’ottimizzazione della memoria garantita dalle matrici sparse, avendo però la cura di sovrascrivere il parametro token_pattern per non perdere pezzi fondamentali della grammatica italiana.

06 — Elaborazione computazionale in streaming su grandi volumi (RAM Costante)

Quando si analizzano log di server o dump di dati da svariati gigabyte, caricare tutto in memoria causa un inevitabile MemoryError. Sfruttiamo i generatori (yield) e gli heap binari per mantenere l’impatto sulla RAM piatto e indipendente dalla dimensione del file.

import heapq
import random
import re
from collections import Counter

def genera_righe_fittizie(n_righe=100000):
    # Sfrutta lo 'yield' per emulare la lettura di un file riga per riga senza allocare la RAM
    vocabolario_tecnico = ["python", "dati", "modello", "rete", "algoritmo",
                          "addestramento", "test", "accuratezza", "errore", "gradiente"]
    for _ in range(n_righe):
        yield " ".join(random.choices(vocabolario_tecnico, k=8))

def conteggio_in_streaming(iterabile_righe):
    contatore_globale = Counter()
    for riga in iterabile_righe:
        # Aggiorna il contatore un pezzo alla volta. In memoria risiede solo la riga corrente
        contatore_globale.update(re.findall(r"[a-z]+", riga.lower()))
    return contatore_globale

def estrai_top_k_efficiente(contatore_frequenze, k):
    # heap di dimensione k -> Complessità O(n log k) invece dell'O(n log n) di un ordinamento totale
    return heapq.nlargest(k, contatore_frequenze.items(), key=lambda x: x[1])

# Esecuzione
flusso_dati = genera_righe_fittizie(n_righe=200000)
frequenze_totali = conteggio_in_streaming(flusso_dati)
top_5 = estrai_top_k_efficiente(frequenze_totali, 5)
print(top_5)

# >>> OUTPUT:
# [('algoritmo', 160534), ('test', 160421), ('modello', 160311), ('gradiente', 160122), ('dati', 160054)]
#
# Perché l'output è interessante:
# Abbiamo processato 1.6 milioni di parole (200.000 righe da 8 parole ciascuna) in circa
# un secondo. La RAM utilizzata è rimasta nell'ordine dei kilobyte. Inoltre, l'estrazione
# tramite `heapq.nlargest evita l'ordinamento completo di un vocabolario potenzialmente # immenso, focalizzando lo sforzo computazionale solo sui K elementi richiesti. 

Il MemoryError non è un semplice imprevisto, è un rito di passaggio. Prima o poi, ogni data analyst si scontra con il limite fisico dell’hardware.

Immaginiamo la classica scena: Marco, analista junior, deve estrarre le parole chiave dai log di un server web dell’ultimo anno. Il file pesa 45 GB. Il suo laptop ha 16 GB di RAM. Marco scrive un banale with open('log.txt') as f: testo = f.read(). Preme invio. La ventola inizia a urlare, il sistema operativo va in panico cercando di usare il disco fisso come memoria virtuale (swap), e dopo cinque minuti di agonia, lo script muore: MemoryError.

Giulia, analista senior, deve fare la stessa cosa. Scrive uno script che impiega un po’ di tempo a girare, ma la ventola del suo computer non fa una piega, e la memoria RAM occupata rimane fissa a una manciata di megabyte dall’inizio alla fine.

Il segreto di Giulia non è un computer più potente, ma un’architettura del software pensata per i flussi continui: lo streaming e i generatori.

Ecco una scomposizione anatomica del perché l’Esercizio 06 funziona, pensata per chi sta iniziando a fare sul serio con i dati.

1. Il problema del caricamento sincrono (L’approccio di Marco)

Quando usiamo i metodi tradizionali per leggere un file (come .read() o .readlines()), stiamo dicendo a Python: “Prendi tutto il contenuto che si trova sul disco rigido e copialo istantaneamente nella memoria RAM del computer”.

Forse potrebbe interessarti anche:  Dal Codice al Cash Flow: Il Modello Economico-Finanziario di una Startup AI

La RAM è velocissima, ma limitata. Se il file è più grande della RAM disponibile, il travaso fallisce. Anche se usiamo strutture dati efficienti come il Counter per il conteggio, non arriveremo mai a quel punto del codice, perché il programma esploderà prima, in fase di lettura.

2. La soluzione: I Generatori e il comando yield (L’approccio di Giulia)

Un generatore è una funzione che non restituisce un singolo valore per poi terminare (come fa return), ma produce una sequenza di valori nel tempo, uno alla volta, mettendosi in pausa tra un valore e l’altro.

In Python, la parola chiave per creare questo comportamento è yield.

Pensate a yield come a un nastro trasportatore industriale:

  1. Python legge solo la prima riga del file di log dal disco rigido.
  2. La carica in RAM (pochi kilobyte).
  3. yield consegna questa singola riga al resto del programma (es. al nostro estrattore di parole).
  4. La riga viene elaborata, contata, e poi cancellata dalla memoria.
  5. Python legge la seconda riga.
  6. Il ciclo si ripete. Che il file abbia 100 righe o 50 miliardi di righe, il consumo di memoria rimane identico e piatto. Il nostro script non deve mai “abbracciare” l’intero file, lo consuma a piccoli morsi.

3. Il collo di bottiglia dell’ordinamento

Risolto il problema della lettura, ne emerge un secondo. Abbiamo aggiornato il nostro Counter riga per riga, ma ora abbiamo un vocabolario di 2 milioni di parole distinte in memoria, con le rispettive frequenze.

Se applichiamo il metodo standard di Marco per trovare le 5 parole più usate, useremo sorted(freq.items()).

Matematicamente, l’algoritmo di ordinamento di Python (Timsort) ha una complessità temporale di [math]O(N \log N)[/math], dove [math]N[/math] è il numero di elementi. Ordinare un dizionario di 2 milioni di voci richiede tempo e parecchia memoria aggiuntiva per creare la copia della lista ordinata, per poi buttare via 1.999.995 elementi e tenere solo i primi 5.

È uno spreco di risorse inaccettabile.

4. L’eleganza dell’Heap Binario (heapq)

È qui che entra in gioco la libreria heapq e il concetto di coda di priorità.

Invece di ordinare tutti i 2 milioni di elementi, Giulia usa heapq.nlargest(5, freq.items(), key=lambda x: x[1]).

Come funziona concettualmente?

Immaginate un piccolo recinto che può contenere solo 5 elementi.

Mentre Python scorre i 2 milioni di parole estratte dal generatore, applica questa logica brutalmente semplice:

  1. Riempi il recinto con le prime 5 parole che trovi.
  2. Ordinale internamente (il più debole è vicino al cancello).
  3. Quando arriva la sesta parola, confrontala solo con il più debole nel recinto.
  4. Se la nuova parola è meno frequente, scartala immediatamente.
  5. Se è più frequente, butta fuori il debole e fai entrare la nuova parola, riorganizzando il piccolo recinto.

Alla fine della fiera, non abbiamo mai ordinato l’intero vocabolario. Abbiamo solo fatto competere ogni parola per un posto nei Top 5.

La complessità temporale crolla a [math]O(N \log k)[/math], dove [math]N[/math] sono i milioni di parole totali e [math]k[/math] è 5. Il risparmio computazionale è abissale.

Il succo del discorso per i Data Analyst

Se i dati stanno su un foglio Excel, potete ignorare l’ingegneria del software. Se i dati provengono dai log di produzione di una multinazionale, scrivere codice Python senza comprendere le implicazioni spaziali (RAM) e temporali (CPU) trasformerà una semplice operazione di estrazione in un incubo operativo.

Il passaggio da Junior a Senior non si misura nel numero di librerie di Machine Learning che conoscete, ma nella capacità di far sopravvivere il vostro codice in ambienti ostili. Lo streaming e gli heap sono il kit di sopravvivenza base per questo viaggio.


Tavola sinottica delle soluzioni: quando usare cosa

Esigenza Strumento Complessità
Conteggio semplice e veloce Dizionario con .get() o Counter [math]O(n)[/math]
Top-N con criteri di ordinamento sorted() con chiave a tupla [math]O(n \log n)[/math]
Testo “sporco” (punteggiatura, apostrofi) Tokenizzazione con regex [math]O(n)[/math]
Solo parole di contenuto Filtro stop-word prima del conteggio [math]O(n)[/math]
Pattern e sequenze nel testo Bigrammi / n-grammi con zip [math]O(n)[/math]
Cosa caratterizza un documento in un corpus TF-IDF [math]O(n)[/math]
Volumi troppo grandi per la RAM Generatori + heapq.nlargest [math]O(n \log k)[/math]

Conclusioni

Contare i termini di un testo è un’operazione computazionalmente banale se confinata a piccoli volumi, ma si trasforma in una sfida ingegneristica non appena entrano in gioco contesti di produzione, vincoli di memoria e necessità di estrazione semantica.

Evitare le trappole prestazionali come la combinazione ingenua di set() e count(), sfruttare la potenza delle espressioni regolari e saper pesare statisticamente i dati tramite la logica del TF-IDF costituiscono i passaggi fondamentali per chiunque decida di strutturare pipeline di computazione testuale solide ed efficienti.

 

Pubblicità