前言: 什么是Python? 优缺点?Python是一个叫龟叔的荷兰老头写的(89年)擅长: 1.爬虫 2.自动化 3.科学计算 4.人工智能;
deepseek给出的简介如下:
Python是一种简洁易读的高级编程语言,以动态类型和解释执行为特点,凭借丰富的标准库和第三方生态(如Django、NumPy、TensorFlow等),广泛应用于Web开发、数据分析、人工智能及自动化领域。其"用缩进定义代码块"的设计哲学降低了学习门槛,同时支持面向对象、函数式编程范式,被誉为平衡开发效率与工程化的"胶水语言",适用于快速原型开发和企业级项目构建。
重要的话
Make English as your working language. (让英语成为你的工作语言)
Practice makes perfect. (熟能生巧)
All experience comes from the mistakes you've made. (所有的经验都源于你犯过的错误)
Don't be a freeloader. (不要当伸手党)
Either outstanding or out. (要么出众,要么出局)
语法基础
注释:#→ 单行注释;"""→ 多行注释
变量:可以发生改变的一个量,变量是用来区分不同数据的,可以指向一个内存空间,帮我们存储一些数据
变量的命名规范:
①必须是数字或字母或下换线组成,
②不能是数字开头,更不能是纯数字
③不能用python的关键字
④不要用中文
⑤不要太长
⑥要有意义
⑦推荐使用下换线命名或者驼峰命名
常量:
字面量:“abc” , 123 ,True ,False ,None
MAX_COUNTS = 5000 PI = 3.1415926
数据类型:区分不同的数据,不同的数据类型应该有不同的操作
数字:+-*/
布尔:条件判断 True False
字符串:"abc" , 'abc'
操作:
+ 左右两端必须是字符串,表示字符串连接操作
* 表示字符串重复操作,一个字符串只能乘以一个数字,表示字符串重复的次数
1 2 3 4 5 6 7 8 name = '张三' looks = "好帅!" print (name + looks)print (name * 3 )
语句
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 if x > 0 : print ("x is positive" ) if x > 0 : print ("x is positive" ) else : print ("x is non-positive" ) if x > 10 : print ("x > 10" ) elif x > 7 : print ("7 < x <= 10" ) else : print ("x < 7" )
1 2 3 4 5 6 7 8 9 10 11 12 def http_response_code_v0 (): status_code = int (input ("模拟http的响应码:" )) match status_code: case 400 | 405 : description = 'invalid request' case 401 | 403 | 404 : description = 'not allowed' case 418 : description = 'I am a teapot' case 429 : description = 'too many requests' case _: description = 'unknown status code' print ('状态码描述:' , description)
1 2 3 4 i = 0 while i < 100 : print (i) i = i + 1
1 2 3 4 5 6 7 8 9 name = "zhangsan" for c in name: print (c) for j in range (10 ): print (j) for k in range (3 , 10 ): print (k)
pass
pass 是一个特殊的占位符关键字,用于表示“什么都不做”。它的主要作用是满足语法要求,避免代码块为空时引发语法错误。
1 2 3 4 5 6 7 8 9 10 11 def my_func (): pass class MyClass : pass x = 5 if x > 3 : print ("x > 3" ) else : pass
字符串
1 2 3 4 5 6 7 8 9 10 11 12 name = input ("请输入你的名字:" ) address = input ("请输入你的住址:" ) age = int (input ("请输入你的年龄:" )) hobby = input ("请输入你的爱好:" ) s = "我叫%s, 我住在%s, 我今年%d岁, 我喜欢做%s" % (name, address, age, hobby) print (s)s1 = "我叫{}, 我住在{}, 我今年{}岁, 我喜欢做{}" .format (name, address, age, hobby) print (s1)s2 = f"我叫{name} , 我住在{address} , 我今年{age} 岁, 我喜欢做{hobby} " print (s2)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 s = "我叫周杰伦" print (s[0 ])print (s[1 ])print (s[2 ])print (s[3 ])print (s[4 ])print (s[-1 ])print (s[-2 ])print (s[-3 ])print (s[-4 ])print (s[-5 ])print (s[2 :5 ]) print (s[:5 ]) print (s[3 :]) print (s[:]) print (s[-5 :-1 ]) s = "我爱你" print (s[::-1 ])s = "abcdefghijklmnopqrstuvwxyz" print (s[1 :15 :2 ]) print (s[-1 :-10 :-2 ])
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 s = "python" s1 = s.capitalize() print (s1)s = "I have a dream!" s1 = s.title() print (s1)s = "I HAVE A DREAM!" s1 = s.lower() print (s1)s = "i have a dream" s1 = s.upper() print (s1)varify_code = "xAd1" user_input = input (f"请输入验证码{varify_code} :" ) if user_input.upper() == varify_code.upper(): print ("验证码正确" ) else : print ("验证码错误" )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 s = " 我是周润发 " s = s.strip() print (s)username = input ("请输入用户名:" ) password = input ("请输入密码:" ) if username.strip() == "admin" and password.strip() == "123456" : print ("登录成功" ) else : print ("登陆失败" ) s = "i am a good man" s = s.replace(" " , "" ) print (s)s = "c++_c_java_c#_javascript_python" slist = s.split("_" ) print (slist)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 s = "我是周润发,你是刘德华" ret = s.find("刘德华123" ) print (ret)print ("周润发" in s) print ("周润发134" not in s) s = "1234e" if s.isdigit(): s = int (s) print ("可以花钱了" ) else : print ("不可以花钱" ) print (len (s))print (len ("我叫周润发" ))list = ["刘德华" , "周润发" , "张三" , "李四" ]s = "_" .join(list ) print (s)
列表
在python中用门来表示一个列表,列表中的元素通过,隔开
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 lst = [] lst.append("刘德华" ) lst.append("周润发" ) print (lst)lst.insert(0 , "赵本山" ) print (lst)lst.extend(["张三" , "李四" ]) print (lst)ret = lst.pop() print (ret)lst.remove("张三" ) print (lst)lst[0 ] = "赵敏" print (lst[2 ])lst = ["赵敏" , "张绍刚" , "赵本山" , "张无忌" , "武则天" , "赢政" , "马超" ] for i in range (0 , len (lst)): item = lst[i] if item.startswith("张" ): lst[i] = "王" + item[1 :] print (lst)lst = [22 , 0 , 9 , 54 , 100 , 99 , 5 , 3 ] lst.sort() print (lst)lst.sort(reverse=True ) print (lst)lst = ["abc" , "def" , ["呵呵哒" , "妈妈呀" , "苦苦脊瓦" , ["可乐" , "scrapy" , 123 ]], "aed" , "qpr" ] lst[2 ][3 ][1 ] = lst[2 ][3 ][1 ].upper() print (lst)lst = ["赵敏" , "张绍刚" , "赵本山" , "张无忌" , "武则天" , "赢政" , "马超" ] tmplst = [] for item in lst: if item.startswith("张" ): tmplst.append(item) for item in tmplst: lst.remove(item) print (lst)
元组
tuple元组,特点:不可变的列表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 t = ("周芷若" , "张三丰" , "张无忌" , "赵敏" ) print (type (t))print (t)print (t[0 ])print (t[1 :3 ])print (t)t = ("哈哈哈哈" ,) print (t)t = (1 , 2 , 3 , 4 , ["zhang" , "wang" , "zhao" , "li" ]) t[4 ].append("tang" ) print (t)
set集合
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 s = {1 , 2 , 3 , "zhan" } print (type (s))print (s)s = set () lst = list () t = tuple () ss = str () s.add("zhan" ) s.add("li" ) s.add("wang" ) s.add("tang" ) print (s)s.remove("tang" ) print (s)s.remove("zhan" ) s.add("zhan-san" ) print (s)for item in s: print (item) s1 = {'刘能' , '赵四' , '皮常山' } s2 = {'皮常山' , '冯乡长' , '刘科长' } print (s1 & s2)print (s1.intersection(s2))print (s1 | s2)print (s1.union(s2))print (s1 - s2)print (s1.difference(s2))lst = ["zhan" , 1 , 1 , 2 , "wang" , "zhao" , "zhan" ] print (lst)print (set (lst))print (list (set (lst)))
字典
首先,字典是以键值对的形式进行存储数据的
字典的表示方式:{key:value,key2:value,key3:value}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 dic = {'zhan' : '张三' , 'zhao' : '赵四' } print (type (dic))print (dic)val = dic['zhan' ] print (val)dic = {'wang' : ['1' , '2' , '3' , '4' ]} print (dic)dic = dict () dic['zhan' ] = '周杰伦' dic[1 ] = 13 print (dic)dic['zhan' ] = '刘德华' print (dic)dic.setdefault('zhao' , '赵华' ) print (dic)dic.setdefault('zhan' , 'ABC' ) print (dic)dic.pop('zhan' ) dic.pop(1 ) print (dic)print (dic['zhao' ])print (dic.get('zhao' ))print (dic.get('zhao10010' )) a = None print (type (a)) print (a) dic = { '赵四' : '特别能歪嘴' , '刘能' : '老,老四啊。。。' , '大脚' : '跟这个跟哪个搞对象' , '大脑袋' : '瞎折腾.....' , } name = 'hh' val = dic.get(name) if val is None : print ('我们村没有这个人!' ) else : print (val) for key in dic: print (key, dic[key]) print (dic.keys())print (type (dic.keys()))print (list (dic.keys()))print (dic.values())print (type (dic.values()))print (list (dic.values()))print (type (dic.items()))print (dic.items())for item in dic.items(): key, val = item print ('{' + f"{key} : {val} " + '}' ) a, b = (1 , 2 ) print (a, b)for k, v in dic.items(): print (k, v) wangfeng = { 'name' : '汪峰' , 'age' : 18 , 'wife' : { 'name' : '章子怡' , 'hobby' : '演戏' , 'assistant' : { 'name' : '樵夫' , 'age' : 19 , 'hobby' : '打游戏' } }, 'children' : [ {'name' : '孩子1' , 'age' : 10 }, {'name' : '孩子2' , 'age' : 8 }, {'name' : '孩子3' , 'age' : 11 }, ] } name = wangfeng['wife' ]['assistant' ]['name' ] print (name)wangfeng['children' ][1 ]['age' ] += 1 print (wangfeng)dic = { '赵四' : '特别能歪嘴' , '刘能' : '老,老四啊。。。' , '大脚' : '跟这个跟哪个搞对象' , '大脑袋' : '瞎折腾.....' , } tmp = [] for key in dic.keys(): if key.startswith('大' ): tmp.append(key) for k in tmp: dic.pop(k) print (dic)
bytes
字符集和编码
①ascii: 8bit, 1byte
②gbk: 16bit,2byte windows默认
③unicode:32bit,4byte(没法用,只是一个标准)
④utf-8: mac默认
英文:8bit,1byte
欧洲:16bit,2byte
中文:24bit,3byte
示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 s = '周杰伦' s1 = s.encode('gbk' ) print (type (s1))print (s1)s2 = s.encode('utf-8' ) print (s2)b1 = b'\xd6\xdc\xbd\xdc\xc2\xd7' s = b1.decode('gbk' ) print (s)b2 = s.encode('utf-8' ) print (b2)s = "你好abc呵呵哒" print (s.encode('utf-8' ))for i in range (len (s)): print (s[i])
运算符
①算数运算
+ - * / % //(地板除)
②比较运算
> < >= <= == !=
③赋值运算
= += -= *= /= %= //=
④逻辑运算
and or not
记住运算顺序:先算括号>算not>and>or
⑤成员运算
in: 判断xxx是否在xxxx中出现了
not in:判断xxx是否环在xxxx中出现了
文件操作
api
open(文件路径,mode=“”,encoding=“”)
文件路径:
①绝对路径 d:/test/xxxx.txt
②相对路径 相对于当前你的程序所在的文件夹
mode:
①r:只读(read)
②w:写(write)
③a:追加(append)
④b:读写的是非文本文件,此时encoding无需写
with:上下文,不需要手动去关闭一个文件
示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 import osf = open ("./file.py" , mode="r" , encoding="utf-8" ) for line in f: print (line.strip()) f = open ("嫩模.md" , mode="w" , encoding="utf-8" ) f.write("胡辣汤" ) f.close() f = open ("嫩模.md" , mode="w" , encoding="utf-8" ) lst = ["汪峰" , "汪峰" , "汪峰" , "汪峰" , "汪峰" ] for item in lst: f.write(item) f.write("\n" ) f.close() f = open ("嫩模.md" , mode="a" , encoding="utf-8" ) f.write("这是一个追加的内容" ) f.close() with open ("嫩模.md" , mode="r" , encoding="utf-8" ) as f: for line in f: print (line.strip()) with open ("beauty.jpg" , mode="rb" ) as f1,\ open ("beauty_bak.jpg" , mode="wb" ) as f2: for line in f1: f2.write(line) with open ("username.txt" , mode="r" , encoding="utf-8" ) as f1, \ open ("username_bak.txt" , mode="w" , encoding="utf-8" ) as f2: for line in f1: line = line.strip() if line.startswith("周" ): line = line.replace("周" , "张" ) f2.write(line) f2.write("\n" ) os.remove("username.txt" ) os.renames("username_bak.txt" , "username.txt" )
函数
基础部分
函数:对某一个特定的功能或者代码块进行封装,在需要使用该功能的时候直接调用即可
定义:
def 函数的名字():
被封装的功能或者代码块->函数体
调用:
函数的名字()
参数:可以在函数调用的时候,给函数传递一些信息分类:
形参,在函数定义的时候,需要准备一些变量来接收信息
①位置参数,按照位置一个一个的去声明变量
②默认值参数,在函数声明的时候给变量一个默认值,如果实参不传递信息。此时默认值生效,否则就不生效
③动态传参
a:*args,表示接收所有的位置参数的动态传参
b:**kwargs,表示接收所有的关键字的动态传参
顺序:位置>*args>默认值>**kwargs
实参,实际在调用的时候传递的信息
①位置参数:按照位置进行传递参数
②关键字参数:按照参数的名字进行传递参数
③混合参数
顺序:位置参数放前面,关键字参数放后面一>否则报错!官方不让这么干
实参在执行的时候,必须要保障形参有数据
返回值:函数执行之后,会给调用方一个结果,这个结果就是返回值
关于return:
函数只要执行到了return,函数就会立即停止并返回内容,函数内的return的后续的代码不会执行
①如果函数内没有return,此时外界收到的是None
②如果写了return:
a.只写了return,后面不跟数据,此时接收到的依然是None
b.return值,此时表示函数有一个返回值,外界能够收到一个数据
c.return值1,值2,值3… 此时函数有多个返回值,外界收到的是元组,并且该元组内存放所有的返回值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 def simple_calc (a, b, opt ): if opt == '+' : print (a + b) elif opt == '-' : print (a - b) elif opt == '*' : print (a * b) elif opt == '/' : print (a / b) else : print ("OpError: opt is not a valid op." ) simple_calc(2 , 3 , "+" ) simple_calc(2 , 3 , "abc" ) simple_calc(2 , 3 , "/" ) def eat (*food ): print (food) eat("主食" , "西红柿" , "水果" , "甜品" ) def hobby (**hobbies ): print (hobbies) hobby(animal="cat" , sport="basketball" ) stu_lst = ["张三" , "李四" , "王五" , "赵六" , "刘七" ] def func (*args ): print (args) func(*stu_lst)
内置函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 def simple_calc (a, b, opt ): if opt == '+' : print (a + b) elif opt == '-' : print (a - b) elif opt == '*' : print (a * b) elif opt == '/' : print (a / b) else : print ("OpError: opt is not a valid op." ) simple_calc(2 , 3 , "+" ) simple_calc(2 , 3 , "abc" ) simple_calc(2 , 3 , "/" ) def eat (*food ): print (food) eat("主食" , "西红柿" , "水果" , "甜品" ) def hobby (**hobbies ): print (hobbies) hobby(animal="cat" , sport="basketball" ) stu_lst = ["张三" , "李四" , "王五" , "赵六" , "刘七" ] def func (*args ): print (args) func(*stu_lst) i = 3 is_true = bool (i) f = float (i) s = str (i) comp = complex (i) print (type (comp))print (comp)a = 18 print (bin (a))print (oct (a))print (hex (a))print (int (0b00100010001 ))a = 3 b = 10 print (pow (10 , 3 ))print (b ** a)lst = [1 , 4 , 7 , 10 , 100 , 99 , 23 ] print (min (lst))print (max (lst))print (sum (lst))sl = slice (1 , 5 , 2 ) print ("哈哈哈哈哈哈哈哈哈哈哈哈" [sl])a = 18 print (format (a, "08b" ))print (format (a, "o" ))print (format (a, "x" ))a = "中" print (ord (a))print (chr (20013 ))print (all (["1" , 0 , "豆沙包" ]))print (any ([0 , "1" , "呵呵哒" ]))lst = ["张翠山" , "张无忌" , "张三丰" , "张大大" ] for item in enumerate (lst): print (item) for index, item in enumerate (lst): print (index, item) s = "呵呵哒" print (hash (s))print (dir (s))
函数的嵌套
gobal:在局部,引入全局变量
nonlocal:在局部,引入外层的局部变量,向外找一层,看看有没有该变量,如果有就引入,如果没有,继续向外一层,直到全局(不包括)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 def outer (): def inner (): print ("这是一个内部函数的打印" ) print (inner) return inner caller = outer() print (caller)caller() def myexec (call ): call() def target_fun (): print ("target is running..." ) myexec(target_fun)
闭包
闭包:本质,内层函数对外层函数的局部变量的使用,此时内层函数被称为闭包函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 def outer (): a = 10 def inner (): nonlocal a a += 1 return a return inner call = outer() ret = call() print (ret)ret = call() print (ret)
装饰器
回顾相关认识
函数可以做为参数进行传递
函数可以作为返回值进行返回
函数名称可以当成变量一样进行赋值操作
装饰器:
装饰器本质上是一个闭包
作用:
在不改变原有函数调用的情况下,给函数增加新的功能
直白:可以在函数前后添加新功能,但是不改原来的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 def play_dnf (): print ("我正在玩dnf这款游戏..." ) def play_lol (): print ("我正在玩lol这款游戏..." ) print ("方式1:没法玩外挂" )play_dnf() play_lol() def game_decorator (play_game ): def inner (): print ("开启外挂" ) play_game() print ("关闭外挂" ) return inner print ("新模式开启==============" )play_lol = game_decorator(play_lol) play_lol() play_dnf = game_decorator(play_dnf) play_dnf() @game_decorator def play_wukong (): print ("我正在玩黑猴,黑风山杀的正起...." ) play_wukong() def game_wrapper (game ): def inner (*args, **kwargs ): print ("开启游戏外挂..." ) ret = game(*args, **kwargs) print ("关闭游戏外挂..." ) return ret return inner @game_wrapper def play_dnf (username, password ): print (f"用户名:{username} ,密码:{password} ,登录中..." ) print ("我要开始玩dnf...." ) return "一把绝世好剑" @game_wrapper def play_lol (username, password, role ): print (f"用户名:{username} , 密码:{password} , 英雄角色:{role} , 登录中..." ) play_lol("张三" , "1234" , "daju" ) ret = play_dnf("李四" , "1234" ) print (ret)login_flag = False def login (): global login_flag print ("请你先登录" ) while True : username = input ("用户名>>>" ) password = input ("密㊙码>>>" ) if username == "admin" and password == "123" : login_flag = True print ("登录成功!" ) break else : print ("登录失败!" ) def login_verify (fn ): def inner (*args, **kwargs ): if not login_flag: login() ret = fn(*args, **kwargs) return ret return inner @login_verify def add (): print ("添加信息" ) @login_verify def update (): print ("更新信息" ) @login_verify def delete (): print ("删除信息" ) @login_verify def search (): print ("查询信息" ) add() update() delete() search()
迭代器
可送代的数据类型都会提供一个叫送代器的东西,这个送代器可以帮我们把数据类型中的所有数据逐一的拿到
获取送代器的两种方案:
iter()内置函数可以直接拿到选代器
__iter__() 特殊方法
从送代器中拿到数据:
next()内置函数
__next__()特殊方法
for里面一定是要拿送代器的,所以所有不可迭代的东西不能用for循环
总结:送代器统一了不同数据类型的遍历工作
生成器
生成器的本质就是迭代器
创建生成器的两种方案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 lst = [i for i in range (10 )] print (lst)lst = [i for i in range (10 ) if i % 2 == 1 ] print (lst)lst = [f"cloth{i} " for i in range (50 )] print (lst)lst1 = ['allen' , 'joy' , 'marin' , 'joe' , 'kevin' ] lst2 = [item.upper() for item in lst1] print (lst2)st = {i for i in range (10 )} print (st)lst = ['赵本山' ,'潘长江' ,'高达' ,'赵敏' ] dic = {i:lst[i] for i in range (len (lst))} print (dic)gen = (i**2 for i in range (10 )) lst = list (gen) print (lst)for i in gen: print (i)
yield:
只要函数中出现了yield,它就是一个生成器函数
作用:
①可以返回数据
②可以分段的执行函数中的内容,通过__next__()可以执行到下一个yield位置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 def order (): lst = [] for i in range (1000 ): print (f"衣服{i} " ) lst.append(f"衣服{i} " ) if (len (lst) == 50 ): yield lst lst = [] gen = order() print (gen.__next__())print (gen.__next__())
lambda匿名函数
1 2 3 4 5 fn = lambda a, b: a + b ret = fn(3 , 5 ) print (ret)
python内置函数_下
sorted: 排序
filter: 过滤
map: 映射
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 lst1 = ['赵本山' , '范伟' , '苏有朋' ] lst2 = [30 , 23 , 18 ] lst3 = ['卖拐' , '耳朵大有福' , '情深深雨蒙蒙' ] result = zip (lst1, lst2, lst3) lst = list (result) print (lst)a = 388 print (locals ())def fun_local (): b = 53 print (locals ()) fun_local() print (globals ())def fun_global (): c = 15 print (globals ()) fun_global() lst = [16 , 22 , 68 , 1 , 147 , 256 , 49 ] s = sorted (lst, reverse=True ) print (s)print (lst)lst = ['春' , '秋' , '张二嘎' , '比克' , '卡卡洛特' , '超级宇宙无敌大帅B' ] s = sorted (lst, key=lambda x: len (x)) print (s)lst = [ {'id' : 1 , 'name' : '周润发' , 'age' : 18 , 'salary' : 5200 }, {'id' : 2 , 'name' : '周星驰' , 'age' : 28 , 'salary' : 511100 }, {'id' : 3 , 'name' : '周海媚' , 'age' : 48 , 'salary' : 451230 }, {'id' : 4 , 'name' : '周伯通' , 'age' : 12 , 'salary' : 54311 }, {'id' : 5 , 'name' : '周大兴' , 'age' : 58 , 'salary' : 54211 }, {'id' : 6 , 'name' : '周有辣' , 'age' : 35 , 'salary' : 53210 }, {'id' : 7 , 'name' : '周扒皮' , 'age' : 47 , 'salary' : 520 }, ] s = sorted (lst, key=lambda dic: dic['age' ]) print (s)s = sorted (lst, key=lambda dic: dic['salary' ], reverse=True ) print (s)lst = ['小狐仙' , '灭绝小师太' , '张翠山' , '张无忌' , '张三丰' ] f = filter (lambda item: item.startswith('张' ), lst) print (list ( f ))lst = [1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 ] m = map (lambda x: x * x, lst) print (list ( m ))
引入:现如今,我们使用的计算机早已是多 CPU 或多核的计算机,而我们使用的操作系统基本都支持“多任务”,这使得我们可以同时运行多个程序,也可以将一个程序分解为若干个相对独立的子任务,让多个子任务“并行”或“并发”的执行,从而缩短程序的执行时间,同时也让用户获得更好的体验。因此当下,不管用什么编程语言进行开发,实现“并行”或“并发”编程已经成为了程序员的标配技能。为了讲述如何在 Python 程序中实现“并行”或“并发”,我们需要先了解两个重要的概念:进程和线程。
进程和线程
进程
简单的说,进程是操作系统分配存储空间的基本单位,每个进程都有自己的地址空间、数据栈以及其他用于跟踪进程执行的辅助数据;操作系统管理所有进程的执行,为它们合理的分配资源。一个进程可以通过 fork 或 spawn 的方式创建新的进程来执行其他的任务,不过新的进程也有自己独立的内存空间,因此两个进程如果要共享数据,必须通过进程间通信机制来实现,具体的方式包括管道、信号、套接字等。
线程
一个进程还可以拥有多个执行线索,简单的说就是拥有多个可以获得 CPU 调度的执行单元,这就是所谓的线程。由于线程在同一个进程下,它们可以共享相同的上下文,因此相对于进程而言,线程间的信息共享和通信更加容易。当然在单核 CPU 系统中,多个线程不可能同时执行,因为在某个时刻只有一个线程能够获得 CPU,多个线程通过共享 CPU 执行时间的方式来达到并发的效果。
两个核心概念
并发
并发通常是指同一时刻只能有一条指令执行,但是多个线程对应的指令被快速轮换地执行。比如一个处理器,它先执行线程 A 的指令一段时间,再执行线程 B 的指令一段时间,再切回到线程 A 执行一段时间。由于处理器执行指令的速度和切换的速度极快,人们完全感知不到计算机在这个过程中有多个线程切换上下文执行的操作,这就使得宏观上看起来多个线程在同时运行,但微观上其实只有一个线程在执行。
并行
并行是指同一时刻,有多条指令在多个处理器上同时执行,并行必须要依赖于多个处理器,不论是从宏观上还是微观上,多个线程可以在同一时刻一起执行的。很多时候,我们并不用严格区分并发和并行两个词,所以我们有时候也把 Python 中的多线程、多进程以及异步 I/O 都视为实现并发编程的手段,但实际上前面两者也可以实现并行编程,当然这里还有一个全局解释器锁(GIL)的问题,我们稍后讨论。
多线程编程
Python 标准库中threading模块的Thread类可以帮助我们非常轻松的实现多线程编程。我们用一个联网下载文件的例子来对比使用多线程和不使用多线程到底有什么区别,代码如下所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 import randomimport threadingimport timedef download (*, filename ): start = time.time() print (f'开始下载 {filename} ....' ) time.sleep(random.randint(1 , 3 )) print (f'{filename} 下载完成.' ) end = time.time() print (f'下载耗时:{end - start:.3 f} 秒.' ) def calc_download_using_single_thread (): start = time.time() download(filename='Python从入门到住院.pdf' ) download(filename='MySQL从删库到跑路.avi' ) download(filename='Linux从精通到放弃.mp4' ) end = time.time() print (f'总耗时:{end - start:.3 f} 秒.' ) def calc_download_using_multi_threads (): threads = [ threading.Thread(target=download, kwargs={'filename' : 'Python从入门到住院.pdf' }), threading.Thread(target=download, kwargs={'filename' : 'MySQL从删库到跑路.avi' }), threading.Thread(target=download, kwargs={'filename' : 'Linux从精通到放弃.mp4' }) ] start = time.time() for thread in threads: thread.start() for thread in threads: thread.join() end = time.time() print (f'总耗时:{end - start:.3 f} 秒.' ) if __name__ == '__main__' : calc_download_using_single_thread() calc_download_using_multi_threads()
使用 Thread 类创建线程对象
相关参数的使用
线程启动后会执行target参数指定的函数,当然前提是获得 CPU 的调度;如果target指定的线程要执行的目标函数有参数,需要通过args参数为其进行指定,对于关键字参数,可以通过kwargs参数进行传入。Thread类的构造器还有很多其他的参数,我们遇到的时候再为大家进行讲解,目前需要大家掌握的,就是target、args和kwargs。
1 2 3 4 5 6 7 8 9 import threadingdef simple_task (): print ("这是一个简单的线程任务" ) thread = threading.Thread(target=simple_task) thread.start() thread.join()
1 2 3 4 5 6 7 8 9 10 11 12 import threadingdef task_with_args (name, age ): print (f"线程收到参数: 姓名={name} , 年龄={age} " ) thread = threading.Thread( target=task_with_args, args=("张三" , 25 ) ) thread.start() thread.join()
1 2 3 4 5 6 7 8 9 10 11 12 import threadingdef task_with_kwargs (name, age, city ): print (f"线程收到参数: 姓名={name} , 年龄={age} , 城市={city} " ) thread = threading.Thread( target=task_with_kwargs, kwargs={'name' : '李四' , 'age' : 30 , 'city' : '北京' } ) thread.start() thread.join()
1 2 3 4 5 6 7 8 9 10 11 12 13 import threadingdef mixed_args_task (name, age, city, country ): print (f"线程收到参数: 姓名={name} , 年龄={age} , 城市={city} , 国家={country} " ) thread = threading.Thread( target=mixed_args_task, args=('王五' , 40 ), kwargs={'city' : '上海' , 'country' : '中国' } ) thread.start() thread.join()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import threadingdef task_with_return (a, b ): return a + b result_container = [] def wrapper_func (a, b, container ): container.append(a + b) thread = threading.Thread( target=wrapper_func, args=(10 , 20 , result_container) ) thread.start() thread.join() print (f"线程计算结果: {result_container[0 ]} " )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import threadingdef task_with_return (a, b ): return a + b result_container = [] def wrapper_func (a, b, container ): container.append(a + b) thread = threading.Thread( target=wrapper_func, args=(10 , 20 , result_container) ) thread.start() thread.join() print (f"线程计算结果: {result_container[0 ]} " )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import threadingimport timedef named_task (): print (f"线程 '{threading.current_thread().name} ' 正在运行" ) time.sleep(1 ) thread = threading.Thread( target=named_task, name="我的自定义线程" ) thread.start() thread.join()
继承 Thread 类自定义线程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 """ 继承 Thread 类自定义线程 除了上面的代码展示的创建线程的方式外,还可以通过继承Thread类并重写run()方法的方式来自定义线程,具体的代码如下所示。 """ class DownloadThread (threading.Thread): """下载类线程""" def __init__ (self, filename ): self .filename = filename super ().__init__() def run (self ) -> None : start = time.time() print (f'开始下载 {self.filename} ....' ) time.sleep(random.randint(1 , 3 )) print (f'{self.filename} 下载完成.' ) end = time.time() print (f'下载耗时:{end - start:.3 f} 秒.' ) def test_download_thread (): threads = [ DownloadThread('Python从入门到住院.pdf' ), DownloadThread('MySQL从删库到跑路.avi' ), DownloadThread('Linux从精通到放弃.mp4' ), ] start = time.time() for thread in threads: thread.start() for thread in threads: thread.join() end = time.time() print (f'总耗时: {end - start:.3 f} 秒.' )
使用线程池
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 from concurrent.futures import ThreadPoolExecutordef test_thread_pool (): with ThreadPoolExecutor(max_workers=8 ) as pool: filenames = [ 'Python从入门到住院.pdf' , 'MySQL从删库到跑路.avi' , 'Linux从精通到放弃.mp4' ] start = time.time() for filename in filenames: pool.submit(download, filename=filename) end = time.time() print (f'总耗时: {end - start:.3 f} 秒.' )
守护线程
所谓“守护线程”就是在主线程结束的时候,不值得再保留的执行线程。这里的不值得保留指的是守护线程会在其他非守护线程全部运行结束之后被销毁,它守护的是当前进程内所有的非守护线程。简单的说,守护线程会跟随主线程一起挂掉,而主线程的生命周期就是一个进程的生命周期。如果不理解,我们可以看一段简单的代码。
1 2 3 4 5 6 7 8 9 10 def daemon_display (content ): while True : print (content, end='\n' , flush=True ) time.sleep(0.1 ) def test_daemon_thread (): threading.Thread(target=daemon_display, args=('Ping' , ), daemon=True ).start() threading.Thread(target=daemon_display, args=('Pong' , ), daemon=True ).start() time.sleep(2 )
资源竞争
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Account : """银行账户""" def __init__ (self ): self .balance = 0.0 self .lock = threading.RLock() def deposit (self, money ): with self .lock: new_balance = self .balance + money time.sleep(0.01 ) self .balance = new_balance def test_multithreads_account (): account = Account() with ThreadPoolExecutor(max_workers=8 ) as pool: for _ in range (100 ): pool.submit(account.deposit, 1 ) print (account.balance)
GIL问题
如果使用官方的 Python 解释器(通常称之为 CPython)运行 Python 程序,我们并不能通过使用多线程的方式将 CPU 的利用率提升到逼近400%(对于4核 CPU)或逼近800%(对于8核 CPU)这样的水平,因为 CPython 在执行代码时,会受到 GIL(全局解释器锁)的限制。具体的说,CPython 在执行任何代码时,都需要对应的线程先获得 GIL,然后每执行100条(字节码)指令,CPython 就会让获得 GIL 的线程主动释放 GIL,这样别的线程才有机会执行。因为 GIL 的存在,无论你的 CPU 有多少个核,我们编写的 Python 代码也没有机会真正并行的执行。
GIL 是官方 Python 解释器在设计上的历史遗留问题,要解决这个问题,让多线程能够发挥 CPU 的多核优势,需要重新实现一个不带 GIL 的 Python 解释器。这个问题按照官方的说法,在 Python 发布4.0版本时会得到解决,就让我们拭目以待吧。当下,对于 CPython 而言,如果希望充分发挥 CPU 的多核优势,可以考虑使用多进程,因为每个进程都对应一个 Python 解释器,因此每个进程都有自己独立的 GIL,这样就可以突破 GIL 的限制。在下一个章节中,我们会为大家介绍关于多进程的相关知识,并对多线程和多进程的代码及其执行效果进行比较。
创建进程
由于 GIL 的存在,CPython 中的多线程并不能发挥 CPU 的多核优势,如果希望突破 GIL 的限制,可以考虑使用多进程。对于多进程的程序,每个进程都有一个属于自己的 GIL,所以多进程不会受到 GIL 的影响。那么,我们应该如何在 Python 程序中创建和使用多进程呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from multiprocessing import Process, current_processimport timedef sub_task (content, nums ): print (f'PID: {current_process().pid} ' ) print (f'Name: {current_process().name} ' ) counter, total = 0 , nums.pop(0 ) print (f'Loop count: {total} ' ) time.sleep(0.5 ) while counter < total: counter += 1 print (f'{counter} : {content} ' ) time.sleep(0.01 ) def test_sub_task (): nums = [20 , 30 , 40 ] Process(target=sub_task, args=('Ping' , nums)).start() Process(target=sub_task, args=('Pong' , nums)).start() sub_task('Good' , nums)
多进程和多线程的比较
对于爬虫这类 I/O 密集型任务来说,使用多进程并没有什么优势;但是对于计算密集型任务来说,多进程相比多线程,在效率上会有显著的提升,我们可以通过下面的代码来加以证明。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 import concurrent.futuresfrom functools import wrapsPRIMES = [ 1116281 , 1297337 , 104395303 , 472882027 , 533000389 , 817504243 , 982451653 , 112272535095293 , 112582705942171 , 112272535095293 , 115280095190773 , 115797848077099 , 1099726899285419 ] * 5 def calc_times (func ): @wraps(func ) def wrapper (*args, **kwargs ): start = time.time() result = func(*args, **kwargs) end = time.time() print (f'\033[31m{func.__name__} : {end - start:.3 f} seconds\033[0m' ) return result return wrapper def is_prime (n ): """判断素数""" for i in range (2 , int (n ** 0.5 ) + 1 ): if n % i == 0 : return False return n != 1 @calc_times def test_multi_threads (): with concurrent.futures.ThreadPoolExecutor(max_workers=16 ) as executor: for number, prime in zip (PRIMES, executor.map (is_prime, PRIMES)): print (f'{number} is prime: {prime} ' ) @calc_times def test_multi_process (): with concurrent.futures.ProcessPoolExecutor(max_workers=16 ) as executor: for number, prime in zip (PRIMES, executor.map (is_prime, PRIMES)): print (f'{number} is prime: {prime} ' ) if __name__ == '__main__' : test_multi_threads() test_multi_process()
进程间通信
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from multiprocessing import Process, Queuedef sub_task (content: str , queue: Queue ): counter = queue.get() while counter < 50 : print (content, end='\n' , flush=True ) counter += 1 queue.put(counter) time.sleep(0.01 ) counter = queue.get() def test_sub_task_count50times (): queue = Queue() queue.put(0 ) p1 = Process(target=sub_task, args=('Ping' , queue)) p1.start() p2 = Process(target=sub_task, args=('Pong' , queue)) p2.start() while p1.is_alive() and p2.is_alive(): pass queue.put(50 )
并发中四个重要概念
同步与异步的关注点是消息通信机制 ,最终表现出来的是“有序”和“无序”的区别;阻塞和非阻塞的关注点是程序在等待消息时状态 ,最终表现出来的是程序在等待时能不能做点别的。
阻塞
阻塞状态指程序未得到所需计算资源时被挂起的状态。程序在等待某个操作完成期间,自身无法继续处理其他的事情,则称该程序在该操作上是阻塞的。阻塞随时都可能发生,最典型的就是 I/O 中断(包括网络 I/O 、磁盘 I/O 、用户输入等)、休眠操作、等待某个线程执行结束,甚至包括在 CPU 切换上下文时,程序都无法真正的执行,这就是所谓的阻塞。
非阻塞
程序在等待某操作过程中,自身不被阻塞,可以继续处理其他的事情,则称该程序在该操作上是非阻塞的。非阻塞并不是在任何程序级别、任何情况下都可以存在的。仅当程序封装的级别可以囊括独立的子程序单元时,它才可能存在非阻塞状态。显然,某个操作的阻塞可能会导程序耗时以及效率低下,所以我们会希望把它变成非阻塞的。
同步
不同程序单元为了完成某个任务,在执行过程中需靠某种通信方式以协调一致,我们称这些程序单元是同步执行的。例如前面讲过的给银行账户存钱的操作,我们在代码中使用了“锁”作为通信信号,让多个存钱操作强制排队顺序执行,这就是所谓的同步。
异步
不同程序单元在执行过程中无需通信协调,也能够完成一个任务,这种方式我们就称之为异步。例如,使用爬虫下载页面时,调度程序调用下载程序后,即可调度其他任务,而无需与该下载任务保持通信以协调行为。不同网页的下载、保存等操作都是不相关的,也无需相互通知协调。很显然,异步操作的完成时刻和先后顺序并不能确定。
生成器和协程
前面我们说过,异步编程是一种“协作式并发”,即通过多个子程序相互协作的方式提升 CPU 的利用率,从而减少程序在阻塞和等待中浪费的时间,最终达到并发的效果。我们可以将多个相互协作的子程序称为“协程”,它是实现异步编程的关键。在介绍协程之前,我们先通过下面的代码,看看什么是生成器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 def fib (max_count ): a, b = 0 , 1 for _ in range (max_count): a, b = b, a + b yield a def test_fib (): for val in fib(20 ): print (val, end=' ' ) print () """生成器经过预激活,就是一个协程,它可以跟其他子程序协作。""" def calc_avg (): total, counter = 0 , 0 avg_value = None while True : curr_value = yield avg_value total += curr_value counter += 1 avg_value = total / counter def test_calc_avg (): obj = calc_avg() obj.send(None ) for _ in range (5 ): print (obj.send(_))
异步函数
Python 3.5版本中,引入了两个非常有意思的元素,一个叫async,一个叫await,它们在Python 3.7版本中成为了正式的关键字。通过这两个关键字,可以简化协程代码的编写,可以用更为简单的方式让多个子程序很好的协作起来。我们通过一个例子来加以说明,请大家先看看下面的代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 import asyncioimport timedef fib (max_count ): a, b = 0 , 1 for _ in range (max_count): a, b = b, a + b yield a def test_fib (): for val in fib(20 ): print (val, end=' ' ) print () """生成器经过预激活,就是一个协程,它可以跟其他子程序协作。""" def calc_avg (): total, counter = 0 , 0 avg_value = None while True : curr_value = yield avg_value total += curr_value counter += 1 avg_value = total / counter def test_calc_avg (): obj = calc_avg() obj.send(None ) for _ in range (5 ): print (obj.send(_)) def sync_display (num ): time.sleep(1 ) print (num) def test_sync_display (): start = time.time() for i in range (1 , 10 ): sync_display(i) end = time.time() print (f'{end - start:.3 f} 秒' ) async def async_display (num ): await asyncio.sleep(1 ) print (num) def test_async_display (): start = time.time() objs = [async_display(i) for i in range (1 , 10 )] loop = asyncio.get_event_loop() loop.run_until_complete(asyncio.wait(objs)) loop.close() end = time.time() print (f'{end - start:.3 f} 秒' )
aiohttp库
我们之前使用的requests三方库并不支持异步 I/O,如果希望使用异步 I/O 的方式来加速爬虫代码的执行,我们可以安装和使用名为aiohttp的三方库。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import reimport aiohttpfrom aiohttp import ClientSessionTITLE_PATTERN = re.compile (r'<title.*?>(.*?)</title>' , re.DOTALL) async def fetch_page_title (url ): async with aiohttp.ClientSession(headers={ 'User-Agent' : 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36' , }) as session: async with session.get(url, ssl=False ) as resp: if resp.status == 200 : html_code = await resp.text() matcher = TITLE_PATTERN.search(html_code) title = matcher.group(1 ).strip() print (title) def main (): urls = [ 'https://www.python.org/' , 'https://www.jd.com/' , 'https://www.baidu.com/' , 'https://www.taobao.com/' , 'https://git-scm.com/' , 'https://www.sohu.com/' , 'https://gitee.com/' , 'https://www.amazon.com/' , 'https://www.usa.gov/' , 'https://www.nasa.gov/' ] objs = [fetch_page_title(url) for url in urls] loop = asyncio.get_event_loop() loop.run_until_complete(asyncio.wait(objs)) loop.close()
NumPy的应用
Numpy 是一个开源的 Python 科学计算库,用于快速处理任意维度的数组。Numpy 支持常见的数组和矩阵操作,对于同样的数值计算任务,使用 NumPy 不仅代码要简洁的多,而且 NumPy 在性能上也远远优于原生 Python,至少是一到两个数量级的差距,而且数据量越大,NumPy 的优势就越明显。
NumPy 最为核心的数据类型是ndarray,使用ndarray可以处理一维、二维和多维数组,该对象相当于是一个快速而灵活的大数据容器。NumPy 底层代码使用 C 语言编写,解决了 GIL 的限制,ndarray在存取数据的时候,数据与数据的地址都是连续的,这确保了可以进行高效率的批量操作,性能上远远优于 Python 中的list;另一方面ndarray对象提供了更多的方法来处理数据,尤其获取数据统计特征的方法,这些方法也是 Python 原生的list没有的。
创建数组对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 import numpy as npimport pandas as pdimport matplotlib.pyplot as plt"""方法一:使用array函数,通过list创建数组对象""" array1 = np.array([1 , 2 , 3 , 4 , 5 , 6 ]) print (array1)array2 = np.array([[1 , 2 , 3 ], [4 , 5 , 6 ]]) print (array2)"""方法二:使用arange函数,指定取值范围和跨度创建数组对象""" array3 = np.arange(0 , 20 , 2 ) print (array3)"""方法三:使用linspace函数,用指定范围和元素个数创建数组对象,生成等差数列""" array4 = np.linspace(-10 , 10 , 11 ) print (array4)""" 方法四:使用logspace函数,生成等比数列 注意:等比数列的起始值是 2^1 ,等比数列的终止值是 2^10 ,num是元素的个数,base就是底数。 """ array5 = np.logspace(1 , 10 , num=10 , base=2 ) print (array5)"""方法五:通过fromstring函数从字符串提取数据创建数组对象""" array6 = np.fromstring('1, 2, 3, 4, 5' , sep=',' , dtype='i8' ) print (array6)"""方法六:通过fromiter函数从生成器(迭代器)中获取数据创建数组对象""" def fib (num ): a, b = 0 , 1 for _ in range (num): a, b = b, a + b yield a gen = fib(20 ) array7 = np.fromiter(gen, dtype='i8' ) print (array7)""" 方法七:使用numpy.random模块的函数生成随机数创建数组对象 产生 10 个 [ 0 , 1 ) 范围的随机小数,代码: """ array8 = np.random.rand(5 ) print (array8)""" 产生 10 个 [ 1 , 100 ) 范围的随机整数,代码: """ array9 = np.random.randint(1 , 100 , 10 ) print (array9)""" 产生 20 个 μ = 50 , σ = 10 的正态分布随机数,代码: """ array10 = np.random.normal(50 , 10 , 20 ) print (array10)""" 产生 [ 0 , 1 ) 范围的随机小数构成的 3 行 4 列的二维数组,代码: """ array11 = np.random.rand(3 , 4 ) print (array11)""" 产生 [ 1 , 100 ) 范围的随机整数构成的三维数组,代码: (3, 4, 5): 3block, 4row, 5col """ array12 = np.random.randint(1 , 100 , (3 , 4 , 5 )) print (array12)""" 方法八:创建全0、全1或指定元素的数组 """ """使用zeros函数,代码:""" array13 = np.zeros((3 , 4 )) print (array13)"""使用ones函数,代码:""" array14 = np.ones((3 , 4 )) print (array14)"""使用full函数,代码:""" array15 = np.full((3 , 4 ), 10 ) print (array15)""" 方法九:使用eye函数创建单位矩阵 """ array16 = np.eye(4 ) print (array16)""" 方法十:读取图片获得对应的三维数组 说明:上面的代码读取了当前路径下名为guido.jpg 的图片文件, 计算机系统中的图片通常由若干行若干列的像素点构成, 而每个像素点又是由红绿蓝三原色构成的,刚好可以用三维数组来表示。 """ array17 = plt.imread('guido.jpg' ) print (array17)
数组对象的属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 """size属性:获取数组元素个数。""" array16 = np.arange(1 , 100 , 2 ) array18 = np.random.rand(3 , 4 ) print (array16.size)print (array17.size)print (array18.size)"""shape属性:获取数组的形状。""" print (array16.shape)print (array17.shape)print (array18.shape)"""dtype属性:获取数组元素的数据类型。""" print (array16.dtype)print (array17.dtype)print (array18.dtype)"""ndim属性:获取数组的维度。""" print (array16.ndim)print (array17.ndim)print (array18.ndim)"""itemsize属性:获取数组单个元素占用内存空间的字节数。""" print (array16.itemsize)print (array17.itemsize)print (array18.itemsize)"""nbytes属性:获取数组所有元素占用内存空间的字节数。""" print (array16.nbytes)print (array17.nbytes)print (array18.nbytes)
数组的索引运算
和 Python 中的列表类似,NumPy 的ndarray对象可以进行索引和切片操作,通过索引可以获取或修改数组中的元素,通过切片操作可以取出数组的一部分,我们把切片操作也称为切片索引。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 """ 普通索引 类似于 Python 中list类型的索引运算。 """ array19 = np.arange(1 , 10 ) print (array19[0 ], array19[array19.size - 1 ])print (array19[-array19.size], array19[-1 ])array20 = np.array([[1 , 2 , 3 ], [4 , 5 , 6 ], [7 , 8 , 9 ]]) print (array20[2 ])print (array20[0 ][0 ])print (array20[-1 ][-1 ])print (array20[1 ][1 ])print (array20[1 , 1 ])array20[1 ][1 ] = 20 print (array20)array20[1 ] = [10 , 11 , 12 ] print (array20)
切片索引
切片索引是形如[开始索引:结束索引:跨度]的语法,通过指定开始索引(默认值无穷小)、结束索引(默认值无穷大)和跨度(默认值1),从数组中取出指定部分的元素并构成新的数组。因为开始索引、结束索引和步长都有默认值,所以它们都可以省略,如果不指定步长,第二个冒号也可以省略。一维数组的切片运算跟 Python 中的list类型的切片非常类似,此处不再赘述,二维数组的切片可以参考下面的代码,相信非常容易理解。
1 2 3 4 5 6 7 8 """"切片索引""" print (array20[:2 , 1 :])print (array20[2 , :])print (array20[2 :, :])print (array20[:, :2 ])print (array20[::2 , ::2 ])print (array20[::-2 , ::-2 ])
花式索引
花式索引是用保存整数的数组充当一个数组的索引,这里所说的数组可以是 NumPy 的ndarray,也可以是 Python 中list、tuple等可迭代类型,可以使用正向或负向索引。
1 2 3 4 5 print (array19[[0 , 1 , 1 , -1 , 4 , -1 ]])print (array20[[0 , 2 ]])print (array20[[0 , 2 ], [1 , 2 ]])print (array20[[0 , 2 ], 1 ])
布尔索引
布尔索引就是通过保存布尔值的数组充当一个数组的索引,布尔值为True的元素保留,布尔值为False的元素不会被选中。布尔值的数组可以手动构造,也可以通过关系运算来产生。
关于索引运算需要说明的是,切片索引虽然创建了新的数组对象,但是新数组和原数组共享了数组中的数据,简单的说,无论你通过新数组对象或原数组对象修改数组中的数据,修改的其实是内存中的同一块数据。花式索引和布尔索引也会创建新的数组对象,而且新数组复制了原数组的元素,新数组和原数组并不是共享数据的关系,这一点可以查看数组对象的base属性,有兴趣的读者可以自行探索。
1 2 3 4 5 6 7 8 9 10 print (array19[[True , True , False , False , True , False , False , True , True ]])print (array19 > 5 )print (~(array19 > 5 ))print (array19[array19 > 5 ])print (array19 % 2 == 0 )print (array19[array19 % 2 == 0 ])print ((array19 > 5 ) & (array19 % 2 == 0 ))print (array19[(array19 > 5 ) & (array19 % 2 == 0 )])print (array19[(array19 > 5 ) | (array19 % 2 == 0 )])
案例:通过数组切片处理图像
学习基础知识总是比较枯燥且没有成就感的,所以我们还是来个案例为大家演示下上面学习的数组索引和切片操作到底有什么用。前面我们说到过,可以用三维数组来表示图像,那么通过图像对应的三维数组进行操作,就可以实现对图像的处理,如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import matplotlibmatplotlib.use('TkAgg' ) guido_img = plt.imread('guido.jpg' ) plt.imshow(guido_img) plt.show() plt.imshow(guido_img[::-1 ]) plt.show() plt.imshow(guido_img[:, ::-1 ]) plt.show() plt.imshow(guido_img[30 :350 , 90 :300 ]) plt.show() plt.imshow(guido_img[::10 , ::10 ]) plt.show()
数组对象的方法
计算总和、均值和中位数。
1 2 3 4 5 6 7 print (np.sum (array1))print (array1.sum ())print (np.mean(array1))print (array1.mean())print (np.median(array1))print (np.quantile(array1, 0.5 ))
极值、全距和四分位距离。
1 2 3 4 5 6 7 8 print (np.max (array1))print (array1.max ())print (array1.min ())print (np.min (array1))print (np.ptp(array1))q1, q3 = np.quantile(array1, [0.25 , 0.75 ]) print (q3 - q1)
方差、标准差和变异系数。
1 2 3 4 5 print (array1.var())print (np.var(array1))print (array1.std())print (np.std(array1))print (array1.std() / array1.mean())
绘制箱线图。
箱线图又称为盒须图,是显示一组数据分散情况的统计图,因形状如箱子而得名。 它主要用于反映原始数据分布的特征,还可以进行多组数据分布特征的比较。
1 2 3 plt.boxplot(array1, showmeans=True ) plt.ylim([-20 , 120 ]) plt.show()
数组的运算
使用 NumPy 最为方便的是当需要对数组元素进行运算时,不用编写循环代码遍历每个元素,所有的运算都会自动的矢量化。简单的说就是,NumPy 中的数学运算和数学函数会自动作用于数组中的每个成员。
数组跟标量的运算
1 2 3 4 5 6 7 8 9 array1= np.arange(1 , 10 ) print (array1 + 10 )print (array1 * 10 )print (array1 > 5 )print (array1 % 2 == 0 )
数组跟数组的运算
NumPy 的数组跟数组也可以执行算术运算和关系运算,运算会作用于两个数组对应的元素上,这就要求两个数组的形状(shape属性)要相同,如下所示。
1 2 3 4 5 6 7 8 9 10 11 array2 = np.array([1 , 1 , 1 , 2 , 2 , 2 , 3 , 3 , 3 ]) print (array1 + array2)print (array1 * array2)print (array1 ** array2)print (array1 > array2)print (array1 % array2 == 0 )
通用一元函数
NumPy 中通用一元函数的参数是一个数组对象,函数会对数组进行元素级的处理,例如:sqrt函数会对数组中的每个元素计算平方根,而log2函数会对数组中的每个元素计算以2为底的对数,代码如下所示。
1 2 print (np.sqrt(array1))print (np.log2(array1))
函数
说明
abs / fabs
求绝对值的函数
sqrt
求平方根的函数,相当于array ** 0.5
square
求平方的函数,相当于array ** 2
exp
计算 e x \small{e^x} e x 的函数
log / log10 / log2
对数函数(e为底 / 10为底 / 2为底)
sign
符号函数(1 - 正数;0 - 零;-1 - 负数)
ceil / floor
上取整 / 下取整
isnan
返回布尔数组,NaN对应True,非NaN对应False
isfinite / isinf
判断数值是否为无穷大的函数
cos / cosh / sin
三角函数
sinh / tan / tanh
三角函数
arccos / arccosh / arcsin
反三角函数
arcsinh / arctan / arctanh
反三角函数
rint / round
四舍五入函数
通用二元函数
NumPy 中通用二元函数的参数是两个数组对象,函数会对两个数组中的对应元素进行运算,例如:maximum函数会对两个数组中对应的元素找最大值,而power函数会对两个数组中对应的元素进行求幂操作,代码如下所示。
1 2 3 4 5 6 7 8 array3 = np.array([[4 , 5 , 6 ], [7 , 8 , 9 ]]) array4 = np.array([[1 , 2 , 3 ], [3 , 2 , 1 ]]) print (np.maximum(array3, array4))print (np.power(array3, array4))
函数
说明
add(x, y) / substract(x, y)
加法函数 / 减法函数
multiply(x, y) / divide(x, y)
乘法函数 / 除法函数
floor_divide(x, y) / mod(x, y)
整除函数 / 求模函数
allclose(x, y)
检查数组x和y元素是否几乎相等
power(x, y)
数组x的元素 x i \small{x_{i}} x i 和数组y的元素 y i \small{y_{i}} y i ,计算 x i y i \small{x_{i}^{y_{i}}} x i y i
maximum(x, y) / fmax(x, y)
两两比较元素获取最大值 / 获取最大值(忽略NaN)
minimum(x, y) / fmin(x, y)
两两比较元素获取最小值 / 获取最小值(忽略NaN)
dot(x, y)
点积运算(数量积,通常记为 ⋅ \small{\cdot} ⋅ ,用于欧几里得空间(Euclidean space))
inner(x, y)
内积运算(内积的含义要高于点积,点积相当于是内积在欧几里得空间 R n \small{\mathbb{R}^{n}} R n 的特例,而内积可以推广到赋范向量空间,只要它满足平行四边形法则即可)
cross(x, y)
叉积运算(向量积,通常记为 × \small{\times} × ,运算结果是一个向量)
outer(x, y)
外积运算(张量积,通常记为 ⨂ \small{\bigotimes} ⨂ ,运算结果通常是一个矩阵)
intersect1d(x, y)
计算x和y的交集,返回这些元素构成的有序数组
union1d(x, y)
计算x和y的并集,返回这些元素构成的有序数组
in1d(x, y)
返回由判断x 的元素是否在y中得到的布尔值构成的数组
setdiff1d(x, y)
计算x和y的差集,返回这些元素构成的数组
setxor1d(x, y)
计算x和y的对称差,返回这些元素构成的数组
广播机制
上面数组运算的例子中,两个数组的形状(shape属性)是完全相同的,我们再来研究一下,两个形状不同的数组是否可以直接做二元运算或使用通用二元函数进行运算,请看下面的例子。
代码:
1 2 3 array5 = np.array([[0 , 0 , 0 ], [1 , 1 , 1 ], [2 , 2 , 2 ], [3 , 3 , 3 ]]) array6 = np.array([1 , 2 , 3 ]) array5 + array6
输出:
1 2 3 4 array([[1, 2, 3], [2, 3, 4], [3, 4, 5], [4, 5, 6]])
代码:
1 2 array7 = np.array([[1 ], [2 ], [3 ], [4 ]]) array5 + array7
通过上面的例子,我们发现形状不同的数组仍然有机会进行二元运算,但这不代表任意形状的数组都可以进行二元运算。简单的说,只有两个数组后缘维度相同或者后缘维度不同但其中一个数组后缘维度为1时,广播机制才会被触发。通过广播机制,NumPy 将两个原本形状不相同的数组变成形状相同,才能进行二元运算。所谓后缘维度,指的是数组形状(shape属性)从后往前看对应的部分,我们举例说明。
其他常用函数
除了上面讲到的函数外,NumPy 中还提供了很多用于处理数组的函数,ndarray对象的很多方法也可以通过调用函数来实现,下表给出了一些常用的函数。
函数
说明
unique
去除数组重复元素,返回唯一元素构成的有序数组
copy
返回拷贝数组得到的数组
sort
返回数组元素排序后的拷贝
split / hsplit / vsplit
将数组拆成若干个子数组
stack / hstack / vstack
将多个数组堆叠成新数组
concatenate
沿着指定的轴连接多个数组构成新数组
append / insert
向数组末尾追加元素 / 在数组指定位置插入元素
argwhere
找出数组中非0元素的位置
extract / select / where
按照指定的条件从数组中抽取或处理数组元素
flip
沿指定的轴翻转数组中的元素
fromregex
通过读取文件和正则表达式解析获取数据创建数组对象
repeat / tile
通过对元素的重复来创建新数组
roll
沿指定轴对数组元素进行移位
resize
重新调整数组的大小
place / put
将数组中满足条件的元素/指定的元素替换为指定的值
partition
用选定的元素对数组进行一次划分并返回划分后的数组
向量
向量 (vector )也叫矢量 ,是一个同时具有大小和方向,且满足平行四边形法则的几何对象。与向量相对的概念叫标量 或数量 ,标量只有大小,绝大多数情况下没有方向。我们通常用带箭头的线段来表示向量,在平面直角坐标系中的向量如下图所示。需要注意的是,向量是表达大小和方向的量,并没有规定起点和终点,所以相同的向量可以画在任意位置,例如下图中 w \small{\boldsymbol{w}} w 和 u \small{\boldsymbol{u}} u 两个向量并没有什么区别。
矩阵对象
NumPy 中提供了专门用于线性代数(linear algebra )的模块和表示矩阵的类型matrix,当然我们通过二维数组也可以表示一个矩阵,官方并不推荐使用matrix类而是建议使用二维数组,而且有可能在将来的版本中会移除matrix类。无论如何,利用这些已经封装好的类和函数,我们可以轻松愉快的实现很多对矩阵的操作。
我们可以通过下面的代码来创建矩阵(matrix)对象。
代码:
1 2 m1 = np.matrix('1 2 3; 4 5 6' ) m1
说明 :matrix构造器可以传入类数组对象也可以传入字符串来构造矩阵对象。
输出:
1 2 matrix([[1, 2, 3], [4, 5, 6]])
代码:
1 2 m2 = np.asmatrix(np.array([[1 , 1 ], [2 , 2 ], [3 , 3 ]])) m2
说明 :asmatrix函数也可以用mat函数代替,这两个函数其实是同一个函数。
输出:
1 2 3 matrix([[1, 1], [2, 2], [3, 3]])
代码:
输出:
1 2 matrix([[14, 14], [32, 32]])
说明 :注意matrix对象和ndarray对象乘法运算的差别,matrix对象的*运算是矩阵乘法运算。如果两个二维数组要做矩阵乘法运算,应该使用@运算符或matmul函数,而不是*运算符。
矩阵对象的属性如下表所示。
属性
说明
A
获取矩阵对象对应的ndarray对象
A1
获取矩阵对象对应的扁平化后的ndarray对象
I
可逆矩阵的逆矩阵
T
矩阵的转置
H
矩阵的共轭转置
shape
矩阵的形状
size
矩阵元素的个数
矩阵对象的方法跟之前讲过的ndarray数组对象的方法基本差不多,此处不再进行赘述。
线性代数模块
NumPy 的linalg模块中有一组标准的矩阵分解运算以及诸如求逆和行列式之类的函数,它们跟 MATLAB 和 R 等语言所使用的是相同的行业标准线性代数库,下面的表格列出了numpy以及linalg模块中一些常用的线性代数相关函数。
函数
说明
diag
以一维数组的形式返回方阵的对角线元素或将一维数组转换为方阵(非对角元素元素为0)
matmul
矩阵乘法运算
trace
计算对角线元素的和
norm
求矩阵或向量的范数
det
计算行列式的值
matrix_rank
计算矩阵的秩
eig
计算矩阵的特征值(eigenvalue )和特征向量(eigenvector )
inv
计算非奇异矩阵( n \small{n} n 阶方阵)的逆矩阵
pinv
计算矩阵的摩尔-彭若斯(Moore-Penrose )广义逆
qr
QR分解(把矩阵分解成一个正交矩阵与一个上三角矩阵的积)
svd
计算奇异值分解(singular value decomposition )
solve
解线性方程组 A x = b \small{\boldsymbol{Ax}=\boldsymbol{b}} A x = b ,其中 A \small{\boldsymbol{A}} A 是一个方阵
lstsq
计算 A x = b \small{\boldsymbol{Ax}=\boldsymbol{b}} A x = b 的最小二乘解
下面我们简单尝试一下上面的函数,先试一试求逆矩阵。
代码:
1 2 3 m3 = np.array([[1. , 2. ], [3. , 4. ]]) m4 = np.linalg.inv(m3) m4
输出:
1 2 array([[-2. , 1. ], [ 1.5, -0.5]])
代码:
说明 :around函数对数组元素进行四舍五入操作,默认小数点后面的位数为0。
输出:
1 2 array([[1., 0.], [0., 1.]])
说明 :矩阵和它的逆矩阵做矩阵乘法会得到单位矩阵。
计算行列式的值。
代码:
1 2 m5 = np.array([[1 , 3 , 5 ], [2 , 4 , 6 ], [4 , 7 , 9 ]]) np.linalg.det(m5)
输出:
计算矩阵的秩。
代码:
1 np.linalg.matrix_rank(m5)
输出:
求解线性方程组。
{ x 1 + 2 x 2 + x 3 = 8 3 x 1 + 7 x 2 + 2 x 3 = 23 2 x 1 + 2 x 2 + x 3 = 9 \begin{cases}
x_1 + 2x_2 + x_3 = 8 \\\\
3x_1 + 7x_2 + 2x_3 = 23 \\\\
2x_1 + 2x_2 + x_3 = 9
\end{cases}
⎩ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎨ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎧ x 1 + 2 x 2 + x 3 = 8 3 x 1 + 7 x 2 + 2 x 3 = 2 3 2 x 1 + 2 x 2 + x 3 = 9
对于上面的线性方程组,我们可以用矩阵的形式来表示它,如下所示。
A = [ 1 2 1 3 7 2 2 2 1 ] , x = [ x 1 x 2 x 3 ] , b = [ 8 23 9 ] \boldsymbol{A} = \begin{bmatrix}
1 & 2 & 1 \\\\
3 & 7 & 2 \\\\
2 & 2 & 1
\end{bmatrix}, \quad
\boldsymbol{x} = \begin{bmatrix}
x_1 \\\\
x_2 \\\\
x_3
\end{bmatrix}, \quad
\boldsymbol{b} = \begin{bmatrix}
8 \\\\
23 \\\\
9
\end{bmatrix}
A = ⎣ ⎢ ⎢ ⎢ ⎢ ⎢ ⎡ 1 3 2 2 7 2 1 2 1 ⎦ ⎥ ⎥ ⎥ ⎥ ⎥ ⎤ , x = ⎣ ⎢ ⎢ ⎢ ⎢ ⎢ ⎡ x 1 x 2 x 3 ⎦ ⎥ ⎥ ⎥ ⎥ ⎥ ⎤ , b = ⎣ ⎢ ⎢ ⎢ ⎢ ⎢ ⎡ 8 2 3 9 ⎦ ⎥ ⎥ ⎥ ⎥ ⎥ ⎤
A x = b \boldsymbol{Ax} = \boldsymbol{b}
A x = b
线性方程组有唯一解的条件:系数矩阵 A \small{\boldsymbol{A}} A 的秩等于增广矩阵 A b \small{\boldsymbol{Ab}} A b 的秩,而且跟未知数的个数相同。
代码:
1 2 3 4 A = np.array([[1 , 2 , 1 ], [3 , 7 , 2 ], [2 , 2 , 1 ]]) b = np.array([8 , 23 , 9 ]).reshape(-1 , 1 ) print (np.linalg.matrix_rank(A))print (np.linalg.matrix_rank(np.hstack((A, b))))
说明 :使用数组对象的reshape方法调形时,如果其中一个参数为-1,那么该维度有多少个元素是通过数组元素个数(size属性)和其他维度的元素个数自动计算出来的。
输出:
代码:
输出:
1 2 3 array([[1.], [2.], [3.]])
说明 :上面的结果表示,线性方程组的解为: x 1 = 1 , x 2 = 2 , x 3 = 3 \small{x_1 = 1, x_2 = 2, x_3 = 3} x 1 = 1 , x 2 = 2 , x 3 = 3 。
下面是另一种求解线性方程组的方法,大家可以停下来思考下为什么。
x = A − 1 ⋅ b \boldsymbol{x} = \boldsymbol{A}^{-1} \cdot \boldsymbol{b}
x = A − 1 ⋅ b
代码:
输出:
1 2 3 array([[1.], [2.], [3.]])
多项式
除了数组,NumPy 中还封装了用于多项式 (polynomial )运算的数据类型。多项式是变量的整数次幂与系数的乘积之和,形如:
f ( x ) = a n x n + a n − 1 x n − 1 + ⋯ + a 1 x 1 + a 0 x 0 f(x)=a_nx^n + a_{n-1}x^{n-1} + \cdots + a_1x^{1} + a_0x^{0}
f ( x ) = a n x n + a n − 1 x n − 1 + ⋯ + a 1 x 1 + a 0 x 0
在 NumPy 1.4版本之前,我们可以用poly1d类型来表示多项式,目前它仍然可用,但是官方提供了新的模块numpy.polynomial,它除了支持基本的幂级数多项式外,还可以支持切比雪夫多项式、拉盖尔多项式等。
创建多项式对象
创建poly1d对象,例如: f ( x ) = 3 x 2 + 2 x + 1 \small{f(x)=3x^{2}+2x+1} f ( x ) = 3 x 2 + 2 x + 1 。
代码:
1 2 3 4 p1 = np.poly1d([3 , 2 , 1 ]) p2 = np.poly1d([1 , 2 , 3 ]) print (p1)print (p2)
输出:
1 2 3 4 2 3 x + 2 x + 1 2 1 x + 2 x + 3
多项式的操作
获取多项式的系数
代码:
1 2 print (p1.coefficients)print (p2.coeffs)
输出:
两个多项式的四则运算
代码:
1 2 print (p1 + p2)print (p1 * p2)
输出:
1 2 3 4 2 4 x + 4 x + 4 4 3 2 3 x + 8 x + 14 x + 8 x + 3
带入 x \small{x} x 求多项式的值
代码:
1 2 print (p1(3 ))print (p2(3 ))
输出:
多项式求导和不定积分
代码:
1 2 print (p1.deriv())print (p1.integ())
输出:
1 2 3 4 6 x + 2 3 2 1 x + 1 x + 1 x
求多项式的根
例如有多项式 f ( x ) = x 2 + 3 x + 2 \small{f(x)=x^2+3x+2} f ( x ) = x 2 + 3 x + 2 ,多项式的根即一元二次方程 x 2 + 3 x + 2 = 0 \small{x^2+3x+2=0} x 2 + 3 x + 2 = 0 的解。
代码:
1 2 p3 = np.poly1d([1 , 3 , 2 ]) print (p3.roots)
输出:
如果使用numpy.polynomial模块的Polynomial类来表示多项式对象,那么对应的操作如下所示。
代码:
1 2 3 4 5 6 7 8 9 from numpy.polynomial import Polynomialp3 = Polynomial((2 , 3 , 1 )) print (p3) print (p3(3 )) print (p3.roots()) print (p3.degree()) print (p3.deriv()) print (p3.integ())
输出:
1 2 3 4 5 6 2.0 + 3.0·x + 1.0·x² 20.0 [-2. -1.] 2 3.0 + 2.0·x 0.0 + 2.0·x + 1.5·x² + 0.33333333·x³
最小二乘解
Polynomial类还有一个名为fit的类方法,它可以给多项式求最小二乘解。所谓最小二乘解(least-squares solution),是用最小二乘法通过最小化误差的平方和来寻找数据的最佳匹配函数的系数。假设多项式为 f ( x ) = a x + b \small{f(x)=ax+b} f ( x ) = a x + b ,最小二乘解就是让下面的残差平方和 R S S \small{RSS} R S S 达到最小的 a \small{a} a 和 b \small{b} b 。
R S S = ∑ i = 0 k ( f ( x i ) − y i ) 2 RSS = \sum_{i=0}^{k}{(f(x_i) - y_i)^{2}}
R S S = i = 0 ∑ k ( f ( x i ) − y i ) 2
例如,我们想利用收集到的月收入和网购支出的历史数据来建立一个预测模型,以达到通过某人的月收入预测他网购支出金额的目标,下面是我们收集到的收入和网购支出的数据,保存在两个数组中。
1 2 3 4 5 6 7 8 9 10 x = np.array([ 25000 , 15850 , 15500 , 20500 , 22000 , 20010 , 26050 , 12500 , 18500 , 27300 , 15000 , 8300 , 23320 , 5250 , 5800 , 9100 , 4800 , 16000 , 28500 , 32000 , 31300 , 10800 , 6750 , 6020 , 13300 , 30020 , 3200 , 17300 , 8835 , 3500 ]) y = np.array([ 2599 , 1400 , 1120 , 2560 , 1900 , 1200 , 2320 , 800 , 1650 , 2200 , 980 , 580 , 1885 , 600 , 400 , 800 , 420 , 1380 , 1980 , 3999 , 3800 , 725 , 520 , 420 , 1200 , 4020 , 350 , 1500 , 560 , 500 ])
我们可以先绘制散点图来了解两组数据是否具有正相关或负相关关系。正相关意味着数组x中较大的值对应到数组y中也是较大的值,而负相关则意味着数组x中较大的值对应到数组y中较小的值。
1 2 3 4 5 import matplotlib.pyplot as pltplt.figure(dpi=120 ) plt.scatter(x, y, color='blue' ) plt.show()
输出:
如果需要定量的研究两组数据的相关性,我们可以计算协方差或相关系数,对应的 NumPy 函数分别是cov和corrcoef。
代码:
输出:
1 2 array([[1. , 0.92275889], [0.92275889, 1. ]])
说明 :相关系数是一个-1到1之间的值,越靠近1 说明正相关性越强,越靠近-1说明负相关性越强,靠近0则说明两组数据没有明显的相关性。上面月收入和网购支出之间的相关系数是0.92275889,说明二者是强正相关关系。
通过上面的操作,我们确定了收入和网购支出之前存在强正相关关系,于是我们用这些数据来创建一个回归模型,找出一条能够很好的拟合这些数据点的直线。这里,我们就可以用到上面提到的fit方法,具体的代码如下所示。
代码:
1 2 3 from numpy.polynomial import PolynomialPolynomial.fit(x, y, deg=1 ).convert().coef
说明 :deg=1说明回归模型最高次项就是1次项,回归模型形如 y = a x + b \small{y=ax+b} y = a x + b ;如果要生一个类似于 y = a x 2 + b x + c \small{y=ax^2+bx+c} y = a x 2 + b x + c 的模型,就需要设置deg=2,以此类推。
输出:
1 array([-2.94883437e+02, 1.10333716e-01])
根据上面输出的结果,我们的回归方程应该是 y = 0.110333716 x − 294.883437 \small{y=0.110333716x-294.883437} y = 0 . 1 1 0 3 3 3 7 1 6 x − 2 9 4 . 8 8 3 4 3 7 。我们将这个回归方程绘制到刚才的散点图上,红色的点是我们的预测值,蓝色的点是历史数据,也就是真实值。
代码:
1 2 3 4 5 6 import matplotlib.pyplot as pltplt.scatter(x, y, color='blue' ) plt.scatter(x, 0.110333716 * x - 294.883437 , color='red' ) plt.plot(x, 0.110333716 * x - 294.883437 , color='darkcyan' ) plt.show()
输出:
如果不使用Polynomial类型的fit方法,我们也可以通过 NumPy 提供的polyfit函数来完成同样的操作,有兴趣的读者可以自行研究。
说明 :本章部分图片来自于维基百科。
Pandas
Pandas 是 Wes McKinney 在2008年开发的一个强大的分析结构化数据 的工具集。Pandas 以 NumPy 为基础(实现数据存储和运算),提供了专门用于数据分析的类型、方法和函数,对数据分析和数据挖掘提供了很好的支持;同时 pandas 还可以跟数据可视化工具 matplotlib 很好的整合在一起,非常轻松愉快的实现数据可视化呈现。
Pandas 核心的数据类型是Series(数据系列)、DataFrame(数据窗/数据框),分别用于处理一维和二维的数据,除此之外,还有一个名为Index的类型及其子类型,它们为Series和DataFrame提供了索引功能。日常工作中DataFrame使用得最为广泛,因为二维的数据结构刚好可以对应有行有列的表格。Series和DataFrame都提供了大量的处理数据的方法,数据分析师以此为基础,可以实现对数据的筛选、合并、拼接、清洗、预处理、聚合、透视和可视化等各种操作。
创建Series对象
Pandas 库中的Series对象可以用来表示一维数据结构,但是多了索引和一些额外的功能。Series类型的内部结构包含了两个数组,其中一个用来保存数据,另一个用来保存数据的索引。我们可以通过列表或数组创建Series对象,代码如下所示。
代码:
1 2 3 4 5 import numpy as npimport pandas as pdser1 = pd.Series(data=[120 , 380 , 250 , 360 ], index=['一季度' , '二季度' , '三季度' , '四季度' ]) ser1
说明 :Series构造器中的data参数表示数据,index参数表示数据的索引,相当于数据对应的标签。
输出:
1 2 3 4 5 一季度 120 二季度 380 三季度 250 四季度 360 dtype: int64
通过字典创建Series对象。
代码:
1 2 ser2 = pd.Series({'一季度' : 320 , '二季度' : 180 , '三季度' : 300 , '四季度' : 405 }) ser2
说明 :通过字典创建Series对象时,字典的键就是数据的标签(索引),键对应的值就是数据。
输出:
1 2 3 4 5 一季度 320 二季度 180 三季度 300 四季度 405 dtype: int64
Series对象的运算
标量运算
我们尝试给刚才的ser1每个季度加上10,代码如下所示。
代码:
输出:
1 2 3 4 5 一季度 130 二季度 390 三季度 260 四季度 370 dtype: int64
矢量运算
我们尝试把ser1和ser2对应季度的数据加起来,代码如下所示。
代码:
输出:
1 2 3 4 5 一季度 450 二季度 570 三季度 560 四季度 775 dtype: int64
索引运算
普通索引
跟数组一样,Series对象也可以进行索引和切片操作,不同的是Series对象因为内部维护了一个保存索引的数组,所以除了可以使用整数索引检索数据外,还可以通过自己设置的索引(标签)获取对应的数据。
使用整数索引。
代码:
输出:
使用自定义索引。
代码:
输出:
代码:
输出:
1 2 3 4 5 一季度 380 二季度 390 三季度 260 四季度 370 dtype: int64
切片索引
Series对象的切片操作跟列表、数组类似,通过给出起始和结束索引,从原来的Series对象中取出或修改部分数据,这里也可以使用整数索引和自定义的索引,代码如下所示。
代码:
输出:
1 2 3 二季度 180 三季度 300 dtype: int64
代码:
输出:
1 2 3 4 二季度 180 三季度 300 四季度 405 dtype: int64
提示 :在使用自定义索引进行切片时,结束索引对应的元素也是可以取到的。
代码:
1 2 ser2[1 :3 ] = 400 , 500 ser2
输出:
1 2 3 4 5 一季度 320 二季度 400 三季度 500 四季度 405 dtype: int64
花式索引
代码:
输出:
1 2 3 二季度 400 四季度 405 dtype: int64
代码:
1 2 ser2[['二季度' , '四季度' ]] = 600 , 520 ser2
输出:
1 2 3 4 5 一季度 320 二季度 600 三季度 500 四季度 520 dtype: int64
布尔索引
代码:
输出:
1 2 3 4 二季度 600 三季度 500 四季度 520 dtype: int64
Series对象的属性和方法
Series对象的属性和方法非常多,我们就捡着重要的跟大家讲吧。先看看下面的表格,它展示了Series对象常用的属性。
属性
说明
dtype / dtypes
返回Series对象的数据类型
hasnans
判断Series对象中有没有空值
at / iat
通过索引访问Series对象中的单个值
loc / iloc
通过索引访问Series对象中的单个值或一组值
index
返回Series对象的索引(Index对象)
is_monotonic
判断Series对象中的数据是否单调
is_monotonic_increasing
判断Series对象中的数据是否单调递增
is_monotonic_decreasing
判断Series对象中的数据是否单调递减
is_unique
判断Series对象中的数据是否独一无二
size
返回Series对象中元素的个数
values
以ndarray的方式返回Series对象中的值(ndarray对象)
我们可以通过下面的代码来了解Series对象的属性。
代码:
1 2 3 4 5 6 print (ser2.dtype) print (ser2.hasnans) print (ser2.index) print (ser2.values) print (ser2.is_monotonic_increasing) print (ser2.is_unique)
输出:
1 2 3 4 5 6 int64 False Index(['一季度', '二季度', '三季度', '四季度'], dtype='object') [320 600 500 520] False True
Series对象的方法很多,下面我们通过一些代码片段为大家介绍常用的方法。
统计相关
Series对象支持各种获取描述性统计信息的方法。
代码:
1 2 3 4 5 6 7 8 print (ser2.count()) print (ser2.sum ()) print (ser2.mean()) print (ser2.median()) print (ser2.max ()) print (ser2.min ()) print (ser2.std()) print (ser2.var())
输出:
1 2 3 4 5 6 7 8 4 1940 485.0 510.0 600 320 118.18065267490557 13966.666666666666
Series对象还有一个名为describe()的方法,可以获得上述所有的描述性统计信息,如下所示。
代码:
输出:
1 2 3 4 5 6 7 8 9 count 4.000000 mean 485.000000 std 118.180653 min 320.000000 25% 455.000000 50% 510.000000 75% 540.000000 max 600.000000 dtype: float64
提示 :因为describe()返回的也是一个Series对象,所以也可以用ser2.describe()['mean']来获取平均值,用ser2.describe()[['max', 'min']]来获取最大值和最小值。
如果Series对象有重复的值,我们可以使用unique()方法获得由独一无二的值构成的数组;可以使用nunique()方法统计不重复值的数量;如果想要统计每个值重复的次数,可以使用value_counts()方法,这个方法会返回一个Series对象,它的索引就是原来的Series对象中的值,而每个值出现的次数就是返回的Series对象中的数据,在默认情况下会按照出现次数做降序排列,如下所示。
代码:
1 2 ser3 = pd.Series(data=['apple' , 'banana' , 'apple' , 'pitaya' , 'apple' , 'pitaya' , 'durian' ]) ser3.value_counts()
输出:
1 2 3 4 5 apple 3 pitaya 2 durian 1 banana 1 dtype: int64
代码:
输出:
对于ser3,我们还可以用mode()方法来找出数据的众数,由于众数可能不唯一,所以mode()方法的返回值仍然是一个Series对象。
代码:
输出:
处理数据
Series对象的isna()和isnull()方法可以用于空值的判断,notna()和notnull()方法可以用于非空值的判断,代码如下所示。
代码:
1 2 ser4 = pd.Series(data=[10 , 20 , np.nan, 30 , np.nan]) ser4.isna()
说明 :np.nan是一个IEEE 754标准的浮点小数,专门用来表示“不是一个数”,在上面的代码中我们用它来代表空值;当然,也可以用 Python 中的None来表示空值,在 pandas 中None也会被处理为np.nan。
输出:
1 2 3 4 5 6 0 False 1 False 2 True 3 False 4 True dtype: bool
代码:
输出:
1 2 3 4 5 6 0 True 1 True 2 False 3 True 4 False dtype: bool
Series对象的dropna()和fillna()方法分别用来删除空值和填充空值,具体的用法如下所示。
代码:
输出:
1 2 3 4 0 10.0 1 20.0 3 30.0 dtype: float64
代码:
输出:
1 2 3 4 5 6 0 10.0 1 20.0 2 40.0 3 30.0 4 40.0 dtype: float64
代码:
1 ser4.fillna(method='ffill' )
输出:
1 2 3 4 5 6 0 10.0 1 20.0 2 20.0 3 30.0 4 30.0 dtype: float64
需要提醒大家注意的是,dropna()和fillna()方法都有一个名为inplace的参数,它的默认值是False,表示删除空值或填充空值不会修改原来的Series对象,而是返回一个新的Series对象。如果将inplace参数的值修改为True,那么删除或填充空值会就地操作,直接修改原来的Series对象,此时方法的返回值是None。后面我们会接触到的很多方法,包括DataFrame对象的很多方法都会有这个参数,它们的意义跟这里是一样的。
Series对象的mask()和where()方法可以将满足或不满足条件的值进行替换,如下所示。
代码:
1 2 ser5 = pd.Series(range (5 )) ser5.where(ser5 > 0 )
输出:
1 2 3 4 5 6 0 NaN 1 1.0 2 2.0 3 3.0 4 4.0 dtype: float64
代码:
1 ser5.where(ser5 > 1 , 10 )
输出:
1 2 3 4 5 6 0 10 1 10 2 2 3 3 4 4 dtype: int64
代码:
输出:
1 2 3 4 5 6 0 0 1 1 2 10 3 10 4 10 dtype: int64
Series对象的duplicated()方法可以帮助我们找出重复的数据,而drop_duplicates()方法可以帮我们删除重复数据。
代码:
输出:
1 2 3 4 5 6 7 8 0 False 1 False 2 True 3 False 4 True 5 True 6 False dtype: bool
代码:
输出:
1 2 3 4 5 0 apple 1 banana 3 pitaya 6 durian dtype: object
Series对象的apply()和map()方法非常重要,它们可以通过字典或者指定的函数来处理数据,把数据映射或转换成我们想要的样子。这两个方法在数据准备阶段非常重要,我们先来试一试这个名为map的方法。
代码:
1 2 ser6 = pd.Series(['cat' , 'dog' , np.nan, 'rabbit' ]) ser6
输出:
1 2 3 4 5 0 cat 1 dog 2 NaN 3 rabbit dtype: object
代码:
1 ser6.map ({'cat' : 'kitten' , 'dog' : 'puppy' })
说明 :通过字典给出的映射规则对数据进行处理。
输出:
1 2 3 4 5 0 kitten 1 puppy 2 NaN 3 NaN dtype: object
代码:
1 ser6.map ('I am a {}' .format , na_action='ignore' )
说明 :将指定字符串的format方法作用到数据系列的数据上,忽略掉所有的空值。
输出:
1 2 3 4 5 0 I am a cat 1 I am a dog 2 NaN 3 I am a rabbit dtype: object
我们创建一个新的Series对象,
1 2 ser7 = pd.Series([20 , 21 , 12 ], index=['London' , 'New York' , 'Helsinki' ]) ser7
输出:
1 2 3 4 London 20 New York 21 Helsinki 12 dtype: int64
代码:
说明 :将求平方的函数作用到数据系列的数据上,也可以将参数np.square替换为lambda x: x ** 2。
输出:
1 2 3 4 London 400 New York 441 Helsinki 144 dtype: int64
代码:
1 ser7.apply(lambda x, value: x - value, args=(5 , ))
注意:上面apply方法中的lambda函数有两个参数,第一个参数是数据系列中的数据,而第二个参数需要我们传入,所以我们给apply方法增加了args参数,用于给lambda函数的第二个参数传值。
输出:
1 2 3 4 London 15 New York 16 Helsinki 7 dtype: int64
取头部值和排序
Series对象的sort_index()和sort_values()方法可以用于对索引和数据的排序,排序方法有一个名为ascending的布尔类型参数,该参数用于控制排序的结果是升序还是降序;而名为kind的参数则用来控制排序使用的算法,默认使用了quicksort,也可以选择mergesort或heapsort;如果存在空值,那么可以用na_position参数空值放在最前还是最后,默认是last,代码如下所示。
代码:
1 2 3 4 5 ser8 = pd.Series( data=[35 , 96 , 12 , 57 , 25 , 89 ], index=['grape' , 'banana' , 'pitaya' , 'apple' , 'peach' , 'orange' ] ) ser8.sort_values()
输出:
1 2 3 4 5 6 7 pitaya 12 peach 25 grape 35 apple 57 orange 89 banana 96 dtype: int64
代码:
1 ser8.sort_index(ascending=False )
输出:
1 2 3 4 5 6 7 pitaya 12 peach 25 orange 89 grape 35 banana 96 apple 57 dtype: int64
如果要从Series对象中找出元素中最大或最小的“Top-N”,我们不需要对所有的值进行排序的,可以使用nlargest()和nsmallest()方法来完成,如下所示。
代码:
输出:
1 2 3 4 banana 96 orange 89 apple 57 dtype: int64
代码:
输出:
1 2 3 pitaya 12 peach 25 dtype: int64
绘制图表
Series对象有一个名为plot的方法可以用来生成图表,如果选择生成折线图、饼图、柱状图等,默认会使用Series对象的索引作为横坐标,使用Series对象的数据作为纵坐标。下面我们创建一个Series对象并基于它绘制柱状图,代码如下所示。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 import matplotlib.pyplot as pltser9 = pd.Series({'Q1' : 400 , 'Q2' : 520 , 'Q3' : 180 , 'Q4' : 380 }) ser9.plot(kind='bar' ) plt.ylim(0 , 600 ) plt.xticks(rotation=0 ) for i in range (ser9.size): plt.text(i, ser9[i] + 5 , ser9[i], ha='center' ) plt.show()
输出:
我们也可以将其绘制为饼图,代码如下所示。
代码:
1 2 3 4 5 ser9.plot(kind='pie' , autopct='%.1f%%' , pctdistance=0.65 ) plt.show()
输出:
创建DataFrame对象
如果使用 pandas 做数据分析,那么DataFrame一定是被使用得最多的类型,它可以用来保存和处理异质的二维数据。这里所谓的“异质”是指DataFrame中每个列的数据类型不需要相同,这也是它区别于 NumPy 二维数组的地方。DataFrame提供了极为丰富的属性和方法,帮助我们实现对数据的重塑、清洗、预处理、透视、呈现等一系列操作。
通过二维数组创建DataFrame对象
代码:
1 2 3 4 5 scores = np.random.randint(60 , 101 , (5 , 3 )) courses = ['语文' , '数学' , '英语' ] stu_ids = np.arange(1001 , 1006 ) df1 = pd.DataFrame(data=scores, columns=courses, index=stu_ids) df1
输出:
1 2 3 4 5 6 语文 数学 英语 1001 69 80 79 1002 71 60 100 1003 94 81 93 1004 88 88 67 1005 82 66 60
通过字典创建DataFrame对象
代码:
1 2 3 4 5 6 7 8 scores = { '语文' : [62 , 72 , 93 , 88 , 93 ], '数学' : [95 , 65 , 86 , 66 , 87 ], '英语' : [66 , 75 , 82 , 69 , 82 ], } stu_ids = np.arange(1001 , 1006 ) df2 = pd.DataFrame(data=scores, index=stu_ids) df2
输出:
1 2 3 4 5 6 语文 数学 英语 1001 62 95 66 1002 72 65 75 1003 93 86 82 1004 88 66 69 1005 93 87 82
读取CSV文件创建DataFrame对象
可以通过pandas 模块的read_csv函数来读取 CSV 文件,read_csv函数的参数非常多,下面介绍几个比较重要的参数。
sep / delimiter:分隔符,默认是,。
header:表头(列索引)的位置,默认值是infer,用第一行的内容作为表头(列索引)。
index_col:用作行索引(标签)的列。
usecols:需要加载的列,可以使用序号或者列名。
true_values / false_values:哪些值被视为布尔值True / False。
skiprows:通过行号、索引或函数指定需要跳过的行。
skipfooter:要跳过的末尾行数。
nrows:需要读取的行数。
na_values:哪些值被视为空值。
iterator:设置为True,函数返回迭代器对象。
chunksize:配合上面的参数,设置每次迭代获取的数据体量。
代码:
1 2 df3 = pd.read_csv('data/2018年北京积分落户数据.csv' , index_col='id' ) df3
提示 :上面代码中的CSV文件是用相对路径进行获取的,也就是说当前工作路径下有名为data的文件夹,而“2018年北京积分落户数据.csv”就在这个文件夹下。如果使用Windows系统,在写路径分隔符时也建议使用/而不是\,如果想使用\,建议在字符串前面添加一个r,使用原始字符串来避开转义字符,例如r'c:\new\data\2018年北京积分落户数据.csv'。
输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 name birthday company score id 1 杨xx 1972-12 北京利德华福xxxx 122.59 2 纪xx 1974-12 北京航天数据xxxx 121.25 3 王x 1974-05 品牌联盟(北京)xx 118.96 4 杨x 1975-07 中科专利商标xxxx 118.21 5 张xx 1974-11 北京阿里巴巴xxxx 117.79 ... ... ... ... ... 6015 孙xx 1978-08 华为海洋网络xxxx 90.75 6016 刘xx 1976-11 福斯(上海)xxxx 90.75 6017 周x 1977-10 赢创德固赛xxxxxx 90.75 6018 赵x 1979-07 澳科利耳医疗xxxx 90.75 6019 贺x 1981-06 北京宝洁技术xxxx 90.75 [6019 rows x 4 columns]
说明 : 上面输出的内容隐去了姓名(name)和公司名称(company)字段中的部分信息。如果需要上面例子中的 CSV 文件,可以通过百度云盘获取,链接:https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g ,提取码:e7b4。
读取Excel工作表创建DataFrame对象
可以通过pandas 模块的read_excel函数来读取 Excel 文件,该函数与上面的read_csv非常类似,多了一个sheet_name参数来指定数据表的名称,但是不同于 CSV 文件,没有sep或delimiter这样的参数。假设有名为“2022年股票数据.xlsx”的 Excel 文件,里面有用股票代码命名的五个表单,分别是阿里巴巴(BABA)、百度(BIDU)、京东(JD)、亚马逊(AMZN)、甲骨文(ORCL)这五个公司2022年的股票数据,如果想加载亚马逊的股票数据,代码如下所示。
代码:
1 2 df4 = pd.read_excel('data/2022年股票数据.xlsx' , sheet_name='AMZN' , index_col='Date' ) df4
说明 :上面例子中的 CSV 文件可以通过百度云盘获取,链接:https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g ,提取码:e7b4。
输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Open High Low Close Volume Date 2022-12-30 83.120 84.050 82.4700 84.000 62401194 2022-12-29 82.870 84.550 82.5500 84.180 54995895 2022-12-28 82.800 83.480 81.6900 81.820 58228575 2022-12-27 84.970 85.350 83.0000 83.040 57284035 2022-12-23 83.250 85.780 82.9344 85.250 57433655 ... ... ... ... ... ... 2022-01-07 163.839 165.243 162.0310 162.554 46605900 2022-01-06 163.450 164.800 161.9370 163.254 51957780 2022-01-05 166.883 167.126 164.3570 164.357 64302720 2022-01-04 170.438 171.400 166.3490 167.522 70725160 2022-01-03 167.550 170.704 166.1600 170.404 63869140 [251 rows x 5 columns]
读取关系数据库二维表创建DataFrame对象
pandas模块的read_sql函数可以通过 SQL 语句从数据库中读取数据创建DataFrame对象,该函数的第二个参数代表了需要连接的数据库。对于 MySQL 数据库,我们可以通过pymysql或mysqlclient来创建数据库连接(需要提前安装好三方库),得到一个Connection 对象,而这个对象就是read_sql函数需要的第二个参数,代码如下所示。
代码:
1 2 3 4 5 6 7 8 9 10 11 import pymysqlconn = pymysql.connect( host='101.42.16.8' , port=3306 , user='guest' , password='Guest.618' , database='hrs' , charset='utf8mb4' ) df5 = pd.read_sql('select * from tb_emp' , conn, index_col='eno' ) df5
提示 :执行上面的代码需要先安装pymysql库,如果尚未安装,可以先在单元格中先执行魔法指令%pip install pymysql,然后再运行上面的代码。上面的代码连接的是我部署在腾讯云上的 MySQL 数据库,公网 IP 地址:101.42.16.8,用户名:guest,密码:Guest.618,数据库:hrs,字符集:utf8mb4,大家可以使用这个数据库,但是不要进行恶意的访问。hrs数据库一共有三张表,分别是:tb_dept(部门表)、tb_emp(员工表)、tb_emp2(员工表2)。
输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ename job mgr sal comm dno eno 1359 胡一刀 销售员 3344.0 1800 200.0 30 2056 乔峰 分析师 7800.0 5000 1500.0 20 3088 李莫愁 设计师 2056.0 3500 800.0 20 3211 张无忌 程序员 2056.0 3200 NaN 20 3233 丘处机 程序员 2056.0 3400 NaN 20 3244 欧阳锋 程序员 3088.0 3200 NaN 20 3251 张翠山 程序员 2056.0 4000 NaN 20 3344 黄蓉 销售主管 7800.0 3000 800.0 30 3577 杨过 会计 5566.0 2200 NaN 10 3588 朱九真 会计 5566.0 2500 NaN 10 4466 苗人凤 销售员 3344.0 2500 NaN 30 5234 郭靖 出纳 5566.0 2000 NaN 10 5566 宋远桥 会计师 7800.0 4000 1000.0 10 7800 张三丰 总裁 NaN 9000 1200.0 20
执行上面的代码会出现一个警告,因为 pandas 库希望我们使用SQLAlchemy三方库接入数据库,具体内容是:“UserWarning: pandas only supports SQLAlchemy connectable (engine/connection) or database string URI or sqlite3 DBAPI2 connection. Other DBAPI2 objects are not tested. Please consider using SQLAlchemy.”。如果不想看到这个警告,我们可以试一试下面的解决方案。
首先,安装三方库SQLAlchemy,在 Jupyter 中可以使用%pip魔法指令。
通过SQLAlchemy的create_engine函数创建Engine对象作为read_sql函数的第二个参数,此时read_sql函数的第一个参数可以是 SQL 语句,也可以是二维表的表名。
1 2 3 4 5 6 7 from sqlalchemy import create_engineengine = create_engine('mysql+pymysql://guest:Guest.618@101.42.16.8:3306/hrs' ) df5 = pd.read_sql('tb_emp' , engine, index_col='eno' ) df5
说明 :如果通过表名加载二维表数据,也可以将上面的函数换成read_sql_table。
我们再来加载部门表的数据创建DataFrame对象。
1 2 df6 = pd.read_sql('select dno, dname, dloc from tb_dept' , engine, index_col='dno' ) df6
说明 :如果通过 SQL 查询获取数据,也可以将上面的函数换成read_sql_query。
输出:
1 2 3 4 5 6 dname dloc dno 10 会计部 北京 20 研发部 成都 30 销售部 重庆 40 运维部 深圳
在完成数据加载后,如果希望释放数据库连接,可以使用下面的代码。
1 engine.connect().close()
基本属性和方法
在开始讲解DataFrame的属性和方法前,我们先从之前提到的hrs数据库中读取三张表的数据,创建出三个DataFrame对象,完整的代码如下所示。
1 2 3 4 5 6 from sqlalchemy import create_engineengine = create_engine('mysql+pymysql://guest:Guest.618@101.42.16.8:3306/hrs' ) dept_df = pd.read_sql_table('tb_dept' , engine, index_col='dno' ) emp_df = pd.read_sql_table('tb_emp' , engine, index_col='eno' ) emp2_df = pd.read_sql_table('tb_emp2' , engine, index_col='eno' )
得到的三个DataFrame对象如下所示。
部门表(dept_df),其中dno是部门的编号,dname和dloc分别是部门的名称和所在地。
1 2 3 4 5 6 dname dloc dno 10 会计部 北京 20 研发部 成都 30 销售部 重庆 40 运维部 深圳
员工表(emp_df),其中eno是员工编号,ename、job、mgr、sal、comm和dno分别代表员工的姓名、职位、主管编号、月薪、补贴和部门编号。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ename job mgr sal comm dno eno 1359 胡一刀 销售员 3344.0 1800 200.0 30 2056 乔峰 分析师 7800.0 5000 1500.0 20 3088 李莫愁 设计师 2056.0 3500 800.0 20 3211 张无忌 程序员 2056.0 3200 NaN 20 3233 丘处机 程序员 2056.0 3400 NaN 20 3244 欧阳锋 程序员 3088.0 3200 NaN 20 3251 张翠山 程序员 2056.0 4000 NaN 20 3344 黄蓉 销售主管 7800.0 3000 800.0 30 3577 杨过 会计 5566.0 2200 NaN 10 3588 朱九真 会计 5566.0 2500 NaN 10 4466 苗人凤 销售员 3344.0 2500 NaN 30 5234 郭靖 出纳 5566.0 2000 NaN 10 5566 宋远桥 会计师 7800.0 4000 1000.0 10 7800 张三丰 总裁 NaN 9000 1200.0 20
说明 :在数据库中mgr和comm两个列的数据类型是int,但是因为有缺失值(空值),读取到DataFrame之后,列的数据类型变成了float,因为我们通常会用float类型的NaN来表示空值。
员工表(emp2_df),跟上面的员工表结构相同,但是保存了不同的员工数据。
1 2 3 4 5 6 7 ename job mgr sal comm dno eno 9500 张三丰 总裁 NaN 50000 8000 20 9600 王大锤 程序员 9800.0 8000 600 20 9700 张三丰 总裁 NaN 60000 6000 20 9800 骆昊 架构师 7800.0 30000 5000 20 9900 陈小刀 分析师 9800.0 10000 1200 20
DataFrame对象的属性如下表所示。
属性名
说明
at / iat
通过标签获取DataFrame中的单个值。
columns
DataFrame对象列的索引
dtypes
DataFrame对象每一列的数据类型
empty
DataFrame对象是否为空
loc / iloc
通过标签获取DataFrame中的一组值。
ndim
DataFrame对象的维度
shape
DataFrame对象的形状(行数和列数)
size
DataFrame对象中元素的个数
values
DataFrame对象的数据对应的二维数组
关于DataFrame的方法,首先需要了解的是info()方法,它可以帮助我们了解DataFrame的相关信息,如下所示。
代码:
输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 <class 'pandas.core.frame.DataFrame'> Int64Index: 14 entries, 1359 to 7800 Data columns (total 6 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 ename 14 non-null object 1 job 14 non-null object 2 mgr 13 non-null float64 3 sal 14 non-null int64 4 comm 6 non-null float64 5 dno 14 non-null int64 dtypes: float64(2), int64(2), object(2) memory usage: 1.3+ KB
如果需要查看DataFrame的头部或尾部的数据,可以使用head()或tail()方法,这两个方法的默认参数是5,表示获取DataFrame最前面5行或最后面5行的数据,如下所示。
输出:
1 2 3 4 5 6 7 ename job mgr sal comm dno eno 1359 胡一刀 销售员 3344 1800 200 30 2056 乔峰 分析师 7800 5000 1500 20 3088 李莫愁 设计师 2056 3500 800 20 3211 张无忌 程序员 2056 3200 NaN 20 3233 丘处机 程序员 2056 3400 NaN 20
操作数据
索引和切片
如果要获取DataFrame的某一列,例如取出上面emp_df的ename列,可以使用下面的两种方式。
或者
执行上面的代码可以发现,我们获得的是一个Series对象。事实上,DataFrame对象就是将多个Series对象组合到一起的结果。
如果要获取DataFrame的某一行,可以使用整数索引或我们设置的索引,例如取出员工编号为2056的员工数据,代码如下所示。
或者
通过执行上面的代码我们发现,单独取DataFrame 的某一行或某一列得到的都是Series对象。我们当然也可以通过花式索引来获取多个行或多个列的数据,花式索引的结果仍然是一个DataFrame对象。
获取多个列:
1 emp_df[['ename' , 'job' ]]
获取多个行:
1 emp_df.loc[[2056 , 7800 , 3344 ]]
如果要获取或修改DataFrame 对象某个单元格的数据,需要同时指定行和列的索引,例如要获取员工编号为2056的员工的职位信息,代码如下所示。
或者
或者
我们推荐大家使用第三种做法,因为它只做了一次索引运算。如果要将该员工的职位修改为“架构师”,可以使用下面的代码。
1 emp_df.loc[2056 , 'job' ] = '架构师'
当然,我们也可以通过切片操作来获取多行多列,相信大家一定已经想到了这一点。
输出:
1 2 3 4 5 6 7 8 9 ename job mgr sal comm dno eno 2056 乔峰 分析师 7800.0 5000 1500.0 20 3088 李莫愁 设计师 2056.0 3500 800.0 20 3211 张无忌 程序员 2056.0 3200 NaN 20 3233 丘处机 程序员 2056.0 3400 NaN 20 3244 欧阳锋 程序员 3088.0 3200 NaN 20 3251 张翠山 程序员 2056.0 4000 NaN 20 3344 黄蓉 销售主管 7800.0 3000 800.0 30
数据筛选
上面我们提到了花式索引,相信大家已经联想到了布尔索引。跟ndarray和Series一样,我们可以通过布尔索引对DataFrame对象进行数据筛选,例如我们要从emp_df中筛选出月薪超过3500的员工,代码如下所示。
1 emp_df[emp_df.sal > 3500 ]
输出:
1 2 3 4 5 6 ename job mgr sal comm dno eno 2056 乔峰 分析师 7800.0 5000 1500.0 20 3251 张翠山 程序员 2056.0 4000 NaN 20 5566 宋远桥 会计师 7800.0 4000 1000.0 10 7800 张三丰 总裁 NaN 9000 1200.0 20
当然,我们也可以组合多个条件来进行数据筛选,例如从emp_df中筛选出月薪超过3500且部门编号为20的员工,代码如下所示。
1 emp_df[(emp_df.sal > 3500 ) & (emp_df.dno == 20 )]
输出:
1 2 3 4 5 ename job mgr sal comm dno eno 2056 乔峰 分析师 7800.0 5000 1500.0 20 3251 张翠山 程序员 2056.0 4000 NaN 20 7800 张三丰 总裁 NaN 9000 1200.0 20
除了使用布尔索引,DataFrame对象的query方法也可以实现数据筛选,query方法的参数是一个字符串,它代表了筛选数据使用的表达式,而且更符合 Python 程序员的使用习惯。下面我们使用query方法将上面的效果重新实现一遍,代码如下所示。
1 emp_df.query('sal > 3500 and dno == 20' )
数据重塑
有的时候,我们做数据分析需要的原始数据可能并不是来自一个地方,就像上一章的例子中,我们从关系型数据库中读取了三张表,得到了三个DataFrame对象,但实际工作可能需要我们把他们的数据整合到一起。例如:emp_df和emp2_df其实都是员工的数据,而且数据结构完全一致,我们可以使用pandas提供的concat函数实现两个或多个DataFrame的数据拼接,代码如下所示。
1 all_emp_df = pd.concat([emp_df, emp2_df])
输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 ename job mgr sal comm dno eno 1359 胡一刀 销售员 3344.0 1800 200.0 30 2056 乔峰 分析师 7800.0 5000 1500.0 20 3088 李莫愁 设计师 2056.0 3500 800.0 20 3211 张无忌 程序员 2056.0 3200 NaN 20 3233 丘处机 程序员 2056.0 3400 NaN 20 3244 欧阳锋 程序员 3088.0 3200 NaN 20 3251 张翠山 程序员 2056.0 4000 NaN 20 3344 黄蓉 销售主管 7800.0 3000 800.0 30 3577 杨过 会计 5566.0 2200 NaN 10 3588 朱九真 会计 5566.0 2500 NaN 10 4466 苗人凤 销售员 3344.0 2500 NaN 30 5234 郭靖 出纳 5566.0 2000 NaN 10 5566 宋远桥 会计师 7800.0 4000 1000.0 10 7800 张三丰 总裁 NaN 9000 1200.0 20 9500 张三丰 总裁 NaN 50000 8000.0 20 9600 王大锤 程序员 9800.0 8000 600.0 20 9700 张三丰 总裁 NaN 60000 6000.0 20 9800 骆昊 架构师 7800.0 30000 5000.0 20 9900 陈小刀 分析师 9800.0 10000 1200.0 20
上面的代码将两个代表员工数据的DataFrame拼接到了一起,接下来我们使用merge函数将员工表和部门表的数据合并到一张表中,代码如下所示。
先使用reset_index方法重新设置all_emp_df的索引,这样eno 不再是索引而是一个普通列,reset_index方法的inplace参数设置为True表示,重置索引的操作直接在all_emp_df上执行,而不是返回修改后的新对象。
1 all_emp_df.reset_index(inplace=True )
通过merge函数合并数据,当然,也可以调用DataFrame对象的merge方法来达到同样的效果。
1 pd.merge(all_emp_df, dept_df, how='inner' , on='dno' )
输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 eno ename job mgr sal comm dno dname dloc 0 1359 胡一刀 销售员 3344.0 1800 200.0 30 销售部 重庆 1 3344 黄蓉 销售主管 7800.0 3000 800.0 30 销售部 重庆 2 4466 苗人凤 销售员 3344.0 2500 NaN 30 销售部 重庆 3 2056 乔峰 分析师 7800.0 5000 1500.0 20 研发部 成都 4 3088 李莫愁 设计师 2056.0 3500 800.0 20 研发部 成都 5 3211 张无忌 程序员 2056.0 3200 NaN 20 研发部 成都 6 3233 丘处机 程序员 2056.0 3400 NaN 20 研发部 成都 7 3244 欧阳锋 程序员 3088.0 3200 NaN 20 研发部 成都 8 3251 张翠山 程序员 2056.0 4000 NaN 20 研发部 成都 9 7800 张三丰 总裁 NaN 9000 1200.0 20 研发部 成都 10 9500 张三丰 总裁 NaN 50000 8000.0 20 研发部 成都 11 9600 王大锤 程序员 9800.0 8000 600.0 20 研发部 成都 12 9700 张三丰 总裁 NaN 60000 6000.0 20 研发部 成都 13 9800 骆昊 架构师 7800.0 30000 5000.0 20 研发部 成都 14 9900 陈小刀 分析师 9800.0 10000 1200.0 20 研发部 成都 15 3577 杨过 会计 5566.0 2200 NaN 10 会计部 北京 16 3588 朱九真 会计 5566.0 2500 NaN 10 会计部 北京 17 5234 郭靖 出纳 5566.0 2000 NaN 10 会计部 北京 18 5566 宋远桥 会计师 7800.0 4000 1000.0 10 会计部 北京
merge函数的一个参数代表合并的左表、第二个参数代表合并的右表,有SQL编程经验的同学对这两个词是不是感觉到非常亲切。正如大家猜想的那样,DataFrame对象的合并跟数据库中的表连接非常类似,所以上面代码中的how代表了合并两张表的方式,有left、right、inner、outer四个选项;而on则代表了基于哪个列实现表的合并,相当于 SQL 表连接中的连表条件,如果左右两表对应的列列名不同,可以用left_on和right_on参数取代on参数分别进行指定。
如果对上面的代码稍作修改,将how参数修改为'right',大家可以思考一下代码执行的结果。
1 pd.merge(all_emp_df, dept_df, how='right' , on='dno' )
运行结果比之前的输出多出了如下所示的一行,这是因为how='right'代表右外连接,也就意味着右表dept_df中的数据会被完整的查出来,但是在all_emp_df中又没有编号为40 部门的员工,所以对应的位置都被填入了空值。
1 19 NaN NaN NaN NaN NaN NaN 40 运维部 深圳
数据清洗
通常,我们从 Excel、CSV 或数据库中获取到的数据并不是非常完美的,里面可能因为系统或人为的原因混入了重复值或异常值,也可能在某些字段上存在缺失值;再者,DataFrame中的数据也可能存在格式不统一、量纲不统一等各种问题。因此,在开始数据分析之前,对数据进行清洗就显得特别重要。
缺失值
可以使用DataFrame对象的isnull或isna方法来找出数据表中的缺失值,如下所示。
或者
输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ename job mgr sal comm dno eno 1359 False False False False False False 2056 False False False False False False 3088 False False False False False False 3211 False False False False True False 3233 False False False False True False 3244 False False False False True False 3251 False False False False True False 3344 False False False False False False 3577 False False False False True False 3588 False False False False True False 4466 False False False False True False 5234 False False False False True False 5566 False False False False False False 7800 False False True False False False
相对应的,notnull和notna方法可以将非空的值标记为True。如果想删除这些缺失值,可以使用DataFrame对象的dropna方法,该方法的axis参数可以指定沿着0轴还是1轴删除,也就是说当遇到空值时,是删除整行还是删除整列,默认是沿0轴进行删除的,代码如下所示。
输出:
1 2 3 4 5 6 7 ename job mgr sal comm dno eno 1359 胡一刀 销售员 3344.0 1800 200.0 30 2056 乔峰 架构师 7800.0 5000 1500.0 20 3088 李莫愁 设计师 2056.0 3500 800.0 20 3344 黄蓉 销售主管 7800.0 3000 800.0 30 5566 宋远桥 会计师 7800.0 4000 1000.0 10
如果要沿着1轴进行删除,可以使用下面的代码。
输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ename job sal dno eno 1359 胡一刀 销售员 1800 30 2056 乔峰 架构师 5000 20 3088 李莫愁 设计师 3500 20 3211 张无忌 程序员 3200 20 3233 丘处机 程序员 3400 20 3244 欧阳锋 程序员 3200 20 3251 张翠山 程序员 4000 20 3344 黄蓉 销售主管 3000 30 3577 杨过 会计 2200 10 3588 朱九真 会计 2500 10 4466 苗人凤 销售员 2500 30 5234 郭靖 出纳 2000 10 5566 宋远桥 会计师 4000 10 7800 张三丰 总裁 9000 20
注意 :DataFrame对象的很多方法都有一个名为inplace的参数,该参数的默认值为False,表示我们的操作不会修改原来的DataFrame对象,而是将处理后的结果通过一个新的DataFrame对象返回。如果将该参数的值设置为True,那么我们的操作就会在原来的DataFrame上面直接修改,方法的返回值为None。简单的说,上面的操作并没有修改emp_df,而是返回了一个新的DataFrame对象。
在某些特定的场景下,我们可以对空值进行填充,对应的方法是fillna,填充空值时可以使用指定的值(通过value参数进行指定),也可以用表格中前一个单元格(通过设置参数method=ffill)或后一个单元格(通过设置参数method=bfill)的值进行填充,当代码如下所示。
注意 :填充的值如何选择也是一个值得探讨的话题,实际工作中,可能会使用某种统计量(如:均值、众数等)进行填充,或者使用某种插值法(如:随机插值法、拉格朗日插值法等)进行填充,甚至有可能通过回归模型、贝叶斯模型等对缺失数据进行填充。
输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ename job mgr sal comm dno eno 1359 胡一刀 销售员 3344.0 1800 200.0 30 2056 乔峰 分析师 7800.0 5000 1500.0 20 3088 李莫愁 设计师 2056.0 3500 800.0 20 3211 张无忌 程序员 2056.0 3200 0.0 20 3233 丘处机 程序员 2056.0 3400 0.0 20 3244 欧阳锋 程序员 3088.0 3200 0.0 20 3251 张翠山 程序员 2056.0 4000 0.0 20 3344 黄蓉 销售主管 7800.0 3000 800.0 30 3577 杨过 会计 5566.0 2200 0.0 10 3588 朱九真 会计 5566.0 2500 0.0 10 4466 苗人凤 销售员 3344.0 2500 0.0 30 5234 郭靖 出纳 5566.0 2000 0.0 10 5566 宋远桥 会计师 7800.0 4000 1000.0 10 7800 张三丰 总裁 0.0 9000 1200.0 20
重复值
接下来,我们先给之前的部门表添加两行数据,让部门表中名为“研发部”和“销售部”的部门各有两个。
1 2 3 dept_df.loc[50 ] = {'dname' : '研发部' , 'dloc' : '上海' } dept_df.loc[60 ] = {'dname' : '销售部' , 'dloc' : '长沙' } dept_df
输出:
1 2 3 4 5 6 7 8 dname dloc dno 10 会计部 北京 20 研发部 成都 30 销售部 重庆 40 运维部 天津 50 研发部 上海 60 销售部 长沙
现在,我们的数据表中有重复数据了,我们可以通过DataFrame对象的duplicated方法判断是否存在重复值,该方法在不指定参数时默认判断行索引是否重复,我们也可以指定根据部门名称dname判断部门是否重复,代码如下所示。
1 dept_df.duplicated('dname' )
输出:
1 2 3 4 5 6 7 8 dno 10 False 20 False 30 False 40 False 50 True 60 True dtype: bool
从上面的输出可以看到,50和60两个部门从部门名称上来看是重复的,如果要删除重复值,可以使用drop_duplicates方法,该方法的keep参数可以控制在遇到重复值时,保留第一项还是保留最后一项,或者多个重复项一个都不用保留,全部删除掉。
1 dept_df.drop_duplicates('dname' )
输出:
1 2 3 4 5 6 dname dloc dno 10 会计部 北京 20 研发部 成都 30 销售部 重庆 40 运维部 天津
将keep参数的值修改为last。
1 dept_df.drop_duplicates('dname' , keep='last' )
输出:
1 2 3 4 5 6 dname dloc dno 10 会计部 北京 40 运维部 天津 50 研发部 上海 60 销售部 长沙
使用同样的方式,我们也可以清除all_emp_df中的重复数据,例如我们认定“ename”和“job”两个字段完全相同的就是重复数据,我们可以用下面的代码去除重复数据。
1 all_emp_df.drop_duplicates(['ename' , 'job' ], inplace=True )
说明 :上面的drop_duplicates方法添加了参数inplace=True,该方法不会返回新的DataFrame对象,而是在原来的DataFrame对象上直接删除,大家可以查看all_emp_df看看是不是已经移除了重复的员工数据。
异常值
异常值在统计学上的全称是疑似异常值,也称作离群点(outlier),异常值的分析也称作离群点分析。异常值是指样本中出现的“极端值”,数据值看起来异常大或异常小,其分布明显偏离其余的观测值。实际工作中,有些异常值可能是由系统或人为原因造成的,但有些异常值却不是,它们能够重复且稳定的出现,属于正常的极端值,例如很多游戏产品中头部玩家的数据往往都是离群的极端值。所以,我们既不能忽视异常值的存在,也不能简单地把异常值从数据分析中剔除。重视异常值的出现,分析其产生的原因,常常成为发现问题进而改进决策的契机。
异常值的检测有Z-score 方法、IQR 方法、DBScan 聚类、孤立森林等,这里我们对前两种方法做一个简单的介绍。
如果数据服从正态分布,依据3σ法则,异常值被定义与平均值的偏差超过三倍标准差的值。在正态分布下,距离平均值3σ之外的值出现的概率为 P ( ∣ x − μ ∣ > 3 σ ) < 0.003 \small{P(\lvert x - \mu \rvert \gt 3 \sigma) < 0.003} P ( ∣ x − μ ∣ > 3 σ ) < 0 . 0 0 3 ,属于小概率事件。如果数据不服从正态分布,那么可以用远离均值的多少倍的标准差来描述,这里的倍数就是Z-score。Z-score以标准差为单位去度量某一原始分数偏离平均值的距离,公式如下所示。
z = X − μ σ z = \frac {X - \mu} {\sigma}
z = σ X − μ
∣ z ∣ > 3 \lvert z \rvert > 3
∣ z ∣ > 3
Z-score需要根据经验和实际情况来决定,通常把远离标准差 3 倍距离以上的数据点视为离群点,下面的代给出了如何通过Z-score方法检测异常值。
1 2 3 4 5 def detect_outliers_zscore (data, threshold=3 ): avg_value = np.mean(data) std_value = np.std(data) z_score = np.abs ((data - avg_value) / std_value) return data[z_score > threshold]
IQR 方法中的 IQR(Inter-Quartile Range)代表四分位距离,即上四分位数(Q3)和下四分位数(Q1)的差值。通常情况下,可以认为小于 Q 1 − 1.5 × I Q R \small{Q1 - 1.5 \times IQR} Q 1 − 1 . 5 × I Q R 或大于 Q 3 + 1.5 × I Q R \small{Q3 + 1.5 \times IQR} Q 3 + 1 . 5 × I Q R 的就是异常值,而这种检测异常值的方法也是箱线图(后面会讲到)默认使用的方法。下面的代码给出了如何通过 IQR 方法检测异常值。
1 2 3 4 5 def detect_outliers_iqr (data, whis=1.5 ): q1, q3 = np.quantile(data, [0.25 , 0.75 ]) iqr = q3 - q1 lower, upper = q1 - whis * iqr, q3 + whis * iqr return data[(data < lower) | (data > upper)]
如果要删除异常值,可以使用DataFrame对象的drop方法,该方法可以根据行索引或列索引删除指定的行或列。例如我们认为月薪低于2000或高于8000的是员工表中的异常值,可以用下面的代码删除对应的记录。
1 emp_df.drop(emp_df[(emp_df.sal > 8000 ) | (emp_df.sal < 2000 )].index)
如果要替换掉异常值,可以通过给单元格赋值的方式来实现,也可以使用replace方法将指定的值替换掉。例如我们要将月薪为1800和9000的替换为月薪的平均值,补贴为800的替换为1000,代码如下所示。
1 2 avg_sal = np.mean(emp_df.sal).astype(int ) emp_df.replace({'sal' : [1800 , 9000 ], 'comm' : 800 }, {'sal' : avg_sal, 'comm' : 1000 })
预处理
对数据进行预处理也是一个很大的话题,它包含了对数据的拆解、变换、归约、离散化等操作。我们先来看看数据的拆解。如果数据表中的数据是一个时间日期,我们通常都需要从年、季度、月、日、星期、小时、分钟等维度对其进行拆解,如果时间日期是用字符串表示的,可以先通过pandas的to_datetime函数将其处理成时间日期。
在下面的例子中,我们先读取 Excel 文件,获取到一组销售数据,其中第一列就是销售日期,我们将其拆解为“月份”、“季度”和“星期”,代码如下所示。
1 2 3 4 5 sales_df = pd.read_excel( 'data/2020年销售数据.xlsx' , usecols=['销售日期' , '销售区域' , '销售渠道' , '品牌' , '销售额' ] ) sales_df.info()
说明 :上面代码中使用了相对路径来获取 Excel 文件,也就是说 Excel 文件在当前工作路径下名为data的文件夹中。如果需要上面例子中的 Excel 文件,可以通过下面的百度云盘地址进行获取。链接:https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g ,提取码:e7b4。
输出:
1 2 3 4 5 6 7 8 9 10 11 12 <class 'pandas.core.frame.DataFrame'> RangeIndex: 1945 entries, 0 to 1944 Data columns (total 5 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 销售日期 1945 non-null datetime64[ns] 1 销售区域 1945 non-null object 2 销售渠道 1945 non-null object 3 品牌 1945 non-null object 4 销售额 1945 non-null int64 dtypes: datetime64[ns](1), int64(1), object(3) memory usage: 76.1+ KB
1 2 3 4 sales_df['月份' ] = sales_df['销售日期' ].dt.month sales_df['季度' ] = sales_df['销售日期' ].dt.quarter sales_df['星期' ] = sales_df['销售日期' ].dt.weekday sales_df
输出:
1 2 3 4 5 6 7 8 9 10 11 12 销售日期 销售区域 销售渠道 品牌 销售额 月份 季度 星期 0 2020-01-01 上海 拼多多 八匹马 8217 1 1 2 1 2020-01-01 上海 抖音 八匹马 6351 1 1 2 2 2020-01-01 上海 天猫 八匹马 14365 1 1 2 3 2020-01-01 上海 天猫 八匹马 2366 1 1 2 4 2020-01-01 上海 天猫 皮皮虾 15189 1 1 2 ... ... ... ... ... ... ... ... ... 1940 2020-12-30 北京 京东 花花姑娘 6994 12 4 2 1941 2020-12-30 福建 实体 八匹马 7663 12 4 2 1942 2020-12-31 福建 实体 花花姑娘 14795 12 4 3 1943 2020-12-31 福建 抖音 八匹马 3481 12 4 3 1944 2020-12-31 福建 天猫 八匹马 2673 12 4 3
在上面的代码中,通过日期时间类型的Series对象的dt 属性,获得一个访问日期时间的对象,通过该对象的year、month、quarter、hour等属性,就可以获取到年、月、季度、小时等时间信息,获取到的仍然是一个Series对象,它包含了一组时间信息,所以我们通常也将这个dt属性称为“日期时间向量”。
我们再来说一说字符串类型的数据的处理,我们先从指定的 Excel 文件中读取某招聘网站的招聘数据。
1 2 3 4 5 jobs_df = pd.read_csv( 'data/某招聘网站招聘数据.csv' , usecols=['city' , 'companyFullName' , 'positionName' , 'salary' ] ) jobs_df.info()
说明 :上面代码中使用了相对路径来获取 CSV 文件,也就是说 CSV 文件在当前工作路径下名为data的文件夹中。如果需要上面例子中的 CSV 文件,可以通过下面的百度云盘地址进行获取。链接:https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g ,提取码:e7b4。
输出:
1 2 3 4 5 6 7 8 9 10 11 <class 'pandas.core.frame.DataFrame'> RangeIndex: 3140 entries, 0 to 3139 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 city 3140 non-null object 1 companyFullName 3140 non-null object 2 positionName 3140 non-null object 3 salary 3140 non-null object dtypes: object(4) memory usage: 98.2+ KB
查看前5条数据。
输出:
1 2 3 4 5 6 city companyFullName positionName salary 0 北京 达疆网络科技(上海)有限公司 数据分析岗 15k-30k 1 北京 北京音娱时光科技有限公司 数据分析 10k-18k 2 北京 北京千喜鹤餐饮管理有限公司 数据分析 20k-30k 3 北京 吉林省海生电子商务有限公司 数据分析 33k-50k 4 北京 韦博网讯科技(北京)有限公司 数据分析 10k-15k
上面的数据表一共有3140条数据,但并非所有的职位都是“数据分析”的岗位,如果要筛选出数据分析的岗位,可以通过检查positionName字段是否包含“数据分析”这个关键词,这里需要模糊匹配,应该如何实现呢?我们可以先获取positionName列,因为这个Series对象的dtype是字符串,所以可以通过str属性获取对应的字符串向量,然后就可以利用我们熟悉的字符串的方法来对其进行操作,代码如下所示。
1 2 jobs_df = jobs_df[jobs_df.positionName.str .contains('数据分析' )] jobs_df.shape
输出:
可以看出,筛选后的数据还有1515条。接下来,我们还需要对salary字段进行处理,如果我们希望统计所有岗位的平均工资或每个城市的平均工资,首先需要将用范围表示的工资处理成其中间值,代码如下所示。
1 jobs_df.salary.str .extract(r'(\d+)[kK]?-(\d+)[kK]?' )
说明 :上面的代码通过正则表达式捕获组从字符串中抽取出两组数字,分别对应工资的下限和上限,对正则表达式不熟悉的读者,可以阅读我的知乎专栏“从零开始学Python”中的《正则表达式的应用》 一文。
输出:
1 2 3 4 5 6 7 8 9 10 11 12 0 1 0 15 30 1 10 18 2 20 30 3 33 50 4 10 15 ... ... ... 3065 8 10 3069 6 10 3070 2 4 3071 6 12 3088 8 12
需要提醒大家的是,抽取出来的两列数据都是字符串类型的值,我们需要将其转换成int类型,才能计算平均值,对应的方法是DataFrame对象的applymap方法,该方法的参数是一个函数,而该函数会作用于DataFrame中的每个元素。完成这一步之后,我们就可以使用apply方法将上面的DataFrame处理成中间值,apply方法的参数也是一个函数,可以通过指定axis参数使其作用于DataFrame 对象的行或列,代码如下所示。
1 2 temp_df = jobs_df.salary.str .extract(r'(\d+)[kK]?-(\d+)[kK]?' ).applymap(int ) temp_df.apply(np.mean, axis=1 )
输出:
1 2 3 4 5 6 7 8 9 10 11 12 0 22.5 1 14.0 2 25.0 3 41.5 4 12.5 ... 3065 9.0 3069 8.0 3070 3.0 3071 9.0 3088 10.0 Length: 1515, dtype: float64
接下来,我们可以用上面的结果替换掉原来的salary列或者增加一个新的列来表示职位对应的工资,完整的代码如下所示。
1 2 3 temp_df = jobs_df.salary.str .extract(r'(\d+)[kK]?-(\d+)[kK]?' ).applymap(int ) jobs_df['salary' ] = temp_df.apply(np.mean, axis=1 ) jobs_df.head()
输出:
1 2 3 4 5 6 city companyFullName positionName salary 0 北京 达疆网络科技(上海)有限公司 数据分析岗 22.5 1 北京 北京音娱时光科技有限公司 数据分析 14.0 2 北京 北京千喜鹤餐饮管理有限公司 数据分析 25.0 3 北京 吉林省海生电子商务有限公司 数据分析 41.5 4 北京 韦博网讯科技(北京)有限公司 数据分析 12.5
applymap和apply两个方法在数据预处理的时候经常用到,Series对象也有apply方法,也是用于数据的预处理,但是DataFrame对象还有一个名为transform 的方法,也是通过传入的函数对数据进行变换,类似Series对象的map方法。需要强调的是,apply方法具有归约效果的,简单的说就是能将较多的数据处理成较少的数据或一条数据;而transform方法没有归约效果,只能对数据进行变换,原来有多少条数据,处理后还是有多少条数据。
如果要对数据进行深度的分析和挖掘,字符串、日期时间这样的非数值类型都需要处理成数值,因为非数值类型没有办法计算相关性,也没有办法进行 χ 2 \small{\chi^{2}} χ 2 检验等操作。对于字符串类型,通常可以其分为以下三类,再进行对应的处理。
有序变量(Ordinal Variable):字符串表示的数据有顺序关系,那么可以对字符串进行序号化处理。
分类变量(Categorical Variable)/ 名义变量(Nominal Variable):字符串表示的数据没有大小关系和等级之分,那么就可以使用独热编码的方式处理成哑变量(虚拟变量)矩阵。
定距变量(Scale Variable):字符串本质上对应到一个有大小高低之分的数据,而且可以进行加减运算,那么只需要将字符串处理成对应的数值即可。
对于第1类和第3类,我们可以用上面提到的apply或transform方法来处理,也可以利用scikit-learn中的OrdinalEncoder处理第1类字符串,这个我们在后续的课程中会讲到。对于第2类字符串,可以使用pandas的get_dummies()函数来生成哑变量(虚拟变量)矩阵,代码如下所示。
1 2 3 4 5 6 7 8 persons_df = pd.DataFrame( data={ '姓名' : ['关羽' , '张飞' , '赵云' , '马超' , '黄忠' ], '职业' : ['医生' , '医生' , '程序员' , '画家' , '教师' ], '学历' : ['研究生' , '大专' , '研究生' , '高中' , '本科' ] } ) persons_df
输出:
1 2 3 4 5 6 姓名 职业 学历 0 关羽 医生 研究生 1 张飞 医生 大专 2 赵云 程序员 研究生 3 马超 画家 高中 4 黄忠 教师 本科
将职业处理成哑变量矩阵。
1 pd.get_dummies(persons_df['职业' ])
输出:
1 2 3 4 5 6 医生 教师 画家 程序员 0 1 0 0 0 1 1 0 0 0 2 0 0 0 1 3 0 0 1 0 4 0 1 0 0
将学历处理成大小不同的值。
1 2 3 4 5 6 def handle_education (x ): edu_dict = {'高中' : 1 , '大专' : 3 , '本科' : 5 , '研究生' : 10 } return edu_dict.get(x, 0 ) persons_df['学历' ].apply(handle_education)
输出:
1 2 3 4 5 6 0 10 1 3 2 10 3 1 4 5 Name: 学历, dtype: int64
我们再来说说数据离散化。离散化也叫分箱,如果变量的取值是连续值,那么它的取值有无数种可能,在进行数据分组的时候就会非常的不方便,这个时候将连续变量离散化就显得非常重要。之所以把离散化叫做分箱,是因为我们可以预先设置一些箱子,每个箱子代表了数据取值的范围,这样就可以将连续的值分配到不同的箱子中,从而实现离散化。下面的例子读取了2018年北京积分落户数据,我们可以根据落户积分对数据进行分组,具体的做法如下所示。
1 2 luohu_df = pd.read_csv('data/2018年北京积分落户数据.csv' , index_col='id' ) luohu_df.score.describe()
输出:
1 2 3 4 5 6 7 8 9 count 6019.000000 mean 95.654552 std 4.354445 min 90.750000 25% 92.330000 50% 94.460000 75% 97.750000 max 122.590000 Name: score, dtype: float64
可以看出,落户积分的最大值是122.59,最小值是90.75,那么我们可以构造一个从90分到125分,每5分一组的7个箱子,pandas的cut函数可以帮助我们首先数据分箱,代码如下所示。
1 2 bins = np.arange(90 , 126 , 5 ) pd.cut(luohu_df.score, bins, right=False )
说明 :cut函数的right参数默认值为True,表示箱子左开右闭;修改为False可以让箱子的右边界为开区间,左边界为闭区间,大家看看下面的输出就明白了。
输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 id 1 [120, 125) 2 [120, 125) 3 [115, 120) 4 [115, 120) 5 [115, 120) ... 6015 [90, 95) 6016 [90, 95) 6017 [90, 95) 6018 [90, 95) 6019 [90, 95) Name: score, Length: 6019, dtype: category Categories (7, interval[int64, left]): [[90, 95) < [95, 100) < [100, 105) < [105, 110) < [110, 115) < [115, 120) < [120, 125)]
我们可以根据分箱的结果对数据进行分组,然后使用聚合函数对每个组进行统计,这是数据分析中经常用到的操作,下一个章节会为大家介绍。除此之外,pandas还提供了一个名为qcut的函数,可以指定分位数对数据进行分箱,有兴趣的读者可以自行研究。
数据透视
经过前面的学习,我们已经将数据准备就绪而且变成了我们想要的样子,接下来就是最为重要的数据透视阶段了。当我们拿到一大堆数据的时候,如何从数据中迅速的解读出有价值的信息,把繁杂的数据变成容易解读的统计图表并再此基础上产生业务洞察,这就是数据分析要解决的核心问题。
获取描述性统计信息
首先,我们可以获取数据的描述性统计信息,通过描述性统计信息,我们可以了解数据的集中趋势和离散趋势。
例如,我们有如下所示的学生成绩表。
1 2 3 4 5 scores = np.random.randint(50 , 101 , (5 , 3 )) names = ('关羽' , '张飞' , '赵云' , '马超' , '黄忠' ) courses = ('语文' , '数学' , '英语' ) df = pd.DataFrame(data=scores, columns=courses, index=names) df
输出:
1 2 3 4 5 6 语文 数学 英语 关羽 96 72 73 张飞 72 70 97 赵云 74 51 79 马超 100 54 54 黄忠 89 100 88
我们可以通过DataFrame对象的方法mean、max、min、std、var等方法分别获取每个学生或每门课程的平均分、最高分、最低分、标准差、方差等信息,也可以直接通过describe方法直接获取描述性统计信息,代码如下所示。
计算每门课程成绩的平均分。
输出:
1 2 3 4 语文 86.2 数学 69.4 英语 78.2 dtype: float64
计算每个学生成绩的平均分。
输出:
1 2 3 4 5 6 关羽 80.333333 张飞 79.666667 赵云 68.000000 马超 69.333333 黄忠 92.333333 dtype: float64
计算每门课程成绩的方差。
输出:
1 2 3 4 语文 161.2 数学 379.8 英语 265.7 dtype: float64
说明 :通过方差可以看出,数学成绩波动最大,两极分化可能更严重。
获取每门课程的描述性统计信息。
输出:
1 2 3 4 5 6 7 8 9 语文 数学 英语 count 5.000000 5.000000 5.000000 mean 86.200000 69.400000 78.200000 std 12.696456 19.488458 16.300307 min 72.000000 51.000000 54.000000 25% 74.000000 54.000000 73.000000 50% 89.000000 70.000000 79.000000 75% 96.000000 72.000000 88.000000 max 100.000000 100.000000 97.000000
排序和取头部值
如果需要对数据进行排序,可以使用DataFrame对象的sort_values方法,该方法的by参数可以指定根据哪个列或哪些列进行排序,而ascending参数可以指定升序或是降序。例如,下面的代码展示了如何将学生表按语文成绩排降序。
1 df.sort_values(by='语文' , ascending=False )
输出:
1 2 3 4 5 6 语文 数学 英语 马超 100 54 54 关羽 96 72 73 黄忠 89 100 88 赵云 74 51 79 张飞 72 70 97
如果DataFrame数据量很大,排序将是一个非常耗费时间的操作。有的时候我们只需要获得排前N名或后N名的数据,这个时候其实没有必要对整个数据进行排序,而是直接利用堆结构找出Top-N的数据。DataFrame的nlargest和nsmallest方法就提供对Top-N操作的支持,代码如下所示。
找出语文成绩前3名的学生信息。
输出:
1 2 3 4 语文 数学 英语 马超 100 54 54 关羽 96 72 73 黄忠 89 100 88
找出数学成绩最低的3名学生的信息。
输出:
1 2 3 4 语文 数学 英语 赵云 74 51 79 马超 100 54 54 张飞 72 70 97
分组聚合
我们先从之前使用过的 Excel 文件中读取2020年销售数据,然后再为大家演示如何进行分组聚合操作。
1 2 df = pd.read_excel('data/2020年销售数据.xlsx' ) df.head()
输出:
1 2 3 4 5 6 销售日期 销售区域 销售渠道 销售订单 品牌 售价 销售数量 0 2020-01-01 上海 拼多多 182894-455 八匹马 99 83 1 2020-01-01 上海 抖音 205635-402 八匹马 219 29 2 2020-01-01 上海 天猫 205654-021 八匹马 169 85 3 2020-01-01 上海 天猫 205654-519 八匹马 169 14 4 2020-01-01 上海 天猫 377781-010 皮皮虾 249 61
如果我们要统计每个销售区域的销售总额,可以先通过“售价”和“销售数量”计算出销售额,为DataFrame添加一个列,代码如下所示。
1 2 df['销售额' ] = df['售价' ] * df['销售数量' ] df.head()
输出:
1 2 3 4 5 6 销售日期 销售区域 销售渠道 销售订单 品牌 售价 销售数量 销售额 0 2020-01-01 上海 拼多多 182894-455 八匹马 99 83 8217 1 2020-01-01 上海 抖音 205635-402 八匹马 219 29 6351 2 2020-01-01 上海 天猫 205654-021 八匹马 169 85 14365 3 2020-01-01 上海 天猫 205654-519 八匹马 169 14 2366 4 2020-01-01 上海 天猫 377781-010 皮皮虾 249 61 15189
然后再根据“销售区域”列对数据进行分组,这里我们使用的是DataFrame对象的groupby方法。分组之后,我们取“销售额”这个列在分组内进行求和处理,代码和结果如下所示。
1 df.groupby('销售区域' ).销售额.sum ()
输出:
1 2 3 4 5 6 7 8 9 销售区域 上海 11610489 北京 12477717 安徽 895463 广东 1617949 江苏 2304380 浙江 687862 福建 10178227 Name: 销售额, dtype: int64
如果我们要统计每个月的销售总额,我们可以将“销售日期”作为groupby`方法的参数,当然这里需要先将“销售日期”处理成月,代码和结果如下所示。
1 df.groupby(df['销售日期' ].dt.month).销售额.sum ()
输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 销售日期 1 5409855 2 4608455 3 4164972 4 3996770 5 3239005 6 2817936 7 3501304 8 2948189 9 2632960 10 2375385 11 2385283 12 1691973 Name: 销售额, dtype: int64
接下来我们将难度升级,统计每个销售区域每个月的销售总额,这又该如何处理呢?事实上,groupby方法的第一个参数可以是一个列表,列表中可以指定多个分组的依据,大家看看下面的代码和输出结果就明白了。
1 df.groupby(['销售区域' , df['销售日期' ].dt.month]).销售额.sum ()
输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 销售区域 销售日期 上海 1 1679125 2 1689527 3 1061193 4 1082187 5 841199 6 785404 7 863906 8 734937 9 1107693 10 412108 11 825169 12 528041 北京 1 1878234 2 1807787 3 1360666 4 1205989 5 807300 6 1216432 7 1219083 8 645727 9 390077 10 671608 11 678668 12 596146 安徽 4 341308 5 554155 广东 3 388180 8 469390 9 365191 11 395188 江苏 4 537079 7 841032 10 710962 12 215307 浙江 3 248354 8 439508 福建 1 1852496 2 1111141 3 1106579 4 830207 5 1036351 6 816100 7 577283 8 658627 9 769999 10 580707 11 486258 12 352479 Name: 销售额, dtype: int64
如果希望统计出每个区域的销售总额以及每个区域单笔金额的最高和最低,我们可以在DataFrame或Series对象上使用agg方法并指定多个聚合函数,代码和结果如下所示。
1 df.groupby('销售区域' ).销售额.agg(['sum' , 'max' , 'min' ])
输出:
1 2 3 4 5 6 7 8 9 sum max min 销售区域 上海 11610489 116303 948 北京 12477717 133411 690 安徽 895463 68502 1683 广东 1617949 120807 990 江苏 2304380 114312 1089 浙江 687862 90909 3927 福建 10178227 87527 897
如果希望自定义聚合后的列的名字,可以使用如下所示的方法。
1 df.groupby('销售区域' ).销售额.agg(销售总额='sum' , 单笔最高='max' , 单笔最低='min' )
输出:
1 2 3 4 5 6 7 8 9 销售总额 单笔最高 单笔最低 销售区域 上海 11610489 116303 948 北京 12477717 133411 690 安徽 895463 68502 1683 广东 1617949 120807 990 江苏 2304380 114312 1089 浙江 687862 90909 3927 福建 10178227 87527 897
如果需要对多个列使用不同的聚合函数,例如“统计每个销售区域销售额的总和以及销售数量的最低值和最高值”,我们可以按照下面的方式来操作。
1 2 3 df.groupby('销售区域' )[['销售额' , '销售数量' ]].agg({ '销售额' : 'sum' , '销售数量' : ['max' , 'min' ] })
输出:
1 2 3 4 5 6 7 8 9 10 销售额 销售数量 sum max min 销售区域 上海 11610489 100 10 北京 12477717 100 10 安徽 895463 98 16 广东 1617949 98 10 江苏 2304380 100 11 浙江 687862 95 20 福建 10178227 100 10
透视表和交叉表
上面的例子中,“统计每个销售区域每个月的销售总额”会产生一个看起来很长的结果,在实际工作中我们通常把那些行很多列很少的表成为“窄表”,如果我们不想得到这样的一个“窄表”,可以使用DataFrame的pivot_table方法或者是pivot_table函数来生成透视表。透视表的本质就是对数据进行分组聚合操作,根据 A 列对 B 列进行统计 ,如果大家有使用 Excel 的经验,相信对透视表这个概念一定不会陌生。例如,我们要“统计每个销售区域的销售总额”,那么“销售区域”就是我们的 A 列,而“销售额”就是我们的 B 列,在pivot_table函数中分别对应index和values参数,这两个参数都可以是单个列或者多个列。
1 pd.pivot_table(df, index='销售区域' , values='销售额' , aggfunc='sum' )
输出:
1 2 3 4 5 6 7 8 9 销售额 销售区域 上海 11610489 北京 12477717 安徽 895463 广东 1617949 江苏 2304380 浙江 687862 福建 10178227
注意 :上面的结果操作跟之前用groupby的方式得到的结果有一些区别,groupby操作后,如果对单个列进行聚合,得到的结果是一个Series对象,而上面的结果是一个DataFrame 对象。
如果要统计每个销售区域每个月的销售总额,也可以使用pivot_table函数,代码如下所示。
1 2 df['月份' ] = df['销售日期' ].dt.month pd.pivot_table(df, index=['销售区域' , '月份' ], values='销售额' , aggfunc='sum' )
上面的操作结果是一个DataFrame,但也是一个长长的“窄表”,如果希望做成一个行比较少列比较多的“宽表”,可以将index参数中的列放到columns参数中,代码如下所示。
1 pd.pivot_table(df, index='销售区域' , columns='月份' , values='销售额' , aggfunc='sum' , fill_value=0 )
说明 :pivot_table函数的fill_value=0会将空值处理为0。
输出:
使用pivot_table函数时,还可以通过添加margins和margins_name参数对分组聚合的结果做一个汇总,具体的操作和效果如下所示。
1 pd.pivot_table(df, index='销售区域' , columns='月份' , values='销售额' , aggfunc='sum' , fill_value=0 , margins=True , margins_name='总计' )
输出:
交叉表就是一种特殊的透视表,它不需要先构造一个DataFrame对象,而是直接通过数组或Series对象指定两个或多个因素进行运算得到统计结果。例如,我们要统计每个销售区域的销售总额,也可以按照如下所示的方式来完成,我们先准备三组数据。
1 sales_area, sales_month, sales_amount = df['销售区域' ], df['月份' ], df['销售额' ]
使用crosstab函数生成交叉表。
1 pd.crosstab(index=sales_area, columns=sales_month, values=sales_amount, aggfunc='sum' ).fillna(0 ).astype('i8' )
说明 :上面的代码使用了DataFrame对象的fillna方法将空值处理为0,再使用astype方法将数据类型处理成整数。
数据呈现
一图胜千言,我们对数据进行透视的结果,最终要通过图表的方式呈现出来,因为图表具有极强的表现力,能够让我们迅速的解读数据中隐藏的价值。和Series一样,DataFrame对象提供了plot方法来支持绘图,底层仍然是通过matplotlib库实现图表的渲染。关于matplotlib的内容,我们在下一个章节进行详细的探讨,这里我们只简单的讲解plot方法的用法。
例如,我们想通过一张柱状图来比较“每个销售区域的销售总额”,可以直接在透视表上使用plot方法生成柱状图。我们先导入matplotlib.pyplot模块,通过修改绘图的参数使其支持中文显示。
1 2 3 import matplotlib.pyplot as pltplt.rcParams['font.sans-serif' ] = 'FZJKai-Z03S'
说明 :上面的FZJKai-Z03S是我电脑上已经安装的一种支持中文的字体的名称,字体的名称可以通过查看用户主目录下.matplotlib文件夹下名为fontlist-v330.json的文件来获得,而这个文件在执行上面的命令后就会生成。
使用魔法指令配置生成矢量图。
1 %config InlineBackend.figure_format = 'svg'
绘制“每个销售区域销售总额”的柱状图。
1 2 3 4 temp = pd.pivot_table(df, index='销售区域' , values='销售额' , aggfunc='sum' ) temp.plot(figsize=(8 , 4 ), kind='bar' ) plt.xticks(rotation=0 ) plt.show()
说明 :上面的第3行代码会将横轴刻度上的文字旋转到0度,第4行代码会显示图像。
输出:
如果要绘制饼图,可以修改plot方法的kind参数为pie,然后使用定制饼图的参数对图表加以定制,代码如下所示。
1 2 3 4 5 6 7 8 9 10 11 temp.sort_values(by='销售额' , ascending=False ).plot( figsize=(6 , 6 ), kind='pie' , y='销售额' , ylabel='' , autopct='%.2f%%' , pctdistance=0.8 , wedgeprops=dict (linewidth=1 , width=0.35 ), legend=False ) plt.show()
输出: