如何有效地将多个列添加到 pandas 数据帧,其值依赖于其他列



我有什么:

  • 具有许多行和多个现有列(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中的所有循环来实现手动循环,这显然会更糟糕的性能。

我可以从概念上想到两个解决方案,但到目前为止一直无法找到如何实际实现它们:

  1. 类似于df.assign()(支持一次添加多个列),但能够将行传递到 lambda 而不是完整的数据帧中

  2. 一种向量化我的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

相关内容

最新更新