Pandas中的分层抽样

70

2
我认为问题的标题应该更改以反映分层是特征列而不是目标列。 - wordsforthewise
你几乎可以使用 imblearn 的下采样或欠采样技术来解决这个问题:https://imbalanced-learn.org/stable/under_sampling.html - wordsforthewise
5个回答

106

使用min来指定采样的数量。考虑数据框df

df = pd.DataFrame(dict(
        A=[1, 1, 1, 2, 2, 2, 2, 3, 4, 4],
        B=range(10)
    ))

df.groupby('A', group_keys=False).apply(lambda x: x.sample(min(len(x), 2)))

   A  B
1  1  1
2  1  2
3  2  3
6  2  6
7  3  7
9  4  9
8  4  8

4
假设我有一个包含100万行的数据框,我想要从中抽取1万行,其中每个用户至少有10个样本,请问您如何处理? - joddm
@whitfa 对我仍然有效,链接的更改不应该对其产生任何影响。你使用的 pandas 版本是多少?我正在使用 0.25 - piRSquared
抱歉 @piRSquared,看起来我错了!我会删除我的原始评论。 - whitfa
当我的分组列具有高基数时,这个解决方案会变得相当慢。我想这是有道理的。不管怎样,你能想到在这种情况下加速它的方法吗? - hipoglucido

19

扩展groupby的答案,我们可以确保样本平衡。为此,当所有类别的样本数 >= n_samples时,我们只需对所有类别取n_samples(之前的答案)。当少数类别包含 < n_samples时,我们可以将所有类别的样本数设置为与少数类别相同。

def stratified_sample_df(df, col, n_samples):
    n = min(n_samples, df[col].value_counts().min())
    df_ = df.groupby(col).apply(lambda x: x.sample(n))
    df_.index = df_.index.droplevel(0)
    return df_

8
解释代码的作用及如何解决问题,很少会导致回答失败。 - MBT

10
以下示例共有N行,每个分组出现的比例最接近整数,并进行混洗和重置索引,使用以下代码:
df = pd.DataFrame(dict(
    A=[1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 4, 4, 4, 4, 4],
    B=range(20)
))

简明扼要:

df.sample(n=N, weights='A', random_state=1).reset_index(drop=True)

长版本

df.groupby('A', group_keys=False).apply(lambda x: x.sample(int(np.rint(N*len(x)/len(df))))).sample(frac=1).reset_index(drop=True)

9
短版本存在问题,它不能保持原始比例:使用参数weights = 类别列并不是很合理,因为它可以是一个字符串。如果你真的想使用df.sample,你需要计算一个额外的列,等于类别列的频率。 但是长版本可以正常工作! - steco
短版本对于二进制数据不起作用,例如 df = pd.DataFrame({'A': [np.random.randint(0, 2) for _ in range(100)]}) - npit
如果列A不是数字,将无法工作。 - hafiz031

2
所以我尝试了上面的所有方法,但它们仍然不是我想要的(将解释原因)。
步骤1:是的,我们需要按目标变量进行分组,让我们称其为“target_variable”。因此,代码的第一部分将如下所示:groupby
df.groupby('target_variable', group_keys=False)

我设置group_keys=False,因为我不想将索引继承到输出中。
步骤2:使用applytarget_variable中的各个类别中进行采样。
这就是我发现上面的答案并不完全通用的地方。在我的示例中,这是我在df中作为标签数字的内容:
array(['S1','S2','normal'], dtype=object),
array([799, 2498,3716391])

所以你可以看到我的目标变量是多么不平衡。我需要确保对于每个类别,我都要取S1标签的数量作为最小样本数。
min(np.unique(df['target_variable'], return_counts=True))

这是 @piRSquared 的答案所缺少的部分。然后你要在班级号码的最小值(这里是 799)和每个班级的数量之间进行选择。这不是一个通用规则,你可以选择其他数字。例如:
max(len(x), min(np.unique(data_use['snd_class'], return_counts=True)[1])

这将给出您最小班级的max与每个班级的数量相比较的结果。
他们回答中的另一个技术问题是建议在抽样后对输出进行洗牌。也就是说,您不希望所有的S1样本都在连续的行中,然后是S2等等。您需要确保您的行是随机堆叠的。这就是sample(frac=1)的作用。值1是因为我想在洗牌后返回所有数据。如果您由于任何原因需要更少的数据,请随意提供一个分数,如0.6,它将返回原始样本的60%,并进行了洗牌。
步骤3:我的最终代码如下:
df.groupby('target_variable', group_keys=False).apply(lambda x: x.sample(min(len(x), min(np.unique(df['target_variable'], return_counts=True)[1]))).sample(frac=1))

我正在选择索引1,即np.unique(df['target_variable'], return_counts=True)[1],因为这是以numpy数组形式获取每个类别数量的适当方式。请根据需要进行修改。

2

根据用户piRSquared的回答,我们可能会有以下内容:

import pandas as pd


def stratified_sample(df: pd.DataFrame, groupby_column: str, sampling_rate: float = 0.01) -> pd.DataFrame:
    assert 0.0 < sampling_rate <= 1.0
    assert groupby_column in df.columns

    num_rows = int((df.shape[0] * sampling_rate) // 1)
    num_classes = len(df[groupby_column].unique())
    num_rows_per_class = int(max(1, ((num_rows / num_classes) // 1)))
    df_sample = df.groupby(groupby_column, group_keys=False).apply(lambda x: x.sample(min(len(x), num_rows_per_class)))

    return df_sample

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接