C语言的滚动中值算法


114

我目前正在研究一种算法,以在C语言中实现滚动中值过滤器(类似于滚动均值过滤器)。从我对文献的搜索来看,似乎有两种相当有效的方法来实现。首先是对值的初始窗口进行排序,然后执行二进制搜索以插入新值,并在每次迭代时都删除现有值。

第二种方法(来自Hardle和Steiger,1995年,JRSS-C,算法296)构建了一个双端堆结构,一端为maxheap,另一端为minheap,中间为中值。这产生了线性时间算法,而不是O(n log n)。

这是我的问题:实施前者是可行的,但是我需要在数百万个时间序列上运行它,因此效率非常重要。事实证明,后者非常难以实施。我在R的stats软件包的代码的Trunmed.c文件中找到了代码,但这是相当难以理解的。

有人知道线性时间滚动中值算法的编写良好的C实现吗?

编辑:链接到Trunmed.c代码http://google.com/codesearch/p?hl=zh-CN&sa=N&cd=1&ct=rc#mYw3h_Lb_e0/R-2.2.0/src/library/stats/src/Trunmed.c


刚刚实施了移动均线...移动中位数有些棘手。尝试谷歌搜索移动中位数。
马特

尝试谷歌和谷歌代码搜索。它为Trunmed代码的SGI端口提供了Trunmed.c代码和另一种语言的实现(据我所知)。另外,我引用的JRSS算法显然是该期刊丛书中唯一未存档原始代码的算法。
AWB

您在每个时间序列中有多少个数字?即使有一百万个,如果您只有几千个数字,则运行它可能不会超过一两分钟(如果您的代码编写高效)。
Dana the Sane,

16
两堆解决方案如何线性?它是O(n log k),其中k是窗口大小,因为堆的删除是O(log k)。
yairchu

Answers:


28

我已经看过R src/library/stats/src/Trunmed.c几次了,因为我在独立的C ++类/ C子例程中也想要类似的东西。请注意,这实际上是两个实现,请参阅src/library/stats/man/runmed.Rd(帮助文件的源),其中说

\details{
  Apart from the end values, the result \code{y = runmed(x, k)} simply has
  \code{y[j] = median(x[(j-k2):(j+k2)])} (k = 2*k2+1), computed very
  efficiently.

  The two algorithms are internally entirely different:
  \describe{
    \item{"Turlach"}{is the Härdle-Steiger
      algorithm (see Ref.) as implemented by Berwin Turlach.
      A tree algorithm is used, ensuring performance \eqn{O(n \log
        k)}{O(n * log(k))} where \code{n <- length(x)} which is
      asymptotically optimal.}
    \item{"Stuetzle"}{is the (older) Stuetzle-Friedman implementation
      which makes use of median \emph{updating} when one observation
      enters and one leaves the smoothing window.  While this performs as
      \eqn{O(n \times k)}{O(n * k)} which is slower asymptotically, it is
      considerably faster for small \eqn{k} or \eqn{n}.}
  }
}

很高兴看到它以更独立的方式重新使用。你在自愿吗?我可以帮助一些R位。

编辑1:除了上面的Trunmed.c的旧版本的链接之外,这是当前的SVN副本

编辑2:Ryan Tibshirani在快速中值装仓中有一些C和Fortran代码,这可能是开窗方法的合适起点。


谢谢德克。获得干净的解决方案后,我计划根据GPL发布它。我也会对设置R和Python接口感兴趣。
AWB

9
@AWB这个想法到底发生了什么?您是否将解决方案整合到软件包中?
徐王

20

我找不到具有订单统计信息的c ++数据结构的现代实现,因此最终在MAK建议的顶级编码器链接中实现了这两种想法(匹配社论:向下滚动至FloatingMedian)。

两个多集

第一个想法将数据划分为两个数据结构(堆,多集等),每个插入/删除的数据为O(ln N),不允许动态更改分位数而不花费大量成本。也就是说,我们可以有一个滚动的中位数,或滚动的75%,但不能同时有两个。

段树

第二个想法使用一个O(ln N)的段树来进行插入/删除/查询,但更为灵活。最重要的是,“ N”是数据范围的大小。因此,如果您的滚动中位数的窗口数为一百万,但是您的数据与1..65536有所不同,则每滚动一百万窗口仅需要进行16次运算!!

该c ++代码类似于上面的丹尼斯(Denis)发表的内容(“这是量化数据的简单算法”)

GNU订单统计树

在放弃之前,我发现stdlibc ++包含订单统计树!!!

这些有两个关键操作:

iter = tree.find_by_order(value)
order = tree.order_of_key(value)

请参见libstdc ++手册policy_based_data_structures_test(搜索“拆分和联接 ”)。

我将树包装在方便标头中,以用于支持c ++ 0x / c ++ 11样式的部分typedefs的编译器:

#if !defined(GNU_ORDER_STATISTIC_SET_H)
#define GNU_ORDER_STATISTIC_SET_H
#include <ext/pb_ds/assoc_container.hpp>
#include <ext/pb_ds/tree_policy.hpp>

// A red-black tree table storing ints and their order
// statistics. Note that since the tree uses
// tree_order_statistics_node_update as its update policy, then it
// includes its methods by_order and order_of_key.
template <typename T>
using t_order_statistic_set = __gnu_pbds::tree<
                                  T,
                                  __gnu_pbds::null_type,
                                  std::less<T>,
                                  __gnu_pbds::rb_tree_tag,
                                  // This policy updates nodes'  metadata for order statistics.
                                  __gnu_pbds::tree_order_statistics_node_update>;

#endif //GNU_ORDER_STATISTIC_SET_H

实际上,的libstdc ++扩展的容器都不会允许多个值!设计!正如我上面的名称(t_order_statistic_set)所建议的,多个值被合并。因此,出于我们的目的,他们需要做更多的工作:-(
Leo Goodstadt,2012年

我们需要1)制作一个要映射的值映射(而不是集合)2)分支大小应反映键的计数(libstdc ++-v3 / include / ext / pb_ds / detail / tree_policy / order_statistics_imp.hpp)继承树,以及3)重载insert()以增加计数/如果值已经存在,则调用update_to_top()4)重载delete()以减少计数/如果值不是唯一的,则调用update_to_top()(请参阅libstdc ++- v3 / include / ext / pb_ds / detail / rb_tree_map_ / rb_tree_.hpp)有志愿者吗?
Leo Goodstadt 2012年

15

我在这里做了一个C实现。这个问题还有更多细节:C-Turlach实现中的滚动中位数

用法示例:

int main(int argc, char* argv[])
{
   int i,v;
   Mediator* m = MediatorNew(15);

   for (i=0;i<30;i++)
   {
      v = rand()&127;
      printf("Inserting %3d \n",v);
      MediatorInsert(m,v);
      v=MediatorMedian(m);
      printf("Median = %3d.\n\n",v);
      ShowTree(m);
   }
}

6
基于min-max-max堆的出色,快速和清晰的实现。很好
约翰内斯·鲁道夫,

如何找到此解决方案的Java版本?
Hengameh 2015年

10

我使用此增量中位数估算器:

median += eta * sgn(sample - median)

与更常见的均值估算器具有相同的形式:

mean += eta * (sample - mean)

此处eta是一个小的学习率参数(例如0.001),并且sgn()是返回之一的信号函数{-1, 0, 1}。(eta如果数据是不稳定的,并且您想跟踪随时间的变化,请使用这样的常数;否则,对于固定源使用类似的方法eta = 1 / n进行收敛,n到目前为止所看到的样本数在哪里。)

另外,我修改了中位数估算器,使其适用于任意分位数。通常,分位数功能会告诉您将数据分为两个部分的值:p1 - p。以下是对该值的递增估算:

quantile += eta * (sgn(sample - quantile) + 2.0 * p - 1.0)

该值p应在范围内[0, 1]。这实质上将sgn()函数的对称输出{-1, 0, 1}移向一侧,从而将数据样本分为两个大小不等的分箱(数据的分数p1 - p分别小于/大于分位数估计)。请注意,对于p = 0.5,这将减少为中位数估计量。


2
太酷了,这是一个根据运行平均值调整'eta'的修改...(均值用作中位数的粗略估计,因此它以收敛于微小值的相同速率收敛于大值)。即eta自动调整。 stackoverflow.com/questions/11482529/...
杰夫·麦克林托克

3
对于类似的技术,请参见关于节俭流的这篇论文:arxiv.org/pdf/1407.1121v1.pdf 它可以估计任何四分位数并适应均值的变化。它只需要存储两个值:最后一个估计值和最后一个调整方向(+1或-1)。该算法易于实现。我发现错误大约在97%的时间内在5%以内。
Paul Chernoch

9

这是一个量化数据的简单算法(数月后):

""" median1.py: moving median 1d for quantized, e.g. 8-bit data

Method: cache the median, so that wider windows are faster.
    The code is simple -- no heaps, no trees.

Keywords: median filter, moving median, running median, numpy, scipy

See Perreault + Hebert, Median Filtering in Constant Time, 2007,
    http://nomis80.org/ctmf.html: nice 6-page paper and C code,
    mainly for 2d images

Example:
    y = medians( x, window=window, nlevel=nlevel )
    uses:
    med = Median1( nlevel, window, counts=np.bincount( x[0:window] ))
    med.addsub( +, - )  -- see the picture in Perreault
    m = med.median()  -- using cached m, summ

How it works:
    picture nlevel=8, window=3 -- 3 1s in an array of 8 counters:
        counts: . 1 . . 1 . 1 .
        sums:   0 1 1 1 2 2 3 3
                        ^ sums[3] < 2 <= sums[4] <=> median 4
        addsub( 0, 1 )  m, summ stay the same
        addsub( 5, 1 )  slide right
        addsub( 5, 6 )  slide left

Updating `counts` in an `addsub` is trivial, updating `sums` is not.
But we can cache the previous median `m` and the sum to m `summ`.
The less often the median changes, the faster;
so fewer levels or *wider* windows are faster.
(Like any cache, run time varies a lot, depending on the input.)

See also:
    scipy.signal.medfilt -- runtime roughly ~ window size
    http://stackoverflow.com/questions/1309263/rolling-median-algorithm-in-c

"""

from __future__ import division
import numpy as np  # bincount, pad0

__date__ = "2009-10-27 oct"
__author_email__ = "denis-bz-py at t-online dot de"


#...............................................................................
class Median1:
    """ moving median 1d for quantized, e.g. 8-bit data """

    def __init__( s, nlevel, window, counts ):
        s.nlevel = nlevel  # >= len(counts)
        s.window = window  # == sum(counts)
        s.half = (window // 2) + 1  # odd or even
        s.setcounts( counts )

    def median( s ):
        """ step up or down until sum cnt to m-1 < half <= sum to m """
        if s.summ - s.cnt[s.m] < s.half <= s.summ:
            return s.m
        j, sumj = s.m, s.summ
        if sumj <= s.half:
            while j < s.nlevel - 1:
                j += 1
                sumj += s.cnt[j]
                # print "j sumj:", j, sumj
                if sumj - s.cnt[j] < s.half <= sumj:  break
        else:
            while j > 0:
                sumj -= s.cnt[j]
                j -= 1
                # print "j sumj:", j, sumj
                if sumj - s.cnt[j] < s.half <= sumj:  break
        s.m, s.summ = j, sumj
        return s.m

    def addsub( s, add, sub ):
        s.cnt[add] += 1
        s.cnt[sub] -= 1
        assert s.cnt[sub] >= 0, (add, sub)
        if add <= s.m:
            s.summ += 1
        if sub <= s.m:
            s.summ -= 1

    def setcounts( s, counts ):
        assert len(counts) <= s.nlevel, (len(counts), s.nlevel)
        if len(counts) < s.nlevel:
            counts = pad0__( counts, s.nlevel )  # numpy array / list
        sumcounts = sum(counts)
        assert sumcounts == s.window, (sumcounts, s.window)
        s.cnt = counts
        s.slowmedian()

    def slowmedian( s ):
        j, sumj = -1, 0
        while sumj < s.half:
            j += 1
            sumj += s.cnt[j]
        s.m, s.summ = j, sumj

    def __str__( s ):
        return ("median %d: " % s.m) + \
            "".join([ (" ." if c == 0 else "%2d" % c) for c in s.cnt ])

#...............................................................................
def medianfilter( x, window, nlevel=256 ):
    """ moving medians, y[j] = median( x[j:j+window] )
        -> a shorter list, len(y) = len(x) - window + 1
    """
    assert len(x) >= window, (len(x), window)
    # np.clip( x, 0, nlevel-1, out=x )
        # cf http://scipy.org/Cookbook/Rebinning
    cnt = np.bincount( x[0:window] )
    med = Median1( nlevel=nlevel, window=window, counts=cnt )
    y = (len(x) - window + 1) * [0]
    y[0] = med.median()
    for j in xrange( len(x) - window ):
        med.addsub( x[j+window], x[j] )
        y[j+1] = med.median()
    return y  # list
    # return np.array( y )

def pad0__( x, tolen ):
    """ pad x with 0 s, numpy array or list """
    n = tolen - len(x)
    if n > 0:
        try:
            x = np.r_[ x, np.zeros( n, dtype=x[0].dtype )]
        except NameError:
            x += n * [0]
    return x

#...............................................................................
if __name__ == "__main__":
    Len = 10000
    window = 3
    nlevel = 256
    period = 100

    np.set_printoptions( 2, threshold=100, edgeitems=10 )
    # print medians( np.arange(3), 3 )

    sinwave = (np.sin( 2 * np.pi * np.arange(Len) / period )
        + 1) * (nlevel-1) / 2
    x = np.asarray( sinwave, int )
    print "x:", x
    for window in ( 3, 31, 63, 127, 255 ):
        if window > Len:  continue
        print "medianfilter: Len=%d window=%d nlevel=%d:" % (Len, window, nlevel)
            y = medianfilter( x, window=window, nlevel=nlevel )
        print np.array( y )

# end median1.py

4

滚动中位数可以通过维护两个数字分区来找到。

要维护分区,请使用“最小堆”和“最大堆”。

“最大堆”将包含小于等于中位数的数字。

最小堆将包含大于等于中位数的数字。

平衡约束: 如果元素总数为偶数,则两个堆都应具有相等的元素。

如果元素总数为奇数,则“最大堆”将比“最小堆”多一个元素。

中值元素:如果两个分区的元素数相等,则中值将是第一个分区的max元素和第二个分区的min元素之和的一半。

否则,中值将是来自第一个分区的max元素。

算法-
1-取两个堆(1个最小堆和1个最大堆)
   最大堆将包含元素的前一半
   最小堆将包含元素的下半部分

2-将流中的新数字与最大堆的顶部进行比较, 
   如果小于或等于该数字,则将其添加到最大堆中。 
   否则在“最小堆”中添加数字。

3-如果最小堆比最大堆具有更多元素 
   然后删除“最小堆”的顶部元素,并添加“最大堆”。
   如果最大堆比最小堆多于一个元素 
   然后删除最大堆的顶部元素并添加最小堆。

4-如果两个堆的元素数相等,则
   中位数将是“最大堆”中最大元素和“最小堆”中最小元素之和的一半。
   否则,中位数将是第一个分区中的max元素。
public class Solution {

    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        RunningMedianHeaps s = new RunningMedianHeaps();
        int n = in.nextInt();
        for(int a_i=0; a_i < n; a_i++){
            printMedian(s,in.nextInt());
        }
        in.close();       
    }

    public static void printMedian(RunningMedianHeaps s, int nextNum){
            s.addNumberInHeap(nextNum);
            System.out.printf("%.1f\n",s.getMedian());
    }
}

class RunningMedianHeaps{
    PriorityQueue<Integer> minHeap = new PriorityQueue<Integer>();
    PriorityQueue<Integer> maxHeap = new PriorityQueue<Integer>(Comparator.reverseOrder());

    public double getMedian() {

        int size = minHeap.size() + maxHeap.size();     
        if(size % 2 == 0)
            return (maxHeap.peek()+minHeap.peek())/2.0;
        return maxHeap.peek()*1.0;
    }

    private void balanceHeaps() {
        if(maxHeap.size() < minHeap.size())
        {
            maxHeap.add(minHeap.poll());
        }   
        else if(maxHeap.size() > 1+minHeap.size())
        {
            minHeap.add(maxHeap.poll());
        }
    }

    public void addNumberInHeap(int num) {
        if(maxHeap.size()==0 || num <= maxHeap.peek())
        {
            maxHeap.add(num);
        }
        else
        {
            minHeap.add(num);
        }
        balanceHeaps();
    }
}

对我来说尚不清楚,第三个Java答案为C问题带来了多少好处。您应该提出一个新问题,然后在该问题中提供Java答案。
jww

读完“然后删除Min Heap的顶部元素并添加Min Heap”后,逻辑就死了。至少在发布之前有礼貌地阅读算法
-Cyclotron3x3

4
该算法不是针对滚动中位数,而是针对越来越多的元素的中位数。对于滚动中位数,还必须从堆中删除一个元素,该元素需要首先找到。
沃尔特

2

可能值得指出的是,有一个特殊情况具有简单的精确解决方案:当流中的所有值都是(相对)较小的定义范围内的整数时。例如,假设它们必须都在0到1023之间。在这种情况下,只需定义一个包含1024个元素和一个计数的数组,然后清除所有这些值即可。对于流中的每个值,增加相应的bin和计数。在流结束之后,找到包含count / 2个最大值的bin-通过添加从0开始的连续bin可以轻松实现。使用相同的方法,可以找到任意排名顺序的值。(如果需要在运行过程中检测存储箱饱和并将存储箱的大小“升级”为更大的类型,则情况会很复杂。)

这种特殊情况似乎是人为的,但在实践中却很常见。如果实数在一定范围内并且已知“足够好”的精度水平,它也可以用作实数的近似值。这几乎可以对一组“现实世界”对象进行任何一组测量。例如,一群人的身高或体重。设置不够大吗?假设有人可以提供数据,它对于地球上所有(个体)细菌的长度或重量也同样适用。

似乎我误读了原始图像-似乎它想要一个滑动窗口的中间值,而不是长流的中间值。这种方法仍然适用。为初始窗口加载前N个流值,然后为第N + 1个流值增加相应的bin,同时递减与第0个流值对应的bin。在这种情况下,必须保留最后的N个值以允许递减,这可以通过循环寻址大小为N的数组来有效地完成。由于中位数的位置只能以-2,-,1,0,1改变,2在滑动窗口的每个步骤中,不必将所有bin总计到每个步骤的中位数,只需根据修改了bin的哪一侧来调整“中值指针”即可。例如,如果新值和要删除的值均低于当前中位数,则它不会改变(偏移= 0)。当N太大而无法方便地保存在内存中时,该方法就会失效。


1

如果您能够根据时间点引用值,则可以通过替换采样值,应用自举以在置信区间内生成自举中值。与不断将输入值排序到数据结构中相比,这可以让您以更高的效率计算近似中位数。


1

对于那些需要Java中的运行中位数的人... PriorityQueue是您的朋友。O(log N)插入,O(1)当前中值,O(N)删除。如果您知道数据的分布,则可以做得更好。

public class RunningMedian {
  // Two priority queues, one of reversed order.
  PriorityQueue<Integer> lower = new PriorityQueue<Integer>(10,
          new Comparator<Integer>() {
              public int compare(Integer arg0, Integer arg1) {
                  return (arg0 < arg1) ? 1 : arg0 == arg1 ? 0 : -1;
              }
          }), higher = new PriorityQueue<Integer>();

  public void insert(Integer n) {
      if (lower.isEmpty() && higher.isEmpty())
          lower.add(n);
      else {
          if (n <= lower.peek())
              lower.add(n);
          else
              higher.add(n);
          rebalance();
      }
  }

  void rebalance() {
      if (lower.size() < higher.size() - 1)
          lower.add(higher.remove());
      else if (higher.size() < lower.size() - 1)
          higher.add(lower.remove());
  }

  public Integer getMedian() {
      if (lower.isEmpty() && higher.isEmpty())
          return null;
      else if (lower.size() == higher.size())
          return (lower.peek() + higher.peek()) / 2;
      else
          return (lower.size() < higher.size()) ? higher.peek() : lower
                  .peek();
  }

  public void remove(Integer n) {
      if (lower.remove(n) || higher.remove(n))
          rebalance();
  }
}

c ++在标准库的扩展中具有gnu的顺序统计树。请参阅下面的我的帖子。
Leo Goodstadt 2012年

我认为您的代码未正确放置在此处。有一些不完整的部分,例如:}), higher = new PriorityQueue<Integer>();new PriorityQueue<Integer>(10,。我无法运行代码。
Hengameh 2015年

@Hengameh Java以分号结束语句-换行符根本不重要。您必须复制不正确。

您应该提出一个新问题,然后在该问题中提供Java答案。
jww

0

当确切的输出不重要(出于显示目的等)时,可以使用此方法。您需要totalcount和lastmedian以及newvalue。

{
totalcount++;
newmedian=lastmedian+(newvalue>lastmedian?1:-1)*(lastmedian==0?newvalue: lastmedian/totalcount*2);
}

对诸如page_display_time之类的东西产生相当精确的结果。

规则:输入流需要按页面显示时间的顺序平滑,计数大(> 30等),并且中位数必须非零。

示例:页面加载时间,800个项目,10ms ... 3000ms,平均90ms,实际中位数:11ms

输入30次后,中位数误差通常<= 20%(9ms..12ms),并且误差越来越小。输入800次后,误差为+ -2%。

另一个具有类似解决方案的思想家在这里:中值滤波器超高效实现


-1

这是java实现

package MedianOfIntegerStream;

import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.TreeSet;


public class MedianOfIntegerStream {

    public Set<Integer> rightMinSet;
    public Set<Integer> leftMaxSet;
    public int numOfElements;

    public MedianOfIntegerStream() {
        rightMinSet = new TreeSet<Integer>();
        leftMaxSet = new TreeSet<Integer>(new DescendingComparator());
        numOfElements = 0;
    }

    public void addNumberToStream(Integer num) {
        leftMaxSet.add(num);

        Iterator<Integer> iterMax = leftMaxSet.iterator();
        Iterator<Integer> iterMin = rightMinSet.iterator();
        int maxEl = iterMax.next();
        int minEl = 0;
        if (iterMin.hasNext()) {
            minEl = iterMin.next();
        }

        if (numOfElements % 2 == 0) {
            if (numOfElements == 0) {
                numOfElements++;
                return;
            } else if (maxEl > minEl) {
                iterMax.remove();

                if (minEl != 0) {
                    iterMin.remove();
                }
                leftMaxSet.add(minEl);
                rightMinSet.add(maxEl);
            }
        } else {

            if (maxEl != 0) {
                iterMax.remove();
            }

            rightMinSet.add(maxEl);
        }
        numOfElements++;
    }

    public Double getMedian() {
        if (numOfElements % 2 != 0)
            return new Double(leftMaxSet.iterator().next());
        else
            return (leftMaxSet.iterator().next() + rightMinSet.iterator().next()) / 2.0;
    }

    private class DescendingComparator implements Comparator<Integer> {
        @Override
        public int compare(Integer o1, Integer o2) {
            return o2 - o1;
        }
    }

    public static void main(String[] args) {
        MedianOfIntegerStream streamMedian = new MedianOfIntegerStream();

        streamMedian.addNumberToStream(1);
        System.out.println(streamMedian.getMedian()); // should be 1

        streamMedian.addNumberToStream(5);
        streamMedian.addNumberToStream(10);
        streamMedian.addNumberToStream(12);
        streamMedian.addNumberToStream(2);
        System.out.println(streamMedian.getMedian()); // should be 5

        streamMedian.addNumberToStream(3);
        streamMedian.addNumberToStream(8);
        streamMedian.addNumberToStream(9);
        System.out.println(streamMedian.getMedian()); // should be 6.5
    }
}

您应该提出一个新问题,然后在该问题中提供Java答案。
jww

-4

如果您只需要平滑的平均值,一种快速/简便的方法是将最新值乘以x,将平均值乘以(1-x),然后将它们相加。然后,这成为新的平均值。

编辑:不是用户所要求的,也不是统计学上有效的,但足以用于许多用途。
我将把它留在这里(尽管投票否定)以进行搜索!


2
这将计算平均值。他想要中位数。此外,他正在计算值的滑动窗口的中位数,而不是整个集合的中值。
A. Levy,

1
这将计算一个值窗口的运行平均值,该值的衰减常数取决于X-这在性能很重要的情况下非常有用,您不必费心做一个卡尔曼滤波器。我将其放入以便搜索可以找到它。
马丁·贝克特

这也是我立即想到的,已将这样的滤波器实现为音频应用程序的一种非常基本且便宜的低通滤波器。
詹姆斯·莫里斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.