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
Mikola
źródło

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
ALollz
źródło
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)
Alexander
źródło
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)
Dani Mesejo
źródło
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
Quang Hoang
źródło
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
YOBEN_S
źródło
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
anky
źródło
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)
Wisznudew
źródło
Używanie score.getjest dobre. Moim zdaniem powinieneś jednak raczej rozumieć. Zobacz czasy @ Alexandra .
piRSquared
Ok @piSquared. Będę o tym pamiętać.
Vishnudev