Reasignar valores en la columna de pandas con un dict

318

Tengo un diccionario que se ve así: di = {1: "A", 2: "B"}

Me gustaría aplicarlo a la columna "col1" de un marco de datos similar a:

     col1   col2
0       w      a
1       1      2
2       2    NaN

Llegar:

     col1   col2
0       w      a
1       A      2
2       B    NaN

¿Cómo puedo hacer esto mejor? Por alguna razón, los términos de Google relacionados con esto solo me muestran enlaces sobre cómo hacer columnas a partir de dictados y viceversa: - /

TheChymera
fuente

Respuestas:

342

Puedes usar .replace. Por ejemplo:

>>> df = pd.DataFrame({'col2': {0: 'a', 1: 2, 2: np.nan}, 'col1': {0: 'w', 1: 1, 2: 2}})
>>> di = {1: "A", 2: "B"}
>>> df
  col1 col2
0    w    a
1    1    2
2    2  NaN
>>> df.replace({"col1": di})
  col1 col2
0    w    a
1    A    2
2    B  NaN

o directamente en el Series, es decir df["col1"].replace(di, inplace=True).

DSM
fuente
1
No funciona para mí cuando col```` is tuple. The error info is no puedo comparar los tipos 'ndarray (dtype = object)' y 'tuple' '' '
Pengju Zhao
18
Parece que esto ya no funciona en absoluto , lo cual no es sorprendente dado que la respuesta fue de hace 4 años. Esta pregunta necesita una nueva respuesta dado lo general que es la operación ...
PrestonH
2
@PrestonH Funciona perfectamente para mí. Correr:'3.6.1 |Anaconda custom (64-bit)| (default, May 11 2017, 13:25:24) [MSC v.1900 64 bit (AMD64)]'
Dan
Esto funciona para mi. Pero, ¿cómo si quiero reemplazar los valores en TODAS las columnas?
famargar
2
El único método que funcionó para mí de las respuestas mostradas fue hacer un reemplazo directo en la Serie. ¡Gracias!
Dirigo
243

map puede ser mucho más rápido que replace

Si su diccionario tiene más de un par de claves, el uso mappuede ser mucho más rápido que replace. Hay dos versiones de este enfoque, dependiendo de si su diccionario mapea exhaustivamente todos los valores posibles (y también si desea que las no coincidencias conserven sus valores o se conviertan en NaN):

Mapeo exhaustivo

En este caso, el formulario es muy simple:

df['col1'].map(di)       # note: if the dictionary does not exhaustively map all
                         # entries then non-matched entries are changed to NaNs

Aunque mapmás comúnmente toma una función como argumento, también puede tomar un diccionario o una serie: Documentación para Pandas.series.map

Mapeo no exhaustivo

Si tiene un mapeo no exhaustivo y desea retener las variables existentes para no coincidencias, puede agregar fillna:

df['col1'].map(di).fillna(df['col1'])

como en la respuesta de @ jpp aquí: Reemplazar valores en una serie de pandas a través del diccionario de manera eficiente

Puntos de referencia

Usando los siguientes datos con pandas versión 0.23.1:

di = {1: "A", 2: "B", 3: "C", 4: "D", 5: "E", 6: "F", 7: "G", 8: "H" }
df = pd.DataFrame({ 'col1': np.random.choice( range(1,9), 100000 ) })

y probando con %timeit, parece que mapes aproximadamente 10 veces más rápido que replace.

Tenga en cuenta que su aceleración mapvariará con sus datos. La mayor aceleración parece ser con grandes diccionarios y reemplazos exhaustivos. Consulte la respuesta @jpp (vinculada anteriormente) para obtener puntos de referencia y debates más extensos.

JohnE
fuente
17
El último bloque de código para esta respuesta ciertamente no es el más elegante, pero esta respuesta merece algo de crédito. Es un orden de magnitud más rápido para diccionarios grandes y no usa toda mi RAM. Reasignó un archivo de 10,000 líneas usando un diccionario que tenía aproximadamente 9 millones de entradas en medio minuto. La df.replacefunción, aunque ordenada y útil para pequeños dictados, se bloqueó después de ejecutarse durante aproximadamente 20 minutos.
griffinc
@griffinc Gracias por los comentarios y nota que desde entonces he actualizado esta respuesta con una forma mucho más simple de hacer el caso no exhaustivo (gracias a @jpp)
JohnE
1
maptambién funciona en un índice en el que no pude encontrar una manera de hacerloreplace
Max Ghenis
1
@AlexSB No puedo dar una respuesta completamente general, pero creo que map sería mucho más rápido y lograría (creo) lo mismo. En general, la fusión será más lenta que otras opciones que hacen lo mismo.
JohnE
59

Hay un poco de ambigüedad en su pregunta. Hay al menos tres interpretaciones:

  1. las claves se direfieren a valores de índice
  2. las claves se direfieren a df['col1']valores
  3. las claves se direfieren a ubicaciones de índice (no es la pregunta del OP, pero son divertidas).

A continuación hay una solución para cada caso.


Caso 1: Si las claves de diestán destinadas a hacer referencia a valores de índice, entonces podría usar el updatemétodo:

df['col1'].update(pd.Series(di))

Por ejemplo,

import pandas as pd
import numpy as np

df = pd.DataFrame({'col1':['w', 10, 20],
                   'col2': ['a', 30, np.nan]},
                  index=[1,2,0])
#   col1 col2
# 1    w    a
# 2   10   30
# 0   20  NaN

di = {0: "A", 2: "B"}

# The value at the 0-index is mapped to 'A', the value at the 2-index is mapped to 'B'
df['col1'].update(pd.Series(di))
print(df)

rendimientos

  col1 col2
1    w    a
2    B   30
0    A  NaN

He modificado los valores de tu publicación original para que quede más claro lo que updateestá haciendo. Observe cómo las claves dise asocian con valores de índice. El orden de los valores del índice, es decir, las ubicaciones del índice , no importa.


Caso 2: Si las claves se direfieren a df['col1']valores, entonces @DanAllan y @DSM muestran cómo lograr esto con replace:

import pandas as pd
import numpy as np

df = pd.DataFrame({'col1':['w', 10, 20],
                   'col2': ['a', 30, np.nan]},
                  index=[1,2,0])
print(df)
#   col1 col2
# 1    w    a
# 2   10   30
# 0   20  NaN

di = {10: "A", 20: "B"}

# The values 10 and 20 are replaced by 'A' and 'B'
df['col1'].replace(di, inplace=True)
print(df)

rendimientos

  col1 col2
1    w    a
2    A   30
0    B  NaN

Tenga en cuenta cómo, en este caso, dise modificaron las claves para que coincidan con los valores de df['col1'].


Caso 3: si las claves se direfieren a ubicaciones de índice, entonces podría usar

df['col1'].put(di.keys(), di.values())

ya que

df = pd.DataFrame({'col1':['w', 10, 20],
                   'col2': ['a', 30, np.nan]},
                  index=[1,2,0])
di = {0: "A", 2: "B"}

# The values at the 0 and 2 index locations are replaced by 'A' and 'B'
df['col1'].put(di.keys(), di.values())
print(df)

rendimientos

  col1 col2
1    A    a
2   10   30
0    B  NaN

Aquí, la primera y la tercera fila fueron alteradas, porque las claves en dison 0y 2, que con la indexación basada en 0 de Python se refieren a las ubicaciones primera y tercera.

unutbu
fuente
replacees igualmente bueno, y tal vez una mejor palabra para lo que está sucediendo aquí.
Dan Allan
¿El marco de datos objetivo publicado del OP no elimina la ambigüedad? Aún así, esta respuesta es útil, entonces +1.
DSM
@DSM: Vaya, tienes razón, no hay posibilidad de Case3, pero no creo que el marco de datos de destino del OP distinga Case1 de Case2 ya que los valores de índice son iguales a los valores de columna.
unutbu
Al igual que muchos otros publicados, el método de @ DSM desafortunadamente no funcionó para mí, pero el caso 1 de @ unutbu sí funcionó. update()parece un poco torpe en comparación con replace(), pero al menos funciona.
Geoff
4

Agregando a esta pregunta si alguna vez tiene más de una columna para reasignar en un marco de datos:

def remap(data,dict_labels):
    """
    This function take in a dictionnary of labels : dict_labels 
    and replace the values (previously labelencode) into the string.

    ex: dict_labels = {{'col1':{1:'A',2:'B'}}

    """
    for field,values in dict_labels.items():
        print("I am remapping %s"%field)
        data.replace({field:values},inplace=True)
    print("DONE")

    return data

Espero que pueda ser útil para alguien.

Salud

Nico Coallier
fuente
1
Esta funcionalidad ya la proporciona DataFrame.replace(), aunque no sé cuándo se agregó.
AMC
3

DSM tiene la respuesta aceptada, pero la codificación no parece funcionar para todos. Aquí hay uno que funciona con la versión actual de pandas (0.23.4 a partir de 8/2018):

import pandas as pd

df = pd.DataFrame({'col1': [1, 2, 2, 3, 1],
            'col2': ['negative', 'positive', 'neutral', 'neutral', 'positive']})

conversion_dict = {'negative': -1, 'neutral': 0, 'positive': 1}
df['converted_column'] = df['col2'].replace(conversion_dict)

print(df.head())

Verás que se ve así:

   col1      col2  converted_column
0     1  negative                -1
1     2  positive                 1
2     2   neutral                 0
3     3   neutral                 0
4     1  positive                 1

Los documentos para pandas.DataFrame.replace están aquí .

palabras por el contrario
fuente
Nunca tuve problemas para obtener la respuesta de DSM y supongo que dado el alto voto total, la mayoría de las otras personas tampoco. Es posible que desee ser más específico sobre el problema que tiene. ¿Quizás tenga que ver con sus datos de muestra que son diferentes a los de DSM?
JohnE
Hmm, quizás un problema de versiones. Sin embargo, ambas respuestas están aquí ahora.
wordsforthewise
1
La solución en la respuesta aceptada solo funciona en ciertos tipos, Series.map()parece más flexible.
AMC
2

O hacer apply:

df['col1'].apply(lambda x: {1: "A", 2: "B"}.get(x,x))

Manifestación:

>>> df['col1']=df['col1'].apply(lambda x: {1: "A", 2: "B"}.get(x,x))
>>> df
  col1 col2
0    w    a
1    1    2
2    2  NaN
>>> 
U10-Adelante
fuente
¿Qué sucede cuando tu didict es un dict de listas? ¿Cómo puede asignar solo un valor en la lista?
FaCoffee
Puedes, aunque no entiendo por qué lo harías.
AMC
2

Dado que mapes más rápido que reemplazar (solución de @ JohnE), debe tener cuidado con las asignaciones no exhaustivas en las que pretende asignar valores específicosNaN . El método adecuado en este caso requiere que usted sea maskla Serie cuando usted .fillna, de lo contrario, deshaga la asignación NaN.

import pandas as pd
import numpy as np

d = {'m': 'Male', 'f': 'Female', 'missing': np.NaN}
df = pd.DataFrame({'gender': ['m', 'f', 'missing', 'Male', 'U']})

keep_nan = [k for k,v in d.items() if pd.isnull(v)]
s = df['gender']

df['mapped'] = s.map(d).fillna(s.mask(s.isin(keep_nan)))

    gender  mapped
0        m    Male
1        f  Female
2  missing     NaN
3     Male    Male
4        U       U
ALollz
fuente
1

Una buena solución completa que mantiene un mapa de las etiquetas de su clase:

labels = features['col1'].unique()
labels_dict = dict(zip(labels, range(len(labels))))
features = features.replace({"col1": labels_dict})

De esta manera, puede referirse en cualquier momento a la etiqueta de clase original de labels_dict.

dorien
fuente
1

Como una extensión de lo que ha propuesto Nico Coallier (se aplica a varias columnas) y U10-Forward (utilizando el estilo de aplicación de métodos), y resumiendo en una línea, propongo:

df.loc[:,['col1','col2']].transform(lambda x: x.map(lambda x: {1: "A", 2: "B"}.get(x,x))

El .transform()procesa cada columna como una serie. Al contrario de lo .apply()que pasa las columnas agregadas en un DataFrame.

En consecuencia, puede aplicar el método de la serie map().

Finalmente, y descubrí este comportamiento gracias a U10, puedes usar toda la serie en la expresión .get (). A menos que haya malinterpretado su comportamiento y procese secuencialmente la serie en lugar de ser torpe.
Las .get(x,x)cuentas de los valores que no mencionó en su diccionario de mapeo que de otra manera se considerarían como Nan por el .map()método

louisD
fuente
El .transform()procesa cada columna como una serie. Al contrario de lo .apply()que pasa las columnas agregadas en un DataFrame. Acabo de intentarlo, apply()funciona bien. No hay necesidad de usar loctampoco, esto parece demasiado complejo. df[["col1", "col2"]].apply(lambda col: col.map(lambda elem: my_dict.get(elem, elem)))debería funcionar bien. Las .get(x,x)cuentas de los valores que no mencionó en su diccionario de mapeo que, de lo contrario, se considerarían Nan por el .map()método que también podría usar fillna()después.
AMC
Finalmente, y descubrí este comportamiento gracias a U10, puedes usar toda la serie en la expresión .get (). A menos que haya malinterpretado su comportamiento y procese secuencialmente la serie en lugar de hacerlo con desdén. No puedo reproducir esto, ¿puedes explicarlo? Las variables con nombres idénticos probablemente juegan algún papel aquí.
AMC
0

Un enfoque de pandas más nativo es aplicar una función de reemplazo como se muestra a continuación:

def multiple_replace(dict, text):
  # Create a regular expression  from the dictionary keys
  regex = re.compile("(%s)" % "|".join(map(re.escape, dict.keys())))

  # For each match, look-up corresponding value in dictionary
  return regex.sub(lambda mo: dict[mo.string[mo.start():mo.end()]], text) 

Una vez que haya definido la función, puede aplicarla a su marco de datos.

di = {1: "A", 2: "B"}
df['col1'] = df.apply(lambda row: multiple_replace(di, row['col1']), axis=1)
Amir Imani
fuente
Un enfoque de pandas más nativo es aplicar una función de reemplazo como la siguiente ¿Cómo es eso más "nativo" (idiomático?) Que los métodos mucho más simples proporcionados por Pandas?
AMC