Operaciones por columnas y filas

python
pandas
eda
Crear columnas calculadas con assign(), aplicar condiciones vectorizadas con np.where() y np.select(), por qué .apply() suele ser la última opción y cuándo sí merece la pena.

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"] * 2

Con 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ápido

Cuá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") # 7800

agg() 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 for sobre filas. Si te encuentras escribiendo for i, fila in df.iterrows():, casi siempre hay una forma vectorizada. iterrows() es muy lento porque crea una Series por cada fila.
  • apply() con axis=1 cuando hay vectorización. Si tu apply solo hace aritmética y condiciones, prueba np.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 > 50 falla 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 assign con lambda. Sin lambda, df.assign(x=df["a"]+1) lee df antes 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.