Graham Clenaghan#

Этот пример демонстрирует 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, replace_undefined_by=1.0)
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, 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"])

Сначала мы проверяем LogisticRegression модель со значениями гиперпараметров по умолчанию, как в предыдущем разделе.

from sklearn.model_selection import cross_validate

estimator = LogisticRegression()
extract_score(cross_validate(estimator, X, y, scoring=scoring, cv=10))
положительный отрицательный
mean 16.661086 0.724702
std 4.383973 0.054045


Мы подтверждаем, что модель полезна: апостериорные шансы в 12-20 раз выше априорных шансов.

Напротив, рассмотрим фиктивный (dummy) модель, которая будет выдавать случайные предсказания с вероятностями, аналогичными средней распространённости заболевания в обучающей выборке:

from sklearn.dummy import DummyClassifier

estimator = DummyClassifier(strategy="stratified", random_state=1234)
extract_score(cross_validate(estimator, X, y, scoring=scoring, cv=10))
положительный отрицательный
mean 1.108843 0.986989
std 0.268147 0.034278


Здесь оба отношения правдоподобия классов совместимы с 1.0, что делает этот классификатор бесполезным в качестве диагностического инструмента для улучшения обнаружения заболеваний.

Другой вариант для фиктивной модели — всегда предсказывать наиболее частый класс, который в данном случае является "без болезни".

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
положительный отрицательный
mean 1.0 1.0
std 0.0 0.0


Отсутствие положительных прогнозов означает, что не будет ни истинно положительных, ни ложноположительных результатов, что приводит к неопределенному 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))
/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
положительный отрицательный
mean 9.400000 0.373333
std 10.532489 0.235430


является пользовательской функцией.#

Отношения правдоподобия не зависят от распространенности заболевания и могут быть экстраполированы между популяциями независимо от любого возможного дисбаланса классов, при условии, что одна и та же модель применяется ко всем из них. Обратите внимание, что на графиках ниже граница принятия решений постоянна (см. SVM: Разделяющая гиперплоскость для несбалансированных классов для исследования границы решения для несбалансированных классов).

Здесь мы обучаем LogisticRegression базовая модель на основе исследования случай-контроль с распространенностью 50%. Затем она оценивается на популяциях с различной распространенностью. Мы используем 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())
prevalence = 0.22, prevalence = 0.34, prevalence = 0.45, prevalence = 0.60, prevalence = 0.76, prevalence = 0.88

Мы определяем функцию для бутстреппинга.

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
положительное отношение правдоподобия negative_likelihood_ratio
mean std mean std
распространенность
0.2039 4.507943 0.113516 0.207667 0.009778
0.3419 4.443238 0.125140 0.198766 0.008915
0.4809 4.421087 0.123828 0.192913 0.006360
0.6196 4.409717 0.164009 0.193949 0.005861
0.7578 4.334795 0.175298 0.189267 0.005840
0.8963 4.197666 0.238955 0.185654 0.005027


На графиках ниже мы видим, что отношения правдоподобия классов, пересчитанные с разными распространённостями, действительно постоянны в пределах одного стандартного отклонения от значений, вычисленных на сбалансированных классах.

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()
Positive likelihood ratio, Negative likelihood ratio

Общее время выполнения скрипта: (0 минут 1.721 секунд)

Связанные примеры

SVM: Взвешенные образцы

SVM: Взвешенные образцы

Пост-фактумная настройка точки отсечения функции принятия решений

Пост-фактумная настройка точки отсечения функции принятия решений

Важность масштабирования признаков

Важность масштабирования признаков

Построение перекрестно проверенных предсказаний

Построение перекрестно проверенных предсказаний

Галерея, созданная Sphinx-Gallery