Latitude e longitude com Python: Calculando distâncias entre vários pontos em um mapa.

Diferentes abordagens e maneiras de pensar o problema

Caio Estrella
Data Hackers
8 min readNov 29, 2021

--

Photo by Annie Spratt on Unsplash

Recentemente participei de projetos com o seguinte desafio: Selecione uma entidades em um mapa e descubra quais outras existem dentro de um raio X.

Este post tem o objetivo de compartilhar o que descobri, o que errei, o que acertei, e a melhor solução que apliquei até agora, e sem recorrer ao Google API. Que, aliás, é ótimo, mas custa dinheiro… e em dólares.

Tenhamos em mente que esse tipo de projeto é geralmente feito para ser apresentado em um dashboard (no Power BI por exemplo), ou em alguma aplicação web.

Desafios

  • Como calcular as distâncias?
  • E se você não tiver as coordenadas?
  • Como estruturar seu dataset?

Distâncias 📏

As distâncias que calcularemos serão via fórmula de haversine que é uma importante equação usada em navegação, fornecendo distâncias entre dois pontos de uma esfera a partir de suas latitudes e longitudes.

Primeiro instale em um prompt de comando:

pip install haversine

E segue um exemplo:

from haversine import haversine

lyon = (45.7597, 4.8422) # (lat, lon)
paris = (48.8567, 2.3508)

haversine(lyon, paris)
>> 392.2172595594006 # in kilometers

E aqui temos a distância entre Lyon e Paris em quilômetros. No caso, as coordenadas correspondem mais ou menos ao centroide de cada cidade. Se você jogar (45.7597, 4.8422) no Google vai ver que fica bem no meio de Lyon mesmo.

Ok, mas e se eu quiser calcular várias distancias? Esse é o principal desafio, e explicarei mais adiante. Mas antes disso, vejamos diverentes formas de obter as coordenadas.

Descobrindo as coordenadas 🌐

Nem sempre possuimos o lat long das nossas entidades. Pode ser que para o seu projeto você só tenha um endereço ou um CEP. Então podemos pensar em obter lat long a partir do que temos.

O primeiro questionamento a se fazer é: Qual é o nivel de precisão necessário? Para muitos projetos essas diferença entre as coordenadas de um endereço completo e de um CEP é irrelevante, mas vale refletir antes de avançar.

Meu palpite é que para a logística de seja lá o que o seu projeto pretenda auxiliar, precisão a nível de endereço é um exagero. E como veremos mais adiante, existe um trade-off entre essa precisão geográfica e a qualidade do projeto como um todo, tanto na estrutura dos dados quanto no tempo de execução dos cálculos.

Contarei a minha experiência com cada abordagem. Talvez o leitor possa sugerir algo melhor para cada caso.

Via endereço 👎

A biblioteca que mais aparece em buscas no Google é a geopy, utilizando o recurso Nominatim, que é uma API que te conecta a um negócio chamado OpenStreetMap.

Ela funciona, mas tem alguns problemas. Na minha experiência, o OpenStreetMap esteve fora do ar diversas vezes. Além disso, achei a documentação confusa. Mas o que mais me incomodou foi o fato da busca muitas vezes a busca não retornar um resultado. Isso se dá por dois motivos:

  • Nominatim é incompleto. Imagine um endereço em uma cidade no interior do Brasil. Tudo isso atualizado e de graça? Talvez para os EUA funcione melhor.
  • A idéia de buscar por um endereço é ruim. E se a sua base tiver um erro de digitação? E “Rua Antônio Pedro” pode aparecer como “R. Antonio Pedro”, “Rua Antônio-Pedro”, “Rua A. Pedro” e por aí vai. O Nominatim até tem maneiras de contornar isso, mas como eu disse, é tudo meio confuso. Sem falar nos bairros, que hora se chamam uma coisa, hora outra. E do nada ele sai do ar… enfim, um caos.
  • Por ser uma API estamos trabalhando com requisições HTTP. O tempo de resposta entre uma requisição e outra é geralmente de 1 segundo. Parece pouco, mas imagine milhões de requisições.

O leitor que tiver uma experiência melhor em obter lat long via endereços, por favor deixe um comentário.

Via CEP 👎

Também podemos obter lat long via Nominatim. Mas muitos não retornam nada. Porém, a ideia de CEP ainda parece boa. Afinal, reduziríamos o número de pontos no mapa, ja que alguns compartilhariam provavelmente o mesmo CEP. Só teríamos que dar um drill-down para vermos o que existe dentro daquela coordenada. É uma ótima ideia. Existem algumas bibliotecas grátis para trabalhar com CEP e Python descritas na internet.

Querendo muito, pagando nada… bom, tive problemas com todas. A que melhor me atendeu foi uma API provida por um projeto chamado “CEP Aberto”. Recomendo muito. O trabalho feito ali é realmente bom, mas não perfeito. Alguns CEPs ainda não retornam nada. Além disso, ao precisar de muitas coordenadas, fui travado no limite e tempo das requisições.

Em suma: Se você não tem as coordenadas, recomendo tentar o CEP Aberto. Se você precisar de muitas coordenadas, faça as contas: Cada requisição leva 1 segundo para retornar um valor. O CEP Aberto te libera 10.000 consultas por cadastro. Quantos cadastros você teria que ter rodando simultaneamente e em quantos dias você baixaria tudo? Seu projeto tem dinheiro e demanda precisão? Use a API do Google. Não tem? Vem comigo.

Via município 👍

A melhor solução caso não precise de especificidade. Basta sabermos o que existe naquele município, e a diferença para as localizações reais das entidades dentro daquela coordenada é uma margem de erro aceitável.

Então vamos trabalhar com o nome do município e do estado, já que existem municipios com o mesmo nome.

OBS: Se não tiver nada de município no seu dataset, você precisa extrair pelos dígitos do CEP ou CNPJ.

Você vai precisar de uma tabela com os municípios, seus respectivos códigos e latitudes e longitudes. Pode pegar aqui.

A ideia é que você tenha uma tabela de distâncias separada da sua tabela com as instâncias.

Você não precisa saber a distância entre as localizações exatas das instâncias. Mas saber a distância entre os municípios em que as instâncias estão. Como muitas instâncias estão no mesmo município, isso reduz a quantidade de cálculos que precisamos realizar.

Procedimento

Merge

Combine a sua tabela com a de municípios para obter a latitude e longitude de cada município. Caso esteja brigando com o merge() ou concat() no Python, escrevi um post que pode te ajudar com isso.

Se você não tinha o código do município no seu dataset, fique atento para fazer o merge utilizando o nome do município e o estado também, pois existem municípios com o mesmo nome espalhados por aí. Segue a dica:

meu_df = meu_df.merge(muns, how = 'left', on = ['municipio', 'UF'])#talvez você tenha que usar "left_on" e "right_on" ao invés de apenas "on" caso as tabelas tenham nomes de colunas diferente, ou apenas renomear a sua tabela :) 

Do contrário, faça o merge usando o código do município e seja feliz.

Calculando distâncias entre os municípios

Agora queremos ter as distâncias entre todos os municípios do Brasil. Não na sua tabela, mas na tabela de municípios que você baixou, aqui chamada de “muns”. Pode parecer estranho, mas já explico mais à frente.

Antes de calcular as distências devevemos fazer alguns ajustes na tabela para evitar problemas lá na frente:

# Renomeia a coluna "nome" e remove algumas colunas
muns = muns[['nome', 'UF', 'latitude', 'longitude']].rename(columns={'nome':'municipio'})
# Coloca em maiúsculas
muns['municipio'] = [x.upper() for x in muns['municipio']]
# remove acentos
muns['nome'] = muns['municipio'].str.normalize('NFKD').str.encode('ascii', errors='ignore').str.decode('utf-8')

O cálculo das distâncias pode ser feito da seguinte forma (Sugestões? alguém?):

def calc_distancia():
for index, (lat, long) in enumerate(zip(muns.latitude.values, muns.longitude.values)):
distancias = muns.apply(lambda x: (haversine((x.latitude, x.longitude), (lat, long)), x.municipio + ' - ' + x.UF) , axis=1)
print(index)
for i in distancias:
df_muns.loc[index, i[1]] = i[0]

Rode calc_distancia() e aguarde. Deixei um print(index) para ir acompanhando o processo. Pode demorar algumas horas. (Aceito sugestões para acelerar. Tentei np.vectorize(), mas os loops não ajudam. Swifter também não fez diferença)

Quando terminar, seu DataFrame ‘muns’ terá muitas colunas a mais. O município X e sua distância para todos os outros. O município Y e sua distância para todos os outros… etc.

Selecione essas colunas que foram criadas:

# Verifique se é isso mesmo! Se nao for, ajuste o slice.cols_muns = df_muns.columns[6:]

Continue para o melt:

df_muns_melt = muns.melt(id_vars=['municipio', 'UF'], value_vars=df_muns[cols_muns]).rename(columns={'variable': 'MUNICIPIO_UF_DESTINO', 'value': 'distancia_melt'})

Nesse ponto você terá um dataframe com todas as distâncias entre os municípios. Mas para a nossa estrutura de dados, precisamos definir qual é o município de origem e o de destino. Continue:

df_muns_melt['MUN_DESTINO'] = df_muns_melt['MUNICIPIO_UF_DESTINO'].swifter.apply(lambda x: x.split(' - ')[0])df_muns_melt['UF_DESTINO'] = df_muns_melt['MUNICIPIO_UF_DESTINO'].swifter.apply(lambda x: x.split(' - ')[1])df_muns_melt['distancia_melt'] = [str(x).replace('.', ',') for x in df_muns_melt['distancia_melt']]df_muns_melt.rename(columns={'municipio': 'municipio_origem', 'UF': 'UF_origem'}, inplace=True)

Como removemos algumas colunas anteriormente, devemos recuperá-las:

df_muns_final = df_muns_melt.merge(muns[['municipio', 'UF', 'latitude', 'longitude']], how= 'left', left_on = ['MUN_DESTINO', 'UF_DESTINO'], right_on = ['municipio', 'UF']).drop(columns = ['municipio', 'UF'])df_muns_final.rename(columns = {'latitude': 'latitude_destino', 'longitude': 'longitude_destino'}, inplace=True)

Agora ficou faltando apenas o código do município (origem e destino) e limpar algumas colunas criadas no merge:

df_muns_final = df_muns_final.merge(muns[['nome', 'UF', 'siafi_id']], how = 'left', left_on = ['MUN_DESTINO', 'UF_DESTINO'], right_on = ['nome', 'UF'])df_muns_final.rename(columns = {'siafi_id': 'codigo_mun_destino'}, inplace=True)df_muns_final = df_muns_final.merge(muns[['nome', 'UF', 'siafi_id']], how = 'left', left_on = ['municipio_origem', 'UF_origem'], right_on = ['nome', 'UF'])
df_muns_final.rename(columns = {'siafi_id': 'codigo_mun_origem'}, inplace=True)
df_muns_final.drop(columns=['nome_x', 'nome_x', 'UF_x', 'nome_y', 'UF_y'], inplace=True)

No final você deve ter um DataFrame com as colunas:

  • municipio_origem
  • UF_origem
  • MUNICIPIO_UF_DESTINO
  • distancia_melt
  • MUN_DESTINO
  • UF_DESTINO
  • latitude_destino
  • longitude_destino
  • codigo_mun_destino
  • codigo_mun_origem

Verifique se todas existem. Se não, volte e tente corrigir algo que passou despercebido.

Estrutura dos dados

Recapitulando:

A ideia é que você tenha uma tabela de distâncias (df_muns_final), separada da sua tabela com as instâncias.

Você não precisa saber a distância entre as localizações exatas das instâncias. Mas saber a distância entre os municípios em que as instâncias estão. Como muitas instâncias estão no mesmo município, isso reduz a quantidade de calculos que precisamos realizar.

A estrutura dos dados (aqui criado no Power BI) segue conforme a imagem:

As tabelas “DESTINOS…” e “ORIGENS…” são a mesma tabela duplicada. São as suas instâncias, com os dados de seja lá o que for que está localizado em um município. Ambas se conectam à nossa tabela de municípios (‘MUNICIPIOS_DISTANCIAS’) já criada anteriormente: uma pelo código do município de origem (em azul) e outra pelo código do município de destino (em vermelho)

Filtrando

Lembrando:

Selecione uma entidade em um mapa e descubra quais outras existem dentro de um raio X

Sem me estender em detalhes de implementação (para qualquer dúvida, meu Linkedin no final):

  • Crie um mapa que plote os pontos dados por ‘latitude_destino’ e ‘longitude_destino’ na tabela de municípios. Ver em um mapa ajuda a compreender se os filtros estão fazendo sentido.
  • Filtre o seu ponto (ou pontos) de partida a partir da tabela ORIGENS utilizando qualquer critério.
  • Filtre o raio que deseja pela coluna “distancia_melt” na tabela municípios.

Essa lógica permitirá que apenas as instâncias dentro de um raio X apareçam no seu mapa, já que apenas os destinos nos interessam. Porém, a sua origem também aparecerá, se você manter a distância = 0 no seu filtro de “distancia_melt”, o que provavelmente é do seu interesse.

Abaixo podemos ver dois mapas (gerados a partir de um dataset X), resultados de ter filtrado a cidade de São Paulo como ponto de partida e raios de 200 km e 500 km respectivamente :

Tendo visualizado o mapa funcionando conforme esperado:

  • Valide seus dados. Faça testes. Crie vários exemplos e veja se está fazendo sentido.
  • Visualize as entidades filtradas (DESTINO)em uma tabela também. Assim você consegue saber quem são essas entidades ou quaisquer outras informações contidas nelas.

Curtiu? Dê umas palminhas, salve e compartilhe! Esperamos vocês na próxima, abraços!

--

--