Idiomas disponíveis:

Zena.food: Usei Machine Learning para Descobrir as Joias Escondidas de Gênova

Existe um artigo da Lauren Leek chamado How Google Maps quietly allocates survival across London’s restaurants. Li ele oito meses depois de me mudar para Gênova e não consegui parar. A premissa é precisa e um pouco desconfortável: a forma como o Google Maps ranqueia restaurantes não tem quase nada a ver com qualidade da comida. É sobre proeminência algorítmica, um composto de quantidade de avaliações, velocidade de acúmulo de avaliações, presença na web e sinais de interação. Estabelecimentos que acumulam mais avaliações ranqueiam mais alto. Um rank maior traz mais visitantes. Mais visitantes trazem mais avaliações. O ciclo não se importa se a comida é boa.

Terminei de ler e o pensamento foi imediato. Moro em Gênova, preciso fazer isso para Gênova. Não porque estava tendo dificuldade de encontrar bons restaurantes, e não porque alguém tinha sugerido. Só porque ainda estava explorando uma cidade para a qual me mudei há oito meses, e essa era uma forma interessante de entendê-la melhor. A cidade seria o dataset. A questão era se os dados confirmariam o que eu havia começado a suspeitar enquanto andava por aí.

O resultado é o zena.food, um mapa interativo do cenário de restaurantes de Gênova que elimina o viés estrutural e revela os lugares que o algoritmo está sistematicamente subvalorizando. E também o contrário.

Zena.Food Logo

O que Gênova realmente é

Gênova não é o que as pessoas imaginam quando pensam na Itália. Não é Florença. Não é Roma, graças a Deus. É uma cidade portuária em funcionamento, empilhada verticalmente contra uma encosta de uma forma que não faz nenhum sentido geográfico óbvio, com uma rede de vielas medievais, os Caruggi, que os turistas acham intimidante e os moradores navegam na memória muscular. As vielas do centro histórico listado pela UNESCO são estreitas o suficiente para que duas pessoas com sacolas de compras não consigam passar sem uma delas dançar na calçada. Os sinais de GPS ricocheteiam em prédios medievais de seis andares e te roteiam para becos sem saída com a confiança de alguém que nunca esteve aqui. Isso não é uma reclamação. É a personalidade da cidade.

O turismo que Gênova recebe é quase inteiramente fluxo de navios de cruzeiro. Os navios chegam em Porto Antico. Os passageiros têm de quatro a seis horas. Eles caminham até o Acquario, que é o maior aquário da Itália e genuinamente merece sua reputação, comem em algum lugar a distância a pé do terminal e voltam para o navio. Deixam avaliações. As avaliações descrevem a experiência de ser turista em Gênova por uma tarde. Não têm quase nada a ver com a cidade em que os moradores realmente vivem. Isso cria um efeito previsível: a orla e as zonas próximas ao turismo acumulam avaliações em um volume que não tem nada a ver com qualidade da comida e tudo a ver com fluxo de pessoas. Um restaurante em uma zona elevada da cidade, acessível por funicular e frequentado quase inteiramente por pessoas que realmente moram aqui, pode ter um décimo das avaliações de um estabelecimento da orla. As notas parecem comparáveis no mapa. Estão medindo coisas completamente diferentes.

A rede de vielas antigas piora isso. Pequenas trattorias ali às vezes operam há três gerações sem um site, sem uma conta no Instagram, sem um perfil no Google Business que alguém gerencie ativamente. Acumulam avaliações da forma como coisas genuinamente boas acumulam reconhecimento: lentamente, de pessoas que estavam realmente procurando. O algoritmo lê isso como baixa confiança. É baixa exposição, o que é uma coisa completamente diferente. O Google Maps trata os dois como restaurantes com notas. O contexto estrutural é invisível para o algoritmo, e essa invisibilidade não é neutra.

O Tourist Index

O Zena.food foi construído para pessoas que moram em Gênova, não para quem está de passagem num navio. O que eu acho genuinamente irritante nos restaurantes armadilha para turistas não é que eles existam. É que o algoritmo os promove ativamente à custa de tudo o mais, porque os sinais que o algoritmo usa para medir qualidade são os mesmos sinais que estabelecimentos com alto fluxo turístico acumulam como efeito colateral estrutural da localização. Corrigir isso exigiu medi-lo primeiro.

O Tourist Index (TI) é uma pontuação composta de 0.0 a 1.0 que mede quanto da nota de um estabelecimento é provavelmente explicado pelo fluxo turístico em vez da qualidade local. Três pilares.

S_i, proximidade espacial (peso 40%). Decaimento de distância a partir de cinco polos turísticos: o Acquario, Piazza De Ferrari, Via Garibaldi, Stazione Principe e Boccadasse. Fórmula: max(0, 1 - d_min / 500). A 50 metros do Acquario, S_i fica em torno de 0.90. Estabelecimentos em zonas elevadas da cidade pontuam próximo a 0. Os cinco polos foram escolhidos olhando para um mapa de Gênova e perguntando onde os turistas de cruzeiro realmente vão. A resposta não era complicada.

V_i, vocabulário semântico (peso 40%). O Gemini 2.5 Flash lê o texto das avaliações de cada estabelecimento e pontua o quanto a linguagem é orientada ao turismo. Marcadores turísticos: “bem do lado do aquário”, avaliações em múltiplos idiomas, referências a cruzeiros, “perfeito para uma parada rápida antes de embarcar.” Marcadores locais: termos em dialeto, referências a pratos genoveses específicos pelos seus nomes reais (pansoti col tocco, trofie al pesto, farinata, cima alla genovese), o vocabulário que os italianos usam quando escrevem para outros italianos em vez de para qualquer um que possa estar lendo. Um 4.5 de alguém que escreve “focaccia incrível!!!! 😍😍” e um 4.5 de alguém que escreve “posto buonissimo, prezzi onesti” sem mais nada carregam informações diferentes. Esse pilar tenta codificar essa diferença.

E_i, acessibilidade topográfica (peso 20%). Elevação do OpenTopoData com resolução SRTM de 30m, com uma correção geoidal de +46m específica para a Ligúria, porque os dados de elevação por satélite têm um offset sistemático nessa região que estava colocando vários restaurantes genoveses tecnicamente embaixo d’água sem ela. Normalizado para que o nível do mar marque 1.0 e 50 metros acima marque 0.0. Abaixo de 50 metros, um turista consegue caminhar da orla. Acima, precisa saber para onde está indo.

O composto: TI = 0.40·S_i + 0.40·V_i + 0.20·E_i

Um pilar foi cortado. O design inicial incluía a proporção linguística das avaliações: a proporção de avaliações em italiano versus outros idiomas. Parecia um sinal turístico limpo. O problema é que a Google Places API retorna apenas cinco avaliações selecionadas algoritmicamente por estabelecimento, e essa seleção é enviesada em direção a conteúdo de alto engajamento, que se correlaciona com turistas, o que significa que para estabelecimentos com alto fluxo turístico a API já estava filtrando para avaliações em inglês antes mesmo de eu as ver. Usar isso como sinal turístico era circular. Teria medido o próprio viés da API, não a composição real de visitantes do restaurante. Foi removido.

Joias Escondidas

O Tourist Index diz ao modelo quanta exposição turística estrutural um estabelecimento tem. O modelo usa isso, mais tudo o mais que sabe sobre o estabelecimento, para prever que nota aquele estabelecimento deveria ter. Uma Joia Escondida é o que acontece quando a previsão está errada numa direção específica: o estabelecimento está indo significativamente melhor do que tudo que trabalha contra ele sugeriria.

Pense no que isso significa concretamente. Uma trattoria no fundo da rede de vielas tem um Tourist Index baixo, poucas avaliações, nenhuma presença na web e fica em uma zona onde o teto algorítmico é baixo. O modelo olha tudo isso e prevê uma nota modesta. Se a nota real é substancialmente maior, a diferença é o resíduo. Estabelecimentos no top 15% dos resíduos são Joias Escondidas. A desvantagem estrutural era real. O estabelecimento a superou mesmo assim. Essa diferença é o proxy mais limpo para qualidade que esse dataset consegue produzir.

No mapa, as joias brilham. O anel animado em volta de um marcador é o sinal visual de que há algo ali que vale a pena investigar. Clicar abre o cartão do estabelecimento com o detalhamento do Tourist Index por pilar e uma explicação SHAP em linguagem simples: quais fatores mais influenciaram a previsão do modelo e em qual direção. “Alta elevação contribuiu positivamente para a classificação como joia” significa que o modelo esperava uma nota mais baixa por causa da visibilidade reduzida da localização, e o estabelecimento superou essa expectativa. Não é garantia de uma ótima refeição. É uma sugestão bem fundamentada.

A Hidden Gem

Existe uma categoria secundária que vale mencionar: os acumuladores lentos. São estabelecimentos com uma nota alta em relação à quantidade de avaliações para o seu grupo de pares de culinária. Têm menos avaliações do que lugares similares em zonas similares, mas a nota se mantém ou supera o que seria esperado naquele volume de avaliações. O modelo não sabe por que têm poucas avaliações. Ele só nota que estão superando seu peso estrutural. São frequentemente lugares mais novos, ou lugares que os moradores encontraram discretamente sem que a máquina de avaliações os alcançasse. A flag existe porque uma joia com 300 avaliações significa algo diferente de uma joia com 18 avaliações e 4.6. As duas são interessantes. A segunda é mais rara.

O modelo

O Tourist Index descreve exposição estrutural. O modelo prevê a nota de referência estrutural: dado tudo o que se pode saber sobre a localização, proeminência, culinária e exposição de um estabelecimento, que nota o algoritmo esperaria?

Regressão com XGBoost, treinada em 935 estabelecimentos genoveses com pelo menos 30 avaliações e em operação. A matriz de features inclui contagem logarítmica de avaliações (não contagem bruta, porque a distribuição é fortemente assimétrica à direita e um punhado de estabelecimentos da orla têm totais de avaliações na casa das dezenas de milhar), nível de preço, densidade de célula hexagonal H3, elevação, uma flag de centro histórico, distância até o nó de transporte público mais próximo, o Tourist Index, categoria de culinária classificada pelo Gemini, uma flag de rede, diversidade de culinária por zona e um tipo de zona derivado de clustering PCA de agregados H3. Esse clustering agrupa áreas por caráter composto: nota média, volume total de avaliações, concentração de redes, diversidade de culinária, produzindo quatro tipos. Uma joia em uma zona residencial comum é um sinal mais forte do que o mesmo resíduo em uma zona turística da orla, porque o teto estrutural é mais baixo no primeiro caso.

O Optuna rodou 100 trials para ajustar hiperparâmetros. RMSE de validação cruzada em 5 folds, estratificado por zona H3: 0.2809. O alvo era 0.25. As notas do Google em Gênova se concentram estreitamente entre 3.8 e 4.8 para a maioria dos estabelecimentos em operação. Ninguém que dá 2.0 para um restaurante estava num cruzeiro tendo uma tarde agradável. A distribuição é comprimida por design, e é por isso que um desvio padrão de resíduo de 0.28 em uma escala de 5 pontos é utilizável mesmo tendo perdido o alvo. Não foi onde o plano dizia que ia chegar, mas perto o suficiente para ser útil.

O output é um resíduo por estabelecimento: nota real menos nota prevista. Os 15% superiores dos resíduos são joias. Os 15% inferiores são armadilhas. Redes são excluídas da classificação de joias independentemente do resíduo, porque o reconhecimento de marca infla a velocidade de acúmulo de avaliações de formas não relacionadas à qualidade da comida. As 20 redes detectadas em Gênova não surpreenderam ninguém.

Contagem final: 1.578 estabelecimentos. 141 joias, 140 armadilhas. 143 sinalizados como acumuladores lentos. O dataset completo cobre restaurantes, bares e cafés em toda a cidade.

Sobre se dar bem com o Google

Uma coisa em que gastei um tempo desproporcional foi garantir que este projeto não cria problemas com o Google. Essa frase soa defensiva. Não é. É só precisa. Construir um produto em cima de uma API exige ler os Termos de Serviço, entender o que eles realmente dizem e estruturar a implementação técnica para cumpri-los. Fiz os três, em parte porque prefiro assim e em parte porque uma carta de cessação e desistência do Google encerraria o projeto mais rápido do que um RMSE ruim.

Os dados vêm da Google Places API (New), uma interface pública, documentada e explicitamente autorizada. O projeto não faz scraping do Google Maps. Não rastreia páginas. Cada requisição passa pela API oficial com uma chave válida, com field masks para solicitar apenas o que é necessário, com rastreamento de custo para ficar dentro do orçamento. Os Termos de Serviço do Google Maps Platform exigem que os dados em cache sejam atualizados em até 30 dias. O pipeline roda trimestralmente e trata disso. Os dados brutos da API, incluindo o texto das avaliações, são deletados após o processamento. O que o Zena.food armazena permanentemente é apenas o output derivado: a nota prevista, o resíduo, o rótulo de joia, a explicação SHAP. Esses são os próprios cálculos do modelo, não o conteúdo do Google. A única parte dos dados brutos da API armazenada indefinidamente é o place_id, o identificador único que o Google permite explicitamente arquivar permanentemente, e a única coisa necessária para vincular de volta à página oficial do Google Maps de um estabelecimento, o que cada cartão de estabelecimento faz.

O frontend exibe a atribuição “Powered by Google”. Linka para o Google Maps para todos os detalhes em nível de estabelecimento. O projeto não hospeda conteúdo de avaliações. Não compete com o Google Maps. Aponta para ele. A documentação legal e de privacidade completa está em zena.food/legale para quem quiser os detalhes.

Quanto ao lado financeiro: o Gemini 2.5 Flash através do AI Studio é gratuito em 1.500 requisições por dia, e uma rodada completa do pipeline de Gênova cabe dentro desse limite. Os dados de elevação e transporte são domínio público e ODbL respectivamente. A Google Places API não é nada disso. O valor que gastei nela é entre mim, o Google Cloud e um painel de faturamento que decidi parar de abrir. Tem um motivo para o pipeline rodar trimestralmente.

A stack

O pipeline completo roda em Python, o que não precisa de justificativa para nada que envolva spatial joins, arquivos Parquet e gradient boosting. A indexação espacial usa a grade hexagonal H3 da Uber, que divide o espaço de forma mais uniforme do que quadrados e fornece cálculos de densidade consistentes, seja em um bloco denso de vielas ou em um terraço esparso no morro onde três trattorias compartilham o que costumava ser o jardim de alguém.

O Gemini 2.5 Flash cuida da classificação de culinária e da pontuação semântica das avaliações. A escolha foi em parte pelo tier gratuito e principalmente pelo output JSON estruturado com validação Pydantic. Um pipeline que falha silenciosamente quando um LLM retorna prosa criativa em vez de um schema válido é um pipeline em que você vai desconfiar para sempre. O Pydantic faz as falhas serem barulhentas.

Go é minha linguagem principal. FastAPI com Pydantic v2 é o mais próximo que Python chega da disciplina de tipos do Go: schemas explícitos, validação rápida, comportamento previsível. A API carrega o arquivo Parquet pontuado na inicialização com um cache LRU e filtra por requisição. Não há banco de dados no momento da query porque não precisa haver. O dataset final é um arquivo. O pipeline o produz, a API o serve, e essa é a arquitetura completa.

O frontend é React com deck.gl para renderização do mapa. Com 1.500 estabelecimentos com anéis de brilho animados para as joias, o Leaflet degrada. O deck.gl roda em WebGL e não degrada. O MapLibre fornece a camada de tiles de base sem uma licença do Google Maps JavaScript. O Zustand gerencia o estado dos filtros. O TanStack Query gerencia o fetch de dados. Não tem Redux porque não há problema aqui que o Redux resolva e o Zustand não resolva em um décimo das linhas.

Page Filter

O que saiu

O site está no ar em zena.food. O mapa mostra todos os 1.578 estabelecimentos, filtráveis por culinária, preço, zona, elevação e status de joia. Clicar em um estabelecimento abre um painel com o detalhamento do Tourist Index por pilar, uma explicação SHAP do que impulsionou a previsão do modelo e um link para o estabelecimento no Google Maps.

As joias se concentram em dois lugares: a rede de vielas antigas e as zonas elevadas da cidade. As armadilhas se concentram na orla turística. Nenhuma das duas é surpreendente em retrospecto. O achado mais interessante é sobre culinárias de minorias étnicas. Estabelecimentos classificados pelo Gemini como Street Food e International aparecem no nível de joias a uma taxa maior do que sua participação no dataset total. As culinárias de minorias étnicas representam cerca de 22% de todos os estabelecimentos. Sua taxa de joias é visivelmente maior. Lauren Leek encontrou o mesmo padrão em Londres. A explicação parcial é localização: esses estabelecimentos tendem a se concentrar em zonas com baixa exposição turística, dando-lhes linhas de base estruturais mais baixas, mais fáceis de superar. Mas depois de controlar por localização e proeminência, ainda há sinal restante. O que ele representa é uma questão genuinamente aberta, e uma que acho mais interessante do que o mapa em si.

A flag de acumulador lento produziu a validação mais satisfatória. Vários dos 143 estabelecimentos sinalizados como ascendentes eram lugares que moradores já tinham me contado. O modelo os encontrou de forma independente, usando apenas contagem de avaliações relativa aos pares de culinária e nota atual. Ele não tem ideia de por que esses estabelecimentos têm poucas avaliações. Só notou que estão superando seu peso estrutural.

Zena.food image.

O Zena.food acabou sendo um excelente projeto de fim de semana, no sentido de que levou consideravelmente mais do que um fim de semana e estou muito feliz de ter construído mesmo assim. É o melhor guia de restaurantes, bares e cafés que encontrei para Gênova, o que é um bar baixo considerando que a competição é o Google Maps com seus vieses conhecidos. Mas o bar foi superado, e minha esposa, que tem opiniões fortes sobre jantares românticos e uma saudável desconfiança de armadilhas para turistas, agora está genuinamente feliz em deixar o app sugerir o próximo.