Saltar al contenido principal

Pandas .apply(): Qué hace, cuándo ayuda y alternativas más rápidas

Aprende qué es Python pandas .apply y cómo utilizarlo para DataFrame. ¡Aprende hoy mismo a iterar sobre DataFrames utilizando la función .apply()!
· 5 min leer

Explora con IA

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 un Series o un dict, pandas expande esas claves en un DataFrame. Si devuelve una lista/arreglo, obtendrás una serie de listas a menos que establezcas result_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, astypey métodos de cadena/accesor.
  • Para operaciones numéricas de fila/columna que aceptan arreglos, pasa raw=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 de DataFrame.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 o dict se expande en un DataFrame con esas claves como columnas (estable desde la versión 0.23).
  • Series.apply() cambios: El argumento convert_dtype está en desuso. Si necesitas tipos mixtos, primero convierte a object (por ejemplo, s.astype("object").apply(fn)). Las firmas más recientes permiten controlar si fn 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.

Temas

Relacionado

data-frames-in-python-banner_cgzjxy.jpeg

Tutorial

Tutorial de Pandas: DataFrames en Python

Tutorial

Tutorial seleccionar columnas con Python

Tutorial

pandas read_csv() Tutorial: Importar datos