Scikit-learn: Стратифицированная группаKFold

Созданный на 11 апр. 2019  ·  48Комментарии  ·  Источник: scikit-learn/scikit-learn

Описание

В настоящее время sklearn не имеет функции стратифицированной группы kfold. Либо мы можем использовать стратификацию, либо мы можем использовать группу kfold. Однако было бы неплохо иметь и то, и другое.

Я хотел бы реализовать его, если мы решим его иметь.

Самый полезный комментарий

Было бы хорошо, если бы заинтересованные люди могли описать свой вариант использования и то, что они действительно хотят от этого.

Очень распространенный случай использования в медицине и биологии, когда у вас есть повторные измерения.
Пример: предположим, что вы хотите классифицировать заболевание, например, болезнь Альцгеймера (БА) по сравнению со здоровым контролем по изображениям МРТ. Для одного и того же субъекта у вас может быть несколько сканирований (из последующих сеансов или продольных данных). Предположим, у вас есть в общей сложности 1000 субъектов, у 200 из них диагностирована БА (несбалансированные классы). У большинства субъектов есть один скан, но для некоторых доступны 2 или 3 изображения. При обучении/тестировании классификатора вы хотите убедиться, что изображения одного и того же объекта всегда находятся в одном и том же месте, чтобы избежать утечки данных.
Для этого лучше всего использовать StratifiedGroupKFold: стратифицировать для учета дисбаланса классов, но с групповым ограничением, согласно которому субъект не должен появляться в разных сгибах.
NB: Было бы неплохо сделать это повторяемым.

Ниже пример реализации, вдохновленный kaggle-kernel .

import numpy as np
from collections import Counter, defaultdict
from sklearn.utils import check_random_state

class RepeatedStratifiedGroupKFold():

    def __init__(self, n_splits=5, n_repeats=1, random_state=None):
        self.n_splits = n_splits
        self.n_repeats = n_repeats
        self.random_state = random_state

    # Implementation based on this kaggle kernel:
    #    https://www.kaggle.com/jakubwasikowski/stratified-group-k-fold-cross-validation
    def split(self, X, y=None, groups=None):
        k = self.n_splits
        def eval_y_counts_per_fold(y_counts, fold):
            y_counts_per_fold[fold] += y_counts
            std_per_label = []
            for label in range(labels_num):
                label_std = np.std(
                    [y_counts_per_fold[i][label] / y_distr[label] for i in range(k)]
                )
                std_per_label.append(label_std)
            y_counts_per_fold[fold] -= y_counts
            return np.mean(std_per_label)

        rnd = check_random_state(self.random_state)
        for repeat in range(self.n_repeats):
            labels_num = np.max(y) + 1
            y_counts_per_group = defaultdict(lambda: np.zeros(labels_num))
            y_distr = Counter()
            for label, g in zip(y, groups):
                y_counts_per_group[g][label] += 1
                y_distr[label] += 1

            y_counts_per_fold = defaultdict(lambda: np.zeros(labels_num))
            groups_per_fold = defaultdict(set)

            groups_and_y_counts = list(y_counts_per_group.items())
            rnd.shuffle(groups_and_y_counts)

            for g, y_counts in sorted(groups_and_y_counts, key=lambda x: -np.std(x[1])):
                best_fold = None
                min_eval = None
                for i in range(k):
                    fold_eval = eval_y_counts_per_fold(y_counts, i)
                    if min_eval is None or fold_eval < min_eval:
                        min_eval = fold_eval
                        best_fold = i
                y_counts_per_fold[best_fold] += y_counts
                groups_per_fold[best_fold].add(g)

            all_groups = set(groups)
            for i in range(k):
                train_groups = all_groups - groups_per_fold[i]
                test_groups = groups_per_fold[i]

                train_indices = [i for i, g in enumerate(groups) if g in train_groups]
                test_indices = [i for i, g in enumerate(groups) if g in test_groups]

                yield train_indices, test_indices

Сравнение RepeatedStratifiedKFold (образец одной и той же группы может появиться в обеих сгибах) с RepeatedStratifiedGroupKFold :

import matplotlib.pyplot as plt
from sklearn import model_selection

def plot_cv_indices(cv, X, y, group, ax, n_splits, lw=10):
    for ii, (tr, tt) in enumerate(cv.split(X=X, y=y, groups=group)):
        indices = np.array([np.nan] * len(X))
        indices[tt] = 1
        indices[tr] = 0

        ax.scatter(range(len(indices)), [ii + .5] * len(indices),
                   c=indices, marker='_', lw=lw, cmap=plt.cm.coolwarm,
                   vmin=-.2, vmax=1.2)

    ax.scatter(range(len(X)), [ii + 1.5] * len(X), c=y, marker='_',
               lw=lw, cmap=plt.cm.Paired)
    ax.scatter(range(len(X)), [ii + 2.5] * len(X), c=group, marker='_',
               lw=lw, cmap=plt.cm.tab20c)

    yticklabels = list(range(n_splits)) + ['class', 'group']
    ax.set(yticks=np.arange(n_splits+2) + .5, yticklabels=yticklabels,
           xlabel='Sample index', ylabel="CV iteration",
           ylim=[n_splits+2.2, -.2], xlim=[0, 100])
    ax.set_title('{}'.format(type(cv).__name__), fontsize=15)


# demonstration
np.random.seed(1338)
n_splits = 4
n_repeats=5


# Generate the class/group data
n_points = 100
X = np.random.randn(100, 10)

percentiles_classes = [.4, .6]
y = np.hstack([[ii] * int(100 * perc) for ii, perc in enumerate(percentiles_classes)])

# Evenly spaced groups
g = np.hstack([[ii] * 5 for ii in range(20)])


fig, ax = plt.subplots(1,2, figsize=(14,4))

cv_nogrp = model_selection.RepeatedStratifiedKFold(n_splits=n_splits,
                                                   n_repeats=n_repeats,
                                                   random_state=1338)
cv_grp = RepeatedStratifiedGroupKFold(n_splits=n_splits,
                                      n_repeats=n_repeats,
                                      random_state=1338)

plot_cv_indices(cv_nogrp, X, y, g, ax[0], n_splits * n_repeats)
plot_cv_indices(cv_grp, X, y, g, ax[1], n_splits * n_repeats)

plt.show()

RepeatedStratifiedGroupKFold_demo

Все 48 Комментарий

@TomDLT @NicolasHug Что ты думаешь?

Теоретически может быть интересно, но не знаю, насколько это будет полезно на практике. Мы, безусловно, можем оставить проблему открытой и посмотреть, сколько людей запрашивают эту функцию.

Вы предполагаете, что каждая группа относится к одному классу?

См. также #9413

@jnothman Да, я имел в виду нечто подобное. Однако я вижу, что запрос на вытягивание все еще открыт. Я имел в виду, что группа не будет повторяться по сгибам. Если у нас есть идентификатор в виде групп, то один и тот же идентификатор не будет встречаться в нескольких сгибах.

Я понимаю, что это имеет отношение к использованию RFECV.
В настоящее время по умолчанию используется резюме StratifiedKFold. Его fit() также принимает groups=
Однако: похоже, что группы не учитываются при выполнении fit(). Нет предупреждения (может считаться ошибкой).

Группировка И стратификация полезны для довольно несбалансированных наборов данных с зависимостью между записями.
(в моем случае один и тот же человек имеет несколько записей, но по-прежнему существует большое количество групп = людей по отношению к количеству расщеплений; я полагаю, что возникнут практические проблемы, поскольку количество уникальных групп в классе меньшинства приближается количество расколов).

Итак: +1!

Это определенно было бы полезно. Например, работая с сильно несбалансированными медицинскими данными временных рядов, разделяя пациентов, но (приблизительно) уравновешивая несбалансированный класс в каждой сгибе.

Я также обнаружил, что StratifiedKFold принимает группы в качестве параметра, но не группирует в соответствии с ними, и, вероятно, его следует пометить.

Еще одним хорошим применением этой функции могут быть финансовые данные, которые обычно очень несбалансированы. В моем случае у меня есть сильно несбалансированный набор данных с несколькими записями для одного и того же объекта (только в разные моменты времени). Мы хотим сделать GroupKFold , чтобы избежать утечки, но также и стратифицировать, поскольку из-за высокого дисбаланса мы можем получить группы с очень небольшим количеством положительных результатов или без них.

также см. # 14524 я думаю?

Другой вариант использования Stratified GroupShuffleSplit и GroupKFold — биологические схемы «повторяющихся измерений», когда у вас есть несколько образцов на субъекта или другую родительскую биологическую единицу. Также во многих реальных наборах данных по биологии существует дисбаланс классов. Каждая группа образцов имеет один и тот же класс. Поэтому важно стратифицировать и держать группы вместе.

Описание

В настоящее время sklearn не имеет функции стратифицированной группы kfold. Либо мы можем использовать стратификацию, либо мы можем использовать группу kfold. Однако было бы неплохо иметь и то, и другое.

Я хотел бы реализовать его, если мы решим его иметь.

Привет, я думаю, что это было бы весьма полезно для медицины ML. Он реализован уже?

@amueller Как вы думаете, мы должны реализовать это, учитывая, что люди заинтересованы в этом?

Мне это тоже очень интересно... было бы очень полезно в спектроскопии, когда у вас есть несколько повторных измерений для каждого из ваших образцов, они действительно должны оставаться в одной и той же складке во время перекрестной проверки. И если у вас есть несколько несбалансированных классов, которые вы пытаетесь классифицировать, вы действительно хотите использовать функцию стратификации. Поэтому я тоже голосую за! Извините, я недостаточно хорош, чтобы участвовать в разработке, но для тех, кто примет участие в этом, вы можете быть уверены, что это будет использовано :-)
палец вверх для всей команды. Благодарность!

Пожалуйста, просмотрите упомянутые проблемы и PR в этой ветке, так как по крайней мере была предпринята попытка работы над StratifiedGroupKFold . Я уже сделал StratifiedGroupShuffleSplit #15239, который просто нуждается в тестах, но я уже довольно много использовал для своей работы.

Я думаю, что мы должны реализовать это, но я думаю, что до сих пор не знаю, чего мы на самом деле хотим. @hermidalc имеет ограничение, согласно которому члены одной и той же группы должны принадлежать к одному классу. Это не общий случай, верно?

Было бы хорошо, если бы заинтересованные люди могли описать свой вариант использования и то, что они действительно хотят от этого.

Есть # 15239 # 14524 и # 9413, которые, насколько я помню, имеют разную семантику.

@amueller полностью согласен с вами, сегодня я провел несколько часов в поисках чего-то между различными доступными версиями (# 15239, # 14524 и # 9413), но не мог понять, подойдет ли какая-либо из них мне. Итак, вот мой вариант использования, если это может помочь:
У меня есть 1000 образцов. каждый образец был измерен 3 раза с помощью NIR-спектрометра, поэтому каждый образец имеет 3 повторения, которые я хочу сохранить вместе на протяжении всего пути...
Эти 1000 сэмплов принадлежат к 6 разным классам с очень разным количеством сэмплов в каждом:
класс 1: 400 образцов
класс 2: 300 образцов
класс 3: 100 образцов
класс 4: 100 образцов
класс 5: 70 образцов
класс 6: 30 образцов
Я хочу создать классификатор для каждого класса. Итак, класс 1 против всех остальных классов, затем класс 2 против всех остальных классов и т. д.
Чтобы максимизировать точность каждого из моих классификаторов, важно, чтобы у меня были образцы 6 классов, представленных в каждой из сгибов, потому что мои классы не так уж отличаются, поэтому действительно помогает создать точную границу, чтобы всегда были представлены 6 классов. в каждой складке.

Вот почему я считаю, что стратифицированная (всегда мои 6 классов представлены в каждой сгибе) группа (всегда сохраняйте 3 повторяющихся измерения каждого из моих образцов вместе) kfold, кажется, именно то, что я ищу здесь.
Есть мнение?

Мой вариант использования и причина, по которой я написал StratifiedGroupShuffleSplit , заключается в поддержке проектов повторных измерений https://en.wikipedia.org/wiki/Repeated_measures_design. В моих случаях использования члены одной и той же группы должны принадлежать к одному классу.

@fcoppey Для вас образцы в группе всегда имеют один и тот же класс, верно?

@hermidalc Я не очень хорошо знаком с терминологией, но из Википедии это звучит так, как будто «дизайн с повторными измерениями» не означает, что одна и та же группа должна быть в одном классе, как говорится: «Перекрестное испытание имеет дизайн с повторными измерениями, в котором каждому пациенту назначается последовательность из двух или более процедур, одна из которых может быть стандартной терапией или плацебо».
Связывая это с настройкой ML, вы можете либо попытаться предсказать на основе измерений, получил ли человек только что лечение или плацебо, либо вы можете попытаться предсказать результат с учетом лечения.
Для любого из них класс одного и того же человека может измениться, верно?

Независимо от названия, мне кажется, что у вас обоих один и тот же вариант использования, а я думал о случае, похожем на то, что описано в перекрестном исследовании. Или, может быть, немного проще: вы можете сделать так, чтобы пациент со временем заболел (или выздоровел), поэтому результат для пациента мог измениться.

На самом деле в статье в Википедии, на которую вы ссылаетесь, прямо говорится: «Продольный анализ - схемы повторяющихся измерений позволяют исследователям отслеживать, как участники меняются с течением времени, как в долгосрочных, так и в краткосрочных ситуациях», поэтому я думаю, что это означает, что изменение класса включено.
Если есть другое слово, означающее, что измерение проводится в тех же условиях, тогда мы могли бы использовать это слово?

@amueller да, вы правы, я понял, что неправильно написал выше, где я хотел сказать, что в моих вариантах использования этого дизайна, а не в этом случае использования в целом.

Может быть много довольно сложных типов планов повторных измерений, хотя в двух типах, которые мне нужны, StratifiedGroupShuffleSplit , внутри группы сохраняется ограничение одного и того же класса (продольная выборка до и после лечения при прогнозировании ответа на лечение, многократное предварительное лечение). образцы от субъекта в разных местах тела при прогнозировании ответа на лечение).

Мне нужно было что-то сразу, что работает, поэтому я хотел выложить это для других, чтобы использовать и начать что-то на sklearn, плюс, если я не ошибаюсь, сложнее разработать логику стратификации, когда внутри группы метки классов могут быть разными.

@amueller да всегда. Они являются повторениями одной и той же меры, чтобы включить в прогноз интравариабельность устройства.

@hermidalc да, этот случай намного проще. Если это общая потребность, я буду рад, если мы ее добавим. Мы просто должны убедиться, что из названия несколько понятно, что он делает, и мы должны подумать о том, должны ли эти две версии жить в одном классе.

Заставить StratifiedKFold сделать это довольно просто. Есть два варианта: убедитесь, что каждая складка содержит одинаковое количество образцов, или убедитесь, что каждая складка содержит одинаковое количество групп.
Второй тривиально сделать (просто притворившись, что каждая группа является одной точкой и перейдя к StratifiedKFold ). Это то, что вы делаете в своем пиаре, похоже.

GroupKFold Я думаю, что эвристически обменивает два из них, добавляя сначала к наименьшему сгибу. Я не уверен, как это отразится на стратифицированном случае, поэтому я доволен использованием вашего подхода.

Должны ли мы также добавить GroupStratifiedKFold в тот же PR? Или оставить это на потом?
Другие PR преследуют несколько иные цели. Было бы хорошо, если бы кто-нибудь мог написать, каковы разные варианты использования (у меня, вероятно, сейчас нет времени).

+1 за отдельную обработку группового ограничения, когда все образцы имеют один и тот же класс.

@hermidalc да, этот случай намного проще. Если это общая потребность, я буду рад, если мы ее добавим. Мы просто должны убедиться, что из названия несколько понятно, что он делает, и мы должны подумать о том, должны ли эти две версии жить в одном классе.

Я не совсем понимаю это, StratifiedGroupShuffleSplit и StratifiedGroupKFold , где вы можете иметь членов каждой группы из разных классов, должны иметь точно такое же поведение разделения, когда пользователь указывает, что все члены группы должны быть того же класса. Когда можно будет просто улучшить внутренности позже и существующее поведение будет таким же?

Второй тривиально сделать (просто притворившись, что каждая группа является одной точкой и перейдя к StratifiedKFold ). Это то, что вы делаете в своем пиаре, похоже.

GroupKFold Я думаю, что эвристически обменивает два из них, добавляя сначала к наименьшему сгибу. Я не уверен, как это отразится на стратифицированном случае, поэтому я доволен использованием вашего подхода.

Должны ли мы также добавить GroupStratifiedKFold в тот же PR? Или оставить это на потом?
Другие PR преследуют несколько иные цели. Было бы хорошо, если бы кто-нибудь мог написать, каковы разные варианты использования (у меня, вероятно, сейчас нет времени).

Я добавлю StatifiedGroupKFold , используя подход «каждая группа по одному образцу», который я использовал.

Было бы хорошо, если бы заинтересованные люди могли описать свой вариант использования и то, что они действительно хотят от этого.

Очень распространенный случай использования в медицине и биологии, когда у вас есть повторные измерения.
Пример: предположим, что вы хотите классифицировать заболевание, например, болезнь Альцгеймера (БА) по сравнению со здоровым контролем по изображениям МРТ. Для одного и того же субъекта у вас может быть несколько сканирований (из последующих сеансов или продольных данных). Предположим, у вас есть в общей сложности 1000 субъектов, у 200 из них диагностирована БА (несбалансированные классы). У большинства субъектов есть один скан, но для некоторых доступны 2 или 3 изображения. При обучении/тестировании классификатора вы хотите убедиться, что изображения одного и того же объекта всегда находятся в одном и том же месте, чтобы избежать утечки данных.
Для этого лучше всего использовать StratifiedGroupKFold: стратифицировать для учета дисбаланса классов, но с групповым ограничением, согласно которому субъект не должен появляться в разных сгибах.
NB: Было бы неплохо сделать это повторяемым.

Ниже пример реализации, вдохновленный kaggle-kernel .

import numpy as np
from collections import Counter, defaultdict
from sklearn.utils import check_random_state

class RepeatedStratifiedGroupKFold():

    def __init__(self, n_splits=5, n_repeats=1, random_state=None):
        self.n_splits = n_splits
        self.n_repeats = n_repeats
        self.random_state = random_state

    # Implementation based on this kaggle kernel:
    #    https://www.kaggle.com/jakubwasikowski/stratified-group-k-fold-cross-validation
    def split(self, X, y=None, groups=None):
        k = self.n_splits
        def eval_y_counts_per_fold(y_counts, fold):
            y_counts_per_fold[fold] += y_counts
            std_per_label = []
            for label in range(labels_num):
                label_std = np.std(
                    [y_counts_per_fold[i][label] / y_distr[label] for i in range(k)]
                )
                std_per_label.append(label_std)
            y_counts_per_fold[fold] -= y_counts
            return np.mean(std_per_label)

        rnd = check_random_state(self.random_state)
        for repeat in range(self.n_repeats):
            labels_num = np.max(y) + 1
            y_counts_per_group = defaultdict(lambda: np.zeros(labels_num))
            y_distr = Counter()
            for label, g in zip(y, groups):
                y_counts_per_group[g][label] += 1
                y_distr[label] += 1

            y_counts_per_fold = defaultdict(lambda: np.zeros(labels_num))
            groups_per_fold = defaultdict(set)

            groups_and_y_counts = list(y_counts_per_group.items())
            rnd.shuffle(groups_and_y_counts)

            for g, y_counts in sorted(groups_and_y_counts, key=lambda x: -np.std(x[1])):
                best_fold = None
                min_eval = None
                for i in range(k):
                    fold_eval = eval_y_counts_per_fold(y_counts, i)
                    if min_eval is None or fold_eval < min_eval:
                        min_eval = fold_eval
                        best_fold = i
                y_counts_per_fold[best_fold] += y_counts
                groups_per_fold[best_fold].add(g)

            all_groups = set(groups)
            for i in range(k):
                train_groups = all_groups - groups_per_fold[i]
                test_groups = groups_per_fold[i]

                train_indices = [i for i, g in enumerate(groups) if g in train_groups]
                test_indices = [i for i, g in enumerate(groups) if g in test_groups]

                yield train_indices, test_indices

Сравнение RepeatedStratifiedKFold (образец одной и той же группы может появиться в обеих сгибах) с RepeatedStratifiedGroupKFold :

import matplotlib.pyplot as plt
from sklearn import model_selection

def plot_cv_indices(cv, X, y, group, ax, n_splits, lw=10):
    for ii, (tr, tt) in enumerate(cv.split(X=X, y=y, groups=group)):
        indices = np.array([np.nan] * len(X))
        indices[tt] = 1
        indices[tr] = 0

        ax.scatter(range(len(indices)), [ii + .5] * len(indices),
                   c=indices, marker='_', lw=lw, cmap=plt.cm.coolwarm,
                   vmin=-.2, vmax=1.2)

    ax.scatter(range(len(X)), [ii + 1.5] * len(X), c=y, marker='_',
               lw=lw, cmap=plt.cm.Paired)
    ax.scatter(range(len(X)), [ii + 2.5] * len(X), c=group, marker='_',
               lw=lw, cmap=plt.cm.tab20c)

    yticklabels = list(range(n_splits)) + ['class', 'group']
    ax.set(yticks=np.arange(n_splits+2) + .5, yticklabels=yticklabels,
           xlabel='Sample index', ylabel="CV iteration",
           ylim=[n_splits+2.2, -.2], xlim=[0, 100])
    ax.set_title('{}'.format(type(cv).__name__), fontsize=15)


# demonstration
np.random.seed(1338)
n_splits = 4
n_repeats=5


# Generate the class/group data
n_points = 100
X = np.random.randn(100, 10)

percentiles_classes = [.4, .6]
y = np.hstack([[ii] * int(100 * perc) for ii, perc in enumerate(percentiles_classes)])

# Evenly spaced groups
g = np.hstack([[ii] * 5 for ii in range(20)])


fig, ax = plt.subplots(1,2, figsize=(14,4))

cv_nogrp = model_selection.RepeatedStratifiedKFold(n_splits=n_splits,
                                                   n_repeats=n_repeats,
                                                   random_state=1338)
cv_grp = RepeatedStratifiedGroupKFold(n_splits=n_splits,
                                      n_repeats=n_repeats,
                                      random_state=1338)

plot_cv_indices(cv_nogrp, X, y, g, ax[0], n_splits * n_repeats)
plot_cv_indices(cv_grp, X, y, g, ax[1], n_splits * n_repeats)

plt.show()

RepeatedStratifiedGroupKFold_demo

+1 для stratifiedGroupKfold. Я пытаюсь обнаружить падения пожилых людей, снимая датчики с самрт-часов. поскольку у нас не так много данных о падении, мы делаем симуляции с разными часами, которые получают разные классы. Я также делаю дополнения к данным, прежде чем обучать их. из каждой точки данных я создаю 9 точек - и это группа. важно, чтобы группа не была одновременно в обучении и тестировании, как объяснялось

Я также хотел бы иметь возможность использовать StratifiedGroupKFold. Я смотрю на набор данных для прогнозирования финансовых кризисов, где годы до, после и во время каждого кризиса — это отдельная группа. Во время обучения и перекрестной проверки члены каждой группы не должны просачиваться между складками.

Можно ли как-то обобщить это для сценария с несколькими метками (Multilabel_
стратифицированная группаKfold)?

+1 за это. Мы анализируем учетные записи пользователей на наличие спама, поэтому мы хотим сгруппировать по пользователям, но также и стратифицировать, поскольку спам встречается относительно редко. В нашем случае любой пользователь, отправивший спам один раз, помечается как спамер во всех данных, поэтому член группы всегда будет иметь один и тот же ярлык.

Спасибо за предоставление классического варианта использования для оформления документации,
@Филип-IV!

Я добавил реализацию StratifiedGroupKFold в тот же PR #15239, что и StratifiedGroupShuffleSplit .

Хотя, как вы можете видеть в PR, логика для обоих намного проще, чем https://github.com/scikit-learn/scikit-learn/issues/13621#issuecomment -557802602, потому что мой только пытается сохранить процент групп для каждый класс (не процент выборок), чтобы я мог использовать существующий код StratifiedKFold и StratifiedShuffleSplit , передавая ему уникальную групповую информацию. Но обе реализации создают складки, в которых образцы каждой группы остаются вместе в одной и той же сгибе.

Хотя я бы проголосовал за более сложные методы, основанные на https://github.com/scikit-learn/scikit-learn/issues/13621#issuecomment -557802602.

Вот полноценные версии StratifiedGroupKFold и RepeatedStratifiedGroupKFold с использованием предоставленного @mrunibe кода, который я еще больше упростил и изменил пару вещей. Эти классы также следуют дизайну других классов sklearn CV того же типа.

class StratifiedGroupKFold(_BaseKFold):
    """Stratified K-Folds iterator variant with non-overlapping groups.

    This cross-validation object is a variation of StratifiedKFold that returns
    stratified folds with non-overlapping groups. The folds are made by
    preserving the percentage of samples for each class.

    The same group will not appear in two different folds (the number of
    distinct groups has to be at least equal to the number of folds).

    The difference between GroupKFold and StratifiedGroupKFold is that
    the former attempts to create balanced folds such that the number of
    distinct groups is approximately the same in each fold, whereas
    StratifiedGroupKFold attempts to create folds which preserve the
    percentage of samples for each class.

    Read more in the :ref:`User Guide <cross_validation>`.

    Parameters
    ----------
    n_splits : int, default=5
        Number of folds. Must be at least 2.

    shuffle : bool, default=False
        Whether to shuffle each class's samples before splitting into batches.
        Note that the samples within each split will not be shuffled.

    random_state : int or RandomState instance, default=None
        When `shuffle` is True, `random_state` affects the ordering of the
        indices, which controls the randomness of each fold for each class.
        Otherwise, leave `random_state` as `None`.
        Pass an int for reproducible output across multiple function calls.
        See :term:`Glossary <random_state>`.

    Examples
    --------
    >>> import numpy as np
    >>> from sklearn.model_selection import StratifiedGroupKFold
    >>> X = np.ones((17, 2))
    >>> y = np.array([0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0])
    >>> groups = np.array([1, 1, 2, 2, 3, 3, 3, 4, 5, 5, 5, 5, 6, 6, 7, 8, 8])
    >>> cv = StratifiedGroupKFold(n_splits=3)
    >>> for train_idxs, test_idxs in cv.split(X, y, groups):
    ...     print("TRAIN:", groups[train_idxs])
    ...     print("      ", y[train_idxs])
    ...     print(" TEST:", groups[test_idxs])
    ...     print("      ", y[test_idxs])
    TRAIN: [2 2 4 5 5 5 5 6 6 7]
           [1 1 1 0 0 0 0 0 0 0]
     TEST: [1 1 3 3 3 8 8]
           [0 0 1 1 1 0 0]
    TRAIN: [1 1 3 3 3 4 5 5 5 5 8 8]
           [0 0 1 1 1 1 0 0 0 0 0 0]
     TEST: [2 2 6 6 7]
           [1 1 0 0 0]
    TRAIN: [1 1 2 2 3 3 3 6 6 7 8 8]
           [0 0 1 1 1 1 1 0 0 0 0 0]
     TEST: [4 5 5 5 5]
           [1 0 0 0 0]

    See also
    --------
    StratifiedKFold: Takes class information into account to build folds which
        retain class distributions (for binary or multiclass classification
        tasks).

    GroupKFold: K-fold iterator variant with non-overlapping groups.
    """

    def __init__(self, n_splits=5, shuffle=False, random_state=None):
        super().__init__(n_splits=n_splits, shuffle=shuffle,
                         random_state=random_state)

    # Implementation based on this kaggle kernel:
    # https://www.kaggle.com/jakubwasikowski/stratified-group-k-fold-cross-validation
    def _iter_test_indices(self, X, y, groups):
        labels_num = np.max(y) + 1
        y_counts_per_group = defaultdict(lambda: np.zeros(labels_num))
        y_distr = Counter()
        for label, group in zip(y, groups):
            y_counts_per_group[group][label] += 1
            y_distr[label] += 1

        y_counts_per_fold = defaultdict(lambda: np.zeros(labels_num))
        groups_per_fold = defaultdict(set)

        groups_and_y_counts = list(y_counts_per_group.items())
        rng = check_random_state(self.random_state)
        if self.shuffle:
            rng.shuffle(groups_and_y_counts)

        for group, y_counts in sorted(groups_and_y_counts,
                                      key=lambda x: -np.std(x[1])):
            best_fold = None
            min_eval = None
            for i in range(self.n_splits):
                y_counts_per_fold[i] += y_counts
                std_per_label = []
                for label in range(labels_num):
                    std_per_label.append(np.std(
                        [y_counts_per_fold[j][label] / y_distr[label]
                         for j in range(self.n_splits)]))
                y_counts_per_fold[i] -= y_counts
                fold_eval = np.mean(std_per_label)
                if min_eval is None or fold_eval < min_eval:
                    min_eval = fold_eval
                    best_fold = i
            y_counts_per_fold[best_fold] += y_counts
            groups_per_fold[best_fold].add(group)

        for i in range(self.n_splits):
            test_indices = [idx for idx, group in enumerate(groups)
                            if group in groups_per_fold[i]]
            yield test_indices


class RepeatedStratifiedGroupKFold(_RepeatedSplits):
    """Repeated Stratified K-Fold cross validator.

    Repeats Stratified K-Fold with non-overlapping groups n times with
    different randomization in each repetition.

    Read more in the :ref:`User Guide <cross_validation>`.

    Parameters
    ----------
    n_splits : int, default=5
        Number of folds. Must be at least 2.

    n_repeats : int, default=10
        Number of times cross-validator needs to be repeated.

    random_state : int or RandomState instance, default=None
        Controls the generation of the random states for each repetition.
        Pass an int for reproducible output across multiple function calls.
        See :term:`Glossary <random_state>`.

    Examples
    --------
    >>> import numpy as np
    >>> from sklearn.model_selection import RepeatedStratifiedGroupKFold
    >>> X = np.ones((17, 2))
    >>> y = np.array([0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0])
    >>> groups = np.array([1, 1, 2, 2, 3, 3, 3, 4, 5, 5, 5, 5, 6, 6, 7, 8, 8])
    >>> cv = RepeatedStratifiedGroupKFold(n_splits=2, n_repeats=2,
    ...                                   random_state=36851234)
    >>> for train_index, test_index in cv.split(X, y, groups):
    ...     print("TRAIN:", groups[train_idxs])
    ...     print("      ", y[train_idxs])
    ...     print(" TEST:", groups[test_idxs])
    ...     print("      ", y[test_idxs])
    TRAIN: [2 2 4 5 5 5 5 8 8]
           [1 1 1 0 0 0 0 0 0]
     TEST: [1 1 3 3 3 6 6 7]
           [0 0 1 1 1 0 0 0]
    TRAIN: [1 1 3 3 3 6 6 7]
           [0 0 1 1 1 0 0 0]
     TEST: [2 2 4 5 5 5 5 8 8]
           [1 1 1 0 0 0 0 0 0]
    TRAIN: [3 3 3 4 7 8 8]
           [1 1 1 1 0 0 0]
     TEST: [1 1 2 2 5 5 5 5 6 6]
           [0 0 1 1 0 0 0 0 0 0]
    TRAIN: [1 1 2 2 5 5 5 5 6 6]
           [0 0 1 1 0 0 0 0 0 0]
     TEST: [3 3 3 4 7 8 8]
           [1 1 1 1 0 0 0]

    Notes
    -----
    Randomized CV splitters may return different results for each call of
    split. You can make the results identical by setting `random_state`
    to an integer.

    See also
    --------
    RepeatedStratifiedKFold: Repeats Stratified K-Fold n times.
    """

    def __init__(self, n_splits=5, n_repeats=10, random_state=None):
        super().__init__(StratifiedGroupKFold, n_splits=n_splits,
                         n_repeats=n_repeats, random_state=random_state)

@hermidalc Я совершенно сбит с толку тем, что мы решили, когда время от времени оглядываюсь назад. (К сожалению, мое время уже не то, что раньше!) Можете ли вы дать мне представление о том, что бы вы порекомендовали включить в scikit-learn?

@hermidalc Я совершенно сбит с толку тем, что мы решили, когда время от времени оглядываюсь назад. (К сожалению, мое время уже не то, что раньше!) Можете ли вы дать мне представление о том, что бы вы порекомендовали включить в scikit-learn?

Я хотел сделать лучшую реализацию, чем то, что я сделал в # 15239. Реализация в этом PR работает, но разделяется на группы, чтобы упростить логику, хотя это не идеально.

Итак, то, что я сделал выше (спасибо @mrunibe и kaggle от jakubwasikowski), — это лучшая реализация StratifiedGroupKFold , которая расслаивается на образцы. Я хочу портировать ту же логику, чтобы сделать лучше StratifiedGroupShuffleSplit , и тогда она будет готова. Я помещу новый код в #15239, чтобы заменить старую реализацию.

Я извиняюсь за то, что мои PR незакончены, я получаю докторскую степень, поэтому у меня никогда нет времени!

Спасибо @hermidalc и @mrunibe за реализацию. Я также искал метод StratifiedGroupKFold для работы с медицинскими данными, который имеет сильный дисбаланс классов и сильно различающееся количество образцов для каждого субъекта. GroupKFold сам по себе создает подмножества обучающих данных, содержащие только один класс.

Я хочу перенести ту же логику, чтобы сделать StratifiedGroupShuffleSplit лучше, и тогда она будет готова.

Конечно, мы могли бы подумать об объединении StratifiedGroupKFold до того, как StratifiedGroupShuffleSplit будет готов.

Я извиняюсь за то, что мои PR незакончены, я получаю докторскую степень, поэтому у меня никогда нет времени!

Дайте нам знать, если вам нужна помощь в его завершении!

И удачи в написании кандидатской диссертации.

Вот полноценные версии StratifiedGroupKFold и RepeatedStratifiedGroupKFold с использованием предоставленного @mrunibe кода, который я еще больше упростил и изменил пару вещей. Эти классы также следуют дизайну других классов sklearn CV того же типа.

Можно ли это попробовать? Я пытался вырезать и вставлять некоторые из различных зависимостей, но это никогда не заканчивалось. Я хотел бы попробовать этот класс в моем проекте. Просто пытаюсь посмотреть, есть ли сейчас способ сделать это.

@hermidalc Надеюсь, ваша докторская работа была успешной!
Я с нетерпением жду, когда эта реализация также будет реализована, так как моя кандидатская работа в области наук о Земле нуждается в этой функции стратификации с групповым контролем. Я потратил несколько часов на реализацию этой идеи разделения вручную в моем проекте. Но я отказался закончить его по той же причине... кандидат наук. Так что я прекрасно понимаю, как работа над докторской диссертацией может мучить человека. ЛОЛ Никакого давления. Пока я использую GroupShuffleSplit в качестве альтернативы.

Ваше здоровье

@bfeeny @dispink очень легко использовать два класса, которые я написал выше. Создайте файл, например, split.py со следующим. Затем в вашем пользовательском коде, если скрипт находится в том же каталоге, что и split.py , вы просто импортируете from split import StratifiedGroupKFold, RepeatedStratifiedGroupKFold

from collections import Counter, defaultdict

import numpy as np

from sklearn.model_selection._split import _BaseKFold, _RepeatedSplits
from sklearn.utils.validation import check_random_state


class StratifiedGroupKFold(_BaseKFold):
    """Stratified K-Folds iterator variant with non-overlapping groups.

    This cross-validation object is a variation of StratifiedKFold that returns
    stratified folds with non-overlapping groups. The folds are made by
    preserving the percentage of samples for each class.

    The same group will not appear in two different folds (the number of
    distinct groups has to be at least equal to the number of folds).

    The difference between GroupKFold and StratifiedGroupKFold is that
    the former attempts to create balanced folds such that the number of
    distinct groups is approximately the same in each fold, whereas
    StratifiedGroupKFold attempts to create folds which preserve the
    percentage of samples for each class.

    Read more in the :ref:`User Guide <cross_validation>`.

    Parameters
    ----------
    n_splits : int, default=5
        Number of folds. Must be at least 2.

    shuffle : bool, default=False
        Whether to shuffle each class's samples before splitting into batches.
        Note that the samples within each split will not be shuffled.

    random_state : int or RandomState instance, default=None
        When `shuffle` is True, `random_state` affects the ordering of the
        indices, which controls the randomness of each fold for each class.
        Otherwise, leave `random_state` as `None`.
        Pass an int for reproducible output across multiple function calls.
        See :term:`Glossary <random_state>`.

    Examples
    --------
    >>> import numpy as np
    >>> from sklearn.model_selection import StratifiedGroupKFold
    >>> X = np.ones((17, 2))
    >>> y = np.array([0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0])
    >>> groups = np.array([1, 1, 2, 2, 3, 3, 3, 4, 5, 5, 5, 5, 6, 6, 7, 8, 8])
    >>> cv = StratifiedGroupKFold(n_splits=3)
    >>> for train_idxs, test_idxs in cv.split(X, y, groups):
    ...     print("TRAIN:", groups[train_idxs])
    ...     print("      ", y[train_idxs])
    ...     print(" TEST:", groups[test_idxs])
    ...     print("      ", y[test_idxs])
    TRAIN: [2 2 4 5 5 5 5 6 6 7]
           [1 1 1 0 0 0 0 0 0 0]
     TEST: [1 1 3 3 3 8 8]
           [0 0 1 1 1 0 0]
    TRAIN: [1 1 3 3 3 4 5 5 5 5 8 8]
           [0 0 1 1 1 1 0 0 0 0 0 0]
     TEST: [2 2 6 6 7]
           [1 1 0 0 0]
    TRAIN: [1 1 2 2 3 3 3 6 6 7 8 8]
           [0 0 1 1 1 1 1 0 0 0 0 0]
     TEST: [4 5 5 5 5]
           [1 0 0 0 0]

    See also
    --------
    StratifiedKFold: Takes class information into account to build folds which
        retain class distributions (for binary or multiclass classification
        tasks).

    GroupKFold: K-fold iterator variant with non-overlapping groups.
    """

    def __init__(self, n_splits=5, shuffle=False, random_state=None):
        super().__init__(n_splits=n_splits, shuffle=shuffle,
                         random_state=random_state)

    # Implementation based on this kaggle kernel:
    # https://www.kaggle.com/jakubwasikowski/stratified-group-k-fold-cross-validation
    def _iter_test_indices(self, X, y, groups):
        labels_num = np.max(y) + 1
        y_counts_per_group = defaultdict(lambda: np.zeros(labels_num))
        y_distr = Counter()
        for label, group in zip(y, groups):
            y_counts_per_group[group][label] += 1
            y_distr[label] += 1

        y_counts_per_fold = defaultdict(lambda: np.zeros(labels_num))
        groups_per_fold = defaultdict(set)

        groups_and_y_counts = list(y_counts_per_group.items())
        rng = check_random_state(self.random_state)
        if self.shuffle:
            rng.shuffle(groups_and_y_counts)

        for group, y_counts in sorted(groups_and_y_counts,
                                      key=lambda x: -np.std(x[1])):
            best_fold = None
            min_eval = None
            for i in range(self.n_splits):
                y_counts_per_fold[i] += y_counts
                std_per_label = []
                for label in range(labels_num):
                    std_per_label.append(np.std(
                        [y_counts_per_fold[j][label] / y_distr[label]
                         for j in range(self.n_splits)]))
                y_counts_per_fold[i] -= y_counts
                fold_eval = np.mean(std_per_label)
                if min_eval is None or fold_eval < min_eval:
                    min_eval = fold_eval
                    best_fold = i
            y_counts_per_fold[best_fold] += y_counts
            groups_per_fold[best_fold].add(group)

        for i in range(self.n_splits):
            test_indices = [idx for idx, group in enumerate(groups)
                            if group in groups_per_fold[i]]
            yield test_indices


class RepeatedStratifiedGroupKFold(_RepeatedSplits):
    """Repeated Stratified K-Fold cross validator.

    Repeats Stratified K-Fold with non-overlapping groups n times with
    different randomization in each repetition.

    Read more in the :ref:`User Guide <cross_validation>`.

    Parameters
    ----------
    n_splits : int, default=5
        Number of folds. Must be at least 2.

    n_repeats : int, default=10
        Number of times cross-validator needs to be repeated.

    random_state : int or RandomState instance, default=None
        Controls the generation of the random states for each repetition.
        Pass an int for reproducible output across multiple function calls.
        See :term:`Glossary <random_state>`.

    Examples
    --------
    >>> import numpy as np
    >>> from sklearn.model_selection import RepeatedStratifiedGroupKFold
    >>> X = np.ones((17, 2))
    >>> y = np.array([0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0])
    >>> groups = np.array([1, 1, 2, 2, 3, 3, 3, 4, 5, 5, 5, 5, 6, 6, 7, 8, 8])
    >>> cv = RepeatedStratifiedGroupKFold(n_splits=2, n_repeats=2,
    ...                                   random_state=36851234)
    >>> for train_index, test_index in cv.split(X, y, groups):
    ...     print("TRAIN:", groups[train_idxs])
    ...     print("      ", y[train_idxs])
    ...     print(" TEST:", groups[test_idxs])
    ...     print("      ", y[test_idxs])
    TRAIN: [2 2 4 5 5 5 5 8 8]
           [1 1 1 0 0 0 0 0 0]
     TEST: [1 1 3 3 3 6 6 7]
           [0 0 1 1 1 0 0 0]
    TRAIN: [1 1 3 3 3 6 6 7]
           [0 0 1 1 1 0 0 0]
     TEST: [2 2 4 5 5 5 5 8 8]
           [1 1 1 0 0 0 0 0 0]
    TRAIN: [3 3 3 4 7 8 8]
           [1 1 1 1 0 0 0]
     TEST: [1 1 2 2 5 5 5 5 6 6]
           [0 0 1 1 0 0 0 0 0 0]
    TRAIN: [1 1 2 2 5 5 5 5 6 6]
           [0 0 1 1 0 0 0 0 0 0]
     TEST: [3 3 3 4 7 8 8]
           [1 1 1 1 0 0 0]

    Notes
    -----
    Randomized CV splitters may return different results for each call of
    split. You can make the results identical by setting `random_state`
    to an integer.

    See also
    --------
    RepeatedStratifiedKFold: Repeats Stratified K-Fold n times.
    """

    def __init__(self, n_splits=5, n_repeats=10, random_state=None):
        super().__init__(StratifiedGroupKFold, n_splits=n_splits,
                         n_repeats=n_repeats, random_state=random_state)

@hermidalc Спасибо за положительный ответ!
Я быстро принимаю это, как вы описали. Однако я могу получить только те расщепления, которые содержат данные только в тренировочном или тестовом наборе. Насколько я понимаю описание кода, там нет параметра для указания пропорции между обучающими и тестовыми наборами, верно?
Я знаю, что это конфликт между стратификацией, групповым контролем и пропорциями наборов данных... Вот почему я отказался от продолжения... Но, может быть, мы все же найдем компромисс, который можно обойти.
image

Искренне

@hermidalc Спасибо за положительный ответ!
Я быстро принимаю это, как вы описали. Однако я могу получить только те расщепления, которые содержат данные только в тренировочном или тестовом наборе. Насколько я понимаю описание кода, там нет параметра для указания пропорции между обучающими и тестовыми наборами, верно?
Я знаю, что это конфликт между стратификацией, групповым контролем и пропорциями наборов данных... Вот почему я отказался от продолжения... Но, может быть, мы все же найдем компромисс, который можно обойти.

Для проверки я сделал split.py и запустил этот пример в ipython, и он работает. Я давно использую эти пользовательские итераторы CV в своей работе, и у меня нет проблем с ними. Кстати, я использую scikit-learn 0.22.2 , а не 0.23.x, поэтому не уверен, что это причина проблемы. Не могли бы вы попробовать запустить приведенный ниже пример и посмотреть, сможете ли вы воспроизвести его? Если вы можете, то это может быть что-то с y и groups в вашей работе.

In [6]: import numpy as np 
   ...: from split import StratifiedGroupKFold 
   ...:  
   ...: X = np.ones((17, 2)) 
   ...: y = np.array([0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]) 
   ...: groups = np.array([1, 1, 2, 2, 3, 3, 3, 4, 5, 5, 5, 5, 6, 6, 7, 8, 8]) 
   ...: cv = StratifiedGroupKFold(n_splits=3, shuffle=True, random_state=777) 
   ...: for train_idxs, test_idxs in cv.split(X, y, groups): 
   ...:     print("TRAIN:", groups[train_idxs]) 
   ...:     print("      ", y[train_idxs]) 
   ...:     print(" TEST:", groups[test_idxs]) 
   ...:     print("      ", y[test_idxs]) 
   ...:                                                                                                                                                                                                    
TRAIN: [2 2 4 5 5 5 5 6 6 7]
       [1 1 1 0 0 0 0 0 0 0]
 TEST: [1 1 3 3 3 8 8]
       [0 0 1 1 1 0 0]
TRAIN: [1 1 3 3 3 4 5 5 5 5 8 8]
       [0 0 1 1 1 1 0 0 0 0 0 0]
 TEST: [2 2 6 6 7]
       [1 1 0 0 0]
TRAIN: [1 1 2 2 3 3 3 6 6 7 8 8]
       [0 0 1 1 1 1 1 0 0 0 0 0]
 TEST: [4 5 5 5 5]
       [1 0 0 0 0]

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

@hermidalc «Вы должны убедиться, что каждый образец в одной группе имеет одинаковую метку класса». Очевидно, это проблема. Мои образцы в одной группе не относятся к одному и тому же классу. Ммм... похоже, это очередная ветвь развития.
В любом случае, большое спасибо.

@hermidalc «Вы должны убедиться, что каждый образец в одной группе имеет одинаковую метку класса». Очевидно, это проблема. Мои образцы в одной группе не относятся к одному и тому же классу. Ммм... похоже, это очередная ветвь развития.
В любом случае, большое спасибо.

Да, это обсуждалось в разных темах здесь. Это еще один более сложный вариант использования, который полезен, но многим, как и мне, в настоящее время этот вариант использования не нужен, но им нужно что-то, что удерживает группы вместе, но стратифицирует образцы. Требование приведенного выше кода состоит в том, чтобы все образцы в каждой группе принадлежали к одному и тому же классу.

На самом деле @dispink Я ошибался, этот алгоритм не требует, чтобы все члены группы принадлежали к одному классу. Например:

In [2]: X = np.ones((17, 2)) 
   ...: y =      np.array([0, 2, 1, 1, 2, 0, 0, 1, 2, 1, 1, 1, 0, 2, 0, 1, 0]) 
   ...: groups = np.array([1, 1, 2, 2, 3, 3, 3, 4, 5, 5, 5, 5, 6, 6, 7, 8, 8]) 
   ...: cv = StratifiedGroupKFold(n_splits=3) 
   ...: for train_idxs, test_idxs in cv.split(X, y, groups): 
   ...:     print("TRAIN:", groups[train_idxs]) 
   ...:     print("      ", y[train_idxs]) 
   ...:     print(" TEST:", groups[test_idxs]) 
   ...:     print("      ", y[test_idxs]) 
   ...:                                                                                                                                                                                                    
TRAIN: [1 1 2 2 3 3 3 4 8 8]
       [0 2 1 1 2 0 0 1 1 0]
 TEST: [5 5 5 5 6 6 7]
       [2 1 1 1 0 2 0]
TRAIN: [1 1 4 5 5 5 5 6 6 7 8 8]
       [0 2 1 2 1 1 1 0 2 0 1 0]
 TEST: [2 2 3 3 3]
       [1 1 2 0 0]
TRAIN: [2 2 3 3 3 5 5 5 5 6 6 7]
       [1 1 2 0 0 2 1 1 1 0 2 0]
 TEST: [1 1 4 8 8]
       [0 2 1 1 0]

Поэтому я не совсем уверен, что происходит с вашими данными, поскольку даже на ваших скриншотах вы не можете по-настоящему увидеть, как выглядит ваш макет данных и что может происходить. Я бы посоветовал вам сначала воспроизвести примеры, которые я показал здесь, чтобы убедиться, что это не проблема версии scikit-learn (поскольку я использую 0.22.2), и если вы можете воспроизвести ее, я бы посоветовал вам начать с небольших частей вашего данные и проверить их. При использовании ~104 000 сэмплов сложно устранить неполадки.

@hermidalc Спасибо за ответ!
На самом деле я могу воспроизвести приведенный выше результат, поэтому сейчас я устраняю неполадки с меньшими данными.

+1

Кто-нибудь возражает, если я подниму этот вопрос?
Кажется, что # 15239 вместе с https://github.com/scikit-learn/scikit-learn/issues/13621#issuecomment -600894432 уже имеют реализацию, и осталось выполнить только модульные тесты.

Была ли эта страница полезной?
0 / 5 - 0 рейтинги