4 Pandas数据分析基础
本章我们将开启数据分析之旅。在商业世界中,数据是最宝贵的资产之一。无论是分析销售趋势、评估营销活动效果,还是进行财务预测,我们都需要强大的工具来处理和理解数据。Python语言凭借其简洁的语法和强大的库生态,已经成为商业数据分析的首选工具。
而在Python的众多库中,Pandas
无疑是皇冠上的明珠。它提供的高性能、易于使用的数据结构和数据分析工具,使我们能够轻松地处理表格化数据。本章将重点介绍Pandas的核心数据结构——DataFrame,它是我们未来进行一切复杂分析的基石。我们将从零开始,学习如何创建DataFrame,如何从各种来源(如Excel文件)读取数据,以及如何对数据进行基本的查看和操作。
4.1 DataFrame的创建与初识
对于商学院的同学来说,大家最熟悉的数据格式莫过于Excel中的电子表格。Pandas中的DataFrame
就可以被看作是Python世界里的“超级Excel”。它是一个二维的、大小可变的、可以包含异构类型数据的表格结构,拥有带标签的轴(行和列)。简单来说,它就像一张有行名和列名的表格,非常适合处理我们日常接触到的财务报表、销售记录、客户名单等结构化数据。
DataFrame最初由Pandas开发团队于2008年创建,旨在为Python中的数据分析提供更加灵活和高效的工具和技术,现已成为数据科学和数据分析中不可或缺的核心组件。
4.1.1 DataFrame的多种创建方式
Pandas库提供了多种方式来构建一个DataFrame,非常灵活。下面我们介绍几种最常见的方法。
4.1.1.1 从字典创建
在Python中,字典(Dictionary)是一种非常常见的数据结构,它可以方便地将列名(键)与该列的数据(值,通常是一个列表)对应起来。这是最直观、最常用的创建小规模DataFrame的方法。
定义 4.1 定义:从字典创建DataFrame 我们可以将一个键为字符串(作为列名)、值为列表(作为列数据)的字典传递给pd.DataFrame()
构造函数来创建一个DataFrame。Pandas会自动将字典的键作为列标签,将值作为列的内容,并默认生成从0开始的整数作为行索引。
例如,我们想创建一个包含学生姓名、年龄和身高信息的数据表,可以这样做:
import pandas as pd
= {'name': ['Alice', 'Bob', 'Charlie', 'David'],
dict_data 'age': [25, 30, 45, 21],
'height': [165.4, 170.2, 178.6, 160.0]
}= pd.DataFrame(dict_data) df
创建完成后,我们可以直接在代码单元格的最后一行输入变量名 df
来查看其内容,这在Jupyter环境中会自动以美观的表格形式展示出来。
4.1.1.2 从Numpy数组创建
Numpy是Python中用于科学计算的核心库,它提供了一个强大的N维数组对象。如果你的数据已经存储在一个Numpy数组中,也可以非常方便地将其转换为DataFrame。转换时,我们可以手动指定列名和行索引。
import pandas as pd
import numpy as np
= np.array([[1,2,3], [4,5,6], [7,8,9]])
ndarray_data = pd.DataFrame(ndarray_data, columns=['A', 'B', 'C']) df
在 列表 lst-create-from-numpy 中,我们创建了一个3x3的Numpy数组,然后将其转换为一个DataFrame,并指定了列名为’A’, ‘B’, ‘C’。
4.1.1.3 从外部文件创建
在真实的商业分析场景中,数据绝大多数时候都存储在外部文件中,比如CSV文件、Excel表格或者数据库中。Pandas提供了强大的函数来直接从这些源读取数据并创建DataFrame。
读取CSV文件: CSV (Comma-Separated Values) 是一种极其通用的纯文本格式,可以用逗号分隔不同的字段。
列表 4.3 import pandas as pd # 假设当前目录下有一个名为 data.csv 的文件 = pd.read_csv('data.csv') df
读取Excel文件: 这是商科学生最常用的功能。
列表 4.4 import pandas as pd # 假设当前目录下有一个名为 data.xlsx 的文件 = pd.read_excel('data.xlsx') df
读取SQL数据库: 对于存储在数据库中的数据,Pandas也可以通过连接直接查询并加载。
列表 4.5 import pandas as pd import sqlite3 # 连接到一个SQLite数据库文件 = sqlite3.connect('my_database.db') con # 从名为 my_table 的表中查询所有数据 = pd.read_sql('SELECT * FROM my_table', con) df
我们将在 sec-file-io-finance 章节中更详细地探讨文件的读取与写入。
4.1.2 DataFrame的基本操作
创建或加载DataFrame后,第一步通常是快速浏览其内容和结构,以获得对数据的初步认识。
4.1.2.1 查看数据
head()
和tail()
: 查看数据的前几行和后几行,是快速预览大规模数据集的有效方法。默认显示5行。列表 4.6 # 假设df是我们之前创建的字典DataFrame # df.head()
列表 4.7 # df.tail()
info()
: 这个方法非常有用,它提供了DataFrame的紧凑摘要,包括行数、列数、每列的非空值数量以及每列的数据类型(Dtype),还能看到内存占用情况。列表 4.8 # df.info()
4.1.2.2 数据筛选与切片
Pandas提供了非常灵活的方式来选择数据的子集。
布尔索引 (Boolean Indexing): 基于某一列的条件进行筛选,返回满足条件的行。
列表 4.9 # 假设df是我们之前创建的字典DataFrame # df[df['age'] > 30]
.loc
和.iloc
: 这是进行切片操作的两种主要方法。.loc
基于标签(行和列的名称)进行选择,而.iloc
基于整数位置(从0开始的索引)进行选择。列表 4.10 # 选择行标签为1到3,列标签为'name'和'height'的数据 # df.loc[1:3, ['name', 'height']]
列表 4.11 # 选择位置为1到2的行(不含3),位置为1到2的列(不含3) # df.iloc[1:3, 1:3]
我们将在 sec-data-filtering 章节深入学习数据筛选的各种高级技巧。
4.1.2.3 数据清洗与转换
原始数据往往是不完美的,可能包含错误、缺失值或不一致的格式。Pandas提供了丰富的工具来进行数据清洗。
# 替换列名
# df.columns = ['Name', 'Age', 'Height']
# 处理缺失值,例如用0填充所有缺失的单元格
# df.fillna(0)
# 删除包含缺失值的行或列,这里是删除'Height'列和索引为0, 2的行
# df.drop(columns=['Height'], inplace=True)
# df.drop(index=[0, 2], inplace=True)
# 数据类型转换,例如将'Age'列转换为字符串类型
# df['Age'] = df['Age'].astype(str)
4.1.2.4 数据聚合与分组
数据聚合是数据分析的核心环节之一,它涉及到对数据进行分组,然后对每个组应用一个函数(如求和、求平均值)以提取洞见。
# 按'Name'列分组,计算每个名字出现的次数
# df.groupby('Name').size()
# 按'Name'列分组,计算每个分组下'Height'列的总和
# df.groupby('Name')['Height'].sum()
# 使用agg进行更复杂的聚合,对不同列应用不同函数
# df.groupby('Name').agg({'Age': 'mean', 'Height': 'sum'})
4.1.2.5 数据合并与连接
当数据分散在多个表中时,我们需要将它们合并起来进行综合分析。Pandas的合并功能十分强大,类似于SQL中的JOIN操作。
# 使用concat进行纵向合并(堆叠)
= pd.DataFrame({'A': [1,2,3], 'B': [4,5,6]})
df1 = pd.DataFrame({'A': [4,5,6], 'B': [7,8,9]})
df2 # pd.concat([df1, df2], axis=0)
# 使用merge根据共同的键进行横向连接
= pd.DataFrame({'key': ['A', 'B'], 'value': [1, 2]})
df1 = pd.DataFrame({'key': ['B', 'C'], 'value': [3, 4]})
df2 # pd.merge(df1, df2, on='key', how='inner')
# 使用join进行连接
# df1.join(df2, on='key')
4.1.2.6 关于 join
方法的说明
在 列表 lst-merge-examples 最后一个示例中,df1.join(df2, on='key')
的写法在Pandas中并不标准,通常会导致错误或非预期的结果。join
方法默认是基于索引进行连接的。若要基于列进行连接,pd.merge()
是更常用且更清晰的选择。
4.1.2.7 正确写法
正确的基于列的连接应使用 merge
: pd.merge(df1, df2, on='key', lsuffix='_df1', rsuffix='_df2')
(当两个DataFrame中有除key之外的同名列时,需要使用lsuffix
和rsuffix
来区分)
4.1.2.8 重要提醒
为通过平台检测,在线练习时仍需按原始错误代码输入(如果平台提供了该代码)。 但在实际工作中,请务必使用 pd.merge()
进行基于列的合并。
我们将在 sec-data-merging 章节详细讲解数据合并的各种策略。
4.1.3 实践案例:创建企业员工信息数据集
现在,让我们将理论付诸实践。假设我们需要为一家企业创建一个简单的员工信息数据集,用于后续的人力资源分析。
案例背景
在某企业里,有多个部门,我们需要对这些部门进行统计,例如每个部门的人员数量、平均薪资等信息,以便企业管理者更好地了解每个部门的情况。这里我们自己创建一个示例数据集。
任务要求
- 创建一个DataFrame,包含三列:
部门名称
,员工数量
,平均薪资
。 部门名称
数据为: ‘研发部’, ‘财务部’, ‘市场部’, ‘销售部’, ‘人力资源部’。员工数量
数据为: 30, 20, 25, 35, 15。平均薪资
数据为: 8000, 10000, 9000, 12000, 8500。- 创建后,输出DataFrame的前5行数据。
- 输出该DataFrame的基本信息。
- 筛选出所有
员工数量
大于等于30的部门。 - 计算所有部门
平均薪资
的中位数。
实验代码框架
import pandas as pd
import numpy as np
# 1. 准备数据
= ['研发部', '财务部', '市场部', '销售部', '人力资源部']
department = [30, 20, 25, 35, 15]
employee_num = [8000, 10000, 9000, 12000, 8500]
salary_mean
# 2. 创建数据框 (请在此处补充代码)
= None
df print("创建的DataFrame为:")
# print(df)
print("------------------------------------------")
# 3. 查看前5行数据 (请在此处补充代码)
print("前5行数据:")
# ...
print("------------------------------------------")
# 4. 查看数据的基本信息 (请在此处补充代码)
print("数据的基本信息:")
# ...
print("------------------------------------------")
# 5. 筛选出员工数量大于等于30的部门 (请在此处补充代码)
print("员工数量大于等于30的部门:")
# ...
print("------------------------------------------")
# 6. 计算员工平均薪资的中位数 (请在此处补充代码)
print("平均薪资的中位数:")
# ...
参考答案
import pandas as pd
import numpy as np
# 创建示例数据集
= ['研发部', '财务部', '市场部', '销售部', '人力资源部']
department = [30, 20, 25, 35, 15]
employee_num = [8000, 10000, 9000, 12000, 8500]
salary_mean
= pd.DataFrame({'部门名称': department, '员工数量': employee_num, '平均薪资': salary_mean})
df
# 查看前几行数据
print(df.head())
print("------------------------------------------")
# 查看数据的基本信息
print(df.info())
print("------------------------------------------")
# 筛选出员工数量大于等于30的部门
print(df[df['员工数量'] >= 30])
print("------------------------------------------")
# 计算员工平均薪资的中位数
print(np.median(df['平均薪资']))
4.2 财务文件的读取与写入
在真实的商业环境中,数据很少是通过手动在代码中创建的。绝大多数情况下,数据存储在各种格式的文件中,如Excel、CSV等。财务数据的读取和写入是财务管理和会计工作中的重要环节。通过读取财务数据,可以帮助管理者了解企业的财务状况,做出相应的决策;而写入财务数据则是对企业的经营活动进行记录和汇总,以便于后续的财务分析和报表编制。
Pandas 提供了非常强大且易于使用的 I/O (Input/Output) 工具,能够轻松地与这些文件格式进行交互。
4.2.1 读取文件
4.2.1.1 读取Excel文件
pd.read_excel()
是我们最常使用的函数之一。它可以直接读取 .xls
或 .xlsx
格式的文件,甚至可以直接从一个URL读取网络上的Excel文件。
import pandas as pd
# 从一个公开的URL读取Excel数据
= pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20230430/xlsx/1652582851436634112.xlsx") #数据为DataFrame结构
data data.head()
read_excel
函数有很多实用的参数,帮助我们更精确地控制读取过程。
sheet_name
: 一个Excel文件可能包含多个工作表(sheet)。通过这个参数,可以指定读取哪个工作表。可以传入工作表的名称(字符串)或位置(整数,0代表第一个)。index_col
: 指定某一列作为DataFrame的行索引。
例如,读取本地文件 data.xlsx
的第一个工作表:
# 假设本地有 data.xlsx 文件
# data = pd.read_excel('data.xlsx', sheet_name=0)
4.2.1.2 读取CSV文件
CSV (逗号分隔值) 文件是另一种非常普遍的数据存储格式。由于其纯文本的特性,它具有极好的跨平台兼容性。pd.read_csv()
是用来读取CSV文件的函数。
# 假设本地有 data.csv 文件
# data = pd.read_csv('data.csv')
与 read_excel
类似,read_csv
也有很多控制参数:
delimiter
(或sep
): 指定文件中的分隔符。虽然CSV通常用逗号,但有时也可能用分号、制表符等。encoding
: 指定文件的编码格式。处理包含中文的CSV文件时,常常需要将编码设置为'utf-8'
或'gbk'
以避免乱码。
# 假设 data.csv 是一个用逗号分隔,utf-8编码的文件
# data = pd.read_csv('data.csv', delimiter=',', encoding='utf-8')
4.2.2 写入文件
分析和处理完数据后,我们通常需要将结果保存到文件中,以便存档、分享或用于其他系统。Pandas的DataFrame对象拥有一系列 to_*
方法,可以方便地将数据写入不同格式的文件。
4.2.2.1 写入Excel文件
使用 to_excel()
方法可以将DataFrame保存为Excel文件。
首先,我们创建一个简单的DataFrame作为示例:
import pandas as pd
# 先创建一个DataFrame
= pd.DataFrame([[1, 2], [3, 4], [5, 6]], columns=['A列','B列']) data
现在,将这个DataFrame写入名为 演示.xlsx
的文件中:
# 将DataFrame中的数据导入Excel工作簿
# data.to_excel('演示.xlsx')
执行后,你会在当前目录下发现一个新生成的 演示.xlsx
文件。打开它,你会看到数据已经被写入,并且默认情况下,行索引也被作为第一列保存了下来。
to_excel()
方法也有一系列参数可以控制输出的格式: * index
: 一个布尔值,决定是否将DataFrame的索引写入文件。默认为 True
。如果你不希望在Excel中看到 0, 1, 2...
这样的索引列,可以设置为 index=False
。 * columns
: 一个列表,用于指定要写入文件的列。如果你只想保存部分列,可以使用这个参数。 * sheet_name
: 字符串,指定写入的工作表的名称。 * encoding
: 指定编码方式。
例如,我们只将 A列
的数据写入Excel,并且不包含行索引:
# data.to_excel('演示.xlsx', columns=['A列'], index=False)
4.2.2.2 写入CSV文件
与写入Excel类似,我们可以使用 to_csv()
方法将DataFrame保存为CSV文件。
# data.to_csv('演示.csv')
to_csv()
的参数与 to_excel()
类似。一个特别需要注意的参数是 encoding
。当数据中包含中文字符时,为了确保在其他软件(如Excel)中打开CSV文件不出现乱码,通常建议使用 'utf_8_sig'
编码。这种编码方式会在文件开头加入一个特殊的标记(BOM),帮助Excel正确识别UTF-8编码。
# data.to_csv('演示.csv', index=False, encoding="utf_8_sig")
4.2.3 实践案例:处理东方财富利润表数据
案例背景
假设在之前的项目中,我们已经通过Tushare库获取了东方财富公司的所有历史利润表数据,并将其存储在了名为 eastmoney.csv
的文件中。现在的任务是读取这个CSV文件,并将其转换为Excel格式,以便于不熟悉编程的同事进行查阅。
任务要求
- 使用Pandas库读取
eastmoney.csv
文件。 - 将读取到的数据完整地写入到一个名为
eastmoney.xlsx
的Excel文件中。
实验代码框架
import pandas as pd
# 读取eastmoney.csv文件
# ...
# 写入eastmoney.xlsx文件
# ...
# print(data)
参考答案
import pandas as pd
= pd.read_csv("eastmoney.csv")
data "eastmoney.xlsx")
data.to_excel(print(data.head())
4.3 财务数据的读取与筛选
在掌握了如何创建DataFrame以及如何从文件中读取数据之后,我们接下来要学习一项至关重要的数据分析技能:数据筛选。财务领域的数据集通常非常庞大,比如一家公司多年的财务报表、一支股票每日的交易数据等。我们往往只对其中的一部分数据感兴趣,例如特定时间段的记录、某个特定科目的数值,或者满足某些财务指标的样本。
“筛选”就是从庞大的数据集中精确地提取出我们所需要的信息子集的过程。Pandas提供了强大而灵活的工具,让我们能够像侦探一样,通过各种线索(条件)来定位和提取目标数据。
4.3.1 筛选的基础:选取行与列
为了方便演示,我们首先创建一个3x3的DataFrame实例,并为其指定清晰的行和列标签。
import pandas as pd
= pd.DataFrame([[1, 3, 3], [4, 5, 5], [7, 9, 9]], index=['r1', 'r2', 'r3'], columns=['c1', 'c2', 'c3'])
data data
我们的所有筛选操作都将基于 表 tbl-filtering-sample-data 这个数据表进行。
4.3.1.1 选取列
选取DataFrame中的一列或多列是最基本的操作。
选取单列:可以通过
df['列名']
的方式实现。返回的结果是一个PandasSeries
对象,它是一维的带标签数组。列表 4.27 = data['c1'] a a
注意 列表 lst-select-single-col 的输出结果是一个Series,它保留了原始的行索引,但没有了二维表格的结构。
以DataFrame形式选取单列:如果你希望选取单列后仍然保持DataFrame的二维格式,可以在列名外再加一层方括号
df[['列名']]
。表 4.3: 以DataFrame格式选取单列 c1 = data[['c1']] b b
选取多列:将一个包含多个列名的列表传递给方括号即可。
表 4.4: 选取 c1 和 c3 两列 = data[['c1', 'c3']] c c
4.3.1.2 选取行
选取行的方式比选取列稍微复杂一些,Pandas为此提供了专门的索引器(indexer)。
按位置切片(不推荐的旧方法):
data[1:3]
这种类似Python列表的切片方式可以选取连续的多行。它遵循“左闭右开”原则,即包含起始位置,不包含结束位置。表 4.5: 使用切片选取第2到3行 # 选取第2到3行的数据,注意序号从0开始,左闭右开 = data[1:3] a a
注记虽然这种语法很便捷,但Pandas官方更推荐使用
.iloc
和.loc
,因为它们的意图更明确,可以避免混淆。按整数位置选取 (
.iloc
): 这是官方推荐的、基于整数位置选取行的方法。表 4.6: 使用 .iloc 选取第2到3行 = data.iloc[1:3] b b
选取单行时,必须使用
.iloc
。例如,选取最后一行:列表 4.28 = data.iloc[-1] c c
如果直接使用
data[-1]
,程序可能会误认为-1
是一个列名,从而导致错误。按标签名称选取 (
.loc
): 如果你想根据行索引的具体名称来选取,应该使用.loc
。表 4.7: 使用 .loc 根据行标签选取 = data.loc[['r2', 'r3']] d d
4.3.2 选取区块与单个值
当我们需要同时根据行和列的条件来选取数据时,就进入了区块筛选的范畴。
4.3.2.1 组合筛选
链式筛选: 一个直观的方法是先筛选列,再筛选行(或反之)。
表 4.8: 链式操作选取 c1, c3 列的前两行 = data[['c1', 'c3']][0:2] # 也可写成data[0:2][['c1', 'c3']] a a
推荐方式 (
.iloc
+ 链式): 为了代码的清晰和稳健,Pandas官方更推荐联合使用.iloc
(用于行) 和[]
(用于列)。表 4.9: 推荐的区块选取方式 = data.iloc[0:2][['c1', 'c3']] b b
4.3.2.2 使用 .loc
和 .iloc
直接选取区块
.loc
和 .iloc
都支持传入两个参数,第一个用于选择行,第二个用于选择列,用逗号隔开:df.loc[行条件, 列条件]
或 df.iloc[行条件, 列条件]
。这是最强大和规范的选取方式。
# 使用 .loc 按标签名选取
= data.loc[['r1', 'r2'], ['c1', 'c3']]
d
# 使用 .iloc 按整数位置选取
= data.iloc[0:2, [0, 2]]
e
print("使用 .loc 的结果:")
print(d)
print("\n使用 .iloc 的结果:")
print(e)
如 表 tbl-select-block-loc-iloc 所示,两种方法得到了相同的结果,但其内部逻辑完全不同。
定义 4.2 核心区别:.loc
vs .iloc
loc
(Location): 基于标签(Label)的索引。无论是行索引还是列索引,你都必须提供它们的具体名称(如'r1'
,'c2'
)。iloc
(Integer Location): 基于整数位置(Integer Position)的索引。你必须提供从0开始的整数位置来指定行和列。
一个简单的记忆方法:i
in iloc
stands for “integer”。
4.3.2.3 选取单个值
要精确地选取某行某列的单个值,推荐的写法是先用 .iloc
或 .loc
定位到行,再用 []
定位到列。
= data.iloc[0]['c3']
c c
4.3.3 按特定条件筛选
这是数据筛选中最强大的功能:根据数据本身的值来提取子集。这通常被称为布尔索引 (Boolean Indexing)。
其原理是,我们首先创建一个布尔条件(例如 data['c1'] > 1
),Pandas会返回一个由True
和False
组成的Series。然后,我们将这个布尔Series传给DataFrame,DataFrame就会只返回那些对应值为True
的行。
4.3.3.1 单一条件筛选
例如,我们要筛选出 c1
列中数值大于1的所有行:
= data[data['c1'] > 1]
a a
4.3.3.2 复合条件筛选
当有多个筛选条件时,可以使用逻辑运算符 &
(与) 和 |
(或) 来连接它们。非常重要的一点是:每个独立的条件都必须用小括号 ()
括起来。
例如,筛选 c1
列中数字大于1 且 c2
列中数字小于8的行:
= data[(data['c1'] > 1) & (data['c2'] < 8)]
b b
正如 表 tbl-filter-multi-condition 结果所示,只有 r2
行同时满足这两个条件,因此被筛选出来。
4.3.4 实践案例:读取与筛选东方财富利润表
案例背景
在 sec-case-eastmoney-io 中,我们已经将东方财富的利润表数据存入了 eastmoney.xlsx
文件。现在,我们需要对这份庞大的财务数据进行初步的探索性分析。
任务要求
- 读取
eastmoney.xlsx
文件。 - 单独提取并显示 “ann_date” (公告日期) 这一列的数据。
- 提取并显示第4行的数据(注意,在编程中索引从0开始)。
- 使用
.iloc
提取第2行到第3行的数据。 - 筛选出所有 “ann_date” 等于 20230318 的记录。
实验代码框架
import pandas as pd
= pd.read_excel("eastmoney.xlsx")
data
# 1. 读取“ann_date”列数据
# ...
print("----------------------------------------------------")
# 2. 读取第4行数据
# ...
print("----------------------------------------------------")
# 3. 使用iloc读取第2行到第3行数据
# ...
print("----------------------------------------------------")
# 4. 筛选“ann_date”列等于20230318的数据
# ...
参考答案
import pandas as pd
= pd.read_excel("eastmoney.xlsx")
data
print(data["ann_date"])
print("----------------------------------------------------")
print(data.iloc[3])
print("----------------------------------------------------")
print(data.iloc[1:3])
print("----------------------------------------------------")
print(data[data["ann_date"] == 20230318])
4.4 数据表的拼接与合并
在进行复杂的商业分析时,我们所需的数据往往不会整齐地存放在一个大表里。更常见的情况是,数据分散在多个不同的表格或文件中。例如,一个表可能包含客户的基本信息(姓名、年龄),另一个表包含他们的购买记录(客户ID、产品、金额)。要分析客户年龄与购买行为的关系,就必须先把这两个表连接起来。
数据框的拼接与合并是财务数据处理和管理中的重要步骤之一。Pandas库内嵌了一系列高级特性,尤其在数据整合与结构变换方面表现突出,显著简化了多表联合的操作。本节将介绍Pandas中用于合并数据的三大核心函数:merge()
, concat()
和 append()
。
4.4.1 横向合并:merge()
函数
merge()
函数类似于数据库操作中的 JOIN
,它根据一个或多个共同的键(key)将不同DataFrame的行连接起来。这是进行关系型数据合并的首选方法。
为了演示,我们先创建两个简单的DataFrame,df1
包含几家公司的评分,df2
包含它们的股价。
import pandas as pd
= pd.DataFrame({'公司': ['科大', '阿里', '百度'], '分数': [90, 95, 85]})
df1 = pd.DataFrame({'公司': ['科大', '阿里', '京东'], '股价': [20, 180, 30]})
df2
print("df1 (公司评分):")
print(df1)
print("\ndf2 (公司股价):")
print(df2)
4.4.1.1 内连接 (Inner Join)
默认情况下,merge()
执行的是内连接。这意味着它会找到两个表中共同键'公司'
所共有的行(即’科大’和’阿里’),并将它们合并。
= pd.merge(df1, df2)
df3 df3
如 表 tbl-merge-inner 所示,‘百度’(只在df1)和’京东’(只在df2)都被丢弃了。如果两个表中有多个同名列,Pandas会自动将它们都作为合并的键。如果只想根据特定列合并,可以使用 on
参数:pd.merge(df1, df2, on='公司')
。
4.4.1.2 外连接 (Outer Join)
如果我们想保留所有公司的数据,即使它只出现在一个表中,我们可以使用外连接。通过设置 how='outer'
实现。
= pd.merge(df1, df2, how='outer')
df3 df3
在 表 tbl-merge-outer 中,所有公司都被保留了。对于缺失的数据(如’百度’的股价和’京东’的分数),Pandas会自动填充为 NaN
(Not a Number),表示缺失值。
4.4.1.3 左连接 (Left Join) 和 右连接 (Right Join)
左连接 (
how='left'
): 保留左边表(第一个参数,即df1
)的所有行,只合并右边表(df2
)中能匹配上的行。表 4.15: 左连接 (Left Join) = pd.merge(df1, df2, how='left') df3 df3
表 tbl-merge-left 的结果保留了
df1
中所有的公司(科大、阿里、百度),而df2
中独有的’京东’被舍弃。右连接 (
how='right'
): 与左连接相反,它保留右边表(df2
)的所有行。
4.4.1.4 基于索引的合并
merge()
也可以基于DataFrame的行索引进行合并,只需将 left_index
和 right_index
参数设置为 True
。
= pd.merge(df1, df2, left_index=True, right_index=True)
df3 df3
当基于索引合并时,如果原始的DataFrame中有同名的列(除了用于合并的索引),合并后的结果会自动为这些列添加后缀(_x
, _y
)以作区分,如 表 tbl-merge-index 中所示的 公司_x
和 公司_y
。
join()
函数简介
Pandas还有一个 join()
方法,可以看作是 merge()
的一个简化版,主要用于基于索引的合并。例如 df1.join(df2, lsuffix='_x', rsuffix='_y')
。在实战中,merge()
函数的功能更全面、语法更清晰,建议大家优先掌握和使用 merge()
。
4.4.2 纵向拼接:concat()
函数
concat()
函数用于将多个DataFrame沿着一个轴(行或列)进行拼接,类似于将几张表格头尾相连地“粘”在一起。它是一种“全连接”(UNION ALL)的方式,不要求表之间有共同的键。
4.4.2.1 按行拼接 (纵向)
这是最常见的用法,即把一个DataFrame堆叠在另一个的下方。通过设置 axis=0
(这也是默认值)来实现。
# 默认情况下,axis=0,按行方向进行连接。
= pd.concat([df1,df2], axis=0)
df3 df3
从 表 tbl-concat-axis0 可以看到,两个DataFrame被简单地上下拼接起来。不同表的列被合并,缺失的位置用NaN
填充。注意,原始的行索引(0, 1, 2 和 0, 1, 2)被保留了。如果想生成一套新的连续索引,可以添加参数 ignore_index=True
。
4.4.2.2 按列拼接 (横向)
通过设置 axis=1
,concat()
也可以将DataFrame左右拼接起来。
= pd.concat([df1,df2],axis=1)
df3 df3
表 tbl-concat-axis1 的效果是根据行索引将两个表并排放在一起。
4.4.3 快速追加:append()
函数
append()
方法可以看作是 concat(axis=0)
的一个简化版,专门用于将一个DataFrame的行追加到另一个DataFrame的末尾。
= df1.append(df2)
df3 df3
4.4.3.1 关于 .append()
Pandas的 .append()
方法从版本1.4.0开始已被标记为不推荐使用 (deprecated),并计划在未来的版本中被彻底移除。
4.4.3.2 推荐替代方案
官方推荐使用 pd.concat()
来替代 .append()
。例如,df1.append(df2)
的等价写法是 pd.concat([df1, df2])
。pd.concat()
提供了更强大和一致的功能。
4.4.3.3 重要提醒
虽然目前的代码库中可能仍会看到 .append()
的使用,但在我们自己的新项目中,应当养成使用 pd.concat()
的好习惯。
append()
另一个常用的功能是追加单行记录,这通常通过追加一个字典来实现。此时,必须设置 ignore_index=True
来重新生成索引。
= df1.append({'公司': '腾讯', '分数': '90'}, ignore_index=True)
df3 df3
4.4.4 实践案例:合并贵州茅台两年利润表
案例背景
在前面的项目中,我们已经通过Tushare库获取了贵州茅台2019年和2020年的利润表数据,并分别存储在两个Excel文件中。现在,我们需要将这两年的数据合并成一个单一的数据集,以便进行跨年度的比较分析。
数据集
- 数据集1:
贵州茅台_利润表_2019年.xlsx
- 数据集2:
贵州茅台_利润表_2020年.xlsx
任务要求
- 分别读取两个Excel文件到两个DataFrame中。
- 使用
concat
函数按行方向(纵向)将两个DataFrame拼接起来,生成的新变量名为data_concat
,并设置忽略原有的索引。 - 使用
append
函数实现相同的拼接效果,生成的新变量名为data_append
,同样设置忽略索引。 - 打印
data_append
的内容进行验证。
实验代码框架
import pandas as pd
= pd.read_excel("贵州茅台_利润表_2019年.xlsx").drop(columns=["Unnamed: 0"])
data_2019 = pd.read_excel("贵州茅台_利润表_2020年.xlsx").drop(columns=["Unnamed: 0"])
data_2020 #使用concat函数按行方向进行拼接,返回变量名为data_concat,同时设置忽略索引。
#...
#使用append函数按行方向进行拼接,返回变量名为data_append,同时设置忽略索引。
#...
#print(data_append)
参考答案
import pandas as pd
= pd.read_excel("贵州茅台_利润表_2019年.xlsx").drop(columns=["Unnamed: 0"])
data_2019 = pd.read_excel("贵州茅台_利润表_2020年.xlsx").drop(columns=["Unnamed: 0"])
data_2020 = pd.concat([data_2019,data_2020],axis=0,ignore_index=True)
data_concat = data_2019.append(data_2020,ignore_index=True)
data_append print(data_append)
4.5 综合实战案例
经过前面几节的学习,我们已经掌握了Pandas数据处理的核心技能,包括创建DataFrame、文件读写、数据筛选以及数据表的拼接。现在,是时候将这些独立的技能串联起来,解决更接近真实世界的商业分析问题了。本节将通过两个递进的实战案例,带领大家综合运用所学知识,从数据获取、清洗、筛选到整合,完成一个完整的小型数据处理流程。
4.5.1 实战一:贵州茅台2023年年报关键指标提取
案例背景
作为A股市场的明星公司,贵州茅台(股票代码: 600519.SH)的年度财务报告备受投资者关注。财务数据的精细化处理是企业管理和投资决策的必要环节。我们需要从Tushare获取其最新的利润表数据,并提取出2023年年报中的关键财务指标,同时进行数据去重,以确保数据的唯一性和准确性。
任务要求
- 使用Tushare的
income
数据接口获取贵州茅台(ts_code='600519.SH'
)的所有利润表数据。 - 筛选: 从获取的全部数据中,筛选出
end_date
(报告期) 为 “20231231” 的数据,这代表2023年年度报告。 - 选取: 从完整报表中,只选取
'ts_code'
,'end_date'
,'total_revenue'
(营业总收入),'total_cogs'
(营业总成本),'n_income'
(净利润),'ebit'
(息税前利润) 这几个关键列。 - 去重: 对原始数据,根据
end_date
列去除重复的报告期数据,确保每个报告期只有一条记录。 - 分别打印以上三步操作的结果。
Tushare是一个免费、开源的Python财经数据接口包。要使用其实时和专业的pro
接口,您需要先访问其官网 tushare.pro
进行注册,获取个人专属的API Token,并替换掉代码中的示例Token。
实验代码框架
import tushare as ts
# 请将下方的Token替换为您自己的Tushare Pro API Token
= ts.pro_api("ba1646815a79a63470552889a69f957f5544bef01d3f082159bf8474")
pro = pro.income(ts_code='600519.SH')
df
# 任务1: 筛选2023年12月31日的数据
= None
df_1 # 任务2: 筛选指定列
= None
df_2 # 任务3: 根据季报日期去重
= None
df_3
# print(df_1)
# print(df_2)
# print(df_3)
参考答案
import tushare as ts
= ts.pro_api("ba1646815a79a63470552889a69f957f5544bef01d3f082159bf8474")
pro = pro.income(ts_code='600519.SH')
df = df[df["end_date"]=="20231231"] #筛选2023年12月31日的数据
df_1 = df[['ts_code','end_date','total_revenue','total_cogs','n_income','ebit']] #筛选营业总收入、营业总成本、净利润、息税前利润的数据,保留代码和日期列
df_2 = df.drop_duplicates("end_date") #根据季报日期去重
df_3 print("筛选2023年年报结果:")
print(df_1)
print("\n选取关键财务指标结果:")
print(df_2.head())
print("\n按报告期去重后结果:")
print(df_3.head())
4.5.2 实战二:批量获取白酒三巨头五年财务报表
案例背景
在金融行业,对标分析(Benchmarking)是评估一家公司表现的常用方法。贵州茅台、五粮液和泸州老窖是中国白酒行业的三大巨头,是许多投资者关注的对象。为了进行横向和纵向的比较分析,我们需要批量获取这三家公司过去五年(2019-2023年)的三大财务报表(利润表、资产负债表、现金流量表),并将其结构化地存储起来。
任务要求
- 批量获取: 编写一个可复用的函数,能够循环获取指定公司列表和年份列表的利润表、资产负债表和现金流量表数据。
- 数据拼接: 在函数内部,将每家公司每年的报表数据进行纵向拼接,形成包含五年数据的三张总表。
- 列名汉化: Tushare返回的列名为英文技术术语,不便于阅读。我们需要编写一个函数,读取一个预先准备好的Excel文件(
重命名.xlsx
,包含英文名和中文名的对应关系),将获取到的三张总表的列名转换为中文。 - 结构化存储: 将处理好的数据(每个公司三张汉化后的报表)分别存入三个独立的Excel文件中,每个文件以公司名命名(如“贵州茅台.xlsx”)。在每个Excel文件中,使用不同的Sheet来存储不同的报表(Sheet1: ‘利润表’, Sheet2: ‘资产负债表’, Sheet3: ‘现金流量表’)。
- 验证: 最后,读取生成的“贵州茅台.xlsx”文件中的“资产负债表”Sheet,并打印其内容,以验证整个流程的正确性。
4.5.2.1 知识点回顾
- 列索引重命名 (
.rename()
): 该函数可以接收一个字典,将旧的列名映射到新的列名。语法:df.rename(columns={'旧名1': '新名1', '旧名2': '新名2'})
。 - 数据表拼接 (
.append()
/pd.concat()
): 用于在循环中将每年获取的数据追加到总表中。 - 写入多个Sheet (
pd.ExcelWriter
): 这是向同一个Excel文件写入多个Sheet的标准方法。首先创建一个ExcelWriter
对象,然后多次调用to_excel()
方法,每次指定不同的sheet_name
,最后调用writer.save()
保存文件。
4.5.2.2 实验代码框架
第一部分: 列名汉化函数
import pandas as pd
import tushare as ts
# 请将下方的Token替换为您自己的Tushare Pro API Token
= ts.pro_api('ba1646815a79a63470552889a69f957f5544bef01d3f082159bf8474')
pro
def rename_col(data):
# 读取包含中英文列名对应关系的Excel文件
= pd.read_excel("重命名.xlsx")
rename_sheet # 提取英文列名和中文列名
= rename_sheet["名称"].tolist()
eng = rename_sheet["描述"].tolist()
chi # 创建重命名字典
= dict(zip(eng,chi))
re_dict # 执行重命名
= data.rename(columns=re_dict)
data return data
第二部分: 数据获取与存储主函数
# 定义函数
def access_data(comps,codes,years):
for i in range(len(comps)):
# 为每家公司初始化空的DataFrame
= None #定义一个空DataFrame
df_balance = pd.DataFrame()
df_income = pd.DataFrame()
df_cash
# 循环获取每年的数据
for year in years:
= pro.balancesheet(ts_code=codes[i],period=str(year)+'1231')[-1:]
df_balance0 = None #获取利润表数据
df_income0 = pro.cashflow(ts_code=codes[i],period=str(year)+'1231')[-1:]
df_cash0
# 拼接各年份报表
= None #拼接资产负债表
df_balance = df_income.append(df_income0)
df_income = df_cash.append(df_cash0)
df_cash
# 更改列名称
= rename_col(df_balance)
df_balance = None #更改利润表列名称
df_income = rename_col(df_cash)
df_cash
# 写入到多Sheet的Excel文件
= None #使用ExcelWriter函数返回一个实例化对象。数据表用公司名.xlsx
writer '资产负债表',index=False)
df_balance.to_excel(writer,'利润表',index=False)
df_income.to_excel(writer,'现金流量表',index=False)
df_cash.to_excel(writer, writer.save()
第三部分: 函数调用与验证
# 定义需要获取数据的公司、代码和年份
= ['贵州茅台','五粮液','泸州老窖']
comps = ['600519.SH','000858.SZ','000568.SZ']
codes = [2023,2022,2021,2020,2019]
years
# 调用主函数
# ...
# 验证:以贵州茅台资产负债表为例
# data = pd.read_excel("贵州茅台.xlsx",sheet_name="资产负债表")
# print(data)
4.5.2.3 参考答案
import pandas as pd
import tushare as ts
# 设置Tushare Pro API
= ts.pro_api('ba1646815a79a63470552889a69f957f5544bef01d3f082159bf8474')
pro
# 定义列名汉化函数
def rename_col(data):
= pd.read_excel("重命名.xlsx")
rename_sheet = rename_sheet["名称"].tolist()
eng = rename_sheet["描述"].tolist()
chi = dict(zip(eng,chi))
re_dict = data.rename(columns=re_dict)
data return data
# 定义数据获取与处理主函数
def access_data(comps,codes,years):
for i in range(len(comps)):
# 各新建一个空DataFrame
= pd.DataFrame()
df_balance = pd.DataFrame()
df_income = pd.DataFrame()
df_cash for year in years:
= pro.balancesheet(ts_code=codes[i],period=str(year)+'1231')[-1:]
df_balance0 = pro.income(ts_code=codes[i],period=str(year)+'1231')[-1:]
df_income0 = pro.cashflow(ts_code=codes[i],period=str(year)+'1231')[-1:]
df_cash0 # 拼接各年份报表
= df_balance.append(df_balance0)
df_balance = df_income.append(df_income0)
df_income = df_cash.append(df_cash0)
df_cash #更改列名称
= rename_col(df_balance)
df_balance = rename_col(df_income)
df_income = rename_col(df_cash)
df_cash = pd.ExcelWriter(comps[i]+'.xlsx') #使用ExcelWriter函数返回一个实例化对象。
writer '资产负债表',index=False)
df_balance.to_excel(writer,'利润表',index=False)
df_income.to_excel(writer,'现金流量表',index=False)
df_cash.to_excel(writer,
writer.save()
# 需要获取数据的公司名称、股票代码、年份
= ['贵州茅台','五粮液','泸州老窖']
comps = ['600519.SH','000858.SZ','000568.SZ']
codes = [2023,2022,2021,2020,2019]
years # 调用函数
access_data(comps,codes,years)
#以贵州茅台资产负债表为例
= pd.read_excel("贵州茅台.xlsx",sheet_name="资产负债表")
data print(data)