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开始的整数作为行索引。

例如,我们想创建一个包含学生姓名、年龄和身高信息的数据表,可以这样做:

列表 4.1
import pandas as pd

dict_data = {'name': ['Alice', 'Bob', 'Charlie', 'David'],
       'age': [25, 30, 45, 21],
       'height': [165.4, 170.2, 178.6, 160.0]
      }
df = pd.DataFrame(dict_data)

创建完成后,我们可以直接在代码单元格的最后一行输入变量名 df 来查看其内容,这在Jupyter环境中会自动以美观的表格形式展示出来。

4.1.1.2 从Numpy数组创建

Numpy是Python中用于科学计算的核心库,它提供了一个强大的N维数组对象。如果你的数据已经存储在一个Numpy数组中,也可以非常方便地将其转换为DataFrame。转换时,我们可以手动指定列名和行索引。

列表 4.2
import pandas as pd
import numpy as np

ndarray_data = np.array([[1,2,3], [4,5,6], [7,8,9]])
df = pd.DataFrame(ndarray_data, columns=['A', 'B', 'C'])

列表 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 的文件
    df = pd.read_csv('data.csv')
  • 读取Excel文件: 这是商科学生最常用的功能。

    列表 4.4
    import pandas as pd
    
    # 假设当前目录下有一个名为 data.xlsx 的文件
    df = pd.read_excel('data.xlsx')
  • 读取SQL数据库: 对于存储在数据库中的数据,Pandas也可以通过连接直接查询并加载。

    列表 4.5
    import pandas as pd
    import sqlite3
    
    # 连接到一个SQLite数据库文件
    con = sqlite3.connect('my_database.db')
    # 从名为 my_table 的表中查询所有数据
    df = pd.read_sql('SELECT * FROM my_table', con)

我们将在 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提供了丰富的工具来进行数据清洗。

列表 4.12
# 替换列名
# 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 数据聚合与分组

数据聚合是数据分析的核心环节之一,它涉及到对数据进行分组,然后对每个组应用一个函数(如求和、求平均值)以提取洞见。

列表 4.13
# 按'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操作。

列表 4.14
# 使用concat进行纵向合并(堆叠)
df1 = pd.DataFrame({'A': [1,2,3], 'B': [4,5,6]})
df2 = pd.DataFrame({'A': [4,5,6], 'B': [7,8,9]})
# pd.concat([df1, df2], axis=0)

# 使用merge根据共同的键进行横向连接
df1 = pd.DataFrame({'key': ['A', 'B'], 'value': [1, 2]})
df2 = pd.DataFrame({'key': ['B', 'C'], 'value': [3, 4]})
# 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 正确写法

正确的基于列的连接应使用 mergepd.merge(df1, df2, on='key', lsuffix='_df1', rsuffix='_df2') (当两个DataFrame中有除key之外的同名列时,需要使用lsuffixrsuffix来区分)

4.1.2.8 重要提醒

为通过平台检测,在线练习时仍需按原始错误代码输入(如果平台提供了该代码)。 但在实际工作中,请务必使用 pd.merge() 进行基于列的合并。

我们将在 sec-data-merging 章节详细讲解数据合并的各种策略。

4.1.3 实践案例:创建企业员工信息数据集

现在,让我们将理论付诸实践。假设我们需要为一家企业创建一个简单的员工信息数据集,用于后续的人力资源分析。

案例背景

在某企业里,有多个部门,我们需要对这些部门进行统计,例如每个部门的人员数量、平均薪资等信息,以便企业管理者更好地了解每个部门的情况。这里我们自己创建一个示例数据集。

任务要求

  1. 创建一个DataFrame,包含三列:部门名称员工数量平均薪资
  2. 部门名称 数据为: ‘研发部’, ‘财务部’, ‘市场部’, ‘销售部’, ‘人力资源部’。
  3. 员工数量 数据为: 30, 20, 25, 35, 15。
  4. 平均薪资 数据为: 8000, 10000, 9000, 12000, 8500。
  5. 创建后,输出DataFrame的前5行数据。
  6. 输出该DataFrame的基本信息。
  7. 筛选出所有 员工数量 大于等于30的部门。
  8. 计算所有部门 平均薪资 的中位数。

实验代码框架

列表 4.15
import pandas as pd
import numpy as np

# 1. 准备数据
department = ['研发部', '财务部', '市场部', '销售部', '人力资源部']
employee_num = [30, 20, 25, 35, 15]
salary_mean = [8000, 10000, 9000, 12000, 8500]

# 2. 创建数据框 (请在此处补充代码)
df = None
print("创建的DataFrame为:")
# print(df)
print("------------------------------------------")

# 3. 查看前5行数据 (请在此处补充代码)
print("前5行数据:")
# ...
print("------------------------------------------")

# 4. 查看数据的基本信息 (请在此处补充代码)
print("数据的基本信息:")
# ...
print("------------------------------------------")

# 5. 筛选出员工数量大于等于30的部门 (请在此处补充代码)
print("员工数量大于等于30的部门:")
# ...
print("------------------------------------------")

# 6. 计算员工平均薪资的中位数 (请在此处补充代码)
print("平均薪资的中位数:")
# ...

参考答案

列表 4.16
import pandas as pd
import numpy as np

# 创建示例数据集
department = ['研发部', '财务部', '市场部', '销售部', '人力资源部']
employee_num = [30, 20, 25, 35, 15]
salary_mean = [8000, 10000, 9000, 12000, 8500]

df = pd.DataFrame({'部门名称': department, '员工数量': employee_num, '平均薪资': salary_mean})

# 查看前几行数据
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文件。

列表 4.17
import pandas as pd
# 从一个公开的URL读取Excel数据
data = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20230430/xlsx/1652582851436634112.xlsx") #数据为DataFrame结构
data.head()

read_excel 函数有很多实用的参数,帮助我们更精确地控制读取过程。

  • sheet_name: 一个Excel文件可能包含多个工作表(sheet)。通过这个参数,可以指定读取哪个工作表。可以传入工作表的名称(字符串)或位置(整数,0代表第一个)。
  • index_col: 指定某一列作为DataFrame的行索引。

例如,读取本地文件 data.xlsx 的第一个工作表:

列表 4.18
# 假设本地有 data.xlsx 文件
# data = pd.read_excel('data.xlsx', sheet_name=0)

4.2.1.2 读取CSV文件

CSV (逗号分隔值) 文件是另一种非常普遍的数据存储格式。由于其纯文本的特性,它具有极好的跨平台兼容性。pd.read_csv() 是用来读取CSV文件的函数。

列表 4.19
# 假设本地有 data.csv 文件
# data = pd.read_csv('data.csv')

read_excel 类似,read_csv 也有很多控制参数:

  • delimiter (或 sep): 指定文件中的分隔符。虽然CSV通常用逗号,但有时也可能用分号、制表符等。
  • encoding: 指定文件的编码格式。处理包含中文的CSV文件时,常常需要将编码设置为 'utf-8''gbk' 以避免乱码。
列表 4.20
# 假设 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作为示例:

列表 4.21
import pandas as pd

# 先创建一个DataFrame
data = pd.DataFrame([[1, 2], [3, 4], [5, 6]], columns=['A列','B列'])

现在,将这个DataFrame写入名为 演示.xlsx 的文件中:

列表 4.22
# 将DataFrame中的数据导入Excel工作簿
# data.to_excel('演示.xlsx')

执行后,你会在当前目录下发现一个新生成的 演示.xlsx 文件。打开它,你会看到数据已经被写入,并且默认情况下,行索引也被作为第一列保存了下来。

to_excel() 方法也有一系列参数可以控制输出的格式: * index: 一个布尔值,决定是否将DataFrame的索引写入文件。默认为 True。如果你不希望在Excel中看到 0, 1, 2... 这样的索引列,可以设置为 index=False。 * columns: 一个列表,用于指定要写入文件的列。如果你只想保存部分列,可以使用这个参数。 * sheet_name: 字符串,指定写入的工作表的名称。 * encoding: 指定编码方式。

例如,我们只将 A列 的数据写入Excel,并且不包含行索引:

列表 4.23
# data.to_excel('演示.xlsx', columns=['A列'], index=False)

4.2.2.2 写入CSV文件

与写入Excel类似,我们可以使用 to_csv() 方法将DataFrame保存为CSV文件。

列表 4.24
# data.to_csv('演示.csv')

to_csv() 的参数与 to_excel() 类似。一个特别需要注意的参数是 encoding。当数据中包含中文字符时,为了确保在其他软件(如Excel)中打开CSV文件不出现乱码,通常建议使用 'utf_8_sig' 编码。这种编码方式会在文件开头加入一个特殊的标记(BOM),帮助Excel正确识别UTF-8编码。

列表 4.25
# data.to_csv('演示.csv', index=False, encoding="utf_8_sig")

4.2.3 实践案例:处理东方财富利润表数据

案例背景

假设在之前的项目中,我们已经通过Tushare库获取了东方财富公司的所有历史利润表数据,并将其存储在了名为 eastmoney.csv 的文件中。现在的任务是读取这个CSV文件,并将其转换为Excel格式,以便于不熟悉编程的同事进行查阅。

任务要求

  1. 使用Pandas库读取 eastmoney.csv 文件。
  2. 将读取到的数据完整地写入到一个名为 eastmoney.xlsx 的Excel文件中。

实验代码框架

列表 4.26
import pandas as pd

# 读取eastmoney.csv文件
# ...
# 写入eastmoney.xlsx文件
# ...
# print(data)

参考答案

表 4.1: 东方财富利润表数据预览
import pandas as pd
data = pd.read_csv("eastmoney.csv")
data.to_excel("eastmoney.xlsx")
print(data.head())

4.3 财务数据的读取与筛选

在掌握了如何创建DataFrame以及如何从文件中读取数据之后,我们接下来要学习一项至关重要的数据分析技能:数据筛选。财务领域的数据集通常非常庞大,比如一家公司多年的财务报表、一支股票每日的交易数据等。我们往往只对其中的一部分数据感兴趣,例如特定时间段的记录、某个特定科目的数值,或者满足某些财务指标的样本。

“筛选”就是从庞大的数据集中精确地提取出我们所需要的信息子集的过程。Pandas提供了强大而灵活的工具,让我们能够像侦探一样,通过各种线索(条件)来定位和提取目标数据。

4.3.1 筛选的基础:选取行与列

为了方便演示,我们首先创建一个3x3的DataFrame实例,并为其指定清晰的行和列标签。

表 4.2: 用于数据筛选演示的样本DataFrame
import pandas as pd

data = pd.DataFrame([[1, 3, 3], [4, 5, 5], [7, 9, 9]], index=['r1', 'r2', 'r3'], columns=['c1', 'c2', 'c3'])
data

我们的所有筛选操作都将基于 表 tbl-filtering-sample-data 这个数据表进行。

4.3.1.1 选取列

选取DataFrame中的一列或多列是最基本的操作。

  • 选取单列:可以通过 df['列名'] 的方式实现。返回的结果是一个Pandas Series对象,它是一维的带标签数组。

    列表 4.27
    a = data['c1']
    a

    注意 列表 lst-select-single-col 的输出结果是一个Series,它保留了原始的行索引,但没有了二维表格的结构。

  • 以DataFrame形式选取单列:如果你希望选取单列后仍然保持DataFrame的二维格式,可以在列名外再加一层方括号 df[['列名']]

    表 4.3: 以DataFrame格式选取单列 c1
    b = data[['c1']]
    b
  • 选取多列:将一个包含多个列名的列表传递给方括号即可。

    表 4.4: 选取 c1 和 c3 两列
    c = data[['c1', 'c3']]
    c

4.3.1.2 选取行

选取行的方式比选取列稍微复杂一些,Pandas为此提供了专门的索引器(indexer)。

  • 按位置切片(不推荐的旧方法): data[1:3] 这种类似Python列表的切片方式可以选取连续的多行。它遵循“左闭右开”原则,即包含起始位置,不包含结束位置。

    表 4.5: 使用切片选取第2到3行
    # 选取第2到3行的数据,注意序号从0开始,左闭右开
    a = data[1:3] 
    a
    注记

    虽然这种语法很便捷,但Pandas官方更推荐使用 .iloc.loc,因为它们的意图更明确,可以避免混淆。

  • 按整数位置选取 (.iloc): 这是官方推荐的、基于整数位置选取行的方法。

    表 4.6: 使用 .iloc 选取第2到3行
    b = data.iloc[1:3]
    b

    选取单行时,必须使用 .iloc。例如,选取最后一行:

    列表 4.28
    c = data.iloc[-1]
    c

    如果直接使用 data[-1],程序可能会误认为 -1 是一个列名,从而导致错误。

  • 按标签名称选取 (.loc): 如果你想根据行索引的具体名称来选取,应该使用 .loc

    表 4.7: 使用 .loc 根据行标签选取
    d = data.loc[['r2', 'r3']]
    d

4.3.2 选取区块与单个值

当我们需要同时根据行和列的条件来选取数据时,就进入了区块筛选的范畴。

4.3.2.1 组合筛选

  • 链式筛选: 一个直观的方法是先筛选列,再筛选行(或反之)。

    表 4.8: 链式操作选取 c1, c3 列的前两行
    a = data[['c1', 'c3']][0:2] # 也可写成data[0:2][['c1', 'c3']]
    a
  • 推荐方式 (.iloc + 链式): 为了代码的清晰和稳健,Pandas官方更推荐联合使用 .iloc (用于行) 和 [] (用于列)。

4.3.2.2 使用 .loc.iloc 直接选取区块

.loc.iloc 都支持传入两个参数,第一个用于选择行,第二个用于选择列,用逗号隔开:df.loc[行条件, 列条件]df.iloc[行条件, 列条件]。这是最强大和规范的选取方式。

表 4.10: 使用 .loc 和 .iloc 直接选取区块
# 使用 .loc 按标签名选取
d = data.loc[['r1', 'r2'], ['c1', 'c3']]  

# 使用 .iloc 按整数位置选取
e = data.iloc[0:2, [0, 2]]

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 定位到行,再用 [] 定位到列。

列表 4.29
c = data.iloc[0]['c3']
c

4.3.3 按特定条件筛选

这是数据筛选中最强大的功能:根据数据本身的值来提取子集。这通常被称为布尔索引 (Boolean Indexing)

其原理是,我们首先创建一个布尔条件(例如 data['c1'] > 1),Pandas会返回一个由TrueFalse组成的Series。然后,我们将这个布尔Series传给DataFrame,DataFrame就会只返回那些对应值为True的行。

4.3.3.1 单一条件筛选

例如,我们要筛选出 c1 列中数值大于1的所有行:

表 4.11: 筛选 c1 列中数值大于1的行
a = data[data['c1'] > 1]
a

4.3.3.2 复合条件筛选

当有多个筛选条件时,可以使用逻辑运算符 & (与) 和 | (或) 来连接它们。非常重要的一点是:每个独立的条件都必须用小括号 () 括起来。

例如,筛选 c1 列中数字大于1 c2 列中数字小于8的行:

表 4.12: 复合条件筛选
b = data[(data['c1'] > 1) & (data['c2'] < 8)]
b

正如 表 tbl-filter-multi-condition 结果所示,只有 r2 行同时满足这两个条件,因此被筛选出来。

4.3.4 实践案例:读取与筛选东方财富利润表

案例背景

sec-case-eastmoney-io 中,我们已经将东方财富的利润表数据存入了 eastmoney.xlsx 文件。现在,我们需要对这份庞大的财务数据进行初步的探索性分析。

任务要求

  1. 读取 eastmoney.xlsx 文件。
  2. 单独提取并显示 “ann_date” (公告日期) 这一列的数据。
  3. 提取并显示第4行的数据(注意,在编程中索引从0开始)。
  4. 使用 .iloc 提取第2行到第3行的数据。
  5. 筛选出所有 “ann_date” 等于 20230318 的记录。

实验代码框架

列表 4.30
import pandas as pd

data = pd.read_excel("eastmoney.xlsx")

# 1. 读取“ann_date”列数据
# ...
print("----------------------------------------------------")

# 2. 读取第4行数据
# ...
print("----------------------------------------------------")

# 3. 使用iloc读取第2行到第3行数据
# ...
print("----------------------------------------------------")

# 4. 筛选“ann_date”列等于20230318的数据
# ...

参考答案

列表 4.31
import pandas as pd

data = pd.read_excel("eastmoney.xlsx")

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 包含它们的股价。

列表 4.32
import pandas as pd
df1 = pd.DataFrame({'公司': ['科大', '阿里', '百度'], '分数': [90, 95, 85]})
df2 = pd.DataFrame({'公司': ['科大', '阿里', '京东'], '股价': [20, 180, 30]})

print("df1 (公司评分):")
print(df1)
print("\ndf2 (公司股价):")
print(df2)

4.4.1.1 内连接 (Inner Join)

默认情况下,merge() 执行的是内连接。这意味着它会找到两个表中共同键'公司'所共有的行(即’科大’和’阿里’),并将它们合并。

表 4.13: 默认的内连接 (Inner Join)
df3 = pd.merge(df1, df2)
df3

表 tbl-merge-inner 所示,‘百度’(只在df1)和’京东’(只在df2)都被丢弃了。如果两个表中有多个同名列,Pandas会自动将它们都作为合并的键。如果只想根据特定列合并,可以使用 on 参数:pd.merge(df1, df2, on='公司')

4.4.1.2 外连接 (Outer Join)

如果我们想保留所有公司的数据,即使它只出现在一个表中,我们可以使用外连接。通过设置 how='outer' 实现。

表 4.14: 外连接 (Outer Join)
df3 = pd.merge(df1, df2, how='outer')
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)
    df3 = pd.merge(df1, df2, how='left')
    df3

    表 tbl-merge-left 的结果保留了df1中所有的公司(科大、阿里、百度),而df2中独有的’京东’被舍弃。

  • 右连接 (how='right'): 与左连接相反,它保留右边表(df2)的所有行。

4.4.1.4 基于索引的合并

merge() 也可以基于DataFrame的行索引进行合并,只需将 left_indexright_index 参数设置为 True

表 4.16: 基于索引的合并
df3 = pd.merge(df1, df2, left_index=True, right_index=True)
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(这也是默认值)来实现。

表 4.17: 按行方向进行纵向拼接
# 默认情况下,axis=0,按行方向进行连接。
df3 = pd.concat([df1,df2], axis=0)
df3

表 tbl-concat-axis0 可以看到,两个DataFrame被简单地上下拼接起来。不同表的列被合并,缺失的位置用NaN填充。注意,原始的行索引(0, 1, 2 和 0, 1, 2)被保留了。如果想生成一套新的连续索引,可以添加参数 ignore_index=True

4.4.2.2 按列拼接 (横向)

通过设置 axis=1concat() 也可以将DataFrame左右拼接起来。

表 4.18: 按列方向进行横向拼接
df3 = pd.concat([df1,df2],axis=1)
df3

表 tbl-concat-axis1 的效果是根据行索引将两个表并排放在一起。

4.4.3 快速追加:append() 函数

append() 方法可以看作是 concat(axis=0) 的一个简化版,专门用于将一个DataFrame的行追加到另一个DataFrame的末尾。

表 4.19: 使用 append() 追加一个DataFrame
df3 = df1.append(df2)
df3
函数即将弃用 (Deprecation Warning)

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 来重新生成索引。

表 4.20: 使用 append() 追加一条新记录
df3 = df1.append({'公司': '腾讯', '分数': '90'}, ignore_index=True)
df3

4.4.4 实践案例:合并贵州茅台两年利润表

案例背景

在前面的项目中,我们已经通过Tushare库获取了贵州茅台2019年和2020年的利润表数据,并分别存储在两个Excel文件中。现在,我们需要将这两年的数据合并成一个单一的数据集,以便进行跨年度的比较分析。

数据集

  • 数据集1: 贵州茅台_利润表_2019年.xlsx
  • 数据集2: 贵州茅台_利润表_2020年.xlsx

任务要求

  1. 分别读取两个Excel文件到两个DataFrame中。
  2. 使用 concat 函数按行方向(纵向)将两个DataFrame拼接起来,生成的新变量名为 data_concat,并设置忽略原有的索引。
  3. 使用 append 函数实现相同的拼接效果,生成的新变量名为 data_append,同样设置忽略索引。
  4. 打印 data_append 的内容进行验证。

实验代码框架

列表 4.33
import pandas as pd
data_2019 = pd.read_excel("贵州茅台_利润表_2019年.xlsx").drop(columns=["Unnamed: 0"])
data_2020 = pd.read_excel("贵州茅台_利润表_2020年.xlsx").drop(columns=["Unnamed: 0"])
#使用concat函数按行方向进行拼接,返回变量名为data_concat,同时设置忽略索引。
#...
#使用append函数按行方向进行拼接,返回变量名为data_append,同时设置忽略索引。
#...
#print(data_append)

参考答案

表 4.21: 合并后的贵州茅台2019-2020年利润表数据
import pandas as pd
data_2019 = pd.read_excel("贵州茅台_利润表_2019年.xlsx").drop(columns=["Unnamed: 0"])
data_2020 = pd.read_excel("贵州茅台_利润表_2020年.xlsx").drop(columns=["Unnamed: 0"])
data_concat = pd.concat([data_2019,data_2020],axis=0,ignore_index=True)
data_append = data_2019.append(data_2020,ignore_index=True)
print(data_append)

4.5 综合实战案例

经过前面几节的学习,我们已经掌握了Pandas数据处理的核心技能,包括创建DataFrame、文件读写、数据筛选以及数据表的拼接。现在,是时候将这些独立的技能串联起来,解决更接近真实世界的商业分析问题了。本节将通过两个递进的实战案例,带领大家综合运用所学知识,从数据获取、清洗、筛选到整合,完成一个完整的小型数据处理流程。

4.5.1 实战一:贵州茅台2023年年报关键指标提取

案例背景

作为A股市场的明星公司,贵州茅台(股票代码: 600519.SH)的年度财务报告备受投资者关注。财务数据的精细化处理是企业管理和投资决策的必要环节。我们需要从Tushare获取其最新的利润表数据,并提取出2023年年报中的关键财务指标,同时进行数据去重,以确保数据的唯一性和准确性。

任务要求

  1. 使用Tushare的 income 数据接口获取贵州茅台(ts_code='600519.SH')的所有利润表数据。
  2. 筛选: 从获取的全部数据中,筛选出 end_date (报告期) 为 “20231231” 的数据,这代表2023年年度报告。
  3. 选取: 从完整报表中,只选取 'ts_code', 'end_date', 'total_revenue' (营业总收入), 'total_cogs' (营业总成本), 'n_income' (净利润), 'ebit' (息税前利润) 这几个关键列。
  4. 去重: 对原始数据,根据 end_date 列去除重复的报告期数据,确保每个报告期只有一条记录。
  5. 分别打印以上三步操作的结果。
关于Tushare Pro接口

Tushare是一个免费、开源的Python财经数据接口包。要使用其实时和专业的pro接口,您需要先访问其官网 tushare.pro 进行注册,获取个人专属的API Token,并替换掉代码中的示例Token。

实验代码框架

列表 4.34
import tushare as ts
# 请将下方的Token替换为您自己的Tushare Pro API Token
pro = ts.pro_api("ba1646815a79a63470552889a69f957f5544bef01d3f082159bf8474")
df = pro.income(ts_code='600519.SH')

# 任务1: 筛选2023年12月31日的数据
df_1 = None
# 任务2: 筛选指定列
df_2 = None
# 任务3: 根据季报日期去重
df_3 = None

# print(df_1)
# print(df_2)       
# print(df_3)

参考答案

列表 4.35
import tushare as ts
pro = ts.pro_api("ba1646815a79a63470552889a69f957f5544bef01d3f082159bf8474")
df = pro.income(ts_code='600519.SH')
df_1 = df[df["end_date"]=="20231231"]      #筛选2023年12月31日的数据
df_2 = df[['ts_code','end_date','total_revenue','total_cogs','n_income','ebit']] #筛选营业总收入、营业总成本、净利润、息税前利润的数据,保留代码和日期列
df_3 = df.drop_duplicates("end_date")      #根据季报日期去重   
print("筛选2023年年报结果:")
print(df_1)
print("\n选取关键财务指标结果:")
print(df_2.head())       
print("\n按报告期去重后结果:")
print(df_3.head())

4.5.2 实战二:批量获取白酒三巨头五年财务报表

案例背景

在金融行业,对标分析(Benchmarking)是评估一家公司表现的常用方法。贵州茅台、五粮液和泸州老窖是中国白酒行业的三大巨头,是许多投资者关注的对象。为了进行横向和纵向的比较分析,我们需要批量获取这三家公司过去五年(2019-2023年)的三大财务报表(利润表、资产负债表、现金流量表),并将其结构化地存储起来。

任务要求

  1. 批量获取: 编写一个可复用的函数,能够循环获取指定公司列表和年份列表的利润表、资产负债表和现金流量表数据。
  2. 数据拼接: 在函数内部,将每家公司每年的报表数据进行纵向拼接,形成包含五年数据的三张总表。
  3. 列名汉化: Tushare返回的列名为英文技术术语,不便于阅读。我们需要编写一个函数,读取一个预先准备好的Excel文件(重命名.xlsx,包含英文名和中文名的对应关系),将获取到的三张总表的列名转换为中文。
  4. 结构化存储: 将处理好的数据(每个公司三张汉化后的报表)分别存入三个独立的Excel文件中,每个文件以公司名命名(如“贵州茅台.xlsx”)。在每个Excel文件中,使用不同的Sheet来存储不同的报表(Sheet1: ‘利润表’, Sheet2: ‘资产负债表’, Sheet3: ‘现金流量表’)。
  5. 验证: 最后,读取生成的“贵州茅台.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 实验代码框架

第一部分: 列名汉化函数

列表 4.36
import pandas as pd
import tushare as ts
# 请将下方的Token替换为您自己的Tushare Pro API Token
pro = ts.pro_api('ba1646815a79a63470552889a69f957f5544bef01d3f082159bf8474')

def rename_col(data):
    # 读取包含中英文列名对应关系的Excel文件
    rename_sheet = pd.read_excel("重命名.xlsx")
    # 提取英文列名和中文列名
    eng = rename_sheet["名称"].tolist()
    chi = rename_sheet["描述"].tolist()
    # 创建重命名字典
    re_dict = dict(zip(eng,chi))
    # 执行重命名
    data = data.rename(columns=re_dict)
    return data

第二部分: 数据获取与存储主函数

列表 4.37
# 定义函数
def access_data(comps,codes,years):
    for i in range(len(comps)):
        # 为每家公司初始化空的DataFrame
        df_balance = None #定义一个空DataFrame
        df_income = pd.DataFrame()
        df_cash = pd.DataFrame()
        
        # 循环获取每年的数据
        for year in years:
            df_balance0 = pro.balancesheet(ts_code=codes[i],period=str(year)+'1231')[-1:]
            df_income0 = None #获取利润表数据
            df_cash0 = pro.cashflow(ts_code=codes[i],period=str(year)+'1231')[-1:]
            
            # 拼接各年份报表
            df_balance = None #拼接资产负债表
            df_income = df_income.append(df_income0)
            df_cash = df_cash.append(df_cash0)
            
        # 更改列名称
        df_balance = rename_col(df_balance)
        df_income = None #更改利润表列名称
        df_cash = rename_col(df_cash)
        
        # 写入到多Sheet的Excel文件
        writer = None #使用ExcelWriter函数返回一个实例化对象。数据表用公司名.xlsx
        df_balance.to_excel(writer,'资产负债表',index=False)
        df_income.to_excel(writer,'利润表',index=False)
        df_cash.to_excel(writer,'现金流量表',index=False)
        writer.save()

第三部分: 函数调用与验证

列表 4.38
# 定义需要获取数据的公司、代码和年份
comps = ['贵州茅台','五粮液','泸州老窖']
codes = ['600519.SH','000858.SZ','000568.SZ']
years = [2023,2022,2021,2020,2019]

# 调用主函数
# ...
                       
# 验证:以贵州茅台资产负债表为例                 
# data = pd.read_excel("贵州茅台.xlsx",sheet_name="资产负债表")
# print(data)

4.5.2.3 参考答案

列表 4.39
import pandas as pd
import tushare as ts

# 设置Tushare Pro API
pro = ts.pro_api('ba1646815a79a63470552889a69f957f5544bef01d3f082159bf8474')

# 定义列名汉化函数
def rename_col(data):
    rename_sheet = pd.read_excel("重命名.xlsx")
    eng = rename_sheet["名称"].tolist()
    chi = rename_sheet["描述"].tolist()
    re_dict = dict(zip(eng,chi))
    data = data.rename(columns=re_dict)
    return data
    
# 定义数据获取与处理主函数
def access_data(comps,codes,years):
    for i in range(len(comps)):
        # 各新建一个空DataFrame
        df_balance = pd.DataFrame()        
        df_income = pd.DataFrame()
        df_cash = pd.DataFrame()
        for year in years:
            df_balance0 = pro.balancesheet(ts_code=codes[i],period=str(year)+'1231')[-1:]
            df_income0 = pro.income(ts_code=codes[i],period=str(year)+'1231')[-1:]
            df_cash0 = pro.cashflow(ts_code=codes[i],period=str(year)+'1231')[-1:]
            # 拼接各年份报表
            df_balance = df_balance.append(df_balance0)                      
            df_income = df_income.append(df_income0)
            df_cash = df_cash.append(df_cash0)
        #更改列名称
        df_balance = rename_col(df_balance)
        df_income = rename_col(df_income)
        df_cash = rename_col(df_cash)
        writer = pd.ExcelWriter(comps[i]+'.xlsx')  #使用ExcelWriter函数返回一个实例化对象。
        df_balance.to_excel(writer,'资产负债表',index=False)
        df_income.to_excel(writer,'利润表',index=False)
        df_cash.to_excel(writer,'现金流量表',index=False)
        writer.save()
               
# 需要获取数据的公司名称、股票代码、年份
comps = ['贵州茅台','五粮液','泸州老窖']
codes = ['600519.SH','000858.SZ','000568.SZ']
years = [2023,2022,2021,2020,2019]
# 调用函数
access_data(comps,codes,years)

#以贵州茅台资产负债表为例                 
data = pd.read_excel("贵州茅台.xlsx",sheet_name="资产负债表")
print(data)