Herramientas Computacionales

para la Investigación Interdisciplinaria Reproducible



  • Profesor: Dr. José Manuel Magallanes, PhD (jmagallanes@pucp.edu.pe)
    Profesor del Departamento de Ciencias Sociales, Pontificia Universidad Católica del Peru.
    Senior Data Scientist del eScience Institute and Visiting Professor at Evans School of Public Policy and Governance, University of Washington.
    Fellow Catalyst, Berkeley Initiative for Transparency in Social Sciences, UC Berkeley.

Sesión 3: Pre Procesamiento de Datos

Parte B: Formateando Valores en Python

Una vez que los datos han sido limpiados, es momento de ver si tienen el formato para ser tratados estadísticamente, es decir, que representan el correcto tipo de escala y que representen una adecuada monotonía.

Traigamos unos datos conocidos, pero acelerando la limpieza:

In [ ]:
wikiLink="https://en.wikipedia.org/wiki/List_of_freedom_indices" 

#traer tabla
import pandas as pd
DF=pd.read_html(wikiLink,header=0,flavor='bs4',attrs={'class': 'wikitable sortable',})[0]

#limpiando celdas
import re  
pattern='\\s+|\\d+|\\[|\\]'
nothing=''
DF.columns=[re.sub(pattern,nothing,element) for element in DF.columns]
DF.head()

Las escalas de medición

Para saber si están en la escala correcta, debemos usar dtypes:

In [ ]:
DF.dtypes

Los cuatro indices son categorías, no texto (object). Hagamos la conversión:

In [ ]:
headers=DF.columns
DF[headers[1:]]=DF[headers[1:]].astype('category')
In [ ]:
# sale:
DF.dtypes

Este cambio es imperceptible a la vista:

In [ ]:
DF.head()

Ahora si podemos saber sus valores:

In [ ]:
DF.FreedomintheWorld.cat.categories
In [ ]:
DF.IndexofEconomicFreedom.cat.categories
In [ ]:
DF.PressFreedomIndex.cat.categories
In [ ]:
DF.DemocracyIndex.cat.categories

Vemos que tenemos hasta 5 niveles en 2 variables, y 3 y 4 niveles en otras. De ahi que lo prudente es encontrar la distribución común de valores que refleja la ordinalidad, y los máximos y mínimos.

Veamos como volverla ordinal. Primero los valores:

In [ ]:
oldFree=list(DF.FreedomintheWorld.cat.categories)
newFree=['very good','very bad','middle']
recodeFree={old:new for old,new in zip (oldFree,newFree)}

oldEco=list(DF.IndexofEconomicFreedom.cat.categories)
newEco=['very good','middle','good','bad','very bad']
recodeEco={old:new for old,new in zip (oldEco,newEco)}


oldPress=list(DF.PressFreedomIndex.cat.categories)
newPress=['bad','very good','middle','good','very bad']
recodePress={old:new for old,new in zip (oldPress,newPress)}

oldDemo=list(DF.DemocracyIndex.cat.categories)
newDemo=['very bad','good','very good','bad']
recodeDemo={old:new for old,new in zip (oldDemo,newDemo)}

Ahora usamos los dicts creado para recodificar:

In [ ]:
DF.FreedomintheWorld.cat.rename_categories(recodeFree,inplace=True)

DF.IndexofEconomicFreedom.cat.rename_categories(recodeEco,inplace=True)

DF.PressFreedomIndex.cat.rename_categories(recodePress,inplace=True)

DF.DemocracyIndex.cat.rename_categories(recodeDemo,inplace=True)

# veamos:
DF

Los datos aun no son ordinales, pero aqui serán:

In [ ]:
from pandas.api.types import CategoricalDtype


ordinal = CategoricalDtype(categories=['very good','good','middle','bad','very bad'],ordered=True)

to_Order=lambda x: x.astype(ordinal)
DF[headers[1:]]=DF[headers[1:]].apply(to_Order)
In [ ]:
# asi va:
DF.head()

Notemos que las modalidades no usadas están presentes:

In [ ]:
DF.FreedomintheWorld.value_counts(sort=False,dropna=False)

Verificaciones adicionales:

In [ ]:
#las categorias:
DF.PressFreedomIndex.cat.categories
In [ ]:
#tipo de escala?
DF.PressFreedomIndex.cat.ordered

Go to page beginning


Cambio de Monotonía:

Verifiquemos si está bien la asignación que hemos hecho:

In [ ]:
DF.PressFreedomIndex.head()
In [ ]:
DF.PressFreedomIndex.max()

Este es un caso donde quiza la intensidad creciente debe ser hacia el sentido positivo del concepto. Claro que pudimos hacerlo al inicio, pero aprovechemos para saber cómo se hace.

Para ello crearé una función:

In [ ]:
def changeMonotony(aColumn):
    # Invierto las categorias:
    newOrder= aColumn.cat.categories[::-1]  # [::-1]  reverses the list.
    # aplico función
    return aColumn.cat.reorder_categories(newOrder,ordered=True)

Esta función la aplica de nuevo, columna por columna:

In [ ]:
# SOLO UNA VEZ!! (sino reintenta)
DF[headers[1:]]=DF[headers[1:]].apply(changeMonotony)

¿Funcionó?

In [ ]:
DF.PressFreedomIndex.head(20)
In [ ]:
DF.PressFreedomIndex.max()

Todo lo que hemos trabajado podríamos entregarselo a R para que haga su trabajo estadístico, pero como no tiene metadata, es mejor guardar los indices ordinales como número:

In [ ]:
oldlevels=['very bad','bad','middle','good','very good']
newlevels=[1,2,3,4,5]
recodeLevels={old:new for old,new in zip (oldlevels,newlevels)}
In [ ]:
renamer=lambda column: column.cat.rename_categories(recodeLevels)
DF[headers[1:]]=DF[headers[1:]].apply(renamer)
DF.head(10)

Un tema adicional son los valores perdidos. Hay varios NaN.

La función para reemplazarlos es sencilla, pero hay que evitar facilismos. Veamos:

In [ ]:
#recordar:
DF.dtypes
In [ ]:
#tienen que ser numericos:
DF[headers[1:]]=DF[headers[1:]].apply(pd.to_numeric)
In [ ]:
#mediana por grupos: 
DF.groupby(headers[1])[headers[2:]].median()

Hemos calculado la mediana de cada indice que no sea Freedom in the world, pues ésta sólo tiene 1 valor perdido:

In [ ]:
DF.info() #206 buenos de 207
In [ ]:
# o
DF.isnull().sum() 
In [ ]:
import numpy as np

for h in headers[2:]:
    DF[h].fillna(DF.groupby(["FreedomintheWorld"])[h].transform(np.median), inplace=True)

Obteniendo:

In [ ]:
DF.head(20)

Un detalle pequeño es enviar esta data con buenos nombres de columnas:

In [ ]:
DF.columns=["Country","WorldFreedom","EconomicFreedom","PressFreedom","Democracy"]

Guardando archivo

A esta altura es bueno guardar el archivo, pues ya está listo:

In [ ]:
DF.to_csv("indexes.csv",index=None)

Traigamos la data de los departamentos de Colombia que vimos al final de la unidad anterior:

In [ ]:
idhCol2='https://es.wikipedia.org/wiki/Anexo:Departamentos_de_Colombia_por_IDH'
idhColT2=pd.read_html(idhCol2,header=0,flavor='bs4',attrs={'class': 'sortable',},
                       thousands='\xa0', decimal=',')[0]
idhColT2.iloc[:,2]=idhColT2.iloc[:,2].str.replace("\s","")
idhColT2.columns=[element.split('[')[0].replace(" ","") for element in idhColT2.columns]
idhColT2.Entidad=[element.split('[')[0] for element in idhColT2.Entidad]
newRows=['Amazonas', 'Guainia', 'Guaviare', 'Vaupés', 'Vichada']
info=idhColT2[idhColT2.Entidad=='Región Amazónica'].values.tolist()[0][1:]
newData = pd.DataFrame([[row] + info for row in newRows], columns=idhColT2.columns)
idhColT2=idhColT2.append(newData,ignore_index=True)
idhColT2.drop([0,24,29],inplace=True)
numericos=list(idhColT2.IDH)
numericos.extend(list(idhColT2.Población))

inapropiados=[]
for n in numericos:
    try:
        float(n)
    except ValueError:
        if not n in inapropiados: # evitar duplicados
            inapropiados.append(n)
idhColT2.replace(inapropiados,value=[None]*len(inapropiados),inplace=True)
idhColT2.reset_index(drop=True,inplace=True)

Así que actualmente, tenemos:

In [ ]:
idhColT2

Aquí el problema es distinto. Los datos faltantes necesitamos reemplazarlos, no estimarlos. Es decir, hay que traer la data de otro sitio.

In [ ]:
import IPython

pobCol='https://es.wikipedia.org/wiki/Anexo:Departamentos_de_Colombia_por_poblaci%C3%B3n'
iframe = '<iframe src=' + pobCol + ' width=700 height=350></iframe>'
IPython.display.HTML(iframe)
In [ ]:
colPOB=pd.read_html(pobCol,header=0,flavor='bs4',attrs={'class': 'sortable',},
                       thousands='\xa0', decimal=',')[0]

# q ha venido?
colPOB.dtypes
In [ ]:
colPOB.info()

Recuerda que tenemos 32 regiones, es decir aquí ha venido algo extra: Bogotá y Colombia:

In [ ]:
colPOB[colPOB.Departamento.isin (['Colombia','Bogotá','Cundinamarca'])]

De aquí, vemos que a la info de Cundinamarca debe sumarsele la de Bogotá, y eliminar luego Bogotá y Colombia.

In [ ]:
colPOB.iloc[3,2:]=colPOB.iloc[3,2:]+colPOB.iloc[0,2:]
In [ ]:
colPOB.drop([0,33],inplace=True)
colPOB.reset_index(drop=True,inplace=True)
In [ ]:
# asi queda:
colPOB

Preparemonos para llevar esta info a la data anterior.

Eliminemos cosas innecesarias. La primera columna (No) no es necesaria:

In [ ]:
colPOB.drop(['N.º'], axis=1,inplace=True)

En la data con el IDH por provincia, Población y PaísComparable tampoco son necesarias:

In [ ]:
idhColT2.drop(['Población','PaísComparable'], axis=1,inplace=True)

Ambas datas tienen el mismo tamaño?

In [ ]:
len(colPOB)==len(idhColT2)

Si es así, la unión de ambas debería ser igual, considerando lo que hemos hecho, pero siempre hay detalles que faltan:

In [ ]:
test=idhColT2.merge(colPOB,left_on='Entidad',right_on='Departamento',how='outer')
test

Nuestro test nos muestra quienes no concuerdan para el merge:

In [ ]:
test[pd.isnull(test.IDH) | pd.isnull(test.Departamento)]

Arriba se nota por que no hubo una combinación perfecta. Resolvamos y re hagamos:

In [ ]:
idhColT2[idhColT2.Entidad.isin(['San Andrés', 'Guainia'])]
In [ ]:
colPOB[colPOB.Departamento.isin(['San Andrés y Providencia', 'Guainía'])]
In [ ]:
idhColT2.loc[8, 'Entidad']=colPOB.loc[28, 'Departamento']
idhColT2.loc[28, 'Entidad']=colPOB.loc[31, 'Departamento']

Ahora debe estar bien:

In [ ]:
idhColT2.merge(colPOB,left_on='Entidad',right_on='Departamento',how='outer')

Hagamos el merge y eliminemos Entidad:

In [ ]:
idhColFinal=idhColT2.merge(colPOB,left_on='Entidad',right_on='Departamento',how='outer')
idhColFinal.drop(['Entidad'],axis=1,inplace=True)
idhColFinal

Hemos visto que el lenguaje español añade pequeñas complejidades, pues usa tildes. Creemos una columna normalizada extra con el nombre del departamento:

In [ ]:
#instalar unidecode
from unidecode import unidecode as notilde
In [ ]:
byeTilde=lambda x: x if x is None else notilde(x)
idhColFinal[['DepartamentoNorm']]=idhColFinal[['Departamento']].applymap(byeTilde)
#
idhColFinal

Al igual que en el caso anterior, debemos a esta altura guardar nuestro archivo:

In [ ]:
idhColFinal.to_csv("colombia.csv",index=None)


AUSPICIO:

El desarrollo de estos contenidos ha sido posible gracias al grant del Berkeley Initiative for Transparency in the Social Sciences (BITSS) at the Center for Effective Global Action (CEGA) at the University of California, Berkeley

RECONOCIMIENTO

El autor reconoce el apoyo que el eScience Institute de la Universidad de Washington le ha brindado desde el 2015 para desarrollar su investigación en Ciencia de Datos.