Clase 5 — Pandas II: operaciones sobre DataFrames#
Python y Políticas Públicas
Contenidos#
GroupBy y agregaciones
Merge y join
Concatenar DataFrames
Pivot tables y reshaping
Apply y funciones sobre columnas
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í