3.Pandas概述
Pandas概述
Pandas(Python Data Analysis Library )是基于NumPy 的一种工具,该工具是为了解决数据分析任务而创建的。Pandas 纳入了大量库和一些标准的数据模型,提供了高效地操作大型数据集所需的工具。Pandas提供了大量能使我们快速便捷地处理数据的函数和方法。你很快就会发现,它是使Python成为强大而高效的数据分析环境的重要因素之一。
Pandas是Python的一个数据分析包,最初由AQR Capital Management于2008年4月开发,并于2009年底开源出来,目前由专注于Python数据包开发的PyData开发team继续开发和维护,属于PyData项目的一部分。Pandas最初被作为金融数据分析工具而开发出来。
Pandas含有使数据分析工作变得更快更简单的高级数据结构和操作工具。pandas是基于Numpy构建的,让以Numpy为中心的应用变得更简单。
Pandas专用于数据预处理和数据分析的Python第三方库,最适合处理大型结构化表格数据
- Pandas是2008年Wes McKinney于AQR资本做量化分析师时创建
- Pandas借鉴了R的数据结构
- Pandas基于Numpy搭建,支持Numpy中定义的大部分计算
- Pandas含有使数据分析工作更简单高效的高级数据结构和操作工具
- Pandas底层用Cython和C做了速度优化,极大提高了执行效率
Pandas 有很多高级的功能,但是想要掌握高级功能前,需要先掌握它的基础知识,Pandas 中的数据结构算是非常基础的知识之一了。
Pandas 常用的数据结构有两种:Series 和 DataFrame。这些数据结构构建在 Numpy 数组之上,这意味着它们效率很高。我们来分别看看这些数据结构都长什么样子吧。
# 导入相关库
import numpy as np
import pandas as pd
Series
简介
Series 是一个带有 名称 和索引的一维数组,既然是数组,肯定要说到的就是数组中的元素类型,在 Series 中包含的数据类型可以是整数、浮点、字符串、Python对象等。
假定有一个场景是:存储一些用户的信息,暂时只包括年龄信息。
我们可以通过 Series 来存储,这里我们通过 Series 存储了四个年龄:18/30/25/40,只需将要存储的数据构建成一个数组,然后赋值给data参数即可。
# 存储了 4 个年龄:18/30/25/40
user_age = pd.Series(data=[18, 30, 25, 40])
user_age
0 18
1 30
2 25
3 40
dtype: int64
可以看到,已经正确将多个年龄存储到 Series 中了,你可能会想,单独存储了年龄有什么用,我怎么知道这个年龄属于哪个用户呢?
我们可以通过 Series 的 index(索引)来解决这个问题。由于有四个年龄,自然地也需要四个姓名,所以我们需要构建一个与 data 长度相同的数组,然后通过下面的操作即可满足要求。
user_age.index = ["Tom", "Bob", "Mary", "James"]
user_age
Tom 18
Bob 30
Mary 25
James 40
dtype: int64
你看,现在姓名与年龄已经完全对应上了。虽然说我们自己知道 Tom/Bob 这些是姓名,但是别人不知道啊,我们怎么告诉他人呢?
要想让别人知道,我们可以为 index 起个名字。
user_age.index.name = "name"
user_age
name
Tom 18
Bob 30
Mary 25
James 40
dtype: int64
可能你还会想,如果别人在看我写的代码,怎么能快速的知道我这写的到底是什么玩意呢?
别急,就像我们给index起名字一样,我们也可以给 Series 起个名字。
user_age.name="user_age_info"
user_age
name
Tom 18
Bob 30
Mary 25
James 40
Name: user_age_info, dtype: int64
通过上面一系列的操作,我们对 Series 的结构上有了基本的了解,简单来说,一个 Series 包括了 data、index 以及 name。
上面的操作非常方便做演示来使用,如果想要快速实现上面的功能,可以通过以下方式来实现。
# 构建索引
name = pd.Index(["Tom", "Bob", "Mary", "James"], name="name")
# 构建 Series
user_age = pd.Series(data=[18, 30, 25, 40], index=name, name="user_age_info")
user_age
name
Tom 18
Bob 30
Mary 25
James 40
Name: user_age_info, dtype: int64
另外,需要说明的是我们在构造 Series 的时候,并没有设定每个元素的数据类型,这个时候,Pandas 会自动判断一个数据类型,并作为 Series 的类型。
当然了,我们也可以自己手动指定数据类型。
# 指定类型为浮点型
user_age = pd.Series(data=[18, 30, 25, 40], index=name, name="user_age_info", dtype=float)
user_age
name
Tom 18.0
Bob 30.0
Mary 25.0
James 40.0
Name: user_age_info, dtype: float64
Series 像什么
Series 包含了 dict 的特点,也就意味着可以使用与 dict 类似的一些操作。我们可以将 index 中的元素看成是 dict 中的 key。
# 获取 Tom 的年龄
user_age["Tom"]
18.0
此外,可以通过 get 方法来获取。通过这种方式的好处是当索引不存在时,不会抛出异常。
user_age.get("Tom")
18.0
Series 除了像 dict 外,也非常像 ndarray,这也就意味着可以采用切片操作。
# 获取第一个元素
user_age[0]
18.0
# 获取前三个元素
user_age[:3]
name
Tom 18.0
Bob 30.0
Mary 25.0
Name: user_age_info, dtype: float64
# 获取年龄大于30的元素
user_age[user_age > 30]
name
James 40.0
Name: user_age_info, dtype: float64
# 获取第4个和第二个元素
user_age[[3, 1]]
name
James 40.0
Bob 30.0
Name: user_age_info, dtype: float64
可以看到,无论我们通过切片如何操作 Series ,它都能够自动对齐 index。
Series 的向量化操作
Series 与 ndarray 一样,也是支持向量化操作的。同时也可以传递给大多数期望 ndarray 的 NumPy 方法。
user_age + 1
name
Tom 19.0
Bob 31.0
Mary 26.0
James 41.0
Name: user_age_info, dtype: float64
np.exp(user_age)
name
Tom 6.565997e+07
Bob 1.068647e+13
Mary 7.200490e+10
James 2.353853e+17
Name: user_age_info, dtype: float64
DataFrame
DataFrame 是一个带有索引的二维数据结构,每列可以有自己的名字,并且可以有不同的数据类型。你可以把它想象成一个 excel 表格或者数据库中的一张表,DataFrame 是最常用的 Pandas 对象。
我们继续使用之前的实例来讲解 DataFrame,在存储用户信息时,除了年龄之外,我还想存储用户所在的城市。如何通过 DataFrame 实现呢?
可以构建一个 dict,key 是需要存储的信息,value 是信息列表。然后将 dict 传递给 data 参数。
index = pd.Index(data=["Tom", "Bob", "Mary", "James"], name="name")
data = {
"age": [18, 30, 25, 40],
"city": ["BeiJing", "ShangHai", "GuangZhou", "ShenZhen"]
}
user_info = pd.DataFrame(data=data, index=index)
user_info
age | city | |
---|---|---|
name | ||
Tom | 18 | BeiJing |
Bob | 30 | ShangHai |
Mary | 25 | GuangZhou |
James | 40 | ShenZhen |
可以看到,我们成功构建了一个 DataFrame,这个 DataFrame 的索引是用户性别,还有两列分别是用户的年龄和城市信息。
除了上面这种传入 dict 的方式构建外,我们还可以通过另外一种方式来构建。这种方式是先构建一个二维数组,然后再生成一个列名称列表。
data = [[18, "BeiJing"],
[30, "ShangHai"],
[25, "GuangZhou"],
[40, "ShenZhen"]]
columns = ["age", "city"]
user_info = pd.DataFrame(data=data, index=index, columns=columns)
user_info
age | city | |
---|---|---|
name | ||
Tom | 18 | BeiJing |
Bob | 30 | ShangHai |
Mary | 25 | GuangZhou |
James | 40 | ShenZhen |
访问行
在生成了 DataFrame 之后,可以看到,每一行就表示某一个用户的信息,假如我想要访问 Tom 的信息,我该如何操作呢?
一种办法是通过索引名来访问某行,这种办法需要借助 loc 方法。
user_info.loc["Tom"]
age 18
city BeiJing
Name: Tom, dtype: object
除了直接通过索引名来访问某一行数据之外,还可以通过这行所在的位置来选择这一行。
user_info.iloc[0]
age 18
city BeiJing
Name: Tom, dtype: object
现在能够访问某一个用户的信息了,那么我如何访问多个用户的信息呢?也就是如何访问多行呢?
借助行切片可以轻松完成,来看这里。
user_info.iloc[1:3]
age | city | |
---|---|---|
name | ||
Bob | 30 | ShangHai |
Mary | 25 | GuangZhou |
访问列
学会了如何访问行数据之外,自然而然会想到如何访问列。我们可以通过属性(“.列名”)的方式来访问该列的数据,也可以通过[column]的形式来访问该列的数据。
假如我想获取所有用户的年龄,那么可以这样操作。
user_info.age
name
Tom 18
Bob 30
Mary 25
James 40
Name: age, dtype: int64
user_info["age"]
name
Tom 18
Bob 30
Mary 25
James 40
Name: age, dtype: int64
如果想要同时获取年龄和城市该如何操作呢?
# 可以变换列的顺序
user_info[["city", "age"]]
city | age | |
---|---|---|
name | ||
Tom | BeiJing | 18 |
Bob | ShangHai | 30 |
Mary | GuangZhou | 25 |
James | ShenZhen | 40 |
新增/删除列
在生成了 DataFrame 之后,突然你发现好像缺失了用户的性别这个信息,那么如何添加呢?
如果所有的性别都一样,我们可以通过传入一个标量,Pandas 会自动帮我们广播来填充所有的位置。
user_info["sex"] = "male"
user_info
age | city | sex | |
---|---|---|---|
name | |||
Tom | 18 | BeiJing | male |
Bob | 30 | ShangHai | male |
Mary | 25 | GuangZhou | male |
James | 40 | ShenZhen | male |
如果想要删除某一列,可以使用 pop 方法来完成。
user_info.pop("sex")
user_info
age | city | |
---|---|---|
name | ||
Tom | 18 | BeiJing |
Bob | 30 | ShangHai |
Mary | 25 | GuangZhou |
James | 40 | ShenZhen |
如果用户的性别不一致的时候,我们可以通过传入一个 like-list 来添加新的一列。
user_info["sex"] = ["male", "male", "female", "male"]
user_info
age | city | sex | |
---|---|---|---|
name | |||
Tom | 18 | BeiJing | male |
Bob | 30 | ShangHai | male |
Mary | 25 | GuangZhou | female |
James | 40 | ShenZhen | male |
通过上面的例子可以看出,我们创建新的列的时候都是在原有的 DataFrame 上修改的,也就是说如果添加了新的一列之后,原有的 DataFrame 会发生改变。
如果想要保证原有的 DataFrame 不改变的话,我们可以通过 assign 方法来创建新的一列。
user_info.assign(age_add_one = user_info["age"] + 1)
age | city | sex | age_add_one | |
---|---|---|---|---|
name | ||||
Tom | 18 | BeiJing | male | 19 |
Bob | 30 | ShangHai | male | 31 |
Mary | 25 | GuangZhou | female | 26 |
James | 40 | ShenZhen | male | 41 |
user_info.assign(sex_code = np.where(user_info["sex"] == "male", 1, 0))
age | city | sex | sex_code | |
---|---|---|---|---|
name | ||||
Tom | 18 | BeiJing | male | 1 |
Bob | 30 | ShangHai | male | 1 |
Mary | 25 | GuangZhou | female | 0 |
James | 40 | ShenZhen | male | 1 |
4.Pandas常用基本功能
# 导入相关库
import numpy as np
import pandas as pd
常用的基本功能
当我们构建好了 Series 和 DataFrame 之后,我们会经常使用哪些功能呢?来跟我看看吧。引用上一章节中的场景,我们有一些用户的的信息,并将它们存储到了 DataFrame 中。
因为大多数情况下 DataFrame 比 Series 更为常用,所以这里以 DataFrame 举例说明,但实际上很多常用功能对于 Series 也适用。
index = pd.Index(data=["Tom", "Bob", "Mary", "James"], name="name")
data = {
"age": [18, 30, 25, 40],
"city": ["BeiJing", "ShangHai", "GuangZhou", "ShenZhen"],
"sex": ["male", "male", "female", "male"]
}
user_info = pd.DataFrame(data=data, index=index)
user_info
age | city | sex | |
---|---|---|---|
name | |||
Tom | 18 | BeiJing | male |
Bob | 30 | ShangHai | male |
Mary | 25 | GuangZhou | female |
James | 40 | ShenZhen | male |
一般拿到数据,我们第一步需要做的是了解下数据的整体情况,可以使用 info
方法来查看。
user_info.info()
Index: 4 entries, Tom to James
Data columns (total 3 columns):
age 4 non-null int64
city 4 non-null object
sex 4 non-null object
dtypes: int64(1), object(2)
memory usage: 128.0+ bytes
如果我们的数据量非常大,我想看看数据长啥样,我当然不希望查看所有的数据了,这时候我们可以采用只看头部的 n 条或者尾部的 n 条。查看头部的 n 条数据可以使用 head
方法,查看尾部的 n 条数据可以使用 tail
方法。
user_info.head(2)
age | city | sex | |
---|---|---|---|
name | |||
Tom | 18 | BeiJing | male |
Bob | 30 | ShangHai | male |
此外,Pandas 中的数据结构都有 ndarray 中的常用方法和属性,如通过 .shape
获取数据的形状,通过 .T
获取数据的转置。
user_info.shape
(4, 3)
user_info.T
name | Tom | Bob | Mary | James |
---|---|---|---|---|
age | 18 | 30 | 25 | 40 |
city | BeiJing | ShangHai | GuangZhou | ShenZhen |
sex | male | male | female | male |
如果我们想要通过 DataFrame 来获取它包含的原有数据,可以通过 .values
来获取,获取后的数据类型其实是一个 ndarray。
user_info.values
array([[18, 'BeiJing', 'male'],
[30, 'ShangHai', 'male'],
[25, 'GuangZhou', 'female'],
[40, 'ShenZhen', 'male']], dtype=object)
描述与统计
有时候我们获取到数据之后,想要查看下数据的简单统计指标(最大值、最小值、平均值、中位数等),比如想要查看年龄的最大值,如何实现呢?
直接对 age
这一列调用 max
方法即可。
user_info.age.max()
40
类似的,通过调用 min
、mean
、quantile
、sum
方法可以实现最小值、平均值、中位数以及求和。可以看到,对一个 Series
调用 这几个方法之后,返回的都只是一个聚合结果。
来介绍个有意思的方法:cumsum
,看名字就发现它和 sum
方法有关系,事实上确实如此,cumsum
也是用来求和的,不过它是用来累加求和的,也就是说它得到的结果与原始的 Series
或 DataFrame
大小相同。
user_info.age.cumsum()
name
Tom 18
Bob 48
Mary 73
James 113
Name: age, dtype: int64
可以看到,cummax
最后的结果就是将上一次求和的结果与原始当前值求和作为当前值。这话听起来有点绕。举个例子,上面的 73 = 48 + 25
。cumsum
也可以用来操作字符串类型的对象。
user_info.sex.cumsum()
name
Tom male
Bob malemale
Mary malemalefemale
James malemalefemalemale
Name: sex, dtype: object
如果想要获取更多的统计方法,可以参见官方链接:Descriptive statistics
虽然说常见的各种统计值都有对应的方法,如果我想要得到多个指标的话,就需要调用多次方法,是不是显得有点麻烦呢?
Pandas 设计者自然也考虑到了这个问题,想要一次性获取多个统计指标,只需调用 describe方法即可。
user_info.describe()
age | |
---|---|
count | 4.000000 |
mean | 28.250000 |
std | 9.251126 |
min | 18.000000 |
25% | 23.250000 |
50% | 27.500000 |
75% | 32.500000 |
max | 40.000000 |
可以看到,直接调用 describe
方法后,会显示出数字类型的列的一些统计指标,如 总数、平均数、标准差、最小值、最大值、25%/50%/75% 分位数。如果想要查看非数字类型的列的统计指标,可以设置 include=["object"] 来获得。
user_info.describe(include=["object"])
city | sex | |
---|---|---|
count | 4 | 4 |
unique | 4 | 2 |
top | BeiJing | male |
freq | 1 | 3 |
上面的结果展示了非数字类型的列的一些统计指标:总数,去重后的个数、最常见的值、最常见的值的频数。
此外,如果我想要统计下某列中每个值出现的次数,如何快速实现呢?调用 value_counts
方法快速获取 Series
中每个值出现的次数。
user_info.sex.value_counts()
male 3
female 1
Name: sex, dtype: int64
如果想要获取某列最大值或最小值对应的索引,可以使用 idxmax
或 idxmin
方法完成。
user_info.age.idxmax()
'James'
离散化
有时候,我们会碰到这样的需求,想要将年龄进行离散化(分桶),直白来说就是将年龄分成几个区间,这里我们想要将年龄分成 3 个区间段。就可以使用 Pandas 的 cut
方法来完成。
pd.cut(user_info.age, 3)
name
Tom (17.978, 25.333]
Bob (25.333, 32.667]
Mary (17.978, 25.333]
James (32.667, 40.0]
Name: age, dtype: category
Categories (3, interval[float64]): [(17.978, 25.333] < (25.333, 32.667] < (32.667, 40.0]]
可以看到, cut
自动生成了等距的离散区间,如果自己想定义也是没问题的。
pd.cut(user_info.age, [1, 18, 30, 50])
name
Tom (1, 18]
Bob (18, 30]
Mary (18, 30]
James (30, 50]
Name: age, dtype: category
Categories (3, interval[int64]): [(1, 18] < (18, 30] < (30, 50]]
有时候离散化之后,想要给每个区间起个名字,可以指定 labels 参数。
pd.cut(user_info.age, [1, 18, 30, 50], labels=["childhood", "youth", "middle"])
name
Tom childhood
Bob youth
Mary youth
James middle
Name: age, dtype: category
Categories (3, object): [childhood < youth < middle]
除了可以使用 cut
进行离散化之外,qcut
也可以实现离散化。cut 是根据每个值的大小来进行离散化的,qcut 是根据每个值出现的次数来进行离散化的。
pd.qcut(user_info.age, 3)
name
Tom (17.999, 25.0]
Bob (25.0, 30.0]
Mary (17.999, 25.0]
James (30.0, 40.0]
Name: age, dtype: category
Categories (3, interval[float64]): [(17.999, 25.0] < (25.0, 30.0] < (30.0, 40.0]]
排序功能
在进行数据分析时,少不了进行数据排序。Pandas 支持两种排序方式:按轴(索引或列)排序和按实际值排序。
先来看下按索引排序:sort_index
方法默认是按照索引进行正序排的。
user_info.sort_index()
age | city | sex | |
---|---|---|---|
name | |||
Bob | 30 | ShangHai | male |
James | 40 | ShenZhen | male |
Mary | 25 | GuangZhou | female |
Tom | 18 | BeiJing | male |
如果想要按照列进行倒序排,可以设置参数 axis=1
和 ascending=False
。
user_info.sort_index(axis=1, ascending=False)
sex | city | age | |
---|---|---|---|
name | |||
Tom | male | BeiJing | 18 |
Bob | male | ShangHai | 30 |
Mary | female | GuangZhou | 25 |
James | male | ShenZhen | 40 |
如果想要实现按照实际值来排序,例如想要按照年龄排序,如何实现呢?
使用 sort_values
方法,设置参数 by="age"
即可。
user_info.sort_values(by="age")
age | city | sex | |
---|---|---|---|
name | |||
Tom | 18 | BeiJing | male |
Mary | 25 | GuangZhou | female |
Bob | 30 | ShangHai | male |
James | 40 | ShenZhen | male |
有时候我们可能需要按照多个值来排序,例如:按照年龄和城市来一起排序,可以设置参数 by 为一个 list 即可。
注意:list 中每个元素的顺序会影响排序优先级的。
user_info.sort_values(by=["age", "city"])
age | city | sex | |
---|---|---|---|
name | |||
Tom | 18 | BeiJing | male |
Mary | 25 | GuangZhou | female |
Bob | 30 | ShangHai | male |
James | 40 | ShenZhen | male |
一般在排序后,我们可能需要获取最大的n个值或最小值的n个值,我们可以使用 nlargest
和 nsmallest
方法来完成,这比先进行排序,再使用 head(n)
方法快得多。
user_info.age.nlargest(2)
name
James 40
Bob 30
Name: age, dtype: int64
函数应用
虽说 Pandas 为我们提供了非常丰富的函数,有时候我们可能需要自己定制一些函数,并将它应用到 DataFrame 或 Series。常用到的函数有:map
、apply
、applymap
。
map
是 Series 中特有的方法,通过它可以对 Series 中的每个元素实现转换。
如果我想通过年龄判断用户是否属于中年人(30岁以上为中年),通过 map
可以轻松搞定它。
# 接收一个 lambda 函数
user_info.age.map(lambda x: "yes" if x >= 30 else "no")
name
Tom no
Bob yes
Mary no
James yes
Name: age, dtype: object
又比如,我想要通过城市来判断是南方还是北方,我可以这样操作。
city_map = {
"BeiJing": "north",
"ShangHai": "south",
"GuangZhou": "south",
"ShenZhen": "south"
}
# 传入一个 map
user_info.city.map(city_map)
name
Tom north
Bob south
Mary south
James south
Name: city, dtype: object
apply
方法既支持 Series,也支持 DataFrame,在对 Series 操作时会作用到每个值上,在对 DataFrame 操作时会作用到所有行或所有列(通过 axis
参数控制)。
# 对 Series 来说,apply 方法 与 map 方法区别不大。
user_info.age.apply(lambda x: "yes" if x >= 30 else "no")
name
Tom no
Bob yes
Mary no
James yes
Name: age, dtype: object
# 对 DataFrame 来说,apply 方法的作用对象是一行或一列数据(一个Series)
user_info.apply(lambda x: x.max(), axis=0)
age 40
city ShenZhen
sex male
dtype: object
applymap
方法针对于 DataFrame,它作用于 DataFrame 中的每个元素,它对 DataFrame 的效果类似于 apply
对 Series 的效果。
user_info.applymap(lambda x: str(x).lower())
age | city | sex | |
---|---|---|---|
name | |||
Tom | 18 | beijing | male |
Bob | 30 | shanghai | male |
Mary | 25 | guangzhou | female |
James | 40 | shenzhen | male |
修改列/索引名称
在使用 DataFrame 的过程中,经常会遇到修改列名,索引名等情况。使用 rename
轻松可以实现。
修改列名只需要设置参数 columns
即可。
user_info.rename(columns={"age": "Age", "city": "City", "sex": "Sex"})
Age | City | Sex | |
---|---|---|---|
name | |||
Tom | 18 | BeiJing | male |
Bob | 30 | ShangHai | male |
Mary | 25 | GuangZhou | female |
James | 40 | ShenZhen | male |
类似的,修改索引名只需要设置参数 index
即可。
user_info.rename(index={"Tom": "tom", "Bob": "bob"})
age | city | sex | |
---|---|---|---|
name | |||
tom | 18 | BeiJing | male |
bob | 30 | ShangHai | male |
Mary | 25 | GuangZhou | female |
James | 40 | ShenZhen | male |
类型操作
如果想要获取每种类型的列数的话,可以使用 get_dtype_counts
方法。
user_info.get_dtype_counts()
int64 1
object 2
dtype: int64
如果想要转换数据类型的话,可以通过 astype
来完成。
user_info["age"].astype(float)
name
Tom 18.0
Bob 30.0
Mary 25.0
James 40.0
Name: age, dtype: float64
有时候会涉及到将 object 类型转为其他类型,常见的有转为数字、日期、时间差,Pandas 中分别对应 to_numeric
、to_datetime
、to_timedelta
方法。
这里给这些用户都添加一些关于身高的信息。
user_info["height"] = ["178", "168", "178", "180cm"]
user_info
age | city | sex | height | |
---|---|---|---|---|
name | ||||
Tom | 18 | BeiJing | male | 178 |
Bob | 30 | ShangHai | male | 168 |
Mary | 25 | GuangZhou | female | 178 |
James | 40 | ShenZhen | male | 180cm |
现在将身高这一列转为数字,很明显,180cm 并非数字,为了强制转换,我们可以传入 errors
参数,这个参数的作用是当强转失败时的处理方式。
默认情况下,errors='raise'
,这意味着强转失败后直接抛出异常,设置 errors='coerce'
可以在强转失败时将有问题的元素赋值为 pd.NaT(对于datetime和timedelta)或 np.nan(数字)。设置 errors='ignore'
可以在强转失败时返回原有的数据。
pd.to_numeric(user_info.height, errors="coerce")
name
Tom 178.0
Bob 168.0
Mary 178.0
James NaN
Name: height, dtype: float64
pd.to_numeric(user_info.height, errors="ignore")
name
Tom 178
Bob 168
Mary 178
James 180cm
Name: height, dtype: object
5.Pandas缺失值处理
这一章节我们来看下如何使用Pandas处理缺失值。
# 导入相关库
import numpy as np
import pandas as pd
什么是缺失值
在了解缺失值(也叫控制)如何处理之前,首先要知道的就是什么是缺失值?直观上理解,缺失值表示的是“缺失的数据”。
可以思考一个问题:是什么原因造成的缺失值呢?其实有很多原因,实际生活中可能由于有的数据不全所以导致数据缺失,也有可能由于误操作导致数据缺失,又或者人为地造成数据缺失。
来看下我们的示例吧。
index = pd.Index(data=["Tom", "Bob", "Mary", "James", "Andy", "Alice"], name="name")
data = {
"age": [18, 30, np.nan, 40, np.nan, 30],
"city": ["BeiJing", "ShangHai", "GuangZhou", "ShenZhen", np.nan, " "],
"sex": [None, "male", "female", "male", np.nan, "unknown"],
"birth": ["2000-02-10", "1988-10-17", None, "1978-08-08", np.nan, "1988-10-17"]
}
user_info = pd.DataFrame(data=data, index=index)
# 将出生日期转为时间戳
user_info["birth"] = pd.to_datetime(user_info.birth)
user_info
age | birth | city | sex | |
---|---|---|---|---|
name | ||||
Tom | 18.0 | 2000-02-10 | BeiJing | None |
Bob | 30.0 | 1988-10-17 | ShangHai | male |
Mary | NaN | NaT | GuangZhou | female |
James | 40.0 | 1978-08-08 | ShenZhen | male |
Andy | NaN | NaT | NaN | NaN |
Alice | 30.0 | 1988-10-17 | unknown |
可以看到,用户 Tom 的性别为 None
,用户 Mary 的年龄为 NAN
,生日为 NaT
。在 Pandas 的眼中,这些都属于缺失值,可以使用 isnull()
或 notnull()
方法来操作。
user_info.isnull()
age | birth | city | sex | |
---|---|---|---|---|
name | ||||
Tom | False | False | False | True |
Bob | False | False | False | False |
Mary | True | True | False | False |
James | False | False | False | False |
Andy | True | True | True | True |
Alice | False | False | False | False |
除了简单的可以识别出哪些是缺失值或非缺失值外,最常用的就是过滤掉一些缺失的行。比如,我想过滤掉用户年龄为空的用户,如何操作呢?
user_info[user_info.age.notnull()]
age | birth | city | sex | |
---|---|---|---|---|
name | ||||
Tom | 18.0 | 2000-02-10 | BeiJing | None |
Bob | 30.0 | 1988-10-17 | ShangHai | male |
James | 40.0 | 1978-08-08 | ShenZhen | male |
Alice | 30.0 | 1988-10-17 | unknown |
丢弃缺失值
既然有缺失值了,常见的一种处理办法就是丢弃缺失值。使用 dropna
方法可以丢弃缺失值。
user_info.age.dropna()
name
Tom 18.0
Bob 30.0
James 40.0
Alice 30.0
Name: age, dtype: float64
Seriese 使用 dropna
比较简单,对于 DataFrame 来说,可以设置更多的参数。
axis
参数用于控制行或列,跟其他不一样的是,axis=0
(默认)表示操作行,axis=1
表示操作列。
how
参数可选的值为 any
(默认) 或者 all
。any
表示一行/列有任意元素为空时即丢弃,all
一行/列所有值都为空时才丢弃。
subset
参数表示删除时只考虑的索引或列名。
thresh
参数的类型为整数,它的作用是,比如 thresh=3
,会在一行/列中至少有 3 个非空值时将其保留。
# 一行数据只要有一个字段存在空值即删除
user_info.dropna(axis=0, how="any")
age | birth | city | sex | |
---|---|---|---|---|
name | ||||
Bob | 30.0 | 1988-10-17 | ShangHai | male |
James | 40.0 | 1978-08-08 | ShenZhen | male |
Alice | 30.0 | 1988-10-17 | unknown |
# 一行数据所有字段都为空值才删除
user_info.dropna(axis=0, how="all")
age | birth | city | sex | |
---|---|---|---|---|
name | ||||
Tom | 18.0 | 2000-02-10 | BeiJing | None |
Bob | 30.0 | 1988-10-17 | ShangHai | male |
Mary | NaN | NaT | GuangZhou | female |
James | 40.0 | 1978-08-08 | ShenZhen | male |
Alice | 30.0 | 1988-10-17 | unknown |
# 一行数据中只要 city 或 sex 存在空值即删除
user_info.dropna(axis=0, how="any", subset=["city", "sex"])
age | birth | city | sex | |
---|---|---|---|---|
name | ||||
Bob | 30.0 | 1988-10-17 | ShangHai | male |
Mary | NaN | NaT | GuangZhou | female |
James | 40.0 | 1978-08-08 | ShenZhen | male |
Alice | 30.0 | 1988-10-17 | unknown |
填充缺失值
除了可以丢弃缺失值外,也可以填充缺失值,最常见的是使用 fillna
完成填充。
fillna
这名字一看就是用来填充缺失值的。
填充缺失值时,常见的一种方式是使用一个标量来填充。例如,这里我样有缺失的年龄都填充为 0。
user_info.age.fillna(0)
name
Tom 18.0
Bob 30.0
Mary 0.0
James 40.0
Andy 0.0
Alice 30.0
Name: age, dtype: float64
除了可以使用标量来填充之外,还可以使用前一个或后一个有效值来填充。
设置参数 method='pad'
或 method='ffill'
可以使用前一个有效值
来填充。
user_info.age.fillna(method="ffill")
name
Tom 18.0
Bob 30.0
Mary 30.0
James 40.0
Andy 40.0
Alice 30.0
Name: age, dtype: float64
设置参数 method='bfill'
或 method='backfill'
可以使用后一个有效值
来填充。
user_info.age.fillna(method="backfill")
name
Tom 18.0
Bob 30.0
Mary 40.0
James 40.0
Andy 30.0
Alice 30.0
Name: age, dtype: float64
除了通过 fillna
方法来填充缺失值外,还可以通过 interpolate
方法来填充。默认情况下使用线性差值,可以是设置 method
参数来改变方式。
user_info.age.interpolate()
name
Tom 18.0
Bob 30.0
Mary 35.0
James 40.0
Andy 35.0
Alice 30.0
Name: age, dtype: float64
替换缺失值
大家有没有想过一个问题:到底什么才是缺失值呢?你可能会奇怪说,前面不是已经说过了么,None
、np.nan
、NaT
这些都是缺失值。但是我也说过了,这些在 Pandas 的眼中是缺失值,有时候在我们人类的眼中,某些异常值我们也会当做缺失值来处理。
例如,在我们的存储的用户信息中,假定我们限定用户都是青年,出现了年龄为 40 的,我们就可以认为这是一个异常值。再比如,我们都知道性别分为男性(male)和女性(female),在记录用户性别的时候,对于未知的用户性别都记为了 “unknown”,很明显,我们也可以认为“unknown”是缺失值。此外,有的时候会出现空白字符串,这些也可以认为是缺失值。
对于上面的这种情况,我们可以使用 replace
方法来替换缺失值。
user_info.age.replace(40, np.nan)
name
Tom 18.0
Bob 30.0
Mary NaN
James NaN
Andy NaN
Alice 30.0
Name: age, dtype: float64
也可以指定一个映射字典。
user_info.age.replace({40: np.nan})
name
Tom 18.0
Bob 30.0
Mary NaN
James NaN
Andy NaN
Alice 30.0
Name: age, dtype: float64
对于 DataFrame,可以指定每列要替换的值。
user_info.replace({"age": 40, "birth": pd.Timestamp("1978-08-08")}, np.nan)
age | birth | city | sex | |
---|---|---|---|---|
name | ||||
Tom | 18.0 | 2000-02-10 | BeiJing | None |
Bob | 30.0 | 1988-10-17 | ShangHai | male |
Mary | NaN | NaT | GuangZhou | female |
James | NaN | NaT | ShenZhen | male |
Andy | NaN | NaT | NaN | NaN |
Alice | 30.0 | 1988-10-17 | unknown |
类似地,我们可以将特定字符串进行替换,如:将 "unknown" 进行替换。
user_info.sex.replace("unknown", np.nan)
name
Tom None
Bob male
Mary female
James male
Andy NaN
Alice NaN
Name: sex, dtype: object
除了可以替换特定的值之外,还可以使用正则表达式来替换,如:将空白字符串替换成空值。
user_info.city.replace(r'\s+', np.nan, regex=True)
name
Tom BeiJing
Bob ShangHai
Mary GuangZhou
James ShenZhen
Andy NaN
Alice NaN
Name: city, dtype: object
使用其他对象填充
除了我们自己手动丢弃、填充已经替换缺失值之外,我们还可以使用其他对象来填充。
例如有两个关于用户年龄的 Series,其中一个有缺失值,另一个没有,我们可以将没有的缺失值的 Series 中的元素传给有缺失值的。
age_new = user_info.age.copy()
age_new.fillna(20, inplace=True)
age_new
name
Tom 18.0
Bob 30.0
Mary 20.0
James 40.0
Andy 20.0
Alice 30.0
Name: age, dtype: float64
user_info.age.combine_first(age_new)
name
Tom 18.0
Bob 30.0
Mary 20.0
James 40.0
Andy 20.0
Alice 30.0
Name: age, dtype: float64
6.Pandas文本数据处理
# 导入相关库
import numpy as np
import pandas as pd
为什么要用str属性
文本数据也就是我们常说的字符串,Pandas 为 Series 提供了 str
属性,通过它可以方便的对每个元素进行操作。
index = pd.Index(data=["Tom", "Bob", "Mary", "James", "Andy", "Alice"], name="name")
data = {
"age": [18, 30, np.nan, 40, np.nan, 30],
"city": ["Bei Jing ", "Shang Hai ", "Guang Zhou", "Shen Zhen", np.nan, " "],
"sex": [None, "male", "female", "male", np.nan, "unknown"],
"birth": ["2000-02-10", "1988-10-17", None, "1978-08-08", np.nan, "1988-10-17"]
}
user_info = pd.DataFrame(data=data, index=index)
# 将出生日期转为时间戳
user_info["birth"] = pd.to_datetime(user_info.birth)
user_info
age | birth | city | sex | |
---|---|---|---|---|
name | ||||
Tom | 18.0 | 2000-02-10 | Bei Jing | None |
Bob | 30.0 | 1988-10-17 | Shang Hai | male |
Mary | NaN | NaT | Guang Zhou | female |
James | 40.0 | 1978-08-08 | Shen Zhen | male |
Andy | NaN | NaT | NaN | NaN |
Alice | 30.0 | 1988-10-17 | unknown |
在之前已经了解过,在对 Series 中每个元素处理时,我们可以使用 map
或 apply
方法。
比如,我想要将每个城市都转为小写,可以使用如下的方式。
user_info.city.map(lambda x: x.lower())
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-3-e661c5ad5c48> in <module>()
----> 1 user_info.city.map(lambda x: x.lower())
C:\soft\py3\lib\site-packages\pandas\core\series.py in map(self, arg, na_action)
2156 else:
2157 # arg is a function
-> 2158 new_values = map_f(values, arg)
2159
2160 return self._constructor(new_values,
pandas/_libs/src\inference.pyx in pandas._libs.lib.map_infer()
<ipython-input-3-e661c5ad5c48> in <lambda>(x)
----> 1 user_info.city.map(lambda x: x.lower())
AttributeError: 'float' object has no attribute 'lower'
What?竟然出错了,错误原因是因为 float 类型的对象没有 lower 属性。这是因为缺失值(np.nan)属于float 类型。
这时候我们的 str
属性操作来了,来看看如何使用吧。
# 将文本转为小写
user_info.city.str.lower()
name
Tom bei jing
Bob shang hai
Mary guang zhou
James shen zhen
Andy NaN
Alice
Name: city, dtype: object
可以看到,通过 str
属性来访问之后用到的方法名与 Python 内置的字符串的方法名一样。并且能够自动排除缺失值。
我们再来试试其他一些方法。例如,统计每个字符串的长度。
user_info.city.str.len()
name
Tom 9.0
Bob 10.0
Mary 10.0
James 9.0
Andy NaN
Alice 1.0
Name: city, dtype: float64
替换和分割
使用 .srt
属性也支持替换与分割操作。
先来看下替换操作,例如:将空字符串替换成下划线。
user_info.city.str.replace(" ", "_")
name
Tom Bei_Jing_
Bob Shang_Hai_
Mary Guang_Zhou
James Shen_Zhen
Andy NaN
Alice _
Name: city, dtype: object
replace
方法还支持正则表达式,例如将所有开头为 S
的城市替换为空字符串。
user_info.city.str.replace("^S.*", " ")
name
Tom Bei Jing
Bob
Mary Guang Zhou
James
Andy NaN
Alice
Name: city, dtype: object
再来看下分割操作,例如根据空字符串来分割某一列。
user_info.city.str.split(" ")
name
Tom [Bei, Jing, ]
Bob [Shang, Hai, ]
Mary [Guang, Zhou]
James [Shen, Zhen]
Andy NaN
Alice [, ]
Name: city, dtype: object
分割列表中的元素可以使用 get
或 []
符号进行访问:
user_info.city.str.split(" ").str.get(1)
name
Tom Jing
Bob Hai
Mary Zhou
James Zhen
Andy NaN
Alice
Name: city, dtype: object
user_info.city.str.split(" ").str[1]
name
Tom Jing
Bob Hai
Mary Zhou
James Zhen
Andy NaN
Alice
Name: city, dtype: object
设置参数 expand=True
可以轻松扩展此项以返回 DataFrame。
user_info.city.str.split(" ", expand=True)
0 | 1 | 2 | |
---|---|---|---|
name | |||
Tom | Bei | Jing | |
Bob | Shang | Hai | |
Mary | Guang | Zhou | None |
James | Shen | Zhen | None |
Andy | NaN | None | None |
Alice | None |
提取子串
既然是在操作字符串,很自然,你可能会想到是否可以从一个长的字符串中提取出子串。答案是可以的。
提取第一个匹配的子串
extract
方法接受一个正则表达式并至少包含一个捕获组,指定参数 expand=True
可以保证每次都返回 DataFrame。
例如,现在想要匹配空字符串前面的所有的字母,可以使用如下操作:
user_info.city.str.extract("(\w+)\s+", expand=True)
0 | |
---|---|
name | |
Tom | Bei |
Bob | Shang |
Mary | Guang |
James | Shen |
Andy | NaN |
Alice | NaN |
如果使用多个组提取正则表达式会返回一个 DataFrame,每个组只有一列。
例如,想要匹配出空字符串前面和后面的所有字母,操作如下:
user_info.city.str.extract("(\w+)\s+(\w+)", expand=True)
0 | 1 | |
---|---|---|
name | ||
Tom | Bei | Jing |
Bob | Shang | Hai |
Mary | Guang | Zhou |
James | Shen | Zhen |
Andy | NaN | NaN |
Alice | NaN | NaN |
匹配所有子串
extract
只能够匹配出第一个子串,使用 extractall
可以匹配出所有的子串。
例如,将所有组的空白字符串前面的字母都匹配出来,可以如下操作。
user_info.city.str.extractall("(\w+)\s+")
0 | ||
---|---|---|
name | match | |
Tom | 0 | Bei |
1 | Jing | |
Bob | 0 | Shang |
1 | Hai | |
Mary | 0 | Guang |
James | 0 | Shen |
测试是否包含子串
除了可以匹配出子串外,我们还可以使用 contains
来测试是否包含子串。例如,想要测试城市是否包含子串 “Zh”。
user_info.city.str.contains("Zh")
name
Tom False
Bob False
Mary True
James True
Andy NaN
Alice False
Name: city, dtype: object
当然了,正则表达式也是支持的。例如,想要测试是否是以字母 “S” 开头。
user_info.city.str.contains("^S")
name
Tom False
Bob True
Mary False
James True
Andy NaN
Alice False
Name: city, dtype: object
生成哑变量
这是一个神奇的功能,通过 get_dummies
方法可以将字符串转为哑变量,sep
参数是指定哑变量之间的分隔符。来看看效果吧。
user_info.city.str.get_dummies(sep=" ")
Bei | Guang | Hai | Jing | Shang | Shen | Zhen | Zhou | |
---|---|---|---|---|---|---|---|---|
name | ||||||||
Tom | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 0 |
Bob | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 0 |
Mary | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 |
James | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 |
Andy | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Alice | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
这样,它提取出了 Bei, Guang, Hai, Jing, Shang, Shen, Zhen, Zhou
这些哑变量,并对每个变量下使用 0 或 1 来表达。实际上与 One-Hot(狂热编码)是一回事。听不懂没关系,之后将机器学习相关知识时会详细介绍这里。
方法摘要
这里列出了一些常用的方法摘要。
方法 | 描述 |
---|---|
cat() | 连接字符串 |
split() | 在分隔符上分割字符串 |
rsplit() | 从字符串末尾开始分隔字符串 |
get() | 索引到每个元素(检索第i个元素) |
join() | 使用分隔符在系列的每个元素中加入字符串 |
get_dummies() | 在分隔符上分割字符串,返回虚拟变量的DataFrame |
contains() | 如果每个字符串都包含pattern / regex,则返回布尔数组 |
replace() | 用其他字符串替换pattern / regex的出现 |
repeat() | 重复值(s.str.repeat(3)等同于x * 3 t2 >) |
pad() | 将空格添加到字符串的左侧,右侧或两侧 |
center() | 相当于str.center |
ljust() | 相当于str.ljust |
rjust() | 相当于str.rjust |
zfill() | 等同于str.zfill |
wrap() | 将长长的字符串拆分为长度小于给定宽度的行 |
slice() | 切分Series中的每个字符串 |
slice_replace() | 用传递的值替换每个字符串中的切片 |
count() | 计数模式的发生 |
startswith() | 相当于每个元素的str.startswith(pat) |
endswith() | 相当于每个元素的str.endswith(pat) |
findall() | 计算每个字符串的所有模式/正则表达式的列表 |
match() | 在每个元素上调用re.match,返回匹配的组作为列表 |
extract() | 在每个元素上调用re.search,为每个元素返回一行DataFrame,为每个正则表达式捕获组返回一列 |
extractall() | 在每个元素上调用re.findall,为每个匹配返回一行DataFrame,为每个正则表达式捕获组返回一列 |
len() | 计算字符串长度 |
strip() | 相当于str.strip |
rstrip() | 相当于str.rstrip |
lstrip() | 相当于str.lstrip |
partition() | 等同于str.partition |
rpartition() | 等同于str.rpartition |
lower() | 相当于str.lower |
upper() | 相当于str.upper |
find() | 相当于str.find |
rfind() | 相当于str.rfind |
index() | 相当于str.index |
rindex() | 相当于str.rindex |
capitalize() | 相当于str.capitalize |
swapcase() | 相当于str.swapcase |
normalize() | 返回Unicode标准格式。相当于unicodedata.normalize |
translate() | 等同于str.translate |
isalnum() | 等同于str.isalnum |
isalpha() | 等同于str.isalpha |
isdigit() | 相当于str.isdigit |
isspace() | 等同于str.isspace |
islower() | 相当于str.islower |
isupper() | 相当于str.isupper |
istitle() | 相当于str.istitle |
isnumeric() | 相当于str.isnumeric |
isdecimal() | 相当于str.isdecimal |
7.pandas分类数据处理
来看看 Pandas 中分类(category)数据如何处理吧。
# 导入相关库
import numpy as np
import pandas as pd
创建对象 在创建分类数据之前,先来了解下什么是分类(Category)数据呢?分类数据直白来说就是取值为有限的,或者说是固定数量的可能值。例如:性别、血型。
这里以血型为例,假定每个用户有以下的血型,我们如何创建一个关于血型的分类对象呢?
一种有效的方法就是明确指定 dtype="category"
index = pd.Index(data=["Tom", "Bob", "Mary", "James", "Andy", "Alice"], name="name")
user_info = pd.Series(data=["A", "AB", np.nan, "AB", "O", "B"], index=index, name="blood_type", dtype="category")
user_info
name
Tom A
Bob AB
Mary NaN
James AB
Andy O
Alice B
Name: blood_type, dtype: category
Categories (4, object): [A, AB, B, O]
我们也可以使用 pd.Categorical 来构建分类数据。
pd.Categorical(["A", "AB", np.nan, "AB", "O", "B"]) [A, AB, NaN, AB, O, B] Categories (4, object): [A, AB, B, O] 当然了,我们也可以自己制定类别数据所有可能的取值,假定我们认为血型只有 A、B 以及 AB 这三类,那么我们可以这样操作。
pd.Categorical(["A", "AB", np.nan, "AB", "O", "B"], categories=["A", "B", "AB"])
[A, AB, NaN, AB, NaN, B]
Categories (3, object): [A, B, AB]
除了上面这些方法外,经常遇到的情况是已经创建了一个 Series,如何将它转为分类数据呢?来看看 astype 用法吧。
user_info = pd.Series(data=["A", "AB", np.nan, "AB", "O", "B"], index=index, name="blood_type")
user_info = user_info.astype("category")
user_info
name
Tom A
Bob AB
Mary NaN
James AB
Andy O
Alice B
Name: blood_type, dtype: category
Categories (4, object): [A, AB, B, O]
此外,一些其他的方法返回的结果也是分类数据。如 cut 、 qcut。具体可以见 Pandas基本功能详解中的离散化部分。
常用操作 可以对分类数据使用 .describe() 方法,它得到的结果与 string类型的数据相同。
user_info.describe()
count 5
unique 4
top AB
freq 2
Name: blood_type, dtype: object
解释下每个指标的含义,count 表示非空的数据有5条,unique 表示去重后的非空数据有4条,top 表示出现次数最多的值为 AB,freq 表示出现次数最多的值的次数为2次。
我们可以使用 .cat.categories 来获取分类数据所有可能的取值。
user_info.cat.categories
Index([u'A', u'AB', u'B', u'O'], dtype='object')
你可能会发现,假如你将分类名称写错了,如何修改呢?难道还需要重新构建一次么?
可以直接使用 .cat.rename_categories 方法来重命名分类名称。
user_info.cat.rename_categories(["A+", "AB+", "B+", "O+"])
name
Tom A+
Bob AB+
Mary NaN
James AB+
Andy O+
Alice B+
Name: blood_type, dtype: category
Categories (4, object): [A+, AB+, B+, O+]
类似的,除了重命名,也会遇到添加类别,删除分类的操作,这些都可以通过 .cat.add_categories ,.cat.remove_categories 来实现。
分类数据也是支持使用 value_counts 方法来查看数据分布的。
user_info.value_counts()
AB 2
O 1
B 1
A 1
Name: blood_type, dtype: int64
分类数据也是支持使用 .str 属性来访问的。例如想要查看下是否包含字母 "A",可以使用 .srt.contains 方法。
user_info.str.contains("A")
name
Tom True
Bob True
Mary NaN
James True
Andy False
Alice False
Name: blood_type, dtype: object
跟多关于 .str 的详细介绍可以见 Pandas文本数据处理。
有时候会遇到合并数据的情况,这时候可以借助 pd.concat 来完成。
blood_type1 = pd.Categorical(["A", "AB"])
blood_type2 = pd.Categorical(["B", "O"])
pd.concat([pd.Series(blood_type1), pd.Series(blood_type2)])
0 A
1 AB
0 B
1 O
dtype: object
可以发现,分类数据经过 pd.concat 合并后类型转为了 object 类型。如果想要保持分类类型的话,可以借助 union_categoricals 来完成。
from pandas.api.types import union_categoricals
union_categoricals([blood_type1, blood_type2])
[A, AB, B, O]
Categories (4, object): [A, AB, B, O]
内存使用量的陷阱 Categorical 的内存使用量是与分类数乘以数据长度成正比,object 类型的数据是一个常数乘以数据的长度。
blood_type = pd.Series(["AB","O"]*1000)
blood_type.nbytes
16000
blood_type.astype("category").nbytes
2016
对比下,是不是发现分类数据非常节省内存。但是当类别的数量接近数据的长度,那么 Categorical 将使用与等效的 object 表示几乎相同或更多的内存。
blood_type = pd.Series(['AB%04d' % i for i in range(2000)])
blood_type.nbytes
16000
blood_type.astype("category").nbytes
20000
8.Pandas时间序列详解
# 导入相关库
import numpy as np
import pandas as pd
在做金融领域方面的分析时,经常会对时间进行一系列的处理。Pandas 内部自带了很多关于时间序列相关的工具,所以它非常适合处理时间序列。在处理时间序列的的过程中,我们经常会去做以下一些任务:
- 生成固定频率日期和时间跨度的序列
- 将时间序列整合或转换为特定频率
- 基于各种非标准时间增量(例如,在一年的最后一个工作日之前的5个工作日)计算“相对”日期,或向前或向后“滚动”日期
使用 Pandas 可以轻松完成以上任务。
基础概述
下面列出了 Pandas中 和时间日期相关常用的类以及创建方法。
类 | 备注 | 创建方法 |
---|---|---|
Timestamp | 时刻数据 | to_datetime,Timestamp |
DatetimeIndex | Timestamp的索引 | to_datetime,date_range,DatetimeIndex |
Period | 时期数据 | Period |
PeriodIndex | Period | period_range,PeriodIndex |
Pandas 中关于时间序列最常见的类型就是时间戳(Timestamp
)了,创建时间戳的方法有很多种,我们分别来看一看。
pd.Timestamp(2018, 5, 21)
Timestamp('2018-05-21 00:00:00')
pd.Timestamp("2018-5-21")
Timestamp('2018-05-21 00:00:00')
除了时间戳之外,另一个常见的结构是时间跨度(Period
)。
pd.Period("2018-01")
Period('2018-01', 'M')
pd.Period("2018-05", freq="D")
Period('2018-05-01', 'D')
Timestamp
和 Period
可以是索引。将Timestamp
和 Period
作为 Series
或 DataFrame
的索引后会自动强制转为为 DatetimeIndex
和 PeriodIndex
。
dates = [pd.Timestamp("2018-05-01"), pd.Timestamp("2018-05-02"), pd.Timestamp("2018-05-03"), pd.Timestamp("2018-05-04")]
ts = pd.Series(data=["Tom", "Bob", "Mary", "James"], index=dates)
ts.index
DatetimeIndex(['2018-05-01', '2018-05-02', '2018-05-03', '2018-05-04'], dtype='datetime64[ns]', freq=None)
periods = [pd.Period("2018-01"), pd.Period("2018-02"), pd.Period("2018-03"), pd.Period("2018-4")]
ts = pd.Series(data=["Tom", "Bob", "Mary", "James"], index=periods)
ts.index
PeriodIndex(['2018-01', '2018-02', '2018-03', '2018-04'], dtype='period[M]', freq='M')
转换时间戳
你可能会想到,我们经常要和文本数据(字符串)打交道,能否快速将文本数据转为时间戳呢?
答案是可以的,通过 to_datetime
能快速将字符串转换为时间戳。当传递一个Series时,它会返回一个Series(具有相同的索引),而类似列表的则转换为DatetimeIndex
。
pd.to_datetime(pd.Series(["Jul 31, 2018", "2018-05-10", None]))
0 2018-07-31
1 2018-05-10
2 NaT
dtype: datetime64[ns]
pd.to_datetime(["2005/11/23", "2010.12.31"])
DatetimeIndex(['2005-11-23', '2010-12-31'], dtype='datetime64[ns]', freq=None)
除了可以将文本数据转为时间戳外,还可以将 unix 时间转为时间戳。
pd.to_datetime([1349720105, 1349806505, 1349892905], unit="s")
DatetimeIndex(['2012-10-08 18:15:05', '2012-10-09 18:15:05',
'2012-10-10 18:15:05'],
dtype='datetime64[ns]', freq=None)
pd.to_datetime([1349720105100, 1349720105200, 1349720105300], unit="ms")
DatetimeIndex(['2012-10-08 18:15:05.100000', '2012-10-08 18:15:05.200000',
'2012-10-08 18:15:05.300000'],
dtype='datetime64[ns]', freq=None)
生成时间戳范围
有时候,我们可能想要生成某个范围内的时间戳。例如,我想要生成 "2018-6-26" 这一天之后的8天时间戳,如何完成呢?我们可以使用 date_range
和 bdate_range
来完成时间戳范围的生成。
pd.date_range("2018-6-26", periods=8)
DatetimeIndex(['2018-06-26', '2018-06-27', '2018-06-28', '2018-06-29',
'2018-06-30', '2018-07-01', '2018-07-02', '2018-07-03'],
dtype='datetime64[ns]', freq='D')
pd.bdate_range("2018-6-26", periods=8)
DatetimeIndex(['2018-06-26', '2018-06-27', '2018-06-28', '2018-06-29',
'2018-07-02', '2018-07-03', '2018-07-04', '2018-07-05'],
dtype='datetime64[ns]', freq='B')
可以看出,date_range
默认使用的频率是 日历日,而 bdate_range
默认使用的频率是 营业日。当然了,我们可以自己指定频率,比如,我们可以按周来生成时间戳范围。
pd.date_range("2018-6-26", periods=8, freq="W")
DatetimeIndex(['2018-07-01', '2018-07-08', '2018-07-15', '2018-07-22',
'2018-07-29', '2018-08-05', '2018-08-12', '2018-08-19'],
dtype='datetime64[ns]', freq='W-SUN')
DatetimeIndex
DatetimeIndex
的主要作用是之一是用作 Pandas 对象的索引,使用它作为索引除了拥有普通索引对象的所有基本功能外,还拥有简化频率处理的高级时间序列方法。
rng = pd.date_range("2018-6-24", periods=4, freq="W")
ts = pd.Series(range(len(rng)), index=rng)
ts
2018-06-24 0
2018-07-01 1
2018-07-08 2
2018-07-15 3
Freq: W-SUN, dtype: int32
# 通过日期访问数据
ts["2018-07-08"]
2
# 通过日期区间访问数据切片
ts["2018-07-08": "2018-07-22"]
2018-07-08 2
2018-07-15 3
Freq: W-SUN, dtype: int32
除了可以将日期作为参数,还可以将年份或者年份、月份作为参数来获取更多的数据。
# 传入年份
ts["2018"]
2018-06-24 0
2018-07-01 1
2018-07-08 2
2018-07-15 3
Freq: W-SUN, dtype: int32
# 传入年份和月份
ts["2018-07"]
2018-07-01 1
2018-07-08 2
2018-07-15 3
Freq: W-SUN, dtype: int32
除了可以使用字符串对 DateTimeIndex
进行索引外,还可以使用 datetime
(日期时间)对象来进行索引。
from datetime import datetime
ts[datetime(2018, 7, 8) : datetime(2018, 7, 22)]
2018-07-08 2
2018-07-15 3
Freq: W-SUN, dtype: int32
我们可以通过 Timestamp
或 DateTimeIndex
访问一些时间/日期的属性。这里列举一些常见的,想要查看所有的属性见官方链接:Time/Date Components(http://pandas.pydata.org/pandas-docs/stable/timeseries.html#time-date-components)
# 获取年份
ts.index.year
Int64Index([2018, 2018, 2018, 2018], dtype='int64')
# 获取星期几
ts.index.dayofweek
Int64Index([6, 6, 6, 6], dtype='int64')
# 获取一年中的几个第几个星期
ts.index.weekofyear
Int64Index([25, 26, 27, 28], dtype='int64')
DateOffset对象
DateOffset
从名称中就可以看出来是要做日期偏移的,它的参数与 dateutil.relativedelta
基本相同,工作方式如下:
from pandas.tseries.offsets import *
d = pd.Timestamp("2018-06-25")
d + DateOffset(weeks=2, days=5)
Timestamp('2018-07-14 00:00:00')
除了可以使用 DateOffset
完成上面的功能外,还可以使用偏移量实例来完成。
d + Week(2) + Day(5)
Timestamp('2018-07-14 00:00:00')
与时间序列相关的方法
在做时间序列相关的工作时,经常要对时间做一些移动/滞后、频率转换、采样等相关操作,我们来看下这些操作如何使用吧。
移动
如果你想移动或滞后时间序列,你可以使用 shift
方法。
ts.shift(2)
2018-06-24 NaN
2018-07-01 NaN
2018-07-08 0.0
2018-07-15 1.0
Freq: W-SUN, dtype: float64
可以看到,Series 所有的值都都移动了 2 个距离。如果不想移动值,而是移动日期索引,可以使用 freq
参数,它可以接受一个 DateOffset
类或其他 timedelta
类对象或一个 offset 别名,所有别名详细介绍见:Offset Aliases(http://pandas.pydata.org/pandas-docs/stable/timeseries.html#offset-aliases)。
ts.shift(2, freq=Day())
2018-06-26 0
2018-07-03 1
2018-07-10 2
2018-07-17 3
Freq: W-TUE, dtype: int32
可以看到,现在日期索引移动了 2 天的间隔。通过 tshift
同样可以达到相同的效果。
ts.tshift(2, freq=Day())
2018-06-26 0
2018-07-03 1
2018-07-10 2
2018-07-17 3
Freq: W-TUE, dtype: int32
频率转换
频率转换可以使用 asfreq
函数来实现。下面演示了将频率由周转为了天。
ts.asfreq(Day())
2018-06-24 0.0
2018-06-25 NaN
2018-06-26 NaN
2018-06-27 NaN
2018-06-28 NaN
2018-06-29 NaN
2018-06-30 NaN
2018-07-01 1.0
2018-07-02 NaN
2018-07-03 NaN
2018-07-04 NaN
2018-07-05 NaN
2018-07-06 NaN
2018-07-07 NaN
2018-07-08 2.0
2018-07-09 NaN
2018-07-10 NaN
2018-07-11 NaN
2018-07-12 NaN
2018-07-13 NaN
2018-07-14 NaN
2018-07-15 3.0
Freq: D, dtype: float64
聪明的你会发现出现了缺失值,因此 Pandas 为你提供了 method
参数来填充缺失值。几种不同的填充方法参考 Pandas 缺失值处理 中 fillna
介绍。
ts.asfreq(Day(), method="pad")
2018-06-24 0
2018-06-25 0
2018-06-26 0
2018-06-27 0
2018-06-28 0
2018-06-29 0
2018-06-30 0
2018-07-01 1
2018-07-02 1
2018-07-03 1
2018-07-04 1
2018-07-05 1
2018-07-06 1
2018-07-07 1
2018-07-08 2
2018-07-09 2
2018-07-10 2
2018-07-11 2
2018-07-12 2
2018-07-13 2
2018-07-14 2
2018-07-15 3
Freq: D, dtype: int32
重采样
resample
表示根据日期维度进行数据聚合,可以按照分钟、小时、工作日、周、月、年等来作为日期维度,更多的日期维度见 Offset Aliases(http://pandas.pydata.org/pandas-docs/stable/timeseries.html#offset-aliases)。
这里我们先以月来作为时间维度来进行聚合。
# 求出每个月的数值之和
ts.resample("1M").sum()
2018-06-30 0
2018-07-31 6
Freq: M, dtype: int32
# 求出每个月的数值平均值
ts.resample("1M").mean()
2018-06-30 0
2018-07-31 2
Freq: M, dtype: int32
9.Pandas计算工具介绍
# 导入相关库
import numpy as np
import pandas as pd
Pandas 中包含了非常丰富的计算工具,如一些统计函数、窗口函数、聚合等计算工具。
统计函数
最常见的计算工具莫过于一些统计函数了。这里我们首先构建一个包含了用户年龄与收入的 DataFrame。
index = pd.Index(data=["Tom", "Bob", "Mary", "James", "Andy", "Alice"], name="name")
data = {
"age": [18, 40, 28, 20, 30, 35],
"income": [1000, 4500 , 1800, 1800, 3000, np.nan],
}
df = pd.DataFrame(data=data, index=index)
df
age | income | |
---|---|---|
name | ||
Tom | 18 | 1000.0 |
Bob | 40 | 4500.0 |
Mary | 28 | 1800.0 |
James | 20 | 1800.0 |
Andy | 30 | 3000.0 |
Alice | 35 | NaN |
我们可以通过 cov
函数来求出年龄与收入之间的协方差,计算的时候会丢弃缺失值。
df.age.cov(df.income)
11320.0
除了协方差之外,我们还可以通过 corr
函数来计算下它们之间的相关性,计算的时候会丢弃缺失值。
默认情况下 corr
计算相关性时用到的方法是 pearson
,当然了你也可以指定 kendall
或 spearman
。
df.age.corr(df.income)
0.94416508951340194
df.age.corr(df.income, method="kendall")
0.94868329805051366
df.age.corr(df.income, method="spearman")
0.97467943448089644
除了相关性的计算外,还可以通过 rank
函数求出数据的排名顺序。
df.income.rank()
name
Tom 1.0
Bob 5.0
Mary 2.5
James 2.5
Andy 4.0
Alice NaN
Name: income, dtype: float64
如果有相同的数,默认取其排名的平均值作为值。我们可以设置参数来得到不同的结果。可以设置的参数有:min
、max
、first
、dense
。
df.income.rank(method="first")
name
Tom 1.0
Bob 5.0
Mary 2.0
James 3.0
Andy 4.0
Alice NaN
Name: income, dtype: float64
窗口函数
有的时候,我们需要对不同窗口的中数据进行一个统计,常见的窗口类型为时间窗口。
例如,下面是某个餐厅 7 天的营业额,我们想要计算每两天的收入总额,如何计算呢?
data = {
"turnover": [12000, 18000, np.nan, 12000, 9000, 16000, 18000],
"date": pd.date_range("2018-07-01", periods=7)
}
df2 = pd.DataFrame(data=data)
df2
date | turnover | |
---|---|---|
0 | 2018-07-01 | 12000.0 |
1 | 2018-07-02 | 18000.0 |
2 | 2018-07-03 | NaN |
3 | 2018-07-04 | 12000.0 |
4 | 2018-07-05 | 9000.0 |
5 | 2018-07-06 | 16000.0 |
6 | 2018-07-07 | 18000.0 |
通过 rolling
我们可以实现,设置 window=2
来保证窗口长度为 2,设置 on="date"
来保证根据日期这一列来滑动窗口(默认不设置,表示根据索引来欢动)
df2.rolling(window=2, on="date").sum()
date | turnover | |
---|---|---|
0 | 2018-07-01 | NaN |
1 | 2018-07-02 | 30000.0 |
2 | 2018-07-03 | NaN |
3 | 2018-07-04 | NaN |
4 | 2018-07-05 | 21000.0 |
5 | 2018-07-06 | 25000.0 |
6 | 2018-07-07 | 34000.0 |
是不是发现,有很多结果是缺失值,导致这个结果的原因是因为在计算时,窗口中默认需要的最小数据个数与窗口长度一致,这里可以设置 min_periods=1
来修改下。
df2.rolling(window=2, on="date", min_periods=1).sum()
date | turnover | |
---|---|---|
0 | 2018-07-01 | 12000.0 |
1 | 2018-07-02 | 30000.0 |
2 | 2018-07-03 | 18000.0 |
3 | 2018-07-04 | 12000.0 |
4 | 2018-07-05 | 21000.0 |
5 | 2018-07-06 | 25000.0 |
6 | 2018-07-07 | 34000.0 |
有时候,我想要计算每段时间的累加和,如何实现呢?先来看看第一种方式吧。
df2.rolling(window=len(df2), on="date", min_periods=1).sum()
date | turnover | |
---|---|---|
0 | 2018-07-01 | 12000.0 |
1 | 2018-07-02 | 30000.0 |
2 | 2018-07-03 | 30000.0 |
3 | 2018-07-04 | 42000.0 |
4 | 2018-07-05 | 51000.0 |
5 | 2018-07-06 | 67000.0 |
6 | 2018-07-07 | 85000.0 |
还有另外一种方式,直接使用 expanding
来生成窗口。
df2.expanding(min_periods=1)["turnover"].sum()
0 12000.0
1 30000.0
2 30000.0
3 42000.0
4 51000.0
5 67000.0
6 85000.0
Name: turnover, dtype: float64
除了可以使用 sum
函数外,还有很多其他的函数可以使用,如:count
、mean
、median
、min
、max
、std
、var
、quantile
、apply
、cov
、corr
等等。
方法 | 描述 |
---|---|
count() | 非空观测值数量 |
sum() | 值的总和 |
mean() | 价值的平均值 |
median() | 值的算术中值 |
min() | 最小值 |
max() | 最大 |
std() | 贝塞尔修正样本标准差 |
var() | 无偏方差 |
skew() | 样品偏斜度(三阶矩) |
kurt() | 样品峰度(四阶矩) |
quantile() | 样本分位数(百分位上的值) |
apply() | 通用适用 |
cov() | 无偏协方差(二元) |
corr() | 相关(二进制) |
不过上面的方式只能生成一个结果,有时候想要同时求出多个结果(如求和和均值),如何实现呢?
借助 agg
函数可以快速实现。
df2.rolling(window=2, min_periods=1)["turnover"].agg([np.sum, np.mean])
sum | mean | |
---|---|---|
0 | 12000.0 | 12000.0 |
1 | 30000.0 | 15000.0 |
2 | 18000.0 | 18000.0 |
3 | 12000.0 | 12000.0 |
4 | 21000.0 | 10500.0 |
5 | 25000.0 | 12500.0 |
6 | 34000.0 | 17000.0 |
如果传入一个字典,可以为生成的统计结果重命名。
df2.rolling(window=2, min_periods=1)["turnover"].agg({"tur_sum": np.sum, "tur_mean": np.mean})
tur_sum | tur_mean | |
---|---|---|
0 | 12000.0 | 12000.0 |
1 | 30000.0 | 15000.0 |
2 | 18000.0 | 18000.0 |
3 | 12000.0 | 12000.0 |
4 | 21000.0 | 10500.0 |
5 | 25000.0 | 12500.0 |
6 | 34000.0 | 17000.0 |
数学知识补充
均值,方差
统计里最基本的概念就是样本的均值,方差,或者再加个标准差。假定有一个含有n个样本的集合X={X1,…,Xn},依次给出这些概念的公式描述:
很显然,均值描述的是样本集合的中间点,它告诉我们的信息是很有限的。
而标准差给我们描述的则是样本集合的各个样本点到均值的距离之平均。以这两个集合为例,[0,8,12,20]和[8,9,11,12],两个集合的均值都是10,但显然两个集合差别是很大的,计算两者的标准差,前者是8.3,后者是1.8,显然后者较为集中,故其标准差小一些,标准差描述的就是这种“散布度”。
看出方差与标准差关系没有? 为什么除以n-1而不是除以n? 这个称为贝塞尔修正。在统计学中样本的均差多是除以自由度(n-1),它的意思是样本能自由选择的程度,当选到只剩一个时,它不可能再有自由了,所以自由度是(n-1)。这样能使我们以较小的样本集更好的逼近总体的标准差,即统计上所谓的“无偏估计”。
下面采用Python演算一下: 参考:https://blog.csdn.net/lyl771857509/article/details/79439184
> import numpy as np
> x=[1,2,3,4]
> print(np.cov(x))</pre>
显示结果: 1.6666666666666665
协方差
上面几个统计量看似已经描述的差不多了,但我们应该注意到,标准差和方差一般是用来描述一维数据的,但现实生活我们常常遇到含有多维数据的数据集,这个时候怎么办? 协方差该出场了! 协方差可以通俗的理解为:两个变量在变化过程中是同方向变化?还是反方向变化?同向或反向程度如何?
- 你变大,同时我也变大,说明两个变量是同向变化的,这时协方差就是正的。
- 你变大,同时我变小,说明两个变量是反向变化的,这时协方差就是负的。 从数值来看,协方差的数值越大,两个变量同向程度也就越大。反之亦然。
换种说法: 协方差是度量各个维度偏离其均值的程度。协方差的值如果为正值,则说明两者是正相关的,结果为负值就说明负相关的,如果为0,也是就是统计上说的“相互独立”。 与方差对比: 方差是用来度量单个变量“自身变异”大小的总体参数,方差越大表明该变量的变异越大 协方差是用来度量两个变量之间“协同变异”大小的总体参数,即二个变量相互影响大小的参数,协方差的绝对值越大,则二个变量相互影响越大。
采用协方差在线计算器练习一下: 输入值 X=1 ,5 ,6 输入值 Y=4, 2, 9
数目输入 3 X 平均值 4 Y 平均值 5 协方差(X,Y) 4
计算步骤:
总和(X) =1 + 5 + 6 = 12 X平均值 = 4 总和(Y) =4 + 2 + 9 = 15 Y平均值 = 5 协方差(X,Y) = 总和(xi - x平均值)(yi - y平均值)/(采样大小 -1) = (1-4)(4-5)+(5-4)(2-5)+(6-4)(9-5))/2 = 4
为了便于理解和验证,可以参考一下,http://www.ab126.com/shuxue/2788.html所提供的协方差的在线计算器。
矩阵维数
在分析协方差矩阵之前有必要搞清矩阵维数的概念!以女孩子找对象为例,一般关心几个点
这里是5个维数。如果同时有几个男孩子备选,则会形成多个行,有对比才有会伤害。
可以这样形象理解:在女孩心中,多个男孩形成一个个行向量,即多个样本。 另外,再回忆一下系数矩阵的来历。含有n个未知量,由m个方程组成线性方程组的一般形式为:
将系数按它们的位置排列形成一个表格:
这个表格就是方程组的系数矩阵,它的维数是由未知量个数即n来决定的。 下面介绍的协方差矩阵仅与维数有关,和样本数量无关。
协方差矩阵
还是有点抽象??? 那就结合实例来理解,可能更方便一些。 假定有下列矩阵:
我们来计算一下协方差矩阵。
X=np.array([[1,4,4,4] ,[5,3,2,7 ],[6,9,9,2]])
print(np.cov(X, rowvar=False))
对于矩阵来说,matlab把每行看做一个观察值,把每列当做一个变量,也就是说对于一个4×3的矩阵求协方差矩阵,matlab会认为存在三个变量,即会求出一个3×3的协方差矩阵。
而Python-NumPy的cov情况略有不同,它默认将每一行视为一个独立的变量,所以在上面的例子中,采用rowvar=False使其视每列为一个变量。
总和(X) =1 + 5 + 6 = 12 X平均值 = 4 总和(Y) =4 + 3 + 9 = 16 Y平均值 = 5.333 协方差(X,Y) = 总和(xi - x平均值)(yi - y平均值)/(采样大小 -1) = (1-4)(4-5.333)+(5-4)(3-5.333)+(6-4)(9-5.333))/2 = 4.5
总和(X) =4 + 3 + 9 = 16 X平均值 = 5.333 总和(Y) =4 + 7 + 2 = 13 Y平均值 = 4.333 协方差(X,Y) = 总和(xi - x平均值)(yi - y平均值)/(采样大小 -1) = (4-5.333)(4-4.333)+(3-5.333)(7-4.333)+(9-5.333)(2-4.333))/2 = -7.167
相关系数
相关系数是用以反映变量之间相关关系密切程度的统计指标。相关系数也可以看成协方差:一种剔除了两个变量量纲影响、标准化后的特殊协方差,它消除了两个变量变化幅度的影响,而只是单纯反应两个变量每单位变化时的相似程度。
相关系数绝对值是 小于等于1的。自性关系数为 1 .
使用NumPy包计算
import numpy as np
# 随机生成两个样本
x = np.random.randint(0, 9, 1000)
y = np.random.randint(0, 9, 1000)
# 计算平均值
mx = x.mean()
my = y.mean()
# 计算标准差
stdx = x.std()
stdy = y.std()
# 计算协方差矩阵
covxy = np.cov(x, y)
print(covxy)
# 我们可以手动进行验证
# covx等于covxy[0, 0], covy等于covxy[1, 1]
# 我们这里的计算结果应该是约等于,因为我们在计算的时候是使用的总体方差(总体方差和样本方差是稍微有点区别的)
covx = np.mean((x - x.mean()) ** 2)
covy = np.mean((y - y.mean()) ** 2)
print(covx)
print(covy)
coef0 = covxy[0, 1] / (stdx * stdy)
print(coef0)
# 这里计算的covxy等于上面的covxy[0, 1]和covxy[1, 0],三者相等
covxy = np.mean((x - x.mean()) * (y - y.mean()))
print(covxy)
# 下面计算的是相关系数矩阵(和上面的协方差矩阵是类似的)
coefxy = np.corrcoef(x, y)
print(coefxy)
[[6.85792893 0.00917317]
[0.00917317 6.88345946]]
6.851071
6.876575999999999
0.0013364546751036613
0.009164000000000101
[[1. 0.00133512]
[0.00133512 1. ]]
10.Pandas筛选操作
在数据处理过程中,经常会遇到要筛选不同要求的数据,通过 Pandas 可以轻松时间,这一篇我们来看下如何使用 Pandas 来完成数据筛选吧。
# 导入相关库
import numpy as np
import pandas as pd
Pandas 中除了支持 Python 和 Numpy 的索引运算符[]和属性运算符.来访问数据之外,还有很多其他的方式来访问数据,我们一起来看看吧。
index = pd.Index(data=["Tom", "Bob", "Mary", "James", "Andy", "Alice"], name="name")
data = {
"age": [18, 30, np.nan, 40, np.nan, 30],
"city": ["Bei Jing ", "Shang Hai ", "Guang Zhou", "Shen Zhen", np.nan, " "],
"sex": [None, "male", "female", "male", np.nan, "unknown"],
"birth": ["2000-02-10", "1988-10-17", None, "1978-08-08", np.nan, "1988-10-17"]
}
user_info = pd.DataFrame(data=data, index=index)
# 将出生日期转为时间戳
user_info["birth"] = pd.to_datetime(user_info.birth)
user_info
name | age | birth | city | sex |
---|---|---|---|---|
Tom | 18.0 | 2000-02-10 | Bei Jing | None |
Bob | 30.0 | 1988-10-17 | Shang Hai | male |
Mary | NaN | NaT | Guang Zhou | female |
James | 40.0 | 1978-08-08 | Shen Zhen | male |
Andy | NaN | NaT | NaN | NaN |
Alice | 30.0 | 1988-10-17 | unknown |
字典式 get 访问
我们都知道,Python 中的字典要获取 value 时可以通过 get
方法来获取,对于 Series 和 DataFrame 也一样,他们一样可以通过 get
方法来获取。
# 获取得到所有年龄相关的这一列的信息,结果为一个 Series
user_info.get("age")
name
Tom 18.0
Bob 30.0
Mary NaN
James 40.0
Andy NaN
Alice 30.0
Name: age, dtype: float64
# 从包含所有的年龄信息的 Series 中得到 Tom 的年龄
user_info.get("age").get("Tom")
18.0
属性访问
除了可以通过 get
方法来获取数据之外,还可以通过属性的方式来访问,同样完成上面的功能,来看下如何通过属性访问的方式来实现。
# 获取得到所有年龄相关的这一列的信息,结果为一个 Series
user_info.age
name
Tom 18.0
Bob 30.0
Mary NaN
James 40.0
Andy NaN
Alice 30.0
Name: age, dtype: float64
# 从包含所有的年龄信息的 Series 中得到 Tom 的年龄
user_info.age.Tom
18.0
切片操作
在学习 Python 时,会发现列表的切片操作非常地方便,Series 和 DataFrame 同样也有切片操作。
对于 Series 来说,通过切片可以完成选择指定的行,对于 Series 来说,通过切片可以完成选择指定的行或者列,来看看怎么玩吧。
# 获取年龄的前两行
user_info.age[:2]
name
Tom 18.0
Bob 30.0
Name: age, dtype: float64
# 获取所有信息的前两行
user_info[:2]
name | age | birth | city | sex |
---|---|---|---|---|
Tom | 18.0 | 2000-02-10 | Bei Jing | None |
Bob | 30.0 | 1988-10-17 | Shang Hai | male |
# 所有信息每两行选择一次数据
user_info[::2]
name | age | birth | city | sex |
---|---|---|---|---|
Tom | 18.0 | 2000-02-10 | Bei Jing | None |
Mary | NaN | NaT | Guang Zhou | female |
Andy | NaN | NaT | NaN | NaN |
# 对所有信息进行反转
user_info[::-1]
name | age | birth | city | sex |
---|---|---|---|---|
Alice | 30.0 | 1988-10-17 | unknown | |
Andy | NaN | NaT | NaN | NaN |
James | 40.0 | 1978-08-08 | Shen Zhen | male |
Mary | NaN | NaT | Guang Zhou | female |
Bob | 30.0 | 1988-10-17 | Shang Hai | male |
Tom | 18.0 | 2000-02-10 | Bei Jing | None |
上面都是筛选行,如何筛选 DataFrame 中的列呢?
只需要将列名传入切片即可完成筛选。
user_info["age"]
name
Tom 18.0
Bob 30.0
Mary NaN
James 40.0
Andy NaN
Alice 30.0
Name: age, dtype: float64
如何筛选出多列的数据呢?只需要将对应的列名传入组成一个列表,传入切片中即可。
user_info[["city", "age"]]
name | city | age |
---|---|---|
Tom | Bei Jing | 18.0 |
Bob | Shang Hai | 30.0 |
Mary | Guang Zhou | NaN |
James | Shen Zhen | 40.0 |
Andy | NaN | NaN |
Alice | 30.0 |
可以看到,列表中的列名的顺序会影响最后的结果。
通过数字筛选行和列
通过切片操作可以完成筛选行或者列,如何同时筛选出行和列呢?
通过 iloc
即可实现, iloc
支持传入行和列的筛选器,并用 , 隔开。无论是行或者里筛选器,都可以为以下几种情况:
- 一个整数,如 2
- 一个整数列表,如 [2, 1, 4]
- 一个整数切片对象,如 2:4
- 一个布尔数组
- 一个callable
先来看下前3种的用法。
# 筛选出第一行数据
user_info.iloc[0]
age 18
birth 2000-02-10 00:00:00
city Bei Jing
sex None
Name: Tom, dtype: object
# 筛选出第二行第一列的数据
user_info.iloc[1, 0]
30.0
# 筛选出第二行、第一行、第三行对应的第一列的数据
user_info.iloc[[1, 0, 2], 0]
name
Bob 30.0
Tom 18.0
Mary NaN
Name: age, dtype: float64
# 筛选出第一行至第三行以及第一列至第二列的数据
user_info.iloc[0:3, 0:2]
name | age | birth |
---|---|---|
Tom | 18.0 | 2000-02-10 |
Bob | 30.0 | 1988-10-17 |
Mary | NaN | NaT |
# 筛选出第一列至第二列的数据
user_info.iloc[:, 0:2]
name | age | birth |
---|---|---|
Tom | 18.0 | 2000-02-10 |
Bob | 30.0 | 1988-10-17 |
Mary | NaN | NaT |
James | 40.0 | 1978-08-08 |
Andy | NaN | NaT |
Alice | 30.0 | 1988-10-17 |
通过名称筛选行和列
虽然通过 iloc
可以实现同时筛选出行和列,但是它接收的是输入,非常不直观, 通过 loc
可实现传入名称来筛选数据,loc
支持传入行和列的筛选器,并用 , 隔开。无论是行或者里筛选器,都可以为以下几种情况:
- 一个索引的名称,如:"Tom"
- 一个索引的列表,如:["Bob", "Tom"]
- 一个标签范围,如:"Tom": "Mary"
- 一个布尔数组
- 一个callable
先来看下前3种的用法。
# 筛选出名称为 Tom 的数据一行数据
user_info.loc["Tom"]
age 18
birth 2000-02-10 00:00:00
city Bei Jing
sex None
Name: Tom, dtype: object
# 筛选出名称为 Tom 的年龄
user_info.loc["Tom", "age"]
18.0
# 筛选出名称在 ["Bob", "Tom"] 中的两行数据
user_info.loc[["Bob", "Tom"]]
name | age | birth | city | sex |
---|---|---|---|---|
Bob | 30.0 | 1988-10-17 | Shang Hai | male |
Tom | 18.0 | 2000-02-10 | Bei Jing | None |
# 筛选出索引名称在 Tom 到 Mary 之间的数据
user_info.loc["Tom": "Mary"]
name | age | birth | city | sex |
---|---|---|---|---|
Tom | 18.0 | 2000-02-10 | Bei Jing | None |
Bob | 30.0 | 1988-10-17 | Shang Hai | male |
Mary | NaN | NaT | Guang Zhou | female |
# 筛选出年龄这一列数据
user_info.loc[:, ["age"]]
name | age |
---|---|
Tom | 18.0 |
Bob | 30.0 |
Mary | NaN |
James | 40.0 |
Andy | NaN |
Alice | 30.0 |
# 筛选出所有 age 到 birth 之间的这几列数据
user_info.loc[:, "age": "birth"]
name | age | birth |
---|---|---|
Tom | 18.0 | 2000-02-10 |
Bob | 30.0 | 1988-10-17 |
Mary | NaN | NaT |
James | 40.0 | 1978-08-08 |
Andy | NaN | NaT |
Alice | 30.0 | 1988-10-17 |
你可能已经发现了,通过名称来筛选时,传入的切片是左右都包含的。
布尔索引
通过布尔操作我们一样可以进行筛选操作,布尔操作时,&
对应 and
,|
对应 or
,~
对应 not
。
当有多个布尔表达式时,需要通过小括号来进行分组。
user_info[user_info.age > 20]
name | age | birth | city | sex |
---|---|---|---|---|
Bob | 30.0 | 1988-10-17 | Shang Hai | male |
James | 40.0 | 1978-08-08 | Shen Zhen | male |
Alice | 30.0 | 1988-10-17 | unknown |
# 筛选出年龄在20岁以上,并且性别为男性的数据
user_info[(user_info.age > 20) & (user_info.sex == "male")]
name | age | birth | city | sex |
---|---|---|---|---|
Bob | 30.0 | 1988-10-17 | Shang Hai | male |
James | 40.0 | 1978-08-08 | Shen Zhen | male |
# 筛选出性别不为 unknown 的数据
user_info[~(user_info.sex == "unknown")]
name | age | birth | city | sex |
---|---|---|---|---|
Tom | 18.0 | 2000-02-10 | Bei Jing | None |
Bob | 30.0 | 1988-10-17 | Shang Hai | male |
Mary | NaN | NaT | Guang Zhou | female |
James | 40.0 | 1978-08-08 | Shen Zhen | male |
Andy | NaN | NaT | NaN | NaN |
除了切片操作可以实现之外, loc
一样可以实现。
user_info.loc[user_info.age > 20, ["age"]]
name | age |
---|---|
Bob | 30.0 |
James | 40.0 |
Alice | 30.0 |
isin 筛选
Series 包含了 isin
方法,它能够返回一个布尔向量,用于筛选数据。
# 筛选出性别属于 male 和 female的数据
user_info[user_info.sex.isin(["male", "female"])]
name | age | birth | city | sex |
---|---|---|---|---|
Bob | 30.0 | 1988-10-17 | Shang Hai | male |
Mary | NaN | NaT | Guang Zhou | female |
James | 40.0 | 1978-08-08 | Shen Zhen | male |
对于索引来说,一样可以使用 isin
方法来筛选。
user_info[user_info.index.isin(["Bob"])]
name | age | birth | city | sex |
---|---|---|---|---|
Bob | 30.0 | 1988-10-17 | Shang Hai | male |
通过Callable筛选
loc
、iloc
、切片操作都支持接收一个 callable 函数,callable必须是带有一个参数(调用Series,DataFrame)的函数,并且返回用于索引的有效输出。
user_info[lambda df: df["age"] > 20]
name | age | birth | city | sex |
---|---|---|---|---|
Bob | 30.0 | 1988-10-17 | Shang Hai | male |
James | 40.0 | 1978-08-08 | Shen Zhen | male |
Alice | 30.0 | 1988-10-17 | unknown |
user_info.loc[lambda df: df.age > 20, lambda df: ["age"]]
name | age |
---|---|
Bob | 30.0 |
James | 40.0 |
Alice | 30.0 |
user_info.iloc[lambda df: [0, 5], lambda df: [0]]
name | age |
---|---|
Tom | 18.0 |
Alice | 30.0 |
11-Pandas分组聚合
# 导入相关库
import numpy as np
import pandas as pd
index = pd.Index(data=["Tom", "Bob", "Mary", "James", "Andy", "Alice"], name="name")
data = {
"age": [18, 30, 35, 18, np.nan, 30],
"city": ["Bei Jing ", "Shang Hai ", "Guang Zhou", "Shen Zhen", np.nan, " "],
"sex": ["male", "male", "female", "male", np.nan, "female"],
"income": [3000, 8000, 8000, 4000, 6000, 7000]
}
user_info = pd.DataFrame(data=data, index=index)
user_info
name | age | city | income | sex |
---|---|---|---|---|
Tom | 18.0 | Bei Jing | 3000 | male |
Bob | 30.0 | Shang Hai | 8000 | male |
Mary | 35.0 | Guang Zhou | 8000 | female |
James | 18.0 | Shen Zhen | 4000 | male |
Andy | NaN | NaN | 6000 | NaN |
Alice | 30.0 | 7000 | female |
将对象分割成组
在进行分组统计前,首先要做的就是进行分组。既然是分组,就需要依赖于某个信息。
比如,依据性别来分组。直接调用 user_info.groupby(user_info["sex"])
即可完成按照性别分组。
grouped = user_info.groupby(user_info["sex"])
grouped.groups
{'female': Index(['Mary', 'Alice'], dtype='object', name='name'),
'male': Index(['Tom', 'Bob', 'James'], dtype='object', name='name')}
可以看到,已经能够正确的按照性别来进行分组了。通常我们为了更简单,会使用这种方式来实现相同的功能:user_info.groupby("sex")
。
grouped = user_info.groupby("sex")
grouped.groups
{'female': Index(['Mary', 'Alice'], dtype='object', name='name'),
'male': Index(['Tom', 'Bob', 'James'], dtype='object', name='name')}
你可能会想,能不能先按照性别来分组,再按照年龄进一步分组呢?答案是可以的,看这里。
grouped = user_info.groupby(["sex", "age"])
grouped.groups
{('female', 30.0): Index(['Alice'], dtype='object', name='name'),
('female', 35.0): Index(['Mary'], dtype='object', name='name'),
('male', 18.0): Index(['Tom', 'James'], dtype='object', name='name'),
('male', 30.0): Index(['Bob'], dtype='object', name='name'),
(nan, nan): Index(['Andy'], dtype='object', name='name')}
关闭排序
默认情况下,groupby
会在操作过程中对数据进行排序。如果为了更好的性能,可以设置 sort=False
。
grouped = user_info.groupby(["sex", "age"], sort=False)
grouped.groups
{('female', 30.0): Index(['Alice'], dtype='object', name='name'),
('female', 35.0): Index(['Mary'], dtype='object', name='name'),
('male', 18.0): Index(['Tom', 'James'], dtype='object', name='name'),
('male', 30.0): Index(['Bob'], dtype='object', name='name'),
(nan, nan): Index(['Andy'], dtype='object', name='name')}
选择列
在使用 groupby
进行分组后,可以使用切片 []
操作来完成对某一列的选择。
grouped = user_info.groupby("sex")
grouped
<pandas.core.groupby.DataFrameGroupBy object at 0x000002E355D787B8>
grouped["city"]
<pandas.core.groupby.SeriesGroupBy object at 0x000002E355D78470>
遍历分组
在对数据进行分组后,可以进行遍历。
grouped = user_info.groupby("sex")
for name, group in grouped:
print("name: {}".format(name))
print("group: {}".format(group))
print("--------------")
name: female
group: age city income sex
name
Mary 35.0 Guang Zhou 8000 female
Alice 30.0 7000 female
--------------
name: male
group: age city income sex
name
Tom 18.0 Bei Jing 3000 male
Bob 30.0 Shang Hai 8000 male
James 18.0 Shen Zhen 4000 male
--------------
如果是根据多个字段来分组的,每个组的名称是一个元组。
grouped = user_info.groupby(["sex", "age"])
for name, group in grouped:
print("name: {}".format(name))
print("group: {}".format(group))
print("--------------")
name: ('female', 30.0)
group: age city income sex
name
Alice 30.0 7000 female
--------------
name: ('female', 35.0)
group: age city income sex
name
Mary 35.0 Guang Zhou 8000 female
--------------
name: ('male', 18.0)
group: age city income sex
name
Tom 18.0 Bei Jing 3000 male
James 18.0 Shen Zhen 4000 male
--------------
name: ('male', 30.0)
group: age city income sex
name
Bob 30.0 Shang Hai 8000 male
--------------
选择一个组
分组后,我们可以通过 get_group
方法来选择其中的某一个组。
grouped = user_info.groupby("sex")
grouped.get_group("male")
name | age | city | income | sex |
---|---|---|---|---|
Tom | 18.0 | Bei Jing | 3000 | male |
Bob | 30.0 | Shang Hai | 8000 | male |
James | 18.0 | Shen Zhen | 4000 | male |
user_info.groupby(["sex", "age"]).get_group(("male", 18))
name | age | city | income | sex |
---|---|---|---|---|
Tom | 18.0 | Bei Jing | 3000 | male |
James | 18.0 | Shen Zhen | 4000 | male |
聚合
分组的目的是为了统计,统计的时候需要聚合,所以我们需要在分完组后来看下如何进行聚合。常见的一些聚合操作有:计数、求和、最大值、最小值、平均值等。
想要实现聚合操作,一种方式就是调用 agg
方法。
# 获取不同性别下所包含的人数
grouped = user_info.groupby("sex")
grouped["age"].agg(len)
sex
female 2.0
male 3.0
Name: age, dtype: float64
# 获取不同性别下包含的最大的年龄
grouped = user_info.groupby("sex")
grouped["age"].agg(np.max)
sex
female 35.0
male 30.0
Name: age, dtype: float64
如果是根据多个键来进行聚合,默认情况下得到的结果是一个多层索引结构。
grouped = user_info.groupby(["sex", "age"])
rs = grouped.agg(len)
rs
sex | age | city | income |
---|---|---|---|
female | 30.0 | 1 | 1 |
35.0 | 1 | 1 | |
male | 18.0 | 2 | 2 |
30.0 | 1 | 1 |
有两种方式可以避免出现多层索引,先来介绍第一种。对包含多层索引的对象调用 reset_index
方法。
rs.reset_index()
sex | age | city | income | |
---|---|---|---|---|
0 | female | 30.0 | 1 | 1 |
1 | female | 35.0 | 1 | 1 |
2 | male | 18.0 | 2 | 2 |
3 | male | 30.0 | 1 | 1 |
另外一种方式是在分组时,设置参数 as_index=False
。
grouped = user_info.groupby(["sex", "age"], as_index=False)
grouped.agg(len)
sex | age | city | income | |
---|---|---|---|---|
0 | female | 30.0 | 1 | 1 |
1 | female | 35.0 | 1 | 1 |
2 | male | 18.0 | 2 | 2 |
3 | male | 30.0 | 1 | 1 |
Series 和 DataFrame 都包含了 describe
方法,我们分组后一样可以使用 describe
方法来查看数据的情况。
grouped = user_info.groupby("sex")
grouped.describe()
一次应用多个聚合操作
有时候进行分组后,不单单想得到一个统计结果,有可能是多个。比如想统计出不同性别下的一个收入的总和和平均值。
grouped = user_info.groupby("sex")
grouped["income"].agg([np.sum, np.mean])
sex | sum | mean |
---|---|---|
female | 15000 | 7500 |
male | 15000 | 5000 |
如果想将统计结果进行重命名,可以传入字典。
grouped = user_info.groupby("sex")
grouped["income"].agg([np.sum, np.mean]).rename(columns={"sum": "income_sum", "mean": "income_mean"})
sex | income_sum | income_mean |
---|---|---|
female | 15000 | 7500 |
male | 15000 | 5000 |
对DataFrame列应用不同的聚合操作
有时候可能需要对不同的列使用不同的聚合操作。例如,想要统计不同性别下人群的年龄的均值以及收入的总和。
grouped = user_info.groupby("sex")
grouped.agg({"age": np.mean, "income": np.sum}).rename(columns={"age": "age_mean", "income": "income_sum"})
sex | age_mean | income_sum |
---|---|---|
female | 32.5 | 15000 |
male | 22.0 | 15000 |
transform 操作
前面进行聚合运算的时候,得到的结果是一个以分组名作为索引的结果对象。虽然可以指定 as_index=False
,但是得到的索引也并不是元数据的索引。如果我们想使用原数组的索引的话,就需要进行 merge 转换。
transform
方法简化了这个过程,它会把 func 参数应用到所有分组,然后把结果放置到原数组的索引上(如果结果是一个标量,就进行广播)
# 通过 agg 得到的结果的索引是分组名
grouped = user_info.groupby("sex")
grouped["income"].agg(np.mean)
sex
female 7500
male 5000
Name: income, dtype: int64
# 通过 transform 得到的结果的索引是原始索引,它会将得到的结果自动关联上原始的索引
grouped = user_info.groupby("sex")
grouped["income"].transform(np.mean)
name
Tom 5000.0
Bob 5000.0
Mary 7500.0
James 5000.0
Andy NaN
Alice 7500.0
Name: income, dtype: float64
可以看到,通过 transform
操作得到的结果的长度与原来保持一致。
apply 操作
除了 transform
操作外,还有更神奇的 apply
操作。
apply
会将待处理的对象拆分成多个片段,然后对各片段调用传入的函数,最后尝试用 pd.concat()
把结果组合起来。func 的返回值可以是 Pandas 对象或标量,并且数组对象的大小不限。
# 使用 apply 来完成上面的聚合
grouped = user_info.groupby("sex")
grouped["income"].apply(np.mean)
sex
female 7500.0
male 5000.0
Name: income, dtype: float64
来看下 apply
不一样的用法吧。
比如想要统计不同性别最高收入的前n个值,可以通过下面这种方式实现。
def f1(ser, num=2):
return ser.nlargest(num).tolist()
grouped["income"].apply(f1)
sex
female [8000, 7000]
male [8000, 4000]
Name: income, dtype: object
另外,如果想要获取不同性别下的年龄的均值,通过 apply
可以如下实现。
def f2(df):
return df["age"].mean()
grouped.apply(f2)
sex
female 32.5
male 22.0
dtype: float64
12-Pandas转换连接
拼接
有两个DataFrame,都存储了用户的一些信息,现在要拼接起来,组成一个DataFrame,如何实现呢?
data1 = {
"name": ["Tom", "Bob"],
"age": [18, 30],
"city": ["Bei Jing ", "Shang Hai "]
}
df1 = pd.DataFrame(data=data1)
df1
age | city | name | |
---|---|---|---|
0 | 18 | Bei Jing | Tom |
1 | 30 | Shang Hai | Bob |
data2 = {
"name": ["Mary", "James"],
"age": [35, 18],
"city": ["Guang Zhou", "Shen Zhen"]
}
df2 = pd.DataFrame(data=data2)
df2
age | city | name | |
---|---|---|---|
0 | 35 | Guang Zhou | Mary |
1 | 18 | Shen Zhen | James |
append
append
是最简单的拼接两个DataFrame的方法。
df1.append(df2)
age | city | name | |
---|---|---|---|
0 | 18 | Bei Jing | Tom |
1 | 30 | Shang Hai | Bob |
0 | 35 | Guang Zhou | Mary |
1 | 18 | Shen Zhen | James |
可以看到,拼接后的索引默认还是原有的索引,如果想要重新生成索引的话,设置参数 ignore_index=True
即可。
df1.append(df2, ignore_index=True)
age | city | name | |
---|---|---|---|
0 | 18 | Bei Jing | Tom |
1 | 30 | Shang Hai | Bob |
2 | 35 | Guang Zhou | Mary |
3 | 18 | Shen Zhen | James |
concat
除了 append
这种方式之外,还有 concat
这种方式可以实现相同的功能。
objs=[df1, df2]
pd.concat(objs, ignore_index=True)
age | city | name | |
---|---|---|---|
0 | 18 | Bei Jing | Tom |
1 | 30 | Shang Hai | Bob |
2 | 35 | Guang Zhou | Mary |
3 | 18 | Shen Zhen | James |
如果想要区分出不同的DataFrame的数据,可以通过设置参数 keys
,当然得设置参数 ignore_index=False
。
pd.concat(objs, ignore_index=False, keys=["df1", "df2"])
age | city | name | ||
---|---|---|---|---|
df1 | 0 | 18 | Bei Jing | Tom |
1 | 30 | Shang Hai | Bob | |
df2 | 0 | 35 | Guang Zhou | Mary |
1 | 18 | Shen Zhen | James |
关联
有两个DataFrame,分别存储了用户的部分信息,现在需要将用户的这些信息关联起来,如何实现呢?
data1 = {
"name": ["Tom", "Bob", "Mary", "James"],
"age": [18, 30, 35, 18],
"city": ["Bei Jing ", "Shang Hai ", "Guang Zhou", "Shen Zhen"]
}
df1 = pd.DataFrame(data=data1)
df1
age | city | name | |
---|---|---|---|
0 | 18 | Bei Jing | Tom |
1 | 30 | Shang Hai | Bob |
2 | 35 | Guang Zhou | Mary |
3 | 18 | Shen Zhen | James |
data2 = {"name": ["Bob", "Mary", "James", "Andy"],
"sex": ["male", "female", "male", np.nan],
"income": [8000, 8000, 4000, 6000]
}
df2 = pd.DataFrame(data=data2)
df2
income | name | sex | |
---|---|---|---|
0 | 8000 | Bob | male |
1 | 8000 | Mary | female |
2 | 4000 | James | male |
3 | 6000 | Andy | NaN |
merge
通过 pd.merge
可以关联两个DataFrame,这里我们设置参数 on="name"
,表示依据 name
来作为关联键。
pd.merge(df1, df2, on="name")
age | city | name | income | sex | |
---|---|---|---|---|---|
0 | 30 | Shang Hai | Bob | 8000 | male |
1 | 35 | Guang Zhou | Mary | 8000 | female |
2 | 18 | Shen Zhen | James | 4000 | male |
关联后发现数据变少了,只有 3 行数据,这是因为默认关联的方式是 inner
,如果不想丢失任何数据,可以设置参数 how="outer"
。
pd.merge(df1, df2, on="name", how="outer")
age | city | name | income | sex | |
---|---|---|---|---|---|
0 | 18.0 | Bei Jing | Tom | NaN | NaN |
1 | 30.0 | Shang Hai | Bob | 8000.0 | male |
2 | 35.0 | Guang Zhou | Mary | 8000.0 | female |
3 | 18.0 | Shen Zhen | James | 4000.0 | male |
4 | NaN | NaN | Andy | 6000.0 | NaN |
可以看到,设置参数 how="outer" 后,确实不会丢失任何数据,他会在不存在的地方填为缺失值。
如果我们想保留左边所有的数据,可以设置参数 how="left"
;反之,如果想保留右边的所有数据,可以设置参数 how="right"
pd.merge(df1, df2, on="name", how="left")
age | city | name | income | sex | |
---|---|---|---|---|---|
0 | 18 | Bei Jing | Tom | NaN | NaN |
1 | 30 | Shang Hai | Bob | 8000.0 | male |
2 | 35 | Guang Zhou | Mary | 8000.0 | female |
3 | 18 | Shen Zhen | James | 4000.0 | male |
有时候,两个 DataFrame 中需要关联的键的名称不一样,可以通过 left_on
和 right_on
来分别设置。
df1.rename(columns={"name": "name1"}, inplace=True)
df1
age | city | name1 | |
---|---|---|---|
0 | 18 | Bei Jing | Tom |
1 | 30 | Shang Hai | Bob |
2 | 35 | Guang Zhou | Mary |
3 | 18 | Shen Zhen | James |
df2.rename(columns={"name": "name2"}, inplace=True)
df2
income | name2 | sex | |
---|---|---|---|
0 | 8000 | Bob | male |
1 | 8000 | Mary | female |
2 | 4000 | James | male |
3 | 6000 | Andy | NaN |
pd.merge(df1, df2, left_on="name1", right_on="name2")
age | city | name1 | income | name2 | sex | |
---|---|---|---|---|---|---|
0 | 30 | Shang Hai | Bob | 8000 | Bob | male |
1 | 35 | Guang Zhou | Mary | 8000 | Mary | female |
2 | 18 | Shen Zhen | James | 4000 | James | male |
有时候,两个DataFrame中都包含相同名称的字段,如何处理呢?
我们可以设置参数 suffixes
,默认 suffixes=('_x', '_y')
表示将相同名称的左边的DataFrame的字段名加上后缀 _x
,右边加上后缀 _y
。
df1["sex"] = "male"
df1
age | city | name1 | sex | |
---|---|---|---|---|
0 | 18 | Bei Jing | Tom | male |
1 | 30 | Shang Hai | Bob | male |
2 | 35 | Guang Zhou | Mary | male |
3 | 18 | Shen Zhen | James | male |
pd.merge(df1, df2, left_on="name1", right_on="name2")
age | city | name1 | sex_x | income | name2 | sex_y | |
---|---|---|---|---|---|---|---|
0 | 30 | Shang Hai | Bob | male | 8000 | Bob | male |
1 | 35 | Guang Zhou | Mary | male | 8000 | Mary | female |
2 | 18 | Shen Zhen | James | male | 4000 | James | male |
pd.merge(df1, df2, left_on="name1", right_on="name2", suffixes=("_left", "_right"))
age | city | name1 | sex_left | income | name2 | sex_right | |
---|---|---|---|---|---|---|---|
0 | 30 | Shang Hai | Bob | male | 8000 | Bob | male |
1 | 35 | Guang Zhou | Mary | male | 8000 | Mary | female |
2 | 18 | Shen Zhen | James | male | 4000 | James | male |
join
除了 merge
这种方式外,还可以通过 join
这种方式实现关联。相比 merge
,join
这种方式有以下几个不同:
- 默认参数
on=None
,表示关联时使用左边和右边的索引作为键,设置参数on
可以指定的是关联时左边的所用到的键名 - 左边和右边字段名称重复时,通过设置参数
lsuffix
和rsuffix
来解决。
df1.join(df2.set_index("name2"), on="name1", lsuffix="_left")
age | city | name1 | sex_left | income | sex | |
---|---|---|---|---|---|---|
0 | 18 | Bei Jing | Tom | male | NaN | NaN |
1 | 30 | Shang Hai | Bob | male | 8000.0 | male |
2 | 35 | Guang Zhou | Mary | male | 8000.0 | female |
3 | 18 | Shen Zhen | James | male | 4000.0 | male |
13-Pandas-IO-操作详解
数据分析过程中经常需要进行读写操作,Pandas实现了很多 IO 操作的API,这里简单做了一个列举。
格式类型 | 数据描述 | Reader | Writer |
---|---|---|---|
text | CSV | read_csv | to_csv |
text | JSON | read_json | to_json |
text | HTML | read_html | to_html |
text | clipboard | read_clipboard | to_clipboard |
binary | Excel | read_excel | to_excel |
binary | HDF5 | read_hdf | to_hdf |
binary | Feather | read_feather | to_feather |
binary | Msgpack | read_msgpack | to_msgpack |
binary | Stata | read_stata | to_stata |
binary | SAS | read_sas | |
binary | Python Pickle | read_pickle | to_pickle |
SQL | SQL | read_sql | to_sql |
SQL | Google Big Query | read_gbq | to_gbq |
可以看到,Pandas 的 I/O API是像 pd.read_csv()
一样访问的一组顶级 reader
函数,相应的 writer
函数是像 df.to_csv()
那样访问的对象方法。
这里我们介绍几个常用的API。
# 导入相关库
import numpy as np
import pandas as pd
from io import StringIO
read_csv
读取 csv 文件算是一种最常见的操作了。假如已经有人将一些用户的信息记录在了一个csv文件中,我们如何通过 Pandas 读取呢?
读取之前先来看下这个文件里的内容吧。
!cat ../data/user_info.csv
name,age,birth,sex
Tom,18.0,2000-02-10,
Bob,30.0,1988-10-17,male
可以看到,一共有 4 列,分别是 name
, age
, birth
, sex
。我们可以直接使用 pd.read_csv
来读取。
pd.read_csv("../data/user_info.csv")
name | age | birth | sex | |
---|---|---|---|---|
0 | Tom | 18.0 | 2000-02-10 | NaN |
1 | Bob | 30.0 | 1988-10-17 | male |
可以看到,读取出来生成了一个 DataFrame,索引是自动创建的一个数字,我们可以设置参数 index_col
来将某列设置为索引,可以传入索引号或者名称。
pd.read_csv("../data/user_info.csv", index_col="name")
age | birth | sex | |
---|---|---|---|
name | |||
Tom | 18.0 | 2000-02-10 | NaN |
Bob | 30.0 | 1988-10-17 | male |
除了可以从文件中读取,我们还可以从 StringIO
对象中读取。
data="name,age,birth,sex\nTom,18.0,2000-02-10,\nBob,30.0,1988-10-17,male"
print(data)
name,age,birth,sex
Tom,18.0,2000-02-10,
Bob,30.0,1988-10-17,male
pd.read_csv(StringIO(data))
name | age | birth | sex | |
---|---|---|---|---|
0 | Tom | 18.0 | 2000-02-10 | NaN |
1 | Bob | 30.0 | 1988-10-17 | male |
当然了,你还可以设置参数 sep
来自定义字段之间的分隔符,设置参数 lineterminator
来自定义每行的分隔符。
data = "name|age|birth|sex~Tom|18.0|2000-02-10|~Bob|30.0|1988-10-17|male"
pd.read_csv(StringIO(data), sep="|", lineterminator="~")
name | age | birth | sex | |
---|---|---|---|---|
0 | Tom | 18.0 | 2000-02-10 | NaN |
1 | Bob | 30.0 | 1988-10-17 | male |
在读取时,解析器会进行类型推断,任何非数字列都会以对象dtype的形式出现。当然我们也可以自己指定数据类型。
pd.read_csv(StringIO(data), sep="|", lineterminator="~", dtype={"age": int})
name | age | birth | sex | |
---|---|---|---|---|
0 | Tom | 18 | 2000-02-10 | NaN |
1 | Bob | 30 | 1988-10-17 | male |
Pandas 默认将第一行作为标题,但是有时候,csv文件并没有标题,我们可以设置参数 names
来添加标题。
data="Tom,18.0,2000-02-10,\nBob,30.0,1988-10-17,male"
print(data)
Tom,18.0,2000-02-10,
Bob,30.0,1988-10-17,male
pd.read_csv(StringIO(data), names=["name", "age", "birth", "sex"])
name | age | birth | sex | |
---|---|---|---|---|
0 | Tom | 18.0 | 2000-02-10 | NaN |
1 | Bob | 30.0 | 1988-10-17 | male |
有时候可能只需要读取部分列的数据,可以指定参数 user_cols
data="name,age,birth,sex\nTom,18.0,2000-02-10,\nBob,30.0,1988-10-17,male"
print(data)
name,age,birth,sex
Tom,18.0,2000-02-10,
Bob,30.0,1988-10-17,male
pd.read_csv(StringIO(data), usecols=["name", "age"])
name | age | |
---|---|---|
0 | Tom | 18.0 |
1 | Bob | 30.0 |
关于缺失值的处理,也是有技巧的。默认参数 keep_default_na=False
,会将空值都填充为 NaN。
pd.read_csv(StringIO(data))
name | age | birth | sex | |
---|---|---|---|---|
0 | Tom | 18.0 | 2000-02-10 | NaN |
1 | Bob | 30.0 | 1988-10-17 | male |
pd.read_csv(StringIO(data), keep_default_na=False)
name | age | birth | sex | |
---|---|---|---|---|
0 | Tom | 18.0 | 2000-02-10 | |
1 | Bob | 30.0 | 1988-10-17 | male |
有时候,空值的定义比较广泛,假定我们认为 18 也是空值,那么将它加入到参数 na_values
中即可。
pd.read_csv(StringIO(data), na_values=[18])
name | age | birth | sex | |
---|---|---|---|---|
0 | Tom | NaN | 2000-02-10 | NaN |
1 | Bob | 30.0 | 1988-10-17 | male |
了解了 pd.read_csv
如何使用之后,to_csv
就非常方便了,这里就不做介绍了。
to_json
通常在得到了 DataFrame 之后,有时候我们需要将它转为一个 json 字符串,可以使用 to_json
来完成。
转换时,可以通过指定参数 orient
来输出不同格式的格式,之后以下几个参数:
split | 字典像索引 - > [索引],列 - > [列],数据 - > [值]} |
records | 列表像{[列 - >值},…,{列 - >值}] |
index | 字典像{索引 - > {列 - >值}} |
columns | 字典像{列 - > {索引 - >值}} |
values | 只是值数组 |
DataFrame 默认情况下使用 columns
这种形式,Series 默认情况下使用 index
这种形式。
设置为 columns
后会将数据作为嵌套JSON对象进行序列化,并将列标签作为主索引。
df = pd.read_csv("../data/user_info.csv", index_col="name")
df
age | birth | sex | |
---|---|---|---|
name | |||
Tom | 18.0 | 2000-02-10 | NaN |
Bob | 30.0 | 1988-10-17 | male |
print(df.to_json())
{"age":{"Tom":18.0,"Bob":30.0},"birth":{"Tom":"2000-02-10","Bob":"1988-10-17"},"sex":{"Tom":null,"Bob":"male"}}
设置为index
后会将数据作为嵌套JSON对象进行序列化,并将索引标签作为主索引。
print(df.to_json(orient="index"))
{"Tom":{"age":18.0,"birth":"2000-02-10","sex":null},"Bob":{"age":30.0,"birth":"1988-10-17","sex":"male"}}
设置为 records
后会将数据序列化为列 - >值记录的JSON数组,不包括索引标签。
print(df.to_json(orient="records"))
[{"age":18.0,"birth":"2000-02-10","sex":null},{"age":30.0,"birth":"1988-10-17","sex":"male"}]
设置为 values
后会将是一个仅用于嵌套JSON数组值,不包含列和索引标签。
print(df.to_json(orient="values"))
[[18.0,"2000-02-10",null],[30.0,"1988-10-17","male"]]
设置为 split
后会将序列化为包含值,索引和列的单独条目的JSON对象。
print(df.to_json(orient="split"))
{"columns":["age","birth","sex"],"index":["Tom","Bob"],"data":[[18.0,"2000-02-10",null],[30.0,"1988-10-17","male"]]}
对于 read_json
,这些参数也是同样的道理。
