线性模型系数解释中的常见缺陷

在线性模型中,目标值被建模为特征的线性组合(请参阅scikit-learn中“Linear Models 用户指南”一节中对一组可用的线性模型的描述)。多个线性模型中的系数表示给定特征与目标之间的关系,假设所有其他特征保持不变(条件依赖)。这与绘制和拟合线性关系不同:在这种情况下,其他特征的所有可能值都会在估计中得到考虑(边际依赖)。

此示例将为解释线性模型中的系数提供一些提示,指出当线性模型不适合描述数据集或当特征相关时会出现的问题。

我们会利用1985年的“目前人口统计调查”的数据,根据经验、年龄或教育程度等各方面因素来预测工资。

print(__doc__)

import numpy as np
import scipy as sp
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

数据集:工资

我们从OpenML获取数据。注意,将参数 as_frame 设置为True, 将以pandas数据的形式检索数据。

from sklearn.datasets import fetch_openml

survey = fetch_openml(data_id=534, as_frame=True)

然后,我们识别特征X和目标y:列WAGE是我们的目标变量(即我们想要预测的变量)。

X = survey.data[survey.feature_names]
X.describe(include="all")

注意,数据集包含分类变量和数值变量。在以后对数据集进行预处理时,我们需要考虑到这一点。

X.head()

我们的预测目标是:the wage。工资被描述为以每小时美元为单位的浮点数。

y = survey.target.values.ravel()
survey.target.head()
0    5.10
1    4.95
2    6.67
3    4.00
4    7.50
Name: WAGE, dtype: float64

我们将样本分解成一个训练集和一个测试集。只有训练数据集将用于以下探索性分析。这是一种模拟在未知目标上执行预测的真实情况的方法,我们不希望我们的分析和决定因我们对测试数据有所了解而有偏差。

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(
    X, y, random_state=42
)

首先,让我们通过查看变量分布和它们之间的成对关系来获得一些洞察力。只数值变量被使用。在下面的图中,每个点表示一个示例。

train_dataset = X_train.copy()
train_dataset.insert(0"WAGE", y_train)
_ = sns.pairplot(train_dataset, kind='reg', diag_kind='kde')

仔细看一看工资分布,就会发现它是长尾的。因此,我们应该把它的对数近似地转化为正态分布(线性模型,如岭回归或Lasso最适合用于正态分布的误差)。

随着 EDUCATION 的增加,WAGE 在增加。请注意,这里表示的工资和教育之间的依赖是边际依赖,也就是说,它描述了一个特定变量的行为,而没有保持其他变量的固定。

此外,EXPERIENCE 与 AGE 呈高度线性相关。

机器学习工作流

为了设计机器学习管道,我们首先手动检查需要处理的数据类型:

survey.data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex:
 534 entries, 0 to 533
Data columns (total 10 columns):
 #   Column      Non-Null Count  Dtype
---  ------      --------------  -----
 0   EDUCATION   534 non-null    float64
 1   SOUTH       534 non-null    category
 2   SEX         534 non-null    category
 3   EXPERIENCE  534 non-null    float64
 4   UNION       534 non-null    category
 5   AGE         534 non-null    float64
 6   RACE        534 non-null    category
 7   OCCUPATION  534 non-null    category
 8   SECTOR      534 non-null    category
 9   MARR        534 non-null    category
dtypes: category(7), float64(3)
memory usage: 17.1 KB

如前所述,数据集包含具有不同数据类型的列,我们需要对每个数据类型应用特定的预处理。特别是,如果分类变量不首先编码为整数,则不能将分类变量包括在线性模型中。此外,为了避免将分类特征视为有序值,我们需要对它们进行一次独热编码。我们的预处理器:

  • 独热编码(即按类别生成一列)分类列;
  • 作为第一种方法(我们将看到数值的标准化将如何影响我们的讨论),保持数值的原样。
from sklearn.compose import make_column_transformer
from sklearn.preprocessing import OneHotEncoder

categorical_columns = ['RACE''OCCUPATION''SECTOR',
                       'MARR''UNION''SEX''SOUTH']
numerical_columns = ['EDUCATION''EXPERIENCE''AGE']

preprocessor = make_column_transformer(
    (OneHotEncoder(drop='if_binary'), categorical_columns),
    remainder='passthrough'
)

为了将数据集描述为线性模型,我们使用了一个正则化程度很小的岭回归,并对WAGE的对数进行了建模。

from sklearn.pipeline import make_pipeline
from sklearn.linear_model import Ridge
from sklearn.compose import TransformedTargetRegressor

model = make_pipeline(
    preprocessor,
    TransformedTargetRegressor(
        regressor=Ridge(alpha=1e-10),
        func=np.log10,
        inverse_func=sp.special.exp10
    )
)

处理数据集

首先,我们拟合模型。

_ = model.fit(X_train, y_train)

然后,我们检查计算模型的性能,绘制它在测试集上的预测,并计算,例如,模型的中位绝对误差。

from sklearn.metrics import median_absolute_error

y_pred = model.predict(X_train)

mae = median_absolute_error(y_train, y_pred)
string_score = f'MAE on training set: {mae:.2f} $/hour'
y_pred = model.predict(X_test)
mae = median_absolute_error(y_test, y_pred)
string_score += f'\nMAE on testing set: {mae:.2f} $/hour'
fig, ax = plt.subplots(figsize=(55))
plt.scatter(y_test, y_pred)
ax.plot([01], [01], transform=ax.transAxes, ls="--", c="red")
plt.text(320, string_score)
plt.title('Ridge model, small regularization')
plt.ylabel('Model predictions')
plt.xlabel('Truths')
plt.xlim([027])
_ = plt.ylim([027])

学习到的模型远不是一个做出准确预测的好模型:从上面的情节来看,这是显而易见的,在那里,好的预测应该在红线上。

在下一节中,我们将解释模型的系数。当我们这样做的时候,我们应该记住,我们得出的任何结论都是关于我们建立的模型,而不是关于数据的真实(现实世界)生成过程。

解释系数:尺度问题

首先,我们可以查看我们所拟合的回归系数的值。

feature_names = (model.named_steps['columntransformer']
                      .named_transformers_['onehotencoder']
                      .get_feature_names(input_features=categorical_columns))
feature_names = np.concatenate(
    [feature_names, numerical_columns])

coefs = pd.DataFrame(
    model.named_steps['transformedtargetregressor'].regressor_.coef_,
    columns=['Coefficients'], index=feature_names
)

coefs

年龄系数以“每活一年美元/小时”表示,而教育系数则以“每受教育一年美元/小时”表示。这个系数的表示说明了模型的实际预测:1岁的增长意味着每小时减少0.030867美元,而1年的教育增长意味着每小时增加0.054699美元。另一方面,范畴变量(如联合变量或性别变量)是取值0或1的维数,其系数以美元/小时表示。然后,由于特征的度量单位不同,因此不能比较不同系数的大小,因为它们具有不同的自然尺度,因而具有不同的取值范围。如果我们绘制系数,这是更明显的。

coefs.plot(kind='barh', figsize=(97))
plt.title('Ridge model, small regularization')
plt.axvline(x=0, color='.5')
plt.subplots_adjust(left=.3)

事实上,从上面的图来看,决定WAGE的最重要因素似乎是UNION,即使我们的直觉可能告诉我们,像 EXPERIENCE这样的变量应该有更大的影响。

观察系数图来衡量特征的重要性可能会产生误导,因为其中一些变化很小,而另一些,如AGE,变化更大,甚至几十年。

如果我们比较不同特征的标准差,这是可见的。

X_train_preprocessed = pd.DataFrame(
    model.named_steps['columntransformer'].transform(X_train),
    columns=feature_names
)

X_train_preprocessed.std(axis=0).plot(kind='barh', figsize=(97))
plt.title('Features std. dev.')
plt.subplots_adjust(left=.3)

将系数乘以相关特征的标准差,将所有系数减少到相同的度量单位。我们将看到,这相当于将数值变量标准化为其标准差,如:

在这种情况下,我们强调,特征的方差越大,输出的相应系数的权重就越大,所有这些都是相等的。

coefs = pd.DataFrame(
    model.named_steps['transformedtargetregressor'].regressor_.coef_ *
    X_train_preprocessed.std(axis=0),
    columns=['Coefficient importance'], index=feature_names
)
coefs.plot(kind='barh', figsize=(97))
plt.title('Ridge model, small regularization')
plt.axvline(x=0, color='.5')
plt.subplots_adjust(left=.3)

现在系数已经被缩放了,我们可以安全地比较它们。

警告:为什么上面的情节意味着年龄的增长会导致工资的下降?为什么 initial pairplot告诉我们相反的情况?

上面的图告诉我们,当所有其他特性保持不变(即条件依赖关系)时,特定特性与目标之间的依赖关系。即:条件依赖。当所有其他特征不变时,年龄的增加将导致工资的下降。相反,当所有其他特征不变时,经验的增加会导致工资的增加。此外,年龄、经验和教育是对模型影响最大的三个变量。

检验系数的可变性

我们可以通过交叉验证来检验系数的可变性:它是数据扰动的一种形式(与重采样有关)。

如果系数在改变输入数据集时变化很大,则不能保证它们的鲁棒性,因此很可能需要谨慎地解释它们。

from sklearn.model_selection import cross_validate
from sklearn.model_selection import RepeatedKFold

cv_model = cross_validate(
    model, X, y, cv=RepeatedKFold(n_splits=5, n_repeats=5),
    return_estimator=True, n_jobs=-1
)
coefs = pd.DataFrame(
    [est.named_steps['transformedtargetregressor'].regressor_.coef_ *
     X_train_preprocessed.std(axis=0)
     for est in cv_model['estimator']],
    columns=feature_names
)
plt.figure(figsize=(97))
sns.swarmplot(data=coefs, orient='h', color='k', alpha=0.5)
sns.boxplot(data=coefs, orient='h', color='cyan', saturation=0.5)
plt.axvline(x=0, color='.5')
plt.xlabel('Coefficient importance')
plt.title('Coefficient importance and its variability')
plt.subplots_adjust(left=.3)

关联变量问题

AGE和EXPERIENCE 系数受强变异性的影响,这可能是由于这两个特征之间的共线性所致:由于AGE和EXPERIENCE在数据中的差异,它们的影响很难分开。

为了验证这一解释,我们绘制了年龄和经验系数的变异性图。

plt.ylabel('Age coefficient')
plt.xlabel('Experience coefficient')
plt.grid(True)
plt.xlim(-0.40.5)
plt.ylim(-0.40.5)
plt.scatter(coefs["AGE"], coefs["EXPERIENCE"])
_ = plt.title('Co-variations of coefficients for AGE and EXPERIENCE '
              'across folds')

有两个区域:当EXPERIENCE系数为正值时,AGE为负,反之亦然。

为了更进一步,我们删除了这两个特征中的一个,并检查了对模型稳定性的影响。

column_to_drop = ['AGE']

cv_model = cross_validate(
    model, X.drop(columns=column_to_drop), y,
    cv=RepeatedKFold(n_splits=5, n_repeats=5),
    return_estimator=True, n_jobs=-1
)
coefs = pd.DataFrame(
    [est.named_steps['transformedtargetregressor'].regressor_.coef_ *
     X_train_preprocessed.drop(columns=column_to_drop).std(axis=0)
     for est in cv_model['estimator']],
    columns=feature_names[:-1]
)
plt.figure(figsize=(97))
sns.swarmplot(data=coefs, orient='h', color='k', alpha=0.5)
sns.boxplot(data=coefs, orient='h', color='cyan', saturation=0.5)
plt.axvline(x=0, color='.5')
plt.title('Coefficient importance and its variability')
plt.xlabel('Coefficient importance')
plt.subplots_adjust(left=.3)

经验系数的估计现在变小了,对所有在交叉验证过程中训练的模型仍然很重要。

预处理数值变量

如前所述(请参阅“机器学习工作流”),我们也可以选择在训练模型之前缩放数值。这将有助于将类似的数量正则化应用于岭中的所有。为了将均值和尺度变化减去单位方差,对预处理器进行了重新定义。

from sklearn.preprocessing import StandardScaler

preprocessor = make_column_transformer(
    (OneHotEncoder(drop='if_binary'), categorical_columns),
    (StandardScaler(), numerical_columns),
    remainder='passthrough'
)

模型将保持不变。

model = make_pipeline(
    preprocessor,
    TransformedTargetRegressor(
        regressor=Ridge(alpha=1e-10),
        func=np.log10,
        inverse_func=sp.special.exp10
    )
)

_ = model.fit(X_train, y_train)

再次,我们用模型的中位绝对误差和R方系数来检验计算模型的性能。

y_pred = model.predict(X_train)
mae = median_absolute_error(y_train, y_pred)
string_score = f'MAE on training set: {mae:.2f} $/hour'
y_pred = model.predict(X_test)
mae = median_absolute_error(y_test, y_pred)
string_score += f'\nMAE on testing set: {mae:.2f} $/hour'
fig, ax = plt.subplots(figsize=(66))
plt.scatter(y_test, y_pred)
ax.plot([01], [01], transform=ax.transAxes, ls="--", c="red")

plt.text(320, string_score)

plt.title('Ridge model, small regularization, normalized variables')
plt.ylabel('Model predictions')
plt.xlabel('Truths')
plt.xlim([027])
_ = plt.ylim([027])

对于系数分析,这次不需要缩放。

coefs = pd.DataFrame(
    model.named_steps['transformedtargetregressor'].regressor_.coef_,
    columns=['Coefficients'], index=feature_names
)
coefs.plot(kind='barh', figsize=(97))
plt.title('Ridge model, small regularization, normalized variables')
plt.axvline(x=0, color='.5')
plt.subplots_adjust(left=.3)

我们现在检查几个交叉验证的系数。

cv_model = cross_validate(
    model, X, y, cv=RepeatedKFold(n_splits=5, n_repeats=5),
    return_estimator=True, n_jobs=-1
)
coefs = pd.DataFrame(
    [est.named_steps['transformedtargetregressor'].regressor_.coef_
     for est in cv_model['estimator']],
    columns=feature_names
)
plt.figure(figsize=(97))
sns.swarmplot(data=coefs, orient='h', color='k', alpha=0.5)
sns.boxplot(data=coefs, orient='h', color='cyan', saturation=0.5)
plt.axvline(x=0, color='.5')
plt.title('Coefficient variability')
plt.subplots_adjust(left=.3)

结果与非归一化情况十分相似。

正则化线性模型

在机器学习实践中,岭回归更常用于不可忽略的正则化。

上面,我们把这个正规化限制在很小的数量上。正则化改善了问题的条件,减少了估计的方差。RidgeCV应用交叉验证来确定正则化参数(Alpha)的哪个值最适合于预测。

from sklearn.linear_model import RidgeCV

model = make_pipeline(
    preprocessor,
    TransformedTargetRegressor(
        regressor=RidgeCV(alphas=np.logspace(-101021)),
        func=np.log10,
        inverse_func=sp.special.exp10
    )
)

_ = model.fit(X_train, y_train)

首先,我们检查选择了哪个值的α。

model[-1].regressor_.alpha_

然后我们检查预测的质量。

y_pred = model.predict(X_train)
mae = median_absolute_error(y_train, y_pred)
string_score = f'MAE on training set: {mae:.2f} $/hour'
y_pred = model.predict(X_test)
mae = median_absolute_error(y_test, y_pred)
string_score += f'\nMAE on testing set: {mae:.2f} $/hour'

fig, ax = plt.subplots(figsize=(66))
plt.scatter(y_test, y_pred)
ax.plot([01], [01], transform=ax.transAxes, ls="--", c="red")

plt.text(320, string_score)

plt.title('Ridge model, regularization, normalized variables')
plt.ylabel('Model predictions')
plt.xlabel('Truths')
plt.xlim([027])
_ = plt.ylim([027])

正则化模型的数据再现能力与非正则模型相似。

coefs = pd.DataFrame(
    model.named_steps['transformedtargetregressor'].regressor_.coef_,
    columns=['Coefficients'], index=feature_names
)
coefs.plot(kind='barh', figsize=(97))
plt.title('Ridge model, regularization, normalized variables')
plt.axvline(x=0, color='.5')
plt.subplots_adjust(left=.3)

这些系数有显着性差异。年龄和经验系数都是正的,但它们现在对预测的影响较小。

正则化减少了相关变量对模型的影响,因为权重在两个预测变量之间是共享的,因此两者都不会有很强的权重。

另一方面,正则化获得的权重更稳定(参见岭回归和分类用户指南一节)。在交叉验证中,从从数据扰动中获得的图中可以看出这种增加的稳定性。这个图可以和前一个相比较。

cv_model = cross_validate(
    model, X, y, cv=RepeatedKFold(n_splits=5, n_repeats=5),
    return_estimator=True, n_jobs=-1
)
coefs = pd.DataFrame(
    [est.named_steps['transformedtargetregressor'].regressor_.coef_ *
     X_train_preprocessed.std(axis=0)
     for est in cv_model['estimator']],
    columns=feature_names
)

plt.ylabel('Age coefficient')
plt.xlabel('Experience coefficient')
plt.grid(True)
plt.xlim(-0.40.5)
plt.ylim(-0.40.5)
plt.scatter(coefs["AGE"], coefs["EXPERIENCE"])
_ = plt.title('Co-variations of coefficients for AGE and EXPERIENCE '
              'across folds')

稀疏系数的线性模型

在数据集中考虑相关变量的另一种可能性是估计稀疏系数。在某种程度上,当我们在以前的岭估计中删除AGE列时,我们已经手动完成了。

LASSO模型(见Lasso用户指南部分)估计稀疏系数。LassoCV应用交叉验证来确定正则化参数(Alpha)的哪个值最适合于模型估计。

from sklearn.linear_model import LassoCV

model = make_pipeline(
    preprocessor,
    TransformedTargetRegressor(
        regressor=LassoCV(alphas=np.logspace(-101021), max_iter=100000),
        func=np.log10,
        inverse_func=sp.special.exp10
    )
)

_ = model.fit(X_train, y_train)

首先,我们检查了选择了哪个值的α。

model[-1].regressor_.alpha_
0.001

然后我们检查预测的质量。

y_pred = model.predict(X_train)
mae = median_absolute_error(y_train, y_pred)
string_score = f'MAE on training set: {mae:.2f} $/hour'
y_pred = model.predict(X_test)
mae = median_absolute_error(y_test, y_pred)
string_score += f'\nMAE on testing set: {mae:.2f} $/hour'

fig, ax = plt.subplots(figsize=(66))
plt.scatter(y_test, y_pred)
ax.plot([01], [01], transform=ax.transAxes, ls="--", c="red")

plt.text(320, string_score)

plt.title('Lasso model, regularization, normalized variables')
plt.ylabel('Model predictions')
plt.xlabel('Truths')
plt.xlim([027])
_ = plt.ylim([027])

同样,对于我们的数据集,模型也不是很有预见性。

coefs = pd.DataFrame(
    model.named_steps['transformedtargetregressor'].regressor_.coef_,
    columns=['Coefficients'], index=feature_names
)
coefs.plot(kind='barh', figsize=(97))
plt.title('Lasso model, regularization, normalized variables')
plt.axvline(x=0, color='.5')
plt.subplots_adjust(left=.3)

Lasso模型识别了年龄和经验之间的相关性,并为了预测而压制其中一个。

重要的是要记住,被丢弃的系数可能仍然与结果本身有关:模型选择了抑制它们,因为它们在其他特征之上很少或没有额外的信息。

课程学习

  • 要检索特征重要性,系数必须缩放到相同的度量单位。使用特性的标准差对它们进行缩放是一个有用的代理。
  • 多元线性模型中的系数表示给定特征与目标之间的依赖关系,这有赖于其他特征。
  • 相关特征导致线性模型系数不稳定,其影响不能很好地分解。
  • 不同的线性模型对特征相关性的响应不同,各相关系数可能存在显著差异。
  • 通过交叉验证循环对系数进行检测,给出了它们的稳定性概念。

脚本的总运行时间:(0分21.601秒)。

Download Python source code: plot_linear_model_coefficient_interpretation.py

Download Jupyter notebook: plot_linear_model_coefficient_interpretation.ipynb