Reemplazar valores en una serie de pandas a través del diccionario de manera eficiente

Cómo reemplazar valores en una serie de Pandas a través de un diccionario se ha preguntado y vuelto a preguntar muchas veces.

El método recomendado ( 1 , 2 , 3 , 4 ) es usar s.replace(d) o, ocasionalmente, usar s.map(d) si todos los valores de sus series se encuentran en las claves del diccionario.

Sin embargo, el rendimiento de s.replace suele ser excesivamente lento, a menudo 5-10 veces más lento que una simple lista de comprensión.

La alternativa, s.map(d) tiene un buen rendimiento, pero solo se recomienda cuando todas las claves se encuentran en el diccionario.

¿Por qué es tan lento en este lugar y cómo puede mejorarse el rendimiento?

 import pandas as pd, numpy as np df = pd.DataFrame({'A': np.random.randint(0, 1000, 1000000)}) lst = df['A'].values.tolist() ##### TEST 1 ##### d = {i: i+1 for i in range(1000)} %timeit df['A'].replace(d) # 1.98s %timeit [d[i] for i in lst] # 134ms ##### TEST 2 ##### d = {i: i+1 for i in range(10)} %timeit df['A'].replace(d) # 20.1ms %timeit [d.get(i, i) for i in lst] # 243ms 

Nota: esta pregunta no está marcada como un duplicado porque está buscando consejos específicos sobre cuándo usar diferentes métodos dados diferentes conjuntos de datos. Esto está explícito en la respuesta y es un aspecto que generalmente no se aborda en otras preguntas.

Una solución trivial es elegir un método que dependa de una estimación de cómo los valores están cubiertos por las claves del diccionario.

Caso general

  • Use df['A'].map(d) si todos los valores asignados; o
  • Utilice df['A'].map(d).fillna(df['A']).astype(int) si se asignan valores> 5%.

Pocos, ej. <5%, valores en d

  • Utilice df['A'].replace(d)

El “punto de cruce” de ~ 5% es específico de la evaluación comparativa a continuación.

Curiosamente, una simple lista de comprensión generalmente tiene un bajo desempeño en el map en cualquiera de los dos escenarios.

Benchmarking

 import pandas as pd, numpy as np df = pd.DataFrame({'A': np.random.randint(0, 1000, 1000000)}) lst = df['A'].values.tolist() ##### TEST 1 - Full Map ##### d = {i: i+1 for i in range(1000)} %timeit df['A'].replace(d) # 1.98s %timeit df['A'].map(d) # 84.3ms %timeit [d[i] for i in lst] # 134ms ##### TEST 2 - Partial Map ##### d = {i: i+1 for i in range(10)} %timeit df['A'].replace(d) # 20.1ms %timeit df['A'].map(d).fillna(df['A']).astype(int) # 111ms %timeit [d.get(i, i) for i in lst] # 243ms 

Explicación

La razón por la que este s.replace es tan lento es que hace mucho más que simplemente mapear un diccionario. Se trata de algunos casos de vanguardia y, posiblemente, de situaciones raras, que normalmente merecen más atención en cualquier caso.

Este es un extracto de replace() en pandas\generic.py .

 items = list(compat.iteritems(to_replace)) keys, values = zip(*items) are_mappings = [is_dict_like(v) for v in values] if any(are_mappings): # handling of nested dictionaries else: to_replace, value = keys, values return self.replace(to_replace, value, inplace=inplace, limit=limit, regex=regex) 

Parece que hay muchos pasos involucrados:

  • Convertir el diccionario a una lista.
  • Iterando a través de la lista y buscando diccionarios nesteds.
  • Alimentar un iterador de claves y valores en una función de reemplazo.

Esto se puede comparar con un código mucho más pandas\series.py de map() en pandas\series.py :

 if isinstance(arg, (dict, Series)): if isinstance(arg, dict): arg = self._constructor(arg, index=arg.keys()) indexer = arg.index.get_indexer(values) new_values = algos.take_1d(arg._values, indexer)