Роллинг-срединный алгоритм в C

В настоящее время я работаю над алгоритмом реализации скользящего медианного фильтра (аналогичного среднему фильтру качения) в C. Из моего поиска литературы есть два достаточно эффективных способа сделать это. Первый - сортировать начальное окно значений, затем выполнять двоичный поиск для вставки нового значения и удаления существующего на каждой итерации.

Второй (от Hardle and Steiger, 1995, JRSS-C, Алгоритм 296) строит структуру с двойным концом, с максимальным переходом на одном конце, минером на другом, а срединным - посередине. Это дает алгоритм с линейным временем, а не один, который равен O (n log n).

Вот моя проблема: реализация первого выполнима, но мне нужно запускать это на миллионы временных рядов, поэтому эффективность имеет большое значение. Последнее очень сложно реализовать. Я нашел код в файле Trunmed.c кода для пакета статистики R, но он довольно неразборчив.

Кто-нибудь знает о хорошо написанной реализации C для линейного математического алгоритма, скользящего по времени?

Изменить: Ссылка на код Trunmed.c http://google.com/codesearch/p?hl=en&sa=N&cd=1&ct=rc#mYw3h_Lb_e0/R-2.2.0/src/library/stats/src/Trunmed.c

Ответ 1

Я просмотрел R src/library/stats/src/Trunmed.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-копии

  • Srunmed.c (для версии Stuetzle)
  • Trunmed.c (для версии Turlach)
  • runmed.R для функции R, вызывающей эти

Редактировать 2: у Райана Тибширани есть код C и Fortran на быстрый медианный битнинг, который может стать подходящей отправной точкой для оконного подхода.

Ответ 2

Я не мог найти современную реализацию структуры данных С++ с порядковой статистикой, так что в итоге реализовано обе идеи в ссылке верхнего кодера, предложенные MAK (Match Editorial: прокрутите вниз до FloatingMedian).

Два мультисети

Первая идея разделяет данные на две структуры данных (кучи, мультимножества и т.д.) с O (ln N) на вставку/удаление, не позволяет динамически изменять квантиль без больших затрат. То есть мы можем иметь скользящую медианную или прокатывающую 75%, но не обе в то же время.

Дерево сегментов

Вторая идея использует дерево сегментов, которое является O (ln N) для вставки/удаления/запросов, но более гибкое. Лучше всего "N" - это размер вашего диапазона данных. Поэтому, если у вашей скользящей медианной есть окно из миллиона предметов, но ваши данные варьируются от 1..65536, тогда требуется только 16 операций за движение окна прокатки 1 миллион!

Код С++ похож на то, что опубликовал Денис ( "Здесь простой алгоритм для квантованных данных" )

Статистика статистики заказа GNU

Как раз перед тем, как сдаться, я обнаружил, что stdlibС++ содержит деревья статистики заказов!!!

Они имеют две критические операции:

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

См. руководство по libstdС++ policy_based_data_structures_test (поиск по "split and join" ).

Я обернул дерево для использования в заголовке удобства для компиляторов, поддерживающих частичные типы typedefs С++ 0x/С++ 11:

#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

Ответ 4

Здесь простой алгоритм для квантованных данных (месяцев спустя):

""" 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

Ответ 5

Я использую эту инкрементную среднюю оценку:

median += eta * sgn(sample - median)

который имеет ту же форму, что и более обычная средняя оценка:

mean += eta * (sample - mean)

Здесь eta - небольшой параметр скорости обучения (например, 0,001), а sgn() - функция signum, которая возвращает один из {-1, 0, 1}. (Используйте константу eta, как это, если данные нестационарны и вы хотите отслеживать изменения с течением времени, в противном случае для стационарных источников сходится что-то вроде eta = 1/n для схождения, где n - количество образцов, которые были просмотрены до сих пор. )

Кроме того, я изменил среднюю оценку, чтобы заставить ее работать для произвольных квантилей. В целом функция квантиля (http://en.wikipedia.org/wiki/Quantile_function) сообщает вам значение, которое делит данные на две фракции: p и 1-p. Следующие значения оцениваются пошагово:

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

Значение p должно быть в пределах [0,1]. Это существенно сдвигает функцию симметричного выхода sgn() {-1,0,1}, наклоняясь в сторону одной стороны, разбивая образцы данных на две ячейки неравномерного размера (доли p и 1-p данных меньше/больше, чем оценка квантиля, соответственно). Заметим, что при р = 0,5 это сводится к медианной оценке.

Ответ 6

Роллинговая медиана может быть найдена путем сохранения двух разделов чисел.

Для поддержки разделов используйте Min Heap и Max Heap.

Max Heap будет содержать числа, меньшие, чем средние.

Min Heap будет содержать числа, превышающие средние значения.

Балансировочное ограничение: если общее количество элементов равно, то обе кучи должны иметь равные элементы.

Если общее число элементов нечетно, то Max Heap будет иметь еще один элемент, чем Min Heap.

Median Element: Если оба раздела имеют равное количество элементов, то медиана будет составлять половину суммы максимального элемента из первого раздела и элемента min из второго раздела.

В противном случае медиана будет максимальным элементом из первого раздела.

Algorithm-
1- Take two Heap(1 Min Heap and 1 Max Heap)
   Max Heap will contain first half number of elements
   Min Heap will contain second half number of elements

2- Compare new number from stream with top of Max Heap, 
   if it is smaller or equal add that number in max heap. 
   Otherwise add number in Min Heap.

3- if min Heap has more elements than Max Heap 
   then remove top element of Min Heap and add in Max Heap.
   if max Heap has more than one element than in Min Heap 
   then remove top element of Max Heap and add in Min Heap.

4- If Both heaps has equal number of elements then
   median will be half of sum of max element from Max Heap and min element from Min Heap.
   Otherwise median will be max element from the first partition.
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();
    }
}

Ответ 7

Если у вас есть возможность ссылаться на значения как функции точек во времени, вы можете выбирать значения с заменой, применяя bootstrapping для генерации загруженного медиана значения в пределах доверительных интервалов. Это может позволить вам рассчитать приблизительную медиану с большей эффективностью, чем постоянная сортировка входящих значений в структуру данных.

Ответ 8

Для тех, кому нужна работающая медиана в 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();
  }
}

Ответ 9

Возможно, стоит отметить, что существует специальный случай, который имеет простое точное решение: когда все значения в потоке являются целыми числами в (относительно) малом определенном диапазоне. Например, предположим, что все они должны находиться между 0 и 1023. В этом случае просто определите массив из 1024 элементов и счетчик и очистите все эти значения. Для каждого значения в приращении потока соответствующий бит и счетчик. После того, как поток заканчивается, найдите бит, который содержит наивысшее значение count/2 - легко достигается добавлением последовательных бункеров, начиная с 0. Используя тот же метод, можно найти значение произвольного порядка ранга. (Существует незначительная сложность, если потребуется обнаружение насыщения бункера и "обновление" размеров ящиков для хранения до более крупного типа во время прогона.)

Этот частный случай может показаться искусственным, но на практике это очень распространено. Его также можно применять в качестве приближения для действительных чисел, если они лежат в пределах диапазона и известен "достаточно хороший" уровень точности. Это будет иметь место практически для любого набора измерений в группе объектов "реального мира". Например, высоты или веса группы людей. Не достаточно большой набор? Он будет работать так же хорошо, как длина или вес всех (отдельных) бактерий на планете - если кто-то может предоставить данные!

Похоже, я неправильно читаю оригинал - похоже, что он хочет скользящего окна, а не только медиану очень длинного потока. Этот подход все еще работает для этого. Загрузите первые значения N потока для начального окна, затем для значения N + 1-го потока увеличивайте соответствующий бит при уменьшении бункера, соответствующего значению 0-го потока. В этом случае необходимо сохранить последние значения N, чтобы разрешить декремент, который можно эффективно выполнить, циклически обращаясь к массиву размера N. Поскольку положение медианы может изменяться только на -2, -1,0,1, 2 на каждом шаге раздвижного окна, нет необходимости суммировать все бункеры до медианного на каждом шаге, просто отрегулируйте "медианный указатель" в зависимости от того, какие боковые (я) ячейки были изменены. Например, если и новое значение, и одно удаляемое, опускаются ниже текущей медианы, тогда оно не изменяется (offset = 0). Метод ломается, когда N становится слишком большим, чтобы удобно удерживать его в памяти.

Ответ 10

Вот тот, который может использоваться, когда точный вывод не важен (для целей показа и т.д.), Вам нужно totalcount и lastmedian, плюс newvalue.

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

Производит довольно точные результаты для таких вещей, как page_display_time.

Правила: входной поток должен быть плавным в порядке времени отображения страницы, большой в счетчике ( > 30 и т.д.) и иметь ненулевую медиану.

Пример: время загрузки страницы, 800 элементов, 10 мс... 3000 мс, среднее значение 90 мс, реальная медиана: 11 мс

После 30 входов ошибка медианов обычно равна <= 20% (9 мс. 12 мс) и получает все меньше и меньше. После 800 входов ошибка составляет + -2%.

Другой мыслитель с аналогичным решением находится здесь: Эффективная реализация медианного фильтра

Ответ 11

Вот реализация 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
    }
}

Ответ 12

Если вам просто требуется сглаженное среднее, быстрый/простой способ - умножить последнее значение на x и среднее значение на (1-x), а затем добавить их. Тогда это станет новым средним значением.

edit: Не то, что пользователь просил, а не как статистически достоверный, но достаточно хороший для многих применений.
Я оставлю его здесь (несмотря на downvotes) для поиска!