Python & Data Science

Deja de hacer sopa de variables: una guía para el encadenamiento de métodos en Pandas

1. El problema de la ‘sopa de variables’

Todos hemos pasado por eso. Abres un nuevo Jupyter notebook, cargas un CSV y empiezas a limpiar los datos. Primero eliminas algunos valores faltantes y lo llamas df2. Luego filtras los valores atípicos y lo llamas df_filtered. Para cuando llegas a la etapa de análisis, estás trabajando con df_final_v3_fixed.

A esto lo llamo la “sopa de variables”. Tu script se convierte en un cementerio de DataFrames intermedios, y no puedes saber qué cambió ni cuándo. Cambia una regla de filtrado en medio del notebook, y a menudo tendrás que reiniciar todo el kernel solo para asegurarte de que df4 no use accidentalmente la versión anterior de df3.

A continuación, un ejemplo típico de este estilo procedural:

import pandas as pd
import numpy as np

# Creating a dummy dataset of store sales
data = {
    'store_id': [1, 2, 3, 4, 5],
    'revenue': [100, 250, np.nan, 400, 150],
    'region': ['North', 'South', 'North', 'East', 'West']
}
df = pd.DataFrame(data)

# The Variable Soup approach
df1 = df.dropna(subset=['revenue'])
df1['revenue_scaled'] = df1['revenue'] * 1.1
df2 = df1[df1['revenue_scaled'] > 150]
df_final = df2.sort_values('revenue_scaled', ascending=False)

print(df_final)
# Output shows 2 rows. The logic is scattered across four different variable names.
Este bloque construye un pequeño DataFrame de juguete a partir de un diccionario de Python, y luego lo transforma a través de cuatro variables intermedias separadas. `dropna(subset=['revenue'])` elimina las filas donde la columna `revenue` es `NaN`. La asignación con corchetes `df1['revenue_scaled'] = ...` crea una nueva columna directamente en `df1`. `df1[df1['revenue_scaled'] > 150]` es indexación booleana: conserva solo las filas donde la condición es `True`. `.sort_values(..., ascending=False)` reordena el DataFrame por la columna dada en orden descendente. Cada paso crea un nuevo objeto DataFrame que se enlaza a un nuevo nombre de variable, lo cual es el núcleo del problema de la "sopa de variables".

El código funciona, pero leerlo es una cacería de pistas. ¿Quieres saber por qué se eliminó una fila? Tienes que rastrear hacia atrás a través de df2, df1 y df. También desperdicia memoria, ya que Python mantiene vivas todas esas copias intermedias. ¿Qué tal si tu código se leyera como una receta en su lugar?

2. La intuición: pensar en pipelines

Piensa en tus datos como un auto en una línea de ensamblaje. El chasis comienza en bruto y sin terminar. A medida que avanza por la línea, cada estación añade su pieza — una coloca el motor, otra pinta las puertas, otra instala los asientos.

El encadenamiento de métodos es simplemente poner esas estaciones en orden. No hay que sacar el auto de la línea, estacionarlo en un garaje (df1) y luego arrastrarlo de vuelta para la siguiente parada. El auto simplemente sigue avanzando.

Queremos que nuestro código diga: “Toma estos datos, luego elimina las filas vacías, luego calcula el impuesto, luego filtra los resultados.” La parte difícil de este cambio es dejar atrás el impulso de nombrar cada paso. No necesitas llamarlo “Auto Con Puertas” y “Auto Con Puertas y Ruedas”. Solo necesitas el auto terminado.

3. Tu primera cadena: el poder de los paréntesis

Poner todo en una sola línea puede convertirse rápidamente en un desastre. La solución es simple: paréntesis. Envuelve toda la operación en (), y Python te permite poner cada paso en su propia línea.

También vamos a dejar de usar df['col'] = ... y recurrir a .assign() en su lugar. Esto mantiene los datos fluyendo a través de la cadena sin romperla. Vamos a refactorizar nuestro ejemplo de “Sopa de Variables” en un pipeline limpio:

# The Method Chaining approach
clean_df = (
    df
    .dropna(subset=['revenue'])
    .assign(revenue_scaled = lambda x: x['revenue'] * 1.1)
    .query("revenue_scaled > 150")
    .sort_values('revenue_scaled', ascending=False)
)

print(clean_df)
# The result is exactly the same, but the logic is a single, readable block.
Toda la expresión está envuelta en `(...)`, lo que permite a Python tratar la cadena multilínea como una sola declaración — sin los paréntesis obtendrías un `SyntaxError` porque una continuación de línea después de un `.` no es válida por sí sola. Cada llamada a un método devuelve un DataFrame *nuevo*, y el siguiente método se llama sobre ese resultado, por lo que los datos fluyen de izquierda a derecha, de arriba hacia abajo. `.assign(revenue_scaled=lambda x: x['revenue'] * 1.1)` usa una lambda que recibe el DataFrame tal como existe *en ese punto de la cadena* (es decir, después de que `dropna` ya se ha ejecutado), por lo que es más seguro que referenciar el `df['revenue']` original. `.query("revenue_scaled > 150")` es un filtro basado en texto — Pandas analiza la expresión internamente, evitando la sintaxis de corchetes y booleanos `df[df['col'] > 150]`. **Encadenamiento de métodos vs. estilo de variables intermedias — cuándo usar cada uno.**

El encadenamiento de métodos (el estilo anterior) brilla cuando:

  • Cada paso es una transformación pura (devuelve un DataFrame nuevo, sin efectos secundarios).
  • Quieres que todo el pipeline se lea como una receta declarativa: “toma los datos, luego limpia, luego enriquece, luego filtra.”
  • Quieres evitar contaminar el espacio de nombres del notebook con df1, df2, df3, … lo que desperdicia memoria y hace poco claro qué versión es la “actual.”

Las variables intermedias (df1 = df.dropna(...), df2 = df1[...]) brillan cuando:

  • Necesitas inspeccionar o depurar un estado intermedio específico en un depurador — puedes poner un punto de interrupción en la línea donde se define df2 y examinarlo directamente. En una cadena, no puedes fácilmente “saltar al medio” de una sola expresión.
  • Necesitas reutilizar un resultado intermedio en dos ramas posteriores diferentes (p. ej., df_clean alimenta tanto un gráfico como un modelo). El encadenamiento te obliga a duplicar los pasos iniciales o a extraer una variable de todos modos.
  • Tus pasos tienen efectos secundarios (mutar un DataFrame in situ) que hacen ambiguo el orden de operaciones en una cadena.

Regla práctica: Usa encadenamiento para pipelines lineales y principalmente de lectura. Extrae una variable con nombre en el momento en que necesites ramificar, reutilizar o depurar profundamente un estado intermedio. El problema de la “sopa de variables” no se debe a usar variables intermedias — se debe a usar variables intermedias sin nombre, sin documentación y numeradas en serie. Un df_clean o sales_long bien nombrado es perfectamente aceptable; el enemigo es df1 hasta df7.

¿Qué está pasando realmente aquí? El punto al inicio de cada línea señala lo que viene a continuación. .query() filtra los datos de manera más limpia que los voluminosos corchetes. Nota el lambda x dentro de .assign() — una pequeña función que dice “toma el dataframe tal como existe en este paso y usa su columna de ingresos.” Vale la pena destacar: esto importa porque la columna de ingresos podría haber cambiado en un paso anterior.

4. El método .pipe(): cuando las funciones integradas no son suficientes

A veces Pandas no tiene un botón integrado para lo que necesitas. Tal vez tengas una forma específica de calcular impuestos regionales, o una rutina compleja de limpieza de texto. Normalmente eso rompe tu cadena — te detendrías, ejecutarías tu función y luego empezarías de nuevo.

Ahí es donde entra .pipe(). Piensa en ello como una estación personalizada en la línea de ensamblaje. Introduces el dataframe en una función y mantienes la cadena en movimiento.

Esto confunde a los principiantes porque parece metaprogramación. Pero la idea es sencilla: pasar el resultado del paso anterior a tu herramienta personalizada.

def apply_tax(dataframe, tax_rate):
    # A custom function that adds a tax column
    dataframe['total_cost'] = dataframe['revenue_scaled'] * (1 + tax_rate)
    return dataframe

final_chain = (
    df
    .dropna(subset=['revenue'])
    .assign(revenue_scaled = lambda x: x['revenue'] * 1.1)
    .pipe(apply_tax, tax_rate=0.05) # Custom step integrated seamlessly
    .query("total_cost > 160")
)

print(final_chain)
# We added a custom calculation without ever creating a 'df_temp' variable.
`apply_tax` es una función común y corriente de Python — no un método de Pandas. Toma un DataFrame como su primer argumento (`dataframe`), un argumento de palabra clave `tax_rate`, añade una columna `total_cost` multiplicando `revenue_scaled` por `(1 + tax_rate)`, y retorna el DataFrame. `.pipe(apply_tax, tax_rate=0.05)` es el puente: toma el DataFrame producido por el paso anterior `.assign(...)` y lo pasa como primer argumento posicional a `apply_tax`, reenviando `tax_rate=0.05` como palabra clave. El valor de retorno de la función se convierte en la entrada del siguiente método en la cadena (`.query`). Nota un detalle: `apply_tax` hace `dataframe['total_cost'] = ...`, lo cual modifica el DataFrame *in situ*. Esto funciona aquí porque la cadena produce un DataFrame nuevo en cada paso, pero en general, es preferible retornar un DataFrame *nuevo* (p. ej., usando `.assign()` dentro de la función) para evitar efectos secundarios inesperados si el mismo objeto está referenciado en otra parte.

5. Depurando la cadena: ‘¿Pero cómo veo qué está pasando?’

El mayor temor de la gente respecto a las cadenas es que son difíciles de depurar. Si el resultado se ve incorrecto, ¿cómo saber qué paso lo rompió?

No tienes que perder la visibilidad. El truco más fácil es el método de “comentar” (comment out) — ya que cada paso está en su propia línea, comenta los pasos posteriores para ver el estado de los datos en cualquier punto intermedio de la cadena.

Un enfoque más limpio es una función de registro (logging). Aquí se muestra lo que ocurre cuando usas una función auxiliar para inspeccionar los datos a mitad de la cadena:

def log_shape(dataframe, label="Step"):
    print(f"{label}: {dataframe.shape[0]} rows remaining")
    return dataframe

debugged_df = (
    df
    .pipe(log_shape, "Start")
    .dropna(subset=['revenue'])
    .pipe(log_shape, "After Dropna")
    .assign(revenue_scaled = lambda x: x['revenue'] * 1.1)
    .query("revenue_scaled > 500") # This will likely filter everything out
    .pipe(log_shape, "After Query")
)

# The output will show: 
# Start: 5 rows remaining
# After Dropna: 4 rows remaining
# After Query: 0 rows remaining
# Now we know exactly where our data disappeared!
`log_shape` es una **función de paso**: imprime un mensaje de diagnóstico (el `label` más `dataframe.shape[0]`, el número de filas) y luego devuelve `dataframe` sin cambios para que la cadena pueda continuar. `.pipe(log_shape, "Start")` llama a `log_shape(df, label="Start")` — el DataFrame del paso anterior es el primer argumento posicional; el `label` se pasa como un argumento de palabra clave. Al insertar `.pipe(log_shape, ...)` en múltiples puntos de la cadena, obtienes un recuento de filas en cada etapa intermedia sin tener que asignar una variable intermedia con nombre. La línea `.query("revenue_scaled > 500")` filtra con un umbral demasiado alto para los datos de juguete, por lo que `log_shape` al final informa 0 filas, localizando exactamente dónde desaparecieron los datos.

6. Resumen: del caos a la maestría

Tu yo del futuro te lo agradecerá por cambiar de la Sopa de Variables al encadenamiento de métodos. Abre un notebook dentro de seis meses y no estarás rastreando df1 hasta df10. Simplemente leerás la receta.

Aquí tienes tu lista de verificación para un Pandas más limpio:

  1. Usa paréntesis: Envuelve el código en () para permitir cadenas de múltiples líneas.
  2. Usa .assign(): Evita df['col'] = ... para mantener el flujo de los datos.
  3. Usa .query(): Más legible que el indexado booleano.
  4. Usa .pipe(): Para lógica personalizada y funciones auxiliares de depuración.
  5. Reconoce cuándo detenerte: Una cadena de 20 líneas se puede dividir en dos fragmentos más pequeños y bien nombrados.

Aquí tienes una última cadena que integra todo:

# The final, clean, professional pipeline
master_df = (
    df
    .rename(columns=str.lower)
    .dropna(subset=['revenue'])
    .pipe(log_shape, "Initial Clean")
    .assign(
        is_high_value = lambda x: x['revenue'] > 200,
        tax_amount = lambda x: x['revenue'] * 0.08
    )
    .query("region != 'West'")
    .sort_values('revenue', ascending=False)
)

print(master_df)
# The effect is clear: one readable block that transforms raw data into insights.
`.rename(columns=str.lower)` convierte a minúsculas todos los nombres de las columnas pasando la función integrada `str.lower` como la función aplicada a cada nombre de columna — una forma elegante de normalizar encabezados desordenados. `.dropna(subset=['revenue'])` elimina las filas con `revenue` faltante. `.pipe(log_shape, "Initial Clean")` inserta la función auxiliar de depuración de la sección anterior a mitad de la cadena. `.assign(...)` añade dos columnas en una sola llamada — ambas lambdas reciben el DataFrame tal como existe *en ese punto*, por lo que tanto `is_high_value` como `tax_amount` ven los datos posteriores a dropna y rename. `.query("region != 'West'")` filtra una región usando una expresión de cadena. `.sort_values('revenue', ascending=False)` reordena por ingresos, de mayor a menor. La cadena se lee de arriba a abajo como una receta: normalizar → limpiar → registrar → enriquecer → filtrar → ordenar.

Abre tu notebook más desordenado. Convierte una de esas Sopas de Variables en un pipeline limpio y elegante. Feliz limpieza.

Comprueba tu comprensión

Las preguntas a continuación pasan de un simple recuerdo a un diseño abierto, siguiendo aproximadamente la Taxonomía de Bloom.

Recordar ¿Qué permite hacer .pipe() que no es posible con los métodos integrados de Pandas como .query() o .assign()?

Comprender Con tus propias palabras, explica por qué el artículo usa lambda x: x['revenue'] * 1.1 dentro de .assign() en lugar de hacer referencia directamente a la columna original df['revenue'].

Aplicar Usando el patrón de depuración log_shape del artículo, si insertaras una llamada .pipe(log_shape, "After Filter") justo después de un paso .query("revenue > 1000") en el conjunto de datos post-dropna de 4 filas (ingresos 100, 250, 400, 150), ¿qué recuento de filas imprimiría?

Analizar El artículo dice que la “Sopa de variables” hace que sea difícil saber por qué se eliminó una fila, lo que requiere que “rastrees a través de df2, df1 y df”. Recorre paso a paso cómo el patrón .pipe(log_shape, ...) resuelve este mismo problema de depuración sin reintroducir el desorden de variables intermedias que la cadena fue diseñada para evitar.

Evaluar El elemento 5 de la lista de verificación del artículo dice que “si tu cadena tiene 20 líneas de largo, está bien dividirla en dos fragmentos más pequeños y bien nombrados”. Critica la falta de una regla más clara aquí: ¿qué señal específica (no solo el recuento de líneas) te indicaría que una cadena se ha vuelto difícil de razonar y debería dividirse, frente a una que es larga pero aún perfectamente clara?

Crear Diseña un refactor de encadenamiento de métodos para un script de “Sopa de variables” que: cargue un dataframe, elimine las filas donde email es nulo, añada una columna domain extraída del correo electrónico, filtre solo los dominios .com, y ordene por fecha de registro. Escribe la cadena usando .dropna(), .assign(), y .query()/.sort_values(), siguiendo el estilo del artículo.


Artículos relacionados

Referencias y lecturas adicionales


Aplica lo que aprendiste

Resumen. Estás implementando un pipeline de preparación de características en Pandas como microservicio. Un compañero de equipo escribió el pipeline de métodos encadenados a continuación siguiendo el estilo del artículo — paréntesis, .assign(), .pipe(), .query() — pero el equipo de modelos reporta dos síntomas: (1) el pipeline elimina una fila que claramente tiene ingresos válidos, y (2) el total_cost resultante para la fila de ingresos de 150 es ~475 en lugar del ~178 esperado. Usa la técnica de depuración de paso del artículo .pipe(log_shape, …) para localizar dónde se rompe la cadena, luego corrige el error y escribe un breve informe de incidente.

Pipeline con errores.

import pandas as pd
import numpy as np

df = pd.DataFrame({
    'store_id': [1, 2, 3, 4, 5],
    'revenue': [100, 250, np.nan, 400, 150],
    'region': ['North', 'South', 'North', 'East', 'West']
})

def compute_tax(df, rate=0.08):
    df['tax_amount'] = df['revenue_scaled'] * rate
    df['total_cost'] = df['revenue_scaled'] + df['tax_amount']
    return df

features = (
    df
    .dropna(subset=['revenue'])
    .reset_index(drop=True)
    .assign(revenue_scaled=df['revenue'] * 1.1)   # ← planted bug
    .pipe(compute_tax, rate=0.08)
    .query("total_cost > 160")
)
# Expected: 3 rows survive (revenues 250, 400, 150 → total_cost ≈ 297, 475, 178)
# Actual:   2 rows; the revenue-400 row vanished, the revenue-150 row has total_cost ≈ 475

Entregable. El código del pipeline corregido más un informe de incidente de 100–150 palabras que nombre la causa raíz, la solución y la técnica de depuración que usaste para aislarlo.

Rúbrica (todos los elementos deben pasar):

  • Encuentra el error. Identifica que .assign(revenue_scaled=df['revenue'] * 1.1) hace referencia al df a nivel de módulo original en lugar del DataFrame tal como existe en ese punto de la cadena. Después de .dropna() + .reset_index(drop=True), el índice [0,1,2,3,4] del df original ya no coincide con el índice [0,1,2,3] del DataFrame actual; pandas alinea por índice, inyectando el NaN original del índice 2 en el índice 2 actual (la fila de ingresos de 400) y colocando 440 donde debería ir 165.
  • Aplica la solución. Reemplaza df['revenue'] * 1.1 con lambda x: x['revenue'] * 1.1, para que .assign() lea desde el estado actual de la cadena — el patrón exacto que prescribe el artículo.
  • Usa la técnica de depuración del artículo. Inserta .pipe(log_shape, …) (o un paso equivalente que imprima la forma y devuelva el DataFrame) antes y después de .assign() para mostrar que 4 filas entran pero revenue_scaled contiene un NaN, localizando el paso sin introducir variables intermedias.
  • El informe es claro y conciso (100–150 palabras). Nombra la causa raíz (desalineación de índices por hacer referencia al DataFrame original después de reset_index), la solución (lambda x:) y el método de depuración utilizado.

Esta traducción fue generada automáticamente y puede contener errores. Si el idioma inglés es tu preferencia, puedes leer el artículo original en inglés .

¿Buscas otra cosa?

Busca en todos los artículos por título, resumen o tema.