pandas有一個(gè)特別的數(shù)據(jù)類型叫category,如其名一樣,是一種分類的數(shù)據(jù)類型。category很嬌氣,使用的時(shí)候稍有不慎就會進(jìn)坑,因此本篇將介紹在pandas中,
1. 為什么要使用category?
2. 以及使用category時(shí)需要注意的一些坑!
文中使用的pandas版本為1.2.3,于今年2021年3月發(fā)布的。
為什么使用category數(shù)據(jù)類型?
總結(jié)一下,使用category有以下一些好處:
內(nèi)存使用情況:對于重復(fù)值很多的字符串列,category可以大大減少將數(shù)據(jù)存儲在內(nèi)存中所需的內(nèi)存量;
運(yùn)行性能:進(jìn)行了一些優(yōu)化,可以提高某些操作的執(zhí)行速度
算法庫的適用:在某些情況下,一些算法模型需要category這種類型。比如,我們知道lightgbm相對于xgboost優(yōu)化的一個(gè)點(diǎn)就是可以處理分類變量,而在構(gòu)建模型時(shí)我們需要指定哪些列是分類變量,并將它們調(diào)整為category作為超參數(shù)傳給模型。
一個(gè)簡單的例子。
df_size = 100_000
df1 = pd.DataFrame(
{
“float_1”: np.random.rand(df_size),
“species”: np.random.choice([“cat”, “dog”, “ape”, “gorilla”], size=df_size),
}
)
df1_cat = df1.astype({“species”: “category”})
創(chuàng)建了兩個(gè)DataFrame,其中df1包含了species并且為object類型,df1_cat復(fù)制了df1,但指定了species為category類型。
》》 df1.memory_usage(deep=True)
Index 128
float_1 800000
species 6100448
dtype: int64
就內(nèi)存使用而言,我們可以直接看到包含字符串的列的成本是多高。species列的字符串大約占用了6MB,如果這些字符串較長,則將會更多。
》》 df1_cat.memory_usage(deep=True)
Index 128
float_1 800000
species 100416
dtype: int64
再看轉(zhuǎn)換為category類別后的內(nèi)存使用情況。有了相當(dāng)大的改進(jìn),使用的內(nèi)存減少了大約60倍。沒有對比,就沒有傷害。
這就是使用category的其中一個(gè)好處。但愛之深,責(zé)之切呀,使用它要格外小心。
使用category的一些坑!
一、category列的操作
好吧,這部分應(yīng)該才是大家較為關(guān)心的,因?yàn)榻?jīng)常會遇到一些莫名其妙的報(bào)錯(cuò)或者感覺哪里不對,又不知道問題出在哪里。
首先,說明一下:使用category的時(shí)候需要格外小心,因?yàn)槿绻藙莶粚Γ秃芸赡茏兓豲bject。而變回object的結(jié)果就是,會降低代碼的性能(因?yàn)閺?qiáng)制轉(zhuǎn)換類型成本很高),并會消耗內(nèi)存。
日常面對category類型的數(shù)據(jù),我們肯定是要對其進(jìn)行操作的,比如做一些轉(zhuǎn)換。下面看一個(gè)例子,我們要分別對category和object類型進(jìn)行同樣的字符串大寫操作,使用accessor的.str方法。
在非category字符串上:
》》 %timeit df1[“species”].str.upper()
25.6 ms ± 2.07 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
在category字符串上:
》》 %timeit df1_cat[“species”].str.upper()
1.85 ms ± 41.1 μs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
結(jié)果很明顯了。在這種情況下,速度提高了大約14倍(因?yàn)閮?nèi)部優(yōu)化會讓.str.upper()僅對分類的唯一類別值調(diào)用一次,然后根據(jù)結(jié)果構(gòu)造一個(gè)seires,而不是對結(jié)果中的每個(gè)值都去調(diào)用一次)。
怎么理解?假設(shè)現(xiàn)有一個(gè)列叫animal,其類別有cat和dog兩種,假設(shè)樣本為10000個(gè),4000個(gè)cat和6000個(gè)dog。那么如果我用對category本身處理,意味著我只分別對cat和dog兩種類別處理一次,一共兩次就解決。如果對每個(gè)值處理,那就需要樣本數(shù)量10000次的處理。
盡管從時(shí)間上有了一些優(yōu)化,然而這種方法的使用也是有一些問題的。。。看一下內(nèi)存使用情況。
》》 df1_cat[“species”].str.upper().memory_usage(deep=True)
6100576
意外的發(fā)現(xiàn)category類型丟了。。結(jié)果竟是一個(gè)object類型,數(shù)據(jù)壓縮的效果也沒了,現(xiàn)在的結(jié)果再次回到剛才的6MB內(nèi)存占用。
這是因?yàn)槭褂胹tr會直接讓原本的category類型強(qiáng)制轉(zhuǎn)換為object,所以內(nèi)存占用又回去了,這是我為什么最開始說要格外小心。
解決方法就是:直接對category本身操作而不是對它的值操作。 要直接使用cat的方法來完成轉(zhuǎn)換操作,如下。
%timeit df1_cat[“species”].cat.rename_categories(str.upper)
239 μs ± 13.9 μs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
可以看到,這個(gè)速度就更快了,因?yàn)槭∪チ藢ategory類別轉(zhuǎn)換為object的時(shí)間,并且內(nèi)存占用也非常少。因此,這才是最優(yōu)的做法。
二、與category列的合并
還是上面那個(gè)例子,但是這次增加了habitat一列,并且species中增加了sanke。
df2 = pd.DataFrame(
{
“species”: [“cat”, “dog”, “ape”, “gorilla”, “snake”],
“habitat”: [“house”, “house”, “jungle”, “jungle”, “jungle”],
}
)
df2_cat = df2.astype({“species”: “category”, “habitat”: “category”})
和前面一樣,創(chuàng)建該數(shù)據(jù)集的一個(gè)category版本,并創(chuàng)建了一個(gè)帶有object字符串的版本。如果將兩個(gè)object列合并在一起的,沒什么意思,因?yàn)榇蠹叶贾罆l(fā)生什么,object+ object= object而已。
把object列合并到category列上
還是一個(gè)例子。
》》 df1.merge(df2_cat, on=“species”).dtypes
float_1 float64
species object
habitat category
dtype: object
左邊的df1中species列為object,右邊的df2_cat中species列為category。我們可以看到,當(dāng)我們合并時(shí),在結(jié)果中的合并列會得到category+ object= object。
這顯然不行了,又回到原來那樣了。我們再試下其他情況。
兩個(gè)category列的合并
》》 df1_cat.merge(df2_cat, on=“species”).dtypes
float_1 float64
species object
habitat category
dtype: object
結(jié)果是:category+ category= object?
有點(diǎn)想打人了,但是別急,我們看看為啥。
在合并中,為了保存分類類型,兩個(gè)category類型必須是完全相同的。 這個(gè)與pandas中的其他數(shù)據(jù)類型略有不同,例如所有float64列都具有相同的數(shù)據(jù)類型,就沒有什么區(qū)分。
而當(dāng)我們討論category數(shù)據(jù)類型時(shí),該數(shù)據(jù)類型實(shí)際上是由該特定類別中存在的一組值來描述的,因此一個(gè)類別包含[“cat”, “dog”, “mouse”]與類別包含[“cheese”, “milk”, “eggs”]是不一樣的。上面的例子之所以沒成功,是因?yàn)槎嗉恿艘粋€(gè)snake。
因此,我們可以得出結(jié)論:
category1+ category2=object
category1+ category1=category1
因此,解決辦法就是:兩個(gè)category類別一模一樣,讓其中一個(gè)等于另外一個(gè)。
》》 df1_cat.astype({“species”: df2_cat[“species”].dtype}).merge(
df2_cat, on=“species”
).dtypes
float_1 float64
species category
habitat category
dtype: object
三、category列的分組
用category類列分組時(shí),一旦誤操作就會發(fā)生意外,結(jié)果是Dataframe會被填成空值,還有可能直接跑死。。
當(dāng)對category列分組時(shí),默認(rèn)情況下,即使category類別的各個(gè)類不存在值,也會對每個(gè)類進(jìn)行分組。
一個(gè)例子來說明。
habitat_df = (
df1_cat.astype({“species”: df2_cat[“species”].dtype})
.merge(df2_cat, on=“species”)
)
house_animals_df = habitat_df.loc[habitat_df[“habitat”] == “house”]
這里采用habitat_df,從上面例子得到的,篩選habitat為house的,只有dog和cat是house,看下面分組結(jié)果。
》》 house_animals_df.groupby(“species”)[“float_1”].mean()
species
ape NaN
cat 0.501507
dog 0.501023
gorilla NaN
snake NaN
Name: float_1, dtype: float64
在groupby中得到了一堆空值。默認(rèn)情況下,當(dāng)按category列分組時(shí),即使數(shù)據(jù)不存在,pandas也會為該類別中的每個(gè)值返回結(jié)果。略坑,如果數(shù)據(jù)類型包含很多不存在的,尤其是在多個(gè)不同的category列上進(jìn)行分組,將會極其損害性能。
因此,解決辦法是:可以傳遞observed=True到groupby調(diào)用中,這確保了我們僅獲取數(shù)據(jù)中有值的組。
》》 house_animals_df.groupby(“species”, observed=True)[“float_1”].mean()
species
cat 0.501507
dog 0.501023
Name: float_1, dtype: float64
四、category列的索引
仍以上面例子舉例,使用groupby-unstack實(shí)現(xiàn)了一個(gè)交叉表,species作為列,habitat作為行,均為category類型。
》》 species_df = habitat_df.groupby([“habitat”, “species”], observed=True)[“float_1”].mean().unstack()
》》 species_df
species cat ape dog gorilla
habitat
house 0.501507 NaN 0.501023 NaN
jungle NaN 0.501284 NaN 0.501108
這好像看似也沒什么毛病,我們繼續(xù)往下看。為這個(gè)交叉表添加一個(gè)新列new_col,值為1。
》》 species_df[“new_col”] = 1
TypeError: ‘fill_value=new_col’ is not present in this Categorical‘s categories
正常情況下,上面這段代碼是完全可以的,但這里報(bào)錯(cuò)了,為什么?
原因是:species和habitat現(xiàn)在均為category類型。使用.unstack()會把species索引移到列索引中(類似pivot交叉表的操作)。而當(dāng)添加的新列不在species的分類索引中時(shí),就會報(bào)錯(cuò)。
雖然平時(shí)使用時(shí)可能很少用分類作為索引,但是萬一恰巧用到了,就要注意一下了。
總結(jié)
總結(jié)一下,pandas的category類型非常有用,可以帶來一些良好的性能優(yōu)勢。但是它也很嬌氣,使用過程中要尤為小心,確保category類型在整個(gè)流程中保持不變,避免變回object。本文介紹的4個(gè)點(diǎn)注意點(diǎn):
category列的變換操作:直接對category本身操作而不是對它的值操作。這樣可以保留分類性質(zhì)并提高性能。
category列的合并:合并時(shí)注意,要保留category類型,且每個(gè)dataframe的合并列中的分類類型必須完全匹配。
category列的分組:默認(rèn)情況下,獲得數(shù)據(jù)類型中每個(gè)值的結(jié)果,即使數(shù)據(jù)中不存在該結(jié)果。可以通過設(shè)置observed=True調(diào)整。
category列的索引:當(dāng)索引為category類型的時(shí)候,注意是否可能與類別變量發(fā)生奇怪的交互作用。
編輯:jq
-
數(shù)據(jù)
+關(guān)注
關(guān)注
8文章
7335瀏覽量
94778 -
數(shù)據(jù)存儲
+關(guān)注
關(guān)注
5文章
1025瀏覽量
52948 -
算法庫
+關(guān)注
關(guān)注
0文章
5瀏覽量
1721
原文標(biāo)題:用了一年 pandas,才知道 category 的這些坑!
文章出處:【微信號:DBDevs,微信公眾號:數(shù)據(jù)分析與開發(fā)】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
終端一體機(jī) 3C 認(rèn)證避坑指南:這些細(xì)節(jié)直接影響拿證效率
K8s生產(chǎn)環(huán)境10大踩坑記錄復(fù)盤
新手必看!華潤微7388全系統(tǒng)改裝零失敗復(fù)盤,這些坑我替你踩過了
采購避坑!北京泰和特這波真假混賣操作,騙了我們好幾年!
那些年我用OpenCV+Qt趟過哪些坑?寫給視覺應(yīng)用開發(fā)者的避坑指南
那些年我用OpenCV+Qt趟過哪些坑?寫給視覺應(yīng)用開發(fā)者的避坑指南
“智匯瀟湘 才聚湖南”成都、天津、深圳三城聯(lián)動,拓維信息引才熱潮再升級!
Hummingbirdv2 E203 仿真排坑之路
光模塊里的「隱形功臣」:金手指憑什么這么關(guān)鍵?
才茂案例|解決工業(yè)物聯(lián)3大痛點(diǎn)!5G邊緣計(jì)算網(wǎng)關(guān)實(shí)戰(zhàn)來了!
CSD船用變壓器鐵芯硅鋼片,真能扛住鹽霧和振動嗎?
【HZ-T536開發(fā)板免費(fèi)體驗(yàn)】2、SDK編譯
購買機(jī)器人氣密性檢測儀:避坑指南和建議
船用防水變壓器安裝總踩坑?這些要點(diǎn)不看虧大了!
你知道船用變壓器有哪些嗎?
用這么久pandas才知道 category里的這些坑!
評論