Traigamos unos datos conocidos, pero acelerando la limpieza:
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()
DF.dtypes
Los cuatro indices son categorías, no texto (object). Hagamos la conversión:
headers=DF.columns
DF[headers[1:]]=DF[headers[1:]].astype('category')
# sale:
DF.dtypes
Este cambio es imperceptible a la vista:
DF.head()
Ahora si podemos saber sus valores:
DF.FreedomintheWorld.cat.categories
DF.IndexofEconomicFreedom.cat.categories
DF.PressFreedomIndex.cat.categories
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:
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:
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:
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)
# asi va:
DF.head()
Notemos que las modalidades no usadas están presentes:
DF.FreedomintheWorld.value_counts(sort=False,dropna=False)
Verificaciones adicionales:
#las categorias:
DF.PressFreedomIndex.cat.categories
#tipo de escala?
DF.PressFreedomIndex.cat.ordered
DF.PressFreedomIndex.head()
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:
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:
# SOLO UNA VEZ!! (sino reintenta)
DF[headers[1:]]=DF[headers[1:]].apply(changeMonotony)
¿Funcionó?
DF.PressFreedomIndex.head(20)
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:
oldlevels=['very bad','bad','middle','good','very good']
newlevels=[1,2,3,4,5]
recodeLevels={old:new for old,new in zip (oldlevels,newlevels)}
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:
#recordar:
DF.dtypes
#tienen que ser numericos:
DF[headers[1:]]=DF[headers[1:]].apply(pd.to_numeric)
#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:
DF.info() #206 buenos de 207
# o
DF.isnull().sum()
import numpy as np
for h in headers[2:]:
DF[h].fillna(DF.groupby(["FreedomintheWorld"])[h].transform(np.median), inplace=True)
Obteniendo:
DF.head(20)
Un detalle pequeño es enviar esta data con buenos nombres de columnas:
DF.columns=["Country","WorldFreedom","EconomicFreedom","PressFreedom","Democracy"]
Guardando archivo
A esta altura es bueno guardar el archivo, pues ya está listo:
DF.to_csv("indexes.csv",index=None)
Traigamos la data de los departamentos de Colombia que vimos al final de la unidad anterior:
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:
idhColT2
Aquí el problema es distinto. Los datos faltantes necesitamos reemplazarlos, no estimarlos. Es decir, hay que traer la data de otro sitio.
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)
colPOB=pd.read_html(pobCol,header=0,flavor='bs4',attrs={'class': 'sortable',},
thousands='\xa0', decimal=',')[0]
# q ha venido?
colPOB.dtypes
colPOB.info()
Recuerda que tenemos 32 regiones, es decir aquí ha venido algo extra: Bogotá y Colombia:
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.
colPOB.iloc[3,2:]=colPOB.iloc[3,2:]+colPOB.iloc[0,2:]
colPOB.drop([0,33],inplace=True)
colPOB.reset_index(drop=True,inplace=True)
# asi queda:
colPOB
Preparemonos para llevar esta info a la data anterior.
Eliminemos cosas innecesarias. La primera columna (No) no es necesaria:
colPOB.drop(['N.º'], axis=1,inplace=True)
En la data con el IDH por provincia, Población y PaísComparable tampoco son necesarias:
idhColT2.drop(['Población','PaísComparable'], axis=1,inplace=True)
Ambas datas tienen el mismo tamaño?
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:
test=idhColT2.merge(colPOB,left_on='Entidad',right_on='Departamento',how='outer')
test
Nuestro test nos muestra quienes no concuerdan para el merge:
test[pd.isnull(test.IDH) | pd.isnull(test.Departamento)]
Arriba se nota por que no hubo una combinación perfecta. Resolvamos y re hagamos:
idhColT2[idhColT2.Entidad.isin(['San Andrés', 'Guainia'])]
colPOB[colPOB.Departamento.isin(['San Andrés y Providencia', 'Guainía'])]
idhColT2.loc[8, 'Entidad']=colPOB.loc[28, 'Departamento']
idhColT2.loc[28, 'Entidad']=colPOB.loc[31, 'Departamento']
Ahora debe estar bien:
idhColT2.merge(colPOB,left_on='Entidad',right_on='Departamento',how='outer')
Hagamos el merge y eliminemos Entidad:
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:
#instalar unidecode
from unidecode import unidecode as notilde
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:
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.