Dodaj nową kolumnę do ramki danych na podstawie słownika


23

Mam ramkę danych i słownik. Muszę dodać nową kolumnę do ramki danych i obliczyć jej wartości na podstawie słownika.

Uczenie maszynowe, dodanie nowej funkcji opartej na niektórych tabelach:

score = {(1, 45, 1, 1) : 4, (0, 1, 2, 1) : 5}
df = pd.DataFrame(data = {
    'gender' :      [1,  1,  0, 1,  1,  0,  0,  0,  1,  0],
    'age' :         [13, 45, 1, 45, 15, 16, 16, 16, 15, 15],
    'cholesterol' : [1,  2,  2, 1, 1, 1, 1, 1, 1, 1],
    'smoke' :       [0,  0,  1, 1, 7, 8, 3, 4, 4, 2]},
     dtype = np.int64)

print(df, '\n')
df['score'] = 0
df.score = score[(df.gender, df.age, df.cholesterol, df.smoke)]
print(df)

Oczekuję następujących danych wyjściowych:

   gender  age  cholesterol  smoke    score
0       1   13            1      0      0 
1       1   45            2      0      0
2       0    1            2      1      5
3       1   45            1      1      4
4       1   15            1      7      0
5       0   16            1      8      0
6       0   16            1      3      0
7       0   16            1      4      0
8       1   15            1      4      0
9       0   15            1      2      0

Odpowiedzi:


13

Ponieważ scorejest to słownik (więc klucze są unikalne), możemy użyć MultiIndexwyrównania

df = df.set_index(['gender', 'age', 'cholesterol', 'smoke'])
df['score'] = pd.Series(score)  # Assign values based on the tuple
df = df.fillna(0, downcast='infer').reset_index()  # Back to columns

   gender  age  cholesterol  smoke  score
0       1   13            1      0      0
1       1   45            2      0      0
2       0    1            2      1      5
3       1   45            1      1      4
4       1   15            1      7      0
5       0   16            1      8      0
6       0   16            1      3      0
7       0   16            1      4      0
8       1   15            1      4      0
9       0   15            1      2      0

1
Niezły z MultiIIndex. Alternatywa: df['score'] =df.set_index(['gender', 'age', 'cholesterol', 'smoke']).index.map(score).fillna(0).to_numpy().
Quang Hoang

4
@ALollz, wybacz mi, uwielbiam twoje odpowiedzi, ale muszę zabrać głos, gdy widzę tyle głosów poparcia dla takiej odpowiedzi. Ta odpowiedź jest w porządku I sprytna. Ale to nie jest świetne. Jest zbyt wiele ruchomych części, aby nie uzyskać wielkiego zysku. W trakcie procesu utworzyłeś nowy dfvia set_index, nowy Seriesvia konstruktor. Chociaż zyskujesz na wyrównaniu indeksu, gdy go przypisujesz df['score']. Na koniec fillna(0, downcast='infer')wykonuje zadanie, ale nikt nie powinien preferować tego długiego rozwiązania z niepotrzebnym tworzeniem wielu obiektów pand.
piRSquared

Znowu przepraszam, masz również moje poparcie, chcę tylko poprowadzić ludzi do prostszych odpowiedzi.
piRSquared

@piRSquared Poszedłem na lunch i byłem zaskoczony, że zwróciło to uwagę, kiedy wróciłem. Zgadzam się, że zrobienie czegoś, co zwykły mergemoże osiągnąć , jest trochę skomplikowane . Uznałem, że odpowiedź zostanie opublikowana szybko, więc zdecydowałem się na alternatywę iz jakiegoś powodu miałem na myśli MultiIndices. Zgadzam się, to prawdopodobnie nie powinna być zaakceptowana odpowiedź, więc mam nadzieję, że tak się nie stanie.
ALollz

1
Och, jestem z tobą. Odpowiedziałem tak samo wiele razy. Po prostu staram się służyć społeczności (-: Ufam, że rozumiesz, o co mi chodzi.
piRSquared,

7

Używanie assignze zrozumieniem listy, pobieranie krotek wartości (każdego wiersza) ze scoresłownika, domyślnie zero, jeśli nie zostanie znalezione.

>>> df.assign(score=[score.get(tuple(row), 0) for row in df.values])
   gender  age  cholesterol  smoke  score
0       1   13            1      0      0
1       1   45            2      0      0
2       0    1            2      1      5
3       1   45            1      1      4
4       1   15            1      7      0
5       0   16            1      8      0
6       0   16            1      3      0
7       0   16            1      4      0
8       1   15            1      4      0
9       0   15            1      2      0

Czasy

Biorąc pod uwagę różnorodność podejść, pomyślałem, że byłoby interesujące porównać niektóre czasy.

# Initial dataframe 100k rows (10 rows of identical data replicated 10k times).
df = pd.DataFrame(data = {
    'gender' :      [1,  1,  0, 1,  1,  0,  0,  0,  1,  0] * 10000,
    'age' :         [13, 45, 1, 45, 15, 16, 16, 16, 15, 15] * 10000,
    'cholesterol' : [1,  2,  2, 1, 1, 1, 1, 1, 1, 1] * 10000,
    'smoke' :       [0,  0,  1, 1, 7, 8, 3, 4, 4, 2] * 10000},
     dtype = np.int64)

%timeit -n 10 df.assign(score=[score.get(tuple(v), 0) for v in df.values])
# 223 ms ± 9.28 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

%%timeit -n 10 
df.assign(score=[score.get(t, 0) for t in zip(*map(df.get, df))])
# 76.8 ms ± 2.8 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

%%timeit -n 10
df.assign(score=[score.get(v, 0) for v in df.itertuples(index=False)])
# 113 ms ± 2.58 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

%timeit -n 10 df.assign(score=df.apply(lambda x: score.get(tuple(x), 0), axis=1))
# 1.84 s ± 77.3 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

%%timeit -n 10
(df
 .set_index(['gender', 'age', 'cholesterol', 'smoke'])
 .assign(score=pd.Series(score))
 .fillna(0, downcast='infer')
 .reset_index()
)
# 138 ms ± 11.5 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

%%timeit -n 10
s=pd.Series(score)
s.index.names=['gender','age','cholesterol','smoke']
df.merge(s.to_frame('score').reset_index(),how='left').fillna(0).astype(int)
# 24 ms ± 2.27 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

%%timeit -n 10
df.assign(score=pd.Series(zip(df.gender, df.age, df.cholesterol, df.smoke))
                .map(score)
                .fillna(0)
                .astype(int))
# 191 ms ± 7.54 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

%%timeit -n 10
df.assign(score=df[['gender', 'age', 'cholesterol', 'smoke']]
                .apply(tuple, axis=1)
                .map(score)
                .fillna(0))
# 1.95 s ± 134 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

Trochę mój ulubiony. Jednak, aby upewnić się, że wszystko pozostaje zgodne z zamierzonym typem podczas przetwarzania score.get, użyłbym itertupleslub zip(*map(df.get, df))... Powtarzam, to jest moje preferowane podejście.
piRSquared

1
df.assign(score=[score.get(t, 0) for t in zip(*map(df.get, df))])
piRSquared

1
Wreszcie, większość tego, co piszę, jest tępy, ponieważ skrót 1.0jest taki sam jak skrót, 1dlatego wyszukiwanie krotek powinno dać tę samą odpowiedź niezależnie od tego. Przepraszam @Alexander za tak wiele komentarzy na ten temat, ale po prostu chcę, aby ludzie głosowali jeszcze bardziej, ponieważ ... powinni (-:
piRSquared

1
Dopóki mierzysz czas, spójrz na moją sugestię. Są .values
chwile,

1
@AndyL. możesz nawet kontrolować, które kolumny i w jakiej kolejności: zip(*map(df.get, ['col2', 'col1', 'col5']))lub uzyskać krotki modyfikacji df:zip(*map(df.eq(1).get, df))
piRSquared

4

Możesz użyć mapy , ponieważ wynikiem jest słownik:

df['score'] = df[['gender', 'age', 'cholesterol', 'smoke']].apply(tuple, axis=1).map(score).fillna(0)
print(df)

Wynik

   gender  age  cholesterol  smoke  score
0       1   13            1      0    0.0
1       1   45            2      0    0.0
2       0    1            2      1    5.0
3       1   45            1      1    4.0
4       1   15            1      7    0.0
5       0   16            1      8    0.0
6       0   16            1      3    0.0
7       0   16            1      4    0.0
8       1   15            1      4    0.0
9       0   15            1      2    0.0

Alternatywnie możesz użyć rozumienia listy:

df['score'] = [score.get(t, 0) for t in zip(df.gender, df.age, df.cholesterol, df.smoke)]
print(df)

Chciałbym rozszerzyć moje pytanie. Naprawdę muszę dodać podstawę kolumny na podstawie zakresu wartości kolumny. Na przykład, jeśli 40 <wiek <50 to wynik = 4 itd. Teraz słownik odwzorowuje dokładnie pewną wartość. To samo prawda i dla innych kluczy ....
Mikola

1
Dodaj przykład tego, czego naprawdę chcesz
Dani Mesejo

Prosty przykład: # Tutaj 40 i 50, 10 i 20 to przedział wiekowy, dla którego powinienem zastosować wynik = 4 (lub 5) wynik = {(1, 40, 50, 1, 1): 4, (0, 10, 20 , 1, 3): 5}
Mikola

@Mikola Więc jeśli płeć = 1 i 40 <wiek <50 i tak dalej ...
Dani Mesejo

1
@Mikola Powinieneś dać znać każdemu ciału, chociaż w tym momencie uważam, że lepiej, jeśli zadasz kolejne pytanie.
Dani Mesejo

4

Zrozumienie listy i mapa:

df['score'] = (pd.Series(zip(df.gender, df.age, df.cholesterol, df.smoke))
               .map(score)
               .fillna(0)
               .astype(int)
              )

Wynik:

   gender  age  cholesterol  smoke  score
0       1   13            1      0      0
1       1   45            2      0      0
2       0    1            2      1      5
3       1   45            1      1      4
4       1   15            1      7      0
5       0   16            1      8      0
6       0   16            1      3      0
7       0   16            1      4      0
8       1   15            1      4      0
9       0   15            1      2      0
9       0   15            1      2    0.0

4

reindex

df['socre']=pd.Series(score).reindex(pd.MultiIndex.from_frame(df),fill_value=0).values
df
Out[173]: 
   gender  age  cholesterol  smoke  socre
0       1   13            1      0      0
1       1   45            2      0      0
2       0    1            2      1      5
3       1   45            1      1      4
4       1   15            1      7      0
5       0   16            1      8      0
6       0   16            1      3      0
7       0   16            1      4      0
8       1   15            1      4      0
9       0   15            1      2      0

Lub merge

s=pd.Series(score)
s.index.names=['gender','age','cholesterol','smoke']
df=df.merge(s.to_frame('score').reset_index(),how='left').fillna(0)
Out[166]: 
   gender  age  cholesterol  smoke  score
0       1   13            1      0    0.0
1       1   45            2      0    0.0
2       0    1            2      1    5.0
3       1   45            1      1    4.0
4       1   15            1      7    0.0
5       0   16            1      8    0.0
6       0   16            1      3    0.0
7       0   16            1      4    0.0
8       1   15            1      4    0.0
9       0   15            1      2    0.0

2

Może być inny sposób użycia .loc[]:

m=df.set_index(df.columns.tolist())
m.loc[list(score.keys())].assign(
           score=score.values()).reindex(m.index,fill_value=0).reset_index()

   gender  age  cholesterol  smoke  score
0       1   13            1      0      0
1       1   45            2      0      0
2       0    1            2      1      5
3       1   45            1      1      4
4       1   15            1      7      0
5       0   16            1      8      0
6       0   16            1      3      0
7       0   16            1      4      0
8       1   15            1      4      0
9       0   15            1      2      0

2

Proste rozwiązanie jednowierszowe, zastosowanie geti tuplewierszowanie,

df['score'] = df.apply(lambda x: score.get(tuple(x), 0), axis=1)

Powyższe rozwiązanie zakłada, że ​​nie ma żadnych kolumn oprócz pożądanych. Jeśli nie, po prostu użyj kolumn

cols = ['gender','age','cholesterol','smoke']
df['score'] = df[cols].apply(lambda x: score.get(tuple(x), 0), axis=1)

Używanie score.getjest dobre. Moim zdaniem powinieneś jednak raczej rozumieć. Zobacz czasy @ Alexandra .
piRSquared

Ok @piSquared. Będę o tym pamiętać.
Vishnudev
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.