In [1]:
import os
from dotenv import load_dotenv, find_dotenv

load_dotenv(find_dotenv())
Out[1]:
True
In [2]:
ilab_path = os.path.join(os.path.expanduser('~'),'ILAB_DATA')
istat_path = os.path.join(ilab_path,'ISTAT','DATA')
work_path = os.path.join(ilab_path,'OCPR','DATA')
out_path = os.path.join(ilab_path,'OCPR','OUT')
In [3]:
CLAUDE_API_KEY = os.getenv('ANTHROPIC_API_KEY_GK')
geoparquet_path=os.path.join(work_path,'grid_06_adv.geoparquet')
metadata_path=os.path.join(out_path,'metadati_dataset_06.csv')
In [4]:
import anthropic
import json

class ClaudeClient:
    def __init__(self, api_key: str):
        self.client = anthropic.Anthropic(api_key=api_key)
    
    def query_interpreter(self, user_query: str, available_columns: dict) -> dict:
        prompt = f"""
Analizza questa domanda sui dati geografici e restituisci un JSON con le operazioni da eseguire.

Colonne disponibili per categoria:
{json.dumps(available_columns, indent=2)}

Domanda utente: "{user_query}"

Restituisci solo un JSON con questa struttura:
{{
    "operation": "filter|aggregate|spatial",
    "columns": ["lista_colonne_coinvolte"],
    "conditions": [
        {{"column": "nome_colonna", "operator": ">|<|=|!=", "value": valore}}
    ],
    "aggregation": "count|sum|mean|max|min|null",
    "spatial_operation": "within|distance|intersects|null",
    "explanation": "breve spiegazione operazione"
}}
"""
        
        response = self.client.messages.create(
            model="claude-3-5-sonnet-20241022",
            max_tokens=1000,
            messages=[{"role": "user", "content": prompt}]
        )
        
        try:
            return json.loads(response.content[0].text)
        except:
            return {"error": "Parsing fallito", "raw_response": response.content[0].text}
In [5]:
import pandas as pd
import geopandas as gpd

class DataAnalyzer:
    def __init__(self, geoparquet_path: str, metadata_path: str):
        self.metadata = pd.read_csv(metadata_path)
        self.gdf = gpd.read_parquet(geoparquet_path)
        self.columns_by_category = self._organize_columns()
    
    def _organize_columns(self) -> dict:
        categories = {}
        for _, row in self.metadata.iterrows():
            cat = row['category']
            if cat not in categories:
                categories[cat] = []
            categories[cat].append({
                'name_adv': row['name_adv'],
                'name': row['name'],
                'description': row['label_adv']
            })
        return categories
    
    def execute_query(self, query_spec: dict):
        try:
            df = self.gdf.copy()
            
            # Applica filtri
            if 'conditions' in query_spec and query_spec['conditions']:
                for condition in query_spec['conditions']:
                    col = condition['column']
                    op = condition['operator']
                    val = condition['value']
                    
                    if col not in df.columns:
                        continue
                        
                    if op == '>':
                        df = df[df[col] > val]
                    elif op == '<':
                        df = df[df[col] < val]
                    elif op == '=':
                        df = df[df[col] == val]
                    elif op == '!=':
                        df = df[df[col] != val]
            
            # Esegui aggregazione
            if query_spec.get('aggregation'):
                agg = query_spec['aggregation']
                cols = query_spec.get('columns', [])
                
                if agg == 'count':
                    return len(df)
                elif cols and agg in ['sum', 'mean', 'max', 'min']:
                    col = cols[0] if cols[0] in df.columns else df.select_dtypes(include='number').columns[0]
                    return getattr(df[col], agg)()
            
            return len(df)
            
        except Exception as e:
            return f"Errore: {str(e)}"
In [6]:
class IntelligentGeoAgent:
    def __init__(self, claude_api_key: str, geoparquet_path: str, metadata_path: str):
        self.claude = ClaudeClient(claude_api_key)
        self.analyzer = DataAnalyzer(geoparquet_path, metadata_path)
    
    def answer_question(self, user_question: str) -> str:
        # Interpreta la domanda con Claude
        query_spec = self.claude.query_interpreter(
            user_question, 
            self.analyzer.columns_by_category
        )
        
        if 'error' in query_spec:
            return f"Non riesco a interpretare la domanda: {query_spec['error']}"
        
        # Esegui la query sui dati
        result = self.analyzer.execute_query(query_spec)
        
        # Genera risposta finale con Claude
        return self._generate_response(user_question, query_spec, result)
    
    def _generate_response(self, question: str, query_spec: dict, result) -> str:
        prompt = f"""
Domanda originale: "{question}"
Operazione eseguita: {query_spec.get('explanation', 'N/A')}
Risultato: {result}

Genera una risposta naturale e comprensibile per l'utente.
"""
        
        response = self.claude.client.messages.create(
            model="claude-3-5-sonnet-20241022",
            max_tokens=300,
            messages=[{"role": "user", "content": prompt}]
        )
        
        return response.content[0].text
In [7]:
import folium
import numpy as np
from folium import plugins

class MapVisualizer:
    def __init__(self, gdf, metadata):
        self.gdf = gdf
        self.metadata = metadata
        self.center_lat, self.center_lon = self._get_center()
    
    def _get_center(self):
        # Assicurati che sia in WGS84
        if self.gdf.crs != 'EPSG:4326':
            gdf_wgs84 = self.gdf.to_crs('EPSG:4326')
        else:
            gdf_wgs84 = self.gdf
            
        bounds = gdf_wgs84.total_bounds
        center_lat = (bounds[1] + bounds[3]) / 2
        center_lon = (bounds[0] + bounds[2]) / 2
        return center_lat, center_lon
    
    def visualize_query_result(self, query_spec: dict, filtered_gdf=None, title="Risultato Query"):
        # Crea mappa base
        m = folium.Map(
            location=[self.center_lat, self.center_lon],
            zoom_start=10,
            tiles='OpenStreetMap'
        )
        
        if filtered_gdf is None or len(filtered_gdf) == 0:
            folium.Marker(
                [self.center_lat, self.center_lon],
                popup="Nessun risultato trovato",
                icon=folium.Icon(color='red')
            ).add_to(m)
            return m
        
        # Determina tipo visualizzazione
        viz_type = self._determine_visualization_type(query_spec)
        
        if viz_type == "choropleth":
            self._add_choropleth(m, filtered_gdf, query_spec)
        elif viz_type == "heatmap":
            self._add_heatmap(m, filtered_gdf, query_spec)
        else:
            self._add_basic_markers(m, filtered_gdf)
        
        # Aggiungi titolo
        title_html = f'<h3 align="center">{title}</h3>'
        m.get_root().html.add_child(folium.Element(title_html))
        
        return m
    
    def _determine_visualization_type(self, query_spec):
        if query_spec.get('aggregation') in ['sum', 'mean']:
            return "choropleth"
        elif len(query_spec.get('columns', [])) > 0:
            return "heatmap"
        else:
            return "markers"
    
    def _add_choropleth(self, m, gdf, query_spec):
        columns = query_spec.get('columns', [])
        if not columns:
            return
        
        col = columns[0]
        if col not in gdf.columns:
            return
        
        # Assicura conversione corretta a WGS84
        if gdf.crs != 'EPSG:4326':
            gdf_wgs84 = gdf.to_crs('EPSG:4326')
        else:
            gdf_wgs84 = gdf.copy()
        
        # Usa indice come chiave se h3_06 non disponibile
        key_col = 'h3_06' if 'h3_06' in gdf.columns else None
        
        if key_col:
            folium.Choropleth(
                geo_data=gdf_wgs84,
                data=gdf_wgs84,
                columns=[key_col, col],
                key_on=f'feature.properties.{key_col}',
                fill_color='YlOrRd',
                fill_opacity=0.7,
                line_opacity=0.2,
                legend_name=self._get_column_description(col)
            ).add_to(m)
        else:
            # Fallback con geometrie semplici
            self._add_basic_polygons(m, gdf_wgs84, col)
    
    def _add_heatmap(self, m, gdf, query_spec):
        # Assicura conversione corretta a WGS84
        if gdf.crs != 'EPSG:4326':
            gdf_wgs84 = gdf.to_crs('EPSG:4326')
        else:
            gdf_wgs84 = gdf.copy()
            
        # Estrai punti centrali delle geometrie
        points = [[geom.centroid.y, geom.centroid.x] for geom in gdf_wgs84.geometry if geom is not None]
        
        # Aggiungi pesi se disponibili
        columns = query_spec.get('columns', [])
        if columns and columns[0] in gdf.columns:
            weights = gdf[columns[0]].fillna(0).tolist()
            heat_data = [[p[0], p[1], w] for p, w in zip(points, weights) if w > 0]
        else:
            heat_data = points
        
        if heat_data:
            plugins.HeatMap(heat_data).add_to(m)
    
    def _add_basic_polygons(self, m, gdf_wgs84, col):
        """Aggiunge poligoni colorati quando choropleth fallisce"""
        # Normalizza valori per colori
        values = gdf_wgs84[col].fillna(0)
        vmin, vmax = values.min(), values.max()
        
        for idx, row in gdf_wgs84.iterrows():
            if row.geometry is None:
                continue
                
            # Colore basato sul valore
            val = row[col] if not pd.isna(row[col]) else 0
            color_intensity = (val - vmin) / (vmax - vmin) if vmax > vmin else 0.5
            color = f"#{int(255 * color_intensity):02x}0000"  # Rosso graduato
            
            folium.GeoJson(
                row.geometry,
                style_function=lambda x, color=color: {
                    'fillColor': color,
                    'color': 'black',
                    'weight': 1,
                    'fillOpacity': 0.7
                },
                popup=f"{self._get_column_description(col)}: {val}"
            ).add_to(m)
    
    def _get_column_description(self, col_name):
        match = self.metadata[self.metadata['name_adv'] == col_name]
        if not match.empty:
            return match.iloc[0]['label_adv']
    def _add_basic_markers(self, m, gdf):
        # Assicura conversione corretta a WGS84  
        if gdf.crs != 'EPSG:4326':
            gdf_wgs84 = gdf.to_crs('EPSG:4326')
        else:
            gdf_wgs84 = gdf.copy()
        
        for idx, row in gdf_wgs84.iterrows():
            if row.geometry is None:
                continue
                
            centroid = row.geometry.centroid
            
            popup_text = f"Cella: {idx}<br>"
            for col in ['popolazione', 'residenti', 'abitanti']:
                if any(col in c.lower() for c in gdf.columns):
                    matching_col = next(c for c in gdf.columns if col in c.lower())
                    popup_text += f"{col.title()}: {row[matching_col]}<br>"
                    break
            
            folium.CircleMarker(
                location=[centroid.y, centroid.x],
                radius=5,
                popup=popup_text,
                color='red',
                fill=True,
                opacity=0.7
            ).add_to(m)
In [8]:
class EnhancedGeoAgent(IntelligentGeoAgent):
    def __init__(self, claude_api_key: str, geoparquet_path: str, metadata_path: str):
        super().__init__(claude_api_key, geoparquet_path, metadata_path)
        self.context = self._build_context()
    
    def _build_context(self) -> str:
        stats = {
            'total_cells': len(self.analyzer.gdf),
            'total_columns': len(self.analyzer.gdf.columns),
            'categories': list(self.analyzer.columns_by_category.keys()),
            'sample_columns': {}
        }
        
        for cat, cols in self.analyzer.columns_by_category.items():
            stats['sample_columns'][cat] = [c['name'] for c in cols[:3]]
        
        return f"""
Dataset: {stats['total_cells']} celle esagonali con {stats['total_columns']} attributi
Categorie: {', '.join(stats['categories'])}
Esempi colonne per categoria: {stats['sample_columns']}
"""
    
    def answer_with_context(self, user_question: str) -> str:
        enhanced_prompt = f"""
Contesto dataset:
{self.context}

Domanda utente: {user_question}

Analizza la domanda considerando il contesto del dataset geografico a griglia esagonale.
"""
        
        query_spec = self.claude.query_interpreter(enhanced_prompt, self.analyzer.columns_by_category)
        
        if 'error' in query_spec:
            return self._suggest_alternatives(user_question)
        
        result = self.analyzer.execute_query(query_spec)
        return self._generate_response(user_question, query_spec, result)
    
    def _suggest_alternatives(self, question: str) -> str:
        prompt = f"""
L'utente ha chiesto: "{question}"

Basandoti su questo dataset geografico:
{self.context}

Suggerisci 3 domande simili che potrebbero essere interessanti e fattibili.
"""
        
        response = self.claude.client.messages.create(
            model="claude-3-sonnet-20240229",
            max_tokens=300,
            messages=[{"role": "user", "content": prompt}]
        )
        
        return f"Non riesco a rispondere direttamente. Ecco alcune alternative:\n{response.content[0].text}"
In [9]:
enhanced_agent = EnhancedGeoAgent(
    claude_api_key=CLAUDE_API_KEY,
    geoparquet_path=geoparquet_path, 
    metadata_path=metadata_path
)

print("✅ Enhanced Agent inizializzato!")
print(f"📊 Contesto dataset:\n{enhanced_agent.context}")
✅ Enhanced Agent inizializzato!
📊 Contesto dataset:

Dataset: 704 celle esagonali con 311 attributi
Categorie: Griglia, Copertura del suolo, Accessibilità trasporti (geoportale sardegna), Demografia, Accessibilità trasporti, Accessibilità cultura, Accessibilità istruzione, Accessibilità ristorazione, Accessibilità sanità, Accessibilità sport, Accessibilità strutture ricettive, Accessibilità sanità - Ospedali
Esempi colonne per categoria: {'Griglia': ['geometry', 'h3_06', 'cell_a_m2'], 'Copertura del suolo': ['agricultural', 'artificial_non_agricultural_vegetated', 'forest_and_semi_natural'], 'Accessibilità trasporti (geoportale sardegna)': ['traffic__15__aeroporto_militare', 'traffic__15__aeroporto_principale', 'traffic__15__aeroporto_secondario'], 'Demografia': ['p_0_14__01', 'p_0_14__11', 'p_0_14__21'], 'Accessibilità trasporti': ['traffic__15__aeroporto', 'traffic__15__bus', 'traffic__15__stazione'], 'Accessibilità cultura': ['traffic__15__biblioteca', 'traffic__15__cinema', 'traffic__15__evento_culturale'], 'Accessibilità istruzione': ['traffic__15__formazione_alternativa', 'traffic__15__formazione_professionale', 'traffic__15__istituti_scolastici'], 'Accessibilità ristorazione': ['traffic__15__altri_servizi_alimentari', 'traffic__15__bar', 'traffic__15__ristorante'], 'Accessibilità sanità': ['traffic__15__centri_sanitari', 'traffic__15__cliniche_mediche', 'traffic__15__servizi_diagnostici'], 'Accessibilità sport': ['traffic__15__altre_attivita_sportive', 'traffic__15__club_e_squadre_sportive', 'traffic__15__fitness_e_allenamento'], 'Accessibilità strutture ricettive': ['traffic__15__strutture_alberghiere', 'traffic__15__strutture_extra_alberghiere', 'traffic__15__total_strutture_ricettive'], 'Accessibilità sanità - Ospedali': ['traffic__15__ao_integrata_con_il_ssn', 'traffic__15__azienda_ospedaliera', 'traffic__15__osped_a_gestione_diretta_presidio_asl']}

In [10]:
# Test con domande varie
domande_test = [
    "Quante celle hanno popolazione superiore a 10000 residenti?",
    "Dove trovo biblioteche raggiungibili in 15 minuti?", 
    "Qual è la media della copertura agricola?",
    "Celle con alta densità di ristoranti",
    "Zone con buona accessibilità agli ospedali"
]

for domanda in domande_test:
    print(f"\n🤔 DOMANDA: {domanda}")
    try:
        risposta = enhanced_agent.answer_with_context(domanda)
        print(f"🤖 RISPOSTA: {risposta}")
    except Exception as e:
        print(f"❌ ERRORE: {e}")
    print("-" * 70)
🤔 DOMANDA: Quante celle hanno popolazione superiore a 10000 residenti?
🤖 RISPOSTA: Analizzando i dati demografici del 2021, ci sono 30 celle che superano i 10.000 residenti. Questo significa che, delle varie aree in cui è suddiviso il territorio, 30 hanno una popolazione particolarmente significativa, con più di diecimila abitanti ciascuna.
----------------------------------------------------------------------

🤔 DOMANDA: Dove trovo biblioteche raggiungibili in 15 minuti?
🤖 RISPOSTA: In base all'analisi effettuata, ci sono 19 aree della città dove puoi trovare almeno una biblioteca raggiungibile comodamente sia a piedi che in auto entro 15 minuti. Questo significa che in queste zone hai la flessibilità di scegliere come raggiungere la biblioteca in base alle tue esigenze, senza dover impiegare troppo tempo per arrivarci. Se desideri dettagli specifici su quali biblioteche si trovano in queste aree o la loro esatta ubicazione, posso fornirti ulteriori informazioni.
----------------------------------------------------------------------

🤔 DOMANDA: Qual è la media della copertura agricola?
🤖 RISPOSTA: La copertura agricola media per cella esagonale è di circa 15,8 milioni di metri quadrati (o 1.584 ettari). Questo valore rappresenta la superficie media dedicata all'agricoltura in ciascuna unità territoriale del dataset analizzato.
----------------------------------------------------------------------

🤔 DOMANDA: Celle con alta densità di ristoranti
🤖 RISPOSTA: In base all'analisi effettuata, sono state identificate 704 zone della città che presentano una concentrazione particolarmente elevata di ristoranti. Queste aree si caratterizzano per avere più di 10 ristoranti raggiungibili a piedi (entro 15 minuti di cammino) e più di 20 ristoranti raggiungibili in auto (entro 15 minuti di guida). Questo suggerisce che si tratta di zone particolarmente vivaci dal punto di vista della ristorazione, con un'ampia scelta di locali sia per chi si muove a piedi che per chi usa l'auto.
----------------------------------------------------------------------

🤔 DOMANDA: Zone con buona accessibilità agli ospedali
🤖 RISPOSTA: In base all'analisi effettuata, sono state identificate 14 zone che offrono una buona accessibilità agli ospedali. Queste aree sono considerate ben servite poiché hanno almeno un ospedale raggiungibile entro 15 minuti di auto e almeno due ospedali raggiungibili entro 30 minuti, tenendo conto del traffico. Questo garantisce sia un accesso rapido per le emergenze che una buona scelta di strutture ospedaliere nel raggio di mezz'ora.
----------------------------------------------------------------------
In [11]:
# Debug per vedere come Claude interpreta le domande
def debug_query(domanda):
    print(f"🔍 DEBUG per: '{domanda}'")
    
    # Vedi interpretazione Claude
    query_spec = enhanced_agent.claude.query_interpreter(
        domanda, 
        enhanced_agent.analyzer.columns_by_category
    )
    
    print(f"📋 Interpretazione Claude:")
    for k, v in query_spec.items():
        print(f"   {k}: {v}")
    
    # Esegui query
    if 'error' not in query_spec:
        result = enhanced_agent.analyzer.execute_query(query_spec)
        print(f"📊 Risultato query: {result}")
    
    return query_spec

# Test debug
debug_query("quante celle hanno più di 10000 abitanti?")
🔍 DEBUG per: 'quante celle hanno più di 10000 abitanti?'
📋 Interpretazione Claude:
   operation: aggregate
   columns: ['istat||pop_tot__21']
   conditions: [{'column': 'istat||pop_tot__21', 'operator': '>', 'value': 10000}]
   aggregation: count
   spatial_operation: None
   explanation: Conta il numero di celle che hanno una popolazione totale nel 2021 superiore a 10000 abitanti
📊 Risultato query: 30
Out[11]:
{'operation': 'aggregate',
 'columns': ['istat||pop_tot__21'],
 'conditions': [{'column': 'istat||pop_tot__21',
   'operator': '>',
   'value': 10000}],
 'aggregation': 'count',
 'spatial_operation': None,
 'explanation': 'Conta il numero di celle che hanno una popolazione totale nel 2021 superiore a 10000 abitanti'}
In [12]:
debug_query('quante celle hanno una popolazione femminile maggiore di 4000?')
🔍 DEBUG per: 'quante celle hanno una popolazione femminile maggiore di 4000?'
📋 Interpretazione Claude:
   operation: aggregate
   columns: ['istat||pop_f__21']
   conditions: [{'column': 'istat||pop_f__21', 'operator': '>', 'value': 4000}]
   aggregation: count
   spatial_operation: None
   explanation: Conta il numero di celle che hanno una popolazione femminile nel 2021 maggiore di 4000 abitanti
📊 Risultato query: 40
Out[12]:
{'operation': 'aggregate',
 'columns': ['istat||pop_f__21'],
 'conditions': [{'column': 'istat||pop_f__21',
   'operator': '>',
   'value': 4000}],
 'aggregation': 'count',
 'spatial_operation': None,
 'explanation': 'Conta il numero di celle che hanno una popolazione femminile nel 2021 maggiore di 4000 abitanti'}
In [13]:
debug_query('Celle con alta densità di ristoranti')
🔍 DEBUG per: 'Celle con alta densità di ristoranti'
📋 Interpretazione Claude:
   operation: filter
   columns: ['ovm_places_ristorazione||traffic__15__ristorante', 'ovm_places_ristorazione||walking__15__ristorante']
   conditions: [{'column': 'ovm_places_ristorazione||traffic__15__ristorante', 'operator': '>', 'value': 10}]
   aggregation: None
   spatial_operation: None
   explanation: Filtra le celle che hanno più di 10 ristoranti raggiungibili in 15 minuti, considerando sia l'accessibilità in auto che a piedi. Questo identifica le aree con alta densità di servizi di ristorazione.
📊 Risultato query: 215
Out[13]:
{'operation': 'filter',
 'columns': ['ovm_places_ristorazione||traffic__15__ristorante',
  'ovm_places_ristorazione||walking__15__ristorante'],
 'conditions': [{'column': 'ovm_places_ristorazione||traffic__15__ristorante',
   'operator': '>',
   'value': 10}],
 'aggregation': None,
 'spatial_operation': None,
 'explanation': "Filtra le celle che hanno più di 10 ristoranti raggiungibili in 15 minuti, considerando sia l'accessibilità in auto che a piedi. Questo identifica le aree con alta densità di servizi di ristorazione."}
In [14]:
# Sessione interattiva
def chat_with_agent(agent):
    print("🚀 Chat con Enhanced GeoAgent - scrivi 'quit' per uscire")
    
    while True:
        domanda = input("\n📍 La tua domanda: ")
        
        if domanda.lower() in ['quit', 'exit', 'stop']:
            print("👋 Arrivederci!")
            break
            
        try:
            risposta = agent.answer_with_context(domanda)
            print(f"\n🤖 {risposta}")
            
        except Exception as e:
            print(f"\n❌ Errore: {e}")

# Per avviare la chat usa: chat_with_agent(enhanced_agent)
In [15]:
#chat_with_agent(enhanced_agent)
In [16]:
class EnhancedGeoAgentWithMap(EnhancedGeoAgent):
    def __init__(self, claude_api_key: str, geoparquet_path: str, metadata_path: str):
        super().__init__(claude_api_key, geoparquet_path, metadata_path)
        self.visualizer = MapVisualizer(self.analyzer.gdf, self.analyzer.metadata)
    
    def answer_with_map(self, user_question: str):
        """Risponde alla domanda e crea mappa del risultato"""
        
        # Interpreta query con Claude
        query_spec = self.claude.query_interpreter(
            user_question, 
            self.analyzer.columns_by_category
        )
        
        if 'error' in query_spec:
            return self._suggest_alternatives(user_question), None
        
        # Esegui query e ottieni risultati filtrati
        filtered_gdf = self._execute_query_with_geodata(query_spec)
        result = len(filtered_gdf) if hasattr(filtered_gdf, '__len__') else filtered_gdf
        
        # Genera risposta testuale
        response_text = self._generate_response(user_question, query_spec, result)
        
        # Crea mappa
        map_viz = self.visualizer.visualize_query_result(
            query_spec, 
            filtered_gdf if hasattr(filtered_gdf, 'geometry') else None,
            title=f"Mappa: {user_question[:50]}..."
        )
        
        return response_text, map_viz
    
    def _execute_query_with_geodata(self, query_spec):
        """Esegue query e restituisce GeoDataFrame filtrato"""
        try:
            gdf = self.analyzer.gdf.copy()
            
            # Applica filtri
            if 'conditions' in query_spec and query_spec['conditions']:
                for condition in query_spec['conditions']:
                    col = condition['column']
                    op = condition['operator']
                    val = condition['value']
                    
                    if col not in gdf.columns:
                        continue
                        
                    if op == '>':
                        gdf = gdf[gdf[col] > val]
                    elif op == '<':
                        gdf = gdf[gdf[col] < val]
                    elif op == '=':
                        gdf = gdf[gdf[col] == val]
                    elif op == '!=':
                        gdf = gdf[gdf[col] != val]
            
            return gdf
            
        except Exception as e:
            print(f"Errore query geodata: {e}")
            return self.analyzer.gdf.head(0)  # GeoDataFrame vuoto
In [17]:
# Setup agent con visualizzazione mappe
map_agent = EnhancedGeoAgentWithMap(
    claude_api_key=CLAUDE_API_KEY,
    geoparquet_path=geoparquet_path,
    metadata_path=metadata_path
)

# Esempio singola query con mappa
def query_with_map(question):
    print(f"🤔 Domanda: {question}")
    
    response, map_viz = map_agent.answer_with_map(question)
    
    print(f"🤖 Risposta: {response}")
    
    if map_viz:
        print("🗺️ Mappa generata! Salvataggio in corso...")
        map_viz.save('risultato_query.html')
        print("✅ Mappa salvata come 'risultato_query.html'")
        return map_viz
    else:
        print("❌ Nessuna mappa generata")
        return None

# Test
mappa = query_with_map("Celle con popolazione superiore a 10000 abitanti")
mappa
🤔 Domanda: Celle con popolazione superiore a 10000 abitanti
🤖 Risposta: In base ai dati analizzati, ci sono 30 celle del territorio che nel 2021 avevano una popolazione superiore a 10.000 abitanti. Queste rappresentano le aree più densamente popolate della zona in esame.
🗺️ Mappa generata! Salvataggio in corso...
✅ Mappa salvata come 'risultato_query.html'
Out[17]:
Make this Notebook Trusted to load map: File -> Trust Notebook
In [18]:
def show_map_in_notebook(question):
    """Mostra mappa direttamente nel notebook"""
    
    print(f"🤔 {question}")
    
    response, mappa = map_agent.answer_with_map(question)
    print(f"🤖 {response}")
    
    if mappa:
        # In Jupyter la mappa appare automaticamente
        print("🗺️ Mappa:")
        return mappa  # Jupyter mostra automaticamente oggetti Folium
    else:
        print("❌ Nessuna mappa generata")
        return None

# Uso diretto nel notebook
mappa = show_map_in_notebook("Celle con popolazione superiore a 10000")

# La mappa appare sotto questa cella!
🤔 Celle con popolazione superiore a 10000
🤖 In base ai dati del 2021, ci sono 30 celle geografiche nel territorio analizzato che superano i 10.000 abitanti. Queste rappresentano le aree più densamente popolate della zona in esame.
🗺️ Mappa:
In [19]:
mappa
Out[19]:
Make this Notebook Trusted to load map: File -> Trust Notebook
In [20]:
# Modo più semplice per vedere mappe nel notebook

# 1. Query diretta - la mappa appare automaticamente
risposta, mappa = map_agent.answer_with_map("Celle con popolazione superiore a 150")
print(risposta)
mappa  # Jupyter mostra automaticamente la mappa

# 2. Con display esplicito
from IPython.display import display

def query_and_display(question):
    resp, map_obj = map_agent.answer_with_map(question)
    print(f"📍 {question}")
    print(f"💬 {resp}")
    if map_obj:
        display(map_obj)  # Forza visualizzazione
    return map_obj

# Esempi
query_and_display("Zone con buona accessibilità")
query_and_display("Aree agricole estese")
In base ai dati analizzati, nel 2021 c'erano 413 celle del territorio che avevano una popolazione superiore a 150 abitanti. Questo dato ci mostra le aree più densamente popolate della zona in esame.
📍 Zone con buona accessibilità
💬 In base all'analisi effettuata, ho identificato 23 zone che offrono un'ottima accessibilità ai servizi essenziali. In queste aree, è possibile raggiungere comodamente in auto (entro 15 minuti) strutture come scuole, ospedali, centri culturali, impianti sportivi e collegamenti di trasporto pubblico. Questo le rende particolarmente attraenti per chi cerca una location ben servita e ben collegata per vivere o lavorare.
Make this Notebook Trusted to load map: File -> Trust Notebook
📍 Aree agricole estese
💬 In base all'analisi effettuata, non sono state trovate aree che soddisfano contemporaneamente i criteri richiesti, ovvero:
- una percentuale di copertura agricola superiore al 50% della superficie totale
- la presenza di aree agricole con valore positivo

Questo potrebbe significare che nelle zone analizzate l'agricoltura non è l'attività predominante del territorio, oppure che le aree agricole sono più frammentate e distribuite in percentuali minori. Se sei interessato a trovare zone agricole, potremmo provare a modificare i criteri di ricerca, ad esempio abbassando la soglia percentuale o considerando altri parametri.
Make this Notebook Trusted to load map: File -> Trust Notebook
Out[20]:
Make this Notebook Trusted to load map: File -> Trust Notebook
In [ ]:
 
In [ ]: