Python - Pandas用法说明
NumPy 和它的 ndarray 对象,为 Python 多维数组提供了高效的存储和处理方法。Pandas 是在 NumPy 基础上建立的新程序库,提供了一种高效的 DataFrame 数据结构。DataFrame 本质上是一种带行标签和列标签、支持相同类型数据和缺失值的多维数组。建立在 NumPy 数组结构上的 Pandas,尤其是它的 Series 和 DataFrame 对象,为数据科学家们处理那些消耗大量时间的“数据清理”(data munging)任务提供了捷径。
Pandas 对象简介
如果从底层视角观察 Pandas 对象,可以把它们看成增强版的 NumPy 结构化数组,行列都不再只是简单的整数索引,还可以带上标签。Pandas 的三个基本数据结构:Series、DataFrame 和 Index。
import numpy as np
import pandas as pd
# Series 对象:带索引数据构成的一维数组
# 与 NumPy 数组间的本质差异其实是索引:
# NumPy 数组通过隐式定义的整数索引获取数值
# Pandas 的 Series 对象用一种显式定义的索引与数值关联,索引不仅可以是数字,还可以是字符串等其他类型
1、Serise 是通用的 NumPy 数组
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 的类数组对象 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 字典创建 Series 对象,默认索引为第一列
population['California'] # 获取值
population['California':'Illinois'] # 还支持数组形式的操作,比如切片
# data 可以是列表或 NumPy 数组,这时 index 默认值为整数序列
pd.Series([2, 4, 6])
# data 也可以是一个标量,创建 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 对象:既有灵活的行索引,又有灵活列名的二维数组
states.index
# Index(['California', 'Florida', 'Illinois', 'New York', 'Texas'], dtype='object')
states.columns
# Index(['area', 'population'], dtype='object')
states['area']
# 每个州的面积
# 1、通过单个 Series 对象创建 DataFrame
pd.DataFrame(population, columns=['population'])
# 2、通过字典列表创建 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 对象字典创建 DataFrame
area_dict = {'California': 423967, 'Texas': 695662, 'New York': 141297, 'Florida': 170312, 'Illinois': 149995}
area = pd.Series(area_dict)
# 结合上面的 population 和 area 两个 Series,创建 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 二维数组创建 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 结构化数组创建 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 对象:不可变数组或有序集合
ind = pd.Index([2, 3, 5, 7, 11])
# Int64Index([2, 3, 5, 7, 11], dtype='int64')
# 1、将Index看作不可变数组:像数组一样进行操作,但是不能修改它的值
ind[1]
ind[::2]
print(ind.size, ind.shape, ind.ndim, ind.dtype)
# 2、将Index看作有序集合:对集合进行并、交、差
# 这些操作还可以通过调用对象方法来实现,例如 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')
数据取值与选择
# 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 一维数组
data['a':'c'] # 切片(使用索引名称):显式索引,结果包含最后一个索引
data[1:3] # 切片(使用索引位置):隐式索引,结果不包含最后一个索引,从0开始
data[1] # 取值:显式索引
data[(data > 0.3) & (data < 0.8)] # 掩码
data[['a', 'e']] # 列表索引
# 2、索引器:loc、iloc 和 ix(使用索引位置取值,取值范围均为左闭右开!)
# 由于整数索引很容易造成混淆,所以 Pandas 提供了一些索引器(indexer)属性来作为取值的方法
# 它们不是 Series 对象的函数方法,而是暴露切片接口的属性
# Python 代码的设计原则之一是“显式优于隐式”
# 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 属性:实现混合效果
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 看作二维数组:可以把许多数组操作方式用在 DataFrame 上
data.values # 按行查看数组数据
data.T # 行列转置
data.values[0] # 获取一行数据
data['area'] # 获取一列数据:向 DataFrame 传递单个列索引
data.iloc[:3, :2] # 3行2列
data.loc[:'Illinois', :'pop'] # 截至到指定行列名的数值
data.ix[:3, :'pop'] # 第0-2行,第1-pop列
data.loc[data.density > 100, ['pop', 'density']] # 组合使用掩码与列表索引
# 以上任何一种取值方法都可以用于调整数据,这一点和 NumPy 的常用方法是相同的
data.iloc[0, 2] = 90
# 3.3、其他取值方法
# 如果对单个标签取值就选择列,而对多个标签用切片就选择行
data['Florida':'Illinois'] # 指定的两行,所有列(缺省)
data[1:3] # 不用索引值,而直接用行数来实现,从0开始的第1-2行
data[data.density > 100] # 掩码操作直接对行进行过滤
Pandas 数值运算方法
NumPy 的基本能力之一是快速对每个元素进行运算,既包括基本算术运算(加、减、乘、除),也包括更复杂的运算(三角函数、指数函数和对数函数等)。Pandas 继承了 NumPy 的功能,其中通用函数是关键。
但是 Pandas 也实现了一些高效技巧:对于一元运算(像函数与三角函数),这些通用函数将在输出结果中保留索引和列标签;而对于二元运算(如加法和乘法),Pandas 在传递通用函数时会自动对齐索引进行计算。这就意味着,保存数据内容与组合不同来源的数据(两处在 NumPy 数组中都容易出错的地方)变成了 Pandas 的杀手锏。
# 1、通用函数:保留索引
# 因为 Pandas 是建立在 NumPy 基础之上的,所以 NumPy 的通用函数同样适用于 Pandas 的 Series 和 DataFrame 对象
rng = np.random.RandomState(42)
ser = pd.Series(rng.randint(0, 10, 4)) # 0-10之间任意取4个整数
# 0 6
# 1 3
# 2 7
# 3 4
# dtype: int64
np.exp(ser) # 对 Series 使用 Numpy 通用函数,结果是一个保留索引的 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 通用函数,结果是一个保留索引的 DataFrame
# 2、通用函数:索引对齐
# 当在两个 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
# 结果数组的索引是两个输入数组索引的并集,缺失位置的数据会用 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,可以设置参数自定义 A 或 B 缺失的数据
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 将二维数组压缩成一维数组)
fill = A.stack().mean()
A.add(B, fill_value=fill)
# 2.3、通用函数:DataFrame 与 Series 的运算
A = rng.randint(10, size=(3, 4))
# array([[3, 8, 2, 4],
# [2, 6, 4, 8],
# [6, 1, 3, 8]])
# 二维数组减自身的一行数据会按行计算,也就是用每一行的值减去第一行的对应值
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 参数设置
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相同的形状(行数),再进行运算
# 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 在运算时会一直保存这些数据内容
# 避免在处理数据类型有差异和 / 或维度不一致的 NumPy 数组时可能遇到的问题
Python运算符 | Pandas方法 |
---|---|
+ | add() |
- | sub()、subtract() |
* | mul()、multiply() |
/ | truediv()、div()、divide() |
// | floordiv() |
% | mod() |
** | pow() |
处理缺失值
缺失值主要有三种形式:null、NaN 或 NA。
# 1、None:Python 对象类型的缺失值(Python 单体对象)
# 不能作为任何 NumPy / Pandas 数组类型的缺失值,只能用于 'object' 数组类型(即由 Python 对象构成的数组)
# 在进行常见的快速操作时,这种类型比其他原生类型数组要消耗更多的资源
vals1 = np.array([1, None, 3, 4])
# array([1, None, 3, 4], dtype=object)
# 如果你对一个包含 None 的数组进行累计操作,通常会出现类型错误
vals1.sum()
# TypeError:在 Python 中没有定义整数与 None 之间的加法运算
# 2、NaN:数值类型的缺失值(特殊浮点数)
# NumPy 会为这个数组选择一个原生浮点类型,和 object 类型数组不同,这个数组会被编译成 C 代码从而实现快速操作
# NaN 会将与它接触过的数据同化,无论和 NaN 进行何种操作,最终结果都是 NaN
1 + np.nan
# NaN
# 不会抛出异常,但是并非有效的
vals2.sum(), vals2.min(), vals2.max()
# (nan, nan, nan)
# NumPy 也提供了一些特殊的累计函数,它们可以忽略缺失值的影响
np.nansum(vals2), np.nanmin(vals2), np.nanmax(vals2)
# (8.0, 1.0, 4.0)
# 3、Pandas 中 NaN 与 None 的差异
# 虽然 NaN 与 None 各有各的用处,但是 Pandas 把它们看成是可以等价交换的,在适当的时候会将两者进行替换
pd.Series([1, np.nan, 2, None])
# 0 1.0
# 1 NaN
# 2 2.0
# 3 NaN
# dtype: float64
# Pandas 会将没有标签值的数据类型自动转换为 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 发现缺失值
data = pd.Series([1, np.nan, 'hello', None])
data.isnull()
data.notnull()
# 0 False
# 1 True
# 2 False
# 3 True
# dtype: bool
# 布尔类型掩码数组可以直接作为 Series 或 DataFrame 的索引使用
data[data.notnull()]
# 0 1
# 2 hello
# dtype: object
# 3.2 剔除缺失值
# 剔除 Series 中的缺失值
data.dropna()
# 剔除 DataFrame 中的缺失值,默认会剔除任何包含缺失值的整行数据
df.dropna()
df.dropna(axis='columns')
df.dropna(axis=1) # 二者等价,剔除任何包含缺失值的整列数据
# 默认设置 how='any'(只要有缺失值就剔除整行或整列)
# 可以设置 how='all'(只会剔除全部是缺失值的行或列)
df.dropna(axis='columns', how='all')
# thresh 参数设置行或列中非缺失值的最小数量
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 类似,只是在填充时需要设置坐标轴参数 axis(axis=1填充列,axis=0填充行)
df.fillna(method='ffill', axis=1)
类型 | 缺失值转换规则 | NA标签值 |
---|---|---|
floating 浮点型 | 无变化 | np.nan |
object 对象类型 | 无变化 | None 或 np.nan |
integer 整数类型 | 强制转换为 float64 | np.nan |
boolean 布尔类型 | 强制转换为 object | None 或 np.nan |
需要注意的是,Pandas 中字符串类型的数据通常是用 object 类型存储的。
层级索引
到目前为止,我们接触的都是一维数据和二维数据,用 Pandas 的 Series 和 DataFrame 对象就可以存储。但我们也经常会遇到存储多维数据的需求,数据索引超过一两个键。因此,Pandas 提供了 Panel 和 Panel4D 对象解决三维数据与四维数据。
而在实践中,更直观的形式是通过层级索引(hierarchical indexing,也被称为多级索引,multi-indexing)配合多个有不同等级(level)的一级索引一起使用,这样就可以将高维数组转换成类似一维 Series 和二维DataFrame 对象的形式。
# 1、多级索引 Series
# 假设你想要分析美国各州在两个不同年份的数据
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 更优方法:Pandas 多级索引
# MultiIndex 里面有一个 levels 属性表示索引的等级,可以将州名和年份作为每个数据点的不同标签
# 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 年的全部数据,结果是单索引的数组
pop[:, 2010]
# California 37253956
# New York 19378102
# Texas 25145561
# 1.3、高维数据的多级索引:可以用一个带行列索引的简单 DataFrame 代替前面的多级索引
# 将一个多级索引的 Series 转化为普通索引的 DataFrame
# 相当于行转列,很方便
pop_df = pop.unstack()
# 2000 2010
# California 33871648 37253956
# New York 18976457 19378102
# Texas 20851820 25145561
# 相当于列转行,结果就是原来的 pop
pop_df.stack()
# 如果可以用含多级索引的一维 Series 数据表示二维数据,那么就可以用 Series 或 DataFrame 表示三维甚至更高维度的数据
# 多级索引每增加一级,就表示数据增加一维,利用这一特点就可以轻松表示任意维度的数据了
# 对于这种带有 MultiIndex 的对象,增加一列就像 DataFrame 的操作一样简单
# 增加一列 under18 显示 18 岁以下的人口
pop_df = pd.DataFrame({'total': pop,
'under18': [9267089, 9284094, 4687374, 4318033, 5906301, 6879014]})
# 通用函数和其他功能也同样适用于层级索引
# 计算上面数据中 18 岁以下的人口占总人口的比例
# f_u18 = pop_df['under18'] / pop_df['total']
# 2、创建多级索引
# 方法1:将 index 参数设置为至少二维的索引数组
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:显式创建多级索引:以下这四种创建方法等价,结果就是最后一种
# 在创建 Series 或 DataFrame 时,可以将这些对象作为 index 参数,或者通过 reindex 方法更新 Series 或 DataFrame 的索引
pd.MultiIndex.from_arrays([['a', 'a', 'b', 'b'], [1, 2, 1, 2]]) # 数组列表
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]])
# 多级索引命名:可以在创建 MultiIndex 时指定 names,也可以过后再创建
# 相当于表头
pop.index.names = ['state', 'year'] # pop 的表头为州名+年份
# 3、多级行列索引
# 如果想获取包含多种标签的数据,需要通过对多个维度(姓名、国家、城市等标签)的多次查询才能实现,使用多级行列索引会非常方便
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'])
# 模拟体检数据
data = np.round(np.random.randn(4, 6), 1)
data[:, ::2] *= 10
data += 37
# 创建 DataFrame(此处的索引使用笛卡尔积创建)
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数据)
health_data['Guido']
# 4、多级索引的取值与切片
# 4.1、Series 多级索引
pop['California', 2000] # 对索引值全部进行限制,获取单个值
pop['California'] # 局部取值,只取最高级的索引,未被选中的低层索引值会被保留
pop.loc['California':'New York'] # 局部切片,要求 MultiIndex 是按顺序排列的
pop[:, 2000] # 如果索引已经排序,可以用较低层级的索引取值,第一层级的索引用空切片
pop[pop > 22000000] # 通过布尔掩码选择数据
pop[['California', 'Texas']] # 通过列表索引选择数据
# 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 不是有序的索引,那么大多数切片操作都会失败
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 参数设置转换的索引层级(设置哪一列横着)
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等
# 可以设置参数 level 实现对数据子集的聚合操作(group by level的字段,列出其余所有字段)
data_mean = health_data.mean(level='year')
data_mean.mean(axis=1, level='type')
合并数据集: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
# 合并高维数据
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
# 逐行合并后出现重复索引:
# pd.concat([df1, df2], verify_integrity=True) # 索引重复报错
# pd.concat([df1, df2], ignore_index=True) # 重复的索引被替换成新索引
# pd.concat([df1, df2], keys=['x', 'y']) # 为数据源设置多级索引标签(df1加上x列,df2加上y列)
# 合并列名不同数据:默认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 方法:不直接更新原有对象的值,而是为合并后的数据创建一个新对象
# 不能被称之为一个非常高效的解决方案,因为每次合并都需要重新创建索引和数据缓存
# 如果需要进行多个 append 操作,建议创建一个 DataFrame 列表,用 concat 函数一次性解决所有合并任务
df1.append(df2) # 等价于 pd.concat([df1, df2])
合并数据集:合并与连接(类似数据库)
# 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、设置数据合并的键: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、通过合并索引来实现合并: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 关联
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 重复,加上后缀
案例:计算美国各州的人口密度排名
# 数据下载地址: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条数据
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') # 连接,确保数据没有丢失
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
# 检查一下数据是否有缺失
merged.isnull().any()
# 部分 population 是缺失值,仔细看看那些数据
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'
用两个数据集共同的 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)
# 现在2010年的数据准备好了
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 库下载行星数据
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 的累计函数
rng = np.random.RandomState(42)
ser = pd.Series(rng.rand(5))
ser.sum()
# DataFrame 的累计函数:默认对每列进行统计
df = pd.DataFrame({'A': rng.rand(5), 'B': rng.rand(5)})
df.mean() # 对A、B列分别计算均值
df.mean(axis='columns') # 设置axis参数,对每行计算均值
# 计算每一列的若干常用统计值:describe
# 包括每一列的count、mean、std、min、25%、50%、75%、max值
planets.dropna().describe()
指标 | 描述 |
---|---|
count() | 计数项 |
first()、last() | 第一项与最后一项 |
mean()、median() | 均值与中位数 |
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 对象:延迟计算(在没有应用累计函数之前不会计算)
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、调用方法:让任何不由 GroupBy 对象直接实现的方法直接应用到每一组
# 用 DataFrame 的 describe 方法进行累计,对每一组数据进行描述性统计
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
# 指定函数列表
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 字典指定不同列需要累计的函数
df.groupby('key').aggregate({'data1': 'min', 'data2': 'max'})
# data1 data2
# key
# A 0 5
# B 1 7
# C 2 9
# 2.2、过滤:filter(按照分组的属性丢弃若干数据)
def filter_func(x):
return x['data2'].std() > 4
# filter 函数会返回一个布尔值,表示每个组是否通过过滤
# key = 'A' 的数据中 data2 列的标准差不大于4,被丢弃了,只保留了原始 df 中 key = 'B' 和 key = 'C'的4行数据
df.groupby('key').filter(filter_func)
# 2.3、转换:transform(返回一个新的全量数据,其形状与原来的输入数据是一样的)
# 将每一组的样本数据减去各组的均值,实现数据标准化
df.groupby('key').transform(lambda x: x - x.mean())
# 2.4、应用:apply(在每个组上应用任意方法,输入分组数据的 DataFrame)
# 将第一列数据以第二列的和为基数进行标准化
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、将列表、数组、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 函数传入 groupby,函数映射到索引,然后新的分组输出
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、分组案例
# 获取不同方法和不同年份发现的行星数量
decade = 10 * (planets['year'] // 10)
decade = decade.astype(str) + 's'
decade.name = 'decade'
planets.groupby(['method', decade])['number'].sum().unstack().fillna(0)
数据透视表
数据透视表将每一列数据作为输入,输出将数据不断细分成多个维度累计信息的二维数据表。数据透视表更像是一种多维的 GroupBy 累计操作,分割与组合不是发生在一维索引上,而是在二维网格上(行列同时分组)。
# 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()
# 数据透视表,代码可读性更强,结果一样
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
# 多级数据透视表
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']) # 性别+年龄+仓位+票价
# 通过字典为不同的列指定不同的累计函数
titanic.pivot_table(index='sex', columns='class',
aggfunc={'survived':sum, 'fare':'mean'})
# 计算每一组的总数
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')
# 异常值处理:直接删除异常值 / 更稳定的 sigma 消除法(按照正态分布标准差划定范围)
quartiles = np.percentile(births['births'], [25, 50, 75])
mu = quartiles[1]
# 样本均值的稳定性估计,0.74 是指标准正态分布的分位数间距
sig = 0.74 * (quartiles[2] - quartiles[0])
# 用这个范围就可以将有效的生日数据筛选出来了
births = births.query('(births > @mu - 5 * @sig) & (births < @mu + 5 * @sig)')
# 将'day'列设置为整数。由于原数据含有缺失值null,因此是字符串
births['day'] = births['day'].astype(int)
# 从年月日创建一个日期索引
births.index = pd.to_datetime(10000 * births.year +
100 * births.month +
births.day, format='%Y%m%d')
births['dayofweek'] = births.index.dayofweek
# 用这个索引可以画出不同年代不同星期的日均出生数据
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');
# 各个年份平均每天的出生人数,可以按照月和日两个维度分别对数据进行分组
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);
向量化字符串操作
# 数组:向量化操作简化了语法,可以快速地对多个数组元素执行同样的操作
x = np.array([2, 3, 5, 7, 11, 13])
x * 2
# 字符串:由于 NumPy 并没有为字符串数组提供简单的接口,需要通过 for 循环来实现
data = ['peter', 'Paul', 'MARY', 'gUIDO']
[s.capitalize() for s in data] # 假如数据中出现了缺失值,就会引起异常
# Pandas 为包含字符串的 Series 和 Index 对象提供 str 属性
# 既可以满足向量化字符串操作的需求,又可以正确地处理缺失值
names = pd.Series(data)
names.str.capitalize() # 将所有的字符串转成大写,缺失值会被跳过
Pandas 的 str 方法借鉴 Python 字符串方法的内容:
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模块函数的对应关系:
方法 | 描述 |
---|---|
match() | 对每个元素调用 re.match(),返回布尔类型值 |
extract() | 对每个元素调用 re.match(),返回匹配的字符串组(groups) |
findall() | 对每个元素调用 re.findall() |
replace() | 用正则模式替换字符串 |
contains() | 对每个元素调用 re.search(),返回布尔类型值 |
count() | 计算符合正则模式的字符串的数量 |
split() | 等价于 str.split(),支持正则表达式 |
rsplit() | 等价于 str.rsplit(),支持正则表达式 |
其他Pandas字符串方法:
方法 | 描述 |
---|---|
get() | 获取元素索引位置上的值,索引从 0 开始 |
slice() | 对元素进行切片取值 |
slice_replace() | 对元素进行切片替换 |
cat() | 连接字符串(此功能比较复杂,建议阅读文档) |
repeat() | 重复元素 |
normalize() | 将字符串转换为 Unicode 规范形式 |
pad() | 在字符串的左边、右边或两边增加空格 |
wrap() | 将字符串按照指定的宽度换行 |
join() | 用分隔符连接 Series 的每个元素 |
get_dummies() | 按照分隔符提取每个元素的 dummy 变量,转换为独热(onehot)编码的 DataFrame |
monte = pd.Series(['Graham Chapman', 'John Cleese', 'Terry Gilliam',
'Eric Idle', 'Terry Jones', 'Michael Palin'])
# 提取元素前面的连续字母作为每个人的名字
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)
# 将指标变量(包含某种编码信息的数据集)分割成独热编码(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)。
案例:食谱数据库
# http://openrecipes.s3.amazonaws.com/recipeitems-latest.json.gz
# 读入 json 数据
try:
recipes = pd.read_json('recipeitems-latest.json')
except ValueError as e:
print("ValueError:", e)
# ValueError:原因好像是虽然文件中的每一行都是一个有效的 JSON 对象,但是全文却不是这样
# 检查文件数据格式
with open('recipeitems-latest.json') as f:
line = f.readline()
pd.read_json(line).shape
# 显然每一行都是一个有效的 JSON 对象
# 新建一个字符串,将所有行 JSON 对象连接起来,然后再读取数据
with open('recipeitems-latest.json', 'r') as f:
data = (line.strip() for line in f) # 提取每一行内容
data_json = "[{0}]".format(','.join(data)) # 将所有内容合并成一个列表
recipes = pd.read_json(data_json) # 用 JSON 形式读取数据
# 食材列表的长度分布,最长的有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()
# 简易的美食推荐系统:如果用户提供一些食材,系统就会推荐使用了所有食材的食谱
# 由于大量不规则(heterogeneity)数据的存在,这个任务变得十分复杂,例如并没有一个简单直接的办法可以从每一行数据中清理出一份干净的食材列表
# 因此我们在这里做简化处理:
# 首先提供一些常见食材列表,然后通过简单搜索判断这些食材是否在食谱中
# 为了简化任务,这里只列举常用的香料和调味料
spice_list = ['salt', 'pepper', 'oregano', 'sage', 'parsley',
'rosemary', 'tarragon', 'thyme', 'paprika', 'cumin']
# 通过一个布尔类型的 DataFrame 来判断食材是否出现在某个食谱中
import re
spice_df = pd.DataFrame(dict((spice, recipes.ingredients.str.contains(spice, re.IGNORECASE))
for spice in spice_list))
# 现在来找一份使用了欧芹(parsley)、辣椒粉(paprika)和龙蒿叶(tarragon)的食谱
selection = spice_df.query('parsley & paprika & tarragon')
# 看看究竟是哪些食谱,可以做推荐了
recipes.name[selection.index]
处理时间序列
日期与时间数据主要包含三类:
1、时间戳:表示某个具体的时间点(例如 2015 年 7 月 4 日上午 7 点)。
2、时间间隔与周期:表示开始时间点与结束时间点之间的时间长度,例如 2015 年(指的是 2015 年 1 月 1 日至 2015 年 12 月 31 日这段时间间隔)。周期通常是指一种特殊形式的时间间隔,每个间隔长度相同,彼此之间不会重叠(例如,以 24 小时为周期构成每一天)。
3、时间增量(time delta)或持续时间(duration)表示精确的时间长度(例如,某程序运行持续时间 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 时间序列的偏移量,具体内容可以在 pd.tseries.offsets 模块中找到。
# 1、原生Python的日期与时间工具:datetime 与 dateutil
from datetime import datetime
from dateutil import parser
# 创建一个日期
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/),解决了绝大多数时间序列数据都会遇到的难题:时区
# 2、时间类型数组:NumPy 的 datetime64 类型
# datetime64 类型将日期编码为 64 位整数,这样可以让日期数组非常紧凑(节省内存)
# 时区将自动设置为执行代码的操作系统的当地时区
# 需要在设置日期时确定具体的输入类型
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的日期与时间工具:理想与现实的最佳解决方案
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 类型,对应的索引数据结构是 DatetimeIndex
# 周期性数据:Period 类型,对应的索引数据结构是 PeriodIndex
# 时间增量:Timedelta 类型,对应的索引数据结构是 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、周期性数据
# to_period 方法可以将 DatetimeIndex 转换为 PeriodIndex,传入频率代码
dates.to_period('D') # 转换为单日时间序列
# 3.3、时间增量
# 用一个日期减去另一个日期时,返回的结果是 TimedeltaIndex 类型
dates - dates[0]
# 3.4、有规律的时间序列
# data_range 方法通过开始日期、结束日期和频率代码创建一个有规律的日期序列,默认的频率是天
pd.date_range('2015-07-03', '2015-07-10') # 指定开始日期和结束日期
pd.date_range('2015-07-03', periods=8) # 指定开始日期和周期数 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') # 以小时递增的序列
# 可以将频率组合起来创建的新的周期,比如 2 小时 30 分钟
pd.timedelta_range(0, periods=9, freq="2H30T")
# 创建工作日偏移序列
from pandas.tseries.offsets import BDay
pd.date_range('2015-07-01', periods=5, freq=BDay())
# 4、重新取样、迁移和窗口
# 导入金融数据
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
# 处理时间序列数据时,经常需要按照新的频率(更高频率、更低频率)对数据进行重新取样
# resample:基于数据累计
# asfreq:基于数据选择
# 用年末('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)
# 对工作日数据按天进行重新取样(即包含周末)
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(迁移数据)和 tshift(迁移索引)
fig, ax = plt.subplots(3, sharey=True)
goog = goog.asfreq('D', method='pad') # 对数据应用时间频率,向后填充解决缺失值
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');
# 常见使用场景:计算数据在不同时段的差异
# 用迁移后的值来计算 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、案例:美国西雅图自行车统计数据的可视化
# 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')
# 看看这三列的统计值
data.dropna().describe()
# 为原始数据画图
%matplotlib inline
import seaborn; seaborn.set()
data.plot()
plt.ylabel('Hourly Bicycle Count')
# 小时数太多了,重新取样,按周累计
weekly = data.resample('W').sum()
weekly.plot(style=[':', '--', '-'])
plt.ylabel('Weekly Bicycle Count')
# 计算数据的 30 日移动均值:
daily = data.resample('D').sum()
daily.rolling(30, center=True).mean().plot(style=[':', '--', '-'])
plt.ylabel('mean of 30 days count')
# 由于窗口太小,图形还不太平滑
# 可以用另一个移动均值的方法获得更平滑的图形,例如高斯分布时间窗口
# 设置窗口的宽度为 50 天和窗口内高斯平滑的宽度为 10 天
daily.rolling(50, center=True,
win_type='gaussian').sum(std=10).plot(style=[':', '--', '-'])
# 计算单日内的小时均值流量
# 小时均值流量呈现出明显的双峰分布特征,早间峰值在上午 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=[':', '--', '-']);
# 计算周内每天的变化
# 工作日与周末的自行车流量差十分显著,工作日通过的自行车差不多是周末的两倍
by_weekday = data.groupby(data.index.dayofweek).mean()
by_weekday.index = ['Mon', 'Tues', 'Wed', 'Thurs', 'Fri', 'Sat', 'Sun']
by_weekday.plot(style=[':', '--', '-'])
# 计算一周内工作日与周末每小时的自行车流量均值
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=[':', '--', '-'])
# 还可以继续挖掘天气、温度以及其他因素对人们通勤模式的影响
高性能 Pandas:eval 与 query(复合代数式)
# 1、Numexpr:可以在不为中间过程分配全部内存的前提下,完成元素到元素的复合代数式运算
# NumPy 与 Pandas 都支持快速的向量化运算
# 比如对下面两个数组进行求和,比普通的 Python 循环或列表综合要快很多
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))
# 但是这种运算在处理复合代数式问题时的效率比较低
mask = (x > 0.5) & (y < 0.5)
# 因为等价于以下操作,每段中间过程都需要显式地分配内存
# 如果 x 数组和 y 数组非常大,这么运算就会占用大量的时间和内存消耗
tmp1 = (x > 0.5)
tmp2 = (y < 0.5)
mask = tmp1 & tmp2
# Numexpr 在计算代数式时不需要为临时数组分配全部内存,因此计算比 NumPy 更高效,尤其适合处理大型数组
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 方法和字符串代数式求和,更高效
pd.eval('df1 + df2 + df3 + df4')
# 效率更高,内存消耗更少
np.allclose(df1 + df2 + df3 + df4, pd.eval('df1 + df2 + df3 + df4'))
# 算数运算符
result1 = -df1 * df2 / (df3 + df4) - df5
# 比较运算符(包括链式代数式)
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)
# 布尔类型的代数式中也可以使用 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 还不支持函数调用、条件语句、循环以及更复杂的运算
# 如果想要进行这些运算,可以借助 Numexpr 来实现
# 列间运算
df = pd.DataFrame(rng.rand(1000, 3), columns=['A', 'B', 'C'])
# pd.eval:通过代数式计算这三列
result1 = (df['A'] + df['B']) / (df['C'] - 1)
result2 = pd.eval("(df.A + df.B) / (df.C - 1)")
np.allclose(result1, result2)
# DataFrame.eval:通过列名称实现简洁的代数式
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、方法选择:时间消耗和内存消耗
# 普通方法:数组较小时速度更快
# eval 和 query 方法:节省内存,语法更加简洁
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] # 二者等价
# 查看变量的内存消耗
df.values.nbytes
参考资料
《利用 Python 进行数据分析》