具有人口平衡的分层随机抽样

Posted

技术标签:

【中文标题】具有人口平衡的分层随机抽样【英文标题】:Stratified random sampling with Population Balancing 【发布时间】:2018-05-14 11:44:19 【问题描述】:

考虑如下所示的类分布偏斜的人口

     ErrorType   Samples
        1          XXXXXXXXXXXXXXX
        2          XXXXXXXX
        3          XX
        4          XXX
        5          XXXXXXXXXXXX

我想从 40 个中随机抽取 20 个样本,而不会对参与人数较少的任何课程进行欠采样。例如在上面的情况下,我想采样如下

     ErrorType   Samples
        1          XXXXX|XXXXXXXXXX
        2          XXXXX|XXX
        3          XX***|
        4          XXX**|
        5          XXXXX|XXXXXXX

即-1型和-2型的5个和-3型的,-3型的2个和-4型的3个

    这保证我的样本大小接近我的目标,即 20 个样本 没有一个课程参与其中,尤其是课程 -3 和 -4。

我最终写了一个迂回的代码,但我相信可以有更简单的方法来利用 pandas 方法或一些 sklearn 函数。

 sample_size = 20 # Just for the example
 # Determine the average participaction per error types
 avg_items = sample_size / len(df.ErrorType.unique())
 value_counts = df.ErrorType.value_counts()
 less_than_avg = value_counts[value_counts < avg_items]
 offset = avg_items * len(value_counts[value_counts < avg_items]) - sum(less_than_avg)
 offset_per_item = offset / (len(value_counts) - len(less_than_avg))
 adj_avg = int(non_act_count / len(value_counts) + offset_per_item)
 df = df.groupby(['ErrorType'],
                 group_keys=False).apply(lambda g: g.sample(min(adj_avg, len(g)))))

【问题讨论】:

所以提供的数据是您实际拥有的数据还是为了说明问题? @Bharath:用于说明目的。 出于好奇,您是否可以向我们展示实际数据的样本?我看到的所有数据都是要替换的正则表达式。但这与字符串无关吧? 数据采用pandas.dataframe 的形式,包含 100 列和数百万行各种数据类型(字符串、整数、浮点数、基数)。我用来分层的类是一个类别代码,目前有 15 个类别代码,但会增长。我的用例是 ML,而不是一些文本处理。请参考我在问题中包含的示例代码。 现在很有趣。您想要从每行中抽取最多 5 个样本的样本,对吗?即使一行少于 5 行,它们都应该存在。 【参考方案1】:

您可以使用辅助列来查找长度大于样本大小的样本并使用pd.Series.sample

例子:

df = pd.DataFrame('ErrorType':[1,2,3,4,5],
               'Samples':[np.arange(100),np.arange(10),np.arange(3),np.arange(2),np.arange(100)])

df['new'] =df['Samples'].str.len().where(df['Samples'].str.len()<5,5)
# this is let us know how many samples can be extracted per row
#0    5
#1    5
#2    3
#3    2
#4    5
Name: new, dtype: int64
# Sampling based on newly obtained column i.e 
df.apply(lambda x : pd.Series(x['Samples']).sample(x['new']).tolist(),1)

0    [52, 81, 43, 60, 46]
1         [8, 7, 0, 9, 1]
2               [2, 1, 0]
3                  [1, 0]
4    [29, 24, 16, 15, 69]
Name: sample2, dtype: object

我写了一个函数来返回带有 thresh 的样本大小,即

def get_thres_arr(sample_size,sample_length): 
    thresh = sample_length.min()
    size = np.array([thresh]*len(sample_length))
    sum_of_size = sum(size)
    while sum_of_size< sample_size:
        # If the lenght is more than threshold then increase the thresh by 1 i.e  
        size = np.where(sample_length>thresh,thresh+1,sample_length)
        sum_of_size = sum(size)
        #increment threshold
        thresh+=1
    return size

df = pd.DataFrame('ErrorType':[1,2,3,4,5,1,7,9,4,5],
                   'Samples':[np.arange(100),np.arange(10),np.arange(3),np.arange(2),np.arange(100),np.arange(100),np.arange(10),np.arange(3),np.arange(2),np.arange(100)])
ndf = pd.DataFrame('ErrorType':[1,2,3,4,5,6],
                   'Samples':[np.arange(100),np.arange(10),np.arange(3),np.arange(1),np.arange(2),np.arange(100)])


get_thres_arr(20,ndf['Samples'].str.len())
#array([5, 5, 3, 1, 2, 5])

get_thres_arr(20,df['Samples'].str.len())
#array([2, 2, 2, 2, 2, 2, 2, 2, 2, 2])

现在你得到了你可以使用的尺寸:

df['new'] = get_thres_arr(20,df['Samples'].str.len())
df.apply(lambda x : pd.Series(x['Samples']).sample(x['new']).tolist(),1)

0    [64, 89]
1      [4, 0]
2      [0, 1]
3      [1, 0]
4    [41, 80]
5    [25, 84]
6      [4, 0]
7      [2, 0]
8      [1, 0]
9     [34, 1]

希望对您有所帮助。

【讨论】:

但是您是如何计算出限制为 5 的?就我而言,它是 5,因为这保证即使某些课程的物品少于 5,我也会拿起 20 件物品。 你在代码中硬编码了 5,但没有显示你是如何得到这个幻数的。 你的代码是我的变种,只是你没有展示如何计算adj_avg @Abhijit 所以你想找到那个神奇的数字,所以从每行取样后的整个总样本量是 20 我对吗? 是和不是。如果有一些函数可以在不确定每个类的样本大小的情况下进行抽样,它也应该可以工作。【参考方案2】:

哇。让书呆子对这个嗤之以鼻。我已经编写了一个函数,它可以在 numpy 中执行您想要的操作,没有任何神奇的数字......它并不漂亮,但我不能浪费所有时间写一些东西而不将它作为答案发布。现在有两个输出 n_for_each_labelrandom_idxs 分别是每个类的选择数和随机选择的数据。我想不出当你有random_idxs 时为什么你会想要n_for_each_label

编辑: 据我所知,在 scikit 中没有执行此操作的功能,这不是为 ML 划分数据的一种非常常见的方法,所以我怀疑是否存在任何问题。

# This is your input, sample size and your labels
sample_size = 20
# in your case you'd just want y = df.ErrorType
y = np.hstack((np.ones(15), np.ones(8)*2,
               np.ones(2)*3, np.ones(3)*4,
               np.ones(12)*5))
y = y.astype(int)
# y = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2,
 #     3, 3, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5]

# Below is the function
unique_labels = np.unique(y)
bin_c = np.bincount(y)[unique_labels]
label_mat = np.ones((bin_c.shape[0], bin_c.max()), dtype=int)*-1
for i in range(unique_labels.shape[0]):
    label_loc = np.where(y == unique_labels[i])[0]
    np.random.shuffle(label_loc)
    label_mat[i, :label_loc.shape[0]] = label_loc
random_size = 0
i = 1
while random_size < sample_size:
    i += 1
    random_size = np.sum(label_mat[:, :i] != -1)

if random_size == sample_size:
    random_idxs = label_mat[:, :i]
    n_for_each_label = np.sum(random_idxs != -1, axis=1)
    random_idxs = random_idxs[random_idxs != -1]
else:
    random_idxs = label_mat[:, :i]
    last_idx = np.where(random_idxs[:, -1] != -1)[0]
    n_drop = random_size - sample_size
    drop_idx = np.random.choice(last_idx, n_drop)
    random_idxs[drop_idx, -1] = -1
    n_for_each_label = np.sum(random_idxs != -1, axis=1)
    random_idxs = random_idxs[random_idxs != -1]

输出:

n_for_each_label = 数组([5, 5, 2, 3, 5])

要抽样的每种错误类型的编号,或者如果您想跳到最后:

random_idxs = array([ 3, 11, 8, 13, 9, 22, 15, 17, 20, 18, 23, 24, 25, 26, 27, 36, 32, 38, 35, 33])

【讨论】:

你知道@ncfirth Op 正在寻找一种找到阈值的方法,即他正在寻找每行中有多少样本会导致所需的样本量,即如果他有 10,10,3,2 ,10 那么他想要 5,5,3,2,5 所以这将等于 20。如果他有 10,10,4,4,10 那么他想要 4,4,4,4,4 所以它将是等于 20。 在我的回答中的示例数据上运行你的代码,没有人能理解仅仅是代码。您应该提及如何将其插入 OPs 数据框。 我的答案中也有示例数据,不过我可以让它更明确。鉴于 OP 的问题,这是一个微不足道的联系 y=df.ErrorType【参考方案3】:

没有神奇的数字。简单地从整个人口中抽样,以明显的方式编码。

第一步是将每个“X”替换为其所在层的数字代码。这样编码后,整个人口都存储在一个字符串中,称为entire_population

>>> strata = 
>>> with open('skewed.txt') as skewed:
...     _ = next(skewed)
...     for line in skewed:
...         error_type, samples = line.rstrip().split()
...         strata[error_type] = samples
... 
>>> whole = []
>>> for _ in strata:
...     strata[_] = strata[_].replace('X', _)
...     _, strata[_]
...     whole.append(strata[_])
...     
('3', '33')
('2', '22222222')
('1', '111111111111111')
('5', '555555555555')
('4', '444')
>>> entire_population = ''.join(whole)

给定sample_size必须为20的约束,从整个总体中随机抽样,形成一个完整的样本。

>>> sample = []
>>> sample_size = 20
>>> from random import choice
>>> for s in range(sample_size):
...     sample.append(choice(entire_population))
...     
>>> sample
['2', '5', '1', '5', '1', '1', '1', '3', '5', '5', '5', '1', '5', '2', '5', '1', '2', '2', '2', '5']

最后,通过计算其中每个层的代表,将样本表征为抽样设计。

>>> from collections import Counter
>>> Counter(sample)
Counter('5': 8, '1': 6, '2': 5, '3': 1)

【讨论】:

以上是关于具有人口平衡的分层随机抽样的主要内容,如果未能解决你的问题,请参考以下文章

考虑不平衡的分层抽样分为3组

定义案例的R(分层)随机抽样

用于具有动态样本大小的分层抽样的 sql 查询

常见概率抽样方法及其适用场景总结(简单随机抽样分层抽样整群抽样系统抽样)

随机分组和随机抽样的区别

train_test_split, 关于随机抽样和分层抽样