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)
股票代码 股票简称 公司成立日期 注册资本 首次上市日期 所属省份 所属城市 上市状态 注册资本_亿
19 000026 飞亚达 1993-04-18 426051015 1993-06-03 广东省 深圳市 正常上市 4.26
38 000050 深天马A 1983-11-08 2457747661 1995-03-15 广东省 深圳市 正常上市 24.58
31 000039 中集集团 1992-09-30 3595014000 1994-03-23 广东省 深圳市 正常上市 35.95
32 000040 东旭蓝天 1994-06-15 1486873870 1994-08-08 广东省 深圳市 正常上市 14.87
15 000020 深华发A 1992-03-20 283161227 1992-04-28 广东省 深圳市 正常上市 2.83

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.074429
1 2 -1.244722
2 3 0.874934
3 4 -0.881983
4 5 0.011209
5 6 -0.577859
6 7 -0.246587
7 8 0.317394

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 1.074429
1 2 0.000000
2 3 0.874934
3 4 0.000000
4 5 0.011209
5 6 0.000000
6 7 0.000000
7 8 0.317394

如果新数据在另一个列表、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.000000
2 3 0.874934
3 4 98.000000
4 5 0.011209
5 6 0.000000
6 7 99.000000
7 8 0.317394

21.8.2 副本(拷贝)和视图

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

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

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

df = pd.DataFrame(dict(A=np.arange(8) + 1, B=np.random.randn(8)))
df
A B
0 1 -0.043541
1 2 0.758782
2 3 0.204050
3 4 0.771142
4 5 -0.028145
5 6 -0.537927
6 7 0.107722
7 8 -0.700706
df1 = df.copy()  # 把df拷贝一份


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

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

df1.loc[df1.B > 0, "A"] = 99
df1
A B
0 1 -0.043541
1 99 0.758782
2 99 0.204050
3 99 0.771142
4 5 -0.028145
5 6 -0.537927
6 99 0.107722
7 8 -0.700706
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_3555889/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.043541
1 2 0.758782
2 3 0.204050
3 4 0.771142
4 5 -0.028145
5 6 -0.537927
6 7 0.107722
7 8 -0.700706
# 失败但无警告

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.043541
1 2 0.758782
2 3 0.204050
3 4 0.771142
4 5 -0.028145
5 6 -0.537927
6 7 0.107722
7 8 -0.700706

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.293811 -1.708470 NaN
1 -0.754209 2.221121 NaN
2 -0.632106 NaN 0.581108
3 -1.070064 NaN 0.011758
4 0.276283 NaN 0.025015
5 1.011524 -1.273671 0.989935
6 -0.587565 0.716118 -0.086710
df.fillna(0)  # 用0填充缺失值
A B C
0 -0.293811 -1.708470 0.000000
1 -0.754209 2.221121 0.000000
2 -0.632106 0.000000 0.581108
3 -1.070064 0.000000 0.011758
4 0.276283 0.000000 0.025015
5 1.011524 -1.273671 0.989935
6 -0.587565 0.716118 -0.086710
# 用前值填充。如果第一个值就是NA就无法填充了。
# df.fillna(method="ffill")  # 老版本Pandas的方法
df.ffill()  # 新版的方法
A B C
0 -0.293811 -1.708470 NaN
1 -0.754209 2.221121 NaN
2 -0.632106 2.221121 0.581108
3 -1.070064 2.221121 0.011758
4 0.276283 2.221121 0.025015
5 1.011524 -1.273671 0.989935
6 -0.587565 0.716118 -0.086710
# 用后值填充。
# df.fillna(method="bfill")  # 老版本Pandas的方法
df.bfill()  # 新版的方法
A B C
0 -0.293811 -1.708470 0.581108
1 -0.754209 2.221121 0.581108
2 -0.632106 -1.273671 0.581108
3 -1.070064 -1.273671 0.011758
4 0.276283 -1.273671 0.025015
5 1.011524 -1.273671 0.989935
6 -0.587565 0.716118 -0.086710

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

# 还可以填充均值或者中位数
df.B.fillna(df.B.mean(), inplace=True)  # 用均值填充
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 86.1
1 32.2
2 36.3
3 36.4
4 68.4
5 41.4

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

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

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

score10x = df.score // 10  # 分数整除(获得十位)
score10x  # 这是一个Series
0    8.0
1    3.0
2    3.0
3    3.0
4    6.0
5    4.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      B
1    NaN
2    NaN
3    NaN
4      D
5    NaN
Name: score, dtype: object
df["rank"] = rank.fillna("E")  # 低于60不在字典中,会映射为NA,填充'E'即可
df
name score rank
0 86.1 B
1 32.2 E
2 36.3 E
3 36.4 E
4 68.4 D
5 41.4 E
  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    B
1    E
2    E
3    E
4    D
5    E
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
0 1 A 69 45 D E
2 3 C 84 93 B A
3 4 D 69 94 D A
5 2 F 52 51 E E
6 3 G 76 57 C E
7 4 H 92 65 A D
8 1 I 83 87 B B
9 2 J 99 75 A C
10 3 K 92 67 A D
11 4 L 89 88 B B

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 80.50 72.20
std 14.12 17.90
min 52.00 45.00
25% 70.75 59.00
50% 83.50 71.00
75% 91.25 87.75
max 99.00 94.00

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

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

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

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

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

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

df[["math_rank"]].value_counts().sort_index()
math_rank
A            3
B            3
C            1
D            2
E            1
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 2 2 2 2 2
3 3 3 3 3 3
4 3 3 3 3 3
# 课程平均分

# 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 76.0
1 2 63.0 76.0
2 3 72.0 84.0
3 4 82.0 83.0
# 每个班,Python课最高分的同学

# 先多重排序,看看我们要选择的人
df.sort_values(["class_id", "python"])
class_id name math python math_rank python_rank
0 1 A 69 45 D E
8 1 I 83 87 B B
5 2 F 52 51 E E
9 2 J 99 75 A C
6 3 G 76 57 C E
10 3 K 92 67 A D
2 3 C 84 93 B A
7 4 H 92 65 A D
11 4 L 89 88 B B
3 4 D 69 94 D 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
3 4 D 69 94 D A
2 3 C 84 93 B A
8 1 I 83 87 B B
9 2 J 99 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 66.00 87 76.00 83
2 63.00 75 75.50 99
3 72.33 93 84.00 92
4 82.33 94 83.33 92
# 也可以写自定义函数


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 42 14
2 24 47
3 36 16
4 29 23
# 注意到,列名是“函数名”,可能不是很合理
# 可以给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 87 42 76.0 83 14
2 63.0 75 24 76.0 99 47
3 72.0 93 36 84.0 92 16
4 82.0 94 29 83.0 92 23

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
0        1    A    69      45         D           E
8        1    I    83      87         B           B)
('2',   class_id name  math  python math_rank python_rank
5        2    F    52      51         E           E
9        2    J    99      75         A           C)
('3',    class_id name  math  python math_rank python_rank
2         3    C    84      93         B           A
6         3    G    76      57         C           E
10        3    K    92      67         A           D)
('4',    class_id name  math  python math_rank python_rank
3         4    D    69      94         D           A
7         4    H    92      65         A           D
11        4    L    89      88         B           B)
# 你可以进行很复杂的操作
# 但这里简单举例
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
8 1 I 83 87 B B
9 2 J 99 75 A C
10 3 K 92 67 A D
7 4 H 92 65 A D
# 列表推导也可以循环分组!
# 这里略过
[print(x) for i, x in df.groupby("class_id")]
  class_id name  math  python math_rank python_rank
0        1    A    69      45         D           E
8        1    I    83      87         B           B
  class_id name  math  python math_rank python_rank
5        2    F    52      51         E           E
9        2    J    99      75         A           C
   class_id name  math  python math_rank python_rank
2         3    C    84      93         B           A
6         3    G    76      57         C           E
10        3    K    92      67         A           D
   class_id name  math  python math_rank python_rank
3         4    D    69      94         D           A
7         4    H    92      65         A           D
11        4    L    89      88         B           B
[None, None, None, None]

21.11.5 小练习

22 练习题1:计算基本统计量

’’’ 根据数据框df,完成以下操作: a) 计算每个班级的数学平均分 b) 找出Python成绩的最高分和最低分 c) 统计每个班级的人数 ’’’ # 答案1: # a) 按班级计算数学平均分 class_math_mean = df.groupby(‘class_id’)[‘math’].mean()

23 b) Python成绩的最高分和最低分

python_max = df[‘python’].max() python_min = df[‘python’].min()

24 c) 统计每个班级的人数

class_counts = df[‘class_id’].value_counts()

25 练习题2:条件筛选

’’’ 请完成以下筛选操作: a) 找出数学成绩大于80分的学生 b) 找出数学和Python都及格(>=60)的学生 c) 统计每个班级有多少人数学成绩大于班级平均分 ’’’ # 答案2: # a) 数学成绩大于80分的学生 good_math = df[df[‘math’] > 80]

26 b) 数学和Python都及格的学生

pass_both = df[(df[‘math’] >= 60) & (df[‘python’] >= 60)]

27 c) 统计高于班级平均分的人数

class_means = df.groupby(‘class_id’)[‘math’].transform(‘mean’) above_avg = df[df[‘math’] > class_means].groupby(‘class_id’).size()

28 练习题3:排名与分组统计

’’’ 请完成以下操作: a) 计算每个班级中数学成绩的最好名次 b) 找出每个班级Python成绩的前2名 c) 计算每个班级的数学和Python平均分,并按总平均分从高到低排序 ’’’ # 答案3: # a) 每个班级数学最好名次 best_math_rank = df.groupby(‘class_id’)[‘math_rank’].min()

29 b) 每个班级Python前2名

top2_python = df.sort_values(‘python’, ascending=False).groupby(‘class_id’).head(2)

30 c) 班级总成绩排名

class_avg = df.groupby(‘class_id’)[[‘math’, ‘python’]].mean() class_avg[‘total_avg’] = class_avg.mean(axis=1) class_avg_sorted = class_avg.sort_values(‘total_avg’, ascending=False)

31 练习题4:数据透视表

’’’ 请使用数据透视表完成以下操作: a) 创建一个数据透视表,显示每个班级的数学和Python平均分 b) 计算每个班级的及格率(分别计算数学和Python) c) 找出数学和Python成绩差距最大的班级 ’’’ # 答案4: # a) 班级平均分透视表 class_scores = pd.pivot_table(df, values=[‘math’, ‘python’], index=‘class_id’, aggfunc=‘mean’)

32 b) 计算及格率

def pass_rate(x): return (x >= 60).mean() * 100

class_pass_rates = pd.pivot_table(df, values=[‘math’, ‘python’], index=‘class_id’, aggfunc=pass_rate)

33 c) 成绩差距

class_scores[‘score_diff’] = abs(class_scores[‘math’] - class_scores[‘python’]) max_diff_class = class_scores[‘score_diff’].idxmax()

34 练习题5:高级分组运算

’’’ 请完成以下高级分组运算: a) 为每个学生计算其数学和Python成绩与班级平均分的差值 b) 计算每个班级的数学和Python成绩的中位数、标准差 c) 找出每个班级中数学比Python成绩好(分数高)的学生人数 ’’’ # 答案5: # a) 计算与班级平均分的差值 class_math_mean = df.groupby(‘class_id’)[‘math’].transform(‘mean’) class_python_mean = df.groupby(‘class_id’)[‘python’].transform(‘mean’) df[‘math_diff_from_mean’] = df[‘math’] - class_math_mean df[‘python_diff_from_mean’] = df[‘python’] - class_python_mean

35 b) 计算统计量

class_stats = df.groupby(‘class_id’).agg({ ‘math’: [‘median’, ‘std’], ‘python’: [‘median’, ‘std’] })

36 c) 统计数学比Python好的学生

better_at_math = df[df[‘math’] > df[‘python’]].groupby(‘class_id’).size()

36.1 时间序列简述

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

36.1.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

36.1.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

36.1.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="M")  # 频率M:月
x
DatetimeIndex(['2022-01-31', '2022-02-28', '2022-03-31', '2022-04-30',
               '2022-05-31'],
              dtype='datetime64[ns]', freq='M')
# 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')

36.1.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

36.1.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

36.1.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’: 周六

# 重采样为月线M,取区间内所有值的和
df.resample("M").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)的聚合。

36.2 保存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.759154 2022-01-15
1 2022-01-16 2 B -1.685706 2022-01-16
2 2022-01-17 3 C 0.266500 2022-01-17
3 2022-01-18 4 D -1.480555 2022-01-18
4 2022-01-19 5 E 1.214168 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进行对比。