快速估算项目数超过给定阈值的方法?概率数据结构?

时间:2015-10-11 19:26:38

标签: algorithm data-structures clojure hyperloglog

我有一个很大的值列表,从0到100,000范围内绘制(为清晰起见,此处表示为字母)。每个输入中可能有几千个项目。

[a a a a b b b b c f d b c f ... ]

我想查找计数超过特定阈值的数字计数。例如,如果阈值为3,则答案为{a: 4, b: 5}

显而易见的方法是按身份分组,计算每个分组然后过滤。

这是一个与语言无关的问题,但在Clojure中(如果你不了解Clojure,不要推迟!):

(filter (fn [[k cnt]] (> cnt threshold)) (frequencies input))

此函数在非常大量的输入上运行,每个输入都非常大,因此分组和过滤是一项昂贵的操作。我想找到某种保护功能,如果输入永远不会产生超过给定阈值的任何输出或以其他方式划分问题空间,它将提前返回。例如,最简单的是if the size of the input is less than the size of the threshold return nil

我正在寻找一个更好的保护功能,如果输入不能产生任何输出,它将跳过计算。或者更快的方式来产生输出。

显然它必须比分组本身便宜。一个很好的解决方案涉及不同输入集的输入计数,但结果与分组一样昂贵......

我知道概率数据结构可能占据了关键。有什么想法吗?

(我标记了hyerloglog,虽然我不认为它适用,因为它不提供计数)

3 个答案:

答案 0 :(得分:0)

您可能希望看一下Narrator。它专为分析和汇总数据流而设计。

一个简单的query-seq来做你最初的事情是:

(require '[narrator.query :refer [query-seq query-stream]])
(require '[narrator.operators :as n])

(def my-seq [:a :a :b :b :b :b :c :a :b :c])
(query-seq (n/group-by identity n/rate) my-seq)
==> {:a 3, :b 5, :c 2}

您可以按照建议进行过滤。

您可以使用quasi-cardinality快速确定样本中的唯一项目数(以及您的分区问题)。它使用HyperLogLog基数估计算法,例如

(query-seq (n/quasi-cardinality) my-seq)
==> 3

quasi-frequency-by在此处演示:

(defn freq-in-seq
  "returns a function that, when given a value, returns the frequency of that value in the sequence s
   e.g. ((freq-in-seq [:a :a :b :c]) :a)  ==> 2"
  [s]
  (query-seq (n/quasi-frequency-by identity) s))

((freq-in-seq my-seq) :a) ==> 3

quasi-distinct-by

(query-seq (n/quasi-distinct-by identity) my-seq)
==> [:a :b :c]

还有query-stream的实时流分析。

以下是向您展示如何对流进行抽样以计算“期间”变化的信息。读取的值:

(s/stream->seq 
  (->> my-seq
       (map #(hash-map :timestamp %1 :value %2) (range))
       (query-stream (n/group-by identity n/rate) 
                     {:value :value :timestamp :timestamp :period 3})))
==> ({:timestamp 3, :value {:a 2, :b 1}} {:timestamp 6, :value {:b 3}} {:timestamp 9, :value {:a 1, :b 1, :c 1}} {:timestamp 12, :value {:c 1}})

结果是每3个项目(期间3)进行一系列更改,并带有适当的时间戳。

您还可以编写自定义流聚合器,这可能就是如何在上面的流中累积值。我快速地使用了这些,并且非常糟糕地使它工作(仅在我的午休时间),但这在它的位置起作用:

(defn lazy-value-accum
  ([s] (lazy-value-accum s {}))
  ([s m]
   (when-not (empty? s)
     (lazy-seq
      (let [new-map (merge-with + m (:value (first s)))]
        (cons new-map
              (lazy-value-accum (rest s) new-map))))))


(lazy-value-accum
  (s/stream->seq 
    (->> my-seq
         (map #(hash-map :timestamp %1 :value %2) (range))
         (query-stream (n/group-by identity n/rate) 
                       {:value :value :timestamp :timestamp :period 3}))))
==> ({:a 2, :b 1} {:a 2, :b 4} {:a 3, :b 5, :c 1} {:a 3, :b 5, :c 2})

显示每个period样本后每个值的逐渐累计计数,可以懒惰地读取。

答案 1 :(得分:0)

如何使用partition-all生成最大大小为n的分区的惰性列表,在每个分区上应用频率,合并它们然后过滤最终地图?

(defn lazy-count-and-filter
  [coll n threshold]
  (filter #(< threshold (val %))
          (apply (partial merge-with +) 
                 (map frequencies 
                      (partition-all n coll)))))

例如:

(lazy-count-and-filter [:a :c :b :c :a :d :a] 2 1)
==> ([:a 3] [:c 2])

答案 2 :(得分:0)

如果您希望加速单个节点上的工作,请考虑使用reducers或core.async,如this blog post所示。

如果这是一个非常大的数据集,并且经常需要此操作,并且您有资源拥有多节点群集,则可以考虑设置Storm或Onyx。

实际上,听起来像减速器会给你最少的工作带来的好处。通过我列出的所有选项,更强大/更灵活/更快的解决方案需要更多时间来预先理解。从最简单到最强大的顺序,它们是reducer,core.async,Storm,Onyx。