从pandas apply()返回多列

229

我有一个名为df_test的pandas DataFrame。它包含了一个名为"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秒。

有没有什么方法可以让它更快?例如,我是否可以一次性返回所有三列而不是从apply返回一列并运行三次,然后再将其插入到原始数据框中?

我找到的其他问题都想要取多个值并返回单个值。 我想要获取单个值并返回多个列。


1
那些寻找这个问题的人可能会在这里找到一个更直接的解决方案:https://dev59.com/QlYN5IYBdhLWcg3wFk22 - zabop
这个回答解决了你的问题吗?将Pandas函数应用于列以创建多个新列? - Gonçalo Peres
13个回答

240

您可以从应用的函数中返回一个包含新数据的Series,避免了需要迭代三次。将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)

22
我很惊讶这个问题竟然没有得到正确的答案就已经过去了将近两年。我本来在找其他的东西时偶然发现了这个问题。希望现在使用还不算太晚! - Nelz11
36
这个答案中的 rows_list 是什么? - David Stansby
1
如果需要给 pd.Series 提供索引,您需要使用 pd.Series(data, index=...) 语法进行赋值。否则,当您尝试将结果重新分配回父数据框时,会出现加密错误。 - smci
9
我建议你使用问题中提供的相同示例,而不是使用“rows_list”表示方法,这样你的答案将无需任何问题即可编译(请参见@David Stansby的评论)。我建议这样修改以避免麻烦,但显然管理员更喜欢评论而不是编辑。 - gibbone
1
请注意,这可能会修改原始DataFrame,如果不希望出现这种情况,您可以使用df_test = df_test.copy().apply(sizes, axis=1)(但这当然可能会影响性能/利用率)。 - levant pied
显示剩余2条评论

200

使用apply和zip比Series方式快3倍。

def sizes(s):    
    return locale.format("%.1f", s / 1024.0, grouping=True) + ' KB', \
        locale.format("%.1f", s / 1024.0 ** 2, grouping=True) + ' MB', \
        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

1
@Jesse 在 pandas 1.1.* 中此方法已失效,如果在整个数据框上执行 apply 而非特定列,则会出现形状错误。 - godimedia
1
请注意,zip 方法不会保留正确的索引。然而,使用 result_type=expand 可以实现此功能。 - v.tralala
这是一个很好的答案,因为它避免了典型的“ValueError: Columns must be same length as key”错误。 - philshem
这在 pandas 1.1.5 中对我有效。 - Kyle
这个方法比我之前使用的方法(将字典解析为数据框,然后将它们全部连接起来)提供了惊人的加速。 - wordsforthewise
唯一的问题是如果你有很多列,你必须逐个指定每个列,而不能提供一个大列表给数据框。 - wordsforthewise

156

一些当前的回答可以很好地工作,但我想提供另一种可能更“pandifyed”的选项。在当前版本的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")

请注意,关键在于apply函数的result_type参数,它将扩展其结果为一个可直接分配给新/旧列的DataFrame


8
不足之处在于这只能在DataFrame上使用.apply(),而不能在Series上使用。另外,在pandas 1.1.5中,这根本行不通。 - MERose
@MERose +1,因为他提到它在Series上不起作用(apply()没有axis参数),但是在我的1.1.5版本中可以使用。 - Skippy le Grand Gourou
1
对我来说,这是最优雅和内置支持的解决方案。在 pandas 1.3.0 上运行良好。 - Pedro Henrique Cardoso

40

非常棒的答案!感谢Jesse和jaumebonet!以下是一些观察:

  • zip(* ...
  • ... result_type="expand")

虽然expand更加优雅(变成了Pandify),但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))

3
“expand”在pandas 1.3中无法使用,但是zip函数非常有效!谢谢。 - wts
第二个解决方案使用压缩文件非常成功。 - Kiran

31

另一种易读的方法。此代码将添加三个新列及其值,返回系列而不使用apply函数中的参数。

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

2
只需一个参数,lambda转换就不是必要的:df.apply(x, axis=1)。此外,这基本上是Jesse的解决方案。 - MERose

24

顶级答案之间的性能差异显着,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('\nAccepted 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('\nPandafied (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('\nTuples (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('\nTuples (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))

1
这真的很有趣...也是一些值得思考的东西。我总是倾向于使用库提供的解决方案,但这里的性能差异不能被忽视。现在我想知道如果只传递值而不是系列,我的解决方案会表现如何。感谢您进行了良好的分析! - jaumebonet
1
我更希望有一个简单的概括来说明“这个方法是最快的”,而不是几段针对不同单位(毫秒与微秒)的冗长比较。虽然这些比较对于我们来说并不困难,但请考虑一下所有穷尽了谷歌搜索却仍未得到答案的用户和 Stack Overflow 的初衷。 - Jimmy Carter
感谢提供代码 - 让我们清楚地知道测量的内容,我会在我的机器上重新运行它并测试不同的情况。我发现如果测试1000行,结果会有很大不同:#3和#4之间只相差50%,但#1比1a慢8倍。 - ipap

10
使用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

5

简单易懂:

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

3
我相信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

1
通常为了返回多个值,我会这样做。
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() 返回的内容,并尝试使用一些函数 ;)

谢谢提供这个示例。但是,它不能输出所有结果的单个数据框。当我尝试将其添加回原始数据框时,会出现“ValueError:数组无法广播到正确的形状”。 - PaulMest
你能提供一些产生小数据样本的代码吗? - FooBar
当然可以。我刚刚在我的原始帖子中更新了代码,包括示例数据和输出结果。 - PaulMest

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