Curso
Muchas personas recurren a .apply()
para «evitar bucles» y esperan que sea rápido y sencillo. En la práctica, el « .apply(axis=1)
» por filas sigue siendo un bucle a nivel de Python. Puede ser lento con datos de gran tamaño y, en ocasiones, devuelve formas que no esperabas. La solución es sencilla: utiliza operaciones vectorizadas de pandas/NumPy para tareas comunes y reserva .apply()
para la lógica que realmente necesite varias columnas.
Esta guía muestra cómo .apply()
funciona hoy en día, destaca los errores comunes y proporciona patrones de inserción que son más rápidos y claros.
¿Qué son DataFrame.apply() y Series.apply()?
DataFrame.apply(func, axis=0)
Llama a func
en cada columna de forma predeterminada. Con axis=1
, llama a func
en cada fila. Series.apply(func)
llama a func
en cada elemento (o en todo Series
, dependiendo de las firmas en las versiones recientes).
- Por filas frente a por columnas:
axis=1
significa «por fila».axis=0
(por defecto) significa «por columna».
- Reglas de forma de retorno (pandas ≥0,23): Si tu función devuelve un escalar por fila/columna, obtienes un
Series
. Si devuelve unSeries
o undict
, pandas expande esas claves en unDataFrame
. Si devuelve una lista/arreglo, obtendrás una serie de listas a menos que establezcasresult_type='expand'
.
- Errores más estrictos (pandas ≥2.0): Las longitudes desiguales en las listas ahora generan errores en lugar de producir resultados inconsistentes de forma silenciosa.
Primera opción recomendada: Operaciones vectorizadas
La mayoría de las tareas que implican combinar o transformar columnas son más rápidas y claras con expresiones vectorizadas o métodos integrados. El siguiente ejemplo calcula la diferencia de carreras de un equipo de béisbol sin utilizar .apply()
.
import pandas as pd
team_stats = pd.DataFrame({
"Team": ["ARI", "ATL", "BAL"],
"League": ["NL", "NL", "AL"],
"Year": [2012, 2012, 2012],
"RunsScored": [734, 700, 712],
"RunsAllowed": [688, 600, 705],
"Wins": [81, 94, 93],
"Games": [162, 162, 162],
"Playoffs": [0, 1, 1],
})
# Vectorized: fast and idiomatic
team_stats["RunDiff"] = team_stats["RunsScored"] - team_stats["RunsAllowed"]
print(team_stats[["Team", "RunsScored", "RunsAllowed", "RunDiff"]])
Row-Wise .apply() cuando realmente lo necesitas
Úsalo .apply(axis=1)
cuando tu lógica realmente abarque varias columnas y no sea fácil de vectorizar (por ejemplo, reglas condicionales que dependan de varios campos).
calcular un valor derivado a partir de varias columnas
Este patrón calcula un valor por fila utilizando múltiples entradas. El enfoque vectorizado anterior sigue siendo el preferido, pero esto muestra el uso correcto por filas y las opciones que afectan a la velocidad y al resultado.
def compute_run_diff(row):
# Treat the row as read-only; return a scalar
return row["RunsScored"] - row["RunsAllowed"]
# Row-wise apply (Python-level loop; can be slow on large data)
team_stats["RunDiff_apply"] = team_stats.apply(compute_run_diff, axis=1)
Cuando la función es numérica y solo necesita arreglos sin procesar, pasar raw=True
omite parte de la sobrecarga de pandas al proporcionar un arreglo NumPy a tu función.
import numpy as np
def compute_run_diff_raw(values):
# values is a NumPy array when raw=True
# Order matches the column order we select
rs, ra = values
return rs - ra
team_stats["RunDiff_raw"] = team_stats[["RunsScored", "RunsAllowed"]].apply(
compute_run_diff_raw, axis=1, raw=True
)
Verifica la entrada de la función una vez.
Cuando conecto una nueva función de fila, imprimo una fila una vez para confirmar lo que se está pasando.
def debug_row(row):
# Print the first row only
if row.name == team_stats.index[0]:
print("Example row:", row.to_dict())
return 0
_ = team_stats.apply(debug_row, axis=1)
Caso práctico: Sumas, totales de temporada y marcadores de texto
Dadas las estadísticas de Rays por año, es preferible utilizar métodos integrados y herramientas elementales en lugar de .apply()
siempre que sea posible.
rays_by_year = pd.DataFrame(
{
"Year": [2012, 2011, 2010, 2009, 2008],
"RunsScored": [697, 707, 802, 803, 774],
"RunsAllowed": [577, 614, 649, 754, 671],
"Wins": [90, 91, 96, 84, 97],
"Playoffs": [0, 1, 1, 0, 1],
}
).set_index("Year")
obtener sumas de columnas de manera eficiente
Utiliza DataFrame.sum()
en lugar de .apply(sum)
. Es más rápido y más claro.
# Preferred
totals = rays_by_year.sum(axis=0)
print(totals)
# If you must use apply (not recommended here)
totals_apply = rays_by_year.apply(sum, axis=0)
Calcular el total de carreras anotadas en una temporada.
Utiliza aritmética vectorizada en todas las columnas; no .apply()
necesidad.
rays_by_year["TotalRuns"] = rays_by_year["RunsScored"] + rays_by_year["RunsAllowed"]
print(rays_by_year[["RunsScored", "RunsAllowed", "TotalRuns"]].head())
Convertir un indicador 0/1 en texto
Prefiero Series.map()
o replace()
para transformaciones elemento por elemento en una columna.
# Using map with a dict
rays_by_year["PlayoffsText"] = rays_by_year["Playoffs"].map({0: "No", 1: "Yes"})
# Equivalent with replace
rays_by_year["PlayoffsText2"] = rays_by_year["Playoffs"].replace({0: "No", 1: "Yes"})
Controla la forma de salida desde .apply()
Cuando tu función devuelva más de un valor por fila, haz que la forma sea explícita. Esto evita sorpresas y problemas con versiones futuras.
Devuelve una lista y la expande en columnas.
Usa result_type='expand'
para dividir una lista/arreglo en varias columnas.
def wins_losses(row):
# Derive wins and losses as two outputs
wins = row["Wins"]
losses = row["Games"] - row["Wins"] if "Games" in row else np.nan
return [wins, losses]
# Example using team_stats, which has "Wins" and "Games"
expanded = team_stats.apply(wins_losses, axis=1, result_type="expand")
expanded.columns = ["WinsOut", "LossesOut"]
team_stats = pd.concat([team_stats, expanded], axis=1)
Devuelve una serie o un diccionario para nombrar columnas automáticamente.
Devolver un Series
o dict
de cada fila produce un DataFrame con etiquetas de columna coincidentes.
def summary_row(row):
return pd.Series(
{
"IsWinningSeason": row["Wins"] >= 90,
"RunRatio": (row["RunsScored"] / row["RunsAllowed"]),
}
)
summary = team_stats.apply(summary_row, axis=1)
team_stats = pd.concat([team_stats, summary], axis=1)
Consejos importantes para mejorar el rendimiento
Por filas .apply(axis=1)
es conveniente, pero lento en marcos grandes, ya que llama a tu función Python una vez por fila. Estos patrones evitan ese cuello de botella.
- Es preferible utilizar operaciones vectorizadas de pandas/NumPy y métodos integrados como
sum
,mean
,clip
,where
,astype
y métodos de cadena/accesor.
- Para operaciones numéricas de fila/columna que aceptan arreglos, pasa
r
aw=True
para obtener un arreglo NumPy y reducir la sobrecarga de pandas.
- Para trabajar elemento por elemento en una sola columna, utiliza
Series.map()
o métodos vectorizados en lugar deDataFrame.apply(axis=1)
.
- Evita modificar la fila/columna proporcionada dentro de tu función; en su lugar, devuelve un nuevo valor.
Notas sobre la versión que debes conocer (pandas 2.x)
Las últimas versiones de pandas han restringido el comportamiento en torno a .apply() para que los resultados sean más predecibles.
- Resultados en forma de lista: En la versión 2.0+, las longitudes de lista que no coinciden generan un error. Utiliza longitudes uniformes y establece
result_type='expand'
cuando desees varias columnas.
- Ampliación de la producción: Al devolver un objeto de tipo
Series
odict
se expande en un DataFrame con esas claves como columnas (estable desde la versión 0.23).
Series.apply()
cambios: El argumentoconvert_dtype
está en desuso. Si necesitas tipos mixtos, primero convierte aobject
(por ejemplo,s.astype("object").apply(fn)
). Las firmas más recientes permiten controlar sifn
recibe escalares o una serie en más contextos (por ejemplo,by_row
).
.apply() vs. .map() vs. .applymap() vs. .agg()
Elige la API que se adapte a la forma de tu transformación.
Series.map(func_or_dict)
: Transformación por elementos en una columna. Ideal para búsquedas o funciones sencillas.
DataFrame.apply(func, axis=1)
: Lógica por filas que requiere varias columnas.
DataFrame.apply(func, axis=0)
: Lógica por columnas (cada entrada es una serie que representa una columna).
DataFrame.applymap(func)
: Por elementos sobre cada celda. Úsalo con moderación; los métodos vectorizados son más rápidos.
.agg()
/.transform()
: Agregaciones y transformaciones por grupos; preferirlos en canalizaciones groupby.
Errores comunes y soluciones rápidas
Estos son los problemas que con mayor frecuencia provocan resultados incorrectos o ralentizaciones, junto con las formas de corregirlos.
- Olvidando axis=1 para la lógica por fila: Si tu función recibe de repente columnas en lugar de filas, añade
axis=1.
- Serie inesperada de listas: Al devolver una lista/arreglo de cada fila, establece
result_type='expand'
para dividir en columnas.
- Código lento por filas: Reemplaza con expresiones vectorizadas o integradas; si no es posible, considera utilizar «
raw=True
» para reducir la sobrecarga.
- Mutación de la fila de entrada: Trata las entradas como de solo lectura y devuelve nuevos valores.
- Desviación del tipo de datos: Si tu función a veces devuelve valores no enteros, las columnas enteras pueden convertirse a flotantes u objetos. A continuación, moldea con
astype
si es necesario.
Conclusión
.apply()
Es una herramienta flexible, pero no es un atajo para mejorar el rendimiento. Utiliza operaciones vectorizadas para realizar operaciones aritméticas, agregaciones y transformaciones elemento por elemento en una sola columna. Utiliza un DataFrame.apply(axis=1)
o solo cuando tu lógica realmente necesite varias columnas por fila. Cuando lo utilices, controla la forma de salida con result_type
, ten en cuenta raw=True
para las funciones numéricas y presta atención a dtypes
. Estos patrones producen resultados predecibles en los pandas modernos y se adaptan mejor a medida que tus datos crecen.