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


215

如何在熊猫中做到这一点:

extract_text_features在单个文本列上有一个函数,返回多个输出列。具体来说,该函数返回6个值。

该函数有效,但是似乎没有任何合适的返回类型(pandas DataFrame / numpy array / Python list),以便可以正确分配输出 df.ix[: ,10:16] = df.textcol.map(extract_text_features)

所以,我想我需要回落到与迭代df.iterrows(),按照这个

更新:进行迭代的df.iterrows()速度至少要慢20倍,所以我放弃了该功能并将其拆分为六个不同的.map(lambda ...)调用。

更新2:这个问题是在v0.11.0左右问的。因此,许多问题和答案不太相关。


1
我认为您无法以书面形式进行多次分配df.ix[: ,10:16]。我认为您必须将merge特征放入数据集中。
Zelazny13年

1
对于那些想要性能更高的解决方案的人,请检查以下不使用的解决方案apply
Ted Petrou

大多数使用熊猫的数字运算都可以向量化-这意味着它们比常规迭代要快得多。OTOH,某些操作(例如string和regex)本来就很难向量化。在这种情况下,了解如何遍历数据非常重要。有关何时以及如何遍历数据的更多信息,请阅读For Pandas的循环-我何时应该关心?
cs95

@coldspeed:主要问题不是在几个选项中选择哪个是性能更高的,而是与pandas语法作斗争以使其完全起作用,回到v0.11.0左右。
smci

的确,此评论面向正在寻找迭代解决方案的未来读者,他们要么不知道更好,要么知道他们在做什么。
cs95

Answers:


109

基于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})))'是一个更好的选择。
Shivam K. Thakkar,

@ShivamKThakkar为什么您认为您的建议会是更好的选择?您认为这会更有效还是会减少内存成本?
桑多


189

我通常使用以下方法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

8
但是,如果您添加了50列而不是6列,该怎么办?
最大

14
@maxtemp = list(zip(*df['num'].map(powers))); for i, c in enumerate(columns): df[c] = temp[c]
ostrokach 2015年

8
@ostrokach我想你的意思是for i, c in enumerate(columns): df[c] = temp[i]。有了这个,我才真正达到了enumerate:D 的目的
rocarvaj

4
这是迄今为止我遇到的最优雅,最易读的解决方案。除非遇到性能问题,否则成语zip(*df['col'].map(function))可能是解决之道。
弗朗索瓦·勒布朗


84

这是我过去所做的

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()更简单。
小茴香

2
一个不错的答案,如果您指定了Apply之外的列,则无需使用dict或mergedf[['col1', 'col2']] = df['col3'].apply(lambda x: pd.Series('val1', 'val2'))
Matt

65

对于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

嗨,迈克尔,您的回答对我的问题有所帮助。绝对您的解决方案比原始熊猫的df.assign()方法更好,因为这是每列一次。使用assign(),如果要创建2个新列,则必须使用df1对df进行操作以获取新的column1,然后使用df2对df1进行处理以创建第二个新列……这很单调。但是你的方法救了我的命!!!谢谢!!!
commentallezvous

1
每行是否运行一次列分配代码?pd.Series({k:v})像Ewan的回答一样,返回a 并序列化列分配会更好吗?
Denis de Bernardy '19

29

在2018年,我apply()与论点一起使用result_type='expand'

>>> appiled_df = df.apply(lambda row: fn(row.text), axis='columns', result_type='expand')
>>> df = pd.concat([df, appiled_df], axis='columns')

6
如今,这就是您的做法!
Make42

1
到2020年,这开箱即用,而其他许多问题都没有。此外,它不使用pd.Series 这始终是很好的关于性能问题
西奥Rubenach

1
这是一个很好的解决方案。唯一的问题是,您不能为2个新添加的列选择名称。您稍后需要做df.rename(columns = {0:'col1',1:'col2'})
pedram bashiri

2
@pedrambashiri如果传递给的函数df.apply返回a dict,则列将根据键命名。
勒布


22

摘要:如果只想创建几列,请使用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)

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

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

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

4
对于大熊猫0.23,您需要使用以下语法:df["mean"], df["sum"] = df[['height_at_age_10','height_at_age_20']] .apply(mean_and_sum(x),axis=1)
SummerEla

此功能可能会引发错误。返回函数必须为 return pd.Series([mean,sum])
Kanishk Mair

22

对我来说,这工作:

输入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])

创建2个新列:

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

13

我已经看过几种方法,这里显示的方法(返回熊猫系列)似乎并不是最有效的方法。

如果我们从随机数据的较大数据帧开始:

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

10个循环,每个循环最好3:2.77 s

替代方法:

# 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

9

对于大量数据,公认的解决方案将非常慢。投票数最多的解决方案有点难以阅读,而且对于数字数据也很慢。如果每个新列都可以独立于其他列进行计算,那么我将直接分配它们,而无需使用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)

伪造数值数据的另一个示例

创建一百万个随机数并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通常为什么不走这条路。


8

在另外两个类似的问题中发布了相同的答案。我更喜欢这样做的方式是将函数的返回值包装成一系列:

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

然后使用Apply如下创建单独的列:

df[['x**2','x**3']] = df.apply(lambda row: f(row['x']), axis=1)

1

您可以返回整行而不是值:

df = df.apply(extract_text_features,axis = 1)

函数返回行的位置

def extract_text_features(row):
      row['new_col1'] = value1
      row['new_col2'] = value2
      return row

不,我不想适用extract_text_features于df的每一列,仅适用于文本列df.textcol
smci

-2
def myfunc(a):
    return a * a

df['New Column'] = df['oldcolumn'].map(myfunc))

这对我有用。将使用已处理的旧列数据创建新列。


2
这不会返回“多个新列”
pedram bashiri

这不会返回“多个新列”,因此不会回答该问题。您能删除它吗?
smci
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.