《Python数据科学实践指南》笔记

Python标准库

这里记录的都是笔者之前没用到或者用的比较少的模块。

math模块

想要进行科学计算,math模块是必不可少的,这个模块实现了很多复合IEEE标准的功能,比如浮点型转换、对数计算,以及三角函数,等等。而且这个模块的大部分功能都是用C语言实现的,拥有极高的计算效率。

常见常量

In [29]: import math

In [30]: math.pi
Out[30]: 3.141592653589793

In [31]: math.e
Out[31]: 2.718281828459045

### 指定留小数后30位的精度
In [32]: print("pi:%.30f" % math.pi)
pi:3.141592653589793115997963468544

In [33]: print("e:%.30f" % math.e)
e:2.718281828459045090795598298428

math模块采用的虽是硬编码的pi和e的值,但在指定了保留小数点后30位的精度时,也能够给出更高精度的数值,这是怎么回事呢?实际上math模块中的值只不过是一个快捷方式,真正计算时会通过内部的C语言模块获取精度更高的版本,所以不用担心计算的精度问题。

无穷

无穷在数学中是一个复杂的问题,在Python中也不简单。一般来说Python中所有的浮点型都能够达到双精度浮点型的取值范围,即1.0E-37到1.0E+37。超出了这个范围的则称作无穷INF,下面来参考一段程序:

import math

for i in range(0, 201, 20):
    x = 10.0 * i
    y = x ** 100
    print("{} {} {} {}".format(math.e,x,y,math.isinf(y)))

会出现异常:

2f5adb3604ec298bac81d6d324663ffd.png

浮点数到整数的转换 ***

浮点型转换为整数类型时,在math模块中共有三种方法,math.trunc()会将浮点型小数点后面的数字全部截掉,只留下整数的部分math.floor()方法会取比当前浮点型小的最近的整数;而math.ceil()则正好与之相反,是取比当前浮点型大的最近的整数,具体的例子可以参考下面的程序:

import math


lst = [3.46,3.21,4.23,5.42,5.78,-6.12]
print("i     int  trunc  floor  ceil")
for i in lst:
    print(i,int(i),math.trunc(i),math.floor(i),math.ceil(i),sep="   ")

结果如下:

i     int trunc floor  ceil
3.46   3   3     3   4
3.21   3   3     3   4
4.23   4   4     4   5
5.42   5   5     5   6
5.78   5   5     5   6
-6.12  -6  -6   -7   -6

绝对值与符号

在math中可以使用fabs()函数计算浮点数的绝对值,比如:

In [40]: import math

In [41]: math.fabs(-1.223344)
Out[41]: 1.223344

In [42]: math.fabs(-0.0)
Out[42]: 0.0

In [43]: math.fabs(0.0)
Out[43]: 0.0

In [44]: math.fabs(1.223344)
Out[44]: 1.223344

为了给一个值设定一个确定的符号,可以使用math.copysign()方法:

import math


lst1 = [-1.0,0.0,float("-inf"),float("inf"),float("-nan"),float("nan")]
print(lst1) # [-1.0, 0.0, -inf, inf, nan, nan]
for f in lst1:
    s = int(math.copysign(1,f))
    print("{:5.1f} {:5d} {} {} {}".format(f, s, f<0, f>0, f==0))
"""
 -1.0    -1 True False False
  0.0     1 False False True
 -inf    -1 True False False
  inf     1 False True False
  nan    -1 False False False
  nan     1 False False False
"""

这里是将第一行程序中列表里的值的符号指定给1,然后观察这个新值的符号的变化,math.copysign()函数的第一个参数是需要被指定符号的值,第二个参数是提供符号的值,即将数据中第一个参数的值指定为第二个参数的符号从上面代码的运行结果可以看出,第二列中的“1”的符号与for循环迭代的列表中的值符号完全一致。值得注意的是,不仅普通的数字有正负之分,0、无穷inf、都是有正负之分的,而无意义nan则既不是小于0也不是大于0,更不是等于0的数,所以它是无意义的数。

常用计算:精确累和与阶乘 ***

在使用计算机程序进行浮点数的计算时,通常会由于精度的问题引入额外的误差,最常见的情况就是将10个0.1相加,其结果并不是1:

In [47]: values = [0.1] * 10

In [48]: values
Out[48]: [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]

In [49]: sum(values)
Out[49]: 0.9999999999999999

上面使用了两种常见的Python累加方法,都无法得到1.0的结果,可能有的时候我们并不太关心这个非常接近1.0的数到底差多少才等于1.0,但是当计算账目或反复的迭代时,误差会被积累,以至于最终产生客观的差距。math模块提供了一个函数fsum()可以进行精确地计算,如下:

In [50]: values = [0.1] * 10

In [51]: values
Out[51]: [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]

In [52]: math.fsum(values)
Out[52]: 1.0

math模块中也提供了简单的阶乘计算函数factorial()

In [54]: import math

In [55]: lst = [0,1.0,2.0,3.0,4.0]

In [56]: for i in lst:
    ...:     print(i,math.factorial(i))
    ...:
    
# 结果
0    1    # 注意 0的结果是1
1.0  1
2.0  2
3.0  6
4.0  24

指数与对数

指数增长曲线,在社会学和经济学中都有着广泛的应用,对数则是指数表达的一种特殊形式。Python的math模块中也提供了指数计算math.pow(),如下代码所示。

import math


x = 2
y = 3
print(x,y,math.pow(x,y)) # x的y次方
"""
2 3 8.0
"""

x = 2.2
y = 3.3
print(x,y,math.pow(x,y)) # x的y次方
"""
2.2 3.3 13.489468760533386
"""

对数计算math.log()/math.log10()/math.exp():

其中log()函数的第二个参数是对数的底,默认情况下是自然底数e,如果要提供第二个参数,则应手动指定底数。

import math


### 默认以e为底
print(math.log(8))
"""
2.0794415416798357
"""

### 第二个参数指定底
print(math.log(8,2))
"""
3.0
"""
print(math.log(0.5,2))
"""
-1.0
"""

而且math还专门提供了一个log10()的函数,用来以更高的精度处理以10为底的对数计算,这是因为以10为底的对数经常用作统计数轴等对精度要求更高的计算

我们可以对比一下以10为底,选择不同指数计算的值在求对数时还原的精度,如下:

# -*- coding:utf-8 -*-
import math


for i in range(0,10):
    x = math.pow(10,i)
    # 准确值 —— 使用math.log10()
    accurate = math.log1p(x)
    # 不准确的值 —— 使用math.log()指定10为底数
    inaccurate = math.log(x,10)
    print(i,x,accurate,inaccurate,sep="   ")
"""
0   1.0   0.6931471805599453   0.0
1   10.0   2.3978952727983707   1.0
2   100.0   4.61512051684126   2.0
3   1000.0   6.90875477931522   2.9999999999999996
4   10000.0   9.210440366976517   4.0
5   100000.0   11.51293546492023   5.0
6   1000000.0   13.815511557963774   5.999999999999999
7   10000000.0   16.118095750958314   7.0
8   100000000.0   18.420680753952364   8.0
9   1000000000.0   20.72326583794641   8.999999999999998
"""    

可以看到,在指数为3、6、9时log()函数的精度相比log10()有一定的损失。

还可以通过math.exp()函数来计算自然底数e的指数值,代码如下:

import math


print(math.e ** 2) # 7.3890560989306495

print(math.pow(math.e,2)) # 7.3890560989306495

print(math.exp(2)) # 7.38905609893065

使用random模块取样

有的时候我们需要打乱一些数据的顺序,随机选择一个或多个值,这个时候就需要借助random模块中的shuffle()、choice()、sample()这几个函数了,示例如下:

# -*- coding:utf-8 -*-
import random


a = [0,1,2,3,4,5,6,7,8,9,10]

### shuffle方法没有返回值 而是把原列表的数打乱
random.shuffle(a)
print(a) # [9, 6, 5, 4, 0, 10, 1, 8, 3, 2, 7]

### choice
new1 = random.choice(a)
print(new1) # 8

### sample
new2 = random.sample(a,5)
print(new2) # [0, 6, 5, 8, 3]

glob与fileinput配合读取多个文件的内容

在Python中最常用的文本读写方式就是使用open()函数了。

open()函数的第一个参数是要打开文件的路径,第二个参数是打开文件的方式,如果该参数是“r”则代表以只读的方式打开,这样就不会不小心篡改了文件的内容而没有发觉。如果第二个参数为“a”则代表以读写的方式打开,这时候就即能读又能写这个文件。open()函数会返回文件操作符,习惯上来讲,如果是以只读的方式打开文件的,那么会将文件描述符赋值为fr;如果是以读写方式打开文件的则赋值为fw,这样处理之后,就可以区分前者是只读,后者是可写(r是read的缩写,w是write的缩写,f则代表file)。然后使用文件描述符的readlines()方法按行读取文件内容[插图],再使用for循环进行迭代打印。最后在退出程序之前不要忘记关闭文件描述符,使用close()方法来关闭,如果在文件写入时没有调用close()方法则可能会导致部分数据写入不成功。

现在就可以读取一个文件的内容了,那么如果想要同时读取多个文件的内容呢?
首先,需要获取所有文件的文件名,这时需要用到glob模块
假设现在我们有一个目录/Users/wanghongwei/Desktop/logs,其中有3个文件:log1.log,log2.log,log3.log。

我们可以使用下面的命令获取这三个文件的绝对路径:

In [57]: from glob import glob

In [58]: file_path = glob("/Users/wanghongwei/Desktop/logs/*")

In [59]: file_path
Out[59]:
['/Users/wanghongwei/Desktop/logs/log2.log',
 '/Users/wanghongwei/Desktop/logs/log3.log',
 '/Users/wanghongwei/Desktop/logs/log1.log']

其中glob()函数的参数是一个Linux通配符的写法,在写完了路径前缀之后以一个“*”号代表这个路径的全部文件都会被选中,如果logs中还有其他的目录,并且在目录中还有其他的文件,那么也可以使用“/Users/jilu/Downloads/logs/*/*”来表示,多进入一层目录。

之后就可以使用fileinput模块的input()方法直接读取文件的每一行了,完整示例如下:

# -*- coding:utf-8 -*-
from glob import glob
import fileinput


# 找出所有文件的绝对路径
file_path = glob("/Users/wanghongwei/Desktop/logs/*")
print(file_path)
"""
['/Users/wanghongwei/Desktop/logs/log2.log', '/Users/wanghongwei/Desktop/logs/log3.log', '/Users/wanghongwei/Desktop/logs/log1.log']
"""
# 读取所有文件的数据
fr = fileinput.input(file_path)
for line in fr:
    print(line.strip(),fileinput.filename(),fileinput.filelineno())
"""
我是log2 ---- start /Users/wanghongwei/Desktop/logs/log2.log 1
我是log2 ---- end /Users/wanghongwei/Desktop/logs/log2.log 2
我是log3 --- start /Users/wanghongwei/Desktop/logs/log3.log 1
我是log3 ---- end /Users/wanghongwei/Desktop/logs/log3.log 2
我是log1 —— start /Users/wanghongwei/Desktop/logs/log1.log 1
我是log1 —— end /Users/wanghongwei/Desktop/logs/log1.log 2
"""

还可以在每一次迭代时调用fileinput.filename()和fileinput.filelineno()方法,分别查看该行数据来自于哪个文件的哪一行。使用glob与fileinput可以极大地简化读取文件行的操作,非常的方便。

bz2与gzip文件压缩处理

有的时候为了节省硬盘空间,还会将文件进行压缩存储,尤其是在数据科学的工作中,经常会接触大量的数据。常见的压缩格式有zip、bz2、gz、rar等,不过笔者不建议使用rar格式,因为这个是专门为Windows提供的压缩格式,很多数据处理工具都没有针对rar的内建支持,即使是Python,也要结合第三方库才能使用rar格式的文件。

另外有些Linux用户可能会见到tar.gz的格式,这里需要说明的是,tar并不是压缩格式,只是一个打包格式,用于将很多小文件合并成一个大文件,甚至大多时候合并完成的文件是比原始文件还要大的,而gz才是真正的压缩格式

zip格式是一个同时打包和压缩的格式。

因为tar与zip都会打包文件,因此都不太适合存储需要程序处理的数据,所以本节主要介绍两种压缩Python文件的处理方法——bz2和gzip模块,它们的使用方式大同小异,示例如下:

# -*- coding:utf-8 -*-
import bz2
import gzip


print("============= bz2 ==============")
f1 = bz2.BZ2File("/Users/wanghongwei/Desktop/logs/abc.log.bz2", "w")
# 注意这里必须是bytes类型的!
for x in [b"a",b"b",b"c"]:
    f1.write(x+b"\n")
f1.close()

f2 = bz2.BZ2File("/Users/wanghongwei/Desktop/logs/abc.log.bz2", "r")
for x in f2.readlines():
    print(x)


print("============= gz ==============")
f1 = gzip.open("/Users/wanghongwei/Desktop/logs/xyz.log.gz", "w")
# 注意这里必须是bytes类型的!
for x in [b"x",b"y",b"z"]:
    f1.write(x+b"\n")
f1.close()

f2 = gzip.open("/Users/wanghongwei/Desktop/logs/xyz.log.gz", "r")
for x in f2.readlines():
    print(x)
  
"""
============= bz2 ==============
b'a\n'
b'b\n'
b'c\n'
============= gz ==============
b'x\n'
b'y\n'
b'z\n'
"""    

用Python读写外部数据

外部数据是多种多样的,比如前面几章已经学习过如何读取文本文件中的数据。在数据科学的应用中,CSV、Excel文件也是常用的文本文件,其中CSV是纯文本文件,而Excel是二进制文件,Python都为我们提供了相应的模块用来读写这些文件。除了文本文件之外,还有一种最常用的数据来源——数据库。在关系型数据库中,MySQL和PostgreSQL是开源数据库的代表;而在商业数据库中,Oracle、SQL Server则最为著名;在非关系型数据库中,则以文档型的数据库MongoDB最为著名。还有一个我个人比较喜欢的全文检索引擎Elasticsearch,基本上也可以当作一个文档型的数据库来使用,非常方便。

ea96729aaa44cced2d2dc8cbeb7ce94e.png

CSV文件的读写 ***

我们可以使用CSV模块中的reader()方法读取CSV文件,其中reader()的参数应该是一个文件描述符,可以参考下面的代码:

import csv

with open("/Users/wanghongwei/Desktop/test.csv","r",encoding="utf-8") as fr:
    rows = csv.reader(fr)
    for row in rows:
        print(row,type(row))
"""
['仄仄平平平仄,平平仄仄平平。'] <class 'list'>
['仄平平仄仄平平,仄仄仄平平仄。'] <class 'list'>
"""

创建csv文件

csv文件默认以逗号作为分隔符

创建一个CSV文件也很容易,可以使用csv模块的writer()方法创建一个CSV文件操作符,然后再调用其writerow方法进行逐行地写入,参考下面的代码:

import csv

with open("/Users/wanghongwei/Desktop/whw.csv","w") as fw:
    writer = csv.writer(fw)
    writer.writerow(["c1","c2","c3"])
    for x in range(10):
        writer.writerow([x,chr(ord("a")+x),"abc"])

打开这个csv文件可以看到里面写入了内容:

 ~/Desktop> cat whw.csv
c1,c2,c3
0,a,abc
1,b,abc
2,c,abc
3,d,abc
4,e,abc
5,f,abc
6,g,abc
7,h,abc
8,i,abc
9,j,abc

写入的时候为字符串加上双引号

默认的csv. writer()并不会自动为字符串增加双引号,若想要增加双引号,可以使用下面的代码:

# -*- coding:utf-8 -*-
import csv

with open("/Users/wanghongwei/Desktop/whw.csv","w") as fw:
    # 为写入的字符串加上双引号
    writer = csv.writer(fw,quoting=csv.QUOTE_NONNUMERIC)
    writer.writerow(["c1","c2","c3"])
    for x in range(10):
        writer.writerow([x,chr(ord("a")+x),"abc"])

再打开文件里面就有双引号了:

 ~/Desktop> cat whw.csv
"c1","c2","c3"
0,"a","abc"
1,"b","abc"
2,"c","abc"
3,"d","abc"
4,"e","abc"
5,"f","abc"
6,"g","abc"
7,"h","abc"
8,"i","abc"
9,"j","abc"

关于双引号的使用,除了QUOTE_NONNUMERIC这一种模式之外,还有另外几种模式,完整的列表可以查看表7-2。

5800a626986c3f613883ee317ba863f0.png

处理方言 ***

虽然CSV格式的文件是以逗号作为分隔符号的文件,但实际上并没有一个严格的定义要求其必须用逗号。比如Hadoop中的表文件,如果以纯文本的形式输出,那么默认的分隔符就是“\x01”。你也可能会见到使用管道符“|”作为分割符的CSV文件,我们统一称这种CSV为CSV的方言。

以上文创建的CSV文件csv_tutorial.csv为例,现在将逗号改为管道符,并且另存为csv_pipe.csv,其中的内容如下:

"c1"|"c2"|"c3"
0|"a"|"abc"
1|"b"|"abc"
2|"c"|"abc"
3|"d"|"abc"

想要读取这个文件可以参考下面的代码:

# -*- coding:utf-8 -*-
import csv


csv.register_dialect("pipes",delimiter="|")

with open("/Users/wanghongwei/Desktop/csv_pipe.csv","r") as fr:
    rows = csv.reader(fr, dialect="pipes")
    for row in rows:
        print(row)
"""
['c1', 'c2', 'c3']
['0', 'a', 'abc']
['1', 'b', 'abc']
['2', 'c', 'abc']
['3', 'd', 'abc']
"""

可以看到,这确实可以读取我们自定义的CSV方言。创建自定义方言的过程与读取的过程一样,只需要在csv.writer()函数中传入一个dialect='pipes’参数即可,在此就不赘述了。

将读取的结果转换为字典 ***

部分读者可能还没有意识到,如果CSV文件拥有大量的栏,那么想要确认某一个数据在第几栏将是一件多么麻烦的事情。所幸,CSV模块提供了一种以字典结构返回数据的方式,即使用CSV模块中的DictReader(),参考下面的代码(还拿上面的那个方言的文件,对比下返回结果)

# -*- coding:utf-8 -*-
import csv


csv.register_dialect("pipes",delimiter="|")

with open("/Users/wanghongwei/Desktop/csv_pipe.csv","r") as fr:
    rows = csv.DictReader(fr, dialect="pipes")
    for row in rows:
        print(row)
"""
# -*- coding:utf-8 -*-
import csv


csv.register_dialect("pipes",delimiter="|")

with open("/Users/wanghongwei/Desktop/csv_pipe.csv","r") as fr:
    rows = csv.DictReader(fr, dialect="pipes")
    for row in rows:
        print(row,row["c1"],row["c2"],row["c3"])
# 结果其实是将第一行的数据当成key了,下面的所有数据都对应这个key的value
"""
OrderedDict([('c1', '0'), ('c2', 'a'), ('c3', 'abc')]) 0 a abc
OrderedDict([('c1', '1'), ('c2', 'b'), ('c3', 'abc')]) 1 b abc
OrderedDict([('c1', '2'), ('c2', 'c'), ('c3', 'abc')]) 2 c abc
OrderedDict([('c1', '3'), ('c2', 'd'), ('c3', 'abc')]) 3 d abc
"""

Excel文件的读写 ***

Excel是微软Office套件中最重要的工具之一,也是数据科学中常用的图形化工具。很多人仍然习惯于使用Excel进行数据分析。本节将学习如何使用Python读写Excel文件,以方便程序员与数据分析师之间的交流,只要你需要跟别人合作,这种交流几乎是不可避免的。

下面将使用Pandas提供的方法来处理Excel文件,实际上Pandas是通过集成xlrd和xlwt来分别完成读和写Excel的工作的。虽然我们还没有正式地学习过Pandas(后面的10.2节会专门学习Pandas),但是这并不妨碍我们先了解其中的这个功能,读者可以通过下面的方式来安装Pandas的这个模块:

pip3 install pandas
pip3 install xlrd
pip3 install xlwt

读取Excel文件

Excel文件最基本的组成部分就是Sheet,一个正常的Excel会拥有一个至多个Sheet,如果没有改过名字的话,应该是Sheet1、Sheet2、Sheet3等。所以读取Excel文件的第一种最基础的方法就是使用Pandas中的read_excel()方法,并且还须制定要读取的文件名Sheet,示例代码如下:

import pandas as pd
from pandas import read_excel


pd.set_option("display.max_columns",3)
pd.set_option("display.max_rows",5)
# Sheet1
df = read_excel("/Users/wanghongwei/Desktop/test1.xlsx","Sheet1")
print(df)
# 因为第一行合并单元格了 所以会有Unnamed的显示
"""
  成绩表 Unnamed: 1 Unnamed: 2
0  id       name      score
1   1        whw        100
2   2     naruto         99
3   3     sasuke         98
"""

可以看到,原始的Excel文件中有一些多余的空行,而且我们也不想在读取的数据中显示第一列的序号,那么将读取Excel的代码改为如下的形式:

# -*- coding:utf-8 -*-
import pandas as pd
from pandas import read_excel


pd.set_option("display.max_columns",3)
pd.set_option("display.max_rows",5)
# Sheet1
# index_col 表示将那一列放在前面
# skiprows 表示从哪一行开始读数据
df = read_excel("/Users/wanghongwei/Desktop/test1.xlsx","Sheet1",index_col=0,skiprows=1)
print(df)
"""
      name  score
id               
1      whw    100
2   naruto     99
3   sasuke     98
"""

与打开普通文件的方式类似,也可以使用上下文管理器with打开Excel文件,如果一个Excel有多个Sheet,则可以使用下面的方法来打开:

with pd.ExcelFile("/Users/wanghonwei/Desktop/test.xlsx") as xls:
    for x in range(1,3):
        df = read_excel(xls, f"Sheet{x}", index_col=0, skiprows=1)

写Excel文件

注意:要写入的Excel文件必须提前创建好!!!

写Excel文件相对来说就更加简单一些,首先要创建一个Pandas的DataFrame的数据结构,然后调用DataFrame的to_excel()方法,参考下面的程序:

# -*- coding:utf-8 -*-
import pandas as pd

df = pd.DataFrame([[1,2,3,4],[5,6,7,8]], index=[0,1],columns=list("ABCD"))
df.to_excel("~/Desktop/test2.xlsx")

结果如下:

77683de2d937070d92d4751c4474dfc3.png

MySQL的读写

pymysql或SQLAlchemy

统计编程 ***

“统计学是最好学的数学分支”,虽然可能会有部分读者不赞同这点,不过笔者还是希望能借此让大家放下戒心来学习本章的内容。统计学之所以容易入门,最主要的原因在于它是源自于生活的一门学科,在古希腊,统计学用于统计人口和农业产量,即使是在现代,很多地方也会用到统计学,比如购物时计算平均价格,投票时统计得票率等,故而大家对于“正态分布”这个概念已比较熟悉。计算概率及贝叶斯方法虽然稍有难度,但也都曾编进中学课本,可以说关于统计的知识,每个人在一定程度上都会有所掌握。本章将带领读者回忆一下这部分内容。

另外本章还会讲解数据可视化的部分内容,这部分主要使用matplotlib库中的pyplot模块,所以请在开始这个章节的学习之前,确保你的计算机已经安装了matplotlib库,可以通过下面的代码进行安装:

pip3 install matplotlib

统计编程:描述性统计 ***

均值中位数方差作为最基本的描述性统计概念,相信大家是再熟悉不过的了,下面就通过实例来简单地复习一下。

人口普查数据

本节所用的测试数据可以在国家统计局官网下载。选择“第六次人口普查数据”——>然后选用“第一部分 全部数据资料”中的“第二卷 民族”中的“2-1全国各民族分年龄、性别的人口”的数据。这样会下载到一个名为“A0201.xls”的Excel文件。

从里面获取数据的程序如下 (代码有错误~~,书中是用py2写的~需要后续再调试下)

### 下面的代码有错误 现在还不熟练pandas的操作,先以理论为主吧

# -*- coding:utf-8 -*-
import json
from collections import OrderedDict

import pandas as pd


def get_num(age_list,lines):
    ret_dict = OrderedDict()
    for k, v in lines.to_dict().items():
        new_v_dict = OrderedDict()
        for vk, vv in v.items():
            new_v_dict[age_list[int(vk)]] = vv
        # 将每一列表头中 "."号后面的字符去掉
        ret_dict[k.split(".", 1)[0]] = new_v_dict
        return ret_dict

# 读取人口普查 民族/年龄/性别统计数据
def read_excel():
    excel_content = pd.read_excel("~/Downloads/A0201.xls",skiprows=2)
    # race_list = excel_content.irow(0)[1:][::3].tolist()
    # irow()方法换成了iloc()方法!!!
    # "'DataFrame' object has no attribute 'tolist'" ———— 需要加上values
    race_list = excel_content.iloc(0)[1:][::3].values.tolist()
    print(race_list)


    # 去掉字符中间的空格
    # "'DataFrame' object has no attribute 'tolist'" ———— 需要加上values
    age_list = map(lambda x:str(x).replace(" ",""),excel_content.iloc(0)[2:].values.tolist())

    excel_content = pd.read_excel("~/Downloads/A0201.xls",skiprows=4)
    result_dict = OrderedDict()
    for i,x in enumerate(range(1,178,3)):
        ids = [x, x+1, x+2]
        race_list[i] = race_list[i].replace(" ","")
        result_dict[race_list[i]] = get_num(age_list,excel_content.iloc(ids))

    return result_dict


if __name__ == '__main__':
    ret = json.dumps(read_excel(),ensure_ascii=False)
    print(ret)

均值与中位数

如果有一个包含有n个值的样本x,那么这个样本的均值就等于这些值的总和除以样本的数量。对于均值,大家应该很容易理解,比如我买了1千克葡萄,总共100粒,那么平均每一粒葡萄就是10克。

衡量葡萄的重量时,使用均值看起来是合理的,但是衡量一个城市人民的收入水平时,均值似乎就会有一点点不公平,因为少数富人会把平均值拉高,这个时候就需要使用中位数了。顾名思义,中位数就是将样本中的所有值按照从大到小的顺序排列,取最中间位置的那个值。而且全部的样本中刚好一半的值比中位数大,一半的数比中位数小。因为富人的比例可能相当低,所以他们的收入几乎不会影响中位数的取值。

方差与标准差

在统计全国人口平均年龄时,使用均值及中位数就能够很客观地反应真实的情况,不过当我们统计各民族人口数时,很明显,均值和中位数都无法给出合理的解释,因为均值是反映集中的趋势。汉族总计12亿多一点,占据了绝大部分人口数,而有些人口较少的民族仅有数千人,这时就需要通过方差来进行统计,因为方差反映的是分散的情况,方差的计算公式如下:

3d75c6d6aaf8c3b670de98b4fe6dd066.png

而方差的平方根就称为标准差。

分布

虽然简单的均值或方差能够在一定程度上反应数据趋势,但也可能掩盖了某些不易察觉的情况,这个时候就需要使用分布这个工具了,而能够展现分布的最好的工具就是直方图。本节将使用pylab来绘制直方图!

统计编程:数据可视化入门 ***

只要几行代码就可以将复杂的数据以直观的图形表达出来,这点正应了我国的一句古话“一图胜千言”。本节将学习如何使用Python中matplotlib库的pyplot模块绘制最基本的图形,以及柱状图、折线图、饼图、散点图这类统计图形。

pyplot基础

在最基本的图形中折线图和散点图是最容易的,下面就来尝试执行下列的代码:

# -*- coding:utf-8 -*-
import matplotlib.pyplot as plt


lst1 = [1,2,3,4]
lst2 = [2,1,4,6]
plt.plot(lst1,lst2)
plt.show()

效果如下:

ecb7be177f579f68cc6cd3a092ccae14.png

请注意show()函数是必须调用的,如果没有调用,虽然图形仍然会被绘制,但却不会显示出来。调用show()函数之后,你的屏幕上会出现一个窗口,

在图8-6中,窗口的标题是Figure 1,这是一个默认的绘图窗口名。而我们所看到的折线,则是按照提供的参数[1, 2, 3, 4]及[2, 1, 5, 6]描点而成,其中第一个参数是x轴的刻度,第二个参数是y轴的刻度。每一个点都由对应的x轴和y轴两个刻度共同决定,最后再用直线将这些点连接起来,就得到了图8-6。

如果想在一张图上多次绘图,或者同时绘制多张图像也是可以的。

(其他代码略)

绘制饼图与柱状图

(略)

统计编程:概率 ***

8.2节不经意间提到了概率——将人口普查的频数图转换成了概率质量函数,那么概率是什么呢?实际上概率就是频数与样本总数的比值,通常来说,这是一个0到1之间的数字,比如我们常说抛一个硬币,当硬币落下时正面朝上的概率是50%,转化成小数表示就是0.5,或者掷一个6面的公平色子,获得1点的概率是1/6,转换成小数大概是0.16667。那么连续掷了两次色子都获得一点的概率是多少呢?

稍等,想要通过中学或大学学习的概率论知识进行计算的读者先不要动手,这是一本学习编程的书,可不是数学书。虽然这个基本的概率计算已经得到了证明,但是我们仍然要用实验的方式进行验证,这种手段在编程中被称作“蒙特卡洛模拟”

在上面的问题中,两次色子的值都为1被称为事件(Event, E),而这个事件发生的概率则可以表示为P(E),为了探求这个结果,投掷了无数次色子的过程称为实验(trial)。通过无限次的实验,并且以最终事件发生的频数除以总共的试验次数,将所得到的最终值作为概率,这一点绝大多数人都能够接受。虽然通过真人做这样一个实验略显愚蠢,但是如果我们足够相信计算机,那么就可以依赖计算机快速地完成实验。

参考下面的代码:

# -*- coding:utf-8 -*-
from random import choice


def throw_dice():
    return choice([1,2,3,4,5,6])

def one_trail(trail_count=100000):
    success_count = 0
    for i in range(trail_count):
        t1 = throw_dice()
        t2 = throw_dice()
        if t1 == t2 == 1:
            success_count += 1
    return success_count / float(trail_count)

if __name__ == '__main__':
    for index,value in enumerate(range(5),1):
        print(index,":",one_trail())
"""
1 : 0.02675
2 : 0.0281
3 : 0.02688
4 : 0.02829
5 : 0.02812
"""

这里首先定义了一个掷色子的函数,这个函数使用random.choice()随机地从1~6的数字中选取一个以模拟色子的功能。然后我们又定义了一个one_trial()函数,这个函数默认会重复10万次掷两次色子,如果两个色子同时为1就记一次成功,然后返回在所有的试验中有多大的比例能够成功。最后运行5次该函数,将得到下面的结果:

1 : 0.02675
2 : 0.0281
3 : 0.02688
4 : 0.02829
5 : 0.02812

可以看到,多次结果之间的差距是非常小的,这与我们的预期相符,只要进行足够多次的实验,某个事件出现的概率应该是稳定的(如果读者已经算出了掷两次色子都为1的概率为1/6*1/6=0.2778,那么就会发现这个实验的结果还是相当准确的)。看起来我们应当相信实验的结果,这是因为“大数定理”这个定律的存在。根据这个定理,对于独立重复的试验(就像本实验一样,每次实验互相之间都没有影响),如果特定的事件概率是p,那么在经过无数次试验之后,出现这个事件的概率一定无限接近概率p。

不过需要注意的是,大数定律并不像很多赌徒(买彩票也算)所想的那样——如果实际的事件发生的概率和计算的概率不相符,那么在未来这种偏差会逐渐缩小。这种对回归原则的错误理解也被称为“赌徒谬误”也就是说每一期无论是买固定的一个号还是随机买一个号,中奖的概率是一致的,并不存在一直买一个号就会逐渐提高中奖概率的情况。

爬虫入门

(略)

数据科学的第三方库介绍

拥有众多数据科学相关的第三方库是Python受到青睐的主要原因,这其中又以Numpy、Pandas和Scikit-learn三者最为著名。

Numpy是Python科学计算库,它为Python提供了矩阵运算的能力,是科学计算的基础。

Pandas是Python统计分析库,可以方便地进行一些数据的统计分析,而且不需要额外的数据库。

Scikit-learn是Python机器学习库,里面内置的算法基本上涵盖了大部分常用的机器学习算法,非常适合新手用于入门机器学习。

Numpy入门与实战 ***

Numpy是Python第三方库中最常用的科学计算库,所谓科学计算往往是指类似Matlab那样的矩阵运算能力。这其中包括多维数组对象、线性代数计算,以及一个高性能的C/C++语言内部实现。而Numpy完全拥有上面的所有特性,而且还有很多方便的快捷函数,是做数据科学必不可少的工具。

一提到线性代数,可能会有很多读者觉得很难,并且也会觉得似乎没有学习的必要。不过,当前其实有很多数据挖掘算法都是通过线性代数的计算来实现的。线性代数一个最明显的优势就是用矩阵乘法代替循环可以极大地提高运算速度。

Numpy基础之创建数组

在Numpy中,最主要的数据结构就是ndarray,这个数据结构不仅可以处理一维数组,还可以处理多维数组。比如下面的数组就是一个二维数组:

[[0 1 2 3 4]
[5 6 7 8 9]
[10 11 12 13 14]]

通常我们称数组的维度为“秩(rank)”,可以通过下面的代码创建并查看一个数组的秩:

# -*- coding:utf-8 -*-
import numpy as np

a = np.array([(1,2),(3,4),(5,6)])
print(a)
"""
[[1 2]
 [3 4]
 [5 6]]
"""
print(a.ndim) # 2

习惯上我们会将numpy重命名为np并进行使用。创建二维数组就使用Python中“列表的列表”这种结构,如果创建三维数组就是使用“列表中的列表中的列表”的结构。有时为了方便,我们也会使用一些手段快速创建数组,可参考下面的代码:

# -*- coding:utf-8 -*-
import numpy as np

a = np.arange(15).reshape(3,5)
b = np.arange(1,30,5)
c = np.arange(0,1,0.2)
d = np.linspace(0,np.e*10,5)
e = np.random.random((3,2))

print("a= ",a,"\n")
print("b= ",b,"\n")
print("c= ",c,"\n")
print("d= ",d,"\n")
print("e= ",e,"\n")

结果如下:

a=  [[ 0  1  2  3  4]
 [ 5  6  7  8  9]
 [10 11 12 13 14]] 

b=  [ 1  6 11 16 21 26] 

c=  [0.  0.2 0.4 0.6 0.8] 

d=  [ 0.          6.79570457 13.59140914 20.38711371 27.18281828] 

e=  [[0.16724746 0.25678171]
 [0.85613378 0.51907046]
 [0.37853092 0.57455832]] 

使用np.arange()的方式与Python的range()类似,会生成一个ndarray类型的数组,只不过ndarray类型的reshape()方法会将原始的一维数组改变为一个二维数组,比如上面的例子中就将其改变为3×5的二维数组了。

与Python的range()函数稍有不同的是,np.arange()支持小数的步长,比如上例中的np.arange(0, 1, 0.2)就生成了小数步长的数组,而使用Python的range时则会报错。

Numpy还提供了一个强大的函数np.linspace(),这个函数的功能类似arange(),但是第三个参数不是步长,而是数量。这个函数可以按照参数中需要生成元素的数量自动选择步长,上例中的d就是一个例子。

另外Numpy中也提供了与math模块中一样的两个常量,即np.e和np.pi。np.e代表自然底数,np.pi是圆周率。

最后np.random.random()函数提供了直接生成随机元素的多维数组的方法,本节的后续部分会经常使用这个函数。

Numpy创建特定数组

# -*- coding:utf-8 -*-
import numpy as np

a = np.zeros((3,4))
# 指定元素的类型,每种方法都支持dtype的配置
b = np.ones((2,3,4),dtype=np.int64)
c = np.empty((4,5))

print("zeros=\n",a,"\n")
print("ones=\n",b,"\n")
print("empty=\n",c,"\n")
"""
zeros=
 [[0. 0. 0. 0.]
 [0. 0. 0. 0.]
 [0. 0. 0. 0.]] 

ones=
 [[[1 1 1 1]
  [1 1 1 1]
  [1 1 1 1]]

 [[1 1 1 1]
  [1 1 1 1]
  [1 1 1 1]]] 

empty=
 [[ 2.68156159e+154  2.68156159e+154 -5.43472210e-323  0.00000000e+000
   2.12199579e-314]
 [ 0.00000000e+000  0.00000000e+000  0.00000000e+000  1.77229088e-310
   3.50977866e+064]
 [ 0.00000000e+000  0.00000000e+000              nan              nan
   3.50977942e+064]
 [ 2.14320617e-314  2.68156159e+154  2.32035148e+077  1.48219694e-323
               nan]] 
"""

值得注意的是,无论是哪种创建数组/矩阵的方法,都支持一个dtype参数,我们可以通过为数字指定一种数据类型来指定这个数组的元素类型。Numpy的数组只能包含一种类型的数据,并且是布尔型、整形、无符号整形、浮点型、复数型中的一种。除了数字的类型之外,还有精度的区分,比如上面代码使用了np.int64表示64位整形,取值的区间从-9223372036854775808到9223372036854775807。当然,还有更低的32位、16位、8位等,详细的数据类型列表可以参考:https://docs.scipy.org/doc/numpy-dev/user/basics.types.html。

Numpy查看数组的各项属性

# -*- coding:utf-8 -*-
import numpy as np

a = np.arange(15).reshape(3,5)

print("a= ",a,"\n")
print("type(a)= ",type(a))
# 返回数组的秩数
print("a.ndim= ",a.ndim)
# 返回数组数组的形状
print("a.shape= ",a.shape)
# 数组中数据的类型
print("a.dtype.name= ",a.dtype.name)
# 数据类型占用的内存空间
print("a.itemsize= ",a.itemsize)
# 数组总共有多少个元素
print("a.size= ",a.size)

"""
a=  [[ 0  1  2  3  4]
 [ 5  6  7  8  9]
 [10 11 12 13 14]] 

type(a)=  <class 'numpy.ndarray'>
a.ndim=  2
a.shape=  (3, 5)
a.dtype.name=  int64
a.itemsize=  8
a.size=  15
"""

数组的格式化+缩略打印

numpy的对象在打印时会自动格式化,二维数组则会以矩阵的方式打印出来。不仅如此,当数组非常大以至于不能够完整地显示出来的时候,numpy还会缩略打印结果,可参考如下代码:

# -*- coding:utf-8 -*-
import numpy as np

ret = np.arange(10000).reshape(100,100)
print(ret)
"""
[[   0    1    2 ...   97   98   99]
 [ 100  101  102 ...  197  198  199]
 [ 200  201  202 ...  297  298  299]
 ...
 [9700 9701 9702 ... 9797 9798 9799]
 [9800 9801 9802 ... 9897 9898 9899]
 [9900 9901 9902 ... 9997 9998 9999]]
"""

程序只打印出了上下左右各3行/列的数据,其余的数据只以“...”代表,这非常便于我们进行程序调试。

Numpy基本运算

Numpy数组运算的基本原则就是“按元素运算”,这一点可能与我们的直觉稍微有点不符,考虑下面的代码:

# -*- coding:utf-8 -*-
import numpy as np

a = np.array([10,20,30,40])
b = np.arange(4)
print(f"a=\n{a}\nb=\n{b}")
"""
a=
[10 20 30 40]
b=
[0 1 2 3]
"""
# 计算 a-4
print(f"a-4=\n{a-4}")
"""
[ 6 16 26 36]
"""
# 计算 a-b
print(f"a-b=\n{a-b}")
"""
a-b=
[10 19 28 37]
"""
# 计算 b*2
print(f"b*2=\n{b*2}")
"""
[0 2 4 6]
"""
# 计算 b**2
print(f"b**2=\n{b**2}")
"""
[0 1 4 9]
"""
# 判断 a<21
print(f"a<21=\n{a<21}")
"""
a<21=
[ True  True False False]
"""

矩阵的乘法

实际上必须通过Numpy数组的dot()方法来进行矩阵点乘。对比下面的两种计算方式:

# -*- coding:utf-8 -*-
import numpy as np

a = np.array(([1,2],[2,4]))
b = np.array(([1,0],[0,4]))
print(f"a=\n{a}\nb=\n{b}\n")
"""
a=
[[1 2]
 [2 4]]
b=
[[1 0]
 [0 4]]
"""

# a*b是对应元素相乘
c1 = a * b
print(f"a*b=\n{a*b}")
"""
a*b=
[[ 1  0]
 [ 0 16]]
"""

# 计算 a与b的矩阵点乘
c2 = a.dot(b)
print(f"a.dot(b)=\n{c2}\n")
"""
a.dot(b)=
[[ 1  8]
 [ 2 16]]
"""

c = np.array([1,2,3,4,5])
d = np.array([2,3,4,5,6])
print(f"c=\n{c}\nd=\n{d}")
"""
c=
[1 2 3 4 5]
d=
[2 3 4 5 6]
"""
e = c.dot(d.T)
print(f"c*d=\n{e}")
"""
c*d=
70
"""

其中a*b是对应元素相乘,这个我们已经知道了,a.dot(b)才是矩阵的点乘。

矩阵的一元操作符

很多Numpy的一元操作符都是通过ndarray对象的方法来实现的,参考下面的代码:

# -*- coding:utf-8 -*-
import numpy as np

a = np.random.random((3,2))
print(f"a=\n{a}\n")
"""
a=
[[0.25480575 0.30680028]
 [0.64272692 0.56776957]
 [0.87292775 0.6863315 ]]
"""
# a.sum()
print(f"a.sum()\n{a.sum()}\n")
"""
3.3313617626694088
"""
# a.mim()
print(f"a.min()\n{a.min()}\n")
"""
0.2548057452445134
"""
# a.max()
print(f"a.max()\n{a.max()}")
"""
0.8729277527946152
"""

在上面的程序中无论原始的数组是几维的,sum()、min()、max()函数都是将其当作一维的数组进行处理的

想要让计算作用于特定的轴上,可以使用参数axis进行指定,比如下面的代码:

# -*- coding:utf-8 -*-
import numpy as np

a = np.random.random((3,2))
print(f"a=\n{a}\n")
"""
a=
[[0.12383538 0.06165397]
 [0.56184257 0.11282393]
 [0.71931073 0.59747316]]
"""
# axis为0时会按照列方向求和
print(f"a.sum(axis=0)\n",a.sum(axis=0),"\n")
"""
a.sum(axis=0)
 [1.40498867 0.77195107]
"""
# axis为1是会按照行方向求和
print(f"a.sum(axis=1)\n",a.sum(axis=1),"\n")
"""
a.sum(axis=1)
 [0.18548935 0.6746665  1.31678389] 
"""

当sum()函数中参数axis的值为0时,会按照列方向进行求和;当axis的值为1时,则按照行方向进行求和。另外cumsum()函数可计算每个轴上的累计和,比如,当axis的值为1时,会计算行方向的累积和,每行结果中第二列的值是原始对象每行中第一列和第二列的值之和。如果有第三列、第四列,那么结果中的值也是原始对象该行之前所有列之和,以此类推。

Numpy的数组下标与切片操作

Numpy的数组下标、切片大致上与Python的列表相似,参考下面的代码:

# -*- coding:utf-8 -*-
import numpy as np

# x y 是每个位置的索引 —— 4行4列
a = np.fromfunction(lambda x,y: 5*x +y,(4,4))
print(f"a=\n{a}\n")
"""
a=
[[ 0.  1.  2.  3.]
 [ 5.  6.  7.  8.]
 [10. 11. 12. 13.]
 [15. 16. 17. 18.]]
"""
# 索引为2的行最后一个数
print(f"a[2, -1]=\n{a[2, -1]}\n")
"""
13.0
"""
# 索引从1到3(不包含3)的行
print(f"a[1:3]=\n{a[1:3]}")
"""
a[1:3]=
[[ 5.  6.  7.  8.]
 [10. 11. 12. 13.]]
"""
# 取a中索引为1以后的行,以及1到3之间所有的列
print(f"a[1:, 1:3]=\n{a[1:, 1:3]}")
"""
a[1:, 1:3]=
[[ 6.  7.]
 [11. 12.]
 [16. 17.]]
"""
# 取a中全部的行,以及1到3之间所有的列
print(f"a[:, 1:3]=\n{a[:, 1:3]}")
"""
a[:, 1:3]=
[[ 1.  2.]
 [ 6.  7.]
 [11. 12.]
 [16. 17.]]
"""

还有下面的例子:

# -*- coding:utf-8 -*-
import numpy as np

# 4维数组 5行6列
b = np.fromfunction(lambda x,y,z:x+y+z,(4,5,6))
print(f"b=\n{b}\n")
"""
b=
[[[ 0.  1.  2.  3.  4.  5.]
  [ 1.  2.  3.  4.  5.  6.]
  [ 2.  3.  4.  5.  6.  7.]
  [ 3.  4.  5.  6.  7.  8.]
  [ 4.  5.  6.  7.  8.  9.]]

 [[ 1.  2.  3.  4.  5.  6.]
  [ 2.  3.  4.  5.  6.  7.]
  [ 3.  4.  5.  6.  7.  8.]
  [ 4.  5.  6.  7.  8.  9.]
  [ 5.  6.  7.  8.  9. 10.]]

 [[ 2.  3.  4.  5.  6.  7.]
  [ 3.  4.  5.  6.  7.  8.]
  [ 4.  5.  6.  7.  8.  9.]
  [ 5.  6.  7.  8.  9. 10.]
  [ 6.  7.  8.  9. 10. 11.]]

 [[ 3.  4.  5.  6.  7.  8.]
  [ 4.  5.  6.  7.  8.  9.]
  [ 5.  6.  7.  8.  9. 10.]
  [ 6.  7.  8.  9. 10. 11.]
  [ 7.  8.  9. 10. 11. 12.]]]
"""
# 在数组维度比较高时,... 代表剩下其余的全部维度
print("b[1, ...]=\n",b[1, ...])
"""
b[1, ...]=
 [[ 1.  2.  3.  4.  5.  6.]
 [ 2.  3.  4.  5.  6.  7.]
 [ 3.  4.  5.  6.  7.  8.]
 [ 4.  5.  6.  7.  8.  9.]
 [ 5.  6.  7.  8.  9. 10.]]
"""

在上面的代码中使用fromfunction()函数生成Numpy数组,这是一种非常方便的方式。该函数的第二个参数是数组的形状,在第一行的程序中(4, 4)代表我们将要生成一个4×4的二维数组,其中行号用x表示,列号用y表示。而这个函数的第一个参数是一个需要两个参数的函数(这里我们使用了Python的匿名函数,参数是x和y,5 * x + y是返回值)。使用fromfunction()函数生成数组时,其中的每一个元素都是将每个元素的坐标分别带入第一个参数的函数中,跟x和y绑定求得的。

通过上面的结果可以看到,对于一个Numpy的二维数组,可以使用a[2, -1]的下标方式获取其中的某一个元素,这与Python的列表下标是类似的,只不过这个下标表达式中包含一个行下标[2]和一个列下标[-1]。Numpy数组中的下标也支持负数下标,可以从数组的尾部向前计数。

实际上,可以简单地认为Numpy数组的下标表示的就是两个独立的Python下标,分别表示行向和列向的位置,使用方法也一致。比如a[:, 1:3]就表示,取a中全部的行,以及1到3之间所有的列

使用numpy对数组的维度进行切片

Numpy还可以对于数组的维度进行切片,参考下面的代码:

# -*- coding:utf-8 -*-
import numpy as np

# 4维数组 5行6列
b = np.fromfunction(lambda x,y,z:x+y+z,(4,5,6))
print(f"b=\n{b}\n")
"""
b=
[[[ 0.  1.  2.  3.  4.  5.]
  [ 1.  2.  3.  4.  5.  6.]
  [ 2.  3.  4.  5.  6.  7.]
  [ 3.  4.  5.  6.  7.  8.]
  [ 4.  5.  6.  7.  8.  9.]]

 [[ 1.  2.  3.  4.  5.  6.]
  [ 2.  3.  4.  5.  6.  7.]
  [ 3.  4.  5.  6.  7.  8.]
  [ 4.  5.  6.  7.  8.  9.]
  [ 5.  6.  7.  8.  9. 10.]]

 [[ 2.  3.  4.  5.  6.  7.]
  [ 3.  4.  5.  6.  7.  8.]
  [ 4.  5.  6.  7.  8.  9.]
  [ 5.  6.  7.  8.  9. 10.]
  [ 6.  7.  8.  9. 10. 11.]]

 [[ 3.  4.  5.  6.  7.  8.]
  [ 4.  5.  6.  7.  8.  9.]
  [ 5.  6.  7.  8.  9. 10.]
  [ 6.  7.  8.  9. 10. 11.]
  [ 7.  8.  9. 10. 11. 12.]]]
"""
# 对数组的维度进行切片
# 在数组维度比较高时,... 代表剩下其余的全部维度
print("b[1, ...]=\n",b[1, ...])
"""
b[1, ...]=
 [[ 1.  2.  3.  4.  5.  6.]
 [ 2.  3.  4.  5.  6.  7.]
 [ 3.  4.  5.  6.  7.  8.]
 [ 4.  5.  6.  7.  8.  9.]
 [ 5.  6.  7.  8.  9. 10.]]
"""

Numpy数组的迭代

Numpy数组的迭代与Python的列表类似,参考下面的代码:

# -*- coding:utf-8 -*-
import numpy as np

a = np.fromfunction(lambda x,y: 5*x +y,(4,4))
print(f"a=\n{a}\n")
"""
a=
[[ 0.  1.  2.  3.]
 [ 5.  6.  7.  8.]
 [10. 11. 12. 13.]
 [15. 16. 17. 18.]]
"""
# 按照行迭代
for row in a:
    print("row>>>",row,type(row),row[1])
"""
row>>> [0. 1. 2. 3.] <class 'numpy.ndarray'> 1.0
row>>> [5. 6. 7. 8.] <class 'numpy.ndarray'> 6.0
row>>> [10. 11. 12. 13.] <class 'numpy.ndarray'> 11.0
row>>> [15. 16. 17. 18.] <class 'numpy.ndarray'> 16.0
"""

# 按元素迭代
for e in a.flat:
    print(e)
"""
0.0
1.0
2.0
... (下面的略去)
"""

其中,需要注意的是,无论原始数组是几维的,Numpy数组的flat属性都会获得一个摊平的一维数组

Numpy改变数组形状的方法

Numpy中还提供了改变数组形状的方法,参考下面的代码:

# -*- coding:utf-8 -*-
import numpy as np

a = np.random.random((3,4))
print(f"a=\n{a}\n")
"""
a=
[[0.98275094 0.06900638 0.10203209 0.95680054]
 [0.91800645 0.25014002 0.73305488 0.20275519]
 [0.38790621 0.04945348 0.93729219 0.90721451]]
"""
# a.shape
print(f"a.shape=\n{a.shape}\n")
"""
a.shape=
(3, 4)
"""
# 转置 a.T
print(f"a.T=\n{a.T}\n")
"""
a.T=
[[0.98275094 0.91800645 0.38790621]
 [0.06900638 0.25014002 0.04945348]
 [0.10203209 0.73305488 0.93729219]
 [0.95680054 0.20275519 0.90721451]]
"""
# 原地修改 a.resize() ———— 注意接收的是一个元组 ———— 在原基础上修改,没有返回值
a.resize((2,6))
print(f"a.resize((2,6))之后的结果为:\n{a}\n")
"""
a.resize((2,6))之后的结果为:
[[0.98275094 0.06900638 0.10203209 0.95680054 0.91800645 0.25014002]
 [0.73305488 0.20275519 0.38790621 0.04945348 0.93729219 0.90721451]]
"""
# a.reshape() ———— 注意是在修改后2行六列的基础上修改的
# 使用reshape()函数,被赋值为-1的维度会自动计算 ———— 注意有返回值
print(f"a.resize(3,-1)=\n{a.reshape(3,-1)}")
"""
a.resize(3,-1)=
[[0.98275094 0.06900638 0.10203209 0.95680054]
 [0.91800645 0.25014002 0.73305488 0.20275519]
 [0.38790621 0.04945348 0.93729219 0.90721451]]
"""

shape属性存储了数组的维度信息,可以看到数组a是一个3×4的二维数组。

属性T可以获取原始二维数组的转置

然后这里有两种改变数组形状的函数,resize()可以原地修改数组,比如将3×4的数组修改为2×6,只需要注意元素的个数不要改变即可

为了方便,有的时候只需要确定一个维度,此时就可以使用reshape()函数,这个函数接受对应维度的参数为-1,这表示这个维度将会根据数组中元素的总数及其他的维度值进行自动计算

Numpy对数组进行堆叠

在Numpy中还可以对数组进行堆叠堆叠分为两个方向,即行方向(垂直方向)列方向(水平方向),分别用下面的代码表示:

# -*- coding:utf-8 -*-
import numpy as np

a = np.random.random((2,3))
b = np.random.random((2,3))

print(f"a=\n{a}\nb=\n{b}\n")
"""
a=
[[0.41319416 0.68928114 0.71826767]
 [0.36826484 0.6859847  0.37729525]]
b=
[[0.30106605 0.49668326 0.36233878]
 [0.70817689 0.8095215  0.8554541 ]]
"""
# 垂直堆叠vstack() —— 注意参数是元组形式的
print(f"np.vstack((a,b)=\n",np.vstack((a,b)))
"""
np.vstack((a,b)=
 [[0.41319416 0.68928114 0.71826767]
 [0.36826484 0.6859847  0.37729525]
 [0.30106605 0.49668326 0.36233878]
 [0.70817689 0.8095215  0.8554541 ]]
"""
# 水平堆叠hstack() —— 注意参数是元组形式的
print(f"np.hstack((a,b))=\n",np.hstack((a,b)))
"""
np.hstack((a,b))=
 [[0.41319416 0.68928114 0.71826767 0.30106605 0.49668326 0.36233878]
 [0.36826484 0.6859847  0.37729525 0.70817689 0.8095215  0.8554541 ]]
"""

Numpy对数组进行切分

与堆叠相对的还有切分:

# -*- coding:utf-8 -*-
import numpy as np

a = np.random.random((2,6))

print(f"a=\n{a}\n")
"""
a=
[[0.44703841 0.82372048 0.06055883 0.11810021 0.03996336 0.22621719]
 [0.90553354 0.36477766 0.68383123 0.98912516 0.37996215 0.50088551]]
"""

# vsplit() 垂直切分
print(f"np.vsplit(a,2)=\n",np.vsplit(a,2),"\n")
"""
np.vsplit(a,2)=
 [
  array([[0.44703841, 0.82372048, 0.06055883, 0.11810021, 0.03996336,0.22621719]]), 
  array([[0.90553354, 0.36477766, 0.68383123, 0.98912516, 0.37996215,0.50088551]])
  ] 
"""

# hsplit() 水平切分
print(f"np.hsplit(a,2)=\n",np.hsplit(a,2),"\n")
"""
np.hsplit(a,2)=
 [
  array([[0.44703841, 0.82372048, 0.06055883],
       [0.90553354, 0.36477766, 0.68383123]]), 
  array([[0.11810021, 0.03996336, 0.22621719],
       [0.98912516, 0.37996215, 0.50088551]])
 ] 
"""

与Python容器对象一样,直接将Numpy数组赋值给一个变量也仅仅是一个别名而已并没有真正复制其中的值我们可以使用a.view()进行浅拷贝用a.copy()进行深拷贝

———— 需要特别注意python可变数据类型的坑!!!

Nmupy高级特性:高级索引取值

除了前面介绍的基础功能和基本操作之外,Numpy还提供了一系列强力的工具。我们可以使用Numpy数组提供的高级索引进行取值,参考下面的代码:

# -*- coding:utf-8 -*-
import numpy as np

a = np.arange(20) * 3
print(f"a=\n{a}\n")
"""
[ 0  3  6  9 12 15 18 21 24 27 30 33 36 39 42 45 48 51 54 57]
"""
# 高级索引取值1
i = np.array([1,3,5,6])
print(f"a{i}={a[i]}\n")
"""
a[1 3 5 6]=[ 3  9 15 18]
"""
# 高级索引取值2
j = np.array([[3,4],[7,8]])
print(f"a{j}=\n{a[j]}\n")
"""
a[[3 4]
 [7 8]]=
[[ 9 12]
 [21 24]]
"""

在上面的代码里,数组a使用另外一个数组i作为下标进行取值,返回值是a中下标为i中元素的元素的列表。

同样当下标为二维数组的j时,返回值会按照j的结构进行重排,也会形成一个二维数组

此外,还可以通过两个轴向的索引分别获取数组中的元素

# -*- coding:utf-8 -*-
import numpy as np

a = np.arange(12).reshape(3,4)
print(f"a=\n{a}")
"""
a=
[[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]]
"""
# 行向索引
i = np.array([[1,1],
              [1,2]])
# 列向索引
j = np.array([[1,1],[3,3]])

print(f"a[i,j]=\n{a[i,j]}")
"""
a[i,j]=
[[ 5  5]
 [ 7 11]]
"""

其中,i中的元素代表行方向的索引,j中的元素代表列方向的索引,i与j的形状必须完全相同,输出的结果形状也要与i或j的形状相同。

Nmupy高级特性:arg为前缀的特殊函数

Numpy中还有一种特殊的函数,以arg前缀开头,比如argsort()函数代表“参数排序”,程序会将原始的数组进行排序,然后返回排序后的索引,而不是排序后的值,可以参考下面的代码:

# -*- coding:utf-8 -*-
import numpy as np

data = np.sin(np.arange(20)).reshape(5,4)
print(f"data=\n{data}\n")
"""
data=
[[ 0.          0.84147098  0.90929743  0.14112001]
 [-0.7568025  -0.95892427 -0.2794155   0.6569866 ]
 [ 0.98935825  0.41211849 -0.54402111 -0.99999021]
 [-0.53657292  0.42016704  0.99060736  0.65028784]
 [-0.28790332 -0.96139749 -0.75098725  0.14987721]]
"""
ind = data.argmax(axis=0)
print(f"ind=\n{ind}")
"""
ind=
[2 0 3 1]
"""
sort = data.argsort()
print(f"sort=\n{sort}")
"""
sort=
[[0 3 1 2]
 [1 0 2 3]
 [3 2 1 0]
 [0 1 3 2]
 [1 2 0 3]]
"""

获得了排序后的数组的索引之后,再结合前面介绍的高级索引取值的方法,不仅可以重新获取排序后的数组,还可以方便地使用这个序列对其他的相关数组进行排序。

Nmupy高级特性:布尔索引

除此之外,还可以使用布尔索引获取我们想要的值,所谓布尔索引就是返回对应位置值为True的元素,参考下面的代码:

# -*- coding:utf-8 -*-
import numpy as np

a = np.arange(12).reshape(3,4)
print(f"a=\n{a}")
"""
a=
[[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]]
"""

b = a > 3

print(f"b=\n{b}")
"""
b=
[[False False False False]
 [ True  True  True  True]
 [ True  True  True  True]]
"""
print(f"a[b]=\n{a[b]}")
"""
a[b]=
[ 4  5  6  7  8  9 10 11]
"""

可以看到,在b中对应位置为True的a中的元素被选择了出来。

Nmupy高级特性:线性代数的计算

对于Numpy数组,除了上面的一些特性之外,还需要简单介绍一下关于线性代数的计算,为了方便起见,将在下面的代码中一起列出来:

# -*- coding:utf-8 -*-
import numpy as np

a = np.array([[1,2],[3,4]])
print(f"a=\n{a}")
"""
a=
[[1 2]
 [3 4]]
"""

# 转置
print(f"a.T=\n{a.T}")
"""
a.T=
[[1 3]
 [2 4]]
"""
print(f"a.transpose()=\n{a.transpose()}")
"""
a.transpose()=
[[1 3]
 [2 4]]
"""

# 矩阵的逆
print(f"np.linalg.inv(a)=\n{np.linalg.inv(a)}")
"""
np.linalg.inv(a)=
[[-2.   1. ]
 [ 1.5 -0.5]]
"""

##########################################################################

# 对角阵
print(f"np.eye(4)=\n{np.eye(4)}")
"""
np.eye(4)=
[[1. 0. 0. 0.]
 [0. 1. 0. 0.]
 [0. 0. 1. 0.]
 [0. 0. 0. 1.]]
"""

# 矩阵的迹
print(f"np.trace(np.eye(3))=\n{np.trace(np.eye(3))}")
"""
np.trace(np.eye(3))=
3.0
"""

##########################################################################

y = np.array([[5.],[7.]])
print(f"y=\n{y}")
"""
y=
[[5.]
 [7.]]
"""

# 解线性方程
print(f"np.linalg.solve(a,y)=\n{np.linalg.solve(a,y)}")
"""
np.linalg.solve(a,y)=
[[-3.]
 [ 4.]]
"""

# 解特征方程
z = np.array([[0.0,-1.0],[1.0,0.0]])
print(np.linalg.eig(z))
"""
(array([0.+1.j, 0.-1.j]), array([[0.70710678+0.j        , 0.70710678-0.j        ],
       [0.        -0.70710678j, 0.        +0.70710678j]]))
"""

kNN实战

kNN(k-邻近算法)是最简单的机器学习分类的算法,虽然简单但却很有效。

(具体例子略)

Pandas入门与实战

Pandas这个第三方库在第7章已经使用过,当时使用的是读取Excel等的功能,其实这个库要比看起来的更加强大。

其中,最为主要的功能是提供了DataFrame这个数据结构它可以让我们直接在数据集上使用关系模型,比如完成分组(groupby)、聚合(agg)或联合(join)等操作,而无需将数据导入一个关系型的数据库中,另外它还集成了强大的时间序列相关函数,该功能在金融领域也应用广泛

Pandas是一个基于Numpy的数据分析库,本节将会学习Pandas库的基本操作,以及实际地使用Pandas操作一定量的数据,从而进行数据分析实战演练。

在一个常规的数据分析项目中,数据通常要经过数据清洗、建模、最终组织结果/绘图这几个步骤,而Pandas能够完成全部的这些工作,它是一个供数据科学家使用的好工具。

Pandas基础

与Numpy的主要数据类型ndarray类似,Pandas也提供了一种基础的数据类型Series

这也是一个序列类型,它的大多数操作与Numpy的ndarray类似,同时Series类型也是一个有索引的类型,又可以像Python中的字典一样工作。

当然无论是ndarray还是Series,都是只能包含同一种类型元素的序列类型,这一点与Python的列表和字典不同。

创建Series

# -*- coding:utf-8 -*-
import numpy as np
import pandas as pd

a = pd.Series([1, 0.3, np.nan])
b = pd.Series(np.array([1,2,3]))
print(f"a=\n{a}")
"""
a=
0    1.0
1    0.3
2    NaN
dtype: float64
"""

print(f"b=\n{b}")
"""
b=
0    1
1    2
2    3
dtype: int64
"""

Series可以从Python的列表进行构建,并且在Series中可以用np.nan表达某个位置没有值

从上面的例子中可以看到,Pandas会自动将不同类型的对象统一成同一种类型(类型推导会尽可能地向数字类型推导),比如整形的1可以转化成1.0的浮点型,NaN则可以是任何类型。所以最终在打印输出的结果中dtype被统一成了float64[插图]。
同样,也可以使用Numpy的数组创建Series。

实际上Pandas的Series在只支持同类型的元素这个方面并不是非常的严格,如果使用下面的方式创建一个Series会产生什么样的结果呢:

import pandas as pd

# 在这里“a”无法向数字的方向转换
print(pd.Series([1,"a"]))
"""
0    1
1    a
dtype: object
"""

可以看到,dtype的值是object,还记得这个对象么?这个对象是所有Python对象的发源地,Pandas还是将其统一成了同一种类型。

Series的索引与基本计算

# -*- coding:utf-8 -*-
import numpy as np
import pandas as pd

a = pd.Series([1, 0.3, np.nan])
print(f"a=\n{a}")
"""
a=
0    1.0
1    0.3
2    NaN
dtype: float64
"""

print(f"a[0]=\n{a[0]}")
"""
a[0]=
1.0
"""

print(f"a[a > 0.5]=\n{a[a > 0.5]}")
"""
a[a > 0.5]=
0    1.0
dtype: float64
"""

print(f"a[[2,1]]=\n{a[[2,1]]}")
"""
a[[2,1]]=
2    NaN
1    0.3
dtype: float64
"""

print(f"a.sum()=\n{a.sum()}")
"""
a.sum()=
1.3
"""

在每一个Series打印的值中,第一列都是一个序号,这是Series类型的索引部分,类似Python字典中的键,我们可以通过这个键直接获取某一行的值,也可以手动指定这个键。

如果不指定键那就会有程序生成自增的键

自定义Series的索引

# -*- coding:utf-8 -*-
import numpy as np
import pandas as pd

# 方式1
c = pd.Series([1,2,3],index=["a","b","c"])
print(f"c=\n{c}")
"""
c=
a    1
b    2
c    3
dtype: int64
"""

print(f"c['b']=\n{c['b']}")
"""
c['b']=
2
"""

print(f"c.get('b')=\n{c.get('b')}")
"""
c.get('b')=
2
"""

# 方式2
d = pd.Series({"c":0,"d":1,"e":2})
print(f"d=\n{d}")
"""
d=
c    0
d    1
e    2
dtype: int64
"""

现在变量c的索引已经是我们所指定的索引了,可以像Python的字典一样使用类似c['b']和c.get('b')的语法进行取值了,同样get的第二个参数是默认取值。

Pandas中的DataFrame

除了Series之外,Pandas还提供了另外一种强大的类型DataFrame,这个类型有点类似于前几章接触过的数据库。DataFrame是一种基于关系模型之上的数据结构,可以看作一个二维的表。

创建DataFrame:使用numpy创建

# -*- coding:utf-8 -*-
import numpy as np
import pandas as pd

my_date = pd.date_range("20160101",periods=5)
print(my_date)
"""
DatetimeIndex(['2016-01-01', '2016-01-02', '2016-01-03', '2016-01-04',
               '2016-01-05'],
              dtype='datetime64[ns]', freq='D')
"""

# 使用numpy对象创建DataFrame
df = pd.DataFrame(np.random.randn(5,4),index=my_date,columns=list("ABCD"))
print(df)
"""
                A         B         C         D
2016-01-01  1.827922 -0.186193 -0.486070  0.295022
2016-01-02  0.613633  1.659226  0.018977  0.321335
2016-01-03  2.318705 -0.637864 -2.167241 -1.451976
2016-01-04  1.111083 -0.705292 -0.286826  0.566900
2016-01-05  0.487277 -0.833802  0.887800 -0.649981
"""

其中date_range()函数可以快速产生一个时间的序列,第一个参数是起始的时间,periods这个参数代表一共需要生成几个元素。在这个例子中表示的就是从2016年1月1日开始生成5个日期数据。步长默认是起始时间的最小单位,比如这里起始时间的最小单位是日,所以在生成时间序列的时候就会依次生成01、02、03…这样的序列。接下来,创建一个5×4的二维数组,并且使用这个时间序列作为索引,使用ABCD作为栏名(还记得Excel或MySQL表的样子吗)。最后输出的结果展示了一个DataFrame应该是什么样子。

创建DataFrame:使用Python的字典创建

# -*- coding:utf-8 -*-
import numpy as np
import pandas as pd

df2 = pd.DataFrame({"A":2,
                    "B":pd.Timestamp("20160101"),
                    "C":pd.Series(3,index=list(range(4)),dtype="float64"),
                    "D":np.array([3]*4,dtype="int64"),
                    "E":pd.Categorical(["t1","t2","t3","t4"]),
                    "F":"abc"
                    })

print(df2)
"""
   A     B        C   D   E    F
0  2 2016-01-01  3.0  3  t1  abc
1  2 2016-01-01  3.0  3  t2  abc
2  2 2016-01-01  3.0  3  t3  abc
3  2 2016-01-01  3.0  3  t4  abc
"""

print(df2.dtypes)
"""
A             int64
B    datetime64[ns]
C           float64
D             int64
E          category
F            object
dtype: object
"""

print(df2.C)
"""
0    3.0
1    3.0
2    3.0
3    3.0
Name: C, dtype: float64
"""

使用字典时,字典的键会自动成为DataFrame的列名,而字典中的值将会按照序列最长的列表进行展开,比如在上面的例子中CDE三列会有4行数据产生,那么ABF三列也要有4行数据产生,不足的部分则使用相同的值进行补全

当我们查看DataFrame的dtype时,可以按照不同的列分别列出每一列的数据类型。

使用DataFrame的另外一个好处是,可以使用属性来访问DataFrame内部的数据,比如想要获取C列的所有数据,只需要调用df.C即可,其输出的结果也在上面的例子中展示了出来。

DataFrame中元素的方法:查看 转置 排序

# -*- coding:utf-8 -*-
import numpy as np
import pandas as pd

my_date = pd.date_range("20160101",periods=5)
df = pd.DataFrame(np.random.randn(5,4),index=my_date,columns=list("ABCD"))
print(df)
"""
               A         B         C         D
2016-01-01  1.415623  0.148712  1.596441  0.959749
2016-01-02 -1.078738  1.021531  2.087534 -0.496682
2016-01-03 -0.484086 -1.415553 -1.571502  0.211942
2016-01-04  1.856794 -1.720884  0.281858 -1.046419
2016-01-05 -0.021975 -0.422327  0.788329  0.908351
"""

### 查看
# 获取前几行数据
print(df.head(1))
"""
                A         B         C         D
2016-01-01 -0.194951 -1.697008  0.061418  0.797248
"""

# 获取后几行数据
print(df.tail(1))
"""
             A         B         C         D
2016-01-05 -0.469077  0.342332 -0.138694  0.583077
"""

# 获取索引
print(df.index)
"""
DatetimeIndex(['2016-01-01', '2016-01-02', '2016-01-03', '2016-01-04',
               '2016-01-05'],
              dtype='datetime64[ns]', freq='D')
"""

# 获取栏名
print(df.columns)
"""
Index(['A', 'B', 'C', 'D'], dtype='object')
"""

# 获取值
print(df.values)
"""
[[-0.02425176  1.48224983  0.47446958  1.31048679]
 [ 0.01233088  0.18172417 -1.03576438  0.89915658]
 [ 0.06795266  1.47446575 -0.7944956   0.22248481]
 [ 1.14552222  0.75163594 -0.55763803  0.17934404]
 [-1.50804572 -0.38000616 -1.67803878 -0.33081717]]
"""

# 获取描述信息
print(df.describe)

# 转置 —— 对索引进行重新排序
print(df.sort_index(axis=1,ascending=False))
"""
              D         C         B         A
2016-01-01 -1.386528 -0.279441  0.736015 -0.458683
2016-01-02  1.045830  0.061031  0.675069 -1.768644
2016-01-03 -0.063683  0.528595  0.362254 -1.217807
2016-01-04  0.533339  0.214229 -0.782902 -0.562077
2016-01-05 -0.533248  0.034389 -1.513554 -0.079807
"""

# 针对某一栏中的元素进行排序
print(df.sort_values(by="D"))
"""
               A         B         C         D
2016-01-04 -0.203148 -2.573900  2.160511 -1.318967
2016-01-03 -0.604611  0.150975  1.323292 -0.816309
2016-01-05 -0.915407  0.261181  1.411738  0.166497
2016-01-01 -0.122500  0.571259  0.317878  0.626655
2016-01-02 -1.567659  0.286159  1.187336  1.293513
"""

DataFrame中元素的方法:选择

# -*- coding:utf-8 -*-
import numpy as np
import pandas as pd

my_date = pd.date_range("20160101",periods=5)
df = pd.DataFrame(np.random.randn(5,4),index=my_date,columns=list("ABCD"))
print(df)
"""
               A         B         C         D
2016-01-01  1.415623  0.148712  1.596441  0.959749
2016-01-02 -1.078738  1.021531  2.087534 -0.496682
2016-01-03 -0.484086 -1.415553 -1.571502  0.211942
2016-01-04  1.856794 -1.720884  0.281858 -1.046419
2016-01-05 -0.021975 -0.422327  0.788329  0.908351
"""

### 选择
# 获取某一栏全部数据
print(df["A"])
"""
2016-01-01   -0.187412
2016-01-02    1.338352
2016-01-03    0.447239
2016-01-04   -2.259044
2016-01-05   -1.307154
Freq: D, Name: A, dtype: float64
"""

# 获取索引[1:3]的行数据
print(df[1:3])
"""
               A         B         C         D
2016-01-02  0.749736  1.050006  0.458519  0.216739
2016-01-03  0.943817 -0.419674 -0.287729  1.621940
"""

# 获取索引值为 "20160101":"20160103" 的行数据
print(df["20160101":"20160103"])
"""
                A         B         C         D
2016-01-01  0.755142  0.913853 -0.832517 -0.267255
2016-01-02 -0.497288 -1.235415 -0.487587 -0.167887
2016-01-03  0.908395  0.076916 -0.005861  0.290383
"""

# loc是定位元素的方法
print(df.loc[my_date[0]]) # 获取my_date第一个索引的数据
"""
A    0.440679
B   -1.352215
C   -0.739573
D    0.653077
Name: 2016-01-01 00:00:00, dtype: float64
"""
print(df.loc[:,["A","B"]]) # 获取栏名为A B 的全部行数据
"""
                   A         B
2016-01-01 -0.637481  0.479722
2016-01-02 -0.931102 -1.261640
2016-01-03 -0.713465 -1.249493
2016-01-04 -0.232026 -0.851380
2016-01-05  0.462942 -1.423308
"""

print(df.loc["20160102":"20160104",["A","B"]]) # 获取索引在"20160102":"20160104"范围的A B栏的数据
"""
               A         B
2016-01-02  0.267070 -1.774202
2016-01-03  0.030455 -0.397541
2016-01-04 -1.617047  0.499951
"""

print(df.loc["20160102",["A","B"]]) # 获取索引为 20160102的A B栏的数据
"""
A   -1.724649
B    1.064905
Name: 2016-01-02 00:00:00, dtype: float64
"""

### 通过布尔值获取数据
print(df[df.A > 0]) # 获取A栏中大于0的数据
"""
                   A         B         C         D
2016-01-01  1.629419 -2.665720  0.603270  0.198982
2016-01-02  0.079991 -1.568322 -0.082646  1.387026
"""

print(df[df > 0]) # 获取所有大于0的数据
"""
                   A         B         C         D
2016-01-01       NaN  0.910568       NaN       NaN
2016-01-02  1.345989  0.477432  1.490751  0.320816
2016-01-03       NaN  0.136794  0.208801       NaN
2016-01-04       NaN  0.489988  1.424240  0.611460
2016-01-05       NaN  0.240396       NaN  0.424275
"""

DataFrame中元素的方法:修改

# -*- coding:utf-8 -*-
import numpy as np
import pandas as pd

my_date = pd.date_range("20160101",periods=5)
df = pd.DataFrame(np.random.randn(5,4),index=my_date,columns=list("ABCD"))
print(df)
"""
               A         B         C         D
2016-01-01  1.415623  0.148712  1.596441  0.959749
2016-01-02 -1.078738  1.021531  2.087534 -0.496682
2016-01-03 -0.484086 -1.415553 -1.571502  0.211942
2016-01-04  1.856794 -1.720884  0.281858 -1.046419
2016-01-05 -0.021975 -0.422327  0.788329  0.908351
"""

### 修改
# 赋值
s1 = pd.Series([1,2,3,4],index=pd.date_range("20160101",periods=4))
print(f"s1=\n{s1}")
"""
s1=
2016-01-01    1
2016-01-02    2
2016-01-03    3
2016-01-04    4
Freq: D, dtype: int64
"""

# 加1栏F
df["F"] = s1
print(df)
"""
                A         B         C         D      F
2016-01-01 -0.939991  0.311983  1.174383  0.355901  1.0
2016-01-02 -0.083306  0.425297  1.368821 -0.256857  2.0
2016-01-03 -0.674964 -0.688836 -0.388943  0.999508  3.0
2016-01-04  0.723221 -0.073543 -1.914656 -0.216977  4.0
2016-01-05 -0.291337 -0.238999  0.376831  1.030592  NaN
"""

# 将A栏的第一个日期索引值设置为0
df.at[my_date[0],"A"] = 0
print(df)
"""
                A         B         C         D      F
2016-01-01  0.000000  1.303723 -1.821936  1.271198  1.0
2016-01-02 -0.278668  0.717928  2.048033  0.902368  2.0
2016-01-03 -0.304052 -0.361824  1.765611  0.852454  3.0
2016-01-04 -1.589332  1.159860 -0.337157  1.472354  4.0
2016-01-05 -0.062895 -0.155142 -0.930719 -0.092175  NaN
"""

# 设置D栏所有索引的值
df.loc[:,"D"] = np.array([5]*len(df))
print(df)
"""
                A         B         C     D   F
2016-01-01  0.000000  0.175238  0.974734  5  1.0
2016-01-02 -0.686830  0.167594  0.407309  5  2.0
2016-01-03 -1.374571  0.257898 -1.014054  5  3.0
2016-01-04  1.439758 -1.388793  0.849746  5  4.0
2016-01-05 -1.096187 -1.584557 -1.125867  5  NaN
"""

Pandas处理包含NaN值的DataFrame

Pandas是如何处理包含NaN值的DataFrame的呢?Pandas中包含了下面三个函数:

# 删除包含NaN的数据行
df.dropna(how="any")
# fillna()函数会使用默认值来填充NaN函数
df.fillna(value=3)
# pd.isnull()函数会判断是否包含NaN函数
pd.insnull(df)
# -*- coding:utf-8 -*-
import numpy as np
import pandas as pd

my_date = pd.date_range("20160101",periods=5)
df = pd.DataFrame(np.random.randn(5,4),index=my_date,columns=list("ABCD"))
print(df)
"""
               A         B         C         D
2016-01-01  1.415623  0.148712  1.596441  0.959749
2016-01-02 -1.078738  1.021531  2.087534 -0.496682
2016-01-03 -0.484086 -1.415553 -1.571502  0.211942
2016-01-04  1.856794 -1.720884  0.281858 -1.046419
2016-01-05 -0.021975 -0.422327  0.788329  0.908351
"""

### 删除NaN
s1 = pd.Series([1,2,3,4],index=pd.date_range("20160103",periods=4))
print(f"s1=\n{s1}")
"""
2016-01-03    1
2016-01-04    2
2016-01-05    3
2016-01-06    4
Freq: D, dtype: int64
"""

df["F"] = s1
print(df)
"""
                   A         B         C         D    F
2016-01-01 -0.618324 -1.285181 -1.458774  0.058836  NaN
2016-01-02  0.873268  0.354392  0.706990  1.926894  NaN
2016-01-03 -0.327956  0.309922  0.254315 -1.872835  1.0
2016-01-04 -0.388160  0.574862 -0.499742 -0.486515  2.0
2016-01-05  2.003128  1.177577  0.397433  0.584909  3.0
"""

### 不同的删除操作
print(df.dropna(how="any"))
"""
                A         B         C         D      F
2016-01-03 -2.607160 -0.037844  2.500703 -0.163195  1.0
2016-01-04 -1.157425 -0.405830 -0.719632  0.627654  2.0
2016-01-05 -0.056595 -1.063298 -0.241264  0.711807  3.0
"""

print(df.fillna(value="xxx")) # 设置默认值
"""
                   A         B         C         D    F
2016-01-01 -1.469645 -0.373754 -1.873026  0.320460  xxx
2016-01-02  0.631470 -0.092641  1.936904 -0.319683  xxx
2016-01-03 -0.043713 -0.909449  0.418214  0.310092    1
2016-01-04  0.506240  1.738119  1.576199 -0.070518    2
2016-01-05 -0.337026  0.196239  0.533299  0.317858    3
"""

print(pd.isnull(df))
"""
                A      B      C      D      F
2016-01-01  False  False  False  False   True
2016-01-02  False  False  False  False   True
2016-01-03  False  False  False  False  False
2016-01-04  False  False  False  False  False
2016-01-05  False  False  False  False  False
"""

DataFrame一元/二元操作符

DataFrame也包含众多的一元、二元操作符,比如df.mean()用于求特定轴上的均值df.cumsum()用于求某一轴向的积累值等,更多的方法可以参考文档:http://pandas.pydata.org/pandas-docs/stable/dsintro.html#dataframe。

DataFrame用于合并或切分

# -*- coding:utf-8 -*-
import numpy as np
import pandas as pd

# concat方法
df = pd.DataFrame(np.random.randn(10,4))
pieces = [df[:3], df[3:7], df[7:]]
print(pd.concat(pieces))
"""
        0         1         2         3
0  0.528516 -0.451417  2.073429 -0.420794
1  0.657546 -0.451011  2.227282  0.632122
2  0.573192 -0.426820 -0.148208 -0.828084
3  1.043294  0.273732 -1.154104  0.398562
4 -0.755257 -0.609876 -0.834406 -0.155497
5 -0.156065  1.062376 -0.183327  0.401098
6 -1.955809 -0.454560  0.809273 -1.483440
7 -0.779205  0.436352  0.189885 -1.513666
8  0.793635 -0.440449 -0.301007  1.332895
9 -1.992193 -0.772319  0.503830 -0.046460
"""

# merge方法
left = pd.DataFrame({"key":["foo","foo"],"lval":[1,2]})
right = pd.DataFrame({"key":["foo","foo"],"rval":[4,5]})
print(pd.merge(left, right, on="key"))
"""
   key  lval  rval
0  foo     1     4
1  foo     1     5
2  foo     2     4
3  foo     2     5
"""

# append方法
df = pd.DataFrame(np.random.randn(8,4),columns=list("ABCD"))
print(df)
"""
          A         B         C         D
0 -1.289150  0.984312 -1.148149 -0.486624
1 -0.367639  0.729776 -0.044727  0.010863
2 -1.529720  0.985287 -0.703415  1.456481
3  0.334052 -0.829051 -0.466206 -1.308944
4 -1.157548  0.586198  0.212263 -0.913455
5 -0.787224 -0.362624  0.118126 -1.088887
6 -0.567499 -1.376846 -1.548002 -1.148021
7  0.009765 -0.581297  0.157844 -0.334789
"""
s = df.iloc[3]
print(s)
"""
A    0.334052
B   -0.829051
C   -0.466206
D   -1.308944
Name: 3, dtype: float64
"""

df.append(s,ignore_index=True)
print(df)
"""
        A         B         C         D
0 -1.289150  0.984312 -1.148149 -0.486624
1 -0.367639  0.729776 -0.044727  0.010863
2 -1.529720  0.985287 -0.703415  1.456481
3  0.334052 -0.829051 -0.466206 -1.308944
4 -1.157548  0.586198  0.212263 -0.913455
5 -0.787224 -0.362624  0.118126 -1.088887
6 -0.567499 -1.376846 -1.548002 -1.148021
7  0.009765 -0.581297  0.157844 -0.334789
"""

这里有三种方法可以做到类似的事情,concat()方法可以将一个列表的列表合并成一个完整的DataFrame; merge()方法则相当于数据库的join,它会将key相同的部分进行全匹配。比如上面的例子中因为所有的key都是foo,所以left与right的数据会做一个笛卡儿积生成4行数据;最后df也支持类似Python列表一样的append()方法

DataFrame的分组操作(类似Excel的透视表)

DataFrame也支持类似数据库的groupby操作,参考下面的代码:

# -*- coding:utf-8 -*-
import numpy as np
import pandas as pd

df = pd.DataFrame({"A":["foo","bar","foo","bar","foo","bar","foo","foo"],
                   "B":["one","two","three","one","two","two","one","three"],
                   "C":np.random.randn(8),
                   "D":np.random.randn(8),
                   })

print(df.groupby("A").sum())
"""
            C         D
A                      
bar  0.406925 -0.920380
foo  4.621226  0.605565
"""

print(df.groupby(["A","B"]).sum())
"""
              C         D
A    B                        
bar one   -0.408521 -0.560719
    two   -0.124907  1.402808
foo one   -0.211453  0.048889
    three  1.422346  0.969328
    two   -2.338338 -0.840154
"""

请仔细查看这个结果,如果有Excel经验的读者可能会发现,这个结果非常类似于Excel的透视图,没错,groupby的功能与Excel透视图的功能非常类似,有兴趣的读者不妨尝试使用这个方法完成一个以前用Excel做的工作,再对比一下结果。

Pandas的分类类型

# -*- coding:utf-8 -*-
import pandas as pd

df = pd.DataFrame({"id":[1,2,3,4,5,6],
                   "raw_grade":["a","b","b","a","a","e"]
                   })

# 一个简单的创建分类序列的方式是,对DataFrame的某一栏调用astype("category")方法
df["grade"] = df["raw_grade"].astype("category")
print(df["grade"])
"""
0    a
1    b
2    b
3    a
4    a
5    e
Name: grade, dtype: category
Categories (3, object): ['a', 'b', 'e']
"""

泰坦尼克号生存率分析

(具体过程见书)

Scikit-learn入门和实战

Scikit-learn是最为著名的Python机器学习库,一般提到机器学习,就表示通过让机器对一小部分已知的样本进行学习,然后对更多的未知样本中的某些属性进行预测。这些属性一般也称为“特征”。根据处理问题的方式不同,机器学习大致分为下面的几类。

1. 监督学习

所谓监督学习就是通过所有特征已知的训练集让机器学习其中的规律,然后再向机器提供有一部分特征未知的数据集,让机器帮我们补全其中未知部分的一种方法,主要包括下面两大类。

  • 分类,根据样本数据中已知的分类进行学习,对未知分类的数据进行分类就称为分类。举例来说某个饮料分类中的物品有下列特征:液体,瓶装,可食用,保质期12个月,那么同样拥有类似特征的物品则可能会被分类到饮料中。虽然对于超市来说,这种分类大多是人工进行的,但是对于更复杂的场景,比如对数亿张照片,如何按照题材对照片进行分类,利用搜索引擎就能很好地完成这份工作。
  • 回归,根据样本中的离散的特征描绘出一个连续的回归曲线,之后只要能给出其他任意几个维度的值就能够确定某个缺失的维度值的方法就称为回归。典型的回归就是,利用人类的性别、年龄、家族成员等信息建立一个身高的回归方程,以预测新生儿各个年龄阶段的身高。

2. 无监督学习

无监督学习不会为机器提供正确的样本进行学习,而是靠机器自己去寻找可以参考的依据,通常使用距离函数或是凸包理论等方式对给定的数据集进行聚类。

聚类的典型应用是对用户进行聚类分析,比如按照用户访问网站的行为将用户分成不同的类型,通过聚类发现不同的收入水平,或者不同风格偏好的用户。

要想讲完机器学习的所有知识,好几本书都不够,所以本节并不会将机器学习的方方面面都覆盖到,甚至因为要想系统地研究机器学习首先需要大量的关于数学、概率、线性代数及算法等方面的知识,所以这里只能避重就轻地讲解一些基础的入门知识。希望能让读者对机器学习有一个粗浅的了解,为未来的继续学习打下一定的基础。

本节将使用Scikit-learn这个第三方模块,可以通过下面的命令进行安装:

pip3 install sklean
pip3 install scipy

机器学习术语

先了解一下机器学习中的术语将有助于我们快速地吸收知识,虽然短短的一节无法涵盖所有的机器学习知识,但是希望读者对机器学习能有一个整体的认识,为以后的学习打下基础。

  • 训练集/测试集:通常在有监督的机器学习中会有一组已知其分类或结果值的数据,一般来说我们不能把这些数据全部用来进行训练,如果使用全部的数据进行训练,那么将有可能导致过拟合。而且我们也需要用一部分的数据来验证算法的效果。
  • 过拟合:所谓过拟合,就是训练后的算法虽然严格地符合训练集,但可能会在面对真正的数据时效果变差,如图10-5所示,通过每个点的这条线就是过拟合的结果,而只是大致描绘每个点分布的这条线则是正常训练的结果。过拟合的训练结果将会使算法在测试时表现得完美无缺,但是实际应用时却很不理想。

61f3c9331da323774d99e5e1eaf261ef.png

  • 特征工程:假设图10-5是一个真正的样本数据,那么x轴和y轴就是数据特征。而特征工程的目的就是针对原始数据中千奇百怪的数据进行数量化,每一个样本将形成一个特征向量来描绘这个样本。在特征工程中,我们不仅会处理确实的数据,还会避免某一维度的特征过分主导结果,进行归一化的操作。
  • 数据挖掘十大算法:经典的数据挖掘算法主要有10种,包括C4.5决策树、K-均值、支持向量机(SVM)、Apriori、最大期望(EM)、Pagerank、AdaBost、k-邻近(kNN)、朴素贝叶斯算法和分类回归树算法。其中Apriori算法和Pagerank算法并不包含在Scikit-learn之中。本章已经介绍过kNN算法了,若想要了解其他算法的具体原理,可以找专门的一些图书进行学习。Scikit-learn已经将这些算法封装成了一个具体的流程,使用者只需要按照流程提供其所需要的数据格式的数据即可。
  • 正确率/召回率/ROC曲线:这些是用来衡量机器学习算法效果的三个指标。正确率,顾名思义就是正确的比率是多少,但这实际上掩盖了样本是如何被分错的。在一个二分类的任务中,被正确分类的正例与所有正例(包含被错误分类为负例的正例)的比值就叫作召回率,召回率越大表示被错判的正例就越少。ROC曲线则是“被正确分为正例的正例vs被错误分为正例的负例”的曲线,如图10-6所示。

ef4fa721e13a9b7ad1286793172c9301.png

该曲线的含义很难描述,图10-6的左上角代表当没有负例被错误地分类为正例时,全部的正例都会被正确地分类。不过当曲线贴近左上角时这条曲线下的面积(AUC面积)就是最大的状态,此时则代表这个分类器是完美的分类器,我们用AUC=1来表示。图10-6中的曲线是表示随机猜测的ROC曲线,此时AUC=0.5,所以可以用AUC来描述一个分类器的好坏。

  • 降维:有的时候我们所获取的数据有几万到几亿个维度(自然语处理很容易达到这个数量级),此时就需要通过一些手段,比如SVD分解,或者组成成分分析等手段来消去对结果不产生影响或影响微小的维度,以减小对算力的需求。

完整的机器学习流程

一个完整的机器学习的流程应当如下所示。

1)收集数据,我们可以通过网络爬虫或系统日志及其他已经结构化好的数据来获取机器学习中必要的数据。在一个机器学习的任务中,数据的重要性是最高的,在行业中流传甚广的一句名言就是“数据决定了机器学习能力的上限,算法只能尽可能地逼近这个上限而已”。

2)特征工程,在常规的机器学习任务中,特征工程是仅次于数据的一个工作,可以说在有了原始数据之后80%的工作都是在做特征工程。也就是将数据处理成适合某种算法处理的结构,补全确实的数据,为数据集构建合理的特征。

3)训练算法,从这一步开始才是进行真正的机器学习,虽然可能要经历选择算法和调参的步骤,不过大多数算法都是值得信任的。而且有些算法的参数极其简单,有些甚至只有一个迭代次数的参数,这里工程人员能做的工作并不多。在训练完算法之后可以得到一个模型。

4)测试模型,通过正确率/召回率/AUC等指标衡量模型的好坏,再根据结果尝试调整特征工程或算法的参数,再次训练算法得到模型,测试模型,直到效果令我们满意为止。

5)应用模型,在完成前面的所有工作之后,就可以将这个模型用于真正的工作中了。

Scikit-learn基础及实战

(笔记不做深入记载,具体见书中介绍)

利用Python进行图数据分析

本章将会对一种特殊的数据结构——图——的分析进行学习。“图”数据结构是一种经典的计算机数据结构,除此之外,搜索引擎也是假设互联网上的网站组成了一个互相连通的图,并通过相应的算法来对搜索结构进行排序的。最近一些年里,社交网络的兴起也推动了图数据分析的发展,因为在社交网络中,好友、粉丝恰好构成了一张图,也称为网络。在社交网络这个大图中可以发现特定的群体(子图发现),或者发现影响力中心的人物,发现热点事件,甚至预测流行病,科学家对此进行了诸多的尝试。

pip3 install networkx

利用Python进行图数据分析基础

f815b121a1e52b446fbd35685e146106.png

图11-1是由节点和边组成的图,如果边没有方向则称为无向图。如果把图11-1的节点看作是城市,而边看作是连接城市间的公路,那么计算从任意一座城市到达另外一座城市的最短行走路径就是一个典型的图问题。如果图11-1的边不是双向的而是单向的,那么这个图又可以被称作有向图,在有向图中,如果有一条边从A节点指向B节点,则说A为源节点或父节点,B则为目标节点或子节点。

关于图有很多有趣的问题,在数学上,首次记载图的使用是1735年瑞士数学家欧拉用图来解决柯尼斯堡七桥问题[插图]。解决这种问题的思想被称为“图论”。当然讨论图论并不是本章的主要内容,下面将会从使用Python解决实际的图问题来入手,以学习图挖掘这样一个热门的数据挖掘分类。一开始我们会学习如何使用NetworkX及一些基本的图的概念,之后会使用公开的数据源进行一些图分析。

NewworkX入门与实战

(略,具体见书中内容)

大数据工具简介

本章将会尝试处理一些真正的“大”数据,使用工业界常用的工具处理一些比较大的数据集(约1GB,这是一个比较合适的学习大数据工具的数据集大小,既可单机处理又能体验大数据带来的麻烦),可以让读者大致了解一下数据科学家们的日常工作内容。本章将会分两个部分来介绍Hadoop和Spark这两个最流行的大数据处理框架。Hadoop是最为知名的大数据批处理框架,并且生态系统中提供了分布式文件存储HDFS及分布式系统任务调度框架Yarn,以及最重要的MapReduce计算模型的实现,还提供了很多基于SQL的工具,可以以非编程的方式实现数据清洗及数据仓库的管理,现代大数据处理中Hadoop已经被列为基础设施之一。Spark是近些年来新兴的内存型大数据处理工具,其最大的改进是分布式内存文件系统RDD,它会将全部数据加载到内存中再进行计算,这极大地提高了处理速度,而且还将Hadoop的MapReduce模型改进为DAG(有向无环图)模型,尤其适合迭代型的机器学习任务,在Spark标准库中甚至还集成了Mllib及ML这两个模块来进行机器学习的计算。除此之外,Spark还支持流式处理,可以在线实时地处理数据。

本章将要介绍的两个框架目前还无法在Windows上运行,所以读者需要一台Mac或Linux的电脑,当然还有另外一种方式那就是使用云计算,具体的方法会在下文介绍。

大数据工具:Hadoop

大数据工具:Spark

posted on 2020-08-09 12:27  江湖乄夜雨  阅读(712)  评论(0编辑  收藏  举报