我有什么:
- 具有许多行和多个现有列(python、pandas)的数据帧。
- Python 3.6,所以依赖于该特定版本的解决方案对我来说很好(但显然也适用于早期版本的解决方案也很好)
我想做什么:
- 向数据帧添加多个附加列,其中这些新列中的值都以某种方式依赖于同一行中现有列中的值。
- 必须保留数据帧的原始顺序。如果解决方案更改了排序,我可以在之后通过基于现有列之一手动排序来恢复它,但显然这会带来额外的开销。
我已经有以下代码,它可以正常工作。但是,分析表明此代码是我的代码中的重要瓶颈之一,因此如果可能的话,我想对其进行优化,并且我也有理由相信这应该是可能的:
df["NewColumn1"] = df.apply(lambda row: compute_new_column1_value(row), axis=1)
df["NewColumn2"] = df.apply(lambda row: compute_new_column2_value(row), axis=1)
# a few more lines of code like the above
我基于对此类问题的回答(这是一个类似于我的问题,但特别是关于添加一个新列,而我的问题关于添加许多新列)。我想这些df.apply()
调用中的每一个都是通过遍历所有行的循环在内部实现的,我怀疑应该可以使用仅遍历所有循环一次的解决方案来优化这一点(而不是每列一次我想添加)。
在其他答案中,我看到了对 assign() 函数的引用,该函数确实支持一次添加多个列。我尝试通过以下方式使用它:
# WARNING: this does NOT work
df = df.assign(
NewColumn1=lambda row: compute_new_column1_value(row),
NewColumn2=lambda row: compute_new_column2_value(row),
# more lines like the two above
)
这不起作用的原因是因为 lambda 实际上根本不接收数据帧的行作为参数,它们似乎只是一次获取整个数据帧。然后,每个 lambda 都应该一次返回一个完整的列/系列/值数组。所以,我的问题是,我最终必须通过这些lambda中的所有循环来实现手动循环,这显然会更糟糕的性能。
我可以从概念上想到两个解决方案,但到目前为止一直无法找到如何实际实现它们:
类似于
df.assign()
(支持一次添加多个列),但能够将行传递到 lambda 而不是完整的数据帧中一种向量化我的
compute_new_columnX_value()
函数的方法,以便它们可以按照df.assign()
期望使用它们的方式用作 lambda。
到目前为止,我对第二个解决方案的问题是,基于行的版本,我的一些函数如下所示,并且我很难找到如何正确矢量化它们:
def compute_new_column1_value(row):
if row["SomeExistingColumn"] in some_dictionary:
return some_dictionary[row["SomeExistingColumn"]]
else:
return some_default_value
您是否尝试过将列初始化为nan
,逐行循环访问数据帧,并使用loc
分配值?
import numpy as np
import pandas as pd
df = pd.DataFrame(np.random.randint(0, 20, (10, 5)))
df[5] = np.nan
df[6] = np.nan
for i, row in df.iterrows():
df.loc[i, 5] = row[1] + row[4]
df.loc[i, 6] = row[3] * 2
print(df)
收益 率
0 1 2 3 4
0 17 4 3 11 10
1 16 1 14 11 16
2 4 18 12 19 7
3 11 3 7 10 5
4 11 0 10 1 17
5 5 17 10 3 8
6 0 0 7 3 6
7 7 18 18 13 8
8 16 4 12 11 16
9 13 9 15 8 19
0 1 2 3 4 5 6
0 17 4 3 11 10 14.0 22.0
1 16 1 14 11 16 17.0 22.0
2 4 18 12 19 7 25.0 38.0
3 11 3 7 10 5 8.0 20.0
4 11 0 10 1 17 17.0 2.0
5 5 17 10 3 8 25.0 6.0
6 0 0 7 3 6 6.0 6.0
7 7 18 18 13 8 26.0 26.0
8 16 4 12 11 16 20.0 22.0
9 13 9 15 8 19 28.0 16.0
如果您只有 50 个条件要检查,则最好遍历条件并以块的形式填充单元格,而不是逐行浏览整个帧。顺便说一下,.assign() 不仅接受 lambda 函数,而且代码的可读性也比我之前的建议高得多。下面是一个修改后的版本,它也填充了额外的列。如果此数据框有 10,000,000 行,而我只想对 A 列中的 10 组数字范围应用不同的操作,这将是填充额外列的一种非常整洁的方法。
import pandas as pd
import numpy as np
# Create data frame
rnd = np.random.randint(1, 10, 10)
rnd2 = np.random.randint(100, 1000, 10)
df = pd.DataFrame(
{'A': rnd, 'B': rnd2, 'C': np.nan, 'D': np.nan, 'E': np.nan })
# Define different ways of filling the extra cells
def f1():
return df['A'].mul(df['B'])
def f2():
return np.log10(df['A'])
def f3():
return df['B'] - df['A']
def f4():
return df['A'].div(df['B'])
def f5():
return np.sqrt(df['B'])
def f6():
return df['A'] + df['B']
# First assign() dependent on a boolean mask
df[df['A'] < 50] = df[df['A'] < 15].assign(C = f1(), D = f2(), E = f3())
# Second assign() dependent on a boolean mask
df[df['A'] >= 50] = df[df['A'] >= 50].assign(C = f4(), D = f5(), E = f6())
print(df)
A B C D E
0 4.0 845.0 3380.0 0.602060 841
1 3.0 967.0 2901.0 0.477121 964
2 3.0 468.0 1404.0 0.477121 465
3 2.0 548.0 1096.0 0.301030 546
4 3.0 393.0 1179.0 0.477121 390
5 7.0 741.0 5187.0 0.845098 734
6 1.0 269.0 269.0 0.000000 268
7 4.0 731.0 2924.0 0.602060 727
8 4.0 193.0 772.0 0.602060 189
9 3.0 306.0 918.0 0.477121 303
与其尝试将行标签引入 .assign(),不如尝试将行标签引入 .assign(),您可以 在将 .assign() 链接到数据框之前,将布尔掩码应用于数据框。下面的示例可以很容易地扩展到多个布尔条件和多个 lambda,带或不带额外的 for 循环或 if 语句。
import pandas as pd
# Create data frame
idx = np.arange(0, 10)
rnd = pd.Series(np.random.randint(10, 20, 10))
alpha_idx = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']
df = pd.DataFrame({'idx': idx, 'A': rnd, 'B': 100})
df.index = alpha_idx
# First assign() dependent on a boolean mask
df_tmp = df[df['A'] < 15].assign(AmulB = lambda x: (x.A.mul(x.B)),
A_B = lambda x: x.B - x.A)
# Second assign() dependent on a boolean mask
df_tmp2 = df[df['A'] >= 15].assign(AmulB = lambda x: (x.A.div(x.B)),
A_B = lambda x: x.B + x.A)
# Create a new df with different lambdas combined
df_lambdas = df_tmp.append(df_tmp2)
# Sort values
df_lambdas.sort_values('idx', axis=0, inplace=True)
print(df_lambdas)
A B idx
a 19 100 0
b 17 100 1
c 16 100 2
d 13 100 3
e 15 100 4
f 10 100 5
g 16 100 6
h 15 100 7
i 13 100 8
j 10 100 9
A B idx A_B AmulB
a 19 100 0 119 0.19
b 17 100 1 117 0.17
c 16 100 2 116 0.16
d 13 100 3 87 1300.00
e 15 100 4 115 0.15
f 10 100 5 90 1000.00
g 16 100 6 116 0.16
h 15 100 7 115 0.15
i 13 100 8 87 1300.00
j 10 100 9 90 1000.00
到目前为止提供的答案并没有为我的具体情况提供加速,原因我在评论中提供了。到目前为止,我能够找到的最佳解决方案主要是基于对另一个问题的答案。它没有为我提供很大的加速(大约 10%),但这是我迄今为止能够做的最好的。如果存在更快的解决方案,我仍然对它们非常感兴趣!
事实证明,与assign
函数一样,apply
实际上也可以提供一次返回多个列的一系列值的lambda,而不仅仅是返回单个标量的lambda。因此,到目前为止,我最快的实现如下所示:
# first initialize all the new columns with standard values for entire df at once
# this turns out to be very important. Skipping this comes at a high computational cost
for new_column in ["NewColumn1", "NewColumn2", "etc."]:
df[new_column] = np.nan
df = df.apply(compute_all_new_columns, axis=1)
然后,不是为所有不同的新列提供所有这些单独的 lambda,而是在同一个函数中实现,如下所示:
def compute_all_new_columns(row):
if row["SomeExistingColumn"] in some_dictionary:
row["NewColumn1"] = some_dictionary[row["SomeExistingColumn"]]
else:
row["NewColumn1"] = some_default_value
if some_other_condition:
row["NewColumn2"] = whatever
else:
row["NewColumn2"] = row["SomeExistingColumn"] * whatever
# assign values to other new columns here
生成的数据帧包含它以前执行的所有列,以及compute_all_new_columns
函数逐行插入的所有新列的值。保留原始顺序。该解决方案不包含基于 python 的循环(速度很慢),只有一个循环通过 pandasapply
函数提供给我们的"幕后"行
我真的被这个问题所吸引,所以这是另一个涉及外部词典的例子:
import pandas as pd
import numpy as np
# Create data frame and external dictionaries
rnd = pd.Series(np.random.randint(10, 100, 10))
names = 'Rafael Roger Grigor Alexander Dominic Marin David Jack Stan Pablo'
name = names.split(' ')
surnames = 'Nadal Federer Dimitrov Zverev Thiem Cilic Goffin Sock Wawrinka Busta'
surname = surnames.split()
countries_str = ('Spain Switzerland Bulgaria Germany Austria Croatia Belgium USA Switzerland Spain')
country = countries_str.split(' ')
player = dict(zip(name, surname))
player_country = dict(zip(name, country))
df = pd.DataFrame(
{'A': rnd, 'B': 100, 'Name': name, 'Points': np.nan, 'Surname': np.nan, 'Country': np.nan})
df = df[['A', 'B', 'Name', 'Surname', 'Country', 'Points']]
df.loc[9, 'Name'] = 'Dennis'
print(df)
# Functions to fill the empty columns
def f1():
return df['A'].mul(df['B'])
def f2():
return np.random.randint(1, 10)
def f3():
return player[key]
def f4():
return player_country[key]
def f5():
return 'Unknown'
def f6():
return 0
# .assign() dependent on a boolean mask
for key, value in player.items():
df[df['Name'] == key] = df[df['Name'] == key].assign(
Surname = f3(), Country = f4(), Points = f1())
df[df['Name']=='Dennis'] = df[df['Name'] == 'Dennis'].assign(
Surname = f5(), Country = f5(), Points = f6())
df = df.sort_values('Points', ascending=False)
print(df)
A B Name Surname Country Points
1 97.0 100.0 Roger Federer Switzerland 9700.0
4 93.0 100.0 Dominic Thiem Austria 9300.0
8 92.0 100.0 Stan Wawrinka Switzerland 9200.0
5 86.0 100.0 Marin Cilic Croatia 8600.0
6 67.0 100.0 David Goffin Belgium 6700.0
7 61.0 100.0 Jack Sock USA 6100.0
0 35.0 100.0 Rafael Nadal Spain 3500.0
2 34.0 100.0 Grigor Dimitrov Bulgaria 3400.0
3 25.0 100.0 Alexander Zverev Germany 2500.0
9 48.0 100.0 Dennis Unknown Unknown 0.0