參考datawhale開源組織:https://datawhalechina.github.io/joyful-pandas/build/html/%E7%9B%AE%E5%BD%95/ch9.html#cat
第九章 分類數(shù)據(jù)
In [1]: import numpy as np
In [2]: import pandas as pd
一、cat對(duì)象
- cat對(duì)象的屬性
在 pandas 中提供了 category 類型,使用戶能夠處理分類類型的變量,將一個(gè)普通序列轉(zhuǎn)換成分類變量可以使用 astype 方法。
In [3]: df = pd.read_csv('data/learn_pandas.csv',
...: usecols = ['Grade', 'Name', 'Gender', 'Height', 'Weight'])
...:
In [4]: s = df.Grade.astype('category')
In [5]: s.head()
Out[5]:
0 Freshman
1 Freshman
2 Senior
3 Sophomore
4 Sophomore
Name: Grade, dtype: category
Categories (4, object): ['Freshman', 'Junior', 'Senior', 'Sophomore']
在一個(gè)分類類型的 Series 中定義了 cat 對(duì)象,它和上一章中介紹的 str 對(duì)象類似,定義了一些屬性和方法來(lái)進(jìn)行分類類別的操作。
In [6]: s.cat
Out[6]: <pandas.core.arrays.categorical.CategoricalAccessor object at 0x000001EBFEED0148>
對(duì)于一個(gè)具體的分類,有兩個(gè)組成部分,其一為類別的本身,它以 Index 類型存儲(chǔ),其二為是否有序,它們都可以通過(guò) cat 的屬性被訪問(wèn):
In [7]: s.cat.categories
Out[7]: Index(['Freshman', 'Junior', 'Senior', 'Sophomore'], dtype='object')
In [8]: s.cat.ordered
Out[8]: False
另外,每一個(gè)序列的類別會(huì)被賦予唯一的整數(shù)編號(hào),它們的編號(hào)取決于 cat.categories 中的順序,該屬性可以通過(guò) codes 訪問(wèn):
In [9]: s.cat.codes.head()
Out[9]:
0 0
1 0
2 2
3 3
4 3
dtype: int8
- 類別的增加、刪除和修改
通過(guò) cat 對(duì)象的 categories 屬性能夠完成對(duì)類別的查詢,那么應(yīng)該如何進(jìn)行“增改查刪”的其他三個(gè)操作呢?
注意:
類別不得直接修改,在第三章中曾提到,索引 Index 類型是無(wú)法用 index_obj[0] = item 來(lái)修改的,而 categories 被存儲(chǔ)在 Index 中,因此 pandas 在 cat 屬性上定義了若干方法來(lái)達(dá)到相同的目的。
首先,對(duì)于類別的增加可以使用 add_categories :
In [10]: s = s.cat.add_categories('Graduate') # 增加一個(gè)畢業(yè)生類別
In [11]: s.cat.categories
Out[11]: Index(['Freshman', 'Junior', 'Senior', 'Sophomore', 'Graduate'], dtype='object')
若要?jiǎng)h除某一個(gè)類別可以使用 remove_categories ,同時(shí)所有原來(lái)序列中的該類會(huì)被設(shè)置為缺失。例如,刪除大一的類別:
In [12]: s = s.cat.remove_categories('Freshman')
In [13]: s.cat.categories
Out[13]: Index(['Junior', 'Senior', 'Sophomore', 'Graduate'], dtype='object')
In [14]: s.head()
Out[14]:
0 NaN
1 NaN
2 Senior
3 Sophomore
4 Sophomore
Name: Grade, dtype: category
Categories (4, object): ['Junior', 'Senior', 'Sophomore', 'Graduate']
此外可以使用 set_categories 直接設(shè)置序列的新類別,原來(lái)的類別中如果存在元素不屬于新類別,那么會(huì)被設(shè)置為缺失。
In [15]: s = s.cat.set_categories(['Sophomore','PhD']) # 新類別為大二學(xué)生和博士
In [16]: s.cat.categories
Out[16]: Index(['Sophomore', 'PhD'], dtype='object')
In [17]: s.head()
Out[17]:
0 NaN
1 NaN
2 NaN
3 Sophomore
4 Sophomore
Name: Grade, dtype: category
Categories (2, object): ['Sophomore', 'PhD']
如果想要?jiǎng)h除未出現(xiàn)在序列中的類別,可以使用 remove_unused_categories 來(lái)實(shí)現(xiàn):
In [18]: s = s.cat.remove_unused_categories() # 移除了未出現(xiàn)的博士生類別
In [19]: s.cat.categories
Out[19]: Index(['Sophomore'], dtype='object')
最后,“增改查刪”中還剩下修改的操作,這可以通過(guò) rename_categories 方法完成,同時(shí)需要注意的是,這個(gè)方法會(huì)對(duì)原序列的對(duì)應(yīng)值也進(jìn)行相應(yīng)修改。例如,現(xiàn)在把 Sophomore 改成中文的 本科二年級(jí)學(xué)生 :
In [20]: s = s.cat.rename_categories({'Sophomore':'本科二年級(jí)學(xué)生'})
In [21]: s.head()
Out[21]:
0 NaN
1 NaN
2 NaN
3 本科二年級(jí)學(xué)生
4 本科二年級(jí)學(xué)生
Name: Grade, dtype: category
Categories (1, object): ['本科二年級(jí)學(xué)生']
二、有序分類
- 序的建立
有序類別和無(wú)序類別可以通過(guò) as_unordered 和 reorder_categories 互相轉(zhuǎn)化,需要注意的是后者傳入的參數(shù)必須是由當(dāng)前序列的無(wú)需類別構(gòu)成的列表,不能夠增加新的類別,也不能缺少原來(lái)的類別,并且必須指定參數(shù) ordered=True ,否則方法無(wú)效。例如,對(duì)年級(jí)高低進(jìn)行相對(duì)大小的類別劃分,然后再恢復(fù)無(wú)序狀態(tài):
In [22]: s = df.Grade.astype('category')
In [23]: s = s.cat.reorder_categories(['Freshman', 'Sophomore',
....: 'Junior', 'Senior'],ordered=True)
....:
In [24]: s.head()
Out[24]:
0 Freshman
1 Freshman
2 Senior
3 Sophomore
4 Sophomore
Name: Grade, dtype: category
Categories (4, object): ['Freshman' < 'Sophomore' < 'Junior' < 'Senior']
In [25]: s.cat.as_unordered().head()
Out[25]:
0 Freshman
1 Freshman
2 Senior
3 Sophomore
4 Sophomore
Name: Grade, dtype: category
Categories (4, object): ['Freshman', 'Sophomore', 'Junior', 'Senior']
- 排序和比較
在第二章中,曾提到了字符串和數(shù)值類型序列的排序,此時(shí)就要說(shuō)明分類變量的排序:只需把列的類型修改為 category 后,再賦予相應(yīng)的大小關(guān)系,就能正常地使用 sort_index 和 sort_values 。例如,對(duì)年級(jí)進(jìn)行排序:
In [26]: df.Grade = df.Grade.astype('category')
In [27]: df.Grade = df.Grade.cat.reorder_categories(['Freshman',
....: 'Sophomore',
....: 'Junior',
....: 'Senior'],ordered=True)
....:
In [28]: df.sort_values('Grade').head() # 值排序
Out[28]:
Grade Name Gender Height Weight
0 Freshman Gaopeng Yang Female 158.9 46.0
105 Freshman Qiang Shi Female 164.5 52.0
96 Freshman Changmei Feng Female 163.8 56.0
88 Freshman Xiaopeng Han Female 164.1 53.0
81 Freshman Yanli Zhang Female 165.1 52.0
In [29]: df.set_index('Grade').sort_index().head() # 索引排序
Out[29]:
Name Gender Height Weight
Grade
Freshman Gaopeng Yang Female 158.9 46.0
Freshman Qiang Shi Female 164.5 52.0
Freshman Changmei Feng Female 163.8 56.0
Freshman Xiaopeng Han Female 164.1 53.0
Freshman Yanli Zhang Female 165.1 52.0
由于序的建立,因此就可以進(jìn)行比較操作。分類變量的比較操作分為兩類,第一種是 == 或 != 關(guān)系的比較,比較的對(duì)象可以是標(biāo)量或者同長(zhǎng)度的 Series (或 list ),第二種是 >,>=,<,<= 四類大小關(guān)系的比較,比較的對(duì)象和第一種類似,但是所有參與比較的元素必須屬于原序列的 categories ,同時(shí)要和原序列具有相同的索引。
In [30]: res1 = df.Grade == 'Sophomore'
In [31]: res1.head()
Out[31]:
0 False
1 False
2 False
3 True
4 True
Name: Grade, dtype: bool
In [32]: res2 = df.Grade == ['PhD']*df.shape[0]
In [33]: res2.head()
Out[33]:
0 False
1 False
2 False
3 False
4 False
Name: Grade, dtype: bool
In [34]: res3 = df.Grade <= 'Sophomore'
In [35]: res3.head()
Out[35]:
0 True
1 True
2 False
3 True
4 True
Name: Grade, dtype: bool
In [36]: res4 = df.Grade <= df.Grade.sample(
....: frac=1).reset_index(
....: drop=True) # 打亂后比較
....:
In [37]: res4.head()
Out[37]:
0 True
1 True
2 False
3 True
4 True
Name: Grade, dtype: bool
三、區(qū)間類別
- 利用cut和qcut進(jìn)行區(qū)間構(gòu)造
區(qū)間是一種特殊的類別,在實(shí)際數(shù)據(jù)分析中,區(qū)間序列往往是通過(guò) cut 和 qcut 方法進(jìn)行構(gòu)造的,這兩個(gè)函數(shù)能夠把原序列的數(shù)值特征進(jìn)行裝箱,即用區(qū)間位置來(lái)代替原來(lái)的具體數(shù)值。
首先介紹 cut 的常見用法:
其中,最重要的參數(shù)是 bin ,如果傳入整數(shù) n ,則代表把整個(gè)傳入數(shù)組的按照最大和最小值等間距地分為 n 段。由于區(qū)間默認(rèn)是左開右閉,需要進(jìn)行調(diào)整把最小值包含進(jìn)去,在 pandas 中的解決方案是在值最小的區(qū)間左端點(diǎn)再減去 0.001(max-min) ,因此如果對(duì)序列 [1,2] 劃分為2個(gè)箱子時(shí),第一個(gè)箱子的范圍 (0.999,1.5] ,第二個(gè)箱子的范圍是 (1.5,2] 。如果需要指定左閉右開時(shí),需要把 right 參數(shù)設(shè)置為 False ,相應(yīng)的區(qū)間調(diào)整方法是在值最大的區(qū)間右端點(diǎn)再加上 0.001(max-min) 。
In [38]: s = pd.Series([1,2])
In [39]: pd.cut(s, bins=2)
Out[39]:
0 (0.999, 1.5]
1 (1.5, 2.0]
dtype: category
Categories (2, interval[float64]): [(0.999, 1.5] < (1.5, 2.0]]
In [40]: pd.cut(s, bins=2, right=False)
Out[40]:
0 [1.0, 1.5)
1 [1.5, 2.001)
dtype: category
Categories (2, interval[float64]): [[1.0, 1.5) < [1.5, 2.001)]
bins 的另一個(gè)常見用法是指定區(qū)間分割點(diǎn)的列表(使用 np.infty 可以表示無(wú)窮大):
In [41]: pd.cut(s, bins=[-np.infty, 1.2, 1.8, 2.2, np.infty])
Out[41]:
0 (-inf, 1.2]
1 (1.8, 2.2]
dtype: category
Categories (4, interval[float64]): [(-inf, 1.2] < (1.2, 1.8] < (1.8, 2.2] < (2.2, inf]]
另外兩個(gè)常用參數(shù)為 labels 和 retbins ,分別代表了區(qū)間的名字和是否返回分割點(diǎn)(默認(rèn)不返回):
In [42]: s = pd.Series([1,2])
In [43]: res = pd.cut(s, bins=2, labels=['small', 'big'], retbins=True)
In [44]: res[0]
Out[44]:
0 small
1 big
dtype: category
Categories (2, object): ['small' < 'big']
In [45]: res[1] # 該元素為返回的分割點(diǎn)
Out[45]: array([0.999, 1.5 , 2. ])
從用法上來(lái)說(shuō), qcut 和 cut 幾乎沒有差別,只是把 bins 參數(shù)變成的 q 參數(shù), qcut 中的 q 是指 quantile 。這里的 q 為整數(shù) n 時(shí),指按照 n 等分位數(shù)把數(shù)據(jù)分箱,還可以傳入浮點(diǎn)列表指代相應(yīng)的分位數(shù)分割點(diǎn)。
In [46]: s = df.Weight
In [47]: pd.qcut(s, q=3).head()
Out[47]:
0 (33.999, 48.0]
1 (55.0, 89.0]
2 (55.0, 89.0]
3 (33.999, 48.0]
4 (55.0, 89.0]
Name: Weight, dtype: category
Categories (3, interval[float64]): [(33.999, 48.0] < (48.0, 55.0] < (55.0, 89.0]]
In [48]: pd.qcut(s, q=[0,0.2,0.8,1]).head()
Out[48]:
0 (44.0, 69.4]
1 (69.4, 89.0]
2 (69.4, 89.0]
3 (33.999, 44.0]
4 (69.4, 89.0]
Name: Weight, dtype: category
Categories (3, interval[float64]): [(33.999, 44.0] < (44.0, 69.4] < (69.4, 89.0]]
- 一般區(qū)間的構(gòu)造
對(duì)于某一個(gè)具體的區(qū)間而言,其具備三個(gè)要素,即左端點(diǎn)、右端點(diǎn)和端點(diǎn)的開閉狀態(tài),其中開閉狀態(tài)可以指定 right, left, both, neither 中的一類:
In [49]: my_interval = pd.Interval(0, 1, 'right')
In [50]: my_interval
Out[50]: Interval(0, 1, closed='right')
其屬性包含了 mid, length, right, left, closed ,分別表示中點(diǎn)、長(zhǎng)度、右端點(diǎn)、左端點(diǎn)和開閉狀態(tài)。
使用 in 可以判斷元素是否屬于區(qū)間:
In [51]: 0.5 in my_interval
Out[51]: True
使用 overlaps 可以判斷兩個(gè)區(qū)間是否有交集:
In [52]: my_interval_2 = pd.Interval(0.5, 1.5, 'left')
In [53]: my_interval.overlaps(my_interval_2)
Out[53]: True
一般而言, pd.IntervalIndex 對(duì)象有四類方法生成,分別是 from_breaks, from_arrays, from_tuples, interval_range ,它們分別應(yīng)用于不同的情況:
from_breaks 的功能類似于 cut 或 qcut 函數(shù),只不過(guò)后兩個(gè)是通過(guò)計(jì)算得到的風(fēng)格點(diǎn),而前者是直接傳入自定義的分割點(diǎn):
In [54]: pd.IntervalIndex.from_breaks([1,3,6,10], closed='both')
Out[54]:
IntervalIndex([[1, 3], [3, 6], [6, 10]],
closed='both',
dtype='interval[int64]')
from_arrays 是分別傳入左端點(diǎn)和右端點(diǎn)的列表,適用于有交集并且知道起點(diǎn)和終點(diǎn)的情況:
In [55]: pd.IntervalIndex.from_arrays(left = [1,3,6,10],
....: right = [5,4,9,11],
....: closed = 'neither')
....:
Out[55]:
IntervalIndex([(1, 5), (3, 4), (6, 9), (10, 11)],
closed='neither',
dtype='interval[int64]')
from_tuples 傳入的是起點(diǎn)和終點(diǎn)元組構(gòu)成的列表:
In [56]: pd.IntervalIndex.from_tuples([(1,5),(3,4),(6,9),(10,11)],
....: closed='neither')
....:
Out[56]:
IntervalIndex([(1, 5), (3, 4), (6, 9), (10, 11)],
closed='neither',
dtype='interval[int64]')
一個(gè)等差的區(qū)間序列由起點(diǎn)、終點(diǎn)、區(qū)間個(gè)數(shù)和區(qū)間長(zhǎng)度決定,其中三個(gè)量確定的情況下,剩下一個(gè)量就確定了, interval_range 中的 start, end, periods, freq 參數(shù)就對(duì)應(yīng)了這四個(gè)量,從而就能構(gòu)造出相應(yīng)的區(qū)間:
In [57]: pd.interval_range(start=1,end=5,periods=8)
Out[57]:
IntervalIndex([(1.0, 1.5], (1.5, 2.0], (2.0, 2.5], (2.5, 3.0], (3.0, 3.5], (3.5, 4.0], (4.0, 4.5], (4.5, 5.0]],
closed='right',
dtype='interval[float64]')
In [58]: pd.interval_range(end=5,periods=8,freq=0.5)
Out[58]:
IntervalIndex([(1.0, 1.5], (1.5, 2.0], (2.0, 2.5], (2.5, 3.0], (3.0, 3.5], (3.5, 4.0], (4.0, 4.5], (4.5, 5.0]],
closed='right',
dtype='interval[float64]')
除此之外,如果直接使用 pd.IntervalIndex([...], closed=...) ,把 Interval 類型的列表組成傳入其中轉(zhuǎn)為區(qū)間索引,那么所有的區(qū)間會(huì)被強(qiáng)制轉(zhuǎn)為指定的 closed 類型,因?yàn)?pd.IntervalIndex 只允許存放同一種開閉區(qū)間的 Interval 對(duì)象。
In [59]: pd.IntervalIndex([my_interval, my_interval_2], closed='left')
Out[59]:
IntervalIndex([[0.0, 1.0), [0.5, 1.5)],
closed='left',
dtype='interval[float64]')
- 區(qū)間的屬性與方法
IntervalIndex 上也定義了一些有用的屬性和方法。同時(shí),如果想要具體利用 cut 或者 qcut 的結(jié)果進(jìn)行分析,那么需要先將其轉(zhuǎn)為該種索引類型:
In [60]: id_interval = pd.IntervalIndex(pd.cut(s, 3))
與單個(gè) Interval 類型相似, IntervalIndex 有若干常用屬性: left, right, mid, length ,分別表示左右端點(diǎn)、兩端點(diǎn)均值和區(qū)間長(zhǎng)度。
In [61]: id_demo = id_interval[:5] # 選出前5個(gè)展示
In [62]: id_demo
Out[62]:
IntervalIndex([(33.945, 52.333], (52.333, 70.667], (70.667, 89.0], (33.945, 52.333], (70.667, 89.0]],
closed='right',
name='Weight',
dtype='interval[float64]')
In [63]: id_demo.left
Out[63]: Float64Index([33.945, 52.333, 70.667, 33.945, 70.667], dtype='float64')
In [64]: id_demo.right
Out[64]: Float64Index([52.333, 70.667, 89.0, 52.333, 89.0], dtype='float64')
In [65]: id_demo.mid
Out[65]: Float64Index([43.138999999999996, 61.5, 79.8335, 43.138999999999996, 79.8335], dtype='float64')
In [66]: id_demo.length
Out[66]:
Float64Index([18.387999999999998, 18.334000000000003, 18.333,
18.387999999999998, 18.333],
dtype='float64')
IntervalIndex 還有兩個(gè)常用方法,包括 contains 和 overlaps ,分別指逐個(gè)判斷每個(gè)區(qū)間是否包含某元素,以及是否和一個(gè) pd.Interval 對(duì)象有交集。
In [67]: id_demo.contains(4)
Out[67]: array([False, False, False, False, False])
In [68]: id_demo.overlaps(pd.Interval(40,60))
Out[68]: array([ True, True, False, True, False])
四、練習(xí)
統(tǒng)計(jì)未出現(xiàn)的類別
df = pd.DataFrame({'A':['a','b','c','a'],
'B':['cat','cat','dog','cat']})
pd.crosstab(df.A, df.B)
df.B = df.B.astype('category').cat.add_categories('sheep')
pd.crosstab(df.A, df.B, dropna=False)
def my_crosstab(s1, s2 ,dropna=True):
idx1 = (s1.cat.categories if s1.dtype.name == 'category' and not dropna else s1.unique())
idx2 = (s2.cat.categories if s2.dtype.name == 'category' and not dropna else s2.unique())
res = pd.DataFrame(np.zeros((idx1.shape[0], idx2.shape[0])),index=idx1, columns=idx2)
for i, j in zip(s1,s2):
res.at[i, j] += 1
res = res.rename_axis(index=s1.name, columns=s2.name).astype('int')
return res
my_crosstab(df.A , df.B , dropna=False)