1. 程式人生 > 實用技巧 >《Python資料科學實踐指南》筆記

《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)))

會出現異常:

浮點數到整數的轉換 ***

浮點型轉換為整數型別時,在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,基本上也可以當作一個文件型的資料庫來使用,非常方便。

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。

處理方言 ***

雖然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")

結果如下:

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億多一點,佔據了絕大部分人口數,而有些人口較少的民族僅有數千人,這時就需要通過方差來進行統計,因為方差反映的是分散的情況,方差的計算公式如下:

而方差的平方根就稱為標準差。

分佈

雖然簡單的均值或方差能夠在一定程度上反應資料趨勢,但也可能掩蓋了某些不易察覺的情況,這個時候就需要使用分佈這個工具了,而能夠展現分佈的最好的工具就是直方圖。本節將使用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()

效果如下:

請注意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所示,通過每個點的這條線就是過擬合的結果,而只是大致描繪每個點分佈的這條線則是正常訓練的結果。過擬合的訓練結果將會使演算法在測試時表現得完美無缺,但是實際應用時卻很不理想。

  • 特徵工程:假設圖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所示。

該曲線的含義很難描述,圖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進行圖資料分析基礎

圖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