一、NumPy
NumPy 是 Python 数值计算的基础包
1.ndarray
1.1.创建 ndarray
np.array
import NumPy as np
data1=[6,7,8,9,0,1]
arr1=np.array(data1)
data2=[[1,2,3,4],[5,6,7,8]]
arr2=np.array(data2)
d3=np.array((1.2,3.4,5,7))
除非特别说明,np.array 会尝试为新建的这个数组推断出一个较为合适的数据类型,数据类型保存在一个特殊的 dtype 对象中.比如
>>> arr1.dtype
dtype('int32')
>>> arr2.dtype
dtype('int32')
>>> d3.dtype
dtype('float64')
除 np.array之外,还有一些函数也可以新建数组.比如,zeros 和 ones 分别可以创建指定长度或形状的全 0 或全 1 数组,empty 可以创建一个没有任何具体值的数组,要用这些方法创建多维数组,只需传入一个表示形状的元组即可
np.zeros((3,6))
array([[0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0.]])
>>> np.empty((2,3,2))
array([[[0., 0.],
[0., 0.],
[0., 0.]],
[[0., 0.],
[0., 0.],
[0., 0.]]])
arrange 是 内置函数 range 的数组版
>>> np.arange(15)
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14])
1.2.ndarray的数据类型
dtype 是一个特殊的对象,它含有 ndarray 将一块内存解释为特定数据类型所需的信息
>>> arr1=np.array([1,2,3],dtype=np.float64)
>>> arr1
array([1., 2., 3.])
>>> arr1.dtype
dtype('float64')
dtype 是 NumPy 灵活交互其他系统的源泉之一.多数情况下,它们直接映射到对象的机器表示,这使得 “读写磁盘上的二进制数据流"以及"集成低级语言代码"等工作变得更见简单.数值型 dtype 的命名方式相同:一个类型名(如 float 和 int),后面跟一个用于表示各元素位长的数字,表述的双精度浮点值需要占用 8 个字节,即 64 位,因此,该类型在 NumPy 中就记作 float64.
可以使用 ndarry.astype 明确地将一个数组从一个 dtype 转换为另一个 dtype
arr=np.arange(5)
>>> arr.dtype
dtype('int32')
>>> float_arr=arr.astype(np.float64)
>>> float_arr.dtype
dtype('float64')
如果浮点数被转换为整数,则小数部分将被截取删除.如果字符串数组表示的全是数字,也可以使用 astype 将其转换为数值形式:
>>> numeric_strings=np.array(['1.25','9.6','7.7','3'])
>>> numeric_strings.astype(float)
array([1.25, 9.6 , 7.7 , 3. ])
如果转换过程因为某种原因而失败了,就会引发一个 ValueError
1.3.NumPy数组的运算
数组很重要,因为它使你不用编写循环即可对数据执行批量运算,NumPy 用户称其为矢量化.大小相等的数组之间的任何算术运算都会将运行应用到元素级:
>>> arr=np.array([[1,2,3],[4,5,6]])
>>> arr
array([[1, 2, 3],
[4, 5, 6]])
>>> arr*arr
array([[ 1, 4, 9],
[16, 25, 36]])
>>> arr-arr
array([[0, 0, 0],
[0, 0, 0]])
数组与标量间的算术运算会将标量值传播到各个元素
>>> arr*3
array([[ 3, 6, 9],
[12, 15, 18]])
大小相同的数组之间的比较会生成布尔数组
>>> arr2=np.array([[0,4,1],[2,4,6]])
>>> arr2>arr
array([[False, True, False],
[False, False, False]])
不同大小的数组之间的运算叫做广播,这里不做深入讨论
1.4.基本的索引和切片
NumPy 一维数组很简单,表面上看他们和 Python 列表的功能查不多
>>> a1=np.arange(10)
>>> a1
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> a1[5]
5
>>> a1[3:6]
array([3, 4, 5])
>>> a1[5:8]=12
>>> a1
array([ 0, 1, 2, 3, 4, 12, 12, 12, 8, 9])
数组切片是原始数组的视图,这意味着数据不会被复制,视图上的任何修改都会直接反映到源数组上
>>> arr_slice=a1[5:8]
>>> arr_slice
array([12, 12, 12])
>>> arr_slice[1]=1234
>>> a1
array([ 0, 1, 2, 3, 4, 12, 1234, 12, 8, 9])
如果想要得到的是 ndarray 的一份副本而非视图,需要明确进行复制
newa1=a1[5:8].copy()
对于二维数组,下面的两种方式是等价的
>>> arr2d=np.array([[1,2,3],[4,5,6]])
>>> arr2d[0,2]
3
>>> arr2d[0][2]
3
1.5.切片索引
ndarray 的切片语法与 Python 列表这样的一维对象差不多
>>> a1
array([ 0, 1, 2, 3, 4, 12, 1234, 12, 8, 9])
>>> a1[1:6]
array([ 1, 2, 3, 4, 12])
对于二维数组,切片方式稍有不同
>>> arr2d=np.array([[1,2,3,4],[5,6,7,8],[9,10,11,12]])
>>> arr2d[:2]
array([[1, 2, 3, 4],
[5, 6, 7, 8]])
>>> arr2d[:2,1:]
array([[2, 3, 4],
[6, 7, 8]])
可以理解为切片是沿着一个轴选取元素的,例如,可以选取第二行的前两列
>>> arr2d[1,:2]
array([5, 6])
1.6.布尔型索引
假设我们有一个用于存储数据的数组以及一个存储姓名的数组,这里可以使用 np.ramdom中的 randn 函数生成一些正态分布的随机数据
>>> names=np.array(['Bob','Jack','Will','Joe','Tim'])
>>> data=np.random.randn(5,4)
>>> names
array(['Bob', 'Jack', 'Will', 'Joe', 'Tim'], dtype='<U4')
>>> data
array([[ 0.78667694, -0.89465253, 0.04595273, -0.70866619],
[-0.24614004, -0.06740967, 0.27178199, -0.34001949],
[ 0.26853935, -1.44356176, 1.37335606, 0.44868366],
[ 1.23932028, -0.49224827, -0.1345172 , -0.66206581],
[ 0.61680412, -0.13877081, 0.1485935 , -0.98164137]])
假设每个名字都对应 data 数组中的一行,而我们想要选出对应名字 ‘Bob’ 的所有行.跟算术运算一样,数组的比较运算也是矢量化的.因此,对于 name 和 字符串 “Bob” 的比较运算将产生一个布尔型数组,这个数组可以用于数组索引
>>> names=='Bob'
array([ True, False, False, False, False])
>>> data[names=='Bob']
array([[ 0.78667694, -0.89465253, 0.04595273, -0.70866619]])
布尔型数组的长度必须与被索引的轴长度一致.此外,还可以将布尔型数组跟切片,整数(或整数序列)混合使用
>>> data[names=='Bob',2:]
array([[ 0.04595273, -0.70866619]])
>>> data[names!='Bob']
array([[-0.24614004, -0.06740967, 0.27178199, -0.34001949],
[ 0.26853935, -1.44356176, 1.37335606, 0.44868366],
[ 1.23932028, -0.49224827, -0.1345172 , -0.66206581],
[ 0.61680412, -0.13877081, 0.1485935 , -0.98164137]])
也可以使用 ~ 操作符作为反转条件
>>> cond=names=='Bob'
>>> data[~cond]
array([[-0.24614004, -0.06740967, 0.27178199, -0.34001949],
[ 0.26853935, -1.44356176, 1.37335606, 0.44868366],
[ 1.23932028, -0.49224827, -0.1345172 , -0.66206581],
[ 0.61680412, -0.13877081, 0.1485935 , -0.98164137]])
运用多个组合条件时,使用 &、| 之类的布尔运算符即可(不支持关键字 and 和 or)
>>> data[(names=='Bob')|(names=='Will')]
array([[ 0.78667694, -0.89465253, 0.04595273, -0.70866619],
[ 0.26853935, -1.44356176, 1.37335606, 0.44868366]])
为了将 data 中的所有负值设为0,只需
>>> data[data<0]=0
>>> data
array([[0.78667694, 0. , 0.04595273, 0. ],
[0. , 0. , 0.27178199, 0. ],
[0.26853935, 0. , 1.37335606, 0.44868366],
[1.23932028, 0. , 0. , 0. ],
[0.61680412, 0. , 0.1485935 , 0. ]])
1.7.花式索引
花式索引 是一个 NumPy 术语,它指的是利用整数数组进行索引,假设有一个 8x4 数组
>>> arr=np.empty((8,4))
>>> for i in range(8):
... arr[i]=i
>>> arr
array([[0., 0., 0., 0.],
[1., 1., 1., 1.],
[2., 2., 2., 2.],
[3., 3., 3., 3.],
[4., 4., 4., 4.],
[5., 5., 5., 5.],
[6., 6., 6., 6.],
[7., 7., 7., 7.]])
为了以特定顺序选取子集,只需传入一个用于指定顺序的整数列表为 ndarray 即可
>>> arr[[4,3,0,6]]
array([[4., 4., 4., 4.],
[3., 3., 3., 3.],
[0., 0., 0., 0.],
[6., 6., 6., 6.]])
>>> arr[[-3,-5,-7]]
array([[5., 5., 5., 5.],
[3., 3., 3., 3.],
[1., 1., 1., 1.]])
一次传入多个索引数组会有一点特别,它返回的是一个一维数组,其中的元素对应各个索引元组
>>> arr=np.arange(32).reshape((8,4))
>>> arr
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11],
[12, 13, 14, 15],
[16, 17, 18, 19],
[20, 21, 22, 23],
[24, 25, 26, 27],
[28, 29, 30, 31]])
>>> arr[[1,5,7,2],[0,3,1,2]]
array([ 4, 23, 29, 10])
最终选取的是元素(1,0),(5,3),(7,1),(2,2).无论数组是多少维的,花式索引总是一维的.这个花式索引的行为可能与我们期望的不一样,选取矩阵的行列子集应该是矩形区域的形才对,下面是得到该结果的一个方法
>>> arr[[1,5,7,2]][:,[0,3,1,2]]
array([[ 4, 7, 5, 6],
[20, 23, 21, 22],
[28, 31, 29, 30],
[ 8, 11, 9, 10]])
花式索引与切片不同,它总是将数据复制到新数组中
1.8.数组转置和轴对称
转置是重塑的一种特殊形式,它返回的是源数据的视图.数组不仅有 transpose 方法,还有一个特殊的 T 属性
>>> arr=np.arange(15).reshape((3,5))
>>> arr
array([[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]])
>>> arr.T
array([[ 0, 5, 10],
[ 1, 6, 11],
[ 2, 7, 12],
[ 3, 8, 13],
[ 4, 9, 14]])
进行矩阵计算时,经常需要用到该操作,比如用 np.dot 计算矩阵内积
>>> arr=np.random.randn(6,3)
>>> arr
array([[-1.05379508, -2.22022699, 0.74830555],
[-1.5887828 , 2.08855534, -0.23345742],
[ 0.01365803, -0.65193559, 0.64825581],
[ 1.98834876, -0.83723806, 0.93235855],
[-0.8361967 , 1.04227256, 0.43267013],
[ 1.40133596, 0.33984628, 2.09784193]])
>>> np.dot(arr.T,arr)
array([[10.25139958, -3.04752799, 4.02304426],
[-3.04752799, 11.61928646, -2.18832004],
[ 4.02304426, -2.18832004, 6.49213582]])
对于高位数组,transpose 需要得到一个由轴编号组成的元组才能对这些轴进行转置
>>> arr=np.arange(16).reshape((2,2,4))
>>> arr
array([[[ 0, 1, 2, 3],
[ 4, 5, 6, 7]],
[[ 8, 9, 10, 11],
[12, 13, 14, 15]]])
>>> arr.transpose((1,0,2))
array([[[ 0, 1, 2, 3],
[ 8, 9, 10, 11]],
[[ 4, 5, 6, 7],
[12, 13, 14, 15]]])
这里第一个轴被换成了第二个,第二个轴被换成了第一个,子u后一个轴不变
简单的转置可以使用 .T,它其实就是进行轴对换而已。ndarray 还有一个 swapaxes 方法,它需要接受一对轴编号
>>> arr
array([[[ 0, 1, 2, 3],
[ 4, 5, 6, 7]],
[[ 8, 9, 10, 11],
[12, 13, 14, 15]]])
>>> arr.swapaxes(1,2)
array([[[ 0, 4],
[ 1, 5],
[ 2, 6],
[ 3, 7]],
[[ 8, 12],
[ 9, 13],
[10, 14],
[11, 15]]])
swapaxes 也是返回源数据的视图(不会进行任何复制操作)
2.通用函数:快速的元素级数组函数
通用函数(即ufunc)是一种对 ndarray 中数据执行元素级运算的函数.可以将其看作一个简单函数的矢量化包装器,许多 ufunc 都是简单的元素级变体,如 sqrt 和 exp
>>> arr=np.arange(10)
>>> arr
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> np.sqrt(arr)
array([0. , 1. , 1.41421356, 1.73205081, 2. ,
2.23606798, 2.44948974, 2.64575131, 2.82842712, 3. ])
>>> np.exp(arr)
array([1.00000000e+00, 2.71828183e+00, 7.38905610e+00, 2.00855369e+01,
5.45981500e+01, 1.48413159e+02, 4.03428793e+02, 1.09663316e+03,
2.98095799e+03, 8.10308393e+03])
这些都是一元 unfunc。另外一些如 add 或 maximum 接受两个数组,并返回一个结果数组
>>> x=np.random.randn(8)
>>> y=np.random.randn(8)
>>> x
array([-0.48107306, 0.75461704, -0.4909556 , 0.06156705, 0.24418227,
0.06613 , -0.81159631, -0.35049848])
>>> y
array([ 1.0264434 , -0.29667443, 1.23448712, 0.26938702, 0.00835983,
-0.89374578, 0.21309029, 0.4258789 ])
>>> np.maximum(x,y)
array([1.0264434 , 0.75461704, 1.23448712, 0.26938702, 0.24418227,
0.06613 , 0.21309029, 0.4258789 ])
这里 np.maximum 计算了 x 和 y 中元素级别最大的元素
虽然并不常见,但有些 ufunc 的确可以返回多个数组。modf 就是一个例子,它是 Python 内置函数 divmod 的矢量化版本,它会返回浮点数数组的小数和整数部分
>>> arr=np.random.randn(7)*5
>>> arr
array([ 3.62417129, 13.73401592, 0.220063 , 0.44405971, 3.5043984 ,
-0.62760798, 7.59859361])
>>> remainder,whole_part=np.modf(arr)
>>> remainder
array([ 0.62417129, 0.73401592, 0.220063 , 0.44405971, 0.5043984 ,
-0.62760798, 0.59859361])
>>> whole_part
array([ 3., 13., 0., 0., 3., -0., 7.])
其他函数见 https://www.cnblogs.com/WSX1994/articles/9061516.html
3.利用数组进行数据处理
NumPy 数组使你可以将许多数据处理任务表述为简洁的数组表达式.用数组表达式代替循环的做法,通常被称为矢量化.一般来说,矢量化数组运算要比等价的纯 Python 方式快一两个数量级,尤其是各种数值计算.
作为简单的例子,假设我们想要在一组值(网格型)上计算函数 sqrt(x2+y2),np.meshgrid 函数接受两个一维数组,并产生两个二维矩阵,对应两个数组中的所有(x,y)对
>>> points=np.arange(-5,5,0.01)
>>> xs,ys=np.meshgrid(points,points)
>>> ys
array([[-5. , -5. , -5. , ..., -5. , -5. , -5. ],
[-4.99, -4.99, -4.99, ..., -4.99, -4.99, -4.99],
[-4.98, -4.98, -4.98, ..., -4.98, -4.98, -4.98],
...,
[ 4.97, 4.97, 4.97, ..., 4.97, 4.97, 4.97],
[ 4.98, 4.98, 4.98, ..., 4.98, 4.98, 4.98],
[ 4.99, 4.99, 4.99, ..., 4.99, 4.99, 4.99]])
>>> z=np.sqrt(xs**2+ys**2)
>>> z
array([[7.07106781, 7.06400028, 7.05693985, ..., 7.04988652, 7.05693985,
7.06400028],
[7.06400028, 7.05692568, 7.04985815, ..., 7.04279774, 7.04985815,
7.05692568],
[7.05693985, 7.04985815, 7.04278354, ..., 7.03571603, 7.04278354,
7.04985815],
...,
[7.04988652, 7.04279774, 7.03571603, ..., 7.0286414 , 7.03571603,
7.04279774],
[7.05693985, 7.04985815, 7.04278354, ..., 7.03571603, 7.04278354,
7.04985815],
[7.06400028, 7.05692568, 7.04985815, ..., 7.04279774, 7.04985815,
7.05692568]])
3.1.将逻辑表述为数组运算
np.where 函数是三元表达式 x if condition else y 的矢量化版本.假设有一个布尔数组和两个值数组
>>> xarr=np.array([1.1,1.2,1.3,1.4,1.5])
>>> yarr=np.array([2.1,2.2,2.3,2.4,2.5])
>>> cond=np.array([True,False,True,True,False])
假设我们想要根据 cond 中的值选取 xarr 和 yarr 的值:当 cond 中的值为 True 时,选取 xarr 的值,否则从 yarr 中选取.列表推导式的写法应该如下
>>> result=[(x if c else y) for x,y,c in zip(xarr,yarr,cond)]
>>> result
[1.1, 2.2, 1.3, 1.4, 2.5]
这有几个问题.第一,它对大数组的处理速度不是很快(因为所有工作都是由 Python 完成的).第二,无法用于多维数组.若使用 np.where,则可以将该功能写得非常简洁
>>> result=np.where(cond,xarr,yarr)
>>> result
array([1.1, 2.2, 1.3, 1.4, 2.5])
np.where 的第二个和第三个参数不必是数组,他们都可以是标量值.在数据分析工作中,where 通常用于根据另一个数组而产生一个新的数组.假设有一个由随机数据组成的矩阵,希望将所有正值替换为 2,将所有的负值替换为 -2,若利用 np.where,则会非常简单
>>> arr=np.random.randn(4,4)
>>> arr
array([[-6.81187446e-04, -3.08173177e-02, 1.94670113e+00,
9.17937213e-01],
[ 2.91347928e-01, 5.33416809e-01, -1.65383139e-03,
-5.18703508e-01],
[ 2.79722394e-01, 4.52300258e-01, -9.86292591e-01,
-8.88423776e-01],
[-3.07760163e-01, 1.58389627e+00, -1.75546309e-01,
1.39253356e-01]])
>>> arr>0
array([[False, False, True, True],
[ True, True, False, False],
[ True, True, False, False],
[False, True, False, True]])
>>> np.where(arr>0,2,-2)
array([[-2, -2, 2, 2],
[ 2, 2, -2, -2],
[ 2, 2, -2, -2],
[-2, 2, -2, 2]])
利用 np.where,可以将标量和数组结合起来.例如,可以使用常数 2 替换 arr 中所有正的值
>>> np.where(arr>0,2,arr)
array([[-6.81187446e-04, -3.08173177e-02, 2.00000000e+00,
2.00000000e+00],
[ 2.00000000e+00, 2.00000000e+00, -1.65383139e-03,
-5.18703508e-01],
[ 2.00000000e+00, 2.00000000e+00, -9.86292591e-01,
-8.88423776e-01],
[-3.07760163e-01, 2.00000000e+00, -1.75546309e-01,
2.00000000e+00]])
传递给 where 的数组大小可以不相等,甚至可以是标量值
3.2.数学和统计方法
可以通过数组上的一组数学函数对整个数组或轴向的数据进行统计计算。sum,mean,以及标准差 std 等聚合计算,(通常也叫做约简)既可以当作数组的实例化方法使用,也可以当作顶级的 NumPy 函数使用
>>> arr=np.random.randn(5,4)
>>> arr
array([[-3.23434804, -0.64442005, 0.42145953, -0.11863871],
[-0.02168163, 0.50264785, -0.69081768, 0.65468194],
[ 0.05753042, -0.2800016 , 0.48061392, 0.56035693],
[-0.40486474, 0.23107818, 1.79671592, 0.23704017],
[-0.07259393, 0.63497811, 0.04348496, 0.61579049]])
>>> arr.mean()
0.03845060250384825
>>> np.mean(arr)
0.03845060250384825
>>> arr.sum()
0.7690120500769649
mean 和 sum 这类函数可以接受一个 axis 选项参数用,用于计算该轴向上的统计值,最终结果是一个少一维的数组
>>> arr.mean(axis=1)
array([-0.89398682, 0.11120762, 0.20462492, 0.46499238, 0.30541491])
>>> arr.sum(axis=0)
array([-3.67595792, 0.4442825 , 2.05145666, 1.94923082])
这里 arr.mean(1) 是计算行的平均值,arr.sum(0) 是计算每列的和
- axis 不设置值,对 m*n 个数求均值,返回一个实数
- axis = 0:压缩行,对各列求均值,返回 1* n 矩阵
- axis =1 :压缩列,对各行求均值,返回 m *1 矩阵
其他如 cumsum 和 cumprod 之类的方法则不聚合,而是产生一个由中间结果组成的数组
>>> arr=np.array([0,1,2,3,4,5,6,7])
>>> arr.cumsum()
array([ 0, 1, 3, 6, 10, 15, 21, 28], dtype=int32)
在多维数组中,累加函数 cumsum 返回的是同样大小的数组,但是会根据每个低纬的切片沿着标记轴计算部分聚类:
>>> arr.cumsum(axis=0)
array([[ 0, 1, 2],
[ 3, 5, 7],
[ 9, 12, 15]], dtype=int32)
>>> arr.cumprod(axis=1)
array([[ 0, 0, 0],
[ 3, 12, 60],
[ 6, 42, 336]], dtype=int32)
更多基本的数组统计方法见 https://blog.csdn.net/weixin_30449239/article/details/99033428
3.3.用于布尔型数组的方法
在上面这些方法中,布尔值会被强制转换为 1 和 0。因此,sum 经常用来对布尔型数组中的 True 值计数
>>> arr=np.random.randn(100)
>>> (arr>0).sum()
47
另外还有两个方法 any 和 all,它们对布尔型数组非常有用。any 用于测试数组中是否存在一个或多个 True,而 all 检查是否全为 True
>>> bools=np.array([False,False,True])
>>> boos.any()
>>> bools.any()
True
>>> bools.all()
False
这两个方法也能用于非布尔型数组,所有非 0 元素将会被当作 True
3.4.排序
与 Python 的内置列表类型一样,NumPy 数组也可以通过 sort 方法就地进行排序
>>> arr=np.random.randn(6)
>>> arr
array([ 0.19241435, -1.97750926, -2.19373232, 0.16377065, -1.53856866,
0.85725756])
>>> arr.sort()
>>> arr
array([-2.19373232, -1.97750926, -1.53856866, 0.16377065, 0.19241435,
0.85725756])
多维数组可以在任何一个轴向上进行排序,只需要将轴编号传递给 sort 即可
>>> arr=np.random.randn(5,3)
>>> arr
array([[ 1.65867043, -1.52429008, -0.76280479],
[ 0.16523488, 1.00124376, -0.91270849],
[-0.66346771, -1.52085457, 0.11703748],
[ 1.00249776, 0.12546034, -1.02326127],
[ 0.69575876, 0.54695987, -0.333557 ]])
>>> arr.sort(1)
>>> arr
array([[-1.52429008, -0.76280479, 1.65867043],
[-0.91270849, 0.16523488, 1.00124376],
[-1.52085457, -0.66346771, 0.11703748],
[-1.02326127, 0.12546034, 1.00249776],
[-0.333557 , 0.54695987, 0.69575876]])
顶级方法 np.sort 返回的是数组的已排序副本,而就地排序则会修改数组本身,计算数组分位数最简单的方法是对其进行排序,然后选取特定位置的值
>>> large_arr=np.random.randn(1000)
>>> large_arr.sort()
>>> large_arr[int(0.05*len(large_arr))]
-1.6251383615945285
3.5.唯一化以及其他的集合逻辑
NumPy 提供了一些针对一维 ndarray 的基本集合运算.最常用的是 np.unique,它用于找出数组中的唯一值并返回已排序的结果
>> names=np.array(['Bob','Jack','Will','Joe','Tim',"Bob","Jack",'Will'])
>>> np.unique(names)
array(['Bob', 'Jack', 'Joe', 'Tim', 'Will'], dtype='<U4')
另一个函数 np.in1d 用于测试一个数组中的值在另一个数组中的成员资格,返回一个布尔型数组
>>> values=np.array([6,0,0,3,2,5,6])
>>> np.in1d(values,[2,3,6])
array([ True, False, False, True, True, False, True])
更多的集合函数见 https://blog.csdn.net/qq_43709590/article/details/115100035
4.用于数组的文件输入输出
NumPy 能够读写磁盘上的文本数据或二进制数据,文本或表格数据可以由 pandas 来处理,NumPy 内置的二进制格式是这节主要内容
np.save 和 np.load 是读写磁盘数组数据的两个主要函数.默认情况下,数组是以未压缩的原始二进制格式保存在扩展名为 .npy 的文件中的;如果文件路径末尾没有扩展名 .npy,则该扩展名会被自动加上,然后可以通过 np.load 读取磁盘上的数组
>>> arr=np.arange(10)
>>> np.save('array',arr)
>>> np.load('array.npy')
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
通过 np.savez 可以将多个数组保存到一个未压缩文件中,将数组以关键字形式存入即可,加载 npz 文件时,会得到一个类似字典的对象,该对象会对整个数组进行延迟加载
>>> np.savez('array_archive.npz',a=arr,b=arr)
>>> arch=np.load('array_archive.npz')
>>> arch['b']
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> arch['a']
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
如果数据压缩得很好,就可以使用 NumPy.savez_compressed:
>>> np.savez_compressed('arr.npz')
5.线性代数
线性代数是任何数组库的重要部分.不像其他语言(如 MATALB),通过 * 对两个二维数组相乘得到的是一个元素级的积,而不是一个矩阵点积.因此,NumPy 提供了一个用于矩阵乘法的 dot 函数,它既是一个数组方法,也是 NumPy 命名空间中的一个函数
>>> x=np.array([[1,2,3],[4,5,6]])
>>> y=np.array([[6,23],[-1,7],[8,9]])
>>> x
array([[1, 2, 3],
[4, 5, 6]])
>>> y
array([[ 6, 23],
[-1, 7],
[ 8, 9]])
>>> x.dot(y)
array([[ 28, 64],
[ 67, 181]])
>>> np.dot(x,y)
array([[ 28, 64],
[ 67, 181]])
一个二维数组与一个大小合适的一维数组的矩阵点积运算之后将会得到一个一维数组
>>> np.dot(x,np.ones(3))
array([ 6., 15.])
@ 符,也可以作为中缀运算符,进行矩阵乘法
>>> x@np.ones(3)
array([ 6., 15.])
NumPy.linalg 中有一组标准的矩阵分解运算以及诸如求逆和行列式之类的东西,它们与 MATLAB 和 R 语言等所使用的是相同的行业标准线性代数库
>>> from NumPy.linalg import inv,qr
>>> x=np.random.randn(5,5)
>>> mat=x.T.dot(x)
>>> inv(mat)
array([[ 96.63610734, 345.17591916, 161.78594311, 82.8657469 ,
280.60713777],
[ 345.17591916, 1244.86817786, 585.92337162, 299.40742459,
1012.78357153],
[ 161.78594311, 585.92337162, 276.5027913 , 141.10741819,
476.96605779],
[ 82.8657469 , 299.40742459, 141.10741819, 72.2925602 ,
243.76338003],
[ 280.60713777, 1012.78357153, 476.96605779, 243.76338003,
824.46449201]])
>>> mat.dot(inv(mat))
array([[ 1.00000000e+00, -1.03358995e-13, -1.78262299e-13,
-5.06871841e-14, -1.30490998e-13],
[-1.35051120e-13, 1.00000000e+00, -5.54420381e-15,
1.23039466e-13, 1.83912356e-13],
[-1.01903888e-13, -2.32123454e-13, 1.00000000e+00,
-9.10686219e-14, -2.76929715e-13],
[ 4.90470196e-14, 1.04289400e-13, 1.57544077e-14,
1.00000000e+00, 1.75555899e-14],
[ 1.41972992e-14, -4.20330324e-13, 8.16715857e-14,
-1.44775684e-13, 1.00000000e+00]])
>>> q,r=qr(mat)
>>> r
array([[-5.40128252e+00, 4.00184911e+00, -6.59129408e+00,
8.01724308e-01, 4.98613800e-01],
[ 0.00000000e+00, -2.11046058e+00, -7.48076251e-01,
-1.61290880e+00, 3.50315675e+00],
[ 0.00000000e+00, 0.00000000e+00, -1.56581454e+00,
2.81687905e+00, 7.30735985e-02],
[ 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
-3.86877452e+00, 1.14391169e+00],
[ 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 6.94858441e-04]])
关于Numpy中的矩阵有:
使用 array:
对于一维向量
array * array 得到的是对应元素相乘的一维向量
array @ array 得到的是对应元素相乘的一维向量所有元素相加的和
对于高维数组
array * array 得到的是对应元素相乘的高维数组
array @ array 得到的是矩阵相乘的结果
- +,-,*,\ 作用于 array 一维以上的对象会是逐个元素处理
- @ 和 .dot 一样,表示的是矩阵乘法
- 处理向量时,array 中,矢量
1
?
N
1*N
1?N 和
N
?
1
N*1
N?1??? 不同,在一维的 array上,转置什么也不做。即对于一维数组(向量),a.T @ a 和 a @ a.T一样,得到的是数字
以下是官方回答:
NumPy 包含一个array 类和一个matrix 类。所述 array 类旨在是通用n维阵列,用于许多种数值计算的,而matrix 意在具体促进线性代数计算。在实践中,两者之间只有少数几个关键区别。
- 运算符
* and @ 、函数dot() 和multiply() :
- 对于
array ,\* 表示逐元素乘法,而 @ 表示矩阵乘法;它们具有相关的功能 multiply() 和dot() 。(在 Python 3.5 之前,@ 不存在,必须dot() 用于矩阵乘法)。 - 对于
matrix ,\* 表示矩阵乘法,对于逐元素乘法,必须使用该multiply() 函数。 - 处理向量(一维数组)
- 对于
array ,矢量形状 1xN、Nx1 和 N 都是不同的东西。类似的操作A[:,1] 返回一个形状为 N 的一维数组,而不是一个形状为 Nx1 的二维数组。在一维array 上转置什么都不做。 - 对于
matrix ,一维数组总是向上转换为 1xN 或 Nx1 矩阵(行或列向量)。A[:,1] 返回形状为 Nx1 的二维矩阵。 - 处理高维数组 (ndim > 2)
array 对象的维数可以大于 2;matrix 对象总是正好有两个维度。 - 便利属性
array 有一个 .T 属性,它返回数据的转置。matrix 还具有 .H、.I 和 .A 属性,它们分别返回asarray() 矩阵的共轭转置、逆矩阵和矩阵。 - 便利构造器
- 该
array 构造采用(嵌套)的Python序列初始化。如,array([[1,2,3],[4,5,6]]) 。 - 该
matrix 构造还需要一个方便的字符串初始化。如matrix("[1 2 3; 4 5 6]") .
两者都有利有弊:
array
:) 按元素乘法很容易:A*B .:( 您必须记住,矩阵乘法有其自己的运算符@ 。:) 你可以把一维数组作为任何行或列向量。视为列向量,而 视为行向量。这可以节省您输入大量移调的麻烦。A @ v``v``v @ A``v :) array 是“默认”的 NumPy 类型,因此它得到最多的测试,并且是使用 NumPy 的第 3 方代码最有可能返回的类型。:) 非常擅长处理任意维度的数据。:) 在语义上更接近张量代数,如果你熟悉的话。:) 所有操作(* ,/ ,+ ,- 等)的逐元素。:( 来自的稀疏矩阵scipy.sparse 不会与数组交互。 matrix
:\\ 行为更像是 MATLAB 矩阵的行为。<:( 二维的最大值。要保存您需要的三维数据,array 或者可能是一个 Python 列表matrix 。<:( 二维的最小值。你不能有向量。它们必须转换为单列或单行矩阵。<:( 由于array 是 NumPy 中的默认值,因此array 即使您将 amatrix 作为参数,某些函数也可能返回a 。这不应该发生在 NumPy 函数中(如果它发生了,那就是一个错误),但是基于 NumPy 的 3rd 方代码可能不像 NumPy 那样尊重类型保留。:) A*B 是矩阵乘法,所以它看起来就像你用线性代数写的一样(对于 Python >= 3.5 普通数组与@ 运算符具有相同的便利性)。<:( 逐元素乘法需要调用函数 multiply(A,B) 。<:( 运算符重载的使用有点不合逻辑:* 不能按元素工作,但/ 可以。- 与的交互
scipy.sparse 更干净一些。
将array 因此更建议使用。事实上,我们打算matrix 最终弃用。
更多的线性代数函数见 https://blog.csdn.net/weixin_45665432/article/details/105738079
6.伪随机数生成
np.random 模块对 Python 内置的 random 进行了补充,增加了一些用于高效生成多种概率分布的样本值的函数,例如,可以通过用 normal 来得到一个标准正太分布的4*4样本数组
>>> samples=np.random.normal(size=(4,4))
>>> samples
array([[-0.84175786, -0.82942681, 1.54383108, 0.42470515],
[-0.37866622, 0.67574588, 2.23220922, 0.46379467],
[ 0.10213208, 1.27697678, 0.1180446 , -0.13052506],
[ 0.83932778, 0.43247417, -1.15065155, -2.00549457]])
这些伪随机数都是在确定条件下生成的,可以使用 NumPy 的 np.random.seed 更改随机数种子,np.random 的数据生成函数使用了全局的随机种子,要想避免全局状态,可以使用 np.random.RandomState,创建一个与其它隔离的随机数生成器
>>> rng=np.random.RandomState(1234)
>>> rng.randn(10)
array([ 0.47143516, -1.19097569, 1.43270697, -0.3126519 , -0.72058873,
0.88716294, 0.85958841, -0.6365235 , 0.01569637, -2.24268495])
7.示例:随机漫步
通过模拟来说明如何运用数组运算,这是一个简单的随机漫步例子:从0开始,步长1和-1出现的概率相等,下面通过 random 模块实现
import random
import matplotlib.pyplot as plt
position=0
walk=[position]
steps=1000
for i in range(steps):
step=1 if random.randint(0,1) else -1
position+=step
walk.append(position)
plt.plot(walk[:1000])
plt.show()
可以使用 np.random 模块一次随机生成 1000 个 “掷硬币” 结果,将其设为0 和 1,然后计算累加和
>>> nsteps=1000
>>> draws=np.random.randint(0,2,size=nsteps)
>>> steps=np.where(draws>0,1,-1)
>>> walk=steps.cumsum()
>>> walk.min()
-1
>>> walk.max()
51
现在有一个更复杂的统计任务 首次穿越时间,即随机漫步过程中第一次到大某个特定值的时间.假设想要知道本次随机漫步需要多久才能距离初始值0至少10步远,np.abs(walk)>=10可以得到一个布尔型数组,它表示的是距离是否达到或超过10,而我们想要的是第一个10或-10的索引,可以使用argmax来解决这个问题,它返回的是该布尔型数组第一个最大值的索引
>>> (np.abs(walk>=10)).argmax()
23
一次模拟多个随机漫步
如果希望模拟多个随机漫步过程(比如5000个),只需对上面的代码做一点点修改即可生成所有的随机漫步过程.只要给np.random的函数传入一个二元元组就可以产生一个二维数组,然后我们就可以一次性计算5000个随机漫步过程(一行一个)的累计和了
>>> nwalks=5000
>>> nsteps=1000
>>> draws=np.random.randint(0,2,size=(nwalks,nsteps))
>>> steps=np.where(draws>0,1,-1)
>>> walks=steps.cumsum(1)
>>> walks
array([[ -1, 0, -1, ..., -38, -39, -38],
[ 1, 2, 1, ..., -76, -77, -76],
[ 1, 0, 1, ..., 32, 33, 34],
...,
[ 1, 2, 3, ..., -26, -25, -26],
[ -1, -2, -3, ..., 24, 25, 26],
[ -1, -2, -3, ..., -62, -61, -62]], dtype=int32)
>>> walks.max()
114
>>> walks.min()
-114
二、pandas入门
pandas 是后续内容的首选库,它包含使数据清洗和分析工作变得更快更简单的数据结构和操作工具.pandas 经常和其他的工具一同使用,比如数值计算工具 NumPy和SciPy,分析库statsmodels和scikit-learn和数据可视化库matplotlib.pandas是基于NumPy数组构建的,特别是基于数组的函数和不使用for循环的数据处理,二者最大的不同是pandas是专门为处理表格和混杂数据设计的,而Numpy更适合处理统一的数值数组数据.因为 Series 和 DataFrame 用的次数非常多,所以将其引入本地命名空间会非常方便
>>> import pandas as pd
>>> from pandas import Series,DataFrame
1.pandas的数据结构介绍
pandas的两个主要数据结构是Series和DataFrame,它们为大多数应用提供了一种可靠的易于使用的基础
1.1.Series
Series是一中类似于一维数组的对象,它由一组数据(各种NumPy数据类型)以及一组与之相关的数据标签(即索引)组成.
>>> obj=Series([4,7,-5,3])
>>> obj
0 4
1 7
2 -5
3 3
dtype: int64
Series的字符串表现形式为:索引在左边,值在右边.由于我们没有为数据指定索引,于是会自动创建一个0到N-1的整数型索引,可以通过Series的values和index属性获取其数组表示形式和索引对象
>>> obj.values
array([ 4, 7, -5, 3], dtype=int64)
>>> obj.index
RangeIndex(start=0, stop=4, step=1)
通常,我们希望所创建的Series带有一个可以对各个数据点进行标记的索引
>>> obj2=Series([4,7,-5,2],index=['d','b','a','c'])
>>> obj2
d 4
b 7
a -5
c 2
dtype: int64
和普通NumPy数组相比,可以通过索引的方式选取Series中的单个或一组值
>>> obj2['a']
-5
>>> obj2['d']=6
>>> obj2[['c','a','d']]
c 2
a -5
d 6
dtype: int64
[‘c’,‘a’,‘d’]是索引列表,即使它包含的是字符串而不是整数
使用NumPy函数或类似NumPy的运算(如根据布尔型数组进行过滤,标量乘法,应用数学函数等)都会保留索引值的链接
>>> obj2[obj2>0]
d 6
b 7
c 2
dtype: int64
>>> obj2*2
d 12
b 14
a -10
c 4
dtype: int64
d 403.428793
b 1096.633158
a 0.006738
c 7.389056
dtype: float64
还可以将Series看成是一个定长的有序字典,因为它是索引值到数据值的一个映射,它可以用在许多原本需要字典参数的函数中,也可以通过字典创建Series
>>> 'b' in obj2
True
>>> 'e' in obj2
False
>>> data={'age':12,'name':'ys','sex':'m'}
>>> obj3=Series(data)
>>> obj3
age 12
name ys
sex m
dtype: object
如果之传入一个字典,则结果Series中的索引就是原字典的键(有序排列),可以传入排好序的字典的键以改变顺序
>>> xdata=['age','sex','grade']
>>> obj4=Series(data,index=xdata)
>>> obj4
age 12
sex m
grade NaN
在这个例子中,xdata中与data匹配的值会被找到并放在相应的位置上,找不到结果就为NaN(即"非数字"),在pandas中,它表示缺少或NA值,可以使用pandas的isnull和notnull函数检测缺失数据,也可以使用Series中的实例方法
>>> pd.isnull(obj4)
age False
sex False
grade True
dtype: bool
>>> pd.notnull(obj4)
age True
sex True
grade False
dtype: bool
>>> obj4.isnull()
age False
sex False
grade True
dtype: bool
对许多应用而言,Series最重要的一个功能是,它会更具运算的索引标签自动对齐数据
>>> obj3
age 12
name ys
sex m
dtype: object
>>> obj4
age 12
sex m
grade NaN
dtype: object
>>> obj3+obj4
age 24
grade NaN
name NaN
sex mm
dtype: object
Series对象本身及其索引都有一个name属性,该属性与pandas其他的关键功能关系非常密切
>>> obj4.name='YS'
>>> obj4.index.name='data'
>>> obj4
data
age 12
sex m
grade NaN
Name: YS, dtype: object
1.2.DataFrame
DataFrame是一个表格型的数据结构,它含有一组有序的列,每列可以是不同的值类型(数值,字符串,布尔值等).DataFrame既有行索引又有列索引,它可以被看做是由Series组成的字典(公用一个索引),DataFrame中的数据是一个或多个二维块存放的(而不是列表,字典或别的一维数据结构).但也可以轻松地将其表示为更高维的数据.
创建DataFrame的方法由很多,最常用的一种是直接传入一个由等长列表或NumPy数组组成的字典
>>> data={'name':['小李子','小罗伯特唐尼','艾玛'],
... 'age':['11','22','33'],
... 'salary':[200,300,400]}
>>> frame=DataFrame(data)
>>> frame
name age salary
0 小李子 11 200
1 小罗伯特唐尼 22 300
2 艾玛 33 400
对于特别大的DataFrame,head方法会选择前五行
>>> frame.head()
name age salary
0 小李子 11 200
1 小罗伯特唐尼 22 300
2 艾玛 33 400
如果指定了列,DataFrame的列就会按照指定顺序进行排列
>>> DataFrame(data,columns=['age','name','salary'])
age name salary
0 11 小李子 200
1 22 小罗伯特唐尼 300
2 33 艾玛 400
如果传入的列在数据中找不到,就会在结果中产生缺失值
age salary name debt
one 11 200 小李子 NaN
two 22 300 小罗伯特唐尼 NaN
three 33 400 艾玛 NaN
通过类似字典标记的方式或属性的方式,可以将DataFrame的列获取为一个Series:
>>> frame2['name']
one 小李子
two 小罗伯特唐尼
three 艾玛
Name: name, dtype: object
>>> frame2.age
one 11
two 22
three 33
Name: age, dtype: object
返回的Series拥有原DataFrame相同的索引,且其name属性也已经被相应的设计号了,行也可以通过位置或名称的方式获取,比如用loc属性
>>> frame2.loc['three']
age 33
salary 400
name 艾玛
debt NaN
Name: three, dtype: object
列可以通过赋值的方式进行修改,例如,可以给空的’debt’列赋上一个标量值或一组值
>>> frame2['debt']=1
>>> frame2
age salary name debt
one 11 200 小李子 1
two 22 300 小罗伯特唐尼 1
three 33 400 艾玛 1
>>> frame2['debt']=np.arange(3)
>>> frame2
age salary name debt
one 11 200 小李子 0
two 22 300 小罗伯特唐尼 1
three 33 400 艾玛 2
将列表赋或数组赋给某个列时,其长度必须与DataFrame的长度相匹配,如果赋值的是一个Series,就会精确匹配DataFrame的索引,所有的空位将被填上缺少值
>>> val=Series([1.2,3.4,5.6],index=['two','one','three'])
>>> frame2['debt']=val
>>> frame2
age salary name debt
one 11 200 小李子 3.4
two 22 300 小罗伯特唐尼 1.2
three 33 400 艾玛 5.6
为不存在的列赋值会创建出一个新列,关键字del用于删除列,作为del的例子,先添加一个新的布尔值的列,salary是否为300:
>>> frame2['Bool']=frame2.salary==200
>>> frame2
age salary name debt Bool
one 11 200 小李子 3.4 True
two 22 300 小罗伯特唐尼 1.2 False
three 33 400 艾玛 5.6 False
>>> del frame2['Bool']
>>> frame2.columns
Index(['age', 'salary', 'name', 'debt'], dtype='object')
另一种常见的数据形式是嵌套字典,pandas会被解释为外层字典的键作为列索引,内层键则作为行索引:
>>> pop={'age':{'艾玛':33,'钢铁侠':11,'小李子':22},'salary':{'钢铁侠':100,'艾玛':300,'小李子':200}}
>>> frame3=DataFrame(pop)
>>> frame3
age salary
艾玛 33 300
钢铁侠 11 100
小李子 22 200
也可以是使用类似NumPy数组的方法,对DataFrame进行转置
>>> frame3.T
艾玛 钢铁侠 小李子
age 33 11 22
salary 300 100 200
内层字典的键会被合并,排序以形成最终的索引,如果明确指定了索引,则不会这样,则不会这样
age salary
钢铁侠 11.0 100.0
艾玛 33.0 300.0
杨森 NaN NaN
由Series组成的字典差不多也是一样的用法,下面是DataFrame构造函数所能接受的各种数据
下表是DataFrame构造函数所能接受的各种数据。
类型 | 说明 |
---|
二维ndarray | 数据矩阵,还可以传入行标和列标 | 由数组、列表或元组组成的字典 | 每个序列会变成Datarame的一列,所有序列的长度必须相同。 | NumPy的结构化/记录数组 | 类似于“由数组组成的字典” | 由Series组成的字典 | 每个Series会称为一列,如果没有显示指定索引,则各Series的索引会被合并成结果的行索引。 | 由字典组成的字典 | 各内层字典会成为一列,键会被合并成结果的行索引,跟“由Series组成的字典”的情况一样。 | 字典或Series的列表 | 各项将会成为DataFrame的一行,字典键或Series索引的并集将会成为DataFrame的列标。 | 由列表或元组组成的列表 | 类似于“二维ndarray” | 另一个DataFrame | 该DataFrame的索引将被沿用,除非显示指定了其他索引 | NumPy的MaskedArray | 类似于“二维ndarray”的情况,知识掩码值在结果DataFrame会变成Na/缺省值。 |
如果设置了DataFrame的index和columns的name属性,则这些信息也会被显示出来
>>> frame3.index.name='data'
>>> frame3.columns.name='xdata'
>>> frame3
xdata age salary
data
艾玛 33 300
钢铁侠 11 100
小李子 22 200
跟Series一样,values属性也会以二维ndarray的形式返回DataFrame中的数据
>>> frame3.values
array([[ 33, 300],
[ 11, 100],
[ 22, 200]], dtype=int64)
如果DataFrame各列的数据类型不同,则值数组中的dtype就会选择能兼容所有列的数据类型
>>> frame2.values
array([['11', 200, '小李子', 3.4],
['22', 300, '小罗伯特唐尼', 1.2],
['33', 400, '艾玛', 5.6]], dtype=object)
1.3.索引对象
pandas的索引对象负责管理轴标签和其他元数据(比如轴名称等).构建Series或DataFrame时,所用到的任何数组或其他序列的标签都会被转换成一个 index
>>> obj=Series(range(3),index=['a','b','c'])
>>> index=obj.index
>>> index
Index(['a', 'b', 'c'], dtype='object')
index对象是不可变的,不能对其进行修改,这可以使得 index 对象在多个数据结构之间安全共享
>>> labels=pd.Index(np.arange(3))
>>> labels
Int64Index([0, 1, 2], dtype='int64')
>>> obj2=Series([1.5,-2.4,0],index=labels)
>>> obj2
0 1.5
1 -2.4
2 0.0
dtype: float64
>>> obj2.index is labels
True
除了类似于数组,index的功能也类似一个固定大小的集合
>>> frame3
xdata age salary
data
艾玛 33 300
钢铁侠 11 100
小李子 22 200
>>> frame3.columns
Index(['age', 'salary'], dtype='object', name='xdata')
>>> 'age' in frame3.columns
True
>>> 300 in frame3.columns
False
与 Python中的集合不同的是,pandas的index可以包含重复的标签
>>> dup_labels=pd.Index(['foo','foo','bar'])
>>> dup_labels
Index(['foo', 'foo', 'bar'], dtype='object')
选择重复的标签,会显示所有的结果,每个索引都有一些方法和属性,它们可以用于设置逻辑并回答有关该索引所包含的数据的常见问题
append 连接另一个index对象,产生一个新的index
difference 计算差集,并得到一个index
intersection 计算交集
union 计算并集
isin 计算一个指示各值是否都包含在参数集合中的布尔型数组
delete 删除索引i处的元素,并得到新的index
drop 删除传入的值,并得到新的index
insert 将元素插入到索引i处,并得到新的index
is_monnotonic 当各元素均大于等于前一个元素时,返回True
is_unique 当index没有重复值时,返回True
unique 计算index中唯一值的数
2.基本功能
本节是Series和DataFrame中的数据的基本手段.
2.1.重新索引
pandas 对象的一个重要方法是 reindex,其作用是创建一个新对象,它的数据符合新的索引
>>> obj=pd.Series([4.5,2,3,-5,3.1],index=['d','a','b','c','e'])
>>> obj
d 4.5
a 2.0
b 3.0
c -5.0
e 3.1
dtype: float64
>>> obj2=obj.reindex(['a','b','c','d','e'])
>>> obj2
a 2.0
b 3.0
c -5.0
d 4.5
e 3.1
dtype: float64
用该 Series 的 reindex 将会更具新索引进行重排.如果某个索引值当前不存在,就引入缺失值.
对于时间序列这样的有序数据,重新索引可能需要做一些插值处理,method选项即可达到此目的.例如,使用ffill可以实现前向值i安崇
>>> obj3=pd.Series(['blue','purple','yellow'],index=[0,2,4])
>>> obj3
0 blue
2 purple
4 yellow
dtype: object
>>> obj3.reindex(range(6),method='ffill')
0 blue
1 blue
2 purple
3 purple
4 yellow
5 yellow
dtype: object
借助 DataFrame,reindex 可以修改(行)索引和列,值传递一个序列时,会重新索引结果的行:
>>> frame=pd.DataFrame(np.arange(9).reshape(3,3),index=['a','c','d'],columns=['Ohio','Texas','California'])
>>> frame
Ohio Texas California
a 0 1 2
c 3 4 5
d 6 7 8
>>> frame2=frame.reindex(['a','b','c','d'])
>>> frame2
Ohio Texas California
a 0.0 1.0 2.0
b NaN NaN NaN
c 3.0 4.0 5.0
d 6.0 7.0 8.0
列可以使用columns关键字重新索引
Texas Utah California
a 1 NaN 2
c 4 NaN 5
d 7 NaN 8
对 reindex 的详解见 https://baijiahao.baidu.com/s?id=1637861793545367173&wfr=spider&for=pc
2.2.丢弃指定轴上的项
丢弃某条轴上的一个或多个项很简单,只要有一个索引数列或列表即可,由于需要执行一些数据整理和集合逻辑,所以drop方法返回的是一个在指定轴上删除了指定值的新对象
>>> obj=pd.Series(np.arange(5.),index=['a','b','c','d','e'])
>>> obj
a 0.0
b 1.0
c 2.0
d 3.0
e 4.0
dtype: float64
>>> new_obj=obj.drop('c')
>>> new_obj
a 0.0
b 1.0
d 3.0
e 4.0
dtype: float64
>>> obj.drop(['d','c'])
a 0.0
b 1.0
e 4.0
对于 DataFrame,可以删除任意轴上的索引值
>>> data=pd.DataFrame(np.arange(16).reshape(4,4),index=['Ohio','Colorado','Utah','New York'],columns=['one','two','three','four'])
>>> data
one two three four
Ohio 0 1 2 3
Colorado 4 5 6 7
Utah 8 9 10 11
New York 12 13 14 15
>>> data.drop(['Colorado','Ohio'])
one two three four
Utah 8 9 10 11
New York 12 13 14 15
通过传递 axis=1或 axis='columns’可以删除列的值
>>> data.drop('two',axis=1)
one three four
Ohio 0 2 3
Colorado 4 6 7
Utah 8 10 11
New York 12 14 15
>>> data.drop(['two','four'],axis='columns')
one three
Ohio 0 2
Colorado 4 6
Utah 8 10
New York 12 14
许多函数,如drop,会修改Series或DataFrame的大小或形状,可以就地修改对象,不会返回新的对象
>>> obj.drop('c',inplace=True)
>>> obj
a 0.0
b 1.0
d 3.0
e 4.0
dtype: float64
2.3.索引,选取和过滤
Series的工作方式类似于NumPy数组的索引,只不过Series的索引值不只是整数
>>> obj=pd.Series(np.arange(4.),index=['a','b','c','d'])
>>> obj
a 0.0
b 1.0
c 2.0
d 3.0
dtype: float64
>>> obj['b']
1.0
>>> obj[1]
1.0
>>> obj[2:4]
c 2.0
d 3.0
dtype: float64
>>> obj['b','a','d']
>>> obj[['b','a','d']]
b 1.0
a 0.0
d 3.0
dtype: float64
>>> obj[obj<2]
a 0.0
b 1.0
dtype: float64
利用标签的切片运算和普通的Python切片运算不同,其末端是包含的
>>> obj['b':'c']
b 1.0
c 2.0
dtype: float64
利用切片可以对Series的相应部分进行设置
>>> obj['b':'c']=5
>>> obj
a 0.0
b 5.0
c 5.0
d 3.0
dtype: float64
用一个值或序列对DataFrame进行索引其实就是获取一个或多个列:
>>> data=pd.DataFrame(np.arange(16).reshape(4,4),index=['Ohio','Colorado','Utah','New York'],columns=['one','two','three','four'])
>>> data
one two three four
Ohio 0 1 2 3
Colorado 4 5 6 7
Utah 8 9 10 11
New York 12 13 14 15
>>> data['two']
Ohio 1
Colorado 5
Utah 9
New York 13
Name: two, dtype: int32
>>> data[['one','three']]
one three
Ohio 0 2
Colorado 4 6
Utah 8 10
New York 12 14
这种索引方式有几个特殊的情况,首先通过切片或布尔型数组选取数据
>>> data[:2]
one two three four
Ohio 0 1 2 3
Colorado 4 5 6 7
>>>
>>> data[data['three']>5]
one two three four
Colorado 4 5 6 7
Utah 8 9 10 11
New York 12 13 14 15
>>> data
one two three four
Ohio 0 1 2 3
Colorado 4 5 6 7
Utah 8 9 10 11
New York 12 13 14 15
另一种用法是通过布尔型DataFrame进行索引
>>> data<5
one two three four
Ohio True True True True
Colorado True False False False
Utah False False False False
New York False False False False
>>> data[data<5]=0
>>> data
one two three four
Ohio 0 0 0 0
Colorado 0 5 6 7
Utah 8 9 10 11
New York 12 13 14 15
这使得DataFrame的语法和NumPy二维数组的语法很像
2.4.用loc和iloc进行选取
对于DataFrame的行的标签索引,引入标签运算发loc和iloc,它们类似于NumPy的标记,使用轴标签(loc)或整数索引iloc,从DataFrame选取行和列的子集
>>> data.loc['Colorado',['two','three']]
two 5
three 6
Name: Colorado, dtype: int32
>>> data.iloc[2,[3,0,1]]
four 11
one 8
two 9
Name: Utah, dtype: int32
>>> data.iloc[2]
one 8
two 9
three 10
four 11
Name: Utah, dtype: int32
这两个索引函数也适用于一个标签或多个标签的切片
>>> data.loc[:'Utah','two']
Ohio 0
Colorado 5
Utah 9
Name: two, dtype: int32
>>> data.iloc[:,:3][data.three>5]
one two three
Colorado 0 5 6
Utah 8 9 10
New York 12 13 14
在pandas中,有多个方法可以选取和重新组合数据.对于DataFrame,下面是总结
ix是iloc和loc的混合索引
2.5.整数索引
处理整数索引的pandas的对象可能比较麻烦,因为它与Python内置的列表和元组的索引语法不同,例如,你可能认为下面的代码不会出错
>>> ser
0 0.0
1 1.0
2 2.0
dtype: float64
>>> ser[-1]
而它实际上会产生错误,因为这里只有索引0,1,2,而没有-1,这使得原本内置的-1产生了歧义.而对于非整数索引则不会产生歧义
>>> ser2=pd.Series(np.arange(3.),index=['a','b','c'])
>>> ser2
a 0.0
b 1.0
c 2.0
dtype: float64
>>> ser2[-1]
2.0
为了进行统一,如果轴索引含有整数,数据选取总会使用整数,为了更准确,可以使用loc(标签)和iloc(整数)
>>> ser[:1]
0 0.0
dtype: float64
>>> ser.loc[:1]
0 0.0
1 1.0
dtype: float64
>>> ser.iloc[:1]
0 0.0
dtype: float64
>>> ser
0 0.0
1 1.0
2 2.0
dtype: float64
2.6.算术运算和数据对齐
pandas最重要的一个功能是,它可以对不同索引的对象进行算术运算.将对象相加时,如果存在不同的索引对,则结果的索引就是该索引对的并集,就像是在索引标签上进行数据库意义上的自动外连接
>>> s1=Series([7.3,-2.5,3.4,1.5],index=['a','c','d','e'])
>>> s2=Series([-2.1,3.6,-1.5,4,3.1],index=['a','c','e','f','g'])
>>> s1+s2
a 5.2
c 1.1
d NaN
e 0.0
f NaN
g NaN
自动的数据对齐在不重叠的索引处引入了NA值,缺失值会在算术运算过程中传播
对于DataFrame,对齐操作会同时发生在行和列上
>>> df1=DataFrame(np.arange(9.).reshape((3,3)),columns=list('bcd'),index=['Ohio','Texas','Colorado'])
>>> df2=DataFrame(np.arange(12.).reshape((4,3)),columns=list('bde'),index=['Utah','Ohio','Texas','Oregon']) >>> df1
b c d
Ohio 0.0 1.0 2.0
Texas 3.0 4.0 5.0
Colorado 6.0 7.0 8.0
>>> df2
b d e
Utah 0.0 1.0 2.0
Ohio 3.0 4.0 5.0
Texas 6.0 7.0 8.0
Oregon 9.0 10.0 11.0
>>> df1+df2
b c d e
Colorado NaN NaN NaN NaN
Ohio 3.0 NaN 6.0 NaN
Oregon NaN NaN NaN NaN
Texas 9.0 NaN 12.0 NaN
Utah NaN NaN NaN NaN
将它们相加后会返回一个新的DataFrame,其索引和列为原来那两个DataFrame的并集
如果DataFrame对象相加,没有共用的列或行标签,结果都会是空
>>> df1=DataFrame({'A':[1,2]})
>>> df1
A
0 1
1 2
>>> df2=DataFrame({'B':[3,4]})
>>> df2
B
0 3
1 4
>>> df1-df2
A B
0 NaN NaN
1 NaN NaN
2.7.在算术方法中填充值
在对不同索引的对象进行算术运算时,可能希望当一个对象中某个轴标签在另一个对象中找不到时填充一个特定值
>>> df1 = pd.DataFrame(np.arange(12.).reshape((3, 4)),columns=list('abcd'))
>>> df2 = pd.DataFrame(np.arange(20.).reshape((4, 5)),columns=list('abcde'))
>>> df2.loc[1, 'b'] = np.nan
>>> df1
a b c d
0 0.0 1.0 2.0 3.0
1 4.0 5.0 6.0 7.0
2 8.0 9.0 10.0 11.0
>>> df2
a b c d e
0 0.0 1.0 2.0 3.0 4.0
1 5.0 NaN 7.0 8.0 9.0
2 10.0 11.0 12.0 13.0 14.0
3 15.0 16.0 17.0 18.0 19.0
将它们相加时,没有重叠的位置就会产生NA值
>>> df1+df2
a b c d e
0 0.0 2.0 4.0 6.0 NaN
1 9.0 NaN 13.0 15.0 NaN
2 18.0 20.0 22.0 24.0 NaN
3 NaN NaN NaN NaN NaN
使用df1的add方法,传入df2以及一个fill_value参数
>>> df1.add(df2,fill_value=0)
a b c d e
0 0.0 2.0 4.0 6.0 4.0
1 9.0 5.0 13.0 15.0 9.0
2 18.0 20.0 22.0 24.0 14.0
3 15.0 16.0 17.0 18.0 19.0
下面的表出了Series和DataFrame的算术方法。它们每个都有一个副本,以字母r开头,它会翻转参数。因此这两个语句是等价的:
In [172]: 1 / df1
Out[172]:
a b c d
0 inf 1.000000 0.500000 0.333333
1 0.250000 0.200000 0.166667 0.142857
2 0.125000 0.111111 0.100000 0.090909
In [173]: df1.rdiv(1)
Out[173]:
a b c d
0 inf 1.000000 0.500000 0.333333
1 0.250000 0.200000 0.166667 0.142857
2 0.125000 0.111111 0.100000 0.090909
与此类似,在对Series或DataFrame重新索引时,也可以指定一个填充值
>>> df1.reindex(columns=df2.columns, fill_value=0)
a b c d e
0 0.0 1.0 2.0 3.0 0
1 4.0 5.0 6.0 7.0 0
2 8.0 9.0 10.0 11.0 0
2.8.DataFrame和Series之间的运算
与不同维度的NumPy数组一样,DataFrame和Series之间的算术运算也是有明确规定的,下面是一个例子,计算二维数组与其某行之间的差
>>> arr=np.arange(12.).reshape((3,4))
>>> arr
array([[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.]])
>>> arr[0]
array([0., 1., 2., 3.])
>>> arr-arr[0]
array([[0., 0., 0., 0.],
[4., 4., 4., 4.],
[8., 8., 8., 8.]])
当从arr减去arr[0]时,每一行都会执行这个操作,这叫做广播.DataFrame和Series之间的运算差不多也是如此
>>> frame = pd.DataFrame(np.arange(12.).reshape((4, 3)),columns=list('bde'), index=['Utah', 'Ohio', 'Texas', 'Oregon'])
>>> series=frame.iloc[0]
>>> frame
b d e
Utah 0.0 1.0 2.0
Ohio 3.0 4.0 5.0
Texas 6.0 7.0 8.0
Oregon 9.0 10.0 11.0
>>> series
b 0.0
d 1.0
e 2.0
Name: Utah, dtype: float64
默认情况下,DataFrame和Series之间的算术运算会将Series的索引匹配到DataFrame的列,然后沿着行一直向下广播
>>> frame-series
b d e
Utah 0.0 0.0 0.0
Ohio 3.0 3.0 3.0
Texas 6.0 6.0 6.0
Oregon 9.0 9.0 9.0
如果某个索引在DataFrame的列或Series的索引中找不到,则参与运算的两个对象就会被重新索引以形成并集这里就是比较计算的series索引名和DataFrame的列名。
>>> series2 = pd.Series(range(3), index=['b', 'e', 'f'])
>>> frame+series2
b d e f
Utah 0.0 NaN 3.0 NaN
Ohio 3.0 NaN 6.0 NaN
Texas 6.0 NaN 9.0 NaN
Oregon 9.0 NaN 12.0 NaN
如果希望匹配行且在列上广播,必须使用算术运算方法,例如
b d e
Utah 0.0 1.0 2.0
Ohio 3.0 4.0 5.0
Texas 6.0 7.0 8.0
Oregon 9.0 10.0 11.0
>>> series3
Utah 1.0
Ohio 4.0
Texas 7.0
Oregon 10.0
Name: d, dtype: float64
>>> frame.sub(series3,axis='index')
b d e
Utah -1.0 0.0 1.0
Ohio -1.0 0.0 1.0
Texas -1.0 0.0 1.0
Oregon -1.0 0.0 1.0
传入的轴号就是希望匹配的轴,在上面的例子中,目的是匹配DataFrame的行索引(axis=‘index’ or axis=0),并进行广播
2.9.函数应用和映射
NumPy的ufuncs(元素级数组方法)也可以操纵pandas对象
>>> frame = pd.DataFrame(np.random.randn(4, 3), columns=list('bde'),index=['Utah', 'Ohio', 'Texas', 'Oregon'])
>>> frame
b d e
Utah -1.694994 -0.270174 0.597397
Ohio 1.451881 0.585898 -0.897480
Texas 0.558496 0.059872 1.012110
Oregon 0.750981 -0.575618 -0.102459
>>> np.abs(frame)
b d e
Utah 1.694994 0.270174 0.597397
Ohio 1.451881 0.585898 0.897480
Texas 0.558496 0.059872 1.012110
Oregon 0.750981 0.575618 0.102459
另一个常见的操作是,将函数应用到由各列或行所形成的一维数组上,DataFrame的apply方法即可实现此功能
>>> f=lambda x:x.max()-x.min()
>>> frame.apply(f)
b 3.146875
d 1.161516
e 1.909590
dtype: float64
这里的函数 f,计算了一个Series的最大和最小值的差,在frame的每列都执行了一次,结果是一个Seires,使用frame的列作为索引,如果传递axis='columns’到apply,这个函数就会在每行执行
>>> frame.apply(f,axis='columns')
Utah 2.292392
Ohio 2.349361
Texas 0.952238
Oregon 1.326599
dtype: float64
许多最为常见的数组统计功能都被实现为DataFrame的方法,因此无需使用apply方法.
传递到apply的函数不是必须返回一个标量,还可以返回由多个值组成的Series
>>> def f(x):
... return pd.Series([x.min(), x.max()], index=['min', 'max'])
...
>>> frame.apply(f)
b d e
min -1.694994 -0.575618 -0.89748
max 1.451881 0.585898 1.01211
元素级的Python函数也是可以使用的.假如想要得到frame中各个浮点值的格式化字符串,使用applymap即可
>>> format = lambda x: '%.2f' % x
>>> frame.applymap(format)
b d e
Utah -1.69 -0.27 0.60
Ohio 1.45 0.59 -0.90
Texas 0.56 0.06 1.01
Oregon 0.75 -0.58 -0.10
之所以叫做applymap,是因为Series有一个用于应用于元素级函数的map方法
>>> frame['e'].map(format)
Utah 0.60
Ohio -0.90
Texas 1.01
Oregon -0.10
Name: e, dtype: object
2.10.排序和排名
根据条件对数据集进行排序也是一种重要的内置运算,要对行或列索引进行排序(按字典排序),可以使用sort_index方法,它返回一个新对象
>>> obj = pd.Series(range(4), index=['d', 'a', 'b', 'c'])
>>> obj
d 0
a 1
b 2
c 3
dtype: int64
>>> obj.sort_index()
a 1
b 2
c 3
d 0
dtype: int64
对于DataFrame,则可以根据任意一个轴上的索引进行排序
>>> frame = pd.DataFrame(np.arange(8).reshape((2, 4)),index=['three', 'one'],columns=['d', 'a', 'b', 'c'])
>>> frame.sort_index()
d a b c
one 4 5 6 7
three 0 1 2 3
>>> frame.sort_index(axis=1)
a b c d
three 1 2 3 0
one 5 6 7 4
数据默认是按照升序排序的,也可以按照降序排序
>>> frame.sort_index(axis=1,ascending=False)
d c b a
three 0 3 2 1
one 4 7 6 5
若要按值对Series排序,可以使用其sort_values方法
>>> obj=pd.Series([4,7,-3,2])
>>> obj.sort_values()
2 -3
3 2
0 4
1 7
在排序时,任何缺失值默认都会被放到Series的末尾
>>> obj = pd.Series([4, np.nan, 7, np.nan, -3, 2])
>>> obj.sort_values()
4 -3.0
5 2.0
0 4.0
2 7.0
1 NaN
3 NaN
dtype: float64
当排序一个DataFrame时,你可能希望根据一个或多个列的值进行排序,将一个或多个列的名字传递给sort_values的by选项即可
>>> frame = pd.DataFrame({'b': [4, 7, -3, 2], 'a': [0, 1, 0, 1]})
>>> frame
b a
0 4 0
1 7 1
2 -3 0
3 2 1
>>> frame.sort_values(by='b')
b a
2 -3 0
3 2 1
0 4 0
1 7 1
>>> frame.sort_values(by=['b','a'])
b a
2 -3 0
3 2 1
0 4 0
1 7 1
排名会从1开始一直到数组中有效数据的数量.
下面是Series和DataFrame的rank方法.默认情况下,rank是通过"为各组分配一个平均排名"的方式破坏平级关系的
>>> obj = pd.Series([7, -5, 7, 4, 2, 0, 4])
>>> obj.rank()
0 6.5
1 1.0
2 6.5
3 4.5
4 3.0
5 2.0
6 4.5
dtype: float64
(我们可以自己先排一下一些没用争议的数字,比如-5最小,其排名为1.0,0其次,因此其排名为2.0,数字2同理,其排名为3.0,这些都没问题,但是到了4的时候,我们发现Series中存在两个4,那么根据不同的规则,我们可以说数字4的排名是4.0,但也可以说是5.0,而“为各组分配一个平均排名”就能很好的解释这个4.5的来源了,即取4.0和5.0的平均值,那么对于数字7来说,也是一个道理了,其既可以取6.0,也可以取7.0,这里平均一下,就成了6.5了)
也可以根据值在原数据中出现的顺序给出排名
>>> obj.rank(method='first')
0 6.0
1 1.0
2 7.0
3 4.0
4 3.0
5 2.0
6 5.0
dtype: float64
也可以按降序进行排名
>>> obj.rank(ascending=False, method='max')
0 2.0
1 7.0
2 2.0
3 4.0
4 5.0
5 6.0
6 4.0
dtype: float64
下面是所有用于破坏平级关系的method选项
DataFrame可以在行或列上计算排名:
>>> frame = pd.DataFrame({'b': [4.3, 7, -3, 2], 'a': [0, 1, 0, 1], 'c': [-2, 5, 8, -2.5]})
>>> frame
b a c
0 4.3 0 -2.0
1 7.0 1 5.0
2 -3.0 0 8.0
3 2.0 1 -2.5
>>> frame.rank(axis='columns')
b a c
0 3.0 2.0 1.0
1 3.0 1.0 2.0
2 1.0 2.0 3.0
3 3.0 2.0 1.0
2.11.带有重复标签的轴索引
虽然许多pandas函数(如reindex)都要求标签唯一,但这并不是强制性的
>>> obj = pd.Series(range(5), index=['a', 'a', 'b', 'b', 'c'])
>>> obj
a 0
a 1
b 2
b 3
c 4
dtype: int64
索引的is_unique属性可以告诉你它的值是否是唯一的obj.index.is_unique:
>>> obj.index.is_unique
False
对于带有重复值的索引,数据选取的行为将会有些不同。如果某个索引对应多个值,则返回一个Series;而对应单个值的,则返回一个标量值
>>> obj['a']
a 0
a 1
dtype: int64
>>> obj['c']
4
这样会使代码变复杂,因为索引的输出类型会根据标签是否有重复发生变化。对DataFrame的行进行索引时也是如此:
>>> df = pd.DataFrame(np.random.randn(4, 3), index=['a', 'a', 'b', 'b'])
>>> df
0 1 2
a -0.650315 -0.822075 -0.145283
a 1.167576 1.102189 0.583245
b 1.047264 -1.398393 -0.001450
b 0.764462 0.536134 2.858152
>>> df.loc['b']
0 1 2
b 1.047264 -1.398393 -0.001450
b 0.764462 0.536134 2.858152
3.汇总和计算描述统计
pandas对象拥有一组常用的数学和统计方法.它们大部分都属于约简和汇总统计,用于Series中提取单个值(如sum或mean)或从DataFrame的行或列中提取一个Series.与对应的NumPy数组方法相比,它们都是基于没有缺失数据的假设而构建的
>>> df=DataFrame([[1.4,np.nan],[7.1,-4.5],[np.nan,np.nan],[0.75,-1.3]],index=['a','b','c','d'],columns=['one','two'])
>>> df
one two
a 1.40 NaN
b 7.10 -4.5
c NaN NaN
d 0.75 -1.3
调用DataFrame的sum方法将会返回一个含有列的小计的Series:
>>> df.sum()
one 9.25
two -5.80
dtype: float64
通过设置axis=1 可以按行进行求和计算。
NA值将会被自动剔除,除非整个切片都是NA。通过skipna选项可以禁用该功能
下面为一些约简方法常用选项:
有些方法(如idxmin和idxmax)返回的是间接统计(比如达到最大值或最小值的索引):
>>> df.idxmax()
one b
two d
dtype: object
另一些方法则是累计型的
>>> df.cumsum()
one two
a 1.40 NaN
b 8.50 -4.5
c NaN NaN
d 9.25 -5.8
还有一种方法,它既不是约简也不是累计型,describe就是一个例子,它用于一次性产生多个汇总统计:
>>> df.describe()
one two
count 3.000000 2.000000
mean 3.083333 -2.900000
std 3.493685 2.262742
min 0.750000 -4.500000
25% 1.075000 -3.700000
50% 1.400000 -2.900000
75% 4.250000 -2.100000
max 7.100000 -1.300000
对于非数值型数据,describe会产生另一种汇总统计
>>> obj=Series(['a','a','b','c']*4)
>>> obj.describe()
count 16
unique 3
top a
freq 8
dtype: object
下面是所有与描述统计相关的方法
3.1.唯一值,值计数以及成员资格
还有一类方法可以从一维Series的值中抽取信息,下面是例子
obj = pd.Series(['c', 'a', 'd', 'a', 'a', 'b', 'b', 'c', 'c'])
>>> uniques = obj.unique()
>>> uniques
array(['c', 'a', 'd', 'b'], dtype=object)
返回的唯一值是未排序的,如果需要的话,可以对结果再次进行排序(uniques.sort()).相似的,value_counts可以用于计算一个Series中各值出现的频率
>>> obj.value_counts()
a 3
c 3
b 2
d 1
dtype: int64
为了便于查看,结果是按值频率降序排列的
value_counts还是一个顶级的pandas方法,可以用于任何数组或序列
>>> pd.value_counts(obj.values,sort=False)
d 1
c 3
b 2
a 3
dtype: int64
isin 用于判断矢量化集合的成员资格,可以用于过滤Seiries中或DataFrame列中数据的子集
>>> obj
0 c
1 a
2 d
3 a
4 a
5 b
6 b
7 c
8 c
dtype: object
>>> mask=obj.isin(['b','c'])
>>> mask
0 True
1 False
2 False
3 False
4 False
5 True
6 True
7 True
8 True
dtype: bool
>>> obj[mask]
0 c
5 b
6 b
7 c
8 c
dtype: object
与isin类似的是index.get_indexer方法,它返回一个索引数组,从可能包含重复值的数组到另一个不同值的数组
>>> to_match = pd.Series(['c', 'a', 'b', 'b', 'c', 'a'])
>>> unique_vals = pd.Series(['c', 'b', 'a'])
>>> pd.Index(unique_vals).get_indexer(to_match)
array([0, 2, 1, 1, 0, 2], dtype=int64)
下面的表是这几个方法的一些参考信息
有时,你可能希望得到DataFrame中多个相关列的一张柱状图。例如:
>>> data = pd.DataFrame({'Qu1': [1, 3, 4, 3, 4],'Qu2': [2, 3, 1, 2, 3],'Qu3': [1, 5, 2, 4, 4]})
>>> data
Qu1 Qu2 Qu3
0 1 2 1
1 3 3 5
2 4 1 2
3 3 2 4
4 4 3 4
将pandas.value_counts传递给该DataFrame的apply函数,就会出现
>>> reslut=data.apply(pd.value_counts).fillna(0)
>>> reslut
Qu1 Qu2 Qu3
1 1.0 1.0 1.0
2 0.0 2.0 1.0
3 2.0 2.0 0.0
4 2.0 0.0 2.0
5 0.0 0.0 1.0
这里,结果中的行标签是所有列的唯一值。后面的频率值是每个列中这些值的相应计数。
三、数据加载.存储与文件格式
输入输出数据可以划分为几个大类:读取文本文件和其他更高效的磁盘存储格式,加载数据库的数据,利用Web API操作网络资源
1.读写文本格式的数据
pandas提供了一些用于将表格数据读取为DataFrame对象的函数,其中read_csv和read_table可能会是使用最多的
我将大致介绍一下这些函数在将文本数据转换为DataFrame时所用到的一些技术。这些函数的选项可以划分为以下几个大类:
- 索引:将一个或多个列当做返回的DataFrame处理,以及是否从文件、用户获取列名。
- 类型推断和数据转换:包括用户定义值的转换、和自定义的缺失值标记列表等。
- 日期解析:包括组合功能,比如将分散在多个列中的日期时间信息组合成结果中的单个列。
- 迭代:支持对大文件进行逐块迭代。
- 不规整数据问题:跳过一些行、页脚、注释或其他一些不重要的东西(比如由成千上万个逗号隔开的数值数据)。
因为工作中实际遇到的数据可能十分混乱,一些数据加载函数(尤其是read_csv)的选项逐渐变得复杂起来,面对不同的参数,感到很烦.pandas文档由这些参数的例子
其中的一些函数,比如pandas.read_csv,有类型推断功能,因为列数据的类型不属于数据类型.也就是说,不需要指定列的类型到底是整数还是字符串等.其他的数据格式,比如HDF5,Feather和msgpack,会在格式中存储数据类型.日期和其他自定义的类型的处理需要更多时间来处理
下面是一个以逗号分隔的CSV文本文件
a,b,c,d,message
1,2,3,4,hello
5,6,7,8,world
9,10,11,12,foo
>>> df=pd.read_csv('C:\\Users\\PC\\Desktop\\程序\\Python\\Test\\ex1.csv')
>>> df
a b c d message
0 1 2 3 4 hello
1 5 6 7 8 world
2 9 10 11 12 foo
还可以使用read_table,并指定分隔符
>>> pd.read_table('C:\\Users\\PC\\Desktop\\程序\\Python\\Test\\ex1.csv',sep=',')
a b c d message
0 1 2 3 4 hello
1 5 6 7 8 world
2 9 10 11 12 foo
并不是所有文件都有标题行。看看下面这个文件:
1,2,3,4,hello
5,6,7,8,world
9,10,11,12,foo
读入该文件的方法有两个.可以让pandas为其分配默认的列名,也可以自定义列名
>>> pd.read_csv('C:\\Users\\PC\\Desktop\\程序\\Python\\Test\\ex2.csv',header=None)
0 1 2 3 4
0 1 2 3 4 hello
1 5 6 7 8 world
2 9 10 11 12 foo
>>> pd.read_csv('C:\\Users\\PC\\Desktop\\程序\\Python\\Test\\ex2.csv',names=['a','b','c','d','message'])
a b c d message
0 1 2 3 4 hello
1 5 6 7 8 world
2 9 10 11 12 foo
假如希望将message列作为DataFrame的索引,可以明确表示要将该列放到索引4的位置上,也可以通过index_col参数指定message
>>> pd.read_csv('C:\\Users\\PC\\Desktop\\程序\\Python\\Test\\ex2.csv',names=names,index_col='messages')
a b c d
messages
hello 1 2 3 4
world 5 6 7 8
foo 9 10 11 12
如果希望将多个列做成一个层次化索引,只需传入由列编号或列名组成的列表即可:
value1 value2
key1 key2
one a 1 2
b 3 4
c 5 6
d 7 8
two a 9 10
b 11 12
c 13 14
d 15 16
有些情况下,有些表格可能不是用固定的分隔符去分隔字段的(比如空白符或其他模式).看下面的文件
>>> list(open('examples/ex3.txt'))
[' A B C\n',
'aaa -0.264438 -1.026059 -0.619500\n',
'bbb 0.927272 0.302904 -0.032399\n',
'ccc -0.264273 -0.386314 -0.217601\n',
'ddd -0.871858 -0.348382 1.100491\n']
虽然可以手动规整,这里的字段是被数量不同的空白字符放开的.这种情况下,可以传递一个正则表达式作为read_table的分隔符.可以用正则表达式为\s+,于是有
>>> result = pd.read_table('examples/ex3.txt', sep='\s+')
>>> result
A B C
aaa -0.264438 -1.026059 -0.619500
bbb 0.927272 0.302904 -0.032399
ccc -0.264273 -0.386314 -0.217601
ddd -0.871858 -0.348382 1.100491
这里,由于列名比数据行的数量少,所以read_table退镀按第一列应该是DataFrame的索引
这些解析器函数还有许多参数可以帮助处理各种各样的异形文件格式,下面列出了一些
比如说,你可以用skiprows跳过文件的第一行、第三行和第四行:
1,2,3,4,hello
5,6,7,8,world
9,10,11,12,foo
>>> pd.read_csv('examples/ex4.csv', skiprows=[0, 2, 3])
a b c d message
0 1 2 3 4 hello
1 5 6 7 8 world
2 9 10 11 12 foo
缺失值处理是文件解析任务中的一个重要组成部分。缺失数据经常是要么没有(空字符串),要么用某个标记值表示。默认情况下,pandas会用一组经常出现的标记值进行识别,比如NA及NULL:
something,a,b,c,d,message
one,1,2,3,4,NA
two,5,6,,8,world
three,9,10,11,12,foo
>>> result = pd.read_csv('examples/ex5.csv')
>>> result
something a b c d message
0 one 1 2 3.0 4 NaN
1 two 5 6 NaN 8 world
2 three 9 10 11.0 12 foo
In [28]: pd.isnull(result)
Out[28]:
something a b c d message
0 False False False False False True
1 False False False True False False
2 False False False False False False
na_values可以用于一个列表或集合的字符串表示缺失值:
>>> result = pd.read_csv('examples/ex5.csv', na_values=['NULL'])
>>> result
something a b c d message
0 one 1 2 3.0 4 NaN
1 two 5 6 NaN 8 world
2 three 9 10 11.0 12 foo
字典的各列可以使用不同的NA标记值:
>>> sentinels = {'message': ['foo', 'NA'], 'something': ['two']}
>>> pd.read_csv('examples/ex5.csv', na_values=sentinels)
something a b c d message
0 one 1 2 3.0 4 NaN
1 NaN 5 6 NaN 8 world
2 three 9 10 11.0 12 NaN
1.2.逐块读取文本文件
在处理很大的文件时,或找出打文件中的参数集以便于后续处理时,你可能只想要读取文件的一小部部分
在看大文件之前,我们先设置pandas显示地更紧些:
>>> pd.options.display.max_rows = 10
然后有:
>>> result = pd.read_csv('examples/ex6.csv')
>>> result
one two three four key
0 0.467976 -0.038649 -0.295344 -1.824726 L
1 -0.358893 1.404453 0.704965 -0.200638 B
2 -0.501840 0.659254 -0.421691 -0.057688 G
3 0.204886 1.074134 1.388361 -0.982404 R
4 0.354628 -0.133116 0.283763 -0.837063 Q
... ... ... ... ... ..
9995 2.311896 -0.417070 -1.409599 -0.515821 L
9996 -0.479893 -0.650419 0.745152 -0.646038 E
9997 0.523331 0.787112 0.486066 1.093156 K
9998 -0.362559 0.598894 -1.843201 0.887292 G
9999 -0.096376 -1.012999 -0.657431 -0.573315 0
[10000 rows x 5 columns]
If you want to only read a small
如果只想读取几行(避免读取整个文件),通过nrows进行指定即可:
>>> pd.read_csv('examples/ex6.csv', nrows=5)
one two three four key
0 0.467976 -0.038649 -0.295344 -1.824726 L
1 -0.358893 1.404453 0.704965 -0.200638 B
2 -0.501840 0.659254 -0.421691 -0.057688 G
3 0.204886 1.074134 1.388361 -0.982404 R
4 0.354628 -0.133116 0.283763 -0.837063 Q
要逐块读取文件,可以指定chunksize(行数):
>>> chunker = pd.read_csv('ch06/ex6.csv', chunksize=1000)
>>> chunker
<pandas.io.parsers.TextParser at 0x8398150>
read_csv所返回的这个TextParser对象使你可以根据chunksize对文件进行逐块迭代。比如说,我们可以迭代处理ex6.csv,将值计数聚合到"key"列中,如下所示:
>>> chunker = pd.read_csv('examples/ex6.csv', chunksize=1000)
>>> tot = pd.Series([])
>>> for piece in chunker:
>>> tot = tot.add(piece['key'].value_counts(), fill_value=0)
>>> tot = tot.sort_values(ascending=False)
然后有:
>>> tot[:10]
E 368.0
X 364.0
L 346.0
O 343.0
Q 340.0
M 338.0
J 337.0
F 335.0
K 334.0
H 330.0
dtype: float64
TextParser还有一个get_chunk方法,它使你可以读取任意大小的块。
1.3.将数据写出到文本格式
数据也可以被输出为分隔符格式的文本,
>>> data=pd.read_csv("ex5.csv")
>>> data
something a b c d message
0 one 1 2 3.0 4 NaN
1 two 5 6 NaN 8 world
2 three 9 10 11.0 12 foo
利用DataFrame的to_csv方法,可以将数据写入到一个以逗号分隔的文件中
>>> data.to_csv("ex5out.csv")
>>> f=open("ex5out.csv","r",encoding="utf-8")
>>> f.read()
',something,a,b,c,d,message\n0,one,1,2,3.0,4,\n1,two,5,6,,8,world\n2,three,9,10,11.0,12,foo\n'
也可以使用其他分隔符
>>> import sys
>>> data.to_csv(sys.stdout,sep="|")
|something|a|b|c|d|message
0|one|1|2|3.0|4|
1|two|5|6||8|world
2|three|9|10|11.0|12|foo
缺失值在输出结果中会被表示为空字符串,可以设置为别的值
>>> data.to_csv(sys.stdout,na_rep="NULL")
,something,a,b,c,d,message
0,one,1,2,3.0,4,NULL
1,two,5,6,NULL,8,world
2,three,9,10,11.0,12,foo
如果没有设置其他选项,则会写出列和行的标签,他们也可以被禁用
>>> data.to_csv(sys.stdout,index=None,header=None)
one,1,2,3.0,4,
two,5,6,,8,world
three,9,10,11.0,12,foo
还可以只写出一部分列,并以指定的顺序排列
>>> data.to_csv(sys.stdout,index=False,columns=["a","message","c"])
a,message,c
1,,3.0
5,world,
9,foo,11.0
Seires也有一个to_csv方法
>>> dates=pd.date_range("1/1/2000",periods=7)
>>> ts=pd.Series(np.arange(7),index=dates)
>>> ts
2000-01-01 0
2000-01-02 1
2000-01-03 2
2000-01-04 3
2000-01-05 4
2000-01-06 5
2000-01-07 6
Freq: D, dtype: int32
>>> ts.to_csv(sys.stdout)
2000-01-01,0
2000-01-02,1
2000-01-03,2
2000-01-04,3
2000-01-05,4
2000-01-06,5
2000-01-07,6
1.4.处理分隔符格式
大部分存储在磁盘上的表格型数据都能用pd.read_table进行加载,然而,有时还是需要做一些手工处理。由于接收到含有畸形行的文件而使read_table出毛病的情况并不少见。为了说明这些基本工具,看看下面这个 简单的CSV文件:
>>> import csv
>>> f=open("test.csv","r",encoding="utf-8")
>>> data=csv.reader(f)
>>> for line in data:
... print(line)
...
['a', 'b', 'c']
['1', '2', '3']
['1', '2', '3']
现在,为了使数据格式合乎要求,你需要对其做一些整理工作。我们一步一步来做。首先,读取文件到一个多行的列表中:
>>> with open("test.csv","r",encoding="utf-8") as f:
... lines=list(csv.reader(f))
...
>>> lines
[['a', 'b', 'c'], ['1', '2', '3'], ['1', '2', '3']]
然后,我们将这些行分为标题行和数据行:
>>> header,value=lines[0],lines[1:]
>>> header
['a', 'b', 'c']
>>> value
[['1', '2', '3'], ['1', '2', '3']]
然后,我们可以用字典构造式和zip(*values),后者将行转置为列,创建数据 列的字典:
>>> data_dict = {h:v for h, v in zip(header, zip(*value))}
>>> data_dict
{'a': ('1', '1'), 'b': ('2', '2'), 'c': ('3', '3')}
CSV文件的形式有很多.只需定义csv.Dialect的一个子类即可定义出新格式 (如专门的分隔符、字符串引用约定、行结束符等):
class my_dialect(csv.Dialect):
lineterminator = '\n'
delimiter = ';'
quotechar = '"'
quoting = csv.QUOTE_MINIMAL
reader = csv.reader(f,dialect=my_dialect)
各个CSV语支的参数也可以用关键字的形式提供给csv.reader,而无需定义子类:
reader = csv.reader(f,delimiter='|')
可用的选项(csv.Dialect的属性)及其功能如下表所示
对于那些使用复杂分隔符或多字符分隔符的文件,csv模块就无 能为力了.这种情况下,你就只能使用字符串的split方法或正则表达式方法re.split进行行拆分和其他整理工作了.
要手工输出分隔符文件,你可以使用csv.writer.它接受一个已打开且可写的文件对象以及跟csv.reader相同的那些语支和格式化选项:
>>> with open("test.csv","w",encoding="utf-8") as f:
... writer=csv.writer(f,dialect=my_dialect)
... writer.writerow(('one','two','three'))
... writer.writerow(('1','2','3'))
... writer.writerow(('1','2','3'))
>>> pd.read_csv("test.csv")
one;two;three
0 1;2;3
1 1;2;3
1.5.JSON
JSON 已经成为通过HTTP请求在Web浏览器和其他程序之间发送数据的标准格式之一.它是一种比表格型文本格式灵活得多的数据格式
obj = """
{"name":"Wes",
"places_lived":["United States","Spain","Germany"],
"pet":null,
"siblings":[{"name":"Scott","age":25,"pet":"Zuko"},
{"name":"Katie","age":31,"pet":"Cisco"}]
}
"""
除其空值null和一些其他的细微差别(如列表末尾不允许存在多余的逗号)之外,JSON非常接近于有效的Python代码。基本类型有对象(字典)、数组(列表)、字符串、数值、布尔值以及null。对象中所有的键都必须是字符串。许多Python库都可以读写JSON数据。我将使用json,因为它是构建于Python标准库中的。通过json.loads即可将JSON字符串转换成Python形式:
>>> result=json.loads(obj)
>>> result
{'name': 'Wes', 'places_lived': ['United States', 'Spain', 'Germany'], 'pet': None, 'siblings': [{'name': 'Scott', 'age': 25, 'pet': 'Zuko'}, {'name': 'Katie', 'age': 31, 'pet': 'Cisco'}]}
json.dumps则将Python对象转换成JSON格式
>>> asjson=json.dumps(result)
>>> asjson
'{"name": "Wes", "places_lived": ["United States", "Spain", "Germany"], "pet": null, "siblings": [{"name": "Scott", "age": 25, "pet": "Zuko"}, {"name": "Katie", "age": 31, "pet": "Cisco"}]}'
如何将(一个或一组)JSON对象转换为DataFrame或其他便于分析的数据结构就由你决定了.最简便的方式是:像DataFrame的构造器中传入一个字典的列表,并选取数据字段的子集:
>>> siblings = pd.DataFrame(result['siblings'], columns=['name', 'age'])
>>> siblings
name age
0 Scott 25
1 Katie 31
pd.read_json可以自动将特别格式的JSON数据集转化为Series或DataFrame.例如
>>> with open("test.json","r") as f:
... data=f.read()
...
>>> data
'\n[{"a": 1, "b": 2, "c": 3},\n {"a": 4, "b": 5, "c": 6},\n {"a": 7, "b": 8, "c": 9}]\n '
>>> data=pd.read_json("test.json")
>>> data
a b c
0 1 2 3
1 4 5 6
2 7 8 9
如果需要将数据从pandas中输出到Json,可以使用to_json方法
{"a":{"0":1,"1":4,"2":7},"b":{"0":2,"1":5,"2":8},"c":{"0":3,"1":6,"2":9}}
>>> print(data.to_json(orient='records'))
[{"a":1,"b":2,"c":3},{"a":4,"b":5,"c":6},{"a":7,"b":8,"c":9}]
1.6.XML和HTML:Web信息收集
Python有许多可以读写常见的HTML和XML格式数据的库,包括lxml、Beautiful Soup和html5lib。lxml的速度比较快,但其它的库处理有误的HTML或XML文件更好。
pandas有一个内置的功能,read_html,它可以使用lxml和Beautiful Soup自 动将HTML文件中的表格解析为DataFrame对象。为了进行展示,我从美国 联邦存款保险公司下载了一个HTML文件(pandas文档中也使用过),它记 录了银行倒闭的情况。首先,你需要安装read_html用到的库:
pip install lxml
pip install beautifulsoup4 html5lib
pandas.read_html有一些选项,默认条件下,它会搜索、尝试解析标签内的的表格数据。结果是一个列表的DataFrame对象:
tables = pd.read_html('examples/fdic_failed_bank_list.html')
print(len(tables))
failures = tables[0]
failures.head()
这里,我们可以做一些数据清洗和分析(后面会进一步讲解),比如按年份计算倒闭的银行数:
close_timestamps = pd.to_datetime(failures['Closing Date'])
close_timestamps.dt.year.value_counts()
1.7. 利用lxml.objectify解析XML
前面,我介绍了pandas.read_html函数,它可以使用lxml或Beautiful Soup从HTML解析数据。XML和HTML的结构很相似,但XML更为通用。这里,我会 用一个例子演示如何利用lxml从XML格式解析数据。
纽约大都会运输署发布了一些有关其公交和列车服务的数据资料(http:// www.mta.info/developers/download.html)。这里,我们将看看包含在一组XML文件中的运行情况数据。每项列车或公交服务都有各自的文件(如Metro-North Railroad的文件是Performance_MNR.xml),其中每条XML记 录就是一条月度数据,如下所示:
我们先用lxml.objectify解析该文件,然后通过getroot得到该XML文件的根节 点的引用:
from lxml import objectify
path = 'datasets/mta_perf/Performance_MNR.xml'
parsed = objectify.parse(open(path))
root = parsed.getroot()
root.INDICATOR返回一个用于产生各个XML元素的生成器。对于每条记录, 我们可以用标记名(如YTD_ACTUAL)和数据值填充一个字典(排除几个标 记):
data = []
skip_fields = ['PARENT_SEQ', 'INDICATOR_SEQ',
'DESIRED_CHANGE', 'DECIMAL_PLACES']
for elt in root.INDICATOR:
el_data = {}
for child in elt.getchildren():
if child.tag in skip_fields:
continue
el_data[child.tag] = child.pyval
data.append(el_data)
最后,将这组字典转换为一个DataFrame:
perf = pd.DataFrame(data)
perf.head()
XML数据可以比本例复杂得多。每个标记都可以有元数据。看看下面这个HTML的链接标签(它也算是一段有效的XML):
from io import StringIO tag = ‘Google’ root = objectify.parse(StringIO(tag)).getroot() 现在就可以访问标签或链接文本中的任何字段了(如href):
print(root)
print(root.get('href'))
print(root.text)
2.二进制数据格式
实现数据的高效二进制格式存储的最简单办法之一是使用Python内置的pickle序列化,pandas对象都有一个用于将数据以pickle格式保存到磁盘上的to_pickle方法
frame=pd.read_csv('test.csv')
frame.to_pickle('example_pickle')
可以通过pickle直接读取数据,或者使用pd.read_pickle
pd.read_pickle('example_pickle')
pandas内置支持两个二进制数据格式:HDF5和MessagePack。pandas或Numpy数据的其他存储格式有:
- bcolz:一种可压缩的列存储二进制格式,基于Blosc压缩库
- Feather:跨语言的列存储文件格式。其使用了Apache Arrow的列式内存格式。
2.1.使用HDF5格式
HDF5是一种存储大规模科学数组数的非常好的数据格式,它可以被作为C库,带有许多语言的接口.HDF5中的HDF指的是层次型数据格式.每个HDF5文件都含有一个文件系统式的节点结构,它能够存储多个数据集并支持元数据.与其他简单格式相比,HDF5支持存储多种压缩器的即时压缩,还能更高效地存储重复数据模式.对于那些非常大的无法直接存入内存的数据集,HDF5就是一个不错的选择,因为它可以高效的分块读取.
虽然可以用PyTables和h5py库直接访问HDF5文件,pandas提供了更为高效的接口,可以简化存储Series和DataFrane对象.HDFStore类可以像字典一样,处理低级的细节
>>> frame=pd.DataFrame({'a':np.random.randn(100)})
>>> store=pd.HDFStore('./mydata.h5')
>>> store['obj1']=frame
>>> store
<class 'pandas.io.pytables.HDFStore'>
File path: ./mydata.h5
HDF5文件中的对象可以通过与字典一样的API进行获取
>>> pd.options.display.max_rows=10
>>> store['obj1']
a
0 -1.852219
1 0.362798
2 -1.224496
3 0.344129
4 -0.528258
.. ...
95 0.186281
96 -1.445115
97 -0.094687
98 2.630851
99 0.180347
[100 rows x 1 columns]
HDFStore支持两种存储模式,‘fixed’和’table’.后者通常更慢,但是支持使用特殊语法进行查询操作
>>> store.put('obj2',frame,format='table')
>>> store.select('obj2',where=['index>=10 and index<=15'])
a
10 -0.388608
11 -1.380919
12 1.030166
13 1.480958
14 0.845949
15 -0.527707
>>> store.close()
put是store[‘obj2’]=frame方法的显示版本,允许我们设置其他的选项,比如格式
pandas.read_hdf()函数可以快捷地使用这些工具:
>>> frame.to_hdf('mydata.h5','obj3',format='table')
>>> pd.read_hdf('mydata.h5','obj3',where=['index < 5'])
a
0 -1.852219
1 0.362798
2 -1.224496
3 0.344129
4 -0.528258
注意:如果需要处理的数据位于远程服务器,比如Amazon S3或HDFS,使用专门为分布式存储(比如Apache Parquet)的二进制格式也许更加合适。
如需要本地处理海量数据,需好好研究PyTables和h5py。由于许多数据分析问题都是IO密集型(非CPU密集型),利用HDF5这类工具能显著提升应用程序的效率。(HDF5不是数据库,是最适合用作“一次写多次读”的数据集)
2.2.读取Miscrosoft Excel文件
pandas的ExcelFile类或pandas.read_excel函数支持读取存储在Excel 2003及更高版本的表格型数据,他们分别使用xlrd或openpyxl读取xls和xlsx文件
要使用ExcelFile,通过传递xls或xlsx路径创建一个实例:
xlsx = pd.ExcelFile('ex1.xlsx')
存储在表单中的数据可以用read_excel读取到DataFrame中
pd.read_excel(xlsx,'sheet1')
如果要读取一个文件中的多个表单,创建ExcelFile会更快,但是也可以将文件名直接传递到pd.read_excel
frame=pd.read_excel('ex1.xlsx')
如果要将pandas的数据写入为Execl格式,必须首先创建一个ExcelWriter,然后使用pandas对象的to_excel方法将数据写入其中
writer=pd.ExcelWriter('newex.xlsx')
frame.to_excel(writer,'sheet1')
writer.save()
还可以不使用ExcelWriter,而是传递文件的路径到to_excel
frame.to_excel('newex,xlsx')
2.3.Web APIs交互
通过爬虫获得数据字典(json文件)后,可以使用DataFrame进行解析,提取想要的字段
frame=pd.DataFrame(data,columns=[xx,xx,xx])
2.4.数据库交互
在商业场景下,大多数数据可能不是存储在文本或Excel文件中。基于SQL的关系型数据库(如SQL Server、PostgreSQL和MySQL等)使用非常广泛,其它一些数据库也很流行。数据库的选择通常取决于性能、数据完整性以及应用程序的伸缩性需求。
将数据从SQL加载到DataFrame的过程很简单,此外pandas还有一些能够简化该过程的函数。例如,我将使用SQLite数据库(通过Python内置的sqlite3驱动器):
import sqlite3
query = """
CREATE TABLE test
(a VARCHAR(20), b VARCHAR(20),
c REAL, d INTEGER
;"""
con = sqlite3.connect('mydata.sqlite')
con.execute(query)
<sqlite3.Cursor at 0x7f6b12a50f10>
con.commit()
然后插入几行数据:
data = [('Atlanta', 'Georgia', 1.25, 6),
.....: ('Tallahassee', 'Florida', 2.6, 3),
.....: ('Sacramento', 'California', 1.7, 5)]
stmt = "INSERT INTO test VALUES(?, ?, ?, ?)"
con.executemany(stmt, data)
<sqlite3.Cursor at 0x7f6b15c66ce0>
从表中选取数据时,大部分Python SQL驱动器(PyODBC、psycopg2、MySQLdb、pymssql等)都会返回一个元组列表:
cursor = con.execute('select * from test')
rows = cursor.fetchall()
rows
[('Atlanta', 'Georgia', 1.25, 6),
('Tallahassee', 'Florida', 2.6, 3),
('Sacramento', 'California', 1.7, 5)]
你可以将这个元组列表传给DataFrame构造器,但还需要列名(位于光标的description属性中):
cursor.description
(('a', None, None, None, None, None, None),
('b', None, None, None, None, None, None),
('c', None, None, None, None, None, None),
('d', None, None, None, None, None, None))
pd.DataFrame(rows, columns=[x[0] for x in cursor.description])
a b c d
0 Atlanta Georgia 1.25 6
1 Tallahassee Florida 2.60 3
2 Sacramento California 1.70 5
四、数据清洗和准备
在数据分析和建模的过程中,相当多的时间要用在数据准备上:加载、清理、转换以及重塑。这些工作会占到分析师时间的80%或更多。有时,存储在文件和数据库中的数据的格式不适合某个特定的任务。许多研究者都选择使用通用编程语言(如Python、Perl、R或Java)或UNIX文本处理工具(如sed或awk)对数据格式进行专门处理。幸运的是,pandas和内置的Python标准库提供了一组高级的、灵活的、快速的工具,可以让你轻松地将数据规整为想要的格式。
如果你发现了一种本书或pandas库中没有的数据操作方式,请在邮件列表或GitHub网站上提出。实际上,pandas的许多设计和实现都是由真实应用的需求所驱动的。
在本章中,我会讨论处理缺失数据、重复数据、字符串操作和其它分析数据转换的工具。
1.处理缺失数据
在许多数据分析工作中,缺失数据是经常发生的。pandas的目标之一就是尽量轻松地处理缺失数据。例如,pandas对象的所有描述性统计默认都不包括缺失数据。
缺失数据在pandas中呈现的方式有些不完美,但对于大多数用户可以保证功能正常。对于数值数据,pandas使用浮点值NaN(Not a Number)表示缺失数据。我们称其为哨兵值,可以方便的检测出来:
>>> string_data = pd.Series(['aardvark', 'artichoke', np.nan, 'avocado'])
>>> string_data
0 aardvark
1 artichoke
2 NaN
3 avocado
dtype: object
>>> string_data.isnull()
0 False
1 False
2 True
3 False
dtype: bool
在pandas中,采用了R语言中的惯用法,即将缺失值表示为NA,它表示不可用not available.在统计应用中,NA数据可能是不存在的数据或者虽然存在,但未被观察到.当进行数据清洗和分析时,最好直接对缺失数据进行分析,以判断数据采集的问题或缺失数据可能导致的偏差
Python内置的None值在对象数组中也可以作为NA:
>>> string_data[0]=None
>>> string_data.isnull()
0 True
1 False
2 True
3 False
dtype: bool
pandas项目中还在不断优化内部细节以更好地处理缺失数据,像用户的API功能,pd.isnull
1.1.滤除缺失数据
过滤掉缺失数据的办法有很多.可以通过pd.isnull或布尔索引的手工方法,但dropna可能会更实用一些
>>> from numpy import nan as NA
>>> data=pd.Series([1, NA, 3.5, NA, 7])
>>> data.dropna()
0 1.0
2 3.5
4 7.0
dtype: float64
这等价于
>>> data[data.notnull()]
0 1.0
2 3.5
4 7.0
dtype: float64
对于DataFrame对象,dropna默认丢弃任何含有缺失值的行
>>> data = pd.DataFrame([[1., 6.5, 3.], [1., NA, NA],[NA, NA, NA], [NA, 6.5, 3.]])
>>> data
0 1 2
0 1.0 6.5 3.0
1 1.0 NaN NaN
2 NaN NaN NaN
3 NaN 6.5 3.0
>>> data.dropna()
0 1 2
0 1.0 6.5 3.0
传入how='all’将之丢弃全为NA的那些行
>>> data.dropna(how='all')
0 1 2
0 1.0 6.5 3.0
1 1.0 NaN NaN
3 NaN 6.5 3.0
用这种方式丢弃,只需传入axis=1
>>> data[4]=NA
>>> data
0 1 2 4
0 1.0 6.5 3.0 NaN
1 1.0 NaN NaN NaN
2 NaN NaN NaN NaN
3 NaN 6.5 3.0 NaN
>>> data.dropna(axis=1)
Empty DataFrame
Columns: []
Index: [0, 1, 2, 3]
>>> data.dropna(axis=1,how='all')
0 1 2
0 1.0 6.5 3.0
1 1.0 NaN NaN
2 NaN NaN NaN
3 NaN 6.5 3.0
另一个滤除DataFrame行的问题涉及时间序列数据.假设只想留下一部分观测数据,可以使用thresh参数实现此目的
>>> df=pd.DataFrame(data=np.random.randn(7,3))
>>> df.iloc[:4,1]=NA
>>> df.iloc[:2,2]=NA
>>> df
0 1 2
0 -1.094214 NaN NaN
1 0.736272 NaN NaN
2 -0.022099 NaN -1.360762
3 -1.237905 NaN -0.439053
4 0.278880 0.334727 -1.822946
5 -0.607863 -0.513699 0.604313
6 0.197915 1.674042 -1.346943
>>> df.dropna()
0 1 2
4 0.278880 0.334727 -1.822946
5 -0.607863 -0.513699 0.604313
6 0.197915 1.674042 -1.346943
>>> df.dropna(thresh=2)
0 1 2
2 -0.022099 NaN -1.360762
3 -1.237905 NaN -0.439053
4 0.278880 0.334727 -1.822946
5 -0.607863 -0.513699 0.604313
6 0.197915 1.674042 -1.346943
1.2.填充缺失数据
大多数情况fillna是最主要的函数
>>> df.fillna(0)
0 1 2
0 -1.094214 0.000000 0.000000
1 0.736272 0.000000 0.000000
2 -0.022099 0.000000 -1.360762
3 -1.237905 0.000000 -0.439053
4 0.278880 0.334727 -1.822946
5 -0.607863 -0.513699 0.604313
6 0.197915 1.674042 -1.346943
若用一个字典,fillna就可以实现对不同列填充不同的值
>>> df.fillna({1:0,2:99})
0 1 2
0 -1.094214 0.000000 99.000000
1 0.736272 0.000000 99.000000
2 -0.022099 0.000000 -1.360762
3 -1.237905 0.000000 -0.439053
4 0.278880 0.334727 -1.822946
5 -0.607863 -0.513699 0.604313
6 0.197915 1.674042 -1.346943
fillna会默认返回新对象,但也可以就现有对象进行就地修改
>>> _=df.fillna(0,inplace=True)
>>> df
0 1 2
0 -1.094214 0.000000 0.000000
1 0.736272 0.000000 0.000000
2 -0.022099 0.000000 -1.360762
3 -1.237905 0.000000 -0.439053
4 0.278880 0.334727 -1.822946
5 -0.607863 -0.513699 0.604313
6 0.197915 1.674042 -1.346943
对于reindexing有效的那些插值的方法也可以用于fillna:
>>> df = pd.DataFrame(np.random.randn(6, 3))
>>> df.iloc[2:, 1] = NA
>>> df.iloc[4:, 2] = NA
>>> df
0 1 2
0 -0.176256 0.420071 0.105654
1 0.032201 1.239530 -0.523275
2 0.070707 NaN -0.002044
3 -1.079108 NaN -0.104748
4 0.458134 NaN NaN
5 0.097470 NaN NaN
>>> df.fillna(method='ffill')
0 1 2
0 -0.176256 0.420071 0.105654
1 0.032201 1.239530 -0.523275
2 0.070707 1.239530 -0.002044
3 -1.079108 1.239530 -0.104748
4 0.458134 1.239530 -0.104748
5 0.097470 1.239530 -0.104748
>>> df.fillna(method='ffill',limit=2)
0 1 2
0 -0.176256 0.420071 0.105654
1 0.032201 1.239530 -0.523275
2 0.070707 1.239530 -0.002044
3 -1.079108 1.239530 -0.104748
4 0.458134 NaN -0.104748
5 0.097470 NaN -0.104748
下面是fillna的参数
2.数据转换
DataFrame中出现重复行有多种原因,下面是一个例子
>>> data = pd.DataFrame({'k1': ['one', 'two'] * 3 + ['two'],'k2': [1, 1, 2, 3, 3, 4, 4]})
>>> data
k1 k2
0 one 1
1 two 1
2 one 2
3 two 3
4 one 3
5 two 4
6 two 4
DataFrame的duplicated方法返回一个布尔型Series,表示各行是否是重复行
>>> data.duplicated()
0 False
1 False
2 False
3 False
4 False
5 False
6 True
还有一个于此相关的drop_duplicates方法,它返回一个DataFrame,重复的数组会被标记为False
>>> data.drop_duplicates()
k1 k2
0 one 1
1 two 1
2 one 2
3 two 3
4 one 3
5 two 4
这两个方法默认会判断全部列,也可以指定部分列进行重复项判断.假设还有一列值,且只希望根据k1列过滤重复值
>>> data['v1']=range(7)
>>> data
k1 k2 v1
0 one 1 0
1 two 1 1
2 one 2 2
3 two 3 3
4 one 3 4
5 two 4 5
6 two 4 6
>>> data.drop_duplicates('k1')
k1 k2 v1
0 one 1 0
1 two 1 1
duplicated和drop_duplicates默认保留的是第一个出现的值组合,传入keep='last’则保留最后一个:
>>> data.drop_duplicates(['k1','k2'],keep='last')
k1 k2 v1
0 one 1 0
1 two 1 1
2 one 2 2
3 two 3 3
4 one 3 4
6 two 4 6
2.1.利用函数或映射进行出局转换
对于许多数据值,可能希望根据数组,Seires或DataFrame列中的值来实现转换工作,下面是一组有关肉类的数据
>>> data = pd.DataFrame({'food': ['bacon', 'pulled pork', 'bacon','Pastrami', 'corned beef', 'Bacon','pastrami', 'honey ham', 'nova lox'],'ounces': [4, 3, 12, 6, 7.5, 8, 3, 5, 6]})
>>> data
food ounces
0 bacon 4.0
1 pulled pork 3.0
2 bacon 12.0
3 Pastrami 6.0
4 corned beef 7.5
5 Bacon 8.0
6 pastrami 3.0
7 honey ham 5.0
8 nova lox 6.0
假如要添加一列表示该肉类食物来源的动物类型,可以先编写一个不同肉类动物的映射
>>> meat_to_animal = {
... 'bacon': 'pig',
... 'pulled pork': 'pig',
... 'pastrami': 'cow',
... 'corned beef': 'cow',
... 'honey ham': 'pig',
... 'nova lox': 'salmon'
... }
Series的map方法接受一个函数或者映射关系的字典型对象,但是这里有一些肉类的首字母大写了,而另一些则没有,因此还需要先将各个值转换为小写
>>> data['anmial']=lowercased.map(meat_to_animal)
>>> data
food ounces anmial
0 bacon 4.0 pig
1 pulled pork 3.0 pig
2 bacon 12.0 pig
3 Pastrami 6.0 cow
4 corned beef 7.5 cow
5 Bacon 8.0 pig
6 pastrami 3.0 cow
7 honey ham 5.0 pig
8 nova lox 6.0 salmon
也可以传入一个能够完成全部工作的函数
>>> data['food'].map(lambda x: meat_to_animal[x.lower()])
0 pig
1 pig
2 pig
3 cow
4 cow
5 pig
6 cow
7 pig
8 salmon
Name: food, dtype: object
使用map是一种实现元素级转换以及其他数据清理工作的便捷方式
2.2.替换值
利用fillna方法填充缺失数据可以看作是替换的一种特殊情况.前面已经看到,map可以用于修改对象的数据子集,而replace则提供了一种实现该功能更简单的方式
>>> data = pd.Series([1., -999., 2., -999., -1000., 3.])
>>> data
0 1.0
1 -999.0
2 2.0
3 -999.0
4 -1000.0
5 3.0
dtype: float64
-999这个值可能是一个表示缺失数据的标记值,要将它替换为pandas能够理解的NA值,可以使用replace来禅师一个新的Series(除非传入inplace=True)
>>> data.replace(-999,np.nan)
0 1.0
1 NaN
2 2.0
3 NaN
4 -1000.0
5 3.0
dtype: float64
如果希望一次替换多个值,可以传入一个由代替换值组成的列表和替换列表
>>> data.replace([-999, -1000], [np.nan, 0])
0 1.0
1 NaN
2 2.0
3 NaN
4 0.0
5 3.0
dtype: float64
传入的参数也可以是一个字典
>>> data.replace({-999: np.nan, -1000: 0})
0 1.0
1 NaN
2 2.0
3 NaN
4 0.0
5 3.0
dtype: float64
2.3.重命名轴索引
与Series中的值一样,轴标签也可以通过函数或映射进行转换,从而得到一个新的不同标签的对象.轴还可以就地修改,而无需新建一个数据结构
>>> data = pd.DataFrame(np.arange(12).reshape((3, 4)),index=['Ohio', 'Colorado', 'New York'],columns=['one', 'two', 'three', 'four'])
>>> data
one two three four
Ohio 0 1 2 3
Colorado 4 5 6 7
New York 8 9 10 11
与Series一样,轴索引也有一个map方法
>>> transform = lambda x: x[:4].upper()
>>> data.index.map(transform)
Index(['OHIO', 'COLO', 'NEW '], dtype='object')
>>> data
one two three four
Ohio 0 1 2 3
Colorado 4 5 6 7
New York 8 9 10 11
>>> data.index=data.index.map(transform)
>>> data
one two three four
OHIO 0 1 2 3
COLO 4 5 6 7
NEW 8 9 10 11
如果想要创建数据集的转换版(而不是修改原始数据),比较实用的方法是rename
>>> data.rename(index=str.title, columns=str.upper)
ONE TWO THREE FOUR
Ohio 0 1 2 3
Colo 4 5 6 7
New 8 9 10 11
rename可以结合字典型对象实现对部分轴标签的更新
>>> data.rename(index={'OHIO': 'INDIANA'},columns={'three': 'peekaboo'})
one two peekaboo four
INDIANA 0 1 2 3
COLO 4 5 6 7
NEW 8 9 10 11
rename可以实现复制DataFrame并对其索引和列标签进行赋值.如果希望就地修改数据集,可以传入inplace=True
>>> data.rename(index={'OHIO': 'INDIANA'},columns={'three': 'peekaboo'})
one two peekaboo four
INDIANA 0 1 2 3
COLO 4 5 6 7
NEW 8 9 10 11
>>> data
one two three four
OHIO 0 1 2 3
COLO 4 5 6 7
NEW 8 9 10 11
>>> _=data.rename(index={'OHIO': 'INDIANA'},columns={'three': 'peekaboo'},inplace=True)
>>> data
one two peekaboo four
INDIANA 0 1 2 3
COLO 4 5 6 7
NEW 8 9 10 11
2.4.离散化和面元划分
为了便于分析,连续数据经常被离散化或拆分为面元.假设由一组人员数据,希望将他们划分为不同的年龄组,需要用到cut函数
ages = [20, 22, 25, 27, 21, 23, 37, 31, 61, 45, 41, 32]
>>> bins = [18, 25, 35, 60, 100]
>>> cats = pd.cut(ages, bins)
>>> cats
[(18, 25], (18, 25], (18, 25], (25, 35], (18, 25], ..., (25, 35], (60, 100], (35, 60], (35, 60], (25, 35]]
Length: 12
Categories (4, interval[int64]): [(18, 25] < (25, 35] < (35, 60] < (60, 100]]
pandas返回的是一个特殊的Categorical对象.结果展示了pd.cut划分的面元.可以将其看作一组表示面元名称的字符串.它的底层含有一个表示不同分类名称的类型数组,以及一个codes属性中的年龄数据的标签
>>> cats.codes
array([0, 0, 0, 1, 0, 0, 2, 1, 3, 2, 2, 1], dtype=int8)
>>> cats.categories
IntervalIndex([(18, 25], (25, 35], (35, 60], (60, 100]],
closed='right',
dtype='interval[int64]')
>>> pd.value_counts(cats)
(18, 25] 5
(25, 35] 3
(35, 60] 3
(60, 100] 1
dtype: int64
pd.value_counts(cats)是pd.cut结果的面元计数.区间的开闭可以通过right=False修改
>>> pd.cut(ages,[18,26,36,61,100],right=False)
[[18, 26), [18, 26), [18, 26), [26, 36), [18, 26), ..., [26, 36), [61, 100), [36, 61), [36, 61), [26, 36)]
Length: 12
Categories (4, interval[int64]): [[18, 26) < [26, 36) < [36, 61) < [61, 100)]
也可以自定义面元名称
>>> group_names=['Youth', 'YoungAdult', 'MiddleAged', 'Senior']
>>> pd.cut(ages, bins, labels=group_names)
['Youth', 'Youth', 'Youth', 'YoungAdult', 'Youth', ..., 'YoungAdult', 'Senior', 'MiddleAged', 'MiddleAged', 'YoungAdult']
Length: 12
Categories (4, object): ['Youth' < 'YoungAdult' < 'MiddleAged' < 'Senior']
如果像cut传入的是面元的数量,而不是确切的面元边界,则它会更具数据的最小值和最大值计算等长面元
>>> data=np.random.rand(20)
>>> pd.cut(data,4,precision=2)
[(0.69, 0.9], (0.69, 0.9], (0.03, 0.25], (0.47, 0.69], (0.03, 0.25], ..., (0.03, 0.25], (0.47, 0.69], (0.69, 0.9], (0.47, 0.69], (0.47, 0.69]]
Length: 20
Categories (4, interval[float64]): [(0.03, 0.25] < (0.25, 0.47] < (0.47, 0.69] < (0.69, 0.9]]
选项precision=2,限定小数只有两位
qcut是一个非常类似于cut的函数,它可以根据样本分位数对数据进行面元划分.根据数据的分布情况,cut可能无法使各个面元中含有相同数量的数据点.而qcut由于使用的是样本分位数,因此可以得到大小基本相等的面元
>>> data=np.random.randn(1000)
>>> cats=pd.qcut(data,4)
>>> cats
[(-3.354, -0.614], (-3.354, -0.614], (-0.614, 0.0551], (-0.614, 0.0551], (0.0551, 0.673], ..., (-3.354, -0.614], (-0.614, 0.0551], (0.0551, 0.673], (0.0551, 0.673], (-0.614, 0.0551]]
Length: 1000
Categories (4, interval[float64]): [(-3.354, -0.614] < (-0.614, 0.0551] < (0.0551, 0.673] <
(0.673, 3.701]]
>>> pd.value_counts(cats)
(-3.354, -0.614] 250
(-0.614, 0.0551] 250
(0.0551, 0.673] 250
(0.673, 3.701] 250
dtype: int64
与cut类似,也可以传递自定义的分位数
>>> pd.qcut(data, [0, 0.1, 0.5, 0.9, 1.])
[(-3.354, -1.198], (-1.198, 0.0551], (-1.198, 0.0551], (-1.198, 0.0551], (0.0551, 1.309], ..., (-3.354, -1.198], (-1.198, 0.0551], (0.0551, 1.309], (0.0551, 1.309], (-1.198, 0.0551]]
Length: 1000
Categories (4, interval[float64]): [(-3.354, -1.198] < (-1.198, 0.0551] < (0.0551, 1.309] <
(1.309, 3.701]]
2.5.检测或过滤异常值
过滤或变换异常值(outlier)在很大程度上就是运用数组运算
>>> data.describe()
0 1 2 3
count 1000.000000 1000.000000 1000.000000 1000.000000
mean -0.002659 0.003051 0.060634 0.004865
std 1.018644 0.999333 0.995145 1.026679
min -3.443144 -2.940907 -2.990296 -3.220375
25% -0.700240 -0.692949 -0.596679 -0.675073
50% 0.053033 0.003125 0.076653 0.044675
75% 0.640721 0.684129 0.751575 0.681967
max 4.351650 3.036456 3.134132 2.890003
假设想要找出某列中绝对值大小超过三的值
>>> col=data[2]
>>> col[np.abs(col)>3]
15 3.134132
Name: 2, dtype: float64
要选出所有"含有绝对值大于3的数"的行,可以在布尔型DataFrame中使用any方法
>>> cond=(np.abs(data)>3).any(1)
>>> cond
0 False
1 False
2 False
3 False
4 False
...
995 False
996 False
997 False
998 False
999 False
Length: 1000, dtype: bool
>>> data[cond]
0 1 2 3
15 -1.735681 0.858698 3.134132 1.865917
60 -3.443144 -0.500578 -0.058665 -0.579071
80 4.351650 -1.404928 0.640888 -1.849651
264 -1.714825 -1.150842 2.292240 -3.125362
372 0.207042 0.444638 -0.061285 -3.018721
424 3.085371 1.169892 1.866065 -0.374582
521 3.102249 -0.612223 1.176108 -0.552468
554 -0.967331 3.036456 -1.068899 -0.683896
629 -3.196722 -0.330125 0.725285 0.422288
656 -1.468287 -1.132358 1.250918 -3.220375
940 -0.798398 1.677230 0.224605 -3.210410
根据这些条件,就可以对值进行设置.下面可以将值限制在区间[-3,3]
>>> data[np.abs(data)>3]=np.sign(data)*3
>>> data.describe()
0 1 2 3
count 1000.000000 1000.000000 1000.000000 1000.000000
mean -0.003558 0.003015 0.060500 0.005440
std 1.011168 0.999223 0.994740 1.024940
min -3.000000 -2.940907 -2.990296 -3.000000
25% -0.700240 -0.692949 -0.596679 -0.675073
50% 0.053033 0.003125 0.076653 0.044675
75% 0.640721 0.684129 0.751575 0.681967
max 3.000000 3.000000 3.000000 2.890003
根据数据的值是正还是负,np.sign(data)可以生成1和-1
>>> np.sign(data).head()
0 1 2 3
0 1.0 -1.0 1.0 -1.0
1 -1.0 -1.0 1.0 -1.0
2 1.0 1.0 1.0 1.0
3 -1.0 -1.0 1.0 1.0
4 -1.0 1.0 -1.0 -1.0
2.6.排列和随机采样
利用np.random.permutation函数可以实现对Series或DataFrame的列的排列工作.通过需要排列的轴的长度调用permutation,可以产生一个表示新顺序的整数数组
>>> df=pd.DataFrame(np.arange(5*4).reshape((5,4)))
>>> sampler=np.random.permutation(5)
>>> sampler
array([3, 2, 4, 1, 0])
>>> df
0 1 2 3
0 0 1 2 3
1 4 5 6 7
2 8 9 10 11
3 12 13 14 15
4 16 17 18 19
然后就可以在基于iloc的索引操作或take函数中使用该数组了
>>> df.take(sampler)
0 1 2 3
3 12 13 14 15
2 8 9 10 11
4 16 17 18 19
1 4 5 6 7
0 0 1 2 3
如果不像用替代的方式选取随机子集,可以在Series和DataFrame上使用sample方法
>>> df.sample(3)
0 1 2 3
1 4 5 6 7
2 8 9 10 11
4 16 17 18 19
要通过替换的方式产生样本(允许重复选择),可以传递replace=True到sample
>>> choies=pd.Series([5, 7, -1, 6, 4])
>>> draws=choices.sample(n=10,replace=True)
>>> draws=choies.sample(n=10,replace=True)
>>> draws
1 7
0 5
2 -1
4 4
0 5
0 5
0 5
0 5
3 6
0 5
dtype: int64
2.7.计算指标/哑变量
另一种常用于统计建模或机器学习的转换方法是:将分类变量(categorical variable)转换为"哑变量"或"指标矩阵"
如果DataFrame的某列含有k个不同的值,则可以派生出一个k列矩阵或DataFrame(其值全为1和0).pd有一个get_dummies函数可以实现该功能
>>> df = pd.DataFrame({'key': ['b', 'b', 'a', 'c', 'a', 'b'],'data1': range(6)})
>>> df
key data1
0 b 0
1 b 1
2 a 2
3 c 3
4 a 4
5 b 5
>>> pd.get_dummies(df['key'])
a b c
0 0 1 0
1 0 1 0
2 1 0 0
3 0 0 1
4 1 0 0
5 0 1 0
有时可能希望给指标DataFrame的列加上一个前缀,以便能够和其他数据进行合并.get_dummies的prefix参数可以实现
>>> dummies = pd.get_dummies(df['key'], prefix='key')
>>> df_with_dummy = df[['data1']].join(dummies)
>>> df_with_dummy
data1 key_a key_b key_c
0 0 0 1 0
1 1 0 1 0
2 2 1 0 0
3 3 0 0 1
4 4 1 0 0
5 5 0 1 0
如果DataFrame中的某行同属于多个分类,就会有点复杂.
3.字符串操作
pandas对字符串的操作进行了加强,它能够对整组数据应用字符串表达式和正则表达式,并且能够处理缺失数据
3.1.内置字符串操作
对于许多字符串处理和脚本应用,内置的字符串方法和函数以及正则表达式已经能够满足需求
3.2.pandas的矢量化字符串函数
清理待分析的散乱数据时,经常需要做一些字符串的规整化工作.更为复杂的是,含有字符串的列有时还含有缺失数据
>>> data = {'Dave': 'dave@google.com', 'Steve': 'steve@gmail.com','Rob': 'rob@gmail.com', 'Wes': np.nan}
>>> data=pd.Series(data)
>>> data
Dave dave@google.com
Steve steve@gmail.com
Rob rob@gmail.com
Wes NaN
dtype: object
>>> data.isnull()
Dave False
Steve False
Rob False
Wes True
dtype: bool
通过data.map,所有字符串和正则表达式方法都能够被应用于各个值,但是如果存在NA就会报错.Series有一些方法能够跳过NA值的面向数组方法,进行字符串操作.通过Series的str属性就可以访问这些方法
>>> data.str.contains('gmail')
Dave False
Steve True
Rob True
Wes NaN
dtype: object
>>> data.str.findall(pattern, flags=re.IGNORECASE)
Dave [(dave, google, com)]
Steve [(steve, gmail, com)]
Rob [(rob, gmail, com)]
Wes NaN
dtype: object
有两个办法可以实现矢量化的元素获取操作:要么使用str.get,要么在str属性上使用索引
>>> matches = data.str.match(pattern, flags=re.IGNORECASE)
>>> matches
Dave True
Steve True
Rob True
Wes NaN
dtype: object
要访问列表中的元素,可以传递索引到这两个函数中
>>> x=data.str.findall(pattern, flags=re.IGNORECASE)
>>> x.str.get(1)
Dave NaN
Steve NaN
Rob NaN
Wes NaN
dtype: float64
>>> x.str[0]
Dave (dave, google, com)
Steve (steve, gmail, com)
Rob (rob, gmail, com)
Wes NaN
dtype: object
>>> x
Dave [(dave, google, com)]
Steve [(steve, gmail, com)]
Rob [(rob, gmail, com)]
Wes NaN
dtype: object
可以用这种办法对字符串进行截取:
>>> data.str[:5]
Dave dave@
Steve steve
Rob rob@g
Wes NaN
dtype: object
五、数据规整:聚合,合并和重塑
在许多应用中,数据可能分散在许多文件或数据库中,存储的形式也不利于分析,本章关注可以聚合、合并、重塑数据的方法
1.层次化索引
层次化索引是pandas的一项重要功能,它使你能够在一个轴上拥有多个索引级别,抽象地说,它能是你以低维度形式处理高维度数据.下面是一个Series,并以一个数组组成的列表为索引
>>> data = pd.Series(np.random.randn(9),index=[['a', 'a', 'a', 'b', 'b', 'c', 'c', 'd', 'd'],[1, 2, 3, 1, 3, 1, 2, 2, 3]])
>>> data
a 1 -0.660543
2 -0.207110
3 0.116586
b 1 -0.538836
3 1.277390
c 1 0.406618
2 -1.699074
d 2 0.050155
3 1.398109
dtype: float64
上面是经过美化的带有MultiIndex索引的Series的格式.索引之间的"间隔"表示"直接使用上面的标签":
>>> data.index
MultiIndex([('a', 1),
('a', 2),
('a', 3),
('b', 1),
('b', 3),
('c', 1),
('c', 2),
('d', 2),
('d', 3)],
)
对于一个层次化索引的对象,可以使用所谓的部分索引,使用它选取数据子集的操作更简单:
>>> data['b']
1 -0.538836
3 1.277390
dtype: float64
>>> data['b':'c']
b 1 -0.538836
3 1.277390
c 1 0.406618
2 -1.699074
dtype: float64
>>> data.loc[['b','d']]
b 1 -0.538836
3 1.277390
d 2 0.050155
3 1.398109
dtype: float64
有时甚至可以在"内层中进行选取"
>>> data.loc[:,2]
a -0.207110
c -1.699074
d 0.050155
dtype: float64
层次化索引阿紫数据重塑和基于分组的操作(如透视表的生成)中扮演重要的角色.例如,可以通过unstack方法将这段数据重新安排到一个DataFrame中:
>>> data.unstack()
1 2 3
a -0.660543 -0.207110 0.116586
b -0.538836 NaN 1.277390
c 0.406618 -1.699074 NaN
d NaN 0.050155 1.398109
unstack的逆运算是stack:
>>> data.unstack().stack()
a 1 -0.660543
2 -0.207110
3 0.116586
b 1 -0.538836
3 1.277390
c 1 0.406618
2 -1.699074
d 2 0.050155
3 1.398109
dtype: float64
stack和unstack将会在后面详细讲解
对于一个DatFrame,每条轴都可以有分层索引:
>>> frame=pd.DataFrame(np.arange(12).reshape((4, 3)),index=[['a', 'a', 'b', 'b'], [1, 2, 1, 2]],columns=[['Ohio', 'Ohio', 'Colorado'],['Green', 'Red', 'Green']])
>>> frame
Ohio Colorado
Green Red Green
a 1 0 1 2
2 3 4 5
b 1 6 7 8
2 9 10 11
各层都可以有名字(可以是字符串,也可以是别的Python对象).如果指定了名字,它们就会显示
state Ohio Colorado
color Green Red Green
key1 key2
a 1 0 1 2
2 3 4 5
b 1 6 7 8
2 9 10 11
有了部分列索引,因此可以轻松地选取列分组
>>> frame['Ohio']
color Green Red
key1 key2
a 1 0 1
2 3 4
b 1 6 7
2 9 10
也可以单独创建MultiIndex然后复用,上面的DataFrame终端列可以像下面这样创建:
>>> pd.MultiIndex.from_arrays([['Ohio', 'Ohio', 'Colorado'], ['Green', 'Red', 'Green']],names=['state', 'color'])
MultiIndex([( 'Ohio', 'Green'),
( 'Ohio', 'Red'),
('Colorado', 'Green')],
names=['state', 'color'])
1.1.重排与分级排序
有时,需要调整某条轴上各级别的顺序,或者根据指定级别上的值对数据进行排序.swaplevel接受两个级别编号或名称,并返回一个互换了级别的新对象(但数据不会发生变化):
>>> frame.swaplevel('key1','key2')
state Ohio Colorado
color Green Red Green
key2 key1
1 a 0 1 2
2 a 3 4 5
1 b 6 7 8
2 b 9 10 11
而sort_index则根据单个级别中的值对数据进行排序.交换级别时,常常也会用到sort_index,这样最终结果就是按照指定顺序字母进行排序了
>>> frame.sort_index(level=1)
state Ohio Colorado
color Green Red Green
key1 key2
a 1 0 1 2
b 1 6 7 8
a 2 3 4 5
b 2 9 10 11
>>> frame
state Ohio Colorado
color Green Red Green
key1 key2
a 1 0 1 2
2 3 4 5
b 1 6 7 8
2 9 10 11
>>> frame.swaplevel(0,1).sort_index(level=0)
state Ohio Colorado
color Green Red Green
key2 key1
1 a 0 1 2
b 6 7 8
2 a 3 4 5
b 9 10 11
1.2.根据级别汇总统计
许多关于DataFrame和Series的描述和汇总统计都有一个level选项,它用于指定某条轴上求和的级别.再以上面的DataFrame为例,可以根据行或列上的级别进行求和
>>> frame.sum(level='key2')
state Ohio Colorado
color Green Red Green
key2
1 6 8 10
2 12 14 16
>>> frame
state Ohio Colorado
color Green Red Green
key1 key2
a 1 0 1 2
2 3 4 5
b 1 6 7 8
2 9 10 11
这其实是利用了pandas的groupby功能
1.3.使用DataFrame的列进行索引
我们常常想要将DataFrame的一个或多个列当作行索引来用,或者可能希望将行索引变成DataFrame的列.以下面的DataFrame为例
>>> frame = pd.DataFrame({'a': range(7), 'b': range(7, 0, -1),'c': ['one', 'one', 'one', 'two', 'two','two', 'two'],'d': [0, 1, 2, 0, 1, 2, 3]})
>>> frame
a b c d
0 0 7 one 0
1 1 6 one 1
2 2 5 one 2
3 3 4 two 0
4 4 3 two 1
5 5 2 two 2
6 6 1 two 3
DataFrame的set_index函数将会将一个或多个列转换为行索引,并创建一个新的DataFrame
>>> frame2=frame.set_index(['c','b'])
>>> frame2
a d
c b
one 7 0 0
6 1 1
5 2 2
two 4 3 0
3 4 1
2 5 2
1 6 3
默认会将这些列从DataFrame中移除,但也可以将其保留
>>> frame.set_index(['c', 'd'], drop=False)
a b c d
c d
one 0 0 7 one 0
1 1 6 one 1
2 2 5 one 2
two 0 3 4 two 0
1 4 3 two 1
2 5 2 two 2
3 6 1 two 3
reset_index的功能跟set_index刚好相反,层次化索引的级别会被转移到列里面:
>>> frame2.reset_index()
c b a d
0 one 7 0 0
1 one 6 1 1
2 one 5 2 2
3 two 4 3 0
4 two 3 4 1
5 two 2 5 2
6 two 1 6 3
2.合并数据集
pandas对象中的数据可以通过一些方式合并
- pandas.merge可根据一个或多个键将不同DataFrame中的行连接起来,SQL或其他关系型数据库的用户对此应该会比较熟悉,因为它实现的就是数据库的join操作
- pandas.concat可以沿着一条轴将多个对象堆叠到一起.
- 实例方法combine_first可以将重复数据拼接在一起,用一个对象中的值填充另一个对象中的缺失值.
下面分别对它们进行讲解,并给出一些例子
2.1.数据库风格的DataFrame合并
数据集的合并(merge)或连接(join)运算是通过一个或多个键将行连接起来的.这些运算是关系型数据库的核心.pd.merge函数时堆数据应用这些算法的主要切入点
>>> df1 = pd.DataFrame({'key': ['b', 'b', 'a', 'c', 'a', 'a', 'b'],'data1': range(7)})
>>> df2 = pd.DataFrame({'key': ['a', 'b', 'd'],'data2': range(3)})
>>> df1
key data1
0 b 0
1 b 1
2 a 2
3 c 3
4 a 4
5 a 5
6 b 6
>>> df2
key data2
0 a 0
1 b 1
2 d 2
>>> pd.merge(df1,df2)
key data1 data2
0 b 0 1
1 b 1 1
2 b 6 1
3 a 2 0
4 a 4 0
5 a 5 0
这是一种多对一的合并.df1中的数据有多个被标记为a和b的行,而df2中key列的每个值则仅对应一行.对这些对象调用merge既可得到.这里并没有指明要用哪个列进行连接,但如果没有指定,merge就会将重叠的列名当作键.不过最好指定
>>> pd.merge(df1,df2,on='key')
key data1 data2
0 b 0 1
1 b 1 1
2 b 6 1
3 a 2 0
4 a 4 0
5 a 5 0
如果两个对象的列名不同,特可以分别进行指定
>>> df3 = pd.DataFrame({'lkey': ['b', 'b', 'a', 'c', 'a', 'a', 'b'],'data1': range(7)})
>>> df4 = pd.DataFrame({'rkey': ['a', 'b', 'd'],'data2': range(3)})
>>> df3
lkey data1
0 b 0
1 b 1
2 a 2
3 c 3
4 a 4
5 a 5
6 b 6
>>> df4
rkey data2
0 a 0
1 b 1
2 d 2
>>> pd.merge(df3, df4, left_on='lkey', right_on='rkey')
lkey data1 rkey data2
0 b 0 b 1
1 b 1 b 1
2 b 6 b 1
3 a 2 a 0
4 a 4 a 0
5 a 5 a 0
上面的例子中,结果里c和d以及相关的数据消失了.默认情况下,merge做的是"内连接";结果中的键是交集.其他的连接方式还有"left",“right"以及"outer”.外连接求取的是键的并集.组合了左连接和右连接的结果
>>> pd.merge(df1,df2,how='outer')
key data1 data2
0 b 0.0 1.0
1 b 1.0 1.0
2 b 6.0 1.0
3 a 2.0 0.0
4 a 4.0 0.0
5 a 5.0 0.0
6 c 3.0 NaN
7 d NaN 2.0
多对多的合并有些不直观
>>> df1 = pd.DataFrame({'key': ['b', 'b', 'a', 'c', 'a', 'b'],'data1': range(6)})
>>> df2 = pd.DataFrame({'key': ['a', 'b', 'a', 'b', 'd'],'data2': range(5)})
>>> df1
key data1
0 b 0
1 b 1
2 a 2
3 c 3
4 a 4
5 b 5
>>> df2
key data2
0 a 0
1 b 1
2 a 2
3 b 3
4 d 4
>>> pd.merge(df1,df2,on='key',how='left')
key data1 data2
0 b 0 1.0
1 b 0 3.0
2 b 1 1.0
3 b 1 3.0
4 a 2 0.0
5 a 2 2.0
6 c 3 NaN
7 a 4 0.0
8 a 4 2.0
9 b 5 1.0
10 b 5 3.0
多对多连接产生的是行的笛卡尔积.由于左边的DataFrame有3个"b"行,右边的有2个,所以最终结果就有6个"b"行.连接方式只影响出现在不同结果中的不同的键的值
>>> pd.merge(df1, df2, how='inner')
key data1 data2
0 b 0 1
1 b 0 3
2 b 1 1
3 b 1 3
4 b 5 1
5 b 5 3
6 a 2 0
7 a 2 2
8 a 4 0
9 a 4 2
要根据多个键进行合并,传入一个由列名组成的列表即可
>>> left = pd.DataFrame({'key1': ['foo', 'foo', 'bar'],'key2': ['one', 'two', 'one'],'lval': [1, 2, 3]})
>>> right = pd.DataFrame({'key1': ['foo', 'foo', 'bar', 'bar'],'key2': ['one', 'one', 'one', 'two'],'rval': [4, 5, 6, 7]})
>>> left
key1 key2 lval
0 foo one 1
1 foo two 2
2 bar one 3
>>> right
key1 key2 rval
0 foo one 4
1 foo one 5
2 bar one 6
3 bar two 7
>>> pd.merge(left,right,on=['key1','key2'],how='outer')
key1 key2 lval rval
0 foo one 1.0 4.0
1 foo one 1.0 5.0
2 foo two 2.0 NaN
3 bar one 3.0 6.0
4 bar two NaN 7.0
结果中会出现哪些键的这取决于所选的合并方式,可以这样理解:多个键形成一系列元组,并将其当做单个连接键(当然实际并不是这么回事)
要注意的是,在进行列-列连接时,DataFrame对象中的索引会被丢弃
对于合并运算需要考虑的最后一个问题是对重复列名的处理.虽然可以手工处理列名重叠的问题,但merge有一个suffixes选项,用于指定附加到左右两个DataFrame对象的重叠列名上的字符串
>>> pd.merge(left,right,on='key1')
key1 key2_x lval key2_y rval
0 foo one 1 one 4
1 foo one 1 one 5
2 foo two 2 one 4
3 foo two 2 one 5
4 bar one 3 one 6
5 bar one 3 two 7
>>> pd.merge(left, right, on='key1', suffixes=('_left', '_right'))
key1 key2_left lval key2_right rval
0 foo one 1 one 4
1 foo one 1 one 5
2 foo two 2 one 4
3 foo two 2 one 5
4 bar one 3 one 6
5 bar one 3 two 7
下面是merge的参数说明
indicator添加特殊的列_merge,它可以指定每个行的来源,它的值有left_only,right_only或both,根据每行的合并数据的来源
2.2.索引上的合并
有时,DataFrame的连接键位于其索引中,在这种情况下,可以传入left_index=True或right_index=True以说明索引应该被当作连接键
>>> left1 = pd.DataFrame({'key': ['a', 'b', 'a', 'a', 'b', 'c'],'value': range(6)})
>>> right1 = pd.DataFrame({'group_val': [3.5, 7]}, index=['a', 'b'])
>>> left1
key value
0 a 0
1 b 1
2 a 2
3 a 3
4 b 4
5 c 5
>>> right1
group_val
a 3.5
b 7.0
>>> pd.merge(left1,right1,left_on='key',right_index=True)
key value group_val
0 a 0 3.5
2 a 2 3.5
3 a 3 3.5
1 b 1 7.0
4 b 4 7.0
由于默认的merge是求取连接键的交集,因此可以通过外连接的方式得到它们的并集
>>> pd.merge(left1,right1,left_on='key',right_index=True,how='outer')
key value group_val
0 a 0 3.5
2 a 2 3.5
3 a 3 3.5
1 b 1 7.0
4 b 4 7.0
5 c 5 NaN
对于层次化索引的数据,事情就有点复杂了,因为索引的合并模式默认是多键合并
>>> lefth = pd.DataFrame({'key1': ['Ohio', 'Ohio', 'Ohio','Nevada', 'Nevada'],'key2': [2000, 2001, 2002, 2001, 2002],'data': np.arange(5.)})
>>> righth = pd.DataFrame(np.arange(12).reshape((6, 2)),index=[['Nevada', 'Nevada', 'Ohio', 'Ohio','Ohio', 'Ohio'],[2001, 2000, 2000, 2000, 2001, 2002]],columns=['event1', 'event2'])
>>> lefth
key1 key2 data
0 Ohio 2000 0.0
1 Ohio 2001 1.0
2 Ohio 2002 2.0
3 Nevada 2001 3.0
4 Nevada 2002 4.0
>>> righth
event1 event2
Nevada 2001 0 1
2000 2 3
Ohio 2000 4 5
2000 6 7
2001 8 9
2002 10 11
这种情况下,必须以列表的形式指明用作合并键的多个列(注意用how='outer’对重复索引值的处理)
>>> pd.merge(lefth,righth,left_on=['key1','key2'],right_index=True)
key1 key2 data event1 event2
0 Ohio 2000 0.0 4 5
0 Ohio 2000 0.0 6 7
1 Ohio 2001 1.0 8 9
2 Ohio 2002 2.0 10 11
3 Nevada 2001 3.0 0 1
>>> pd.merge(lefth, righth, left_on=['key1', 'key2'],right_index=True, how='outer')
key1 key2 data event1 event2
0 Ohio 2000 0.0 4.0 5.0
0 Ohio 2000 0.0 6.0 7.0
1 Ohio 2001 1.0 8.0 9.0
2 Ohio 2002 2.0 10.0 11.0
3 Nevada 2001 3.0 0.0 1.0
4 Nevada 2002 4.0 NaN NaN
4 Nevada 2000 NaN 2.0 3.0
同时使用合并双方的索引也没问题
>>> left2 = pd.DataFrame([[1., 2.], [3., 4.], [5., 6.]],index=['a', 'c', 'e'],columns=['Ohio', 'Nevada'])
>>> right2 = pd.DataFrame([[7., 8.], [9., 10.], [11., 12.], [13, 14]],index=['b', 'c', 'd', 'e'],columns=['Missouri', 'Alabama'])
>>> left2
Ohio Nevada
a 1.0 2.0
c 3.0 4.0
e 5.0 6.0
>>> right2
Missouri Alabama
b 7.0 8.0
c 9.0 10.0
d 11.0 12.0
e 13.0 14.0
>>> pd.merge(left2,right2,how='outer',left_index=True,right_index=True)
Ohio Nevada Missouri Alabama
a 1.0 2.0 NaN NaN
b NaN NaN 7.0 8.0
c 3.0 4.0 9.0 10.0
d NaN NaN 11.0 12.0
e 5.0 6.0 13.0 14.0
DataFrame还有更便捷的join实例方法,它能更为方便地实现按索引合并.它还可以用于合并多个带有想要或相似索引的DataFrame对象,但要求没有重叠的列.
在上面的例子中,可以编写:
>>> left2.join(right2, how='outer')
Ohio Nevada Missouri Alabama
a 1.0 2.0 NaN NaN
b NaN NaN 7.0 8.0
c 3.0 4.0 9.0 10.0
d NaN NaN 11.0 12.0
e 5.0 6.0 13.0 14.0
因为一些历史版本的遗留原因.DataFrame的join方法默认使用的是左连接,保留左边表的行索引.它还支持在调用的DataFrame的列上,连接传递的DataFrame索引:
>>> left1.join(right1, on='key')
key value group_val
0 a 0 3.5
1 b 1 7.0
2 a 2 3.5
3 a 3 3.5
4 b 4 7.0
5 c 5 NaN
最后,对于简单的索引合并,还可以向join传入一组DataFrame,下面会介绍更为通用的concat函数,也能实现此功能
>>> another = pd.DataFrame([[7., 8.], [9., 10.], [11., 12.], [16., 17.]],index=['a', 'c', 'e', 'f'],columns=['New York','Oregon'])
>>> another
New York Oregon
a 7.0 8.0
c 9.0 10.0
e 11.0 12.0
f 16.0 17.0
>>> left2.join([right,another])
Ohio Nevada key1 key2 rval New York Oregon
a 1.0 2.0 NaN NaN NaN 7.0 8.0
c 3.0 4.0 NaN NaN NaN 9.0 10.0
e 5.0 6.0 NaN NaN NaN 11.0 12.0
>>> left2.join([right2, another], how='outer')
Ohio Nevada Missouri Alabama New York Oregon
a 1.0 2.0 NaN NaN 7.0 8.0
c 3.0 4.0 9.0 10.0 9.0 10.0
e 5.0 6.0 13.0 14.0 11.0 12.0
b NaN NaN 7.0 8.0 NaN NaN
d NaN NaN 11.0 12.0 NaN NaN
f NaN NaN NaN NaN 16.0 17.0
2.4.轴向连接
另一种数据合并也被称为连接,绑定或堆叠.NumPy的concatenation函数可以
>>> arr=np.arange(12).reshape((3,4))
>>> arr
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])
>>> np.concatenate([arr,arr],axis=1)
array([[ 0, 1, 2, 3, 0, 1, 2, 3],
[ 4, 5, 6, 7, 4, 5, 6, 7],
[ 8, 9, 10, 11, 8, 9, 10, 11]])
对于pandas对象(如Series和DataFrame),带有标签的轴使你能够进一步推广数组的连接运算.具体来说,还需要考虑以下内容
如果对象在其它轴上的索引不同,我们应该合并这些轴的不同元素还是只使用交集?
连接的数据集是否需要在结果对象中可识别?
连接轴中保存的数据是否需要保留?许多情况下,DataFrame默认的整数标签最好在连接时删掉
pandas的concat函数提供了一种能够解决这些问题的可靠方式.假设有三个没有重叠索引的Series
>>> s1 = pd.Series([0, 1], index=['a', 'b'])
>>> s2=pd.Series([2,3,4],index=['c','d','e'])
>>> s3=pd.Series([5,6],index=['f','g'])
>>> pd.concat([s1,s2,s3])
a 0
b 1
c 2
d 3
e 4
f 5
g 6
dtype: int64
默认情况下,concat是在axis=0上工作的,最终产生一个新的Series.如果传入的axis=1,则结果会变成一个DataFrame(axis=1是列):
>>> pd.concat([s1,s2,s3],axis=1)
0 1 2
a 0.0 NaN NaN
b 1.0 NaN NaN
c NaN 2.0 NaN
d NaN 3.0 NaN
e NaN 4.0 NaN
f NaN NaN 5.0
g NaN NaN 6.0
这种情况下,另外的轴上没有重叠,从索引的有序并集(外连接)上就可以看出来.传入join='inner’即可得到它们的交集
>>> s4=pd.concat([s1,s3])
>>> s4
a 0
b 1
f 5
g 6
dtype: int64
>>> pd.concat([s1,s4],axis=1)
0 1
a 0.0 0
b 1.0 1
f NaN 5
g NaN 6
>>> pd.concat([s1,s4],axis=1,join='inner')
0 1
a 0 0
b 1 1
在这个例子中,f和g标签消失了,是因为使用的是join='inner’选项.
可以使用key参数创建一个层次化索引来表明参与连接的片段来源
>>> result=pd.concat([s1,s2,s3],keys=['one','two','three'])
>>> result
one a 0
b 1
two c 2
d 3
e 4
three f 5
g 6
dtype: int64
>>> result.unstack()
a b c d e f g
one 0.0 1.0 NaN NaN NaN NaN NaN
two NaN NaN 2.0 3.0 4.0 NaN NaN
three NaN NaN NaN NaN NaN 5.0 6.0
同样的逻辑页适用于DataFrame对象
>>> df1 = pd.DataFrame(np.arange(6).reshape(3, 2), index=['a', 'b', 'c'],columns=['one', 'two'])
>>> df2 = pd.DataFrame(5 + np.arange(4).reshape(2, 2), index=['a', 'c'],columns=['three', 'four'])
>>> df1
one two
a 0 1
b 2 3
c 4 5
>>> df2
three four
a 5 6
c 7 8
>>> pd.concat([df1, df2], axis=1, keys=['level1', 'level2'])
level1 level2
one two three four
a 0 1 5.0 6.0
b 2 3 NaN NaN
c 4 5 7.0 8.0
如果传入的不是列表而是一个字典,则字典的键就会被当作keys选项的值
>>> pd.concat({'level1':df1,'level2':df2},axis=1)
level1 level2
one two three four
a 0 1 5.0 6.0
b 2 3 NaN NaN
c 4 5 7.0 8.0
此外还有两个用于管理层次化索引创建方式的参数,例如,可以适用names参数命名创建的轴级别
>>> pd.concat([df1,df2],axis=1,keys=['level1','level2'],names=['upper','lower'])
upper level1 level2
lower one two three four
a 0 1 5.0 6.0
b 2 3 NaN NaN
c 4 5 7.0 8.0
最后一个关于DataFrame的问题是,DataFrame的行索引不包含任何相关数据
>>> pd.concat([df1,df2],axis=1,keys=['level1','level2'],names=['upper','lower'])
upper level1 level2
lower one two three four
a 0 1 5.0 6.0
b 2 3 NaN NaN
c 4 5 7.0 8.0
>>> df1=pd.DataFrame(np.random.randn(3,4),columns=['a','b','c','d'])
>>> df2 = pd.DataFrame(np.random.randn(2, 3), columns=['b', 'd', 'a'])
>>> df1
a b c d
0 1.431297 -0.542141 0.803909 0.422123
1 1.878594 -1.396852 0.787974 -0.243392
2 1.380877 1.419340 2.031413 -0.282092
>>> df2
b d a
0 -0.066064 -3.479455 0.106850
1 0.050414 0.259385 1.159822
这种情况下,传入ignore_index=True即可
>>> pd.concat([df1, df2], ignore_index=True)
a b c d
0 1.431297 -0.542141 0.803909 0.422123
1 1.878594 -1.396852 0.787974 -0.243392
2 1.380877 1.419340 2.031413 -0.282092
3 0.106850 -0.066064 NaN -3.479455
4 1.159822 0.050414 NaN 0.259385
2.5.合并重叠数据
还有一种数据组合问题不能用简单的合并或连接运算来处理,比如,可能有全部或部分重叠的两个数据集.例如,适用NumPy的where函数,它表示一种等价于面向数组的if-else
>>> a=pd.Series([np.nan, 2.5, np.nan, 3.5, 4.5, np.nan],index=['f', 'e', 'd', 'c', 'b', 'a'])
>>> b = pd.Series(np.arange(len(a), dtype=np.float64),index=['f', 'e', 'd', 'c', 'b', 'a'])
>>> a
f NaN
e 2.5
d NaN
c 3.5
b 4.5
a NaN
dtype: float64
>>> b[-1] = np.nan
>>> b
f 0.0
e 1.0
d 2.0
c 3.0
b 4.0
a NaN
dtype: float64
>>> np.where(pd.isnull(a),b,a)
array([0. , 2.5, 2. , 3.5, 4.5, nan])
Series有一个combine_first方法,实现的也是一样的功能,还带有pandas的数据对齐
>>> b[:-2].combine_first(a[2:])
a NaN
b 4.5
c 3.0
d 2.0
e 1.0
f 0.0
dtype: float64
对于DataFrame,combine_first自然也会在列上做同样的事情,因此可以将其看作:用传递对象中的数据为调用对象的缺失数据"打补丁"
>>> df1 = pd.DataFrame({'a': [1., np.nan, 5., np.nan],'b': [np.nan, 2., np.nan, 6.],'c': range(2, 18, 4)})
>>> df2 = pd.DataFrame({'a': [5., 4., np.nan, 3., 7.],'b': [np.nan, 3., 4., 6., 8.]})
>>> df1
a b c
0 1.0 NaN 2
1 NaN 2.0 6
2 5.0 NaN 10
3 NaN 6.0 14
>>> df2
a b
0 5.0 NaN
1 4.0 3.0
2 NaN 4.0
3 3.0 6.0
4 7.0 8.0
>>> df1.combine_first(df2)
a b c
0 1.0 NaN 2.0
1 4.0 2.0 6.0
2 5.0 4.0 10.0
3 3.0 6.0 14.0
4 7.0 8.0 NaN
3.重塑和轴向旋转
有许多用于重新排列表格型数据的基础运算,这些函数也称作重塑或轴向旋转运算
3.1.重塑层次化索引
层次化索引未DataFrame数据的重排人物提供了一种具有良好一致性的方式,功能有二:
- stack:将数据的列“旋转”为行。
- unstack:将数据的行“旋转”为列。
下面是一系列范例.
>>>data=pd.DataFrame(np.arange(6).reshape((2,3)),index=pd.Index(['Ohio','Colorado'],name='state'),columns=pd.Index(['one','two','three'],name='number'))
>>> data=pd.DataFrame(np.arange(6).reshape((2,3)),index=pd.Index(['Ohio','Colorado'],name='state'),columns=pd.Index(['one','two','three'],name='number'))
>>> data
number one two three
state
Ohio 0 1 2
Colorado 3 4 5
对该数据使用stack方法即可将列转换为行,得到一个Series:
>>> result=data.stack()
>>> result
state number
Ohio one 0
two 1
three 2
Colorado one 3
two 4
three 5
dtype: int32
对于一个层次化索引的Series,可以使用unstack将其重拍为一个DataFrame
>>> result.unstack()
number one two three
state
Ohio 0 1 2
Colorado 3 4 5
默认情况下,stack和unstack操作的是最内层,传入分词级别的编号或者名称即可对其他级别操作
>>> result.unstack(0)
state Ohio Colorado
number
one 0 3
two 1 4
three 2 5
如果不是所有的级别值都能在各分组中找到的话,则unstack操作可能会引入缺失数据
>>> s1=pd.Series([0,1,2,3],index=['a','b','c','d'])
>>> s2=pd.Series([4,5,6],index=['c','d','e'])
>>> data2=pd.concat([s1,s2],keys=['one','two'])
>>> data2
one a 0
b 1
c 2
d 3
two c 4
d 5
e 6
dtype: int64
>>> data2.unstack()
a b c d e
one 0.0 1.0 2.0 3.0 NaN
two NaN NaN 4.0 5.0 6.0
stack默认会滤除缺失数据,因此该运算是可逆的
>>> data2.unstack().stack()
one a 0.0
b 1.0
c 2.0
d 3.0
two c 4.0
d 5.0
e 6.0
dtype: float64
>>> data2.unstack().stack(dropna=False)
one a 0.0
b 1.0
c 2.0
d 3.0
e NaN
two a NaN
b NaN
c 4.0
d 5.0
e 6.0
dtype: float64
在对DataFrame进行unstack操作时,作为旋转轴的级别将会成为结果中的最低级别:
>>> data=pd.DataFrame(np.arange(6).reshape((2,3)),index=pd.Index(['Ohio','Colorado'],name='state'),columns=pd.Index(['one','two','three'],name='number'))
>>> result=data.stack()
>>> result
state number
Ohio one 0
two 1
three 2
Colorado one 3
two 4
three 5
dtype: int32
>>> df=pd.DataFrame({'left':result,'right':result+5},columns=pd.Index(['left','right'],name='side'))
>>> df
side left right
state number
Ohio one 0 5
two 1 6
three 2 7
Colorado one 3 8
two 4 9
three 5 10
>>> df.unstack('state')
side left right
state Ohio Colorado Ohio Colorado
number
one 0 3 5 8
two 1 4 6 9
three 2 5 7 10
当调用stack,我们可以指明轴的名字
>>> df.unstack('state').stack('side')
state Colorado Ohio
number side
one left 3 0
right 8 5
two left 4 1
right 9 6
three left 5 2
right 10 7
3.2.将"长格式"旋转为"宽格式"
多个事件序列数据通常是以所谓的"长格式"或"堆叠格式"存储在数据库和CSV中的.这里先加载一些示例数据,做一些时间序列规整和数据清洗
In [139]: data = pd.read_csv('examples/macrodata.csv')
In [140]: data.head()
Out[140]:
year quarter realgdp realcons realinv realgovt realdpi cpi \
0 1959.0 1.0 2710.349 1707.4 286.898 470.045 1886.9 28.98
1 1959.0 2.0 2778.801 1733.7 310.859 481.301 1919.7 29.15
2 1959.0 3.0 2775.488 1751.8 289.226 491.260 1916.4 29.35
3 1959.0 4.0 2785.204 1753.7 299.356 484.052 1931.3 29.37
4 1960.0 1.0 2847.699 1770.5 331.722 462.199 1955.5 29.54
m1 tbilrate unemp pop infl realint
0 139.7 2.82 5.8 177.146 0.00 0.00
1 141.7 3.08 5.1 177.830 2.34 0.74
2 140.5 3.82 5.3 178.657 2.74 1.09
3 140.0 4.33 5.6 179.386 0.27 4.06
4 139.6 3.50 5.2 180.007 2.31 1.19
In [141]: periods = pd.PeriodIndex(year=data.year, quarter=data.quarter,
.....: name='date')
In [142]: columns = pd.Index(['realgdp', 'infl', 'unemp'], name='item')
In [143]: data = data.reindex(columns=columns)
In [144]: data.index = periods.to_timestamp('D', 'end')
In [145]: ldata = data.stack().reset_index().rename(columns={0: 'value'})
这就是多个时间序列(或者其它带有两个或多个键的可观察数据,这里,我们的键是date和item)的长格式。表中的每行代表一次观察。
关系型数据库(如MySQL)中的数据经常都是这样存储的,因为固定架构(即列名和数据类型)有一个好处:随着表中数据的添加,item列中的值的种类能够增加。在前面的例子中,date和item通常就是主键(用关系型数据库的说法),不仅提供了关系完整性,而且提供了更为简单的查询支持。有的情况下,使用这样的数据会很麻烦,你可能会更喜欢DataFrame,不同的item值分别形成一列,date列中的时间戳则用作索引。DataFrame的pivot方法完全可以实现这个转换:
In [147]: pivoted = ldata.pivot('date', 'item', 'value')
In [148]: pivoted
Out[148]:
item infl realgdp unemp
date
1959-03-31 0.00 2710.349 5.8
1959-06-30 2.34 2778.801 5.1
1959-09-30 2.74 2775.488 5.3
1959-12-31 0.27 2785.204 5.6
1960-03-31 2.31 2847.699 5.2
1960-06-30 0.14 2834.390 5.2
1960-09-30 2.70 2839.022 5.6
1960-12-31 1.21 2802.616 6.3
1961-03-31 -0.40 2819.264 6.8
1961-06-30 1.47 2872.005 7.0
... ... ... ...
2007-06-30 2.75 13203.977 4.5
2007-09-30 3.45 13321.109 4.7
2007-12-31 6.38 13391.249 4.8
2008-03-31 2.82 13366.865 4.9
2008-06-30 8.53 13415.266 5.4
2008-09-30 -3.16 13324.600 6.0
2008-12-31 -8.79 13141.920 6.9
2009-03-31 0.94 12925.410 8.1
2009-06-30 3.37 12901.504 9.2
2009-09-30 3.56 12990.341 9.6
[203 rows x 3 columns]
前两个传递的值分别用作行和列索引,最后一个可选值则是用于填充DataFrame的数据列。假设有两个需要同时重塑的数据列:
?```python
In [149]: ldata['value2'] = np.random.randn(len(ldata))
In [150]: ldata[:10]
Out[150]:
date item value value2
0 1959-03-31 realgdp 2710.349 0.523772
1 1959-03-31 infl 0.000 0.000940
2 1959-03-31 unemp 5.800 1.343810
3 1959-06-30 realgdp 2778.801 -0.713544
4 1959-06-30 infl 2.340 -0.831154
5 1959-06-30 unemp 5.100 -2.370232
6 1959-09-30 realgdp 2775.488 -1.860761
7 1959-09-30 infl 2.740 -0.860757
8 1959-09-30 unemp 5.300 0.560145
9 1959-12-31 realgdp 2785.204 -1.265934
如果忽略最后一个参数,得到的DataFrame就会带有层次化的列:
In [151]: pivoted = ldata.pivot('date', 'item')
In [152]: pivoted[:5]
Out[152]:
value value2
item infl realgdp unemp infl realgdp unemp
date
1959-03-31 0.00 2710.349 5.8 0.000940 0.523772 1.343810
1959-06-30 2.34 2778.801 5.1 -0.831154 -0.713544 -2.370232
1959-09-30 2.74 2775.488 5.3 -0.860757 -1.860761 0.560145
1959-12-31 0.27 2785.204 5.6 0.119827 -1.265934 -1.063512
1960-03-31 2.31 2847.699 5.2 -2.359419 0.332883 -0.199543
In [153]: pivoted['value'][:5]
Out[153]:
item infl realgdp unemp
date
1959-03-31 0.00 2710.349 5.8
1959-06-30 2.34 2778.801 5.1
1959-09-30 2.74 2775.488 5.3
1959-12-31 0.27 2785.204 5.6
1960-03-31 2.31 2847.699 5.2
注意,pivot其实救是用set_index创建层次化索引,再用unstack重塑:
In [154]: unstacked = ldata.set_index(['date', 'item']).unstack('item')
In [155]: unstacked[:7]
Out[155]:
value value2
item infl realgdp unemp infl realgdp unemp
date
1959-03-31 0.00 2710.349 5.8 0.000940 0.523772 1.343810
1959-06-30 2.34 2778.801 5.1 -0.831154 -0.713544 -2.370232
1959-09-30 2.74 2775.488 5.3 -0.860757 -1.860761 0.560145
1959-12-31 0.27 2785.204 5.6 0.119827 -1.265934 -1.063512
1960-03-31 2.31 2847.699 5.2 -2.359419 0.332883 -0.199543
1960-06-30 0.14 2834.390 5.2 -0.970736 -1.541996 -1.307030
1960-09-30 2.70 2839.022 5.6 0.377984 0.286350 -0.753887
3.3.将"宽格式"旋转为"长格式"
旋转DataFrame的逆运算是pd.melt.它不是将一列转换到多个新的DataFrame,而是合并多个列成为一个,产生一个比输入长的DataFrame
>>> df=pd.DataFrame({'key':['foo','bar','baz'],'A':[1,2,3],'B':[4,5,6],'C':[7,8,9]})
>>> df
key A B C
0 foo 1 4 7
1 bar 2 5 8
2 baz 3 6 9
key列可能是分组指标,其他的列是数据值.当使用pd.melt时,必须指明哪些列是分组指标,下面使用key作为唯一的分组指标
>>> melted=pd.melt(df,['key'])
>>> melted
key variable value
0 foo A 1
1 bar A 2
2 baz A 3
3 foo B 4
4 bar B 5
5 baz B 6
6 foo C 7
7 bar C 8
8 baz C 9
使用pivot,可以重塑回原来的样子
>>> reshaped=melted.pivot('key','variable','value')
>>> reshaped
variable A B C
key
bar 2 5 8
baz 3 6 9
foo 1 4 7
因为pivot的结果从列创建了一个索引,用作行标签,可以使用reset_index将数据移回列
>>> reshaped.reset_index()
variable key A B C
0 bar 2 5 8
1 baz 3 6 9
2 foo 1 4 7
还可以指定列的子集,作为值的列
>>> pd.melt(df,id_vars=['key'],value_vars=['A','B'])
key variable value
0 foo A 1
1 bar A 2
2 baz A 3
3 foo B 4
4 bar B 5
5 baz B 6
pandas.melt也可以不用分组指标:
>>> pd.melt(df, value_vars=['A', 'B', 'C'])
variable value
0 A 1
1 A 2
2 A 3
3 B 4
4 B 5
5 B 6
6 C 7
7 C 8
8 C 9
>>> pd.melt(df, value_vars=['key', 'A', 'B'])
variable value
0 key foo
1 key bar
2 key baz
3 A 1
4 A 2
5 A 3
6 B 4
7 B 5
8 B 6
六、绘图和可视化
信息可视化是数据分析中最重要的工作之一.它可能是探索过程的一部分,例如,帮助找出异常值,必要的数据转换,得出有关模型的idea等.另外,做一个可交互的数据可视化也许是工作的最终目标.Python有许多库进行静态或动态的数据可视化,这里关注于matplotlib和基于它的库
matplotlib是一个创建高质量图标的桌面绘图包(主要是2D方面),其目的是为Python构建一个MATLAB式的绘图接口.matplotlib支持各种操作系统上许多不同的GUI后端,而且还能将图片导出为各种常见的矢量和光栅图.matplotlib有多个数据可视化工具集,它们使用matplotlib作为底层.其中之一是seaborn
6.1.matplotlib入门
import numpy as np
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(5, 5))
ax = fig.add_subplot(1, 1, 1)
ax.set_title('Logistic function/Sigmoid function')
ax.scatter(0,0.5)
ax.spines['right'].set_color('none')
ax.spines['top'].set_color('none')
ax.spines['bottom'].set_position(('data', 0))
ax.spines['left'].set_position(('data', 0))
x = np.linspace(-10, 10, 100)
y = 1 / (1 + np.exp(-x))
ax.set_ylim(-0.5,1.5)
plt.tight_layout()
plt.plot(x, y)
plt.plot(x,[1]*100,'k--')
plt.show()
6.1.1.Figure和Subplot
matplotlib的图像都存储于Figure对象中,可以使用plt.figure创建一个新的figure
>>> fig=plt.figure()
>>> fig
<Figure size 640x480 with 0 Axes>
不能通过空的Figure绘图,必须用add_subplot创建一个或多个subplot才可以
>>> ax1=fig.add_subplot(2,2,1)
这条代码的意思是:图像应该是2×2的(即最多4张图),且当前选中的是4个subplot中的第一个(编号从1开始)
>>> ax1=fig.add_subplot(2,2,1)
>>> ax2=fig.add_subplot(2,2,2)
>>> ax3=fig.add_subplot(2,2,3)
>>> ax4=fig.add_subplot(2,2,4)
>>> plt.show()
创建figure后,matplotlib会默认在最后用过的subplot(如果没有则创建一个)上进行绘制,隐藏创建figure和subplot的过程
>>> fig=plt.figure()
>>> ax1=fig.add_subplot(2,2,1)
>>> ax2=fig.add_subplot(2,2,2)
>>> ax3=fig.add_subplot(2,2,3)
>>> plt.plot(np.random.randn(50).cumsum(), 'k--')
[<matplotlib.lines.Line2D object at 0x000001C119057320>]
>>> plt.show()
'k–'是一个线型选项,表示黑色虚线,上面由fig.add_subplot所返回的对象是AxesSubplot对象,直接调用它们的实例方法就可以在其他空着的格子例画图了
>>> fig=plt.figure()
>>> ax1=fig.add_subplot(2,2,1)
>>> ax2=fig.add_subplot(2,2,2)
>>> ax3=fig.add_subplot(2,2,3)
>>> ax1.hist(np.random.randn(100),bins=20,color='k',alpha=0.3)
(array([3., 2., 1., 6., 9., 9., 8., 7., 8., 5., 9., 5., 7., 5., 6., 4., 3.,
1., 1., 1.]), array([-1.84287048, -1.63650905, -1.43014761, -1.22378618, -1.01742474,
-0.81106331, -0.60470187, -0.39834044, -0.191979 , 0.01438243,
0.22074387, 0.4271053 , 0.63346674, 0.83982817, 1.04618961,
1.25255104, 1.45891248, 1.66527391, 1.87163535, 2.07799678,
2.28435822]), <a list of 20 Patch objects>)
>>> ax2.scatter(np.arange(30),np.arange(30)+3*np.random.randn(30))
<matplotlib.collections.PathCollection object at 0x000001C11BFFB240>
>>> plt.show()
创建包含subplot网格的figure是一个非常常见的任务,matplotlib有一个更为方便的方法,plt.subplots,它可以创建一个新的Figure,并返回一个含有已创建的subplot对象的Numpy数组
>>> fig,axes=plt.subplots(2,3)
>>> axes
array([[<matplotlib.axes._subplots.AxesSubplot object at 0x000001C11C302390>,
<matplotlib.axes._subplots.AxesSubplot object at 0x000001C11C33E2E8>,
<matplotlib.axes._subplots.AxesSubplot object at 0x000001C11C367978>],
[<matplotlib.axes._subplots.AxesSubplot object at 0x000001C11C398048>,
<matplotlib.axes._subplots.AxesSubplot object at 0x000001C11C3C1710>,
<matplotlib.axes._subplots.AxesSubplot object at 0x000001C11C3EAD68>]],
dtype=object)
可以通过对axes数组进行索引,就像一个二维数组,例如axes[0,1],还可以通过sharex和sharey指定subplot应该具有相同的X轴和Y轴.在比较相同范围的数据时,这也是非常使用的,否则matplotlib会自动缩放各图表的界限,下面是更多信息
6.1.2.调整subplot周围的间距
默认情况下,matplotlib会在subplot外围留下一定的边距,并在subplot之间留下一定的间距.间距与图像的高度和宽度有关.因此,如果调整了图像大小,间距也会自动调整.利用Figure的subplots_adjust方法可以轻而易举地修改边距.另外它也是个顶级函数
>>> plt.subplots_adjust(left=None, bottom=None, right=None, top=None,wspace=None, hspace=None)
6.1.3.颜色、标记和线型
matplotlib的plot函数接受一组X和Y坐标,还可以接受一个表示颜色和线型的字符串缩写.例如,根据x和y绘制绿色虚线,可以用’g–’
常用的颜色可以使用颜色缩写,也可以指定颜色码(如’#CECECE’).可以通过查看plot的文档字符串查看所有线型的合集
线图可以使用标记强调点.因为matplotlib可以创建连续线图,在点之间进行插值,因此有时可能不太容易看出真实数据点的位置.标记也可以放到格式字符串中,但标记类型和线型必须放在颜色后面
>>> plt.plot(np.random.randn(30).cumsum(),'ko--')
[<matplotlib.lines.Line2D object at 0x000001C11BCCA438>]
>>> plt.show()
还可以将其写位更明确的形式:
>>>plt.plot(np.random.randn(30).cumsum(),color='k',linestyle='dashed',marker='o')
[<matplotlib.lines.Line2D object at 0x000001C11BE8D0F0>]
>>> plt.show()
在线型图中,非实际数据点默认是按线性方式插值的.可以通过drawstyle选项修改
>>> data=np.random.randn(30).cumsum()
>>> plt.plot(data,'k--',label='Default')
[<matplotlib.lines.Line2D object at 0x000001C11CB8C710>]
>>> plt.plot(data,'k--',drawstyle='steps-post',label='steps-post')
[<matplotlib.lines.Line2D object at 0x000001C11BEB0AC8>]
>>> plt.legend(loc='best')
<matplotlib.legend.Legend object at 0x000001C11CB8CCC0>
>>> plt.show()
这里传递了label参数到plot,可以创建一个plot图例,指明每条使用plt.legend的线,必须调用plt.legend来创建图例,无论绘图时是否传递label标签选项
6.1.4.刻度、图例和标签
对于大多数图标装饰项,其主要实现方式有二:使用过程型的pyplot接口以及更为面向对象的原生matplotlib API
pyplot接口的设计目的就是交互式使用,含有诸如xlim,xticks和xticklabels之类的方法.它们分别控制图标的范围,刻度位置,刻度标签等,使用方式有两种:
1.调用时不带参数,则返回当前的参数值(例如,plt.xlim()返回当前的X轴绘图范围)
2.调用时带参数,则设置参数值(例如,plt.xlim([0,10])会将X轴的范围设置为0到10)
所有这些方法都是对当前或最近创建的AxesSubplot起作用的.它们各自对应subplot对象上的两个方法,以xlim为例,就是ax.get_xlim和ax.set_xlim.
6.1.4.1.设置标题、轴标签、刻度以及刻度标签
为了说明自定义轴,创建一个简单的图像并绘制一段随机漫步
>>> fig=plt.figure()
>>> ax=fig.add_subplot(1,1,1)
>>> ax.plot(np.random.randn(1000).cumsum)
>>> ax.plot(np.random.randn(1000).cumsum())
[<matplotlib.lines.Line2D object at 0x000001C11CE9F748>]
>>> plt.show()
要改变x轴的刻度,最简单的办法是使用set_xticks和set_xticklabels.前者告诉matplotlib要将刻度放在数据范围中的哪些位置,默认情况下,这些位置也就是刻度标签,但可以通过set_xticklabels将其他的值用作标签:
>>> ticks=ax.set_xticks([0,250,500,750,1000])
>>>labels=ax.set_xticklabels(['one','two','three','four','five'],rotation=30,fontsize='small')
rotation选项设定x轴刻度标签倾斜30度.最后再用set_xlabel为x轴设置一个名称,并用set_title()为X轴设置一个名称
>>> ticks=ax.set_xticks([0,250,500,750,1000])
>>>labels=ax.set_xticklabels(['one','two','three','four','five'],rotation=30,fontsize='small')
>>> ax.set_title('My matplotlib plot')
Text(0.5,1,'My matplotlib plot')
>>> ax.set_xlabel('Stages')
Text(0.5,23.1922,'Stages')
>>> plt.show()
Y轴的修改方式与之类似,只需将上述代码中的x替换为y即可
6.1.4.2.添加图例
图例(legend)是另一种用于标识图标元素的重要工具.添加图例的方式有很多种.最简单的是阿紫添加subplot的时候传入label参数
>>> from numpy.random import randn
>>> fig=plt.figure();ax=fig.add_subplot(1,1,1)
>>> ax.plot(randn(1000).cumsum(),'k',label='one')
[<matplotlib.lines.Line2D object at 0x000001C11D3090F0>]
>>> ax.plot(randn(1000).cumsum(),'k--',label='two')
[<matplotlib.lines.Line2D object at 0x000001C11D3096D8>]
>>> ax.plot(randn(1000).cumsum(),'k.',label='three')
[<matplotlib.lines.Line2D object at 0x000001C11D309B38>]
在此之后,可以使用ax.legend()或plt.legend()来自动创建图例
>>> from numpy.random import randn
>>> fig=plt.figure();ax=fig.add_subplot(1,1,1)
>>> ax.plot(randn(1000).cumsum(),'k',label='one')
[<matplotlib.lines.Line2D object at 0x000001C11D3090F0>]
>>> ax.plot(randn(1000).cumsum(),'k--',label='two')
[<matplotlib.lines.Line2D object at 0x000001C11D3096D8>]
>>> ax.plot(randn(1000).cumsum(),'k.',label='three')
[<matplotlib.lines.Line2D object at 0x000001C11D309B38>]
>>> ax.legend(loc='best')
<matplotlib.legend.Legend object at 0x000001C11BE9EE48>
>>> plt.show()
loc告诉matplotlib要将图例放在哪里.要从图例中取出一个或多个元素,不传入label或传入label='nolegend’即可
6.1.4.3.注解以及在Subplot上绘图
除标准的绘图类型,还可能会希望绘制一些子集的注解,可能是文本,箭头或者其他图像等.注解和文字可以通过text,arrow和annotate函数进行添加.text可以将文本绘制在图标的指定坐标(x,y),还可以加上一些自定义的格式
>>> ax.text(x,y,'hello world',family='monospace',fontsize=10)
注解中既可以含有文本也含有见箭.
例如,我们根据最近的标准普尔500指数价格绘制一张曲线图,并标出2008年到2009年金融危机期间的一些重要日期
from datetime import datetime
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
data = pd.read_csv('examples/spx.csv', index_col=0, parse_dates=True)
spx = data['SPX']
spx.plot(ax=ax, style='k-')
crisis_data = [
(datetime(2007, 10, 11), 'Peak of bull market'),
(datetime(2008, 3, 12), 'Bear Stearns Fails'),
(datetime(2008, 9, 15), 'Lehman Bankruptcy')
]
for date, label in crisis_data:
ax.annotate(label, xy=(date, spx.asof(date) + 75),
xytext=(date, spx.asof(date) + 225),
arrowprops=dict(facecolor='black', headwidth=4, width=2,
headlength=4),
horizontalalignment='left', verticalalignment='top')
ax.set_xlim(['1/1/2007', '1/1/2011'])
ax.set_ylim([600, 1800])
ax.set_title('Important dates in the 2008-2009 financial crisis')
这张图中有几个重要的点:ax.annotate方法可以在指定的x和y坐标轴绘制标签,使用set_xlim和set_ylim人工设定起始和结束边界,而不是使用matplotlib的默认方法,最后用ax.set_title添加图标标题
图形的绘制要麻烦一些,mamatplotlib.pyplot中找到tplotlib有一些标识常见图形的对象.这些对象被成为块(patch).其中有些可以在matplotlib.pyplot中找到,但完整集合位于matplotlib.patches
如果查看许多常见图表对象的具体实现代码,就会发现它们其实就是由块patch组装而成的.
6.1.4.4.将图表保存到文件
利用plt.savefig可以将当前的图表保存到文件.改方法相当于Figure对象的实例方法savefig,例如,要将图表保存为SVG文件,只需要使用
>>> plt.savefig('figpath.svg')
文件类型是通过文件扩展名推断的.因此,如果使用.pdf就会得到一个PDF文件
savefig并非一定要写入磁盘,也可以写入任何文件型对象.比如BytesIO
>>> from io import BytesIO
>>> buffer=BytesIO()
>>> plt.savefig(buffer)
>>> plot_data=buffer.getvalue()
6.1.2.matplotlib配置
matplotlib自带一些配色方案,以及为生成高质量图片而默认的配置信息.几乎所有默认行为都能通过一组全局参数进行自定义,它们可以管理图像大小,subplot边距,配色方案,字体大小.网格类型等.一种Python编程方式配置系统的方法是使用rc方法,例如,要将全局的图像默认大小设这位10*10
>>> plt.rc('figure'.figsize=(10,10))
rc的第一个参数是希望自定义的对象,例如’figure’,‘axes’,‘xtick’,‘ytick’,‘grid’,'legend’等.其后可以跟上一些列的关键字参数.一个简单的办法是将这些选项写成一个字典
>>> font_options={'family':'monospace','weight':'bold','size':'small'}
>>> plt.rc('font',**font_options)
6.2.使用pandas和seaborn绘图
matplotlib实际上是一种比较低级的工具.要绘制一张图表,组装一些基本组件即可
在pandas中,有多列数据,还有行和列标签,pandas自身就有内置的方法,用于简化从DataFrame和Series绘制图形.另一个库seaborn简化了许多创建可视类型的创建
引入seaborn会修改matplotlib默认的颜色方案和绘图类型,以提高可读性和美观度,即使不使用seaborn API,也可能会引入seaborn,作为绘制常见matplotlib图形的简化方法
6.2.1.线型图
Series和DataFrame都有一个用于生成各类图表的plot方法.默认情况下生成的是线型图
>>>s=pd.Series(np.random.randn(10).cumsum(),index=np.arange(0,100,10))
>>> s.plot()
<matplotlib.axes._subplots.AxesSubplot object at 0x000001F75396A400>
Series和DataFrame都有一个用于生成各类图表的plot方法,默认情况下,它们所生成的是线型图
Series对象的索引会被传递给matplotlib绘制X轴,可用通过user_index=False禁用该功能.X轴的刻度和界限可以通过xticks和xlim选项进行调节,plot的参数如下
pandas的大部分绘图方法都有一个ax参数,它可以是一个matplotlib的subplot对象.
DataFrame的plot方法会在一个subplot中为各列绘制一条线,并自动创建图例
>>> df=pd.DataFrame(np.random.randn(10,4).cumsum(0),columns=['A','B','C','D'],index=np.arange(0,100,10))
>>> df
A B C D
0 -2.347612 -0.862134 -0.821176 0.814170
10 -4.489411 -0.665648 -1.957796 0.094580
20 -4.707920 -0.448724 -1.795481 2.262587
30 -4.449770 0.941905 -2.244556 3.406902
40 -3.572486 1.645728 -2.521541 4.322482
50 -4.230183 1.813664 -1.980417 3.498881
60 -3.586020 1.386056 -1.608901 1.992205
70 -3.575958 0.458509 -0.416607 1.294539
80 -3.034812 -0.548598 -1.781025 2.072930
90 -1.621034 -0.776007 -1.515647 -0.524305
>>> df.plot()
<matplotlib.axes._subplots.AxesSubplot object at 0x000001F753E87358>
>>> plt.show()
plt属性包含了一批不同的绘图类型的方法.例如,df.plot()等价于df.plot.line()
DataFrame还有一些对列灵活处理的选项,例如,是要将所有列都绘制到同一个subplot里还是创建各自的subplot
6.2.2.柱状图
plt.bar()和plot.barh()分别绘制水平和垂直的柱状图.这时,Series和DataFrame的索引会被用作X(bar)或Y(barh)刻度
>>> fig,axes=plt.subplots(2,1)
>>> fig
<Figure size 640x480 with 2 Axes>
>>> axes
array([<matplotlib.axes._subplots.AxesSubplot object at 0x000001F753F5FBA8>,
<matplotlib.axes._subplots.AxesSubplot object at 0x000001F754261240>],
dtype=object)
>>> data=pd.Series(np.random.randn(16),index=list('abcdefghijklmnop'))
>>> data
a 0.086807
b -0.963601
c 0.185999
d 0.853310
e 0.302532
f 0.368298
g 0.849371
h 0.096502
i -1.590980
j 1.386940
k 0.815924
l -0.443347
m -0.227171
n -0.542757
o -1.105512
p 0.011670
dtype: float64
>>> data.plot.bar(ax=axes[0],color='k',alpha=0.7)
<matplotlib.axes._subplots.AxesSubplot object at 0x000001F753F5FBA8>
>>> data.plot.barh(ax=axes[1],color='k',alpha=0.7)
<matplotlib.axes._subplots.AxesSubplot object at 0x000001F754261240>
>>> plt.show()
alpha指的是透明度.对于DataFrame,柱状图会将每一行的值分为一组,并排显示
>>> df=pd.DataFrame(np.random.randn(6,4),index=['one', 'two', 'three', 'four', 'five', 'six'],columns=pd.Index(['A', 'B', 'C', 'D'], name='Genus'))
>>> df
Genus A B C D
one 0.202340 -0.997565 -0.056434 -1.543022
two -0.679184 -0.104587 0.365165 -0.543126
three -1.024907 -0.112693 0.692220 -0.007389
four -0.395821 -1.998137 -0.927106 -1.264030
five -1.591818 -0.682712 -0.053640 -2.240465
six -0.114664 -0.743805 -0.373008 0.603921
>>> df.plot.bar()
<matplotlib.axes._subplots.AxesSubplot object at 0x000001F756246400>
>>> plt.show()
注意DataFrame各列的名称"Genus"被用作了图例的标题
设置stacked=True即可为DataFrame生成堆积柱状图,这样每行的值就会被堆积在一起
>>> df.plot.barh(stacked=True,alpha=0.5)
<matplotlib.axes._subplots.AxesSubplot object at 0x000001F7562DACF8>
>>> plt.show()
柱状图有一个不错的用法:利用value_counts图形化显示Series中各值的出现频率,比如s.value_counts().plot.bar()
再以前面用过的那个有关小费的数据集为例,假设我们想要做一张堆积柱状图以展示每天各种聚会规模的数据点的百分比。我用read_csv将数据加载进来,然后根据日期和聚会规模创建一张交叉表
In [75]: tips = pd.read_csv('examples/tips.csv')
In [76]: party_counts = pd.crosstab(tips['day'], tips['size'])
In [77]: party_counts
Out[77]:
size 1 2 3 4 5 6
day
Fri 1 16 1 1 0 0
Sat 2 53 18 13 1 0
Sun 0 39 15 18 3 1
Thur 1 48 4 5 1 3
In [78]: party_counts = party_counts.loc[:, 2:5]
然后进行规格化,使得各行的和为1,并生成图表
In [79]: party_pcts = party_counts.div(party_counts.sum(1), axis=0)
In [80]: party_pcts
Out[80]:
size 2 3 4 5
day
Fri 0.888889 0.055556 0.055556 0.000000
Sat 0.623529 0.211765 0.152941 0.011765
Sun 0.520000 0.200000 0.240000 0.040000
Thur 0.827586 0.068966 0.086207 0.017241
In [81]: party_pcts.plot.bar()
于是,通过该数据集就可以看出,聚会规模在周末会变大
对于在绘制一个图形之前,需要进行合计的数据,使用seaborn可以减少工作量.用seaborn来看每天的小费比例
In [83]: import seaborn as sns
In [84]: tips['tip_pct'] = tips['tip'] / (tips['total_bill'] - tips['tip'])
In [85]: tips.head()
Out[85]:
total_bill tip smoker day time size tip_pct
0 16.99 1.01 No Sun Dinner 2 0.063204
1 10.34 1.66 No Sun Dinner 3 0.191244
2 21.01 3.50 No Sun Dinner 3 0.199886
3 23.68 3.31 No Sun Dinner 2 0.162494
4 24.59 3.61 No Sun Dinner 4 0.172069
In [86]: sns.barplot(x='tip_pct', y='day', data=tips, orient='h')
seaborn的绘制函数使用data参数,它可能是pandas的DataFrame.其它的参数是关于列的名字.因为一天的每个值有多次观察,柱状图的值是tip_pct的平均值.绘制在柱状图上的黑线代表95%置信区间(可以通过可选参数配置)
seaborn.barplot有颜色选项,使我们能够通过一个额外的值设置
In [88]: sns.barplot(x='tip_pct', y='day', hue='time', data=tips, orient='h')
注意,seaborn已经自动修改了图形的美观度:默认调色板,图形背景和网格线的颜色.你可以用seaborn.set在不同的图形外观之间切换:
In [90]: sns.set(style="whitegrid")
6.2.3.直方图和密度图
直方图是一种可以对值频率进行离散化显示的柱状图,数据点被拆分到离散点再以前面那个小费数据为例,通过在Series使用plot.hist方法,可以生成一张“小费占消费总额百分比”的直方图
in [92]: tips['tip_pct'].plot.hist(bins=50)
与此相关的一种图表类型是密度图,它是通过计算“可能会产生观测数据的连续概率分布的估计”而产生的.一般的过程是将该分布近似为一组核(即诸如正态分布之类的较为简单的分布).因此,密度图也被称作KDE(Kernel Density Estimate,核密度估计)图.使用plot.kde和标准混合正态分布估计即可生成一张密度图
In [94]: tips['tip_pct'].plot.density()
seaborn的distplot方法绘制直方图和密度图更加简单,还可以同时画出直方图和连续密度估计图.作为例子,考虑一个双峰分布,由两个不同的标准正态分布组成
In [96]: comp1 = np.random.normal(0, 1, size=200)
In [97]: comp2 = np.random.normal(10, 2, size=200)
In [98]: values = pd.Series(np.concatenate([comp1, comp2]))
In [99]: sns.distplot(values, bins=100, color='k')
6.2.4.散布图或点图
点图或散布图是观察两个一维数据序列之间的关系的有效手段.在下面这个例子中,加载了来自statsmodels项目的macrodata数据集,选择了几个变量,然后计算对数差:
In [100]: macro = pd.read_csv('examples/macrodata.csv')
In [101]: data = macro[['cpi', 'm1', 'tbilrate', 'unemp']]
In [102]: trans_data = np.log(data).diff().dropna()
In [103]: trans_data[-5:]
Out[103]:
cpi m1 tbilrate unemp
198 -0.007904 0.045361 -0.396881 0.105361
199 -0.021979 0.066753 -2.277267 0.139762
200 0.002340 0.010286 0.606136 0.160343
201 0.008419 0.037461 -0.200671 0.127339
202 0.008894 0.012202 -0.405465 0.042560
然后可以使用seaborn的regplot方法,它可以做一个散布图,并加上一条线性回归的线
In [105]: sns.regplot('m1', 'unemp', data=trans_data)
Out[105]: <matplotlib.axes._subplots.AxesSubplot at 0x7fb613720be0>
In [106]: plt.title('Changes in log %s versus log %s' % ('m1', 'unemp'))
在探索式数据分析工作中,同时观察一组变量的散布图是很有意义的,这也被称为散布图矩阵(scatter plot matrix).纯手工创建这样的图表很费工夫,所以seaborn提供了一个便捷的pairplot函数,它支持在对角线上放置每个变量的直方图或密度估计
In [107]: sns.pairplot(trans_data, diag_kind='kde', plot_kws={'alpha': 0.2})
plot_kws参数可以让我们传递配置选项到非对角线元素上的图形使用.对于更详细的配置选项,可以查阅seaborn.pairplot文档字符串
6.2.5.分面网格和类型数据
要是数据集有额外的分组维度呢?有多个分类变量的数据可视化的一种方法是使用小面网格.seaborn有一个有用的内置函数factorplot,可以简化制作多种分面图
In [108]: sns.factorplot(x='day', y='tip_pct', hue='time', col='smoker',
.....: kind='bar', data=tips[tips.tip_pct < 1])
除了在分面中用不同的颜色按时间分组,我们还可以通过给每个时间值添加一行来扩展分面网格:
In [109]: sns.factorplot(x='day', y='tip_pct', row='time',
.....: col='smoker',
.....: kind='bar', data=tips[tips.tip_pct < 1])
factorplot支持其它的绘图类型,也可能会用到.例如,盒图(它可以显示中位数,四分位数,异常值)就是一个有用的可视化类型
In [110]: sns.factorplot(x='tip_pct', y='day', kind='box',
.....: data=tips[tips.tip_pct < 0.5])
使用更通用的seaborn.FacetGrid类,你可以创建自己的分面网格。请查阅seaborn的文档(https://seaborn.pydata.org/)。
6.3 其它的Python可视化工具
与其它开源库类似,Python创建图形的方式非常多.自从2010年,许多开发工作都集中在创建交互式图形以便在Web上发布.利用工具如Boken(https://bokeh.pydata.org/en/latest/)和Plotly(https://github.com/plotly/plotly.py),现在可以创建动态交互图形,用于网页浏览器.
七、数据聚合和分组运算
对数据集进行分组并对各组应用一个函数,通常是数据分析中的重要环节.在将数据集加载,融合,准备好之后,通常就是计算分组统计或生成透视表.Python和pandas提供了比SQL更为复杂的分组运算
7.1.GroupBy机制
分组运算包括:拆分-应用-合并.pandas对象中的数据根据一个或多个键被拆分为多组.拆分操作是在对象的特定轴上执行的,然后将一个函数应用到各个分组并产生一个新值.最后,所有函数的执行结果被合并到最终的结果对象中.结果对象的形式一般取决于所执行的操作
下面是一个案例
>>> df=pd.DataFrame({'key1':['a', 'a', 'b', 'b', 'a'],'key2' : ['one', 'two', 'one', 'two', 'one'],'data1' : np.random.randn(5),'data2' : np.random.randn(5)})
>>> df
key1 key2 data1 data2
0 a one -0.553457 1.408032
1 a two -0.033779 -1.319348
2 b one 0.138450 0.885473
3 b two -0.133778 -1.125302
4 a one 1.862205 -2.089357
假设想要按照key1进行分组,并计算data1列的平均值,访问data1,并根据key1调用groupby
>>> grouped=df['data1'].groupby(df['key1'])
>>> grouped
<pandas.core.groupby.groupby.SeriesGroupBy object at 0x000002191103CF60>
>>> grouped.mean()
key1
a 0.424990
b 0.002336
Name: data1, dtype: float64
变量grouped是一个GroupBy对象,它实际还没有进行任何运算,只是含有数据,这里数据根据分组键进行了聚合,产生了一个新的Series,其索引key1列中的唯一值,如果传入多个数据的列表.就会得到不同结果
>>> means=df['data1'].groupby([df['key1'],df['key2']]).mean()
>>> means
key1 key2
a one 0.654374
two -0.033779
b one 0.138450
two -0.133778
Name: data1, dtype: float64
这里,通过两个键对数据进行了分组,得到的Series具有一个层次化索引
>>> means.unstack()
key2 one two
key1
a 0.654374 -0.033779
b 0.138450 -0.133778
分组键可以是任何长度适当的数组
>>> states = np.array(['Ohio', 'California', 'California', 'Ohio', 'Ohio'])
>>> years = np.array([2005, 2005, 2006, 2005, 2006])
>>> df['data1'].groupby([states,years]).mean()
California 2005 -0.033779
2006 0.138450
Ohio 2005 -0.343618
2006 1.862205
Name: data1, dtype: float64
>>> df
key1 key2 data1 data2
0 a one -0.553457 1.408032
1 a two -0.033779 -1.319348
2 b one 0.138450 0.885473
3 b two -0.133778 -1.125302
4 a one 1.862205 -2.089357
通常,分组信息就位于相同的要处理DataFrame中.这里,还可以将列名作为分组键
>>> df.groupby('key1').mean()
data1 data2
key1
a 0.424990 -0.666891
b 0.002336 -0.119914
>>> df.groupby(['key1','key2']).mean()
data1 data2
key1 key2
a one 0.654374 -0.340662
two -0.033779 -1.319348
b one 0.138450 0.885473
two -0.133778 -1.125302
groupby的size方法可以返回一个含有分组大小的Series
>>> df.groupby(['key1', 'key2']).size()
key1 key2
a one 2
two 1
b one 1
two 1
dtype: int64
任何分组关键词中的缺失值都会被从结果中滤去
g-ET17whMu-1633015922155)]
注意,seaborn已经自动修改了图形的美观度:默认调色板,图形背景和网格线的颜色.你可以用seaborn.set在不同的图形外观之间切换:
In [90]: sns.set(style="whitegrid")
6.2.3.直方图和密度图
直方图是一种可以对值频率进行离散化显示的柱状图,数据点被拆分到离散点再以前面那个小费数据为例,通过在Series使用plot.hist方法,可以生成一张“小费占消费总额百分比”的直方图
in [92]: tips['tip_pct'].plot.hist(bins=50)
与此相关的一种图表类型是密度图,它是通过计算“可能会产生观测数据的连续概率分布的估计”而产生的.一般的过程是将该分布近似为一组核(即诸如正态分布之类的较为简单的分布).因此,密度图也被称作KDE(Kernel Density Estimate,核密度估计)图.使用plot.kde和标准混合正态分布估计即可生成一张密度图
In [94]: tips['tip_pct'].plot.density()
seaborn的distplot方法绘制直方图和密度图更加简单,还可以同时画出直方图和连续密度估计图.作为例子,考虑一个双峰分布,由两个不同的标准正态分布组成
In [96]: comp1 = np.random.normal(0, 1, size=200)
In [97]: comp2 = np.random.normal(10, 2, size=200)
In [98]: values = pd.Series(np.concatenate([comp1, comp2]))
In [99]: sns.distplot(values, bins=100, color='k')
6.2.4.散布图或点图
点图或散布图是观察两个一维数据序列之间的关系的有效手段.在下面这个例子中,加载了来自statsmodels项目的macrodata数据集,选择了几个变量,然后计算对数差:
In [100]: macro = pd.read_csv('examples/macrodata.csv')
In [101]: data = macro[['cpi', 'm1', 'tbilrate', 'unemp']]
In [102]: trans_data = np.log(data).diff().dropna()
In [103]: trans_data[-5:]
Out[103]:
cpi m1 tbilrate unemp
198 -0.007904 0.045361 -0.396881 0.105361
199 -0.021979 0.066753 -2.277267 0.139762
200 0.002340 0.010286 0.606136 0.160343
201 0.008419 0.037461 -0.200671 0.127339
202 0.008894 0.012202 -0.405465 0.042560
然后可以使用seaborn的regplot方法,它可以做一个散布图,并加上一条线性回归的线
In [105]: sns.regplot('m1', 'unemp', data=trans_data)
Out[105]: <matplotlib.axes._subplots.AxesSubplot at 0x7fb613720be0>
In [106]: plt.title('Changes in log %s versus log %s' % ('m1', 'unemp'))
在探索式数据分析工作中,同时观察一组变量的散布图是很有意义的,这也被称为散布图矩阵(scatter plot matrix).纯手工创建这样的图表很费工夫,所以seaborn提供了一个便捷的pairplot函数,它支持在对角线上放置每个变量的直方图或密度估计
In [107]: sns.pairplot(trans_data, diag_kind='kde', plot_kws={'alpha': 0.2})
plot_kws参数可以让我们传递配置选项到非对角线元素上的图形使用.对于更详细的配置选项,可以查阅seaborn.pairplot文档字符串
6.2.5.分面网格和类型数据
要是数据集有额外的分组维度呢?有多个分类变量的数据可视化的一种方法是使用小面网格.seaborn有一个有用的内置函数factorplot,可以简化制作多种分面图
In [108]: sns.factorplot(x='day', y='tip_pct', hue='time', col='smoker',
.....: kind='bar', data=tips[tips.tip_pct < 1])
除了在分面中用不同的颜色按时间分组,我们还可以通过给每个时间值添加一行来扩展分面网格:
In [109]: sns.factorplot(x='day', y='tip_pct', row='time',
.....: col='smoker',
.....: kind='bar', data=tips[tips.tip_pct < 1])
factorplot支持其它的绘图类型,也可能会用到.例如,盒图(它可以显示中位数,四分位数,异常值)就是一个有用的可视化类型
In [110]: sns.factorplot(x='tip_pct', y='day', kind='box',
.....: data=tips[tips.tip_pct < 0.5])
使用更通用的seaborn.FacetGrid类,你可以创建自己的分面网格。请查阅seaborn的文档(https://seaborn.pydata.org/)。
6.3 其它的Python可视化工具
与其它开源库类似,Python创建图形的方式非常多.自从2010年,许多开发工作都集中在创建交互式图形以便在Web上发布.利用工具如Boken(https://bokeh.pydata.org/en/latest/)和Plotly(https://github.com/plotly/plotly.py),现在可以创建动态交互图形,用于网页浏览器.
七、数据聚合和分组运算
对数据集进行分组并对各组应用一个函数,通常是数据分析中的重要环节.在将数据集加载,融合,准备好之后,通常就是计算分组统计或生成透视表.Python和pandas提供了比SQL更为复杂的分组运算
7.1.GroupBy机制
分组运算包括:拆分-应用-合并.pandas对象中的数据根据一个或多个键被拆分为多组.拆分操作是在对象的特定轴上执行的,然后将一个函数应用到各个分组并产生一个新值.最后,所有函数的执行结果被合并到最终的结果对象中.结果对象的形式一般取决于所执行的操作
下面是一个案例
>>> df=pd.DataFrame({'key1':['a', 'a', 'b', 'b', 'a'],'key2' : ['one', 'two', 'one', 'two', 'one'],'data1' : np.random.randn(5),'data2' : np.random.randn(5)})
>>> df
key1 key2 data1 data2
0 a one -0.553457 1.408032
1 a two -0.033779 -1.319348
2 b one 0.138450 0.885473
3 b two -0.133778 -1.125302
4 a one 1.862205 -2.089357
假设想要按照key1进行分组,并计算data1列的平均值,访问data1,并根据key1调用groupby
>>> grouped=df['data1'].groupby(df['key1'])
>>> grouped
<pandas.core.groupby.groupby.SeriesGroupBy object at 0x000002191103CF60>
>>> grouped.mean()
key1
a 0.424990
b 0.002336
Name: data1, dtype: float64
变量grouped是一个GroupBy对象,它实际还没有进行任何运算,只是含有数据,这里数据根据分组键进行了聚合,产生了一个新的Series,其索引key1列中的唯一值,如果传入多个数据的列表.就会得到不同结果
>>> means=df['data1'].groupby([df['key1'],df['key2']]).mean()
>>> means
key1 key2
a one 0.654374
two -0.033779
b one 0.138450
two -0.133778
Name: data1, dtype: float64
这里,通过两个键对数据进行了分组,得到的Series具有一个层次化索引
>>> means.unstack()
key2 one two
key1
a 0.654374 -0.033779
b 0.138450 -0.133778
分组键可以是任何长度适当的数组
>>> states = np.array(['Ohio', 'California', 'California', 'Ohio', 'Ohio'])
>>> years = np.array([2005, 2005, 2006, 2005, 2006])
>>> df['data1'].groupby([states,years]).mean()
California 2005 -0.033779
2006 0.138450
Ohio 2005 -0.343618
2006 1.862205
Name: data1, dtype: float64
>>> df
key1 key2 data1 data2
0 a one -0.553457 1.408032
1 a two -0.033779 -1.319348
2 b one 0.138450 0.885473
3 b two -0.133778 -1.125302
4 a one 1.862205 -2.089357
通常,分组信息就位于相同的要处理DataFrame中.这里,还可以将列名作为分组键
>>> df.groupby('key1').mean()
data1 data2
key1
a 0.424990 -0.666891
b 0.002336 -0.119914
>>> df.groupby(['key1','key2']).mean()
data1 data2
key1 key2
a one 0.654374 -0.340662
two -0.033779 -1.319348
b one 0.138450 0.885473
two -0.133778 -1.125302
groupby的size方法可以返回一个含有分组大小的Series
>>> df.groupby(['key1', 'key2']).size()
key1 key2
a one 2
two 1
b one 1
two 1
dtype: int64
任何分组关键词中的缺失值都会被从结果中滤去
|