本文所整理的技巧與以前整理過(guò)10個(gè)Pandas的常用技巧不同,你可能并不會(huì)經(jīng)常的使用它,但是有時(shí)候當(dāng)你遇到一些非常棘手的問(wèn)題時(shí),這些技巧可以幫你快速解決一些不常見(jiàn)的問(wèn)題。

1、Categorical類型
默認(rèn)情況下,具有有限數(shù)量選項(xiàng)的列都會(huì)被分配object 類型。 但是就內(nèi)存來(lái)說(shuō)并不是一個(gè)有效的選擇。 我們可以這些列建立索引,并僅使用對(duì)對(duì)象的引用而實(shí)際值。Pandas 提供了一種稱為 Categorical的Dtype來(lái)解決這個(gè)問(wèn)題。
例如一個(gè)帶有圖片路徑的大型數(shù)據(jù)集組成。 每行有三列:anchor, positive, and negative.。
如果類別列使用 Categorical 可以顯著減少內(nèi)存使用量。
# raw data
 +----------+------------------------+
 |  class   |        filename        |
 +----------+------------------------+
 | Bathroom | Bathroom\bath_1.jpg    |
 | Bathroom | Bathroom\bath_100.jpg  |
 | Bathroom | Bathroom\bath_1003.jpg |
 | Bathroom | Bathroom\bath_1004.jpg |
 | Bathroom | Bathroom\bath_1005.jpg |
 +----------+------------------------+
 
 # target
 +------------------------+------------------------+----------------------------+
 |         anchor         |        positive        |          negative          |
 +------------------------+------------------------+----------------------------+
 | Bathroom\bath_1.jpg    | Bathroom\bath_100.jpg  | Dinning\din_540.jpg        |
 | Bathroom\bath_100.jpg  | Bathroom\bath_1003.jpg | Dinning\din_1593.jpg       |
 | Bathroom\bath_1003.jpg | Bathroom\bath_1004.jpg | Bedroom\bed_329.jpg        |
 | Bathroom\bath_1004.jpg | Bathroom\bath_1005.jpg | Livingroom\living_1030.jpg |
 | Bathroom\bath_1005.jpg | Bathroom\bath_1007.jpg | Bedroom\bed_1240.jpg       |
 +------------------------+------------------------+----------------------------+
filename列的值會(huì)經(jīng)常被復(fù)制重復(fù)。因此,所以通過(guò)使用Categorical可以極大的減少內(nèi)存使用量。
讓我們讀取目標(biāo)數(shù)據(jù)集,看看內(nèi)存的差異:
triplets.info(memory_usage="deep")
 
 #   Column   Non-Null Count   Dtype  
 # --- ------   --------------   -----  
 # 0   anchor   525000 non-null category
 # 1   positive 525000 non-null category
 # 2   negative 525000 non-null category
 # dtypes: category(3)
 # memory usage: 4.6 MB
 
 # without categories
 triplets_raw.info(memory_usage="deep")
 
 #   Column   Non-Null Count   Dtype
 # --- ------   --------------   -----
 # 0   anchor   525000 non-null object
 # 1   positive 525000 non-null object
 # 2   negative 525000 non-null object
 # dtypes: object(3)
 # memory usage: 118.1 MB
差異非常大,并且隨著重復(fù)次數(shù)的增加,差異呈非線性增長(zhǎng)。
2、行列轉(zhuǎn)換
sql中經(jīng)常會(huì)遇到行列轉(zhuǎn)換的問(wèn)題,Pandas有時(shí)候也需要,讓我們看看來(lái)自Kaggle比賽的數(shù)據(jù)集。census_start .csv文件:

可以看到,這些按年來(lái)保存的,如果有一個(gè)列year和pct_bb,并且每一行有相應(yīng)的值,則會(huì)好得多,對(duì)吧。
cols = sorted([col for col in original_df.columns \
               if col.startswith("pct_bb")])
 df = original_df[(["cfips"] + cols)]
 df = df.melt(id_vars="cfips",
              value_vars=cols,
              var_name="year",
              value_name="feature").sort_values(by=["cfips", "year"])
看看結(jié)果,這樣是不是就好很多了:

3、apply()很慢
我們上次已經(jīng)介紹過(guò),最好不要使用這個(gè)方法,因?yàn)樗闅v每行并調(diào)用指定的方法。但是要是我們沒(méi)有別的選擇,那還有沒(méi)有辦法提高速度呢?
可以使用swifter或pandarallew這樣的包,使過(guò)程并行化。
Swifter
import pandas as pd
 import swifter
 
 def target_function(row):
     return row * 10
 
 def traditional_way(data):
     data['out'] = data['in'].apply(target_function)
 
 def swifter_way(data):
     data['out'] = data['in'].swifter.apply(target_function)
Pandarallel 
import pandas as pd
 from pandarallel import pandarallel
 
 def target_function(row):
     return row * 10
 
 def traditional_way(data):
     data['out'] = data['in'].apply(target_function)
 
 def pandarallel_way(data):
     pandarallel.initialize()
     data['out'] = data['in'].parallel_apply(target_function)
通過(guò)多線程,可以提高計(jì)算的速度,當(dāng)然當(dāng)然,如果有集群,那么最好使用dask或pyspark
4、空值,int, Int64
標(biāo)準(zhǔn)整型數(shù)據(jù)類型不支持空值,所以會(huì)自動(dòng)轉(zhuǎn)換為浮點(diǎn)數(shù)。所以如果數(shù)據(jù)要求在整數(shù)字段中使用空值,請(qǐng)考慮使用Int64數(shù)據(jù)類型,因?yàn)樗鼤?huì)使用pandas.NA來(lái)表示空值。
5、Csv, 壓縮還是parquet?
盡可能選擇parquet。parquet會(huì)保留數(shù)據(jù)類型,在讀取數(shù)據(jù)時(shí)就不需要指定dtypes。parquet文件默認(rèn)已經(jīng)使用了snappy進(jìn)行壓縮,所以占用的磁盤空間小。下面可以看看幾個(gè)的對(duì)比
|        file            |  size   |
 +------------------------+---------+
 | triplets_525k.csv      | 38.4 MB |
 | triplets_525k.csv.gzip |  4.3 MB |
 | triplets_525k.csv.zip  |  4.5 MB |
 | triplets_525k.parquet  |  1.9 MB |
 +------------------------+---------+
讀取parquet需要額外的包,比如pyarrow或fastparquet。chatgpt說(shuō)pyarrow比f(wàn)astparquet要快,但是我在小數(shù)據(jù)集上測(cè)試時(shí)fastparquet比pyarrow要快,但是這里建議使用pyarrow,因?yàn)閜andas 2.0也是默認(rèn)的使用這個(gè)。
6、value_counts ()
計(jì)算相對(duì)頻率,包括獲得絕對(duì)值、計(jì)數(shù)和除以總數(shù)是很復(fù)雜的,但是使用value_counts,可以更容易地完成這項(xiàng)任務(wù),并且該方法提供了包含或排除空值的選項(xiàng)。
df = pd.DataFrame({"a": [1, 2, None], "b": [4., 5.1, 14.02]})
 df["a"] = df["a"].astype("Int64")
 print(df.info())
 print(df["a"].value_counts(normalize=True, dropna=False),
      df["a"].value_counts(normalize=True, dropna=True), sep="\n\n")
這樣是不是就簡(jiǎn)單很多了
7、Modin
注意:Modin現(xiàn)在還在測(cè)試階段。
pandas是單線程的,但Modin可以通過(guò)縮放pandas來(lái)加快工作流程,它在較大的數(shù)據(jù)集上工作得特別好,因?yàn)樵谶@些數(shù)據(jù)集上,pandas會(huì)變得非常緩慢或內(nèi)存占用過(guò)大導(dǎo)致OOM。
!pip install modin[all]
 
 import modin.pandas as pd
 df = pd.read_csv("my_dataset.csv")
以下是modin官網(wǎng)的架構(gòu)圖,有興趣的研究把:

8、extract()
如果經(jīng)常遇到復(fù)雜的半結(jié)構(gòu)化的數(shù)據(jù),并且需要從中分離出單獨(dú)的列,那么可以使用這個(gè)方法:
import pandas as pd
 
 regex = (r'(?P<title>[A-Za-z\'\s]+),'
          r'(?P<author>[A-Za-z\s\']+),'
          r'(?P<isbn>[\d-]+),'
          r'(?P<year>\d{4}),'
          r'(?P<publisher>.+)')
 addr = pd.Series([
     "The Lost City of Amara,Olivia Garcia,978-1-234567-89-0,2023,HarperCollins",
     "The Alchemist's Daughter,Maxwell Greene,978-0-987654-32-1,2022,Penguin Random House",
     "The Last Voyage of the HMS Endeavour,Jessica Kim,978-5-432109-87-6,2021,Simon & Schuster",
     "The Ghosts of Summer House,Isabella Lee,978-3-456789-12-3,2000,Macmillan Publishers",
     "The Secret of the Blackthorn Manor,Emma Chen,978-9-876543-21-0,2023,Random House Children's Books"
  ])
 addr.str.extract(regex)

9、讀寫剪貼板
這個(gè)技巧有人一次也用不到,但是有人可能就是需要,比如:在分析中包含PDF文件中的表格時(shí)。通常的方法是復(fù)制數(shù)據(jù),粘貼到Excel中,導(dǎo)出到csv文件中,然后導(dǎo)入Pandas。但是,這里有一個(gè)更簡(jiǎn)單的解決方案:pd.read_clipboard()。我們所需要做的就是復(fù)制所需的數(shù)據(jù)并執(zhí)行一個(gè)方法。
有讀就可以寫,所以還可以使用to_clipboard()方法導(dǎo)出到剪貼板。
但是要記住,這里的剪貼板是你運(yùn)行python/jupyter主機(jī)的剪切板,并不可能跨主機(jī)粘貼,一定不要搞混了。
10、數(shù)組列分成多列
假設(shè)我們有這樣一個(gè)數(shù)據(jù)集,這是一個(gè)相當(dāng)?shù)湫偷那闆r:
import pandas as pd
 df = pd.DataFrame({"a": [1, 2, 3],
              "b": [4, 5, 6],
              "category": [["foo", "bar"], ["foo"], ["qux"]]})
 
 # let's increase the number of rows in a dataframe
 df = pd.concat([df]*10000, ignore_index=True)

我們想將category分成多列顯示,例如下面的

先看看最慢的apply:
def dummies_series_apply(df):
    return df.join(df['category'].apply(pd.Series) \
                                  .stack() \
                                  .str.get_dummies() \
                                  .groupby(level=0) \
                                  .sum()) \
              .drop("category", axis=1)
 %timeit dummies_series_apply(df.copy())
 #5.96 s ± 66.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
sklearn的MultiLabelBinarizer
from sklearn.preprocessing import MultiLabelBinarizer
 def sklearn_mlb(df):
    mlb = MultiLabelBinarizer()
    return df.join(pd.DataFrame(mlb.fit_transform(df['category']), columns=mlb.classes_)) \
              .drop("category", axis=1)
 %timeit sklearn_mlb(df.copy())
 #35.1 ms ± 1.31 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
是不是快了很多,我們還可以使用一般的向量化操作對(duì)其求和:
def dummies_vectorized(df):
    return pd.get_dummies(df.explode("category"), prefix="cat") \
              .groupby(["a", "b"]) \
              .sum() \
              .reset_index()
 %timeit dummies_vectorized(df.copy())
 #29.3 ms ± 1.22 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

使用第一個(gè)方法(在StackOverflow上的回答中非常常見(jiàn))會(huì)給出一個(gè)非常慢的結(jié)果。而其他兩個(gè)優(yōu)化的方法的時(shí)間是非??焖俚?。
總結(jié)
我希望每個(gè)人都能從這些技巧中學(xué)到一些新的東西。重要的是要記住盡可能使用向量化操作而不是apply()。此外,除了csv之外,還有其他有趣的存儲(chǔ)數(shù)據(jù)集的方法。不要忘記使用分類數(shù)據(jù)類型,它可以節(jié)省大量?jī)?nèi)存。感謝閱讀!