从 pandas apply() 返回多列
- 2025-03-13 09:10:00
- admin 原创
- 70
问题描述:
我有一个 pandas DataFrame。df_test
它包含一个列“size”,表示字节大小。我使用以下代码计算了 KB、MB 和 GB:
df_test = pd.DataFrame([
{'dir': '/Users/uname1', 'size': 994933},
{'dir': '/Users/uname2', 'size': 109338711},
])
df_test['size_kb'] = df_test['size'].astype(int).apply(lambda x: locale.format("%.1f", x / 1024.0, grouping=True) + ' KB')
df_test['size_mb'] = df_test['size'].astype(int).apply(lambda x: locale.format("%.1f", x / 1024.0 ** 2, grouping=True) + ' MB')
df_test['size_gb'] = df_test['size'].astype(int).apply(lambda x: locale.format("%.1f", x / 1024.0 ** 3, grouping=True) + ' GB')
df_test
dir size size_kb size_mb size_gb
0 /Users/uname1 994933 971.6 KB 0.9 MB 0.0 GB
1 /Users/uname2 109338711 106,776.1 KB 104.3 MB 0.1 GB
[2 rows x 5 columns]
我已经运行了超过 120,000 行,根据 %timeit,每列大约需要 2.97 秒 * 3 = ~9 秒。
有什么办法可以加快速度吗?例如,我是否可以一次返回一列并运行 3 次,而不是一次返回所有三列以插入回原始数据框?
我发现的其他问题都希望获取多个值并返回单个值。 我想获取单个值并返回多列。
解决方案 1:
您可以从应用函数返回包含新数据的系列,从而避免迭代三次。传递axis=1
给 apply 函数会将函数应用于sizes
数据框的每一行,返回一个系列以添加到新数据框。此系列 s 包含新值以及原始数据。
def sizes(s):
s['size_kb'] = locale.format("%.1f", s['size'] / 1024.0, grouping=True) + ' KB'
s['size_mb'] = locale.format("%.1f", s['size'] / 1024.0 ** 2, grouping=True) + ' MB'
s['size_gb'] = locale.format("%.1f", s['size'] / 1024.0 ** 3, grouping=True) + ' GB'
return s
df_test = df_test.append(rows_list)
df_test = df_test.apply(sizes, axis=1)
解决方案 2:
使用apply和zip会比Series方式快3倍。
def sizes(s):
return locale.format("%.1f", s / 1024.0, grouping=True) + ' KB', \n locale.format("%.1f", s / 1024.0 ** 2, grouping=True) + ' MB', \n locale.format("%.1f", s / 1024.0 ** 3, grouping=True) + ' GB'
df_test['size_kb'], df_test['size_mb'], df_test['size_gb'] = zip(*df_test['size'].apply(sizes))
测试结果是:
Separate df.apply():
100 loops, best of 3: 1.43 ms per loop
Return Series:
100 loops, best of 3: 2.61 ms per loop
Return tuple:
1000 loops, best of 3: 819 µs per loop
解决方案 3:
一些当前的回复工作正常,但我想提供另一个可能更“pandified”的选项。这对我来说适用于当前的pandas 0.23(不确定它是否适用于以前的版本):
import pandas as pd
df_test = pd.DataFrame([
{'dir': '/Users/uname1', 'size': 994933},
{'dir': '/Users/uname2', 'size': 109338711},
])
def sizes(s):
a = locale.format_string("%.1f", s['size'] / 1024.0, grouping=True) + ' KB'
b = locale.format_string("%.1f", s['size'] / 1024.0 ** 2, grouping=True) + ' MB'
c = locale.format_string("%.1f", s['size'] / 1024.0 ** 3, grouping=True) + ' GB'
return a, b, c
df_test[['size_kb', 'size_mb', 'size_gb']] = df_test.apply(sizes, axis=1, result_type="expand")
请注意,诀窍在于result_type
的参数apply
,它将扩展其结果为DataFrame
可以直接分配给新/旧列的。
解决方案 4:
答案真的很棒!感谢 Jesse 和 jaumebonet!以下是一些观察:
zip(* ...
... result_type="expand")
尽管 expand 更优雅一些(pandified),但 zip 至少快 2 倍。在下面这个简单的例子中,我的速度提高了 4 倍**。
import pandas as pd
dat = [ [i, 10*i] for i in range(1000)]
df = pd.DataFrame(dat, columns = ["a","b"])
def add_and_sub(row):
add = row["a"] + row["b"]
sub = row["a"] - row["b"]
return add, sub
df[["add", "sub"]] = df.apply(add_and_sub, axis=1, result_type="expand")
# versus
df["add"], df["sub"] = zip(*df.apply(add_and_sub, axis=1))
解决方案 5:
这只是另一种可读的方式。此代码将添加三个新列及其值,并在应用函数中返回不使用参数的系列。
def sizes(s):
val_kb = locale.format("%.1f", s['size'] / 1024.0, grouping=True) + ' KB'
val_mb = locale.format("%.1f", s['size'] / 1024.0 ** 2, grouping=True) + ' MB'
val_gb = locale.format("%.1f", s['size'] / 1024.0 ** 3, grouping=True) + ' GB'
return pd.Series([val_kb,val_mb,val_gb],index=['size_kb','size_mb','size_gb'])
df[['size_kb','size_mb','size_gb']] = df.apply(lambda x: sizes(x) , axis=1)
来自的一般示例:https://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.apply.html
df.apply(lambda x: pd.Series([1, 2], index=['foo', 'bar']), axis=1)
#foo bar
#0 1 2
#1 1 2
#2 1 2
解决方案 6:
最佳答案之间的表现差异很大,Jesse&famaral42 已经讨论过这个问题,但值得分享最佳答案之间的公平比较,并详细阐述 Jesse 答案的一个微妙但重要的细节:传递给函数的参数也会影响性能。
(Python 3.7.4,Pandas 1.0.3)
import pandas as pd
import locale
import timeit
def create_new_df_test():
df_test = pd.DataFrame([
{'dir': '/Users/uname1', 'size': 994933},
{'dir': '/Users/uname2', 'size': 109338711},
])
return df_test
def sizes_pass_series_return_series(series):
series['size_kb'] = locale.format_string("%.1f", series['size'] / 1024.0, grouping=True) + ' KB'
series['size_mb'] = locale.format_string("%.1f", series['size'] / 1024.0 ** 2, grouping=True) + ' MB'
series['size_gb'] = locale.format_string("%.1f", series['size'] / 1024.0 ** 3, grouping=True) + ' GB'
return series
def sizes_pass_series_return_tuple(series):
a = locale.format_string("%.1f", series['size'] / 1024.0, grouping=True) + ' KB'
b = locale.format_string("%.1f", series['size'] / 1024.0 ** 2, grouping=True) + ' MB'
c = locale.format_string("%.1f", series['size'] / 1024.0 ** 3, grouping=True) + ' GB'
return a, b, c
def sizes_pass_value_return_tuple(value):
a = locale.format_string("%.1f", value / 1024.0, grouping=True) + ' KB'
b = locale.format_string("%.1f", value / 1024.0 ** 2, grouping=True) + ' MB'
c = locale.format_string("%.1f", value / 1024.0 ** 3, grouping=True) + ' GB'
return a, b, c
结果如下:
# 1 - Accepted (Nels11 Answer) - (pass series, return series):
9.82 ms ± 377 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
# 2 - Pandafied (jaumebonet Answer) - (pass series, return tuple):
2.34 ms ± 48.6 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
# 3 - Tuples (pass series, return tuple then zip):
1.36 ms ± 62.8 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
# 4 - Tuples (Jesse Answer) - (pass value, return tuple then zip):
752 µs ± 18.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
请注意,返回元组是最快的方法,但作为参数传入的内容也会影响性能。代码中的差异很微妙,但性能改进却很显著。
尽管执行的操作表面上相同,但测试 #4(传递单个值)的速度是测试 #3(传递一系列值)的两倍。
但还有更多...
# 1a - Accepted (Nels11 Answer) - (pass series, return series, new columns exist):
3.23 ms ± 141 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
# 2a - Pandafied (jaumebonet Answer) - (pass series, return tuple, new columns exist):
2.31 ms ± 39.3 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
# 3a - Tuples (pass series, return tuple then zip, new columns exist):
1.36 ms ± 58.4 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
# 4a - Tuples (Jesse Answer) - (pass value, return tuple then zip, new columns exist):
694 µs ± 3.9 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
在某些情况下(#1a 和 #4a),将函数应用于已经存在的输出列的 DataFrame 比从函数创建它们更快。
以下是运行测试的代码:
# Paste and run the following in ipython console. It will not work if you run it from a .py file.
print('
Accepted Answer (pass series, return series, new columns dont exist):')
df_test = create_new_df_test()
%timeit result = df_test.apply(sizes_pass_series_return_series, axis=1)
print('Accepted Answer (pass series, return series, new columns exist):')
df_test = create_new_df_test()
df_test = pd.concat([df_test, pd.DataFrame(columns=['size_kb', 'size_mb', 'size_gb'])])
%timeit result = df_test.apply(sizes_pass_series_return_series, axis=1)
print('
Pandafied (pass series, return tuple, new columns dont exist):')
df_test = create_new_df_test()
%timeit df_test[['size_kb', 'size_mb', 'size_gb']] = df_test.apply(sizes_pass_series_return_tuple, axis=1, result_type="expand")
print('Pandafied (pass series, return tuple, new columns exist):')
df_test = create_new_df_test()
df_test = pd.concat([df_test, pd.DataFrame(columns=['size_kb', 'size_mb', 'size_gb'])])
%timeit df_test[['size_kb', 'size_mb', 'size_gb']] = df_test.apply(sizes_pass_series_return_tuple, axis=1, result_type="expand")
print('
Tuples (pass series, return tuple then zip, new columns dont exist):')
df_test = create_new_df_test()
%timeit df_test['size_kb'], df_test['size_mb'], df_test['size_gb'] = zip(*df_test.apply(sizes_pass_series_return_tuple, axis=1))
print('Tuples (pass series, return tuple then zip, new columns exist):')
df_test = create_new_df_test()
df_test = pd.concat([df_test, pd.DataFrame(columns=['size_kb', 'size_mb', 'size_gb'])])
%timeit df_test['size_kb'], df_test['size_mb'], df_test['size_gb'] = zip(*df_test.apply(sizes_pass_series_return_tuple, axis=1))
print('
Tuples (pass value, return tuple then zip, new columns dont exist):')
df_test = create_new_df_test()
%timeit df_test['size_kb'], df_test['size_mb'], df_test['size_gb'] = zip(*df_test['size'].apply(sizes_pass_value_return_tuple))
print('Tuples (pass value, return tuple then zip, new columns exist):')
df_test = create_new_df_test()
df_test = pd.concat([df_test, pd.DataFrame(columns=['size_kb', 'size_mb', 'size_gb'])])
%timeit df_test['size_kb'], df_test['size_mb'], df_test['size_gb'] = zip(*df_test['size'].apply(sizes_pass_value_return_tuple))
解决方案 7:
使用 apply 和 lambda 可以相当快速地完成此操作。只需将多个值作为列表返回,然后使用 to_list()
import pandas as pd
dat = [ [i, 10*i] for i in range(100000)]
df = pd.DataFrame(dat, columns = ["a","b"])
def add_and_div(x):
add = x + 3
div = x / 3
return [add, div]
start = time.time()
df[['c','d']] = df['a'].apply(lambda x: add_and_div(x)).to_list()
end = time.time()
print(end-start) # output: 0.27606
解决方案 8:
简单又容易:
def func(item_df):
return [1,'Label 1'] if item_df['col_0'] > 0 else [0,'Label 0']
my_df[['col_1','col2']] = my_df.apply(func, axis=1,result_type='expand')
解决方案 9:
我相信 1.1 版本破坏了这里最佳答案中所建议的行为。
import pandas as pd
def test_func(row):
row['c'] = str(row['a']) + str(row['b'])
row['d'] = row['a'] + 1
return row
df = pd.DataFrame({'a': [1, 2, 3], 'b': ['i', 'j', 'k']})
df.apply(test_func, axis=1)
上述代码在 pandas 1.1.0 上运行返回:
a b c d
0 1 i 1i 2
1 1 i 1i 2
2 1 i 1i 2
而在 pandas 1.0.5 中它返回:
a b c d
0 1 i 1i 2
1 2 j 2j 3
2 3 k 3k 4
我想这正是您所期望的。
不确定发行说明如何解释这种行为,但是如此处所述,通过复制原始行来避免原始行的变异会恢复旧的行为。即:
def test_func(row):
row = row.copy() # <---- Avoid mutating the original reference
row['c'] = str(row['a']) + str(row['b'])
row['d'] = row['a'] + 1
return row
解决方案 10:
如果您改用 numpy 进行数学运算,则可以比此处的最佳答案快 40 多倍。改编 @Rocky K 的前两个答案。主要区别在于实际运行 120k 行的 df。当您按数组方式应用函数(而不是按值方式应用函数)时,Numpy 的数学运算速度要快得多。到目前为止,最好的答案是第三个,因为它使用 numpy 进行数学运算。还请注意,它只计算 10242 和 10243 一次,而不是每行一次,从而节省了 240k 次计算。以下是我的计算机上的计时:
Tuples (pass value, return tuple then zip, new columns dont exist):
Runtime: 10.935037851333618
Tuples (pass value, return tuple then zip, new columns exist):
Runtime: 11.120025157928467
Use numpy for math portions:
Runtime: 0.24799370765686035
这是我用来计算这些时间的脚本(改编自 Rocky K):
import numpy as np
import pandas as pd
import locale
import time
size = np.random.random(120000) * 1000000000
data = pd.DataFrame({'Size': size})
def sizes_pass_value_return_tuple(value):
a = locale.format_string("%.1f", value / 1024.0, grouping=True) + ' KB'
b = locale.format_string("%.1f", value / 1024.0 ** 2, grouping=True) + ' MB'
c = locale.format_string("%.1f", value / 1024.0 ** 3, grouping=True) + ' GB'
return a, b, c
print('
Tuples (pass value, return tuple then zip, new columns dont exist):')
df1 = data.copy()
start = time.time()
df1['size_kb'], df1['size_mb'], df1['size_gb'] = zip(*df1['Size'].apply(sizes_pass_value_return_tuple))
end = time.time()
print('Runtime:', end - start, '
')
print('Tuples (pass value, return tuple then zip, new columns exist):')
df2 = data.copy()
start = time.time()
df2 = pd.concat([df2, pd.DataFrame(columns=['size_kb', 'size_mb', 'size_gb'])])
df2['size_kb'], df2['size_mb'], df2['size_gb'] = zip(*df2['Size'].apply(sizes_pass_value_return_tuple))
end = time.time()
print('Runtime:', end - start, '
')
print('Use numpy for math portions:')
df3 = data.copy()
start = time.time()
df3['size_kb'] = (df3.Size.values / 1024).round(1)
df3['size_kb'] = df3.size_kb.astype(str) + ' KB'
df3['size_mb'] = (df3.Size.values / 1024 ** 2).round(1)
df3['size_mb'] = df3.size_mb.astype(str) + ' MB'
df3['size_gb'] = (df3.Size.values / 1024 ** 3).round(1)
df3['size_gb'] = df3.size_gb.astype(str) + ' GB'
end = time.time()
print('Runtime:', end - start, '
')
解决方案 11:
一般来说,要返回多个值,我会这样做
def gimmeMultiple(group):
x1 = 1
x2 = 2
return array([[1, 2]])
def gimmeMultipleDf(group):
x1 = 1
x2 = 2
return pd.DataFrame(array([[1,2]]), columns=['x1', 'x2'])
df['size'].astype(int).apply(gimmeMultiple)
df['size'].astype(int).apply(gimmeMultipleDf)
返回数据框确实有其好处,但有时不是必需的。您可以查看apply()
返回的内容并尝试一下函数 ;)
解决方案 12:
它提供了一个包含原始数据框中的两列的新数据框。
import pandas as pd
df = ...
df_with_two_columns = df.apply(lambda row:pd.Series([row['column_1'], row['column_2']], index=['column_1', 'column_2']),axis = 1)
解决方案 13:
我想在 groupby 上使用 apply。我尝试使用您在此处建议的方法。它确实在一定程度上帮助了我,但并非完全有效。
添加result_type='expand'
不起作用(因为我使用应用在 Series 上而不是 DataFrame 上?)并且zip(*___)
我丢失了索引。
如果其他人遇到同样的问题,以下是我(最终)解决问题的方法:
dfg = df.groupby(by=['Column1','Column2']).Column3.apply(myfunc)
dfres = pd.DataFrame()
dfres['a'], dfres['b'], dfres['c'] = (dfg.apply(lambda x: x[0]), dfg.apply(lambda x: x[1]), dfg.apply(lambda x: x[2]))
或者如果你知道更好的方法,请告诉我。
如果这超出了本次讨论的范围,请告诉我。
扫码咨询,免费领取项目管理大礼包!