Operaciones por columnas y filas
Crear columnas: tres formas
Para añadir una columna nueva a un DataFrame, tres opciones:
import numpy as np
import pandas as pd
df = pd.DataFrame({
"ventas": [1500, 2100, 1800, 2400],
"clientes": [120, 180, 145, 200],
})
# Opción 1: asignación directa
df["ticket_medio"] = df["ventas"] / df["clientes"]
# Opción 2: assign (devuelve nuevo DataFrame)
df = df.assign(ticket_medio=df["ventas"] / df["clientes"])
# Opción 3: assign con lambda (más limpio en chains)
df = df.assign(ticket_medio=lambda x: x["ventas"] / x["clientes"])Las tres dan el mismo resultado. Pero hay una diferencia importante: assign() devuelve un nuevo DataFrame sin modificar el original. La asignación directa muta el DataFrame existente.
Para análisis encadenado (method chaining), assign() es la opción idiomática:
resumen = (
df
.assign(ticket_medio=lambda x: x["ventas"] / x["clientes"])
.assign(categoria=lambda x: np.where(x["ticket_medio"] > 12, "alto", "bajo"))
.sort_values("ticket_medio", ascending=False)
)El lambda x: recibe el DataFrame en el estado actual del chain, lo que importa porque la columna ticket_medio que se usa en el segundo assign() se acaba de crear en el primero.
Equivalente al mutate() de dplyr, con el mismo patrón de uso.
Vectorización: la regla de oro
Las operaciones de pandas/NumPy son vectorizadas, operan sobre toda la columna a la vez, en C, sin bucle de Python. Esto es órdenes de magnitud más rápido que iterar fila por fila.
Lo lento:
# NO HAGAS ESTO
df["nueva"] = [x * 2 for x in df["ventas"]]Lo rápido:
df["nueva"] = df["ventas"] * 2Con un DataFrame de un millón de filas, la primera tarda segundos, la segunda milisegundos. Esta es la lección fundamental al venir de R: no iteres, vectoriza.
Lógica condicional vectorizada
Lo más común: crear una columna que depende de una condición. En R sería ifelse() o case_when(). En pandas, NumPy tiene los equivalentes idiomáticos.
np.where(): if/else binario
df["categoria"] = np.where(df["ventas"] > 2000, "alto", "bajo")Argumentos: condición, valor si verdadero, valor si falso. Equivalente al ifelse() clásico de R.
np.select(): múltiples condiciones (como case_when)
condiciones = [
df["ventas"] > 2200,
df["ventas"] > 1700,
df["ventas"] > 0,
]
valores = ["alto", "medio", "bajo"]
df["categoria"] = np.select(condiciones, valores, default="desconocido")np.select() toma una lista de condiciones y una lista paralela de valores. Aplica la primera que sea verdadera (orden importa). Equivalente a dplyr::case_when().
Operadores entre columnas
df["margen"] = df["ventas"] * 0.30
df["ratio"] = df["ventas"] / df["clientes"]
df["es_grande"] = (df["ventas"] > 1500) & (df["clientes"] > 100)Todo elemento a elemento, sin bucle.
.apply(): lo que parece intuitivo pero suele ser lento
.apply() recibe una función y la llama por fila o por columna. Es lo que parece la forma natural de usar pandas si vienes de Python básico:
df["doble"] = df["ventas"].apply(lambda x: x * 2)Pero es lento. .apply() itera en Python, no vectoriza. Para esa misma operación:
df["doble"] = df["ventas"] * 2 # mucho más rápidoCuándo SÍ usar .apply()
- Cuando la lógica no es vectorizable: una llamada a una función externa (regex compleja, hash, API).
- Cuando necesitas una función arbitraria por fila que combina varias columnas de forma no trivial.
def clasificar(fila):
if fila["ventas"] > 2000 and fila["clientes"] > 150:
return "estrella"
elif fila["ventas"] > 2000:
return "alto_volumen"
else:
return "normal"
df["tipo"] = df.apply(clasificar, axis=1)axis=1 aplica por fila (cada fila es una Series). Sin axis=1, aplica por columna.
Pero antes de llegar aquí, siempre prueba np.select() primero. Casi todo “apply por fila con if/else” puede expresarse como np.select().
transform(): para operaciones que preservan tamaño
.transform() aplica una función pero devuelve el mismo shape que el input. Útil para normalizar o centrar:
df["ventas_centradas"] = df["ventas"].transform(lambda x: x - x.mean())Lo verdaderamente útil de transform() aparece con groupby (en el próximo tutorial): permite hacer mean() por grupo y “expandir” el resultado a cada fila, sin tener que hacer un merge manual.
agg() y reducción
Si quieres una sola estadística por columna:
df.agg("mean") # media de cada columna numérica
df.agg(["mean", "std"]) # media y desviación de cada columna
df["ventas"].agg("sum") # 7800agg() es la operación de “reducir”, toma N elementos y devuelve menos. Lo veremos a fondo con groupby() en el siguiente tutorial.
pipe(): encadenar funciones propias
A veces tienes una función que toma un DataFrame y devuelve otro. .pipe() la integra en el chain:
def añadir_metricas(d):
return d.assign(
ticket_medio=d["ventas"] / d["clientes"],
margen=d["ventas"] * 0.30,
)
(df
.query("ventas > 1500")
.pipe(añadir_metricas)
.sort_values("ticket_medio", ascending=False)
)Equivalente al %>% con función propia en R. Permite separar lógica reutilizable sin romper el flujo del chain.
Trampas habituales
- Iterar con
forsobre filas. Si te encuentras escribiendofor i, fila in df.iterrows():, casi siempre hay una forma vectorizada.iterrows()es muy lento porque crea una Series por cada fila. apply()conaxis=1cuando hay vectorización. Si tu apply solo hace aritmética y condiciones, pruebanp.where,np.select, o combinación de columnas. Suele ser 10-100× más rápido.- Comparar Series con
==y olvidar paréntesis.df.ventas > 1000 & df.clientes > 50falla por precedencia. Pon paréntesis:(df.ventas > 1000) & (df.clientes > 50). - Modificar el DataFrame durante un bucle. Patrón anti-idiomático. Construye una lista de resultados, después
pd.concat. - No usar
assigncon lambda. Sin lambda,df.assign(x=df["a"]+1)leedfantes del chain, y si el chain hizo cambios, ignoras esos cambios. Con lambda,df.assign(x=lambda d: d["a"]+1)accede al estado actual.
En la siguiente entrega
Ya manipulas columnas. Lo más potente de pandas todavía no ha aparecido: groupby(), la operación split-apply-combine. Es donde pandas brilla y donde la sintaxis se vuelve más rica. Equivalente a group_by() + summarise() de dplyr, pero con más opciones. Lo siguiente.