# 惯例导入2个模块
import pandas as pd
import numpy as np
# 从字典创建,后面会详述
= pd.DataFrame(dict(id=[1, 2, 3], name=["a", "b", "c"]))
df
df
id | name | |
---|---|---|
0 | 1 | a |
1 | 2 | b |
2 | 3 | c |
主要内容:
.py
是Python的源代码文件。这里采用另一种文件,更加常用于数据分析:Jupyter Notebook,.ipynb
。
Why:单元格的输出,会直接显示在单元格的下方,比较方便看。但用起来和单元格分割的.py
类似。
新建文件,扩展名改为.ipynb
即可。
常见的分工:.ipynb
写数据分析的过程;.py
保存共用的代码,作为模块在ipynb
中导入。
每个单元格可以是代码,也可以Markdown格式的文本。单元个的右下角可以选择。
“文本”的单元格,ctrl+enter可以看渲染的结果,双击可以回到编辑。
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
# 从字典创建,后面会详述
= pd.DataFrame(dict(id=[1, 2, 3], name=["a", "b", "c"]))
df
df
id | name | |
---|---|---|
0 | 1 | a |
1 | 2 | b |
2 | 3 | c |
# 取某一列:得到一个Series,注意Series由index和value组成
= df["name"]
x 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'>
Series可以视为二维表格的一列。我们先用pd.Series()
函数构建
# 使用pd.Series构建一列,参数可以是一个序列结构,比如一个List
= pd.Series(["a", "b", "c", "d"])
x x
0 a
1 b
2 c
3 d
dtype: object
此时默认的index(看左侧)是从0开始的整数,并且这一列没有名称。我们可以用索引访问Series中的值。
2] # 注意2指的是index x[
'c'
# 当然也可以写入
2] = 99
x[ x
0 a
1 b
2 99
3 d
dtype: object
创建的时候可以指定index和name,name后面会成为DataFrame(二维表格,即多个列Series的横向合并)中这一列的标题。
# 创建一个简单的 Series,指定index和name
= pd.Series(
grades 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
# 从ndarray或者list创建
= np.array([8, 6, 4, 2])
a print(a)
= pd.DataFrame(a)
df1 df1
[8 6 4 2]
0 | |
---|---|
0 | 8 |
1 | 6 |
2 | 4 |
3 | 2 |
= list("apple")
b print(b)
= pd.DataFrame(b, columns=["B"], index=list("abcde")) # 指定columns和index
df2 df2
['a', 'p', 'p', 'l', 'e']
B | |
---|---|
a | a |
b | p |
c | p |
d | l |
e | e |
# 从字典创建
# 字典的每一个item会成为表格的一列,key会成为列标签(列的标题),value会成为列的值
= dict(A=[1, 2, 3, 4, 5], B=list("APPLE"))
x print(x)
= pd.DataFrame(x)
df 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)
= ["C", "D"]
df.columns 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)
= [5, 6, 7, 8, 9]
df.index 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 |
一般常见的数据格式包括:
.xlsx
,2007版之前为.xls
,可以直接用Excel打开处理,不详细叙述。.csv
,也可以用Excel打开。但这种文件本质上是一个纯文本文件,和.txt
文件或者python代码.py
文件并无区别,都可以用记事本或者vscode打开。其中:
.xlsx
。对于我们大部分工作,两种保存数据的格式都没有本质的区别。
以下数据如果不做说明,都来自CSMAR(国泰安)数据库。
vscode管理工程以目录为基础,很多功能需要你打开一个目录才能生效。你在vscode中打开的目录即工作目录。
在本课程中,所有的数据,都会放在工作目录下的data文件夹。即data
文件夹现在这个.ipynb
处于目录同一层。
所用数据包括:
# 导入pandas和numpy是惯例
import pandas as pd
import numpy as np
# 读取工作目录下的data文件夹下的basic_info.xlsx文件
# 保存到df变量
= pd.read_excel("data/basic_info.xlsx") df
注意:如果read_excel出错,提示你缺少openpyxl
包(你应该可以看懂出错信息),那么按照这个包可以用以下方法二选一:
!pip install -i https://pypi.tuna.tsinghua.edu.cn/simple openpyxl
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple openpyxl
读取数据后,首先检查数据读取是否符合预期。
用.head()
方法检查前几行,.tail()
检查后几行,默认都是5行。
# 查看df的前5行 df.head()
股票代码 | 股票简称 | 公司成立日期 | 注册资本 | 首次上市日期 | 所属省份 | 所属城市 | 上市状态 | |
---|---|---|---|---|---|---|---|---|
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
我们首先观察到:
000001
。 读取时默认把股票代码
列识别为数字int64
(见info()
的结果), 因此前面的0就被去掉了。info()
结果看,数据类型Dtype
中,字符串str
会被显示为object
。 (更深入的解释见 https://stackoverflow.com/questions/21018654/strings-in-a-dataframe-but-dtype-is-object )(为什么日期数据最好是日期格式?比如你可以比较日期的先后,两个日期相减获得相差几天等等,但你无法对字符串型的日期做这类操作。)
处理这类问题一般可以 1. 在读取数据的时候就指定格式。2. 读取了数据后再转换。
pd.read_xxx()
方法,可以接收一个参数converters
,这个参数是一个字典,其中key为列名,value转换的函数。
这里我们指定股票代码
为字符串str
,用str
函数即可;公司成立日期为datetime
格式,用pd.to_datetime
函数。(这里故意漏掉首次上市日期
)
注:这里只是告诉pandas可以用什么函数来转换指定的列,而不是你自己去调用某个函数,因此只要函数名即可。
= pd.read_excel(
df "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
查看数据的前几行以及列信息,股票代码和公司成立日期都符合我们的要求了。
读取首次上市日期
列,转换格式,再写回到同一列中。对行和列的读写是后面的内容,但逻辑还是比较简单。
"首次上市日期"] = pd.to_datetime(df["首次上市日期"])
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()
,数据已经符合我们的要求。
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.set_index("股票代码")
df 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='股票代码')
重命名列标签是可以常用操作(改列名),一般有两种方式:
.rename()
方法:这个方法接受一个字典参数columns
,其中key是旧名,value是新名。(从旧到新的映射)df.columns
整体重新赋值。注意,这个方法会直接修改原数据。例如:把“首次上市日期”改为“IPO_DATE”:
# 方法1:rename()
={"首次上市日期": "IPO_DATE"}).head()
df.rename(columns# 这里不使用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重新赋值
= df.copy() # 这个方法会直接修改原值。为了保持原数据不变,在一个副本上演示。
df2
= list("ABCDEFG") # 赋值一个同样长度的list。
df2.columns # df2的列名直接被改变了。 df2.head()
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 | 广东省 | 深圳市 | 正常上市 |
选择行和列的一万种方法。
选1列,[列名]
,返回一个Series
对象。
= df["股票简称"]
sec_name 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
对象。
"股票简称", "上市状态"]].head() df[[
股票简称 | 上市状态 | |
---|---|---|
股票代码 | ||
000001 | 平安银行 | 正常上市 |
000002 | 万科A | 正常上市 |
000004 | 国华网安 | ST |
000005 | ST 星源 | ST |
000006 | 深振业A | 正常上市 |
.loc[行索引,列索引]
可以按索引选择行或者列(行的索引是index,列的索引是columns)
"000001", "000002"], ["股票简称", "上市状态"]] df.loc[[
股票简称 | 上市状态 | |
---|---|---|
股票代码 | ||
000001 | 平安银行 | 正常上市 |
000002 | 万科A | 正常上市 |
也可以只取一个值:
"000001", "股票简称"] df.loc[
'平安银行'
行索引和列索引位置,都可以用冒号:
表示起点和终点(注:起点终点都包括,和Python的List切片不同!)。
如果只有:
,则代表所有行或者列。
比如:获取行索引为000001
到000005
,列索引为股票简称
到注册资本
之间的所有数据。
"000001":"000005", "股票简称":"注册资本"] df.loc[
股票简称 | 公司成立日期 | 注册资本 | |
---|---|---|---|
股票代码 | |||
000001 | 平安银行 | 1987-12-22 | 19405918198 |
000002 | 万科A | 1988-11-01 | 11625383375 |
000004 | 国华网安 | 1986-05-05 | 156003000 |
000005 | ST 星源 | 1990-02-01 | 1058536842 |
获取“所有行(所有股票)的股票简称和上市状况”。但太长,只查看前几行
= df.loc[:, ["股票简称", "上市状态"]]
x x.head()
股票简称 | 上市状态 | |
---|---|---|
股票代码 | ||
000001 | 平安银行 | 正常上市 |
000002 | 万科A | 正常上市 |
000004 | 国华网安 | ST |
000005 | ST 星源 | ST |
000006 | 深振业A | 正常上市 |
当然,如果只是取某列的全部行,上面的代码就和df[['股票简称','上市状态']]
等价。
同Python的切片,冒号一侧不写,则表示“到尽头”。
= df.loc[:"000002", "所属城市":]
x x
所属城市 | 上市状态 | |
---|---|---|
股票代码 | ||
000001 | 深圳市 | 正常上市 |
000002 | 深圳市 | 正常上市 |
在选择列的操作中,列的次序和你提供的列名List的次序完全一样。因此,取列的操作,包括[]
和.loc[]
,都可以用于改变列的次序。
例如,把列名逆序:(当然,只要你获得了列名的List,就可以任意排序)
# 逆序列名
= df.columns # 取得列名
cols
df[-1]
cols[::# 对逆序后的列名,用`[]`取列。当然,用df.loc[:,cols[::-1]]也一样。 ].head()
上市状态 | 所属城市 | 所属省份 | 首次上市日期 | 注册资本 | 公司成立日期 | 股票简称 | |
---|---|---|---|---|---|---|---|
股票代码 | |||||||
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 |
每一列之间可以很方便地进行运算:任何操作都会自动应用到每一列的所有元素(如同NumPy中的广播)
例如,注册资本转换为亿元,并保留2位数字。
"注册资本"] / 100000000).round(2).head() (df[
股票代码
000001 194.06
000002 116.25
000004 1.56
000005 10.59
000006 13.50
Name: 注册资本, dtype: float64
注意到,Series经过运算后也会得到一个Series,可以加入原数据中的一个列:直接=赋值即可。
"注册资本_亿"] = (df["注册资本"] / 100000000).round(2)
df[ 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 |
使用df.drop(行或者列标签,axis=0|1)
删除行或者列,其中参数axis = 0为行,=1为列。
# 删除“注册资本”列;如果要修改df本身,则inplace = True
"注册资本", axis=1).head() df.drop(
股票简称 | 公司成立日期 | 首次上市日期 | 所属省份 | 所属城市 | 上市状态 | 注册资本_亿 | |
---|---|---|---|---|---|---|---|
股票代码 | |||||||
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的行
"000001", axis=0).head() df.drop(
股票简称 | 公司成立日期 | 注册资本 | 首次上市日期 | 所属省份 | 所属城市 | 上市状态 | 注册资本_亿 | |
---|---|---|---|---|---|---|---|---|
股票代码 | ||||||||
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 ) # 在原始数据上修改,不用再赋值
.iloc[行坐标,列坐标]
:用法和loc
非常类似,但只是把索引index(索引可以是任何序列),换成下标(从0开始)。
用法和Python的切片很类似。
# 首先把股票代码还原成普通的列
=True)
df.reset_index(inplace 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()
5] df.iloc[:
股票代码 | 股票简称 | 公司成立日期 | 注册资本 | 首次上市日期 | 所属省份 | 所属城市 | 上市状态 | 注册资本_亿 | |
---|---|---|---|---|---|---|---|---|---|
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行
0, 2, 4, 6]] df.iloc[[
股票代码 | 股票简称 | 公司成立日期 | 注册资本 | 首次上市日期 | 所属省份 | 所属城市 | 上市状态 | 注册资本_亿 | |
---|---|---|---|---|---|---|---|---|---|
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 |
# 抽样:随机选行。
5) df.sample(
股票代码 | 股票简称 | 公司成立日期 | 注册资本 | 首次上市日期 | 所属省份 | 所属城市 | 上市状态 | 注册资本_亿 | |
---|---|---|---|---|---|---|---|---|---|
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 |
# 按条件筛选:如,选择注册资本大于20亿的公司
# 创建条件
= df["注册资本"] > 2000000000
mask 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 |
# 复合条件:选择注册资本>20亿,且所属省份位广东省的公司
= df["注册资本"] > 2000000000 # 数字列,可以直接运算(包括算数运算和条件运算)
cond1 = df["所属省份"].str.contains(
cond2 "广东"
# 字符串列,需要调用字符串方法`.str.某函数()`,
)
# Pandas字符串方法见https://pandas.pydata.org/pandas-docs/stable/user_guide/text.html
# 常用的如包含.str.contains(),以什么开头.str.startswith(),以什么结尾.str.endswith()
# 方法1,用np.logical_xxx()函数,构造一个新的布尔型序列
= np.logical_and(cond1, cond2)
mask 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的[]操作,接受"与&, 或|,非~"操作
& cond2] df[cond1
股票代码 | 股票简称 | 公司成立日期 | 注册资本 | 首次上市日期 | 所属省份 | 所属城市 | 上市状态 | 注册资本_亿 | |
---|---|---|---|---|---|---|---|---|---|
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亿,但不在广东省
& (~cond2)] df[cond1
股票代码 | 股票简称 | 公司成立日期 | 注册资本 | 首次上市日期 | 所属省份 | 所属城市 | 上市状态 | 注册资本_亿 | |
---|---|---|---|---|---|---|---|---|---|
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 |
# 时间方法: '.dt.某函数()'或者'.dt.某属性'
# 更多时间方法,见https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html
# 常见如取得年月日:.dt.year, .dt.month,.dt.day,取得星期几.dt.weekday等等
# 如:选择91年下半年上市的公司
= df["首次上市日期"].dt.year == 1991
cond1 = df["首次上市日期"].dt.month >= 6
cond2 & cond2] df[cond1
股票代码 | 股票简称 | 公司成立日期 | 注册资本 | 首次上市日期 | 所属省份 | 所属城市 | 上市状态 | 注册资本_亿 | |
---|---|---|---|---|---|---|---|---|---|
7 | 000009 | 中国宝安 | 1990-09-01 | 2579213965 | 1991-06-25 | 广东省 | 深圳市 | 正常上市 | 25.79 |
# 查询函数query(),可以把几个条件写成一个查询字符串
# 注意引号的嵌套:字符串必须有引号,如'万科A',查询语句本身也是字符串,
# 因此外层可以用不同的引号。这里内层用单引号,外层用双引号。
" 股票简称 == '万科A' ") df.query(
股票代码 | 股票简称 | 公司成立日期 | 注册资本 | 首次上市日期 | 所属省份 | 所属城市 | 上市状态 | 注册资本_亿 | |
---|---|---|---|---|---|---|---|---|---|
1 | 000002 | 万科A | 1988-11-01 | 11625383375 | 1991-01-29 | 广东省 | 深圳市 | 正常上市 | 116.25 |
# 查询语句也可以用复合(& | ~)条件
# 并且接受变量作为条件,只要在查询字符串中加入`@变量名` 即可
# 上市年份 = a_year,且省份不是广东省
= 1992
a_year
df.query("首次上市日期.dt.year == @a_year & ~( 所属省份 == '广东省')"
# 用"所属省份 != '广东省'"也可 )
股票代码 | 股票简称 | 公司成立日期 | 注册资本 | 首次上市日期 | 所属省份 | 所属城市 | 上市状态 | 注册资本_亿 | |
---|---|---|---|---|---|---|---|---|---|
6 | 000008 | 神州高铁 | 1989-10-11 | 2780795346 | 1992-05-07 | 北京市 | 北京市 | 正常上市 | 27.81 |
df.sort_values( [<列名>] )
可以按某一列排序,多个列名可以使用List。参数ascending=True
为从小到大排序,默认为True
。
类似的,df.sort_index
可以按索引排序。
# 按注册资本,逆序排序,看最前面(最大)5个
"注册资本", ascending=False).head() df.sort_values(
股票代码 | 股票简称 | 公司成立日期 | 注册资本 | 首次上市日期 | 所属省份 | 所属城市 | 上市状态 | 注册资本_亿 | |
---|---|---|---|---|---|---|---|---|---|
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 |
# 先按省份排序,同省份内,按注册资本排序
"所属省份", "注册资本_亿"]).head(10) df.sort_values([
股票代码 | 股票简称 | 公司成立日期 | 注册资本 | 首次上市日期 | 所属省份 | 所属城市 | 上市状态 | 注册资本_亿 | |
---|---|---|---|---|---|---|---|---|---|
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 |
继续利用上述basic_info.xlsx
数据,建立一个新的ipynb文件(完整学号-姓名-行列基本操作练习.ipynb),完成以下练习。(题目如果要求打印DF,都只打印前5行)
练习1:基础操作
从 DataFrame df
中选择 股票代码
和 股票简称
两列,并保存到变量 selected_columns
。然后选择 股票代码
为 000002
的行,并仅显示 公司成立日期
和 注册资本
两列,保存到变量 selected_row
。打印 selected_columns
和 selected_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
。
构造一个示例数据
import pandas as pd
import numpy as np
# np.arange(8) + 1 生成1-8的数组
# np.random.randn(8) 生成标准正态分布随机数
= pd.DataFrame(dict(A=np.arange(8) + 1, B=np.random.randn(8)))
df 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 |
可以(并且推荐)采用.loc[ <行> , <列>]
,可以引用指定列上的指定列的值。
例如:把负数全部改为0
= df.B < 0 # 一个bool序列
mask "B"] = 0 # 简写成 df.loc[df.B < 0 , 'B']也可以
df.loc[mask, 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中
= np.array([97, 98, 99])
new_data
# 要替换的值,在index的0,3,6号
= df.index % 3 == 0
mask # 0,3,6号是True
# 把new_data覆盖到df的B列的指定位置
"B"] = new_data
df.loc[mask,
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 |
还是那个问题:Pandas也要区分副本和视图。
loc[]
,你对此进行赋值,将会修改原数据。.copy()
方法,你对此赋值,不会改变原值。但问题在于:这两种操作的区分往往不明显,因此直接修改DF数据时,最好复查。
= pd.DataFrame(dict(A=np.arange(8) + 1, B=np.random.randn(8)))
df 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 |
= df.copy() # 把df拷贝一份
df1
# 选择B > 0 的样本,并且把他们的A属性改为99
# `loc[]`生成一个视图,直接赋值可以正常修改原值
> 0, "A"] = 99
df1.loc[df1.B 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 |
= df.copy()
df2
# 链式操作:多次取行或列,串联在一起,最终赋值
# 先选择B>0的行,在选择A列,赋值
> 0]["A"] = 99
df2.loc[df2.B
# 修改原值失败!并且弹出警告
# 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 |
# 失败但无警告
= df.copy()
df3
# 这是个隐蔽的链式操作!
# 等价于 df3[['A','B']].loc[df3.B>0,'A'] = 999
= df3[["A", "B"]]
x > 0, "A"] = 999
x.loc[df3.B 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 |
利用以下虚拟数据,在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,
}= pd.DataFrame(data)
df 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 |
Pandas使用依然使用np.nan
来表示缺失值。
构造一个带有缺失值的Series对象(一个列):
= pd.Series(["a", "b", np.nan, "d", np.nan]) # 创建一个Series对象(一列)
df print(df)
0 a
1 b
2 NaN
3 d
4 NaN
dtype: object
常用的NA方法有:
isnull()
: 判断什么值为缺失值notnull()
: 与上一个方法相反。dropna()
:去除缺失值fillna()
: 按一定的规则填充缺失值# 使用Series对象自带的isnull()方法 df.isnull()
0 False
1 False
2 True
3 False
4 True
dtype: bool
问:这一列是否有NA,或者有多少个?对上述结果求和即可!True会被是为1,False被视为0
sum() # 2个缺失值 df.isnull().
2
使用dropna
可以返回所有非NA的值:
# 注意,要修改df本身,需要用参数inplace = True, df.dropna()
0 a
1 b
3 d
dtype: object
与下面的代码是等价的。
# 先获得非na值的布尔序列,再筛选。 df[df.notna()]
0 a
1 b
3 d
dtype: object
但是对于DataFrame对象(二维表格,多个列的横向合并),情况稍微复杂一点:不同的列的缺失值可能在不同的位置。
= pd.DataFrame(
df 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的行。
="all") df.dropna(how
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
"D"] = np.nan # 加一列全NA列
df[ 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 |
="all", axis=1) # 去掉所有值都为NA的列。 df.dropna(how
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 |
如果需要填充缺失值,而不是去除,可以使用fillna()
。
# 创建随机数df,
# 利用np.random.randn()创建7行3列的标准正态分布随机数,转为DataFrame
= pd.DataFrame(np.random.randn(7, 3))
df = ["A", "B", "C"]
df.columns 2:5, 1] = np.nan
df.iloc[2, 2] = np.nan
df.iloc[: 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 |
0) # 用0填充缺失值 df.fillna(
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 |
还可以用均值填充,这样填充后不改变均值
# 还可以填充均值或者中位数
=True) # 用均值填充
df.B.fillna(df.B.mean(), inplace=True) # 用中位数填充 df.C.fillna(df.C.median(), inplace
/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) # 用中位数填充
= pd.DataFrame(dict(A=["a", "b", "c"] * 2, B=[1, 2, 3] * 2))
df 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 |
= np.random.uniform(30, 100, 6).round(1)
scores = pd.DataFrame(dict(name=list("陈李张王周吴"), score=scores))
df df
name | score | |
---|---|---|
0 | 陈 | 44.5 |
1 | 李 | 55.8 |
2 | 张 | 67.8 |
3 | 王 | 30.0 |
4 | 周 | 89.9 |
5 | 吴 | 83.2 |
例如,把score列转为rank(ABCDE)。
利用map()函数,你提供一个转换器(字典),可以把ii某一列所有的值,转为 dadf
= df.score // 10 # 分数整除(获得十位)
score10x # 这是一个Series score10x
0 4.0
1 5.0
2 6.0
3 3.0
4 8.0
5 8.0
Name: score, dtype: float64
= {10: "A", 9: "A", 8: "B", 7: "C", 6: "D"} # 不同的分数段,对应的评级
score_to_rank = score10x.map(score_to_rank) # 利用map()函数,把分数的10位映射为rank
rank rank
0 NaN
1 NaN
2 D
3 NaN
4 B
5 B
Name: score, dtype: object
"rank"] = rank.fillna("E") # 低于60不在字典中,会映射为NA,填充'E'即可
df[ 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 |
除了提供一个字典,也可以提供一个函数。显然函数的灵活性会高一点,比如可以写函数说明,参数检查,简单测试等等。
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"
map(calc_rank) # Series的map方法,既可以接受字典dict,也可以接受函数。 df.score.
0 E
1 E
2 D
3 E
4 B
5 B
Name: score, dtype: object
某些特殊的值有特殊的含义,比如被访者不在,拒绝回答,无法识别答案等等。
# 有一列数据,999和-1分别表示不同的异常情况,比如999是拒绝回答,-1是被访者不在
= pd.Series([1, 2, 999, 3, 4, -1])
df df
0 1
1 2
2 999
3 3
4 4
5 -1
dtype: int64
# replace()方法可以接受一个字典,其中定义了不同值的替代,如999 -> nan,-1 -> 0。
999: np.nan, -1: 0}) df.replace({
0 1.0
1 2.0
2 NaN
3 3.0
4 4.0
5 0.0
dtype: float64
999, -1], np.nan) # 或者多个值转为1个值: 999和-1都转为nan。 df.replace([
0 1.0
1 2.0
2 NaN
3 3.0
4 4.0
5 NaN
dtype: float64
使用以下虚拟数据,完成下列练习。全部完成后,打印最终数据。
注意:和前面的所有练习一样,这个练习也有一个小知识点需要大家自行探索, 并且题目中埋了个雷。
Age
列中的缺失值。Score
列中的缺失值。Age
列中的特殊值-1替换为平均年龄。Score
列中的特殊值9999替换为中位数。City
列中的特殊值-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",
],
}
= pd.DataFrame(data) df
Series是序列对象,DataFrame是二维表格对象。序列可以看成是表格的一行或者一列, 表格可以也看出由序列横向或者纵向合并而成。
Series可以由Python的列表,或者NumPy的ndarray创建。
import pandas as pd
import numpy as np
= pd.Series([5, 6, 7, 8])
a a
0 5
1 6
2 7
3 8
dtype: int64
可见,Series创建时已经自带了index。也可以在创建时指定name属性,以及自定index
= pd.Series([5, 6, 7, 8], name="A", index=list("abcd"))
a a
a 5
b 6
c 7
d 8
Name: A, dtype: int64
我们后面用默认的整数索引,因此用.reset_index()
方法重置索引。(当然,上一个cell中,不用index参数也一样)
= a.reset_index(drop=True) # drop=True会完全丢弃索引;默认为false,索引会成为index列
a a
0 5
1 6
2 7
3 8
Name: A, dtype: int64
创建另一个索引,名字叫B
= pd.Series(list("ABCD"), name="B")
b b
0 A
1 B
2 C
3 D
Name: B, dtype: object
pd.concat()
可以把Series或者DataFrame横向或者纵向地“粘贴”在一起,并且对齐行标签(横向粘贴)或者对其列标签(纵向粘贴)。
我们可以把两个Series横向合并,获得一个DataFrame。
此时,Series的name属性,会成为每一列的header。
注意,pd.concat
接收的参数是要合并的Series或者DataFrame的列表。参数axis=1
表示横向合并(列合并)。
= pd.concat([a, b], axis=1)
df df
A | B | |
---|---|---|
0 | 5 | A |
1 | 6 | B |
2 | 7 | C |
3 | 8 | D |
显然,把Series横向合并到DataFrame也是常用操作。
= pd.Series(["陈", "李", "张", "黄"], name="C")
c =1) pd.concat([df, c], axis
A | B | C | |
---|---|---|---|
0 | 5 | A | 陈 |
1 | 6 | B | 李 |
2 | 7 | C | 张 |
3 | 8 | D | 黄 |
如果index不完全相同,会发生什么?
= pd.Series(["陈", "李", "张", "黄"], name="C", index=[1, 2, 3, 4])
c =1) pd.concat([df, c], axis
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的索引,也是可以的。
= pd.Series(["陈", "李", "张", "黄"], name="C", index=[1, 2, 3, 4])
c = pd.concat([df.reset_index(drop=True), c.reset_index(drop=True)], axis=1)
d d
A | B | C | |
---|---|---|---|
0 | 5 | A | 陈 |
1 | 6 | B | 李 |
2 | 7 | C | 张 |
3 | 8 | D | 黄 |
当然,纵向合并(增加行)也完全可以。以2个DataFrame为例:
先构造2个DataFrame
= d.iloc[:2]
g g
A | B | C | |
---|---|---|---|
0 | 5 | A | 陈 |
1 | 6 | B | 李 |
= d.iloc[2:]
h = h.reset_index(drop=True)
h h
A | B | C | |
---|---|---|---|
0 | 7 | C | 张 |
1 | 8 | D | 黄 |
2个DataFrame的index会原样保留,axis=0
表示纵向(上下)合并。
=0) pd.concat([g, h], axis
A | B | C | |
---|---|---|---|
0 | 5 | A | 陈 |
1 | 6 | B | 李 |
0 | 7 | C | 张 |
1 | 8 | D | 黄 |
参数ignore_index = True
则会重置index。
=0, ignore_index=True) pd.concat([g, h], axis
A | B | C | |
---|---|---|---|
0 | 5 | A | 陈 |
1 | 6 | B | 李 |
2 | 7 | C | 张 |
3 | 8 | D | 黄 |
Merge和横向的concat有点类似,但是Merge横向合并2个DataFrame的依据是某一列,或者多列。
构造2个示范数据:
# 同学信息
= pd.DataFrame(dict(id=[1, 2, 3], name=["A", "B", "C"]))
df1 df1
id | name | |
---|---|---|
0 | 1 | A |
1 | 2 | B |
2 | 3 | C |
# 数学成绩表
= pd.DataFrame(dict(id=[2, 3, 4], math=[80, 90, 95]))
df2 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
# 默认情况是取交集:id = 1 同学没数学成绩,因此不会被合并进来 pd.merge(df1, df2)
id | name | math | |
---|---|---|---|
0 | 2 | B | 80 |
1 | 3 | C | 90 |
pd.merge(="id"
df1, df2, on# 和上一段代码等价。id列是同名列,因此可以选择是否手动指定。 )
id | name | math | |
---|---|---|---|
0 | 2 | B | 80 |
1 | 3 | C | 90 |
参数 how = 'outer'
,可以进行外合并(取并集),缺失的部分自动填充NaN。
="outer") pd.merge(df1, df2, how
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'
)为标准,只保留指定的表的个案。
="right") # 只保留df2“右表”中有的个案,左表没有的个案会填充NaN pd.merge(df1, df2, how
id | name | math | |
---|---|---|---|
0 | 2 | B | 80 |
1 | 3 | C | 90 |
2 | 4 | NaN | 95 |
merge()除了可以自动判断同名列,还可以有你指定列名。
构造一个新的df,其中学号的列名为stu_id
= pd.DataFrame(dict(stu_id=[1, 2], python=[75, 85]))
df3 df3
stu_id | python | |
---|---|---|
0 | 1 | 75 |
1 | 2 | 85 |
参数left_on
和right_on
分别指定坐标和右表的列
# 左表指定id,右表指定stu_id
="id", right_on="stu_id", how="outer") pd.merge(df1, df3, left_on
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 ="outer")
df1.merge(df2, how="id", right_on="stu_id", how="outer")
.merge(df3, left_on"stu_id", axis=1)
.drop(
) 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 |
见作业专用ipynb文件。
先构造一个示例数据。
# 例行公事
import pandas as pd
import numpy as np
# 创建示例代码
# 创建4个班,12位同学
= pd.DataFrame(dict(class_id=list("1234") * 3, name=list("ABCDEFGHIJKL")))
df
# 随机分配2门课的分数
"math"] = np.random.randint(50, 100, 12) # 随机整数(起点,终点,数量)
df["python"] = np.random.randint(45, 95, 12)
df["math_rank"] = df.math.map(
df[
calc_rank# 如果提示找不到calc_rank,记得把前面定义calc_rank的cell执行一下
) "python_rank"] = df.python.map(calc_rank)
df[
= df.sample(10).sort_index() # 抽取10人/随机排除2人,再按index排序
df 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 |
在进行分组和聚合之前,先说明一些简单统计的方法。
.describe()
:计算数值列的:个案数,均值,标准差,最小值,分位数:25%、50%(中位数)、75%,最大值.mean()
。.agg()
聚合:计算列的多种统计值。.value_counts()
:计算离散变量出现的频率。统计型描述:.describe()
:
计算数值列的:个案数,均值,标准差,最小值,分位数:25%、50%(中位数)、75%,最大值
round(2) df.describe().
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等等,不能一一列举,请用搜索引擎。
"math", "python"]].mean() df[[
math 78.3
python 71.7
dtype: float64
对多个列求多个统计量:.agg()
# 利用agg进行聚合:对于每一列,计算指定函数的结果。参数是“函数名”,是字符串,有引号。如“mean”
"math", "python"]].agg(["mean", "median"]) df[[
math | python | |
---|---|---|
mean | 78.3 | 71.7 |
median | 80.0 | 72.5 |
计算离散变量出现的频率:.value_counts()
# math_rank是一个离散型变量:ABCDE,因此可以用.value_counts()来统计变量值出现的频率。
"math_rank"]].value_counts().sort_index() df[[
math_rank
A 4
B 1
C 2
D 1
E 2
Name: count, dtype: int64
有大量操作是按分组进行的,如
这种情况一般可以采用分组和聚合的操作:
df.groupby(<分组列>).<聚合操作>
# 求班级人数: count()分组的样本数
# groupby('class_id') :按class_id分组
# count(): 对于每个组,求非NA的样本数
"class_id").count() df.groupby(
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转为一个普通列
"class_id")[["python", "math"]].mean().round().reset_index() df.groupby(
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课最高分的同学
# 先多重排序,看看我们要选择的人
"class_id", "python"]) df.sort_values([
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): 取每组的第一个
"python", ascending=False).groupby("class_id").head(1) df.sort_values(
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 |
如果要对每一个分组都进行多个聚合,可以用agg()
方法,传递一个list,其中包括每个函数的“函数名”
agg([ <函数名>, <函数名>,...])
# 对于python和math,求每个班的平均分,最高分
# 传递给agg()的是一个list,其中包括每个函数的“函数名”
# 分组 —> 要计算的列 -> agg(多个函数)
"class_id")[["python", "math"]].agg(["mean", "max"]).round(2) df.groupby(
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,传递的是函数本身,而不是函数名的字符串(没有引号)
"class_id")[["python", "math"]].agg([calc_range]) df.groupby(
python | math | |
---|---|---|
calc_range | calc_range | |
class_id | ||
1 | 19 | 13 |
2 | 15 | 49 |
3 | 35 | 35 |
4 | 35 | 9 |
# 注意到,列名是“函数名”,可能不是很合理
# 可以给agg传递函数名,也可以传递一个元组 `(列名, 函数名)`
"class_id")[["python", "math"]].agg(
df.groupby("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 |
如果对于每个分组,都要进行一个很复杂的操作,那么可以对分组进行循环,可以在循环体内获得该分组的数据。
对每一个组处理完毕后,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最高分的行:正序排列取最末尾行
1].sort_values("math").tail(1))
result.append(x[
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]
见作业专用ipynb文件。
典型的时间序列数据就是股价。在Pandas中,一般把时间信息(表示时间的列),转为索引index,便于我们操作。
# 例行公事的导入
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",
]
= pd.DataFrame(dict(date=date, x=np.arange(len(date)) + 8))
df 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 |
# date 的类型是object(即字符串str) df.info()
<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
一般的预处理:
= pd.to_datetime(df.date)
df.date "date", inplace=True)
df.set_index( 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 |
按整数序号索引,切片等,和一般的DataFrame类似。
# 索引
1] df.iloc[
x 9
Name: 2021-03-01 00:00:00, dtype: int64
# 切片
2:4] df.iloc[
x | |
---|---|
date | |
2021-03-05 | 10 |
2022-01-01 | 11 |
对于时间序列数据(index是datetime的DataFrame),按索引取值(.loc()
)有一些特殊的便利:可以接受任何“可以解释为时间”的字符串:
"2022-01-01"] # 按行标签取值,标准做法,和一般的DataFrame一样 df.loc[
x 11
Name: 2022-01-01 00:00:00, dtype: int64
# 写成其他格式(或者其他地区的标准)也可以被loc[]理解
"20220101"]
df.loc["01/01/2022"] df.loc[
x 11
Name: 2022-01-01 00:00:00, dtype: int64
只使用月份或者年份也可以
"2021"] # 提取2021年所有数据 df.loc[
x | |
---|---|
date | |
2021-01-01 | 8 |
2021-03-01 | 9 |
2021-03-05 | 10 |
"2021-03"] # 提取2021年3月所有数据 df.loc[
x | |
---|---|
date | |
2021-03-01 | 9 |
2021-03-05 | 10 |
# 切片也可以接受这类“看起来像日期”的字符串
# 其他的用法和一般的DataFrame类似。
"2021":"2022-1"] # 2021年(全年)到2022年1月的数据 df.loc[
x | |
---|---|
date | |
2021-01-01 | 8 |
2021-03-01 | 9 |
2021-03-05 | 10 |
2022-01-01 | 11 |
时间序列的运算,自然也会以时间为基准。
# 构造另一个DataFrame,和df有所不同
= df.copy()
df2 "2021-03-05", "2022-03-01"], axis=0, inplace=True)
df2.drop(["2022-09-01")] = 14
df2.loc[pd.to_datetime(= ["y"]
df2.columns = df2.y / 2
df2.y 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会自动对齐时间
= pd.concat([df, df2], axis=1) # 横向合并(concat),会自动对齐时间
df3 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 |
# 同样,一般的列运算也会对齐时间
"result"] = (df3.x / 3 - np.sqrt(df3.y)).round(2)
df3[ 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 |
用 pd.date_range()
= pd.date_range("2022-01-15", "2022-02-15") # 定义起点和终点,默认的频率是“D”(日)
x 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')
= pd.date_range("2022-01-15", "2022-06-15", freq="ME") # 频率ME:月
x x
DatetimeIndex(['2022-01-31', '2022-02-28', '2022-03-31', '2022-04-30',
'2022-05-31'],
dtype='datetime64[ns]', freq='ME')
# periods:指定日期的数量
# 频率B:工作日
= pd.date_range("2022-01-15", periods=5, freq="B") # 2022-01-15起的5个工作日
x 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')
构造一个虚拟股价序列,计算(一阶)滞后、(一阶)差分和(日)回报率。
# 构造一个虚拟的价格序列
= pd.DataFrame(dict(price=[10.9, 12.1, 11.3, 11.6, 12.7]), index=x) # x见上一节。
df 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阶
"price_1"] = df.price.shift()
df[ 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()方法。
"diff"] = df.price.diff()
df[ 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()方法。
"ret"] = df["diff"] / df["price_1"]
df[
"ret_2"] = df["price"].pct_change()
df[ 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 |
# 从回报率计算价格
# 利用comprod():序列的累积连乘
# 见:https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.cumprod.html
# 从1开始的价格:
# 价格1 = (1 + 回报率).cumprod()
"price_2"] = (1 + df["ret"].fillna(0)).cumprod()
df[ 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的日回报应该相同。
round(3) == df.ret.round(3) df.price_2.pct_change().
2022-01-17 False
2022-01-18 True
2022-01-19 True
2022-01-20 True
2022-01-21 True
Freq: B, dtype: bool
不同频率周期的数据相互转换:如日频数据到周频或者月频(日线到周线、月线等),或者反过来。
使用.resample()
。
df
# 构造一个3个月的日线序列
= pd.date_range("2022-01-01", "2022-03-31")
ts = pd.DataFrame(dict(value=np.arange(len(ts))), index=ts)
df 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",取每周的最后一个值
"W").last().head() # 周数据的最后一个值,默认是取"周日"为最后一天 df.resample(
value | |
---|---|
2022-01-02 | 1 |
2022-01-09 | 8 |
2022-01-16 | 15 |
2022-01-23 | 22 |
2022-01-30 | 29 |
# 可以设置以周五为最后一天,这样就可以取得全部的周五
# 符合世界上主要国家每周的最后一个工作日,比如股票的周线收盘价
"W-FRI").last().head() df.resample(
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,取区间内所有值的和
"ME").sum() # 月数据的求和 df.resample(
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)的聚合。
import pandas as pd
import numpy as np
构造示例数据
注意:
df['datetime']
列是“日期和时间”格式(Pandas默认),有日期和时间的信息,这会反映在保存后的数据中。df['datetime'].dt.date
可以从中获得仅有“日期”信息(见前面的“时间方法”)。= pd.DataFrame(
df dict(
=pd.date_range("2022-01-15", "2022-01-19"),
datetime=range(1, 6),
number=list("ABCDE"),
name=np.random.randn(5),
value
)
)"date"] = df["datetime"].dt.date
df[ 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
保存为文件。
index
也保存为列,并且可能没有列名(注意看index有没有名字)# 把df保存到工作目录(本ipynb文件所在目录)下的data文件夹下的output.xlsx文件中
# 默认参数,会连带保存index一起保存。如果index无名称,则就会产生一系列无名列。
"data/output.xlsx") df.to_excel(
# 丢弃index
# 如果index没有价值的信息,或者有价值的信息已经通过reset_index()转为普通列,则可以放心丢弃
"data/output_noindex.xlsx", index=False) df.to_excel(
你可以打开这output.xlsx
和output_noindex
进行对比。