类似然比用于衡量分类性能#
本示例演示了 class_likelihood_ratios
函数,该函数计算正似然比和负似然比 (LR+
, LR-
) 以评估二元分类器的预测能力。正如我们将看到的,这些指标独立于测试集中类别的比例,这使得它们在可用数据的研究类别比例与目标应用不同时非常有用。
一个典型的用例是医学中的病例对照研究,它具有近乎平衡的类别,而一般人群具有较大的类别不平衡。在这种应用中,个体患有目标疾病的先验概率可以被选择为患病率,即在特定人群中发现患有某种疾病的比例。然后,后验概率表示在获得阳性测试结果的情况下,该疾病真正存在的概率。
在本示例中,我们首先讨论由 类似然比 给出的先验概率与后验概率之间的联系。然后,我们在一些受控场景中评估它们的行为。在最后一部分,我们将它们绘制为正类患病率的函数。
# Authors: Arturo Amor <[email protected]>
# Olivier Grisel <[email protected]>
先验分析与后验分析#
假设我们有一群受试者,他们具有生理测量值 X
,这些测量值可以作为疾病的间接生物标志物,以及实际的疾病指标 y
(真实情况)。人群中大多数人没有患病,但少数人(在本例中约为 10%)患病。
from sklearn.datasets import make_classification
X, y = make_classification(n_samples=10_000, weights=[0.9, 0.1], random_state=0)
print(f"Percentage of people carrying the disease: {100*y.mean():.2f}%")
Percentage of people carrying the disease: 10.37%
建立一个机器学习模型来诊断具有某些生理测量值的人是否可能患有目标疾病。为了评估模型,我们需要评估它在保留的测试集上的性能。
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
然后,我们可以拟合我们的诊断模型并计算正似然比来评估此分类器作为疾病诊断工具的有用性。
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import class_likelihood_ratios
estimator = LogisticRegression().fit(X_train, y_train)
y_pred = estimator.predict(X_test)
pos_LR, neg_LR = class_likelihood_ratios(y_test, y_pred)
print(f"LR+: {pos_LR:.3f}")
LR+: 12.617
由于正类似然比远大于 1.0,这意味着基于机器学习的诊断工具是有用的:在获得阳性测试结果的情况下,该疾病真正存在的后验概率比先验概率大 12 倍以上。
似然比的交叉验证#
我们在一些特定情况下评估类似然比测量的可变性。
import pandas as pd
def scoring(estimator, X, y):
y_pred = estimator.predict(X)
pos_lr, neg_lr = class_likelihood_ratios(y, y_pred, raise_warning=False)
return {"positive_likelihood_ratio": pos_lr, "negative_likelihood_ratio": neg_lr}
def extract_score(cv_results):
lr = pd.DataFrame(
{
"positive": cv_results["test_positive_likelihood_ratio"],
"negative": cv_results["test_negative_likelihood_ratio"],
}
)
return lr.aggregate(["mean", "std"])
我们首先使用上一节中使用的默认超参数验证 LogisticRegression
模型。
from sklearn.model_selection import cross_validate
estimator = LogisticRegression()
extract_score(cross_validate(estimator, X, y, scoring=scoring, cv=10))
我们确认该模型是有用的:后验概率比先验概率大 12 到 20 倍。
相反,让我们考虑一个虚拟模型,它将输出与训练集中平均疾病患病率类似的随机预测。
from sklearn.dummy import DummyClassifier
estimator = DummyClassifier(strategy="stratified", random_state=1234)
extract_score(cross_validate(estimator, X, y, scoring=scoring, cv=10))
这里,两个类似然比都与 1.0 兼容,这使得此分类器作为诊断工具来改善疾病检测毫无用处。
虚拟模型的另一个选择是始终预测最频繁的类别,在本例中为“无疾病”。
estimator = DummyClassifier(strategy="most_frequent")
extract_score(cross_validate(estimator, X, y, scoring=scoring, cv=10))
没有阳性预测意味着没有真阳性或假阳性,导致 LR+
未定义,绝不应将其解释为无限的 LR+
(分类器完美地识别阳性案例)。在这种情况下,class_likelihood_ratios
函数返回 nan
,并默认情况下发出警告。实际上,LR-
的值可以帮助我们丢弃此模型。
类似的情况可能出现在对高度不平衡的数据进行交叉验证时,样本很少:一些折叠将没有患有疾病的样本,因此在用于测试时,它们将不会输出任何真阳性或假阴性。从数学上讲,这会导致无限的 LR+
,这也绝不应将其解释为模型完美地识别阳性案例。此类事件会导致估计的似然比方差更大,但仍然可以解释为患有该疾病的后验概率的增加。
estimator = LogisticRegression()
X, y = make_classification(n_samples=300, weights=[0.9, 0.1], random_state=0)
extract_score(cross_validate(estimator, X, y, scoring=scoring, cv=10))
对患病率的不变性#
似然比独立于疾病患病率,可以在人群之间进行推断,无论是否存在任何可能的类别不平衡,**只要对所有人群应用相同的模型**。请注意,在下图中,**决策边界是恒定的**(有关不平衡类别的边界决策研究,请参阅 SVM:不平衡类别的分离超平面)。
在这里,我们在患病率为 50% 的病例对照研究中训练一个 LogisticRegression
基础模型。然后,它在患病率不同的群体中进行评估。我们使用 make_classification
函数来确保数据生成过程始终与下图所示相同。标签 1
对应于正类“疾病”,而标签 0
代表“无疾病”。
from collections import defaultdict
import matplotlib.pyplot as plt
import numpy as np
from sklearn.inspection import DecisionBoundaryDisplay
populations = defaultdict(list)
common_params = {
"n_samples": 10_000,
"n_features": 2,
"n_informative": 2,
"n_redundant": 0,
"random_state": 0,
}
weights = np.linspace(0.1, 0.8, 6)
weights = weights[::-1]
# fit and evaluate base model on balanced classes
X, y = make_classification(**common_params, weights=[0.5, 0.5])
estimator = LogisticRegression().fit(X, y)
lr_base = extract_score(cross_validate(estimator, X, y, scoring=scoring, cv=10))
pos_lr_base, pos_lr_base_std = lr_base["positive"].values
neg_lr_base, neg_lr_base_std = lr_base["negative"].values
现在,我们将显示每个患病率水平的决策边界。请注意,我们只绘制了原始数据的一个子集,以便更好地评估线性模型决策边界。
fig, axs = plt.subplots(nrows=3, ncols=2, figsize=(15, 12))
for ax, (n, weight) in zip(axs.ravel(), enumerate(weights)):
X, y = make_classification(
**common_params,
weights=[weight, 1 - weight],
)
prevalence = y.mean()
populations["prevalence"].append(prevalence)
populations["X"].append(X)
populations["y"].append(y)
# down-sample for plotting
rng = np.random.RandomState(1)
plot_indices = rng.choice(np.arange(X.shape[0]), size=500, replace=True)
X_plot, y_plot = X[plot_indices], y[plot_indices]
# plot fixed decision boundary of base model with varying prevalence
disp = DecisionBoundaryDisplay.from_estimator(
estimator,
X_plot,
response_method="predict",
alpha=0.5,
ax=ax,
)
scatter = disp.ax_.scatter(X_plot[:, 0], X_plot[:, 1], c=y_plot, edgecolor="k")
disp.ax_.set_title(f"prevalence = {y_plot.mean():.2f}")
disp.ax_.legend(*scatter.legend_elements())
我们定义一个用于自举的函数。
def scoring_on_bootstrap(estimator, X, y, rng, n_bootstrap=100):
results_for_prevalence = defaultdict(list)
for _ in range(n_bootstrap):
bootstrap_indices = rng.choice(
np.arange(X.shape[0]), size=X.shape[0], replace=True
)
for key, value in scoring(
estimator, X[bootstrap_indices], y[bootstrap_indices]
).items():
results_for_prevalence[key].append(value)
return pd.DataFrame(results_for_prevalence)
我们使用自举方法对每个患病率的基础模型进行评分。
results = defaultdict(list)
n_bootstrap = 100
rng = np.random.default_rng(seed=0)
for prevalence, X, y in zip(
populations["prevalence"], populations["X"], populations["y"]
):
results_for_prevalence = scoring_on_bootstrap(
estimator, X, y, rng, n_bootstrap=n_bootstrap
)
results["prevalence"].append(prevalence)
results["metrics"].append(
results_for_prevalence.aggregate(["mean", "std"]).unstack()
)
results = pd.DataFrame(results["metrics"], index=results["prevalence"])
results.index.name = "prevalence"
results
在下图中,我们观察到用不同流行率重新计算的类似然比确实在与平衡类计算的类似然比的一个标准差内保持恒定。
fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(15, 6))
results["positive_likelihood_ratio"]["mean"].plot(
ax=ax1, color="r", label="extrapolation through populations"
)
ax1.axhline(y=pos_lr_base + pos_lr_base_std, color="r", linestyle="--")
ax1.axhline(
y=pos_lr_base - pos_lr_base_std,
color="r",
linestyle="--",
label="base model confidence band",
)
ax1.fill_between(
results.index,
results["positive_likelihood_ratio"]["mean"]
- results["positive_likelihood_ratio"]["std"],
results["positive_likelihood_ratio"]["mean"]
+ results["positive_likelihood_ratio"]["std"],
color="r",
alpha=0.3,
)
ax1.set(
title="Positive likelihood ratio",
ylabel="LR+",
ylim=[0, 5],
)
ax1.legend(loc="lower right")
ax2 = results["negative_likelihood_ratio"]["mean"].plot(
ax=ax2, color="b", label="extrapolation through populations"
)
ax2.axhline(y=neg_lr_base + neg_lr_base_std, color="b", linestyle="--")
ax2.axhline(
y=neg_lr_base - neg_lr_base_std,
color="b",
linestyle="--",
label="base model confidence band",
)
ax2.fill_between(
results.index,
results["negative_likelihood_ratio"]["mean"]
- results["negative_likelihood_ratio"]["std"],
results["negative_likelihood_ratio"]["mean"]
+ results["negative_likelihood_ratio"]["std"],
color="b",
alpha=0.3,
)
ax2.set(
title="Negative likelihood ratio",
ylabel="LR-",
ylim=[0, 0.5],
)
ax2.legend(loc="lower right")
plt.show()
脚本总运行时间:(0 分钟 2.492 秒)
相关示例