<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Pandas騷操作:再見(jiàn) for 循環(huán)!速度提升315倍!

          共 9940字,需瀏覽 20分鐘

           ·

          2021-09-20 07:37

          ↑ 關(guān)注 + 星標(biāo) ,每天學(xué)Python新技能

          后臺(tái)回復(fù)【大禮包】送你Python自學(xué)大禮包

          來(lái)源:Python數(shù)據(jù)科學(xué)

          作者:東哥起飛


          for是所有編程語(yǔ)言的基礎(chǔ)語(yǔ)法,初學(xué)者為了快速實(shí)現(xiàn)功能,依懶性較強(qiáng)。但如果從運(yùn)算時(shí)間性能上考慮可能不是特別好的選擇。
          本次東哥介紹幾個(gè)常見(jiàn)的提速方法,一個(gè)比一個(gè)快,了解pandas本質(zhì),才能知道如何提速。
          下面是一個(gè)例子,數(shù)據(jù)獲取方式見(jiàn)文末。
          >>> import pandas as pd
          # 導(dǎo)入數(shù)據(jù)集
          >>> df = pd.read_csv('demand_profile.csv')
          >>> df.head()
               date_time  energy_kwh
          0  1/1/13 0:00       0.586
          1  1/1/13 1:00       0.580
          2  1/1/13 2:00       0.572
          3  1/1/13 3:00       0.596
          4  1/1/13 4:00       0.592
          基于上面的數(shù)據(jù),我們現(xiàn)在要增加一個(gè)新的特征,但這個(gè)新的特征是基于一些時(shí)間條件生成的,根據(jù)時(shí)長(zhǎng)(小時(shí))而變化,如下:

          因此,如果你不知道如何提速,那正常第一想法可能就是用apply方法寫(xiě)一個(gè)函數(shù),函數(shù)里面寫(xiě)好時(shí)間條件的邏輯代碼。

          def apply_tariff(kwh, hour):
              """計(jì)算每個(gè)小時(shí)的電費(fèi)"""    
              if 0 <= hour < 7:
                  rate = 12
              elif 7 <= hour < 17:
                  rate = 20
              elif 17 <= hour < 24:
                  rate = 28
              else:
                  raise ValueError(f'Invalid hour: {hour}')
              return rate * kwh
          然后使用for循環(huán)來(lái)遍歷df,根據(jù)apply函數(shù)邏輯添加新的特征,如下:
          >>> # 不贊同這種操作
          >>> @timeit(repeat=3, number=100)
          ... def apply_tariff_loop(df):
          ...     """用for循環(huán)計(jì)算enery cost,并添加到列表"""
          ...     energy_cost_list = []
          ...     for i in range(len(df)):
          ...         # 獲取用電量和時(shí)間(小時(shí))
          ...         energy_used = df.iloc[i]['energy_kwh']
          ...         hour = df.iloc[i]['date_time'].hour
          ...         energy_cost = apply_tariff(energy_used, hour)
          ...         energy_cost_list.append(energy_cost)
          ...     df['cost_cents'] = energy_cost_list
          ... 
          >>> apply_tariff_loop(df)
          Best of 3 trials with 100 function calls per trial:
          Function `apply_tariff_loop` ran in average of 3.152 seconds.
          對(duì)于那些寫(xiě)Pythonic風(fēng)格的人來(lái)說(shuō),這個(gè)設(shè)計(jì)看起來(lái)很自然。然而,這個(gè)循環(huán)將會(huì)嚴(yán)重影響效率。原因有幾個(gè):
          首先,它需要初始化一個(gè)將記錄輸出的列表。
          其次,它使用不透明對(duì)象范圍(0,len(df))循環(huán),然后再應(yīng)用apply_tariff()之后,它必須將結(jié)果附加到用于創(chuàng)建新DataFrame列的列表中。另外,還使用df.iloc [i]['date_time']執(zhí)行所謂的鏈?zhǔn)剿饕?,這通常會(huì)導(dǎo)致意外的結(jié)果。
          這種方法的最大問(wèn)題是計(jì)算的時(shí)間成本。對(duì)于8760行數(shù)據(jù),此循環(huán)花費(fèi)了3秒鐘。
          接下來(lái),一起看下優(yōu)化的提速方案。

          一、使用 iterrows循環(huán)

          第一種可以通過(guò)pandas引入iterrows方法讓效率更高。這些都是一次產(chǎn)生一行的生成器方法,類(lèi)似scrapy中使用的yield用法。
          .itertuples為每一行產(chǎn)生一個(gè)namedtuple,并且行的索引值作為元組的第一個(gè)元素。nametuplePythoncollections模塊中的一種數(shù)據(jù)結(jié)構(gòu),其行為類(lèi)似于Python元組,但具有可通過(guò)屬性查找訪(fǎng)問(wèn)的字段。
          .iterrowsDataFrame中的每一行產(chǎn)生(index,series)這樣的元組。
          在這個(gè)例子中使用.iterrows,我們看看這使用iterrows后效果如何。
          >>> @timeit(repeat=3, number=100)
          ... def apply_tariff_iterrows(df):
          ...     energy_cost_list = []
          ...     for index, row in df.iterrows():
          ...         # 獲取用電量和時(shí)間(小時(shí))
          ...         energy_used = row['energy_kwh']
          ...         hour = row['date_time'].hour
          ...         # 添加cost列表
          ...         energy_cost = apply_tariff(energy_used, hour)
          ...         energy_cost_list.append(energy_cost)
          ...     df['cost_cents'] = energy_cost_list
          ...
          >>> apply_tariff_iterrows(df)
          Best of 3 trials with 100 function calls per trial:
          Function `apply_tariff_iterrows` ran in average of 0.713 seconds.
          這樣的語(yǔ)法更明確,并且行值引用中的混亂更少,因此它更具可讀性。
          時(shí)間成本方面:快了近5倍!
          但是,還有更多的改進(jìn)空間,理想情況是可以用pandas內(nèi)置更快的方法完成。

          二、pandas的apply方法

          我們可以使用.apply方法而不是.iterrows進(jìn)一步改進(jìn)此操作。pandas.apply方法接受函數(shù)callables并沿DataFrame的軸(所有行或所有列)應(yīng)用。下面代碼中,lambda函數(shù)將兩列數(shù)據(jù)傳遞給apply_tariff()
          >>> @timeit(repeat=3, number=100)
          ... def apply_tariff_withapply(df):
          ...     df['cost_cents'] = df.apply(
          ...         lambda row: apply_tariff(
          ...             kwh=row['energy_kwh'],
          ...             hour=row['date_time'].hour),
          ...         axis=1)
          ...
          >>> apply_tariff_withapply(df)
          Best of 3 trials with 100 function calls per trial:
          Function `apply_tariff_withapply` ran in average of 0.272 seconds.
          apply的語(yǔ)法優(yōu)點(diǎn)很明顯,行數(shù)少,代碼可讀性高。在這種情況下,所花費(fèi)的時(shí)間大約是iterrows方法的一半。
          但是,這還不是“非??臁?。一個(gè)原因是apply()將在內(nèi)部嘗試循環(huán)遍歷Cython迭代器。但是在這種情況下,傳遞的lambda不是可以在Cython中處理的東西,因此它在Python中調(diào)用并不是那么快。
          如果我們使用apply()方法獲取10年的小時(shí)數(shù)據(jù),那么將需要大約15分鐘的處理時(shí)間。如果這個(gè)計(jì)算只是大規(guī)模計(jì)算的一小部分,那么真的應(yīng)該提速了。這也就是矢量化操作派上用場(chǎng)的地方。

          三、矢量化操作:使用.isin選擇數(shù)據(jù)

          什么是矢量化操作?
          如果你不基于一些條件,而是可以在一行代碼中將所有電力消耗數(shù)據(jù)應(yīng)用于該價(jià)格:df ['energy_kwh'] * 28,類(lèi)似這種。那么這個(gè)特定的操作就是矢量化操作的一個(gè)例子,它是在pandas中執(zhí)行的最快方法。
          但是如何將條件計(jì)算應(yīng)用為pandas中的矢量化運(yùn)算?
          一個(gè)技巧是:根據(jù)你的條件,選擇和分組DataFrame,然后對(duì)每個(gè)選定的組應(yīng)用矢量化操作。
          在下面代碼中,我們將看到如何使用pandas.isin()方法選擇行,然后在矢量化操作中實(shí)現(xiàn)新特征的添加。在執(zhí)行此操作之前,如果將date_time列設(shè)置為DataFrame的索引,會(huì)更方便:
          # 將date_time列設(shè)置為DataFrame的索引
          df.set_index('date_time', inplace=True)

          @timeit(repeat=3, number=100)
          def apply_tariff_isin(df):
              # 定義小時(shí)范圍Boolean數(shù)組
              peak_hours = df.index.hour.isin(range(1724))
              shoulder_hours = df.index.hour.isin(range(717))
              off_peak_hours = df.index.hour.isin(range(07))

              # 使用上面apply_traffic函數(shù)中的定義
              df.loc[peak_hours, 'cost_cents'] = df.loc[peak_hours, 'energy_kwh'] * 28
              df.loc[shoulder_hours,'cost_cents'] = df.loc[shoulder_hours, 'energy_kwh'] * 20
              df.loc[off_peak_hours,'cost_cents'] = df.loc[off_peak_hours, 'energy_kwh'] * 12
          我們來(lái)看一下結(jié)果如何。
          >>> apply_tariff_isin(df)
          Best of 3 trials with 100 function calls per trial:
          Function `apply_tariff_isin` ran in average of 0.010 seconds.
          提示,上面.isin()方法返回的是一個(gè)布爾值數(shù)組,如下:
          [FalseFalseFalse, ..., TrueTrueTrue]
          布爾值標(biāo)識(shí)了DataFrame索引datetimes是否落在了指定的小時(shí)范圍內(nèi)。然后把這些布爾數(shù)組傳遞給DataFrame.loc,將獲得一個(gè)與這些小時(shí)匹配的DataFrame切片。然后再將切片乘以適當(dāng)?shù)馁M(fèi)率,這就是一種快速的矢量化操作了。
          上面的方法完全取代了我們最開(kāi)始自定義的函數(shù)apply_tariff(),代碼大大減少,同時(shí)速度起飛。
          運(yùn)行時(shí)間比Pythonic的for循環(huán)快315倍,比iterrows快71倍,比apply快27倍!

          四、還能更快?

          太刺激了,我們繼續(xù)加速。
          在上面apply_tariff_isin中,我們通過(guò)調(diào)用df.locdf.index.hour.isin三次來(lái)進(jìn)行一些手動(dòng)調(diào)整。如果我們有更精細(xì)的時(shí)間范圍,你可能會(huì)說(shuō)這個(gè)解決方案是不可擴(kuò)展的。但在這種情況下,我們可以使用pandaspd.cut()函數(shù)來(lái)自動(dòng)完成切割:
          @timeit(repeat=3, number=100)
          def apply_tariff_cut(df):
              cents_per_kwh = pd.cut(x=df.index.hour,
                                     bins=[071724],
                                     include_lowest=True,
                                     labels=[122028]).astype(int)
              df['cost_cents'] = cents_per_kwh * df['energy_kwh']
          上面代碼pd.cut()會(huì)根據(jù)bin列表應(yīng)用分組。
          其中include_lowest參數(shù)表示第一個(gè)間隔是否應(yīng)該是包含左邊的。
          這是一種完全矢量化的方法,它在時(shí)間方面是最快的:
          >>> apply_tariff_cut(df)
          Best of 3 trials with 100 function calls per trial:
          Function `apply_tariff_cut` ran in average of 0.003 seconds.
          到目前為止,使用pandas處理的時(shí)間上基本快達(dá)到極限了!只需要花費(fèi)不到一秒的時(shí)間即可處理完整的10年的小時(shí)數(shù)據(jù)集。
          但是,最后一個(gè)其它選擇,就是使用 NumPy,還可以更快!

          五、使用Numpy繼續(xù)加速

          使用pandas時(shí)不應(yīng)忘記的一點(diǎn)是PandasSeriesDataFrames是在NumPy庫(kù)之上設(shè)計(jì)的。并且,pandas可以與NumPy陣列和操作無(wú)縫銜接。
          下面我們使用NumPy的 digitize()函數(shù)更進(jìn)一步。它類(lèi)似于上面pandascut(),因?yàn)閿?shù)據(jù)將被分箱,但這次它將由一個(gè)索引數(shù)組表示,這些索引表示每小時(shí)所屬的bin。然后將這些索引應(yīng)用于價(jià)格數(shù)組:
          @timeit(repeat=3, number=100)
          def apply_tariff_digitize(df):
              prices = np.array([122028])
              bins = np.digitize(df.index.hour.values, bins=[71724])
              df['cost_cents'] = prices[bins] * df['energy_kwh'].values
          cut函數(shù)一樣,這種語(yǔ)法非常簡(jiǎn)潔易讀。
          >>> apply_tariff_digitize(df)
          Best of 3 trials with 100 function calls per trial:
          Function `apply_tariff_digitize` ran in average of 0.002 seconds.
          0.002秒! 雖然仍有性能提升,但已經(jīng)很邊緣化了。
          - END -

          推薦閱讀

          1. 高薪程序員被推到風(fēng)口浪尖,真相竟然是……

          2. 個(gè)個(gè)年薪都五十萬(wàn)的數(shù)據(jù)分析師,是真的嗎?

          3. 打架打出來(lái)的牛逼城市!

          4. 985高校的學(xué)生“夫妻宿舍”令人羨慕,網(wǎng)友:這福利心動(dòng)了,我要去讀博!

          5. 微軟這個(gè)太強(qiáng)了


          推薦一個(gè)公眾號(hào),幫助程序員自學(xué)與成長(zhǎng)


          覺(jué)得還不錯(cuò)就給我一個(gè)小小的鼓勵(lì)吧!




          瀏覽 49
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  色噜噜亚洲欧美在线视频 | 亚洲第一成人网址 | 销魂美女一级A片免费看 | 婬乱A片欧美大片无码芳芳 | 妖精视频看A片 |