前言
书接上文,上一小节简单介绍了多元回归的基本原理、使用方式,本小节来实践:qps与cpu、内存、磁盘io、网络io之间的关系
获取数据
参考一元线性回归的获取方式
from flow import *
from datetime import datetime
start_time = datetime.strptime('2025-04-06 00:00:00', '%Y-%m-%d %H:%M:%S').timestamp()
end_time = datetime.strptime('2025-04-06 23:59:59', '%Y-%m-%d %H:%M:%S').timestamp()
step = 600
sls_step = 3600*6
query = get_query_data(start_time, end_time, sls_step)
cpu = get_cpu_data(start_time, end_time, step)
memory = get_memory_data(start_time, end_time, step)
network_in = get_network_in_data(start_time, end_time, step)
network_out = get_network_out_data(start_time, end_time, step)
file_read = get_file_read_data(start_time, end_time, step)
file_write = get_file_write_data(start_time, end_time, step)
print('cpu 数据个数为{} ,前10数据为{}'.format(len(cpu), cpu[:10]))
print('query 数据个数为{} ,前10数据为{}'.format(len(query), query[:10]))
print('memory 数据个数为{} ,前10数据为{}'.format(len(memory), memory[:10]))
print('network_in 数据个数为{} ,前10数据为{}'.format(len(network_in), network_in[:10]))
print('network_out 数据个数为{} ,前10数据为{}'.format(len(network_out), network_out[:10]))
print('file_read 数据个数为{} ,前10数据为{}'.format(len(file_read), file_read[:10]))
print('file_write 数据个数为{} ,前10数据为{}'.format(len(file_write), file_write[:10]))
脚本!启动:
特征标准化
特征数据已经获取完成,看起来是没问题,但是仔细分析,好像又有点问题,首先cpu数据非常小,内存数据又很大,特征数据之间的数量级差距太大了,特别是在多元回归中,不同特征的量纲和尺度可能差异巨大。若未标准化, 回归系数的数值大小会受特征尺度影响,导致难以直接比较特征的重要性
那首先先人为的进行数据缩放
query = [round(x/10000, 4) for x in get_query_data(start_time, end_time, sls_step)]
cpu = get_cpu_data(start_time, end_time, step)
memory = [round(x/1024/1024/1024, 4) for x in get_memory_data(start_time, end_time, step)]
network_in = [round(x/1024/1024, 4) for x in get_network_in_data(start_time, end_time, step)]
network_out = [round(x/1024/1024, 4) for x in get_network_out_data(start_time, end_time, step)]
file_read = [round(x/1024/1024, 4) for x in get_file_read_data(start_time, end_time, step)]
file_write = [round(x/1024, 4) for x in get_file_write_data(start_time, end_time, step)]
看看效果
调整过后,特征数据都是2位数的了,只不过单位不一样
- query的单位是万
- memory的单位是G
- network_in的单位是M
- network_out的单位是M
- file_read的单位是K
- file_write的单位是K
再进行数据标准化,数据标准化的公式
\[z = \frac{x - \mu}{\sigma} \]
- μ 是特征的均值,
- σ 是特征的标准差。
标准化后,所有特征的尺度统一,均值为0,标准差为1
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
训练模型
多元回归!启动
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error, r2_score
from sklearn.preprocessing import StandardScaler
from flow import *
from datetime import datetime
import pandas as pd
def adjusted_r2(r2, n, p):
return 1 - (1 - r2) * (n - 1) / (n - p - 1)
start_time = datetime.strptime('2025-04-06 00:00:00', '%Y-%m-%d %H:%M:%S').timestamp()
end_time = datetime.strptime('2025-04-06 23:59:59', '%Y-%m-%d %H:%M:%S').timestamp()
step = 600
sls_step = 3600*6
query = [round(x/10000, 4) for x in get_query_data(start_time, end_time, sls_step)]
cpu = get_cpu_data(start_time, end_time, step)
memory = [round(x/1024/1024/1024, 4) for x in get_memory_data(start_time, end_time, step)]
network_in = [round(x/1024/1024, 4) for x in get_network_in_data(start_time, end_time, step)]
network_out = [round(x/1024/1024, 4) for x in get_network_out_data(start_time, end_time, step)]
file_read = [round(x/1024/1024, 4) for x in get_file_read_data(start_time, end_time, step)]
file_write = [round(x/1024, 4) for x in get_file_write_data(start_time, end_time, step)]
features = {
'feature1': cpu,
'feature2': memory,
'feature3': network_in,
'feature4': network_out,
'feature5': file_read,
'feature6': file_write,
}
data = {
'result': query,
}
data.update(features)
df = pd.DataFrame(data)
X = df[[
'feature1',
'feature2',
'feature3',
'feature4',
'feature5',
'feature6',
]]
y = df['result']
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
model = LinearRegression()
model.fit(X, y)
y_pred = model.predict(X)
MSE = mean_squared_error(y, y_pred)
r2 = r2_score(y, y_pred)
print("R²:", r2)
print("MSE:", MSE)
p = len(features.keys())
n = len(data['result'])
adjusted_r2 = adjusted_r2(r2, n, p)
print(f"调整决定系数 (Adjusted R²): {adjusted_r2}")
脚本!启动:
完美的模型,来得太顺利反而有点不太习惯了
通过lasso回归来看下哪一些参数是强相关的
from sklearn.linear_model import Lasso
lasso = Lasso(alpha=0.1)
lasso.fit(X_scaled, y)
for i, coef in enumerate(lasso.coef_, 1):
print(f'x{i} 的回归系数:{coef:.4f}')
lasso回归检查特征
脚本!启动:
这里面已经有一些特征在划水了,找出来,把他们裁掉!当然lasso回归已经自动帮我们把假装干活的特征自动裁员了(打工人,哭死=_=!),但是还是有必要找出原因来的
将这4个特征画个图看一下
X2(内存)、X5(file_read)、X6(file_write)符合预期:特征是一条直线,不随着query波动
- 首先看X2,这是内存。在业务服务中,由于是java服务,内存在启动的时候划分了一大块交给jvm管理,所以在操作系统看来,变化不大,要折腾都在jvm内部折腾。所以memory是一条直线
- X5,这是file_read。在业务服务中,几乎没有的file_read,都是通过network_read,所以file_read一条直线
- X6,这是file_write。在业务服务中,都是缓存写,再由操作系统同步到磁盘,所以file_write是由操作系统决定的,近似一条直线
再看X3,这是network_in,按理说这个特征是与query强相关的,有明显的数据波动,但是lasso回归的时候还是把它略掉了,认为它是一个多余的特征
t检验
定义
检验每个自变量是否真正影响了结果。更直接一点,揪出谁在工作谁在划水!
实践
先介绍一个专门用于数据分析的工具:statsmodels,用来做统计推断,并且提供额外的模型检查工具
安装也很简单:
pip3 install statsmodels
添加代码:
import statsmodels.api as sm
X_with_const = sm.add_constant(X)
sm_model = sm.OLS(y, X_with_const).fit()
print(sm_model.summary())
脚本!启动:
这怎么还越整越复杂了!这输出的都是些什么东西啊?!
别着急,这部分主要是描述t检验,先拿出t检验相关的数据
简单解释一下:
- 第一列就是参数,const是常数项,也就是公式中的\(β_0\),其余的是6个参数
- 第二列coef,是所谓的系数,就是\(β_1 β_2 \dots β_n\)
- 第三列std err,就是所谓的标准误差
- 第四列t值,计算公式为:
\[t=\frac{coef}{std err} \]
-
第五列P>|t|,所谓的P值,计算公式就不列出来了,我自己都没搞明白 =_=!,只要记住非常有用就行,一会会用
-
最后两列一起看[0.025 0.975],这是所谓的置信区间,什么置信区间?正态分布熟悉吧,记住置信区间和正态分布强相关就行了,
feature1 31.2856 9.573 3.268 0.001 12.356 50.215
这里意味着,有95%的系数是落在[12.36, 50.21] 之间
说了这么半天,最直接有效的就是看P值,越接近0,那就说明该系数越相关
还有个更简单的方法,直接丢gpt看看
ai牛逼,科技提高工作效率。至此,通过t检验,也可以发现有哪些特征是强相关的,哪些特征是无用的。feature1(cpu)、feature3(network_in)、feature4(network_out)这3个特征对于结果有重大的影响。之前的lasso回 归中,feature3(network_in)已经被判定了对于结果没有重大影响
为什么这两个评估工具给出了不一样的答案呢?
多重共线性
回归模型中两个或多个预测变量(自变量)之间存在高度相关性的情况。针对于我们的这个模型,很容易联想到feature3(network_in)、feature4(network_out),是高度相关性的
VIF(方差膨胀因子)
from statsmodels.stats.outliers_influence import variance_inflation_factor
vif_df = pd.DataFrame()
vif_df['features'] = X.columns
vif_df['VIF'] = [variance_inflation_factor(X.values, i) for i in range(X.shape[1])]
print(vif_df)
VIF > 10,说明存在严重的多重共线性
相关系数矩阵
corr_matrix = df.corr()
print(corr_matrix)
直接丢进gpt
小结
根据上述分析,query可以直接通过cpu特征就可以分析出来了,其他的特征存在与否,其实并不重要
在statsmodels的报告中已经揭示了我们的模型存在严重的共线性:
Cond. No.
这个值大于1000的时候,就表示存在了严重的多重共线性问题
多重共线性带来的问题:
- 回归系数估计变得不稳定,小的数据变化可能导致系数大幅变化
- 系数标准误增大,t统计量减小,导致变量可能显得不显著
- 难以区分单个变量对因变量的独立影响
- 虽然预测可能仍然准确,但解释单个变量的影响变得困难
ridge(岭回归)
数学模型
\[\mathcal{L} = \frac{1}{2n} \sum_{i=1}^{n} (y_i - \hat{y}i)^2 + \lambda \sum{j=1}^{p} β_j^2 \]
实践
from sklearn.linear_model import Ridge
ridge = Ridge(alpha=1.0)
ridge.fit(X_scaled, y)
y_ridge = ridge.predict(X_scaled)
r2_ridge = r2_score(y, y_ridge)
r2_adj_ridge = adjusted_r2(r2_ridge, n, 7)
for i, coef in enumerate(ridge.coef_, 1):
print(f'x{i} 的回归系数:{coef:.4f}')
print("\nridge模型:")
print(f"R² = {r2_ridge:.4f}, Adjusted R² = {r2_adj_ridge:.4f}")
ridge回归中,所有系数都会被压缩向零但不会完全为零,所以不像lasso回归中直接为0
那么直观:
- 绝对值较大的系数通常对应更重要的特征
- 正负号表示特征与目标变量的正/负相关关系
如图所示,X1与X4是更为重要的特征,这与lasso回归得出的结论是一样的
总结
多元回归的复杂性,是由于特征参数过多带来的新的问题,无用特征、多重共线性,要找出特征的权重,要用到一些检验方法,比如t检验、VIF、相关矩阵系数等。如果只想关注模型泛化能力,可以通过lasso、ridge等回归来自动筛选特征
综上所述,本次多元回归之旅最终的结果又回到了一元回归,饶了一大圈又回到了原点,但是并非毫无收获,除了收获了一大堆模型评估方法,什么调整决定系数、t检验、VIF等,还有一堆陌生的检测算法,lasso回归、ridge回归等。并且提供了今后对于多元回归的一些方法论
装杯时刻
那位兄弟问了,你这洋洋洒洒搞了这么半天,怎么装杯呢?
今天这个就不给老板汇报了,毕竟如果只关注结果的话,又回到了一元回归的方法论来,但是依然有价值分享给同事,毕竟这一堆猛烈的方法输出,每月一次的团队培训内部培训,这不是又有题材了不是吗?
联系我
- 联系我,做深入的交流
至此,本文结束
在下才疏学浅,有撒汤漏水的,请各位不吝赐教...