【问题标题】:Is it possible to compute median while data is still being generated? Python online median calculator是否可以在仍在生成数据时计算中位数? Python 在线中位数计算器
【发布时间】:2020-06-12 21:26:52
【问题描述】:

我看到了这个问题的更广泛版本,个人正在寻找多个汇总统计数据,但我没有看到提出的解决方案。我只对 Python 中的中位数感兴趣。

假设我在一个循环中生成一百万个值。由于内存问题,一旦完成,我无法将百万值保存到列表中并计算中位数。是否可以边走边计算中位数?平均而言,我只是逐步对值求和,然后除以一百万。对于中位数,答案似乎并不直观。

我被困在这个“思想实验”部分,所以我无法真正尝试任何我认为可能有用的东西。我不确定这是否是已经实现的算法,但如果已经实现,我找不到它。

【问题讨论】:

  • 我不这么认为。我正在尝试理解解决方案,而我的问题在某些方面似乎有所不同:1)我正在生成要在循环和 Python 中计算中位数的值。 Python 似乎对 list/dict 构造有很高的内存开销,所以保存这些值似乎是内存禁止的。 2) 保存我在每个循环中计算的值对我来说没有任何价值。如果我能甩掉它们,那就太理想了。 3)我认为这个解决方案需要我对列表进行排序。由于内存开销,我无法在 Python 中生成所需大小的列表。
  • 你是在一些袖珍计算器上运行这个,还是为什么一百万个值的内存令人望而却步?
  • 可能值的范围是多少?它越小,就越容易。

标签: python median


【解决方案1】:

除非您的“价值”概念以某种可利用的方式受到限制,否则这是不可能的;和/或您可以对数据进行多次传递;和/或您也愿意将内容存储在磁盘上。假设您知道有 5 个值,都是不同的整数,并且您知道前 3 个是 5、6、7。中位数将是其中之一,但此时您不知道是哪一个,所以您必须记住他们都是。如果接下来是 1 和 2,则中位数为 5;如果接下来是 4 和 8,则为 6;如果接下来是 8 和 9,那就是 7。

这显然可以推广到任何奇数个值 range(i, i + 2*N+1),此时您已经看到了其中的第一个 N+1:中位数可以是第一个 N+1 中的任何一个,所以除非有一些关于价值观性质的可利用的东西,你必须在那时记住所有这些。

一个可利用的例子:你知道最多有 100 个 distinct 值。然后,您可以使用 dict 计算每个出现的次数,并根据分布的压缩表示轻松计算最后的中位数。

近似

由于已经提到的原因,这里一般没有“捷径”。但我将附上 Python 代码以获取合理的一次性逼近方法,详见"The Remedian: A Robust Averaging Method for Large Data Sets"。该论文还指出了其他近似方法。

关键:选择一个大于 1 的奇数 B。然后将连续的元素存储在缓冲区中,直到其中的 B 被记录。在这一点上,这些中位数进入下一个级别,并且缓冲区被清除。它们的中位数仍然是保留的那些B 元素的唯一记忆。

同样的模式也在更深的层次上继续存在:在B 的这些中位数中的B 中位数被记录后,那些的中位数进入下一个级别,第二个级缓冲区被清除。高级中位数仍然是进入其中的B**2 元素的唯一内存。

等等。在最坏的情况下,它可能需要存储B * log(N, B) 值,其中N 是元素的总数。在 Python 中,很容易对其进行编码,因此可以根据需要创建缓冲区,因此无需提前知道 N

如果B >= N,该方法是精确的,但是您还存储了每个元素。如果B < N,它是中位数的近似值。有关详细信息,请参阅论文 - 它非常复杂。这是一个让它看起来非常好的案例;-)

>>> import random
>>> xs = [random.random() for i in range(1000001)]
>>> sorted(xs)[500000] # true median
0.5006315438367565
>>> w = MedianEst(11)
>>> for x in xs:
...     w.add(x)
>>> w.get()
0.5008443883489089

也许令人惊讶的是,如果输入按排序顺序添加,情况会更糟:

>>> w.clear()
>>> for x in sorted(xs):
...     w.add(x)
>>> w.get()
0.5021045181828147

用户小心!代码如下:

class MedianEst:
    def __init__(self, B):
        assert B > 1 and B & 1
        self.B = B
        self.half = B >> 1
        self.clear()

    def add(self, x):
        for xs in self.a:
            xs.append(x)
            if len(xs) == self.B:
                x = sorted(xs)[self.half]
                xs.clear()
            else:
                break
        else:
            self.a.append([x])

    def get(self):
        total = 0
        weight = 1
        accum = []
        for xs in self.a:
            total += len(xs) * weight
            accum.extend((x, weight) for x in xs)
            weight *= self.B
        # `total` elements in all
        limit = total // 2 + 1
        total = 0
        for x, weight in sorted(accum):
            total += weight
            if total >= limit:
                return x

    def clear(self):
        self.a = []

【讨论】:

  • 这是一个很好的答案。我做了很多思考,除了阅读回复之外,听起来我所问的似乎是不可能的。我赞成这个答案,但我不想误导未来的读者认为我的要求是可能的,所以我不会接受这个答案。
  • 嗯,是否接受取决于您自己,但您的问题标题询问是否可能。在我看来,“不,但是……”是对您所问问题的回答;-)
猜你喜欢
  • 2011-05-07
  • 2013-01-29
  • 2018-01-24
  • 2019-04-14
  • 2019-03-07
  • 2011-12-05
  • 1970-01-01
  • 2018-07-26
相关资源
最近更新 更多