【问题标题】:Apply pandas function to column to create multiple new columns?将熊猫函数应用于列以创建多个新列?
【发布时间】:2013-04-20 15:24:56
【问题描述】:

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

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

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

所以我认为我需要退回到使用df.iterrows() 进行迭代,就像this 一样?

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

更新 2:在 df.apply 可用性得到改进或 df.assign()added in v0.16 之前,这个问题在 v0.11.0 周围被问到。因此,很多问题和答案都不太相关。

【问题讨论】:

  • 我不认为你可以按照你写的方式做多个作业:df.ix[: ,10:16]。我认为您必须将merge 您的特征添加到数据集中。
  • 对于那些想要性能更高的解决方案check this one below 的人不使用apply
  • 大多数使用 pandas 的数值运算都可以向量化——这意味着它们比传统迭代要快得多。 OTOH,某些操作(例如字符串和正则表达式)本质上很难矢量化。在这种情况下,了解 如何 循环数据非常重要。有关何时以及如何循环数据的更多信息,请阅读For loops with Pandas - When should I care?
  • @coldspeed:主要问题不是在几个选项中选择哪个性能更高,而是在与 pandas 语法进行斗争以使其完全正常工作,回到 v0.11.0
  • 确实,该评论是为正在寻找迭代解决方案的未来读者准备的,他们要么不知道更好,要么知道自己在做什么。

标签: python pandas merge multiple-columns return-type


【解决方案1】:

这是我过去做过的事情

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() 更简单,用于将新 cols 连接到原始数据帧。
  • 不错的答案,如果您指定应用之外的列,则不需要使用字典或合并df[['col1', 'col2']] = df['col3'].apply(lambda x: pd.Series('val1', 'val2'))
【解决方案2】:

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

【讨论】:

  • 只是出于好奇,这样做会消耗大量内存吗?我在一个包含 250 万行的数据帧上执行此操作,我几乎遇到了内存问题(而且它比只返回 1 列要慢得多)。
  • 'df.join(df.textcol.apply(lambda s: pd.Series({'feature1':s+1, 'feature2':s-1})))' 将是我认为更好的选择。
  • @ShivamKThakkar 为什么您认为您的建议会是更好的选择?您认为它会更高效还是内存成本更低?
【解决方案3】:

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

【讨论】:

  • 但是如果你像这样添加了 50 列而不是 6 列,你会怎么做?
  • @max temp = list(zip(*df['num'].map(powers))); for i, c in enumerate(columns): df[c] = temp[c]
  • @ostrokach 我想你的意思是for i, c in enumerate(columns): df[c] = temp[i]。多亏了这个,我才真正明白了enumerate的目的:D
  • 这是迄今为止我遇到的最优雅和可读的解决方案。除非您遇到性能问题,否则成语 zip(*df['col'].map(function)) 可能是要走的路。
【解决方案4】:

我已经查看了几种执行此操作的方法,此处显示的方法(返回熊猫系列)似乎不是最有效的。

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

# 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 效率更高。如果我的工作出现错误,我很想听听人们的想法。

【讨论】:

  • 这真的很有用!与函数返回系列方法相比,我的速度提高了 30 倍。
【解决方案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

【讨论】:

  • 你不应该写:df = df.apply(example(df), axis=1) 如果我错了请纠正我,我只是一个新手
  • @user299791,不,在这种情况下,您将示例视为第一类对象,因此您正在传递函数本身。此函数将应用于每一行。
  • 嗨迈克尔,你的回答帮助我解决了我的问题。绝对您的解决方案比原始 pandas 的 df.assign() 方法更好,因为这是每列一次。使用assign(),如果要创建2个新列,则必须使用df1处理df获取新的column1,然后使用df2处理df1创建第二个新列……这很单调。但是你的方法救了我的命!!!谢谢!!!
  • 不会每行运行一次列分配代码吗?返回pd.Series({k:v}) 并像 Ewan 的回答那样序列化列分配不是更好吗?
  • 如果它可以帮助任何人,虽然这种方法是正确的,也是所有提出的解决方案中最简单的,但像这样直接更新行最终会出奇地慢 - 比使用 ' 应用慢一个数量级expand' + pd.concat 解决方案
【解决方案6】:

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

【讨论】:

  • 对于 pandas 0.23,您需要使用语法:df["mean"], df["sum"] = df[['height_at_age_10','height_at_age_20']] .apply(mean_and_sum(x),axis=1)
  • 这个函数可能会报错。返回函数必须是return pd.Series([mean,sum])
【解决方案7】:

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

我对 more details here 做出了类似的回应,说明为什么 apply 通常不是可行的方法。

【讨论】:

    【解决方案8】:

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

    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
    【解决方案9】:

    在 2020 年,我使用 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')
    

    【讨论】:

    • 现在你就是这样做的!
    • 这在 2020 年开箱即用,而许多其他问题却没有。它也不使用pd.Series,这对于性能问题总是很好
    • 这是一个很好的解决方案。唯一的问题是,您不能为 2 个新添加的列选择名称。您需要稍后执行 df.rename(columns={0:'col1', 1:'col2'})
    • @pedrambashiri 如果您传递给df.apply 的函数返回dict,则列将根据键命名。
    • 这是最好的答案!通常情况下,您必须从单个数据框列或系列中创建一个基于原始列/系列转换的多个新列的数据框。转换函数通常返回 k 元组,这些 k 元组必须根据某种顺序分成 k 列。 @Ben 的回答显然非常巧妙地做到了这一点。谢谢!
    【解决方案10】:

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

    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)
    

    【讨论】:

      【解决方案11】:

      对我来说这很有效:

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

      【讨论】:

        【解决方案12】:

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

        【讨论】:

        • 指出选项是new in 0.23 会有所帮助。这个问题在 0.11 被问到了
        • 很好,这很简单,仍然可以很好地工作。这是我一直在寻找的那个。谢谢
        • 复制之前的答案:stackoverflow.com/a/52363890/823470
        • @tar 实际上第二行是不同的,对我来说很有帮助!
        【解决方案13】:

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

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

        【讨论】:

          【解决方案14】:
          def extract_text_features(feature):
              ...
              ...
              return pd.Series((feature1, feature2)) 
          
          df[['NewFeature1', 'NewFeature1']] = df[['feature']].apply(extract_text_features, axis=1)
          
          

          在这里,具有单个特征的数据框被转换为两个新特征。 也试试这个。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2013-02-13
            • 1970-01-01
            • 1970-01-01
            • 2016-09-13
            • 1970-01-01
            • 1970-01-01
            • 2020-04-22
            相关资源
            最近更新 更多