Remapear valores na coluna pandas com um dict

318

Eu tenho um dicionário que se parece com isso: di = {1: "A", 2: "B"}

Gostaria de aplicá-lo à coluna "col1" de um dataframe semelhante a:

     col1   col2
0       w      a
1       1      2
2       2    NaN

para obter:

     col1   col2
0       w      a
1       A      2
2       B    NaN

Como posso fazer isso da melhor maneira? Por alguma razão, os termos do Google relacionados a isso mostram apenas links sobre como criar colunas a partir de dictos e vice-versa: - /

TheChymera
fonte

Respostas:

341

Você pode usar .replace. Por exemplo:

>>> 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

ou diretamente no Series, ie df["col1"].replace(di, inplace=True).

DSM
fonte
1
Ele não funciona para mim quando se col```` is tuple. The error info is não pode comparar 'ndarray (dtipo = objeto)' tipos e 'tuple'```
Pengju Zhao
18
Parece que este não funciona mais em tudo , o que não é surpreendente dada a resposta foi a partir de 4 anos atrás. Esta questão precisa de uma nova resposta dada como em geral a operação é ...
PrestonH
2
@PrestonH Funciona perfeitamente para mim. Correndo:'3.6.1 |Anaconda custom (64-bit)| (default, May 11 2017, 13:25:24) [MSC v.1900 64 bit (AMD64)]'
Dan
Funciona para mim. Mas como se eu quiser substituir valores em TODAS as colunas?
precisa saber é
2
O único método que funcionou para mim nas respostas mostradas foi fazer uma substituição direta na série. Obrigado!
Dirigo
243

map pode ser muito mais rápido que replace

Se o seu dicionário tiver mais do que algumas teclas, o uso mappoderá ser muito mais rápido que replace. Existem duas versões dessa abordagem, dependendo se o seu dicionário mapeia exaustivamente todos os valores possíveis (e também se você deseja que as não correspondências mantenham seus valores ou sejam convertidas em NaNs):

Mapeamento exaustivo

Nesse caso, o formulário é muito simples:

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

Embora mapo argumento mais comum seja uma função, ela pode usar um dicionário ou uma série: Documentation for Pandas.series.map

Mapeamento não exaustivo

Se você possui um mapeamento não exaustivo e deseja manter as variáveis ​​existentes para não correspondências, você pode adicionar fillna:

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

Como na resposta da @ jpp aqui: Substitua valores em uma série de pandas via dicionário de maneira eficiente

Benchmarks

Usando os seguintes dados com o pandas versão 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 ) })

e testando com %timeit, parece que mapé aproximadamente 10x mais rápido que replace.

Observe que sua aceleração mapvaria de acordo com seus dados. A maior aceleração parece ser com dicionários grandes e substituições exaustivas. Veja @jpp answer (link acima) para benchmarks e discussões mais abrangentes.

JohnE
fonte
17
O último bloco de código para essa resposta certamente não é o mais elegante, mas essa resposta merece algum crédito. É ordens de magnitude mais rápidas para dicionários grandes e não consome toda a minha RAM. Ele remapeou um arquivo de 10.000 linhas usando um dicionário que tinha cerca de 9 milhões de entradas em meio minuto. A df.replacefunção, embora arrumada e útil para dictos pequenos, falhou após ser executada por 20 minutos ou mais.
Griffinc
@griffinc Obrigado pelo feedback e nota que eu tenho desde que atualizou esta resposta com uma maneira muito mais simples de fazer caso não exaustiva (graças a @jpp)
Johne
1
maptambém funciona em um índice onde eu não conseguia descobrir uma maneira de fazer isso com #replace
Max Ghenis 15/03/19
1
@AlexSB Não posso dar uma resposta completamente geral, mas acho que o mapa seria muito mais rápido e realizaria (acho) a mesma coisa. Geralmente, a mesclagem será mais lenta que outras opções que fazem a mesma coisa.
JohnE
59

Há um pouco de ambiguidade na sua pergunta. Há pelo menos três duas interpretações:

  1. as chaves direferem-se aos valores do índice
  2. as chaves direferem-se a df['col1']valores
  3. as chaves direferem-se aos locais do índice (não a pergunta do OP, mas lançada por diversão).

Abaixo está uma solução para cada caso.


Caso 1: se as chaves de dise referirem aos valores do índice, você pode usar o updatemétodo:

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

Por exemplo,

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)

rendimentos

  col1 col2
1    w    a
2    B   30
0    A  NaN

Eu modifiquei os valores da sua postagem original para ficar mais claro o que updateestá fazendo. Observe como as chaves diestão associadas aos valores do índice. A ordem dos valores do índice - ou seja, os locais do índice - não importa.


Caso 2: se as chaves se direferirem a df['col1']valores, @DanAllan e @DSM mostram como conseguir isso com 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)

rendimentos

  col1 col2
1    w    a
2    A   30
0    B  NaN

Note como neste caso, as chaves no diforam alterados para coincidir com valores em df['col1'].


Caso 3: se as chaves se direferirem aos locais do índice, você pode usar

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

Desde a

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)

rendimentos

  col1 col2
1    A    a
2   10   30
0    B  NaN

Aqui, a primeira e a terceira linhas foram alteradas, porque as chaves disão são 0e 2, que com a indexação baseada em 0 do Python, se referem aos primeiro e terceiro locais.

unutbu
fonte
replaceé igualmente bom e talvez uma palavra melhor para o que está acontecendo aqui.
Dan Allan
O dataframe de destino postado pelo OP não elimina a ambiguidade? Ainda assim, essa resposta é útil, então +1.
DSM
@DSM: Opa, você está certo, não há possibilidade de Case3, mas não acho que o dataframe de destino do OP distingue Case1 de Case2, pois os valores de índice são iguais aos valores da coluna.
unutbu
Como vários outros postados, o método do @ DSM infelizmente não funcionou para mim, mas o caso 1 do @ unutbu funcionou. update()parece um pouco arrogante em comparação com replace(), mas pelo menos funciona.
Geoff
4

Adicionando a esta pergunta se você tiver mais de uma coluna para remapear em um dataframe de dados:

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 possa ser útil para alguém.

Felicidades

Nico Coallier
fonte
1
Essa funcionalidade já é fornecida por DataFrame.replace(), embora eu não saiba quando foi adicionada.
AMC
3

O DSM tem a resposta aceita, mas a codificação parece não funcionar para todos. Aqui está um que funciona com a versão atual do 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())

Você verá que se parece com:

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

Os documentos para pandas.DataFrame.replace estão aqui .

wordsforthewise
fonte
Eu nunca tive um problema em conseguir a resposta do DSM e acho que, devido ao grande número de votos, a maioria das outras pessoas também não. Você pode querer ser mais específico sobre o problema que está tendo. Talvez isso tenha a ver com seus dados de amostra, que são diferentes dos DSMs?
Johne
Hmm, talvez um problema de versão. No entanto, ambas as respostas estão aqui agora.
wordsforthewise
1
A solução na resposta aceita funciona apenas em certos tipos, Series.map()parece mais flexível.
AMC
2

Ou faça apply:

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

Demo:

>>> 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
>>> 
Sub-10
fonte
O que acontece quando o seu diditado é um ditado de listas? Como você pode mapear apenas um valor na lista?
FaCoffee 12/02/19
Você pode, embora eu não veja por que você faria.
AMC
2

Dado que mapé mais rápido que substituir (solução da @ JohnE), você precisa ter cuidado com os mapeamentos não exaustivos para os quais pretende mapear valores específicosNaN . O método apropriado, neste caso, requer que você maska série quando você .fillna, senão você desfaz o mapeamento 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
fonte
1

Uma boa solução completa que mantém um mapa dos rótulos das suas turmas:

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

Dessa forma, você pode, a qualquer momento, consultar o rótulo da classe original em labels_dict.

dorien
fonte
1

Como uma extensão do que foi proposto por Nico Coallier (aplicar a várias colunas) e U10-Forward (usando o estilo de métodos de aplicação), e resumindo-o em uma linha, proponho:

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

Os .transform()processos de cada coluna como uma série. Ao contrário do .apply()que passa as colunas agregadas em um DataFrame.

Conseqüentemente, você pode aplicar o método Series map().

Finalmente, e eu descobri esse comportamento graças ao U10, você pode usar toda a série na expressão .get (). A menos que eu tenha entendido mal o seu comportamento e ele processe seqüencialmente a série, em vez de de maneira pouco inteligente.
As .get(x,x)contas dos valores que você não mencionou no seu dicionário de mapeamento, que seriam consideradas Nan de outra forma pelo .map()método

louisD
fonte
Os .transform()processos de cada coluna como uma série. Ao contrário do .apply()que passa as colunas agregadas em um DataFrame. Eu apenas tentei, apply()funciona bem. Também não há necessidade de usar loc, isso parece muito complexo. df[["col1", "col2"]].apply(lambda col: col.map(lambda elem: my_dict.get(elem, elem)))deve funcionar muito bem. As .get(x,x)contas dos valores que você não mencionou no seu dicionário de mapeamento, que seriam consideradas Nan de outra forma pelo .map()método Você também pode usar fillna()posteriormente.
AMC
Finalmente, e eu descobri esse comportamento graças ao U10, você pode usar toda a série na expressão .get (). A menos que eu tenha entendido mal o seu comportamento e ele processe seqüencialmente a série, em vez de de maneira pouco inteligente. Eu não posso reproduzir isso, você pode elaborar? As variáveis ​​nomeadas identicamente provavelmente estão desempenhando algum papel aqui.
AMC
0

Uma abordagem mais nativa dos pandas é aplicar uma função de substituição como abaixo:

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) 

Depois de definir a função, você pode aplicá-la ao seu quadro de dados.

di = {1: "A", 2: "B"}
df['col1'] = df.apply(lambda row: multiple_replace(di, row['col1']), axis=1)
Amir Imani
fonte
Uma abordagem mais nativa dos pandas é aplicar uma função de substituição, conforme abaixo. Como isso é mais "nativo" (idiomático?) Do que os métodos muito mais simples fornecidos pelo Pandas?
AMC