Python - Pandas用法說明

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:分割、應用和組合
3-1
分組(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
參考資料

Pandas 在線文檔

《利用 Python 進行數(shù)據(jù)分析》

Stack Overflow 網(wǎng)站的 Pandas 話題

PyVideo 上關于 Pandas 的教學視頻

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容