Aggiungi una nuova colonna al frame di dati in base al dizionario


23

Ho un dataframe e un dizionario. Devo aggiungere una nuova colonna al dataframe e calcolare i suoi valori in base al dizionario.

Apprendimento automatico, aggiunta di nuove funzionalità basate su alcune tabelle:

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)

Mi aspetto il seguente risultato:

   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

Risposte:


13

Dato che scoreè un dizionario (quindi i tasti sono univoci) possiamo usare l' MultiIndexallineamento

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
Bello uno di MultiIIndex. Alternativa: df['score'] =df.set_index(['gender', 'age', 'cholesterol', 'smoke']).index.map(score).fillna(0).to_numpy().
Quang Hoang,

4
@ALollz, perdonami, adoro le tue risposte ma devo parlare quando vedo tanti voti positivi su una risposta come questa. Questa risposta è buona E intelligente. Ma non è eccezionale. Ci sono troppe parti in movimento per nessun guadagno. Nel processo, hai creato una nuova dfvia set_index, una nuova Seriesvia costruttore. Anche se ottieni un vantaggio dall'allineamento dell'indice quando lo assegni df['score']. Infine, fillna(0, downcast='infer')ottiene il lavoro fatto, ma nessuno dovrebbe preferire questa lunga soluzione con la creazione di molti oggetti Panda inutilmente.
piRSquared

Ancora una volta, mi scuso, hai anche il mio voto, voglio solo guidare la gente a risposte più semplici.
piRSquared

@piRSquared Sono andato a pranzo e sono rimasto sorpreso dall'attenzione che ho avuto quando sono tornato. Sono d'accordo che è un po 'contorto fare tutto ciò che un semplice mergepotrebbe realizzare. Ho pensato che la risposta sarebbe stata pubblicata rapidamente, quindi ho optato per un'alternativa e per qualche ragione avevo in mente MultiIndices. Sono d'accordo, questa probabilmente non dovrebbe essere la risposta accettata, quindi spero che ciò non accada.
ALollz,

1
Oh sono con te. Ho risposto lo stesso molte volte. Sto solo facendo del mio meglio per servire la comunità (-: confido che tu ottenga la mia intenzione.
piRSquared

7

Usando assigncon una comprensione dell'elenco, ottenendo una tupla di valori (ogni riga) dal scoredizionario, per impostazione predefinita a zero se non trovato.

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

Tempi

Data la varietà di approcci, ho pensato che sarebbe interessante confrontare alcuni dei tempi.

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

Il mio preferito da un po '. Tuttavia, solo per assicurarmi che tutto rimanga del tipo previsto durante l'elaborazione score.get, utilizzerei itertupleso zip(*map(df.get, df))... Per ribadire, questo è il mio approccio preferito.
piRSquared

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

1
Infine, la maggior parte di ciò che sto scrivendo è bluster perché l'hash di 1.0è lo stesso dell'hash per 1quindi le ricerche di tuple dovrebbero dare la stessa risposta a prescindere. Mi scuso @Alexander per così tanti commenti su questo, ma voglio solo che le persone lo votino di più perché ... dovrebbero (-:
piRSquared

1
Finché hai i tempi, guarda il mio suggerimento. Ci sono occasioni in cui .valuesè costoso
piRSquared il

1
@AndyL. puoi persino controllare quali colonne e in quale ordine: zip(*map(df.get, ['col2', 'col1', 'col5']))o ottenere le tuple di una modifica di df:zip(*map(df.eq(1).get, df))
piRSquared

4

Puoi usare la mappa , poiché score è un dizionario:

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

Produzione

   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

In alternativa è possibile utilizzare una comprensione dell'elenco:

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

Vorrei estendere la mia domanda. Devo davvero aggiungere una base di colonne sull'intervallo del valore della colonna. Ad esempio, se 40 <età <50 allora segna = 4 ecc ... Ora il dizionario mappa esattamente un valore. Lo stesso vale e per altri tasti ....
Mikola,

1
Aggiungi un esempio di ciò che vuoi davvero
Dani Mesejo,

Esempio semplice: # Qui 40 e 50, 10 e 20 sono intervalli di età per i quali dovrei usare il punteggio = 4 (o 5) il punteggio = {(1, 40, 50, 1, 1): 4, (0, 10, 20 , 1, 3): 5}
Mikola,

@Mikola Quindi se sesso = 1 e 40 <età <50 e così via ...
Dani Mesejo,

1
@Mikola Dovresti far conoscere ogni corpo, anche se a questo punto credo sia meglio se fai un'altra domanda.
Dani Mesejo,

4

Elenco comprensione e mappa:

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

Produzione:

   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

O 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

Potrebbe essere un altro modo sarebbe usare .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

Semplice soluzione a una riga, uso gete tupleriga di saggio,

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

La soluzione precedente presuppone che non vi siano colonne diverse da quelle desiderate nell'ordine. Altrimenti, usa solo le colonne

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

L'uso di score.getè buono. Tuttavia, dovresti preferire una comprensione, secondo me. Vedi i tempi di @ Alexander .
piRSquared

Ok @piSquared. Lo terrò a mente.
Vishnudev,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.