Herramientas Informáticas

para la Investigación Interdisciplinaria



  • 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.
    • Research Associate, Center for Social Complexity, George Mason University.

Sesión 3: Pre Procesamiento de Datos

Vamos a realizar dos procesos en esta etapa de pre-procesamiento:

Cuando hablamos de limpieza nos referiremos a verificar que la data haya sido leída adecuadamente, y que no estén presentes caracteres extraños que "desorienten" a los cálculos posteriores. Cuando hablamos de formato, nos referimos a que los datos limpios representen adecuadamente los valores o estructuras que el tratamiento metodológico posterior requiere.

Como ves, usamos Jupyter, pues nos permite ver lo que está pasando con los datos, de mejor manera de lo que ofrece RStudio.

Parte A: Limpieza de Data

El pre procesamiento de datos es la parte más tediosa del proceso de investigación.

Esta primera parte delata diversos problemas que se tienen con los datos reales que están en la web, como la que vemos a continuación:

In [ ]:
import IPython
wikiLink="https://en.wikipedia.org/wiki/List_of_freedom_indices" 
iframe = '<iframe src=' + wikiLink + ' width=700 height=350></iframe>'
IPython.display.HTML(iframe)

Recuerda inspeccionar la tabla para encontrar algun atributo que sirva para su descarga. De ahí, continúa.

Para trabajar con tablas, necesitaremos la ayuda de Pandas. Verifica qué versión de Pandas tienes:

In [ ]:
# si obtienes error es por que no lo has instalado
import pandas as pd
pd.__version__

Si la versión es 23, continua, sino, actualizalo.

In [ ]:
# antes instala'beautifulsoup4'
# es posible que necesites salir y volver a cargar notebook

wikiTables=pd.read_html(wikiLink,header=0,
                        flavor='bs4',
                        attrs={'class': 'wikitable sortable'})

La función read_html ha traido las tablas que hay en esa página de Wikipedia. Veamos cuantas tablaes hay:

In [ ]:
# cuantas tablas tenemos?
len(wikiTables)

Es importante saber qué estructura se ha utilizado para almacenar las tablas traidas, aunque solo haya sido una:

In [ ]:
# las tenemos en:
type(wikiTables)

Entonces, nuestro tabla (o dataframe) será el primer elemento de esa lista:

In [ ]:
DF=wikiTables[0] # python cuenta desde '0'

#primera mirada
DF.head()

La limpieza requiere estrategia. Lo primero que salta a la vista, son los footnotes que están en los títulos:

In [ ]:
DF.columns

Podrias intentar poner nombres nuevos y alterar los anteriores, pero pensemos en una estrategia donde tendrías muchas columnas. En ese caso, es mejor eliminar los errores sin importar cuantas columnas hay:

In [ ]:
import re  # debe estar instalado.

# encuentra uno o más espacios: \\s+
# encuentra uno o mas numeros \\d+
# encuentra un bracket que abre \\[
# encuentra un bracket que cierra \\]

pattern='\\s+|\\d+|\\[|\\]'
nothing=''

#viendo que sucede al aplicar lo anterior:
[re.sub(pattern,nothing,element) for element in DF.columns]

Ya tengo nuevos titulos de columna (headers). Ahora creo nuevos nombres:

In [ ]:
newHeaders=[re.sub(pattern,nothing,element) for element in DF.columns]

Preparemos los cambios. Hay que preparar los matches entre lo antiguo y lo nuevo. Usemos el comando zip:

In [ ]:
list(zip(DF.columns,newHeaders))
In [ ]:
# tenemos que crear un 'diccionario' usando la anterior:

{old:new for old,new in zip(DF.columns,newHeaders)}

El dict tiene lo que necesito. Eso lo uso en la función rename:

In [ ]:
changes={old:new for old,new in zip(DF.columns,newHeaders)}
DF.rename(columns=changes,inplace=True)
In [ ]:
# ahora tenemos:
DF.head()

Los contenidos de la columnas son texto, veamos si todas se han escrito de la manera correcta:

In [ ]:
DF.FreedomintheWorld.value_counts()
In [ ]:
DF.IndexofEconomicFreedom.value_counts()
In [ ]:
DF.PressFreedomIndex.value_counts()
In [ ]:
DF.DemocracyIndex.value_counts()

No hay problema con los contenidos.

Ir a inicio


Parte B: Formateando Valores en Python

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 # guardando los nombres de todas las columnas

# cambiar desde la segunda columna en adelante '[1:]':
DF[headers[1:]]=DF[headers[1:]].astype('category')
In [ ]:
# obtenemos:
DF.dtypes

Este cambio es imperceptible a la vista:

In [ ]:
DF.head()

Mientras no sean variables categóricas no podemos utilizar las funciones que tiene Pandas para esas variables. Por ejemplo, pidamos los modalidades:

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 los pasos iniciales:

In [ ]:
#guardando en una lista las modalidades de la variable Freedom in the world:
oldFree=list(DF.FreedomintheWorld.cat.categories)
oldFree
In [ ]:
# usando palabras que representen la ordinalidad, 
# pero que puedan ser usadas en las otras variables
# DEBEN crearse en el mismo orden que 'oldFree'
newFree=['very good','very bad','middle']
In [ ]:
# cambiar match entre lo antiguo por lo nuevo:
recodeFree={old:new for old,new in zip (oldFree,newFree)}
recodeFree

Con el dict recodeFree puedo renombrar luego las categorías.

Preparemos ahora los dicts para las otras variables:

In [ ]:
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 creados 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.head()

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

In [ ]:
# creemos la secuencia:
from pandas.api.types import CategoricalDtype

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

#aquí está la secuencia pero con propiedades 
ordinal
In [ ]:
# apliquemos la secuencia con sus propiedades a la data:

DF[headers[1:]]=DF[headers[1:]].astype(ordinal)
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

Ir a inicio


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 [ ]:
# la función recibe una columna:
def changeMonotony(aColumn):

    # Invierto las categorias de la columna:
    newOrder= aColumn.cat.categories[::-1]  # [::-1]  reverses the list.
    
    # se retorna columa con modalidades reordenadas:
    return aColumn.cat.reorder_categories(newOrder,ordered=True)

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

In [ ]:
DF[headers[1:]]=DF[headers[1:]].apply(changeMonotony)

¿Funcionó?

In [ ]:
DF.PressFreedomIndex.head()
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]
recodeMatch={old:new for old,new in zip (oldlevels,newlevels)}
In [ ]:
renamer=lambda column: column.cat.rename_categories(recodeMatch)
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:]].astype(dtype='float',errors='ignore')
In [ ]:
# ahora:
DF.dtypes
In [ ]:
DF.head(10)

Veamos qué variables tienen menos valores perdidos:

In [ ]:
# sumo los perdidos en cada una:
DF.isnull().sum() 

Como la FreedomintheWorld es quien tiene menos perdidos, debo calcular la mediana de cada variable, segun el nivel de FreedomintheWorld:

In [ ]:
#mediana por grupos: 
DF.groupby(headers[1])[headers[2:]].median()

Lo que veo tiene sentido, entonces lo lógico sería que la mediana de cada uno de estos subgrupos reemplace a los perdidos de cada subgrupo. Osea:

In [ ]:
# Si

DF.IndexofEconomicFreedom.median()
In [ ]:
# no quiero esto:

DF['IndexofEconomicFreedom'].fillna(DF.IndexofEconomicFreedom.median())
In [ ]:
# esta es la mediana para 'EconomicFreedom' segun 'FreedomintheWorld':
DF.groupby(['FreedomintheWorld'])['IndexofEconomicFreedom'].median()
In [ ]:
# le pido que llene los perdidos de a esa columna segun la tabla anterior:
columna='IndexofEconomicFreedom'
agrupador='FreedomintheWorld'
DF[columna].fillna(DF.groupby([agrupador])[columna].median())
In [ ]:
# mas facil:
for col in headers[2:]:
    DF[col].fillna(DF.groupby(["FreedomintheWorld"])[col].median(), inplace=True)

Obteniendo:

In [ ]:
DF.head(10)

Un detalle pequeño es enviar esta data con nombres faciles:

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)

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.