通俗易懂 Python 正则表达式
官方文档
正则表达式, 也叫规则表达式, 是强大的文本字符串处理工具, 通常用来验证, 查找, 筛选, 提取, 替换那些符合某个规则的文本字符串,是实现文本高效处理的神器
1, 匹配规则
正则表达式的核心就是设计一个规则, 按照这个规则”按图索骥”, 去寻找符合这个规则的字符串, 并将它按需处理
先以一个最简单的例子进行探索:
re
: 正则表达式模块
findall
: 模块的一个方法, 传入 正则表达式 和 要去匹配字符串 将匹配结果以列表形式返回, 没有匹配结果返回空列表
\d
: 定义的规则, 表示匹配任意一个 0~9 的数字
198\d年
: 匹配符合 198某年
的字符串
然后按照规则去匹配字符串: '1988年 2000年 2020年 1980年'
# 导入模块 re
import re
# 按规则 r'198\d年' 匹配, r 的作用在 python 基础部分已介绍
re.findall(r'198\d年', '1988年 2000年 2020年 1980年')
['1988年', '1980年']
\d
是其中一个规则定义符, 可以和其他字符组合成正则表达式, 它自身也是一个正则表达式
d
则是字母 d 本身,像 \d
这样的特殊规则定义符有许多,如 .
,*
,+
,\s
,()
,[]
等等
任意一个非特殊字符都可以作为匹配它自身的正则表达式
如果要匹配规则定义符,例如如要匹配 \d
, 需要用 \
进行转义,也就是要用 \\d
来匹配 \d
(其实是 \\
来匹配 \
,d
来匹配 d
)
re.findall(r'\\d.+\[a]', r'a\d.+[a]')
['\\d.+[a]']
2, 常用规则定义符
2.01, 定义类别匹配
\w
匹配任意一个可以构成词语的 Unicode 字符, 包括数字及下划线, \W
则相反
a = r'my\wname'
b = r'my\Wname'
c = 'my1name, my_name, my.name, my我name'
re.findall(a, c)
['my1name', 'my_name', 'my我name']
['my.name']
\d
匹配任意一个 十进制 数字, \D
匹配任意一个非数字
a = r'01\d-\D123'
b = '010-0123, 010-o123, 010-P123'
re.findall(a, b)
['010-o123', '010-P123']
\s
匹配任何Unicode空白字符(包括 [ \t\n\r\f\v]
,还有很多其他字符,比如不同语言排版规则约定的不换行空格), \S
则相反
a = r'a\sb\Sc'
b = 'a b c, a bcc, a bcc'
re.findall(a, b)
['a bcc']
.
匹配除换行符 \n
之外的任意一个字符
a = r'a.b'
b = '''a
b, a-b, a b, a\nb'''
re.findall(a, b)
['a-b', 'a b']
\b
匹配 \w
和 \W
之间(或 \w
开头和结尾的边界)的空字符串, \B
与 \b
相反,匹配非 \w
和 \W
之间的空字符串(或 \W
开头和结尾的边界)
re.findall(r'\b.', 'ab啊_c。d,\n')
['a', '。', 'd', ',']
re.findall(r'.\B', 'ab啊_c。d,\n')
['a', 'b', '啊', '_', ',']
2.02, 定义范围匹配
用括号 []
将字符(表达式)包围起来, 表示在括号内指定的范围内匹配任意一个
re.findall('[abc]', 'bill')
['b']
在 []
内, 以 ^
开头, 表示排除括号内的字符(表达式)范围匹配
re.findall('[^abc]', 'abcd')
['d']
在 []
内, 数字或字母之间用 -
连接, 表示在两者(包含)之间的范围内匹配
re.findall('[a-d0-5A\-D]', 'af357AB-')
['a', '3', '5', 'A', '-']
一些特殊字符在 []
内失去特殊含义
re.findall('[(+*)\]\w]', '+(*)a].?')
['+', '(', '*', ')', 'a', ']']
2.03, 定义边界匹配
^
或 \A
, 表示必须以接下来的字符(表达式)开头才能被匹配, 换行开头也不能匹配
a = '^b\d[bc]'
b = '''a2b
b2b'''
c = 'b2bcd'
re.findall(a, b)
[]
['b2b']
a = '\Ab\d[bc]'
b = '''a2b
b2b'''
c = 'b2bcd'
re.findall(a, b)
[]
['b2b']
$
或 \Z
表示必须以其前面的字符(表达式)结尾才能被匹配, 换行之前的结束也不能匹配
re.findall('a\w$', '''ab
ac''')
['ac']
re.findall('a\w\Z', 'ab\nad')
['ad']
2.04, 定义数量匹配
+
其前面的字符(表达式)至少有一个的都能匹配
re.findall(r'10+', '110, 100, 1001')
['10', '100', '100']
?
其前面的字符(表达式)最多有一个的才能匹配
re.findall(r'10?', '1, 10, 100')
['1', '10', '10']
*
其前面的字符(表达式)没有或有多个都可以匹配
re.findall(r'10*', '1, 10, 1001')
['1', '10', '100', '1']
{n}
其前面的字符(表达式)有 n 个才能匹配
re.findall(r'1\d{3}', '12, 123, 1a23, 1234')
['1234']
{n,}
其前面的字符(表达式)有至少 n 个才能匹配, {m,n}
则表示有 m~n 个才能匹配, m 和 n 之间只有 ,
号而不能有空格
re.findall(r'1\d{2,}', '12, 123, 1234, 12345')
['123', '1234', '12345']
re.findall(r'1\d{2,3}', '12, 123, 1234, 12345')
['123', '1234', '1234']
2.05, 或
匹配符 |
符号 |
两边的内容, 有一边匹配 或 两边都匹配都可以, 但当 |
在括号 []
中则无此作用, 只代表它自身
re.findall(r'aa|bb', 'aacbbcaabbab')
['aa', 'bb', 'aa', 'bb']
re.findall(r'a[|]b', 'ab, a|b')
['a|b']
2.06, 定义组合匹配
用 ()
将多个字符组合成一个整体来匹配, 不管 ()
外是否有数量匹配符, 都只返回 ()
内的内容, 如果表达式内有多个 ()
封装的内容, 匹配结果以元组形式返回
# 匹配的是 ab 和 abab 但只返回括号内的 ab
re.findall(r'(ab)+', 'ab11abab')
['ab', 'ab']
re.findall(r'^(ab)+', 'ab11abab') # 必须 ab 开头
['ab']
# 匹配的是后面的 abab 返回元组
re.findall(r'(ab)(ab)', 'ab11abab')
[('ab', 'ab')]
将贪婪匹配转为非贪婪匹配
# 贪婪匹配 \d+ 使得无法将 000 取出
re.findall(r'(\d+)(0*)$', '123000')
[('123000', '')]
# + 后面加个 ? 号
re.findall(r'(\d+?)(0*)$', '123000')
[('123', '000')]
re.findall(r'[(ab)]', 'ab, (ab)')
['a', 'b', '(', 'a', 'b', ')']
如果 ()
内以 ?:
开头, 只有一组时,返回匹配的字符串,多组则 ?:
开头的不捕获不返回
re.findall(r'(?:ab)+', 'ab11abab')
['ab', 'abab']
# 匹配的是后面的 abab, 但只返回前一个 () 内的内容
re.findall(r'(ab)(?:ab)+', 'ab11abab')
['ab']
如果 ()
内以 ?=
开头, 则 ()
内的内容只是用来匹配, 不返回也不消耗匹配的字符, 也就是说, 匹配完了, 后面的字符需要继续匹配
re.findall(r'a(?=bc)bc', 'ab, abc, abcde')
['abc', 'abc']
如果 ()
内以 ?!
开头, 则 ()
内的内容只是用来排除, 也就是说, 排除括号内的内容都可以匹配, 不占字符, 匹配完了,
后面的字符需要继续匹配
# a 后面除了 bc 都可以匹配
re.findall(r'a(?!bc)\w+', 'abc, acb, abde')
['acb', 'abde']
\number
匹配指定的组合,组合从 1 开始编号
# \1 匹配第一个组合 (a)
re.findall(r'(a)(\d+)(\1)', 'aba1aa34a2')
[('a', '1', 'a'), ('a', '34', 'a')]
绝大部分 Python 的标准转义字符也被正则表达式分析器支持:
\a \b \f \n
\N \r \t \u
\U \v \x \\
(注意 \b
被用于表示词语的边界,它只在 []
内表示退格,比如 [\b]
)
2.07, 匹配规则修改标志
介绍几个常用的规则修改标志
re.I
大小写不敏感匹配
re.findall(r'aA','aA, aa', re.I)
['aA', 'aa']
re.S
使 .
匹配任何字符, 包括换行符
s = '''a
b, a-b'''
re.findall(r'a.b', s, re.S)
['a\nb', 'a-b']
re.M
使得 ^
和 $
能匹配换行的开始或换行前的结束
a = '^b\d[bc]'
b = '''b2c
b2b'''
re.findall(a, b, re.M)
['b2c', 'b2b']
a = 'a\d[bc]$'
b = '''a2b
a2c'''
re.findall(a, b, re.M)
['a2b', 'a2c']
3, 匹配和处理方法
match
从字符串起始位置匹配, 匹配到的字符返回为 match 对象, 可以用方法加以获取
a = re.match(r'ab', 'abc')
b = re.match(r'a(bc)d(ef)g', 'abcdefgh')
c = re.match(r'ab', 'bab')
a
<re.Match object; span=(0, 2), match='ab'>
<re.Match object; span=(0, 7), match='abcdefg'>
# group() 获取匹配的全部字符,
# group(1) 获取 () 内匹配的第一个, 以此类推
a.group(), b.group(), b.group(1), b.group(2)
('ab', 'abcdefg', 'bc', 'ef')
# start 获取匹配开始的位置
a.start(), b.start()
(0, 0)
# end 获取匹配结束的位置
a.end(), b.end()
(2, 7)
# span 获取 (开始, 结束) 元组
a.span(), b.span()
((0, 2), (0, 7))
compile
编译正则表达式, 返回一个 Pattern 对象(后面简写为p), 然后可以用该对象调用方法
re.compile(r'\d+', re.UNICODE)
# 调用上面的 match 方法, 可以指定开始和结束位置
a = p.match('12a21', 3)
a
<re.Match object; span=(3, 5), match='21'>
re.search
扫描整个字符串, 第一个匹配的字符串返回为 match 对象
p.search
扫描指定范围, 不指定就全部扫描
r = 'python\d.\d'
s = 'python3.0, python3.8'
p = re.compile(r)
a = re.search(r, s)
b = p.search(s)
c = p.search(s, 1)
a
<re.Match object; span=(0, 9), match='python3.0'>
<re.Match object; span=(0, 9), match='python3.0'>
<re.Match object; span=(11, 20), match='python3.8'>
sub
替换字符串中的匹配项, 可以控制替换次数, 还可传入函数高级替换
r = '[,.]'
s = '去掉,逗号和.句号.'
p = re.compile(r)
re.sub(p, '', s)
'去掉逗号和句号'
'去掉逗号和.句号.'
findall
已经介绍过
r = r'\D+(\d+)\D+(\d+)\D+(\d+)*'
s = '分组提取123所有456数字78.'
p = re.compile(r)
re.findall(p, s)
[('123', '456', '78')]
[('456', '78', '')]
finditer
和 findall
类似, 只是返回的是一个 match 迭代器
import re
r = r'\D+(\d+)\D+(\d+)\D+(\d+)*'
s = '分组提取123所有456数字78.'
p = re.compile(r)
a = re.finditer(p, s)
b = p.finditer(s, 8)
a
<callable_iterator at 0x18d4fa31460>
<callable_iterator at 0x18d4fa31430>
for i, j in zip(a, b):
print(i)
print(j)
print(i.group(1, 2, 3))
print(j.group(1, 2, 3))
<re.Match object; span=(0, 16), match='分组提取123所有456数字78'>
<re.Match object; span=(8, 17), match='有456数字78.'>
('123', '456', '78')
('456', '78', None)
split
用匹配到的字符(或字符串), 将整个字符串分割返回列表, 可设置最大拆分数
r = '[\s\,-]'
s = '将,每 个-字 拆,开'
p = re.compile(r)
re.split(p, s)
['将', '每', '个', '字', '拆', '开']
['将', '每', '个-字 拆,开']
Numpy 基础快速了解和查询
官方文档
numpy 是 python 的强大科学计算库, 它让 python 处理数据, 进行科学计算变得极其便捷, 高效
一, N 维数组 ndarray 对象
1, ndaraay 介绍
ndarray 是具有相同类型和大小的项目的多维容器, 它的数据存储效率和输入输出性能远远优于 python 中等价的数据结构
1.01, 理解 ndarray
创建 ndarray: np.array()
, np.ndarray()
, 下面只介绍 3 维以下数组, 理解它们的结构
import numpy as np
# 一维
a = np.array((2,3))
# 二维,2 行 3 列
b = np.ndarray((2,3))
array([2, 3])
array([[0., 0., 0.],
[0., 0., 0.]])
(dtype('int32'), dtype('float64'))
array(['1', '2'], dtype='<U11')
(numpy.ndarray, numpy.str_)
可见, ndarray 对象中的元素类型只有一种, 如上例,都统一成 Unicode, 元素位长是 11, 所以说 ndarray 是一个具有相同类型和大小的项目的多维容器
一维就是一行,二维就像一个表格, 横着是一行, 竖着是一列
# 三维
np.ndarray((3, 3, 3))
array([[[6.23042070e-307, 7.56587584e-307, 1.37961302e-306],
[6.23053614e-307, 6.23053954e-307, 9.34609790e-307],
[8.45593934e-307, 9.34600963e-307, 9.34598925e-307]],
[[1.42418715e-306, 1.95819401e-306, 1.37961370e-306],
[6.23054633e-307, 9.34598926e-307, 6.89804132e-307],
[9.34598926e-307, 8.01091099e-307, 1.24610927e-306]],
[[8.90094053e-307, 1.02361342e-306, 7.56593017e-307],
[1.60219035e-306, 8.90111708e-307, 6.89806849e-307],
[9.34601642e-307, 1.42410974e-306, 5.97819431e-322]]])
三维类似一个 excel 文件里的多个表格
1.02, ndarray 对象计算的优势
用实例来对比利用 ndarray 进行计算的优势
import random
import time
# 假设有如下列表和 array, 使其中每个数都变成它的平方
a1 = [random.random() for i in range(100000000)]
# a2 = np.array(a1)
t1 = time.time()
b = [i**2 for i in a1]
t2 = time.time()
a2 = np.array(a1)
c = a2**2
t3 = time.time()
print(t2 - t1, t3 - t2, sep='\n')
15.133688688278198
4.770834445953369
差距非常明显(如果 a2 在计算之前就已经是 ndarray 对象会更明显), numpy 有其自身机制(广播)实现快速计算
1.03, ndarray 的数据类型和常量
ndarray 对象数据的数据类型有 bool
, int
, float
, U
(字符串), O
(python 对象)等, 查看数据类型用
dtype
import numpy as np
a = np.array([1, 'a', np.nan])
a.dtype, a[0].dtype, a[2].dtype
(dtype('<U32'), dtype('<U1'), dtype('<U3'))
dtype('bool')
dtype('int32')
np.array(3.1415926).dtype
dtype('float64')
dtype('O')
ndarray 对象数据的常量有 pi
, e
, nan
(空值), inf
(无穷大)等
3.141592653589793
2.718281828459045
nan
inf
2, ndarray 的属性
常用 ndarray 对象的属性
a = np.array([[1, 2, 3], [4, 5, 6]])
a
array([[1, 2, 3],
[4, 5, 6]])
查看数组形状 (行, 列)
, 这在数据处理和计算时非常有用, 例如遍历出所有行或列
(2, 3)
# 遍历所有列
for i in range(a.shape[1]):
print(a[:, i])
[1 4]
[2 5]
[3 6]
查看数组维度
2
查看数组元素个数
6
查看数组类型
dtype('int32')
查看数组元素的大小, 字节为单位
4
数组转置, 就是行列互换
array([[1, 4],
[2, 5],
[3, 6]])
二, ndarray 对象的切片操作
1, 切片取值
ndarray 的切片操作和 python 类似, 一维切片 [起始:结束:步长]
, 二维的切片 [起始行:结束行:步长, 起始列:结束列:步长]
或 [[行, ...], [列, ...]]
a = np.arange(20)
a.shape = (4, 5)
a
array([[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14],
[15, 16, 17, 18, 19]])
# 第一行的第 1, 3, 5 个数
a[0][0::2]
array([0, 2, 4])
13
array([[1, 2, 3, 4],
[6, 7, 8, 9]])
array([[ 0, 2],
[10, 12]])
array([[ 5, 6, 7],
[15, 16, 17]])
还可以通过逻辑运算取值
# 取出数组中的数翻倍之后小于 15 的数
# 结果展开为 1 维
a[a * 2 < 15]
array([0, 1, 2, 3, 4, 5, 6, 7])
# & 与
a[(a < 5) & (a > 1)]
array([2, 3, 4])
# | 或
a[(a < 3) | (a > 15)]
array([ 0, 1, 2, 16, 17, 18, 19])
array([0, 1, 2, 3])
2, 利用切片修改值
切片修改值, 其实就是将取到的值重新赋值
array([[ 1, 1, 1, 1, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14],
[15, 16, 17, 18, 19]])
# 将 1 修改为 nan, nan 是 float 类型,
# 保证类型一致性, 需要将 a 转为 float
a = a.astype(float)
a[a == 1] = np.nan
a
array([[nan, nan, nan, nan, 4.],
[ 5., 6., 7., 8., 9.],
[10., 11., 12., 13., 14.],
[15., 16., 17., 18., 19.]])
# 任何 nan 都不等于 nan,
# 利用此特性取 nan, 或非 nan 的数
a[a != a], a[a == a]
(array([nan, nan, nan, nan]),
array([ 4., 5., 6., 7., 8., 9., 10., 11., 12., 13., 14., 15., 16.,
17., 18., 19.]))
# 交换行, 交换列
a[[0, 3], :] = a[[3, 0], :]
a[:, [4, 0]] = a[:, [0, 4]]
a
array([[19., 16., 17., 18., 15.],
[ 9., 6., 7., 8., 5.],
[14., 11., 12., 13., 10.],
[ 4., nan, nan, nan, nan]])
三, ndarray 对象的运算及其方法
1, ndarray 的运算
numpy 在运算时, 会通过广播机制, 将运算相对应到每个元素上:
数和数组计算, 数会和数组中的每一个元素进行计算
# 方法 reshape 用于设置数组的形状, 后面会说
import numpy as np
a = np.arange(9).reshape((3, 3))
a
array([[0, 1, 2],
[3, 4, 5],
[6, 7, 8]])
array([[ 0, -2, -4],
[ -6, -8, -10],
[-12, -14, -16]])
相同形状的多维数组之间的运算, 对应位置的元素进行计算
array([[ 0, -1, -2],
[-3, -4, -5],
[-6, -7, -8]])
array([[ 3, 3, 3],
[ 0, 0, 0],
[-3, -3, -3]])
二维一列的数组和多维数组之间的运算, 需要列元素个数相同, 然后一一对应并广播计算
# 列相同
# a 的列取出来成了一维, 需要转为二维
a[:, 1].reshape((3, 1)) + (a + b)
array([[ 1, 0, -1],
[ 1, 0, -1],
[ 1, 0, -1]])
2, 常用 ndarray 对象的方法
就是直接用 ndarray 对象调用的方法
2.01, 形状操作
flatten 或 ravel 将多维数组展开为一维, 不改变原数组
array([[0, 1, 2],
[3, 4, 5],
[6, 7, 8]])
# 默认参数 'C' 以行展开, 'F' 以列展开
display(a.flatten())
display(a.flatten('F'))
a.ravel('F')
array([0, 1, 2, 3, 4, 5, 6, 7, 8])
array([0, 3, 6, 1, 4, 7, 2, 5, 8])
array([0, 3, 6, 1, 4, 7, 2, 5, 8])
reshape 修改数组的形状, 不改变原数组, 行列数相乘要等于元素总数; resize 就地修改原数组形状
# 默认以行展开修改
import numpy as np
b = np.arange(12).reshape((3, 4))
display(b)
b.reshape((2, 6), order='F')
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])
array([[ 0, 8, 5, 2, 10, 7],
[ 4, 1, 9, 6, 3, 11]])
# 就地修改 b
display(b)
b.resize((4, 3))
b
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])
array([[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8],
[ 9, 10, 11]])
2.02, 转换类型
tolist 数组转列表
[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10, 11]]
astype 改变数组类型
array([[ 0., 1., 2.],
[ 3., 4., 5.],
[ 6., 7., 8.],
[ 9., 10., 11.]])
2.03, 修改, 排序及查找
fill 给原数组重新填值, 类型不变
array([[ 2, 2, 2],
[ 3, 4, 5],
[ 6, 7, 8],
[ 9, 10, 11]])
sort 就地排序, 参数 0 将列排序, 1 将行排序
a = np.array([[3, 0, 5], [8, 4, 6], [0, 7, 3]])
a.sort(0)
a
array([[0, 0, 3],
[3, 4, 5],
[8, 7, 6]])
array([[0, 1, 2],
[0, 1, 2],
[2, 1, 0]], dtype=int64)
nonzero 返回非零元素的索引, 行坐标一个数组, 列坐标一个数组
(array([0, 1, 1, 1, 2, 2, 2], dtype=int64),
array([2, 0, 1, 2, 0, 1, 2], dtype=int64))
2.04, 计算
max 返回最大值, 给定轴将返回给定轴的最大值, argmax 返回索引, 与之对应的还有最小min 和 argmin
# 参数 0 列的最大值, 参数 1 行的最大值
a = np.arange(1.01, 13.13, 1.01).reshape(3, 4)
print(a)
print(a.max(), a.max(axis=0), a.max(axis=1))
[[ 1.01 2.02 3.03 4.04]
[ 5.05 6.06 7.07 8.08]
[ 9.09 10.1 11.11 12.12]]
12.12 [ 9.09 10.1 11.11 12.12] [ 4.04 8.08 12.12]
# 参数 0 行索引, 1 列索引
print(a.argmax(), a.argmax(0), a.argmax(1))
11 [2 2 2 2] [3 3 3]
ptp 返回极值或给定轴的极值(最大-最小)
a.ptp(), a.ptp(axis=0), a.ptp(axis=1)
(11.11, array([8.08, 8.08, 8.08, 8.08]), array([3.03, 3.03, 3.03]))
clip 返回指定值之间的数组, 小于或大于指定值的, 用指定值填充
array([[2. , 2.02, 3.03, 4.04],
[5.05, 6. , 6. , 6. ],
[6. , 6. , 6. , 6. ]])
round 四舍五入到指定位数
array([[ 1. , 2. , 3. , 4. ],
[ 5. , 6.1, 7.1, 8.1],
[ 9.1, 10.1, 11.1, 12.1]])
sum 求和, cumsum 累积求和
print(a.sum()) # 总和
print(a.sum(0)) # 列求和
print(a.sum(1)) # 行求和
78.78
[15.15 18.18 21.21 24.24]
[10.1 26.26 42.42]
print(a.cumsum()) # 全部累积
print(a.cumsum(0)) # 列累积
print(a.cumsum(1)) # 行累积
[ 1.01 3.03 6.06 10.1 15.15 21.21 28.28 36.36 45.45 55.55 66.66 78.78]
[[ 1.01 2.02 3.03 4.04]
[ 6.06 8.08 10.1 12.12]
[15.15 18.18 21.21 24.24]]
[[ 1.01 3.03 6.06 10.1 ]
[ 5.05 11.11 18.18 26.26]
[ 9.09 19.19 30.3 42.42]]
类似还有 mean 均值, var 方差, std 标准差, prod 乘积, cumprod 累积乘积, 不在一一举例
all 数组所有元素都判断为 True, 返回 True, 而 any 只要任何一个 True 返回 True
a = np.array([1, 2])
b = np.array([0, 0])
a.all(), b.all(), a.any(), b.any()
(True, False, True, False)
四, 创建或生成 ndarray 对象
1, 生成随机数
利用 numpy 生成随机数的模块 random, 创建数据来作例子非常有用. 先研究一下这个模块, 再研究通用函数
import numpy as np
# random 生成 0 ~ 1 之间的随机数
display(1 + np.random.random(3))
np.random.random((2,3)) * 10
array([1.33317465, 1.03829035, 1.19401465])
array([[2.13175522, 1.41162992, 9.67005144],
[0.03480687, 4.63431381, 5.88380759]])
# 同上, 传参方式不一样
display(np.random.rand(3))
np.random.rand(1, 3, 2)
array([0.60852454, 0.70296852, 0.31611002])
array([[[0.76136557, 0.14321424],
[0.01322461, 0.89676522],
[0.71400795, 0.00755323]]])
# 返回指定范围内的随机整数 [a,b) 包含 a 不包含 b, 不指定 b 则 [0,a)
display(np.random.randint(3, size=3))
np.random.randint(1, 4, (2, 3))
array([0, 2, 0])
array([[2, 2, 1],
[3, 2, 3]])
# 生成的随机数服从标准正态分布
display(np.random.randn(3))
np.random.randn(3, 2)
array([-1.46427887, 0.87862733, -0.98698207])
array([[ 0.51350851, -0.67478546],
[ 0.543379 , 0.63665877],
[-1.31200705, -0.30697352]])
类似还有拉普拉斯分布 laplace, 逻辑分布 logistic, 正态分布 normal 等, 可以设置分布的参数
np.random.logistic(5, 2, (2, 3))
array([[2.40647744, 5.62327677, 5.8642342 ],
[4.10741882, 7.65751053, 4.99483744]])
# 打乱原数组元素, 二维时只能打乱行的顺序
a = np.arange(5)
b = np.arange(9).reshape(3, 3)
np.random.shuffle(a)
np.random.shuffle(b)
a, b
(array([0, 2, 4, 1, 3]),
array([[3, 4, 5],
[6, 7, 8],
[0, 1, 2]]))
# 从一个一维数组里随机选择指定数量的元素,
# 或随机生成 0 到指定整数(不包含)的随机数
# replace=True 元素可以重复选择(默认)
display(np.random.choice(5, 5, replace=False))
np.random.choice(a, (2, 3))
array([4, 3, 1, 2, 0])
array([[3, 2, 2],
[3, 0, 0]])
2, 创建数组
array([[2.40647744, 5.62327677, 5.8642342 ],
[4.10741882, 7.65751053, 4.99483744]])
array([[1., 0., 0.],
[0., 1., 0.],
[0., 0., 1.]])
array([[1., 1., 1.],
[1., 1., 1.]])
array([[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.]])
array([[0., 0., 0.],
[0., 0., 0.]])
array([[3, 3, 3],
[3, 3, 3]])
array(<class 'list'>, dtype=object)
array([0, 1, 2])
array([1., 5., 9.])
array([ 10., 100., 1000.])
五, 常用的通函数及 API
numpy 调用, 用来操作 ndarray 对象的函数
1, 修改数组形状
reshape 与 ndarray 对象的方法一样, 另有 resize 只能按行展开
a = np.random.choice(5, (2, 3))
display(a)
np.reshape(a,(1, 6), 'F')
array([[0, 2, 2],
[4, 1, 2]])
array([[0, 4, 2, 1, 2, 2]])
array([[0, 2, 2, 4, 1, 2]])
ravel 展开为一维(ndarray 对象也有该方法)
array([0, 4, 2, 1, 2, 2])
2, 组合数组
concatenate, hstack, vstack 连接数组
# axis=0, 行方向连接(默认), axis=1, 列方向连接
b = np.random.choice(6, (2, 3))
np.concatenate((a, b), axis=1)
array([[0, 2, 2, 2, 5, 5],
[4, 1, 2, 2, 5, 2]])
np.hstack((a, b)) # 列方向连接
array([[0, 2, 2, 2, 5, 5],
[4, 1, 2, 2, 5, 2]])
np.vstack((a, b)) # 行方向连接
array([[0, 2, 2],
[4, 1, 2],
[2, 5, 5],
[2, 5, 2]])
stack 堆叠数组, 堆叠后维度增加 1
# axis=0, 行方向堆叠(默认), axis=1, 列方向堆叠
np.stack((a[0], b[0])), np.stack((a[0], b[0]), axis=1)
(array([[0, 2, 2],
[2, 5, 5]]),
array([[0, 2],
[2, 5],
[2, 5]]))
3, 拆分数组
split, hsplit, vsplit 将数组拆分, 返回为列表
np.split(a, 2), np.vsplit(a, 2), np.hsplit(a, 3)
([array([[0, 2, 2]]), array([[4, 1, 2]])],
[array([[0, 2, 2]]), array([[4, 1, 2]])],
[array([[0],
[4]]),
array([[2],
[1]]),
array([[2],
[2]])])
4, 添加, 删除元素
delete 删除指定索引的元素, 行或列
a, np.delete(a, 1) # 删除第 2 个元素
(array([[0, 2, 2],
[4, 1, 2]]),
array([0, 2, 4, 1, 2]))
# 删除第 2 行, 或第 2 列
np.delete(a, 1, axis=0), np.delete(a, 1, axis=1)
(array([[0, 2, 2]]),
array([[0, 2],
[4, 2]]))
insert 在指定索引前插入元素, 行或列
array([0, 9, 2, 2, 4, 1, 2])
np.insert(a, 1, 9, axis=0), np.insert(a, 1, [1, 2, 3], axis=0)
(array([[0, 2, 2],
[9, 9, 9],
[4, 1, 2]]),
array([[0, 2, 2],
[1, 2, 3],
[4, 1, 2]]))
np.insert(a, 1, [1, 3], axis=1)
array([[0, 1, 2, 2],
[4, 3, 1, 2]])
append 在数组末尾(或行,列末尾)加入元素, 行或列(维数必须相同)
array([0, 2, 2, 4, 1, 2, 2])
np.append(a, [[2, 2, 2]], axis=0), np.append(a, [[2], [2]], axis=1)
(array([[0, 2, 2],
[4, 1, 2],
[2, 2, 2]]),
array([[0, 2, 2, 2],
[4, 1, 2, 2]]))
unique 查找数组中的唯一值
a = np.random.choice(4, (2, 3))
display(a)
np.unique(a,return_index=True,
return_inverse=True,
return_counts=True, axis=None)
array([[3, 0, 3],
[1, 3, 1]])
(array([0, 1, 3]),
array([1, 3, 0], dtype=int64),
array([2, 0, 2, 1, 2, 1], dtype=int64),
array([1, 2, 3], dtype=int64))
5, 重新排列元素
flip, fliplr, flipud 翻转数组
np.flip(a), np.flip(a, 0), np.flip(a, 1)
(array([[1, 3, 1],
[3, 0, 3]]),
array([[1, 3, 1],
[3, 0, 3]]),
array([[3, 0, 3],
[1, 3, 1]]))
sort 排序数组元素
array([[0, 3, 3],
[1, 1, 3]])
roll 滚动数组元素
np.roll(a, 1), np.roll(a, 1, axis=0), np.roll(a, 1, axis=1)
(array([[1, 3, 0],
[3, 1, 3]]),
array([[1, 3, 1],
[3, 0, 3]]),
array([[3, 3, 0],
[1, 1, 3]]))
6, 常用数学运算函数
import numpy as np
# 固定随机种子
np.random.seed(0)
a = np.random.randint(1, 6, (3, 4))
b = np.arange(12).reshape((3, 4))
a, b
(array([[5, 1, 4, 4],
[4, 2, 4, 3],
[5, 1, 1, 5]]),
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]]))
array([[ 0, 1, 16, 81],
[ 256, 25, 1296, 343],
[ 32768, 9, 10, 161051]], dtype=int32)
(array([[ 0, 1, 0, 0],
[ 1, 2, 1, 2],
[ 1, 9, 10, 2]], dtype=int32),
array([[0, 0, 2, 3],
[0, 1, 2, 1],
[3, 0, 0, 1]], dtype=int32))
np.fabs(-a), np.abs(-a) # 取绝对值
(array([[5., 1., 4., 4.],
[4., 2., 4., 3.],
[5., 1., 1., 5.]]),
array([[5, 1, 4, 4],
[4, 2, 4, 3],
[5, 1, 1, 5]]))
c = np.random.rand(3, 4) * 10
display(c)
np.rint(c) # 舍入最接近的整数
array([[3.83441519, 7.91725038, 5.2889492 , 5.68044561],
[9.25596638, 0.71036058, 0.871293 , 0.20218397],
[8.32619846, 7.78156751, 8.70012148, 9.78618342]])
array([[ 4., 8., 5., 6.],
[ 9., 1., 1., 0.],
[ 8., 8., 9., 10.]])
np.exp(a).round(1) # np.e ** a
array([[148.4, 2.7, 54.6, 54.6],
[ 54.6, 7.4, 54.6, 20.1],
[148.4, 2.7, 2.7, 148.4]])
b = np.exp2(a) # 2**a
a, b
(array([[5, 1, 4, 4],
[4, 2, 4, 3],
[5, 1, 1, 5]]),
array([[32., 2., 16., 16.],
[16., 4., 16., 8.],
[32., 2., 2., 32.]]))
np.log(a)
np.log2(a)
np.log10(a)
np.sqrt(a) # 开根号
np.gcd(a, b.astype(int)) # 最大公约数
np.lcm(a, b.astype(int)) # 最小公倍数
array([[160, 2, 16, 16],
[ 16, 4, 16, 24],
[160, 2, 2, 160]])
np.sin(a)
np.cos(a)
np.tan(a)
array([[-3.38051501, 1.55740772, 1.15782128, 1.15782128],
[ 1.15782128, -2.18503986, 1.15782128, -0.14254654],
[-3.38051501, 1.55740772, 1.55740772, -3.38051501]])
np.greater(a, b) # a > b
np.less(a, b) # a < b
a != b # np.not_equal(a, b)
(a > 2) | ~(b < 5) # np.logical_or(a>2, ~(b<5))
array([[ True, False, True, True],
[ True, False, True, True],
[ True, False, False, True]])
# 两者取其大, 同 np.fmax(a*5, b) 对应还有 minimum, fmin
np.maximum(a*5, b)
array([[32., 5., 20., 20.],
[20., 10., 20., 15.],
[32., 5., 5., 32.]])
np.isinf(a) # 判断是否正负无穷大
np.isnan(a) # 判断是否 nan
array([[False, False, False, False],
[False, False, False, False],
[False, False, False, False]])
(array([[0.83441519, 0.91725038, 0.2889492 , 0.68044561],
[0.25596638, 0.71036058, 0.871293 , 0.20218397],
[0.32619846, 0.78156751, 0.70012148, 0.78618342]]),
array([[3., 7., 5., 5.],
[9., 0., 0., 0.],
[8., 7., 8., 9.]]))
array([[2., 0., 0., 0.],
[0., 0., 0., 2.],
[2., 0., 0., 2.]])
c = np.random.rand(3, 4) * 10
display(c)
np.floor(c) # 向下取整
np.ceil(c) # 向上取整
array([[7.99158564, 4.61479362, 7.80529176, 1.18274426],
[6.39921021, 1.43353287, 9.44668917, 5.21848322],
[4.1466194 , 2.64555612, 7.74233689, 4.56150332]])
array([[ 8., 5., 8., 2.],
[ 7., 2., 10., 6.],
[ 5., 3., 8., 5.]])
7, 字符串操作
字符串的操作函数, 和 python 的操作函数相似, 只是 numpy 作用到整个数组上
a = np.array([['a', 'b', 'c'], [20, 19, 18]])
np.char.add(a, a)
array([['aa', 'bb', 'cc'],
['2020', '1919', '1818']], dtype='<U22')
array([['aaa', 'bbb', 'ccc'],
['202020', '191919', '181818']], dtype='<U33')
# 将每一个制表符替换成指定个数的空格
b = np.array('\ta\t\tbc\td')
np.char.expandtabs(b, 1)
array(' a bc d', dtype='<U8')
np.char.replace(a, '1', '2') # 替换
array([['a', 'b', 'c'],
['20', '29', '28']], dtype='<U2')
array([['A', 'B', 'C'],
['20', '19', '18']], dtype='<U11')
b = np.char.zfill(a, 4)
b
array([['000a', '000b', '000c'],
['0020', '0019', '0018']], dtype='<U4')
np.char.count(b, '0') # 0 出现的次数
array([[3, 3, 3],
[3, 2, 2]])
np.char.str_len(a) # 字符串长度
array([[1, 1, 1],
[2, 2, 2]])
8, 索引与迭代
a = np.random.choice(4, (3, 4))
display(a)
np.nonzero(a) # 非 0 索引
array([[2, 0, 0, 0],
[1, 1, 2, 0],
[0, 1, 3, 0]])
(array([0, 1, 1, 1, 2, 2], dtype=int64),
array([0, 0, 1, 2, 1, 2], dtype=int64))
# 返回指定条件的元素的索引, 并可进行替换
# 不设条件, 和 nonzero 一样
np.where(a>0)
(array([0, 1, 1, 1, 2, 2], dtype=int64),
array([0, 0, 1, 2, 1, 2], dtype=int64))
# 大于 0 都换成 a, 否则都换成 b
np.where(a>0, 'a', 'b')
array([['a', 'b', 'b', 'b'],
['a', 'a', 'a', 'b'],
['b', 'a', 'a', 'b']], dtype='<U1')
np.nditer(a) # 将数组变成一个高效的迭代器
<numpy.nditer at 0x2b410944d50>
9, 统计运算函数
# 下述与 ndarray 对象直接调用的方法一样
a = np.random.rand(3, 4) * 5
np.all(a)
np.any(a)
np.max(a)
np.sum(a)
np.mean(a)
np.cumsum(a)
np.var(a)
np.std(a)
np.clip(a, 1, 3)
np.around(a, 2) # ndarray 对象的方法是 round
# 等等
array([[3.41, 1.8 , 2.19, 3.49],
[0.3 , 3.33, 3.35, 1.05],
[0.64, 1.58, 1.82, 2.85]])
任何数与 nan 计算都是 nan, 可以用相应的函数排除 nan
b = np.array([1, np.nan, 3, 4])
np.sum(b), np.nansum(b)
(nan, 8.0)
array([1., 1., 4., 8.])
Pandas 基础快速了解和查询
官方文档
Pandas 是 Python 的核心数据分析支持库, 基于 NumPy 创建, 它使 python 成为强大而高效的数据分析环境
一, 数据结构
pandas 的数据结构是基于 numpy 的, 因此其有着 numpy 的基因, 许多操作和大部分函数与 numpy 类似
pandas 的数据结构其实是 numpy 数组数据对象 array 的容器, 在 pandas 中有了索引结构, 可以利用索引来取 array 或 array 中的元素
pandas 中最重要的数据结构是 Series 和 DataFrame
1, 创建 Series 和 DataFrame
创建时若不指定索引, 将自动生成(从 0 开始)
Series 是一维, 只有行索引, 而 DataFrame 有行和列索引
创建方式灵活多变, 可以查看参数, 根据要求传参进行创建
import numpy as np
import pandas as pd
s = pd.Series(range(3))
s
0 0
1 1
2 2
dtype: int64
df = pd.DataFrame(range(3))
print(df)
0
0 0
1 1
2 2
array([0, 1, 2], dtype=int64)
array([[0],
[1],
[2]], dtype=int64)
可见,pandas 的数据结构,是 numpy 的 array 对象的容器,着意味着 pandas 在处理数据时,可以使用 numpy 的所有函数和方法
s = pd.Series(range(3), index=list('abc'))
s
a 0
b 1
c 2
dtype: int64
df = pd.DataFrame({'A': range(3), 'B': list('jkl')}, index=list('abc'))
print(df)
A B
a 0 j
b 1 k
c 2 l
dict_1 = {'a': 0, 'b': 1, 'c': 2}
dict_2 = {'a': range(3), 'b': list('jkl')}
s = pd.Series(dict_1)
s
a 0
b 1
c 2
dtype: int64
df1 = pd.DataFrame(dict_1, index=['A'])
print(df1)
a b c
A 0 1 2
df2 = pd.DataFrame(dict_2)
print(df2)
a b
0 0 j
1 1 k
2 2 l
# df2 每一列是一个类型的 array 对象
df2["a"].values, df2["b"].values
(array([0, 1, 2], dtype=int64), array(['j', 'k', 'l'], dtype=object))
2, Series 和 DataFrame 的常用属性
大部分属性和 numpy 一样
s = pd.Series(1, index=list('abc'))
s
a 1
b 1
c 1
dtype: int64
dict_3 = {'a': range(3), 'b': list('jkl')}
df = pd.DataFrame(dict_3)
print(df)
a b
0 0 j
1 1 k
2 2 l
# 查看形状, 形状不包括 索引
s.shape, df.shape
((3,), (3, 2))
# 获取索引
s.index, df.index, df.columns
(Index(['a', 'b', 'c'], dtype='object'),
RangeIndex(start=0, stop=3, step=1),
Index(['a', 'b'], dtype='object'))
(3, 6)
# 查看数据类型
s.dtype, df.dtypes
(dtype('int64'),
a int64
b object
dtype: object)
# 查看值
s.values, df.values, df['a'].values
(array([1, 1, 1], dtype=int64),
array([[0, 'j'],
[1, 'k'],
[2, 'l']], dtype=object),
array([0, 1, 2], dtype=int64))
3, Series 和 DataFrame 结构理解
从上述可以看出, pandas 数据结构的值, 是一个 array 对象. 对于 df, 每一列的值取出来也是一个 array 对象,
并且每一列可以是不同的数据类型
需要注意的是, DataFrame 每一列取出来, 整体是一个 Series , 因此 DataFrame 又可以看成 Series 的容器
s = pd.Series(1, index=list('abc'))
dict_3 = {'a': range(3), 'b': list('jkl')}
df = pd.DataFrame(dict_3)
type(s), type(df['a']), type(df)
(pandas.core.series.Series,
pandas.core.series.Series,
pandas.core.frame.DataFrame)
Series 和 DataFrame 可以有多层索引
s.index = [['a', 'b', 'c'], [1, 2, 3]]
s
a 1 1
b 2 1
c 3 1
dtype: int64
df.index = [['a', 'b', 'c'], [1, 2, 3]]
print(df)
a b
a 1 0 j
b 2 1 k
c 3 2 l
二, 数据结构操作, 处理与计算
1, 查看 Series 和 DataFrame 数据信息常用方法
a = np.random.randint(1, 9, (6, 2))
s = pd.Series(a[:, 0])
s
0 4
1 7
2 4
3 8
4 1
5 8
dtype: int32
df = pd.DataFrame(a)
print(df)
0 1
0 4 4
1 7 4
2 4 8
3 8 3
4 1 3
5 8 1
# 查看前 5 行(默认)
s.head()
print(df.head())
0 1
0 4 4
1 7 4
2 4 8
3 8 3
4 1 3
# 查看后 5 行(默认)
s.tail()
print(df.tail())
0 1
1 7 4
2 4 8
3 8 3
4 1 3
5 8 1
# 查看详情, Series 没有该方法
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 6 entries, 0 to 5
Data columns (total 2 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 0 6 non-null int32
1 1 6 non-null int32
dtypes: int32(2)
memory usage: 176.0 bytes
# 查看统计量
s.describe()
print(df.describe())
0 1
count 6.000000 6.000000
mean 5.333333 3.833333
std 2.804758 2.316607
min 1.000000 1.000000
25% 4.000000 3.000000
50% 5.500000 3.500000
75% 7.750000 4.000000
max 8.000000 8.000000
2, 数据的直接计算
Series 和 DataFrame 的数据可以像 numpy 的数组一样直接进行计算, 索引相同的行、列进行计算, 索引不同的 NaN 填充
a = np.random.randint(1, 9, (6, 2))
s = pd.Series(a[:, 0])
df = pd.DataFrame(a)
print(df)
0 1
0 6 2
1 1 6
2 5 1
3 3 2
4 5 5
5 2 6
df1 = pd.DataFrame(a, columns=[1, 'b'])
print(df1 * df)
0 1 b
0 NaN 12 NaN
1 NaN 6 NaN
2 NaN 5 NaN
3 NaN 6 NaN
4 NaN 25 NaN
5 NaN 12 NaN
0 6
1 1
2 5
3 3
4 5
5 2
dtype: int32
s1 = pd.Series(a[:, 0], index=['a', 1, 2, 3, 4, 5])
s1
a 6
1 1
2 5
3 3
4 5
5 2
dtype: int32
0 NaN
1 2.0
2 30.0
3 12.0
4 30.0
5 6.0
a NaN
dtype: float64
0 1 2 3 4 5
0 12 3 NaN NaN NaN NaN
1 7 7 NaN NaN NaN NaN
2 11 2 NaN NaN NaN NaN
3 9 3 NaN NaN NaN NaN
4 11 6 NaN NaN NaN NaN
5 8 7 NaN NaN NaN NaN
3, Series 和 DataFrame 下标和索引切片操作
3.01, Series 和 DataFrame 下标切片取值
Series 和 DataFrame 下标切片取值有许多不同之处
s1 = pd.Series(a[:, 0], index=['a', 1, 2, 3, 4, 5])
s1
a 6
1 1
2 5
3 3
4 5
5 2
dtype: int32
a = np.random.randint(1, 9, (6, 2))
df1 = pd.DataFrame(a, columns=[1, 'b'])
print(df1)
1 b
0 6 5
1 3 8
2 8 7
3 1 5
4 7 4
5 6 3
# 此方法 DataFrame 只能取行, 不能取列
s1[1:3]
1 1
2 5
dtype: int32
1 b
0 6 5
1 3 8
1 1
4 5
dtype: int32
同 numpy 一样, 可以条件取值, 此方法常用来筛选和重新赋值等
a 6
2 5
4 5
dtype: int32
1 b
0 6.0 NaN
1 NaN 8.0
2 8.0 7.0
3 NaN NaN
4 7.0 NaN
5 6.0 NaN
1 b
0 NaN NaN
1 NaN NaN
2 NaN NaN
3 NaN NaN
4 NaN 4.0
5 NaN NaN
3.02, Series 和 DataFrame 索引切片取值
索引切片取值, 左右都包含
s1 = pd.Series(a[:, 0], index=['a', 1, 2, 3, 4, 5])
s1
a 6
1 3
2 8
3 1
4 7
5 6
dtype: int32
a = np.random.randint(1, 9, (6, 2))
df1 = pd.DataFrame(a, columns=[1, 'b'])
print(df1)
1 b
0 3 5
1 7 3
2 8 7
3 7 6
4 3 7
5 5 4
# 需要将索引转换类型
s1.index = s1.index.astype(str)
df1.index = df1.index.astype(str)
a 6
1 3
2 8
dtype: int32
1 b
1 7 3
2 8 7
3 7 6
1
1 7
2 8
3 7
0 5
1 3
2 7
3 6
4 7
5 4
Name: b, dtype: int32
b 1
0 5 3
1 3 7
2 7 8
3 6 7
4 7 3
5 4 5
3.03, 用 iloc 方法取值(推荐)
上述的取值太复杂麻烦, iloc 方法传入下标取值更方便且条理清晰. 存在多层索引时, iloc 按最内层索引取值
s1 = pd.Series(a[:, 0], index=['a', 1, 2, 3, 4, 5])
s1
a 3
1 7
2 8
3 7
4 3
5 5
dtype: int32
a = np.random.randint(1, 9, (6, 2))
df1 = pd.DataFrame(a, columns=[1, 'b'])
print(df1)
1 b
0 8 8
1 6 2
2 1 6
3 6 3
4 8 3
5 3 8
a 3
1 7
2 8
dtype: int32
1 b
0 8 8
2 1 6
4 8 3
print(df1.iloc[[1, 4, 2], 0:])
1 b
1 6 2
4 8 3
2 1 6
3.04, 用 loc 方法
loc 方法取值左右都包含, 传入索引标签取值
s1 = pd.Series(a[:, 0], index=['a', 1, 2, 3, 4, 5])
s1
a 8
1 6
2 1
3 6
4 8
5 3
dtype: int32
a = np.random.randint(1, 9, (6, 2))
df1 = pd.DataFrame(a, columns=[1, 'b'])
print(df1)
1 b
0 4 3
1 6 4
2 1 6
3 6 2
4 6 1
5 2 7
# 需要将索引转换类型
s1.index = s1.index.astype(str)
df1.index = df1.index.astype(str)
a 8
1 6
2 1
dtype: int32
1 4
2 6
3 2
Name: b, dtype: int32
3.05, 利用切片修改数据
利用切片修改数据, 其实就是取值重新赋值
import numpy as np
import pandas as pd
df = pd.DataFrame(np.random.rand(5, 4),
index=list('abcde'),
columns=list('ABCD'))
print(df[df>0.5])
A B C D
a 0.908869 NaN NaN 0.584259
b NaN NaN 0.940409 NaN
c 0.947715 0.898426 0.745999 NaN
d NaN 0.855820 NaN 0.742919
e NaN NaN NaN 0.593532
# 将大于 0.5 的数据换成 nan
df[df>0.5] = np.nan
print(df)
A B C D
a NaN 0.019952 0.004443 NaN
b 0.391594 0.406059 NaN 0.021655
c NaN NaN NaN 0.067905
d 0.238486 NaN 0.232429 NaN
e 0.378054 0.422029 0.386046 NaN
# 将 A 列全部改成 1 , 增加一列 E, 值为 0
df['A'] = 1 # 与 df.A = 1 等价
df['E'] = 0
print(df)
A B C D E
a 1 0.019952 0.004443 NaN 0
b 1 0.406059 NaN 0.021655 0
c 1 NaN NaN 0.067905 0
d 1 NaN 0.232429 NaN 0
e 1 0.422029 0.386046 NaN 0
# 行列值互换
df.loc[['a', 'c'], ['A', 'C']] = df.loc[['c', 'a'], ['C', 'A']].to_numpy()
print(df)
A B C D E
a NaN 0.019952 1.000000 NaN 0
b 1.000000 0.406059 NaN 0.021655 0
c 0.004443 NaN 1.000000 0.067905 0
d 1.000000 NaN 0.232429 NaN 0
e 1.000000 0.422029 0.386046 NaN 0
# 只要 B 不为 nan 的数据
print(df[df['B'] == df['B']])
A B C D E
a NaN 0.019952 1.000000 NaN 0
b 1.0 0.406059 NaN 0.021655 0
e 1.0 0.422029 0.386046 NaN 0
三, Series 和 DataFrame 的处理和计算函数
1, 索引
在 pandas 里, 索引非常重要, 一个行索引, 通常就是数据的一条记录(例如一个人的信息), 一个列索引就是数据的一个特征(例如某个人的性别, 年龄等),
通过索引能够更方便数据处理与计算
import numpy as np
import pandas as pd
s = pd.Series(np.random.rand(4), index=list('abcd'))
s
a 0.930171
b 0.263544
c 0.684870
d 0.702820
dtype: float64
df = pd.DataFrame(np.random.rand(4, 4),
index=list('abcd'),
columns=list('ABCD'))
print(df)
A B C D
a 0.236401 0.972351 0.030862 0.438897
b 0.774498 0.670181 0.379171 0.941319
c 0.701424 0.732319 0.882208 0.527572
d 0.442419 0.597335 0.258880 0.420447
reindex 索引重排, 新增的索引 nan 填充, 缺少索引的数据舍弃
# 新增 e 舍弃 d, 有许多参数可以调节
s1 = s.reindex(list('bcae'))
s1
b 0.263544
c 0.684870
a 0.930171
e NaN
dtype: float64
df2 = df.reindex(columns=list('BCAE'))
print(df2)
B C A E
a 0.972351 0.030862 0.236401 NaN
b 0.670181 0.379171 0.774498 NaN
c 0.732319 0.882208 0.701424 NaN
d 0.597335 0.258880 0.442419 NaN
sort_index 索引排序
# 默认 True 升序, 许多参数可调, DataFrame 只排行索引
s2 = s.sort_index(ascending=False)
s2
d 0.702820
c 0.684870
b 0.263544
a 0.930171
dtype: float64
df2 = df.sort_index(ascending=False)
print(df2)
A B C D
d 0.442419 0.597335 0.258880 0.420447
c 0.701424 0.732319 0.882208 0.527572
b 0.774498 0.670181 0.379171 0.941319
a 0.236401 0.972351 0.030862 0.438897
rename, set_index, reset_index 设置索引
# 注意参数及传参方式
s3 = s.rename(index={'a': 'f'})
s3
f 0.930171
b 0.263544
c 0.684870
d 0.702820
dtype: float64
df3 = df.rename(lambda x: x + x)
print(df3)
A B C D
aa 0.236401 0.972351 0.030862 0.438897
bb 0.774498 0.670181 0.379171 0.941319
cc 0.701424 0.732319 0.882208 0.527572
dd 0.442419 0.597335 0.258880 0.420447
# Series 没有此方法
df4 = df.set_index([['a','b','c','d'], [1, 2, 3, 4]])
print(df4)
A B C D
a 1 0.236401 0.972351 0.030862 0.438897
b 2 0.774498 0.670181 0.379171 0.941319
c 3 0.701424 0.732319 0.882208 0.527572
d 4 0.442419 0.597335 0.258880 0.420447
# 可以将某列设为索引, 默认不保留原列, 可设参数 drop 保留
df5 = df.set_index('A')
print(df5)
B C D
A
0.236401 0.972351 0.030862 0.438897
0.774498 0.670181 0.379171 0.941319
0.701424 0.732319 0.882208 0.527572
0.442419 0.597335 0.258880 0.420447
# 将索引设为列(drop=True将其删除), 多层索引时可选某层
df6 = df4.reset_index(level=1)
print(df6)
level_1 A B C D
a 1 0.236401 0.972351 0.030862 0.438897
b 2 0.774498 0.670181 0.379171 0.941319
c 3 0.701424 0.732319 0.882208 0.527572
d 4 0.442419 0.597335 0.258880 0.420447
df7 = df5.reset_index()
print(df7)
A B C D
0 0.236401 0.972351 0.030862 0.438897
1 0.774498 0.670181 0.379171 0.941319
2 0.701424 0.732319 0.882208 0.527572
3 0.442419 0.597335 0.258880 0.420447
# 索引可以设置名字, 设置索引还可以通过获取索引重新赋值
# DataFrame 还可以转置
df5.index.name
'A'
df5.index = list('abcd') # 重设索引后, 索引名消失
print(df5)
B C D
a 0.972351 0.030862 0.438897
b 0.670181 0.379171 0.941319
c 0.732319 0.882208 0.527572
d 0.597335 0.258880 0.420447
a b c d
B 0.972351 0.670181 0.732319 0.597335
C 0.030862 0.379171 0.882208 0.258880
D 0.438897 0.941319 0.527572 0.420447
多层索引
ar = [['a', 'b', 'c'], [1, 2, 3]]
tup = list(zip(*ar))
pd.MultiIndex.from_tuples(tup)
MultiIndex([('a', 1),
('b', 2),
('c', 3)],
)
index = pd.MultiIndex.from_product(ar)
s = pd.Series(range(9), index=index)
s
a 1 0
2 1
3 2
b 1 3
2 4
3 5
c 1 6
2 7
3 8
dtype: int64
pd.MultiIndex.from_frame(pd.DataFrame(np.random.randint(1, 9, (2, 3))))
MultiIndex([(6, 5, 3),
(8, 1, 3)],
names=[0, 1, 2])
2, 增删与合并数据
import numpy as np
import pandas as pd
df = pd.DataFrame(np.random.randint(1, 9, (4, 5)))
print(df)
0 1 2 3 4
0 4 8 5 3 7
1 7 3 5 2 4
2 3 2 5 7 2
3 1 2 4 8 7
insert 在指定位置前插入数据
# Series 没有该方法
df.insert(2, 'A', 1)
print(df)
0 1 A 2 3 4
0 4 8 1 5 3 7
1 7 3 1 5 2 4
2 3 2 1 5 7 2
3 1 2 1 4 8 7
append 在数据最后增加数据
df1 = pd.DataFrame(np.random.rand(4, 4),
index=list('abcd'),
columns=list('ABCD'))
print(df1)
A B C D
a 0.748362 0.049567 0.603201 0.383037
b 0.658722 0.225040 0.107199 0.416646
c 0.024967 0.161487 0.338823 0.889825
d 0.584625 0.582264 0.228898 0.817555
# Series 只能传 Series
df.iloc[0].append(pd.Series(8))
0 4
1 8
A 1
2 5
3 3
4 7
0 8
dtype: int64
# DataFrame 可以在行后面增加, 也可在列后面新增
print(df.append([1]))
0 1 A 2 3 4
0 4 8.0 1.0 5.0 3.0 7.0
1 7 3.0 1.0 5.0 2.0 4.0
2 3 2.0 1.0 5.0 7.0 2.0
3 1 2.0 1.0 4.0 8.0 7.0
0 1 NaN NaN NaN NaN NaN
print(df.append(df1, ignore_index=True))
0 1 A 2 3 4 B C D
0 4.0 8.0 1.000000 5.0 3.0 7.0 NaN NaN NaN
1 7.0 3.0 1.000000 5.0 2.0 4.0 NaN NaN NaN
2 3.0 2.0 1.000000 5.0 7.0 2.0 NaN NaN NaN
3 1.0 2.0 1.000000 4.0 8.0 7.0 NaN NaN NaN
4 NaN NaN 0.748362 NaN NaN NaN 0.049567 0.603201 0.383037
5 NaN NaN 0.658722 NaN NaN NaN 0.225040 0.107199 0.416646
6 NaN NaN 0.024967 NaN NaN NaN 0.161487 0.338823 0.889825
7 NaN NaN 0.584625 NaN NaN NaN 0.582264 0.228898 0.817555
drop 删除指定数据
df['A'].drop(1), df.drop(1, axis=1)
(0 1
2 1
3 1
Name: A, dtype: int64,
0 A 2 3 4
0 4 1 5 3 7
1 7 1 5 2 4
2 3 1 5 7 2
3 1 1 4 8 7)
concat 主要用于行索引的合并
0 1 A 2 3 4
0 4 8 1 5 3 7
1 7 3 1 5 2 4
2 3 2 1 5 7 2
3 1 2 1 4 8 7
A B C D
a 0.748362 0.049567 0.603201 0.383037
b 0.658722 0.225040 0.107199 0.416646
c 0.024967 0.161487 0.338823 0.889825
d 0.584625 0.582264 0.228898 0.817555
# 默认按列索引合并, 保留合并后的全部索引, 缺失用 nan 填充
# join 参数可控制合并的方式: inner 只留下都有的索引
print(pd.concat([df, df1]))
0 1 A 2 3 4 B C D
0 4.0 8.0 1.000000 5.0 3.0 7.0 NaN NaN NaN
1 7.0 3.0 1.000000 5.0 2.0 4.0 NaN NaN NaN
2 3.0 2.0 1.000000 5.0 7.0 2.0 NaN NaN NaN
3 1.0 2.0 1.000000 4.0 8.0 7.0 NaN NaN NaN
a NaN NaN 0.748362 NaN NaN NaN 0.049567 0.603201 0.383037
b NaN NaN 0.658722 NaN NaN NaN 0.225040 0.107199 0.416646
c NaN NaN 0.024967 NaN NaN NaN 0.161487 0.338823 0.889825
d NaN NaN 0.584625 NaN NaN NaN 0.582264 0.228898 0.817555
df2 = pd.DataFrame(np.random.rand(5, 4),
index=list('abcde'),
columns=list('BCDE'))
print(df2)
B C D E
a 0.061604 0.505149 0.247496 0.474912
b 0.431163 0.828570 0.302236 0.695153
c 0.238852 0.998520 0.516351 0.665134
d 0.383425 0.684282 0.857654 0.621860
e 0.186235 0.006509 0.612608 0.875893
print(pd.concat([df1, df2], join='inner'))
B C D
a 0.049567 0.603201 0.383037
b 0.225040 0.107199 0.416646
c 0.161487 0.338823 0.889825
d 0.582264 0.228898 0.817555
a 0.061604 0.505149 0.247496
b 0.431163 0.828570 0.302236
c 0.238852 0.998520 0.516351
d 0.383425 0.684282 0.857654
e 0.186235 0.006509 0.612608
join 主要用于行索引的合并
# 默认以左边(df1)为基准, 相同列名需要加以区分
# how 参数: outer 全保留, inner 只保留共同部分
print(df1.join(df2, lsuffix='_1', rsuffix='_2'))
A B_1 C_1 D_1 B_2 C_2 D_2 \
a 0.748362 0.049567 0.603201 0.383037 0.061604 0.505149 0.247496
b 0.658722 0.225040 0.107199 0.416646 0.431163 0.828570 0.302236
c 0.024967 0.161487 0.338823 0.889825 0.238852 0.998520 0.516351
d 0.584625 0.582264 0.228898 0.817555 0.383425 0.684282 0.857654
E
a 0.474912
b 0.695153
c 0.665134
d 0.621860
print(df1.join(df2, how='outer', lsuffix='_1', rsuffix='_2'))
A B_1 C_1 D_1 B_2 C_2 D_2 \
a 0.748362 0.049567 0.603201 0.383037 0.061604 0.505149 0.247496
b 0.658722 0.225040 0.107199 0.416646 0.431163 0.828570 0.302236
c 0.024967 0.161487 0.338823 0.889825 0.238852 0.998520 0.516351
d 0.584625 0.582264 0.228898 0.817555 0.383425 0.684282 0.857654
e NaN NaN NaN NaN 0.186235 0.006509 0.612608
E
a 0.474912
b 0.695153
c 0.665134
d 0.621860
e 0.875893
merge 合并
df1 = pd.DataFrame({'K': ['K0', 'K1', 'K2', 'K3'],
'A': ['A0', 'A1', 'M2', 'M3'],
'B': ['B0', 'B1', 'B2', 'B3']})
print(df1)
K A B
0 K0 A0 B0
1 K1 A1 B1
2 K2 M2 B2
3 K3 M3 B3
df2 = pd.DataFrame({'K': ['K0', 'K1', 'K4', 'K5'],
'C': ['C0', 'C1', 'M2', 'M3'],
'D': ['D0', 'D1', 'D2', 'D3']})
print(df2)
K C D
0 K0 C0 D0
1 K1 C1 D1
2 K4 M2 D2
3 K5 M3 D3
# 默认 inner, 全部列都保留,
# 但只保留两者都有的列(K),且列内容相同(K0,K1)的 行, 行索引都舍弃
print(pd.merge(df1, df2))
K A B C D
0 K0 A0 B0 C0 D0
1 K1 A1 B1 C1 D1
# 可以选择以某一个为基准, 需要合并的列内容, 以及保留某一个的索引
print(pd.merge(df1, df2, how='left')) # 以 df1 为准, 匹配不上的保留 df1
K A B C D
0 K0 A0 B0 C0 D0
1 K1 A1 B1 C1 D1
2 K2 M2 B2 NaN NaN
3 K3 M3 B3 NaN NaN
# df1 的 A 列与 df2 的 C 列有内容相同需要合并
print(pd.merge(df1, df2, left_on='A', right_on='C'))
K_x A B K_y C D
0 K2 M2 B2 K4 M2 D2
1 K3 M3 B3 K5 M3 D3
3, 数据选择与处理
import numpy as np
import pandas as pd
np.random.seed(0)
df = pd.DataFrame(np.random.randint(1, 9, (6, 6)),
index=list('abcdef'),
columns=list('ABCDEF'))
print(df)
A B C D E F
a 5 8 6 1 4 4
b 4 8 2 4 6 3
c 5 8 7 1 1 5
d 3 2 7 8 8 7
e 1 2 6 2 6 1
f 2 5 4 1 4 6
sample 随机选择数据
# 可以指定数量, 也可以按比例选
df.iloc[0].sample(2) # Series 也可
E 4
B 8
Name: a, dtype: int32
A B C D E F
a 5 8 6 1 4 4
b 4 8 2 4 6 3
c 5 8 7 1 1 5
print(df.sample(frac=0.5, axis=1))
F C D
a 4 6 1
b 3 2 4
c 5 7 1
d 7 7 8
e 1 6 2
f 6 4 1
where 按条件选择数据, 且可替换, 替换的是条件之外的数据
df.A.where(df.A>3) # Series 也可
a 5.0
b 4.0
c 5.0
d NaN
e NaN
f NaN
Name: A, dtype: float64
df1 = df.where(df>1)
print(df1)
A B C D E F
a 5.0 8 6 NaN 4.0 4.0
b 4.0 8 2 4.0 6.0 3.0
c 5.0 8 7 NaN NaN 5.0
d 3.0 2 7 8.0 8.0 7.0
e NaN 2 6 2.0 6.0 NaN
f 2.0 5 4 NaN 4.0 6.0
print(df.where(df==1, lambda x: x*x))
A B C D E F
a 25 64 36 1 16 16
b 16 64 4 16 36 9
c 25 64 49 1 1 25
d 9 4 49 64 64 49
e 1 4 36 4 36 1
f 4 25 16 1 16 36
isin 生成布尔数组来选择数据
values = [3, 5, 7, 'a', 'c']
df.C.isin(values)
a False
b False
c True
d True
e False
f False
Name: C, dtype: bool
c 5
d 3
Name: A, dtype: int32
print(df[df.isin(values)])
A B C D E F
a 5.0 NaN NaN NaN NaN NaN
b NaN NaN NaN NaN NaN 3.0
c 5.0 NaN 7.0 NaN NaN 5.0
d 3.0 NaN 7.0 NaN NaN 7.0
e NaN NaN NaN NaN NaN NaN
f NaN 5.0 NaN NaN NaN NaN
print(df[df.index.isin(values)])
A B C D E F
a 5 8 6 1 4 4
c 5 8 7 1 1 5
isna( isnull ) 和 notna ( notnull ) 生成布尔数组
# 可以 pd 调用, 也可 DataFrame 或 Series 调用
pd.isna(df1.D)
a True
b False
c True
d False
e False
f True
Name: D, dtype: bool
a True
b False
c True
d False
e False
f True
Name: D, dtype: bool
A B C D E F
a False False False True False False
b False False False False False False
c False False False True True False
d False False False False False False
e True False False False False True
f False False False True False False
dropna 删除 nan, fillna 将 nan 填充
A B C D E F
a 5.0 8 6 NaN 4.0 4.0
b 4.0 8 2 4.0 6.0 3.0
c 5.0 8 7 NaN NaN 5.0
d 3.0 2 7 8.0 8.0 7.0
e NaN 2 6 2.0 6.0 NaN
f 2.0 5 4 NaN 4.0 6.0
# 默认有 nan 的行就删除, 参数 all: 全部 nan 才删除
df1.A.dropna() # Series 也可
a 5.0
b 4.0
c 5.0
d 3.0
f 2.0
Name: A, dtype: float64
A B C D E F
b 4.0 8 2 4.0 6.0 3.0
d 3.0 2 7 8.0 8.0 7.0
print(df1.dropna(axis=1, how='all'))
A B C D E F
a 5.0 8 6 NaN 4.0 4.0
b 4.0 8 2 4.0 6.0 3.0
c 5.0 8 7 NaN NaN 5.0
d 3.0 2 7 8.0 8.0 7.0
e NaN 2 6 2.0 6.0 NaN
f 2.0 5 4 NaN 4.0 6.0
# 给定填充值, 默认全部填充,
# 可以指定填充数, 填充方式
df1.A.fillna(0) # Series 也可
a 5.0
b 4.0
c 5.0
d 3.0
e 0.0
f 2.0
Name: A, dtype: float64
A B C D E F
a 5.0 8 6 0.0 4.0 4.0
b 4.0 8 2 4.0 6.0 3.0
c 5.0 8 7 0.0 0.0 5.0
d 3.0 2 7 8.0 8.0 7.0
e 0.0 2 6 2.0 6.0 0.0
f 2.0 5 4 0.0 4.0 6.0
print(df1.fillna(0, limit=1)) # 每一列填充一个
A B C D E F
a 5.0 8 6 0.0 4.0 4.0
b 4.0 8 2 4.0 6.0 3.0
c 5.0 8 7 NaN 0.0 5.0
d 3.0 2 7 8.0 8.0 7.0
e 0.0 2 6 2.0 6.0 0.0
f 2.0 5 4 NaN 4.0 6.0
# ffill 前面值填充, bfill 后面值填充
print(df1.fillna(method='ffill'))
A B C D E F
a 5.0 8 6 NaN 4.0 4.0
b 4.0 8 2 4.0 6.0 3.0
c 5.0 8 7 4.0 6.0 5.0
d 3.0 2 7 8.0 8.0 7.0
e 3.0 2 6 2.0 6.0 7.0
f 2.0 5 4 2.0 4.0 6.0
print(df1.fillna(method='bfill'))
A B C D E F
a 5.0 8 6 4.0 4.0 4.0
b 4.0 8 2 4.0 6.0 3.0
c 5.0 8 7 8.0 8.0 5.0
d 3.0 2 7 8.0 8.0 7.0
e 2.0 2 6 2.0 6.0 6.0
f 2.0 5 4 NaN 4.0 6.0
drop_duplicates 去重
df.iloc[0] = df.iloc[1]
print(df)
A B C D E F
a 4 8 2 4 6 3
b 4 8 2 4 6 3
c 5 8 7 1 1 5
d 3 2 7 8 8 7
e 1 2 6 2 6 1
f 2 5 4 1 4 6
# 默认保留第 1 条数据
df.A.drop_duplicates() # Series 也可
a 4
c 5
d 3
e 1
f 2
Name: A, dtype: int32
print(df.drop_duplicates())
A B C D E F
a 4 8 2 4 6 3
c 5 8 7 1 1 5
d 3 2 7 8 8 7
e 1 2 6 2 6 1
f 2 5 4 1 4 6
nlargest 选择某列(某几列)值最大的几条数据, 对应还有 nsmallest
print(df.nlargest(3, 'A'))
A B C D E F
c 5 8 7 1 1 5
a 4 8 2 4 6 3
b 4 8 2 4 6 3
print(df.nsmallest(3, 'A'))
A B C D E F
e 1 2 6 2 6 1
f 2 5 4 1 4 6
d 3 2 7 8 8 7
print(df.nlargest(3, ['C', 'D']))
A B C D E F
d 3 2 7 8 8 7
c 5 8 7 1 1 5
e 1 2 6 2 6 1
filter 按索引查找数据, 可正则模糊查找
print(df.filter(['A', 'B']))
A B
a 4 8
b 4 8
c 5 8
d 3 2
e 1 2
f 2 5
print(df.filter(like='a', axis=0))
A B C D E F
a 4 8 2 4 6 3
print(df.filter(regex='c', axis=0))
A B C D E F
c 5 8 7 1 1 5
assign 用于新增辅助列
print(df.assign(A1=df['A']/df['F']))
A B C D E F A1
a 4 8 2 4 6 3 1.333333
b 4 8 2 4 6 3 1.333333
c 5 8 7 1 1 5 1.000000
d 3 2 7 8 8 7 0.428571
e 1 2 6 2 6 1 1.000000
f 2 5 4 1 4 6 0.333333
print(df.assign(A1=lambda x:x.A/x.F))
A B C D E F A1
a 4 8 2 4 6 3 1.333333
b 4 8 2 4 6 3 1.333333
c 5 8 7 1 1 5 1.000000
d 3 2 7 8 8 7 0.428571
e 1 2 6 2 6 1 1.000000
f 2 5 4 1 4 6 0.333333
clip 将过大或过小的数据去掉, 并填充指定值
# 小于指定值的填充为指定值小者, 大于的反之
print(df.clip(2, 6))
A B C D E F
a 4 6 2 4 6 3
b 4 6 2 4 6 3
c 5 6 6 2 2 5
d 3 2 6 6 6 6
e 2 2 6 2 6 2
f 2 5 4 2 4 6
print(df.clip(df.A, df.A + 2, axis=0))
A B C D E F
a 4 6 4 4 6 4
b 4 6 4 4 6 4
c 5 7 7 5 5 5
d 3 3 5 5 5 5
e 1 2 3 2 3 1
f 2 4 4 2 4 4
4, 数据分组聚合计算
聚合计算和 numpy 函数基本一样, 例如 sum, count, median, min, max, mean, var, std 等, 比较容易
更为重要的, 是将数据按需分组后再聚合运算
import numpy as np
import pandas as pd
np.random.seed(0)
df = pd.DataFrame(np.random.randint(1, 9, (6, 6)),
index=list('abcdef'),
columns=list('ABCDEF'))
df.A.where(df.A>3, 'M', inplace=True)
df.A.where(df.A=='M', 'N', inplace=True)
df.B.where(df.B>3, 'J', inplace=True)
df.B.where(df.B=='J', 'K', inplace=True)
df.C.where(df.C<5, np.nan, inplace=True)
print(df)
A B C D E F
a N K NaN 1 4 4
b N K 2.0 4 6 3
c N K NaN 1 1 5
d M J NaN 8 8 7
e M J NaN 2 6 1
f M K 4.0 1 4 6
# 默认按列加和, 将 nan 转换为 0 来计算
df.sum()
A NNNMMM
B KKKJJK
C 6.0
D 17
E 29
F 26
dtype: object
df.sum(axis=1, numeric_only=True)
a 9.0
b 15.0
c 7.0
d 23.0
e 9.0
f 15.0
dtype: float64
# 按行统计忽略 nan
df.count(axis=1)
a 5
b 6
c 5
d 5
e 5
f 6
dtype: int64
print(df.set_index(["A", "B"]).groupby(level="A").count())
C D E F
A
M 1 3 3 3
N 1 3 3 3
print(df.set_index(["A", "B"]).groupby(level="B").count())
C D E F
B
J 0 2 2 2
K 2 4 4 4
value_counts 统计 Series 中每个值出现次数
# Series 的值统计, 也即是 DataFrame 的每一列中每个值的数量统计
df.A.value_counts()
N 3
M 3
Name: A, dtype: int64
2.0 1
4.0 1
Name: C, dtype: int64
# 可以统计索引, 可按百分比显示, 可以分组统计
df.set_index('A').index.value_counts(normalize=True)
N 0.5
M 0.5
Name: A, dtype: float64
df.D.value_counts(bins=2)
(0.992, 4.5] 5
(4.5, 8.0] 1
Name: D, dtype: int64
nunique 去重计数, 统计每一行或列不同值的数量
A 2
B 2
C 2
D 4
E 4
F 6
dtype: int64
quantile 计算分位数
print(df.quantile([0.3, 0.6]))
C D E F
0.3 2.6 1.0 4.0 3.5
0.6 3.2 2.0 6.0 5.0
cut 与 qcut 数据分箱
# 将某一列中的值分别分到一个范围中, 默认左不包含右包含
# 这对分组非常有用, 例如年龄分段
c = pd.cut(df.D, bins=[0, 3, 8], labels=['0到3', '3到8'])
c
a 0到3
b 3到8
c 0到3
d 3到8
e 0到3
f 0到3
Name: D, dtype: category
Categories (2, object): ['0到3' < '3到8']
# 根据数值的频率来选择间隔, 使每个分段里值的个数相同
pd.qcut(df.D, q=2)
a (0.999, 1.5]
b (1.5, 8.0]
c (0.999, 1.5]
d (1.5, 8.0]
e (1.5, 8.0]
f (0.999, 1.5]
Name: D, dtype: category
Categories (2, interval[float64, right]): [(0.999, 1.5] < (1.5, 8.0]]
pd.qcut(df.D, q=2).value_counts()
(0.999, 1.5] 3
(1.5, 8.0] 3
Name: D, dtype: int64
rank 用来给数据排名, 例如销售额, 成绩等
# 有多种排名方式可供选择, 举一例: 给 D 列排名
df['排名'] = df.D.rank(method='dense', ascending=False)
print(df)
A B C D E F 排名
a N K NaN 1 4 4 4.0
b N K 2.0 4 6 3 2.0
c N K NaN 1 1 5 4.0
d M J NaN 8 8 7 1.0
e M J NaN 2 6 1 3.0
f M K 4.0 1 4 6 4.0
sort_values 数据排序
df.sort_values('排名', inplace=True)
print(df)
A B C D E F 排名
d M J NaN 8 8 7 1.0
b N K 2.0 4 6 3 2.0
e M J NaN 2 6 1 3.0
a N K NaN 1 4 4 4.0
c N K NaN 1 1 5 4.0
f M K 4.0 1 4 6 4.0
shift 数据移动, 索引不变
A B C D E F 排名
d NaN NaN NaN NaN NaN NaN NaN
b NaN NaN NaN NaN NaN NaN NaN
e M J NaN 8.0 8.0 7.0 1.0
a N K 2.0 4.0 6.0 3.0 2.0
c M J NaN 2.0 6.0 1.0 3.0
f N K NaN 1.0 4.0 4.0 4.0
print(df.shift(-1, axis=1))
A B C D E F 排名
d J NaN 8 8 7 1.0 NaN
b K 2.0 4 6 3 2.0 NaN
e J NaN 2 6 1 3.0 NaN
a K NaN 1 4 4 4.0 NaN
c K NaN 1 1 5 4.0 NaN
f K 4.0 1 4 6 4.0 NaN
agg 和 apply 传入函数进行高级聚合运算, 已存在的函数用字符串形式传入, 自定义函数传入函数名
A MNMNNM
B JKJKKK
C 6.0
D 17
E 29
F 26
排名 18.0
dtype: object
print(df.agg(['max', 'min']))
A B C D E F 排名
max N K 4.0 8 8 7 4.0
min M J 2.0 1 1 1 1.0
print(df.apply(['max', 'min']))
A B C D E F 排名
max N K 4.0 8 8 7 4.0
min M J 2.0 1 1 1 1.0
🔺🔺 groupby 按指定的列(行)中不同值分组, 与前面的聚合函数组合出无限的变化, 满足各种需求
# 分组后是一个迭代器, 可以查看分组, 获取分组
df.groupby('A')
<pandas.core.groupby.generic.DataFrameGroupBy object at 0x0000021E8A8295B0>
{'M': ['d', 'e', 'f'], 'N': ['b', 'a', 'c']}
print(df.groupby('A').get_group('M'))
A B C D E F 排名
d M J NaN 8 8 7 1.0
e M J NaN 2 6 1 3.0
f M K 4.0 1 4 6 4.0
# 分别取出 M 和 N 中 F 列任意排名的数据
def get_second(x, m, n):
return x[x[m].rank(method='dense', ascending=False)==n]
# 通过修改 n 参数取出任意排名
print(df.groupby('A').apply(get_second, m='F', n=3.0))
A B C D E F 排名
A
M e M J NaN 2 6 1 3.0
N b N K 2.0 4 6 3 2.0
# 将上述 cut 分箱得到的数据用来分组统计
print(df.groupby(c).count())
A B C D E F 排名
D
0到3 4 4 1 4 4 4 4
3到8 2 2 1 2 2 2 2
# 对不同的列作不同的分组聚合运算
print(df.groupby('A').agg({'E': ['mean', 'max'], 'F': 'sum'}))
E F
mean max sum
A
M 6.000000 8 14
N 3.666667 6 12
5, 数据透视与窗口函数
import numpy as np
import pandas as pd
np.random.seed(0)
df = pd.DataFrame(np.random.randint(1, 20, (4, 5)))
df.columns = list('ABCDE')
print(df)
A B C D E
0 13 16 1 4 4
1 8 10 19 5 7
2 13 2 7 8 15
3 18 6 14 9 10
melt 将列索引展开成数据
# 默认全部展开
df1 = pd.melt(df, id_vars=['A', 'B'], var_name='F', value_name='G')
print(df1)
A B F G
0 13 16 C 1
1 8 10 C 19
2 13 2 C 7
3 18 6 C 14
4 13 16 D 4
5 8 10 D 5
6 13 2 D 8
7 18 6 D 9
8 13 16 E 4
9 8 10 E 7
10 13 2 E 15
11 18 6 E 10
print(pd.melt(df, id_vars=['A', 'B'], value_vars=['C']))
A B variable value
0 13 16 C 1
1 8 10 C 19
2 13 2 C 7
3 18 6 C 14
pivot 将行值展开成为列索引
print(df1.pivot(columns='F', values=['A', 'B']))
A B
F C D E C D E
0 13.0 NaN NaN 16.0 NaN NaN
1 8.0 NaN NaN 10.0 NaN NaN
2 13.0 NaN NaN 2.0 NaN NaN
3 18.0 NaN NaN 6.0 NaN NaN
4 NaN 13.0 NaN NaN 16.0 NaN
5 NaN 8.0 NaN NaN 10.0 NaN
6 NaN 13.0 NaN NaN 2.0 NaN
7 NaN 18.0 NaN NaN 6.0 NaN
8 NaN NaN 13.0 NaN NaN 16.0
9 NaN NaN 8.0 NaN NaN 10.0
10 NaN NaN 13.0 NaN NaN 2.0
11 NaN NaN 18.0 NaN NaN 6.0
pivot_table 与 groupby + 聚合函数 类似, 可以对表格进行各种需求的透视
df1.index = list('LMNLMNLMNLMN')
df1.reset_index(inplace=True)
df1.rename(columns={'index': 'Q'}, inplace=True)
print(df1)
Q A B F G
0 L 13 16 C 1
1 M 8 10 C 19
2 N 13 2 C 7
3 L 18 6 C 14
4 M 13 16 D 4
5 N 8 10 D 5
6 L 13 2 D 8
7 M 18 6 D 9
8 N 13 16 E 4
9 L 8 10 E 7
10 M 13 2 E 15
11 N 18 6 E 10
print(df1.pivot_table(index=['Q', 'F'], aggfunc='mean'))
A B G
Q F
L C 15.5 11 7.5
D 13.0 2 8.0
E 8.0 10 7.0
M C 8.0 10 19.0
D 15.5 11 6.5
E 13.0 2 15.0
N C 13.0 2 7.0
D 8.0 10 5.0
E 15.5 11 7.0
print(df1.pivot_table(index='Q',
columns='F',
values='A',
aggfunc=['mean', 'sum']))
mean sum
F C D E C D E
Q
L 15.5 13.0 8.0 31 13 8
M 8.0 15.5 13.0 8 31 13
N 13.0 8.0 15.5 13 8 31
rolling 将数据依次移动指定尺寸的窗口并进行聚合运算
Rolling [window=3,center=False,axis=0,method=single]
Q A B F G
0 L 13 16 C 1
1 M 8 10 C 19
2 N 13 2 C 7
3 L 18 6 C 14
4 M 13 16 D 4
5 N 8 10 D 5
6 L 13 2 D 8
7 M 18 6 D 9
8 N 13 16 E 4
9 L 8 10 E 7
10 M 13 2 E 15
11 N 18 6 E 10
# 移动 3 条数据加和一次作为一条新数据,
# 前面默认 nan 填充
print(df1.rolling(3).sum())
A B G
0 NaN NaN NaN
1 NaN NaN NaN
2 34.0 28.0 27.0
3 39.0 18.0 40.0
4 44.0 24.0 25.0
5 39.0 32.0 23.0
6 34.0 28.0 17.0
7 39.0 18.0 22.0
8 44.0 24.0 21.0
9 39.0 32.0 20.0
10 34.0 28.0 26.0
11 39.0 18.0 32.0
# 可以设置最小观察值(必须小于移动尺寸)
# 可以用高级函数聚合运算
print(df1.rolling(len(df1), min_periods=1).sum())
A B G
0 13.0 16.0 1.0
1 21.0 26.0 20.0
2 34.0 28.0 27.0
3 52.0 34.0 41.0
4 65.0 50.0 45.0
5 73.0 60.0 50.0
6 86.0 62.0 58.0
7 104.0 68.0 67.0
8 117.0 84.0 71.0
9 125.0 94.0 78.0
10 138.0 96.0 93.0
11 156.0 102.0 103.0
print(df1.rolling(2).agg(['sum', np.max]))
A B G
sum amax sum amax sum amax
0 NaN NaN NaN NaN NaN NaN
1 21.0 13.0 26.0 16.0 20.0 19.0
2 21.0 13.0 12.0 10.0 26.0 19.0
3 31.0 18.0 8.0 6.0 21.0 14.0
4 31.0 18.0 22.0 16.0 18.0 14.0
5 21.0 13.0 26.0 16.0 9.0 5.0
6 21.0 13.0 12.0 10.0 13.0 8.0
7 31.0 18.0 8.0 6.0 17.0 9.0
8 31.0 18.0 22.0 16.0 13.0 9.0
9 21.0 13.0 26.0 16.0 11.0 7.0
10 21.0 13.0 12.0 10.0 22.0 15.0
11 31.0 18.0 8.0 6.0 25.0 15.0
def f(x):
return x.iloc[0] * x.iloc[1]
print(df1[['A', 'G']])
A G
0 13 1
1 8 19
2 13 7
3 18 14
4 13 4
5 8 5
6 13 8
7 18 9
8 13 4
9 8 7
10 13 15
11 18 10
print(df1.rolling(2)['A', 'G'].apply(f))
A G
0 NaN NaN
1 104.0 19.0
2 104.0 133.0
3 234.0 98.0
4 234.0 56.0
5 104.0 20.0
6 104.0 40.0
7 234.0 72.0
8 234.0 36.0
9 104.0 28.0
10 104.0 105.0
11 234.0 150.0
6, 文本字符串处理
文本字符串处理方法基本上和 python 内建字符串方法同名, 这些方法自动忽略 nan 进行处理
方法较多, 常用的举几个例子:
s = pd.Series(['A_1', 'B_2', 'C_3', np.nan],
index=['A_a', 'B_b', 'C_c', 'D'])
s
A_a A_1
B_b B_2
C_c C_3
D NaN
dtype: object
A_a A
B_b B
C_c C
D NaN
dtype: object
A_a A_
B_b B_
C_c C_
D NaN
dtype: object
Index(['a', 'b', 'c', nan], dtype='object')
A_a [A, 1]
B_b [B, 2]
C_c [C, 3]
D NaN
dtype: object
s.str.split('_').str.get(0)
A_a A
B_b B
C_c C
D NaN
dtype: object
A_a 1
B_b 2
C_c 3
D NaN
dtype: object
A_a A_1
B_b B_2
C_c C_3
D NaN
dtype: object
print(s.str.split('_', expand=True))
0 1
A_a A 1
B_b B 2
C_c C 3
D NaN NaN
# 替换, 默认正则匹配, 可传入函数高级匹配
s.str.replace('_', '')
A_a A1
B_b B2
C_c C3
D NaN
dtype: object
s.index.str.replace('_', '')
Index(['Aa', 'Bb', 'Cc', 'D'], dtype='object')
# 拼接
s1 = s.str.split('_').str[0]
s1
A_a A
B_b B
C_c C
D NaN
dtype: object
'ABC'
'A_B_C'
s1.str.cat(sep='_', na_rep='_')
'A_B_C__'
s1.str.cat(['1', '2', '3', '4'], na_rep='_')
A_a A1
B_b B2
C_c C3
D _4
dtype: object
A_a A_1
B_b B_2
C_c C_3
D NaN
dtype: object
print(s.str.extract(r'([ABC])_(\d)'))
0 1
A_a A 1
B_b B 2
C_c C 3
D NaN NaN
三, 时间序列
时间序列对数据分析很重要, 很多数据都和时间发生的先后顺序相关
date_range 生成时间序列
import numpy as np
import pandas as pd
pd.date_range(start='20200701', end='20200705')
DatetimeIndex(['2020-07-01', '2020-07-02', '2020-07-03', '2020-07-04',
'2020-07-05'],
dtype='datetime64[ns]', freq='D')
# 可以指定生成个数与频率等
pd.date_range(start='6/1/2020', periods=5, freq='10D')
DatetimeIndex(['2020-06-01', '2020-06-11', '2020-06-21', '2020-07-01',
'2020-07-11'],
dtype='datetime64[ns]', freq='10D')
to_datetime 转换时间格式
1970年 1 月 1 日 00:00:00 UTC+00:00 时区的时刻称为 epoch time,记为 0,当前时间就是相对于 epoch time
的秒数
# 获取本地当前时间
from datetime import datetime
print(datetime.now())
d = datetime.now().timestamp()
print(datetime.fromtimestamp(d))
d
2022-08-20 19:48:43.236534
2022-08-20 19:48:43.237531
1660996123.237531
# 数字形式的时间, 用 to_datetime 转换为时间格式后与上述有差别,
# 是由于时区的原因, 转换时区即可一样
print(pd.to_datetime(d, utc=True, unit='s'))
d = pd.Series(d)
pd.to_datetime(d, utc=True, unit='s').dt.tz_convert('Asia/Shanghai')
2022-08-20 11:48:43.237530880+00:00
0 2022-08-20 19:48:43.237530880+08:00
dtype: datetime64[ns, Asia/Shanghai]
# 各种日期格式的转换
print(pd.to_datetime(['07-17-2020', '11-07-2020'], dayfirst=True))
print(pd.to_datetime('2020年7月17日', format='%Y年%m月%d日'))
pd.to_datetime(['jul 17, 2020',
'2020-07-17',
'20200717',
'2020/07/17',
'2020.07.17',
np.nan])
DatetimeIndex(['2020-07-17', '2020-07-11'], dtype='datetime64[ns]', freq=None)
2020-07-17 00:00:00
DatetimeIndex(['2020-07-17', '2020-07-17', '2020-07-17', '2020-07-17',
'2020-07-17', 'NaT'],
dtype='datetime64[ns]', freq=None)
# 可以跳过非时间, 可以转换 DataFrame 但索引名是固定的名称
print(pd.to_datetime(['2020.07.17', '日期'], errors='coerce'))
df = pd.DataFrame({'year': [2019, 2020],
'month': [6, 7],
'day': [4, 5]})
pd.to_datetime(df)
DatetimeIndex(['2020-07-17', 'NaT'], dtype='datetime64[ns]', freq=None)
0 2019-06-04
1 2020-07-05
dtype: datetime64[ns]
df = pd.DataFrame(np.random.randint(0, 10, (5, 2)),
index=pd.date_range('20180717', periods=5, freq='200D'))
print(df)
0 1
2018-07-17 4 3
2019-02-02 0 3
2019-08-21 5 0
2020-03-08 2 3
2020-09-24 8 1
时间索引取值, between_time 取时间段
0 1
2018-07-17 4 3
2019-02-02 0 3
2019-08-21 5 0
print(df['2019-01':'2020-01'])
0 1
2019-02-02 0 3
2019-08-21 5 0
df.index = pd.date_range('20200717', periods=5, freq='2H')
print(df)
0 1
2020-07-17 00:00:00 4 3
2020-07-17 02:00:00 0 3
2020-07-17 04:00:00 5 0
2020-07-17 06:00:00 2 3
2020-07-17 08:00:00 8 1
print(df.between_time('3:00', '7:00'))
0 1
2020-07-17 04:00:00 5 0
2020-07-17 06:00:00 2 3
时间序列作为数据的操作
df.index = pd.date_range('20180717', periods=5, freq='100D')
df.index.name = '日期'
df.reset_index(inplace=True)
print(df)
日期 0 1
0 2018-07-17 4 3
1 2018-10-25 0 3
2 2019-02-02 5 0
3 2019-05-13 2 3
4 2019-08-21 8 1
0 17
1 25
2 2
3 13
4 21
Name: 日期, dtype: int64
df['月份'] = df['日期'].dt.month
print(df)
日期 0 1 月份
0 2018-07-17 4 3 7
1 2018-10-25 0 3 10
2 2019-02-02 5 0 2
3 2019-05-13 2 3 5
4 2019-08-21 8 1 8
print(df[df.日期.dt.month >= 5])
日期 0 1 月份
0 2018-07-17 4 3 7
1 2018-10-25 0 3 10
3 2019-05-13 2 3 5
4 2019-08-21 8 1 8
日期 0 1 月份
0 2018-07-17 4 3 7
1 2018-10-25 0 3 10
3 2019-05-13 2 3 5
4 2019-08-21 8 1 8
d = df.日期.astype(str).str.split('-', expand=True)
print(d)
0 1 2
0 2018 07 17
1 2018 10 25
2 2019 02 02
3 2019 05 13
4 2019 08 21
日期 0 1 月份
0 2018-07-17 4 3 7
1 2018-10-25 0 3 10
四, 数据的导入导出与可视化
1, pandas 可以导入导出多种格式的数据:
read_csv, to_csv
read_json, to_json
read_html, to_html
read_excel, to_excel
read_hdf, to_hdf
等等
# 默认读取第一个 sheet, 默认第一行为列索引
df = pd.read_excel(r'./sheet.xlsx',
sheet_name=0, header=0)
print(df)
名次 战队名 说明
0 1 FPX 四包二战术
1 2 G2 个人能力强
2 3 IG 喜欢打架
3 4 SKT Faker状态低迷
4 5 GRF 上单是短板
5 6 DWG 下路弱
6 7 FNC 欧洲强队
7 8 SPY AD强
8 9 RNG 四保一
9 10 TL 北美强队
# 可以设置将某列作为行索引, 某列作为列索引
df1 = pd.read_excel(r'./sheet.xlsx',
sheet_name=1)
print(df1)
名次 上单
0 1 GIMGOOM
1 2 WUNDER
2 3 KHAN
3 4 FLANDER
4 5 THESHY
5 6 NUGURI
6 7 BWIPO
7 8 IPMPACT
8 9 LICORICE
9 10 HUNI
df2 = pd.read_excel(r'./sheet.xlsx',
sheet_name=1,
header=1,
index_col=0)
print(df2)
GIMGOOM
1
2 WUNDER
3 KHAN
4 FLANDER
5 THESHY
6 NUGURI
7 BWIPO
8 IPMPACT
9 LICORICE
10 HUNI
# 有时需要根据文件调节编码和引擎参数
df = pd.read_csv(r'./ratings_chinses.csv',
engine=None,
encoding='gbk')
print(df)
数量 收获 评分
0 1 1 4
1 2 3 4
2 3 6 4
3 4 47 5
4 5 50 5
.. ... ... ..
105 106 47 5
106 107 50 3
107 108 70 5
108 109 101 4
109 110 110 5
[110 rows x 3 columns]
2, 可视化
import matplotlib.pyplot as plt
plt.rcParams['font.family'] = 'Microsoft YaHei'
plt.rcParams['font.size'] = 12
df.收获.plot()
df['评分'].plot(kind='hist')
描述统计
数理统计以概率论为基础, 研究大量随机现象的统计规律性. 分为 描述统计 和 推断统计 , 在数据分析领域具有非常重要的地位
描述统计, 就是从总体数据中提取变量的主要信息(总和, 均值, 最大, 最多等), 从而从总体层面上, 对数据进行统计性描述.
通常配合绘制相关统计图进行辅助
统计学的变量类型
统计学中的变量指研究对象的特征(属性), 每个变量都有变量值和类型, 类型可分为:
类别变量 : 对研究对象定性, 分类
类别变量又可分为:
- 有序类别变量: 描述对象等级或顺序等, 例如, 优良中差
- 无序类别变量: 仅做分类, 例如 A, B 血型, 男女
数值变量 : 对研究对象定量描述
数值变量又可分为:
- 离散变量: 取值只能用自然数或整数个单位计算, 例如统计人数
- 连续变量: 在一定区间内可以任意取值, 例如计算身高
数值变量对加, 减, 求平均等操作有意义, 而类别变量无意义
统计量
描述统计所提取的统计信息, 称为统计量, 主要包括:
- 类别分析: 频数, 频率
- 集中趋势分析: 均值, 中位数, 众数, 分位数
- 离散程度分析: 极差, 方差, 标准差
- 描述分布形状: 偏度, 峰度
准备数据:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
plt.rcParams['font.family'] = 'SimHei'
plt.rcParams['axes.unicode_minus'] = False
# 正态分布
data1 = np.around(np.random.normal(10, 3, 600)).reshape(-1, 1)
# 左偏
t1 = np.random.randint(1, 21, size=100)
t2 = np.random.randint(21, 31, size=500)
left_data = np.concatenate([t1, t2]).reshape(-1, 1)
# 右偏
t3 = np.random.randint(1, 11, size=500)
t4 = np.random.randint(11, 21, size=100)
right_data = np.concatenate([t3, t4]).reshape(-1, 1)
# 类别
type_data = np.random.randint(0, 2, size=600).reshape(-1, 1)
data = np.concatenate([data1, left_data, right_data, type_data], axis=1)
data = pd.DataFrame(data,
columns=['data1', 'left_data', 'right_data', 'type_data'])
# 随机取 10 条数据
data.sample(10)
data1 left_data right_data type_data
202 13.0 27.0 8.0 0.0
595 12.0 23.0 15.0 0.0
523 11.0 21.0 20.0 1.0
259 12.0 29.0 8.0 0.0
498 12.0 24.0 3.0 0.0
110 8.0 27.0 1.0 0.0
65 7.0 12.0 5.0 0.0
231 13.0 25.0 2.0 0.0
321 8.0 30.0 3.0 0.0
544 5.0 29.0 19.0 1.0
a, 频数
数据中某个类别出现的次数称为该类别的频数
例如, 计算上述两个类别(0.0
和 1.0
)出现的频数:
frequency = data['type_data'].value_counts()
frequency
0.0 309
1.0 291
Name: type_data, dtype: int64
b, 频率
数据中某个类别出现次数与总次数的比值称为该类别的频率
例如, 计算上述两个类别(0.0
和 1.0
)出现的频率:
percentage = frequency * 100 / len(data)
percentage
0.0 51.5
1.0 48.5
Name: type_data, dtype: float64
c, 均值
平均值, 一组数据的总和除以数据的个数
d, 中位数
将一组数据按顺序排列, 位于最中间位置的值, 即是中位数, 如果数据个数为偶数, 取中间两个的平均值
e, 众数
一组数据中出现次数最多的值
通常三者的关系如下图所示:
注意点 :
数值变量通常使用均值和中值表示集中趋势, 类别变量则通常使用众数
正态分布下, 数据量足够多, 三者相同
均值使用所有数据计算, 容易受极端值影响, 中位数和众数则不会
众数在一组数据中可能不唯一
例, 计算字段 data1
的均值, 中位数和众数:
mean = data['data1'].mean()
median = data['data1'].median()
mode = data['data1'].mode()
print(f'均值:{mean} 中位数:{median}\n众数:\n{mode}')
均值:10.121666666666666 中位数:10.0
众数:
0 9.0
dtype: float64
f, 分位数
通过 n - 1 个分位, 将升序排列的数据分为 n 个区间, 使得每个区间数值个数相等(或近似相等), 则每个分位对应的数, 就是该 n 分位的分位数.
常用的有四分位数和百分位数
以四分位数为例:
第一个分位称为 1/4 分位(下四分位), 第二个称为 2/4 分位(中四分位), 第三个称为 3/4 分位(上四分位), 其中中四分位数, 其实就是中位数
求四分位的值:
-
首先计算各个分位的位置
index1 = (n - 1) * 0.25
index2 = (n - 1) * 0.5
index3 = (n - 1) * 0.75
(index 从 0 开始, n 为元素的个数)
-
根据位置计算各个分位的值
index 为整数, 值就是相应的 index 对应的元素
index 不为整数, 四分位位置介于 ceil(index) 和 floor(index) 之间, 加权计算分位值
例, 求 x 的四分位数:
index 为整数
x = np.arange(0, 9)
n = len(x)
index1 = (n - 1) * 0.25
index2 = (n - 1) * 0.5
index3 = (n - 1) * 0.75
index = np.array([index1, index2, index3]).astype(np.int32)
x[index]
array([2, 4, 6])
index 不是整数
x = np.arange(0, 10)
n = len(x)
index1 = (n - 1) * 0.25
index2 = (n - 1) * 0.5
index3 = (n - 1) * 0.75
index = np.array([index1, index2, index3])
left = np.floor(index).astype(np.int32)
right = np.ceil(index).astype(np.int32)
weight, _ = np.modf(index) # 获取 index 整数和小数部分
result = x[left] * (1 - weight) + x[right] * weight
result
array([2.25, 4.5 , 6.75])
Numpy 中计算分位数可直接用方法 np.quantile
和 np.percentile
np.quantile(x, q=[0.25, 0.5, 0.75]), np.percentile(x, q=[25, 50, 75])
(array([2.25, 4.5 , 6.75]), array([2.25, 4.5 , 6.75]))
Pandas 中计算分位数可利用 describe
(默认 4 分位)
s = pd.Series(x)
s.describe()
count 10.00000
mean 4.50000
std 3.02765
min 0.00000
25% 2.25000
50% 4.50000
75% 6.75000
max 9.00000
dtype: float64
25% 2.25
50% 4.50
75% 6.75
dtype: float64
可自定义分位:
s.describe(percentiles=[0.15, 0.4, 0.8])
count 10.00000
mean 4.50000
std 3.02765
min 0.00000
15% 1.35000
40% 3.60000
50% 4.50000
80% 7.20000
max 9.00000
dtype: float64
g, 极差
一组数据中, 最大值与最小值之差
h, 方差
方差体现一组数据中, 每个元素与均值的偏离程度
$$\sigma^{2}=\frac{1}{n-1} \sum_{i=1}^{n}\left(x_{i}-\bar{x}\right)^{2}$$
$x_{i}:$ 数组中的每个元素
$n:$ 数组元素的个数
$\bar{x}:$ 数组中所有元素的均值
i, 标准差
标准差为方差的开方. 方差和标准差可以体现数据的分散性, 越大越分散, 越小越集中. 也可体现数据波动性(稳定性), 越大波动越大, 反之亦然
当数据足够多时, 可用 n 代替 n - 1
例, 计算 left_data
字段的极差, 方差, 标准差:
sub = np.ptp(data['left_data'])
var = data['left_data'].var()
std = data['left_data'].std()
sub, var, std
(29.0, 44.631048970506306, 6.680647346665315)
绘图对比 data1
和 left_data
的分散程度
plt.figure(figsize=(11, 1))
plt.ylim(-0.5, 1.5)
plt.plot(data['data1'], np.zeros(len(data)), ls='', marker='o', color='r', label='data1')
plt.plot(data['left_data'], np.ones(len(data)), ls='', marker='o', color='g', label='left_data')
plt.axvline(data['data1'].mean(), ls='--', color='r', label='data1均值')
plt.axvline(data['left_data'].mean(), ls='--', color='g', label='left_data均值')
plt.legend()
plt.show()
j, 偏度
统计数据分布偏斜方向和程度的度量, 统计数据分布非对称程度的数字特征, 偏度为 0 , 对称分布, 小于 0, 左偏分别, 大于 0, 右偏分布
k, 峰度
表征概率密度分布曲线在平均值处峰值高低的特征数. 直观看来, 峰度反映了峰部的尖度, 峰度高意味着标准差增大是由低频度的大于或小于平均值的极端差值引起的.
在相同的标准差下,峰度越大,分布就有更多的极端值,那么其余值必然要更加集中在众数周围,其分布必然就更加陡峭
样本的峰度是和正态分布相比较而言的统计量, 符合正态分布的峰度为 0
例, 计算 data
中前三个字段的偏度, 峰度与标准差, 并绘图比较:
print('偏度:', data['data1'].skew(), data['left_data'].skew(), data['right_data'].skew())
print('峰度:', data['data1'].kurt(), data['left_data'].kurt(), data['right_data'].kurt())
print('标准差:', data['data1'].std(), data['left_data'].std(), data['right_data'].std())
偏度: 0.0013827051273872734 -1.704193031847586 0.9122511031664028
峰度: 0.01807838530280126 2.5013831586663304 0.29539776195275813
标准差: 2.891504548352662 6.680647346665315 4.672046842962734
sns.kdeplot(data['data1'], shade=True, label='正态')
sns.kdeplot(data['left_data'], shade=True, label='左偏')
sns.kdeplot(data['right_data'], shade=True, label='右偏')
plt.show()
推断统计
推断统计, 通过样本推断总体的统计方法, 包括对总体的未知参数进行估计; 对关于参数的假设进行检查; 对总体进行预测预报等.
推断统计的基本问题可以分为两大类:一类是 参数估计 问题; 另一类是 假设检验 问题
1, 总体, 个体与样本
总体, 要研究对象的所有数据, 获取通常比较困难. 总体中的某个数据, 就是个体. 从总体中抽取部分个体, 就构成了样本, 样本中的个体数, 称为样本容量.
2, 参数估计
参数估计, 用样本指标(统计量)估计总体指标(参数). 参数估计有 点估计 和 区间估计 两种
2.01, 点估计
点估计是依据样本统计量估计总体中的未知参数. 通常它们是总体的某个特征值,如数学期望, 方差和相关系数等.
点估计问题就是要构造一个只依赖于样本的量,作为总体未知参数的估计值.
2.02, 区间估计
区间估计是根据样本的统计量, 计算出一个可能的区间(置信区间) 和 概率(置信度), 表示总体的未知参数有多少概率位于该区间.
注意:
点估计使用一个值来作为总体参数值, 能给出具体值, 但易受随机抽样影响, 准确性不够
区间估计使用一个置信区间和置信度, 表示总体参数值有多少可能(置信度)会在该范围(置信区间)内, 能给出合理的范围和信心指数, 不能给出具体值
2.03, 中心极限定理
要确定置信区间与置信度, 我们先要知道总体与样本之间, 在分布上有着怎样的联系. 中心极限定理(独立同分布的中心极限定理)给出了它们之间的联系:
如果总体均值为
$\mu$, 方差为
$\sigma^{2}$, 我们进行随机抽样, 样本容量为 n, 当 n 增大时,则样本均值
$\bar{X}$
逐渐趋近服从均值为
$\mu$, 方差为
$\sigma^{2} / n$ 的正态分布:
$$\bar{X} \sim N\left(\mu, \sigma^{2} / n\right)$$说明:
进行多次抽样,每次抽样会得到一个均值, 这些均值会围绕在总体均值左右,呈正态分布
当样本容量 n 足够大时, 抽样样本均值的均值 ≈ 样本均值
$\bar{X}$ ≈ 总体均值
$\mu$, 样本均值分布的标准差等于
$\sigma / \sqrt{n}$
样本均值分布的标准差, 称为标准误差, 简称标准误
模拟证明:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
plt.rcParams['font.family'] = 'SimHei'
plt.rcParams['axes.unicode_minus'] = False
# 定义非正态分布总体(也可以是正态分布)
data = np.random.normal(20, 5, size=10000)
data.sort()
all_ = np.random.choice(data[0:8000], size=10000)
# sns.displot(all_)
# 将总体的均值和标准差设为已知条件
print('总体均值:', all_.mean(), '总体标准差:', all_.std())
# 创建存放每次抽样的平均值的数组(初始值为 0)
mean_arr = np.zeros(1000)
# 循环抽取 1000 个样本, 每次抽 100 个
for i in range(len(mean_arr)):
mean_arr[i] = np.random.choice(all_, size=100, replace=False).mean()
# 验证结果
print('样本均值:', mean_arr[1], '样本均值的均值:', mean_arr.mean(),
'标准误差:', mean_arr.std(), '偏度:', pd.Series(mean_arr).skew(), sep='\n')
sns.displot(mean_arr, kde=True)
plt.show()
总体均值: 18.270423532980452 总体标准差: 3.8201265113791596
样本均值:
18.194948520041606
样本均值的均值:
18.26385715935595
标准误差:
0.373202226318143
偏度:
0.00746666188264042
2.04, 正态分布的特性
正态分布:
$X \sim N\left(\mu, \sigma^{2}\right)$
以均值为中心:
在 1 倍标准差内包含约 68.2% 的样本数据
在 2 倍标准差内包含约 95.4% 的样本数据
在 3 倍标准差内包含约 99.7% 的样本数据
证明:
# 定义标准差
scale = 10
# 定义数据
x = np.random.normal(0, scale, size=100000)
# 计算
for times in range(1, 4):
y = x[(x > -times * scale) & (x < times * scale)]
print(f'{times}倍的标准差:')
print(f'{len(y) * 100 / len(x)}%')
1倍的标准差:
68.206%
2倍的标准差:
95.354%
3倍的标准差:
99.711%
2.05, 重要结论
根据中心极限定理和正态分布的特性, 如果总体标准差为
$\sigma$, 对总体进行一次抽样, 如果样本足够大, 则样品均值
$\bar{X}$
服从正态分布, 该均值约有 95.4% 的概率会在 2 倍的标准误差 (
$\mu - 2\sigma / \sqrt{n}, \mu + 2\sigma / \sqrt{n}$) 范围内, 并且该样本均值约等于总体均值
$\mu$. 从而, 可以利用这一结论, 对总体均值进行区间估计.
结论验证:
# 随机生成总体均值, 其值未知
mean = np.random.randint(0, 10000)
# 总体的标准差已知为 50
std = 50
# 定义总体数据
all_ = np.random.normal(mean, std, size=100000)
# 从总体抽取 100 个元素构成样本
sample = np.random.choice(all_, size=100, replace=False)
# 计算样本均值
sample_mean = sample.mean()
print('样本均值:', sample_mean)
# 计算样本的标准误差
se = std / np.sqrt(n)
# 计算置信区间 95%置信度
min_ = sample_mean - 1.96 * se
max_ = sample_mean + 1.96 * se
print('置信区间(95%置信度):', (min_, max_))
# 区间估计
print(f'总体均值有 95% 的概率在{(min_, max_)}区间内')
print('总体均值:', mean)
# 绘图辅助
plt.plot(mean, 0, marker='*', color='orange', ms=12, label='总体均值')
plt.plot(sample_mean, 0, marker='o', color='r', label='样本均值')
plt.hlines(0, xmin=min_, xmax=max_, color='b', label='置信区间')
plt.axvline(min_, 0.4, 0.6, color='r', ls='--', label='左边界')
plt.axvline(max_, 0.4, 0.6, color='g', ls='--', label='右边界')
plt.legend()
plt.show()
样本均值: 9695.658932218576
置信区间(95%置信度): (9685.858932218576, 9705.458932218575)
总体均值有 95% 的概率在(9685.858932218576, 9705.458932218575)区间内
总体均值: 9696
3, 假设检验
假设检验(显著性检验), 先对总体做出假设, 然后通过判断样本与总体之间是否存在显著性差异, 来验证总体的假设
假设检验使用了一种类似于 “反证法” 的推理方法,它的特点是:
-
先对总体做出两个完全相反的假设, 原假设(设为真) 和 备择假设, 计算后导致不合理现象产生,则拒绝原假设, 接受备择假设, 反之接受原假设, 放弃备择假设
-
这种 “反证法” 不同于一般的反证法. 所谓不合理现象产生,并非指形式逻辑上的绝对矛盾,而是基于小概率原理:概率很小的事件在一次试验中几乎是不可能发生的,若发生了,就是不合理的.
-
怎样才算 “小概率”, 通常可将概率不超过 0.05 的事件称为 “小概率事件” ,也可视具体情形而取 0.1 或 0.01 等. 在假设检验中常记这个概率为 α,称为显著性水平
假设检验可分为正态分布检验, 正态总体均值检验, 非参数检验三类, 本文只介绍 正态总体均值检验 , 包括 Z检验 和 t检验 两种情况
3.01, 关键概念:
对总体参数做出两个完全对立的假设, 分别为:
原假设(零假设)
$H_{0}$
备择假设(对立假设)
$H_{1}$
双边假设检验 :
$H_{0}: \mu=\mu_{0}, H_{1}: \mu \neq \mu_{0}$
单边假设检验 :
$H_{0}: \mu \geq \mu_{0}, H_{1}: \mu<\mu_{0}$ (左边检验)
$H_{0}: \mu \leq \mu_{0}, H_{1}: \mu>\mu_{0}$ ( 右边检验 )
$\mu$ 为总体均值,
$\mu_{0}$ 为假设均值
显著性水平 : 根据需要设定的小概率事件的概率 α (1 - α 为置信度)
检验统计量 (Z 和 t): 用来判断样本均值与总体均值是否存在显著性差异
P值: 通过检验统计量计算而得的概率值, 表示原假设可被拒绝的最小值(或可支持原假设的概率):
P ≤ α, 原假设可被拒绝的最小值比显著性水平还低, 原假设可被拒绝, 则拒绝原假设
P > α, 原假设可被拒绝的最小值大于显著性水平, 原假设不可被拒绝, 支持原假设
3.02, 假设检验的步骤
设置原假设与备择假设
设置显著性水平 α
根据问题选择假设检验的方式
计算统计量(Z 或 t)
计算 P值(Z 或 t 围成的分布面积)
根据 P值 与 α值, 决定接受原假设还是备择假设
例, 某车间用一台包装机包装葡萄糖. 袋装糖的净重是一个随机变量,它服从正态分布. 当机器正常时,其均值为 0.5kg,标准差为 0.015kg.
某日开工后为检验包装机是否正常,随机地抽取它所包装的糖 9 袋,称得净重为(kg):
0.497, 0.506, 0.518, 0.524, 0.498, 0.511, 0.520, 0.515, 0.512
判断下面说法是否正确:
(1) 机器正常
例, 某车间用包装机包装葡萄糖. 袋装糖的净重是一个随机变量,它服从正态分布. 随机地抽取糖 9 袋,称得净重为(kg):
0.497, 0.506, 0.518, 0.524, 0.498, 0.511, 0.520, 0.515, 0.512
判断下面说法是否正确:
(2) 该车间袋装糖净重均值为 0.5kg
(3) 该车间袋装糖净重均值不少于 0.5kg
(4) 该车间袋装糖净重均值不多于 0.5kg
3.03, Z检验
Z检验适用于: 总体正态分布且方差已知, 样本容量较大(一般 ≥ 30)
Z统计量计算公式:
$$Z=\frac{\bar{x}-\mu_{0}}{S_{\bar{x}}}=\frac{\bar{x}-\mu_{0}}{\sigma /
\sqrt{n}}$$
$\bar{x}$: 样本均值
$\mu_{0}$: 假设的总体均值
$S_{\bar{x}}$: 样本的标准误差
$\sigma$: 总体的标准差
$n$: 样本容量
检验说法(1): 机器正常
双边检验:
原假设机器正常:
$H_{0}: \mu=\mu_{0}=0.5kg$
备择假设机器不正常:
$H_{1}: \mu \neq \mu_{0} \neq 0.5kg$
设置显著性水平: α = 0.05
import numpy as np
from scipy import stats
# 样本已知
a = np.array([0.497, 0.506, 0.518, 0.524, 0.498, 0.511, 0.520, 0.515, 0.512])
# 总体均值和标准差已知
mean, std = 0.5, 0.015
# 计算样本均值
sample_mean = a.mean()
# 计算样本标准误差
se = std / np.sqrt(len(a))
# 计算 Z统计量
Z = (sample_mean - mean) / se
print('Z统计量:', Z)
# 计算 P值, 双边检验: Z值与其右边曲线围成的面积的 2 倍
P = 2 * stats.norm.sf(abs(Z))
print('P值:' , P)
Z统计量: 2.244444444444471
P值: 0.02480381963225589
由结果可知, Z值 超过了 1.96, 由 Z值 与其右边曲线围成的面积的 2 倍, 必然小于 α(1.96 与其右边曲线围成的面积的 2 倍), 计算结果 P < α, 因此拒绝原假设, 接受备择假设, 机器不正常
3.04, t检验
t检验适用于: 总体正态分布, 方差未知, 样本数量较少(一般 < 30), 但是随着样本容量的增加, 分布逐渐趋于正态分布
t统计量计算公式:
$$t=\frac{\bar{x}-\mu_{0}}{S_{\bar{x}}}=\frac{\bar{x}-\mu_{0}}{S / \sqrt{n}}$$
$\bar{x}$: 样本均值
$\mu_{0}$: 假设的总体均值
$S_{\bar{x}}$: 样本的标准误差
$S$: 样本的标准差
$n$: 样本容量
双边检验 :
检验说法(2): 该车间袋装糖净重均值为 0.5kg
原假设, 该车间袋装糖净重均值为 0.5kg:
$H_{0}: \mu=\mu_{0}=0.5kg$
备择假设, 该车间袋装糖净重均值不为 0.5kg:
$H_{1}: \mu \neq \mu_{0} \neq 0.5kg$
设置显著性水平: α = 0.05
# 样本已知
a = np.array([0.497, 0.506, 0.518, 0.524, 0.498, 0.511, 0.520, 0.515, 0.512])
# 假设的总体均值已知
mean = 0.5
# 计算样本均值
sample_mean = a.mean()
# 计算样本标准差
std = a.std()
# 计算 t统计量
t = (sample_mean - mean) / ( std / np.sqrt(len(a)))
print('t统计量:', t)
# 计算 P值, df 是自由度: 样本变量可自由取值的个数
P = 2 * stats.t.sf(abs(t), df=len(a) - 1)
print('P值:', P)
t统计量: 3.802382179137283
P值: 0.005218925008708613
P < α, 拒绝原假设, 接受备择假设: 该车间袋装糖净重均值不为 0.5kg
还可以通过 scipy 提供的方法 ttest_1samp
来进行 t检验计算:
from scipy import stats
stats.ttest_1samp(a, 0.5)
Ttest_1sampResult(statistic=3.584920298041139, pvalue=0.007137006417828698)
左边检验 :
检验说法(3): 该车间袋装糖净重均值不少于 0.5kg
原假设, 该车间袋装糖净重均值不少于 0.5kg:
$H_{0}: \mu \geq \mu_{0}$
备择假设, 该车间袋装糖净重均值少于 0.5kg:
$H_{1}: \mu<\mu_{0}$
设置显著性水平: α = 0.05
# t统计量上述已经计算, 只需计算 P值: t统计量与其左边曲线围成的面积
P = stats.t.cdf(t, df=len(a) - 1)
print('P值:', P)
P值: 0.9973905374956458
P > α, 接受原假设, 该车间袋装糖净重均值不少于 0.5kg
右边检验 :
检验说法(4): 该车间袋装糖净重均值不多于 0.5kg
原假设, 该车间袋装糖净重均值不多于 0.5kg:
$H_{0}: \mu \leq \mu_{0}$
备择假设, 该车间袋装糖净重均值多于 0.5kg:
$H_{1}: \mu>\mu_{0}$
设置显著性水平: α = 0.05
# 计算 P值: t统计量与其右边曲线围成的面积
P = stats.t.sf(t, df=len(a) - 1)
print('P值:', P)
P值: 0.0026094625043543065
P < α, 拒绝原假设, 接受备择假设, 该车间袋装糖净重均值多于 0.5kg
线性回归
1, 模型
模型是指对于某个(类)实际问题的求解或客观事物运行规律进行抽象后的一种形式化表达方式, 可以理解为一个函数(一种映射规则)
任何模型都是由三个部分组成: 目标, 变量和关系.
建模时明确了模型的目标,才能进一步确定影响目标(因变量)的各关键变量(自变量),进而确定变量之间的关系(函数关系)
通过大量数据检验(训练)模型, 将模型(函数)的各个参数求解, 当参数确定之后, 便可利用模型对未知数据进行求值, 预测
用于训练模型的样本数据中的每个属性称为特征, 用 x 表示, 样本中的每条数据经过模型计算得到的输出值称为标签(监督学习), 用 y 表示, 从而得到 y
= f(x) 的函数关系
2, 回归分析
在统计学中, 回归分析指的是确定两种或两种以上变量间相互依赖的定量关系的一种统计分析方法
回归分析按照涉及的变量的多少,分为一元回归分析和多元回归分析;按照因变量的多少,可分为简单回归分析和多重回归分析;按照自变量和因变量之间的关系类型,可分为线性回归分析和非线性回归分析
回归分析解释自变量 x 发生改变, 因变量 y 会如何改变
拟合 , 插值 和 逼近 是数值分析的三大基础工具. 线性回归和非线性回归, 也叫线性拟合和非线性拟合,
拟合就是从整体上靠近已知点列,构造一种算法(模型或函数), 使得算法能够更加符合真实数据
3, 简单线性回归
线性回归分析的自变量和因变量之间是线性关系, 只有一个自变量时称为 简单线性回归 , 多个自变量时称为 多元线性回归
简单线性回归方程:
$$\hat{y}=w * x+b$$
$\hat{y}$ 为因变量, x 为自变量, w 为比例关系, b 为截距, w 和 b 就是模型的参数. 例如房屋价格与房屋面积的正比例关系
4, 多元线性回归
现实生活中自变量通常不止一个, 例如影响房屋价格的, 除了房屋面积, 还有交通, 地段, 新旧, 楼层等等因素.
不同的因素对房屋的价格影响力度(权重)不同, 因此使用多个因素来分析房屋的价格(各个因素与房屋价格近似线性关系), 可以得出多元线性回归方程:
$\hat{y}=w_{1} * x_{1}+w_{2} * x_{2}+w_{3} * x_{3}+\cdots+w_{n} * x_{n}+b$
$x$: 影响因素, 特征
$w$: 每个 x 的影响力度
$n$: 特征个数
$\hat{y}$: 房屋的预测价格
令:
$x_{0}=1, w_{0}=b$
设
$\vec{w}$ 和
$\vec{x}$ 为两个向量如下:
$$\vec{w}=\left(w_{0}, w_{1}, w_{2}, w_{3}, \ldots, w_{n}\right)^{T}$$
$$\vec{x}=\left(x_{0}, x_{1}, x_{2}, x_{3}, \ldots, x_{n}\right)^{T}$$则方程可表示为:
$$\begin{aligned}
\hat{y} &=w_{0} * x_{0}+w_{1} * x_{1}+w_{2} * x_{2}+w_{3} * x_{3}+\ldots
\ldots+w_{n} * x_{n} \
=\sum_{j=0}^{n} w_{j} * x_{j} \
=\vec{w}^{T} \cdot \vec{x}
\end{aligned}$$接下来只需要计算出参数
$\vec{w}^{T}$, 便可以建立模型
5, 损失函数
损失函数, 用来衡量模型预测值与真实值之间的差异的函数, 也称目标函数或代价函数. 损失函数的值越小, 表示预测值与真实值之间的差异越小.
因此, 求解上述模型的参数
$\vec{w}^{T}$, 就是要建立一个关于模型参数的损失函数(以模型参数
$\vec{w}^{T}$ 为自变量的函数),
然而
$\vec{w}^{T}$ 的取值组合是无限的, 目标就是通过机器学习, 求出一组最佳组合, 使得损失函数的值最小
在线性回归中, 使用平方损失函数(最小二乘法), 用 J(w) 表示:
$$\begin{array}{l}
J(w)=\frac{1}{2} \sum_{i=1}^{m}\left(y^{(i)}-\hat{y}^{(i)}\right)^{2} \
=\frac{1}{2} \sum_{i=1}^{m}\left(y^{(i)}-\vec{w}^{T} \vec{x}^{(i)}\right)^{2}
\end{array}$$m: 样本(训练集)数据的条数
$y^{(i)}$: 样本第 i 条数据的真实值
$\hat{y}^{(i)}$: 样本第 i 条数据的预测值
$\vec{x}^{(i)}$: 样本第 i 条数据的特征
m,
$y^{(i)}$ 和
$\vec{x}^{(i)}$ 已知, 要使 J(w) 最小, 对
$\vec{w}^{T}$ 求导并令导数等于 0 ,
便可求得
$\vec{w}^{T}$, 然后将样本(训练集)输入通过机器学习计算出具体的
$\vec{w}^{T}$
6, 回归模型评估
建立模型之后, 模型的效果如何, 需要进行评估, 对于回归模型, 可用如下指标来衡量:
MSE :
平均平方误差, 所有样本数据误差的平方和取均值:
$$M S E=\frac{1}{m} \sum_{i=1}^{m}\left(y^{(i)}-\hat{y}^{(i)}\right)^{2}$$RMSE :
平均平方误差的平方根:
$$R M S E=\sqrt{M S E}=\sqrt{\frac{1}{m}
\sum_{i=1}^{m}\left(y^{(i)}-\hat{y}^{(i)}\right)^{2}}$$MAE :
平均绝对值误差, 所有样本数据误差的绝对值的和取均值:
$$M A E=\frac{1}{m} \sum_{i=1}^{m}\left|y^{(i)}-\hat{y}^{(i)}\right|$$上述指标越小越好, 小到什么程度, 不同的对象建立的模型不一样
R² :
决定系数,反应因变量的全部变异能通过回归关系被自变量解释的比例. 如 R²=0.8,则表示回归关系可以解释因变量 80% 的变异.
换句话说,如果我们能控制自变量不变,则因变量的变异程度会减少 80%
在训练集中 R² 取值范围为 [0, 1], 在测试集(未知数据)中, R² 的取值范围为 [-∞, 1], R² 的值越大, 模型拟合越好
R² 的计算公式:
$$R^{2}=1-\frac{R S S}{T S
S}=1-\frac{\sum_{i=1}^{m}\left(y^{(i)}-\hat{y}^{(i)}\right)^{2}}{\sum_{i=1}^{m}\left(y^{(i)}-\bar{y}\right)^{2}}$$
$\bar{y}$: 样本(测试集)的平均值
不管何种对象建立的模型, R² 都是越大模拟越好
例一, 简单线性回归模型: 求鸢尾花花瓣长度和宽度的关系
import numpy as np
# 导入用于线性回归的类
from sklearn.linear_model import LinearRegression
# 切分训练集与测试集的模块
from sklearn.model_selection import train_test_split
# 鸢尾花数据集
from sklearn.datasets import load_iris
# 设置输出数据的精度为 2 (默认是8)
np.set_printoptions(precision=2)
# 获取花瓣长度 x, 宽度 y
iris = load_iris()
x, y = iris.data[:, 2].reshape(-1, 1), iris.data[:, 3]
# 将数据拆分为训练集和测试集, 指定测试集占比 test_size
# 指定随机种子 random_state(可以任意值但必须确定), 锁定拆分行为
x_train, x_test, y_train, y_test = train_test_split(
x, y, test_size=0.25, random_state=5)
# 使用训练集训练模型
lr = LinearRegression()
lr.fit(x_train, y_train)
# 求得模型参数
print('权重 w:', lr.coef_, '截距 b:', lr.intercept_)
# 调用模型进行预测
y_hat = lr.predict(x_test)
# 结果可视化
import matplotlib.pyplot as plt
plt.rcParams['font.family'] = 'SimHei'
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams['font.size'] = 10
plt.scatter(x_train, y_train, c='orange', label='训练集')
plt.scatter(x_test, y_test, c='g', marker='D', label='测试集')
plt.plot(x, lr.predict(x), 'r-')
plt.legend()
plt.xlabel('花瓣长度')
plt.ylabel('花瓣宽度')
plt.show()
权重 w: [0.42] 截距 b: -0.370615595909495
# 模型评估
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
print('MSE:', mean_squared_error(y_test, y_hat))
print('RMSE:', np.sqrt(mean_squared_error(y_test, y_hat)))
print('MAE:', mean_absolute_error(y_test, y_hat))
print('训练集R²:', r2_score(y_train, lr.predict(x_train))) # 可换成 lr.score(x_train, y_train)
print('测试集R²:', r2_score(y_test, y_hat)) # 可换成 lr.score(x_test, y_test)
MSE: 0.047866747643216113
RMSE: 0.21878470614559903
MAE: 0.1543808898175286
训练集R²: 0.9317841638431329
测试集R²: 0.9119955391492289
列二, 多元线性回归模型: 波士顿房价预测
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from sklearn.datasets import load_boston
import pandas as pd
boston = load_boston()
x, y = boston.data, boston.target
df = pd.DataFrame(np.concatenate([x, y.reshape(-1, 1)], axis=1),
columns=boston.feature_names.tolist() + ['MEDV'])
# 部分数据
df.head(3)
CRIM ZN INDUS CHAS NOX RM AGE DIS RAD TAX PTRATIO B LSTAT MEDV
0 0.00632 18.0 2.31 0.0 0.538 6.575 65.2 4.0900 1.0 296.0 15.3 396.90 4.98 24.0
1 0.02731 0.0 7.07 0.0 0.469 6.421 78.9 4.9671 2.0 242.0 17.8 396.90 9.14 21.6
2 0.02729 0.0 7.07 0.0 0.469 7.185 61.1 4.9671 2.0 242.0 17.8 392.83 4.03 34.7
x_train, x_test, y_train, y_test = train_test_split(
x, y, test_size=0.25, random_state=5)
lr = LinearRegression()
lr.fit(x_train, y_train)
print('权重:', lr.coef_)
print('截距:', lr.intercept_)
y_hat = lr.predict(x_test)
print('训练集R²:', lr.score(x_train, y_train))
print('测试集R²:', lr.score(x_test, y_test))
# 假如获取了一间房屋的数据, 预测其房价
room_data = np.array([0.00732, 17.0, 1.31, 1.0, 0.638, 7.575, 62.2, 5.0900,
1.0, 296.0, 15.3, 396.90, 4.98]).reshape(1, -1)
y_price = lr.predict(room_data)
print('房屋价格:', y_price)
权重: [-1.53e-01 4.79e-02 -8.60e-03 2.58e+00 -1.46e+01 3.96e+00 -7.92e-03
-1.46e+00 3.45e-01 -1.25e-02 -9.19e-01 1.32e-02 -5.17e-01]
截距: 32.214120389743606
训练集R²: 0.7468034208269784
测试集R²: 0.7059096071098042
房屋价格: [33.62]
多元线性回归在空间中, 可表示为一个超平面去拟合空间中的数据点
逻辑回归
逻辑回归和线性回归有类似之处, 都是利用线性加权计算的模型, 但逻辑回归是分类算法, 例如对是否患癌症进行预测, 因变量就是 是 和 否 ,
两个类别, 自变量可以是年龄, 性别, 饮食, 作息, 病菌感染等, 自变量既可以是数值变量, 也可以是类别变量
1, 逻辑回归二分类推导
和线性回归类似, 设自变量为 x, 每个自变量的权重为 w, 令:
$$\begin{array}{l}
z=w_{1} x_{1}+w_{2} x_{2}+\cdots+w_{n} x_{n}+b \
=\sum_{j=1}^{n} w_{j} x_{j}+b \
=\sum_{j=0}^{n} w_{j} x_{j} \
=\vec{w}^{T} \cdot \vec{x}
\end{array}$$z 是一个连续值, 取值范围(-∞, +∞), 为了实现分类, 一般设置阈值 z = 0, 当 z > 0 时, 将样本判定为一个类别(正例), 该类别设为
1, 当 z ≤ 0 时, 判定为另一个类别(负例), 该类别设为 0, 再设因变量为 y, 从而逻辑回归方程可表示为:
$y=1, z>0$
$y=0, z \leq 0$
上述方程虽然实现了分类, 但提供的信息有限, 因此引入 sigmoid函数 (也叫 Logistic函数), 将 z 映射到 (0, 1)
区间,可以实现二分类的同时, 还能体现将样本分为某个类的可能性, 这个可能性设为 p:
$$p=\operatorname{sigmoid}(z)=\frac{1}{1+e^{-z}}$$sigmoid 函数图像如下:
于是, 逻辑回归方程又可表示为:
$y=1, p>0.5$
$y=0, 1-p \geq 0.5$
从而可见, 通过比较 p 和 1-p 哪个更大(z 的阈值不取 0 时做出调整即可), 预测结果就是对应的一类
2, 逻辑回归的损失函数
通过上述推导过程可知, 要得到逻辑回归模型, 最终就是要求得参数
$\vec{w}^{T}$, 于是将 p 和 1-p 统一, 构造一个损失函数来求
$\vec{w}^{T}$:
$$p(y=1 | x ; w)=s(z)$$
$$p(y=0 | x ; w)=1-s(z)$$合并:
$$p(y | x ; w)=s(z)^{y}(1-s(z))^{1-y}$$上式表示一个样本的概率, 我们要求解能够使所有样本联合概率密度最大的
$\vec{w}^{T}$ 值, 根据极大似然估计,
所有样本的联合概率密度函数(似然函数)为:
$$\begin{array}{l}
L(w)=\prod_{i=1}^{m} p\left(y^{(i)} | x^{(i)} ; w\right) \
=\prod_{i=1}^{m}
s\left(z^{(i)}\right)^{y^{(i)}}\left(1-s\left(z^{(i)}\right)\right)^{1-y^{(i)}}
\end{array}$$取对数, 让累积乘积变累积求和:
$$\begin{array}{l}
\ln L(w)=\ln \left(\prod_{i=1}^{m}
s\left(z^{(i)}\right)^{y^{(i)}}\left(1-s\left(z^{(i)}\right)^{1-y^{(i)}}\right)\right)
\
=\sum_{i=1}^{m}\left(y^{(i)} \ln s\left(z^{(i)}\right)+\left(1-y^{(i)}\right)
\ln \left(1-s\left(z^{(i)}\right)\right)\right)
\end{array}$$要求上式最大值, 取反变成求最小值, 就作为逻辑回归的损失函数(交叉熵损失函数):
$$J(w)=-\sum_{i=1}^{m}\left(y^{(i)} \ln
s\left(z^{(i)}\right)+\left(1-y^{(i)}\right) \ln
\left(1-s\left(z^{(i)}\right)\right)\right)$$利用梯度下降法最终求得
$\vec{w}^{T}$ (省略)
例, 对鸢尾花实现二分类并分析:
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.datasets import load_iris
import warnings
warnings.filterwarnings('ignore')
iris = load_iris()
x, y = iris.data, iris.target
# 鸢尾花数据集有 3 个类别, 4 个特性, 取两个类别, 两个特性
x = x[y!=0, 2:]
y = y[y!=0]
# 拆分训练集与测试集
x_train, x_test, y_train, y_test = train_test_split(x, y,
test_size=0.25, random_state=2)
# 训练分类模型
lr = LogisticRegression()
lr.fit(x_train, y_train)
# 测试
y_hat = lr.predict(x_test)
print('权重:', lr.coef_)
print('偏置:', lr.intercept_)
print('真实值:', y_test)
print('预测值:', y_hat)
权重: [[2.54536368 2.15257324]]
偏置: [-16.08741502]
真实值: [2 1 2 1 1 1 1 1 1 1 2 2 2 1 1 1 1 1 2 2 1 1 2 1 2]
预测值: [2 1 1 1 1 1 1 2 1 1 2 2 2 1 1 1 1 1 2 2 1 1 2 1 2]
# 样本的真实类别可视化
import matplotlib.pyplot as plt
plt.rcParams['font.family'] = 'SimHei'
# 取出两种鸢尾花的特征
c1 = x[y==1]
c2 = x[y==2]
# 绘制样本分布
plt.scatter(x=c1[:, 0], y=c1[:, 1], c='g', label='类别1')
plt.scatter(x=c2[:, 0], y=c2[:, 1], c='r', label='类别2')
plt.xlabel('花瓣长度')
plt.ylabel('花瓣宽度')
plt.title('鸢尾花样本分布')
plt.legend()
plt.show()
# 将预测类别和真实类别可视化对比
plt.figure(figsize=(6, 2.2))
plt.plot(y_test, marker='o', ls='', ms=10, c='r', label='真实类别')
plt.plot(y_hat, marker='x', ls='', ms=10, c='g', label='预测类别')
plt.xlabel('样本序号')
plt.ylabel('类别')
plt.title('预测结果')
plt.legend()
plt.show()
# 因预测样本所属类别时, 通过比较概率得到结果,
# 我们可将结果对应的概率可视化
import numpy as np
# 获取预测的概率值
probability = lr.predict_proba(x_test)
print('概率:', probability[:5], sep='\n')
index = np.arange(len(x_test))
pro_0 = probability[:, 0]
pro_1 = probability[:, 1]
# 设置预测结果标签, 对和错
tick_label = np.where(y_test==y_hat, '对', '错')
# 绘制堆叠图
plt.figure(figsize=(8, 2))
plt.bar(index, height=pro_0, color='g', label='类别1的概率')
plt.bar(index, height=pro_1, color='r', bottom=pro_0,
label='类别2的概率', tick_label=tick_label)
plt.xlabel('预测结果')
plt.ylabel('各类别的概率')
plt.title('分类概率')
plt.legend()
plt.show()
概率:
[[0.46933862 0.53066138]
[0.98282882 0.01717118]
[0.72589695 0.27410305]
[0.91245661 0.08754339]
[0.80288412 0.19711588]]
# 绘制决策边界
# 决策边界: 不同类别的分界线
from matplotlib.colors import ListedColormap
# 定义绘制函数
def plot_decision_boundary(model, x, y):
color = ['r', 'g', 'b']
marker = ['o', 'v', 'x']
class_label = np.unique(y)
cmap = ListedColormap(color[:len(class_label)])
x1_min, x2_min = np.min(x, axis=0)
x1_max, x2_max = np.max(x, axis=0)
x1 = np.arange(x1_min - 1, x1_max + 1, 0.02)
x2 = np.arange(x2_min - 1, x2_max + 1, 0.02)
x1, x2 = np.meshgrid(x1, x2)
z = model.predict(np.array([x1.ravel(), x2.ravel()]).T).reshape(x1.shape)
plt.contourf(x1, x2, z, cmap=cmap, alpha=0.5)
for i, class_ in enumerate(class_label):
plt.scatter(x=x[y==class_, 0], y=x[y==class_, 1],
c=cmap.colors[i], label=class_, marker=marker[i])
plt.legend()
plt.show()
# 绘制模型在训练集上的决策边界
plot_decision_boundary(lr, x_train, y_train)
拓展 :
逻辑回归实现多分类
iris = load_iris()
x, y = iris.data, iris.target
x = x[:, 2:]
x_train, x_test, y_train, y_test = train_test_split(x, y,
test_size=0.25, random_state=2)
lr = LogisticRegression()
lr.fit(x_train, y_train)
# 测试分类
y_hat = lr.predict(x_test)
# 可视化结果
plt.rcParams['axes.unicode_minus']=False
plot_decision_boundary(lr, x_test, y_test)
分类模型评估
在完成模型训练之后,需要对模型的效果进行评估,根据评估结果继续调整模型的参数, 特征或者算法,以达到满意的结果
1, 混淆矩阵
将 真正例(TP), 假正例(FP), 真负例(TN), 假负例(FN) 统计于一个方阵中, 观察比较, 评价模型好坏, 矩阵如下:
混淆矩阵统计数量, 评价不直观也有限, 基于混淆矩阵又延伸出 正确率, 精准率, 召回率, F1(调和平均值), ROC曲线和AUC等
2, 评估指标分析
正确率:
$$\text { 正确率 }=\frac{T P+T N}{T P+T N+F P+F N}$$正确率, 表示总体(包括正负)预测正确的比率, 在模型对正例和负例的预测准确度差异较大时, 难以评价模型的好坏, 例如正例较多, 负例较少,
正例全部预测对了, 负例只预测对几个, 正确率却可能较高
精准率:
$$\text { 精准率 }=\frac{T P}{T P+F P}$$精准率, 表示所有预测为正例的结果中 预测正确的正例 的占比, 精准率越高, 说明正例预测正确概率越高, 因此精准率更关注”一击必中”,
比如通过预测找出上涨的概率很高的一支股票
召回率:
$$\text { 召回率 }=\frac{T P}{T P+F N}$$召回率, 表示所有真实的正例中, 预测正确的正例 的占比, 召回率越高, 说明正例被”召回”的越多, 因此召回率更关注”宁错一千, 不放一个”,
例如通过预测尽可能将新冠肺炎患者全部隔离观察
调和平均值 F1 :
$$F 1=\frac{2 * \text {精准率} * \text {召回率}}{\text {精准率}+\text {召回率}}$$F1 将综合了精准率和召回率, F1越高, 说明模型预测效果越好, F1 能够直接评估模型的好坏
ROC曲线:
ROC (Receiver Operating Characteristic) 曲线, 用图像来描述分类模型的性能好坏. 图像纵轴为 真 正例率(TPR),
横轴为 假 正例率(FPR):
$$\begin{array}{l}
T P R=\text { 召回率 }=\frac{T P}{T P+F N} \
F P R=\frac{F P}{F P+T N}
\end{array}$$上述两式通过取分类模型的不同阈值, 从而计算出不同的值, 绘制出曲线, 曲线必过 (0,0) 和 (1, 1) 两个点, TPR 增长得越快,
曲线越往上凸, 模型的分类性能就越好. 如果 ROC 曲线为对角线, 可将模型理解为随机猜测; 如果 ROC 曲线在 0 点 真 正例率就达到了 1,
此时模型最完美
AUC:
AUC (Area Under the Curve), 是 ROC 曲线下面的面积, 因为有时通过 ROC 曲线看不出哪个分类模型性能好, 而 AUC
比较数值就不存在这样的问题
以鸢尾花数据集做如下练习:
import numpy as np
from sklearn.datasets import load_iris
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import confusion_matrix
import matplotlib.pyplot as plt
import warnings
plt.rcParams["font.family"] = "SimHei"
plt.rcParams["axes.unicode_minus"] = False
plt.rcParams["font.size"] = 12
warnings.filterwarnings("ignore")
iris = load_iris()
x, y = iris.data, iris.target
x = x[y!=0, 2:]
y = y[y!=0]
x_train, x_test, y_train, y_test = train_test_split(x, y,
test_size=0.25, random_state=2)
lr = LogisticRegression()
lr.fit(x_train, y_train)
y_hat = lr.predict(x_test)
# 传入真实值与预测值, 创建混淆矩阵
matrix = confusion_matrix(y_true=y_test, y_pred=y_hat)
print(matrix)
y_hat[y_hat==1].sum()
[[15 1]
[ 1 8]]
16
# 将混淆矩阵可视化
mat = plt.matshow(matrix, cmap=plt.cm.Blues, alpha=0.5)
label = ["负例", "正例"]
# 获取当前的绘图对象
ax = plt.gca()
# 设置属性, 设类别 1 为负例
ax.set(
xticks=np.arange(matrix.shape[1]),
yticks=np.arange(matrix.shape[0]),
xticklabels=label,
yticklabels=label,
title="混淆矩阵可视化\n",
ylabel="真实值",
xlabel="预测值")
# 设置统计值的位置
for i in range(matrix.shape[0]):
for j in range(matrix.shape[1]):
plt.text(x=j, y=i, s=matrix[i, j], va="center", ha="center")
plt.show()
# 计算各个评估指标
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score
print("正确率:", accuracy_score(y_test, y_hat))
# 默认以 1 为正例, 我们将 2 设为正例
print("精准率:", precision_score(y_test, y_hat, pos_label=2))
print("召回率:", recall_score(y_test, y_hat, pos_label=2))
print("F1:", f1_score(y_test, y_hat, pos_label=2))
# 也可以用逻辑回归模型对象的score方法计算正确率
print("score方法计算正确率:", lr.score(x_test, y_test))
正确率: 0.92
精准率: 0.8888888888888888
召回率: 0.8888888888888888
F1: 0.8888888888888888
score方法计算正确率: 0.92
# 还可以用 classification_report 方法直接计算各个指标
from sklearn.metrics import classification_report
print(classification_report(y_true=y_test, y_pred=y_hat))
precision recall f1-score support
1 0.94 0.94 0.94 16
2 0.89 0.89 0.89 9
accuracy 0.92 25
macro avg 0.91 0.91 0.91 25
weighted avg 0.92 0.92 0.92 25
# 绘制 ROC曲线 和计算 AUC
from sklearn.metrics import roc_curve, auc, roc_auc_score
iris = load_iris()
x, y = iris.data, iris.target
x = x[y!=0, 2:]
y = y[y!=0]
x_train, x_test, y_train, y_test = train_test_split(x, y,
test_size=0.25, random_state=2)
# 设置模型参数(有默认值可以不设), 并进行训练
# 不同的参数训练结果不一样, 需要注意参数之间关系
lr = LogisticRegression(multi_class="ovr", solver="liblinear")
# lr = LogisticRegression(multi_class="multinomial")
lr.fit(x_train, y_train)
# 获取样本的概率
probo = lr.predict_proba(x_test)
print('类别 2 的概率:', probo[:, 1][:5])
# 将概率值传入 roc_curve 方法, 从概率中选择若干个值作为阈值
# 同时根据阈值判定正负例, 返回 fpr, tpr 和 阈值 thresholds
fpr, tpr, thresholds = roc_curve(y_true=y_test,
y_score=probo[:, 1], pos_label=2)
# 阈值中的第一个值是第二个值 +1 得到, 为了让让曲线过 0 点
print('阈值:', thresholds)
# 计算 AUC
print('用auc计算:', auc(fpr, tpr))
print('用roc_auc_score计算:', roc_auc_score(y_true=y_test,
y_score=probo[:, 1]))
类别 2 的概率: [0.4663913 0.28570842 0.60050037 0.3758227 0.48450719]
阈值: [1.69092453 0.69092453 0.60050037 0.54308778 0.50384451 0.49358343
0.48450719 0.47242245 0.4663913 0.42043757 0.39590375 0.39413886
0.3843811 0.24698327]
用auc计算: 0.8819444444444444
用roc_auc_score计算: 0.8819444444444444
# 绘制 ROC 曲线
plt.figure(figsize=(6, 2))
plt.plot(fpr, tpr, marker="o", label="ROC曲线")
plt.plot([0,1], [0,1], lw=2, ls="--", label="随机猜测")
plt.plot([0, 0, 1], [0, 1, 1], lw=2, ls="-.", label="完美预测")
plt.xlim(-0.01, 1.02)
plt.ylim(-0.01, 1.02)
plt.xticks(np.arange(0, 1.1, 0.2))
plt.yticks(np.arange(0, 1.1, 0.2))
plt.xlabel("FPR")
plt.ylabel("TPR")
plt.grid()
plt.title(f"ROC曲线, AUC值为:{auc(fpr, tpr):.2f}")
plt.legend()
plt.show()
KNN 算法
1, 关于 KNN
KNN (K-Nearest Neighbor), 即 K 近邻算法, K 个最近的邻居. 当需要预测一个未知样本的时候, 就由与该样本最近的 K
个邻居来决定
KNN 既可以用于分类, 也可用于回归. 用来分类时, 使用 K 个邻居中, 类别数量最多(或加权最多)者, 作为预测结果; 当用来回归分析时, 使用 K
个邻居的均值(或加权均值), 作为预测结果
KNN 算法的原理是: 样本映射到多维空间时, 相似度较高的样本, 距离也会较接近, “近朱者赤近墨者黑”
2, K 值
KNN 算法的 K 值是一个模型训练前就要人为指定的参数 超参数 , 不同于模型内部通过训练数据计算得到的参数. KNN 的超参数, 需要通常通过
交叉验证 的方式来选择最合适的参数组合
K 值的选择非常重要, K 值较小时, 模型预测依赖附近的邻居, 敏感性高, 稳定性低, 容易导致过拟合; 反之, K 值较大, 敏感性低, 稳定性高,
容易欠拟合
K 值在数据量小时, 可以通过遍历所有样本(穷举)的方式找出最近的 K 个邻居, 当数据量庞大时, 穷举耗费大量时间, 此时可以采用 KD树 来找
K 个邻居
3, 交叉验证
KNN 的网格搜索交叉验证: 取不同的 K, 选择不同的距离或权重计算方式等, 将数据分为多个组, 一个组作为测试集, 其他部分作为训练集,
不断循环训练和测试, 对模型进行循环验证, 找出最佳参数组合
4, 距离的度量方式
闵可夫斯基距离:
设 n 维空间中两个点位 X 和 Y:
$X=\left(x_{1}, x_{2}, \ldots \ldots, x_{n}\right)$
$Y=\left(y_{1}, y_{2}, \ldots \ldots, y_{n}\right)$
则阁可夫斯基距离为:
$D(X, Y)=\left(\sum_{i=1}^{n}\left|x_{i}-y_{i}\right|^{p}\right)^{1 / p}$
当 p 为 1 时, 又称 曼哈顿距离 ; 当 p 为 2 时, 称 欧几里得距离
5, 权重
统一权重: K 个邻居权重相同, 不管近远都是 1/K
距离加权权重: K 个邻居的权重, 与他们各自和待测样本的距离成反比, 同时要保证权重之和为 1. 例如 3 个邻居 a, b, c
距离待测样本的距离分别为 a, b 和 c, 则 a 的权重为:
$$\frac{\frac{1}{a}}{\frac{1}{a}+\frac{1}{b}+\frac{1}{c}}=\frac{b c}{b c+a c+a
b}$$b 和 c 同理
6, 数据标准化
样本中的特征通常非常多,由于各特征的性质不同,通常具有不同的量纲(数量级).
当各特征间的量纲相差很大时,如果直接用原始特征值进行分析,就会突出数值较高的特征在综合分析中的作用,相对削弱数值较低特征的作用, 因此需要通过数据标准化,
将量纲统一, 才能客观地描述各个特征对模型的影响程度
线性回归和逻辑回归, 都是通过每个特征与其权重的乘积相加来进行计算, 不进行数据标准化(不考虑正则化), 对每个特征的权重影响较大, 但对结果不会造成影响,
而 KNN 是基于距离计算的, 如果特征的量纲不同, 量纲较大的特征会占据主导地位, 导致忽略量纲较小的特征, 从而对模型性能造成较大影响
7, 算法实现步骤
a, 确定超参数
确定 K
确定距离度量方式
确定权重计算方式
其他超参数
b, 从训练集中选择距离待测样本最近的 K 个样本
c, 根据 K 个样本对待测样本进行预测, 如果遇到多个样本距离相同的情况, 默认选取训练集中靠前的
8, 流水线 Pipline
流水线可以将每个评估器视为一个步骤, 然后将多个步骤作为整体依次执行. 例如数据处理工作较多时, 可能涉及更多步骤, 例如多项式扩展, One-Hot
编码, 特征选择, 数据标准化, 交叉验证等, 分别执行过于繁琐, 我们可以将数据处理与模型训练各个步骤作为一个整体来执行
流水线具有最后一个评估器的所有方法:
a, 当流水线对象调用 fit 方法时, 会从第一个评估器依次调用 fit_transform 方法, 然后到最后一个评估器调用 fit 方法
b, 当流水线对象调用 其他 方法时, 会从第一个评估器依次调用 transform 方法, 然后到最后一个评估器调用 其他 方法
9, 以鸢尾花为例, 对逻辑回归和 KNN 进行比较:
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import classification_report
from sklearn.datasets import load_iris
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
import matplotlib as mpl
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings("ignore")
import numpy as np
mpl.rcParams["font.family"] = "SimHei"
mpl.rcParams["axes.unicode_minus"] = False
iris = load_iris()
X = iris.data[:, :2]
y = iris.target
X_train, X_test, y_train, y_test = train_test_split(X, y,
test_size=0.25, random_state=0)
# 数据标准化: StandardScaler 均值标准差标准化, MinMaxScaler 最大最小值标准化
ss = StandardScaler()
X_train = ss.fit_transform(X_train)
X_test = ss.transform(X_test)
# 逻辑回归训练
lr = LogisticRegression()
lr.fit(X_train,y_train)
# KNN 训练
# n_neighbors: 邻居的数量
# weights:权重计算方式, 可选值为 uniform 统一权重, 与 distance 加权权重
knn = KNeighborsClassifier(n_neighbors=3, weights="uniform")
knn.fit(X_train, y_train)
# 比较 AUC
from sklearn.metrics import roc_curve, auc,roc_auc_score
lr_fpr, lr_tpr, lr_thresholds = roc_curve(y_test,
lr.predict_proba(X_test)[:,1], pos_label=1)
lr_auc = auc(lr_fpr, lr_tpr)
print('Logistic 算法: AUC = %.3f' % lr_auc)
knn_fpr, knn_tpr, knn_thresholds = roc_curve(y_test,
knn.predict_proba(X_test)[:,1], pos_label=1)
knn_auc = auc(knn_fpr, knn_tpr)
print('KNN 算法: AUC = %.3f' % knn_auc)
Logistic 算法: AUC = 0.835
KNN 算法: AUC = 0.794
# 将 KNN 算法参数进行调优再来比较
from sklearn.model_selection import GridSearchCV
# K 值取 1~10, 并定义需要的参数组合
knn = KNeighborsClassifier()
grid = {'n_neighbors': range(1,11,1), 'weights': ['uniform','distance']}
# 网格搜索交叉验证
# param_grid:需要检验的超参数组合
# scoring:模型评估标准, accuracy 正确率
# n_jobs:并发数量
# cv:交叉验证折数
# verbose:输出冗余信息
gs = GridSearchCV(estimator=knn, param_grid=grid, scoring='accuracy',
n_jobs=-1, cv=5, verbose=0)
gs.fit(X_train, y_train)
gs_fpr, gs_tpr, gs_thresholds = roc_curve(y_test,
gs.predict_proba(X_test)[:,1], pos_label=1)
gs_auc = auc(gs_fpr, gs_tpr)
print('KNN 算法: AUC = %.3f' % gs_auc)
KNN 算法: AUC = 0.855
10, 以波士顿房价为例, 对线性回归和 KNN 进行比较:
from sklearn.datasets import load_boston
from sklearn.neighbors import KNeighborsRegressor
from sklearn.linear_model import LinearRegression
X, y = load_boston(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y,
test_size=0.25, random_state=0)
knn = KNeighborsRegressor(n_neighbors=3, weights="distance")
knn.fit(X_train, y_train)
print("KNN 算法 R²:", knn.score(X_test, y_test))
lr = LinearRegression()
lr.fit(X_train, y_train)
print("线性回归算法 R²:", lr.score(X_test, y_test))
KNN 算法 R²: 0.5158073940789912
线性回归算法 R²: 0.6354638433202129
# 对 KNN 数据标准化和参数调优之后再来比较
knn = KNeighborsRegressor()
grid = {'n_neighbors': range(1,11,1), 'weights': ['uniform','distance']}
gs = GridSearchCV(estimator=knn, param_grid=grid, scoring='r2',
n_jobs=-1, cv=5, verbose=0)
# 利用流水线处理
from sklearn.pipeline import Pipeline
# 定义流水线的步骤: 类型为一个列表, 列表中的每个元素是元组类型
# 格式为:[(步骤名1,评估器1), (步骤名2, 评估器2), ……, (步骤名n, 评估器n)
knn_steps = [("scaler", StandardScaler()), ("knn", gs)]
knn_p = Pipeline(knn_steps)
# 可以设置流水线的参数. 所有可用的参数可以通过 get_params 获取
# 设置格式如下: (步骤名__参数)
# p.set_params(knn__n_neighbors=3, knn__weights="uniform")
knn_p.fit(X_train, y_train)
print("KNN 算法 R²:", knn_p.score(X_test, y_test))
# 线性回归数据标准化
lr_steps = [("scaler", StandardScaler()), ("lr", LinearRegression())]
lr_p = Pipeline(lr_steps)
lr_p.fit(X_train, y_train)
print("线性回归算法 R²:", lr_p.score(X_test, y_test))
KNN 算法 R²: 0.6441485149216897
线性回归算法 R²: 0.6354638433202131
朴素贝叶斯
1, 概率基础
样本空间 :
在 随机试验 E 中, 实验的所有可能结果组成的集合, 称为 样本空间 S, 样本空间的每个元素, 即 E 的每个结果, 称 样本点
随机事件 :
进行随机试验时, 满足某种条件的样本点组成的集合, S 的子集, 称作 随机事件 , 只有一个样本点时, 称作 基本事件
概率 :
对于随机事件 A, 概率为:
$P(A)=\frac{A \text { 中基本事件数 }}{S \text { 中基本事件数 }}$
条件概率 :
定义事件 A 发生的前提下, 事件 B 发生的概率 P(B | A) 为条件概率:
$$P(B \mid A)=\frac{P(A B)}{P(A)}$$由条件概率的定义可得, 事件 A 和 B 同时发生的概率 P(AB) 满足如下 乘法定理 :
$$P(A B)=P(B \mid A) P(A)$$独立性:
定义 A 和 B 两个事件, 如果满足:
$$P(A B)=P(A) P(B)$$则称事件 A, B 相互独立. 再结合乘法定理, 则有:
$$P(B \mid A) = P(B)$$全概率公式:
设随机试验 E 的样本空间为 S, 若事件
$B_{1}$,
$B_{2}$,…,
$B_{n}$ 构成一个完备事件组(即它们两两相互独立,事件并集为 S),
且都有正概率,则对任意一个 E 的事件 A,有如下公式成立:
$$P(A)=P\left(A \mid B_{1}\right) P\left(B_{1}\right)+P\left(A \mid
B_{2}\right) P\left(B_{2}\right)+\ldots \ldots+P\left(A \mid B_{n}\right)
P\left(B_{n}\right)$$此公式即为全概率公式. 特别地,对于任意两随机事件 A 和 B,有如下成立:
$$P(B)=P(B \mid A) P(A)+P(B \mid \bar{A}) P(\bar{A})$$贝叶斯公式:
设随机试验 E 的样本空间为 S, 若事件
$B_{1}$,
$B_{2}$,…,
$B_{n}$ 构成一个完备事件组(即它们两两相互独立,事件并集为 S),
且都有正概率,则对任意一个 E 的正概率事件 A,有如下公式成立( i 为 1~n 的正整数):
$$P\left(B_{i} \mid A\right)=\frac{P\left(A B_{i}\right)}{P(A)}=\frac{P\left(A
\mid B_{i}\right) P\left(B_{i}\right)}{P(A)} \
=\frac{P\left(A \mid B_{i}\right) P\left(B_{i}\right)}{\sum_{j=1}^{n} P\left(A
\mid B_{j}\right) P\left(B_{j}\right)}$$贝叶斯公式将求解 P(B | A) 的概率转换成求 P(A | B) 的概率, 在求解某个事件概率非常困难时, 转换一下更方便求解
例: 从以往数据得出, 机器调整良好时生产的产品合格的概率是 98%, 机器故障时合格的概率是 55%, 每天开机时机器调整良好的概率为 95%.
求某日开机生产的第一件产品是合格品时, 机器调整良好的概率?
解: 设事件 A 为产品合格, B 为机器调整良好, 则
$\bar{B}$ 为机器故障
$$P(B \mid A)=\frac{P(A \mid B) P(B)}{P(A \mid B) P(B)+P(A \mid \bar{B})
P(\bar{B})} \
=\frac{0.98 \times 0.95}{0.98 \times 0.95+0.55 \times 0.05} \
=0.97$$先验概率和后验概率:
由以往的数据得出的概率称为 先验概率 , 如上例中的已知概率
得到某些信息后, 在先验概率的基础上进行修正而得到的概率, 称为 后验概率 , 如上例中求解的概率
2, 朴素贝叶斯算法原理
朴素贝叶斯是基于概率的分类算法, 前提假设各个特征(自变量)之间是相互独立的, 设类别(因变量)为 Y, Y 包含 m 个类别(
$y_{1},\ldots, y_{m}$), 特征为 X, X 包含含有 n 个特征 (
$x_{1}, \ldots, x_{n}$), 然后通过计算比较, 在特征 X
确定的前提下, 类别 Y 中每个类别的概率大小, 概率最大者即为预测结果
设 Y 中任意一个类别为 y, 则:
$$P(y \mid X) = P\left(y \mid x_{1}, \ldots, x_{n}\right) \
=\frac{P(y) P\left(x_{1}, \ldots, x_{n} \mid y\right)}{P\left(x_{1}, \ldots,
x_{n}\right)} \
=\frac{P(y) P\left(x_{1} \mid y\right) P\left(x_{2} \mid y\right) \ldots
P\left(x_{n} \mid y\right)}{P\left(x_{1}, \ldots, x_{n}\right)} \
=\frac{P(y) \prod_{i=1}^{n} P\left(x_{i} \mid y\right)}{P\left(x_{1}, \ldots,
x_{n}\right)}$$上式分母为定值, 则:
$$P\left(y \mid X \right) \propto P(y) \prod_{i=1}^{n} P\left(x_{i} \mid
y\right)$$所以最终预测类别
$\hat{y}$ 为分子部分值最大对应的类别:
$$\hat{y}=\arg \max_{y} P(y) \prod_{i=1}^{n} P\left(x_{i} \mid y\right)$$不同的朴素贝叶斯算法, 主要是对
$P\left(x_{i} \mid y\right)$ 的分布假设不同, 进而采取不同的参数估计方式.
最终主要就是通过计算
$P\left(x_{i} \mid y\right)$ 的概率来计算结果
例: 预测第 11 条记录, 学生是否上课
序号 |
天气 |
上课距离 |
成绩 |
课程 |
上课情况 |
1 |
晴 |
远 |
差 |
选修 |
逃课 |
2 |
晴 |
近 |
差 |
必修 |
上课 |
3 |
晴 |
近 |
好 |
必修 |
上课 |
4 |
阴 |
远 |
差 |
选修 |
逃课 |
5 |
阴 |
近 |
好 |
选修 |
上课 |
6 |
阴 |
近 |
好 |
必修 |
上课 |
7 |
雨 |
远 |
差 |
选修 |
逃课 |
8 |
雨 |
近 |
好 |
必修 |
上课 |
9 |
雨 |
近 |
差 |
必修 |
逃课 |
10 |
雨 |
远 |
好 |
选修 |
逃课 |
11 |
阴 |
近 |
差 |
选修 |
? |
12 |
晴 |
远 |
好 |
选修 |
? |
分别计算上课和逃课情况下, 各自的概率:
$$P(y=\text { 上课 }) \prod_{i=1}^{n} P\left(x_{i} \mid y=\text { 上课 }\right) \
=P(y=\text { 上课 }) P\left(x_{1}=\text { 阴 } \mid y=\text { 上课 }\right)
P\left(x_{2}=\text { 近 } \mid y=\text { 上课 }\right) \
P\left(x_{3}=\text {差 } \mid y=\text { 上课 }\right) P\left(x_{4}=\text { 选修 }
\mid y=\text { 上课 }\right) \
=0.5 \times 0.4 \times 1 \times 0.2 \times 0.2 \
=0.008$$
$$P(y=\text { 逃课 }) \prod_{i=1}^{n} P\left(x_{i} \mid y=\text { 逃课 }\right) \
=P(y=\text { 逃课 }) P\left(x_{1}=\text { 阴 } \mid y=\text { 逃课 }\right)
P\left(x_{2}=\text { 近 } \mid y=\text { 逃课 }\right) \
P\left(x_{3}=\text { 差 } \mid y=\text { 逃课 }\right) P\left(x_{4}=\text { 选修 }
\mid y=\text { 逃课 }\right) \
=0.5 \times 0.2 \times 0.2 \times 0.8 \times 0.8 \
=0.0128$$可得预测结果为: 逃课
3, 平滑改进
当我们预测上例中, 第 12 条记录所属的类别时, 因为样本不是总体, 会出现上课的前提下, 距离远的概率为 0, 造成计算结果也为 0, 影响了预测结果,
因此需要平滑改进:
$$ P\left(x_{i} \mid y\right)=\frac{\text { 类别 } y \text { 中 } x_{i} \text {
取某个值出现的次数 }+ \alpha}{\text { 类别别 } y \text { 的总数 }+k * \alpha} $$其中, k 为特征
$x_{i}$ 可能的取值数, α (α ≥ 0) 称为平滑系数, 当 α = 1 时, 称拉普拉斯平滑( Laplace
smoothing)
4, 算法优点
即使训练集数据较少, 也能实现不错的预测; 算法训练速度非常快
因此算法假设特征之间是独立的, 可以单独考虑. 如果训练集有 N 个特征, 每个特征需要 M 个样本来训练, 则只需要训练 N*M 的样本数量,
而不是笛卡儿积的形式指数级增加
常用的朴素贝叶斯有: 高斯朴素贝叶斯, 伯努利朴素贝叶斯, 多项式朴素贝叶斯
5, 高斯朴素贝叶斯
适用于连续变量, 其假定各个特征 x 在各个类别 y 下服从正态分布:
$$x_{i} \sim N\left(\mu_{y}, \sigma_{y}^{2}\right)$$算法使用概率密度函数来计算
$P\left(x_{i} \mid y\right)$ 的概率:
$$P\left(x_{i} \mid y\right)=\frac{1}{\sqrt{2 \pi \sigma_{y}^{2}}} \exp
\left(-\frac{\left(x_{i}-\mu_{y}\right)^{2}}{2 \sigma_{y}^{2}}\right) $$
$\mu_{y}$: 在类别为 y 的样本中, 特征
$x_{i}$ 的均值
$\sigma_{y}$: 在类别为 y 的样本中, 特征
$x_{i}$ 的标件差
import numpy as np
import pandas as pd
from sklearn.naive_bayes import GaussianNB
np.random.seed(0)
x = np.random.randint(0, 10, size=(8, 3))
y = np.array([0, 1, 0, 0, 0, 1, 1, 1])
data = pd.DataFrame(np.concatenate([x, y.reshape(-1, 1)], axis=1),
columns=['x1', 'x2', 'x3', 'y'])
display(data[:3])
gnb = GaussianNB()
gnb.fit(x, y)
print('类别标签:', gnb.classes_)
print('每个类别的先验概率:', gnb.class_prior_)
print('样本数量:', gnb.class_count_)
print('每个类别下特征的均值:', gnb.theta_)
print('每个类别下特征的方差:', gnb.sigma_)
# 测试集
x_test = np.array([[5, 7, 2]])
print('预测结果:', gnb.predict(x_test))
print('预测结果概率:', gnb.predict_proba(x_test))
x1 x2 x3 y
0 5 0 3 0
1 3 7 9 1
2 3 5 2 0
类别标签: [0 1]
每个类别的先验概率: [0.5 0.5]
样本数量: [4. 4.]
每个类别下特征的均值: [[5. 5. 3. ]
[6.5 5.75 7.5 ]]
每个类别下特征的方差: [[3.50000001 9.50000001 3.50000001]
[5.25000001 7.68750001 2.75000001]]
预测结果: [0]
预测结果概率: [[0.99567424 0.00432576]]
6, 伯努利朴素贝叶斯
设实验 E 只有两个可能的结果, A 与
$\bar{A}$, 则称 E 为伯努利试验
伯努利朴素贝叶斯, 适用于离散变量, 其假设各个特征 x 在各个类别 y 下服从 n 重伯努利分布(二项分布), 因伯努利试验仅有两个结果,
算法会首先对特征值进行二值化处理(假设二值化结果为 1 和 0 )
$P\left(x_{i} \mid y\right)$ 的概率为:
$$P\left(x_{i} \mid y\right)=P\left(x_{i}=1 \mid y\right)
x_{i}+\left(1-P\left(x_{i}=1 \mid y\right)\right)\left(1-x_{i}\right)$$在训练集中, 会进行如下评估:
$$ P\left(x_{i}=1 \mid y\right)=\frac{N_{y i}+\alpha}{N_{y}+2 * \alpha} \
P\left(x_{i}=0 \mid y\right)=1-P\left(x_{i}=1 \mid y\right) $$
$N_{y i}$: 第 i 特征中, 属于类别 y, 数值为 1 的样本个数
$N_{y}$: 属于类別 y 的所有样本个数
$\alpha$: 平滑系数
from sklearn.naive_bayes import BernoulliNB
np.random.seed(0)
x = np.random.randint(-5, 5, size=(8, 3))
y = np.array([0, 1, 0, 0, 0, 1, 1, 1])
data = pd.DataFrame(np.concatenate([x, y.reshape(-1, 1)], axis=1),
columns=['x1', 'x2', 'x3', 'y'])
display(data[:3])
bnb = BernoulliNB()
bnb.fit(x, y)
# 统计每个类别下, 特征中二值化后, 每个特征下值 1 出现的次数
print('值 1 出现的次数:', bnb.feature_count_)
# 每个类别的先验概率, 算法得到的该概率值是取对数后的结果,
# 需要取指数还原
print('每个类别的先验概率:', np.exp(bnb.class_log_prior_))
# 每个类别下, 每个特征的概率(也需要取指数还原)
print('每个特征的概率:', np.exp(bnb.feature_log_prob_))
# 测试集
x_test = np.array([[-5, 0, 2]])
print('预测结果:', bnb.predict(x_test))
print('预测结果概率:', bnb.predict_proba(x_test))
x1 x2 x3 y
0 0 -5 -2 0
1 -2 2 4 1
2 -2 0 -3 0
值 1 出现的次数: [[1. 2. 1.]
[3. 3. 3.]]
每个类别的先验概率: [0.5 0.5]
每个特征的概率: [[0.33333333 0.5 0.33333333]
[0.66666667 0.66666667 0.66666667]]
预测结果: [0]
预测结果概率: [[0.6 0.4]]
7, 多项式朴素贝叶斯
多项式朴素贝叶斯, 适用于离散变量, 其假设各个特征 x 在各个类别 y 下服从多项式分布(每个特征下的值之和, 就是该特征发生(出现)的次数),
因此每个特征值不能是负数
$P\left(x_{i} \mid y\right)$ 的概率为:
$$P\left(x_{i} \mid y\right)=\frac{N_{y i}+\alpha}{N_{y}+\alpha n} $$
$N_{y i}$: 特征 i 在类别 y 的样本中发生(出现)的次数
$N_{y}$: 类别 y 的样本中, 所有特征发生(出现)的次数
$n$: 特征数量
$\alpha$: 平滑系数
from sklearn.naive_bayes import MultinomialNB
np.random.seed(0)
x = np.random.randint(1, 5, size=(8, 3))
y = np.array([0, 1, 0, 0, 0, 1, 1, 1])
data = pd.DataFrame(np.concatenate([x, y.reshape(-1, 1)], axis=1),
columns=['x1', 'x2', 'x3', 'y'])
display(data[:3])
mnb = MultinomialNB()
mnb.fit(x, y)
# 每个类别的样本数量
print('每个类别的样本数:', mnb.class_count_)
# 每个特征在每个类别下发生(出现)的次数
print('每个特征发生(出现)次数:', mnb.feature_count_)
# 每个类别下, 每个特征的概率(需要取指数还原)
print('每个类别下特征的概率:', np.exp(mnb.feature_log_prob_))
# 测试集
x_test = np.array([[1, 1, 4]])
print('预测结果:', mnb.predict(x_test))
print('预测结果概率:', mnb.predict_proba(x_test))
x1 x2 x3 y
0 1 4 2 0
1 1 4 4 1
2 4 4 2 0
每个类别的样本数: [4. 4.]
每个特征发生(出现)次数: [[10. 14. 10.]
[ 9. 11. 11.]]
每个类别下特征的概率: [[0.2972973 0.40540541 0.2972973 ]
[0.29411765 0.35294118 0.35294118]]
预测结果: [1]
预测结果概率: [[0.36890061 0.63109939]]
利用鸢尾花数据集比较上述 3 个贝叶斯算法:
对不同的数据集, 根据其分布情况选择适合的算法, 能得到更好的结果
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
x, y = load_iris(return_X_y=True)
x_train, x_test, y_train, y_test = train_test_split(x, y,
test_size=0.25, random_state=0)
models = [('高斯朴素贝叶斯分值:', GaussianNB()),
('伯努利朴素贝叶斯分值:', BernoulliNB()),
('多项式朴素贝叶斯分值:', MultinomialNB())]
for name, m in models:
m.fit(x_train, y_train)
print(name, m.score(x_test, y_test))
高斯朴素贝叶斯分值: 1.0
伯努利朴素贝叶斯分值: 0.23684210526315788
多项式朴素贝叶斯分值: 0.5789473684210527
决策树
1, 概念理解
决策树: 通过数据特征的差别, 用已知数据训练将不同数据划分到不同分支(子树)中, 层层划分, 最终得到一个树型结构, 用来对未知数据进行预测,
实现分类或回归
例如, 有如下数据集, 预测第 11 条数据能否偿还债务:
序号 |
有无房产 |
婚姻状况 |
年收入 |
能否偿还债务 |
1 |
是 |
单身 |
125 |
能 |
2 |
否 |
已婚 |
100 |
能 |
3 |
否 |
单身 |
100 |
能 |
4 |
是 |
已婚 |
110 |
能 |
5 |
是 |
离婚 |
60 |
能 |
6 |
否 |
离婚 |
95 |
不能 |
7 |
否 |
单身 |
85 |
不能 |
8 |
否 |
已婚 |
75 |
能 |
9 |
否 |
单身 |
90 |
不能 |
10 |
是 |
离婚 |
220 |
能 |
11 |
否 |
已婚 |
94 |
? |
我们可以将已知样本作如下划分(训练), 构建一颗决策树, 然后将第 11 条数据代入(测试), 落在哪一个叶子中, 它就是对应叶子的类别: 预测结果是
能
上例中, 层级已经不可再分, 但如果只划分到婚姻状况就不再划分如何实现预测?
决策树实现预测:
对于分类树, 叶子节点中, 哪个类别样本数量最多, 就将其作为未知样本的类别
对于回归树, 使用叶子节点中, 所有样本的均值, 作为未知样本的结果
对于上例, 如果只划分到婚姻状况, 那对于婚姻状况这个叶子中, 不能偿还的最多, 预测结果就是 不能
2, 分类决策树
对上例出现的情况, 我们会有如下问题:
我们为什么以年收入开始划分, 依据是什么? 划分顺序怎么定?
年收入为什么选 97.5 为划分阈值?
要划分多少层才好, 是否越多越好?
等等…
下面一步步来作讨论:
2.01, 信息熵
信息熵 : 用来描述信源的不确定度, 不确定性越大, 信息熵越大. 例如, 明天海南要下雪, 不确定性非常小, 信息熵很小, 明天海南要下雨,
不确定性大, 信息熵就大
设随机变量 X 具有 m 个特征值, 各个值出现的概率为
$p_{1}$, …,
$p_{m}$, 且
$$p_{1}+p_{2}+\cdots+p_{m} = 1$$则变量 X 的信息熵(信息期望值)为:
$$H(X)=-p_{1} \log_{2} p_{1} -p_{2} \log_{2} p_{2}-\cdots -p_{m} \log_{2}
p_{m}$$
$$ =-\sum_{i=1}^{m}p_{i}\log_{2}p_{i}$$2.02, 概率分布与信息商的关系
假设明天下雨的概率从 0.01 ~ 0.99 递增, 那么不下雨的概率就从 0.99 ~ 0.01 递减, 看看概率分布和信息熵的关系:
import numpy as np
import matplotlib.pyplot as plt
plt.rcParams['font.family'] = 'SimHei'
plt.rcParams['font.size'] = 14
# 下雨的概率
p = np.linspace(0.01, 0.99, 100)
# 信息熵
h = -p * np.log2(p) - (1-p) * np.log2(1-p)
# 绘制关系图
plt.plot(p, h)
plt.xlabel('概率分布')
plt.ylabel('信息熵')
plt.title('概率分布和信息熵关系图')
plt.show()
可见, 概率分布越均衡, 不确定性越大, 信息熵越大, 在所有概率都相等(p下雨=p不下雨)时, 信息熵最大
如果把概率分布转换到决策树的数据集上, 信息熵体现的就是数据的 不纯度 , 即样本类别的均衡程度. 因为数据集是未分类的, 要把它分类,
样本类别越均衡, 各个类别的占比(概率)分布越均衡, 不纯度越高, 信息熵越大
2.03, 信息增益
信息增益的定义如下:
$$I G\left(D_{p}, f\right)=I\left(D_{p}\right)-\sum_{j=1}^{n}
\frac{N_{j}}{N_{p}} I\left(D_{j}\right) $$
$f$: 划分的特征
$D_{p}$: 父节点, 即使用特征 f 分割之前的节点
$I G\left(D_{p}, f\right)$: 父节点
$D_{p}$ 使用特征 f 划分下, 获得的信息增益
$I\left(D_{p}\right)$:父节点不纯度, 信息熵是度量标准之一
$D_{j}$: 父节点
$D_{p}$ 经过分割之后, 会产生 n 个子节点,
$D_{j}$ 为第 j 个子节点
$I\left(D_{j}\right)$:子节点不纯度
$N_{p}$: 父节点
$D_{p}$ 包含样本的数量
$N_{j}$: 第 j 个子节点
$D_{j}$ 包含样本的数量
如果是二叉树, 即父节点最多分为左右两个子节点, 此时, 信息增益为:
$$I G\left(D_{p}, f\right)=I\left(D_{p}\right)-\frac{N_{l e f t}}{N_{p}}
I\left(D_{l e f t}\right)-\frac{N_{r i g h t}}{N_{p}} I\left(D_{r i g h
t}\right)$$可见, 信息增益就是父节点的不纯度减去所有子节点的(加权)不纯度
父节点的不纯度是不变的, 在选择特征进行类别划分时, 应该让子节点的不纯度尽可能低, 这样训练可以更快完成, 信息增益也最大. 这正是训练决策树时,
选择特征顺序的依据
以开头的例子为例, 不纯度使用信息熵度量, 则根节点的信息熵为:
$$I\left(D_{p}\right)=-0.7 * \log _{2} 0.7-0.3 * \log _{2} 0.3=0.88$$如果以”有无房产”划分, 则可计算得子节点信息熵:
$$\begin{array}{l}
I\left(D_{\text {有房产 }}\right)=0 \
I\left(D_{\text {无房产 }}\right)=1
\end{array}$$从而可得根节点信息增益为:
$$I G(\text { 有无房产 })=0.88-0.4 * 0-0.6 * 1=0.28 $$同理,
$$I G(\text { 婚姻状况 })=0.205 $$而对于年收入, 将年收入排序后, 取不同类别的分界点年收入(75 与 85, 95 与 100)的均值进行划分, 比较哪一个信息增益大:
$$\begin{array}{l}
I\left(D_{\text {年收入 }< 80}\right)=0 \
I\left(D_{\text { 年收入 } >=80}\right)=0.954 \
I G(\text { 年收入 }=80)=0.88-0.2 * 0-0.8 * 0.954=0.117
\end{array}$$同理,
$$I G(\text { 年收入 }=97.5)=0.395$$可见, 以 年收入=97.5 划分时, 信息增益最大, 故首先选它进行划分
根节点划分结束, 第二层的父节点以同样的方式计算之后再次划分, 一直到划分停止
2.04, 过拟合与欠拟合
如果不设置条件, 是不是划分深度越大越好呢?
下面以鸢尾花数据集为例, 看看划分深度对模型效果的影响:
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
x, y = load_iris(return_X_y=True)
x = x[:, :2]
x_train, x_test, y_train, y_test = train_test_split(
x, y, test_size=0.25, random_state=0)
'''
参数介绍:
criterion: 不纯度度量标准, 默认基尼系数 gini, 信息熵为 entropy
splitter: 选择划分节点的方式, 默认最好 best, 随机 random
max_depth: 划分深度, 默认 None 不设限
min_samples_split: 划分节点的最小样本数, 默认 2
min_samples_leaf: 划分节点后, 叶子节点的最少样本数, 默认 1
max_features: 划分节点时, 考虑的最大特征数, 默认 None 考虑所有, 设置数量后会随机选择
random_state: 随机种子, 控制模型的随机行为
'''
tree = DecisionTreeClassifier()
# 定义列表, 用来储存不同深度下, 模型的分值
train_score = []
test_score = []
# 设置深度 1~12 开始训练
for depth in range(1, 13):
tree = DecisionTreeClassifier(criterion='entropy',
max_depth=depth, random_state=0)
tree.fit(x_train, y_train)
train_score.append(tree.score(x_train, y_train))
test_score.append(tree.score(x_test, y_test))
plt.plot(train_score, label='训练集分值')
plt.plot(test_score, label='测试集分值')
plt.xlabel('划分深度')
plt.ylabel('分值')
plt.legend()
plt.show()
可见, 划分深度小, 训练集和测试集的分值都小, 容易欠拟合
随着划分深度的增加, 分值都在增加, 模型预测效果也在变好
当深度增加到一定程度, 深度再增加, 训练集分值随着增加, 但造成了模型过分依赖训练集数据特征, 从而测试集分值减小, 容易过拟合
3, 不纯度度量标准
不纯度度量标准有:
信息熵
$$I_{H}(D)=-\sum_{i=1}^{m} p(i \mid D) \log _{2} p(i \mid D) $$m: 节点 D 中含有样本的类别数量
$p(i \mid D)$: 节点 D 中, 属于类别 i 的样本占节点 D 中样本总数的比例(概率)
基尼系数
$$I_{G}(D)=1-\sum_{i=1}^{m} p(i \mid D)^{2}$$错误率
$$I_{E}(D)=1-\max {p(i \mid D)}$$看看各个度量标准与概率分布的关系:
def entropy(p):
return -p * np.log2(p) - (1-p) * np.log2(1-p)
def gini(p):
return 1 - p**2 - (1-p)**2
def error(p):
return 1 - np.max([p, 1-p], axis=0)
p = np.linspace(0.0001, 0.9999, 200)
en = entropy(p)
er = error(p)
g = gini(p)
for i, lab, ls in zip([en, g, er],
['信息熵', '基尼系数', '错误率'],
['-', ':', '--']):
plt.plot(p, i, label=lab, linestyle=ls, lw=2)
plt.legend()
plt.xlabel('概率分布')
plt.ylabel('不纯度')
plt.show()
可见, 无论选哪一种度量标准, 样本属于同一类, 不纯度都是 0; 样本中不同类别占比相同时, 不纯度最大
4, 决策树常用算法介绍
ID3
ID3 (Iterative Dichotomiser3), 迭代二分法特点:
-使用多叉树结构
-使用信息熵作为不纯度度量, 选择信息增益最大的特征划分
-经典算法, 简单, 训练快
局限:
-不支持连续特征
-不支持缺失值处理
-不支持回归
-倾向选择特征取值多的特征来划分, 例如按身份证号划分, 一个号码就是一个特征
C4.5
ID3算法改进而来, 特点:
-使用多叉树结构
-不支持回归
优化:
-支持缺失值处理
-连续值进行离散化处理
-信息熵作为不纯度度量, 但选择 信息增益率 最大的特征划分
信息增益率:
$$I G_{\text {Ratio}}\left(D_{p}, f\right)=\frac{I G_{H}\left(D_{p},
f\right)}{I_{H}(f)} $$
$I_{H}(f)$: 在特征
$f$ 下, 取各个特征值计算得到的信息熵之和, 其实就是特征
$f$ 的不纯度, 特征值越多, 特征不纯度越大
选择信息增益最大的特征来划分, 父节点的信息熵不变, 就要求信息增益的第二项 $\sum_{j=1}^{n} \frac{N_{j}}{N_{p}}
I\left(D_{j}\right)$ 最小, 从而会倾向选择特征取值多的特征
因为, 特征取值多, 通常划分之后子节点的不纯度(信息熵)就更低, 例如极端情况, 选身份证划分, 划分之后不管什么类别, 子节点都只有一种类别,
不纯度都是 0, 第二项就是 0, 信息增益就最大
因此, 采用信息增益率, 将 信息增益/特征不纯度, 就避免了 特征不纯度大 造成 信息增益大 而选择类别多的特征来划分的情况
看看类别数量与信息熵的关系:
en = lambda p: np.sum(-p * np.log2(p))
a1 = np.array([0.3, 0.7])
a2 = np.array([0.3, 0.3, 0.4])
a3 = np.array([0.25] * 4)
a4 = np.array([0.1] * 10)
print(en(a1), en(a2), en(a3), en(a4), sep='\n')
0.8812908992306927
1.5709505944546684
2.0
3.321928094887362
CART
CART (Classification And Regression Tree), 分类回归树, 特点如下:
-使用二叉树结构
-支持连续值与缺失值处理
-分类时, 使用基尼系数作为不纯度度量, 选择基尼增益最大的特征划分
-回归时, 使用 MSE 或 MAE 最小的特征划分
5, 回归决策树
回归决策树因变量 y 是连续的, 使用叶子节点的均值来预测未知样本, 使用 MSE 或 MAE 作为特征划分的评估指标
在 scikit-learn 中, 使用的是优化的 CART 算法来实现决策树
以波士顿房价为例来实现(参数参考分类决策树):
from sklearn.datasets import load_boston
from sklearn.tree import DecisionTreeRegressor
from sklearn.model_selection import train_test_split
x, y = load_boston(return_X_y=True)
x_train, x_test, y_train, y_test = train_test_split(
x, y, test_size=0.25, random_state=1)
tree = DecisionTreeRegressor(max_depth=5, random_state=0)
tree.fit(x_train, y_train)
print(tree.score(x_train, y_train))
print(tree.score(x_test, y_test))
0.9204825770764915
0.8763987309111113
K-Means 算法
1, 聚类
前面接触的算法, 都是 监督学习 , 即训练数据中自变量(特征)和因变量(结果)都是已知的, 用含有结果的训练集建立模型,
然后对未知结果的数据进行预测
聚类属于 无监督学习 , 训练数据中没有”已知结果的监督”. 聚类的目的, 就是通过已知样本数据的特征, 将数据划分为若干个类别,
每个类别成一个类簇, 使得同一个簇内的数据相似度越大, “物以类聚”, 不同簇之间的数据相似度越小, 聚类效果越好
聚类的样本相似度根据距离来度量
2, K-Means
即 K 均值算法, 是常见的聚类算法, 该算法将数据集分为 K 个簇, 每个簇使用簇内所有样本的均值来表示, 该均值称为”质心”
K-Means 算法的目标, 就是选择适合的质心, 使得每个簇内, 样本点距质心的距离尽可能的小, 从而保证簇内样本有较高相似度
算法实现步骤:
a, 从样本中选择 K 个点作为初始质心
b, 计算每个样本点到各个质心的距离, 将样本点划分到距离最近的质心所对应的簇中
c, 计算每个簇内所有样本的均值, 使用该均值作为新的质心
d, 重复 b 和 c, 重复一定次数质心一般会趋于稳定, 如果达到以下条件, 重复结束:
– 质心位置变化小于指定的阈值
– 达到最迭代环次数
对于算法的实现步骤, 我们有几个重要的疑问:
– 1.怎么评价质心是否达到了最佳位置?
– 2.初始质心随机选, 还是选在哪里?
– 3. K 值怎么定?
3, 算法优化目标
样本的相似度是根据距离来度量的, 一般使用簇内 误差平方和 (within-cluster SSE 簇惯性) 来作为优化算法的目标函数,
距离常用欧氏距离, 优化目标就是使 SSE 最小化:
$$S S E=\sum_{i=1}^{k}
\sum_{j=1}^{m_{i}}\left(\left|x_{j}-\mu_{i}\right|^{2}\right)$$k: 族的数量
$m_{i}$: 第 i 个簇含有的样本数量
${\mu}_{i}$: 第 i 个族的质心
$\left|x_{j}-\mu_{i}\right|$: 第 i 个族中,每个样本
$x_{j}$ 与质心
$\mu_{i}$ 的距离
同一个数据集, 相同的簇数, SSE 越小, 通常质心位置更佳, 算法模型更好
4, 初始质心的影响
初始质心可以随机选择, 但由于算法是通过迭代初始质心一步步实现, 初始质心的位置受随机性影响, 算法训练的最终结果也会受到影响
import numpy as np
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs
import matplotlib.pyplot as plt
plt.rcParams['font.family'] = 'Microsoft YaHei'
plt.rcParams['font.size'] = 12
plt.rcParams['axes.unicode_minus'] = False
'''
生成数据:
n_samples: 样本数量
n_features: 特征数
centers: 聚类中心
cluster_std: 簇的标准差, 可以统一指定, 也分别指定
'''
centers = [[1, 1], [5, 2], [2, 5]]
x, y = make_blobs(n_samples=90,
n_features=2,
centers=centers,
cluster_std=[2.2, 2.5, 2],
random_state=0)
# x 是特征, y 是类别标签
# 绘制原始数据
plt.figure(figsize=(12,8))
plt.subplot(221)
colors = np.array(['Coral', 'SeaGreen', 'RoyalBlue'])
plt.scatter(x[:, 0], x[:, 1], c=colors[y], marker='.', label='原始数据')
plt.title('原始数据')
# 定义绘制聚类结果的函数
def plot_cluster(model, train, test=None):
global colors # 使用上面的颜色
cc = model.cluster_centers_ # 获取质心
label = model.labels_ # 获取聚类结果的标签
# 绘制质心
plt.scatter(cc[:, 0], # 质心的 x 坐标
cc[:, 1], # 质心的 y 坐标
marker='*',
s=150,
c=colors)
# 绘制训练集
plt.scatter(train[:, 0], train[:, 1], marker='.', c=colors[label])
# 绘制测试集
if test is not None:
y_hat = model.predict(test)
plt.scatter(test[:, 0], test[:, 1], marker='+',
s=150, c=colors[y_hat])
# 标题
plt.title(f'SSE:{model.inertia_:.1f} 迭代次数:{model.n_iter_}')
# 测试集
test = np.array([[6, 5]])
# 绘制不同初始质心的聚类结果
seed = [1, 10, 100]
for i in range(2, 5):
plt.subplot(2, 2, i)
kmeans = KMeans(n_clusters=3, # 簇数
init='random', # 初始化方式
n_init=1, # 初始化质心组数
random_state=seed[i-2])
kmeans.fit(x)
plot_cluster(kmeans, x)
# 测试结果
plot_cluster(kmeans, x, test)
从上图可以看出受初始化质心的影响, 聚类效果(SSE) 与 收敛速度(迭代次数) 会不同, 也即是可能会收敛到局部最小, 而不是整体最优; 同时,
也可以看出 SSE 越小, 整体结果越优, 越接近原始数据
5, K-Means++ 优化
针对上述初始化质心造成的问题, 设置初始化多组质心可以得到缓解, 但通常限于聚类簇数较少的情况, 如果簇数较多, 可能就不会有效
于是有了 K-Means++, 选择初始化质心时, 不在随机选, 而是按下述步骤进行选择:
– 1, 从训练数据中随机选择一个样本点, 作为初始质心
– 2, 对任意一个非质心样本点
$x^{(i)}$, 计算
$x^{(i)}$ 与现有最近质心的距离
$D\left(x^{(i)}\right)$
– 3, 根据概率
$\frac{D\left(x^{(i)}\right)^{2}}{\sum_{j=1}^{m}D\left(x^{(j)}\right)^{2}}$ 最大, 来选择最远的一个样本点
$x^{(i)}$ 作为质心, m 为非质心样本点数量
– 4, 重复 2 和 3, 直到选择了 K 个质心为止
做了优化之后, 保证了初始质心不会集中, 而是分散在数据集中
下面试试 K-Means++ 的聚类效果:
kmeans = KMeans(n_clusters=3, init='k-means++', n_init=1)
kmeans.fit(x)
plot_cluster(kmeans, x)
6, 确定 K 值
K 是超参数, 需要预先人为指定
有时需要按照建模的需求和目的来选择聚类的个数, 但是 K 值选择不当, 聚类效果可能不佳. 例如实际 3 类, K 选了 10, 或者 K 无限制,
取值和样本点个数一样, 最后每个点一个类, SEE 为 0, 但是聚类已经毫无意义
如果不是硬性要求 K 的取值, 怎么确定最佳的 K 值呢? 一个比较好的方法就是 肘部法则 :
SEE 需要越小越好, K 又不能取太大, 我们可以看看他们之间的关系:
# 设置列表储存 SSE
sse = []
# K 值从 1~9 变化
scope = range(1, 10)
for k in scope:
kmeans = KMeans(n_clusters=k)
kmeans.fit(x)
sse.append(kmeans.inertia_)
plt.xticks(scope)
plt.plot(scope, sse, marker='o')
plt.show()
从上图可以看出, K 增加, SSE 减小, 但当 K > 3 时, K 再增加, SSE 减小变得缓慢, 所以 K 选择 3, 实际情况也是 3
6, Mini Batch K-Means
K-Means 每次迭代都会使用所有数据参与运算, 当数据集较大时, 会比较耗时. Mini Batch K-Means (小批量 K-Means)
算法每次迭代使用小批量样本训练, 逐批次累计的方式进行计算, 从而大大减少计算时间. 效果上, 通常只是略差于 K-Means
Mini Batch K-Means 算法实现步骤:
a, 从数据集中随机选择部分数据, 使用 K-Means 算法在这部分数据上聚类, 获取质心
b, 再从数据集中随机选择部分数据, 分别分配给最近的质心
c, 每个簇根据现有的数据集更新质心
d, 重复 b 和 c, 直到质心变化小于指定阈值或达到最大迭代次数
下面比较一下两个算法:
import time
import pandas as pd
from sklearn.cluster import MiniBatchKMeans
from sklearn.metrics.pairwise import pairwise_distances_argmin
import warnings
warnings.filterwarnings('ignore')
# 生成数据
centers = [[1, 1], [400, 100], [100, 400]]
x, y = make_blobs(n_samples=8000, n_features=2, centers=centers,
cluster_std=120, random_state=0)
# 定义函数, 用于计算模型训练时间
def elapsed_time(model, data):
start = time.time()
model.fit(data)
end = time.time()
return end - start
n_clusters = len(centers)
kmeans = KMeans(n_clusters=n_clusters)
mbk = MiniBatchKMeans(n_clusters=n_clusters,
batch_size=200, # 小批量的大小
n_init=10 # 和 KMeans 统一为 10
)
kmeans_time = elapsed_time(kmeans, x)
mbk_time = elapsed_time(mbk, x)
print('K-Means耗时:', kmeans_time)
print('Mini Batch K-Means耗时:', mbk_time)
# 绘制聚类效果
plt.figure(figsize=(12, 5))
model = [kmeans, mbk]
for i, m in enumerate(model, start=1):
plt.subplot(1, 2, i)
plot_cluster(m, x)
K-Means耗时: 0.051812171936035156
Mini Batch K-Means耗时: 0.04886937141418457
可见, 聚类耗时 K-Means 更多, 如果数据量很大, 耗时会更明显, 而聚类效果基本一样. 但发现颜色对不上, 这是因为质心的随机性,
聚类之后质心虽然最终落在相同的位置, 但是顺序不一致, 从而聚类的结果标签不一致, 即使是同一个算法, 运行几次, 标签结果也会不一致
我们将相同簇用相同的颜色绘制:
plt.figure(figsize=(12, 5))
# 定义列表, 用来保存两个模型预测结果
y_hat_list = []
for i, m in enumerate(model, start=1):
plt.subplot(1, 2, i)
y_hat = m.predict(x)
if m == mbk:
'''
因为输出的质心顺序就是训练结果标签的顺序
故可以按 mbk 训练的质心, 去找 kmeans 训练的相同簇的质心
pairwise_distances_argmin(x, y) 解释:
依次取出数组 X 中的元素 x,
计算找到数组 Y 中与 x 距离最近的元素 y 的索引,
返回索引构成的数组
'''
# 将两者相同簇的质心一一对应并按 mbk 质心的顺序封装成字典
ar = pairwise_distances_argmin(
mbk.cluster_centers_, kmeans.cluster_centers_)
dict_ = dict(enumerate(ar))
# 用 mbk 的训练结果标签 y_hat 就可以寻找到对应的 kmeans 的质心
y_hat = pd.Series(y_hat).map(dict_).values
# 将预测结果加入到列表中
y_hat_list.append(y_hat)
plt.scatter(x[:, 0], x[:, 1], c=colors[y_hat], marker='.')
比较两个算法聚类结果的差异:
same = y_hat_list[0] == y_hat_list[1]
diff = y_hat_list[0] != y_hat_list[1]
plt.scatter(x[same, 0], x[same, 1], c='g', marker='.', label='预测相同')
plt.scatter(x[diff, 0], x[diff, 1], c='r', marker='.', label='预测不同')
plt.legend()
print('相同数量:', x[same].shape[0])
print('不同数量:', x[diff].shape[0])
相同数量: 7967
不同数量: 33
两个算法聚类结果只有 33 个样本点不同