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:¶
- Elaborazione dei dati: Puliamo e organizziamo le risposte del questionario
- Geocoding: Troviamo le coordinate geografiche di ogni luogo menzionato
- Filtraggio geografico: Manteniamo solo i luoghi effettivamente dentro Bari
- Aggregazione: Contiamo quante persone frequentano ogni spazio
- 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:
# 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.
# Leggiamo il file Excel con i dati del questionario
df = pd.read_excel("spazi.xlsx")
# Rimuoviamo le righe dove non è stata data risposta alla domanda principale
# "Quali?" si riferisce a "Quali spazi frequenti?"
df = df[df['Quali?'].notna()]
# Controlliamo i valori unici per la domanda "Frequenti questi spazi?"
# Tutti hanno risposto "Si", quindi possiamo rimuovere questa colonna
df["Frequenti questi spazi? "].unique()
array(['Si'], dtype=object)
# Rimuoviamo le colonne che non servono per l'analisi
df.drop(["Frequenti questi spazi? ", "Quale spazio sogni a Bari?"], axis=1, inplace=True)
# 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.
# Carichiamo il file con i luoghi già mappati
# (questo file contiene i luoghi standardizzati e raggruppati)
df = pd.read_csv("spazi mapped.csv")
# Manteniamo solo le righe dove è stata fatta una mappatura
df = df[df['Map'].notna()]
# 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)
df
| 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").
# 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]
# 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.
# 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.
# 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).
# 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.
# Identifichiamo i luoghi senza coordinate (0,0)
missingPlaces = {}
for k, v in places.items():
if v["lat"] == 0 or v["lng"] == 0:
missingPlaces[k] = []
# 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": []
}
# 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": []
}
# 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.
# 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
{'(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}}
# 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)
# Verifichiamo quanti luoghi abbiamo geocodificato
len(places)
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.
# 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?')
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
# 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]
# 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}
# 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]
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
create_map(outer, r"..\..\mappa\data\bari.geojson")
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.
# 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))
# 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
})
# 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.
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
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()