注意
转到结尾 下载完整的示例代码。或通过 JupyterLite 或 Binder 在浏览器中运行此示例
类似然比用于衡量分类性能#
此示例演示了 class_likelihood_ratios
函数,该函数计算正似然比和负似然比 (LR+
,LR-
) 以评估二元分类器的预测能力。我们将看到,这些指标与测试集中类别之间的比例无关,这使得它们在研究的可用数据与目标应用程序具有不同类别比例时非常有用。
一个典型的应用是医学中的病例对照研究,它具有几乎平衡的类别,而普通人群则存在较大的类别不平衡。在这种应用中,个体患有目标疾病的测试前概率可以选择为患病率,即在特定人群中发现受某种医疗状况影响的比例。然后,测试后概率表示在获得阳性测试结果的情况下,疾病真正存在的概率。
在此示例中,我们首先讨论由 类似然比 给出的测试前和测试后优势之间的联系。然后,我们在一些受控场景中评估它们的行为。在最后一节中,我们将它们绘制为阳性类别患病率的函数。
# Authors: The scikit-learn developers
# SPDX-License-Identifier: BSD-3-Clause
测试前与测试后分析#
假设我们有一组受试者,他们具有生理测量值 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())
我们定义一个用于 bootstrapping 的函数。
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)
我们使用 bootstrapping 对每个患病率下的基础模型进行评分。
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.682 秒)
相关示例