从apply pandas返回多个列

Pau*_*est 63 python pandas

我有一个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]
Run Code Online (Sandbox Code Playgroud)

我已经运行了超过120,000行,每列需要大约2.97秒*3 = ~9秒根据%timeit.

无论如何我可以加快速度吗?例如,我可以不应用一次从应用和运行它返回一个列3次,我可以在一次传递中返回所有三列以插回原始数据帧吗?

我发现的其他问题都想要获取多个值并返回单个值.我想获取单个值并返回多个列.

Nel*_*z11 71

这是一个老问题,但为了完整性,您可以从包含新数据的应用函数返回一个Series,从而无需迭代三次.传递axis=1给apply函数将函数应用于sizes数据帧的每一行,返回一系列以添加到新的数据帧.此系列包含新值以及原始数据.

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)
Run Code Online (Sandbox Code Playgroud)

  • 我很惊讶它花了将近2年没有正确的答案.我正在寻找别的东西,偶然发现了这一点.希望有用的还为时不晚! (7认同)
  • 这个答案中的“ rows_list”是什么? (2认同)
  • @Nelz11我建议您使用问题中提供的相同示例而不是“rows_list”公式,以便您的答案编译时不会出现任何问题(另请参阅@David Stansby评论)。我建议将此作为编辑以避免您的麻烦,但显然版主更喜欢评论而不是编辑。 (2认同)

Jes*_*sse 41

使用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))
Run Code Online (Sandbox Code Playgroud)

测试结果如下:

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
Run Code Online (Sandbox Code Playgroud)


jau*_*net 30

一些当前的回复工作正常,但我想提供另一个,也许更多"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("%.1f", s['size'] / 1024.0, grouping=True) + ' KB'
  b = locale.format("%.1f", s['size'] / 1024.0 ** 2, grouping=True) + ' MB'
  c = locale.format("%.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")
Run Code Online (Sandbox Code Playgroud)

请注意,技巧是在result_type参数上apply,将其结果扩展为DataFrame可以直接分配给新/旧列的参数.

  • 这太棒了。但是请注意,这是针对熊猫0.23的,尚未完全淘汰。 (2认同)
  • 这是最佳的答案。谢谢 (2认同)
  • 缺点是这适用于 DataFrame 上的“.apply()”,而不适用于 Series。另外,对于 pandas 1.1.5,这根本不起作用。 (2认同)

Roc*_*y K 16

顶级答案之间的表现差异很大,Jesse & famaral42 已经讨论过这一点,但值得分享顶级答案之间的公平比较,并详细阐述 Jesse 答案的一个微妙但重要的细节:参数传递给功能,也会影响性能

(Python 3.7.4,熊猫 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
Run Code Online (Sandbox Code Playgroud)

结果如下:

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

Run Code Online (Sandbox Code Playgroud)

请注意如何返回元组是最快的方法,但是什么传递作为一个参数,也是影响性能。代码中的差异是细微的,但性能改进是显着的。

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

Run Code Online (Sandbox Code Playgroud)

在某些情况下(#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))
Run Code Online (Sandbox Code Playgroud)


fam*_*l42 13

真的很酷的答案!谢谢杰西和 jaumebonet!只是关于以下方面的一些观察:

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

虽然 expand 更优雅(pandifyed),但zip 至少 **2x 快。在下面这个简单的示例中,我的速度提高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))
Run Code Online (Sandbox Code Playgroud)

  • “expand”不适用于 pandas 1.3,但 zip 却能发挥神奇作用!谢谢 (3认同)

bor*_*ked 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
Run Code Online (Sandbox Code Playgroud)


alv*_*tes 5

只是另一种可读的方式。此代码将添加三个新列及其值,并返回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)
Run Code Online (Sandbox Code Playgroud)

来自以下的一般示例:https : //pandas.pydata.org/pandas-docs/stable/genic/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
Run Code Online (Sandbox Code Playgroud)

  • 如果只有一个参数,则不需要 lambda 转换:“df.apply(x, axis=1)”。而且,它的解决方案与杰西的解决方案基本相同。 (2认同)

小智 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')
Run Code Online (Sandbox Code Playgroud)