11. Operaciones avanzadas con DataFrames#
Contenidos de esta lección
Creación de DataFrames con índices jerárquicos:
MultiIndex
Patrón Split-Apply-Combine con
groupBy
Consideremos la siguiente tabla de la lección anterior con los datos del Censo 2017 de Chile
import pandas as pd
import pickle
with open("censo.pkl", "rb") as f:
df = pickle.load(f)
df.head()
NOMBRE REGIÓN | Código Región | NOMBRE PROVINCIA | Código Provincia | NOMBRE COMUNA | Código Comuna | Viviendas Particulares Ocupadas con Moradores Presentes | Viviendas Particulares Ocupadas con Moradores Ausentes | Viviendas Particulares Desocupadas (en Venta, para arriendo, Abandonada u otro) | Viviendas Particulares Desocupadas\n(de Temporada) | Viviendas Colectivas | TOTAL VIVIENDAS | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
ORDEN | ||||||||||||
1 | ARICA Y PARINACOTA | 15.0 | ARICA | 151.0 | ARICA | 15101.0 | 62129.0 | 4574.0 | 4045.0 | 1666.0 | 225.0 | 72639.0 |
2 | ARICA Y PARINACOTA | 15.0 | ARICA | 151.0 | CAMARONES | 15102.0 | 431.0 | 96.0 | 158.0 | 242.0 | 21.0 | 948.0 |
3 | ARICA Y PARINACOTA | 15.0 | PARINACOTA | 152.0 | PUTRE | 15201.0 | 540.0 | 197.0 | 143.0 | 995.0 | 42.0 | 1917.0 |
4 | ARICA Y PARINACOTA | 15.0 | PARINACOTA | 152.0 | GENERAL LAGOS | 15202.0 | 218.0 | 90.0 | 162.0 | 216.0 | 11.0 | 697.0 |
5 | TARAPACÁ | 1.0 | IQUIQUE | 11.0 | IQUIQUE | 1101.0 | 56007.0 | 3673.0 | 5481.0 | 1564.0 | 261.0 | 66986.0 |
11.1. Multi-índices#
Estudiando la tabla anterior notamos que tiene una estructura jerárquica
REGIÓN, PROVINCIA, COMUNA
Podemos representar este tipo de estructuras en pandas usando un MultiIndex
Asignar y reestablecer índices en un DataFrame
Para asignar un índice a un DataFrame que ya está creado podemos usar el atributo
df.set_index(keys, # Una etiqueta o una lista de etiquetas que serán los nuevos índices
drop=True, # Eliminar las columnas que pasarán a ser índices
inplace=False, # Retornar un nuevo dataframe o modificar df
...
)
Importante
Si keys es una etiqueta crearemos un índice regular
Si keys es una lista crearemos un
MultiIndex
Si queremos que nuestro índice o multi-índice vuelva a convertirse en columna podemos usar el atributo
df.reset_index(level = None, # Permite especificar cuantos niveles de índices se removeran
drop: bool = False, # Si los índices se deben eliminar o agregar como columnas
inplace: bool = False, # Retornar un nuevo dataframe o modificar df
...
)
Ejemplo
Crearemos un MultiIndex
para la tabla anterior. En este caso usaremos tres niveles de jerarquía:
df.set_index(["NOMBRE REGIÓN", "NOMBRE PROVINCIA", "NOMBRE COMUNA"], inplace=True)
Si inspeccionamos el DataFrame veremos que:
df
Código Región | Código Provincia | Código Comuna | Viviendas Particulares Ocupadas con Moradores Presentes | Viviendas Particulares Ocupadas con Moradores Ausentes | Viviendas Particulares Desocupadas (en Venta, para arriendo, Abandonada u otro) | Viviendas Particulares Desocupadas\n(de Temporada) | Viviendas Colectivas | TOTAL VIVIENDAS | |||
---|---|---|---|---|---|---|---|---|---|---|---|
NOMBRE REGIÓN | NOMBRE PROVINCIA | NOMBRE COMUNA | |||||||||
ARICA Y PARINACOTA | ARICA | ARICA | 15.0 | 151.0 | 15101.0 | 62129.0 | 4574.0 | 4045.0 | 1666.0 | 225.0 | 72639.0 |
CAMARONES | 15.0 | 151.0 | 15102.0 | 431.0 | 96.0 | 158.0 | 242.0 | 21.0 | 948.0 | ||
PARINACOTA | PUTRE | 15.0 | 152.0 | 15201.0 | 540.0 | 197.0 | 143.0 | 995.0 | 42.0 | 1917.0 | |
GENERAL LAGOS | 15.0 | 152.0 | 15202.0 | 218.0 | 90.0 | 162.0 | 216.0 | 11.0 | 697.0 | ||
TARAPACÁ | IQUIQUE | IQUIQUE | 1.0 | 11.0 | 1101.0 | 56007.0 | 3673.0 | 5481.0 | 1564.0 | 261.0 | 66986.0 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
MAGALLANES Y DE LA ANTÁRTICA CHILENA | TIERRA DEL FUEGO | PORVENIR | 12.0 | 123.0 | 12301.0 | 2097.0 | 57.0 | 462.0 | 511.0 | 60.0 | 3187.0 |
PRIMAVERA | 12.0 | 123.0 | 12302.0 | 190.0 | 47.0 | 62.0 | 100.0 | 73.0 | 472.0 | ||
TIMAUKEL | 12.0 | 123.0 | 12303.0 | 87.0 | 9.0 | 34.0 | 76.0 | 8.0 | 214.0 | ||
ÚLTIMA ESPERANZA | NATALES | 12.0 | 124.0 | 12401.0 | 7274.0 | 754.0 | 900.0 | 331.0 | 110.0 | 9369.0 | |
TORRES DEL PAINE | 12.0 | 124.0 | 12402.0 | 127.0 | 16.0 | 7.0 | 38.0 | 38.0 | 226.0 |
346 rows × 9 columns
Slicing con MultiIndex
Para recuperar un elemento de un DataFrame con MultiIndex
podemos indexar usando una tupla especificando cada uno de los niveles de índices, por ejemplo:
df.loc[("LOS RÍOS", "VALDIVIA", "VALDIVIA")]
Código Región 14.0
Código Provincia 141.0
Código Comuna 14101.0
Viviendas Particulares Ocupadas con Moradores Presentes 53624.0
Viviendas Particulares Ocupadas con Moradores Ausentes 2939.0
Viviendas Particulares Desocupadas (en Venta, para arriendo, Abandonada u otro) 2876.0
Viviendas Particulares Desocupadas\n(de Temporada) 1516.0
Viviendas Colectivas 237.0
TOTAL VIVIENDAS 61192.0
Name: (LOS RÍOS, VALDIVIA, VALDIVIA), dtype: float64
retorna la comuna de Valdivia, mientras que:
df.loc[("LOS RÍOS", "VALDIVIA")]
/tmp/ipykernel_35404/853128477.py:1: PerformanceWarning: indexing past lexsort depth may impact performance.
df.loc[("LOS RÍOS", "VALDIVIA")]
Código Región | Código Provincia | Código Comuna | Viviendas Particulares Ocupadas con Moradores Presentes | Viviendas Particulares Ocupadas con Moradores Ausentes | Viviendas Particulares Desocupadas (en Venta, para arriendo, Abandonada u otro) | Viviendas Particulares Desocupadas\n(de Temporada) | Viviendas Colectivas | TOTAL VIVIENDAS | |
---|---|---|---|---|---|---|---|---|---|
NOMBRE COMUNA | |||||||||
VALDIVIA | 14.0 | 141.0 | 14101.0 | 53624.0 | 2939.0 | 2876.0 | 1516.0 | 237.0 | 61192.0 |
CORRAL | 14.0 | 141.0 | 14102.0 | 1842.0 | 93.0 | 158.0 | 470.0 | 11.0 | 2574.0 |
LANCO | 14.0 | 141.0 | 14103.0 | 5643.0 | 265.0 | 308.0 | 217.0 | 17.0 | 6450.0 |
LOS LAGOS | 14.0 | 141.0 | 14104.0 | 6446.0 | 304.0 | 601.0 | 539.0 | 23.0 | 7913.0 |
MÁFIL | 14.0 | 141.0 | 14105.0 | 2317.0 | 152.0 | 161.0 | 77.0 | 5.0 | 2712.0 |
MARIQUINA | 14.0 | 141.0 | 14106.0 | 6808.0 | 351.0 | 519.0 | 727.0 | 12.0 | 8417.0 |
PAILLACO | 14.0 | 141.0 | 14107.0 | 6476.0 | 363.0 | 542.0 | 222.0 | 23.0 | 7626.0 |
PANGUIPULLI | 14.0 | 141.0 | 14108.0 | 11615.0 | 840.0 | 1155.0 | 3142.0 | 58.0 | 16810.0 |
retorna todas las comunas de la provincia de Valdivia.
Para hacer slices o fancy indexing
lo más simple es usar el objeto IndexSlice
, por ejemplo:
idx = pd.IndexSlice
df.loc[idx[:, :, ["VALDIVIA", "OSORNO"]], :]
Código Región | Código Provincia | Código Comuna | Viviendas Particulares Ocupadas con Moradores Presentes | Viviendas Particulares Ocupadas con Moradores Ausentes | Viviendas Particulares Desocupadas (en Venta, para arriendo, Abandonada u otro) | Viviendas Particulares Desocupadas\n(de Temporada) | Viviendas Colectivas | TOTAL VIVIENDAS | |||
---|---|---|---|---|---|---|---|---|---|---|---|
NOMBRE REGIÓN | NOMBRE PROVINCIA | NOMBRE COMUNA | |||||||||
LOS RÍOS | VALDIVIA | VALDIVIA | 14.0 | 141.0 | 14101.0 | 53624.0 | 2939.0 | 2876.0 | 1516.0 | 237.0 | 61192.0 |
LOS LAGOS | OSORNO | OSORNO | 10.0 | 103.0 | 10301.0 | 53821.0 | 3082.0 | 2685.0 | 471.0 | 73.0 | 60132.0 |
retorna las comunas de Valdivia y Osorno, sin importar que estén en distintas provincias, mientras que:
idx = pd.IndexSlice
df.loc[idx[:, ["LLANQUIHUE", "PALENA"], : ], :]
Código Región | Código Provincia | Código Comuna | Viviendas Particulares Ocupadas con Moradores Presentes | Viviendas Particulares Ocupadas con Moradores Ausentes | Viviendas Particulares Desocupadas (en Venta, para arriendo, Abandonada u otro) | Viviendas Particulares Desocupadas\n(de Temporada) | Viviendas Colectivas | TOTAL VIVIENDAS | |||
---|---|---|---|---|---|---|---|---|---|---|---|
NOMBRE REGIÓN | NOMBRE PROVINCIA | NOMBRE COMUNA | |||||||||
LOS LAGOS | LLANQUIHUE | PUERTO MONTT | 10.0 | 101.0 | 10101.0 | 78820.0 | 5503.0 | 6655.0 | 2194.0 | 208.0 | 93380.0 |
CALBUCO | 10.0 | 101.0 | 10102.0 | 11146.0 | 745.0 | 1006.0 | 637.0 | 36.0 | 13570.0 | ||
COCHAMÓ | 10.0 | 101.0 | 10103.0 | 1600.0 | 90.0 | 172.0 | 358.0 | 20.0 | 2240.0 | ||
FRESIA | 10.0 | 101.0 | 10104.0 | 4356.0 | 236.0 | 435.0 | 227.0 | 8.0 | 5262.0 | ||
FRUTILLAR | 10.0 | 101.0 | 10105.0 | 6267.0 | 626.0 | 594.0 | 398.0 | 24.0 | 7909.0 | ||
LOS MUERMOS | 10.0 | 101.0 | 10106.0 | 5990.0 | 327.0 | 619.0 | 321.0 | 7.0 | 7264.0 | ||
LLANQUIHUE | 10.0 | 101.0 | 10107.0 | 5550.0 | 200.0 | 258.0 | 115.0 | 12.0 | 6135.0 | ||
MAULLÍN | 10.0 | 101.0 | 10108.0 | 5112.0 | 259.0 | 743.0 | 502.0 | 14.0 | 6630.0 | ||
PUERTO VARAS | 10.0 | 101.0 | 10109.0 | 14231.0 | 1034.0 | 1168.0 | 1108.0 | 102.0 | 17643.0 | ||
PALENA | CHAITÉN | 10.0 | 104.0 | 10401.0 | 1625.0 | 151.0 | 195.0 | 190.0 | 60.0 | 2221.0 | |
FUTALEUFÚ | 10.0 | 104.0 | 10402.0 | 1002.0 | 130.0 | 101.0 | 176.0 | 24.0 | 1433.0 | ||
HUALAIHUÉ | 10.0 | 104.0 | 10403.0 | 3093.0 | 270.0 | 616.0 | 439.0 | 21.0 | 4439.0 | ||
PALENA | 10.0 | 104.0 | 10404.0 | 715.0 | 61.0 | 58.0 | 165.0 | 3.0 | 1002.0 |
retorna las comunas pertenecientes a las provincias de Llanquihue y Palena
Importante
Con IndexSlice podemos recuperar subconjuntos arbitrarios de filas en base al índice jerárquico
11.2. Patrón Split-Apply-Combine en DataFrames#
Digamos que queremos obtener los totales de todos los tipos de vivienda a nivel de provincia
Si asignamos “NOMBRE PROVINCIA” como índice podríamos usar
result = []
for provincia in df.index.unique():
sub_df = df.loc[provincia, col_mask]
if sub_df.ndim>1:
result.append(df.loc[provincia, col_mask].sum())
else: # No hacer reducción suma si la provincia tiene una sola comuna
result.append(df.loc[provincia, col_mask])
pd.DataFrame(result, columns=col_mask, index=df.index.unique())
que obtiene el resultado deseado, pero es ineficiente y bastante engorroso
El ejemplo anterior representa un patrón de “operaciones condicionadas por llave” que es muy utilizado en bases de datos y se conoce como split-apply-combine
Donde
split: Divide los datos según una llave
apply: Realiza una función sobre cada grupo
combine: Mezcla el resultado en un nuevo dataframe donde la llave se convierte en el índice
En el ejemplo anterior
split: Crea subconjuntos con las comunas de cada provincia
apply: Hace una reducción suma en las columnas de viviendas
combine: Construye un nuevo dataframe con los resultados donde la llave son las provincias
11.2.1. Atributo groupby
#
El patrón split-apply-combine está implementado de forma muy eficiente en pandas
a través del atributo groupby()
Los argumentos básicos de groupby
son
df.groupby(by=None, # Columna o lista de columnas con se hace el split
axis=0, # Dividir en filas (0) o en columnas (1)
as_index: bool = True, # Retornar las etiquetas de cada grupo como índice
sort: bool = True, # Retornar las etiquetas de grupo ordenadas alfabeticamente
...
)
Notemos que groupby
actua como un iterador
for (region, sub_df) in df.groupby('NOMBRE REGIÓN'):
display(region, # La etiqueta
sub_df # El dataframe con las filas que comparten esa etiqueta
)
La función que se ejecuta a cada grupo en el paso apply es un atributo de groupby
, existen cuatro atributos
aggregate
: Para hacer reduccionesfilter
: Para eliminar filastransform
: Para modificar filasapply
: Función flexible que puede combinar lo que haceaggregate
ytransform
A continuación revisaremos las primeras tres en detalle
11.2.2. Reducción con aggregate
#
La sintaxis básica de este atributo es
# Para aplicar la misma función a todos las columnas
df.groupby(by=llave).aggregate(funcion1)
# Para aplicar varias funciones a todos las columnas
df.groupby(by=llave).aggregate([funcion1, funcion2, ...])
# Para aplicar funciones específicas a columnas específicas
df.groupby(by=llave).aggregate({columna1: funcion1, columna2: funcion2})
Las funciones debe entregar un sólo valor por cada columna del grupo
En general las reducciones se usan para hacer resumenes, por ejemplo sumas, promedios o varianzas
Ejemplo
Podemos encontrar los totales de vivienda por provincia en una sola linea usando
df.groupby(by="NOMBRE PROVINCIA", sort=False).aggregate(np.sum)
O usando el alias
df.groupby(by="NOMBRE PROVINCIA", sort=False).sum()
11.2.3. Filtrado con filter
#
La sintaxis básica de este atributo es
df.groupby(by=llave).filter(funcion)
La función debe retornar True
o False
El resultado es un nuevo DataFrame con todos los grupos que “pasaron el filtro”
En general este atributo se usa para eliminar/descartar grupos de filas (drop)
11.2.4. Modificando el DataFrame
con transform
#
La sintaxis básica de este atributo es
df.groupby(by=llave).transform(funcion)
La función debe retornar un dataframe con la misma dimensión y tamaño que el original y se aplica columna a columna
La función puede ser explicita o anónima (lambda)
Un uso típico de este atributo es el reescalamiento/normalización a nivel de grupo