Fuente: CARTO

Georreferenciar direcciones: un problema de interpretación de lenguaje natural

Datos Argentina
Datos Argentina
Published in
7 min readJun 6, 2019

--

Federico Tedin, desarrollador principal del Servicio de Normalización de Datos Geográficos, nos cuenta cómo abordó el problema de entender direcciones escritas en lenguaje natural en Argentina.

Uno de los desafíos a resolver cuando desarrollamos la API del Servicio de Normalización de Datos Geográficos fue: ¿Cómo puede un sistema recibir una dirección y entender qué partes la componen? El problema surgió porque no existe una forma estándar de escribir una dirección, cada persona tiende a escribirla de una forma distinta.

Tomemos algunos ejemplos basados en datos reales:

  • General Savio N° 3001
  • Tucumán y Av. Mitre
  • Pasteur S/N
  • Calle 54 1300
  • Tacuarí, entre Lavalle y España
  • Entre Ríos 771, piso 4 dpto. B
  • Paraguay 504 Barrio Dolores

Con esta breve lista de direcciones ya podemos reconocer los aspectos que varían entre cada dirección:

  • El número de calle no siempre está presente (“S/N”), y a veces tiene un prefijo en forma de “N°”, “N.”, etc, o ninguno en absoluto.
  • La cantidad de calles que aparecen en la dirección varían, por lo general entre una y tres calles.
  • Las calles se separan con palabras como “esquina”, “y”, “entre”, etc.
  • Algunas calles tienen números en sus nombres — o sus nombres son enteramente números.
  • Algunas direcciones incluyen información adicional, como el número de piso/departamento y/o la localidad o barrio.
  • Algunas calles tienen palabras como “entre” e “y” en sus nombres, lo cual hace difícil detectar si se trata de una única calle o varias calles separadas.

Por todo esto no es fácil interpretar direcciones sistemáticamente. Si a eso le sumamos otros problemas comunes como datos mal cargados y errores de escritura (por ejemplo, palabras y números pegados) la tarea es aún más compleja.

¿Cómo desarrollar una herramienta que, automáticamente y sin intervención humana, interprete componentes de cualquier dirección dada?

Para facilitar su integración con la API, dicha herramienta debería funcionar de forma totalmente local, sin consultar recursos externos, y de forma eficiente.

Así, planteamos una estructura para el problema: buscamos definir su alcance y decidir qué partes nos interesaba resolver ahora.

Primero, definimos las estructuras preferidas de direcciones:

Dirección simple:

  • <Calle> [Número]

Dirección de intersección:

  • <Calle 1> [Número] y <Calle 2>
  • <Calle 1> y <Calle 2> [Número]

Dirección entre calles:

  • <Calle 1> [Número] entre <Calle 2> y <Calle 3>
  • <Calle 1> entre <Calle 2> y <Calle 3> [Número]

En todos los casos, el valor [Número] es opcional, y puede estar inmediatamente seguido por un número de piso o departamento.

Lo que queremos, entonces, es: extraer de la dirección una, dos o tres calles, una altura (opcional) y finalmente un piso (también opcional).

¿Cuál es la teoría detrás de la solución?

Una manera posible de aproximar el problema es pensar que las direcciones son un lenguaje. ¿Qué es un lenguaje? Podemos suponer que es simplemente un conjunto de listas de símbolos.

Una lista de símbolos podría ser, por ejemplo, “H”, “o”, “l”, “a”, más convenientemente escrita como “Hola”. No necesariamente cualquier secuencia de símbolos está contenida en el lenguaje.

Volviendo a nuestro caso, en el lenguaje de las direcciones tenemos nombres, letras, comas, números y más.

Es importante destacar que siempre hablamos de símbolos (tokens) y no de valores específicos: nos interesa hablar de “número” pero no de “1”, “53” o “200”, así como nos interesa hablar de “letra” pero no de “A”, “f” o “R”. El primer paso para procesar direcciones, entonces, fue escribir una función (tokenizer) que convierta una dirección en una lista de símbolos. Por ejemplo:

Santa Fe N 1301 => PALABRA, PALABRA, LETRA, NÚMERO

Una vez obtenida la lista de símbolos, tuvimos que resolver el paso más difícil: ¿Cómo sabemos que esta lista está contenida en el lenguaje de las direcciones? Hasta el momento, solo hablamos de los símbolos que están contenidos dentro de un lenguaje, pero nunca se explicó cómo saber cuáles listas de símbolos son válidas y cuáles no. Es necesario, entonces, definir el lenguaje de alguna manera más precisa (no solo describir sus símbolos).

Una de las herramientas de uso más difundido para resolver este tipo de problemas son las “expresiones regulares”. La mayoría de los lenguajes de programación tienen librerías nativas que permiten rápidamente el uso de estas expresiones.

El problema de las expresiones regulares es que, como dice su nombre, sólo permiten definir lenguajes regulares. El lenguaje de las direcciones es más complejo y requiere otras herramientas para ser modelado.

Una herramienta mejor preparada para este problema son las gramáticas libres de contexto (GLC) que nos permiten describir con exactitud cuáles listas de símbolos son parte del lenguaje y cuáles no, mediante un proceso iterativo.

Los lenguajes regulares son un subconjunto de los lenguajes libres de contexto.

Las GLC manejan dos conceptos adicionales: los símbolos terminales y los no terminales. Los símbolos terminales son los que forman parte del lenguaje en sí. Los símbolos no terminales se utilizan como estado intermedio al momento de construir listas de símbolos terminales. Veamos cómo funciona esto para las direcciones.

Las direcciones de calle pueden ser definidas utilizando una GLC. Comenzamos definiendo cuales son las tres estructuras principales de direcciones, y por cada una definimos las partes que la componen. Las partes están siempre definidas a partir de símbolos (tokens) como “PALABRA”, “NÚMERO”, “SEPARADOR”, “COMA”, “LETRA”, etc. Debajo se muestra una versión simplificada de la gramática:

dirección -> calle altura

calle -> PALABRA | calle PALABRA

altura -> LETRA NÚMERO | NÚMERO

Cada línea representa una producción; el lado izquierdo de cada producción es un no terminal mientras que el lado derecho es una combinación de terminales y no terminales.

Para derivar texto a partir de las producciones, se debe partir de la producción inicial dirección, y sustituir su valor por lo que aparece en el lado derecho de las producciones que le corresponden. El proceso se repite por cada no terminal hasta que se termina con una secuencia puramente de terminales. Por ejemplo:

  • Comenzamos con: dirección
  • Aplicamos: dirección -> calle altura
  • Tenemos: calle altura
  • Aplicamos: calle -> calle PALABRA
  • Tenemos: calle PALABRA altura
  • Aplicamos: calle -> PALABRA
  • Tenemos PALABRA PALABRA altura
  • Aplicamos: altura -> LETRA NÚMERO
  • Terminamos con: PALABRA PALABRA LETRA NÚMERO

Entonces, teniendo la lista de símbolos para una dirección específica (“Santa Fe N 1301”), podemos utilizar la librería NLTK para automatizar el proceso de calcular si la misma puede ser generada a partir de la gramática que definimos anteriormente.

El resultado es un árbol sintáctico, que describe cuáles producciones deben ser aplicadas para llegar al resultado final. El árbol sintáctico correspondiente al ejemplo anterior sería:

Árbol Sintáctico

Una vez obtenido el árbol sintáctico, es fácil identificar cada parte de la dirección: simplemente tomamos el nodo de interés (por ejemplo, “calle”) y visitamos todas las hojas de su subárbol (“Santa”, “Fe”). Luego de haber visitado todos los nodos de interés, finalmente logramos identificar todas las componentes de la dirección. Con gramáticas más complejas, es posible identificar componentes más específicas de la dirección, como el piso, la unidad de la altura, etc.

¿Cómo ir de la teoría al desarrollo?

Todos los pasos descritos anteriormente fueron integrados en una librería de código abierto escrita en Python, cuya interfaz es simple: cuenta con un objeto AddressParser que ofrece un método parse(), el cual recibe una dirección en forma de string, y devuelve un objeto AddressData con las componentes de la dirección:

>>> from georef_ar_address import AddressParser>>> parser = AddressParser()>>> parser.parse(‘Sarmiento N° 1100 2A’)AddressData({
“street_names”: [“Sarmiento”],
“door_number”: {“value”: “1100”, “unit”: “N°”},
“floor”: “2A”,
“type”: “simple”
})

La librería puede ser descargada localmente e integrada a cualquier proyecto. Para probar la herramienta mediante una consola interactiva, se pueden ejecutar los siguientes comandos:

$ pip3 install georef-ar-address$ python3 -m georef_ar_addressIngresar una dirección y presionar [ENTER] para extraer sus componentes:> …

El uso de datos fue esencial durante el desarrollo de la librería. Se creó una base de alrededor de 91000 direcciones utilizando el portal de Datos Abiertos como fuente para buscar casos donde la herramienta no podía identificar componentes correctamente, y corregirlos. Finalmente, se escribieron alrededor de 250 casos de prueba para asegurar la calidad de los resultados en cualquier desarrollo futuro.

Para demostrar en términos prácticos la precisión de la librería diseñada, se tomó el conjunto de datos de museos (espacios culturales), y se aplicó el parser a cada fila de datos, utilizando la columna direccion como entrada. De las 1040 filas procesadas, el 88.5% fueron interpretadas correctamente.

En la mayoría de los casos no interpretados correctamente, la dirección contaba con una estructura demasiado particular o específica, al estilo de “Chacra X, Circuito de Y, a 10 Km de la localidad de Z” o también “Predio del Ferrocarril X”. Estos casos poseen estructuras tan variadas que extender la librería para procesarlos implicaría un desarrollo extenso para una ganancia limitada.

Por último, la integración con el servicio

Actualmente, la API utiliza georef-ar-address para interpretar la dirección recibida y buscarla dentro de los datos indexados.

El recurso /direcciones de la API acepta un parámetro direccion, donde el usuario puede especificar una dirección de calle a normalizar.

Por ejemplo:

https://apis.datos.gob.ar/georef/api/direcciones?direccion=av. paseo colon al 850&localidad=montserrat

El valor direccion dentro de parametro en la respuesta contiene los resultados intepretados por el parser.

{
“cantidad”: 1,
“direcciones”: […],
“inicio”: 0,
“parametros”: {
“direccion”: {
“altura”: {“unidad”: null, “valor”: “850”},
“calles”: [“av. paseo colon”],
“piso”: null,
“tipo”: “simple”
},
“localidad”: “montserrat”
},
“total”: 1
}

Ampliar las capacidades de interpretación del servicio, nos permitió incrementar la cantidad de respuestas positivas con sólo entender mejor los datos ingresados. Un paso más hacia la normalización de datos geográficos.

Si te interesa saber más podés revisar el repositorio del parser en nuestro GitHub, el Python Package y la documentación de la API.

¿Usás datos geográficos que requieren normalización? ¿Sentís que esta API te sirve para tu trabajo diario? Nos encantaría que nos cuentes por Twitter o por mail para qué y de qué manera la vas a usar, y qué otros servicios de datos te gustaría encontrar dentro del Estado.

Si te sirvió este post, hacé clic en el ❤ acá abajo, así más personas se suman a #DatosArgentina.

--

--

Datos Argentina
Datos Argentina

Abrimos los datos. Abrimos el conocimiento. Y hacemos posible tus ganas de saber y crear.