水库抽样算法
水库抽样算法为空间亚线性算法,可以在减少计算内存使用量的同时保证抽样数据的均匀性和准确性。
水库抽样算法的应用场景
- 输入:一组数据,其大小未知
- 输出:这组数据的k个均匀抽样
- 要求:
- 进扫描一次数据
- 空间复杂性位O(k)
- 扫描到数据的前n个数字时(n>k),保存当前已扫描数据的k个均匀抽样
水库抽抽样算法的实现
-
申请长度为k的数组A保存抽样;
-
首先保存最先接收的k个数据;
-
当收到第i个数据t时,生成[1,i]间的随机数j,如若j<=k,则以t替换A[j]。
第三步的随机替换保证了数据的均匀性和完整数据的概率特征。
水库抽样算法的原理
对于每个新到来的数据i,收入样本的概率为k/i。当第i+1个数据到来时,第i+1个数据被替换到抽样中的概率Pi=k/(i + 1)。而此时,前一个元素i被从中替换出的概率为Pc=1/k。则第i+1个数据替换出第i个数据的概率为PiPc,第i个数据没有被第i+1个数据替换出的概率为P=1-PiPc=1 - (k/(i + 1) * (1/k))=1 - 1/(i + 1)。
由此可知,当第i+2,i+3…个数据到来时,第i个数据没有被替换出来的概率为1 - 1/(i + 2),1 - 1/(i + 3)…
根据古典概型,当元素i被选入且保留至最后扫描完成时的概率为:
P
=
k
i
×
(
1
−
1
i
+
1
)
×
(
1
−
1
i
+
2
)
×
⋯
×
(
1
−
1
n
)
P=\frac k i\times(1-\frac {1}{i+1})\times(1-\frac 1{i+2})\times\cdots\times(1-\frac 1{n})
P=ik×(1−i+11)×(1−i+21)×⋯×(1−n1)
P = k i × i i + 1 × i + 1 i + 2 × ⋯ × n − 2 n − 1 × n − 1 n = k n P=\frac k i\times\frac{i}{i+1}\times\frac{i+1}{i+2}\times\cdots\times\frac{n-2}{n-1}\times\frac{n-1}{n}=\frac{k}{n} P=ik×i+1i×i+2i+1×⋯×n−1n−2×nn−1=nk
由公式可以推导出对于这组数据的所有元素,选入样本的概率都为k/n,这样便可以得到从A中均匀抽样的数据。
Python代码实现
创建一个类封装,定义feed()方法抽样。
class ReservoirSampling(object):
def __init__(self, size):
self._size = size
self.numlist = []
self._count = 0
self._counter = 0
def __iter__(self):
return self
def __next__(self):
self._count += 1
return self.numlist[self._count - 1]
def feed(self, item):
"""
利用迭代器每传递一个数据参数运算一次节约时间与空间
:param item: 抽样样本元素
:return:
"""
if not item:
pass
else:
num = int(item)
self._counter += 1
import random as rd
if len(self.numlist) == self._size:
index = rd.randint(0, self._counter - 1)
if index < self._size:
self.numlist[index] = num # 计判断替换与否以及替换位置
else:
self.numlist.append(num)
def __repr__(self):
return str(self.numlist)
创建一个数据生成器测试抽样算法
import random
def creator():
counter = 0
with open("data.txt", 'a') as file:
while True:
counter += 1
if counter == 100000:
break
else:
file.write(str(random.randint(1, 3)))
if __name__ == '__main__':
creator()
测试水库抽样算法
def test():
p = ReservoirSampling(10000)
with open("data.txt", 'r', encoding='UTF-8') as file:
flag = True
while flag:
num = file.read(1)
if not num:
flag = False
else:
p.feed(num)
count_1 = 0
count_2 = 0
count_3 = 0
for i in p.numlist:
if i == 1:
count_1 += 1
elif i == 2:
count_2 += 1
else:
count_3 += 1
print("出现1的概率是{}".format(count_1 / 10000))
print("出现2的概率是{}".format(count_2 / 10000))
print("出现3的概率是{}".format(count_3 / 10000))
测试结果为:
出现1的概率是0.3366
出现2的概率是0.3335
出现3的概率是0.3299
满足1、2、3各占1/3的数据概率特点,抽样算法可以比较好的保留数据的概率特征。