Pandas基础知识

从文件中读取数据,并使用 Pandas 的基本数据结构(Series 和 DataFrame)进行数据简单分析和处理,最后将新的数据结果保存到文件里,这就是基本的 Pandas 操作。

1
import pandas as pd

读取数据

最常见的文件主要是csv、excel 和 txt,所以我们重点介绍这三种文件的读取函数:

1
2
3
df_table = pd.read_table('./data/my_table.txt', header=None, sep='\|', engine='python')
df_csv = pd.read_csv('./data/my_csv.csv', index_col=['col1', 'col2'], parse_dates=['col5'])
df_excel = pd.read_excel('./data/my_excel.xlsx', nrows=20)

这里有一些公共参数,header=None 表示第一行不是表头,index_col 表示把指定的列按级作为索引,parse_dates 表示将指定列解析为时间,nrows 表示读取多少行数据。对于读取 txt 文件,可能会有比较灵活的分隔符,我们可以通过 sep 参数来指定,对于与正则冲突的特殊符号需要进行转义,并指明引擎为 python。

数据写入

写入数据是通过对内置数据结构直接调用 to_csv 等方法,通常对于没有意义的(只是用来分析数据用的)索引我们会将 index 参数设置为 False,将其在保存时忽略掉。

1
2
df_csv.to_csv('data/my_csv_saved.csv', index=False)
df_excel.to_excel('data/my_excel_saved.xlsx', index=False)

如果想要把表格快速转换为 markdown 和 latex 语言,可以使用 to_markdown 和 to_latex 函数,此处需要安装 tabulate 包。

基本数据结构

Pandas 具有两种基本的数据结构:一维的 Series 和 二维的 DataFrame。

  • Series 一般由四个部分组成,分别是序列的值 data 、索引 index 、存储类型 dtype 、序列的名字 name 。其中,索引也可以指定它的名字,默认为空
    1
    2
    3
    4
    s = pd.Series(data = [100, 'a', {'dic1':5}],
    index = pd.Index(['id1', 20, 'third'], name='my_idx'),
    dtype = 'object',
    name = 'my_name')
  • DataFrame 在 Series 的基础上增加了列索引 columns,一个 DataFrame 由二维的 data 与行列索引来构造
    1
    2
    3
    df = pd.DataFrame(data = {'col_0': [1,2,3], 'col_1':list('abc'),
    'col_2': [1.2, 2.2, 3.2]},
    index = ['row_%d'%i for i in range(3)])

上述的这些属性,可以通过 . 的方式来获取:

1
2
3
4
5
6
7
8
print(s.values)
print(s.index)
print(df.shape)
print(df.columns)

print(s['third'])
print(df['col_0']) # 单列则是一个Series对象
sub_df = df[['col_0', 'col_1']]

另外,Series 可以使用索引取出对应的元素值。DataFrame 可以使用列索引取出相应的一个或多个列组成的表。

基本函数

我们读取一份包含了四所学校学生的体侧信息的数据,这些数据包括学校、年级、姓名、性别、身高、体重、是否为转系生、体测场次、测试时间、1000 米成绩等信息,以此作为数据来讲解 DataFrame 的基础操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
df = pd.read_csv('./data/learn_pandas.csv')
print(df.head(2)) # 输出表格的前两行
print(df.tail(3)) # 输出表格的倒数三行
df = df[df.columns[:7]] # 取出表格数据的前七列进行分析
print(df.info()) # 输出表格数据的信息概况(都有哪些列,每列有多少非空数据,数据类型是什么等)
print(df.describe()) # 输出表格数据中浮点数类型列的统计信息(均值、方差、最值、分位数等)

# 我们可以显示调用一些统计函数:sum、mean、median、var、std、max、min等
df_demo = df[['Height', 'Weight']]
print(df_demo.mean()) # 计算均值
print(df_demo.median()) # 计算中位数
print(df_demo.quantile(0.75)) # 计算75%分位数
print(df_demo.count()) # 计算非缺失值的个数
print(df_demo.idxmax()) # 最大值对应的索引
# 上面这些所有的函数,由于操作后返回的是标量,所以又称为聚合函数,它们有一个公共参数axis,
# 默认为 0 代表逐列聚合,如果设置为 1 则表示逐行聚合

print(df['School'].unique()) # 得到去重后的“学校”array
print(df['School'].nunique()) # 得到去重后“学校”的个数
print(df['School'].value_counts()) # 各“学校”对应的频数

# 查看多列组合的唯一值
df_demo = df[['Gender','Transfer','Name']]
print(df_demo.drop_duplicates(['Gender', 'Transfer'])) # keep参数:默认first保留重复结果第一次出现的行,可选last/False(所有重复都剔除)
print(df_demo.duplicated(['Gender', 'Transfer'])) # 输出是否重复元素的布尔序列

# 对某列中的元素进行替换操作
print(df['Gender'].replace({'Female': 0, 'Male': 1})) # 通过字典映射替换
# replace 还支持方向替换,指定method参数为ffill则为用前面一个最近的未被替换的值进行
# 替换,bfill则使用后面最近的未被替换的值进行替换
s = pd.Series(['a', 1, 'b', 2, 1, 1, 'a'])
print(s.replace([1, 2], method='ffill')) # 1->a, 2-> b, 1->b, 1->b
# replace 还支持逻辑替换,包括了where和mask,
# 这两个函数是完全对称的:where函数在传入条件为False的对应行进行替换,
# 而mask在传入条件为True的对应行进行替换,当不指定替换值时,替换为缺失值。
s = pd.Series([-1, 1.2345, 100, -50])
print(s.where(s<0)) # 1.2345->NaN, 100->NaN
print(s.mask(s<0, 0)) # -1->0, -50->0
# 数值替换包含了round, abs, clip方法,它们分别表示取整、取绝对值和截断
print(s.clip(0, 2)) # 0和2指定了上下截断边界:-1->0, 100->2, -50->0

# 排序共有两种方式:值排序sort_values和索引排序sort_index
df_demo = df[['Grade', 'Name', 'Height','Weight']].set_index(['Grade','Name'])
print(df_demo.sort_values('Height')) # 默认是升序排序
print(df_demo.sort_values(['Weight','Height'],ascending=[True,False])) # 体重升序,体重相同时升高降序排
# 索引排序需要指定索引level
print(df_demo.sort_index(level=['Grade','Name'],ascending=[True,False]))

# apply方法常用于DataFrame的行或列的迭代,通过参数axis指定行或列
df_demo = df[['Height', 'Weight']]
def my_mean(x):
res = x.mean()
return res
print(df_demo.apply(my_mean)) # 默认对列进行迭代(求平均)
# 求偏离该序列均值的绝对值大小的均值,pandas内置了mad函数实现了同样功能,内置函数要比使用apply实现速度更高效
print(df_demo.apply(lambda x:(x-x.mean()).abs().mean()))

简单总结一下,Pandas 中对于基本数据结构的内置函数包括了汇总、特征统计、唯一值、替换、排序,以及提供了 apply 函数迭代调用自定义的操作。

窗口对象

Pandas 中有 3 类窗口,分别是滑动窗口 rolling 、扩张窗口 expanding 以及指数加权窗口 ewm。得到窗口对象后,可以在指定窗口大小的数据上进行计算操作。

我们首先重点介绍一下滑动窗口,对一个序列使用 .rolling( ) 函数即可得到滑窗对象,其最重要的参数为窗口大小window。在得到了滑窗对象后,能够使用相应的聚合函数进行计算,计算则以窗口包含的数据为作用域。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
s = pd.Series([1, 2, 3, 4, 5])
roller = s.rolling(window=3)
print(roller.mean()) # 第一、二位置上都是NaN,第三个位置是(1+2+3)/3=2
print(roller.apply(lambda x:x.mean())) # 也可以使用apply函数

'''
输出:
0 NaN
1 NaN
2 2.0
3 3.0
4 4.0
dtype: float64
'''

shift(平移), diff(后减前的差), pct_change(后减前再除以前==“每日涨跌幅”) 是一组类滑窗函数,它们的公共参数为 periods=n ,默认为 1,分别表示取向前第 n 个元素的值、与向前第 n 个元素做差(与 Numpy 中不同,后者表示 n 阶差分)、与向前第 n 个元素相比计算增长率。这里的 n 可以为负,表示反方向的类似操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
s = pd.Series([1, 3, 6, 10, 15])
print(s.shift(-2))
print(s.diff(3))
print(s.pct_change())

'''
-----------------------------------------------
|shift输出: |diff输出: |pct_change输出: |
-----------------------------------------------
|0 6.0 |0 NaN |0 NaN |
|1 10.0 |1 NaN |1 2.000000 |
|2 15.0 |2 NaN |2 1.000000 |
|3 NaN |3 9.0 |3 0.666667 |
|4 NaN |4 12.0 |4 0.500000 |
|dtype: float64|dtype: float64|dtype: float64 |
-----------------------------------------------
'''

之所以将其视作类滑窗函数的原因是,它们的功能可以用窗口大小为 n+1 的 rolling 方法等价代替:

1
2
3
4
5
6
s.rolling(3).apply(lambda x:list(x)[0]) # s.shift(2)

def my_pct(x):
L = list(x)
return L[-1]/L[0]-1
s.rolling(2).apply(my_pct) # s.pct_change()

扩张窗口又称累计窗口,可以理解为一个动态长度的窗口,其窗口的大小就是从序列开始处到具体操作的对应位置,其使用的聚合函数会作用于这些逐步扩张的窗口上。具体地说,设序列为 a1, a2, a3, a4,则其每个位置对应的窗口即 [a1]、[a1, a2]、[a1, a2, a3]、[a1, a2, a3, a4]。在上述两种窗口对象分组中的所有数值,计算的权重都是一样的,指数加权就是我们对分组中的数据给予不同的权重用于后边的计算中。

1
2
3
4
5
6
7
s = pd.Series([1, 3, 6, 10])
# min_periods参数扩张到最少包含3个元素时才开始计算
print(s.expanding(min_periods=3).mean())

# 指数加权滑动
ewm(com=None, span=None, halflife=None, alpha=None, min_periods=0,\
adjust=True, ignore_na=False, axis=0)

我们简单解释一下指数加权滑动函数中的几个参数,指数加权滑动的移动窗口计算公式为:

其中$\alpha$是平滑因子;如果根据跨度进行衰减,则$\alpha=\frac{2}{span+1}$。如果根据质心指定衰减,则$\alpha=\frac{2}{com+1}$。如果根据半衰期进行衰减,则$\alpha=1-\exp\frac{log(0.5)}{halflife}$。

内置对象

str对象

当一个 Pandas 序列 s 是字符串时,可以通过 s.str 获取其内置的字符串对象,Pandas 为 str 对象提供了许多操作函数,其中大量操作与 Python 本身的 str 对象的方法具有相同的用法,包括正则表达式。调用 s.str 对象的方法可以直接在整个序列上完成对字符串的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
s = pd.Series(['a', 'bc', 'def'])
print(s.str.len())
'''
0 1
1 2
2 3
dtype: int64
'''

print(s.str.match('a|d'))
'''
0 True
1 False
2 True
dtype: bool
'''

当然,Pandas 还提供了许多 Python str 对象不具备的功能函数,如 s.str.extract() 用以提取满足特定正则的匹配结果等。

cat对象

在 Pandas 中提供了 category 类型,使用户能够处理分类类型的变量,将一个普通序列转换成分类变量可以使用 astype 方法。在一个分类类型的 Series 中定义了 cat 对象,对于一个具体的分类,有两个组成部分,其一为类别的本身,它以 Index 类型存储,其二为是否有序,它们都可以通过 cat 的属性被访问。

1
2
3
4
5
6
7
8
9
10
11
df = pd.read_csv('data/learn_pandas.csv',
usecols = ['Grade', 'Name', 'Gender', 'Height', 'Weight'])
s = df.Grade.astype('category')
print(s.cat.categories)
'''
Index(['Freshman', 'Junior', 'Senior', 'Sophomore'], dtype='object')
'''
s.cat.ordered # False

s = s.cat.add_categories('Graduate') # 增加一个毕业生类别
# 删除某一个类别使用 remove_categories,使用 set_categories 直接设置序列的新类别

常用的 cat 对象操作包括对分类类型的增删改查,以及构建顺序,有序类别和无序类别可以通过 as_unordered 和 reorder_categories 互相转化。

时序对象

当 Pandas 的某一列数据是时间格式串,该列就可以转换为时序对象,时序对象是一个复杂的概念,其中包括了时间戳、时间差、时间段、时间偏置等概念。对应的 Pandas 内置对象也有多种(s.dt 对象、pd.offsets.*中的 offset 对象等),我们在后面的《Pandas时序数据》中详细展开介绍。