<jupyter_start><jupyter_text>Importación, manejo y análisis de un dataset en pandas Este *notebook* contiene los sigueintes temas:* Importación de datos y análisis rápido* Cómo renombrar columnas* Como crear y trabajar con subconjuntos de un DataFrame (iloc, loc y filtros booleanos)* Manejo de datos faltantes * Cómo agrupar observaciones (groupby)* Cómo trabajar con multiindex (en Series y DataFrames)* Merge y join de DataFrames Algunos *shortcuts* útiles:* tab --> ofrece sugerencias sobre funciones y atributos * shift + tab --> muestra la documentación * shift + enter --> run cell Documentación de *pandas*:  http://pandas.pydata.org/pandas-docs/stable/Los indicadores con los que trabajaremos en esta sección se obtuvieron de la página del [Banco Mundial]( https://data.worldbank.org/indicator?tab=featured)<jupyter_code>import pandas as pd<jupyter_output><empty_output><jupyter_text>------  Importación de datos y análisis rápido<jupyter_code># Importamos los datos del archivo csv, estructurándolos en un DataFrame
data = pd.read_csv('data/indicadores.csv')
type(data)
# Pandas también ofrece funciones para importar otro tipo de archivos (txt, en formato JSON, XML, etc.)
# Veamos cuántos renglones y columnas tiene el df
print(data.shape)
# Veamos cuáles son las columnas que tiene (útil con datasets con muchas columnas):
for col in data.columns: print(col)
# Para saber a qué se refiere cada columna, importamos los metadatos para nuestro dataframe:
metadata = pd.read_csv('data/indicadores_metadata.csv')
metadata
# Para echar una vista rápita al dataset:
data.head() # muestra los primeros renglones (default=5)
# Ver los últimos renglones (default=5):
data.tail()
# Ver infomación general de cada columna:
data.info() # Vemos que no todas las columnas tienen el mismo número de valores non-null --> este potencial problema lo trataremos más adelante
# Para obtener estadísticas descriptivas sobre cada columna
display(data.describe())

# También podemos transponer el dataframe que regresa describe():
display(round(data.describe().transpose(), ndigits=2))
# Para saber cuántas observaciones tenemos para cada categoría de una columna (especialmente útil para variables categóricas):
print('Regiones:')
print(data.Region.value_counts(dropna=False), '\n') # dropna=False es para que nos muestre cuántas observaciones son NaN

print('Grupos de ingreso:')
print(data.IncomeGroup.value_counts(dropna=False))<jupyter_output>Regiones:
Europe & Central Asia         58
Sub-Saharan Africa            48
NaN                           46
Latin America & Caribbean     42
East Asia & Pacific           37
Middle East & North Africa    21
South Asia                     8
North America                  3
Name: Region, dtype: int64 

Grupos de ingreso:
high         79
upper_mid    60
lower_mid    47
NaN          46
low          31
Name: IncomeGroup, dtype: int64<jupyter_text>----  Cómo renombrar las columnas<jupyter_code># Para ver los nombres de las columnas
for col in data.columns: print(col)<jupyter_output>Country Name
Country Code
Region
IncomeGroup
cellph_per100
co2_perc
land_area
electricity
fertility
fuel_exps_pc
start_business
foreign_inv
gdp
gdp_pc
growth_gdp_pc
hiv_prev
servs_mill
mil_exp
internet_users
population
rd_exp
taxRev_pc
val_primary
val_industry
val_manufacturing
val_services
region_code<jupyter_text>Se observa que no todos los nombres tienen el mismo formato: hay algunos que tienen mayúsculas y otros que tienen espacios. Es particularmente útil quitar los espacios de los nombres<jupyter_code># Creamos un diccionario donde las keys son los nombres de las columnas que
# nos interesa cambiar, y los values son lo nuevos nombres para éstas
dict_rename = {'Country Name': 'country', 'Country Code': 'code', 'Region': 'region', 'IncomeGroup':'inc_group'}

# Aplicamos el método rename del DataFrame para renombrar columnas
data.rename(columns = dict_rename, inplace=True) 
# inplace=True es para que la operación se haga sobre el dataframe, 
# sin necesidad de que se tenga que reasignar explícitamente
data.head(3)<jupyter_output><empty_output><jupyter_text>-----  Cómo crear y trabajar con subconjuntos de un DataFrame  *iloc* y *loc*En esta sección trabajaremos con *loc* y *iloc* de *pandas*, pero no es la única manera en que se pueden crear subconjuntos de nuestros datos.*loc* lo usamos cuando queremos trabajar con las etiquetas, mientras que *iloc* nos sirve cuando queremos trabajar con las columnas en función de su índice posicional<jupyter_code># Supongamos que nos interesa trabajar con los renglones 30 a 35 y todas las columnas
data.loc[30:35, :] # Nótese que con loc sí se toma en cuenta el valor stop del rango que indicamos (en este caso, el 35)
# Si nos interesan los renglones 30 a 35, y sólo ciertas columnas
data.loc[30:35, ['country', 'inc_group', 'co2_perc']]
# Si nos interesa trabajar con todos los valores de una columna en específico ('country')
data.loc[:, ['country']].head()
# Para trabajar con una columna específica usando iloc tenemos que especificar su posición
data.iloc[:, [0]].head() # country es el primero en la lista de columnas (índice 0)
# Para obtener las columnas 30 a 35 con iloc y todas las columnas
data.iloc[30:36, :] # Notemos que, a diferencia de loc, iloc no toma en cuenta el valor stop del rango indicado<jupyter_output><empty_output><jupyter_text>Subconjuntos a partir de filtros booleanosAl igual que en *numpy*, en *pandas* también podemos crear filtros basados en los valores (en este caso de las "celdas" de los dataframes).<jupyter_code>data.head(3)
# Supongamos que nos interesa trabajar con las tasas de fertilidad y
# el pib per cápita únicamente con países del sur de Asia (en la columna 'region')

# Conviene volver a imprimir las columnas, para ver cuáles son las que nos interesan
data.columns.values
# Usamos la siguiente función para ver exactamente cómo se llama la región del sur de Asia en nuestra base
data.region.unique() # 'Sout Asia'
data.loc[(data.region == 'South Asia'), ['country', 'fertility', 'gdp_pc']]
# También podemos usar filtros múltiples.
# Supongamos que nos interesa trabajar con todos los países de América y las mismas variables del ejemplo pasado
filter = (data.region == 'Latin America & Caribbean') | (data.region == 'North America')

# Lista con los indicadores que nos interesan
indicator_list = ['country', 'fertility', 'gdp_pc'] 
data.loc[filter, indicator_list]\
    .head(10)
# Otro ejemplo
# Supongamos que nos interesa trabajar con los gastos militares de países de altos ingresos 
# que tienen una tasa de fertilidad mayor a 1.9
print(data.inc_group.unique())
filter = (data.fertility > 1.9) & (data.inc_group == 'high')

data.loc[filter, ['country', 'fertility',  'mil_exp']]\
    .sort_values(by=['fertility'], ascending = False)\
    .head(10)
# Si nos interesa obtener todos los valores para México, Guatemala y Belice
filter = data.country.isin(['Mexico', 'Guatemala', 'Belize']) # Una manera práctica de crear el filtro para este caso
data.loc[filter, :]
# Nota: cuando aplicamos un filtro a una Series de pandas, se crea una Series de igual tamaño, pero con valores booleanos

filter = data.cellph_per100 > 100
print(data.cellph_per100.shape, '\n')
print(data.cellph_per100[:5], '\n')

print(filter.shape, '\n')
print(filter[:5])<jupyter_output>(263,) 

0           NaN
1     67.350573
2     44.734977
3    123.736096
4    104.381212
Name: cellph_per100, dtype: float64 

(263,) 

0    False
1    False
2    False
3     True
4     True
Name: cellph_per100, dtype: bool<jupyter_text>----  Manejo de datos faltantes en una base<jupyter_code>data.head(3)
# Veamos cuáles on los valores que se leen como nan:
data.head().isnull() # isnull() es un método que regresa un dataframe booleano de las mismas dimensiones
# Ahora veamos cómo son los valores que se leen como no nan:
data.head().notnull() # notnull() es un método que regresa un dataframe booleano de las mismas dimensiones
# Veamos para cuáles columnas tenemos por lo menos un valor nan:
data.isnull().any()
# Veamos cuántos valores nan tenemos para cada columna:
data.isnull().sum()
# Nos interesará saber, por ejemplo, cuáles son los países que tienen valores valtantes (NaN) para la región
# Para ello, podemos usar isnull() o notnull() a manera de filtros
print(data.region.isnull()[:5]) # Regresa una Series booleana. Por lo tanto, la podemos usar para crear el filtro
display(data.loc[data.region.isnull(), :].head(10)) # vemos que se trata de categorías o grupos de paíse
# Si queremos eliminar todos los renglones que tengan al menos un valor faltante:
data.dropna(how='any')[:5] 
# Nota: si queremos que el DataFrame original se sustituya con el DF nuevo, hay que pasar el argumento inplace=True
# (en este caso, no lo hicimos)
# Si queremos elminar los renglones para los cuales todos sus valores son faltantes 
data.dropna(how='all')[:5] 
# En este caso, no habría cambio porque no hay valores faltantes para 'country' ni 'region'
# También podemos crear un subconjunto de columnas para aplicar el método dropna():
# Supongamos que queremos eliminar todas las observaciones que tengan uno o más valores faltantes en 'inc_group',
# 'cellph_per100', 'co2_perc' y 'fertility'
data.dropna(subset=['inc_group', 'cellph_per100', 'co2_perc', 'fertility'], how='any')[:5]
# si nos interesa sustituir los valores faltantes con un valor específico:
data.fillna(value={'cellph_per100':'FALTA CEL', 'fuel_exps_pc':'FALTA FUEL'})
# Recordar que si queremos sustituir el DF original con el nuevo, hay que pasar el argumento inplace=True
# Si queremos trabajar con una base que estrictamente sólo tenga países individuales,
# vamos a querer quitar estas obervaciones que se refieren a regiones 
data.dropna(subset=['region'], how='any', inplace=True) 
# Nótese que pasamos el argumento inplace=True para susituir la base original
data.isnull().sum()
# Veamos cuál es la observación que no tiene dato para la variable población
data.loc[data.population.isnull(), :]
data.dropna(subset=['population'], how='any', inplace=True)<jupyter_output><empty_output><jupyter_text>Exportar base con sólo países (para trabajar posteriormente en la sección de visualización)<jupyter_code># data.to_csv('data/indicadores_clean.csv', index=False)<jupyter_output><empty_output><jupyter_text>-----  Cómo agrupar observaciones (groupby)<jupyter_code>data.head(2)
# Supongamos que nos interesa calcular qué porcentaje del pib regional aporta cada país y 
# qué porcentaje de la población regional vive en cada país<jupyter_output><empty_output><jupyter_text>groupby() con map()<jupyter_code># Supongamos que nos interesa crear una columna donde se capture la importancia de un país (en  términos de población) para su región...

# Aplicamos groupby por región, y sumamos sobre la columna de population
p1 = data.groupby(by=['region']) \
         .population \
         .sum() # otros métodos comunes para trabajar con groupby son mean(), count(), max() y min()
        
# Pasamos como argumento la población por región al método map
p2 = data.region.map(p1) # shift + tab pra ver cómo funciona map

# Dividimos la población de cada observación entre su población regional
data['pop_pc_reg'] = 100*(data.population/p2)# Multiplicamos por 100 para que el porcentaje quede entre 0 y 1

# También se puede hacer todo en una sola línea...
data['pop_pc_reg'] = 100*(data.population/data.region.map(data.groupby(by=['region']).population.sum()))

data.head()
# Eliminamos la columna nueva que acabamos de crear (sólo era para ilustrar el caso)
data.drop(columns=['pop_pc_reg'], inplace=True, errors='ignore') # errors='ignore' hace que no mande error si la columna no existe
data.head(2)<jupyter_output><empty_output><jupyter_text>groupby() con agg()<jupyter_code># agg() nos permite observar varios métodos de agregación en un DF
data.groupby(by=['region']) \
    .fertility \
    .agg(['count', 'min', 'max', 'mean'])<jupyter_output><empty_output><jupyter_text>---  Multi Index (multi level indexing)<jupyter_code>data.head(3)<jupyter_output><empty_output><jupyter_text>Series con multi index<jupyter_code># Veamos primero cómo se crea una Series con multi index. En este caso lo hacemos con groupby y mean
ser = data.groupby(by=['inc_group', 'region_code']).cellph_per100.mean()
# Un multi index le agrega una dimensión a los datos en este caso.
ser
# Veamos cómo son los índices
print('class: ', type(ser.index))
ser.index
# Para seleccionar todos los elementos que pertenecen a inc_group=high
print('SELECCION: \n', ser.loc['high'], '\n')
print(ser.loc['high'].index, '\n')
print('-------------')

print('SELECCIÓN: \n', ser.loc['high', :], '\n')
print(ser.loc['high', :].index)
# Vemos que en un caso .index regresa un objeto de tipo Index y en otro uno de MultiIndex
# Para seleccionar todos las observacione que tienen un index 'EAs_Pa' en su segundo nivel
ser.loc[:, 'EAs_Pa']
# Una selección un poco más compleja
ser.loc[['high', 'low'],['EAs_Pa', 'LAm_Ca']]<jupyter_output><empty_output><jupyter_text>DataFrames con multi index<jupyter_code># Hay varios caminos para construir un df con multiindex. Un ejemplo:
data_multi = data.groupby(by=['inc_group', 'region_code'])\
                 .mean()\
                 .loc[:, ['cellph_per100', 'co2_perc', 'fertility']]\
                 .sort_index()
data_multi[:10]
# una manera alternativa sería usando el método set_index(['level1', 'level2', ... ])
data_multi.index
# Para hacer selecciones con multi index
display(data_multi.loc[('high'), :]) # Nótese que usamos paréntesis (tuple)
print('type:', type(data_multi.loc[('high'), :])); print('---------------------------------------')

# Especificar un index de cada nivel (tenemos dos niveles aquí)
display(data_multi.loc[('high','EAs_Pa'),:])
print('type:', type(data_multi.loc[('high','EAs_Pa'),:])); print('---------------------------------------')

# Para elegir varios elementos de un nivel (queremos high y low)
display(data_multi.loc[(['high', 'low']), ['co2_perc', 'fertility']])
print('type:', type(data_multi.loc[(['high', 'low']), ['co2_perc', 'fertility']])); print('---------------------------------------')

# Para elegir todos los elementos de un nivel (es un poco más complicado):
display(data_multi.loc[(slice(None), ['EAs_Pa']), :]) # Aquí elegimos todas las columnas para todos las observaciones que en su segundo nivel tienen un índice de 'EAs_Pa'
print('type:', type(data_multi.loc[(slice(None), ['EAs_Pa']), :])); print('---------------------------------------')<jupyter_output><empty_output><jupyter_text>Merge y join de DataFrames<jupyter_code># Importamos dos DataFrames
df1 = pd.read_csv('data/gdp_merge.csv')
df2 = pd.read_csv('data/gdp_perc_merge.csv')
# Creamos un tercer DaraFrame a partir de los dos que importamos
df3 = pd.merge(df1, df2)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.1119377747717281, 'fraction_numerical': 0.013527223537368955, 'mean_word_length': 4.757788161993769, 'pattern_counts': {'":': 0, '<': 58, '<?xml version=': 0, '>': 64, 'https://': 1, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '15', 'dir': 'jupyter-structured-clean-dedup', 'id': '484774', 'license': 'MIT', 'n_tokens_mistral': 5068, 'n_tokens_neox': 4935, 'n_words': 1709, 'path': 'NotasCienciaDeDatos/pandas/pandas_02_analisis.ipynb', 'repo_name': 'fwchj/cursoABMPythonPublic', 'size': '14785'}