Analisi dei Sogni per la Città di Bari¶
Introduzione¶
Questo notebook analizza le risposte alla domanda aperta "Qual è il tuo sogno per la città di Bari?" raccolta tramite un questionario.
Obiettivi dell'analisi:¶
- Identificare temi comuni nelle risposte
- Raggruppare automaticamente le risposte simili (clustering)
- Estrarre le parole chiave più significative per ogni gruppo
Metodologia:¶
Utilizziamo tecniche di Natural Language Processing (NLP) e Machine Learning per:
- Pulire e preparare il testo delle risposte
- Identificare automaticamente i temi principali (Topic Modeling)
- Raggruppare le risposte simili (Clustering)
1. Importazione delle Librerie¶
Prima di tutto, importiamo tutte le librerie necessarie per l'analisi. Ogni libreria ha uno scopo specifico:
- pandas: per gestire i dati in formato tabellare
- spacy: per l'analisi del linguaggio naturale in italiano
- nltk: per la tokenizzazione e rimozione delle stopwords
- BERTopic: per il topic modeling avanzato
- SentenceTransformer: per convertire il testo in rappresentazioni numeriche
# Librerie base per gestione dati e configurazione
import pandas as pd
import json # Per salvare i risultati in formato JSON
import re # Per operazioni avanzate sui testi (espressioni regolari)
import warnings
warnings.filterwarnings('ignore') # Nasconde i messaggi di warning per una visualizzazione più pulita
# Librerie per il preprocessing del testo italiano
import spacy # Analisi avanzata del linguaggio naturale
import nltk # Toolkit per il linguaggio naturale
from nltk.corpus import stopwords # Per rimuovere parole comuni come "il", "di", "che"
from nltk.stem import SnowballStemmer # Per ridurre le parole alla loro radice
from nltk.tokenize import word_tokenize, sent_tokenize # Per dividere il testo in parole e frasi
# Librerie per topic modeling e clustering
from bertopic import BERTopic # Algoritmo moderno per identificare automaticamente i temi
# Librerie per convertire il testo in numeri (embeddings)
from sentence_transformers import SentenceTransformer
2. Classe per la Pulizia del Testo¶
Questa sezione definisce una classe specializzata per pulire e preparare il testo italiano per l'analisi.
Cosa fa la pulizia del testo:¶
- Rimuove elementi non utili: URL, email, punteggiatura eccessiva
- Standardizza il formato: tutto in minuscolo, spazi uniformi
- Identifica le parole importanti: rimuove articoli, preposizioni e altre parole comuni
- Estrae informazioni utili: nomi di persone, luoghi, concetti importanti
class ItalianTextPreprocessor:
"""
Classe specializzata per la pulizia e preparazione del testo italiano
Prima dell'analisi, il testo deve essere "pulito" per ottenere risultati migliori
"""
def __init__(self, nlp_model=None):
"""Inizializza il preprocessore con i modelli necessari"""
self.nlp = nlp_model # Modello spaCy per l'italiano
self.stopwords = italian_stopwords # Parole da ignorare ("il", "di", "che", ecc.)
self.stemmer = stemmer # Strumento per ridurre le parole alla radice
def clean_text(self, text):
"""Pulizia base del testo - rimuove elementi che possono disturbare l'analisi"""
# Converte tutto in minuscolo per uniformità
text = text.lower()
# Rimuove URL, email e menzioni social (non utili per i sogni dei cittadini)
text = re.sub(r'http\S+|www\S+|https\S+', '', text, flags=re.MULTILINE)
text = re.sub(r'\S*@\S*\s?', '', text)
text = re.sub(r'@\w+', '', text)
# Rimuove punteggiatura eccessiva e la sostituisce con spazi
text = re.sub(r'[^\w\s]', ' ', text)
# Rimuove numeri (spesso non rilevanti per i temi)
text = re.sub(r'\d+', '', text)
# Rimuove spazi multipli e pulisce
text = re.sub(r'\s+', ' ', text).strip()
return text
def tokenize_and_filter(self, text):
"""Divide il testo in parole singole e rimuove quelle non significative"""
# Divide il testo in parole singole
tokens = word_tokenize(text, language='italian')
# Mantiene solo parole significative:
# - Lunghe almeno 3 caratteri
# - Non sono stopwords ("il", "di", "che", ecc.)
# - Contengono solo lettere (no numeri o simboli)
filtered_tokens = [
token for token in tokens
if len(token) > 2
and token not in self.stopwords
and token.isalpha()
]
return filtered_tokens
def extract_entities(self, text):
"""Trova nomi di persone, luoghi, organizzazioni nel testo"""
if self.nlp is None:
return []
doc = self.nlp(text)
# Estrae entità nominate (nomi propri, luoghi, organizzazioni)
entities = [(ent.text, ent.label_) for ent in doc.ents]
return entities
def extract_pos_tags(self, text):
"""Identifica sostantivi, aggettivi e verbi (le parti più significative del discorso)"""
if self.nlp is None:
return []
doc = self.nlp(text)
# Mantiene solo sostantivi, aggettivi e verbi (le parole più informative)
pos_tags = [(token.text, token.pos_) for token in doc
if token.pos_ in ['NOUN', 'ADJ', 'VERB']]
return pos_tags
def get_ngrams(self, tokens, n=2):
"""Crea combinazioni di parole consecutive (es: "centro storico", "trasporto pubblico")"""
from nltk.util import ngrams
return list(ngrams(tokens, n))
def preprocess_full(self, text):
"""Esegue tutti i passaggi di pulizia e analisi del testo"""
cleaned = self.clean_text(text)
tokens = self.tokenize_and_filter(cleaned)
stemmed = [self.stemmer.stem(token) for token in tokens] # Riduce le parole alla radice
return {
'cleaned_text': cleaned,
'tokens': tokens,
'stemmed_tokens': stemmed,
'entities': self.extract_entities(text),
'pos_tags': self.extract_pos_tags(text)
}
3. Caricamento e Preparazione dei Dati¶
In questa sezione carichiamo le risposte alla domanda "Qual è il tuo sogno per la città di Bari?" da un file di testo.
Struttura dei dati:¶
- Ogni riga del file contiene una risposta
- Le risposte vengono caricate in una tabella (DataFrame) per facilitare l'analisi
- Ogni risposta riceve un ID univoco per il tracciamento
# Carica le risposte dal file di testo
# Ogni riga contiene una risposta alla domanda sui sogni per Bari
file_path = "sogni.txt"
with open(file_path, 'r', encoding='utf-8') as file:
lines = file.readlines()
# Pulisce le righe rimuovendo spazi extra e righe vuote
# Mantiene solo le risposte che contengono effettivamente del testo
texts = [line.strip() for line in lines if line.strip()]
# Crea una tabella (DataFrame) per organizzare meglio i dati
# Ogni riga rappresenta una risposta di un cittadino
df = pd.DataFrame({
'response': texts, # Il testo originale della risposta
'id': range(len(texts)) # Un numero identificativo per ogni risposta
})
4. Preparazione degli Strumenti di Analisi¶
Prima di analizzare il testo, dobbiamo preparare gli strumenti specializzati per l'italiano:
Strumenti utilizzati:¶
- spaCy italiano: modello pre-addestrato per comprendere la grammatica italiana
- Stopwords italiane: lista di parole comuni da ignorare ("il", "la", "di", "che", ecc.)
- Stemmer italiano: strumento per ridurre le parole alla loro forma base
# Carica il modello spaCy per l'italiano
# Questo modello "comprende" la grammatica e la struttura dell'italiano
nlp = spacy.load("it_core_news_sm")
# Scarica i dati necessari per NLTK (se non già presenti)
nltk.download('stopwords', quiet=True) # Lista delle parole comuni da ignorare
nltk.download('punkt', quiet=True) # Strumento per dividere il testo in frasi e parole
# Carica le stopwords italiane
# Queste sono parole molto comuni ("il", "di", "che") che non aiutano a distinguere i temi
italian_stopwords = set(stopwords.words('italian'))
# Inizializza lo stemmer per l'italiano
# Riduce le parole alla loro radice: "mangiare", "mangio" -> "mangi"
stemmer = SnowballStemmer('italian')
5. Pulizia e Analisi del Testo¶
Ora applichiamo la pulizia del testo a tutte le risposte. Questo passaggio è cruciale perché:
Perché pulire il testo:¶
- Standardizza il formato: tutte le risposte vengono elaborate allo stesso modo
- Rimuove il "rumore": elimina elementi che possono confondere l'analisi
- Identifica le parole chiave: mantiene solo le parole più significative
- Prepara per l'algoritmo: converte il testo in un formato che l'algoritmo può analizzare
# Inizializza il nostro strumento di pulizia del testo
preprocessor = ItalianTextPreprocessor(nlp_model=nlp)
# Applica la pulizia completa a ogni risposta
processed_data = []
# Per ogni risposta nel nostro dataset
for i, text in enumerate(texts):
# Esegue tutti i passaggi di pulizia e analisi
result = preprocessor.preprocess_full(text)
# Aggiunge informazioni aggiuntive per il tracciamento
result['original_text'] = text # Mantiene il testo originale
result['id'] = i # Assegna un ID univoco
processed_data.append(result)
# Aggiunge tutti i dati processati alla nostra tabella principale
# Ora ogni risposta ha sia la versione originale che quella pulita
df['cleaned_text'] = [item['cleaned_text'] for item in processed_data] # Testo pulito
df['tokens'] = [item['tokens'] for item in processed_data] # Parole singole significative
df['stemmed_tokens'] = [item['stemmed_tokens'] for item in processed_data] # Parole ridotte alla radice
df['entities'] = [item['entities'] for item in processed_data] # Nomi propri, luoghi, ecc.
df['pos_tags'] = [item['pos_tags'] for item in processed_data] # Sostantivi, aggettivi, verbi
6. Topic Modeling e Clustering¶
Questa è la parte più importante dell'analisi: identifichiamo automaticamente i temi principali nelle risposte sui sogni per Bari.
Come funziona il Topic Modeling:¶
- Converte il testo in numeri: l'algoritmo non può leggere il testo, ma può analizzare rappresentazioni numeriche
- Trova pattern simili: raggruppa automaticamente risposte che parlano di temi simili
- Identifica parole chiave: per ogni gruppo, trova le parole più caratteristiche
- Crea i cluster: organizza le risposte in gruppi tematici
Utilizziamo BERTopic perché:¶
- È un algoritmo moderno e molto accurato
- Funziona bene con testi in italiano
- Non richiede di definire in anticipo quanti temi cercare
- Produce risultati interpretabili
# Prepara i testi per l'analisi dei topic
# Unisce le parole pulite di ogni risposta in un unico testo
texts_for_modeling = [' '.join(tokens) for tokens in df['tokens']]
# Configura BERTopic per l'analisi dei temi
# Inizializza il modello per convertire il testo in numeri
# Usiamo un modello multilingue che funziona bene con l'italiano
sentence_model = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')
# Configura BERTopic con le impostazioni ottimali per il nostro caso
topic_model = BERTopic(
language="italian", # Specifica che stiamo analizzando testo italiano
embedding_model=sentence_model, # Usa il nostro modello per convertire testo in numeri
min_topic_size=5, # Un tema deve contenere almeno 5 risposte per essere considerato
verbose=True, # Mostra il progresso dell'analisi
)
7. Esecuzione dell'Analisi¶
Ora eseguiamo l'analisi vera e propria. Questo processo:
- Analizza ogni risposta e la converte in una rappresentazione numerica
- Trova gruppi di risposte simili basandosi sul contenuto
- Assegna ogni risposta a un tema principale
- Calcola la probabilità di appartenenza di ogni risposta al suo tema
- Identifica le parole chiave più rappresentative per ogni tema
# Esegue l'analisi dei temi su tutte le risposte
# Questo è il passaggio principale che identifica automaticamente i temi
topics, probs = topic_model.fit_transform(texts_for_modeling)
# Aggiunge i risultati alla nostra tabella
df['bertopic'] = topics # A quale tema appartiene ogni risposta
df['bertopic_prob'] = probs # Quanto è "sicuro" l'algoritmo dell'assegnazione
# Mostra i risultati principali
print(f"🎯 RISULTATI DELL'ANALISI:")
print(f"📊 BERTopic ha identificato {len(topic_model.get_topic_info())} temi diversi nelle risposte")
print(f"📝 Questo significa che i cittadini baresi hanno almeno {len(topic_model.get_topic_info())} tipi diversi di sogni per la loro città!")
# Ottiene informazioni dettagliate sui temi identificati
topic_info = topic_model.get_topic_info()
2025-08-12 02:15:01,459 - BERTopic - Embedding - Transforming documents to embeddings. Batches: 100%|██████████| 15/15 [00:01<00:00, 7.52it/s] 2025-08-12 02:15:03,467 - BERTopic - Embedding - Completed ✓ 2025-08-12 02:15:03,468 - BERTopic - Dimensionality - Fitting the dimensionality reduction algorithm 2025-08-12 02:15:13,411 - BERTopic - Dimensionality - Completed ✓ 2025-08-12 02:15:13,413 - BERTopic - Cluster - Start clustering the reduced embeddings 2025-08-12 02:15:13,443 - BERTopic - Cluster - Completed ✓ 2025-08-12 02:15:13,449 - BERTopic - Representation - Fine-tuning topics using representation models. 2025-08-12 02:15:13,497 - BERTopic - Representation - Completed ✓
🎯 RISULTATI DELL'ANALISI: 📊 BERTopic ha identificato 25 temi diversi nelle risposte 📝 Questo significa che i cittadini baresi hanno almeno 25 tipi diversi di sogni per la loro città!
8. Elaborazione e Salvataggio dei Risultati¶
Ora organizziamo i risultati in un formato facilmente comprensibile e li salviamo per analisi future.
Cosa contiene il file finale:¶
- Lista di tutti i temi identificati automaticamente
- Parole chiave più rappresentative per ogni tema
- Risposte originali che appartengono a ogni tema
- Statistiche su quante risposte appartengono a ogni tema
- Frasi specifiche che contengono le parole chiave
Il file JSON risultante sarà utile per creare la rappresentazione in html
def get_streamlined_representative_docs(topic_model, texts_for_modeling, df):
"""
Crea un'analisi dettagliata e facilmente comprensibile dei temi identificati
Questa funzione organizza i risultati in modo che siano facilmente leggibili
anche da persone che non conoscono i dettagli tecnici dell'analisi
"""
streamlined_topic_info = []
topic_info = topic_model.get_topic_info()
# Analizza ogni tema identificato dall'algoritmo
for _, row in topic_info.iterrows():
topic_id = row['Topic']
# Salta il tema "outlier" (risposte che non appartengono a nessun gruppo specifico)
if topic_id == -1:
continue
topic_name = row['Name'] # Nome automatico del tema
topic_count = row['Count'] # Quante risposte appartengono a questo tema
representative_words = row['Representation'] # Parole più caratteristiche del tema
# Trova tutte le risposte che appartengono a questo tema
topic_docs_indices = df[df['bertopic'] == topic_id].index.tolist()
topic_docs = [texts_for_modeling[i] for i in topic_docs_indices] # Versioni pulite
original_docs = [df.iloc[i]['response'] for i in topic_docs_indices] # Versioni originali
probabilities = [df.iloc[i]['bertopic_prob'] for i in topic_docs_indices] # Probabilità di appartenenza
# Analizza ogni parola chiave del tema
streamlined_word_analysis = {}
# Considera le 10 parole più rappresentative del tema
for word in representative_words[:10]:
word_docs = []
word_occurrences = 0
# Per ogni risposta in questo tema
for i, (processed_doc, original_doc, prob) in enumerate(zip(topic_docs, original_docs, probabilities)):
# Conta quante volte appare questa parola chiave
word_count = processed_doc.lower().count(word.lower())
if word_count > 0:
word_occurrences += word_count
# Trova le frasi specifiche che contengono questa parola
sentences_with_word = []
sentences = sent_tokenize(original_doc, language='italian')
for sentence in sentences:
if word.lower() in sentence.lower():
sentences_with_word.append(sentence.strip())
# Salva le informazioni per questa risposta
word_docs.append({
'testo originale': original_doc,
'testo processato': processed_doc,
'index': topic_docs_indices[i],
'lunghezza testo': len(original_doc),
'bertopic prob': float(prob),
'risposte con parola': sentences_with_word
})
# Ordina le risposte per probabilità (le più "tipiche" del tema prima)
word_docs.sort(key=lambda x: (x['bertopic prob'], processed_doc.lower().count(word.lower())), reverse=True)
# Organizza le informazioni per questa parola chiave
streamlined_word_analysis[word] = {
'quantità risposte rappresentative': len(word_docs),
'risposte rappresentative': [
{
'testo originale': doc['testo originale'],
'testo processato': doc['testo processato'],
'lunghezza testo': doc['lunghezza testo'],
'bertopic prob': doc['bertopic prob']
}
for doc in word_docs
],
}
# Crea l'entry finale per questo tema
topic_detail = {
# Pulisce il nome del tema rendendolo più leggibile
topic_name.title().replace(f'{topic_id}_', '').replace('_', ' ') : {
'quantità risposte': int(topic_count),
'parole chiavi': streamlined_word_analysis
}
}
streamlined_topic_info.append(topic_detail)
return streamlined_topic_info
# Genera l'analisi completa
print("🔄 Elaborazione dei risultati in corso...")
streamlined_analysis = get_streamlined_representative_docs(topic_model, texts_for_modeling, df)
# Salva i risultati in un file JSON facilmente leggibile
with open('analisi_topic.json', 'w', encoding='utf-8') as file:
json.dump(streamlined_analysis, file, ensure_ascii=False, indent=4)
print("✅ Analisi completata!")
print(f"📁 I risultati sono stati salvati nel file 'analisi_topic.json'")
print(f"📊 Puoi ora esplorare i {len(streamlined_analysis)} temi identificati sui sogni dei baresi per la loro città")
🔄 Elaborazione dei risultati in corso... ✅ Analisi completata! 📁 I risultati sono stati salvati nel file 'analisi_topic.json' 📊 Puoi ora esplorare i 24 temi identificati sui sogni dei baresi per la loro città