Remapper les valeurs dans la colonne pandas avec un dict

318

J'ai un dictionnaire qui ressemble à ceci: di = {1: "A", 2: "B"}

Je voudrais l'appliquer à la colonne "col1" d'une trame de données similaire à:

     col1   col2
0       w      a
1       1      2
2       2    NaN

obtenir:

     col1   col2
0       w      a
1       A      2
2       B    NaN

Comment puis-je faire cela au mieux? Pour une raison quelconque, les termes de recherche sur Google à ce sujet ne me montrent que des liens sur la façon de créer des colonnes à partir de dict et vice-versa: - /

TheChymera
la source

Réponses:

342

Vous pouvez utiliser .replace. Par exemple:

>>> 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 directement sur le Series, à savoir df["col1"].replace(di, inplace=True).

DSM
la source
1
Cela ne fonctionne pas pour moi si, si col```` is tuple. The error info is je ne peux pas comparer les types 'ndarray (dtype = object)' et 'tuple'''`
Pengju Zhao
18
Il semble que cela ne fonctionne plus du tout , ce qui n'est pas surprenant étant donné que la réponse remonte à 4 ans. Cette question a besoin d'une nouvelle réponse étant donné la généralité de l'opération ...
PrestonH
2
@PrestonH Cela fonctionne parfaitement pour moi. En cours d'exécution:'3.6.1 |Anaconda custom (64-bit)| (default, May 11 2017, 13:25:24) [MSC v.1900 64 bit (AMD64)]'
Dan
Ça marche pour moi. Mais comment faire pour remplacer des valeurs dans TOUTES les colonnes?
famargar
2
La seule méthode qui a fonctionné pour moi des réponses montrées était de faire un remplacement direct sur la série. Merci!
Dirigo
243

map peut être beaucoup plus rapide que replace

Si votre dictionnaire comporte plusieurs clés, son utilisation mappeut être beaucoup plus rapide que replace. Il existe deux versions de cette approche, selon que votre dictionnaire mappe de manière exhaustive toutes les valeurs possibles (et également si vous souhaitez que les non-correspondances conservent leurs valeurs ou soient converties en NaN):

Cartographie exhaustive

Dans ce cas, le formulaire est très simple:

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

Bien que map plupart du temps utilise une fonction comme argument, elle peut également prendre un dictionnaire ou une série: Documentation for Pandas.series.map

Cartographie non exhaustive

Si vous disposez d'un mappage non exhaustif et souhaitez conserver les variables existantes pour les non-correspondances, vous pouvez ajouter fillna:

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

comme dans la réponse de @ jpp ici: remplacer efficacement les valeurs d'une série de pandas via le dictionnaire

Repères

Utilisation des données suivantes avec pandas version 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 ) })

et les tests avec %timeit, il semble que mapsoit environ 10 fois plus rapide quereplace .

Notez que votre accélération avec mapvariera avec vos données. La plus grande accélération semble être avec de gros dictionnaires et des remplacements exhaustifs. Voir la réponse @jpp (liée ci-dessus) pour des références et une discussion plus approfondies.

JohnE
la source
17
Le dernier bloc de code pour cette réponse n'est certainement pas le plus élégant, mais cette réponse mérite un certain crédit. C'est des ordres de grandeur plus rapides pour les grands dictionnaires et n'utilise pas toute ma RAM. Il a remappé un fichier de 10 000 lignes à l'aide d'un dictionnaire qui avait environ 9 millions d'entrées en une demi-minute. La df.replacefonction, bien que bien rangée et utile pour les petits dict, s'est écrasée après avoir fonctionné pendant environ 20 minutes.
griffinc
@griffinc Merci pour les commentaires et notez que j'ai depuis mis à jour cette réponse avec une manière beaucoup plus simple de faire le cas non exhaustif (merci à @jpp)
JohnE
1
maptravaille également sur un index où je ne pouvais pas trouver un moyen de le faire avecreplace
Max Ghenis
1
@AlexSB Je ne peux pas donner une réponse complètement générale, mais je pense que la carte serait beaucoup plus rapide et accomplirait (je pense) la même chose. En règle générale, la fusion sera plus lente que les autres options qui font la même chose.
JohnE
59

Il y a un peu d'ambiguïté dans votre question. Il existe au moins trois deux interprétations:

  1. les clés di réfèrent aux valeurs d'index
  2. les touches se diréfèrent àdf['col1'] valeurs
  3. les clés se diréfèrent aux emplacements d'index (pas la question de l'OP, mais jetées pour le plaisir.)

Voici une solution pour chaque cas.


Cas 1: Si les clés de disont destinées à faire référence à des valeurs d'index, vous pouvez alors utiliser la updateméthode:

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

Par exemple,

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)

les rendements

  col1 col2
1    w    a
2    B   30
0    A  NaN

J'ai modifié les valeurs de votre message d'origine pour que ce soit plus clair update. Notez comment les clés disont associées aux valeurs d'index. L'ordre des valeurs d'index - c'est-à-dire les emplacements d' index - n'a pas d'importance.


Cas 2: si les clés dans difont référence à des df['col1']valeurs, alors @DanAllan et @DSM montrent comment y parvenir avec 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)

les rendements

  col1 col2
1    w    a
2    A   30
0    B  NaN

Notez comment, dans ce cas, les clés diont été modifiées pour correspondre aux valeurs de df['col1'].


Cas 3: si les clés se diréfèrent à des emplacements d'index, vous pouvez utiliser

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

puisque

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)

les rendements

  col1 col2
1    A    a
2   10   30
0    B  NaN

Ici, les première et troisième lignes ont été modifiées, car les clés dans disont 0et 2qui, avec l'indexation basée sur 0 de Python, font référence aux premier et troisième emplacements.

unutbu
la source
replaceest tout aussi bon, et peut-être un meilleur mot pour ce qui se passe ici.
Dan Allan
La trame de données cible publiée par le PO n'élimine-t-elle pas l'ambiguïté? Pourtant, cette réponse est utile, donc +1.
DSM
@DSM: Oups, vous avez raison, il n'y a aucune possibilité de Case3, mais je ne pense pas que la trame de données cible de l'OP distingue Case1 de Case2 car les valeurs d'index sont égales aux valeurs de colonne.
unutbu
Comme un certain nombre d'autres personnes, la méthode de @ DSM n'a malheureusement pas fonctionné pour moi, mais le cas 1 de @ unutbu a fonctionné. update()semble un peu maladroit par rapport à replace(), mais au moins ça marche.
Geoff
4

Ajout à cette question si vous avez déjà plusieurs colonnes à remapper dans une trame de données:

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

J'espère que cela peut être utile à quelqu'un.

À votre santé

Nico Coallier
la source
1
Cette fonctionnalité est déjà fournie par DataFrame.replace(), bien que je ne sache pas quand elle a été ajoutée.
AMC
3

DSM a la réponse acceptée, mais le codage ne semble pas fonctionner pour tout le monde. En voici un qui fonctionne avec la version actuelle des pandas (0.23.4 au 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())

Vous verrez que cela ressemble à:

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

Les documents pour pandas.DataFrame.replace sont ici .

mots dans le sens
la source
Je n'ai jamais eu de problème pour que la réponse de DSM s'exécute et je suppose que compte tenu du nombre élevé de votes, la plupart des autres personnes non plus. Vous voudrez peut-être être plus précis sur le problème que vous rencontrez. Peut-être que cela a à voir avec vos données d'échantillonnage qui sont différentes de celles de DSM?
JohnE
Hmm, peut-être un problème de version. Néanmoins, les deux réponses sont ici maintenant.
wordsforhewise
1
La solution dans la réponse acceptée ne fonctionne que sur certains types, Series.map()semble plus flexible.
AMC
2

Ou faites apply:

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

Démo:

>>> 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-Forward
la source
Que se passe-t-il lorsque votre didict est un dict de listes? Comment pouvez-vous mapper une seule valeur dans la liste?
FaCoffee
Vous pouvez, bien que je ne vois pas pourquoi vous le feriez.
AMC
2

Étant donné que mapc'est plus rapide que de remplacer (la solution de @ JohnE), vous devez être prudent avec les mappages non exhaustifs où vous avez l'intention de mapper des valeurs spécifiquesNaN . La méthode appropriée dans ce cas nécessite que vous maskla série lorsque vous .fillna, sinon vous annulez le mappage 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
la source
1

Une belle solution complète qui conserve une carte de vos étiquettes de classe:

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

De cette façon, vous pouvez à tout moment vous référer à l'étiquette de classe d'origine de labels_dict.

dorien
la source
1

Dans le prolongement de ce qui a été proposé par Nico Coallier (appliquer à plusieurs colonnes) et U10-Forward (en utilisant le style de méthodes d'application), et en le résumant en une ligne, je propose:

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

Le .transform()traite chaque colonne comme une série. Contrairement à .apply()ce qui passe les colonnes agrégées dans un DataFrame.

Par conséquent, vous pouvez appliquer la méthode Series map().

Enfin, et j'ai découvert ce comportement grâce à U10, vous pouvez utiliser toute la série dans l'expression .get (). À moins que j'aie mal compris son comportement et qu'il traite séquentiellement la série au lieu de le faire avec sagesse.
Les .get(x,x)comptes pour les valeurs que vous n'avez pas mentionnées dans votre dictionnaire de mappage qui seraient considérées comme Nan sinon par la .map()méthode

louisD
la source
Le .transform()traite chaque colonne comme une série. Contrairement à .apply()ce qui passe les colonnes agrégées dans un DataFrame. J'ai juste essayé, ça apply()marche bien. Il n'est pas nécessaire d'utiliser locnon plus, cela semble trop complexe. df[["col1", "col2"]].apply(lambda col: col.map(lambda elem: my_dict.get(elem, elem)))devrait fonctionner très bien. Les .get(x,x)comptes pour les valeurs que vous n'avez pas mentionnées dans votre dictionnaire de mappage qui seraient considérées comme Nan sinon par la .map()méthode que vous pourriez également utiliser fillna()par la suite.
AMC
Enfin, et j'ai découvert ce comportement grâce à U10, vous pouvez utiliser toute la série dans l'expression .get (). À moins que j'aie mal compris son comportement et qu'il traite séquentiellement la série au lieu de le faire avec sagesse. Je ne peux pas reproduire cela, pouvez-vous élaborer? Les variables portant le même nom jouent probablement un rôle ici.
AMC
0

Une approche plus native des pandas consiste à appliquer une fonction de remplacement comme ci-dessous:

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) 

Une fois que vous avez défini la fonction, vous pouvez l'appliquer à votre trame de données.

di = {1: "A", 2: "B"}
df['col1'] = df.apply(lambda row: multiple_replace(di, row['col1']), axis=1)
Amir Imani
la source
Une approche plus native des pandas consiste à appliquer une fonction de remplacement comme ci-dessous. Comment est-ce plus "natif" (idiomatique?) Que les méthodes beaucoup plus simples fournies par Pandas?
AMC