各位同学,欢迎来到《商业大数据分析与应用》的全新篇章。
今天,我们将踏上一段激动人心的旅程,从自动化财务分析到构建智能预测模型。
规模化分析能力:掌握如何从分析单一公司升级到自动化、批量化地评价整个行业。
构建预测模型:学习并实践经典的线性回归模型,利用历史数据预测未来的财务表现。
解决分类问题:探索决策树与随机森林模型,解决如“财务舞弊识别”等复杂的商业决策问题。
我们将从一个高度实用的案例开始:如何自动化地对一个行业的所有公司进行综合评价与排名。
分析一家公司已是挑战,当面对一个包含数十家公司的行业时,我们如何高效、客观地进行筛选?
答案:构建一个自动化的、量化的评价框架。
我们的评价体系建立在两个坚实的分析维度之上:
趋势分析关注一家公司自身的发展轨迹。
同业分析将公司置于行业竞争的背景下进行考察。
我们的评价体系将覆盖衡量公司经营状况的五个核心维度,确保评估的全面性。
定义:企业获取利润的本领。这是评价一家公司的核心。
定义:企业利用其资产创造收入的效率。
定义:企业偿还债务、抵御财务风险的能力。
定义:企业在未来扩大规模、提升利润的潜力。
定义:企业经营活动的“血液循环”是否通畅。
为了得到最终排名,我们对两个分析维度进行加权平均。
综合得分 = (趋势分析得分 × 40%) + (同业分析得分 × 60%)
接下来,我们将用Python代码,将上述理论框架付诸实践。
目标:自动化输出A股白酒行业上市公司的综合排名。
首先,我们导入所需库,并使用Tushare API获取白酒行业所有上市公司的名单。
下面的代码片段展示了如何筛选出所有行业为’白酒’的公司。
## 获取同行业股票代码
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皇台
)可能与我们本地文件名(皇台酒业
)不完全匹配,这会导致程序错误。编写鲁棒的名称匹配逻辑是数据预处理的关键一环。
核心逻辑:对于每家公司,遍历其所有财务指标。如果一个指标逐年改善(在此案例中均为数值增大),则加分。
最终得分 = (改善的年数 / 总比较年数) × 100
我们通过嵌套循环实现评分:外层循环遍历公司,内层循环遍历指标和年份。
# 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)
# ... 存储分数 ...
核心逻辑:衡量一家公司的指标在整个行业中所处的位置。我们使用分位数进行评分。
首先计算整个行业所有指标的描述性统计(包括分位数),然后将每家公司与这些“行业标准”进行比较。
# ... 计算所有公司各指标的均值,存入 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
# # ...以此类推...
最后,我们将两部分得分合并,按40/60
的权重计算综合分,并进行降序排名。
通过这个自动化的流程,我们得到了一个清晰、量化的行业排名。
从本章开始,我们将进入一个全新的领域:利用机器学习模型进行预测。
例如,我们能否通过一家公司过去的线上销量,来预测它未来的营业收入?
答案:可以。我们将从最经典的一元线性回归模型开始。
核心思想:寻找一条直线,来最好地拟合(或解释)两个变量之间的关系。
一元线性回归模型可以用一个我们非常熟悉的中学数学公式来表示:
\[ \large{ y = ax + b } \]
系数a
是模型的核心,它量化了x
与y
之间的关系强度。
a
的含义:在其他条件不变的情况下,自变量x
每增加一个单位,因变量y
平均会发生a
个单位的变化。
截距b
的数学含义很清晰,但在商业场景中需要谨慎解读。
b
的含义:当自变量x
为0时,因变量y
的期望值。
注意外推风险
在很多商业场景中,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)。
scikit-learn
库scikit-learn
是Python中最流行、功能最强大的机器学习库。
基本步骤:
regr = LinearRegression()
regr.fit(X, Y)
regr.predict(new_X)
regr.coef_
(系数a), regr.intercept_
(截距b)商业问题:贵州茅台的线上销量与其季度营业收入之间是否存在线性关系?我们能否通过线上销量来预测其营业收入?
数据:贵州茅台2015-2020年季度数据。
这是一个至关重要的步骤,用以检验模型的泛化能力。
我们使用Pandas加载数据,并用切片操作划分数据集。
我们使用训练数据X
和Y
来训练模型,并绘制散点图和回归线。
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()
模型训练完成后,我们可以查看其学到的参数。
输出结果告诉我们,回归方程为:
营业收入 ≈ 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()
这初步证明了我们的模型具有一定的预测价值。但是,“看起来不错”还不够…
我们需要一套量化的、客观的标准来评估模型的性能。
我们将学习评估线性回归模型的三个核心指标:
我们将使用一个新的库 statsmodels
,它能提供详尽的统计报告。
R²的直观理解:“因变量的总变异中,能够被自变量解释的百分比”。
0
到 1
。1
,表示模型对数据的拟合优度越高。R²的计算基于三个“平方和”:
\[ \large{ R^2 = 1 - \frac{RSS}{TSS} = \frac{\text{模型解释的波动}}{\text{Y的总波动}} } \]
问题:在模型中增加任何自变量(即使是无关的),\(R^2\)都只会上升或不变,这会误导我们选择更复杂的模型。
Adjusted R² 通过引入对自变量个数的“惩罚项”来解决这个问题。
R²告诉我们模型整体好不好,而P值判断单个自变量是否真的重要。
核心问题:我们观察到的自变量与因变量的关系,究竟是真实存在的,还是仅仅由“巧合”造成的?
对于每个自变量,我们都进行一次检验:
P值的含义:如果原假设为真(变量真的无关),我们观测到现有数据的概率。
我们通常使用0.05
作为显著性水平的阈值。
一句话总结:P值越小,变量越重要!
现在,让我们使用statsmodels
库来获取茅台模型的详细评估报告。
statsmodels
代码与输出核心代码非常简洁:
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
==============================================================================
a
。思考:变量间的关系一定是直线吗?会不会是曲线?
我们可以通过引入自变量的高次项(如 \(x^2\))来构建多项式回归,以拟合曲线关系。
我们为模型增加一个“线上销量的平方”项,看看能否进一步提升性能。
二次模型的评估报告摘要:
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
==============================================================================
虽然加入平方项让R²有微乎其微的提升,但该项的P值表明它并不显著。
奥卡姆剃刀原则:如无必要,勿增实体。
我们应该选择更简单、但同样有效的一元线性模型。这个例子展示了为何不能只看R-squared。
现实世界中,一个结果往往是多个因素共同作用的结果。
多元线性回归:用多个自变量来预测一个因变量。
通用模型: \[ \large{ y = k_0 + k_1x_1 + k_2x_2 + \dots + k_px_p } \]
问题:当多个自变量的量纲(Scale) 相差巨大时会发生什么?
例如,用“门店数量”(数值:几十到几千)和“广告投入”(数值:百万到上亿)来预测销售额。
后果:数值范围大的特征(广告投入)可能会在模型中占据主导地位,影响模型的准确性和解释性。
将原始数据通过线性变换,缩放到 [0, 1]
的区间内。
\[ \large{ x^* = \frac{x - \text{min}(x)}{\text{max}(x) - \text{min}(x)} } \]
将原始数据转换为均值为0,标准差为1的标准正态分布。
\[ \large{ x^* = \frac{x - \mu}{\sigma} } \]
与茅台不同,海天味业的收入由多种产品共同贡献:
这是一个应用多元线性回归的绝佳案例。
我们首先在原始数据上构建模型,并查看statsmodels
的评估报告。
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
==============================================================================
虽然初步模型效果很好,但为了养成良好习惯并更好地解释系数,我们仍进行特征缩放。
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]
我们即将进入一个新的领域:解决分类问题,例如判断一家公司“舞弊”或“不舞弊”。
决策树是一种非常直观的模型,它的结构就像一个流程图,充满了“如果…那么…”的逻辑分支。
决策树的目标是在每个节点进行一次划分,使得划分后的子节点尽可能“纯”。
“纯”指的是子节点内的数据样本尽可能属于同一个类别。
我们使用基尼不纯度(Gini Impurity)来衡量纯度。基尼不纯度越低,节点越纯。
目标:构建一个模型,提前识别出具有舞弊风险的公司。这是一个典型的二分类问题。
数据:中国上市公司2010-2019年财务、市场和宏观经济数据。
真实世界的舞弊数据,存在一个严重的问题:数据不平衡。
正常公司 (32924) 的样本数量远多于舞弊公司 (206)。
如果直接用不平衡数据训练,模型会“偷懒”,简单地将所有公司都预测为“正常”,也能获得极高的准确率(Accuracy),但这毫无意义。
我们需要模型能够准确地“揪出”那些少数的舞弊公司。
SMOTE (Synthetic Minority Over-sampling Technique) 是一种强大的过采样技术。
核心思想:对于少数类(舞弊公司),通过在其样本附近人工合成新的、相似的样本,从而增加少数类的数量,使数据集变得平衡。
在不平衡分类问题中,准确率(Accuracy) 不是好的评估指标。
我们更关心 ROC曲线 和 AUC值。
我们使用过采样后的数据训练一个最大深度为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()
决策树的一大优点是其可解释性。我们可以轻易地知道模型在决策时最看重哪些特征。
这为审计人员和监管机构提供了非常有价值的线索。
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的基础上,又增加了一层随机性: - 数据随机 (行抽样): Bagging的自助采样。 - 特征随机 (列抽样): 在构建树的每个节点时,随机抽取部分特征来参与最佳分裂点的选择。
这“双重随机”使得模型中的每棵树差异性更大,整体泛化能力更强。
我们将使用RandomForestClassifier
,并引入超参数调优技术,力求获得最佳的模型性能。
超参数是模型训练前需要我们手动设定的参数,如树的数量n_estimators
、树的最大深度max_depth
。
网格搜索(Grid Search) 是一种自动化寻找最优超参数组合的强大工具。
我们使用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)
网格搜索会自动找到最优的参数组合,并用这个最佳模型进行预测。
经过调优后的随机森林模型,其AUC分数通常会比单一决策树和默认参数的随机森林更高,是解决复杂分类问题的有力武器。
今天,我们完成了一次从数据分析到机器学习建模的完整旅程。
希望今天的课程能为大家打开一扇通往数据科学与智能金融的大门。
商业大数据分析与应用