Zena.food: Ho Usato il Machine Learning per Trovare i Ristoranti Nascosti di Genova
Esiste un articolo di Lauren Leek intitolato How Google Maps quietly allocates survival across London’s restaurants. L’ho letto otto mesi dopo essere arrivato a Genova e non sono riuscito a smettere. La premessa è precisa e un po’ scomoda: il modo in cui Google Maps classifica i ristoranti non ha quasi nulla a che fare con la qualità del cibo. Riguarda la prominenza algoritmica, un punteggio composito di numero di recensioni, velocità con cui arrivano, presenza sul web e segnali di interazione. I locali che accumulano più recensioni salgono in classifica. Una posizione più alta porta più visitatori. Più visitatori portano più recensioni. Al loop non interessa se il cibo è buono.
Ho finito di leggere e il pensiero è stato immediato. Vivo a Genova, dovrei farlo per Genova. Non perché facessi fatica a trovare buoni ristoranti, e non perché qualcuno me lo avesse suggerito. Semplicemente perché stavo ancora esplorando una città in cui mi ero trasferito otto mesi prima, e questo era un modo interessante per capirla meglio. La città sarebbe diventata il dataset. La domanda era se i dati avrebbero confermato quello che avevo cominciato a sospettare camminando per le strade.
Il risultato è zena.food, una mappa interattiva del panorama ristorativo genovese che elimina i bias strutturali e porta in superficie i posti che l’algoritmo sottovaluta sistematicamente. E anche il contrario.

Cos’è davvero Genova
Genova non è quello che la gente si immagina quando pensa all’Italia. Non è Firenze. Non è Roma, grazie al cielo. È una città portuale che lavora, costruita verticalmente contro una collina in un modo che non ha nessun senso geografico ovvio, con una rete di vicoli medievali, i Caruggi, che i turisti trovano intimidenti e i residenti percorrono a memoria muscolare. I vicoli del centro storico, patrimonio UNESCO, sono così stretti che due persone con le borse della spesa non riescono a passarsi senza che una delle due balli sul marciapiede. I segnali GPS rimbalzano sugli edifici medievali di sei piani e ti portano in vicoli ciechi con la sicurezza di qualcuno che non è mai stato qui. Questa non è una lamentela. È la personalità della città.
Il turismo che Genova riceve è quasi interamente traffico di navi da crociera. Le navi arrivano al Porto Antico. I passeggeri hanno da quattro a sei ore. Vanno all’Acquario di Genova, che è il più grande acquario d’Italia e merita davvero la sua reputazione, mangiano da qualche parte a portata a piedi dal terminal e tornano sulla nave. Lasciano recensioni. Le recensioni descrivono l’esperienza di essere turisti a Genova per un pomeriggio. Non hanno quasi nulla a che fare con la città in cui i residenti vivono davvero. Questo crea un effetto prevedibile: il lungomare e le zone adiacenti ai turisti accumulano recensioni in un volume che non ha nulla a che fare con la qualità del cibo e tutto a che fare con il passaggio di persone. Un ristorante in una zona elevata della città, raggiungibile con la funicolare e frequentato quasi esclusivamente da chi ci vive davvero, potrebbe avere un decimo delle recensioni di un locale sul lungomare. Le valutazioni sembrano comparabili sulla mappa. Misurano cose completamente diverse.
La rete di vicoli peggiora la situazione. Piccole trattorie lì dentro hanno a volte operato per tre generazioni senza un sito web, senza un account Instagram, senza un profilo Google Business che qualcuno gestisca attivamente. Accumulano recensioni nel modo in cui le cose davvero buone accumulano riconoscimento: lentamente, da persone che stavano cercando davvero. L’algoritmo legge questo come bassa fiducia. È bassa esposizione, che è una cosa completamente diversa. Google Maps tratta entrambi come ristoranti con valutazioni. Il contesto strutturale è invisibile all’algoritmo, e quella invisibilità non è neutrale.
Il Tourist Index
Zena.food è costruito per le persone che vivono a Genova, non per quelle di passaggio su una nave. La cosa che trovo genuinamente fastidiosa dei ristoranti trappola per turisti non è che esistano. È che l’algoritmo li promuove attivamente a scapito di tutto il resto, perché i segnali che l’algoritmo usa per misurare la qualità sono gli stessi segnali che i locali con molto traffico turistico accumulano come effetto collaterale strutturale della posizione. Misurarlo era il prerequisito per correggerlo.
Il Tourist Index (TI) è un punteggio composito da 0.0 a 1.0 che misura quanto della valutazione di un locale sia probabilmente spiegato dal traffico turistico piuttosto che dalla qualità locale. Tre pilastri.
S_i, prossimità spaziale (peso 40%). Decadimento della distanza da cinque poli turistici: l’Acquario di Genova, Piazza De Ferrari, Via Garibaldi, Stazione Principe e Boccadasse. Formula: max(0, 1 - d_min / 500). A 50 metri dall’Acquario, S_i è intorno a 0.90. I locali nelle zone elevate della città si avvicinano a 0. I cinque poli sono stati scelti guardando una mappa di Genova e chiedendosi dove vadano davvero i turisti delle crociere. La risposta non era complicata.
V_i, vocabolario semantico (peso 40%). Gemini 2.5 Flash legge il testo delle recensioni per ogni locale e valuta quanto il linguaggio sia orientato ai turisti. Segnali turistici: “right next to the aquarium”, recensioni in più lingue, riferimenti alle navi da crociera, “perfect for a quick stop before boarding.” Segnali locali: termini dialettali, riferimenti a piatti genovesi specifici con i loro nomi reali (pansoti col tocco, trofie al pesto, farinata, cima alla genovese), il vocabolario che usano gli italiani quando scrivono per altri italiani piuttosto che per chiunque possa leggere. Un 4.5 da qualcuno che scrive “amazing focaccia!!!! 😍😍” e un 4.5 da qualcuno che scrive “posto buonissimo, prezzi onesti” e nient’altro portano informazioni diverse. Questo pilastro cerca di codificare quella differenza.
E_i, accessibilità topografica (peso 20%). Elevazione da OpenTopoData a risoluzione SRTM 30m, con una correzione del geoide di +46m specifica per la Liguria, perché i dati di elevazione satellitare hanno un offset sistematico in questa regione che metteva diversi ristoranti genovesi tecnicamente sott’acqua senza di essa. Normalizzata in modo che il livello del mare valga 1.0 e 50 metri sopra valga 0.0. Sotto i 50 metri, un turista può camminare dal lungomare. Sopra, deve sapere dove sta andando.
Il composito: TI = 0.40·S_i + 0.40·V_i + 0.20·E_i
Un pilastro è stato eliminato. Il progetto iniziale includeva il rapporto linguistico delle recensioni: la proporzione di recensioni in italiano rispetto alle altre lingue. Sembrava un segnale turistico pulito. Il problema è che Google Places API restituisce solo cinque recensioni selezionate algoritmicamente per locale, e quella selezione è orientata verso i contenuti ad alto engagement, che si correlano con i turisti, il che significa che per i locali ad alto traffico turistico l’API stava già filtrando verso le recensioni in inglese prima ancora che le vedessi. Usarlo come segnale turistico era circolare. Avrebbe misurato il bias dell’API, non la composizione reale dei visitatori del ristorante. È stato rimosso.
Gli Hidden Gems
Il Tourist Index dice al modello quanta esposizione turistica strutturale ha un locale. Il modello usa quello, più tutto il resto che sa sul locale, per prevedere quale valutazione quel locale dovrebbe avere. Un Hidden Gem è quello che succede quando la previsione è sbagliata in una direzione specifica: il locale sta andando significativamente meglio di quanto tutto ciò che lavora contro di lui suggerirebbe.
Pensate a cosa significa concretamente. Una trattoria nel profondo della rete di vicoli ha un tourist index basso, poche recensioni, nessuna presenza sul web e si trova in una zona dove il soffitto algoritmico è basso. Il modello guarda tutto questo e prevede una valutazione modesta. Se la valutazione reale è sostanzialmente più alta, il gap è il residuo. I locali nel top 15% dei residui sono Hidden Gems. Lo svantaggio strutturale era reale. Il locale l’ha superato comunque. Quel gap è il proxy più pulito per la qualità che questo dataset può produrre.
Sulla mappa, i gems brillano. L’anello animato attorno a un marcatore è il segnale visivo che c’è qualcosa lì che vale la pena esplorare. Cliccandoci si apre la scheda del locale con la ripartizione del Tourist Index per pilastro e una spiegazione SHAP in linguaggio chiaro: quali fattori hanno influenzato maggiormente la previsione del modello e in quale direzione. “High elevation contributed positively to the gem classification” significa che il modello si aspettava una valutazione più bassa a causa della ridotta visibilità della posizione, e il locale ha superato quella aspettativa. Non è una garanzia di un ottimo pasto. È un suggerimento ben ragionato.

C’è una categoria secondaria che vale la pena citare: gli slow accumulators. Sono locali con una valutazione alta rispetto al loro numero di recensioni per il loro gruppo di pari per cucina. Hanno meno recensioni di posti simili in zone simili, ma la loro valutazione regge o supera quello che ci si aspetterebbe a quel volume di recensioni. Il modello non sa perché abbiano poche recensioni. Nota solo che stanno sovraperformando il loro peso strutturale. Spesso sono posti più nuovi, o posti che i residenti hanno trovato in silenzio senza che il meccanismo delle recensioni si sia ancora aggiornato. Il flag esiste perché un gem con 300 recensioni significa qualcosa di diverso da un gem con 18 recensioni e un 4.6. Entrambi sono interessanti. Il secondo è più raro.
Il modello
Il Tourist Index descrive l’esposizione strutturale. Il modello prevede la valutazione di base strutturale: dato tutto ciò che si può sapere sulla posizione, la prominenza, la cucina e l’esposizione di un locale, quale valutazione si aspetterebbe l’algoritmo?
Regressione XGBoost, addestrata su 935 locali genovesi con almeno 30 recensioni e attualmente operativi. La matrice di feature include il log del numero di recensioni (non il numero grezzo, perché la distribuzione è fortemente asimmetrica a destra e una manciata di locali sul lungomare ha totali di recensioni a cinque cifre), il livello di prezzo, la densità delle celle esagonali H3, l’elevazione, un flag per il centro storico, la distanza al nodo di trasporto pubblico più vicino, il Tourist Index, la categoria di cucina classificata da Gemini, un flag per le catene, la diversità di cucina per zona e un tipo di zona derivato dal clustering PCA degli aggregati H3. Quel clustering raggruppa le aree per carattere composito: valutazione media, volume totale di recensioni, concentrazione di catene, diversità di cucina, producendo quattro tipi. Un gem in una zona residenziale ordinaria è un segnale più forte dello stesso residuo in una zona lungomare ad alto traffico turistico, perché il soffitto strutturale è più basso nel primo caso.
Optuna ha eseguito 100 trial per ottimizzare gli iperparametri. RMSE di cross-validation su 5 fold, stratificato per zona H3: 0.2809. Il target era 0.25. Le valutazioni Google a Genova si concentrano strettamente tra 3.8 e 4.8 per la maggior parte dei locali operativi. Nessuno che dà a un ristorante un 2.0 era su una nave da crociera passando un pomeriggio piacevole. La distribuzione è compressa by design, ed è per questo che una deviazione standard del residuo di 0.28 su una scala a 5 punti è utilizzabile anche se ha mancato il target. Non dove il piano diceva che sarebbe atterrato, ma abbastanza vicino da essere utile.
L’output è un residuo per locale: valutazione reale meno valutazione prevista. Il top 15% dei residui sono gems. Il bottom 15% sono traps. Le catene sono escluse dalla classificazione gem indipendentemente dal residuo, perché il riconoscimento del marchio gonfia la velocità delle recensioni in modi non correlati alla qualità del cibo. Le 20 catene rilevate a Genova non erano sorprendenti.
Conteggio finale: 1.578 locali. 141 gems, 140 traps. 143 segnalati come slow accumulators. Il dataset completo copre ristoranti, bar e caffè in tutta la città.
Giocare bene con Google
Una cosa su cui ho speso una quantità sproporzionata di tempo è stata assicurarmi che questo progetto non creasse problemi con Google. Quella frase sembra difensiva. Non lo è. È solo accurata. Costruire un prodotto sopra un’API richiede di leggere i Termini di Servizio, capire cosa dicono davvero e strutturare l’implementazione tecnica per rispettarli. Ho fatto tutte e tre le cose, in parte perché preferisco così e in parte perché una lettera di cessazione e desistenza da Google chiuderebbe il progetto più velocemente di un RMSE pessimo.
I dati provengono dalla Google Places API (New), un’interfaccia pubblica, documentata ed esplicitamente autorizzata. Il progetto non fa scraping di Google Maps. Non esegue crawling di pagine. Ogni richiesta passa attraverso l’API ufficiale con una chiave valida, con field mask per richiedere solo ciò che è necessario, con cost tracking per restare nel budget. I Termini di Servizio della Google Maps Platform richiedono che i dati in cache vengano aggiornati entro 30 giorni. La pipeline gira trimestralmente e gestisce questo. I dati grezzi dall’API, incluso il testo delle recensioni, vengono eliminati dopo l’elaborazione. Ciò che Zena.food archivia in modo permanente è solo l’output derivato: la valutazione prevista, il residuo, l’etichetta gem, la spiegazione SHAP. Questi sono i calcoli del modello, non i contenuti di Google. L’unico dato grezzo dell’API archiviato indefinitamente è il place_id, l’identificatore univoco che Google permette esplicitamente di archiviare in modo permanente, e l’unica cosa necessaria per collegarsi alla pagina ufficiale Google Maps di un locale, cosa che fa ogni scheda locale.
Il frontend mostra l’attribuzione “Powered by Google”. Collega a Google Maps per tutti i dettagli a livello di locale. Il progetto non ospita contenuti di recensioni. Non compete con Google Maps. Punta verso di esso. La documentazione legale e sulla privacy completa è su zena.food/legale per chi vuole i dettagli.
Sul lato finanziario: Gemini 2.5 Flash tramite AI Studio è gratuito fino a 1.500 richieste al giorno, e un’intera esecuzione della pipeline genovese rientra in quel limite. I dati di elevazione e trasporto sono rispettivamente di dominio pubblico e ODbL. La Google Places API non è nessuna di queste cose. Quanto ho speso per essa è una questione tra me, Google Cloud e un dashboard di fatturazione che ho deciso di smettere di aprire. C’è un motivo per cui la pipeline gira trimestralmente.
Lo stack
La pipeline completa gira su Python, che non richiede giustificazioni per nulla che riguardi join spaziali, file Parquet e gradient boosting. L’indicizzazione spaziale usa la griglia esagonale H3 di Uber, che suddivide lo spazio in modo più uniforme dei quadrati e fornisce calcoli di densità coerenti sia che ci si trovi in un blocco di vicoli denso sia in una terrazza collinare sparsa dove tre trattorie condividono quello che una volta era il giardino di qualcuno.
Gemini 2.5 Flash gestisce la classificazione della cucina e il punteggio semantico delle recensioni. La scelta era in parte il tier gratuito e soprattutto l’output JSON strutturato con validazione Pydantic. Una pipeline che fallisce silenziosamente quando un LLM restituisce prosa creativa invece di uno schema valido è una pipeline di cui non ti fiderai mai più. Pydantic rende i fallimenti rumorosi.
Go è il mio linguaggio principale. FastAPI con Pydantic v2 è la cosa più vicina che Python ha alla disciplina dei tipi di Go: schemi espliciti, validazione rapida, comportamento prevedibile. L’API carica il file Parquet con punteggi all’avvio con una cache LRU e filtra su richiesta. Non c’è nessun database al momento della query perché non ce n’è bisogno. Il dataset finale è un file. La pipeline lo produce, l’API lo serve, e questa è tutta l’architettura.
Il frontend è React con deck.gl per il rendering della mappa. Con 1.500 locali con anelli luminosi animati per i gems, Leaflet degrada. deck.gl gira su WebGL e non degrada. MapLibre fornisce il layer di tile di base senza una licenza Google Maps JavaScript. Zustand gestisce lo stato dei filtri. TanStack Query gestisce il fetching dei dati. Non c’è Redux perché non c’è nessun problema qui che Redux risolva e che Zustand non risolva in un decimo delle righe.

Cosa è venuto fuori
Il sito è live su zena.food. La mappa mostra tutti i 1.578 locali, filtrabili per cucina, prezzo, zona, elevazione e stato gem. Cliccando su un locale si apre un pannello con la ripartizione del Tourist Index per pilastro, una spiegazione SHAP di cosa ha guidato la previsione del modello e un link al locale su Google Maps.
I gems si concentrano in due posti: la rete di vicoli del centro storico e le zone elevate della città. Le traps si concentrano nel lungomare turistico. Nessuna delle due cose è sorprendente a posteriori. Il risultato più interessante riguarda le cucine delle minoranze etniche. I locali classificati da Gemini come Street Food e International appaiono nel tier gem a un tasso superiore alla loro quota del dataset totale. Le cucine delle minoranze etniche rappresentano circa il 22% di tutti i locali. Il loro tasso di gem è notevolmente più alto. Lauren Leek ha trovato lo stesso schema a Londra. La spiegazione parziale è la posizione: questi locali tendono a concentrarsi in zone con bassa esposizione turistica, dando loro basi strutturali più basse che sono più facili da superare. Ma dopo aver controllato per posizione e prominenza, rimane ancora del segnale. Cosa rappresenta è una domanda aperta, e una che trovo più interessante della mappa stessa.
Il flag slow accumulator ha prodotto la validazione più soddisfacente. Diversi dei 143 locali segnalati come in ascesa erano posti che i residenti mi avevano già citato. Il modello li ha trovati in modo indipendente, usando solo il numero di recensioni rispetto ai pari per cucina e la valutazione attuale. Non sa perché quei locali abbiano poche recensioni. Ha solo notato che stanno sovraperformando il loro peso strutturale.

Zena.food si è rivelato un ottimo progetto da weekend, nel senso che ha richiesto considerevolmente più di un weekend e sono molto contento di averlo costruito comunque. È la migliore guida a ristoranti, bar e caffè che ho trovato per Genova, il che è un asticella bassa da superare considerando che la concorrenza è Google Maps con i suoi bias noti. Ma l’asticella è stata superata, e mia moglie, che ha opinioni forti sulle cene romantiche e una sana diffidenza verso le trappole per turisti, è ora genuinamente felice di lasciare che l’app suggerisca la nostra prossima.