Ajouter une nouvelle colonne à la trame de données basée sur le dictionnaire

23

J'ai un dataframe et un dictionnaire. J'ai besoin d'ajouter une nouvelle colonne au cadre de données et de calculer ses valeurs en fonction du dictionnaire.

Apprentissage automatique, ajout d'une nouvelle fonctionnalité basée sur un tableau:

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)

J'attends la sortie suivante:

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

Réponses:

13

Puisqu'il scores'agit d'un dictionnaire (les clés sont donc uniques), nous pouvons utiliser l' MultiIndexalignement

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
la source
1
Nice one of MultiIIndex. Alternative: df['score'] =df.set_index(['gender', 'age', 'cholesterol', 'smoke']).index.map(score).fillna(0).to_numpy().
Quang Hoang
4
@ALollz, pardonnez-moi, j'aime vos réponses mais je dois parler quand je vois autant de votes positifs sur une réponse comme celle-ci. Cette réponse est fine ET intelligente. Mais ce n'est pas génial. Il y a trop de pièces mobiles sans grand gain. Dans le processus, vous avez créé un nouveau dfvia set_index, un nouveau Seriesconstructeur via. Bien que vous bénéficiez d'un alignement d'index lorsque vous l'affectez à df['score']. Enfin, fillna(0, downcast='infer')fait le travail mais personne ne devrait préférer cette longue solution avec la création de nombreux objets pandas inutilement.
piRSquared
Encore une fois, excuses, vous avez aussi mon vote positif, je veux juste guider les gens vers des réponses plus simples.
piRSquared
@piRSquared Je suis allé déjeuner et j'ai été surpris que cela ait attiré l'attention à mon retour. Je suis d'accord que c'est un peu compliqué de faire quelque chose qu'un simple mergepourrait accomplir. J'ai pensé que la réponse serait publiée rapidement, alors j'ai opté pour une alternative et pour une raison quelconque, j'avais MultiIndices en tête. Je suis d'accord, cela ne devrait probablement pas être la réponse acceptée, alors j'espère que cela ne se produira pas.
ALollz
1
Oh je suis avec toi. J'ai répondu la même fois plusieurs fois. Je fais juste de mon mieux pour servir la communauté (-: j'espère que vous obtenez mon intention.
piRSquared
7

Utilisation assignavec une compréhension de liste, obtention d'un tuple de valeurs (chaque ligne) du scoredictionnaire, par défaut à zéro s'il n'est pas trouvé.

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

Timings

Compte tenu de la variété des approches, je pensais qu'il serait intéressant de comparer certains des horaires.

# 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)
Alexandre
la source
Mon préféré par un peu. Cependant, juste pour m'assurer que tout reste le type voulu lors du traitement, score.getj'utiliserais itertuplesou zip(*map(df.get, df))... Pour réitérer, c'est mon approche préférée.
piRSquared
1
df.assign(score=[score.get(t, 0) for t in zip(*map(df.get, df))])
piRSquared
1
Enfin, la plupart de ce que j'écris est un fanfaron car le hachage de 1.0est le même que le hachage, par 1conséquent, les recherches de tuple devraient avoir pour résultat la même réponse. Excuses @Alexander pour tant de commentaires à ce sujet, mais je veux juste que les gens votent davantage parce que ... ils devraient (-:
piRSquared
1
Tant que vous chronométrez, regardez ma suggestion. Il y a des occasions où .valuesc'est cher
piRSquared
1
@AndyL. vous pouvez même contrôler quelles colonnes et dans quel ordre: zip(*map(df.get, ['col2', 'col1', 'col5']))ou obtenir des tuples d'une modification de df:zip(*map(df.eq(1).get, df))
piRSquared
4

Vous pouvez utiliser la carte , car le score est un dictionnaire:

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

Production

   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

Comme alternative, vous pouvez utiliser une compréhension de liste:

df['score'] = [score.get(t, 0) for t in zip(df.gender, df.age, df.cholesterol, df.smoke)]
print(df)
Dani Mesejo
la source
J'aimerais étendre ma question. Vraiment, je dois ajouter une base de colonne sur la plage de valeurs de colonne. Par exemple, si 40 <âge <50 alors alors score = 4 etc ... Maintenant le dictionnaire mappe sur une valeur exacte. Même vrai et pour les autres touches ....
Mikola
1
Ajoutez un exemple de ce que vous voulez vraiment
Dani Mesejo
Exemple simple: # Ici 40 et 50, 10 et 20 sont des tranches d'âge pour lesquelles je devrais utiliser score = 4 (ou 5) score = {(1, 40, 50, 1, 1): 4, (0, 10, 20 , 1, 3): 5}
Mikola
@Mikola Donc si le sexe = 1 et 40 <âge <50 et ainsi de suite ...
Dani Mesejo
1
@Mikola Vous devriez faire connaître chaque corps, bien qu'à ce stade, je pense qu'il vaut mieux que vous posiez une autre question.
Dani Mesejo
4

Comprendre la liste et la carte:

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

Production:

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

Ou 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
la source
2

Peut-être qu'une autre façon serait d'utiliser .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
la source
2

Solution simple en une ligne, Utilisation getet par tupleligne,

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

La solution ci-dessus suppose qu'il n'y a pas de colonnes autres que celles souhaitées dans l'ordre. Sinon, utilisez simplement des colonnes

cols = ['gender','age','cholesterol','smoke']
df['score'] = df[cols].apply(lambda x: score.get(tuple(x), 0), axis=1)
Vishnudev
la source
L'utilisation de score.getest bonne. Cependant, vous devriez préférer une compréhension, à mon avis. Voir les horaires @ Alexander .
piRSquared
Ok @piSquared. Gardera cela à l'esprit.
Vishnudev