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

2025-01-16 08:37:00
admin
原创
107
摘要:问题描述:如何在熊猫中做到这一点:extract_text_features我有一个针对单个文本列的函数,返回多个输出列。具体来说,该函数返回 6 个值。该函数有效,但似乎没有任何适当的返回类型(pandas DataFrame/numpy 数组/Python 列表),因此输出可以正确分配df.ix[: ,1...

问题描述:

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

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

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

所以我认为我需要df.iterrows()按照这个重新进行迭代?

更新:迭代df.iterrows()至少慢了 20 倍,因此我放弃了,并将该函数拆分为六个不同的.map(lambda ...)调用。

更新 2:这个问题是在v0.11.0左右提出的,当时的可用性尚未在 v0.16 中df.apply得到改进或df.assign()添加。因此,从那时起,许多问题和答案都不太相关。


解决方案 1:

我通常使用以下方法执行此操作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'] = \n>>>     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

解决方案 2:

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

解决方案 3:

根据 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/

解决方案 4:

这是我过去做过的事情

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

解决方案 5:

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

解决方案 6:

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

解决方案 7:

对我来说这是有效的:

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

解决方案 8:

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

(需要明确的是:这个应用函数接受子集数据框中每一行的值并返回一个列表。)

但是,如果你这样做:

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)

解决方案 9:

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

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

# 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 效率更高。如果我的工作有误,我很想听听大家的想法。

解决方案 10:

对于大量数据,可接受的解决方案将非常慢。获得最多点赞的解决方案有点难以阅读,而且处理数字数据也很慢。如果每个新列都可以独立于其他列进行计算,我会直接对每个列进行赋值,而不使用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'] = \n       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这通常不是可行的方法。

解决方案 11:

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

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

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

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

解决方案 12:

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

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

这里将具有单个特征的数据框转换为两个新特征。 也尝试一下。

解决方案 13:

这对我有用:

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

解决方案 14:

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

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

函数返回行

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

解决方案 15:

尽管问题指定该函数应应用于 Series,但大多数答案似乎都是将该函数应用于 DataFrame,函数从每行获取相关列。这似乎有点不雅观,而且可能会很慢。

假设函数f接受列中的一个值df["argument"]并返回两个值。我发现通过应用于列系列来实现此目的的最佳方式是这样的:

df[["value_1", "value_2"]] = df["argument"].apply(f).to_list()

与 不同DataFrame.apply,遗憾的是Series.apply没有result_type参数来将结果扩展为 DataFrame 以进行分配。但是如果你分配给元组列表,pandas 也能理解。

解决方案 16:

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

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 对象,因此我使用applywithfrom_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结构变得扁平了。

  1. 使用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课堂。

  1. 使用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列,并且您想要保留它,您可能需要尝试第三种方法。

解决方案 17:

除此之外,对我来说,在某些情况下也有必要使用该unstack()方法,因为否则我只会得到一个包含字典的新列。

它的工作原理如下:

df.groupby('variable')['value'].apply(lambda grp: {
    'Min': grp.min(),
    'Median': grp.median(),
    'Max': grp.max()
}).unstack()
相关推荐
  政府信创国产化的10大政策解读一、信创国产化的背景与意义信创国产化,即信息技术应用创新国产化,是当前中国信息技术领域的一个重要发展方向。其核心在于通过自主研发和创新,实现信息技术应用的自主可控,减少对外部技术的依赖,并规避潜在的技术制裁和风险。随着全球信息技术竞争的加剧,以及某些国家对中国在科技领域的打压,信创国产化显...
工程项目管理   1565  
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1354  
  信创国产芯片作为信息技术创新的核心领域,对于推动国家自主可控生态建设具有至关重要的意义。在全球科技竞争日益激烈的背景下,实现信息技术的自主可控,摆脱对国外技术的依赖,已成为保障国家信息安全和产业可持续发展的关键。国产芯片作为信创产业的基石,其发展水平直接影响着整个信创生态的构建与完善。通过不断提升国产芯片的技术实力、产...
国产信创系统   21  
  信创生态建设旨在实现信息技术领域的自主创新和安全可控,涵盖了从硬件到软件的全产业链。随着数字化转型的加速,信创生态建设的重要性日益凸显,它不仅关乎国家的信息安全,更是推动产业升级和经济高质量发展的关键力量。然而,在推进信创生态建设的过程中,面临着诸多复杂且严峻的挑战,需要深入剖析并寻找切实可行的解决方案。技术创新难题技...
信创操作系统   27  
  信创产业作为国家信息技术创新发展的重要领域,对于保障国家信息安全、推动产业升级具有关键意义。而国产芯片作为信创产业的核心基石,其研发进展备受关注。在信创国产芯片的研发征程中,面临着诸多复杂且艰巨的难点,这些难点犹如一道道关卡,阻碍着国产芯片的快速发展。然而,科研人员和相关企业并未退缩,积极探索并提出了一系列切实可行的解...
国产化替代产品目录   28  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用