根据字典在数据框中添加新列

24

我有一个数据框和一个字典。我需要向数据框添加一个新列,并根据字典计算其值。

机器学习,基于某个表格添加新特征:

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)

我期望以下输出:

我期望以下输出:

   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
7个回答

13

由于score是一个字典(因此键是唯一的),我们可以使用MultiIndex对齐。


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
很棒的MultiIndex,另一种方法是df['score'] = df.set_index(['gender', 'age', 'cholesterol', 'smoke']).index.map(score).fillna(0).to_numpy() - Quang Hoang
4
@ALollz,对不起,我很喜欢你的回答,但当我看到这样一个答案得到了这么多的赞时,我必须说出来。这个答案很好,而且很聪明。但它并不是很好。其中有太多的移动部分,而没有很大的收益。在这个过程中,你通过set_index创建了一个新的df,通过构造函数创建了一个新的Series。尽管当你将其分配给df['score']时,可以获得索引对齐的好处。最后,fillna(0,downcast ='infer')可以完成工作,但没有人应该偏爱这种冗长的解决方案,并且不必要地创建许多pandas对象。 - piRSquared
再次道歉,您也得到了我的点赞,我只是想引导大家寻找更简单的答案。 - piRSquared
@piRSquared,我去吃午饭了,当我回来时惊讶于这个问题引起了很多关注。我同意这有点复杂,而只需要一个简单的“合并”就可以完成。我想到这个答案会很快发布,所以选择了另一种方法,出于某种原因,我想到了MultiIndices。我同意,这可能不应该成为被接受的答案,希望不要发生这种情况。 - ALollz
1
哦,我明白你的意思。我已经回答过很多次了。我只是尽力为社区服务 (-: 我相信你能理解我的意图。 - piRSquared

8
使用列表推导式和`assign`,从`score`字典中获取一组值(每行),如果未找到则默认为零。
>>> 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

时间

考虑到各种方法的多样性,我认为比较一些时间会很有趣。

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

1
df.assign(score=[score.get(t, 0) for t in zip(*map(df.get, df))]) - piRSquared
1
最后,我写的大部分都是虚张声势,因为1.0的哈希值与1的哈希值相同,因此元组查找应该得出相同的答案。对于这个问题,我向@Alexander道歉,因为我发了很多评论,但我只是想让更多人点赞,因为...他们应该这样做(-: - piRSquared
1
只要你在计时,请看看我的建议。有时候使用 ".values" 会很耗费资源。 - piRSquared
@piRSquared 是的,[score.get(t, 0) for t in zip(*map(df.get, df))] 更好,但是 df.assign(score=[score.get(v, 0) for i, v in df.itertuples()]) 不起作用。让我看看能否修改它。 - Alexander
1
@AndyL. 你甚至可以控制哪些列以及它们的顺序:zip(*map(df.get, ['col2', 'col1', 'col5'])) 或者获取 df 的修改元组:zip(*map(df.eq(1).get, df)) - piRSquared
显示剩余4条评论

4
您可以使用 map,因为分数是一个字典:
df['score'] = df[['gender', 'age', 'cholesterol', 'smoke']].apply(tuple, axis=1).map(score).fillna(0)
print(df)

输出

   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

作为另一种选择,您可以使用列表推导式:

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

我想扩展我的问题。实际上,我需要根据列值范围添加列。例如,如果40 < 年龄 < 50,则分数= 4等等...现在字典映射到确切的某些值。对于其他键也是如此。 - Roman Kazmin
1
添加一个你真正想要的示例。 - Dani Mesejo
简单示例:#这里的40和50,10和20是我应该使用score = 4(或5)的年龄范围 score = {(1, 40, 50, 1, 1):4,(0, 10, 20, 1, 3):5} - Roman Kazmin
1
@Mikola 你应该让大家知道,尽管我认为现在最好是你提出另一个问题。 - Dani Mesejo

4

列表推导式和 map 函数:

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

输出:

   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

重新索引

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

或者 合并
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
也许另一种方法是使用.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

简单的一行解决方案,使用gettuple逐行处理。

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

上述解决方案假设订单中除所需列外没有其他列。如果有其他列,请使用columns。
cols = ['gender','age','cholesterol','smoke']
df['score'] = df[cols].apply(lambda x: score.get(tuple(x), 0), axis=1)

使用score.get是很好的。然而,在我看来,你应该更喜欢使用推导式。请参阅@Alexander's的时间数据。 - piRSquared

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接