Hinweis
Zum Ende springen, um den vollständigen Beispielcode herunterzuladen oder dieses Beispiel über JupyterLite oder Binder in Ihrem Browser auszuführen.
Klassen-Likelihood-Ratios zur Messung der Klassifikationsleistung#
Dieses Beispiel demonstriert die Funktion class_likelihood_ratios, die die positiven und negativen Likelihood-Ratios (LR+, LR-) berechnet, um die prädiktive Aussagekraft eines binären Klassifikators zu bewerten. Wie wir sehen werden, sind diese Metriken unabhängig vom Verhältnis zwischen den Klassen im Testdatensatz, was sie sehr nützlich macht, wenn die verfügbaren Daten für eine Studie ein anderes Klassenverhältnis aufweisen als die Zielanwendung.
Ein typischer Anwendungsfall ist eine Fall-Kontroll-Studie in der Medizin, bei der die Klassen nahezu ausgewogen sind, während die Allgemeinbevölkerung eine starke Klassenungleichheit aufweist. In einer solchen Anwendung kann die Prä-Test-Wahrscheinlichkeit, dass eine Person die Zielerkrankung hat, als Prävalenz gewählt werden, d. h. als der Anteil einer bestimmten Population, der von einer medizinischen Erkrankung betroffen ist. Die Post-Test-Wahrscheinlichkeiten stellen dann die Wahrscheinlichkeit dar, dass die Krankheit bei einem positiven Testergebnis tatsächlich vorliegt.
In diesem Beispiel diskutieren wir zunächst den Zusammenhang zwischen Prä-Test- und Post-Test-Odds, der durch die Klassen-Likelihood-Ratios gegeben ist. Anschließend bewerten wir ihr Verhalten in einigen kontrollierten Szenarien. Im letzten Abschnitt plotten wir sie als Funktion der Prävalenz der positiven Klasse.
# Authors: The scikit-learn developers
# SPDX-License-Identifier: BSD-3-Clause
Prä-Test vs. Post-Test-Analyse#
Angenommen, wir haben eine Population von Subjekten mit physiologischen Messungen X, die hoffentlich als indirekte Biomarker für die Krankheit dienen können, und tatsächliche Krankheitsindikatoren y (Ground Truth). Die meisten Menschen in der Population tragen die Krankheit nicht, aber eine Minderheit (in diesem Fall etwa 10%) tut es.
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%
Ein Machine-Learning-Modell wird erstellt, um zu diagnostizieren, ob eine Person mit bestimmten physiologischen Messungen wahrscheinlich an der betreffenden Krankheit leidet. Um das Modell zu bewerten, müssen wir seine Leistung auf einem zurückgehaltenen Testdatensatz bewerten.
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
Dann können wir unser Diagnosemodell anpassen und das positive Likelihood-Ratio berechnen, um den Nutzen dieses Klassifikators als Werkzeug zur Krankheitsdiagnose zu bewerten.
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, replace_undefined_by=1.0)
print(f"LR+: {pos_LR:.3f}")
LR+: 12.617
Da das positive Klassen-Likelihood-Ratio deutlich größer als 1,0 ist, bedeutet dies, dass das auf maschinellem Lernen basierende Diagnosewerkzeug nützlich ist: Die Post-Test-Odds, dass die Krankheit bei einem positiven Testergebnis tatsächlich vorliegt, sind mehr als 12-mal größer als die Prä-Test-Odds.
Kreuzvalidierung von Likelihood-Ratios#
Wir bewerten die Variabilität der Messungen für die Klassen-Likelihood-Ratios in einigen spezifischen Fällen.
import pandas as pd
def scoring(estimator, X, y):
y_pred = estimator.predict(X)
pos_lr, neg_lr = class_likelihood_ratios(y, y_pred, replace_undefined_by=1.0)
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"])
Zuerst validieren wir das Modell LogisticRegression mit Standardhyperparametern, wie im vorherigen Abschnitt verwendet.
from sklearn.model_selection import cross_validate
estimator = LogisticRegression()
extract_score(cross_validate(estimator, X, y, scoring=scoring, cv=10))
Wir bestätigen, dass das Modell nützlich ist: Die Post-Test-Odds sind 12- bis 20-mal größer als die Prä-Test-Odds.
Im Gegensatz dazu betrachten wir ein Dummy-Modell, das zufällige Vorhersagen mit ähnlichen Odds wie die durchschnittliche Krankheitsprävalenz im Trainingsdatensatz ausgibt.
from sklearn.dummy import DummyClassifier
estimator = DummyClassifier(strategy="stratified", random_state=1234)
extract_score(cross_validate(estimator, X, y, scoring=scoring, cv=10))
Hier sind beide Klassen-Likelihood-Ratios mit 1,0 vereinbar, was diesen Klassifikator als diagnostisches Werkzeug zur Verbesserung der Krankheitserkennung nutzlos macht.
Eine weitere Option für das Dummy-Modell ist, immer die häufigste Klasse vorherzusagen, die in diesem Fall "keine Krankheit" ist.
estimator = DummyClassifier(strategy="most_frequent")
extract_score(cross_validate(estimator, X, y, scoring=scoring, cv=10))
/home/circleci/project/sklearn/utils/_param_validation.py:218: UndefinedMetricWarning:
No samples were predicted for the positive class and `positive_likelihood_ratio` is set to `np.nan`. Use the `replace_undefined_by` param to
/home/circleci/project/sklearn/utils/_param_validation.py:218: UndefinedMetricWarning:
No samples were predicted for the positive class and `positive_likelihood_ratio` is set to `np.nan`. Use the `replace_undefined_by` param to
/home/circleci/project/sklearn/utils/_param_validation.py:218: UndefinedMetricWarning:
No samples were predicted for the positive class and `positive_likelihood_ratio` is set to `np.nan`. Use the `replace_undefined_by` param to
/home/circleci/project/sklearn/utils/_param_validation.py:218: UndefinedMetricWarning:
No samples were predicted for the positive class and `positive_likelihood_ratio` is set to `np.nan`. Use the `replace_undefined_by` param to
/home/circleci/project/sklearn/utils/_param_validation.py:218: UndefinedMetricWarning:
No samples were predicted for the positive class and `positive_likelihood_ratio` is set to `np.nan`. Use the `replace_undefined_by` param to
/home/circleci/project/sklearn/utils/_param_validation.py:218: UndefinedMetricWarning:
No samples were predicted for the positive class and `positive_likelihood_ratio` is set to `np.nan`. Use the `replace_undefined_by` param to
/home/circleci/project/sklearn/utils/_param_validation.py:218: UndefinedMetricWarning:
No samples were predicted for the positive class and `positive_likelihood_ratio` is set to `np.nan`. Use the `replace_undefined_by` param to
/home/circleci/project/sklearn/utils/_param_validation.py:218: UndefinedMetricWarning:
No samples were predicted for the positive class and `positive_likelihood_ratio` is set to `np.nan`. Use the `replace_undefined_by` param to
/home/circleci/project/sklearn/utils/_param_validation.py:218: UndefinedMetricWarning:
No samples were predicted for the positive class and `positive_likelihood_ratio` is set to `np.nan`. Use the `replace_undefined_by` param to
/home/circleci/project/sklearn/utils/_param_validation.py:218: UndefinedMetricWarning:
No samples were predicted for the positive class and `positive_likelihood_ratio` is set to `np.nan`. Use the `replace_undefined_by` param to
Das Fehlen positiver Vorhersagen bedeutet, dass es keine wahren Positiven und keine falschen Positiven geben wird, was zu einem undefinierten LR+ führt, das keinesfalls als unendliches LR+ interpretiert werden sollte (der Klassifikator identifiziert positive Fälle perfekt). In einer solchen Situation gibt die Funktion class_likelihood_ratios standardmäßig nan zurück und löst eine Warnung aus. Tatsächlich hilft uns der Wert von LR-, dieses Modell zu verwerfen.
Ein ähnliches Szenario kann bei der Kreuzvalidierung von stark unausgeglichenen Daten mit wenigen Stichproben auftreten: Einige Faltungen enthalten keine Stichproben mit der Krankheit und geben daher keine wahren Positiven oder falschen Negativen aus, wenn sie zum Testen verwendet werden. Mathematisch führt dies zu einem unendlichen LR+, das ebenfalls nicht als perfekte Identifizierung positiver Fälle durch das Modell interpretiert werden sollte. Ein solches Ereignis führt zu einer höheren Varianz der geschätzten Likelihood-Ratios, kann aber dennoch als Erhöhung der Post-Test-Odds auf die Erkrankung interpretiert werden.
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))
/home/circleci/project/sklearn/utils/_param_validation.py:218: UndefinedMetricWarning:
`positive_likelihood_ratio` is ill-defined and set to `np.nan`. Use the `replace_undefined_by` param to
/home/circleci/project/sklearn/utils/_param_validation.py:218: UndefinedMetricWarning:
`positive_likelihood_ratio` is ill-defined and set to `np.nan`. Use the `replace_undefined_by` param to
/home/circleci/project/sklearn/utils/_param_validation.py:218: UndefinedMetricWarning:
`positive_likelihood_ratio` is ill-defined and set to `np.nan`. Use the `replace_undefined_by` param to
/home/circleci/project/sklearn/utils/_param_validation.py:218: UndefinedMetricWarning:
`positive_likelihood_ratio` is ill-defined and set to `np.nan`. Use the `replace_undefined_by` param to
/home/circleci/project/sklearn/utils/_param_validation.py:218: UndefinedMetricWarning:
`positive_likelihood_ratio` is ill-defined and set to `np.nan`. Use the `replace_undefined_by` param to
Invarianz gegenüber der Prävalenz#
Die Likelihood-Ratios sind unabhängig von der Krankheitsprävalenz und können zwischen Populationen unabhängig von jeglicher möglicher Klassenungleichheit extrapoliert werden, **vorausgesetzt, dass dasselbe Modell auf alle angewendet wird**. Beachten Sie, dass in den folgenden Diagrammen **die Entscheidungsgrenze konstant ist** (siehe SVM: Trennende Hyperebene für unausgeglichene Klassen für eine Untersuchung der Entscheidungsgrenze für unausgeglichene Klassen).
Hier trainieren wir ein LogisticRegression-Basismodell auf einer Fall-Kontroll-Studie mit einer Prävalenz von 50 %. Es wird dann über Populationen mit unterschiedlicher Prävalenz ausgewertet. Wir verwenden die Funktion make_classification, um sicherzustellen, dass der datengenerierende Prozess immer derselbe ist, wie in den folgenden Diagrammen gezeigt. Das Label 1 entspricht der positiven Klasse "Krankheit", während das Label 0 für "keine Krankheit" steht.
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
Wir werden nun die Entscheidungsgrenze für jedes Prävalenzniveau zeigen. Beachten Sie, dass wir nur eine Teilmenge der Originaldaten plotten, um die Entscheidungsgrenze des linearen Modells besser einschätzen zu können.
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())

Wir definieren eine Funktion für 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)
Wir bewerten das Basismodell für jede Prävalenz mithilfe von 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
In den folgenden Diagrammen beobachten wir, dass die Klassen-Likelihood-Ratios, die mit verschiedenen Prävalenzen neu berechnet werden, innerhalb einer Standardabweichung der mit ausgewogenen Klassen berechneten Werte konstant sind.
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()

Gesamtlaufzeit des Skripts: (0 Minuten 1,750 Sekunden)
Verwandte Beispiele
Post-hoc-Anpassung des Cut-off-Punkts der Entscheidungskfunktion