Zmień jedną wartość na podstawie innej wartości w pandach

109

Próbuję przeprogramować mój kod Stata na Python w celu zwiększenia szybkości i zostałem wskazany w kierunku PANDAS. Trudno mi jednak zrozumieć, jak przetwarzać dane.

Powiedzmy, że chcę iterować po wszystkich wartościach w nagłówku kolumny „ID”. Jeśli ten identyfikator pasuje do określonej liczby, chcę zmienić dwie odpowiadające mu wartości FirstName i LastName.

W Stata wygląda to tak:

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

Więc to zastępuje wszystkie wartości w FirstName, które odpowiadają wartościom ID == 103 Matt.

W PANDAS próbuję czegoś takiego

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

Nie wiem, dokąd się stąd udać. Jakieś pomysły?

Parseltongue
źródło

Odpowiedzi:

183

Jedną z opcji jest użycie funkcji krojenia i indeksowania Pythona do logicznej oceny miejsc, w których zachowuje się twój warunek, i nadpisania tam danych.

Zakładając, że można załadować swoje dane bezpośrednio pandasz pandas.read_csvczym następujący kod może być pomocne dla Ciebie.

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

Jak wspomniano w komentarzach, możesz również przypisać obie kolumny za jednym razem:

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

Zauważ, że będziesz potrzebować pandaswersji 0.11 lub nowszej, aby używać jej locdo operacji nadpisywania.


Innym sposobem jest użycie tak zwanego przypisania łańcuchowego. Zachowanie tego jest mniej stabilne i dlatego nie jest uważane za najlepsze rozwiązanie (jest to wyraźnie odradzane w dokumentacji), ale warto wiedzieć o:

import pandas
df = pandas.read_csv("test.csv")
df['FirstName'][df.ID == 103] = "Matt"
df['LastName'][df.ID == 103] = "Jones"
ely
źródło
16
co powiesz na dodanie tego smaku:df.loc[df.ID == 103, ['FirstName', 'LastName']] = 'Matt', 'Jones'
Boud
2
-1 „Innym sposobem jest użycie tak zwanego przypisania łańcuchowego”. Nie. Zdecydowanie nie. Warto tylko wiedzieć, że przypisanie łańcuchowe nie jest niezawodne. Nie chodzi o to, że jest to niezawodne, nieoptymalne rozwiązanie, sytuacja jest znacznie gorsza . Potwierdziłeś to nawet w innym miejscu w Stack Overflow . Staraj się nie dawać złudzenia, że ​​przypisanie w łańcuchu jest realną opcją. Pierwsze dwie metody, które podałeś, były wystarczające i są preferowanym sposobem, aby to zrobić.
Phillip Cloud
9
Nie zgadzam się. Nie rozumiem, dlaczego upierasz się przy pedantycznych próbach stwierdzenia, że ​​przypisanie na łańcuchu nie jest wykonalnym sposobem. Potwierdziłem, że nie jest to uważane za preferowany sposób. Czego jeszcze chcesz. To niedorzeczne zachowywać się tak, jakby to nie było sposób na zrobienie tego. W rzeczywistości w moim systemie w tej chwili (wersja 0.8) jest to właściwy sposób . Nie jestem zainteresowany twoimi głosami pozytywnymi, jeśli zamierzasz zająć to stanowisko. Możesz zasygnalizować swój punkt negatywnym głosem, ale już zastanawiałem się nad twoim punktem i nie zgadzam się z nim.
ely
11
Internet to poważna sprawa. W każdym razie EMS, doceniłem fakt, że istnieje taka opcja.
Parseltongue
Jednym z problemów, które możesz napotkać, jest to, że csv ma ​​kropki / kropki w nazwach kolumn, a przypisania są pomieszane. Możesz naprawić kolumny, używając czegoś takiego: cols = df.columns cols = cols.map (lambda x: x.replace ('.', '_') If isinstance (x, str) else x) df.columns = cols
ski_squaw
37

Możesz użyć map, może mapować wartości z dyktatury, a nawet funkcję niestandardową.

Załóżmy, że to jest twój plik df:

    ID First_Name Last_Name
0  103          a         b
1  104          c         d

Utwórz dykty:

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

I mapa:

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

Rezultatem będzie:

    ID First_Name Last_Name
0  103       Matt     Jones
1  104         Mr         X

Lub użyj funkcji niestandardowej:

names = {103: ("Matt", "Jones"), 104: ("Mr", "X")}
df['First_Name'] = df['ID'].map(lambda x: names[x][0])
Rutger Kassies
źródło
2
Czy to nie wygeneruje KeyError, jeśli wartości nie istnieją w Twoim dyktandzie?
EdChum
1
Funkcja niestandardowa będzie działać, inne i tak będą działać. Ale założyłem, że dictjest tworzony do mapowania. W przeciwnym razie można przeprowadzić pewne sprawdzenie / czyszczenie w oparciu o coś takiego:df.ID.isin(names.keys())
Rutger Kassies
Funkcję niestandardową można rozszerzyć do dowolnej (nieanonimowej) funkcji.
user989762
14

Oryginalne pytanie dotyczy konkretnego wąskiego przypadku użycia. Dla tych, którzy potrzebują bardziej ogólnych odpowiedzi, oto kilka przykładów:

Tworzenie nowej kolumny na podstawie danych z innych kolumn

Biorąc pod uwagę ramkę danych poniżej:

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

Poniżej dodajemy nową descriptionkolumnę jako konkatenację innych kolumn za pomocą +operacji, która jest nadpisywana dla serii. Fantazyjne formatowanie ciągów, f-stringi itp. Nie będą tutaj działać, ponieważ mają +zastosowanie do skalarów, a nie wartości „pierwotnych”:

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

Dostajemy 1 yearsza kota (zamiast 1 year), który będziemy naprawiać poniżej za pomocą warunków.

Modyfikowanie istniejącej kolumny za pomocą warunków

Tutaj zastępujemy oryginalną animalkolumnę wartościami z innych kolumn i używamy np.wheredo ustawienia podciągu warunkowego na podstawie wartości 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

Modyfikowanie wielu kolumn za pomocą warunków

Bardziej elastycznym podejściem jest wywołanie .apply()całej ramki danych zamiast pojedynczej kolumny:

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

W powyższym kodzie transform_row(r)funkcja przyjmuje Seriesobiekt reprezentujący dany wiersz (oznaczony axis=1jako domyślna wartość axis=0zapewni Seriesobiekt dla każdej kolumny). Upraszcza to przetwarzanie, ponieważ możemy uzyskać dostęp do rzeczywistych wartości „pierwotnych” w wierszu za pomocą nazw kolumn i mieć widoczność innych komórek w danym wierszu / kolumnie.

ccpizza
źródło
1
Dziękuję za poświęcenie czasu na napisanie tak wyczerpującej odpowiedzi. Bardzo cenione.
Parseltongue
Dzięki za tę niezwykle pomocną odpowiedź. Jedna kontynuacja - co zrobić, jeśli chcemy zmodyfikować kolumnę, wykonując obliczenia matematyczne na kolumnie, zamiast modyfikować ciąg? Na przykład, korzystając z powyższego przykładu, co zrobić, jeśli chcemy pomnożyć kolumnę df.age przez 7, jeśli df.animal == 'dog'? Dziękuję Ci!
GbG
1
@GbG: np.whereprawdopodobnie jest to, czego szukasz, patrz np stackoverflow.com/a/42540310/191246 ale jest to również możliwe, że nie będzie w stanie dopasować do logiki działania skalarnym, to trzeba by jednoznacznie przekształcić komórka numerycznie podobna do tego, jak to się robitransform_row
ccpizza
Dziękuję @ccpizza! Właśnie tego szukałem.
GbG
13

To pytanie może być wciąż odwiedzane na tyle często, że warto zaproponować uzupełnienie odpowiedzi pana Kassiesa. dictWbudowany w klasie może być sub-klasyfikowane tak, że domyślnie jest zwracana na klucze „brakujących”. Ten mechanizm działa dobrze w przypadku pand. Ale patrz poniżej.

W ten sposób można uniknąć kluczowych błędów.

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

To samo można zrobić prościej w następujący sposób. Użycie argumentu „default” w getmetodzie obiektu dict sprawia, że ​​nie jest konieczne tworzenie podklasy 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
źródło
1
jest to zdecydowanie najlepsza i najłatwiejsza odpowiedź, jaką widziałem, z doskonałą obsługą domyślną. Dziękuję Ci.
Brendan
@Brendan: Och! Dziękuję bardzo.
Bill Bell