Geocodificação— sem Google maps API — Parte I

Pedro Paulo dos Santos
Data Hackers
Published in
11 min readFeb 14, 2020

Parte 2 disponível no link: https://medium.com/data-hackers/geocodifica%C3%A7%C3%A3o-sem-google-maps-api-parte-ii-82722f62628

Photo by henry perks on Unsplash.

Olá, eu me chamo Pedro Paulo, sou cientista de dados, R-user e recentemente eu tive um problema… minha intenção hoje é compartilhar como eu resolvi!

Como vocês já devem ter notado no título no artigo, a questão que me atormentou nos últimos trabalhos em que fui requisitado envolvia encontrar: um ponto, em um mapa.

Mas até ai tudo bem. Pega o navegador, abre em uma aba o Google maps e digita uma referência geográfica, certo!?

Ok. Então vamos procurar o meu prédio pelo CEP, que é 21021350:

Esse nível de geolocalização da rua é o que teremos no final desse artigo para uma base de dados pública.
Sem dificuldades o ponto aparece no mapa, com o nome do bairro, cidade e até uma foto antiga do prédio em que moro.

Repare no link de navegação que aparece após eu digitar o CEP:

https://www.google.com/maps/place/Olaria,+Rio+de+Janeiro+-+State+of+Rio+de+Janeiro,+21021-350/@-22.8397375,-43.2686432,17z/data=!3m1!4b1!4m5!3m4!1s0x997beb858604d9:0xa4fed3a45a73a84d!8m2!3d-22.8396973!4d-43.2662032

Ali estão a latitude e a longitude necessárias para desenhar o ponto no mapa. Geocodificação! Ótimo. Agora vamos escalar o problema. E se eu quiser desenhar 1.000 pontos no mapa?

Segundo a Google, por esse link https://cloud.google.com/maps-platform/pricing/?hl=pt isso vai me custar míseros U$5! e o registro de uma chave de API (com meu cartão de crédito anexado para cobrança), fazendo as solicitações pela web mesmo (seguindo esse modelo: https://maps.googleapis.com/maps/api/geocode/json?address=numero+endereco+usando+de+separador,+cidade&key=KEY_DA_API) e recebendo em JSON (JavaScript object) a resposta. Genial!

Pense em um banco de dados com 100.000 mil pessoas. O custo aumenta para U$ 500... e um milhão de pessoas? Sabe quanto? “Entre em contato com a equipe de vendas, para saber mais sobre os descontos por volume” ¬¬.

Bem, tendo em vista esse cenário, fui atrás de alternativas. A mais óbvia que me ocorreu foi tentar o OpenStreetMap, que é a base de uma biblioteca famosa entre os desenvolvedores JavaScript e entre os R-users chamada leaflet (ou também nesse link).

Vamos repetir o teste. Vou digitar o meu CEP novamente:

Existe uma maneira de integrar os ceps no mapa do OpenStreetMap mas é muito complexo e as chances de darem certo são pequenas
Nenhum resultado encontrado!?!?!?!?! (guarde o nome Nominatim)

Eita! não deu certo. E eu explico o porquê. O OpenStreetMap não possui os códigos postais brasileiros anexados à base de mapa disponível para consulta pública via web. Mas é possível buscar por endereço seguindo essa estrutura:

https://www.openstreetmap.org/search?query=rua+professor+plinio+bastos

E logo aparece no endereço do navegador a latitude e a longitude:

https://www.openstreetmap.org/search?query=rua+professor+plinio+bastos#map=18/-22.84087/-43.26758

A única restrição que encontrei de uso para Geocodificar pelo OpenStreetMap foi que ele não é feito para “uso pesado”. Eu não saberia o que isso significaria até testar (parte II do artigo).

Especificações Técnicas

Photo by Malachi Brooks on Unsplash

Tudo o que for passado nesse artigo já foi testado em várias configurações diferentes, incluindo em ambientes de aprendizado como o Play with Docker.

Base de dados

Photo by Fredy Jacob on Unsplash

A base de dados utilizada para esse artigo é pública, de livre acesso e disponibilizada pela plataforma DATASUS, então fiquem tranquilos! Não teremos problemas em utilizá-la.

O data.frame consiste em um conjunto de informações obtidas do Sistema de Informações Hospitalares — SIH-SUS — que consiste de Autorizações de Internação Hospitalar (AIH) dos pacientes residentes no estado do Rio de Janeiro entre os anos de 2009 a 2019.

Crie uma pasta para o projeto em seu diretório padrão e divida em:

~.(nome do seu diretório padrão)
Teste/
|--Datasets/

Clique na base da dados do SIH-SUS, nas caixas de diálogo selecione as opções de: Dados, AIH Reduzida, 2009:2019, RJ e Janeiro:Dezembro. Faça o download.

O limite de compressão dos arquivos é de 1 Gb. Caso ultrapasse repita as seleções e baixe os arquivos faltantes.

Extraia os arquivos na pasta Datasets/ que foi criada.

Abra uma sessão do R no terminal e instale os pacotes necessários para o carregamento dos dados:

> install.packages(c('tidyverse','read.dbc', 'sparklyr'))

É possível usar o comando require( ) no script, mas geralmente eu opto por fazer a instalação e depois o carregamento.

A extensão .dbc é uma adaptação do Ministério da Saúde das extensões clássicas .db e por isso precisa de um pacote de leitura específico. Vamos carregar esses arquivos todos em uma única lista:

> library(read.dbc)
> library(tidyverse) #necessário apenas mais tarde, mas eu carrego sempre no início
> setwd("Datasets")
> input <- dir()
> L <- as.numeric(length(input))
> dados <- NULL
> for (i in 1:L){
dados[[i]] <- read.dbc(input[i])
cat(input[i],'\n')
}
> rm(input, i, L)

Agora que os arquivos estão dentro de uma lista, precisamos carregá-los como .csv para o que Spark possa lê-los como um data.frame único, sendo assim:

> dir.create("bancos_csv")
> setwd("bancos_csv")

> L <- as.numeric(length(dados))

> for(i in 1:L){
write.csv(dados[[i]], paste0(i, ".csv"))
}

> rm(i,L)
> setwd(’~./Teste’)

Instalando o Spark e o Hadoop através das funções do sparklyr:

> library(sparklyr)

> spark_available_versions() #consultando as versões disponíveis
> spark_install(version = 2.4)

> system("java -version") #conferindo se o Java 8 está instalado

> spark_installed_versions() #chegando a instalação

> conf <- spark_config() #objeto de config do Spark

> conf$spark.driver.cores <- 6 #altere as configurações a seguir para sua realidade
> conf$spark.driver.memory <- "16g"
> conf$spark.executor.memory <- '16g'
> conf$spark.memory.fraction <- 0.8
> conf$`sparklyr.shell.executor-memory`<- '16G'
> conf$`sparklyr.shell.driver-memory`<- '16G'

> sc <- spark_connect(master = "local",
config = conf) #Conexão

Após a conexão feita, é necessário sinalizar ao Spark aonde está a base de dados que será utilizada. No caso desse artigo, que consiste em diversos arquivos separados, é possível apontar para a pasta aonde estão contidos todos eles. Vale lembrar, que as colunas escolhidas para a análise precisam existir em todos os arquivos, por isso separaremos o objeto file_columns, caso contrário uma mensagem de erro aparecerá.

> top_rows <- read.csv("bancos_csv/1.csv", nrows = 5)
> file_columns <- top_rows%>%
map(function(x)"character") #garantindo as colunas iguais e em character
> rm(top_rows)> AIHS <- spark_read_csv(sc,
name = "AIHS",
path = "Datasets/bancos_csv",
memory = FALSE,
columns = file_columns,
infer_schema = FALSE) #spark.data.frame
> AIHS%>%
tally # sete milhões?
> CEPs <-AIHS%>%
select(CEP)%>%
collect() #coletando apenas os CEPs

Nesse momento o ideal é ter em uma pasta separada esses ceps coletados pois mais a frente será possível reestruturar o diretório de trabalho para uma programação mais funcional. Uma sugestão é:

~.
Teste/
|--Datasets/
RJ0109.dbc
RJ0209.dbc
...
|--bancos_csv/
1.csv
2.csv
...
|--BuscaCep/
cep.txt

Para isso utilize o comando de escrita:

> write.table(CEPs, '~./Teste/BuscaCep/ceps.txt', row.names = FALSE)

Depois de concluída essa etapa. Encerre a sessão do R e inicie uma nova na pasta BuscaCep, ou simplesmente crie um novo script na sessão que está aberta e desloque o diretório de trabalho para lá.

Cep API

Photo by Capturing the human heart. on Unsplash

Converter o CEP em endereço é relativamente fácil. O problema surge quando são muitas solicitações que precisam ser feitas a um determinado local que forneça esse serviço. Na próxima parte iremos ver algumas opções que eu achei, testei e a qual foi a solução mais “bruta” para resolver essa situação.

Ao procurar por conversão de CEP em endereços na internet, existem muitos serviços disponíveis, pagos ou não. Entre os gratuitos o que chamou a atenção foi o ViaCEP, que possibilita que a solicitação seja feita direto no endereço do navegador, assim:

https://viacep.com.br/ws/21021350/json/{
"cep": "21021-350",
"logradouro": "Rua Professor Plínio Bastos",
"complemento": "",
"bairro": "Olaria",
"localidade": "Rio de Janeiro",
"uf": "RJ",
"unidade": "",
"ibge": "3304557",
"gia": ""
}

Repare que ele retorna um objeto JSON possível de ser baixado através do R. Para pequenas consultas ele é útil, mas parece existir um limite, que no meu caso foram 500 solicitações, depois disso o site me bloqueava. Além disso nem todos os dias o site estava no ar, portanto, para grandes trabalhos não serviria. Adivinha qual foi a solução?

Font: © 2020 Docker Inc.

DOCKER!!!!! \o/

A maioria das aplicações podem ser bem planejadas hoje em dia usando containers e sabendo disso, fui em busca de uma Docker image que contivesse uma API para conversão de ceps. Existe uma quantidade razoável de imagens para esse fim, em meu Docker hub separei as duas opções mais recorrentes: postmon e cepnode(JS).

O primeiro é um pacote comum no phyton de mesmo nome, postmon. A única restrição que encontrei foi que as solicitações não podem atingir uma taxa de 60 por minuto, caso contrário ele “desligará” por 4 minutos.

Então vamos carregar o postmon, abra uma aba do prompt de comando e digite:

$ docker run -d -p 10:9876 dr2p/geocoding:postmon

Agora acesse:

http://localhost:10/v1/cep/21021350// 20200206215724
// http://localhost:10/v1/cep/21021350
{
"bairro": "Olaria",
"cidade": "Rio de Janeiro",
"logradouro": "Rua Professor Plínio Bastos",
"cep": "21021350",
"estado": "RJ"
}

O teste é solicitar alguns desses .json para os ceps que temos, mas antes, vamos esclarecer que faremos isso paralelizando as solicitações e registraremos o tempo gasto. Então instale:

> install.packages(c('lubridate','jsonlite', 'foreach', 'doParallel'))

Repita a estrutura a seguir para as solicitações via R (vale dar uma olhada nessa documentação para mais informações sobre as configurações dos pacotes foreach e doParallel).

> base_url <- "http://localhost:10/v1/cep/"> library(tidyverse)
> library(lubridate)
> library(jsonlite)
> library(foreach)
> library(doParallel)
> registerDoParallel(detectCores()-1)
> start <- Sys.time()
> enderecos <- NULL> data_list <-
foreach(i=1:1000, .packages = c("jsonlite", "httr"), .errorhandling = "remove")%dopar%{
enderecos[[i]] <- unlist(fromJSON(flatten=TRUE,paste0(base_url, ceps$CEP[i])))

}
> end <- Sys.time()
> time_interval <- start%--%end
> print(paste0('Tempo gasto = ' , round(as.numeric(time_interval, "minutes"),2)))

No meu demorou 5 minutos e aparentemente não atingiu o limite de solicitações. Por essa estimativa, para 1 milhão de solicitações iríamos demorar 5000 minutos (83 horas aproximadamente). Inviável.

A outra opção que temos é o cepnode. Até onde eu sei o cepnode não possui nenhuma restrição. Façamos o teste:

$ docker run -d -p 10000:10000 dr2p/geocoding:cepnode

E no R:

> base_url <- "http://localhost:10000/api/v1/cep/"> start <- Sys.time()> enderecos <- NULL> data_list <- 
foreach(i=1:1000, .packages = c("jsonlite", "httr"), .errorhandling = "remove")%dopar%{
enderecos[[i]] <- unlist(fromJSON(flatten=TRUE,paste0(base_url, ceps$CEP[i])))

}
> end <- Sys.time()
> time_interval <- start%--%end
> print(paste0('Tempo gasto = ' , round(as.numeric(time_interval, "minutes"),2)))

No meu foi registrado pouco mais de 2 minutos. Mas para muitas solicitações o tempo seria grande também.

Com as duas API é possível reduzir o tempo de resposta para mil solicitações praticamente para 1 minuto. Nesse caso o que eu fiz foi utilizar um cluster em Docker Swarm (no play with docker) e fazer as solicitações por outro container com o R e os pacotes necessários. Acredito que pra quem tem máquinas físicas em cluster (PC, raspberry e afins) o resultado seja semelhante. O tempo total também reduziria para aproximadamente 16 horas caso o banco contivesse 1 milhão de ceps. Dependendo do que se pretende, trabalhar com essa estimativa pode ser viável, mas não ideal.

Agora pare e pense:

E se eu esse banco que as APIs usam estivesse disponível, para que pudesse ser lido como um data.frame diretamente? Iria reduzir o tempo ao combiná-lo (merge) com os ceps?

A reposta é: Sim!

E essa foi a solução “bruta” que eu encontrei. Porém aqui vale lembrar que esse é um dos serviços que é vendido pelos Correios, quando o fim for comercial/empresarial, opte por usá-lo, afinal a base de dados estará atualizada.

Todavia, para fins didáticos podemos usar as que estão disponíveis em acesso público, mesmo estando desatualizada. Faça o download para sua pasta de trabalho do arquivo cep.db3 disponível nesse repositório: https://github.com/avmesquita/CEP-API-node. Instale o pacote de leitura do SQLite no R:

> install.packages("RSQLite")

Abra a conexão com o a base de dados:

> con <- dbConnect(RSQLite::SQLite(), "cep.db3")
> cep_node <- dbReadTable(con, "cep")
> colnames(cep_node)[2] <- "CEP"

E agora faça a combinação desse data.frame cep_node com o ceps.

> banco <- merge(ceps, cep_node, by="CEP", all.x = TRUE)

Pronto! Muito mais ceps em muito menos tempo! Solução ideal.

Data Cleaning

Photo by Mika Baumeister on Unsplash

Em bases de dados públicas, que não sejam derivadas de grandes inquéritos nacionais, é comum que exista viés de informação (ex.: ceps registrados errados). Nessa parte iremos filtrar esse “ruído” e trabalhar um pouco melhor na base que temos.

A primeira coisa que precisa fazer é separar uma coluna para a cidade e outra para o estado, já que as duas informações vêm unidas por uma ‘/’ em na coluna TXT_CIDADE_UF; e uma coluna para a rua e outra para o complemento, já que as duas vêm unidas por um ‘-’ na coluna TXT_LOCALIDADE.

Por fim, altere a leitura de character para factor da coluna UF, selecione apenas os do Rio de Janeiro e renomeie as colunas.

Veja abaixo:

> con <- dbConnect(RSQLite::SQLite(), "cep.db3")
> cep_node <- dbReadTable(con, "cep")
> colnames(cep_node)[2] <- "CEP"

> banco <- merge(ceps, cep_node, by="CEP", all.x = TRUE)

# Separando a cidade do Estado.
> banco <-
banco%>%
separate(TXT_CIDADE_UF, c("cidade", "UF"), sep = "/")

# a rua do complemento
> banco <-
banco%>%
separate(TXT_LOCALIDADE, c("rua", "complemento"), sep = "-")

# filtrando apenas para os dados corretamente lançados, ou seja, pessoas que são do RJ mesmo.
> banco$UF <- factor(banco$UF)

> banco <-
banco%>%
filter(UF=="RJ" | UF=="RJ - Distrito")

> colnames(banco) <- c('cep','id', 'cidade','UF', 'bairro', 'rua', 'complemento')

Muito bem! Chegamos ao fim desse artigo. Se você conseguiu ter a paciência de chegar até aqui, parabéns!

Na parte II nós iremos:

  • Configurar o container Nominatim que contém a base de mapas do OpenStreetMap para geocodificação das latitudes e longitudes;
  • Aprender a utilizar a API do IBGE para download das malhas geográficas brasileiras em .geojson;
  • Geolocalizar todas as AIHs de internação (com a malha e o OpenStreetMap) e aplicar interação com o usuário final usando as ferramentas disponíveis no leaflet (e talvez shiny e flexdashboard);

Para a próxima parte certifique-se de coletar outras informações além do cep, como CID da doença, data da internação, sexo, idade e etc… e separá-las em um banco (pode estar salvo no environment do R para facilitar).

Espero que me desculpem pelas linhas de código não serem clean. Para quem puder dar uma moral:

Github: https://github.com/BRhelp

Linkedin: https://www.linkedin.com/in/pedro-paulo-teixeira-dos-santos-75b81b199/

Lattes: http://lattes.cnpq.br/7722439455514062

Obrigado!

--

--

Pedro Paulo dos Santos
Data Hackers

M.S. Science, PgD. in Patient Safety, Family Health and BDent, working as Data Science.