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

../../_images/groupby.svg

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 reducciones

  • filter: Para eliminar filas

  • transform: Para modificar filas

  • apply: Función flexible que puede combinar lo que hace aggregate y transform

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