Clase 5 — Pandas II: operaciones sobre DataFrames#

Python y Políticas Públicas


Contenidos#

  1. GroupBy y agregaciones

  2. Merge y join

  3. Concatenar DataFrames

  4. Pivot tables y reshaping

  5. Apply y funciones sobre columnas

  6. Ejercicios

import pandas as pd
import numpy as np

pd.set_option('display.max_columns', None)
pd.set_option('display.float_format', '{:.2f}'.format)

# Recreamos el dataset de la clase anterior
np.random.seed(42)
n = 200
provincias_lista = [
    "Buenos Aires", "Córdoba", "Santa Fe", "Mendoza", "Tucumán",
    "Salta", "Entre Ríos", "Chaco", "Misiones", "Corrientes"
]
programas_lista = ["AUH", "Progresar", "Potenciar Trabajo", "Alimentar", "Crédito Argenta"]

df = pd.DataFrame({
    "id_beneficiario": range(1001, 1001 + n),
    "provincia": np.random.choice(provincias_lista, n),
    "programa": np.random.choice(programas_lista, n),
    "edad": np.random.randint(18, 65, n),
    "genero": np.random.choice(["F", "M", "X"], n, p=[0.58, 0.40, 0.02]),
    "ingreso_mensual": np.random.lognormal(10.2, 0.6, n).round(0),
    "anios_educacion": np.random.randint(6, 18, n),
    "tiene_empleo_formal": np.random.choice([True, False], n, p=[0.35, 0.65]),
    "monto_transferencia": np.random.choice([18000, 25000, 42000, 55000, 10000], n),
})

print(f"Dataset: {df.shape[0]} filas × {df.shape[1]} columnas")
df.head(3)
Dataset: 200 filas × 9 columnas
id_beneficiario provincia programa edad genero ingreso_mensual anios_educacion tiene_empleo_formal monto_transferencia
0 1001 Entre Ríos Potenciar Trabajo 54 M 43412.00 6 True 10000
1 1002 Mendoza AUH 39 M 29352.00 7 False 55000
2 1003 Chaco AUH 46 F 78992.00 8 True 25000
import matplotlib as mpl
import matplotlib.pyplot as plt

# --- Paleta de identidad del curso ---
C = ['#2A6496', '#E07B3F', '#3D9970', '#8E5EA2', '#C0A830', '#637A8A']

mpl.rcParams.update({
    'figure.figsize'       : (10, 5),
    'font.size'            : 11,
    'axes.titlesize'       : 12,
    'axes.titleweight'     : 'normal',
    'axes.spines.top'      : False,
    'axes.spines.right'    : False,
    'legend.frameon'       : False,
    'axes.prop_cycle'      : mpl.cycler(color=C),
    'figure.dpi'           : 110,
})

def tit(ax, t, **kw):
    """Título sin negrita, alineado a la izquierda."""
    ax.set_title(t, loc='left', fontweight='normal', **kw)

1. GroupBy y agregaciones#

groupby es una de las operaciones más poderosas de pandas. Permite dividir los datos en grupos, aplicar una función a cada grupo y combinar los resultados.

# Promedio de ingreso y monto por programa
df.groupby('programa')[['ingreso_mensual', 'monto_transferencia']].mean().round(0)
ingreso_mensual monto_transferencia
programa
AUH 31916.00 29044.00
Alimentar 36054.00 30614.00
Crédito Argenta 42747.00 28676.00
Potenciar Trabajo 35840.00 32925.00
Progresar 32448.00 33703.00
# Múltiples funciones de agregación
resumen = df.groupby('provincia').agg(
    cantidad=("id_beneficiario", "count"),
    ingreso_promedio=("ingreso_mensual", "mean"),
    edad_promedio=("edad", "mean"),
    pct_empleo_formal=("tiene_empleo_formal", "mean"),
).round(1)

resumen['pct_empleo_formal'] = (resumen['pct_empleo_formal'] * 100).round(1)
resumen.sort_values('cantidad', ascending=False)
cantidad ingreso_promedio edad_promedio pct_empleo_formal
provincia
Entre Ríos 26 46482.50 43.10 40.00
Buenos Aires 23 35800.70 38.70 20.00
Chaco 23 34679.80 41.30 30.00
Misiones 20 27960.40 38.80 30.00
Tucumán 20 33069.40 38.90 40.00
Corrientes 19 46704.20 41.30 30.00
Santa Fe 19 34611.70 43.50 30.00
Mendoza 18 30335.40 42.90 20.00
Córdoba 17 32492.40 46.10 50.00
Salta 15 27769.70 37.10 30.00
# Agrupar por múltiples columnas
por_programa_genero = df.groupby(['programa', 'genero'])['ingreso_mensual'].mean().round(0)
print(por_programa_genero.unstack())  # unstack convierte el último nivel en columnas
genero                   F        M        X
programa                                    
AUH               33685.00 27967.00 53926.00
Alimentar         34902.00 38070.00      NaN
Crédito Argenta   35689.00 54887.00 45284.00
Potenciar Trabajo 37300.00 32513.00 31558.00
Progresar         33551.00 30410.00      NaN
# transform: agrega el resultado al DataFrame original (sin reducir filas)
df['ingreso_promedio_provincial'] = df.groupby('provincia')['ingreso_mensual'].transform('mean')
df['ingreso_relativo'] = (df['ingreso_mensual'] / df['ingreso_promedio_provincial']).round(2)

df[['provincia', 'ingreso_mensual', 'ingreso_promedio_provincial', 'ingreso_relativo']].head(8)
provincia ingreso_mensual ingreso_promedio_provincial ingreso_relativo
0 Entre Ríos 43412.00 46482.50 0.93
1 Mendoza 29352.00 30335.44 0.97
2 Chaco 78992.00 34679.78 2.28
3 Tucumán 25727.00 33069.35 0.78
4 Entre Ríos 37790.00 46482.50 0.81
5 Corrientes 44316.00 46704.21 0.95
6 Santa Fe 9436.00 34611.68 0.27
7 Entre Ríos 129317.00 46482.50 2.78

2. Merge y join#

merge combina dos DataFrames usando una o más columnas clave, como un JOIN en SQL.

# DataFrame de indicadores provinciales (tabla de referencia)
indicadores = pd.DataFrame({
    "provincia": provincias_lista,
    "pobreza_pct": [42.3, 38.1, 39.7, 34.2, 48.6, 52.1, 37.5, 58.3, 44.1, 46.8],
    "region": ["Pampeana", "Pampeana", "Pampeana", "Cuyo", "NOA",
               "NOA", "Pampeana", "NEA", "NEA", "NEA"],
    "pbi_pc_miles": [9.2, 10.1, 9.8, 8.5, 6.8, 6.1, 7.9, 5.5, 5.8, 5.9],
})
indicadores
provincia pobreza_pct region pbi_pc_miles
0 Buenos Aires 42.30 Pampeana 9.20
1 Córdoba 38.10 Pampeana 10.10
2 Santa Fe 39.70 Pampeana 9.80
3 Mendoza 34.20 Cuyo 8.50
4 Tucumán 48.60 NOA 6.80
5 Salta 52.10 NOA 6.10
6 Entre Ríos 37.50 Pampeana 7.90
7 Chaco 58.30 NEA 5.50
8 Misiones 44.10 NEA 5.80
9 Corrientes 46.80 NEA 5.90
# Inner join: solo filas con clave en ambas tablas
df_enriquecido = df.merge(indicadores, on='provincia', how='left')

print(f"Filas antes del merge: {len(df)}")
print(f"Filas después del merge: {len(df_enriquecido)}")

df_enriquecido[['provincia', 'programa', 'ingreso_mensual', 'pobreza_pct', 'region']].head()
Filas antes del merge: 200
Filas después del merge: 200
provincia programa ingreso_mensual pobreza_pct region
0 Entre Ríos Potenciar Trabajo 43412.00 37.50 Pampeana
1 Mendoza AUH 29352.00 34.20 Cuyo
2 Chaco AUH 78992.00 58.30 NEA
3 Tucumán Alimentar 25727.00 48.60 NOA
4 Entre Ríos Potenciar Trabajo 37790.00 37.50 Pampeana
# Tipos de merge
# how='inner'  → solo filas con coincidencia en ambas tablas (por defecto)
# how='left'   → todas las filas de la izquierda, NaN donde no hay match a la derecha
# how='right'  → todas las filas de la derecha
# how='outer'  → todas las filas de ambas tablas

# Merge con nombres de columna distintos
df_alt = pd.DataFrame({'prov': ['Buenos Aires', 'Córdoba'], 'codigo': [1, 2]})
# df.merge(df_alt, left_on='provincia', right_on='prov')

print("Ver comentarios en el código para los tipos de merge")
Ver comentarios en el código para los tipos de merge

3. Concatenar DataFrames#

# Datos de tres trimestres (mismo formato, distintos períodos)
q1 = pd.DataFrame({'trimestre': ['Q1'] * 3, 'provincia': ['BA', 'Cba', 'SF'], 'valor': [100, 95, 102]})
q2 = pd.DataFrame({'trimestre': ['Q2'] * 3, 'provincia': ['BA', 'Cba', 'SF'], 'valor': [105, 98, 108]})
q3 = pd.DataFrame({'trimestre': ['Q3'] * 3, 'provincia': ['BA', 'Cba', 'SF'], 'valor': [110, 103, 112]})

# Concatenar verticalmente (apilar)
todos = pd.concat([q1, q2, q3], ignore_index=True)
print("Concatenación vertical:")
print(todos)

# Uso típico: cargar múltiples archivos y apilarlos
# archivos = ['datos_2021.csv', 'datos_2022.csv', 'datos_2023.csv']
# df_total = pd.concat([pd.read_csv(f) for f in archivos], ignore_index=True)
Concatenación vertical:
  trimestre provincia  valor
0        Q1        BA    100
1        Q1       Cba     95
2        Q1        SF    102
3        Q2        BA    105
4        Q2       Cba     98
5        Q2        SF    108
6        Q3        BA    110
7        Q3       Cba    103
8        Q3        SF    112

4. Pivot tables y reshaping#

# Pivot table: resumen cruzado (como tabla dinámica de Excel)
pivot = df_enriquecido.pivot_table(
    values='monto_transferencia',
    index='region',
    columns='genero',
    aggfunc='mean',
    margins=True,  # fila/columna de totales
    margins_name='Total'
).round(0)

pivot
genero F M X Total
region
Cuyo 32100.00 34750.00 NaN 33278.00
NEA 29878.00 27900.00 25000.00 29161.00
NOA 34048.00 28429.00 NaN 31800.00
Pampeana 34078.00 28161.00 20667.00 31447.00
Total 32512.00 28863.00 21750.00 30965.00
# Tabla de conteos: beneficiarios por provincia y programa
tabla_cruzada = pd.crosstab(
    df['provincia'],
    df['programa'],
    margins=True
)
tabla_cruzada
programa AUH Alimentar Crédito Argenta Potenciar Trabajo Progresar All
provincia
Buenos Aires 5 4 4 5 5 23
Chaco 5 7 5 4 2 23
Corrientes 2 5 4 3 5 19
Córdoba 5 2 4 0 6 17
Entre Ríos 4 4 7 7 4 26
Mendoza 4 5 3 4 2 18
Misiones 6 3 1 6 4 20
Salta 2 4 3 2 4 15
Santa Fe 6 4 2 4 3 19
Tucumán 6 6 1 5 2 20
All 45 44 34 40 37 200
# melt: pasar de formato wide a long (útil para graficar series temporales)
wide = pd.DataFrame({
    'provincia': ['Buenos Aires', 'Córdoba', 'Santa Fe'],
    'pobreza_2021': [40.1, 36.2, 38.0],
    'pobreza_2022': [42.3, 38.1, 39.7],
    'pobreza_2023': [40.9, 37.4, 38.5],
})

long = wide.melt(
    id_vars='provincia',
    value_vars=['pobreza_2021', 'pobreza_2022', 'pobreza_2023'],
    var_name='anio',
    value_name='pobreza_pct'
)
long['anio'] = long['anio'].str.replace('pobreza_', '').astype(int)
print("Formato long (ideal para gráficos de series temporales):")
long.sort_values(['provincia', 'anio'])
Formato long (ideal para gráficos de series temporales):
provincia anio pobreza_pct
0 Buenos Aires 2021 40.10
3 Buenos Aires 2022 42.30
6 Buenos Aires 2023 40.90
1 Córdoba 2021 36.20
4 Córdoba 2022 38.10
7 Córdoba 2023 37.40
2 Santa Fe 2021 38.00
5 Santa Fe 2022 39.70
8 Santa Fe 2023 38.50

5. Apply y funciones sobre columnas#

# apply: aplicar una función a cada fila o columna
def clasificar_ingreso(ingreso):
    if pd.isna(ingreso):
        return "Sin dato"
    elif ingreso < 50_000:
        return "Bajo"
    elif ingreso < 150_000:
        return "Medio"
    else:
        return "Alto"

df['nivel_ingreso'] = df['ingreso_mensual'].apply(clasificar_ingreso)
print(df['nivel_ingreso'].value_counts())
nivel_ingreso
Bajo     166
Medio     33
Alto       1
Name: count, dtype: int64
# map: reemplazar valores usando un diccionario
mapa_regiones = {
    "Buenos Aires": "Pampeana", "Córdoba": "Pampeana", "Santa Fe": "Pampeana", "Entre Ríos": "Pampeana",
    "Mendoza": "Cuyo",
    "Tucumán": "NOA", "Salta": "NOA",
    "Chaco": "NEA", "Misiones": "NEA", "Corrientes": "NEA"
}
df['region'] = df['provincia'].map(mapa_regiones)
print(df['region'].value_counts())
region
Pampeana    85
NEA         62
NOA         35
Cuyo        18
Name: count, dtype: int64
# apply sobre filas (axis=1): combinar múltiples columnas
def score_vulnerabilidad(row):
    score = 0
    if not row['tiene_empleo_formal']:
        score += 2
    if row['ingreso_mensual'] < 50_000:
        score += 2
    if row['anios_educacion'] < 10:
        score += 1
    return score

df['score_vulnerabilidad'] = df.apply(score_vulnerabilidad, axis=1)
print("Distribución del score de vulnerabilidad:")
print(df['score_vulnerabilidad'].value_counts().sort_index())
Distribución del score de vulnerabilidad:
score_vulnerabilidad
0    10
1     2
2    52
3    22
4    77
5    37
Name: count, dtype: int64

6. Ejercicios#

Ejercicio 1#

Calculá el monto total de transferencias por región (usá la columna region creada con map). ¿Qué región concentra más recursos?

# Tu solución aquí

Ejercicio 2#

Creá una pivot table que muestre el ingreso mensual promedio por region (filas) y nivel_ingreso (columnas). Incluí márgenes.

# Tu solución aquí

Ejercicio 3#

Creá un DataFrame metas con las siguientes metas de cobertura por programa, y hacé un merge con un resumen del df que contenga la cantidad real de beneficiarios por programa. Calculá qué porcentaje de la meta se alcanzó.

metas = pd.DataFrame({
    'programa': ['AUH', 'Progresar', 'Potenciar Trabajo', 'Alimentar', 'Crédito Argenta'],
    'meta_beneficiarios': [50, 45, 35, 40, 30]  # metas en el dataset simulado
})
# Tu solución aquí