NumPy 和它的 ndarray 對象,為 Python 多維數(shù)組提供了高效的存儲和處理方法。Pandas 是在 NumPy 基礎上建立的新程序庫,提供了一種高效的 DataFrame 數(shù)據(jù)結構。DataFrame 本質上是一種帶行標簽和列標簽、支持相同類型數(shù)據(jù)和缺失值的多維數(shù)組。建立在 NumPy 數(shù)組結構上的 Pandas,尤其是它的 Series 和 DataFrame 對象,為數(shù)據(jù)科學家們處理那些消耗大量時間的“數(shù)據(jù)清理”(data munging)任務提供了捷徑。
Pandas 對象簡介
如果從底層視角觀察 Pandas 對象,可以把它們看成增強版的 NumPy 結構化數(shù)組,行列都不再只是簡單的整數(shù)索引,還可以帶上標簽。Pandas 的三個基本數(shù)據(jù)結構:Series、DataFrame 和 Index。
import numpy as np
import pandas as pd
# Series 對象:帶索引數(shù)據(jù)構成的一維數(shù)組
# 與 NumPy 數(shù)組間的本質差異其實是索引:
# NumPy 數(shù)組通過隱式定義的整數(shù)索引獲取數(shù)值
# Pandas 的 Series 對象用一種顯式定義的索引與數(shù)值關聯(lián),索引不僅可以是數(shù)字,還可以是字符串等其他類型
1、Serise 是通用的 NumPy 數(shù)組
data = [0.25, 0.5, 0.75, 1.0]
index = ['a', 'b', 'c', 'd']
data = pd.Series(data, index=index)
# 0 0.25
# 1 0.50
# 2 0.75
# 3 1.00
# dtype: float64
data.values # 獲取值
data.index # 獲取索引,類型為 pd.Index 的類數(shù)組對象 RangeIndex(start=0, stop=4, step=1)
data[1:3] # 通過中括號索引獲取值
data['b'] # 通過字符串索引獲取值
# 2、Series 是特殊的 Python 字典
population_dict = {'California': 38332521,
'Texas': 26448193,
'New York': 19651127,
'Florida': 19552860,
'Illinois': 12882135}
population = pd.Series(population_dict) # 使用 Python 字典創(chuàng)建 Series 對象,默認索引為第一列
population['California'] # 獲取值
population['California':'Illinois'] # 還支持數(shù)組形式的操作,比如切片
# data 可以是列表或 NumPy 數(shù)組,這時 index 默認值為整數(shù)序列
pd.Series([2, 4, 6])
# data 也可以是一個標量,創(chuàng)建 Series 對象時會重復填充到每個索引上
pd.Series(5, index=[100, 200, 300])
# data 還可以是一個字典,index 默認是排序的字典鍵
pd.Series({2:'a', 1:'b', 3:'c'})
# 每一種形式都可以通過顯式指定索引篩選需要的結果
pd.Series({2:'a', 1:'b', 3:'c'}, index=[3, 2]) # 篩選出c和a
# DataFrame 對象:既有靈活的行索引,又有靈活列名的二維數(shù)組
states.index
# Index(['California', 'Florida', 'Illinois', 'New York', 'Texas'], dtype='object')
states.columns
# Index(['area', 'population'], dtype='object')
states['area']
# 每個州的面積
# 1、通過單個 Series 對象創(chuàng)建 DataFrame
pd.DataFrame(population, columns=['population'])
# 2、通過字典列表創(chuàng)建 DataFrame
data = [{'a': i, 'b': 2 * i}
for i in range(3)]
pd.DataFrame(data)
# 這里相當于用 b 做笛卡爾積,因為 b 沒有相等的時候,所以 a 和 c 都有缺失,缺失值用 NaN 表示
pd.DataFrame([{'a': 1, 'b': 2}, {'b': 3, 'c': 4}])
# a b c
# 0 1.0 2 NaN
# 1 NaN 3 4.0
# 3、通過 Series 對象字典創(chuàng)建 DataFrame
area_dict = {'California': 423967, 'Texas': 695662, 'New York': 141297, 'Florida': 170312, 'Illinois': 149995}
area = pd.Series(area_dict)
# 結合上面的 population 和 area 兩個 Series,創(chuàng)建 DataFrame
states = pd.DataFrame({'population': population, 'area': area})
# area population
# California 423967 38332521
# Florida 170312 19552860
# Illinois 149995 12882135
# New York 141297 19651127
# Texas 695662 26448193
# 4、通過 NumPy 二維數(shù)組創(chuàng)建 DataFrame
pd.DataFrame(np.random.rand(3, 2), columns=['foo', 'bar'], index=['a', 'b', 'c'])
# foo bar
# a 0.865257 0.213169
# b 0.442759 0.108267
# c 0.047110 0.905718
# 5、通過 NumPy 結構化數(shù)組創(chuàng)建 DataFrame
A = np.zeros(3, dtype=[('A', 'i8'), ('B', 'f8')])
pd.DataFrame(A)
# A B
# 0 0 0.0
# 1 0 0.0
# 2 0 0.0
# Index 對象:不可變數(shù)組或有序集合
ind = pd.Index([2, 3, 5, 7, 11])
# Int64Index([2, 3, 5, 7, 11], dtype='int64')
# 1、將Index看作不可變數(shù)組:像數(shù)組一樣進行操作,但是不能修改它的值
ind[1]
ind[::2]
print(ind.size, ind.shape, ind.ndim, ind.dtype)
# 2、將Index看作有序集合:對集合進行并、交、差
# 這些操作還可以通過調(diào)用對象方法來實現(xiàn),例如 indA.intersection(indB)
indA = pd.Index([1, 3, 5, 7, 9])
indB = pd.Index([2, 3, 5, 7, 11])
indA & indB # 交集 Int64Index([3, 5, 7], dtype='int64')
indA | indB # 并集 Int64Index([1, 2, 3, 5, 7, 9, 11], dtype='int64')
indA ^ indB # 異或 Int64Index([1, 2, 9, 11], dtype='int64')
數(shù)據(jù)取值與選擇
# 1、Series 對象
# 1.1、將 Series 看作 Python 字典:鍵值對的映射
data = pd.Series([0.25, 0.5, 0.75, 1.0], index=['a', 'b', 'c', 'd'])
data['b']
'a' in data # 檢測鍵 True
data.keys() # 列出索引
list(data.items()) # 列出鍵值
data['e'] = 1.25 # 增加新的索引值擴展 Series
# 1.2、將 Series 看作 Numpy 一維數(shù)組
data['a':'c'] # 切片(使用索引名稱):顯式索引,結果包含最后一個索引
data[1:3] # 切片(使用索引位置):隱式索引,結果不包含最后一個索引,從0開始
data[1] # 取值:顯式索引
data[(data > 0.3) & (data < 0.8)] # 掩碼
data[['a', 'e']] # 列表索引
# 2、索引器:loc、iloc 和 ix(使用索引位置取值,取值范圍均為左閉右開?。?# 由于整數(shù)索引很容易造成混淆,所以 Pandas 提供了一些索引器(indexer)屬性來作為取值的方法
# 它們不是 Series 對象的函數(shù)方法,而是暴露切片接口的屬性
# Python 代碼的設計原則之一是“顯式優(yōu)于隱式”
# 2.1、loc 屬性:取值和切片都是顯式的,從1開始
data.loc[1] # 第1行
data.loc[1:3] # 第1-2行(左閉右開)
# 2.2、iloc 屬性:取值和切片都是 Python 形式的隱式索引,從0開始
data.iloc[1] # 第2行
data.iloc[1:3] # 第2-3行(左閉右開)
# 2.3、ix 屬性:實現(xiàn)混合效果
data.ix[:3, :'pop'] # 第0-2行,第1-pop列
# 3、DataFrame 對象
# 3.1、將 DataFrame 看作 Python 字典:鍵值對的映射
data['area'] # 字典形式取值:建議使用
data.area # 屬性形式取值:如果列名不是純字符串,或者列名與 DataFrame 的方法同名,那么就不能用屬性索引
data.area is data['area'] # True
data['density'] = data['pop'] / data['area'] # 增加一列
# 3.2、將 DataFrame 看作二維數(shù)組:可以把許多數(shù)組操作方式用在 DataFrame 上
data.values # 按行查看數(shù)組數(shù)據(jù)
data.T # 行列轉置
data.values[0] # 獲取一行數(shù)據(jù)
data['area'] # 獲取一列數(shù)據(jù):向 DataFrame 傳遞單個列索引
data.iloc[:3, :2] # 3行2列
data.loc[:'Illinois', :'pop'] # 截至到指定行列名的數(shù)值
data.ix[:3, :'pop'] # 第0-2行,第1-pop列
data.loc[data.density > 100, ['pop', 'density']] # 組合使用掩碼與列表索引
# 以上任何一種取值方法都可以用于調(diào)整數(shù)據(jù),這一點和 NumPy 的常用方法是相同的
data.iloc[0, 2] = 90
# 3.3、其他取值方法
# 如果對單個標簽取值就選擇列,而對多個標簽用切片就選擇行
data['Florida':'Illinois'] # 指定的兩行,所有列(缺省)
data[1:3] # 不用索引值,而直接用行數(shù)來實現(xiàn),從0開始的第1-2行
data[data.density > 100] # 掩碼操作直接對行進行過濾
Pandas 數(shù)值運算方法
NumPy 的基本能力之一是快速對每個元素進行運算,既包括基本算術運算(加、減、乘、除),也包括更復雜的運算(三角函數(shù)、指數(shù)函數(shù)和對數(shù)函數(shù)等)。Pandas 繼承了 NumPy 的功能,其中通用函數(shù)是關鍵。
但是 Pandas 也實現(xiàn)了一些高效技巧:對于一元運算(像函數(shù)與三角函數(shù)),這些通用函數(shù)將在輸出結果中保留索引和列標簽;而對于二元運算(如加法和乘法),Pandas 在傳遞通用函數(shù)時會自動對齊索引進行計算。這就意味著,保存數(shù)據(jù)內(nèi)容與組合不同來源的數(shù)據(jù)(兩處在 NumPy 數(shù)組中都容易出錯的地方)變成了 Pandas 的殺手锏。
# 1、通用函數(shù):保留索引
# 因為 Pandas 是建立在 NumPy 基礎之上的,所以 NumPy 的通用函數(shù)同樣適用于 Pandas 的 Series 和 DataFrame 對象
rng = np.random.RandomState(42)
ser = pd.Series(rng.randint(0, 10, 4)) # 0-10之間任意取4個整數(shù)
# 0 6
# 1 3
# 2 7
# 3 4
# dtype: int64
np.exp(ser) # 對 Series 使用 Numpy 通用函數(shù),結果是一個保留索引的 Series
df = pd.DataFrame(rng.randint(0, 10, (3, 4)), columns=['A', 'B', 'C', 'D']) # 0-10,3行4列
# A B C D
# 0 6 9 2 6
# 1 7 4 3 7
# 2 7 2 5 4
np.sin(df * np.pi / 4) # 對 DataFrame 使用 Numpy 通用函數(shù),結果是一個保留索引的 DataFrame
# 2、通用函數(shù):索引對齊
# 當在兩個 Series 或 DataFrame 對象上進行二元計算時,Pandas 會在計算過程中對齊兩個對象的索引!
# 實際上就是對索引的全外連接
# 2.1、Series 索引對齊
area = pd.Series({'Alaska': 1723337, 'Texas': 695662, 'California': 423967}, name='area')
population = pd.Series({'California': 38332521, 'Texas': 26448193, 'New York': 19651127}, name='population')
population / area
# 結果數(shù)組的索引是兩個輸入數(shù)組索引的并集,缺失位置的數(shù)據(jù)會用 NaN 填充
# Alaska NaN
# California 90.413926
# New York NaN
# Texas 38.018740
A = pd.Series([2, 4, 6], index=[0, 1, 2])
B = pd.Series([1, 3, 5], index=[1, 2, 3])
A + B
# 此處重復的索引是1和2,索引0和3的值運算結果為NaN
# 0 NaN
# 1 5.0
# 2 9.0
# 3 NaN
# 等價于 A + B,可以設置參數(shù)自定義 A 或 B 缺失的數(shù)據(jù)
A.add(B, fill_value=0)
# 2.2、DataFrame 索引對齊
A = pd.DataFrame(rng.randint(0, 20, (2, 2)), columns=list('AB'))
# A B
# 0 1 11
# 1 5 1
B = pd.DataFrame(rng.randint(0, 10, (3, 3)), columns=list('BAC'))
# B A C
# 0 4 0 9
# 1 5 8 0
# 2 9 2 6
A + B
# 行索引對齊(0行對0行),對應的列索引做運算(A列+A列)
# A B C
# 0 1.0 15.0 NaN
# 1 13.0 6.0 NaN
# 2 NaN NaN NaN
# 用 A 中所有值的均值來填充缺失值(計算 A 的均值需要用 stack 將二維數(shù)組壓縮成一維數(shù)組)
fill = A.stack().mean()
A.add(B, fill_value=fill)
# 2.3、通用函數(shù):DataFrame 與 Series 的運算
A = rng.randint(10, size=(3, 4))
# array([[3, 8, 2, 4],
# [2, 6, 4, 8],
# [6, 1, 3, 8]])
# 二維數(shù)組減自身的一行數(shù)據(jù)會按行計算,也就是用每一行的值減去第一行的對應值
A - A[0]
# array([[ 0, 0, 0, 0],
# [-1, -2, 2, 4],
# [ 3, -7, 1, 4]])
# 在 Pandas 里默認也是按行運算的
df = pd.DataFrame(A, columns=list('QRST'))
# Q R S T
# 0 3 8 2 4
# 1 2 6 4 8
# 2 6 1 3 8
df - df.iloc[0]
# Q R S T
# 0 0 0 0 0
# 1 -1 -2 2 4
# 2 3 -7 1 4
# 如果想按列計算,那么就需要通過 axis 參數(shù)設置
df.subtract(df['R'], axis=0)
# Q R S T
# 0 -5 0 -6 4
# 1 -4 0 -2 2
# 2 5 0 2 7
# DataFrame / Series 的運算結果的索引都會自動對齊
halfrow = df.iloc[0, ::2] # 每2列取1列
# Q S
# 0 3 2
df - halfrow
# df中的每一行都減掉halfrow中的0行,對應列相減,halfrow中沒有的列運算結果為NaN
# 相當于將halfrow復制成與df相同的形狀(行數(shù)),再進行運算
# Q R S T
# 0 0.0 NaN 0.0 NaN
# 1 -1.0 NaN 2.0 NaN
# 2 3.0 NaN 1.0 NaN
# 這些行列索引的保留與對齊方法說明 Pandas 在運算時會一直保存這些數(shù)據(jù)內(nèi)容
# 避免在處理數(shù)據(jù)類型有差異和 / 或維度不一致的 NumPy 數(shù)組時可能遇到的問題
| Python運算符 | Pandas方法 |
|---|---|
| + | add() |
| - | sub()、subtract() |
| * | mul()、multiply() |
| / | truediv()、div()、divide() |
| // | floordiv() |
| % | mod() |
| ** | pow() |
處理缺失值
缺失值主要有三種形式:null、NaN 或 NA。
# 1、None:Python 對象類型的缺失值(Python 單體對象)
# 不能作為任何 NumPy / Pandas 數(shù)組類型的缺失值,只能用于 'object' 數(shù)組類型(即由 Python 對象構成的數(shù)組)
# 在進行常見的快速操作時,這種類型比其他原生類型數(shù)組要消耗更多的資源
vals1 = np.array([1, None, 3, 4])
# array([1, None, 3, 4], dtype=object)
# 如果你對一個包含 None 的數(shù)組進行累計操作,通常會出現(xiàn)類型錯誤
vals1.sum()
# TypeError:在 Python 中沒有定義整數(shù)與 None 之間的加法運算
# 2、NaN:數(shù)值類型的缺失值(特殊浮點數(shù))
# NumPy 會為這個數(shù)組選擇一個原生浮點類型,和 object 類型數(shù)組不同,這個數(shù)組會被編譯成 C 代碼從而實現(xiàn)快速操作
# NaN 會將與它接觸過的數(shù)據(jù)同化,無論和 NaN 進行何種操作,最終結果都是 NaN
1 + np.nan
# NaN
# 不會拋出異常,但是并非有效的
vals2.sum(), vals2.min(), vals2.max()
# (nan, nan, nan)
# NumPy 也提供了一些特殊的累計函數(shù),它們可以忽略缺失值的影響
np.nansum(vals2), np.nanmin(vals2), np.nanmax(vals2)
# (8.0, 1.0, 4.0)
# 3、Pandas 中 NaN 與 None 的差異
# 雖然 NaN 與 None 各有各的用處,但是 Pandas 把它們看成是可以等價交換的,在適當?shù)臅r候會將兩者進行替換
pd.Series([1, np.nan, 2, None])
# 0 1.0
# 1 NaN
# 2 2.0
# 3 NaN
# dtype: float64
# Pandas 會將沒有標簽值的數(shù)據(jù)類型自動轉換為 NA
x = pd.Series(range(2), dtype=int)
# 0 0
# 1 1
# dtype: int64
x[0] = None
# 0 NaN
# 1 1.0
# dtype: float64
# 3、處理缺失值
# 3.1 發(fā)現(xiàn)缺失值
data = pd.Series([1, np.nan, 'hello', None])
data.isnull()
data.notnull()
# 0 False
# 1 True
# 2 False
# 3 True
# dtype: bool
# 布爾類型掩碼數(shù)組可以直接作為 Series 或 DataFrame 的索引使用
data[data.notnull()]
# 0 1
# 2 hello
# dtype: object
# 3.2 剔除缺失值
# 剔除 Series 中的缺失值
data.dropna()
# 剔除 DataFrame 中的缺失值,默認會剔除任何包含缺失值的整行數(shù)據(jù)
df.dropna()
df.dropna(axis='columns')
df.dropna(axis=1) # 二者等價,剔除任何包含缺失值的整列數(shù)據(jù)
# 默認設置 how='any'(只要有缺失值就剔除整行或整列)
# 可以設置 how='all'(只會剔除全部是缺失值的行或列)
df.dropna(axis='columns', how='all')
# thresh 參數(shù)設置行或列中非缺失值的最小數(shù)量
df.dropna(axis='rows', thresh=3)
# 3.3 填充缺失值
data = pd.Series([1, np.nan, 2, None, 3], index=list('abcde'))
# 用值填充 Series
data.fillna(0)
# 用缺失值前面的有效值來從前往后填充 Series(forward-fill)
data.fillna(method='ffill')
# 用缺失值后面的有效值來從后往前填充 Series(back-fill)
data.fillna(method='bfill')
# DataFrame 的操作方法與 Series 類似,只是在填充時需要設置坐標軸參數(shù) axis(axis=1填充列,axis=0填充行)
df.fillna(method='ffill', axis=1)
| 類型 | 缺失值轉換規(guī)則 | NA標簽值 |
|---|---|---|
| floating 浮點型 | 無變化 | np.nan |
| object 對象類型 | 無變化 | None 或 np.nan |
| integer 整數(shù)類型 | 強制轉換為 float64 | np.nan |
| boolean 布爾類型 | 強制轉換為 object | None 或 np.nan |
需要注意的是,Pandas 中字符串類型的數(shù)據(jù)通常是用 object 類型存儲的。
層級索引
到目前為止,我們接觸的都是一維數(shù)據(jù)和二維數(shù)據(jù),用 Pandas 的 Series 和 DataFrame 對象就可以存儲。但我們也經(jīng)常會遇到存儲多維數(shù)據(jù)的需求,數(shù)據(jù)索引超過一兩個鍵。因此,Pandas 提供了 Panel 和 Panel4D 對象解決三維數(shù)據(jù)與四維數(shù)據(jù)。
而在實踐中,更直觀的形式是通過層級索引(hierarchical indexing,也被稱為多級索引,multi-indexing)配合多個有不同等級(level)的一級索引一起使用,這樣就可以將高維數(shù)組轉換成類似一維 Series 和二維DataFrame 對象的形式。
# 1、多級索引 Series
# 假設你想要分析美國各州在兩個不同年份的數(shù)據(jù)
index = [('California', 2000),
('California', 2010),
('New York', 2000),
('New York', 2010),
('Texas', 2000),
('Texas', 2010)
]
populations = [33871648,
37253956,
18976457,
19378102,
20851820,
25145561
]
# 1.1 普通方法:用一個 Python 元組來表示索引
pop = pd.Series(populations, index=index)
# (California, 2000) 33871648
# (California, 2010) 37253956
# (New York, 2000) 18976457
# (New York, 2010) 19378102
# (Texas, 2000) 20851820
# (Texas, 2010) 25145561
pop[('California', 2010):('Texas', 2000)]
pop[[i for i in pop.index if i[1] == 2010]] # 切片很不方便
# 1.2 更優(yōu)方法:Pandas 多級索引
# MultiIndex 里面有一個 levels 屬性表示索引的等級,可以將州名和年份作為每個數(shù)據(jù)點的不同標簽
# lebels 表示 levels 中的 元素下標
index = pd.MultiIndex.from_tuples(index)
# MultiIndex(levels=[['California', 'New York', 'Texas'], [2000, 2010]],
# labels=[[0, 0, 1, 1, 2, 2], [0, 1, 0, 1, 0, 1]])
pop = pop.reindex(index) # 將 pop 的索引重置為 MultiIndex
state year
# California 2000 33871648
# 2010 37253956
# New York 2000 18976457
# 2010 19378102
# Texas 2000 20851820
# 2010 25145561
# 獲取 2010 年的全部數(shù)據(jù),結果是單索引的數(shù)組
pop[:, 2010]
# California 37253956
# New York 19378102
# Texas 25145561
# 1.3、高維數(shù)據(jù)的多級索引:可以用一個帶行列索引的簡單 DataFrame 代替前面的多級索引
# 將一個多級索引的 Series 轉化為普通索引的 DataFrame
# 相當于行轉列,很方便
pop_df = pop.unstack()
# 2000 2010
# California 33871648 37253956
# New York 18976457 19378102
# Texas 20851820 25145561
# 相當于列轉行,結果就是原來的 pop
pop_df.stack()
# 如果可以用含多級索引的一維 Series 數(shù)據(jù)表示二維數(shù)據(jù),那么就可以用 Series 或 DataFrame 表示三維甚至更高維度的數(shù)據(jù)
# 多級索引每增加一級,就表示數(shù)據(jù)增加一維,利用這一特點就可以輕松表示任意維度的數(shù)據(jù)了
# 對于這種帶有 MultiIndex 的對象,增加一列就像 DataFrame 的操作一樣簡單
# 增加一列 under18 顯示 18 歲以下的人口
pop_df = pd.DataFrame({'total': pop,
'under18': [9267089, 9284094, 4687374, 4318033, 5906301, 6879014]})
# 通用函數(shù)和其他功能也同樣適用于層級索引
# 計算上面數(shù)據(jù)中 18 歲以下的人口占總人口的比例
# f_u18 = pop_df['under18'] / pop_df['total']
# 2、創(chuàng)建多級索引
# 方法1:將 index 參數(shù)設置為至少二維的索引數(shù)組
df = pd.DataFrame(np.random.rand(4, 2),
index=[['a', 'a', 'b', 'b'], [1, 2, 1, 2]],
columns=['data1', 'data2'])
# data1 data2
# a 1 0.554233 0.356072
# 2 0.925244 0.219474
# b 1 0.441759 0.610054
# 2 0.171495 0.886688
# 方法2:將元組作為鍵的字典傳遞給 Pandas,Pandas 也會默認轉換為 MultiIndex
data = {('California', 2000): 33871648,
('California', 2010): 37253956,
('Texas', 2000): 20851820,
('Texas', 2010): 25145561,
('New York', 2000): 18976457,
('New York', 2010): 19378102}
pd.Series(data)
# 方法3:顯式創(chuàng)建多級索引:以下這四種創(chuàng)建方法等價,結果就是最后一種
# 在創(chuàng)建 Series 或 DataFrame 時,可以將這些對象作為 index 參數(shù),或者通過 reindex 方法更新 Series 或 DataFrame 的索引
pd.MultiIndex.from_arrays([['a', 'a', 'b', 'b'], [1, 2, 1, 2]]) # 數(shù)組列表
pd.MultiIndex.from_tuples([('a', 1), ('a', 2), ('b', 1), ('b', 2)]) # 元組
pd.MultiIndex.from_product([['a', 'b'], [1, 2]]) # 兩個索引的笛卡爾積
pd.MultiIndex(levels=[['a', 'b'], [1, 2]],
labels=[[0, 0, 1, 1], [0, 1, 0, 1]])
# 多級索引命名:可以在創(chuàng)建 MultiIndex 時指定 names,也可以過后再創(chuàng)建
# 相當于表頭
pop.index.names = ['state', 'year'] # pop 的表頭為州名+年份
# 3、多級行列索引
# 如果想獲取包含多種標簽的數(shù)據(jù),需要通過對多個維度(姓名、國家、城市等標簽)的多次查詢才能實現(xiàn),使用多級行列索引會非常方便
index = pd.MultiIndex.from_product([[2013, 2014], [1, 2]], names=['year', 'visit'])
columns = pd.MultiIndex.from_product([['Bob', 'Guido', 'Sue'], ['HR', 'Temp']], names=['subject', 'type'])
# 模擬體檢數(shù)據(jù)
data = np.round(np.random.randn(4, 6), 1)
data[:, ::2] *= 10
data += 37
# 創(chuàng)建 DataFrame(此處的索引使用笛卡爾積創(chuàng)建)
health_data = pd.DataFrame(data, index=index, columns=columns)
# subject Bob Guido Sue
# type HR Temp HR Temp HR Temp
# year visit
# 2013 1 31.0 38.7 32.0 36.7 35.0 37.2
# 2 44.0 37.7 50.0 35.0 29.0 36.7
# 2014 1 30.0 37.4 39.0 37.8 61.0 36.9
# 2 47.0 37.8 48.0 37.3 51.0 36.5
# 獲取某個人的全部體檢信息(只留下這個人的HR和Temp數(shù)據(jù))
health_data['Guido']
# 4、多級索引的取值與切片
# 4.1、Series 多級索引
pop['California', 2000] # 對索引值全部進行限制,獲取單個值
pop['California'] # 局部取值,只取最高級的索引,未被選中的低層索引值會被保留
pop.loc['California':'New York'] # 局部切片,要求 MultiIndex 是按順序排列的
pop[:, 2000] # 如果索引已經(jīng)排序,可以用較低層級的索引取值,第一層級的索引用空切片
pop[pop > 22000000] # 通過布爾掩碼選擇數(shù)據(jù)
pop[['California', 'Texas']] # 通過列表索引選擇數(shù)據(jù)
# 4.2、DataFrame 多級索引:應用在列上(第一個列索引,第二個列索引…)
health_data['Guido', 'HR']
health_data.iloc[:2, :2] # loc、iloc 和 ix 索引器都可以使用
health_data.loc[(:, 1), (:, 'HR')] # 如果在元組中使用切片會報錯
idx = pd.IndexSlice
health_data.loc[idx[:, 1], idx[:, 'HR']] # 使用 IndexSlice 對象
# 5、多級索引行列轉換
# 5.1、索引有序:如果 MultiIndex 不是有序的索引,那么大多數(shù)切片操作都會失敗
index = pd.MultiIndex.from_product([['a', 'c', 'b'], [1, 2]])
data = pd.Series(np.random.rand(6), index=index)
data['a':'b'] # 切片報錯
# Pandas 對索引進行排序
data = data.sort_index()
data['a':'b'] # 不再報錯
# 5.2、行列轉換:stack 和 unstack,互為逆運算
# 可以通過 level 參數(shù)設置轉換的索引層級(設置哪一列橫著)
pop.unstack(level=0)
# state California New York Texas
# year
# 2000 33871648 18976457 20851820
# 2010 37253956 19378102 25145561
pop.unstack(level=1)
# year 2000 2010
# state
# California 33871648 37253956
# New York 18976457 19378102
# Texas 20851820 25145561
# 5.3、行列標簽互換:reset_index
pop_flat = pop.reset_index(name='population')
pop_flat.set_index(['state', 'year'])
# 6、多級索引聚合操作:mean、sum、max、min等
# 可以設置參數(shù) level 實現(xiàn)對數(shù)據(jù)子集的聚合操作(group by level的字段,列出其余所有字段)
data_mean = health_data.mean(level='year')
data_mean.mean(axis=1, level='type')
合并數(shù)據(jù)集:Concat 與 Append 操作
def make_df(cols, ind):
data = {c: [str(c) + str(i) for i in ind]
for c in cols
}
return pd.DataFrame(data, ind)
make_df('ABC', range(3))
# A B C
# 0 A0 B0 C0
# 1 A1 B1 C1
# 2 A2 B2 C2
# 1、concat 方法
# 合并一維的 Series 或 DataFrame 對象
ser1 = pd.Series(['A', 'B', 'C'], index=[1, 2, 3])
ser2 = pd.Series(['D', 'E', 'F'], index=[4, 5, 6])
pd.concat([ser1, ser2])
# 1 A
# ...
# 6 F
# 合并高維數(shù)據(jù)
df1 = make_df('AB', [1, 2])
# A B
# 1 A1 B1
# 2 A2 B2
df2 = make_df('AB', [3, 4])
# A B
# 1 A3 B3
# 2 A4 B4
# 逐行合并:相當于 A union all B(包含A和B中的全部列,缺失值為NaN)
pd.concat([df1, df2])
# 合并坐標軸:相當于A full outer join B on(A.index = B.index)
pd.concat([df1, df2], axis='col') # axis=1
# 逐行合并后出現(xiàn)重復索引:
# pd.concat([df1, df2], verify_integrity=True) # 索引重復報錯
# pd.concat([df1, df2], ignore_index=True) # 重復的索引被替換成新索引
# pd.concat([df1, df2], keys=['x', 'y']) # 為數(shù)據(jù)源設置多級索引標簽(df1加上x列,df2加上y列)
# 合并列名不同數(shù)據(jù):默認join='outer',包含全部列
df5 = make_df('ABC', [1, 2])
df6 = make_df('BCD', [3, 4])
pd.concat([df5, df6], join='inner') # 只包含兩個對象都存在的列
pd.concat([df5, df6], join_axes=[df5.columns]) # 設置列名(合并后本來包含ABCD四列,設置后只有前三列)
# 2、append 方法:不直接更新原有對象的值,而是為合并后的數(shù)據(jù)創(chuàng)建一個新對象
# 不能被稱之為一個非常高效的解決方案,因為每次合并都需要重新創(chuàng)建索引和數(shù)據(jù)緩存
# 如果需要進行多個 append 操作,建議創(chuàng)建一個 DataFrame 列表,用 concat 函數(shù)一次性解決所有合并任務
df1.append(df2) # 等價于 pd.concat([df1, df2])
合并數(shù)據(jù)集:合并與連接(類似數(shù)據(jù)庫)
# pd.merge:自動以共同列作為鍵進行連接(共同列的位置可以不一致)
# 1.1、一對一連接
df1 = pd.DataFrame({'employee': ['Bob', 'Jake', 'Lisa', 'Sue'],
'group': ['Accounting', 'Engineering', 'Engineering', 'HR']})
df2 = pd.DataFrame({'employee': ['Lisa', 'Bob', 'Jake', 'Sue'],
'hire_date': [2004, 2008, 2012, 2014]})
df3 = pd.merge(df1, df2)
# employee group hire_date
# 0 Bob Accounting 2008
# 1 Jake Engineering 2012
# 2 Lisa Engineering 2004
# 3 Sue HR 2014
# 1.2、多對一連接
df4 = pd.DataFrame({'group': ['Accounting', 'Engineering', 'HR'],
'supervisor': ['Carly', 'Guido', 'Steve']})
pd.merge(df3, df4)
# employee group hire_date supervisor
# 0 Bob Accounting 2008 Carly
# 1 Jake Engineering 2012 Guido
# 2 Lisa Engineering 2004 Guido
# 3 Sue HR 2014 Steve
# 1.3、多對多連接:如果左右兩個輸入的共同列都包含重復值,合并的結果就是多對多連接
df5 = pd.DataFrame({'group': ['Accounting', 'Accounting','Engineering', 'Engineering', 'HR', 'HR'],
'skills': ['math', 'spreadsheets', 'coding', 'linux','spreadsheets', 'organization']})
pd.merge(df1, df5)
# 2、設置數(shù)據(jù)合并的鍵:on
# 2.1、有共同列名
pd.merge(df1, df2, on='employee')
# 2.2、列名不同:left 和 right
df6 = pd.DataFrame({'name': ['Bob', 'Jake', 'Lisa', 'Sue'],
'salary': [70000, 80000, 120000, 90000]})
# 獲取的結果中會有一個多余的列,可以通過 drop 方法將這列去掉
pd.merge(df1, df3, left_on="employee", right_on="name").drop('name', axis=1)
# 2.3、通過合并索引來實現(xiàn)合并:left_index 和 right_index
df1a = df1.set_index('employee')
df2a = df2.set_index('employee')
pd.merge(df1a, df2a, left_index=True, right_index=True)
# 將索引與列混合使用,左邊的 index 與右邊的 name 關聯(lián)
pd.merge(df1a, df3, left_index=True, right_on='name')
# 3、連接方式:how='inner' / 'outer' / 'left' / 'right'
pd.merge(df6, df7, how='inner')
# 4、重復列名:增加列名后綴 suffixes
df8 = pd.DataFrame({'name': ['Bob', 'Jake', 'Lisa', 'Sue'], 'rank': [1, 2, 3, 4]})
df9 = pd.DataFrame({'name': ['Bob', 'Jake', 'Lisa', 'Sue'], 'rank': [3, 1, 4, 2]})
pd.merge(df8, df9, on="name", suffixes=["_L", "_R"]) # 列名 rank 重復,加上后綴
案例:計算美國各州的人口密度排名
# 數(shù)據(jù)下載地址:https://github.com/jakevdp/data-USstates/
# 讀取 csv
pop = pd.read_csv('state-population.csv')
areas = pd.read_csv('state-areas.csv')
abbrevs = pd.read_csv('state-abbrevs.csv')
# 查看前5條數(shù)據(jù)
pop.head()
# state/region ages year population
# 0 AL under18 2012 1117489.0
# 1 AL total 2012 4817528.0
# 2 AL under18 2010 1130966.0
# 3 AL total 2010 4785570.0
# 4 AL under18 2011 1125763.0
areas.head()
areas.head()
# state area (sq. mi)
# 0 Alabama 52423
# 1 Alaska 656425
# 2 Arizona 114006
# 3 Arkansas 53182
# 3 Arkansas 53182
# 4 California 163707
abbrevs.head()
# state abbreviation
# 0 Alabama AL
# 1 Alaska AK
# 2 Arizona AZ
# 3 Arkansas AR
# 4 California CA
merged = pd.merge(pop, abbrevs, how='outer',
left_on='state/region', right_on='abbreviation') # 連接,確保數(shù)據(jù)沒有丟失
merged = merged.drop('abbreviation', 1) # 丟棄重復信息
merged.head()
# state/region ages year population state
# 0 AL under18 2012 1117489.0 Alabama
# 1 AL total 2012 4817528.0 Alabama
# 2 AL under18 2010 1130966.0 Alabama
# 3 AL total 2010 4785570.0 Alabama
# 4 AL under18 2011 1125763.0 Alabama
# 檢查一下數(shù)據(jù)是否有缺失
merged.isnull().any()
# 部分 population 是缺失值,仔細看看那些數(shù)據(jù)
merged[merged['population'].isnull()].head()
# state/region = 'PR', year <= 2000
# 看看究竟是哪個州有缺失
merged.loc[merged['state'].isnull(), 'state/region'].unique()
# array(['PR', 'USA'], dtype=object)
# 針對缺失值做處理,填充州名稱縮寫表中缺少的州名
merged.loc[merged['state/region'] == 'PR', 'state'] = 'Puerto Rico'
merged.loc[merged['state/region'] == 'USA', 'state'] = 'United States'
用兩個數(shù)據(jù)集共同的 state 列來合并
final = pd.merge(merged, areas, on='state', how='left')
final.head()
# state/region ages year population state area (sq. mi)
# 0 AL under18 2012 1117489.0 Alabama 52423.0
# 1 AL total 2012 4817528.0 Alabama 52423.0
# 2 AL under18 2010 1130966.0 Alabama 52423.0
# 3 AL total 2010 4785570.0 Alabama 52423.0
# 4 AL under18 2011 1125763.0 Alabama 52423.0
# 檢查最后的結果集還有哪些缺失值,并進行處理
final.isnull().any() # area 列
final['state'][final['area (sq. mi)'].isnull()].unique()
final.dropna(inplace=True)
# 現(xiàn)在2010年的數(shù)據(jù)準備好了
data2010 = final.query("year == 2010 & ages == 'total'")
data2010.set_index('state', inplace=True) # 設置索引
density = data2010['population'] / data2010['area (sq. mi)'] # 計算人口密度
density.sort_values(ascending=False, inplace=True) # 排序
density.tail() # 人口密度最低的幾個州
累計與分組
# 計算累計指標:sum、mean、median、min 和 max 等
# 通過 Seaborn 庫下載行星數(shù)據(jù)
import seaborn as sns
planets = sns.load_dataset('planets')
planets.shape
# (1035, 6)
planets.head()
# method number orbital_period mass distance year
# 0 Radial Velocity 1 269.300 7.10 77.40 2006
# 1 Radial Velocity 1 874.774 2.21 56.95 2008
# 2 Radial Velocity 1 763.000 2.60 19.84 2011
# 3 Radial Velocity 1 326.030 19.40 110.62 2007
# 4 Radial Velocity 1 516.220 10.50 119.47 2009
# Series 的累計函數(shù)
rng = np.random.RandomState(42)
ser = pd.Series(rng.rand(5))
ser.sum()
# DataFrame 的累計函數(shù):默認對每列進行統(tǒng)計
df = pd.DataFrame({'A': rng.rand(5), 'B': rng.rand(5)})
df.mean() # 對A、B列分別計算均值
df.mean(axis='columns') # 設置axis參數(shù),對每行計算均值
# 計算每一列的若干常用統(tǒng)計值:describe
# 包括每一列的count、mean、std、min、25%、50%、75%、max值
planets.dropna().describe()
| 指標 | 描述 |
|---|---|
| count() | 計數(shù)項 |
| first()、last() | 第一項與最后一項 |
| mean()、median() | 均值與中位數(shù) |
| min()、max() | 最小值與最大值 |
| std()、var() | 標準差與方差 |
| mad() | 均值絕對偏差(mean absolute deviation) |
| prod() | 所有項乘積 |
| sum() | 所有項求和 |
Pandas的累計方法見上表,DataFrame 和 Series 對象支持以上所有方法。
GroupBy:分割、應用和組合

分組(group by):分割(split)、應用(apply)和組合(combine)
df = pd.DataFrame({'key': ['A', 'B', 'C', 'A', 'B', 'C'],
'data': range(6)}, columns=['key', 'data'])
# DataFrameGroupBy 對象:延遲計算(在沒有應用累計函數(shù)之前不會計算)
df.groupby('key')
# select sum(其余所有列) group by key
df.groupby('key').sum()
# 1.1、按列取值
# select median(orbital_period) group by method
planets.groupby('method')['orbital_period'].median()
# 1.2、按組迭代:GroupBy 對象支持直接按組進行迭代,返回的每一組都是 Series 或 DataFrame
for (method, group) in planets.groupby('method'):
print("{0:30s} shape={1}".format(method, group.shape))
# 1.3、調(diào)用方法:讓任何不由 GroupBy 對象直接實現(xiàn)的方法直接應用到每一組
# 用 DataFrame 的 describe 方法進行累計,對每一組數(shù)據(jù)進行描述性統(tǒng)計
planets.groupby('method')['year'].describe().unstack()
# 2、累計、過濾、轉換和應用:aggregate、filter、transform 和 apply
rng = np.random.RandomState(0)
df = pd.DataFrame({'key': ['A', 'B', 'C', 'A', 'B', 'C'],
'data1': range(6),
'data2': rng.randint(0, 10, 6)},
columns = ['key', 'data1', 'data2'])
# key data1 data2
# 0 A 0 5
# 1 B 1 0
# 2 C 2 3
# 3 A 3 3
# 4 B 4 7
# 5 C 5 9
# 2.1、累計:aggregate
# 指定函數(shù)列表
df.groupby('key').aggregate(['min', np.median, max])
# data1 data2
# min median max min median max
# key
# A 0 1.5 3 3 4.0 5
# B 1 2.5 4 0 3.5 7
# C 2 3.5 5 3 6.0 9
# 通過 Python 字典指定不同列需要累計的函數(shù)
df.groupby('key').aggregate({'data1': 'min', 'data2': 'max'})
# data1 data2
# key
# A 0 5
# B 1 7
# C 2 9
# 2.2、過濾:filter(按照分組的屬性丟棄若干數(shù)據(jù))
def filter_func(x):
return x['data2'].std() > 4
# filter 函數(shù)會返回一個布爾值,表示每個組是否通過過濾
# key = 'A' 的數(shù)據(jù)中 data2 列的標準差不大于4,被丟棄了,只保留了原始 df 中 key = 'B' 和 key = 'C'的4行數(shù)據(jù)
df.groupby('key').filter(filter_func)
# 2.3、轉換:transform(返回一個新的全量數(shù)據(jù),其形狀與原來的輸入數(shù)據(jù)是一樣的)
# 將每一組的樣本數(shù)據(jù)減去各組的均值,實現(xiàn)數(shù)據(jù)標準化
df.groupby('key').transform(lambda x: x - x.mean())
# 2.4、應用:apply(在每個組上應用任意方法,輸入分組數(shù)據(jù)的 DataFrame)
# 將第一列數(shù)據(jù)以第二列的和為基數(shù)進行標準化
def norm_by_data2(x):
x['data1'] /= x['data2'].sum()
return x
# 應用該方法
df.groupby('key').apply(norm_by_data2)
# 3、設置 DataFrame 分組鍵
# 3.1、用列名分組
df.groupby('key').sum()
# 3.2、將列表、數(shù)組、Series 或索引作為分組鍵(長度與 DataFrame 匹配)
L = [0, 1, 0, 1, 2, 0]
df.groupby(L).sum()
# data1 data2
# 0 7 17
# 1 4 3
# 2 4 7
# 3.3、用字典或 Series 將索引映射到分組名稱
df2 = df.set_index('key')
mapping = {'A': 'vowel', 'B': 'consonant', 'C': 'consonant'}
df2.groupby(mapping).sum()
# data1 data2
# consonant 12 19
# vowel 3 8
# 3.4、將任意 Python 函數(shù)傳入 groupby,函數(shù)映射到索引,然后新的分組輸出
df2.groupby(str.lower).mean()
# data1 data2
# a 1.5 4.0
# b 2.5 3.5
# c 3.5 6.0
# 3.5、多個有效鍵構成的列表:任意有效的鍵都可以組合起來進行分組,返回一個多級索引的分組結果
df2.groupby([str.lower, mapping]).mean()
# data1 data2
# a vowel 1.5 4.0
# b consonant 2.5 3.5
# c consonant 3.5 6.0
# 3.6、分組案例
# 獲取不同方法和不同年份發(fā)現(xiàn)的行星數(shù)量
decade = 10 * (planets['year'] // 10)
decade = decade.astype(str) + 's'
decade.name = 'decade'
planets.groupby(['method', decade])['number'].sum().unstack().fillna(0)
數(shù)據(jù)透視表
數(shù)據(jù)透視表將每一列數(shù)據(jù)作為輸入,輸出將數(shù)據(jù)不斷細分成多個維度累計信息的二維數(shù)據(jù)表。數(shù)據(jù)透視表更像是一種多維的 GroupBy 累計操作,分割與組合不是發(fā)生在一維索引上,而是在二維網(wǎng)格上(行列同時分組)。
# 1、案例:泰坦尼克號乘客生還率
import numpy as np
import pandas as pd
import seaborn as sns
# 泰坦尼克號的乘客信息,包括性別(gender)、年齡(age)、船艙等級(class)和船票價格(fare paid)等
titanic = sns.load_dataset('titanic')
# 不同性別乘客的生還率
titanic.groupby('sex')[['survived']].mean()
# survived
sex
female 0.742038
male 0.188908
# 不同性別、倉位乘客的生還率
titanic.groupby(['sex', 'class'])['survived'].aggregate('mean').unstack()
# 數(shù)據(jù)透視表,代碼可讀性更強,結果一樣
titanic.pivot_table('survived', index='sex', columns='class')
# class First Second Third
# sex
# female 0.968085 0.921053 0.500000
# male 0.368852 0.157407 0.135447
# 多級數(shù)據(jù)透視表
age = pd.cut(titanic['age'], [0, 18, 80]) # 年齡分段
titanic.pivot_table('survived', ['sex', age], 'class') # 性別+年齡+倉位
# class First Second Third
# sex age
# female (0, 18] 0.909091 1.000000 0.511628
# (18, 80] 0.972973 0.900000 0.423729
# male (0, 18] 0.800000 0.600000 0.215686
# (18, 80] 0.375000 0.071429 0.133663
fare = pd.qcut(titanic['fare'], 2) # 票價分段
titanic.pivot_table('survived', ['sex', age], [fare, 'class']) # 性別+年齡+倉位+票價
# 通過字典為不同的列指定不同的累計函數(shù)
titanic.pivot_table(index='sex', columns='class',
aggfunc={'survived':sum, 'fare':'mean'})
# 計算每一組的總數(shù)
titanic.pivot_table('survived', index='sex', columns='class', margins=True)
# 2、案例:美國人的生日分布
# https://raw.githubusercontent.com/jakevdp/data-CDCbirths/master/births.csv
births = pd.read_csv('births.csv')
births['decade'] = 10 * (births['year'] // 10)
births.pivot_table('births', index='decade', columns='gender', aggfunc='sum')
# 異常值處理:直接刪除異常值 / 更穩(wěn)定的 sigma 消除法(按照正態(tài)分布標準差劃定范圍)
quartiles = np.percentile(births['births'], [25, 50, 75])
mu = quartiles[1]
# 樣本均值的穩(wěn)定性估計,0.74 是指標準正態(tài)分布的分位數(shù)間距
sig = 0.74 * (quartiles[2] - quartiles[0])
# 用這個范圍就可以將有效的生日數(shù)據(jù)篩選出來了
births = births.query('(births > @mu - 5 * @sig) & (births < @mu + 5 * @sig)')
# 將'day'列設置為整數(shù)。由于原數(shù)據(jù)含有缺失值null,因此是字符串
births['day'] = births['day'].astype(int)
# 從年月日創(chuàng)建一個日期索引
births.index = pd.to_datetime(10000 * births.year +
100 * births.month +
births.day, format='%Y%m%d')
births['dayofweek'] = births.index.dayofweek
# 用這個索引可以畫出不同年代不同星期的日均出生數(shù)據(jù)
import matplotlib.pyplot as plt
import matplotlib as mpl
births.pivot_table('births', index='dayofweek', columns='decade', aggfunc='mean').plot()
plt.gca().set_xticklabels(['Mon', 'Tues', 'Wed', 'Thurs', 'Fri', 'Sat', 'Sun'])
plt.ylabel('mean births by day');
# 各個年份平均每天的出生人數(shù),可以按照月和日兩個維度分別對數(shù)據(jù)進行分組
births_by_date = births.pivot_table('births', [births.index.month, births.index.day]) # 多級索引
# 虛構一個年份,與月和日組合成新索引
births_by_date.index = [pd.datetime(2012, month, day)
for (month, day) in births_by_date.index]
# 畫圖
fig, ax = plt.subplots(figsize=(12, 4))
births_by_date.plot(ax=ax);
向量化字符串操作
# 數(shù)組:向量化操作簡化了語法,可以快速地對多個數(shù)組元素執(zhí)行同樣的操作
x = np.array([2, 3, 5, 7, 11, 13])
x * 2
# 字符串:由于 NumPy 并沒有為字符串數(shù)組提供簡單的接口,需要通過 for 循環(huán)來實現(xiàn)
data = ['peter', 'Paul', 'MARY', 'gUIDO']
[s.capitalize() for s in data] # 假如數(shù)據(jù)中出現(xiàn)了缺失值,就會引起異常
# Pandas 為包含字符串的 Series 和 Index 對象提供 str 屬性
# 既可以滿足向量化字符串操作的需求,又可以正確地處理缺失值
names = pd.Series(data)
names.str.capitalize() # 將所有的字符串轉成大寫,缺失值會被跳過
Pandas 的 str 方法借鑒 Python 字符串方法的內(nèi)容:
len() lower() translate() islower()
ljust() upper() startswith() isupper()
rjust() find() endswith() isnumeric()
center() rfind() isalnum() isdecimal()
zfill() index() isalpha() split()
strip() rindex() isdigit() rsplit()
rstrip() capitalize() isspace() partition()
lstrip() swapcase() istitle() rpartition()
Pandas向量化字符串方法與Python標準庫的re模塊函數(shù)的對應關系:
| 方法 | 描述 |
|---|---|
| match() | 對每個元素調(diào)用 re.match(),返回布爾類型值 |
| extract() | 對每個元素調(diào)用 re.match(),返回匹配的字符串組(groups) |
| findall() | 對每個元素調(diào)用 re.findall() |
| replace() | 用正則模式替換字符串 |
| contains() | 對每個元素調(diào)用 re.search(),返回布爾類型值 |
| count() | 計算符合正則模式的字符串的數(shù)量 |
| split() | 等價于 str.split(),支持正則表達式 |
| rsplit() | 等價于 str.rsplit(),支持正則表達式 |
其他Pandas字符串方法:
| 方法 | 描述 |
|---|---|
| get() | 獲取元素索引位置上的值,索引從 0 開始 |
| slice() | 對元素進行切片取值 |
| slice_replace() | 對元素進行切片替換 |
| cat() | 連接字符串(此功能比較復雜,建議閱讀文檔) |
| repeat() | 重復元素 |
| normalize() | 將字符串轉換為 Unicode 規(guī)范形式 |
| pad() | 在字符串的左邊、右邊或兩邊增加空格 |
| wrap() | 將字符串按照指定的寬度換行 |
| join() | 用分隔符連接 Series 的每個元素 |
| get_dummies() | 按照分隔符提取每個元素的 dummy 變量,轉換為獨熱(onehot)編碼的 DataFrame |
monte = pd.Series(['Graham Chapman', 'John Cleese', 'Terry Gilliam',
'Eric Idle', 'Terry Jones', 'Michael Palin'])
# 提取元素前面的連續(xù)字母作為每個人的名字
monte.str.extract('([A-Za-z]+)')
# 找出所有開頭和結尾都是輔音字母的名字
# 開始符號(^)與結尾符號($)
monte.str.findall(r'^[^AEIOU].*[^aeiou]$')
# 1、向量化字符串的取值與切片操作:get 和 slice
# 獲取前三個字符
monte.str[0:3]
df.str.slice(0, 3) # 二者等價
# 獲取第i列
df.str[i]
df.str.get(i) # 效果類似
# 獲取每個姓名的姓(最后一列)
monte.str.split().str.get(-1)
# 將指標變量(包含某種編碼信息的數(shù)據(jù)集)分割成獨熱編碼(0或1):get_dummies
# A= 出生在美國、B= 出生在英國、C= 喜歡奶酪、D= 喜歡午餐肉
full_monte = pd.DataFrame({'name': monte,
'info': ['B|C|D', 'B|D', 'A|C', 'B|D', 'B|C', 'B|C|D']})
# info name
# 0 B|C|D Graham Chapman
# 1 B|D John Cleese
# 2 A|C Terry Gilliam
# 3 B|D Eric Idle
# 4 B|C Terry Jones
# 5 B|C|D Michael Palin
# 分割指標變量
full_monte['info'].str.get_dummies('|')
# A B C D
# 0 0 1 1 1
# 1 0 1 0 1
# 2 1 0 1 0
# 3 0 1 0 1
# 4 0 1 1 0
# 5 0 1 1 1
更多案例:Pandas 在線文檔中的“Working with Text Data”(http://pandas.pydata.org/pandasdocs/stable/text.html)。
案例:食譜數(shù)據(jù)庫
# http://openrecipes.s3.amazonaws.com/recipeitems-latest.json.gz
# 讀入 json 數(shù)據(jù)
try:
recipes = pd.read_json('recipeitems-latest.json')
except ValueError as e:
print("ValueError:", e)
# ValueError:原因好像是雖然文件中的每一行都是一個有效的 JSON 對象,但是全文卻不是這樣
# 檢查文件數(shù)據(jù)格式
with open('recipeitems-latest.json') as f:
line = f.readline()
pd.read_json(line).shape
# 顯然每一行都是一個有效的 JSON 對象
# 新建一個字符串,將所有行 JSON 對象連接起來,然后再讀取數(shù)據(jù)
with open('recipeitems-latest.json', 'r') as f:
data = (line.strip() for line in f) # 提取每一行內(nèi)容
data_json = "[{0}]".format(','.join(data)) # 將所有內(nèi)容合并成一個列表
recipes = pd.read_json(data_json) # 用 JSON 形式讀取數(shù)據(jù)
# 食材列表的長度分布,最長的有9000字符
recipes.ingredients.str.len().describe()
# 來看看這個擁有最長食材列表的究竟是哪道菜
recipes.name[np.argmax(recipes.ingredients.str.len())]
# 哪些食譜是早餐
recipes.description.str.contains('[Bb]reakfast').sum()
# 有多少食譜用肉桂(cinnamon)作為食材
recipes.ingredients.str.contains('[Cc]innamon').sum()
# 簡易的美食推薦系統(tǒng):如果用戶提供一些食材,系統(tǒng)就會推薦使用了所有食材的食譜
# 由于大量不規(guī)則(heterogeneity)數(shù)據(jù)的存在,這個任務變得十分復雜,例如并沒有一個簡單直接的辦法可以從每一行數(shù)據(jù)中清理出一份干凈的食材列表
# 因此我們在這里做簡化處理:
# 首先提供一些常見食材列表,然后通過簡單搜索判斷這些食材是否在食譜中
# 為了簡化任務,這里只列舉常用的香料和調(diào)味料
spice_list = ['salt', 'pepper', 'oregano', 'sage', 'parsley',
'rosemary', 'tarragon', 'thyme', 'paprika', 'cumin']
# 通過一個布爾類型的 DataFrame 來判斷食材是否出現(xiàn)在某個食譜中
import re
spice_df = pd.DataFrame(dict((spice, recipes.ingredients.str.contains(spice, re.IGNORECASE))
for spice in spice_list))
# 現(xiàn)在來找一份使用了歐芹(parsley)、辣椒粉(paprika)和龍蒿葉(tarragon)的食譜
selection = spice_df.query('parsley & paprika & tarragon')
# 看看究竟是哪些食譜,可以做推薦了
recipes.name[selection.index]
處理時間序列
日期與時間數(shù)據(jù)主要包含三類:
1、時間戳:表示某個具體的時間點(例如 2015 年 7 月 4 日上午 7 點)。
2、時間間隔與周期:表示開始時間點與結束時間點之間的時間長度,例如 2015 年(指的是 2015 年 1 月 1 日至 2015 年 12 月 31 日這段時間間隔)。周期通常是指一種特殊形式的時間間隔,每個間隔長度相同,彼此之間不會重疊(例如,以 24 小時為周期構成每一天)。
3、時間增量(time delta)或持續(xù)時間(duration)表示精確的時間長度(例如,某程序運行持續(xù)時間 22.56 秒)。
Pandas頻率代碼(結束時間):
| 代碼 | 描述 | 代碼 | 描述 |
|---|---|---|---|
| D | 天(calendar day) | B | 天(business day,僅含工作日) |
| W | 周(weekly) | ||
| M | 月末(month end) | BM | 月末(business month end,僅含工作日) |
| Q | 季末(quarter end) | BQ | 季末(business quarter end,僅含工作日) |
| A | 年末(year end) | BA | 年末(business year end,僅含工作日) |
| H | 小時(hours) | BH | 小時(business hours,工作時間) |
| T | 分鐘(minutes) | ||
| S | 秒(seconds) | ||
| L | 毫秒(milliseonds) | ||
| U | 微秒(microseconds) | ||
| N | 納秒(nanoseconds) |
Pandas頻率代碼(結束時間):
| 代碼 | 描述 |
|---|---|
| MS | 月初(month start) |
| BMS | 月初(business month start,僅含工作日) |
| QS | 季初(quarter start) |
| BQS | 季初(business quarter start,僅含工作日) |
| AS | 年初(year start) |
| BAS | 年初(business year start,僅含工作日) |
可以在頻率代碼后面加三位月份縮寫字母來改變季、年頻率的開始時間:
- Q-JAN、BQ-FEB、QS-MAR、BQS-APR 等。
- A-JAN、BA-FEB、AS-MAR、BAS-APR 等。
同理,也可以在后面加三位星期縮寫字母來改變一周的開始時間:
- W-SUN、W-MON、W-TUE、W-WED 等。
所有這些頻率代碼都對應 Pandas 時間序列的偏移量,具體內(nèi)容可以在 pd.tseries.offsets 模塊中找到。
# 1、原生Python的日期與時間工具:datetime 與 dateutil
from datetime import datetime
from dateutil import parser
# 創(chuàng)建一個日期
date = datetime(year=2015, month=7, day=4)
date = parser.parse("4th of July, 2015") # 對字符串格式的日期進行解析
# 這一天是星期幾
date.strftime('%A')
# 標準字符串代碼格式參見 datetime 文檔的 strftime 說明
# https://docs.python.org/3/library/datetime.html#strftime-and-strptimebehavior
# 關于 dateutil 的其他日期功能可以通過 dateutil 的在線文檔(http://labix.org/python-dateutil)學習
# 還有一個值得關注的程序包是 pytz(http://pytz.sourceforge.net/),解決了絕大多數(shù)時間序列數(shù)據(jù)都會遇到的難題:時區(qū)
# 2、時間類型數(shù)組:NumPy 的 datetime64 類型
# datetime64 類型將日期編碼為 64 位整數(shù),這樣可以讓日期數(shù)組非常緊湊(節(jié)省內(nèi)存)
# 時區(qū)將自動設置為執(zhí)行代碼的操作系統(tǒng)的當?shù)貢r區(qū)
# 需要在設置日期時確定具體的輸入類型
date = np.array('2015-07-04', dtype=np.datetime64)
# 有了datetime64 就可以進行快速向量化操作
date + np.arange(12)
# NumPy 會自動判斷輸入時間需要使用的時間單位
np.datetime64('2015-07-04') # 以天為單位
np.datetime64('2015-07-04 12:00') # 以分鐘為單位
np.datetime64('2015-07-04 12:59:59.50', 'ns') # 設置時間單位為納秒
# 3、Pandas的日期與時間工具:理想與現(xiàn)實的最佳解決方案
index = pd.DatetimeIndex(['2014-07-04', '2014-08-04', '2015-07-04', '2015-08-04'])
data = pd.Series([0, 1, 2, 3], index=index)
# 2014-07-04 0
# 2014-08-04 1
# 2015-07-04 2
# 2015-08-04 3
# 用日期進行切片取值
data['2014-07-04':'2015-07-04']
# 年份切片(僅在此類 Series 上可用)
data['2015']
# 時間戳:Timestamp 類型,對應的索引數(shù)據(jù)結構是 DatetimeIndex
# 周期性數(shù)據(jù):Period 類型,對應的索引數(shù)據(jù)結構是 PeriodIndex
# 時間增量:Timedelta 類型,對應的索引數(shù)據(jù)結構是 TimedeltaIndex
# 3.1、時間戳:Timestamp
# to_datetime 方法可以解析許多日期與時間格式
# 傳遞一個日期會返回一個 Timestamp 類型,傳遞一個時間序列會返回一個 DatetimeIndex 類型
dates = pd.to_datetime([datetime(2015, 7, 3), '4th of July, 2015', '2015-Jul-6', '07-07-2015', '20150708'])
# 3.2、周期性數(shù)據(jù)
# to_period 方法可以將 DatetimeIndex 轉換為 PeriodIndex,傳入頻率代碼
dates.to_period('D') # 轉換為單日時間序列
# 3.3、時間增量
# 用一個日期減去另一個日期時,返回的結果是 TimedeltaIndex 類型
dates - dates[0]
# 3.4、有規(guī)律的時間序列
# data_range 方法通過開始日期、結束日期和頻率代碼創(chuàng)建一個有規(guī)律的日期序列,默認的頻率是天
pd.date_range('2015-07-03', '2015-07-10') # 指定開始日期和結束日期
pd.date_range('2015-07-03', periods=8) # 指定開始日期和周期數(shù) periods
pd.date_range('2015-07-03', periods=8, freq='H') # 時間間隔 freq 為小時
pd.period_range('2015-07', periods=8, freq='M') # 以月為周期
pd.timedelta_range(0, periods=10, freq='H') # 以小時遞增的序列
# 可以將頻率組合起來創(chuàng)建的新的周期,比如 2 小時 30 分鐘
pd.timedelta_range(0, periods=9, freq="2H30T")
# 創(chuàng)建工作日偏移序列
from pandas.tseries.offsets import BDay
pd.date_range('2015-07-01', periods=5, freq=BDay())
# 4、重新取樣、遷移和窗口
# 導入金融數(shù)據(jù)
from pandas_datareader import data
goog = data.DataReader('GOOG', start='2004', end='2016', data_source='google')
goog = goog['Close'] # 只保留 Google 的收盤價
# 畫出 Google 股價走勢圖
%matplotlib inline
import matplotlib.pyplot as plt
import seaborn; seaborn.set()
goog.plot();
# 4.1、重新取樣與頻率轉換:resample 和 asfreq
# 處理時間序列數(shù)據(jù)時,經(jīng)常需要按照新的頻率(更高頻率、更低頻率)對數(shù)據(jù)進行重新取樣
# resample:基于數(shù)據(jù)累計
# asfreq:基于數(shù)據(jù)選擇
# 用年末('BA',最后一個工作日)對收盤價進行重新取樣
goog.plot(alpha=0.5, style='-')
goog.resample('BA').mean().plot(style=':') # 反映上一年的均值
goog.asfreq('BA').plot(style='--') # 反映上一年最后一個工作日的收盤價
plt.legend(['input', 'resample', 'asfreq'], loc='upper left');
# resample 和 asfreq 方法都默認將向前取樣作為缺失值處理(填充 NaN)
# 對工作日數(shù)據(jù)按天進行重新取樣(即包含周末)
fig, ax = plt.subplots(2, sharex=True)
data = goog.iloc[:10]
data.asfreq('D').plot(ax=ax[0], marker='o')
data.asfreq('D', method='bfill').plot(ax=ax[1], style='-o') # 向前填充
data.asfreq('D', method='ffill').plot(ax=ax[1], style='--o') # 向后填充
ax[1].legend(["back-fill", "forward-fill"])
# 4.2、時間遷移:shift(遷移數(shù)據(jù))和 tshift(遷移索引)
fig, ax = plt.subplots(3, sharey=True)
goog = goog.asfreq('D', method='pad') # 對數(shù)據(jù)應用時間頻率,向后填充解決缺失值
goog.plot(ax=ax[0])
goog.shift(900).plot(ax=ax[1])
goog.tshift(900).plot(ax=ax[2])
# 設置圖例與標簽
local_max = pd.to_datetime('2007-11-05')
offset = pd.Timedelta(900, 'D')
ax[0].legend(['input'], loc=2)
ax[0].get_xticklabels()[4].set(weight='heavy', color='red')
ax[0].axvline(local_max, alpha=0.3, color='red')
ax[1].legend(['shift(900)'], loc=2)
ax[1].get_xticklabels()[4].set(weight='heavy', color='red')
ax[1].axvline(local_max + offset, alpha=0.3, color='red')
ax[2].legend(['tshift(900)'], loc=2)
ax[2].get_xticklabels()[1].set(weight='heavy', color='red')
ax[2].axvline(local_max + offset, alpha=0.3, color='red');
# 常見使用場景:計算數(shù)據(jù)在不同時段的差異
# 用遷移后的值來計算 Google 股票一年期的投資回報率
ROI = 100 * (goog.tshift(-365) / goog - 1)
ROI.plot()
plt.ylabel('% Return on Investment');
# 4.3、移動時間窗口:rolling(簡化累計操作,返回與 groupby 操作類似的結果)
# 與 groupby 操作一樣,aggregate 和 apply 方法都可以用來自定義移動計算
# 獲取 Google 股票收盤價的一年期移動平均值和標準差
rolling = goog.rolling(365, center=True)
data = pd.DataFrame({'input': goog,
'one-year rolling_mean': rolling.mean(),
'one-year rolling_std': rolling.std()})
ax = data.plot(style=['-', '--', ':'])
ax.lines[0].set_alpha(0.3)
# 5、案例:美國西雅圖自行車統(tǒng)計數(shù)據(jù)的可視化
# https://data.seattle.gov/api/views/65db-xm6k/rows.csv?accessType=DOWNLOAD
pd.read_csv('FremontBridge.csv', index_col='Date', parse_dates=True)
# 重新設置列名,縮短一點
data.columns = ['West', 'East']
# 新增一列
data['Total'] = data.eval('West + East')
# 看看這三列的統(tǒng)計值
data.dropna().describe()
# 為原始數(shù)據(jù)畫圖
%matplotlib inline
import seaborn; seaborn.set()
data.plot()
plt.ylabel('Hourly Bicycle Count')
# 小時數(shù)太多了,重新取樣,按周累計
weekly = data.resample('W').sum()
weekly.plot(style=[':', '--', '-'])
plt.ylabel('Weekly Bicycle Count')
# 計算數(shù)據(jù)的 30 日移動均值:
daily = data.resample('D').sum()
daily.rolling(30, center=True).mean().plot(style=[':', '--', '-'])
plt.ylabel('mean of 30 days count')
# 由于窗口太小,圖形還不太平滑
# 可以用另一個移動均值的方法獲得更平滑的圖形,例如高斯分布時間窗口
# 設置窗口的寬度為 50 天和窗口內(nèi)高斯平滑的寬度為 10 天
daily.rolling(50, center=True,
win_type='gaussian').sum(std=10).plot(style=[':', '--', '-'])
# 計算單日內(nèi)的小時均值流量
# 小時均值流量呈現(xiàn)出明顯的雙峰分布特征,早間峰值在上午 8 點,晚間峰值在下午 5 點
by_time = data.groupby(data.index.time).mean()
hourly_ticks = 4 * 60 * 60 * np.arange(6)
by_time.plot(xticks=hourly_ticks, style=[':', '--', '-']);
# 計算周內(nèi)每天的變化
# 工作日與周末的自行車流量差十分顯著,工作日通過的自行車差不多是周末的兩倍
by_weekday = data.groupby(data.index.dayofweek).mean()
by_weekday.index = ['Mon', 'Tues', 'Wed', 'Thurs', 'Fri', 'Sat', 'Sun']
by_weekday.plot(style=[':', '--', '-'])
# 計算一周內(nèi)工作日與周末每小時的自行車流量均值
weekend = np.where(data.index.weekday < 5, 'Weekday', 'Weekend')
by_time = data.groupby([weekend, data.index.time]).mean()
# 畫出工作日和周末的兩張圖
# 工作日的自行車流量呈雙峰通勤模式,而到了周末就變成了單峰娛樂模式
import matplotlib.pyplot as plt
fig, ax = plt.subplots(1, 2, figsize=(14, 5))
by_time.ix['Weekday'].plot(ax=ax[0], title='Weekdays',
xticks=hourly_ticks, style=[':', '--', '-'])
by_time.ix['Weekend'].plot(ax=ax[1], title='Weekends',
xticks=hourly_ticks, style=[':', '--', '-'])
# 還可以繼續(xù)挖掘天氣、溫度以及其他因素對人們通勤模式的影響
高性能 Pandas:eval 與 query(復合代數(shù)式)
# 1、Numexpr:可以在不為中間過程分配全部內(nèi)存的前提下,完成元素到元素的復合代數(shù)式運算
# NumPy 與 Pandas 都支持快速的向量化運算
# 比如對下面兩個數(shù)組進行求和,比普通的 Python 循環(huán)或列表綜合要快很多
rng = np.random.RandomState(42)
x = rng.rand(1E6)
y = rng.rand(1E6)
x + y
np.fromiter((xi + yi for xi, yi in zip(x, y)), dtype=x.dtype, count=len(x))
# 但是這種運算在處理復合代數(shù)式問題時的效率比較低
mask = (x > 0.5) & (y < 0.5)
# 因為等價于以下操作,每段中間過程都需要顯式地分配內(nèi)存
# 如果 x 數(shù)組和 y 數(shù)組非常大,這么運算就會占用大量的時間和內(nèi)存消耗
tmp1 = (x > 0.5)
tmp2 = (y < 0.5)
mask = tmp1 & tmp2
# Numexpr 在計算代數(shù)式時不需要為臨時數(shù)組分配全部內(nèi)存,因此計算比 NumPy 更高效,尤其適合處理大型數(shù)組
import numexpr
mask_numexpr = numexpr.evaluate('(x > 0.5) & (y < 0.5)')
np.allclose(mask, mask_numexpr)
# 2、高性能運算:eval
nrows, ncols = 100000, 100
rng = np.random.RandomState(42)
df1, df2, df3, df4, df5 = (pd.DataFrame(rng.randint(0, 1000, (100, 3)))
for i in range(5))
# 用普通的 Pandas 方法求和
df1 + df2 + df3 + df4
# 通過 eval 方法和字符串代數(shù)式求和,更高效
pd.eval('df1 + df2 + df3 + df4')
# 效率更高,內(nèi)存消耗更少
np.allclose(df1 + df2 + df3 + df4, pd.eval('df1 + df2 + df3 + df4'))
# 算數(shù)運算符
result1 = -df1 * df2 / (df3 + df4) - df5
# 比較運算符(包括鏈式代數(shù)式)
result1 = (df1 < df2) & (df2 <= df3) & (df3 != df4)
result2 = pd.eval('df1 < df2 <= df3 != df4')
np.allclose(result1, result2)
# 位運算符
result1 = (df1 < 0.5) & (df2 < 0.5) | (df3 < df4)
result2 = pd.eval('(df1 < 0.5) & (df2 < 0.5) | (df3 < df4)')
np.allclose(result1, result2)
# 布爾類型的代數(shù)式中也可以使用 and 和 or
result3 = pd.eval('(df1 < 0.5) and (df2 < 0.5) or (df3 < df4)')
np.allclose(result1, result3)
# 對象屬性:obj.attr,對象索引:obj[index]
result1 = df2.T[0] + df3.iloc[1]
result2 = pd.eval('df2.T[0] + df3.iloc[1]')
np.allclose(result1, result2)
# 目前 eval 還不支持函數(shù)調(diào)用、條件語句、循環(huán)以及更復雜的運算
# 如果想要進行這些運算,可以借助 Numexpr 來實現(xiàn)
# 列間運算
df = pd.DataFrame(rng.rand(1000, 3), columns=['A', 'B', 'C'])
# pd.eval:通過代數(shù)式計算這三列
result1 = (df['A'] + df['B']) / (df['C'] - 1)
result2 = pd.eval("(df.A + df.B) / (df.C - 1)")
np.allclose(result1, result2)
# DataFrame.eval:通過列名稱實現(xiàn)簡潔的代數(shù)式
result3 = df.eval('(A + B) / (C - 1)')
np.allclose(result1, result3)
# 新增列
df.eval('D = (A + B) / C', inplace=True)
# 修改列
df.eval('D = (A - B) / C', inplace=True)
# 局部變量(這是一個變量名稱而不是一個列名稱)
# 可以靈活地使用兩個命名空間的資源
column_mean = df.mean(1)
result1 = df['A'] + column_mean
result2 = df.eval('A + @column_mean')
np.allclose(result1, result2)
# 3、過濾運算:query
result2 = df.query('A < 0.5 and B < 0.5')
np.allclose(result1, result2)
# 4、方法選擇:時間消耗和內(nèi)存消耗
# 普通方法:數(shù)組較小時速度更快
# eval 和 query 方法:節(jié)省內(nèi)存,語法更加簡潔
x = df[(df.A < 0.5) & (df.B < 0.5)]
tmp1 = df.A < 0.5
tmp2 = df.B < 0.5
tmp3 = tmp1 & tmp2
x = df[tmp3] # 二者等價
# 查看變量的內(nèi)存消耗
df.values.nbytes
參考資料
《利用 Python 進行數(shù)據(jù)分析》