21  Pandas:二维数据处理

Note

主要内容:

  1. DataFrame结构和创建
  2. 数据读取和列类型
  3. 行和列的选择与运算
  4. 值修改和数据清洗、数据合并
  5. 统计、分组和聚合
  6. 时间序列的部分操作
  7. 保存

21.1 前置: Jupyter Notebook

.py是Python的源代码文件。这里采用另一种文件,更加常用于数据分析:Jupyter Notebook,.ipynb

Why:单元格的输出,会直接显示在单元格的下方,比较方便看。但用起来和单元格分割的.py类似。

新建文件,扩展名改为.ipynb即可。

常见的分工:.ipynb写数据分析的过程;.py保存共用的代码,作为模块在ipynb中导入。

每个单元格可以是代码,也可以Markdown格式的文本。单元个的右下角可以选择。

“文本”的单元格,ctrl+enter可以看渲染的结果,双击可以回到编辑。

21.2 数据基本结构

Pandas处理二维表格,其中的DataFrame对象几乎可以理解为一个Excel表:有行、列、题头等等。

DataFrame可以视为是“按列”组织的:n个列的横向并排:每个列是一个Series对象。

一个Series表示“一列”,每个Series对象又有两个部件组成,索引index和值values,其中的值values, 是一个numpy的ndarray。

反过来,一个数据序列ndarray和一个与数据等长的索引index,组成一个列Series,多个Series横向合并,组成一个DataFrame。

(index + ndarray) -> Series

(index + Series + Series + …) -> DataFrame

# 惯例导入2个模块
import pandas as pd
import numpy as np

# 从字典创建,后面会详述
df = pd.DataFrame(dict(id=[1, 2, 3], name=["a", "b", "c"]))

df
id name
0 1 a
1 2 b
2 3 c
# 取某一列:得到一个Series,注意Series由index和value组成
x = df["name"]
print(x)
print(type(x))
0    a
1    b
2    c
Name: name, dtype: object
<class 'pandas.core.series.Series'>
# x的index
print(x.index)
print(type(x.index))
RangeIndex(start=0, stop=3, step=1)
<class 'pandas.core.indexes.range.RangeIndex'>
# x的value
print(x.values)
print(type(x.values))  # 是一个ndarray
['a' 'b' 'c']
<class 'numpy.ndarray'>

21.3 Series的创建

Series可以视为二维表格的一列。我们先用pd.Series()函数构建

# 使用pd.Series构建一列,参数可以是一个序列结构,比如一个List
x = pd.Series(["a", "b", "c", "d"])
x
0    a
1    b
2    c
3    d
dtype: object

此时默认的index(看左侧)是从0开始的整数,并且这一列没有名称。我们可以用索引访问Series中的值。

x[2]  # 注意2指的是index
'c'
# 当然也可以写入
x[2] = 99
x
0     a
1     b
2    99
3     d
dtype: object

创建的时候可以指定index和name,name后面会成为DataFrame(二维表格,即多个列Series的横向合并)中这一列的标题。

# 创建一个简单的 Series,指定index和name
grades = pd.Series(
    [85, 92, 78, 90], index=["Alice", "Bob", "Charlie", "David"], name="score"
)

# 打印 Series
print(grades)

# 通过索引获取数据
print("Alice的分数是:", grades["Alice"])
Alice      85
Bob        92
Charlie    78
David      90
Name: score, dtype: int64
Alice的分数是: 85

21.4 DataFrame的创建

# 从ndarray或者list创建

a = np.array([8, 6, 4, 2])
print(a)

df1 = pd.DataFrame(a)
df1
[8 6 4 2]
0
0 8
1 6
2 4
3 2
b = list("apple")
print(b)
df2 = pd.DataFrame(b, columns=["B"], index=list("abcde"))  # 指定columns和index
df2
['a', 'p', 'p', 'l', 'e']
B
a a
b p
c p
d l
e e
# 从字典创建

# 字典的每一个item会成为表格的一列,key会成为列标签(列的标题),value会成为列的值
x = dict(A=[1, 2, 3, 4, 5], B=list("APPLE"))
print(x)
df = pd.DataFrame(x)
df
{'A': [1, 2, 3, 4, 5], 'B': ['A', 'P', 'P', 'L', 'E']}
A B
0 1 A
1 2 P
2 3 P
3 4 L
4 5 E
# 单独设置或者修改列标签(columns)
print(df.columns)
df.columns = ["C", "D"]
df
Index(['A', 'B'], dtype='object')
C D
0 1 A
1 2 P
2 3 P
3 4 L
4 5 E
# 单独修改行标签(index)
print(df.index)
df.index = [5, 6, 7, 8, 9]
df
RangeIndex(start=0, stop=5, step=1)
C D
5 1 A
6 2 P
7 3 P
8 4 L
9 5 E

21.5 数据读取

21.5.1 预备知识:常见数据格式

一般常见的数据格式包括:

  1. 微软的Excel格式:2007版以后扩展名为.xlsx,2007版之前为.xls,可以直接用Excel打开处理,不详细叙述。
  2. CSV格式:扩展名为.csv,也可以用Excel打开。但这种文件本质上是一个纯文本文件,和.txt文件或者python代码.py文件并无区别,都可以用记事本或者vscode打开。

其中:

  1. CSV文件:其中所有的信息都是数据,不包括格式、排版、颜色等等,文件体积小,几乎任何数据处理软件都可以处理CSV文件。可以视为通用标准,你不知道要保存成什么格式,就可以用CSV。
  2. Excel文件:文件里包括了大量的格式、排版、颜色等信息,文件体积比较大,多数数据处理软件都可以处理。如果你可以选择,就选择新版格式.xlsx

对于我们大部分工作,两种保存数据的格式都没有本质的区别。

以下数据如果不做说明,都来自CSMAR(国泰安)数据库。

vscode管理工程以目录为基础,很多功能需要你打开一个目录才能生效。你在vscode中打开的目录即工作目录

在本课程中,所有的数据,都会放在工作目录下的data文件夹。即data文件夹现在这个.ipynb处于目录同一层。

所用数据包括:

  1. data/basic_info.xlsx:部分上市公司的基本信息。 下载
# 导入pandas和numpy是惯例
import pandas as pd
import numpy as np

# 读取工作目录下的data文件夹下的basic_info.xlsx文件
# 保存到df变量
df = pd.read_excel("data/basic_info.xlsx")

注意:如果read_excel出错,提示你缺少openpyxl包(你应该可以看懂出错信息),那么按照这个包可以用以下方法二选一:

  1. 可以在任何一个python单元格中执行以下命令(开头有感叹号):
!pip install -i https://pypi.tuna.tsinghua.edu.cn/simple openpyxl
  1. 或者在anaconda prompt(开始菜单中有)执行(开头没感叹号):
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple openpyxl

读取数据后,首先检查数据读取是否符合预期。

.head()方法检查前几行,.tail()检查后几行,默认都是5行。

df.head()  # 查看df的前5行
股票代码 股票简称 公司成立日期 注册资本 首次上市日期 所属省份 所属城市 上市状态
0 1 平安银行 1987-12-22 19405918198 1991-04-03 广东省 深圳市 正常上市
1 2 万科A 1988-11-01 11625383375 1991-01-29 广东省 深圳市 正常上市
2 4 国华网安 1986-05-05 156003000 1991-01-14 广东省 深圳市 ST
3 5 ST 星源 1990-02-01 1058536842 1990-12-10 广东省 深圳市 ST
4 6 深振业A 1989-04-01 1349995046 1992-04-27 广东省 深圳市 正常上市

再用.info()看每一列的信息,主要看格式。

df.info()  # 每一列的信息
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 39 entries, 0 to 38
Data columns (total 8 columns):
 #   Column  Non-Null Count  Dtype 
---  ------  --------------  ----- 
 0   股票代码    39 non-null     int64 
 1   股票简称    39 non-null     object
 2   公司成立日期  39 non-null     object
 3   注册资本    39 non-null     int64 
 4   首次上市日期  39 non-null     object
 5   所属省份    39 non-null     object
 6   所属城市    39 non-null     object
 7   上市状态    39 non-null     object
dtypes: int64(2), object(6)
memory usage: 2.6+ KB

21.5.2 数据格式的转换

我们首先观察到:

  1. 股票代码不对,如平安银行的代码应该是000001。 读取时默认把股票代码列识别为数字int64(见info()的结果), 因此前面的0就被去掉了。
  2. 日期不是日期格式:从info()结果看,数据类型Dtype中,字符串str会被显示为object。 (更深入的解释见 https://stackoverflow.com/questions/21018654/strings-in-a-dataframe-but-dtype-is-object

(为什么日期数据最好是日期格式?比如你可以比较日期的先后,两个日期相减获得相差几天等等,但你无法对字符串型的日期做这类操作。)

处理这类问题一般可以 1. 在读取数据的时候就指定格式。2. 读取了数据后再转换。

  1. 读取时指定格式:

pd.read_xxx()方法,可以接收一个参数converters,这个参数是一个字典,其中key为列名,value转换的函数。

这里我们指定股票代码为字符串str,用str函数即可;公司成立日期为datetime格式,用pd.to_datetime函数。(这里故意漏掉首次上市日期

注:这里只是告诉pandas可以用什么函数来转换指定的列,而不是你自己去调用某个函数,因此只要函数名即可。

df = pd.read_excel(
    "data/basic_info.xlsx", converters={"股票代码": str, "公司成立日期": pd.to_datetime}
)

df.head()
股票代码 股票简称 公司成立日期 注册资本 首次上市日期 所属省份 所属城市 上市状态
0 000001 平安银行 1987-12-22 19405918198 1991-04-03 广东省 深圳市 正常上市
1 000002 万科A 1988-11-01 11625383375 1991-01-29 广东省 深圳市 正常上市
2 000004 国华网安 1986-05-05 156003000 1991-01-14 广东省 深圳市 ST
3 000005 ST 星源 1990-02-01 1058536842 1990-12-10 广东省 深圳市 ST
4 000006 深振业A 1989-04-01 1349995046 1992-04-27 广东省 深圳市 正常上市
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 39 entries, 0 to 38
Data columns (total 8 columns):
 #   Column  Non-Null Count  Dtype         
---  ------  --------------  -----         
 0   股票代码    39 non-null     object        
 1   股票简称    39 non-null     object        
 2   公司成立日期  39 non-null     datetime64[ns]
 3   注册资本    39 non-null     int64         
 4   首次上市日期  39 non-null     object        
 5   所属省份    39 non-null     object        
 6   所属城市    39 non-null     object        
 7   上市状态    39 non-null     object        
dtypes: datetime64[ns](1), int64(1), object(6)
memory usage: 2.6+ KB

查看数据的前几行以及列信息,股票代码和公司成立日期都符合我们的要求了。

  1. 读取后再转换

读取首次上市日期列,转换格式,再写回到同一列中。对行和列的读写是后面的内容,但逻辑还是比较简单。

df["首次上市日期"] = pd.to_datetime(df["首次上市日期"])
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 39 entries, 0 to 38
Data columns (total 8 columns):
 #   Column  Non-Null Count  Dtype         
---  ------  --------------  -----         
 0   股票代码    39 non-null     object        
 1   股票简称    39 non-null     object        
 2   公司成立日期  39 non-null     datetime64[ns]
 3   注册资本    39 non-null     int64         
 4   首次上市日期  39 non-null     datetime64[ns]
 5   所属省份    39 non-null     object        
 6   所属城市    39 non-null     object        
 7   上市状态    39 non-null     object        
dtypes: datetime64[ns](2), int64(1), object(5)
memory usage: 2.6+ KB

查看info(),数据已经符合我们的要求。

21.6 DataFrame的索引和行列标签

df.head()
股票代码 股票简称 公司成立日期 注册资本 首次上市日期 所属省份 所属城市 上市状态
0 000001 平安银行 1987-12-22 19405918198 1991-04-03 广东省 深圳市 正常上市
1 000002 万科A 1988-11-01 11625383375 1991-01-29 广东省 深圳市 正常上市
2 000004 国华网安 1986-05-05 156003000 1991-01-14 广东省 深圳市 ST
3 000005 ST 星源 1990-02-01 1058536842 1990-12-10 广东省 深圳市 ST
4 000006 深振业A 1989-04-01 1349995046 1992-04-27 广东省 深圳市 正常上市

对于一个DataFrame对象,每一列的标题(题头)和每一行的索引,其实都是索引index,或可称为行索引和列索引。

获得所有的列名(即获得所有的列索引):这是一个字符串类型的索引,包括每一列的列名。

df.columns
Index(['股票代码', '股票简称', '公司成立日期', '注册资本', '首次上市日期', '所属省份', '所属城市', '上市状态'], dtype='object')

获得所有的行索引:这是一个range类的索引,从0到39。如果是时间序列数据,还可以是日期格式的行索引。

df.index
RangeIndex(start=0, stop=39, step=1)

我们可以用set_index()指定一列作为行索引,比如股票代码。

df = df.set_index("股票代码")
df.head()
股票简称 公司成立日期 注册资本 首次上市日期 所属省份 所属城市 上市状态
股票代码
000001 平安银行 1987-12-22 19405918198 1991-04-03 广东省 深圳市 正常上市
000002 万科A 1988-11-01 11625383375 1991-01-29 广东省 深圳市 正常上市
000004 国华网安 1986-05-05 156003000 1991-01-14 广东省 深圳市 ST
000005 ST 星源 1990-02-01 1058536842 1990-12-10 广东省 深圳市 ST
000006 深振业A 1989-04-01 1349995046 1992-04-27 广东省 深圳市 正常上市

注意:pandas中,大部分“写”操作(修改某个东西),默认都“不会改变原值”:你会获得一个包括修改后的新数据的DataFrame。 因此,如果要改变原值,可以把新数据重新赋值给原来的变量名(如上个cell),也可以加入参数inplace = True,如

# 仅作示范,不要执行,因为经过上个cell之后,已经没有`股票代码`这一列了
# df.set_index('股票代码',inplace = True)

再看df.index,此时索引已经变成我们指定列,

df.index
Index(['000001', '000002', '000004', '000005', '000006', '000007', '000008',
       '000009', '000010', '000011', '000012', '000014', '000016', '000017',
       '000019', '000020', '000021', '000023', '000025', '000026', '000027',
       '000028', '000029', '000030', '000031', '000032', '000034', '000035',
       '000036', '000037', '000038', '000039', '000040', '000042', '000045',
       '000046', '000048', '000049', '000050'],
      dtype='object', name='股票代码')

重命名列标签是可以常用操作(改列名),一般有两种方式:

  1. 使用.rename()方法:这个方法接受一个字典参数columns,其中key是旧名,value是新名。(从旧到新的映射)
  2. df.columns 整体重新赋值。注意,这个方法会直接修改原数据。

例如:把“首次上市日期”改为“IPO_DATE”:

# 方法1:rename()

df.rename(columns={"首次上市日期": "IPO_DATE"}).head()
# 这里不使用inplace = True,因此会返回一个新的数据,不会改写原数据。
股票简称 公司成立日期 注册资本 IPO_DATE 所属省份 所属城市 上市状态
股票代码
000001 平安银行 1987-12-22 19405918198 1991-04-03 广东省 深圳市 正常上市
000002 万科A 1988-11-01 11625383375 1991-01-29 广东省 深圳市 正常上市
000004 国华网安 1986-05-05 156003000 1991-01-14 广东省 深圳市 ST
000005 ST 星源 1990-02-01 1058536842 1990-12-10 广东省 深圳市 ST
000006 深振业A 1989-04-01 1349995046 1992-04-27 广东省 深圳市 正常上市
# 方法2:对columns重新赋值

df2 = df.copy()  # 这个方法会直接修改原值。为了保持原数据不变,在一个副本上演示。

df2.columns = list("ABCDEFG")  # 赋值一个同样长度的list。
df2.head()  # df2的列名直接被改变了。
A B C D E F G
股票代码
000001 平安银行 1987-12-22 19405918198 1991-04-03 广东省 深圳市 正常上市
000002 万科A 1988-11-01 11625383375 1991-01-29 广东省 深圳市 正常上市
000004 国华网安 1986-05-05 156003000 1991-01-14 广东省 深圳市 ST
000005 ST 星源 1990-02-01 1058536842 1990-12-10 广东省 深圳市 ST
000006 深振业A 1989-04-01 1349995046 1992-04-27 广东省 深圳市 正常上市

21.7 在行与列上工作

选择行和列的一万种方法。

21.7.1 选择列

选1列,[列名],返回一个Series对象。

sec_name = df["股票简称"]
sec_name.head()
股票代码
000001     平安银行
000002      万科A
000004     国华网安
000005    ST 星源
000006     深振业A
Name: 股票简称, dtype: object

查看列的类型

type(sec_name)  # pandas.core.series.Series
pandas.core.series.Series

选择多列,[[列名的List]],返回一个DataFrame对象。

df[["股票简称", "上市状态"]].head()
股票简称 上市状态
股票代码
000001 平安银行 正常上市
000002 万科A 正常上市
000004 国华网安 ST
000005 ST 星源 ST
000006 深振业A 正常上市

21.7.2 loc:按索引取得行和列

.loc[行索引,列索引]可以按索引选择行或者列(行的索引是index,列的索引是columns)

df.loc[["000001", "000002"], ["股票简称", "上市状态"]]
股票简称 上市状态
股票代码
000001 平安银行 正常上市
000002 万科A 正常上市

也可以只取一个值:

df.loc["000001", "股票简称"]
'平安银行'

行索引和列索引位置,都可以用冒号:表示起点和终点(注:起点终点都包括,和Python的List切片不同!)。

如果只有:,则代表所有行或者列。

比如:获取行索引为000001000005,列索引为股票简称注册资本之间的所有数据。

df.loc["000001":"000005", "股票简称":"注册资本"]
股票简称 公司成立日期 注册资本
股票代码
000001 平安银行 1987-12-22 19405918198
000002 万科A 1988-11-01 11625383375
000004 国华网安 1986-05-05 156003000
000005 ST 星源 1990-02-01 1058536842

获取“所有行(所有股票)的股票简称和上市状况”。但太长,只查看前几行

x = df.loc[:, ["股票简称", "上市状态"]]
x.head()
股票简称 上市状态
股票代码
000001 平安银行 正常上市
000002 万科A 正常上市
000004 国华网安 ST
000005 ST 星源 ST
000006 深振业A 正常上市

当然,如果只是取某列的全部行,上面的代码就和df[['股票简称','上市状态']]等价。

同Python的切片,冒号一侧不写,则表示“到尽头”。

x = df.loc[:"000002", "所属城市":]
x
所属城市 上市状态
股票代码
000001 深圳市 正常上市
000002 深圳市 正常上市

21.7.3 列的次序

在选择列的操作中,列的次序和你提供的列名List的次序完全一样。因此,取列的操作,包括[].loc[],都可以用于改变列的次序。

例如,把列名逆序:(当然,只要你获得了列名的List,就可以任意排序)

# 逆序列名
cols = df.columns  # 取得列名
df[
    cols[::-1]
].head()  # 对逆序后的列名,用`[]`取列。当然,用df.loc[:,cols[::-1]]也一样。
上市状态 所属城市 所属省份 首次上市日期 注册资本 公司成立日期 股票简称
股票代码
000001 正常上市 深圳市 广东省 1991-04-03 19405918198 1987-12-22 平安银行
000002 正常上市 深圳市 广东省 1991-01-29 11625383375 1988-11-01 万科A
000004 ST 深圳市 广东省 1991-01-14 156003000 1986-05-05 国华网安
000005 ST 深圳市 广东省 1990-12-10 1058536842 1990-02-01 ST 星源
000006 正常上市 深圳市 广东省 1992-04-27 1349995046 1989-04-01 深振业A

21.7.4 列运算

每一列之间可以很方便地进行运算:任何操作都会自动应用到每一列的所有元素(如同NumPy中的广播)

例如,注册资本转换为亿元,并保留2位数字。

(df["注册资本"] / 100000000).round(2).head()
股票代码
000001    194.06
000002    116.25
000004      1.56
000005     10.59
000006     13.50
Name: 注册资本, dtype: float64

注意到,Series经过运算后也会得到一个Series,可以加入原数据中的一个列:直接=赋值即可。

df["注册资本_亿"] = (df["注册资本"] / 100000000).round(2)
df.head()
股票简称 公司成立日期 注册资本 首次上市日期 所属省份 所属城市 上市状态 注册资本_亿
股票代码
000001 平安银行 1987-12-22 19405918198 1991-04-03 广东省 深圳市 正常上市 194.06
000002 万科A 1988-11-01 11625383375 1991-01-29 广东省 深圳市 正常上市 116.25
000004 国华网安 1986-05-05 156003000 1991-01-14 广东省 深圳市 ST 1.56
000005 ST 星源 1990-02-01 1058536842 1990-12-10 广东省 深圳市 ST 10.59
000006 深振业A 1989-04-01 1349995046 1992-04-27 广东省 深圳市 正常上市 13.50

21.7.5 删除行或者列

使用df.drop(行或者列标签,axis=0|1)删除行或者列,其中参数axis = 0为行,=1为列。

# 删除“注册资本”列;如果要修改df本身,则inplace = True
df.drop("注册资本", axis=1).head()
股票简称 公司成立日期 首次上市日期 所属省份 所属城市 上市状态 注册资本_亿
股票代码
000001 平安银行 1987-12-22 1991-04-03 广东省 深圳市 正常上市 194.06
000002 万科A 1988-11-01 1991-01-29 广东省 深圳市 正常上市 116.25
000004 国华网安 1986-05-05 1991-01-14 广东省 深圳市 ST 1.56
000005 ST 星源 1990-02-01 1990-12-10 广东省 深圳市 ST 10.59
000006 深振业A 1989-04-01 1992-04-27 广东省 深圳市 正常上市 13.50
# 删除标签为00001的行
df.drop("000001", axis=0).head()
股票简称 公司成立日期 注册资本 首次上市日期 所属省份 所属城市 上市状态 注册资本_亿
股票代码
000002 万科A 1988-11-01 11625383375 1991-01-29 广东省 深圳市 正常上市 116.25
000004 国华网安 1986-05-05 156003000 1991-01-14 广东省 深圳市 ST 1.56
000005 ST 星源 1990-02-01 1058536842 1990-12-10 广东省 深圳市 ST 10.59
000006 深振业A 1989-04-01 1349995046 1992-04-27 广东省 深圳市 正常上市 13.50
000007 *ST 全新 1988-11-21 346448044 1992-04-13 广东省 深圳市 正常上市 3.46

注意: pandas中,包括drop在内,很多操作“默认不修改原数据”,而是“修改后的数据作为返回值”。因此,需要这样把修改后的数据再次赋值。

df = df.drop( <参数> ) # 把修改后的数据再次赋值给df

如果要“原地修改”,直接改变原始数据,那么需要添加 inplace=True,此时原值被修改,返回值变为None。

df.drop( <参数>, inplace=True ) # 在原始数据上修改,不用再赋值

21.7.6 iloc:按位置取得行和列

.iloc[行坐标,列坐标]:用法和loc非常类似,但只是把索引index(索引可以是任何序列),换成下标(从0开始)。

用法和Python的切片很类似。

# 首先把股票代码还原成普通的列
df.reset_index(inplace=True)
df.head()
股票代码 股票简称 公司成立日期 注册资本 首次上市日期 所属省份 所属城市 上市状态 注册资本_亿
0 000001 平安银行 1987-12-22 19405918198 1991-04-03 广东省 深圳市 正常上市 194.06
1 000002 万科A 1988-11-01 11625383375 1991-01-29 广东省 深圳市 正常上市 116.25
2 000004 国华网安 1986-05-05 156003000 1991-01-14 广东省 深圳市 ST 1.56
3 000005 ST 星源 1990-02-01 1058536842 1990-12-10 广东省 深圳市 ST 10.59
4 000006 深振业A 1989-04-01 1349995046 1992-04-27 广东省 深圳市 正常上市 13.50
# 取前5行,近似于df.head()
df.iloc[:5]
股票代码 股票简称 公司成立日期 注册资本 首次上市日期 所属省份 所属城市 上市状态 注册资本_亿
0 000001 平安银行 1987-12-22 19405918198 1991-04-03 广东省 深圳市 正常上市 194.06
1 000002 万科A 1988-11-01 11625383375 1991-01-29 广东省 深圳市 正常上市 116.25
2 000004 国华网安 1986-05-05 156003000 1991-01-14 广东省 深圳市 ST 1.56
3 000005 ST 星源 1990-02-01 1058536842 1990-12-10 广东省 深圳市 ST 10.59
4 000006 深振业A 1989-04-01 1349995046 1992-04-27 广东省 深圳市 正常上市 13.50
# iloc接受列表,如取0,2,4,6行
df.iloc[[0, 2, 4, 6]]
股票代码 股票简称 公司成立日期 注册资本 首次上市日期 所属省份 所属城市 上市状态 注册资本_亿
0 000001 平安银行 1987-12-22 19405918198 1991-04-03 广东省 深圳市 正常上市 194.06
2 000004 国华网安 1986-05-05 156003000 1991-01-14 广东省 深圳市 ST 1.56
4 000006 深振业A 1989-04-01 1349995046 1992-04-27 广东省 深圳市 正常上市 13.50
6 000008 神州高铁 1989-10-11 2780795346 1992-05-07 北京市 北京市 正常上市 27.81
# 抽样:随机选行。
df.sample(5)
股票代码 股票简称 公司成立日期 注册资本 首次上市日期 所属省份 所属城市 上市状态 注册资本_亿
7 000009 中国宝安 1990-09-01 2579213965 1991-06-25 广东省 深圳市 正常上市 25.79
24 000031 大悦城 1993-09-26 4286313339 1993-10-08 广东省 深圳市 正常上市 42.86
31 000039 中集集团 1992-09-30 3595014000 1994-03-23 广东省 深圳市 正常上市 35.95
8 000010 美丽生态 1988-12-13 819854713 1995-10-27 广东省 深圳市 正常上市 8.20
28 000036 华联控股 1994-01-29 1483934025 1994-06-17 广东省 深圳市 正常上市 14.84

21.7.7 按条件筛选

# 按条件筛选:如,选择注册资本大于20亿的公司

# 创建条件
mask = df["注册资本"] > 2000000000
mask.head()
0     True
1     True
2    False
3    False
4    False
Name: 注册资本, dtype: bool
df[mask]
股票代码 股票简称 公司成立日期 注册资本 首次上市日期 所属省份 所属城市 上市状态 注册资本_亿
0 000001 平安银行 1987-12-22 19405918198 1991-04-03 广东省 深圳市 正常上市 194.06
1 000002 万科A 1988-11-01 11625383375 1991-01-29 广东省 深圳市 正常上市 116.25
6 000008 神州高铁 1989-10-11 2780795346 1992-05-07 北京市 北京市 正常上市 27.81
7 000009 中国宝安 1990-09-01 2579213965 1991-06-25 广东省 深圳市 正常上市 25.79
10 000012 南玻A 1984-09-10 3070692107 1992-02-28 广东省 深圳市 正常上市 30.71
12 000016 深康佳A 1980-10-01 2407945408 1992-03-27 广东省 深圳市 正常上市 24.08
20 000027 深圳能源 1993-06-02 4757389916 1993-09-03 广东省 深圳市 正常上市 47.57
24 000031 大悦城 1993-09-26 4286313339 1993-10-08 广东省 深圳市 正常上市 42.86
27 000035 中国天楹 1994-01-08 2523777297 1994-04-08 江苏省 南通市 正常上市 25.24
31 000039 中集集团 1992-09-30 3595014000 1994-03-23 广东省 深圳市 正常上市 35.95
35 000046 泛海控股 1989-01-04 5196200656 1994-09-12 北京市 北京市 正常上市 51.96
38 000050 深天马A 1983-11-08 2457747661 1995-03-15 广东省 深圳市 正常上市 24.58
# 用loc操作也可以
df.loc[mask]
股票代码 股票简称 公司成立日期 注册资本 首次上市日期 所属省份 所属城市 上市状态 注册资本_亿
0 000001 平安银行 1987-12-22 19405918198 1991-04-03 广东省 深圳市 正常上市 194.06
1 000002 万科A 1988-11-01 11625383375 1991-01-29 广东省 深圳市 正常上市 116.25
6 000008 神州高铁 1989-10-11 2780795346 1992-05-07 北京市 北京市 正常上市 27.81
7 000009 中国宝安 1990-09-01 2579213965 1991-06-25 广东省 深圳市 正常上市 25.79
10 000012 南玻A 1984-09-10 3070692107 1992-02-28 广东省 深圳市 正常上市 30.71
12 000016 深康佳A 1980-10-01 2407945408 1992-03-27 广东省 深圳市 正常上市 24.08
20 000027 深圳能源 1993-06-02 4757389916 1993-09-03 广东省 深圳市 正常上市 47.57
24 000031 大悦城 1993-09-26 4286313339 1993-10-08 广东省 深圳市 正常上市 42.86
27 000035 中国天楹 1994-01-08 2523777297 1994-04-08 江苏省 南通市 正常上市 25.24
31 000039 中集集团 1992-09-30 3595014000 1994-03-23 广东省 深圳市 正常上市 35.95
35 000046 泛海控股 1989-01-04 5196200656 1994-09-12 北京市 北京市 正常上市 51.96
38 000050 深天马A 1983-11-08 2457747661 1995-03-15 广东省 深圳市 正常上市 24.58

21.7.8 复合条件和字符串方法

# 复合条件:选择注册资本>20亿,且所属省份位广东省的公司

cond1 = df["注册资本"] > 2000000000  # 数字列,可以直接运算(包括算数运算和条件运算)
cond2 = df["所属省份"].str.contains(
    "广东"
)  # 字符串列,需要调用字符串方法`.str.某函数()`,

# Pandas字符串方法见https://pandas.pydata.org/pandas-docs/stable/user_guide/text.html
# 常用的如包含.str.contains(),以什么开头.str.startswith(),以什么结尾.str.endswith()

# 方法1,用np.logical_xxx()函数,构造一个新的布尔型序列
mask = np.logical_and(cond1, cond2)
df[mask]
股票代码 股票简称 公司成立日期 注册资本 首次上市日期 所属省份 所属城市 上市状态 注册资本_亿
0 000001 平安银行 1987-12-22 19405918198 1991-04-03 广东省 深圳市 正常上市 194.06
1 000002 万科A 1988-11-01 11625383375 1991-01-29 广东省 深圳市 正常上市 116.25
7 000009 中国宝安 1990-09-01 2579213965 1991-06-25 广东省 深圳市 正常上市 25.79
10 000012 南玻A 1984-09-10 3070692107 1992-02-28 广东省 深圳市 正常上市 30.71
12 000016 深康佳A 1980-10-01 2407945408 1992-03-27 广东省 深圳市 正常上市 24.08
20 000027 深圳能源 1993-06-02 4757389916 1993-09-03 广东省 深圳市 正常上市 47.57
24 000031 大悦城 1993-09-26 4286313339 1993-10-08 广东省 深圳市 正常上市 42.86
31 000039 中集集团 1992-09-30 3595014000 1994-03-23 广东省 深圳市 正常上市 35.95
38 000050 深天马A 1983-11-08 2457747661 1995-03-15 广东省 深圳市 正常上市 24.58
# 方法2,直接在DataFrame中选,Pandas的[]操作,接受"与&, 或|,非~"操作
df[cond1 & cond2]
股票代码 股票简称 公司成立日期 注册资本 首次上市日期 所属省份 所属城市 上市状态 注册资本_亿
0 000001 平安银行 1987-12-22 19405918198 1991-04-03 广东省 深圳市 正常上市 194.06
1 000002 万科A 1988-11-01 11625383375 1991-01-29 广东省 深圳市 正常上市 116.25
7 000009 中国宝安 1990-09-01 2579213965 1991-06-25 广东省 深圳市 正常上市 25.79
10 000012 南玻A 1984-09-10 3070692107 1992-02-28 广东省 深圳市 正常上市 30.71
12 000016 深康佳A 1980-10-01 2407945408 1992-03-27 广东省 深圳市 正常上市 24.08
20 000027 深圳能源 1993-06-02 4757389916 1993-09-03 广东省 深圳市 正常上市 47.57
24 000031 大悦城 1993-09-26 4286313339 1993-10-08 广东省 深圳市 正常上市 42.86
31 000039 中集集团 1992-09-30 3595014000 1994-03-23 广东省 深圳市 正常上市 35.95
38 000050 深天马A 1983-11-08 2457747661 1995-03-15 广东省 深圳市 正常上市 24.58
# 稍微复杂一点,注册资本大于20亿,但不在广东省
df[cond1 & (~cond2)]
股票代码 股票简称 公司成立日期 注册资本 首次上市日期 所属省份 所属城市 上市状态 注册资本_亿
6 000008 神州高铁 1989-10-11 2780795346 1992-05-07 北京市 北京市 正常上市 27.81
27 000035 中国天楹 1994-01-08 2523777297 1994-04-08 江苏省 南通市 正常上市 25.24
35 000046 泛海控股 1989-01-04 5196200656 1994-09-12 北京市 北京市 正常上市 51.96

21.7.9 时间方法

# 时间方法: '.dt.某函数()'或者'.dt.某属性'
# 更多时间方法,见https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html
# 常见如取得年月日:.dt.year, .dt.month,.dt.day,取得星期几.dt.weekday等等

# 如:选择91年下半年上市的公司

cond1 = df["首次上市日期"].dt.year == 1991
cond2 = df["首次上市日期"].dt.month >= 6
df[cond1 & cond2]
股票代码 股票简称 公司成立日期 注册资本 首次上市日期 所属省份 所属城市 上市状态 注册资本_亿
7 000009 中国宝安 1990-09-01 2579213965 1991-06-25 广东省 深圳市 正常上市 25.79

21.7.10 查询语句

# 查询函数query(),可以把几个条件写成一个查询字符串
# 注意引号的嵌套:字符串必须有引号,如'万科A',查询语句本身也是字符串,
# 因此外层可以用不同的引号。这里内层用单引号,外层用双引号。

df.query(" 股票简称 == '万科A' ")
股票代码 股票简称 公司成立日期 注册资本 首次上市日期 所属省份 所属城市 上市状态 注册资本_亿
1 000002 万科A 1988-11-01 11625383375 1991-01-29 广东省 深圳市 正常上市 116.25
# 查询语句也可以用复合(& | ~)条件
# 并且接受变量作为条件,只要在查询字符串中加入`@变量名` 即可

# 上市年份 = a_year,且省份不是广东省

a_year = 1992
df.query(
    "首次上市日期.dt.year == @a_year & ~( 所属省份 == '广东省')"
)  # 用"所属省份 != '广东省'"也可
股票代码 股票简称 公司成立日期 注册资本 首次上市日期 所属省份 所属城市 上市状态 注册资本_亿
6 000008 神州高铁 1989-10-11 2780795346 1992-05-07 北京市 北京市 正常上市 27.81

21.7.11 排序

df.sort_values( [<列名>] )可以按某一列排序,多个列名可以使用List。参数ascending=True为从小到大排序,默认为True

类似的,df.sort_index可以按索引排序。

# 按注册资本,逆序排序,看最前面(最大)5个
df.sort_values("注册资本", ascending=False).head()
股票代码 股票简称 公司成立日期 注册资本 首次上市日期 所属省份 所属城市 上市状态 注册资本_亿
0 000001 平安银行 1987-12-22 19405918198 1991-04-03 广东省 深圳市 正常上市 194.06
1 000002 万科A 1988-11-01 11625383375 1991-01-29 广东省 深圳市 正常上市 116.25
35 000046 泛海控股 1989-01-04 5196200656 1994-09-12 北京市 北京市 正常上市 51.96
20 000027 深圳能源 1993-06-02 4757389916 1993-09-03 广东省 深圳市 正常上市 47.57
24 000031 大悦城 1993-09-26 4286313339 1993-10-08 广东省 深圳市 正常上市 42.86
# 先按省份排序,同省份内,按注册资本排序
df.sort_values(["所属省份", "注册资本_亿"]).head(10)
股票代码 股票简称 公司成立日期 注册资本 首次上市日期 所属省份 所属城市 上市状态 注册资本_亿
6 000008 神州高铁 1989-10-11 2780795346 1992-05-07 北京市 北京市 正常上市 27.81
35 000046 泛海控股 1989-01-04 5196200656 1994-09-12 北京市 北京市 正常上市 51.96
23 000030 富奥股份 1993-08-28 1810552111 1993-09-29 吉林省 长春市 正常上市 18.11
17 000023 深天地A 1984-09-18 138756240 1993-04-29 广东省 深圳市 正常上市 1.39
2 000004 国华网安 1986-05-05 156003000 1991-01-14 广东省 深圳市 ST 1.56
11 000014 沙河股份 1992-04-21 201705187 1992-06-02 广东省 深圳市 正常上市 2.02
15 000020 深华发A 1992-03-20 283161227 1992-04-28 广东省 深圳市 正常上市 2.83
37 000049 德赛电池 1995-02-18 300298970 1995-03-20 广东省 深圳市 正常上市 3.00
5 000007 *ST 全新 1988-11-21 346448044 1992-04-13 广东省 深圳市 正常上市 3.46
19 000026 飞亚达 1993-04-18 426051015 1993-06-03 广东省 深圳市 正常上市 4.26

21.7.12 行列基本操作练习

继续利用上述basic_info.xlsx数据,建立一个新的ipynb文件(完整学号-姓名-行列基本操作练习.ipynb),完成以下练习。(题目如果要求打印DF,都只打印前5行)

练习1:基础操作

从 DataFrame df 中选择 股票代码股票简称 两列,并保存到变量 selected_columns。然后选择 股票代码000002 的行,并仅显示 公司成立日期注册资本 两列,保存到变量 selected_row。打印 selected_columnsselected_row

练习2:列运算与删除列

df 中添加一个新列 注册资本(亿),其值为 注册资本 列的值除以 10^8,然后删除 注册资本 列。保存修改后的 DataFrame到变量 df_modified,并打印前5行。

练习3:按条件筛选与排序

筛选出 所属省份广东省上市状态正常上市 的所有行,并按 公司成立日期 降序排列。将结果保存到变量 sorted_df,并打印 sorted_df

练习4:复合条件筛选与字符串方法

筛选出 公司成立日期1990年 之前且 股票简称 包含 A 字符的所有行,并创建一个新列 成立年份,其值为 公司成立日期 的年份。将结果保存到变量 filtered_df,并打印 filtered_df

练习5:复杂逻辑判断和后续操作

首次上市日期 转换为 datetime 格式。筛选出 首次上市日期 早于 1992-01-01 的公司,并判断这些公司的 注册资本(亿) 是否大于 100 亿元。如果大于 100 亿元,则标记为 “大公司”,否则标记为 “小公司”。将这个标记作为新列 公司规模 加入 DataFrame。将结果保存到变量 final_df,并打印 final_df

21.8 修改DataFrame中的值

构造一个示例数据

import pandas as pd
import numpy as np

# np.arange(8) + 1 生成1-8的数组
# np.random.randn(8) 生成标准正态分布随机数
df = pd.DataFrame(dict(A=np.arange(8) + 1, B=np.random.randn(8)))
df
A B
0 1 -1.016251
1 2 0.815455
2 3 0.975249
3 4 0.182091
4 5 2.827289
5 6 0.051366
6 7 -1.820894
7 8 1.855078

21.8.1 按条件修改值

可以(并且推荐)采用.loc[ <行> , <列>],可以引用指定列上的指定列的值。

例如:把负数全部改为0

mask = df.B < 0  # 一个bool序列
df.loc[mask, "B"] = 0  # 简写成 df.loc[df.B < 0 , 'B']也可以
df
A B
0 1 0.000000
1 2 0.815455
2 3 0.975249
3 4 0.182091
4 5 2.827289
5 6 0.051366
6 7 0.000000
7 8 1.855078

如果新数据在另一个列表、ndarray或者Series中,向要覆盖到原数据的指定位置, 同样采用loc[]

# 新数据在另一个array或者Series中
new_data = np.array([97, 98, 99])


# 要替换的值,在index的0,3,6号
mask = df.index % 3 == 0
# 0,3,6号是True

# 把new_data覆盖到df的B列的指定位置
df.loc[mask, "B"] = new_data

df
A B
0 1 97.000000
1 2 0.815455
2 3 0.975249
3 4 98.000000
4 5 2.827289
5 6 0.051366
6 7 99.000000
7 8 1.855078

21.8.2 副本(拷贝)和视图

还是那个问题:Pandas也要区分副本和视图。

  1. 某些操作会产生视图:比如loc[],你对此进行赋值,将会修改原数据。
  2. 某些操作会产生拷贝:比如链式操作,.copy()方法,你对此赋值,不会改变原值。

但问题在于:这两种操作的区分往往不明显,因此直接修改DF数据时,最好复查。

df = pd.DataFrame(dict(A=np.arange(8) + 1, B=np.random.randn(8)))
df
A B
0 1 -0.573370
1 2 0.900201
2 3 0.910912
3 4 0.824337
4 5 -0.464455
5 6 0.158240
6 7 1.386610
7 8 1.635394
df1 = df.copy()  # 把df拷贝一份


# 选择B > 0 的样本,并且把他们的A属性改为99

# `loc[]`生成一个视图,直接赋值可以正常修改原值

df1.loc[df1.B > 0, "A"] = 99
df1
A B
0 1 -0.573370
1 99 0.900201
2 99 0.910912
3 99 0.824337
4 5 -0.464455
5 99 0.158240
6 99 1.386610
7 99 1.635394
df2 = df.copy()

# 链式操作:多次取行或列,串联在一起,最终赋值
# 先选择B>0的行,在选择A列,赋值
df2.loc[df2.B > 0]["A"] = 99

# 修改原值失败!并且弹出警告
# A value is trying to be set on a copy of a slice from a DataFrame.
df2
/tmp/ipykernel_505789/2321145153.py:5: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  df2.loc[df2.B > 0]["A"] = 99
A B
0 1 -0.573370
1 2 0.900201
2 3 0.910912
3 4 0.824337
4 5 -0.464455
5 6 0.158240
6 7 1.386610
7 8 1.635394
# 失败但无警告

df3 = df.copy()

# 这是个隐蔽的链式操作!
# 等价于 df3[['A','B']].loc[df3.B>0,'A'] = 999
x = df3[["A", "B"]]
x.loc[df3.B > 0, "A"] = 999
df3
A B
0 1 -0.573370
1 2 0.900201
2 3 0.910912
3 4 0.824337
4 5 -0.464455
5 6 0.158240
6 7 1.386610
7 8 1.635394

21.8.3 小练习

利用以下虚拟数据,在Passed列中,把合格的同学标记为True,不合格的同学标记为False,并打印数据。

import pandas as pd

# 创建数据集
data = {
    "Name": [
        "Alice",
        "Bob",
        "Charlie",
        "David",
        "Eve",
        "Frank",
        "Grace",
        "Hannah",
        "Isaac",
        "Jack",
    ],
    "Score": [88, 58, 65, 95, 78, 55, 90, 87, 52, 91],
    "Passed": [False] * 10,
}
df = pd.DataFrame(data)
df
Name Score Passed
0 Alice 88 False
1 Bob 58 False
2 Charlie 65 False
3 David 95 False
4 Eve 78 False
5 Frank 55 False
6 Grace 90 False
7 Hannah 87 False
8 Isaac 52 False
9 Jack 91 False

21.9 数据清洗

21.9.1 处理缺失值

Pandas使用依然使用np.nan来表示缺失值。

构造一个带有缺失值的Series对象(一个列):

df = pd.Series(["a", "b", np.nan, "d", np.nan])  # 创建一个Series对象(一列)
print(df)
0      a
1      b
2    NaN
3      d
4    NaN
dtype: object

常用的NA方法有:

  1. isnull(): 判断什么值为缺失值
  2. notnull(): 与上一个方法相反。
  3. dropna():去除缺失值
  4. fillna(): 按一定的规则填充缺失值
df.isnull()  # 使用Series对象自带的isnull()方法
0    False
1    False
2     True
3    False
4     True
dtype: bool

问:这一列是否有NA,或者有多少个?对上述结果求和即可!True会被是为1,False被视为0

df.isnull().sum()  # 2个缺失值
2

使用dropna可以返回所有非NA的值:

df.dropna()  # 注意,要修改df本身,需要用参数inplace = True,
0    a
1    b
3    d
dtype: object

与下面的代码是等价的。

df[df.notna()]  # 先获得非na值的布尔序列,再筛选。
0    a
1    b
3    d
dtype: object

但是对于DataFrame对象(二维表格,多个列的横向合并),情况稍微复杂一点:不同的列的缺失值可能在不同的位置。

df = pd.DataFrame(
    dict(A=[1, 2, np.nan, np.nan], B=[4, np.nan, np.nan, 7], C=[7, np.nan, np.nan, 0])
)
df
A B C
0 1.0 4.0 7.0
1 2.0 NaN NaN
2 NaN NaN NaN
3 NaN 7.0 0.0

dropna()默认会去掉包括“任何”缺失值的行:只要有缺失值,这一行就会被去掉。

df.dropna()
A B C
0 1.0 4.0 7.0

参数how='all'只会去掉所有值都是NA的行。

df.dropna(how="all")
A B C
0 1.0 4.0 7.0
1 2.0 NaN NaN
3 NaN 7.0 0.0

参数axis = 0|1指示对行还是列操作(默认是行),所以要对列操作,只要加入axis = 1

df["D"] = np.nan  # 加一列全NA列
df
A B C D
0 1.0 4.0 7.0 NaN
1 2.0 NaN NaN NaN
2 NaN NaN NaN NaN
3 NaN 7.0 0.0 NaN
df.dropna(how="all", axis=1)  # 去掉所有值都为NA的列。
A B C
0 1.0 4.0 7.0
1 2.0 NaN NaN
2 NaN NaN NaN
3 NaN 7.0 0.0

21.9.2 填充缺失值

如果需要填充缺失值,而不是去除,可以使用fillna()

# 创建随机数df,

# 利用np.random.randn()创建7行3列的标准正态分布随机数,转为DataFrame

df = pd.DataFrame(np.random.randn(7, 3))
df.columns = ["A", "B", "C"]
df.iloc[2:5, 1] = np.nan
df.iloc[:2, 2] = np.nan
df
A B C
0 -0.528753 -1.303227 NaN
1 1.189555 -0.523968 NaN
2 2.375218 NaN 1.522707
3 -0.048647 NaN 1.025043
4 -0.537738 NaN 0.980053
5 -0.550232 -0.045109 -0.482685
6 0.354554 -1.340997 0.080305
df.fillna(0)  # 用0填充缺失值
A B C
0 -0.528753 -1.303227 0.000000
1 1.189555 -0.523968 0.000000
2 2.375218 0.000000 1.522707
3 -0.048647 0.000000 1.025043
4 -0.537738 0.000000 0.980053
5 -0.550232 -0.045109 -0.482685
6 0.354554 -1.340997 0.080305
# 用前值填充。如果第一个值就是NA就无法填充了。
# df.fillna(method="ffill")  # 老版本Pandas的方法
df.ffill()  # 新版的方法
A B C
0 -0.528753 -1.303227 NaN
1 1.189555 -0.523968 NaN
2 2.375218 -0.523968 1.522707
3 -0.048647 -0.523968 1.025043
4 -0.537738 -0.523968 0.980053
5 -0.550232 -0.045109 -0.482685
6 0.354554 -1.340997 0.080305
# 用后值填充。
# df.fillna(method="bfill")  # 老版本Pandas的方法
df.bfill()  # 新版的方法
A B C
0 -0.528753 -1.303227 1.522707
1 1.189555 -0.523968 1.522707
2 2.375218 -0.045109 1.522707
3 -0.048647 -0.045109 1.025043
4 -0.537738 -0.045109 0.980053
5 -0.550232 -0.045109 -0.482685
6 0.354554 -1.340997 0.080305

还可以用均值填充,这样填充后不改变均值

# 还可以填充均值或者中位数
df.B.fillna(df.B.mean(), inplace=True)  # 用均值填充
df.C.fillna(df.C.median(), inplace=True)  # 用中位数填充
/tmp/ipykernel_505789/3452444329.py:2: FutureWarning: A value is trying to be set on a copy of a DataFrame or Series through chained assignment using an inplace method.
The behavior will change in pandas 3.0. This inplace method will never work because the intermediate object on which we are setting values always behaves as a copy.

For example, when doing 'df[col].method(value, inplace=True)', try using 'df.method({col: value}, inplace=True)' or df[col] = df[col].method(value) instead, to perform the operation inplace on the original object.


  df.B.fillna(df.B.mean(), inplace=True)  # 用均值填充
/tmp/ipykernel_505789/3452444329.py:3: FutureWarning: A value is trying to be set on a copy of a DataFrame or Series through chained assignment using an inplace method.
The behavior will change in pandas 3.0. This inplace method will never work because the intermediate object on which we are setting values always behaves as a copy.

For example, when doing 'df[col].method(value, inplace=True)', try using 'df.method({col: value}, inplace=True)' or df[col] = df[col].method(value) instead, to perform the operation inplace on the original object.


  df.C.fillna(df.C.median(), inplace=True)  # 用中位数填充

21.9.3 数据转换

21.9.3.1 去除重复行

df = pd.DataFrame(dict(A=["a", "b", "c"] * 2, B=[1, 2, 3] * 2))
df
A B
0 a 1
1 b 2
2 c 3
3 a 1
4 b 2
5 c 3
df.duplicated()  # 判断这一整行是否前面出现过(默认以首次出现为基准)
0    False
1    False
2    False
3     True
4     True
5     True
dtype: bool
df.drop_duplicates()  # 去除重复行
# 下面代码等价
# df[~df.duplicated()] # 获得重复行,取反作为筛选条件
A B
0 a 1
1 b 2
2 c 3

21.9.3.2 使用函数或者映射进行转换

scores = np.random.uniform(30, 100, 6).round(1)
df = pd.DataFrame(dict(name=list("陈李张王周吴"), score=scores))
df
name score
0 44.5
1 55.8
2 67.8
3 30.0
4 89.9
5 83.2

例如,把score列转为rank(ABCDE)。

  1. 映射(map)转换:提供一个字典。

利用map()函数,你提供一个转换器(字典),可以把ii某一列所有的值,转为 dadf

score10x = df.score // 10  # 分数整除(获得十位)
score10x  # 这是一个Series
0    4.0
1    5.0
2    6.0
3    3.0
4    8.0
5    8.0
Name: score, dtype: float64
score_to_rank = {10: "A", 9: "A", 8: "B", 7: "C", 6: "D"}  # 不同的分数段,对应的评级
rank = score10x.map(score_to_rank)  # 利用map()函数,把分数的10位映射为rank
rank
0    NaN
1    NaN
2      D
3    NaN
4      B
5      B
Name: score, dtype: object
df["rank"] = rank.fillna("E")  # 低于60不在字典中,会映射为NA,填充'E'即可
df
name score rank
0 44.5 E
1 55.8 E
2 67.8 D
3 30.0 E
4 89.9 B
5 83.2 B
  1. 函数转换:提供一个函数

除了提供一个字典,也可以提供一个函数。显然函数的灵活性会高一点,比如可以写函数说明,参数检查,简单测试等等。

def calc_rank(x):
    """根据分数0~100,计算评级rank,返回字符串ABC等"""

    # 参数的定义域检测等略
    score_to_rank = {
        10: "A",
        9: "A",
        8: "B",
        7: "C",
        6: "D",
    }  # 不同的分数段,对应的评级
    if x >= 60:
        return score_to_rank[x // 10]  # 直接利用前面定义好的字典

    return "E"


# 简单测试一下
assert calc_rank(40) == "E"
assert calc_rank(100) == "A"
df.score.map(calc_rank)  # Series的map方法,既可以接受字典dict,也可以接受函数。
0    E
1    E
2    D
3    E
4    B
5    B
Name: score, dtype: object

21.9.3.3 特定值替代

某些特殊的值有特殊的含义,比如被访者不在,拒绝回答,无法识别答案等等。

# 有一列数据,999和-1分别表示不同的异常情况,比如999是拒绝回答,-1是被访者不在
df = pd.Series([1, 2, 999, 3, 4, -1])
df
0      1
1      2
2    999
3      3
4      4
5     -1
dtype: int64
# replace()方法可以接受一个字典,其中定义了不同值的替代,如999 -> nan,-1 -> 0。

df.replace({999: np.nan, -1: 0})
0    1.0
1    2.0
2    NaN
3    3.0
4    4.0
5    0.0
dtype: float64
df.replace([999, -1], np.nan)  # 或者多个值转为1个值: 999和-1都转为nan。
0    1.0
1    2.0
2    NaN
3    3.0
4    4.0
5    NaN
dtype: float64

21.9.4 小练习

使用以下虚拟数据,完成下列练习。全部完成后,打印最终数据。

注意:和前面的所有练习一样,这个练习也有一个小知识点需要大家自行探索, 并且题目中埋了个雷

  1. 检查缺失值
  • 使用代码检查数据集中每一列是否有,有多少缺失值,打印你的回答。
  1. 删除重复数据
  • 删除数据集中所有重复的行,只保留一份。
  1. 填补缺失值
  • 使用平均值填补Age列中的缺失值。
  • 使用中位数填补Score列中的缺失值。
  • 使用字符串”Unknown”填补City列中的缺失值。
  1. 特殊值替代
  • Age列中的特殊值-1替换为平均年龄。
  • Score列中的特殊值9999替换为中位数。
  • City列中的特殊值-1替换为最常见的城市。
  1. 数据转换
  • Score列中所有大于90分的分数都转换为”Excellent”,其他值为”Not Excellent”,并添加一列Performance来存储这个结果。
  • City列中的所有城市名称转换为大写。

全部完成后,打印最终数据。

import numpy as np

# 创建数据集
data = {
    "ID": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
    "Name": [
        "Alice",
        "Bob",
        "Charlie",
        "David",
        "Eve",
        "Frank",
        "Grace",
        "Hannah",
        "Isaac",
        "Jack",
        "Bob",
        "Grace",
    ],
    "Age": [23, -1, 22, 24, 29, np.nan, 26, 21, 24, 28, -1, 26],
    "Score": [88, 92, 9999, 95, 78, 85, 90, 88, 89, 91, 92, 90],
    "City": [
        "New York",
        "Los Angeles",
        "Chicago",
        -1,
        "Houston",
        -1,
        "San Francisco",
        "New York",
        "Chicago",
        np.nan,
        "Los Angeles",
        "San Francisco",
    ],
}

df = pd.DataFrame(data)

21.10 数据合并

Series是序列对象,DataFrame是二维表格对象。序列可以看成是表格的一行或者一列, 表格可以也看出由序列横向或者纵向合并而成。

Series可以由Python的列表,或者NumPy的ndarray创建。

import pandas as pd
import numpy as np

a = pd.Series([5, 6, 7, 8])
a
0    5
1    6
2    7
3    8
dtype: int64

可见,Series创建时已经自带了index。也可以在创建时指定name属性,以及自定index

a = pd.Series([5, 6, 7, 8], name="A", index=list("abcd"))
a
a    5
b    6
c    7
d    8
Name: A, dtype: int64

我们后面用默认的整数索引,因此用.reset_index()方法重置索引。(当然,上一个cell中,不用index参数也一样)

a = a.reset_index(drop=True)  # drop=True会完全丢弃索引;默认为false,索引会成为index列
a
0    5
1    6
2    7
3    8
Name: A, dtype: int64

创建另一个索引,名字叫B

b = pd.Series(list("ABCD"), name="B")
b
0    A
1    B
2    C
3    D
Name: B, dtype: object

21.10.1 Concat

pd.concat()可以把Series或者DataFrame横向或者纵向地“粘贴”在一起,并且对齐行标签(横向粘贴)或者对其列标签(纵向粘贴)。

我们可以把两个Series横向合并,获得一个DataFrame。

此时,Series的name属性,会成为每一列的header。

注意,pd.concat接收的参数是要合并的Series或者DataFrame的列表。参数axis=1表示横向合并(列合并)。

df = pd.concat([a, b], axis=1)
df
A B
0 5 A
1 6 B
2 7 C
3 8 D

显然,把Series横向合并到DataFrame也是常用操作。

c = pd.Series(["陈", "李", "张", "黄"], name="C")
pd.concat([df, c], axis=1)
A B C
0 5 A
1 6 B
2 7 C
3 8 D

如果index不完全相同,会发生什么?

c = pd.Series(["陈", "李", "张", "黄"], name="C", index=[1, 2, 3, 4])
pd.concat([df, c], axis=1)
A B C
0 5.0 A NaN
1 6.0 B
2 7.0 C
3 8.0 D
4 NaN NaN

注意:连接操作pd.concat会自动对齐所有要合并的对象的索引,如上一个cell。

如果你想忽略索引,只是想简单粗暴地合并,那么可以用 reset_index(drop=True),把2个df的索引重置成0开始的序列,即可把2个df强行粘贴到一起。当然,只要你保持2个df的索引一致,比如你把其中一个df的索引改写为另一个df的索引,也是可以的。

c = pd.Series(["陈", "李", "张", "黄"], name="C", index=[1, 2, 3, 4])
d = pd.concat([df.reset_index(drop=True), c.reset_index(drop=True)], axis=1)
d
A B C
0 5 A
1 6 B
2 7 C
3 8 D

当然,纵向合并(增加行)也完全可以。以2个DataFrame为例:

先构造2个DataFrame

g = d.iloc[:2]
g
A B C
0 5 A
1 6 B
h = d.iloc[2:]
h = h.reset_index(drop=True)
h
A B C
0 7 C
1 8 D

2个DataFrame的index会原样保留,axis=0表示纵向(上下)合并。

pd.concat([g, h], axis=0)
A B C
0 5 A
1 6 B
0 7 C
1 8 D

参数ignore_index = True则会重置index。

pd.concat([g, h], axis=0, ignore_index=True)
A B C
0 5 A
1 6 B
2 7 C
3 8 D

21.10.2 Merge

Merge和横向的concat有点类似,但是Merge横向合并2个DataFrame的依据是某一列,或者多列。

构造2个示范数据:

# 同学信息
df1 = pd.DataFrame(dict(id=[1, 2, 3], name=["A", "B", "C"]))
df1
id name
0 1 A
1 2 B
2 3 C
# 数学成绩表
df2 = pd.DataFrame(dict(id=[2, 3, 4], math=[80, 90, 95]))
df2
id math
0 2 80
1 3 90
2 4 95

pd.merge(df1, df2)接收2个DataFrame,并且会自动识别同名的列,并以同名列为标准,合并2个表。

也可以用on参数指定。

默认情况下取2个表的同名列的交集:2个表都存在的个案,才会合并。

# 例如,把个人信息和数学成绩合并
# 同名列是id,因此merge时会自动对齐id
pd.merge(df1, df2)  # 默认情况是取交集:id = 1 同学没数学成绩,因此不会被合并进来
id name math
0 2 B 80
1 3 C 90
pd.merge(
    df1, df2, on="id"
)  # 和上一段代码等价。id列是同名列,因此可以选择是否手动指定。
id name math
0 2 B 80
1 3 C 90

参数 how = 'outer',可以进行外合并(取并集),缺失的部分自动填充NaN。

pd.merge(df1, df2, how="outer")
id name math
0 1 A NaN
1 2 B 80.0
2 3 C 90.0
3 4 NaN 95.0

还可以以“左表”(how ='left')或者“右表”(how ='right')为标准,只保留指定的表的个案。

pd.merge(df1, df2, how="right")  # 只保留df2“右表”中有的个案,左表没有的个案会填充NaN
id name math
0 2 B 80
1 3 C 90
2 4 NaN 95

merge()除了可以自动判断同名列,还可以有你指定列名。

构造一个新的df,其中学号的列名为stu_id

df3 = pd.DataFrame(dict(stu_id=[1, 2], python=[75, 85]))
df3
stu_id python
0 1 75
1 2 85

参数left_onright_on分别指定坐标和右表的列

# 左表指定id,右表指定stu_id
pd.merge(df1, df3, left_on="id", right_on="stu_id", how="outer")
id name stu_id python
0 1 A 1.0 75.0
1 2 B 2.0 85.0
2 3 C NaN NaN

merge还可以串联:merge的结果是一个新的DataFrame,因此可以持续地merge下去。

df4 = (
    df1.merge(df2, how="outer")
    .merge(df3, left_on="id", right_on="stu_id", how="outer")
    .drop("stu_id", axis=1)
)
df4
id name math python
0 1 A NaN 75.0
1 2 B 80.0 85.0
2 3 C 90.0 NaN
3 4 NaN 95.0 NaN

21.10.3 小练习

见作业专用ipynb文件。

21.11 简单统计、分组与聚合

先构造一个示例数据。

# 例行公事
import pandas as pd
import numpy as np

# 创建示例代码
# 创建4个班,12位同学
df = pd.DataFrame(dict(class_id=list("1234") * 3, name=list("ABCDEFGHIJKL")))

# 随机分配2门课的分数
df["math"] = np.random.randint(50, 100, 12)  # 随机整数(起点,终点,数量)
df["python"] = np.random.randint(45, 95, 12)
df["math_rank"] = df.math.map(
    calc_rank
)  # 如果提示找不到calc_rank,记得把前面定义calc_rank的cell执行一下
df["python_rank"] = df.python.map(calc_rank)

df = df.sample(10).sort_index()  # 抽取10人/随机排除2人,再按index排序
df
class_id name math python math_rank python_rank
1 2 B 96 62 A D
2 3 C 92 57 A E
4 1 E 92 75 A C
5 2 F 50 70 E C
6 3 G 57 92 E A
7 4 H 64 59 D E
8 1 I 79 56 C E
9 2 J 99 77 A C
10 3 K 81 75 B C
11 4 L 73 94 C A

21.11.1 简单的统计

在进行分组和聚合之前,先说明一些简单统计的方法。

  1. .describe() :计算数值列的:个案数,均值,标准差,最小值,分位数:25%、50%(中位数)、75%,最大值
  2. 对指定列调用统计方法,如.mean()
  3. .agg()聚合:计算列的多种统计值。
  4. .value_counts():计算离散变量出现的频率。

统计型描述:.describe()

计算数值列的:个案数,均值,标准差,最小值,分位数:25%、50%(中位数)、75%,最大值

df.describe().round(2)
math python
count 10.00 10.00
mean 78.30 71.70
std 17.04 13.66
min 50.00 56.00
25% 66.25 59.75
50% 80.00 72.50
75% 92.00 76.50
max 99.00 94.00

计算某个统计量,如均值:

# 支持的统计函数很多,如sum, max, std等等,不能一一列举,请用搜索引擎。
df[["math", "python"]].mean()
math      78.3
python    71.7
dtype: float64

对多个列求多个统计量:.agg()

# 利用agg进行聚合:对于每一列,计算指定函数的结果。参数是“函数名”,是字符串,有引号。如“mean”
df[["math", "python"]].agg(["mean", "median"])
math python
mean 78.3 71.7
median 80.0 72.5

计算离散变量出现的频率:.value_counts()

# math_rank是一个离散型变量:ABCDE,因此可以用.value_counts()来统计变量值出现的频率。

df[["math_rank"]].value_counts().sort_index()
math_rank
A            4
B            1
C            2
D            1
E            2
Name: count, dtype: int64

21.11.2 GroupBy操作

有大量操作是按分组进行的,如

  1. 求每个班的Python课的最高分:对每个班(班级分组)求最高分(聚合)
  2. 求每个板块的股票的平均涨幅:对股票(板块分组)求平均涨幅(聚合)

这种情况一般可以采用分组和聚合的操作:

df.groupby(<分组列>).<聚合操作>

# 求班级人数: count()分组的样本数

# groupby('class_id') :按class_id分组
# count(): 对于每个组,求非NA的样本数
df.groupby("class_id").count()
name math python math_rank python_rank
class_id
1 2 2 2 2 2
2 3 3 3 3 3
3 3 3 3 3 3
4 2 2 2 2 2
# 课程平均分

# class_id 分组 -> 选择列['python','math']
# .mean() : 求分组后的均值
# .reset_index() : 把class_id从index转为一个普通列
df.groupby("class_id")[["python", "math"]].mean().round().reset_index()
class_id python math
0 1 66.0 86.0
1 2 70.0 82.0
2 3 75.0 77.0
3 4 76.0 68.0
# 每个班,Python课最高分的同学

# 先多重排序,看看我们要选择的人
df.sort_values(["class_id", "python"])
class_id name math python math_rank python_rank
8 1 I 79 56 C E
4 1 E 92 75 A C
1 2 B 96 62 A D
5 2 F 50 70 E C
9 2 J 99 77 A C
2 3 C 92 57 A E
10 3 K 81 75 B C
6 3 G 57 92 E A
7 4 H 64 59 D E
11 4 L 73 94 C A
# 某个分类中,某个值最高(最低)的n个样本:先排序,再分组,再head/tail

# df.sort_index('python',ascending=False) : 整个df先按python分数逆序排序(大值在前)
# .groupby('class_id'): 按班级分组(组内已经是Python高分在前)
# .head(1): 取每组的第一个
df.sort_values("python", ascending=False).groupby("class_id").head(1)
class_id name math python math_rank python_rank
11 4 L 73 94 C A
6 3 G 57 92 E A
9 2 J 99 77 A C
4 1 E 92 75 A C

21.11.3 Agg多重聚合与自定义函数

如果要对每一个分组都进行多个聚合,可以用agg()方法,传递一个list,其中包括每个函数的“函数名”

agg([ <函数名>, <函数名>,...])

# 对于python和math,求每个班的平均分,最高分
# 传递给agg()的是一个list,其中包括每个函数的“函数名”
# 分组 —> 要计算的列 -> agg(多个函数)
df.groupby("class_id")[["python", "math"]].agg(["mean", "max"]).round(2)
python math
mean max mean max
class_id
1 65.50 75 85.50 92
2 69.67 77 81.67 99
3 74.67 92 76.67 92
4 76.50 94 68.50 73
# 也可以写自定义函数


def calc_range(x):
    """计算序列x中,最大值和最小值之差"""
    return max(x) - min(x)


# 注意:转递自定义函数给agg,传递的是函数本身,而不是函数名的字符串(没有引号)
df.groupby("class_id")[["python", "math"]].agg([calc_range])
python math
calc_range calc_range
class_id
1 19 13
2 15 49
3 35 35
4 35 9
# 注意到,列名是“函数名”,可能不是很合理
# 可以给agg传递函数名,也可以传递一个元组 `(列名, 函数名)`
df.groupby("class_id")[["python", "math"]].agg(
    ["mean", "max", ("range", calc_range)]
).round()
python math
mean max range mean max range
class_id
1 66.0 75 19 86.0 92 13
2 70.0 77 15 82.0 99 49
3 75.0 92 35 77.0 92 35
4 76.0 94 35 68.0 73 9

21.11.4 分组循环

如果对于每个分组,都要进行一个很复杂的操作,那么可以对分组进行循环,可以在循环体内获得该分组的数据。

对每一个组处理完毕后,append到一个空列表,然后pd.concat()即可。

# 简单循环一下,看看每个组是什么结构
for x in df.groupby("class_id"):
    # x 是一个元组,第一个元素是分组的标签(序号),第二个元素是分组后的DF
    print(x)
('1',   class_id name  math  python math_rank python_rank
4        1    E    92      75         A           C
8        1    I    79      56         C           E)
('2',   class_id name  math  python math_rank python_rank
1        2    B    96      62         A           D
5        2    F    50      70         E           C
9        2    J    99      77         A           C)
('3',    class_id name  math  python math_rank python_rank
2         3    C    92      57         A           E
6         3    G    57      92         E           A
10        3    K    81      75         B           C)
('4',    class_id name  math  python math_rank python_rank
7         4    H    64      59         D           E
11        4    L    73      94         C           A)
# 你可以进行很复杂的操作
# 但这里简单举例
result = []

for x in df.groupby("class_id"):
    # x 是一个元组,第一个元素是分组的标签(序号),第二个元素是分组后的DF

    # x[1]元组的第二个元素,就是本组的数据

    # 你可以每个x[1]做很复杂的操作,然后append到result后面。

    # 取得math最高分的行:正序排列取最末尾行

    result.append(x[1].sort_values("math").tail(1))


pd.concat(result)
class_id name math python math_rank python_rank
4 1 E 92 75 A C
9 2 J 99 77 A C
2 3 C 92 57 A E
11 4 L 73 94 C A
# 列表推导也可以循环分组!
# 这里略过
[print(x) for i, x in df.groupby("class_id")]
  class_id name  math  python math_rank python_rank
4        1    E    92      75         A           C
8        1    I    79      56         C           E
  class_id name  math  python math_rank python_rank
1        2    B    96      62         A           D
5        2    F    50      70         E           C
9        2    J    99      77         A           C
   class_id name  math  python math_rank python_rank
2         3    C    92      57         A           E
6         3    G    57      92         E           A
10        3    K    81      75         B           C
   class_id name  math  python math_rank python_rank
7         4    H    64      59         D           E
11        4    L    73      94         C           A
[None, None, None, None]

21.11.5 小练习

见作业专用ipynb文件。

21.12 时间序列简述

典型的时间序列数据就是股价。在Pandas中,一般把时间信息(表示时间的列),转为索引index,便于我们操作。

21.12.1 时间序列数据的构造

# 例行公事的导入
import pandas as pd
import numpy as np

# 构造一个普通的DF,但其中一列是字符串形式的时间
# 我们平时读取的数据,往往也是这种形式
date = [
    "2021-01-01",
    "2021-03-01",
    "2021-03-05",
    "2022-01-01",
    "2022-03-01",
    "2022-03-05",
]

df = pd.DataFrame(dict(date=date, x=np.arange(len(date)) + 8))
df
date x
0 2021-01-01 8
1 2021-03-01 9
2 2021-03-05 10
3 2022-01-01 11
4 2022-03-01 12
5 2022-03-05 13
df.info()  # date 的类型是object(即字符串str)
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 6 entries, 0 to 5
Data columns (total 2 columns):
 #   Column  Non-Null Count  Dtype 
---  ------  --------------  ----- 
 0   date    6 non-null      object
 1   x       6 non-null      int64 
dtypes: int64(1), object(1)
memory usage: 228.0+ bytes

一般的预处理:

  1. 把日期或者时间列转为datetime格式
  2. 把上述列设置为index
df.date = pd.to_datetime(df.date)
df.set_index("date", inplace=True)
df
x
date
2021-01-01 8
2021-03-01 9
2021-03-05 10
2022-01-01 11
2022-03-01 12
2022-03-05 13

21.12.2 索引和切片

按整数序号索引,切片等,和一般的DataFrame类似。

# 索引
df.iloc[1]
x    9
Name: 2021-03-01 00:00:00, dtype: int64
# 切片
df.iloc[2:4]
x
date
2021-03-05 10
2022-01-01 11

对于时间序列数据(index是datetime的DataFrame),按索引取值(.loc())有一些特殊的便利:可以接受任何“可以解释为时间”的字符串:

df.loc["2022-01-01"]  # 按行标签取值,标准做法,和一般的DataFrame一样
x    11
Name: 2022-01-01 00:00:00, dtype: int64
# 写成其他格式(或者其他地区的标准)也可以被loc[]理解
df.loc["20220101"]
df.loc["01/01/2022"]
x    11
Name: 2022-01-01 00:00:00, dtype: int64

只使用月份或者年份也可以

df.loc["2021"]  # 提取2021年所有数据
x
date
2021-01-01 8
2021-03-01 9
2021-03-05 10
df.loc["2021-03"]  # 提取2021年3月所有数据
x
date
2021-03-01 9
2021-03-05 10
# 切片也可以接受这类“看起来像日期”的字符串
# 其他的用法和一般的DataFrame类似。

df.loc["2021":"2022-1"]  # 2021年(全年)到2022年1月的数据
x
date
2021-01-01 8
2021-03-01 9
2021-03-05 10
2022-01-01 11

时间序列的运算,自然也会以时间为基准。

# 构造另一个DataFrame,和df有所不同
df2 = df.copy()
df2.drop(["2021-03-05", "2022-03-01"], axis=0, inplace=True)
df2.loc[pd.to_datetime("2022-09-01")] = 14
df2.columns = ["y"]
df2.y = df2.y / 2
df2
y
date
2021-01-01 4.0
2021-03-01 4.5
2022-01-01 5.5
2022-03-05 6.5
2022-09-01 7.0

横向concat会自动对齐时间

df3 = pd.concat([df, df2], axis=1)  # 横向合并(concat),会自动对齐时间
df3
x y
date
2021-01-01 8.0 4.0
2021-03-01 9.0 4.5
2021-03-05 10.0 NaN
2022-01-01 11.0 5.5
2022-03-01 12.0 NaN
2022-03-05 13.0 6.5
2022-09-01 NaN 7.0
# 同样,一般的列运算也会对齐时间
df3["result"] = (df3.x / 3 - np.sqrt(df3.y)).round(2)
df3
x y result
date
2021-01-01 8.0 4.0 0.67
2021-03-01 9.0 4.5 0.88
2021-03-05 10.0 NaN NaN
2022-01-01 11.0 5.5 1.32
2022-03-01 12.0 NaN NaN
2022-03-05 13.0 6.5 1.78
2022-09-01 NaN 7.0 NaN

21.12.3 构造时间Series

pd.date_range()

x = pd.date_range("2022-01-15", "2022-02-15")  # 定义起点和终点,默认的频率是“D”(日)
x
DatetimeIndex(['2022-01-15', '2022-01-16', '2022-01-17', '2022-01-18',
               '2022-01-19', '2022-01-20', '2022-01-21', '2022-01-22',
               '2022-01-23', '2022-01-24', '2022-01-25', '2022-01-26',
               '2022-01-27', '2022-01-28', '2022-01-29', '2022-01-30',
               '2022-01-31', '2022-02-01', '2022-02-02', '2022-02-03',
               '2022-02-04', '2022-02-05', '2022-02-06', '2022-02-07',
               '2022-02-08', '2022-02-09', '2022-02-10', '2022-02-11',
               '2022-02-12', '2022-02-13', '2022-02-14', '2022-02-15'],
              dtype='datetime64[ns]', freq='D')
x = pd.date_range("2022-01-15", "2022-06-15", freq="ME")  # 频率ME:月
x
DatetimeIndex(['2022-01-31', '2022-02-28', '2022-03-31', '2022-04-30',
               '2022-05-31'],
              dtype='datetime64[ns]', freq='ME')
# periods:指定日期的数量
# 频率B:工作日
x = pd.date_range("2022-01-15", periods=5, freq="B")  # 2022-01-15起的5个工作日
x
DatetimeIndex(['2022-01-17', '2022-01-18', '2022-01-19', '2022-01-20',
               '2022-01-21'],
              dtype='datetime64[ns]', freq='B')
# 获得年、月、日的序列
print(x.year)
print(x.month)
print(x.day)
Index([2022, 2022, 2022, 2022, 2022], dtype='int32')
Index([1, 1, 1, 1, 1], dtype='int32')
Index([17, 18, 19, 20, 21], dtype='int32')

21.12.4 位移和差分

构造一个虚拟股价序列,计算(一阶)滞后、(一阶)差分和(日)回报率。

# 构造一个虚拟的价格序列
df = pd.DataFrame(dict(price=[10.9, 12.1, 11.3, 11.6, 12.7]), index=x)  # x见上一节。
df
price
2022-01-17 10.9
2022-01-18 12.1
2022-01-19 11.3
2022-01-20 11.6
2022-01-21 12.7
# n阶滞后:默认是1阶
df["price_1"] = df.price.shift()
df
price price_1
2022-01-17 10.9 NaN
2022-01-18 12.1 10.9
2022-01-19 11.3 12.1
2022-01-20 11.6 11.3
2022-01-21 12.7 11.6
# 差分:默认是1阶
# 可以:1. 用price - price_1,2. 用price.diff()方法。
df["diff"] = df.price.diff()
df
price price_1 diff
2022-01-17 10.9 NaN NaN
2022-01-18 12.1 10.9 1.2
2022-01-19 11.3 12.1 -0.8
2022-01-20 11.6 11.3 0.3
2022-01-21 12.7 11.6 1.1
# 百分比变化(日回报率)
# 可以 1. 用一阶差分除以一阶滞后。 或者2. 直接用price.pct_change()方法。
df["ret"] = df["diff"] / df["price_1"]

df["ret_2"] = df["price"].pct_change()
df
price price_1 diff ret ret_2
2022-01-17 10.9 NaN NaN NaN NaN
2022-01-18 12.1 10.9 1.2 0.110092 0.110092
2022-01-19 11.3 12.1 -0.8 -0.066116 -0.066116
2022-01-20 11.6 11.3 0.3 0.026549 0.026549
2022-01-21 12.7 11.6 1.1 0.094828 0.094828

21.12.5 从回报率计算价格

# 从回报率计算价格
# 利用comprod():序列的累积连乘
# 见:https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.cumprod.html

# 从1开始的价格:
# 价格1 = (1 + 回报率).cumprod()


df["price_2"] = (1 + df["ret"].fillna(0)).cumprod()
df
price price_1 diff ret ret_2 price_2
2022-01-17 10.9 NaN NaN NaN NaN 1.000000
2022-01-18 12.1 10.9 1.2 0.110092 0.110092 1.110092
2022-01-19 11.3 12.1 -0.8 -0.066116 -0.066116 1.036697
2022-01-20 11.6 11.3 0.3 0.026549 0.026549 1.064220
2022-01-21 12.7 11.6 1.1 0.094828 0.094828 1.165138
# 简单验算: 除了第一天,序列price和price_2的日回报应该相同。
df.price_2.pct_change().round(3) == df.ret.round(3)
2022-01-17    False
2022-01-18     True
2022-01-19     True
2022-01-20     True
2022-01-21     True
Freq: B, dtype: bool

21.12.6 重采样

不同频率周期的数据相互转换:如日频数据到周频或者月频(日线到周线、月线等),或者反过来。

使用.resample()

df

# 构造一个3个月的日线序列
ts = pd.date_range("2022-01-01", "2022-03-31")
df = pd.DataFrame(dict(value=np.arange(len(ts))), index=ts)
df
value
2022-01-01 0
2022-01-02 1
2022-01-03 2
2022-01-04 3
2022-01-05 4
... ...
2022-03-27 85
2022-03-28 86
2022-03-29 87
2022-03-30 88
2022-03-31 89

90 rows × 1 columns

需要指定周期和归纳的方法,如

# 重采样为周"W",取每周的最后一个值
df.resample("W").last().head()  # 周数据的最后一个值,默认是取"周日"为最后一天
value
2022-01-02 1
2022-01-09 8
2022-01-16 15
2022-01-23 22
2022-01-30 29
# 可以设置以周五为最后一天,这样就可以取得全部的周五
# 符合世界上主要国家每周的最后一个工作日,比如股票的周线收盘价
df.resample("W-FRI").last().head()
value
2022-01-07 6
2022-01-14 13
2022-01-21 20
2022-01-28 27
2022-02-04 34

可用的每周结束日期包括

‘W-SUN’ 或 ‘W’: 周日

‘W-MON’: 周一

‘W-TUE’: 周二

‘W-WED’: 周三

‘W-THU’: 周四

‘W-FRI’: 周五

‘W-SAT’: 周六

# 重采样为月线ME,取区间内所有值的和
df.resample("ME").sum()  # 月数据的求和
value
2022-01-31 465
2022-02-28 1246
2022-03-31 2294

可以用的函数包括:

mean(): 计算时间段内的平均值。

sum(): 计算时间段内的总和。

max(): 找到时间段内的最大值。

min(): 找到时间段内的最小值。

count(): 计算时间段内的观测数。

first(): 获取时间段内的第一个值。

last(): 获取时间段内的最后一个值。

median(): 计算时间段内的中位数。

std(): 计算时间段内的标准差。

var(): 计算时间段内的方差。

ohlc(): 为金融数据提供开盘价(open)、最高价(high)、最低价(low)、收盘价(close)的聚合。

21.13 保存DataFrame到文件

import pandas as pd
import numpy as np

构造示例数据

注意:

  1. df['datetime']列是“日期和时间”格式(Pandas默认),有日期和时间的信息,这会反映在保存后的数据中。
  2. df['datetime'].dt.date可以从中获得仅有“日期”信息(见前面的“时间方法”)。
  3. 你打开保存后的excel文件就可以看出差异
df = pd.DataFrame(
    dict(
        datetime=pd.date_range("2022-01-15", "2022-01-19"),
        number=range(1, 6),
        name=list("ABCDE"),
        value=np.random.randn(5),
    )
)
df["date"] = df["datetime"].dt.date
df
datetime number name value date
0 2022-01-15 1 A -0.510968 2022-01-15
1 2022-01-16 2 B -1.546606 2022-01-16
2 2022-01-17 3 C 0.535961 2022-01-17
3 2022-01-18 4 D 0.617336 2022-01-18
4 2022-01-19 5 E -0.586049 2022-01-19

使用df.to_excel()df.to_csv()等方法,可以把df保存为文件。

  1. 默认会保存把索引index也保存为列,并且可能没有列名(注意看index有没有名字)
  2. 一般而言,为了保持在不同软件之间的通用性,可以考虑把所以有意义的信息放在一个普通的列中,则保存DF到文件时就可以放心地丢弃index。
# 把df保存到工作目录(本ipynb文件所在目录)下的data文件夹下的output.xlsx文件中
# 默认参数,会连带保存index一起保存。如果index无名称,则就会产生一系列无名列。
df.to_excel("data/output.xlsx")
# 丢弃index
# 如果index没有价值的信息,或者有价值的信息已经通过reset_index()转为普通列,则可以放心丢弃
df.to_excel("data/output_noindex.xlsx", index=False)

你可以打开这output.xlsxoutput_noindex进行对比。