¿Te has encontrado con bases de datos que tienen pequeños errores de transcripción? ¿Espacios de más, uso desordenado de mayúsculas y minúsculas, o registros que representan al mismo dato pero que fueron escritos con pequeñas diferencias? Con la herramienta OpenRefine puedes automatizar mucho del doloroso proceso de limpiar una base de datos. En este tutorial te enseñaremos una de sus funciones más útiles: la clusterización —o generación de agrupaciones automáticas— y los diferentes algoritmos que determinan las coincidencias entre registros.
El concepto de clusters (o agrupaciones, en español) se utiliza mucho en ciencias sociales y exactas para referirse a un tipo de análisis que toma un conjunto de datos y las reorganiza en grupos con características similares.
En OpenRefine, cuando uno hace clusters significa que el programa está encontrando grupos de valores diferentes que pueden ser representaciones alternativas del mismo valor. Por ejemplo, si hablamos de ciudades, “New York”, “new york” y “Nueva York” son tres valores diferentes pero que se refieren al mismo concepto, sólo con cambios de idioma y de uso de mayúsculas y minúsculas.
Vale la pena mencionar que las agrupaciones en OpenRefine sólo se generan automáticamente en la sintaxis (o sea, el orden y la composición de caracteres que tiene como valor una celda) y aunque estos métodos son útiles para encontrar errores e inconsistencias, no son lo suficientemente avanzados para determinar agrupaciones a nivel semántico (o sea, el significado de un valor).
Estos métodos se pueden aplicar determinando cuántos grados de cercanía -en otras palabras, qué tan estrechas o flojas quieres encontrar las coincidencias-. Al graduar la cercanía encuentras coincidencias más o menos exactas. Por eso es importante que si bien, los algoritmos ayudan a automatizar la tarea de limpieza, un ojo y cerebro humano va administrando qué tan agresivas deben ser estas uniones para encontrar coincidencias, para evitar que asocie datos que no deberían ir juntos.
Existen dos grandes metodologías para hacer clusters: la colisión clave y el vecino más cercano. Open Refine utiliza diferentes variantes de estos dos métodos. Aquí te explicamos cuál es el proceso detrás de cada uno.
Estos se basan en la idea de crear una representación alternativa de un valor inicial, el cual se convierte en una clave. Una clave contiene las partes más distintivas y significativas de un valor. OpenRefine va buscando en los demás registros qué otros valores se parecen a esta clave para agruparlos. El procesamiento requerido para este método no es muy complejo, por lo que presenta resultados muy rápidos. Este método tiene varias funciones diferentes que se pueden administrar en OpenRefine.
Un método fácil y simple. Quita todos los espacios en blanco, cambia todos los caracteres a minúsculas, remueve toda la puntuación y normaliza cualquier caracter especial a una versión estándar. Luego, parte el texto y aplica espacios en blanco. Así encuentra las coincidencias.
Es similar al anterior, pero en vez de separar los caracteres por espacios en blanco, usa una cantidad a la enésima (n) potencia de espacios que el usuario puede determinar.
Este método no revisa los caracteres textuales sino su pronunciación y fonética: la manera en que esa palabra se pronunciaría, en vez de revisar similitudes en la escritura. Es muy útil para limpiar datos con nombres particulares, ya sea de lugares y personas. En ocasiones, los errores de registro se deben a que se registran a partir de la pronunciación. Sirve para encontrar similitudes entre sonidos parecidos pero que se escriben muy distinto como el sonido de “sh” y “x”, que en ocasiones son similares.
Estos métodos proveen un parámetro o radio de aproximación alrededor de un valor o palabra, y va encontrando los grados de similitud entre éste y otros registros. Debido a los cálculos necesarios, estos métodos son más tardados en procesar.
Este método se basa en el trabajo y proceso que implicaría cambiar a un registro A para que sea igual a un registro B. La distancia Levenshtein mide cuántas operaciones de edición -o cuántos pasos- le tomaría a alguien hacer que un dato se parezca al otro. Encuentra coincidencias entre los datos que están separados por la menor cantidad de pasos o cambios.
Por ejemplo, “Paris” y “paris” tienen una distancia de edición de 1, ya que solo se debe cambiar la P mayúscula a una minúscula. Sin embargo, “Nueva York” y “nuevayork” tienen una distancia de 3 pasos: dos sustituciones y un borrón.
Este método se utiliza para encontrar coincidencias en secuencias de ADN. Estima la similitud entre textos y determina su contenido idéntico. Por ejemplo, con el ADN encuentra similitud entre dos muestras para indicar un grado de familiaridad. Es común en este campo que no se busque una coincidencia exacta (que implicaría trabajar con muestras de ADN de la misma persona) sino encontrar un alto grado de coincidencia y familiaridad.
Si dos cadenas A y B son idénticas, al concatenar A+B debería de producirse muy poca diferencia. Pero si A y B son diferentes, al concatenar A+B se deberían producir diferencias muy dramáticas en la longitud de la cadena.
OpenRefine es un programa que corre a través de tu browser o navegador de internet. Para instalarlo, es necesario que lo descargues en este link y sigas las instrucciones para tu equipo. Usualmente, solo requiere que descargues la carpeta, la descomprimas y abras la aplicación.
OpenRefine debería abrir una ventana negra con algunos códigos y abrirse automáticamente en tu navegador de internet. Si no funciona, prueba ir a la dirección http://127.0.0.1:3333/
Vamos a hacer un ejemplo con un conjunto de datos sobre financistas a las elecciones del 2017-2018 en Estados Unidos que puedes descargar aquí.
Para subir el archivo, solo sigue los siguientes pasos:
Create project > Elegir archivo (selecciona el archivo ZIP que descargaste) > Next
OpenRefine te mostrará una previsualización de tu conjunto de datos. En este caso, deberás desmarcar la opción >Parse Next para indicar que tu base de datos no tiene títulos de columna en la primera fila.
En >Project Name, escribe “Financiamiento político_Estados Unidos 2017-2018” y da click a >Create project para guardar este proyecto.
En la columna 8 encontrarás el listado de financistas. Haciendo click en el triángulo a la par del título de esta columna, selecciona >Facet >Text facet para generar un filtro de texto.
A un lado, te aparecerán todos los registros de financistas en orden alfabético, con un número a la par que indica cuántas veces aparece este nombre en la base de datos. Haz click en el botón >Cluster para empezar a generar agrupaciones automáticas.
En la siguiente ventana puedes aplicar todos los métodos de clusters que te enseñamos. Puedes administrarlo cambiando las opciones >Method, >Keying Function o >Distance Function.
Con estos controles podrás ir determinando qué tan agresivos son tus clusters. Independientemente del método que eligas, el proceso es el mismo. Al seleccionar el método y sus opciones, OpenRefine comenzará a procesar los datos para encontrar coincidencias y armarlas en un cluster o agrupación.
En este ejemplo podemos ver que el programa encontró 531 valores muy similares, escritos de 8 maneras diferentes para decir lo mismo: que un financista se llama “JEFF FLAKE FOR U.S SENATE, INC”. Como puedes ver, a la par de cada manera de escribir, OpenRefine te muestra cuántas veces aparece de esta manera el valor.
En este caso te muestra dos opciones. La primera, >Merge incluye una casilla que puedes seleccionar en caso de que sí quieras que OpenRefine una estos valores. En la segunda opción >New Cell Value, el programa te da la oportunidad de que edites y decidas de qué manera quieres que se reescriba este cluster. Así, irás administrando la agrupación valor por valor, decidiendo si quieres o no agrupar los valores con >Merge y la opción de escritura bajo la cual estos valores se agruparán con >New Cell Value
Con este ejemplo, si aceptas todas las agrupaciones de cluster que te permite el método >Key Collision >Fingerprint verás como la columna de financistas pasó de tener 5,664 opciones diferentes, a tener 5,136 registros diferentes. 528 valores menos que eran repetidos pero contenían errores gramaticales o de sintaxis que hacían que la computadora no los tomara como iguales.
Así, en estos sencillos pasos, OpenRefine editó los valores de 54,807 celdas que manualmente tomarían demasiado tiempo para limpiar y estandarizar.
Para finalizar, haz click en >Export para descargar tu base de datos limpia en el formato que prefieras.Ya sea valores separados por coma, o por tabulaciones; formato para Excel o HTML, OpenRefine te permite escoger entre diversos formatos para descargar la versión limpia de tu base de datos.
Cuéntanos en qué casos puedes utilizar los clusters y OpenRefine para limpiar tus datos. Escríbenos a [email protected] o por twitter @escueladedatos y estaremos compartiendo algunos ejemplos de usos de esta herramienta.
]]>
Ese tutorial y documentación detallados en Jupyter Notebook fueron escritos por Sebastián Oliva, fellow 2017 de Escuela de Datos por Guatemala. En el webinar lanzado el 28 de junio puedes seguir paso a paso este ejercicio de limpieza y análisis de datos.
Este es el primero de varios tutoriales introductorios al procesamiento y limpieza de datos. En este estaremos usando como ambiente de trabajo a Jupyter, que permite crear documentos con código y prosa, además de almacenar resultados de las operaciones ejecutadas (cálculos, graficas, etc). Jupyter permite interactuar con varios lenguajes de programación, en este, usaremos Python, un lenguaje de programación bastante simple y poderoso, con acceso a una gran variedad de librerias para procesamiento de datos. Entre estas, está Pandas, una biblioteca que nos da acceso a estructuras de datos muy poderosas para manipular datos.
¡Comenzemos entonces!
Para poder ejecutar este Notebook, necesitas tener instalado Python 3, el cual corre en todos los sistemas operativos actuales, sin embargo, para instalar las dependencias: Pandas y Jupyter.
Recomiendo utilizar la distribución Anaconda https://www.continuum.io/downloads en su versión para Python 3, esta incluye instalado Jupyter, Pandas, Numpy y Scipy, y mucho otro software útil. Sigue las instrucciones en la documentación de Anaconda para configurar un ambiente de desarollo con Jupyter.
https://docs.continuum.io/anaconda/navigator/getting-started.html
Una vez instalado, prueba a seguir los paso de https://www.tutorialpython.com/modulos-python/ o tu tutorial de Python Favorito.
Te recomiendo utilizar Python 3.6 o superior, instalar la version mas reciente posible de virtualenv y pip. Usa Git para obtener el codigo, crea un nuevo entorno de desarollo y ahi instala las dependencias necesarias.
~/$ cd notebooks
~/notebooks/$ git clone https://github.com/tian2992/notebooks_dateros.git
~/notebooks/$ cd notebooks_dateros/
~/notebooks/notebooks_dateros/$
~/notebooks/notebooks_dateros/$ virtualenv venv/
~/notebooks/notebooks_dateros/$ source venv/bin/activate
~/notebooks/notebooks_dateros/$ pip install -r requirements.txt
~/notebooks/notebooks_dateros/$ cd 01-Intro
~/notebooks/notebooks_dateros/$ 7z e municipal_guatemala_2008-2011.7z
~/notebooks/notebooks_dateros/$ jupyter-notebook
## En Jupyter Notebooks existen varios tipos de celdas, las celdas de código, como esta:
print(1+1)
print(5+4)
6+4
Y las celdas de texto, que se escriben en Markdown y son hechas para humanos. Pueden incluir negritas, itálicas Entre otros tipos de estilos. Tambien pueden incluirse imagenes o incluso interactivos.
%pylab inline
import seaborn as sns
import pandas as pd
pd.set_option('precision', 5)
Con estos comandos, cargamos a nuestro entorno de trabajo las librerias necesarias.
Usemos la funcion de pandas read_csv
para cargar los datos. Esto crea un DataFrame
, una unidad de datos en Pandas, que nos da mucha funcionalidad y tiene bastantes propiedades convenientes para el análisis. Probablemente esta operación tome un tiempo asi que sigamos avanzando, cuando esté lista, verás que el numero de la celda habrá sido actualizado.
muni_data = pd.read_csv("GUATEMALA MUNICIPAL 2008-2011.csv",
sep=";")
muni_data.head()
Aqui podemos ver el dataframe que creamos.
En Pandas, los DataFrames son unidades básicas, junto con las Series.
Veamos una serie muy sencilla antes de pasar a evaluar muni_data
, el DataFrame que acabamos de crear. Crearemos una serie de numeros aleatorios, y usaremos funciones estadisticas para analizarlo.
serie_prueba_s = pd.Series(np.random.randn(5), name='prueba')
print(serie_prueba_s)
print(serie_prueba_s.describe())
serie_prueba_s.plot()
Con esto podemos ver ya unas propiedades muy interesantes. Las series están basadas en el concepto estadistico, pero incluyen un título (del eje), un índice (el cual identifica a los elementos) y el dato en sí, que puede ser numerico (float), string unicode (texto) u otro tipo de dato.
Las series estan basadas tambien en conceptos de vectores, asi que se pueden realizar operaciones vectoriales en las cuales implicitamente se alinean los indíces, esto es muy util por ejemplo para restar dos columnas, sin importar el tamaño de ambas, automaticamente Pandas unirá inteligentemente ambas series. Puedes tambien obtener elementos de las series por su valor de índice, o por un rango, usando la notación usual en Python. Como nota final, las Series comparten mucho del comportamiento de los NumPy Arrays, haciendolos instantaneamente compatibles con muchas librerias y recursos. https://pandas.pydata.org/pandas-docs/stable/dsintro.html#series
serie_prueba_d = pd.Series(np.random.randn(5), name='prueba 2')
print(serie_prueba_d)
print(serie_prueba_d[0:3]) # Solo los elementos del 0 al 3
# Esto funciona porque ambas series tienen indices en común.
# Si sumamos dos con tamaños distintos, los espacios vacios son marcados como NaN
serie_prueba_y = serie_prueba_d + (serie_prueba_s * 2)
print(serie_prueba_y)
print("La suma de la serie y es: {suma}".format(suma=serie_prueba_y.sum()))
Pasemos ahora a DataFrames, como nuestro muni_data DataFrame. Los DataFrames son estructuras bi-dimensionales de datos. Son muy usadas porque proveen una abstracción similar a una hoja de calculo o a una tabla de SQL. Los DataFrame tienen índices (etiquetas de fila) y columnas, ambos ejes deben encajar, y el resto será llenado de datos no validos.
Por ejemplo podemos unir ambas series y crear un DataFrame nuevo, usando un diccionario de Python, por ejemplo. Tambien podemos graficar los resultados.
prueba_dict = {
"col1": serie_prueba_s,
"col2": serie_prueba_d,
"col3": [1, 2, 3, 4, 0]
}
prueba_data_frame = pd.DataFrame(prueba_dict)
print(prueba_data_frame)
# La operacion .sum() ahora retorna un DataFrame, pero Pandas sabe no combinar peras con manzanas.
print("La suma de cada columna es: \n{suma}".format(suma=prueba_data_frame.sum()))
prueba_data_frame.plot()
Veamos ahora ya, nuestro DataFrame creado con los datos, muni_data.
#muni_data
# Una grafica bastante inutil, ¿porque?
muni_data.plot()
# Veamos los datos, limitamos a solo los primeros 5 filas.
muni_data.head(5)
## La columna 'APROBADO' se ve un poco sospechosa.
## Python toma a los numeros como números, no con una Q ni un punto (si no lo tiene) ni comas innecesarias.
## Veamos mas a detalle.
muni_data['APROBADO'].head()
# Vamos a ignorar esto por un momento, pero los números de verdad son de tipo float
Veamos cuantas columnas son, podemos explorar un poco mas asi.
muni_data.columns
muni_data['MUNICIPIO'].unique()[:5] # Listame 5 municipios
print("Funcion 1: \n {func1} \n Funcion 2: \n {func2} \n Funcion 3: \n{func3}".format(
func1=muni_data["FUNC1"].unique(),
func2=muni_data["FUNC2"].unique(),
func3=muni_data["FUNC3"].unique()
)
)
Vamos a explorar un poco con indices y etiquetas:
index_geo_data = muni_data.set_index("DEPTO","MUNICIPIO").sort_index()
index_geo_data.loc[
["GUATEMALA","ESCUINTLA","SACATEPEQUEZ"],
['FUNC1','FUNC2','FUNC3','APROBADO','EJECUTADO']
].head()
Ahora que podemos realizar selección basica, pensamos, que podemos hacer con estos datos, y nos enfrentamos a un problema…
muni_data['APROBADO'][3] * 2
¡Rayos! porque no puedo manipular estos datos así como los otros, y es porque son de tipo texto y no números.
# muni_data['APROBADO'].sum() ## No correr, falla...
Necesitamos crear una funcion para limpiar estos tipos de dato que son texto, para poderlos convertir a numeros de tipo punto flotante (decimales).
## Esto es una funcion en Python, con def definimos el nombre de esta funcion, 'clean_q'
## esta recibe un objeto de entrada.
def clean_q(input_object):
from re import sub ## importamos la función sub, que substituye utilizando patrones
## https://es.wikipedia.org/wiki/Expresión_regular
## NaN es un objeto especial que representa un valor numérico invalido, Not A Number.
if input_object == NaN:
return 0
inp = unicode(input_object) # De objeto a un texto
cleansed_q = sub(r'Q\.','', inp) # Remueve Q., el slash evita que . sea interpretado como un caracter especial
cleansed_00 = sub(r'\.00', '', cleansed_q) # Igual aqui
cleansed_comma = sub(',', '', cleansed_00)
cleansed_dash = sub('-', '', cleansed_comma)
cleansed_nonchar = sub(r'[^0-9]+', '', cleansed_dash)
if cleansed_nonchar == '':
return 0
return cleansed_nonchar
presupuesto_aprobado = muni_data['APROBADO'].map(clean_q).astype(float)
presupuesto_aprobado.describe()
muni_data['EJECUTADO'].head()
muni_data['FUNC1'].str.upper().value_counts()
presupuesto_aprobado.plot()
Bueno, ahora ya tenemos estas series de datos convertidas. ¿como las volvemos a agregar al dataset? ¡Facil! lo volvemos a insertar al DataFrame original, sobreescribiendo esa columna.
for col in ('APROBADO', 'RETRASADO', 'EJECUTADO', 'PAGADO'):
muni_data[col] = muni_data[col].map(clean_q).astype(float)
muni_data['APROBADO'].sum()
muni_data.head()
muni_data['ECON1'].unique()
Ahora si, ¡ya podemos agrupar y hacer indices bien!
index_geo_data = muni_data.set_index("DEPTO","MUNICIPIO").sort_index()
index_geo_data.head(40)
mi_muni_d = muni_data.set_index(["ANNO"],["DEPTO","MUNICIPIO"],["FUNC1","ECON1","ORIGEN1"]).sort_index()
mi_muni_d.head()
## Para obtener mas ayuda, ejecuta:
# help(mi_muni_d)
mi_muni_d.columns
mi_muni_d["DEPTO"].describe()
year_grouped = mi_muni_d.groupby("ANNO").sum()
year_grouped
year_dep_grouped = mi_muni_d.groupby(["ANNO","DEPTO"]).sum()
year_dep_grouped.head()
sns.set(style="whitegrid")
# Draw a nested barplot to show survival for class and sex
g = sns.factorplot( data=year_dep_grouped,
size=6, kind="bar", palette="muted")
# g.despine(left=True)
g.set_ylabels("cantidad")
year_dep_grouped.head()
Ahora ya podemos contestar algunas clases de preguntas agrupando estas entradas individuales de de datos.
¿Que tal el departamento que tiene mas gasto en Seguridad? ¿Los tipos de gasto mas elevados como suelen ser pagados?
year_grouped.plot()
year_dep_group = mi_muni_d.groupby(["DEPTO","ANNO"]).sum()
year_dep_group.unstack().head()
func_p = mi_muni_d.groupby(["FUNC1"]).sum()
func_dep = mi_muni_d.groupby(["FUNC1","DEPTO"]).sum()
func_p
func_dep_flat = func_dep.unstack()
func_dep_flat.head()
mi_muni_d.groupby(["DEPTO"]).sum()
func_p.plot(kind="barh", figsize=(8,6), linewidth=2.5)