Analisi degli Spazi di Aggregazione a Bari¶

Obiettivo dell'Analisi¶

Questo notebook analizza i dati raccolti tramite un questionario somministrato agli abitanti di Bari per identificare i luoghi di aggregazione nella città.

Cosa faremo:¶

  1. Elaborazione dei dati: Puliamo e organizziamo le risposte del questionario
  2. Geocoding: Troviamo le coordinate geografiche di ogni luogo menzionato
  3. Filtraggio geografico: Manteniamo solo i luoghi effettivamente dentro Bari
  4. Aggregazione: Contiamo quante persone frequentano ogni spazio
  5. Visualizzazione: Creiamo mappe interattive e grafici per mostrare i risultati

Risultati finali:¶

  • Mappa interattiva con heatmap dei luoghi più frequentati (index.html nella cartella mappa)
  • Treemap plot che mostra visivamente la popolarità relativa dei luoghi

1. Importazione delle Librerie¶

Prima di tutto importiamo tutte le librerie necessarie per l'analisi:

In [1]:
# Librerie per manipolazione dati e visualizzazione
import pandas as pd
import matplotlib.pyplot as plt
import json

# Librerie per richieste web (geocoding)
import requests
import time

# Librerie di sistema
import collections
import os

# Librerie per operazioni geografiche
from shapely.geometry import Point, Polygon, MultiPolygon
import folium
from folium import plugins

2. Caricamento e Prima Pulizia dei Dati¶

Carichiamo il file Excel contenente le risposte al questionario e iniziamo a pulire i dati.

In [2]:
# Leggiamo il file Excel con i dati del questionario
df = pd.read_excel("spazi.xlsx")
In [3]:
# Rimuoviamo le righe dove non è stata data risposta alla domanda principale
# "Quali?" si riferisce a "Quali spazi frequenti?"
df = df[df['Quali?'].notna()]
In [4]:
# Controlliamo i valori unici per la domanda "Frequenti questi spazi?"
# Tutti hanno risposto "Si", quindi possiamo rimuovere questa colonna
df["Frequenti questi spazi? "].unique()
Out[4]:
array(['Si'], dtype=object)
In [5]:
# Rimuoviamo le colonne che non servono per l'analisi
df.drop(["Frequenti questi spazi? ", "Quale spazio sogni a Bari?"], axis=1, inplace=True)
In [6]:
# Salviamo una versione pulita in CSV per backup
df.to_csv("spazi.csv", index=False)

3. Elaborazione delle Risposte Multiple¶

Nel questionario, le persone potevano selezionare più luoghi. Dobbiamo "espandere" queste risposte multiple in colonne separate per poter contare facilmente le frequenze.

In [7]:
# Carichiamo il file con i luoghi già mappati
# (questo file contiene i luoghi standardizzati e raggruppati)
df = pd.read_csv("spazi mapped.csv")
In [8]:
# Manteniamo solo le righe dove è stata fatta una mappatura
df = df[df['Map'].notna()]
In [9]:
# Trasformiamo le risposte multiple in colonne binarie (0/1)
# Esempio: "Parco, Teatro, Bar" diventa tre colonne separate

# Separiamo le risposte multiple usando la virgola
df['map_split'] = df['Map'].str.split(',')

# "Esplodiamo" ogni lista in righe separate
df_exploded = df.explode('map_split')

# Rimuoviamo gli spazi extra
df_exploded['map_split'] = df_exploded['map_split'].str.strip()

# Creiamo variabili dummy (0/1) per ogni luogo
dummies = pd.get_dummies(df_exploded['map_split'])

# Raggruppiamo per persona (indice originale) e sommiamo
dummies_grouped = dummies.groupby(df_exploded.index).sum()

# Uniamo le dummy con il dataframe originale
df = pd.concat([df.drop(['map_split', 'Map'], axis=1), dummies_grouped], axis=1)
In [10]:
df
Out[10]:
Quali? Quanto ti senti accoltə negli spazi che frequenti? (G) Aree fitness (G) Associazioni (G) Auditorium (G) Aule studio (G) Bar (G) Biblioteca (G) Caffe letterari (G) Campi da calcio ... Torre quetta Umbertino Vallisa Via Amoruso Via de Amiciis Via mazzitelli Via sparano Voga Art Project Zampə Mostruosə Zona franka
0 Teatro Margherita, kismet, kursaal quelli pubb... 5 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
1 Bread and roses, storie del vecchio sud, il pi... 3 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
2 Frequento vari centri sociali 5 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
3 Circolo arci, piccolo bar 3 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
4 Quelli presenti a Bari sono al momento Ex Case... 2 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
276 Zampə mostruosə e mixed lgbti 4 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 1 0
278 Officina degli Esordi, Spazio13, AncheCinema 5 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
279 parchi 4 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
280 Feltrinelli, Parco Rossani, 2 Giugno 3 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
281 bar, piazzette 2 0 0 0 0 1 0 0 0 ... 0 0 0 0 0 0 0 0 0 0

250 rows × 134 columns

4. Organizzazione delle Colonne¶

Riorganizziamo le colonne per una migliore leggibilità, separando i luoghi specifici (es. "Teatro Petruzzelli") dalle categorie generiche (es. "(G) Teatri").

In [11]:
# Rinominiamo la colonna principale per chiarezza
df = df.rename(columns={"Quali?": "Quali spazi frequenti?"})

# Definiamo l'ordine delle colonne: prima le domande, poi i luoghi, poi le categorie
top_columns = ["Quali spazi frequenti?", "Quanto ti senti accoltə negli spazi che frequenti?"]
remaining_cols = [col for col in df.columns if col not in top_columns]

# Separiamo le categorie generiche (iniziano con "(G)") dai luoghi specifici
g_columns = sorted([col for col in remaining_cols if col.startswith("(G)")])
other_columns = sorted([col for col in remaining_cols if not col.startswith("(G)")])

# Riordiniamo le colonne
new_column_order = top_columns + other_columns + g_columns
df = df[new_column_order]
In [12]:
# Creiamo una lista di tutti i luoghi da geocodificare
places = other_columns + g_columns

5. Geocoding - Trovare le Coordinate dei Luoghi¶

Per creare la mappa, dobbiamo trovare le coordinate geografiche (latitudine e longitudine) di ogni luogo menzionato. Utilizziamo servizi di geocoding gratuiti.

In [13]:
# Inizializziamo un dizionario per memorizzare le coordinate
temp = {}
for place in places:
    temp[place] = {"lat":0, "lng":0}  # Inizializziamo a (0,0)
places = temp

Funzioni per il Geocoding¶

Utilizziamo due servizi diversi (Nominatim e Photon) per maggiore affidabilità e confrontiamo i risultati.

In [14]:
# Funzioni per il geocoding usando API gratuite

def geocode_nominatim(query, user_agent="geocoder-script"):
    """Geocoding usando Nominatim (OpenStreetMap)"""
    base_url = "https://nominatim.openstreetmap.org/search"
    
    params = {
        'q': query.strip() + ", Bari, Puglia, Italy",  # Aggiungiamo Bari per precisione
        'format': 'json',
        'limit': 1,
        'addressdetails': 1
    }
    
    headers = {'User-Agent': user_agent}
    
    try:
        response = requests.get(base_url, params=params, headers=headers)
        response.raise_for_status()
        data = response.json()
        
        if data:
            result = data[0]
            return {
                'lat': float(result['lat']),
                'lng': float(result['lon']),
                'display_name': result['display_name']
            }
        else:
            return None
            
    except requests.RequestException as e:
        print(f"Errore nella richiesta a Nominatim: {e}")
        return None

def geocode_photon(query, user_agent="geocoder-script"):
    """Geocoding usando Photon (alternativo)"""
    base_url = "https://photon.komoot.io/api/"
    
    params = {
        'q': query.strip() + ", Bari, Puglia, Italy",
        'limit': 1
    }
    
    headers = {'User-Agent': user_agent}
    
    try:
        response = requests.get(base_url, params=params, headers=headers)
        response.raise_for_status()
        data = response.json()
        
        if data.get('features'):
            result = data['features'][0]
            coords = result['geometry']['coordinates']  # [lng, lat] format
            properties = result['properties']
            
            return {
                'lat': float(coords[1]),  # latitudine è il secondo elemento
                'lng': float(coords[0]),  # longitudine è il primo elemento
                'display_name': properties.get('name', '') + ', ' + properties.get('city', '') + ', ' + properties.get('country', '')
            }
        else:
            return None
            
    except requests.RequestException as e:
        print(f"Errore nella richiesta a Photon: {e}")
        return None

def geocode_batch(queries, geocode_func, user_agent="geocoder-script", delay=1):
    """Geocoding per un gruppo di luoghi con rate limiting"""
    results = []
    
    for i, query in enumerate(queries):
        result = geocode_func(query, user_agent)
        results.append(result)
        
        # Aspettiamo tra una richiesta e l'altra per rispettare i limiti
        if i < len(queries) - 1:
            time.sleep(delay)
    
    return results

def compare_coordinates(coord1, coord2, precision=2):
    """Confronta due coordinate con una data precisione"""
    if coord1 is None or coord2 is None:
        return False
    
    lat1_rounded = round(coord1['lat'], precision)
    lng1_rounded = round(coord1['lng'], precision)
    lat2_rounded = round(coord2['lat'], precision)
    lng2_rounded = round(coord2['lng'], precision)
    
    return lat1_rounded == lat2_rounded and lng1_rounded == lng2_rounded

def compare_and_process_results(places, queries):
    """Confronta i risultati di due API e mantiene solo quelli concordi"""
    print("Ottenendo risultati da Nominatim API...")
    nominatim_results = geocode_batch(queries, geocode_nominatim, delay=1)
    
    print("Ottenendo risultati da Photon API...")
    photon_results = geocode_batch(queries, geocode_photon, delay=0.5)
    
    print("\nConfronto risultati (precisione: 4 cifre decimali):")
    print("="*60)
    
    for i, query in enumerate(queries):
        nominatim_result = nominatim_results[i]
        photon_result = photon_results[i]
        
        print(f"\n{i+1}. {query}")
        
        if nominatim_result and photon_result:
            # Entrambe le API hanno trovato risultati
            if compare_coordinates(nominatim_result, photon_result, precision=2):
                # Le coordinate coincidono - usiamo Nominatim
                places[query]['lat'] = nominatim_result['lat']
                places[query]['lng'] = nominatim_result['lng']
                print(f"   ✓ MATCH - Lat: {nominatim_result['lat']:.6f}, Lng: {nominatim_result['lng']:.6f}")
            else:
                # Le coordinate non coincidono - settiamo a (0,0) per sicurezza
                places[query]['lat'] = 0
                places[query]['lng'] = 0
                print(f"   ✗ MISMATCH - Impostato a (0, 0)")
                print(f"     Nominatim: Lat: {nominatim_result['lat']:.6f}, Lng: {nominatim_result['lng']:.6f}")
                print(f"     Photon:    Lat: {photon_result['lat']:.6f}, Lng: {photon_result['lng']:.6f}")
        
        elif nominatim_result and not photon_result:
            # Solo Nominatim ha trovato il risultato - settiamo a (0,0) per inconsistenza
            places[query]['lat'] = 0
            places[query]['lng'] = 0
            print(f"   ✗ SOLO NOMINATIM TROVATO - Impostato a (0, 0)")
            print(f"     Nominatim: Lat: {nominatim_result['lat']:.6f}, Lng: {nominatim_result['lng']:.6f}")
        
        elif not nominatim_result and photon_result:
            # Solo Photon ha trovato il risultato - settiamo a (0,0) per inconsistenza
            places[query]['lat'] = 0
            places[query]['lng'] = 0
            print(f"   ✗ SOLO PHOTON TROVATO - Impostato a (0, 0)")
            print(f"     Photon: Lat: {photon_result['lat']:.6f}, Lng: {photon_result['lng']:.6f}")
        
        else:
            # Nessuna API ha trovato il risultato
            places[query]['lat'] = 0
            places[query]['lng'] = 0
            print(f"   ✗ NON TROVATO DA NESSUNA API - Impostato a (0, 0)")

Esecuzione del Geocoding¶

Eseguiamo il geocoding solo se non abbiamo già i risultati salvati (per evitare di ripetere le chiamate API).

In [15]:
# Eseguiamo le query geocoding solo se non abbiamo già i risultati
if not os.path.exists("placesWithMissing.json"):
    queries = list(places.keys())
    compare_and_process_results(places, queries)

    # Salviamo i risultati
    with open("placesWithMissing.json", "w", encoding="utf-8") as f:
        json.dump(temp, f, indent=4, ensure_ascii=False)

# Carichiamo i risultati salvati
with open("placesWithMissing.json") as f:
    places = json.load(f)

6. Completamento Manuale delle Coordinate¶

Per i luoghi che le API automatiche non sono riuscite a trovare, aggiungiamo manualmente le coordinate usando Google Maps.

In [16]:
# Identifichiamo i luoghi senza coordinate (0,0)
missingPlaces = {}
for k, v in places.items():
    if v["lat"] == 0 or v["lng"] == 0:
        missingPlaces[k] = []
In [17]:
# Mostriamo tutti i luoghi che necessitano coordinate manuali
print(json.dumps(missingPlaces, indent=4, ensure_ascii=False))
{
    "Accademia di belle arti": [],
    "Bachi da setola (polignano)": [],
    "Bari Ludens": [],
    "Biblioteca Gaetano Richetti": [],
    "Biblioteca Lombardi": [],
    "Biglietteria": [],
    "Bollenti spiriti": [],
    "Bread and roses": [],
    "Caffè noir": [],
    "Cafè portineria": [],
    "Campus": [],
    "Castello Svevo": [],
    "Cicchetteria": [],
    "Cinema Galleria": [],
    "Corso vittorio emanuele": [],
    "ExPost Moderno": [],
    "Fiere": [],
    "Finibus Terrae": [],
    "Ianus": [],
    "La pineta": [],
    "Libreria Laterza": [],
    "Libreria Spine": [],
    "Lungomare": [],
    "Luoghi comuni": [],
    "Mate": [],
    "MiXED lgbtqia+": [],
    "Millelibri liberia": [],
    "Muraglia": [],
    "Nevermind club": [],
    "Norcineria": [],
    "ODE": [],
    "OKAY": [],
    "Officine clandestine": [],
    "Organic": [],
    "Orto sociale Campagneros": [],
    "Piazza umberto": [],
    "Piccolo Bar": [],
    "Pinacoteca Corrado Giacquinto": [],
    "Prinz zaum": [],
    "Puglia Woman Lead": [],
    "Spazi Comunali": [],
    "Spazi di coworking": [],
    "The magic spot": [],
    "Torre quetta": [],
    "Via de Amiciis": [],
    "Voga Art Project": [],
    "Zampə Mostruosə": [],
    "Zona franka": [],
    "(G) Aree fitness": [],
    "(G) Associazioni": [],
    "(G) Auditorium": [],
    "(G) Aule studio": [],
    "(G) Bar": [],
    "(G) Biblioteca": [],
    "(G) Caffe letterari": [],
    "(G) Campi da calcio": [],
    "(G) Centri sociali": [],
    "(G) Centro": [],
    "(G) Cinema": [],
    "(G) Club": [],
    "(G) Collettivi": [],
    "(G) Concerti": [],
    "(G) Conferenze/seminari": [],
    "(G) Discoteca": [],
    "(G) Eventi culturali": [],
    "(G) Gallerie d'arte": [],
    "(G) Laboratori urbani": [],
    "(G) Librerie": [],
    "(G) Musei/Mostre": [],
    "(G) Parchi": [],
    "(G) Piazze": [],
    "(G) Pub": [],
    "(G) Sale lettura": [],
    "(G) Scuole di musica": [],
    "(G) Spazi occupati": [],
    "(G) Teatri": [],
    "(G) Università": [],
    "(G) Workshop": [],
    "(G) Zone residenziali": []
}
In [18]:
# Aggiungiamo manualmente le coordinate trovate su Google Maps
# Per le categorie generiche (G), usiamo coordinate rappresentative o le lasciamo vuote
#fill missing values using gmaps queries
missingPlaces = {
    "Accademia di belle arti": [41.11136372509544, 16.87640159697652],
    "Bachi da setola (polignano)": [40.99210799486919, 17.23005706170202],
    "Bari Ludens": [41.11336613442185, 16.87291289986268],
    "Biblioteca Gaetano Richetti": [41.11993828740921, 16.86990448452394],
    "Biblioteca Lombardi": [41.120516890488155, 16.78997499246642],
    "Biblioteca dei ragazzi": [41.10265950709727, 16.87502641574076],
    "Biglietteria": [41.123855288361646, 16.875569564445904],
    "Bollenti spiriti": [41.89451859478436, 15.567448424121014],
    "Bread and roses": [41.10658030286168, 16.886579137545002],
    "Caffè noir": [41.12703266373722, 16.871737674143223],
    "Cafè portineria": [41.1245731777968, 16.868377949327428],
    "Campus": [41.10967263119022, 16.87983793330308],
    "Castello Svevo": [41.128111516922296, 16.866466311632706],
    "Cicchetteria": [41.12860055150233, 16.871057061785486],
    "Cinema Galleria": [41.11928927430121, 16.867967155223145],
    "Corso vittorio emanuele": [41.12659449793266, 16.86730490781312],
    "ExPost Moderno": [41.12720155341268, 16.852415030617234],
    "Finibus Terrae": [41.120425087437454, 16.87674114478415],
    "Ianus": [41.1079652646532, 16.862681672970787],
    # "La pineta": [],
    "Libreria Laterza": [41.12277120894988, 16.87010121521149],
    "Libreria Spine": [41.125872162836615, 16.859796792469893],
    "Lungomare": [41.123238063420665, 16.878373032360013],
    "Luoghi comuni": [41.09916397769848, 16.86665306308122],
    # "Mate": [],
    "MiXED lgbtqia+": [41.1211898952258, 16.87582101575211],
    "Millelibri liberia": [41.11753875342157, 16.876604322652483],
    "Muraglia": [41.12804373185526, 16.873160636207857],
    "Nevermind club": [41.103593002260304, 16.85870724642707],
    "Norcineria": [41.12337751728451, 16.876027865731942],
    "ODE": [41.125427038765764, 16.86030337578236],
    "OKAY": [41.10772150069555, 16.86206086432247],
    "Officine clandestine": [41.111298921410004, 16.875358607766483],
    "Organic": [41.12341852475953, 16.876750033114963],
    "Orto sociale Campagneros": [41.09702120821604, 16.88562208844041],
    "Piazza umberto": [41.12109759298002, 16.87077116913902],
    "Piccolo Bar": [41.12341513470912, 16.875530177823432],
    "Pinacoteca Corrado Giacquinto": [41.121434540268744, 16.88158619512904],
    "Prinz zaum": [41.121894044267925, 16.87564816174823],
    "Puglia Woman Lead": [41.10235933850214, 16.607642987635636],
    "The magic spot": [41.11734764424779, 16.87657784894823],
    "Torre quetta": [41.11271924653431, 16.914471444773646],
    "Via de Amiciis": [41.11231751287967, 16.873432792425476],
    "Via mazzitelli": [41.10433266778997, 16.85397046117845],
    "Voga Art Project": [41.12646226774262, 16.84874204640919],
    "Zampə Mostruosə": [41.12234892256071, 16.86225969274399],
    "Zona franka": [41.12144658472404, 16.880246328657993],
    # "(G) Aree fitness": [],
    # "(G) Associazioni": [],
    # "(G) Auditorium": [],
    # "(G) Aule studio": [],
    # "(G) Bar": [],
    # "(G) Biblioteca": [],
    # "(G) Caffe letterari": [],
    # "(G) Campi da calcio": [],
    # "(G) Centri sociali": [],
    "(G) Centro": [41.12335141826763, 16.8696648691402], # Via sparano
    # "(G) Cinema": [],
    # "(G) Club": [],
    # "(G) Collettivi": [],
    # "(G) Concerti": [],
    # "(G) Conferenze/seminari": [],
    # "(G) Discoteca": [],
    # "(G) Eventi culturali": [],
    "(G) Fiere": [41.13665028837323, 16.838192435481186], # Fiera del levante
    # "(G) Gallerie d'arte": [],
    # "(G) Laboratori urbani": [],
    # "(G) Librerie": [],
    # "(G) Musei/Mostre": [],
    # "(G) Parchi": [],
    # "(G) Piazze": [],
    # "(G) Pub": [],
    # "(G) Sale lettura": [],
    # "(G) Spazi Comunali": [],
    # "(G) Spazi di coworking": [],
    # "(G) Scuole di musica": [],
    # "(G) Spazi occupati": [],
    # "(G) Teatri": [],
    # "(G) Università": [],
    # "(G) Workshop": [],
    # "(G) Zone residenziali": []
}
In [19]:
# Convertiamo le coordinate nel formato corretto
for place in missingPlaces:
    missingPlaces[place] = {"lat": missingPlaces[place][0], "lng": missingPlaces[place][1]}

7. Unione e Organizzazione delle Coordinate¶

Uniamo le coordinate trovate automaticamente con quelle inserite manualmente.

In [20]:
# Uniamo le coordinate automatiche con quelle manuali
temp = {}

# Prima aggiungiamo tutti i luoghi con coordinate valide trovate automaticamente
for place, coordinates in places.items():
    if not (coordinates['lat'] == 0 or coordinates['lng'] == 0):
        temp[place] = coordinates

# Poi aggiungiamo le coordinate inserite manualmente
for place, coordinates in missingPlaces.items():
    temp[place] = coordinates

# Ordiniamo alfabeticamente per consistenza
places = dict(collections.OrderedDict(sorted(temp.items())))
places
Out[20]:
{'(G) Centro': {'lat': 41.12335141826763, 'lng': 16.8696648691402},
 '(G) Fiere': {'lat': 41.13665028837323, 'lng': 16.838192435481186},
 'Accademia del Cinema di Enziteto': {'lat': 41.1478573, 'lng': 16.7405872},
 'Accademia di belle arti': {'lat': 41.11136372509544,
  'lng': 16.87640159697652},
 'AncheCinema': {'lat': 41.1179704, 'lng': 16.8642494},
 'Arcimboldo': {'lat': 41.1631624, 'lng': 16.7473564},
 'Ateneo': {'lat': 41.1210943, 'lng': 16.8690626},
 'Bachi da setola (polignano)': {'lat': 40.99210799486919,
  'lng': 17.23005706170202},
 'Bari Ludens': {'lat': 41.11336613442185, 'lng': 16.87291289986268},
 'Bari vecchia': {'lat': 41.1295686, 'lng': 16.869599},
 'Biblioteca Gaetano Richetti': {'lat': 41.11993828740921,
  'lng': 16.86990448452394},
 'Biblioteca Lombardi': {'lat': 41.120516890488155, 'lng': 16.78997499246642},
 'Biblioteca dei ragazzi': {'lat': 41.10265950709727,
  'lng': 16.87502641574076},
 'Biglietteria': {'lat': 41.123855288361646, 'lng': 16.875569564445904},
 'Bollenti spiriti': {'lat': 41.89451859478436, 'lng': 15.567448424121014},
 'Bosco di cancello rotto': {'lat': 41.0987991, 'lng': 16.8687698},
 'Bread and roses': {'lat': 41.10658030286168, 'lng': 16.886579137545002},
 'Cabaret voltaire': {'lat': 41.1124195, 'lng': 16.8731886},
 'Caffè noir': {'lat': 41.12703266373722, 'lng': 16.871737674143223},
 'Cafè portineria': {'lat': 41.1245731777968, 'lng': 16.868377949327428},
 'Calamandrei': {'lat': 40.80133, 'lng': 16.7629362},
 'Campus': {'lat': 41.10967263119022, 'lng': 16.87983793330308},
 'Castello Svevo': {'lat': 41.128111516922296, 'lng': 16.866466311632706},
 'Cicchetteria': {'lat': 41.12860055150233, 'lng': 16.871057061785486},
 'Cinema Galleria': {'lat': 41.11928927430121, 'lng': 16.867967155223145},
 'Cineporto': {'lat': 41.1370541, 'lng': 16.8387754},
 'Circolo Arci': {'lat': 41.1810192, 'lng': 16.6827688},
 'Corso vittorio emanuele': {'lat': 41.12659449793266,
  'lng': 16.86730490781312},
 'Dipartimento di fisica': {'lat': 41.1081053, 'lng': 16.8836911},
 'Ekoinè': {'lat': 41.1077125, 'lng': 16.8637537},
 'El Chiringuito': {'lat': 41.1258931, 'lng': 16.8742226},
 'Eremo Club (Molfetta)': {'lat': 41.1935401, 'lng': 16.6296192},
 'Ex caserma Rossani': {'lat': 41.114737, 'lng': 16.8716544},
 'ExPost Moderno': {'lat': 41.12720155341268, 'lng': 16.852415030617234},
 'Feltrinelli': {'lat': 41.1225422, 'lng': 16.8712829},
 'Finibus Terrae': {'lat': 41.120425087437454, 'lng': 16.87674114478415},
 'Frequenza libera': {'lat': 41.1087106, 'lng': 16.8796868},
 'Fuori binaria': {'lat': 41.1515082, 'lng': 16.7666684},
 'Giardino Mimmo Bucci': {'lat': 41.1195282, 'lng': 16.8536371},
 'Ianus': {'lat': 41.1079652646532, 'lng': 16.862681672970787},
 'Kursaal Santalucia': {'lat': 41.1237074, 'lng': 16.8755875},
 'Largo Ciaia': {'lat': 41.1154101, 'lng': 16.8727067},
 'Largo adua': {'lat': 41.1237316, 'lng': 16.8760275},
 'Liberrima': {'lat': 41.124051, 'lng': 16.8717065},
 'Libertà': {'lat': 41.1235748, 'lng': 16.8580636},
 'Libreria 101': {'lat': 41.1227851, 'lng': 16.8671358},
 'Libreria Laterza': {'lat': 41.12277120894988, 'lng': 16.87010121521149},
 'Libreria Spine': {'lat': 41.125872162836615, 'lng': 16.859796792469893},
 'Lungomare': {'lat': 41.123238063420665, 'lng': 16.878373032360013},
 'Luoghi comuni': {'lat': 41.09916397769848, 'lng': 16.86665306308122},
 'MiXED lgbtqia+': {'lat': 41.1211898952258, 'lng': 16.87582101575211},
 'Millelibri liberia': {'lat': 41.11753875342157, 'lng': 16.876604322652483},
 'Muraglia': {'lat': 41.12804373185526, 'lng': 16.873160636207857},
 'Nevermind club': {'lat': 41.103593002260304, 'lng': 16.85870724642707},
 'Norcineria': {'lat': 41.12337751728451, 'lng': 16.876027865731942},
 'Nuovo Splendor': {'lat': 41.1106541, 'lng': 16.8699432},
 'ODE': {'lat': 41.125427038765764, 'lng': 16.86030337578236},
 'OKAY': {'lat': 41.10772150069555, 'lng': 16.86206086432247},
 'Officina degli esordi': {'lat': 41.1252592, 'lng': 16.8601813},
 'Officine clandestine': {'lat': 41.111298921410004,
  'lng': 16.875358607766483},
 'Organic': {'lat': 41.12341852475953, 'lng': 16.876750033114963},
 'Orto sociale Campagneros': {'lat': 41.09702120821604,
  'lng': 16.88562208844041},
 'Pane e pomodoro': {'lat': 41.118344, 'lng': 16.891917},
 'Parco 2 giugno': {'lat': 41.1024695, 'lng': 16.8747617},
 'Parco Perotti': {'lat': 41.115612, 'lng': 16.9005706},
 'Parco Rossani': {'lat': 41.1158968, 'lng': 16.8704457},
 'Parco gargasole': {'lat': 41.1140003, 'lng': 16.8711249},
 'Petruzzelli': {'lat': 41.1235649, 'lng': 16.873158},
 'Piazza Mercantile': {'lat': 41.1278569, 'lng': 16.8725188},
 'Piazza del Ferrarese': {'lat': 41.128336, 'lng': 16.8722057},
 'Piazza umberto': {'lat': 41.12109759298002, 'lng': 16.87077116913902},
 'Piccinni': {'lat': 41.1255127, 'lng': 16.8674783},
 'Piccolo Bar': {'lat': 41.12341513470912, 'lng': 16.875530177823432},
 'Pinacoteca Corrado Giacquinto': {'lat': 41.121434540268744,
  'lng': 16.88158619512904},
 'Poggiofranco': {'lat': 41.0988416, 'lng': 16.8586321},
 'Policlinico': {'lat': 41.112726, 'lng': 16.8620525},
 'Prinz zaum': {'lat': 41.121894044267925, 'lng': 16.87564816174823},
 'Puglia Woman Lead': {'lat': 41.10235933850214, 'lng': 16.607642987635636},
 'Punto X': {'lat': 41.0984624, 'lng': 16.8517052},
 'San Girolamo': {'lat': 41.1337709, 'lng': 16.8200141},
 'Skatepark': {'lat': 41.1225756, 'lng': 16.8480295},
 'Spazio 13': {'lat': 41.1246725, 'lng': 16.8560676},
 'Spazio Murat': {'lat': 41.126522, 'lng': 16.8716853},
 'Stazione': {'lat': 41.1179496, 'lng': 16.8702402},
 'Storie del vecchio sud': {'lat': 41.1131195, 'lng': 16.8706468},
 'Teatro Kismet': {'lat': 41.1095339, 'lng': 16.8385175},
 'Teatro Margherita': {'lat': 41.1263619, 'lng': 16.872861},
 'The magic spot': {'lat': 41.11734764424779, 'lng': 16.87657784894823},
 'Torre quetta': {'lat': 41.11271924653431, 'lng': 16.914471444773646},
 'Umbertino': {'lat': 41.1219291, 'lng': 16.8742368},
 'Vallisa': {'lat': 41.1271214, 'lng': 16.8714034},
 'Via Amoruso': {'lat': 41.1027426, 'lng': 16.8582992},
 'Via de Amiciis': {'lat': 41.11231751287967, 'lng': 16.873432792425476},
 'Via mazzitelli': {'lat': 41.10433266778997, 'lng': 16.85397046117845},
 'Via sparano': {'lat': 41.1236852, 'lng': 16.8695372},
 'Voga Art Project': {'lat': 41.12646226774262, 'lng': 16.84874204640919},
 'Zampə Mostruosə': {'lat': 41.12234892256071, 'lng': 16.86225969274399},
 'Zona franka': {'lat': 41.12144658472404, 'lng': 16.880246328657993}}
In [21]:
# Salviamo il file completo delle coordinate
with open("places.json", "w", encoding="utf-8") as f:
    json.dump(places, f, indent=4, ensure_ascii=False)
In [22]:
# Verifichiamo quanti luoghi abbiamo geocodificato
len(places)
Out[22]:
98

8. Filtraggio Geografico¶

Filtriamo i luoghi per mantenere solo quelli che si trovano effettivamente dentro i confini di Bari, usando un file GeoJSON dei quartieri.

In [23]:
# Otteniamo la lista dei luoghi dal nostro dataframe
dfPlaces = list(df.columns)
dfPlaces.remove('Quali spazi frequenti?')
dfPlaces.remove('Quanto ti senti accoltə negli spazi che frequenti?')
In [24]:
def filter_places_by_geojson(places_dict, geojson_file_path):
    """Filtra i luoghi mantenendo solo quelli dentro l'area definita dal GeoJSON"""
    with open(geojson_file_path, 'r') as f:
        geojson = json.load(f)
    
    # Estraiamo i poligoni dal GeoJSON
    polygons = []
    features = geojson.get('features', [geojson])
    
    for feature in features:
        geom = feature.get('geometry', feature)
        coords = geom['coordinates']
        
        if geom['type'] == 'Polygon':
            polygons.append(Polygon(coords[0], coords[1:]))
        elif geom['type'] == 'MultiPolygon':
            for poly_coords in coords:
                polygons.append(Polygon(poly_coords[0], poly_coords[1:]))
    
    # Filtriamo i luoghi
    filtered = {}
    outer = {}
    for name, coord in places_dict.items():
        point = Point(coord['lng'], coord['lat'])
        if any(poly.contains(point) for poly in polygons):
            filtered[name] = coord
        else:
            outer[name] = coord

    return filtered, outer
In [25]:
# Filtriamo i luoghi usando i confini di Bari
filtered_places, outer = filter_places_by_geojson(places, r"..\..\mappa\data\bari_neigh.geojson")

print("Luoghi fuori dai confini di Bari:")
for k, v in outer.items():
    print(k, [v["lat"], v["lng"]])
Luoghi fuori dai confini di Bari:
Bachi da setola (polignano) [40.99210799486919, 17.23005706170202]
Bollenti spiriti [41.89451859478436, 15.567448424121014]
Calamandrei [40.80133, 16.7629362]
Circolo Arci [41.1810192, 16.6827688]
Eremo Club (Molfetta) [41.1935401, 16.6296192]
Puglia Woman Lead [41.10235933850214, 16.607642987635636]
In [26]:
# Correggiamo manualmente alcune coordinate che erano fuori dai confini
# (alcune coordinate automatiche erano imprecise)
# Gli altri spazi presenti sono effettivamente fuori da bari
places["Calamandrei"] = {"lat":41.06873134965177, "lng":16.86430261968942}
places["Circolo Arci"] = {"lat":41.12615843543517, "lng":16.86591651967947}
In [27]:
# Verifichiamo di nuovo dopo le correzioni
filtered_places, outer = filter_places_by_geojson(places, r"..\..\mappa\data\bari_neigh.geojson")

# Ora dovrebbero esserci meno luoghi fuori dai confini
print("Luoghi fuori dai confini di Bari:")
for k, v in outer.items():
    print(k, [v["lat"], v["lng"]])
Luoghi fuori dai confini di Bari:
Bachi da setola (polignano) [40.99210799486919, 17.23005706170202]
Bollenti spiriti [41.89451859478436, 15.567448424121014]
Eremo Club (Molfetta) [41.1935401, 16.6296192]
Puglia Woman Lead [41.10235933850214, 16.607642987635636]
In [28]:
def create_map(locations_dict, geojson_filepath=None):
    """
    Create a map with points from dictionary and optional GeoJSON file
    
    Args:
        locations_dict: Dict with format {'name': {'lat': float, 'lng': float}}
        geojson_filepath: Optional path to GeoJSON file to load additional points
    
    Returns:
        folium.Map object
    """
    # Try to get center from polygon in GeoJSON file first
    center_lat, center_lng = None, None
    geojson_data = None
    
    if geojson_filepath:
        try:
            with open(geojson_filepath, 'r') as f:
                geojson_data = json.load(f)
            
            # Find polygon and calculate its centroid
            for feature in geojson_data.get('features', []):
                if feature['geometry']['type'] in ['Polygon', 'MultiPolygon']:
                    coords = feature['geometry']['coordinates']
                    if feature['geometry']['type'] == 'Polygon':
                        # Get exterior ring coordinates
                        ring_coords = coords[0]
                    else:  # MultiPolygon
                        # Use first polygon's exterior ring
                        ring_coords = coords[0][0]
                    
                    # Calculate centroid (average of coordinates)
                    lats = [coord[1] for coord in ring_coords]
                    lngs = [coord[0] for coord in ring_coords]
                    center_lat = sum(lats) / len(lats)
                    center_lng = sum(lngs) / len(lngs)
                    break
        except FileNotFoundError:
            print(f"GeoJSON file {geojson_filepath} not found")
    
    # Fall back to dictionary coordinates if no polygon center found
    if center_lat is None:
        lats = [coords['lat'] for coords in locations_dict.values()]
        lngs = [coords['lng'] for coords in locations_dict.values()]
        center_lat = sum(lats) / len(lats)
        center_lng = sum(lngs) / len(lngs)
    
    # Create map
    m = folium.Map(location=[center_lat, center_lng], zoom_start=8)
    
    # Add points from dictionary
    for name, coords in locations_dict.items():
        folium.Marker(
            location=[coords['lat'], coords['lng']],
            popup=name,
            tooltip=name
        ).add_to(m)
    
    # Add points from GeoJSON file if loaded
    if geojson_data:
        folium.GeoJson(geojson_data).add_to(m)
    
    return m
In [29]:
create_map(outer, r"..\..\mappa\data\bari.geojson")
Out[29]:
Make this Notebook Trusted to load map: File -> Trust Notebook

9. Aggregazione Finale e Preparazione Dati per Visualizzazione¶

Contiamo quante persone hanno indicato ogni luogo e prepariamo i dati nel formato necessario per la mappa.

In [30]:
# Creiamo il dizionario finale con il conteggio delle frequenze
res = {}
for place in dfPlaces:
    if place in filtered_places:
        # Contiamo quante persone hanno selezionato questo luogo
        res[place] = {
            "val": sum(df[place]),  # Somma delle colonne binarie (0/1)
            "lat": filtered_places[place]["lat"], 
            "lng": filtered_places[place]["lng"]
        }

# Ordiniamo per popolarità (dal più al meno frequentato)
res = dict(sorted(res.items(), key=lambda x: x[1]["val"], reverse=True))
In [31]:
# Convertiamo nel formato JSON necessario per la mappa
jsonFormat = []
for k, v in res.items():
    jsonFormat.append({
        "latitude": v["lat"],
        "longitudine": v["lng"],
        "name": k,
        "value": v["val"]  # Numero di persone che frequentano il luogo
    })
In [32]:
# Salviamo il file finale per la visualizzazione
with open(r"..\..\mappa\data\bari_coordinates.json", "w", encoding="utf-8") as f:
    json.dump(jsonFormat, f, indent=4, ensure_ascii=False)

10. Riepilogo dei Risultati¶

Analizziamo brevemente i risultati ottenuti.

In [33]:
print("=== ANALISI COMPLETATA ===")
print(f"\n📊 Statistiche generali:")
print(f"- Luoghi totali analizzati: {len(res)}")
print(f"- Luoghi più popolari (top 10):\n")

for i, (place, data) in enumerate(list(res.items())[:10], 1):
    print(f"  {i:2d}. {place}: {data['val']} persone")

print(f"\n🗺️  I dati sono stati salvati in 'bari_coordinates.json' e sono pronti per:")
print(f"   • Mappa interattiva con heatmap")
print(f"   • Treemap plot della popolarità")
print(f"   • Analisi geografica degli spazi di aggregazione")
=== ANALISI COMPLETATA ===

📊 Statistiche generali:
- Luoghi totali analizzati: 93
- Luoghi più popolari (top 10):

   1. Spazio 13: 42 persone
   2. Prinz zaum: 33 persone
   3. Officina degli esordi: 26 persone
   4. Bread and roses: 23 persone
   5. Parco 2 giugno: 22 persone
   6. Ex caserma Rossani: 17 persone
   7. Piccolo Bar: 12 persone
   8. MiXED lgbtqia+: 10 persone
   9. Zona franka: 10 persone
  10. (G) Centro: 9 persone

🗺️  I dati sono stati salvati in 'bari_coordinates.json' e sono pronti per:
   • Mappa interattiva con heatmap
   • Treemap plot della popolarità
   • Analisi geografica degli spazi di aggregazione
In [34]:
import matplotlib.pyplot as plt
import squarify

# Extract names and values
names = list(res.keys())
values = [res[name]['val'] for name in names]

# Create treemap
plt.figure(figsize=(12, 8))
squarify.plot(sizes=values, label=names, alpha=0.8, text_kwargs={'fontsize': 10})
plt.title('Locations Treemap', fontsize=16)
plt.axis('off')
plt.tight_layout()
plt.show()
No description has been provided for this image