应用熊猫功能列创建多个新列?

如何在熊猫身上做到这一点:

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

函数可以工作,但是似乎没有任何合适的返回类型(pandas DataFrame/ numpy数组/ Python列表),从而可以正确地分配输出df.ix[: ,10:16] = df.textcol.map(extract_text_features)

所以我认为我需要回到用df.iterrows()迭代,就像根据?

< p >更新: 使用df.iterrows()进行迭代至少要慢20倍,因此我放弃并将该函数分解为6个不同的.map(lambda ...)调用

更新2:这个问题是在v0.11.0前后提出的,在可用性df.apply得到改进或df.assign()v0.16新增之前。因此,很多问题和答案都不太相关。

299057 次浏览

这是我过去所做的

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

基于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/ !

. Please be aware of memory consumption and low speed: https://ys-l.github.io/posts/2015/08/28/how-not-to-use-pandas-apply/

我通常使用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

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

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

# 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秒每圈

另一种方法:

# 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要有效得多。如果我的工作中出现了错误,我很想听听人们的想法。

对于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[['new_col1','new_col2']] = df[['data1','data2']].apply( function_of_your_choosing(x), axis=1)

对于这个解决方案,创建的新列数必须等于用作.apply()函数输入的列数。如果你想做别的事情,看看其他答案。

< >强细节 假设你有两列数据框架。第一列是一个人10岁时的身高;第二个是20岁时的身高。< / p >

假设你需要计算每个人身高的平均值和每个人身高的和。每一行有两个值。

你可以通过下面即将应用的函数来实现:

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)

公认的解决方案对于大量数据来说将会非常慢。获得最多赞数的解决方案读起来有点困难,而且处理数字数据也很慢。如果每一列都可以独立于其他列计算,我将直接为每一列赋值,而不使用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通常不是正确的方法,我用详情请点击这里做了类似的回答。

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

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

函数在哪里返回行

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

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

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

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)

对我来说,这很有效:

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

只需使用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")

我有一个更复杂的情况,数据集有一个嵌套结构:

import json
data = '{"TextID":{"0":"0038f0569e","1":"003eb6998d","2":"006da49ea0"},"Summary":{"0":{"Crisis_Level":["c"],"Type":["d"],"Special_Date":["a"]},"1":{"Crisis_Level":["d"],"Type":["a","d"],"Special_Date":["a"]},"2":{"Crisis_Level":["d"],"Type":["a"],"Special_Date":["a"]}}}'
df = pd.DataFrame.from_dict(json.loads(data))
print(df)

输出:

        TextID                                            Summary
0  0038f0569e  {'Crisis_Level': ['c'], 'Type': ['d'], 'Specia...
1  003eb6998d  {'Crisis_Level': ['d'], 'Type': ['a', 'd'], 'S...
2  006da49ea0  {'Crisis_Level': ['d'], 'Type': ['a'], 'Specia...

Summary列包含dict对象,所以我使用applyfrom_dictstack来提取每行dict:

df2 = df.apply(
lambda x: pd.DataFrame.from_dict(x[1], orient='index').stack(), axis=1)
print(df2)

输出:

    Crisis_Level Special_Date Type
0            0    0    1
0            c            a    d  NaN
1            d            a    a    d
2            d            a    a  NaN

看起来不错,但是缺少TextID列。为了返回TextID列,我尝试了三种方法:

  1. 修改apply以返回多个列:

    df_tmp = df.copy()
    
    
    df_tmp[['TextID', 'Summary']] = df.apply(
    lambda x: pd.Series([x[0], pd.DataFrame.from_dict(x[1], orient='index').stack()]), axis=1)
    print(df_tmp)
    

    输出:

        TextID                                            Summary
    0  0038f0569e  Crisis_Level  0    c
    Type          0    d
    Spec...
    1  003eb6998d  Crisis_Level  0    d
    Type          0    a
    ...
    2  006da49ea0  Crisis_Level  0    d
    Type          0    a
    Spec...
    

    但这不是我想要的,Summary结构是扁平的。

  2. < p >使用pd.concat:

    df_tmp2 = pd.concat([df['TextID'], df2], axis=1)
    print(df_tmp2)
    

    输出:

        TextID (Crisis_Level, 0) (Special_Date, 0) (Type, 0) (Type, 1)
    0  0038f0569e                 c                 a         d       NaN
    1  003eb6998d                 d                 a         a         d
    2  006da49ea0                 d                 a         a       NaN
    

    看起来很好,MultiIndex列结构被保存为元组。但检查列类型:

    df_tmp2.columns
    

    输出:

    Index(['TextID', ('Crisis_Level', 0), ('Special_Date', 0), ('Type', 0),
    ('Type', 1)],
    dtype='object')
    

    就像一个普通的Index类,而不是MultiIndex类。

  3. < p >使用set_index:

    在一些复杂的apply函数和reset_index函数之后,将你想保留的所有列转换为行索引,以返回列:

    df_tmp3 = df.set_index('TextID')
    
    
    df_tmp3 = df_tmp3.apply(
    lambda x: pd.DataFrame.from_dict(x[0], orient='index').stack(), axis=1)
    
    
    df_tmp3 = df_tmp3.reset_index(level=0)
    print(df_tmp3)
    

    输出:

        TextID Crisis_Level Special_Date Type
    0            0    0    1
    0  0038f0569e            c            a    d  NaN
    1  003eb6998d            d            a    a    d
    2  006da49ea0            d            a    a  NaN
    

    检查列的类型

    df_tmp3.columns
    

    输出:

    MultiIndex(levels=[['Crisis_Level', 'Special_Date', 'Type', 'TextID'], [0, 1, '']],
    codes=[[3, 0, 1, 2, 2], [2, 0, 0, 0, 1]])
    

因此,如果你的apply函数将返回MultiIndex列,而你想保留它,你可能想尝试第三种方法。

def extract_text_features(feature):
...
...
return pd.Series((feature1, feature2))


df[['NewFeature1', 'NewFeature1']] = df[['feature']].apply(extract_text_features, axis=1)


在这里,具有单个特征的a数据帧被转换为两个新特征。 你也可以试一下

这对我来说很管用:

import pandas as pd
import numpy as np
future = pd.DataFrame(
pd.date_range('2022-09-01',periods=360),
columns=['date']
)


def featurize(datetime):
return pd.Series({
'month':datetime.month,
'year':datetime.year,
'dayofweek':datetime.dayofweek,
'dayofyear':datetime.dayofyear
})
    

future.loc[
:,['month','year','dayofweek','dayofyear']
] = future.date.apply(featurize)


future.head()

输出:

    date    month   year    dayofweek   dayofyear
0   2022-09-01  9   2022    3           244
1   2022-09-02  9   2022    4           245
2   2022-09-03  9   2022    5           246
3   2022-09-04  9   2022    6           247
4   2022-09-05  9   2022    0           248