如何使用多列生成没有数据泄漏的测试集

数据挖掘 scikit-学习 熊猫 训练 sql 数据泄露
2022-03-15 20:59:52

我正在开发一种欺诈检测算法。除其他外,我的数据集包含电话号码、电子邮件地址和一些其他应该唯一标识用户的字段(我们称它们为“唯一字段”)。为了防止我的训练集和测试集之间的数据泄漏,我想确保我的测试集只包含全新的用户,这意味着他们不应该有任何用户的唯一字段与培训的任何用户的任何唯一字段匹配放。而且我在构建这个测试集时遇到了麻烦。基本上,我正在寻找一种基于多列值生成唯一 ID 的方法,这意味着如果它们的任何唯一字段匹配,则 2 行应该具有相同的唯一 ID。你有什么解决办法吗?答案可能在 SQL 或 Pandas 或任何其他 python 库中,我可以适应。

我能想到的唯一解决方案是从基本的 train_test_split 开始,并从测试集中迭代地删除具有与训练集匹配的字段的任何行,但这比生成唯一 ID 更麻烦且不那么优雅。

2个回答

我最终随机浏览了我的数据框,并根据其唯一标识符将每一行分配给训练或测试集。对于我的用例来说,它恰好足够快(我的 10M 行数据帧需要 1 分钟,有 4 个标识符)。

import random
from tqdm import tqdm

def train_test_split_identifiers(df, identifier_cols, target_test_size):

train_idx = []
train_values = {identifier_col : set() for identifier_col in identifier_cols}
test_idx = []
test_values = {identifier_col : set() for identifier_col in identifier_cols}
aside_idx = []

for row in tqdm(df.sample(frac=1.0).itertuples()):

    in_train = False
    in_test = False

    for i, identifier_col in enumerate(identifier_cols):
        if row[i + 1] in train_values[identifier_col]:
            in_train = True
        elif row[i + 1] in test_values[identifier_col]:
            in_test = True

    if not in_train and not in_test:
        if random.random() < target_test_size:
            test_idx.append(row[0])
            for i, identifier_col in enumerate(identifier_cols):
                test_values[identifier_col].add(row[i + 1])
        else:
            train_idx.append(row[0])
            for i, identifier_col in enumerate(identifier_cols):
                train_values[identifier_col].add(row[i + 1])
    elif in_train and not in_test:
        train_idx.append(row[0])
        for i, identifier_col in enumerate(identifier_cols):
            train_values[identifier_col].add(row[i + 1])
    elif in_test and not in_train:
        test_idx.append(row[0])
        for i, identifier_col in enumerate(identifier_cols):
            test_values[identifier_col].add(row[i + 1])
    else:
        aside_idx.append(row[0])
        
        
assert len(df) == len(test_idx + train_idx + aside_idx)

train = df.loc[train_idx]
test = df.loc[test_idx]

print(f'Train size = {round(100 * len(train_idx) / len(df), 2)} %')
print(f'Test size = {round(100 * len(test_idx) / len(df), 2)} %')
print(f'Left aside = {round(100 * len(aside_idx) / len(df), 2)} %')

for identifier_col in identifier_cols:
    assert len(set(train[identifier_col]).intersection(test[identifier_col])) == 0, 'Data leakage detected'

return train, test

编辑:我想出了一个使用图表更好的解决方案,它更快并且不会让任何用户搁置。基本上你用这个方法创建'true_user_id'然后train_test_split

import networkx as nx

def get_unique_ids(df, id_col, compare_cols):

    print('Creating links between pairs of users')
    links = set()

    for col in compare_cols:
        df_self_merged = df.merge(df, on=col)
        links = links.union(set(df_self_merged.loc[df_self_merged[id_col+'_x'] != df_self_merged[id_col+'_y'], [id_col+'_x', id_col+'_y']].itertuples(index=False, name=None)))
    
    print('Building graph from links')
    G = nx.Graph(links)

    print('Adding users that have no links')
    G.add_nodes_from(set(df[id_col]) - set(G.nodes))

    print('Assigning a new unique ID to connected users')
    tuples = []
    for i, cluster in enumerate(nx.connected_components(G)):
        tuples.extend([(user, i) for user in cluster])

    return df.merge(pd.DataFrame(tuples), how='left', left_on=id_col, right_on=0)[1]

df['unique_id'] = get_unique_ids(df, 'user_id', ['email', 'phone_number', 'card_fingerprint'])

简单的解决方案是创建一个 sudo Id 列,它是所有唯一标识符列的串联(例如 mail@com9825403)。然后,您对该列的唯一条目进行采样以进行测试和训练。