草庐IT

python - 有条件地创建 Pandas 列的最快方法

coder 2023-08-14 原文

在 Pandas DataFrame 中,我想根据另一列的值有条件地创建一个新列。在我的应用程序中,DataFrame 通常有几百万行,并且唯一条件值的数量很少,按统一顺序排列。性能极其重要:生成新列的最快方法是什么?

我在下面创建了一个示例案例,并且已经尝试并比较了不同的方法。
在示例中,条件填充表示为
基于列值的字典查找label (此处: 1, 2, 3 之一)。

lookup_dict = {
    1: 100,   # arbitrary
    2: 200,   # arbitrary
    3: 300,   # arbitrary
    }

然后我希望我的 DataFrame 被填充为:
       label  output
0      3     300
1      2     200
2      3     300
3      3     300
4      2     200
5      2     200
6      1     100
7      1     100

以下是在 10M 行上测试的 6 种不同方法(测试代码中的参数 Nlines):
  • 方法一:pandas.groupby().apply()
  • 方法二:pandas.groupby().indices.items()
  • 方法三:pandas.Series.map
  • 方法 4:for 标签循环
  • 方法五:numpy.select
  • 方法 6:numba

  • 答案末尾提供了完整代码,以及所有方法的运行时。在比较性能之前,每种方法的输出都被断言为相等。

    方法一:pandas.groupby().apply()
    我用 pandas.groupby()label ,然后使用 apply() 用相同的值填充每个块.
    def fill_output(r):
        ''' called by groupby().apply(): all r.label values are the same '''
        r.loc[:, 'output'] = lookup_dict[r.iloc[0]['label']]
        return r
    
    df = df.groupby('label').apply(fill_output)
    

    我得到
    >>> method_1_groupby ran in 2.29s (average over 3 iterations)
    

    请注意 groupby().apply() 在第一组上运行两次以确定要使用的代码路径(请参阅 Pandas #2936 )。对于少数群体,这可能会减慢速度。我欺骗了方法 1 可以添加第一个虚拟组,但我没有得到太大的改进。

    方法二:pandas.groupby().indices.items()
    第二个是变体:而不是使用 apply我使用 groupby().indices.items() 直接访问索引.这最终比方法1快了两倍,这是我使用了很长时间的方法
    dgb = df.groupby('label')
    for label, idx in dgb.indices.items():
        df.loc[idx, 'output'] = lookup_dict[label]
    

    得到了:
    method_2_indices ran in 1.21s (average over 3 iterations)
    

    方法三:pandas.Series.map
    我用过 Pandas.Series.map .
    df['output'] = df.label.map(lookup_dict.get)
    

    在查找值的数量与行数相当的类似情况下,我得到了非常好的结果。在本案中,map最终比方法 1 慢两倍。

    method_3_map 在 3.07 秒内运行(平均超过 3 次迭代)

    我将其归因于少量的查找值,但可能只是我实现它的方式存在问题。

    方法 4:for 标签循环

    第 4 种方法非常简单:我只是遍历所有标签并选择 DataFrame 的匹配部分。
    for label, value in lookup_dict.items():
        df.loc[df.label == label, 'output'] = value
    

    然而,令人惊讶的是,我最终得到的结果比以前的情况快得多。我期待 groupby基于的解决方案比这个更快,因为 Pandas 必须与 df.label == label 进行三个比较这里。结果证明我错了:
    method_4_forloop ran in 0.54s (average over 3 iterations)
    

    方法五:numpy.select
    第五种方法使用 numpy select函数,基于此 StackOverflow answer .
    conditions = [df.label == k for k in lookup_dict.keys()]
    choices = list(lookup_dict.values())
    
    df['output'] = np.select(conditions, choices)
    

    这会产生最好的结果:
    method_5_select ran in 0.29s (average over 3 iterations)
    

    最后,我尝试了一个 numba方法 6 中的方法。

    方法六:麻麻

    仅作为示例,条件填充值是编译函数中的硬编码。我不知道如何给 Numba 一个列表作为运行时常量:
    @jit(int64[:](int64[:]), nopython=True)
    def hardcoded_conditional_filling(column):
        output = np.zeros_like(column)
        i = 0
        for c in column:
            if c == 1:
                output[i] = 100
            elif c == 2:
                output[i] = 200
            elif c == 3:
                output[i] = 300
            i += 1
        return output
    
    df['output'] = hardcoded_conditional_filling(df.label.values)
    

    我得到了最好的时间,比方法 5 快了 50%。
    method_6_numba ran in 0.19s (average over 3 iterations)
    

    由于上述原因,我还没有实现这个:我不知道如何在不显着降低性能的情况下给 Numba 一个列表作为运行时常量。

    完整代码
    import pandas as pd
    import numpy as np
    from timeit import timeit
    from numba import jit, int64
    
    lookup_dict = {
            1: 100,   # arbitrary
            2: 200,   # arbitrary
            3: 300,   # arbitrary
            }
    
    Nlines = int(1e7)
    
    # Generate 
    label = np.round(np.random.rand(Nlines)*2+1).astype(np.int64)
    df0 = pd.DataFrame(label, columns=['label'])
    
    # Now the goal is to assign the look_up_dict values to a new column 'output' 
    # based on the value of label
    
    # Method 1
    # using groupby().apply()
    
    def method_1_groupby(df):
    
        def fill_output(r):
            ''' called by groupby().apply(): all r.label values are the same '''
            #print(r.iloc[0]['label'])   # activate to reveal the #2936 issue in Pandas
            r.loc[:, 'output'] = lookup_dict[r.iloc[0]['label']]
            return r
    
        df = df.groupby('label').apply(fill_output)
        return df 
    
    def method_2_indices(df):
    
        dgb = df.groupby('label')
        for label, idx in dgb.indices.items():
            df.loc[idx, 'output'] = lookup_dict[label]
    
        return df
    
    def method_3_map(df):
    
        df['output'] = df.label.map(lookup_dict.get)
    
        return df
    
    def method_4_forloop(df):
        ''' naive '''
    
        for label, value in lookup_dict.items():
            df.loc[df.label == label, 'output'] = value
    
        return df
    
    def method_5_select(df):
        ''' Based on answer from 
        https://stackoverflow.com/a/19913845/5622825
        '''
    
        conditions = [df.label == k for k in lookup_dict.keys()]
        choices = list(lookup_dict.values())
    
        df['output'] = np.select(conditions, choices)
    
        return df
    
    def method_6_numba(df):
        ''' This works, but it is hardcoded and i don't really know how
        to make it compile with list as runtime constants'''
    
    
        @jit(int64[:](int64[:]), nopython=True)
        def hardcoded_conditional_filling(column):
            output = np.zeros_like(column)
            i = 0
            for c in column:
                if c == 1:
                    output[i] = 100
                elif c == 2:
                    output[i] = 200
                elif c == 3:
                    output[i] = 300
                i += 1
            return output
    
        df['output'] = hardcoded_conditional_filling(df.label.values)
    
        return df
    
    df1 = method_1_groupby(df0)
    df2 = method_2_indices(df0.copy())
    df3 = method_3_map(df0.copy())
    df4 = method_4_forloop(df0.copy())
    df5 = method_5_select(df0.copy())
    df6 = method_6_numba(df0.copy())
    
    # make sure we havent modified the input (would bias the results)
    assert 'output' not in df0.columns 
    
    # Test validity
    assert (df1 == df2).all().all()
    assert (df1 == df3).all().all()
    assert (df1 == df4).all().all()
    assert (df1 == df5).all().all()
    assert (df1 == df6).all().all()
    
    # Compare performances
    Nites = 3
    print('Compare performances for {0:.1g} lines'.format(Nlines))
    print('-'*30)
    for method in [
                   'method_1_groupby', 'method_2_indices', 
                   'method_3_map', 'method_4_forloop', 
                   'method_5_select', 'method_6_numba']:
        print('{0} ran in {1:.2f}s (average over {2} iterations)'.format(
                method, 
                timeit("{0}(df)".format(method), setup="from __main__ import df0, {0}; df=df0.copy()".format(method), number=Nites)/Nites,
                Nites))
    

    输出:
    Compare performances for 1e+07 lines
    ------------------------------
    method_1_groupby ran in 2.29s (average over 3 iterations)
    method_2_indices ran in 1.21s (average over 3 iterations)
    method_3_map ran in 3.07s (average over 3 iterations)
    method_4_forloop ran in 0.54s (average over 3 iterations)
    method_5_select ran in 0.29s (average over 3 iterations)
    method_6_numba ran in 0.19s (average over 3 iterations)
    

    我会对任何其他可以产生更好性能的解决方案感兴趣。
    我最初是在寻找基于 Pandas 的方法,但我也接受基于 numba/cython 的解决方案。

    编辑

    添加 Chrisb's methods比较:
    def method_3b_mapdirect(df):
        ''' Suggested by https://stackoverflow.com/a/51388828/5622825'''
    
        df['output'] = df.label.map(lookup_dict)
    
        return df
    
    def method_7_take(df):
        ''' Based on answer from 
        https://stackoverflow.com/a/19913845/5622825
    
        Exploiting that labels are continuous integers
        '''
    
        lookup_arr = np.array(list(lookup_dict.values()))
        df['output'] = lookup_arr.take(df['label'] - 1)
    
        return df
    

    运行时为:
    method_3_mapdirect ran in 0.23s (average over 3 iterations)
    method_7_take ran in 0.11s (average over 3 iterations)
    

    这使得#3 比任何其他方法都快(#6 除外),而且也是最优雅的。如果您的用户案例兼容,请使用 #7。

    最佳答案

    我会考虑 .map (#3) 这样做的惯用方法 - 但不要通过 .get - 单独使用字典,应该会看到非常显着的改进。

    df = pd.DataFrame({'label': np.random.randint(, 4, size=1000000, dtype='i8')})
    
    %timeit df['output'] = df.label.map(lookup_dict.get)
    261 ms ± 12.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
    
    %timeit df['output'] = df.label.map(lookup_dict)
    69.6 ms ± 3.08 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
    

    如果条件数量很少,并且比较便宜(即整数和您的查找表),则直接比较值(4,尤其是 5)比 .map 快,但这并不总是正确的,例如如果你有一组字符串。

    如果您的查找标签确实是连续的整数,您可以利用这一点并使用 take 进行查找。 ,这应该和 numba 一样快。我认为这基本上是尽可能快的 - 可以在 cython 中编写等价物,但不会更快。
    %%timeit
    lookup_arr = np.array(list(lookup_dict.values()))
    df['output'] = lookup_arr.take(df['label'] - 1)
    8.68 ms ± 332 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
    

    关于python - 有条件地创建 Pandas 列的最快方法,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51388201/

    有关python - 有条件地创建 Pandas 列的最快方法的更多相关文章

    1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

      我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

    2. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

      总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

    3. ruby - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

      类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

    4. ruby - 如何在 Ruby 中顺序创建 PI - 2

      出于纯粹的兴趣,我很好奇如何按顺序创建PI,而不是在过程结果之后生成数字,而是让数字在过程本身生成时显示。如果是这种情况,那么数字可以自行产生,我可以对以前看到的数字实现垃圾收集,从而创建一个无限系列。结果只是在Pi系列之后每秒生成一个数字。这是我通过互联网筛选的结果:这是流行的计算机友好算法,类机器算法:defarccot(x,unity)xpow=unity/xn=1sign=1sum=0loopdoterm=xpow/nbreakifterm==0sum+=sign*(xpow/n)xpow/=x*xn+=2sign=-signendsumenddefcalc_pi(digits

    5. ruby - Facter::Util::Uptime:Module 的未定义方法 get_uptime (NoMethodError) - 2

      我正在尝试设置一个puppet节点,但ruby​​gems似乎不正常。如果我通过它自己的二进制文件(/usr/lib/ruby/gems/1.8/gems/facter-1.5.8/bin/facter)在cli上运行facter,它工作正常,但如果我通过由ruby​​gems(/usr/bin/facter)安装的二进制文件,它抛出:/usr/lib/ruby/1.8/facter/uptime.rb:11:undefinedmethod`get_uptime'forFacter::Util::Uptime:Module(NoMethodError)from/usr/lib/ruby

    6. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

      关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

    7. Ruby 方法() 方法 - 2

      我想了解Ruby方法methods()是如何工作的。我尝试使用“ruby方法”在Google上搜索,但这不是我需要的。我也看过ruby​​-doc.org,但我没有找到这种方法。你能详细解释一下它是如何工作的或者给我一个链接吗?更新我用methods()方法做了实验,得到了这样的结果:'labrat'代码classFirstdeffirst_instance_mymethodenddefself.first_class_mymethodendendclassSecond使用类#returnsavailablemethodslistforclassandancestorsputsSeco

    8. ruby - 使用 Vim Rails,您可以创建一个新的迁移文件并一次性打开它吗? - 2

      使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta

    9. ruby-on-rails - 无法使用 Rails 3.2 创建插件? - 2

      我对最新版本的Rails有疑问。我创建了一个新应用程序(railsnewMyProject),但我没有脚本/生成,只有脚本/rails,当我输入ruby./script/railsgeneratepluginmy_plugin"Couldnotfindgeneratorplugin.".你知道如何生成插件模板吗?没有这个命令可以创建插件吗?PS:我正在使用Rails3.2.1和ruby​​1.8.7[universal-darwin11.0] 最佳答案 随着Rails3.2.0的发布,插件生成器已经被移除。查看变更日志here.现在

    10. ruby-on-rails - Rails 3.2.1 中 ActionMailer 中的未定义方法 'default_content_type=' - 2

      我在我的项目中添加了一个系统来重置用户密码并通过电子邮件将密码发送给他,以防他忘记密码。昨天它运行良好(当我实现它时)。当我今天尝试启动服务器时,出现以下错误。=>BootingWEBrick=>Rails3.2.1applicationstartingindevelopmentonhttp://0.0.0.0:3000=>Callwith-dtodetach=>Ctrl-CtoshutdownserverExiting/Users/vinayshenoy/.rvm/gems/ruby-1.9.3-p0/gems/actionmailer-3.2.1/lib/action_mailer

    随机推荐