10 机器学习方法应用财务数据

欢迎:今天的学习之旅

各位同学,欢迎来到《商业大数据分析与应用》的全新篇章。

今天,我们将踏上一段激动人心的旅程,从自动化财务分析构建智能预测模型

本章学习目标:三大核心能力

  1. 规模化分析能力:掌握如何从分析单一公司升级到自动化、批量化地评价整个行业。

  2. 构建预测模型:学习并实践经典的线性回归模型,利用历史数据预测未来的财务表现。

  3. 解决分类问题:探索决策树与随机森林模型,解决如“财务舞弊识别”等复杂的商业决策问题。

第一部分:批量筛选优质上市公司

我们将从一个高度实用的案例开始:如何自动化地对一个行业的所有公司进行综合评价与排名。

核心问题:如何从众多公司中发现“璞玉”?

分析一家公司已是挑战,当面对一个包含数十家公司的行业时,我们如何高效、客观地进行筛选?

答案:构建一个自动化的、量化的评价框架。

核心评价框架:两大分析维度

我们的评价体系建立在两个坚实的分析维度之上:

  • 趋势分析 (纵向比较): 公司与自身的“过去”对话。
  • 同业分析 (横向比较): 公司在“行业赛道”上的定位。

维度一:趋势分析

趋势分析关注一家公司自身的发展轨迹。

  • 核心问题:这家公司的经营状况是在持续改善,还是在恶化?
  • 分析方法:考察关键财务指标在过去几年的变化趋势。
  • 核心洞察:一个持续向好的趋势,是公司内生增长动力强劲的体现。

维度二:同业分析

同业分析将公司置于行业竞争的背景下进行考察。

  • 核心问题:这家公司的表现在行业中处于什么水平?
  • 分析方法:将公司的财务指标与行业均值或分位数进行比较。
  • 核心洞察:即便趋势向好,若增速低于行业平均,可能也并非最佳选择。相对位置至关重要。

评价指标体系:五个维度,全方位考察

我们的评价体系将覆盖衡量公司经营状况的五个核心维度,确保评估的全面性。

指标维度1:盈利能力

定义:企业获取利润的本领。这是评价一家公司的核心。

  • 毛利率
  • 营业利润率
  • 净利润率
  • 净资产收益率(ROE)

指标维度2:营运能力

定义:企业利用其资产创造收入的效率。

  • 存货周转率
  • 总资产周转率
  • 应收账款周转率

指标维度3:偿债能力

定义:企业偿还债务、抵御财务风险的能力。

  • 流动比率
  • 速动比率
  • 利息保障倍数

指标维度4:成长能力

定义:企业在未来扩大规模、提升利润的潜力。

  • 营业收入增长率
  • 营业利润增长率
  • 净利润增长率

指标维度5:现金流健康度

定义:企业经营活动的“血液循环”是否通畅。

  • 经营活动产生的现金流量净额 / 净利润
  • 销售商品/提供劳务收到的现金 / 营业收入
  • 期末现金及现金等价物余额 / 有息负债

综合评分模型:量化决策

为了得到最终排名,我们对两个分析维度进行加权平均。

综合得分 = (趋势分析得分 × 40%) + (同业分析得分 × 60%)

  • 权重设定:我们认为公司在行业中的相对位置(60%)比其自身的发展历史趋势(40%)更为重要。
  • 主观性说明:权重是可调整的,反映了分析师的投资策略与行业判断。

Python实战:批量评价白酒行业

接下来,我们将用Python代码,将上述理论框架付诸实践。

目标:自动化输出A股白酒行业上市公司的综合排名。

步骤1:数据准备与环境设置

首先,我们导入所需库,并使用Tushare API获取白酒行业所有上市公司的名单。

# 本代码块仅为说明,实际执行在后续隐藏代码块中
import pandas as pd
import numpy as np
import tushare as ts
import time
import warnings;warnings.simplefilter("ignore")

pro = ts.pro_api('ba1646815a79a63470552889a69f957f5544bef01d3f082159bf8474')

步骤1(续):获取白酒行业公司列表

下面的代码片段展示了如何筛选出所有行业为’白酒’的公司。

## 获取同行业股票代码
com_data = pro.stock_basic(exchange='', list_status='L', 
fields='ts_code,symbol,name,area,industry,list_date')
## 要求1: 筛选出白酒行业的公司
bj_com = com_data[com_data['industry'] == '白酒']

## 将筛选出的公司代码和名称转换为列表,方便后续循环调用
bj_code = bj_com['ts_code'].tolist()
bj_name = bj_com['name'].tolist()

## 手动修正部分公司名称以匹配本地文件名
bj_name = "皇台酒业" # 这是一个典型的数据清洗步骤

真实世界的数据清洗

请注意代码中的’手动修正’。在实际项目中,API返回的官方名称(如 *ST皇台)可能与我们本地文件名(皇台酒业)不完全匹配,这会导致程序错误。编写鲁棒的名称匹配逻辑是数据预处理的关键一环。

步骤2:计算趋势得分

核心逻辑:对于每家公司,遍历其所有财务指标。如果一个指标逐年改善(在此案例中均为数值增大),则加分。

最终得分 = (改善的年数 / 总比较年数) × 100

步骤2(续):趋势得分代码解析

我们通过嵌套循环实现评分:外层循环遍历公司,内层循环遍历指标和年份。

# for comp in bj_name: # 遍历每个公司
  # ... 读取数据 ...
  # scores = []
  # for i in range(len(data.T)): # 遍历每个财务指标
    # n = 0
    # for j in range(len(data)-1): # 遍历每一年
      # # 如果指标值逐年改善 (这里是增长),则加分
      # if data.iloc[j,i] > data.iloc[j+1,i]:
        # n = n+1
    # # 分数标准化为100分制
    # n = n/(len(data)-1) * 100
    # scores.append(n)
  # ... 存储分数 ...

步骤3:计算同业得分

核心逻辑:衡量一家公司的指标在整个行业中所处的位置。我们使用分位数进行评分。

同业得分的分位数评分标准 一个水平条形图,显示了四个分位数区间对应的分数:0-25%为25分,25-50%为50分,50-75%为75分,75-100%为100分。 0% (min) 25% 50% 75% 100% (max) 25分 (较差) 50分 (中等) 75分 (良好) 100分 (优秀) 同业得分:基于行业分位数的评分标准

步骤3(续):同业得分代码解析

首先计算整个行业所有指标的描述性统计(包括分位数),然后将每家公司与这些“行业标准”进行比较。

# ... 计算所有公司各指标的均值,存入 ratio_ind ...

## standard 表包含了整个行业的 25%, 50%, 75% 分位数
standard = ratio_ind.describe()

# for i in range(len(ratio_ind)): # 遍历每个公司
  # for j in range(len(ratio_ind.T)): # 遍历每个指标
    # # 指标值高于75%分位数,得100分
    # if ratio_ind.iloc[i,j] > standard.loc['75%'][j]:
      # n = 100
    # # 指标值在50%和75%分位数之间
    # elif standard.loc['50%'][j] < ratio_ind.iloc[i,j] <= standard.loc['75%'][j]:
      # n = 75
    # # ...以此类推...

步骤4:合并、计算综合分并排名

最后,我们将两部分得分合并,按40/60的权重计算综合分,并进行降序排名。

## 使用join函数将趋势评分表和同业评分表合并
score_sheet = score_sheet_trend.join(score_sheet_ty)

## 计算综合评分
score_sheet['综合评分'] = 0.4*score_sheet['趋势评分'] + 0.6*score_sheet['同业评分']

## 按综合评分降序排列
score_sheet = score_sheet.sort_values(by='综合评分', ascending=False)

## 将结果输出到Excel文件
score_sheet.to_excel('综合评分表.xlsx')

结果解读与启示

通过这个自动化的流程,我们得到了一个清晰、量化的行业排名。

  • 价值:为投资者提供了极有价值的参考,帮助从众多公司中快速锁定值得深入研究的标的。
  • 局限:任何量化模型都是对现实的简化。最终决策还需结合品牌、治理等定性因素

第二部分:基于线性回归模型的财务预测

从本章开始,我们将进入一个全新的领域:利用机器学习模型进行预测

核心问题:我们能否利用历史数据预测未来?

例如,我们能否通过一家公司过去的线上销量,来预测它未来的营业收入?

答案:可以。我们将从最经典的一元线性回归模型开始。

一元线性回归:理论基础

核心思想:寻找一条直线,来最好地拟合(或解释)两个变量之间的关系。

  • 因变量 (Y): 我们希望预测的变量(如:营业收入)。
  • 自变量 (X): 我们用来预测的变量(如:线上销量)。

模型的数学表达

一元线性回归模型可以用一个我们非常熟悉的中学数学公式来表示:

\[ \large{ y = ax + b } \]

  • \(y\): 因变量
  • \(x\): 自变量
  • \(a\): 回归系数 (斜率)
  • \(b\): 截距

回归系数 a (斜率) 的含义

系数a是模型的核心,它量化了xy之间的关系强度。

a的含义:在其他条件不变的情况下,自变量x每增加一个单位,因变量y平均会发生a个单位的变化。

斜率的直观解释 一个坐标系中有一条斜向上的直线,标注了x增加1单位时y的变化量a。 x y Δx = 1 Δy = a

截距 b 的含义

截距b的数学含义很清晰,但在商业场景中需要谨慎解读。

b的含义:当自变量x为0时,因变量y的期望值。

截距的直观解释 一个坐标系中有一条斜向上的直线,标注了当x=0时y轴上的截距b。 x y 0 b

注意外推风险

在很多商业场景中,x=0 是一个没有实际意义或从未出现过的情况(例如线上销量为0)。此时,截距b更多是模型的数学构成,不应过度解读其商业含义。

如何找到“最优”的直线?

我们使用普通最小二乘法 (Ordinary Least Squares, OLS)

核心思想:最优的直线,是那条能让所有数据点的实际值与直线上对应的预测值之间的差值(残差)的平方和最小的直线。

\[ \large{ SSR = \sum_{i=1}^{n} (y_i - \hat{y}_i)^2 = \sum_{i=1}^{n} (y_i - (ax_i + b))^2 } \]

这个SSR在机器学习中也被称为损失函数(Loss Function)

Python实现:使用scikit-learn

scikit-learn是Python中最流行、功能最强大的机器学习库。

基本步骤

  1. 创建模型对象regr = LinearRegression()
  2. 训练模型regr.fit(X, Y)
  3. 进行预测regr.predict(new_X)
  4. 查看参数regr.coef_ (系数a), regr.intercept_ (截距b)

案例分析:茅台线上销量与营收的关系

商业问题:贵州茅台的线上销量与其季度营业收入之间是否存在线性关系?我们能否通过线上销量来预测其营业收入?

数据:贵州茅台2015-2020年季度数据。

数据准备:划分训练集与测试集

这是一个至关重要的步骤,用以检验模型的泛化能力

  • 训练集(Training Set):用于构建和“训练”模型。我们使用历史数据的大部分。
  • 测试集(Test Set):用于检验模型在“未见过”的数据上的表现。我们保留最近的四个季度。

数据准备代码

我们使用Pandas加载数据,并用切片操作划分数据集。

# 只是为了演示,完整代码在隐藏块中
import pandas as pd
df = pd.read_excel('一元回归-茅台酒.xlsx')
## 线上销量为自变量,营业收入为因变量
## 我们去掉近四个季度的数据来做训练
X = df[['线上销量']][:-4]
Y = df['营业收入'][:-4]

## 将最后四个季度的数据作为测试数据
Online_data = df[['线上销量']][-4:]

模型训练与可视化

我们使用训练数据XY来训练模型,并绘制散点图和回归线。

import pandas as pd
from sklearn.linear_model import LinearRegression
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
df = pd.read_excel('一元回归-茅台酒.xlsx')
X = df[['线上销量']][:-4]
Y = df['营业收入'][:-4]
regr = LinearRegression()
regr.fit(X,Y)
plt.figure(figsize=(10, 6))
plt.scatter(X,Y, label='训练数据')
plt.plot(X, regr.predict(X), color='red', label='拟合的回归线')
plt.title('贵州茅台线上销量与营业收入关系')
plt.xlabel('线上销量 (X)')
plt.ylabel('营业收入 (Y)')
plt.grid(True)
plt.legend()
plt.show()
Figure 1

结果解读:回归方程

模型训练完成后,我们可以查看其学到的参数。

import pandas as pd
from sklearn.linear_model import LinearRegression
df = pd.read_excel('一元回归-茅台酒.xlsx')
X = df[['线上销量']][:-4]
Y = df['营业收入'][:-4]
regr = LinearRegression()
regr.fit(X,Y)
# 打印出学习到的线性函数中的系数
print('系数a为:' + str(round(regr.coef_,2)))
print('截距b为:' + str(round(regr.intercept_,2)))

输出结果告诉我们,回归方程为:

营业收入 ≈ 10.2 × 线上销量 + 813847

解读:在其他条件不变的情况下,线上销量每增加1单位,季度营业收入预计会增加约10.2单位。

模型预测与评估

现在,我们用训练好的模型来预测测试集(最后四个季度)的营收,并与真实值比较。

import pandas as pd
from sklearn.linear_model import LinearRegression
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
df = pd.read_excel('一元回归-茅台酒.xlsx')
X = df[['线上销量']][:-4]
Y = df['营业收入'][:-4]
Online_data = df[['线上销量']][-4:]
regr = LinearRegression()
regr.fit(X,Y)
P = pd.DataFrame(regr.predict(Online_data))
Origin_Y = df['营业收入'] 
Predict_Y = pd.concat([Y,P],ignore_index=True)
plt.figure(figsize=(12, 7))
plt.plot(Origin_Y.index, Origin_Y, color='red', marker='o', linestyle='--', label='真实营业收入')
plt.plot(Predict_Y.index, Predict_Y, color='blue', marker='x', linestyle='-', label='模型预测营业收入')
plt.title('贵州茅台营业收入:真实值 vs. 预测值')
plt.xlabel('季度索引')
plt.ylabel('营业收入')
plt.axvline(x=len(Y)-0.5, color='green', linestyle=':', label='训练/测试分割点')
plt.grid(True)
plt.legend()
plt.show()
Figure 2

图表解读

  • 分割线左侧 (训练集):蓝线与红线几乎重合,这很正常,因为模型就是基于这些数据训练的。
  • 分割线右侧 (测试集):模型的预测值(蓝线)虽然与真实值(红线)存在误差,但它准确地捕捉到了营业收入持续增长的总体趋势

这初步证明了我们的模型具有一定的预测价值。但是,“看起来不错”还不够…

第三部分:严谨的模型评估

我们需要一套量化的、客观的标准来评估模型的性能。

模型评估的“三驾马车”

我们将学习评估线性回归模型的三个核心指标:

  1. R-squared (R²): 模型的整体解释力有多强?
  2. Adjusted R-squared: 考虑模型复杂度的解释力。
  3. P-value: 自变量的影响是否显著?

我们将使用一个新的库 statsmodels,它能提供详尽的统计报告。

评估指标一:R-squared (决定系数, R²)

R²的直观理解:“因变量的总变异中,能够被自变量解释的百分比”。

  • 取值范围:01
  • 数值越接近1,表示模型对数据的拟合优度越高。
  • 例如,\(R^2=0.92\) 意味着自变量(线上销量)能够解释因变量(营业收入)92%的变化。

R-squared的计算原理

R²的计算基于三个“平方和”:

  • TSS (Total Sum of Squares): Y的总波动。
  • RSS (Residual Sum of Squares): 模型未能解释的波动(残差)。

\[ \large{ R^2 = 1 - \frac{RSS}{TSS} = \frac{\text{模型解释的波动}}{\text{Y的总波动}} } \]

评估指标二:Adjusted R-squared

问题:在模型中增加任何自变量(即使是无关的),\(R^2\)都只会上升或不变,这会误导我们选择更复杂的模型。

Adjusted R² 通过引入对自变量个数的“惩罚项”来解决这个问题。

  • 用途:在比较包含不同数量自变量的模型时,Adjusted R² 是一个更公允的指标。

评估指标三:P-value (P值)

R²告诉我们模型整体好不好,而P值判断单个自变量是否真的重要。

核心问题:我们观察到的自变量与因变量的关系,究竟是真实存在的,还是仅仅由“巧合”造成的?

理解P值:基于假设检验

对于每个自变量,我们都进行一次检验:

  • 原假设 (\(H_0\)): 该自变量与因变量无关(其真实系数为0)。
  • 备择假设 (\(H_a\)): 该自变量与因变量有关(其真实系数不为0)。

P值的含义:如果原假设为真(变量真的无关),我们观测到现有数据的概率。

如何解读P值?

我们通常使用0.05作为显著性水平的阈值。

  • P-value < 0.05: 小概率事件发生了!我们有充分理由拒绝原假设,认为该自变量是统计显著的
  • P-value > 0.05: 我们没有足够理由拒绝原假设,认为该自变量是不显著的

一句话总结:P值越小,变量越重要!

案例实战:评估茅台销量预测模型

现在,让我们使用statsmodels库来获取茅台模型的详细评估报告。

# 只是为了演示,完整代码在隐藏块中
import pandas as pd
import statsmodels.api as sm
df = pd.read_excel('一元回归-茅台酒.xlsx')
X = df[['线上销量']]
Y = df['营业收入']
X2 = sm.add_constant(X)
est = sm.OLS(Y, X2).fit()
# print(est.summary())

statsmodels代码与输出

核心代码非常简洁:

import statsmodels.api as sm

# 准备X和Y
X = df[['线上销量']]
Y = df['营业收入']

# statsmodels默认不包含截距,需手动添加
X2 = sm.add_constant(X)

# 构建并拟合OLS模型
est = sm.OLS(Y, X2).fit()

# 打印详细评估报告
print(est.summary())

解读statsmodels评估报告

                           OLS Regression Results
==============================================================================
Dep. Variable:                   营业收入   R-squared:                       0.993
Model:                            OLS   Adj. R-squared:                  0.992
...
==============================================================================
                 coef    std err          t      P>|t|      [0.025      0.975]
------------------------------------------------------------------------------
const       8.138e+05   2.11e+04     38.653      0.000    7.69e+05    8.58e+05
线上销量          10.1517      0.228     44.593      0.000       9.676      10.627
==============================================================================
  • R-squared: 0.993。极高!线上销量解释了营业收入99.3%的变异。
  • coef (线上销量): 10.1517。这就是系数a
  • P>|t| (线上销量): 0.000。远小于0.05,表明线上销量是极显著的预测变量。

模型改进探索:多项式回归

思考:变量间的关系一定是直线吗?会不会是曲线?

我们可以通过引入自变量的高次项(如 \(x^2\))来构建多项式回归,以拟合曲线关系。

尝试为茅台模型增加二次项

我们为模型增加一个“线上销量的平方”项,看看能否进一步提升性能。

from sklearn.preprocessing import PolynomialFeatures

# 创建一个二次多项式特征生成器
poly_reg = PolynomialFeatures(degree=2)
# 将原始X转换为包含X和X^2的新特征矩阵
X_ = poly_reg.fit_transform(X)

# 使用statsmodels重新拟合模型
X2 = sm.add_constant(X_) 
est = sm.OLS(Y, X2).fit()
print(est.summary())

比较模型:线性 vs. 二次

二次模型的评估报告摘要:

R-squared:                       0.993
Adj. R-squared:                  0.993
...
==============================================================================
                 coef    std err          t      P>|t|      [0.025      0.975]
------------------------------------------------------------------------------
...
x1             8.9958      2.368      3.800      0.001       4.032      13.960
x2             0.0001      0.000      0.556      0.582      -0.000       0.000
==============================================================================
  • Adj. R-squared: 从0.992微升至0.993,提升极小。
  • P>|t| (x2): 代表 \(x^2\) 项的P值为 0.582,远大于0.05!

最终结论:奥卡姆剃刀原则

虽然加入平方项让R²有微乎其微的提升,但该项的P值表明它并不显著

奥卡姆剃刀原则:如无必要,勿增实体。

我们应该选择更简单、但同样有效的一元线性模型。这个例子展示了为何不能只看R-squared。

第四部分:多元线性回归

现实世界中,一个结果往往是多个因素共同作用的结果。

从“一”到“多”

多元线性回归:用多个自变量来预测一个因变量。

通用模型\[ \large{ y = k_0 + k_1x_1 + k_2x_2 + \dots + k_px_p } \]

  • \(x_1, x_2, \dots, x_p\): 是 \(p\) 个不同的自变量。
  • \(k_i\): \(x_i\) 的系数。表示在其他所有自变量保持不变的情况下\(x_i\) 每增加一单位,\(y\) 的平均变化。

数据预处理:特征缩放的必要性

问题:当多个自变量的量纲(Scale) 相差巨大时会发生什么?

例如,用“门店数量”(数值:几十到几千)和“广告投入”(数值:百万到上亿)来预测销售额。

后果:数值范围大的特征(广告投入)可能会在模型中占据主导地位,影响模型的准确性和解释性。

特征缩放方法一:Min-Max标准化

将原始数据通过线性变换,缩放到 [0, 1] 的区间内。

\[ \large{ x^* = \frac{x - \text{min}(x)}{\text{max}(x) - \text{min}(x)} } \]

  • 优点:简单直观。
  • 缺点:对异常值(outliers)非常敏感。

特征缩放方法二:Z-score标准化

将原始数据转换为均值为0,标准差为1的标准正态分布。

\[ \large{ x^* = \frac{x - \mu}{\sigma} } \]

  • 优点:对异常值不那么敏感,是更常用的方法。
  • 适用场景:适用于大多数机器学习算法。

案例分析:海天味业营收预测

与茅台不同,海天味业的收入由多种产品共同贡献:

  • 酱油
  • 蚝油
  • 酱类
  • 其他

这是一个应用多元线性回归的绝佳案例。

初步建模:未经缩放的数据

我们首先在原始数据上构建模型,并查看statsmodels的评估报告。

# 只是为了演示,完整代码在隐藏块中
import pandas as pd
import statsmodels.api as sm
df = pd.read_excel('多元回归-海天味业.xlsx')
X = df[['酱油', '蚝油', '酱类','其他']]
Y = df['营业收入']
X2 = sm.add_constant(X)
est = sm.OLS(Y, X2).fit()
# print(est.summary())
                           OLS Regression Results
==============================================================================
Dep. Variable:                   营业收入   R-squared:                       1.000
Model:                            OLS   Adj. R-squared:                  0.999
...
==============================================================================
                 coef    std err          t      P>|t|      [0.025      0.975]
------------------------------------------------------------------------------
const      -1.385e+04   9587.202     -1.444      0.170   -3.41e+04    6417.842
酱油             2.3733      0.224     10.575      0.000       1.895       2.852
蚝油             2.4571      0.655      3.754      0.002       1.069       3.845
酱类             2.5074      0.547      4.581      0.000       1.341       3.674
其他            -0.2796      1.018     -0.275      0.78 исчез71      -2.428       1.869
==============================================================================

初步模型解读

  • Adj. R-squared: 0.999,模型整体拟合度极高。
  • P>|t| (P值)
    • “酱油”、“蚝油”、“酱类”的P值均为0.000,是极显著的预测变量。
    • “其他”的P值为0.781,不显著。在其他变量存在的情况下,它没有独立的解释能力。

特征缩放后的模型

虽然初步模型效果很好,但为了养成良好习惯并更好地解释系数,我们仍进行特征缩放。

from sklearn.preprocessing import MinMaxScaler, StandardScaler

## min-max标准化
X_new_minmax = MinMaxScaler().fit_transform(X)
# ... 训练模型 ...
print('min-max标准化后各系数为:' + str(regr_minmax.coef_.round(2)))

## Z-score标准化
X_new_zscore = StandardScaler().fit_transform(X)
# ... 训练模型 ...
print('Z-score标准化后各系数为:' + str(regr_zscore.coef_.round(2)))

特征缩放后的系数解读

Z-score标准化后的系数: [16578.43, 4443.08, 1261.27, -90.95]

  • 由于所有特征现在都在同一个尺度上,我们可以直接比较系数的绝对值大小来判断其相对重要性。
  • 结论:酱油 (16578) 对营收的贡献最大,其次是蚝油 (4443),再其次是酱类 (1261)。这与我们的商业直觉相符。

第五部分:财务舞弊识别与决策树模型

我们即将进入一个新的领域:解决分类问题,例如判断一家公司“舞弊”或“不舞弊”。

决策的艺术:理解决策树

决策树是一种非常直观的模型,它的结构就像一个流程图,充满了“如果…那么…”的逻辑分支。

员工离职预测决策树 一个简单的决策树流程图,用于预测员工是否会离职。 员工满意度 < 5? True False 离职 收入 < 10,000? True False 离职 不离职

决策树如何学习?寻找“最纯”的划分

决策树的目标是在每个节点进行一次划分,使得划分后的子节点尽可能“纯”

“纯”指的是子节点内的数据样本尽可能属于同一个类别。

我们使用基尼不纯度(Gini Impurity)来衡量纯度。基尼不纯度越低,节点越纯。

案例分析:构建财务舞弊预测模型

目标:构建一个模型,提前识别出具有舞弊风险的公司。这是一个典型的二分类问题

数据:中国上市公司2010-2019年财务、市场和宏观经济数据。

数据探索:发现严重的不平衡问题

真实世界的舞弊数据,存在一个严重的问题:数据不平衡

# print(fraud_data['是否舞弊'].value_counts())
# 0    32924
# 1      206
# Name: 是否舞弊, dtype: int64

正常公司 (32924) 的样本数量远多于舞弊公司 (206)。

为何不平衡数据是个大问题?

如果直接用不平衡数据训练,模型会“偷懒”,简单地将所有公司都预测为“正常”,也能获得极高的准确率(Accuracy),但这毫无意义。

我们需要模型能够准确地“揪出”那些少数的舞弊公司。

解决方案:SMOTE过采样技术

SMOTE (Synthetic Minority Over-sampling Technique) 是一种强大的过采样技术。

核心思想:对于少数类(舞弊公司),通过在其样本附近人工合成新的、相似的样本,从而增加少数类的数量,使数据集变得平衡。

SMOTE技术图解

SMOTE过采样技术示意图 图中显示了多数类和少数类样本点。SMOTE在两个少数类样本点之间生成了一个新的人工样本点。 多数类样本 少数类样本 生成新的人工样本

模型训练与评估指标

在不平衡分类问题中,准确率(Accuracy) 不是好的评估指标。

我们更关心 ROC曲线AUC值

  • ROC曲线 (Receiver Operating Characteristic): 衡量模型在不同阈值下的“命中率”与“假警报率”的权衡。曲线越靠近左上角越好。
  • AUC (Area Under the Curve): ROC曲线下的面积,取值在0.5到1之间。AUC越高,模型区分正负样本的能力越强。

财务舞弊预测模型的ROC曲线

我们使用过采样后的数据训练一个最大深度为3的决策树,并绘制其ROC曲线。

import pandas as pd
import numpy as np
from collections import Counter
from imblearn.over_sampling import SMOTE
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import roc_auc_score, roc_curve
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

fraud_data = pd.read_excel('财务数据2010_2019.xlsx')
for t in fraud_data.columns:
    if fraud_data[t].isnull().sum()/len(fraud_data)>0.5:
        fraud_data = fraud_data.drop(columns=t)
fraud_data = fraud_data.fillna(fraud_data.median())
fraud_data[np.isinf(fraud_data)] = np.nan
fraud_data = fraud_data.fillna(fraud_data.max())
train_year = [i for i in range(2009,2018)]
test_year = [2018]
train = fraud_data[fraud_data["年份"].isin(train_year)]
test = fraud_data[fraud_data["年份"].isin(test_year)]
X_train = train.drop(columns=['索引', '年份', '是否舞弊'])
y_train = train['是否舞弊']
X_test = test.drop(columns=['索引', '年份', '是否舞弊'])
y_test = test['是否舞弊']
model_smote = SMOTE(random_state=123)
X_smote_resampled, y_smote_resampled = model_smote.fit_resample(X_train, y_train)
model = DecisionTreeClassifier(max_depth=3, random_state=123)
model.fit(X_smote_resampled, y_smote_resampled)
y_pred_proba = model.predict_proba(X_test)[:, 1]
auc = roc_auc_score(y_test, y_pred_proba)
fpr, tpr, thres = roc_curve(y_test, y_pred_proba)

plt.figure(figsize=(8, 6))
plt.plot(fpr, tpr, label=f'Decision Tree (AUC = {auc:.2f})', color='blue', linewidth=2)
plt.plot([0,1],[0,1], 'r--', label='Random Guess')
plt.xlabel('假警报率 (False Positive Rate)')
plt.ylabel('命中率 (True Positive Rate)')
plt.title('ROC曲线')
plt.legend()
plt.grid(True)
plt.show()
Figure 3

解读模型:特征重要性

决策树的一大优点是其可解释性。我们可以轻易地知道模型在决策时最看重哪些特征。

这为审计人员和监管机构提供了非常有价值的线索。

舞弊预测模型中最重要的五个特征

Table 1: 财务舞弊预测模型中最重要的五个特征
importances = model.feature_importances_
features = X_train.columns
importances_df = pd.DataFrame()
importances_df['特征名称'] = features
importances_df['特征重要性'] = model.feature_importances_
importances_df = importances_df.sort_values('特征重要性', ascending=False)
print(importances_df.head().to_markdown(index=False))

从表中可以看到,模型认为“资产减值损失”、“存货周转率”等指标对于识别舞弊最为关键。

第六部分:集成学习与随机森林

单个决策树虽然直观,但稳定性较差。为了构建更强大的模型,我们引入集成学习的思想。

集思广益:集成学习的力量

核心思想:三个臭皮匠,顶个诸葛亮。

集成学习不依赖单个“天才”模型,而是将多个相对较弱的模型(基学习器)结合起来,通过集体智慧做出最终决策。

我们主要学习其中的 Bagging 方法。

Bagging方法图解

Bagging (Bootstrap Aggregating) 流程图 展示了从原始数据通过有放回抽样生成多个数据子集,分别训练模型,最终通过投票得出结果的流程。 原始数据 有放回抽样 (Bootstrap) 数据子集 1 数据子集 2 数据子集 n 模型 1 模型 2 模型 n 投票/平均 最终结果

随机森林:决策树的“进化版”

随机森林 = Bagging + 决策树 + 特征随机

它在Bagging的基础上,又增加了一层随机性: - 数据随机 (行抽样): Bagging的自助采样。 - 特征随机 (列抽样): 在构建树的每个节点时,随机抽取部分特征来参与最佳分裂点的选择。

这“双重随机”使得模型中的每棵树差异性更大,整体泛化能力更强。

案例再战:使用随机森林提升舞弊预测能力

我们将使用RandomForestClassifier,并引入超参数调优技术,力求获得最佳的模型性能。

超参数调优:网格搜索

超参数是模型训练前需要我们手动设定的参数,如树的数量n_estimators、树的最大深度max_depth

网格搜索(Grid Search) 是一种自动化寻找最优超参数组合的强大工具。

  • 原理:我们定义一个参数“网格”,程序会自动遍历所有组合,通过交叉验证评估每种组合的性能,最终返回最佳组合。

网格搜索与交叉验证图解

1. 参数网格 (Parameter Grid) 参数2: n_estimators 10 20 参数1: max_depth 3 5 2. 5折交叉验证 (5-Fold CV) 第1轮 第2轮 ... 第5轮 验证集 (Validation) 训练集 (Train) 评估组合 (3, 10) 计算平均性能得分

网格搜索代码实现

我们使用scikit-learn中的GridSearchCV来执行这个过程。

from sklearn.model_selection import GridSearchCV

## 定义要搜索的参数网格
parameters = {
    'n_estimators': [10, 20], 
    'max_depth': [3, 5], 
    'criterion': ['gini', 'entropy']
}

## 创建一个随机森林模型实例
model = RandomForestClassifier(random_state=123) 

## 设置网格搜索,指定评估指标为AUC,使用5折交叉验证
grid_search = GridSearchCV(model, parameters, scoring='roc_auc', cv=5)

## 在过采样后的训练数据上执行搜索
grid_search.fit(X_smote_resampled, y_smote_resampled)

最终结果与对比

网格搜索会自动找到最优的参数组合,并用这个最佳模型进行预测。

# 打印最优参数组合
print("网格搜索找到的最优参数:", grid_search.best_params_)

# 使用找到的最佳模型进行预测和评估
y_pred_proba = grid_search.predict_proba(X_test)[:, 1]
tuned_auc_score = roc_auc_score(y_test, y_pred_proba)
print(f"调优后模型的AUC: {tuned_auc_score:.4f}")

经过调优后的随机森林模型,其AUC分数通常会比单一决策树和默认参数的随机森林更高,是解决复杂分类问题的有力武器。

课程总结

今天,我们完成了一次从数据分析到机器学习建模的完整旅程。

  • 批量化分析:学习了如何将分析能力规模化,自动化评价整个行业。
  • 回归预测:掌握了线性回归模型,并学会了如何严谨地评估它。
  • 分类决策:探索了决策树与随机森林,并解决了财务舞弊识别这一真实、复杂的商业问题,掌握了处理不平衡数据、评估分类模型和超参数调优等关键技能。

感谢聆听!

希望今天的课程能为大家打开一扇通往数据科学与智能金融的大门。