Changer une valeur en fonction d'une autre valeur dans les pandas

109

J'essaie de reprogrammer mon code Stata en Python pour des améliorations de vitesse, et j'ai été dirigé vers PANDAS. Cependant, j'ai du mal à comprendre comment traiter les données.

Disons que je souhaite parcourir toutes les valeurs de l'en-tête de colonne «ID». Si cet identifiant correspond à un nombre spécifique, je souhaite modifier deux valeurs correspondantes FirstName et LastName.

Dans Stata, cela ressemble à ceci:

replace FirstName = "Matt" if ID==103
replace LastName =  "Jones" if ID==103

Cela remplace donc toutes les valeurs de FirstName qui correspondent aux valeurs de ID == 103 à Matt.

Dans PANDAS, j'essaye quelque chose comme ça

df = read_csv("test.csv")
for i in df['ID']:
    if i ==103:
          ...

Je ne sais pas où aller d'ici. Des idées?

Fourchelangue
la source

Réponses:

183

Une option consiste à utiliser les fonctionnalités de découpage et d'indexation de Python pour évaluer logiquement les endroits où votre condition est maintenue et y écraser les données.

En supposant que vous puissiez charger vos données directement dans pandasavec, pandas.read_csvle code suivant pourrait vous être utile.

import pandas
df = pandas.read_csv("test.csv")
df.loc[df.ID == 103, 'FirstName'] = "Matt"
df.loc[df.ID == 103, 'LastName'] = "Jones"

Comme mentionné dans les commentaires, vous pouvez également effectuer l'affectation aux deux colonnes en une seule fois:

df.loc[df.ID == 103, ['FirstName', 'LastName']] = 'Matt', 'Jones'

Notez que vous aurez besoin de la pandasversion 0.11 ou plus récente pour utiliser locpour les opérations d'assignation d'écrasement.


Une autre façon de procéder consiste à utiliser ce qu'on appelle l'affectation chaînée. Le comportement de celui-ci est moins stable et n'est donc pas considéré comme la meilleure solution (cela est explicitement déconseillé dans la documentation), mais il est utile de connaître:

import pandas
df = pandas.read_csv("test.csv")
df['FirstName'][df.ID == 103] = "Matt"
df['LastName'][df.ID == 103] = "Jones"
ely
la source
16
que diriez-vous d'ajouter aussi cette saveur:df.loc[df.ID == 103, ['FirstName', 'LastName']] = 'Matt', 'Jones'
Boud
2
-1 "Une autre façon de faire est d'utiliser ce que l'on appelle l'affectation chaînée." Non, catégoriquement, non. Il est seulement utile de savoir que l'affectation chaînée n'est pas fiable. Ce n'est pas que ce soit une solution fiable et non optimale, la situation est bien pire . Vous avez même reconnu cela ailleurs sur Stack Overflow . Essayez d'éviter de donner l'illusion qu'une affectation enchaînée est une option viable. Les deux premières méthodes que vous avez données étaient suffisantes et sont la meilleure façon de procéder.
Phillip Cloud
9
Je ne suis pas d'accord. Je ne comprends pas pourquoi vous persistez à essayer de manière pédante d'affirmer que l'affectation enchaînée n'est pas un moyen viable. J'ai reconnu que ce n'était pas considéré comme la méthode préférée. Que veux-tu de plus. C'est absurde d'agir comme ça n'est pas une façon de le faire. En fait, dans mon système actuel (version 0.8), c'est la bonne façon de le faire. Je ne suis pas intéressé par vos votes à la hausse si vous allez prendre cette position. N'hésitez pas à signaler votre point avec un vote défavorable, mais j'ai déjà réfléchi à votre point et je ne suis pas d'accord avec lui.
ely
11
Internet est une affaire sérieuse. En tout cas, EMS, j'ai apprécié de savoir que l'option existe.
Fourchelangue
Un problème que vous pourriez rencontrer est que le csv a des points / points dans les noms de colonne et que les affectations sont gâchées. Vous pouvez corriger les colonnes en utilisant quelque chose comme ceci: cols = df.columns cols = cols.map (lambda x: x.replace ('.', '_') If isinstance (x, str) else x) df.columns = cols
ski_squaw
37

Vous pouvez utiliser map, il peut mapper les valeurs d'une dictonairy ou même d'une fonction personnalisée.

Supposons que ce soit votre df:

    ID First_Name Last_Name
0  103          a         b
1  104          c         d

Créez les dictats:

fnames = {103: "Matt", 104: "Mr"}
lnames = {103: "Jones", 104: "X"}

Et carte:

df['First_Name'] = df['ID'].map(fnames)
df['Last_Name'] = df['ID'].map(lnames)

Le résultat sera:

    ID First_Name Last_Name
0  103       Matt     Jones
1  104         Mr         X

Ou utilisez une fonction personnalisée:

names = {103: ("Matt", "Jones"), 104: ("Mr", "X")}
df['First_Name'] = df['ID'].map(lambda x: names[x][0])
Rutger Kassies
la source
2
Cela ne générera-t-il pas une KeyError si les valeurs n'existent pas dans votre dict?
EdChum
1
La fonction personnalisée fonctionnera, les autres fonctionneront de toute façon. Mais j'ai supposé que le dictétait créé pour le mappage. Sinon, une vérification / nettoyage peut être effectué sur la base de quelque chose comme:df.ID.isin(names.keys())
Rutger Kassies
La fonction personnalisée peut être étendue à n'importe quelle fonction (non anonyme).
user989762 le
14

La question d'origine concerne un cas d'utilisation restreint spécifique. Pour ceux qui ont besoin de réponses plus génériques, voici quelques exemples:

Créer une nouvelle colonne en utilisant les données d'autres colonnes

Compte tenu de la base de données ci-dessous:

import pandas as pd
import numpy as np

df = pd.DataFrame([['dog', 'hound', 5],
                   ['cat', 'ragdoll', 1]],
                  columns=['animal', 'type', 'age'])

In[1]:
Out[1]:
  animal     type  age
----------------------
0    dog    hound    5
1    cat  ragdoll    1

Ci-dessous, nous ajoutons une nouvelle descriptioncolonne en tant que concaténation d'autres colonnes en utilisant l' +opération qui est remplacée pour les séries. Le formatage de chaîne fantaisie, les chaînes f, etc. ne fonctionneront pas ici car le +s'applique aux scalaires et non aux valeurs 'primitives':

df['description'] = 'A ' + df.age.astype(str) + ' years old ' \
                    + df.type + ' ' + df.animal

In [2]: df
Out[2]:
  animal     type  age                description
-------------------------------------------------
0    dog    hound    5    A 5 years old hound dog
1    cat  ragdoll    1  A 1 years old ragdoll cat

Nous obtenons 1 yearspour le chat (au lieu de 1 year) que nous corrigerons ci-dessous en utilisant des conditions.

Modifier une colonne existante avec des conditions

Ici, nous remplaçons la animalcolonne d' origine par des valeurs d'autres colonnes et utilisons np.wherepour définir une sous-chaîne conditionnelle basée sur la valeur de age:

# append 's' to 'age' if it's greater than 1
df.animal = df.animal + ", " + df.type + ", " + \
    df.age.astype(str) + " year" + np.where(df.age > 1, 's', '')

In [3]: df
Out[3]:
                 animal     type  age
-------------------------------------
0   dog, hound, 5 years    hound    5
1  cat, ragdoll, 1 year  ragdoll    1

Modification de plusieurs colonnes avec des conditions

Une approche plus flexible consiste à faire appel .apply()à un dataframe entier plutôt qu'à une seule colonne:

def transform_row(r):
    r.animal = 'wild ' + r.type
    r.type = r.animal + ' creature'
    r.age = "{} year{}".format(r.age, r.age > 1 and 's' or '')
    return r

df.apply(transform_row, axis=1)

In[4]:
Out[4]:
         animal            type      age
----------------------------------------
0    wild hound    dog creature  5 years
1  wild ragdoll    cat creature   1 year

Dans le code ci-dessus, la transform_row(r)fonction prend un Seriesobjet représentant une ligne donnée (indiqué par axis=1, la valeur par défaut de axis=0fournira un Seriesobjet pour chaque colonne). Cela simplifie le traitement car nous pouvons accéder aux valeurs «primitives» réelles de la ligne en utilisant les noms de colonne et avoir la visibilité des autres cellules dans la ligne / colonne donnée.

ccpizza
la source
1
Merci d'avoir pris le temps de rédiger une réponse aussi complète. Très appréciée.
Fourchelangue
Merci pour cette réponse extrêmement utile. Un suivi - et si nous voulons modifier une colonne en faisant des maths sur la colonne, plutôt que de modifier une chaîne? Par exemple, en utilisant l'exemple ci-dessus, que se passe-t-il si nous voulons multiplier la colonne df.age par 7 si df.animal == 'dog'? Je vous remercie!
GbG
1
@GbG: np.whereest probablement ce que vous recherchez, voir par exemple stackoverflow.com/a/42540310/191246 mais il est également possible que vous ne puissiez pas adapter la logique dans une opération scalaire, alors vous devrez explicitement transformer la cellule numériquement similaire à la façon dont c'est fait danstransform_row
ccpizza
Merci @ccpizza! Exactement ce que je cherchais.
GbG
13

Cette question pourrait encore être visitée assez souvent pour qu'il vaille la peine d'offrir un addendum à la réponse de M. Kassies. La dictclasse intégrée peut être sous-classée afin qu'une valeur par défaut soit renvoyée pour les clés «manquantes». Ce mécanisme fonctionne bien pour les pandas.Mais voyez ci-dessous.

De cette manière, il est possible d'éviter les erreurs clés.

>>> import pandas as pd
>>> data = { 'ID': [ 101, 201, 301, 401 ] }
>>> df = pd.DataFrame(data)
>>> class SurnameMap(dict):
...     def __missing__(self, key):
...         return ''
...     
>>> surnamemap = SurnameMap()
>>> surnamemap[101] = 'Mohanty'
>>> surnamemap[301] = 'Drake'
>>> df['Surname'] = df['ID'].apply(lambda x: surnamemap[x])
>>> df
    ID  Surname
0  101  Mohanty
1  201         
2  301    Drake
3  401         

La même chose peut être faite plus simplement de la manière suivante. L'utilisation de l'argument 'default' pour la getméthode d'un objet dict rend inutile la sous-classe d'un dict.

>>> import pandas as pd
>>> data = { 'ID': [ 101, 201, 301, 401 ] }
>>> df = pd.DataFrame(data)
>>> surnamemap = {}
>>> surnamemap[101] = 'Mohanty'
>>> surnamemap[301] = 'Drake'
>>> df['Surname'] = df['ID'].apply(lambda x: surnamemap.get(x, ''))
>>> df
    ID  Surname
0  101  Mohanty
1  201         
2  301    Drake
3  401         
Bill Bell
la source
1
c'est de loin la meilleure et la plus simple réponse que j'ai vue, avec une excellente gestion par défaut. Je vous remercie.
Brendan
@Brendan: Oh! Merci beaucoup.
Bill Bell