Tutorial: Limpieza y visualizaciones para encontrar patrones con Python
sebastianoliva - el agosto 31, 2017 en Tutoriales
sebastianoliva - el agosto 31, 2017 en Tutoriales
invitado - el julio 26, 2017 en Fuentes de datos, Guest posts, Tutoriales, Uncategorized
import pandas as pd
import numpy as np
import seaborn
%pylab notebook
import hashlib
import humanhash
Es muy común la agrupación de información en formato ‘stack’ donde tenemos filas de datos que demuestran una correlación entre dos sets de valores.
Las tablas pivote son una forma de re-ordenar los datos en una estructura tabular donde podemos agrupar los valores convirtiendo las tuplas entre valores numéricos.
En este caso de ejemplo, crearemos un dataset de prueba con la diversidad de restaurantes en varias ciudades centroamericanas. En esta vamos a comenzar a trabajar con dos columnas, una donde describe cada ciudad y la otra con la variedad de cocina disponible en cada una.
data_restaurantes = {
'ciudades': ['Guatemala','Guatemala','Guatemala','Guatemala','Guatemala','Guatemala','San José','San José','San José','San José','San José','San Salvador','San Salvador','San Salvador'],
'culinaria': ['Chapina','Chapina','China','Thai','Italiana','Chapina','Italiana','China','Tica','Chapina','Tica','Tica','Italiana','China']
}
restaurantes_dataframe_pares = pd.DataFrame(data_restaurantes)
restaurantes_dataframe_pares
Podemos ver entonces este listado de valores, tupla por tupla. Que tal si queremos contar la presencia de cada tipo de cocina en cada región. Probemos utilizando entonces el comando DataFrame.pivot_table de Pandas.
Este pide unos cuantos argumentos los que podemos ver en la documentación. Unos cuantos son obvios, como el definir las filas y columnas que esperamos de la tabla objetivo. Sin embargo, lo más notable es que ya que los tipos de datos que estamos utilizando no son numéricos, es necesario que definamos una funcion de agrupación que nos permita contar la cantidad de instancias de cada combinación.
def funcion_agrupacion(elemento):
return True
agrupacion_culinaria = restaurantes_dataframe_pares.pivot_table(
index=["ciudades"],
columns="culinaria",
aggfunc=funcion_agrupacion,
fill_value=False)
agrupacion_culinaria
Hmm, esto ya se ve con la forma que queremos, sin embargo solo nos muestra la presencia o ausencia de algún tipo de cocina. Esto es fácil de explicar ya que definimos nuestra función de agrupación como retornar True si existe presencia a retornar False si No. ¿Qué tal si hacemos una mejor función de agrupación?
def funcion_agrupacion(elemento):
'''Contemos cuantas instancias de cada tupla existen.'''
## El comando len(iterable) cuenta la cantidad de elementos que tiene el objeto iterable que le pasemos
## los elementos iterables pueden ser listas normales, Series de NumPy o Pandas, o diccionarios y otros tipos de datos.
return len(elemento)
agrupacion_culinaria = restaurantes_dataframe_pares.pivot_table(index=["ciudades"], columns="culinaria", aggfunc=lambda x: funcion_agrupacion(x), fill_value=0)
agrupacion_culinaria
¡Genial! Ahora ya tenemos agrupadas estas de una forma coherente. Ahora ya podemos seguir manipulando y editando estos datos. Pero, ¿qué tal si hacemos un poco más simple esta llamada? Al final nuestra función de agrupación lo unico que hace es contar la cantidad de tuplas sobre las cuales aplica el pivote. ¿Qué tal si lo hacemos un poco más simple?
agrupacion_culinaria = restaurantes_dataframe_pares.pivot_table(
index=["ciudades"],
columns="culinaria",
aggfunc=len, ## Enviamos directamente la función de agrupación. Entre otras funcionas de agrupacíon útiles está np.sum (la función de suma de NumPy) y np.mean (media)
fill_value=0)
agrupacion_culinaria
Claro, la tabla resultante se comporta exactamente igual y tiene todas las propiedades nativas de los DataFrames. ¿Qué tal si limitamos la query a solo los lugares en ciudad de Guatemala?
agrupacion_culinaria.query('ciudades == ["Guatemala"]')
help(restaurantes_dataframe_pares.pivot_table)
## Con el argumento Margins, Panda calcula los valores sumados de los totales por agrupación.
agrupacion_culinaria_m = restaurantes_dataframe_pares.pivot_table(
index=["ciudades"],
columns="culinaria",
aggfunc=len,
fill_value=0,
margins=True,
margins_name="Total")
agrupacion_culinaria_m
¿Qué tal si tenemos datos que tienen una estructura Jerárquica inherente? Podemos utilizar la misma forma de multi indexación que vimos en el seminario pasado, lo importante es que a la hora de la definición del índice, Pandas es capaz de manipularlos e inteligentemente ordenar los niveles acorde.
restaurantes_dataframe_pares['estrellas'] = [5,3,3,5,3,1,2,2,4,3,4,3,2,3]
restaurantes_dataframe_estrellas = restaurantes_dataframe_pares
restaurantes_dataframe_estrellas
Pivotando sobre la especialidad culinaria y estrellas, podemos las ciudades con la mayor oferta culinaria, o cuales tienen el mejor promedio de estrellas.
agrupacion_culinaria_promedio_estrellas = restaurantes_dataframe_estrellas.pivot_table(
index=["ciudades"],
values=["culinaria", "estrellas"],
aggfunc={"culinaria":len,"estrellas":np.mean},
fill_value=0)
agrupacion_culinaria_promedio_estrellas
¿Qué tal si queremos ver cuantas estrellas en promedio tienen los restaurantes, por clase de comida, por ciudad?
agrupacion_culinaria_por_estrellas = restaurantes_dataframe_estrellas.pivot_table(
index=["culinaria"],
values=["estrellas"],
columns=["estrellas"],
aggfunc={"estrellas":np.mean},
fill_value=0)
agrupacion_culinaria_por_estrellas
agrupacion_culinaria_por_estrellas = restaurantes_dataframe_estrellas.pivot_table(
index=["culinaria"],
values=["estrellas"],
columns=["estrellas"],
aggfunc={"estrellas":len},
fill_value=0)
agrupacion_culinaria_por_estrellas
Hmm… esto no es muy útil, solo nos dice tautológicamente, que los restaurantes de ‘n’ estrellas tienen ‘n’ estrellas. ¿Pueden ver porque el error?
agrupacion_culinaria_por_estrellas = restaurantes_dataframe_estrellas.pivot_table(
index=["ciudades"],
values=["estrellas"],
columns=["culinaria"], ## Aqui es obvio ver que lo que queremos es diferenciar por variedad culinaria.
aggfunc={"estrellas":np.mean},
fill_value=0)
agrupacion_culinaria_por_estrellas
agrupacion_culinaria_por_estrellas.plot(kind="bar")
Entonces, recapitulando:
¿De qué nos sirven las tablas pivote?
¿Que clase de operación representan?
¿En que casos podemos usarlas?
Ya vimos algunos de los criterios básicos de agrupación en el primer webinar, ahora podemos avanzar un poco, combinando agrupación con pivote.
Tambien podemos usar stack, que es otra forma de agrupación basada en índices.
maga_fitosanitario = pd.read_csv("MAGA - CERTIFICADOS FITOSANITARIOS.csv")
hashlib.md5("Hola".encode("UTF-8")).hexdigest()
pd.set_option('display.float_format', lambda x: '%.1f' % x)
## Vamos a limpiar un poco de información
def ofusca_nombre(nombre):
return humanhash.humanize(hashlib.md5(nombre.encode("UTF-8")).hexdigest())
maga_fitosanitario["Solicitante"] = maga_fitosanitario["Solicitante"].map(ofusca_nombre)
maga_fitosanitario["Fecha Autorización"] = maga_fitosanitario["Fecha Autorización"].map(pd.Timestamp)
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_00 = sub(r'\.000', '000', inp)
cleansed_nonchar = sub(r'[^0-9]+', '', cleansed_00)
if cleansed_nonchar == '':
return 0
return cleansed_nonchar
maga_fitosanitario["Kg. Netos"] = maga_fitosanitario["Kg. Netos"].map(clean_q).astype(float)
maga_fitosanitario["Kg. Netos"].head()
maga_fitosanitario = pd.read_csv("MAGA - CERTIFICADOS FITOSANITARIOS - LIMPIO.csv")
maga_fitosanitario.head()
maga_fitosanitario.groupby("Producto").sum().sort_values("CIF $", ascending=False).head(20)
maga_productos_pivot = maga_fitosanitario.pivot_table(
index=["Categoría", "Producto"],
values=["CIF $", "Permiso","Kg. Netos"],
aggfunc={"CIF $":np.sum,"Permiso":len, "Kg. Netos": np.sum},
fill_value=0)
maga_productos_pivot
maga_aduanas_pivot = maga_fitosanitario.pivot_table(
index=["País origen", "Aduana"],
values=["CIF $", "Kg. Netos"],
aggfunc={"CIF $":np.sum,"Kg. Netos":np.mean},
fill_value=0)
maga_aduanas_pivot
Que tal si indagamos mas en las categorias que se importan de cada país.
maga_aduanas_pivot = maga_fitosanitario.pivot_table(
index=["País origen", "Aduana", "Categoría"],
values=["CIF $"],
aggfunc={"CIF $":np.sum},
fill_value=0)
maga_aduanas_pivot
maga_aduanas_pivot_top10 = maga_aduanas_pivot.sort_values("CIF $", ascending=False).head(10)
maga_aduanas_pivot_top10.plot(kind="barh")
Tambien es util mostrar la tabla, podemos ponerle un poco de estilo con la funcionalidad de Seaborn + Pandas
cm_paleta_verde = seaborn.light_palette("green", as_cmap=True)
s = maga_aduanas_pivot_top10.style.background_gradient(cmap=cm_paleta_verde)
s
maga_aduanas_pivot = maga_fitosanitario.pivot_table(
index=["País origen", "Aduana", "Categoría"],
values=["CIF $"],
aggfunc={"CIF $":np.sum},
fill_value=0)
maga_aduanas_pivot
cm_paleta_verde = seaborn.light_palette("green", as_cmap=True)
s = maga_aduanas_pivot_top10.style.background_gradient(cmap=cm_paleta_verde)
s
maga_fitosanitario
maga_fitosanitario.groupby("Solicitante").sum().sort_values("CIF $", ascending=False).head(10)
maga_fitosanitario.pivot_table(
index=["Solicitante", "País origen", "Categoría"],
values=["CIF $"],
aggfunc={"CIF $":np.sum},
fill_value=0).sort_values("CIF $", ascending=False).head(10)
maga_fitosanitario.pivot_table(
index=["País procedencia"],
columns=["Aduana"],
values=["CIF $"],
aggfunc={"CIF $":np.sum},
fill_value=0).style.background_gradient(cmap=cm_paleta_verde)
Que tal si queremos obtener el precio por kilogramo de cada producto y en base a eso obtener los productos mas ‘preciosos’.
maga_fitosanitario.to_csv("MAGA - CERTIFICADOS FITOSANITARIOS - LIMPIO.csv")
invitado - el junio 28, 2017 en DAL, Fuentes de datos, Tutoriales, Uncategorized
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)