Saltar al contenido

Indice / pandas: combinar y resumir / 6.5

pivot_table y reshape

Los mismos datos tienen dos formas: ancha, que es como se llena una planilla, y larga, que es como los quiere pandas. Pasar de una a otra es una línea, y saber cuál necesitás es media unidad 6.

Para qué sirve

Una planilla de asistencia tiene un alumno por fila y una columna por sesión. Es cómoda de llenar y de mirar, y es hostil para trabajar: contar ausencias obliga a sumar doce columnas a mano, y si el mes que viene hay trece sesiones hay que cambiar el código.

En formato largo cada celda es una fila - alumno, sesión, estado - y la misma pregunta es un groupby de una línea que no depende de cuántas sesiones haya.

ELI5Explicado facil

Es la diferencia entre una grilla de asistencia pegada en la pared y un cuaderno donde anotás un renglón cada vez que alguien falta.

La grilla se lee mejor de un vistazo. El cuaderno se cuenta mejor. Los dos tienen la misma información, y hay que saber pasar de uno al otro porque cada tarea pide uno.

El ejemplo mínimo

EjemploCada celda de datos se convierte en una fila
import pandas as pd

ancho = pd.DataFrame({
    "legajo": ["L01", "L02"],
    "sesion_1": ["P", "A"],
    "sesion_2": ["A", "P"],
    "sesion_3": ["P", "P"],
})
print("formato ancho:")
print(ancho.to_string(index=False))
print("2 filas x 3 sesiones = 6 celdas de datos")

largo = ancho.melt(id_vars="legajo", var_name="sesion", value_name="estado")
print()
print("formato largo:")
print(largo.to_string(index=False))
print(len(largo), "filas: una por celda")

print()
print("id_vars son las columnas que NO se derriten: quedan repetidas.")
print("var_name es como se llama la columna con los nombres viejos.")
print("value_name es como se llama la columna con el contenido.")
formato ancho:
legajo sesion_1 sesion_2 sesion_3
   L01        P        A        P
   L02        A        P        P
2 filas x 3 sesiones = 6 celdas de datos

formato largo:
legajo   sesion estado
   L01 sesion_1      P
   L02 sesion_1      A
   L01 sesion_2      A
   L02 sesion_2      P
   L01 sesion_3      P
   L02 sesion_3      P
6 filas: una por celda

id_vars son las columnas que NO se derriten: quedan repetidas.
var_name es como se llama la columna con los nombres viejos.
value_name es como se llama la columna con el contenido.

Lo mismo, paso a paso

Seguí los colores: las cuatro celdas de un alumno en la tabla ancha son sus cuatro filas en la larga.

Paso a pasoDe ancho a largo con melt, y la vuelta con pivot_table
  1. Formato anchoTres alumnos y cuatro sesiones: 3 filas por 4 columnas. Es como se llena una planilla a mano, y es comodo de mirar. Cada color es un alumno.
    asis = pd.read_csv("asistencia_sesiones.csv")
  2. meltCada celda de datos se convierte en una FILA. Lo que era el nombre de la columna pasa a ser un valor de la columna sesion, y el contenido de la celda va a la columna estado. Seguí los colores: las cuatro celdas de un alumno son sus cuatro filas.
    asis.melt(id_vars="legajo", value_vars=[...],
              var_name="sesion", value_name="estado")
  3. Por que sirveEn formato largo, «cuantas ausencias tuvo cada alumno» es un groupby de una linea. En formato ancho hay que sumar doce columnas a mano, y si manana hay una sesion mas hay que cambiar el codigo.
    largo[largo["estado"] == "A"].groupby("legajo").size()
  4. pivot_tableY la vuelta: pivot_table toma el formato largo y reconstruye la planilla. index dice que va en las filas, columns que va en las columnas, y values que se pone adentro.
    largo.pivot_table(index="legajo", columns="sesion",
                      values="estado", aggfunc="first")
  5. Sobre el archivo enteroEl archivo real tiene 30 alumnos y 12 sesiones. En largo son 360 filas, y ahi adentro estan los cinco valores raros que la planilla ancha esconde entre 360 celdas.
    largo["estado"].value_counts(dropna=False)

Sobre datos de verdad

EjemploSobre datos de verdad: 360 celdas y lo que esconden
import pandas as pd

asis = pd.read_csv("asistencia_sesiones.csv")
sesiones = [c for c in asis.columns if c.startswith("sesion_")]
print("formato ancho:", asis.shape, "|", len(sesiones), "sesiones")

largo = asis.melt(id_vars=["legajo", "estudiante"], value_vars=sesiones,
                  var_name="sesion", value_name="estado")
print("formato largo:", largo.shape)
print(len(asis), "x", len(sesiones), "=", len(asis) * len(sesiones))

print()
print("Y ahora una linea contesta que hay adentro de las 360 celdas:")
print(largo["estado"].value_counts(dropna=False).to_string())

print()
print("P y A son 355. Los otros 5 son basura repartida en la planilla:")
raros = largo[~largo["estado"].isin(["P", "A"])]
print(raros[["legajo", "sesion", "estado"]].to_string(index=False))

print()
print("En formato ancho habria que revisar 12 columnas una por una.")
print("En largo es un value_counts.")
formato ancho: (30, 14) | 12 sesiones
formato largo: (360, 4)
30 x 12 = 360

Y ahora una linea contesta que hay adentro de las 360 celdas:
estado
P           283
A            72
X             1
PRESENTE      1
NaN           1
p             1
-             1

P y A son 355. Los otros 5 son basura repartida en la planilla:
legajo    sesion   estado
 L2711 sesion_01        X
 L2724 sesion_03 PRESENTE
 L2703 sesion_05      NaN
 L2717 sesion_08        p
 L2728 sesion_10        -

En formato ancho habria que revisar 12 columnas una por una.
En largo es un value_counts.
El formato largo es un diagnóstico

La planilla ancha tiene 360 celdas repartidas en 12 columnas. Revisarlas obliga a mirar columna por columna. En formato largo, un solo value_counts encuentra los cinco valores que no son ni P ni A: una X, un PRESENTE, una p minúscula, un guion y un ausente.

Cinco defectos escondidos entre 360 celdas, en una línea.

pivot_table

Ejemplopivot_table: un panel, y una columna que no deberia existir
import pandas as pd

tx = pd.read_csv("transacciones_sucursales.csv")
tx["mes"] = tx["fecha"].str.slice(0, 7)      # 2026-07, sin usar fechas todavia

panel = tx.pivot_table(index="sucursal", columns="mes", values="importe",
                       aggfunc="sum")
print("forma del panel:", panel.shape, "= sucursales x meses")
print("columnas:", list(panel.columns))

print()
print("Hay 7 meses de verdad y una columna que dice 'fecha_i'.")
print("Sale de una fila cuya fecha es literalmente 'fecha_invalida':")
print("cortar los primeros 7 caracteres de un texto roto da otro texto roto,")
print("y pivot_table lo acepta como si fuera un mes mas.")

print()
print(panel[["2026-01", "2026-02", "fecha_i"]].round(2).to_string())

print()
lleno = tx.pivot_table(index="sucursal", columns="mes", values="importe",
                       aggfunc="sum", fill_value=0)
print("con fill_value=0 los huecos se llenan:")
print(lleno[["2026-01", "2026-02", "fecha_i"]].round(2).to_string())

print()
print("Y ahi esta el problema de fill_value: pone 0 en cuatro sucursales")
print("que no tienen ninguna transaccion con fecha rota. Un 0 afirma")
print("'no hubo movimiento'; lo correcto seria que esa columna no exista.")
print("Validar la fecha ANTES de pivotar es lo que evita todo esto.")
forma del panel: (5, 8) = sucursales x meses
columnas: ['2026-01', '2026-02', '2026-03', '2026-04', '2026-05', '2026-06', '2026-07', 'fecha_i']

Hay 7 meses de verdad y una columna que dice 'fecha_i'.
Sale de una fila cuya fecha es literalmente 'fecha_invalida':
cortar los primeros 7 caracteres de un texto roto da otro texto roto,
y pivot_table lo acepta como si fuera un mes mas.

mes        2026-01   2026-02  fecha_i
sucursal                             
Centro    12199.04   6194.21      NaN
Este      11326.56  22356.94   588.64
Norte     13916.99  17228.04      NaN
Oeste     22827.87  10431.05      NaN
Sur       19709.38  32258.93      NaN

con fill_value=0 los huecos se llenan:
mes        2026-01   2026-02  fecha_i
sucursal                             
Centro    12199.04   6194.21     0.00
Este      11326.56  22356.94   588.64
Norte     13916.99  17228.04     0.00
Oeste     22827.87  10431.05     0.00
Sur       19709.38  32258.93     0.00

Y ahi esta el problema de fill_value: pone 0 en cuatro sucursales
que no tienen ninguna transaccion con fecha rota. Un 0 afirma
'no hubo movimiento'; lo correcto seria que esa columna no exista.
Validar la fecha ANTES de pivotar es lo que evita todo esto.

El panel salió con ocho columnas cuando hay siete meses. La octava se llama fecha_i y viene de una fila cuya fecha es literalmente fecha_invalida: cortar los primeros 7 caracteres de un texto roto da otro texto roto, y pivot_table lo acepta como si fuera un mes más.

Y hay una fila más que directamente no aparece: la que tiene la fecha vacía. Su «mes» quedó NaN y pivot_table descarta las claves ausentes, igual que groupby.

La regla, otra vez

Validar antes de pivotar. Un pivote no valida nada: toma los valores que encuentra y arma una columna por cada uno. Si entra basura, sale una columna de basura con un nombre creíble.

pivot no es pivot_table

Ejemplopivot y pivot_table no son lo mismo
import pandas as pd

tx = pd.read_csv("transacciones_sucursales.csv")
tx["mes"] = tx["fecha"].str.slice(0, 7)

try:
    tx.pivot(index="sucursal", columns="mes", values="importe")
except ValueError as error:
    print("pivot ->", type(error).__name__)
    print(str(error).splitlines()[0])

print()
print("pivot solo REACOMODA: exige que cada par (sucursal, mes) aparezca")
print("una sola vez. Aca hay varias transacciones por sucursal y mes.")

print()
print("pivot_table AGREGA, y por eso hay que decirle como:")
print(tx.pivot_table(index="sucursal", columns="mes", values="importe",
                     aggfunc="sum").iloc[:3, :3].round(2).to_string())

print()
print("Su aggfunc por defecto es 'mean', que casi nunca es lo que queres")
print("y no avisa. Escribilo siempre.")
print()
media = tx.pivot_table(index="sucursal", columns="mes", values="importe")
suma = tx.pivot_table(index="sucursal", columns="mes", values="importe",
                      aggfunc="sum")
print("sin aggfunc (mean):", round(media.iloc[0, 0], 2))
print("con aggfunc='sum' :", round(suma.iloc[0, 0], 2))
pivot -> ValueError
Index contains duplicate entries, cannot reshape

pivot solo REACOMODA: exige que cada par (sucursal, mes) aparezca
una sola vez. Aca hay varias transacciones por sucursal y mes.

pivot_table AGREGA, y por eso hay que decirle como:
mes        2026-01   2026-02   2026-03
sucursal                              
Centro    12199.04   6194.21  21897.63
Este      11326.56  22356.94  37122.28
Norte     13916.99  17228.04   7845.09

Su aggfunc por defecto es 'mean', que casi nunca es lo que queres
y no avisa. Escribilo siempre.

sin aggfunc (mean): 3049.76
con aggfunc='sum' : 12199.04
Las dos, y cuándo
pivotpivot_table
Qué haceSolo reacomodaAgrega y reacomoda
Si la clave se repiteValueErrorAgrega con aggfunc
aggfuncNo tienePor defecto "mean"
CuándoCuando cada par aparece una vez y querés que falle si noEl resto del tiempo
El aggfunc por defecto es "mean"

Y casi nunca es lo que querés. Un panel de importes por sucursal y mes con el default da el promedio de las transacciones, no el total: 3049.76 en vez de 12199.04. Los dos son números plausibles y nadie avisa cuál estás mirando.

Escribí aggfunc siempre, aunque sea para poner "mean".

La vuelta completa

EjemploLa vuelta completa: melt, pivot y comprobar que no se perdio nada
import pandas as pd

asis = pd.read_csv("asistencia_sesiones.csv")
sesiones = [c for c in asis.columns if c.startswith("sesion_")]

largo = asis.melt(id_vars=["legajo", "estudiante"], value_vars=sesiones,
                  var_name="sesion", value_name="estado")

vuelta = (largo.pivot_table(index=["legajo", "estudiante"], columns="sesion",
                            values="estado", aggfunc="first")
          .reset_index())
vuelta.columns.name = None

print("original:", asis.shape)
print("de vuelta:", vuelta.shape)

# Para comparar hay que ponerlos en el mismo orden y con las mismas columnas.
a = asis.sort_values("legajo", kind="stable").reset_index(drop=True)
b = vuelta[asis.columns].sort_values("legajo", kind="stable").reset_index(drop=True)
print()
print("son iguales:", bool(a.equals(b)))

print()
print("Ojo con el aggfunc='first': el pivote NECESITA una funcion aunque no")
print("haya nada que agregar, porque no sabe que cada par (alumno, sesion)")
print("aparece una sola vez. Si apareciera dos veces, 'first' elegiria una")
print("y no diria nada.")

print()
print("Y las celdas ausentes:", int(asis[sesiones].isna().sum().sum()),
      "en el original,", int(vuelta[sesiones].isna().sum().sum()), "en la vuelta.")
original: (30, 14)
de vuelta: (30, 14)

son iguales: True

Ojo con el aggfunc='first': el pivote NECESITA una funcion aunque no
haya nada que agregar, porque no sabe que cada par (alumno, sesion)
aparece una sola vez. Si apareciera dos veces, 'first' elegiria una
y no diria nada.

Y las celdas ausentes: 1 en el original, 1 en la vuelta.

Ancho → largo → ancho devuelve exactamente el original. Eso es lo que hace seguro trabajar en largo: no se pierde nada, y la forma de presentación se elige al final.

Dónde se rompe

ValueError: Index contains duplicate entries, cannot reshape

Causa: pivot y la combinación de índice y columna se repite.

Arreglo: si el duplicado es esperado, pivot_table con un aggfunc declarado. Si no lo es, encontraste un problema de datos y el error te lo está diciendo.

(sin error) el panel tiene una columna con un nombre raro

Causa: la columna que fue a columns= tiene un valor inválido, y el pivote arma una columna con él.

Arreglo: validar antes de pivotar. Un pivote no filtra ni normaliza.

(sin error) los totales del panel dan menos que el archivo

Causa: las filas cuya clave de índice o de columna es ausente se descartan, igual que en groupby.

Arreglo: contarlas antes, y un assert comparando la suma del panel con la del archivo.

(sin error) los números del panel son promedios y esperabas totales

Causa: no se declaró aggfunc y el default es "mean".

Arreglo: escribirlo siempre.

(sin error) después de un melt hay muchas más filas de las esperadas

Causa: no se declaró value_vars, así que todas las columnas que no son id_vars se derritieron - incluidas las que querías conservar como identificadores.

Arreglo: declarar las dos listas: id_vars y value_vars. Es más largo y no sorprende.

Acá hay que elegir

fill_value: cero, o vacío

Un hueco en un panel puede significar dos cosas distintas y el pivote no las distingue:

  • No hubo movimiento - entonces 0 es correcto e informativo.
  • No hay datos de ese período - entonces 0 es una mentira: afirma que se midió y dio cero.

El ejemplo de arriba muestra el caso feo: fill_value=0 puso un cero en cuatro sucursales para una columna que ni siquiera debería existir. Antes de rellenar, preguntate si el hueco es un dato o una ausencia de dato.

Recetario

melt y pivot_table
ExpresiónQué haceCuándo
df.melt(id_vars=, value_vars=)Ancho → largoAntes de agrupar o cruzar
var_name= / value_name=Nombra las dos columnas nuevasSiempre: los defaults son variable y value
df.pivot(index=, columns=, values=)Reacomoda; falla si hay duplicadosCuando querés que falle
df.pivot_table(..., aggfunc="sum")Agrega y reacomodaEl caso normal
aggfunc="first"Toma un valor sin agregarVolver a ancho un largo sin repeticiones
fill_value=0Rellena los huecosSolo si el hueco significa cero
margins=TrueAgrega fila y columna de totalesPaneles para leer, no para seguir procesando
.reset_index()Baja el índice a columnasAntes de escribir el panel
df.columns.name = NoneSaca el nombre del eje de columnasCosmético, pero el CSV sale más limpio
panel.sum().sum() == df["c"].sum()boolEl assert que atrapa las claves ausentes

Ponete a prueba

1. Una planilla de 30 alumnos por 12 sesiones se pasa a formato largo. ¿Cuantas filas tiene el resultado?Nivel 1 · Recordar
2. ¿Que son id_vars en melt?Nivel 1 · Recordar
3. ¿Cual es el aggfunc por defecto de pivot_table?Nivel 2 · Predecir la salida
4. pivot levanta ValueError: Index contains duplicate entries. ¿Que significa?Nivel 2 · Predecir la salida
5. Un panel de sucursal por mes sale con 8 columnas y hay 7 meses. La octava se llama fecha_i. ¿Que paso?Nivel 3 · Elegir entre dos casi correctas
6. ¿Cuando conviene fill_value=0 en un panel?Nivel 3 · Elegir entre dos casi correctas
7. Este panel tiene que mostrar el total facturado por sucursal y mes. Los numeros son mucho mas chicos de lo esperado y la suma del panel no da el total del archivo. ¿Que hay que revisar?
tx["mes"] = tx["fecha"].str.slice(0, 7)
panel = tx.pivot_table(index="sucursal", columns="mes", values="importe")
Nivel 4 · Encontrar el defecto

Prácticas de esta lección