首页 > 解决方案 > 复杂数据集拆分 - StratifiedGroupShuffleSplit

问题描述

我有一个大约 2m 观察的数据集,我需要以 60:20:20 的比例将其分成训练、验证和测试集。我的数据集的简化摘录如下所示:

+---------+------------+-----------+-----------+
| note_id | subject_id | category  |   note    |
+---------+------------+-----------+-----------+
|       1 |          1 | ECG       | blah ...  |
|       2 |          1 | Discharge | blah ...  |
|       3 |          1 | Nursing   | blah ...  |
|       4 |          2 | Nursing   | blah ...  |
|       5 |          2 | Nursing   | blah ...  |
|       6 |          3 | ECG       | blah ...  |
+---------+------------+-----------+-----------+

有多个类别——它们并不均衡——所以我需要确保训练集、验证集和测试集都具有与原始数据集中相同的类别比例。这部分很好,我可以StratifiedShuffleSplitsklearn图书馆使用。

但是,我还需要确保每个主题的观察结果不会分散到训练、验证和测试数据集中。来自给定主题的所有观察结果都需要放在同一个桶中,以确保我的训练模型在验证/测试之前从未见过该主题。例如, subject_id 1的每个观察值都应该在训练集中。

我想不出一种方法来确保按类别分层拆分,防止跨数据集的subject_id污染(因为需要更好的词),确保 60:20:20 拆分并确保数据集以某种方式被打乱。任何帮助,将不胜感激!

谢谢!


编辑:

我现在了解到,也可以sklearn通过该GroupShuffleSplit函数来完成按类别分组和跨数据集拆分将组保持在一起。所以本质上,我需要的是一个组合的分层和分组的随机拆分,即StratifiedGroupShuffleSplit不存在。Github 问题:https ://github.com/scikit-learn/scikit-learn/issues/12076

标签: pythonmachine-learningscikit-learndataset

解决方案


本质上我需要StratifiedGroupShuffleSplit的是不存在的(Github 问题)。这是因为这样一个函数的行为是不清楚的,并且实现这一点来产生一个既分组又分层的数据集并不总是可能的(也在这里讨论)——尤其是对于像我这样的严重不平衡的数据集。就我而言,我希望严格进行分组,以确保在分层和数据集比率拆分为 60:20:20 的情况下不存在任何重叠,即尽可能地完成。

正如 Ghanem 提到的,我别无选择,只能自己构建一个函数来拆分数据集,如下所示:

def StratifiedGroupShuffleSplit(df_main):

    df_main = df_main.reindex(np.random.permutation(df_main.index)) # shuffle dataset

    # create empty train, val and test datasets
    df_train = pd.DataFrame()
    df_val = pd.DataFrame()
    df_test = pd.DataFrame()

    hparam_mse_wgt = 0.1 # must be between 0 and 1
    assert(0 <= hparam_mse_wgt <= 1)
    train_proportion = 0.6 # must be between 0 and 1
    assert(0 <= train_proportion <= 1)
    val_test_proportion = (1-train_proportion)/2

    subject_grouped_df_main = df_main.groupby(['subject_id'], sort=False, as_index=False)
    category_grouped_df_main = df_main.groupby('category').count()[['subject_id']]/len(df_main)*100

    def calc_mse_loss(df):
        grouped_df = df.groupby('category').count()[['subject_id']]/len(df)*100
        df_temp = category_grouped_df_main.join(grouped_df, on = 'category', how = 'left', lsuffix = '_main')
        df_temp.fillna(0, inplace=True)
        df_temp['diff'] = (df_temp['subject_id_main'] - df_temp['subject_id'])**2
        mse_loss = np.mean(df_temp['diff'])
        return mse_loss

    i = 0
    for _, group in subject_grouped_df_main:

        if (i < 3):
            if (i == 0):
                df_train = df_train.append(pd.DataFrame(group), ignore_index=True)
                i += 1
                continue
            elif (i == 1):
                df_val = df_val.append(pd.DataFrame(group), ignore_index=True)
                i += 1
                continue
            else:
                df_test = df_test.append(pd.DataFrame(group), ignore_index=True)
                i += 1
                continue

        mse_loss_diff_train = calc_mse_loss(df_train) - calc_mse_loss(df_train.append(pd.DataFrame(group), ignore_index=True))
        mse_loss_diff_val = calc_mse_loss(df_val) - calc_mse_loss(df_val.append(pd.DataFrame(group), ignore_index=True))
        mse_loss_diff_test = calc_mse_loss(df_test) - calc_mse_loss(df_test.append(pd.DataFrame(group), ignore_index=True))

        total_records = len(df_train) + len(df_val) + len(df_test)

        len_diff_train = (train_proportion - (len(df_train)/total_records))
        len_diff_val = (val_test_proportion - (len(df_val)/total_records))
        len_diff_test = (val_test_proportion - (len(df_test)/total_records)) 

        len_loss_diff_train = len_diff_train * abs(len_diff_train)
        len_loss_diff_val = len_diff_val * abs(len_diff_val)
        len_loss_diff_test = len_diff_test * abs(len_diff_test)

        loss_train = (hparam_mse_wgt * mse_loss_diff_train) + ((1-hparam_mse_wgt) * len_loss_diff_train)
        loss_val = (hparam_mse_wgt * mse_loss_diff_val) + ((1-hparam_mse_wgt) * len_loss_diff_val)
        loss_test = (hparam_mse_wgt * mse_loss_diff_test) + ((1-hparam_mse_wgt) * len_loss_diff_test)

        if (max(loss_train,loss_val,loss_test) == loss_train):
            df_train = df_train.append(pd.DataFrame(group), ignore_index=True)
        elif (max(loss_train,loss_val,loss_test) == loss_val):
            df_val = df_val.append(pd.DataFrame(group), ignore_index=True)
        else:
            df_test = df_test.append(pd.DataFrame(group), ignore_index=True)

        print ("Group " + str(i) + ". loss_train: " + str(loss_train) + " | " + "loss_val: " + str(loss_val) + " | " + "loss_test: " + str(loss_test) + " | ")
        i += 1

    return df_train, df_val, df_test

df_train, df_val, df_test = StratifiedGroupShuffleSplit(df_main)

我基于两件事创建了一些任意损失函数:

  1. 与整个数据集相比,每个类别的百分比表示的平均平方差
  2. 数据集的比例长度与根据提供的比率(60:20:20)应有的长度之间的平方差

将这两个输入加权到损失函数是由静态超参数完成的hparam_mse_wgt。对于我的特定数据集,0.1 的值效果很好,但如果你使用这个函数,我鼓励你尝试一下。将其设置为 0 将优先考虑仅保持分流比并忽略分层。将其设置为 1 反之亦然。

使用这个损失函数,然后我遍历每个主题(组)并根据具有最高损失函数的那个​​将其附加到适当的数据集(训练、验证或测试)中。

它不是特别复杂,但它为我完成了这项工作。它不一定适用于每个数据集,但它越大,机会就越大。希望其他人会发现它有用。


推荐阅读