Scikit-learn: 分层组KFold

创建于 2019-04-11  ·  48评论  ·  资料来源: scikit-learn/scikit-learn

描述

目前 sklearn 没有分层组 kfold 功能。 我们可以使用分层,也可以使用 group kfold。 不过,两者兼得就好了。

如果我们决定拥有它,我想实施它。

最有用的评论

如果有兴趣的人可以描述他们的用例以及他们真正想要的东西,那就太好了。

当您重复测量时,医学和生物学中非常常见的用例。
一个例子:假设您想对一种疾病进行分类,例如阿尔茨海默病 (AD) 与 MR 图像中的健康对照。 对于同一主题,您可能需要进行多次扫描(来自后续会话或纵向数据)。 假设您总共有 1000 名受试者,其中 200 名被诊断出患有 AD(不平衡类)。 大多数受试者只有一次扫描,但对于其中一些人来说,有 2 或 3 张图像可用。 在训练/测试分类器时,您要确保来自同一主题的图像始终处于同一折叠中,以避免数据泄漏。
最好为此使用 StratifiedGroupKFold:分层以解决类不平衡问题,但具有主题不能出现在不同折叠中的组约束。
注意:让它可重复会很好。

下面是一个受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是的,我也有类似的想法。 但是,我看到拉取请求仍然处于打开状态。 我的意思是一个组不会跨折叠重复。 如果我们将 ID 作为组,那么相同的 ID 将不会出现在多个折叠中

我知道这与使用 RFECV 有关。
目前,这默认使用 StratifiedKFold 简历。 它的 fit() 也需要 groups=
但是:执行 fit() 时似乎不尊重组。 没有警告(可能被认为是一个错误)。

分组和分层对于具有记录间依赖性的非常不平衡的数据集很有用
(在我的情况下,同一个人有多个记录,但仍然有大量的群体=人相对于分裂的数量;我想随着少数群体中独特群体的数量接近而存在实际问题分割数)。

所以:+1!

这肯定会有用。 例如,使用高度不平衡的时间序列医学数据,将患者分开但(大约)平衡每个折叠中的不平衡类别。

我还发现 StratifiedKFold 将组作为参数,但不根据它们进行分组,可能应该被标记。

此功能的另一个很好的用途是财务数据,这些数据通常非常不平衡。 就我而言,我有一个高度不平衡的数据集,其中包含同一实体的多个记录(只是不同的时间点)。 我们想做一个GroupKFold来避免泄漏,但也要分层,因为由于高度不平衡,我们最终可能会得到很少或没有阳性结果的组。

另请参阅#14524 我认为?

Stratified GroupShuffleSplit 和 GroupKFold 的另一个用例是生物“重复测量”设计,其中每个受试者或其他父生物单元有多个样本。 在生物学的许多现实世界数据集中也存在类别不平衡。 每组样本具有相同的类别。 因此,重要的是分层和保持组在一起。

描述

目前 sklearn 没有分层组 kfold 功能。 我们可以使用分层,也可以使用 group kfold。 不过,两者兼得就好了。

如果我们决定拥有它,我想实施它。

嗨,我认为这对医学 ML 非常有用。 已经实施了吗?

@amueller鉴于人们对此感兴趣,您认为我们应该实施吗?

我也很感兴趣......当您对每个样品进行多次重复测量时,它在光谱学中非常有用,它们确实需要在交叉验证期间保持相同的折叠。 如果你有几个不平衡的类要分类,你真的也想使用分层功能。 所以我也投赞成票! 对不起,我不够好,无法参与开发,但对于那些将参与其中的人,你可以确定它会被使用:-)
为所有团队竖起大拇指。 谢谢!

请查看此线程中引用的问题和 PR,因为至少已在StratifiedGroupKFold上尝试过工作。 我已经完成了StratifiedGroupShuffleSplit #15239 ,它只需要测试,但我已经在自己的工作中使用了很多。

我认为我们应该实施它,但我认为我仍然不知道我们真正想要什么。 @hermidalc有一个限制,即同一组的成员必须属于同一类。 这不是一般情况,对吧?

如果有兴趣的人可以描述他们的用例以及他们真正想要的东西,那就太好了。

有 #15239 #14524 和 #9413 我记得它们都有不同的语义。

@amueller完全同意你的看法,我今天花了几个小时寻找可用的不同版本(#15239 #14524 和 #9413)之间的东西,但我真的不明白这些是否适合我的需要。 因此,如果可以提供帮助,这是我的用例:
我有 1000 个样本。 每个样品都用 NIR 光谱仪测量了 3 次,所以每个样品都有 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 )。 这就是你在 PR 中所做的,看起来是这样的。

GroupKFold 我认为通过首先添加最小的折叠来启发式地权衡两者。 我不确定这将如何转化为分层案例,所以我很高兴使用你的方法。

我们还应该在同一个 PR 中添加 GroupStratifiedKFold 吗? 还是留到以后再说?
其他 PR 的目标略有不同。 如果有人能写出不同的用例是什么,那就太好了(我现在可能没有时间)。

+1 用于单独处理所有样本具有相同类别的组约束。

@hermidalc是的,这种情况要容易得多。 如果这是一个共同的需求,我很高兴我们添加它。 我们应该确保从名称中可以清楚地知道它的作用,并且我们应该考虑这两个版本是否应该存在于同一个类中。

我不完全理解这一点,当用户指定所有StratifiedGroupKFold成员StratifiedGroupShuffleSplit属于同一类。 什么时候可以稍后改进内部结构并且现有的行为将是相同的?

第二个是微不足道的(只需假装每个组都是一个点并传递给StratifiedKFold )。 这就是你在 PR 中所做的,看起来是这样的。

GroupKFold 我认为通过首先添加最小的折叠来启发式地权衡两者。 我不确定这将如何转化为分层案例,所以我很高兴使用你的方法。

我们还应该在同一个 PR 中添加 GroupStratifiedKFold 吗? 还是留到以后再说?
其他 PR 的目标略有不同。 如果有人能写出不同的用例是什么,那就太好了(我现在可能没有时间)。

我将使用我使用的“每组单个样本”方法添加StatifiedGroupKFold

如果有兴趣的人可以描述他们的用例以及他们真正想要的东西,那就太好了。

当您重复测量时,医学和生物学中非常常见的用例。
一个例子:假设您想对一种疾病进行分类,例如阿尔茨海默病 (AD) 与 MR 图像中的健康对照。 对于同一主题,您可能需要进行多次扫描(来自后续会话或纵向数据)。 假设您总共有 1000 名受试者,其中 200 名被诊断出患有 AD(不平衡类)。 大多数受试者只有一次扫描,但对于其中一些人来说,有 2 或 3 张图像可用。 在训练/测试分类器时,您要确保来自同一主题的图像始终处于同一折叠中,以避免数据泄漏。
最好为此使用 StratifiedGroupKFold:分层以解决类不平衡问题,但具有主题不能出现在不同折叠中的组约束。
注意:让它可重复会很好。

下面是一个受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 分层GroupKfold。 我正在尝试检测老年人的跌倒,从智能手表中获取传感器。 因为我们没有太多的跌倒数据——我们用不同的手表进行模拟,得到不同的类别。 在训练数据之前,我也会对数据进行扩充。 从每个数据点我创建 9 个点 - 这是一个组。 正如解释的那样,一个小组不会同时在训练和测试中,这一点很重要

我也希望能够使用 StratifiedGroupKFold。 我正在查看一个用于预测金融危机的数据集,其中在每次危机之前、之后和期间都有自己的组。 在训练和交叉验证期间,每个组的成员不应在折叠之间泄漏。

无论如何,对于多标签场景(Multilabel_
分层组Kfold)?

为此+1。 我们正在分析垃圾邮件的用户帐户,因此我们希望按用户分组,但也分层,因为垃圾邮件的发生率相对较低。 对于我们的用例,任何发送一次垃圾邮件的用户在所有数据中都被标记为垃圾邮件发送者,因此组成员将始终具有相同的标签。

感谢您提供经典用例来构建文档,
@philip-iv!

我在与StratifiedGroupShuffleSplit相同的 PR #15239 中添加了StratifiedGroupKFold实现。

尽管正如您在 PR 中看到的那样,两者的逻辑都比https://github.com/scikit-learn/scikit-learn/issues/13621#issuecomment -557802602 简单得多,因为我只尝试保留组的百分比每个类(不是样本的百分比),以便我可以通过传递唯一的组信息来利用现有的StratifiedKFoldStratifiedShuffleSplit代码。 但是这两种实现都会产生折叠,其中每个组的样本都放在同一个折叠中。

虽然我会投票支持基于https://github.com/scikit-learn/scikit-learn/issues/13621#issuecomment -557802602 的更复杂的方法

以下是使用@mrunibe提供的代码的StratifiedGroupKFoldRepeatedStratifiedGroupKFold的完整版本,我进一步简化并更改了一些内容。 这些类也遵循相同类型的其他 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和 jakubwasikowski 的 kaggle)是对样本进行分层的StratifiedGroupKFold的更好实现。 我想移植相同的逻辑来做一个更好的StratifiedGroupShuffleSplit然后它就准备好了。 我将把新代码放在#15239 中以替换旧的实现。

对于我的 PR 未完成,我深表歉意,我正在攻读博士学位,所以没有时间!

感谢@hermidalc@mrunibe提供实施。 我也一直在寻找一种StratifiedGroupKFold方法来处理具有严重类别不平衡和每个主题的样本数量差异很大的医学数据。 GroupKFold本身会创建仅包含一个类的训练数据子集。

我想移植相同的逻辑来做一个更好的 StratifiedGroupShuffleSplit 然后它就准备好了。

我们当然可以考虑在StratifiedGroupShuffleSplit准备好之前合并StratifiedGroupKFold

对于我的 PR 未完成,我深表歉意,我正在攻读博士学位,所以没有时间!

让我们知道您是否需要支持完成它!

祝你的博士工作好运

以下是使用@mrunibe提供的代码的StratifiedGroupKFoldRepeatedStratifiedGroupKFold的完整版本,我进一步简化并更改了一些内容。 这些类也遵循相同类型的其他 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,所以不确定这是否是问题的原因。 您能否尝试在下面运行此示例,看看是否可以重现它? 如果可以的话,那么它可能与你工作中的ygroups有关。

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),如果您可以重现它,那么我建议您从小部分开始数据并进行测试。 使用约 104k 样本很难排除故障。

@hermidalc谢谢你的回复!
我实际上可以重现上面的结果,所以我现在用较小的数据进行故障排除。

+1

有人介意我接这个问题吗?
似乎#15239 和https://github.com/scikit-learn/scikit-learn/issues/13621#issuecomment -600894432 已经有了一个实现,只剩下单元测试要做。

此页面是否有帮助?
0 / 5 - 0 等级

相关问题

jnothman picture jnothman  ·  60评论

amueller picture amueller  ·  79评论

joelkuiper picture joelkuiper  ·  108评论

mikeroberts3000 picture mikeroberts3000  ·  102评论

thomasjpfan picture thomasjpfan  ·  60评论