复杂数据集拆分-StratifiedGroupShuffleSplit

时间:2019-07-03 14:44:12

标签: python machine-learning scikit-learn dataset

我有一个约200万个观测值的数据集,我需要按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 ...  |
+---------+------------+-----------+-----------+

有多个类别-分布不均衡-因此,我需要确保训练,验证和测试集的类别比例与原始数据集中的比例相同。这部分很好,我可以只使用StratifiedShuffleSplit库中的sklearn

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

我想不出一种方法来确保按类别进行分层划分,防止跨数据集的 subject_id 污染(因为缺少更好的词), 60:20:20分割并确保以某种方式对数据集进行混洗。任何帮助,将不胜感激!

谢谢!


编辑:

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

5 个答案:

答案 0 :(得分:1)

我只需要解决同样的问题。在我的文档处理用例中,我希望来自同一页面的单词能够保持在一起(分组),而文档类别应该在训练和测试集之间平均分配(分层)。对于我的问题,它认为对于一组的所有实例,我们具有相同的分层类别,即,一页中的所有单词都属于同一类别。因此,我发现最简单的方法是直接对组执行分层拆分,然后使用拆分组选择实例。如果此假设不成立,则此解决方案不适用。

from typing import Tuple

import pandas as pd
from sklearn.model_selection import train_test_split


def stratified_group_train_test_split(
    samples: pd.DataFrame, group: str, stratify_by: str, test_size: float
) -> Tuple[pd.DataFrame, pd.DataFrame]:
    groups = samples[group].drop_duplicates()
    stratify = samples.drop_duplicates(group)[stratify_by].to_numpy()
    groups_train, groups_test = train_test_split(groups, stratify=stratify, test_size=test_size)

    samples_train = samples.loc[lambda d: d[group].isin(groups_train)]
    samples_test = samples.loc[lambda d: d[group].isin(groups_test)]

    return samples_train, samples_test

答案 1 :(得分:0)

我认为在这种情况下,您必须构建自己的函数来拆分数据。 这是我的实现:

def split(df, based_on='subject_id', cv=5):
    splits = []
    based_on_uniq = df[based_on]#set(df[based_on].tolist())
    based_on_uniq = np.array_split(based_on_uniq, cv)
    for fold in based_on_uniq:
        splits.append(df[df[based_on] == fold.tolist()[0]])
    return splits


if __name__ == '__main__':
    df = pd.DataFrame([{'note_id': 1, 'subject_id': 1, 'category': 'test1', 'note': 'test1'},
                       {'note_id': 2, 'subject_id': 1, 'category': 'test2', 'note': 'test2'},
                       {'note_id': 3, 'subject_id': 2, 'category': 'test3', 'note': 'test3'},
                       {'note_id': 4, 'subject_id': 2, 'category': 'test4', 'note': 'test4'},
                       {'note_id': 5, 'subject_id': 3, 'category': 'test5', 'note': 'test5'},
                       {'note_id': 6, 'subject_id': 3, 'category': 'test6', 'note': 'test6'},
                       {'note_id': 7, 'subject_id': 4, 'category': 'test7', 'note': 'test7'},
                       {'note_id': 8, 'subject_id': 4, 'category': 'test8', 'note': 'test8'},
                       {'note_id': 9, 'subject_id': 5, 'category': 'test9', 'note': 'test9'},
                       {'note_id': 10, 'subject_id': 5, 'category': 'test10', 'note': 'test10'},
                       ])
    print(split(df))

答案 2 :(得分:0)

基本上,我需要< form method="POST" enctype="multipart/form-data" action="{{ url('users/profile', $data->id) }}" accept-charset="UTF-8" > @method('PUT') @csrf ... // input fields here ... < /form > 不存在(Github issue)。这是因为此类函数的行为尚不清楚,并且完成此操作以生成既分组又分层的数据集并非总是可能的(also discussed here)-尤其是对于像我这样的严重失衡的数据集。就我而言,我希望严格进行分组,以确保在进行分层和大约60.20:20的数据集比率分割时(即尽可能)进行分组。

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

StratifiedGroupShuffleSplit

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

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

将这两个输入加权到损失函数是通过静态超参数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) 完成的。对于我的特定数据集,0.1的效果很好,但是如果您使用此功能,我建议您试用一下。将其设置为0将仅保留分割率的优先级,而忽略分层。反之亦然。

然后使用此损失函数,对每个受试者(组)进行迭代,然后根据损失函数最高的那个追加到适当的数据集(训练,验证或测试)。

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

答案 3 :(得分:0)

这花了一年多的时间,但是我发现自己处于类似的情况,我有标签和一组,由于这些组的性质,一组数据点只能在测试中或仅在训练中,我已经使用pandas和sklearn编写了一个小算法,希望对您有所帮助

from sklearn.model_selection import GroupShuffleSplit
groups = df.groupby('label')
all_train = []
all_test = []
for group_id, group in groups:
    # if a group is already taken in test or train it must stay there
    group = group[~group['groups'].isin(all_train+all_test)]
    # if group is empty 
    if group.shape[0] == 0:
        continue
    train_inds, test_inds = next(GroupShuffleSplit(
        test_size=valid_size, n_splits=2, random_state=7).split(group, groups=group['groups']))

    all_train += group.iloc[train_inds]['groups'].tolist()
    all_test += group.iloc[test_inds]['groups'].tolist()



train= df[df['groups'].isin(all_train)]
test= df[df['groups'].isin(all_test)]

form_train = set(train['groups'].tolist())
form_test = set(test['groups'].tolist())
inter = form_train.intersection(form_test)

print(df.groupby('label').count())
print(train.groupby('label').count())
print(test.groupby('label').count())
print(inter) # this should be empty

答案 4 :(得分:0)

正如其他人之前评论的那样:StratifiedGroupShuffleSplit 不存在,因为您可能无法保证分组的拆分将具有每个类的相似数量的实例。 但是,您可以选择一个愚蠢但非常简单的解决方案,最终将提供一个足够好的解决方案:

  1. 使用带有随机状态集的 GroupShuffleSplit(例如 GroupShuffleSplit(n_splits=1, test_size=0.3, random_state=0)
  2. 计算每个分组中类之间的平衡。
  3. 如果您不满意,只需将 random_state 设置为其他值再次运行。
  4. 继续直到分裂足够好。

这种方法显然最适合少量拆分和二进制标签。