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:

  1. Pulire e preparare il testo delle risposte
  2. Identificare automaticamente i temi principali (Topic Modeling)
  3. 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
In [1]:
# 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
In [2]:
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
In [3]:
# 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()]
In [4]:
# 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
In [5]:
# Carica il modello spaCy per l'italiano
# Questo modello "comprende" la grammatica e la struttura dell'italiano
nlp = spacy.load("it_core_news_sm")
In [6]:
# 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
In [7]:
# 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)
In [8]:
# 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:¶

  1. Converte il testo in numeri: l'algoritmo non può leggere il testo, ma può analizzare rappresentazioni numeriche
  2. Trova pattern simili: raggruppa automaticamente risposte che parlano di temi simili
  3. Identifica parole chiave: per ogni gruppo, trova le parole più caratteristiche
  4. 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
In [9]:
# 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']]
In [10]:
# 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:

  1. Analizza ogni risposta e la converte in una rappresentazione numerica
  2. Trova gruppi di risposte simili basandosi sul contenuto
  3. Assegna ogni risposta a un tema principale
  4. Calcola la probabilità di appartenenza di ogni risposta al suo tema
  5. Identifica le parole chiave più rappresentative per ogni tema
In [11]:
# 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

In [12]:
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à