将pandas函数应用于列以创建多个新列?

367
如何在pandas中实现这个需求: 我有一个名为extract_text_features的函数,用于处理单个文本列,并返回多个输出列。具体来说,该函数返回6个值。 该函数可以正常工作,但似乎没有正确的返回类型(pandas DataFrame/ numpy array/ Python list),以便将输出正确地分配给df.ix[: ,10:16] = df.textcol.map(extract_text_features) 因此,我认为我需要回到使用df.iterrows()迭代,就像这里所述一样? 更新: 使用df.iterrows()迭代至少比使用lambda表达式调用.map()慢20倍,因此我放弃了使用df.iterrows()方法,并将函数拆分成了六个独立的.map(lambda ...)调用。 更新2:此问题是在 v0.11.0 左右提出的,在那个版本之前,df.apply功能的可用性得到了改善,或者在 df.assign() 中添加了 在v0.16中添加。因此,该问题和答案的大部分内容不太相关。

1
我不认为你可以按照你所写的方式进行多重赋值:df.ix[:, 10:16]。我觉得你需要将特征与数据集进行合并。 - Zelazny7
2
对于那些想要更高性能解决方案的人,请查看下面的链接(https://dev59.com/8mQo5IYBdhLWcg3wMtC2#47097625),它不使用`apply`。 - Ted Petrou
大多数使用pandas的数字操作都可以进行向量化处理,这意味着它们比传统迭代要快得多。另一方面,某些操作(例如字符串和正则表达式)本质上很难进行向量化处理。在这种情况下,重要的是要了解如何循环遍历您的数据。有关何时以及如何循环遍历数据的更多信息,请阅读使用Pandas的For循环-我应该关心什么? - cs95
@coldspeed:主要问题不是在几个选项中选择哪一个性能更高,而是在v0.11.0左右,为了让它正常工作而与Pandas语法进行斗争。 - smci
实际上,这个注释是为了未来寻找迭代解决方案的读者而写的,无论他们是不知道更好的方法,还是已经知道自己在做什么。 - cs95
显示剩余2条评论
17个回答

291

我通常使用zip来完成此操作:

>>> df = pd.DataFrame([[i] for i in range(10)], columns=['num'])
>>> df
    num
0    0
1    1
2    2
3    3
4    4
5    5
6    6
7    7
8    8
9    9

>>> def powers(x):
>>>     return x, x**2, x**3, x**4, x**5, x**6

>>> df['p1'], df['p2'], df['p3'], df['p4'], df['p5'], df['p6'] = \
>>>     zip(*df['num'].map(powers))

>>> df
        num     p1      p2      p3      p4      p5      p6
0       0       0       0       0       0       0       0
1       1       1       1       1       1       1       1
2       2       2       4       8       16      32      64
3       3       3       9       27      81      243     729
4       4       4       16      64      256     1024    4096
5       5       5       25      125     625     3125    15625
6       6       6       36      216     1296    7776    46656
7       7       7       49      343     2401    16807   117649
8       8       8       64      512     4096    32768   262144
9       9       9       81      729     6561    59049   531441

16
如果您需要添加的列不是6列而是50列,该怎么办?请您提供更多信息。 - max
20
@max temp = list(zip(*df['num'].map(powers))); for i, c in enumerate(columns): df[c] = temp[c] 翻译:将df['num']的每个元素进行powers操作,然后将结果转置后储存在temp变量中。接着,根据索引值和列名遍历columns,将temp中相应列的值分别存入df的对应列中。 - ostrokach
9
我认为你的意思是 for i, c in enumerate(columns): df[c] = temp[i]。由于这一步,我真正理解了 enumerate 的用途:D。 - rocarvaj
9
到目前为止,这是我遇到的最优雅和易读的解决方案。除非出现性能问题,否则使用“zip(*df ['col'].map(function))”这个习语可能是正确的方法。 - François Leblanc
@rocarvaj 虽然我留了一条相当晚的评论,但如果有人发现我的错误,我会感激你提供的见解。很显然这里发布的两个不起作用,似乎应该是 for i, c in enumerate(temp): df[c] = temp[c] - dia
1
@XiaoyuLu 请查看 https://dev59.com/rXA75IYBdhLWcg3wP2kg。 - ostrokach

249
在2020年,我使用了apply()函数,并设置参数为result_type='expand'
applied_df = df.apply(lambda row: fn(row.text), axis='columns', result_type='expand')
df = pd.concat([df, applied_df], axis='columns')

fn() 应该返回一个 dict,其键将是新列的名称。

或者,您也可以通过指定列名来进行一行代码:

df[["col1", "col2", ...]] = df.apply(lambda row: fn(row.text), axis='columns', result_type='expand')

24
现在就是这么做的! - Make42
2
这在2020年开箱即用,而许多其他问题则没有。此外,它不使用pd.Series,这在性能问题方面总是很好的。 - Théo Rubenach
21
如果你传给 df.apply 的函数返回一个字典,那么列的名称将按照字典键来命名。 - Seb
9
appiled_df =替换为df[["col1", "col2", ...]] =可以将其简化为一行代码。这也会给出具有命名的列。 - NumesSanguis
8
жҲ‘йңҖиҰҒзҡ„жҳҜиҝҷдёӘзӯ”жЎҲдёӯзҡ„result_type='expand'гҖӮдҫӢеҰӮпјҢdf[new_cols] = df.apply(extract_text_features, axis=1, result_type='expand')е°ұеҸҜд»ҘдәҶгҖӮиҷҪ然дҪ йңҖиҰҒзҹҘйҒ“ж–°еҲ—зҡ„еҗҚз§°гҖӮ - Ufos
显示剩余6条评论

135

在 user1827356 的回答基础上,你可以使用 df.merge 一次性完成分配:

df.merge(df.textcol.apply(lambda s: pd.Series({'feature1':s+1, 'feature2':s-1})), 
    left_index=True, right_index=True)

    textcol  feature1  feature2
0  0.772692  1.772692 -0.227308
1  0.857210  1.857210 -0.142790
2  0.065639  1.065639 -0.934361
3  0.819160  1.819160 -0.180840
4  0.088212  1.088212 -0.911788

编辑:请注意巨大的内存消耗和低速度:https://ys-l.github.io/posts/2015/08/28/how-not-to-use-pandas-apply/


2
只是出于好奇,这样做是否会使用大量内存?我正在对一个包含250万行的数据框执行此操作,几乎遇到了内存问题(而且比仅返回1列要慢得多)。 - Jeffrey04
2
我认为'df.join(df.textcol.apply(lambda s: pd.Series({'feature1':s+1, 'feature2':s-1})))'是更好的选择。 - skt7
@ShivamKThakkar,你认为你的建议为什么会是更好的选择?你认为它会更有效率还是更节省内存成本? - tsando
4
请考虑速度和内存占用:https://ys-l.github.io/posts/2015/08/28/how-not-to-use-pandas-apply/ - Make42
截至2023年,这个巨大的内存消耗仍然存在吗? - Eric Burel

94

这是我过去所做的事情

df = pd.DataFrame({'textcol' : np.random.rand(5)})

df
    textcol
0  0.626524
1  0.119967
2  0.803650
3  0.100880
4  0.017859

df.textcol.apply(lambda s: pd.Series({'feature1':s+1, 'feature2':s-1}))
   feature1  feature2
0  1.626524 -0.373476
1  1.119967 -0.880033
2  1.803650 -0.196350
3  1.100880 -0.899120
4  1.017859 -0.982141

编辑以确保完整性

pd.concat([df, df.textcol.apply(lambda s: pd.Series({'feature1':s+1, 'feature2':s-1}))], axis=1)
    textcol feature1  feature2
0  0.626524 1.626524 -0.373476
1  0.119967 1.119967 -0.880033
2  0.803650 1.803650 -0.196350
3  0.100880 1.100880 -0.899120
4  0.017859 1.017859 -0.982141

concat() 看起来比 merge() 更简单,用于将新列连接到原始数据框中。 - cumin
6
好的回答,如果您在应用函数时指定了列,那么就不需要使用字典或合并操作。例如:df[['col1', 'col2']] = df['col3'].apply(lambda x: pd.Series('val1', 'val2')) - Matt

88

这是完成95%使用案例的正确且最简单的方法:

>>> df = pd.DataFrame(zip(*[range(10)]), columns=['num'])
>>> df
    num
0    0
1    1
2    2
3    3
4    4
5    5

>>> def example(x):
...     x['p1'] = x['num']**2
...     x['p2'] = x['num']**3
...     x['p3'] = x['num']**4
...     return x

>>> df = df.apply(example, axis=1)
>>> df
    num  p1  p2  p3
0    0   0   0    0
1    1   1   1    1
2    2   4   8   16
3    3   9  27   81
4    4  16  64  256

你应该这样写:df = df.apply(example(df), axis=1),如果我错了,请纠正我,我只是一个新手。 - user299791
1
@user299791,在这种情况下,您将示例视为一等对象,因此您正在传递函数本身。该函数将应用于每一行。 - Michael David Watson
嗨,Michael,你的答案帮助了我解决了问题。毫无疑问,你的解决方案比原始的pandas df.assign()方法更好,因为这是每列一次性的。使用assign(),如果你想创建两个新列,你必须使用df1来处理df以获得新的列1,然后使用df2来处理df1以创建第二个新列...这相当单调乏味。但是你的方法拯救了我的生命!!!谢谢!!! - ACuriousCat
1
那不会对每一行都运行列赋值代码吗?返回一个pd.Series({k:v})并像Ewan的回答中那样序列化列赋值,这样不是更好吗? - Denis de Bernardy
1
如果有帮助的话,虽然这种方法是正确的,也是所有提出的解决方案中最简单的,但直接像这样更新行最终变得非常慢 - 比使用'expand' + pd.concat解决方案要慢一个数量级。 - Dmytro Bugayev
显示剩余2条评论

63

只需使用 result_type="expand"

df = pd.DataFrame(np.random.randint(0,10,(10,2)), columns=["random", "a"])
df[["sq_a","cube_a"]] = df.apply(lambda x: [x.a**2, x.a**3], axis=1, result_type="expand")

9
需要翻译的内容:It helps to point out that option is new in 0.23. The question was asked back on 0.11。这个选项是在0.23版本中新增的,需要指出来。该问题是在0.11版本时提出的。 - smci
1
很好,这很简单,而且仍然非常整洁。这就是我在寻找的那个。谢谢。 - Isaac Sim
1
重复了之前的答案:https://dev59.com/8mQo5IYBdhLWcg3wMtC2#52363890 - tar
2
@tar 实际上第二行是不同的,对我来说非常有帮助! - Aaron Gibralter

43

对我而言,这个方法有效:

输入 df

df = pd.DataFrame({'col x': [1,2,3]})
   col x
0      1
1      2
2      3

函数

def f(x):
    return pd.Series([x*x, x*x*x])

创建两个新列:

df[['square x', 'cube x']] = df['col x'].apply(f)

输出:

   col x  square x  cube x
0      1         1       1
1      2         4       8
2      3         9      27

23

摘要:如果您只想创建几列,请使用 df[['new_col1','new_col2']] = df [['data1','data2']]. apply(function_of_your_choosing(x),axis = 1)

对于这种解决方案,您要创建的新列数必须等于您在.apply()函数中使用为输入的列数。如果您想做其他事情,请查看其他答案。

详情 假设您有一个两列的数据框。第一列是一个人10岁时的身高; 第二列是该人20岁时的身高。

假设您需要计算每个人身高的平均值和总和。这是每行两个值。

您可以通过以下即将应用的函数来完成此操作:

def mean_and_sum(x):
    """
    Calculates the mean and sum of two heights.
    Parameters:
    :x -- the values in the row this function is applied to. Could also work on a list or a tuple.
    """

    sum=x[0]+x[1]
    mean=sum/2
    return [mean,sum]
您可以像这样使用此函数:
 df[['height_at_age_10','height_at_age_20']].apply(mean_and_sum(x),axis=1)

明确一点:这个 apply 函数会接收子数据框中每行的值并返回一个列表。

但是,如果您执行以下操作:

df['Mean_&_Sum'] = df[['height_at_age_10','height_at_age_20']].apply(mean_and_sum(x),axis=1)

您需要创建一个新列,其中包含 [mean,sum] 列表,但您很可能希望避免这样做,因为这将需要另一个 Lambda/Apply。

相反,您想要将每个值分解成自己的列。为此,您可以同时创建两个列:

df[['Mean','Sum']] = df[['height_at_age_10','height_at_age_20']]
.apply(mean_and_sum(x),axis=1)

5
对于 pandas 0.23 版本,你需要使用以下语法:df["mean"], df["sum"] = df[['height_at_age_10','height_at_age_20']].apply(mean_and_sum(x),axis=1)。其中的 mean_and_sum 是一个函数名,它将应用于 DataFrame 的每行数据,并返回两个值:均值和总和。最终,这两个值将存储在新的 "mean" 和 "sum" 列中。 - SummerEla
1
这个函数可能会引发错误。返回函数必须是return pd.Series([mean,sum]) - Kanishk Mair

14

我已经尝试了几种实现方法,但是这里介绍的方式(返回一个pandas series)似乎不是最有效率的。

如果我们从一个较大的随机数据dataframe开始:

# Setup a dataframe of random numbers and create a 
df = pd.DataFrame(np.random.randn(10000,3),columns=list('ABC'))
df['D'] = df.apply(lambda r: ':'.join(map(str, (r.A, r.B, r.C))), axis=1)
columns = 'new_a', 'new_b', 'new_c'

这里展示的例子:

# Create the dataframe by returning a series
def method_b(v):
    return pd.Series({k: v for k, v in zip(columns, v.split(':'))})
%timeit -n10 -r3 df.D.apply(method_b)

3次循环取最佳结果:每个循环花费2.77秒

另一种方法:

# Create a dataframe from a series of tuples
def method_a(v):
    return v.split(':')
%timeit -n10 -r3 pd.DataFrame(df.D.apply(method_a).tolist(), columns=columns)

每轮循环10次,3次中取最佳结果平均每轮耗时8.85毫秒

据我估算,将一系列元组转换为DataFrame更加高效。但如果我的推理有误,我很想听听其他人的想法。


这真的非常有用!与返回系列方法的函数相比,我获得了30倍的加速。 - Pushkar Nimkar

13

对于大量数据,被接受的解决方案将非常缓慢。得票最多的解决方案有些难以阅读,在处理数值数据时也很慢。如果每个新列可以独立于其他列进行计算,我会直接分配它们而不使用apply

使用虚假字符数据的示例

在DataFrame中创建100,000个字符串

df = pd.DataFrame(np.random.choice(['he jumped', 'she ran', 'they hiked'],
                                   size=100000, replace=True),
                  columns=['words'])
df.head()
        words
0     she ran
1     she ran
2  they hiked
3  they hiked
4  they hiked

假设我们想提取一些文本特征,就像原问题中所做的那样。例如,让我们提取第一个字符,计算字母'e'出现的次数并将短语大写。

df['first'] = df['words'].str[0]
df['count_e'] = df['words'].str.count('e')
df['cap'] = df['words'].str.capitalize()
df.head()
        words first  count_e         cap
0     she ran     s        1     She ran
1     she ran     s        1     She ran
2  they hiked     t        2  They hiked
3  they hiked     t        2  They hiked
4  they hiked     t        2  They hiked

时间

%%timeit
df['first'] = df['words'].str[0]
df['count_e'] = df['words'].str.count('e')
df['cap'] = df['words'].str.capitalize()
127 ms ± 585 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

def extract_text_features(x):
    return x[0], x.count('e'), x.capitalize()

%timeit df['first'], df['count_e'], df['cap'] = zip(*df['words'].apply(extract_text_features))
101 ms ± 2.96 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

令人惊讶的是,通过遍历每个值,您可以获得更好的性能

%%timeit
a,b,c = [], [], []
for s in df['words']:
    a.append(s[0]), b.append(s.count('e')), c.append(s.capitalize())

df['first'] = a
df['count_e'] = b
df['cap'] = c
79.1 ms ± 294 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

使用虚假数字数据的另一个示例

生成100万个随机数,并测试上述powers函数。

df = pd.DataFrame(np.random.rand(1000000), columns=['num'])


def powers(x):
    return x, x**2, x**3, x**4, x**5, x**6

%%timeit
df['p1'], df['p2'], df['p3'], df['p4'], df['p5'], df['p6'] = \
       zip(*df['num'].map(powers))
1.35 s ± 83.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

给每列分配值的速度提高了25倍,并且非常易于阅读:

%%timeit 
df['p1'] = df['num'] ** 1
df['p2'] = df['num'] ** 2
df['p3'] = df['num'] ** 3
df['p4'] = df['num'] ** 4
df['p5'] = df['num'] ** 5
df['p6'] = df['num'] ** 6
51.6 ms ± 1.9 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

我在这里提供了更详细的解释,说明为什么通常不建议使用apply方法。


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