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.