三、区间类别

1. 利用cut和qcut进行区间构造

区间是一种特殊的类别,在实际数据分析中,区间序列往往是通过 cutqcut 方法进行构造的,这两个函数能够把原序列的数值特征进行装箱,即用区间位置来代替原来的具体数值。

首先介绍 cut 的常见用法:

其中,最重要的参数是 bins ,如果传入整数 n ,则代表把整个传入数组的按照最大和最小值等间距地分为 n 段。由于区间默认是左开右闭,需要在调整时把最小值包含进去,在 pandas 中的解决方案是在值最小的区间左端点再减去 0.001*(max-min) ,因此如果对序列 [1,2] 划分为2个箱子时,第一个箱子的范围 (0.999,1.5] ,第二个箱子的范围是 (1.5,2] 。如果需要指定左闭右开时,需要把 right 参数设置为 False ,相应的区间调整方法是在值最大的区间右端点再加上 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 的另一个常见用法是指定区间分割点的列表(使用 np.infty 可以表示无穷大):

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]]

另外两个常用参数为 labelsretbins ,分别代表了区间的名字和是否返回分割点(默认不返回):

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] # 该元素为返回的分割点
Out[45]: array([0.999, 1.5  , 2.   ])

从用法上来说, qcutcut 几乎没有差别,只是把 bins 参数变成的 q 参数, qcut 中的 q 是指 quantile 。这里的 q 为整数 n 时,指按照 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]]

2. 一般区间的构造

对于某一个具体的区间而言,其具备三个要素,即左端点、右端点和端点的开闭状态,其中开闭状态可以指定 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 ,分别表示中点、长度、右端点、左端点和开闭状态。

使用 in 可以判断元素是否属于区间:

In [51]: 0.5 in my_interval
Out[51]: True

使用 overlaps 可以判断两个区间是否有交集:

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 对象有四类方法生成,分别是 from_breaks, from_arrays, from_tuples, interval_range ,它们分别应用于不同的情况:

from_breaks 的功能类似于 cutqcut 函数,只不过后两个是通过计算得到的分割点,而前者是直接传入自定义的分割点:

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 是分别传入左端点和右端点的列表,适用于有交集并且知道起点和终点的情况:

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 传入的是起点和终点元组构成的列表:

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]')

一个等差的区间序列由起点、终点、区间个数和区间长度决定,其中三个量确定的情况下,剩下一个量就确定了, interval_range 中的 start, end, periods, freq 参数就对应了这四个量,从而就能构造出相应的区间:

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]')

练一练

无论是 interval_range 还是下一章时间序列中的 date_range 都是给定了等差序列中四要素中的三个,从而确定整个序列。请回顾等差数列中的首项、末项、项数和公差的联系,写出 interval_range 中四个参数之间的恒等关系。

除此之外,如果直接使用 pd.IntervalIndex([...], closed=...) ,把 Interval 类型的列表组成传入其中转为区间索引,那么所有的区间会被强制转为指定的 closed 类型,因为 pd.IntervalIndex 只允许存放同一种开闭区间的 Interval 对象。

In [59]: my_interval
Out[59]: Interval(0, 1, closed='right')
In [60]: my_interval_2
Out[60]: Interval(0.5, 1.5, closed='left')
In [61]: pd.IntervalIndex([my_interval, my_interval_2], closed='left')
Out[61]: 
IntervalIndex([[0.0, 1.0), [0.5, 1.5)],
              closed='left',
              dtype='interval[float64]')

3. 区间的属性与方法

IntervalIndex 上也定义了一些有用的属性和方法。同时,如果想要具体利用 cut 或者 qcut 的结果进行分析,那么需要先将其转为该种索引类型:

In [62]: id_interval = pd.IntervalIndex(pd.cut(s, 3))

与单个 Interval 类型相似, IntervalIndex 有若干常用属性: left, right, mid, length ,分别表示左右端点、两端点均值和区间长度。

In [63]: id_demo = id_interval[:5] # 选出前5个展示
In [64]: id_demo
Out[64]: 
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 [65]: id_demo.left
Out[65]: Float64Index([33.945, 52.333, 70.667, 33.945, 70.667], dtype='float64')
In [66]: id_demo.right
Out[66]: Float64Index([52.333, 70.667, 89.0, 52.333, 89.0], dtype='float64')
In [67]: id_demo.mid
Out[67]: Float64Index([43.138999999999996, 61.5, 79.8335, 43.138999999999996, 79.8335], dtype='float64')
In [68]: id_demo.length
Out[68]: 
Float64Index([18.387999999999998, 18.334000000000003, 18.333,
              18.387999999999998, 18.333],
             dtype='float64')

IntervalIndex 还有两个常用方法,包括 containsoverlaps ,分别指逐个判断每个区间是否包含某元素,以及是否和一个 pd.Interval 对象有交集。

In [69]: id_demo.contains(4)
Out[69]: array([False, False, False, False, False])
In [70]: id_demo.overlaps(pd.Interval(40,60))
Out[70]: array([ True,  True, False,  True, False])