web-dev-qa-db-fra.com

Algorithme de médiane mobile en C

Je travaille actuellement sur un algorithme pour implémenter un filtre médian roulant (analogue à un filtre moyen roulant) en C. D'après ma recherche dans la littérature, il semble y avoir deux façons raisonnablement efficaces de le faire. La première consiste à trier la fenêtre de valeurs initiale, puis à effectuer une recherche binaire pour insérer la nouvelle valeur et supprimer celle existante à chaque itération.

Le second (de Hardle et Steiger, 1995, JRSS-C, algorithme 296) construit une structure de tas à double extrémité, avec un maxheap à une extrémité, un minheap à l'autre et la médiane au milieu. Cela donne un algorithme à temps linéaire au lieu de celui qui est O (n log n).

Voici mon problème: l'implémentation de la première est faisable, mais je dois l'exécuter sur des millions de séries chronologiques, donc l'efficacité compte beaucoup. Ce dernier s'avère très difficile à mettre en œuvre. J'ai trouvé du code dans le fichier Trunmed.c du code du package de statistiques de R, mais il est plutôt indéchiffrable.

Quelqu'un connaît-il une implémentation C bien écrite pour l'algorithme médian de roulement linéaire dans le temps?

Modifier: lien vers le code 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

108
AWB

J'ai regardé _ src/library/stats/src/Trunmed.c plusieurs fois car je voulais quelque chose de similaire aussi dans un sous-programme de classe C/C autonome. Notez qu'il s'agit en fait de deux implémentations en une, voir src/library/stats/man/runmed.Rd (la source du fichier d'aide) qui dit

\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}.}
  }
}

Ce serait bien de le voir réutilisé de façon plus autonome. Faites-vous du bénévolat? Je peux vous aider avec certains des bits R.

Édition 1 : Outre le lien vers l'ancienne version de Trunmed.c ci-dessus, voici des copies SVN actuelles de

Edit 2 : Ryan Tibshirani a du code C et Fortran sur binage médian rapide qui peut être un point de départ approprié pour une fenêtre approche.

25
Dirk Eddelbuettel

Je ne pouvais pas trouver une implémentation moderne d'une structure de données c ++ avec une statistique d'ordre, donc j'ai fini par implémenter les deux idées dans le lien des codeurs principaux suggéré par MAK ( Match Editorial : faites défiler vers le bas jusqu'à FloatingMedian).

Deux multisets

La première idée partitionne les données en deux structures de données (tas, multisets, etc.) avec O (ln N) par insertion/suppression ne permet pas de changer dynamiquement le quantile sans coût élevé. C'est à dire. nous pouvons avoir une médiane mobile, ou un 75% mobile, mais pas les deux en même temps.

Arbre de segment

La deuxième idée utilise un arbre de segments qui est O (ln N) pour les insertions/suppressions/requêtes mais est plus flexible. Le meilleur de tous les "N" est la taille de votre plage de données. Donc, si votre médiane mobile a une fenêtre d'un million d'articles, mais vos données varient de 1 à 65536, alors seulement 16 opérations sont nécessaires par mouvement de la fenêtre mobile de 1 million !!

Le code c ++ est similaire à ce que Denis a posté ci-dessus ("Voici un algorithme simple pour les données quantifiées")

Arbres statistiques de l'ordre GNU

Juste avant d'abandonner, j'ai trouvé que stdlibc ++ contient des arbres de statistiques d'ordre !!!

Ceux-ci ont deux opérations critiques:

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

Voir manuel libstdc ++ policy_based_data_structures_test (recherche de "split and join").

J'ai encapsulé l'arborescence pour une utilisation dans un en-tête pratique pour les compilateurs prenant en charge les typedefs partiels de style c ++ 0x/c ++ 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
17
Leo Goodstadt

J'ai fait un implémentation Cici . Quelques détails supplémentaires sont dans cette question: Rolling median in C - Turlach implementation .

Exemple d'utilisation:

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);
   }
}
15
AShelly

J'utilise cet estimateur médian incrémentiel:

median += eta * sgn(sample - median)

qui a la même forme que l'estimateur moyen le plus courant:

mean += eta * (sample - mean)

Ici eta est un petit paramètre de taux d'apprentissage (par exemple 0.001), et sgn() est la fonction signum qui renvoie l'un des {-1, 0, 1}. (Utilisez une constante eta comme celle-ci si les données ne sont pas stationnaires et que vous souhaitez suivre les modifications au fil du temps; sinon, pour les sources stationnaires, utilisez quelque chose comme eta = 1 / n pour converger, où n est le nombre d'échantillons vus jusqu'à présent.)

De plus, j'ai modifié l'estimateur médian pour le faire fonctionner pour des quantiles arbitraires. En général, une fonction quantile vous indique la valeur qui divise les données en deux fractions: p et 1 - p. Ce qui suit estime cette valeur progressivement:

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

La valeur p doit être comprise entre [0, 1]. Cela déplace essentiellement la sortie symétrique de la fonction sgn(){-1, 0, 1} pour se pencher vers un côté, partitionnant les échantillons de données en deux compartiments de taille inégale (fractions p et 1 - p des données sont respectivement inférieures ou supérieures à l'estimation quantile). Notez que pour p = 0.5, cela se réduit à l'estimateur médian.

9
Tyler Streeter

Voici un algorithme simple pour les données quantifiées (des mois plus tard):

""" 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 __== "__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
8
denis

La médiane mobile peut être trouvée en conservant deux partitions de nombres.

Pour la maintenance des partitions, utilisez Min Heap et Max Heap.

Max Heap contiendra des nombres inférieurs ou égaux à la médiane.

Le tas minimal contiendra des nombres supérieurs ou égaux à la médiane.

Contrainte d'équilibrage: si le nombre total d'éléments est pair, les deux tas doivent avoir des éléments égaux.

si le nombre total d'éléments est impair, alors Max Heap aura un élément de plus que Min Heap.

Élément médian: Si les deux partitions ont un nombre égal d'éléments, la médiane sera la moitié de la somme de l'élément max de la première partition et de l'élément min de la deuxième partition.

Sinon, la médiane sera l'élément max de la première partition.

 Algorithme - 
 1- Prendre deux tas (1 tas de Min et 1 tas de Max) 
 Le tas de Max contiendra la première moitié du nombre d'éléments 
 Le tas de Min contiendra la seconde demi-nombre d'éléments 
 
 2- Comparer le nouveau numéro du flux avec le haut du tas max, 
 s'il est plus petit ou égal ajouter ce nombre dans le tas max. 
 Sinon, ajoutez un nombre dans le segment Min. 
 
 3- si le segment Min a plus d'éléments que le segment Max 
, Supprimez l'élément supérieur du segment Min et ajoutez le segment Max. . 
 si le tas max a plus d'un élément que dans le tas min 
 puis supprimez l'élément supérieur du tas max et ajoutez dans le tas min. 
 
 4- Si les deux tas a un nombre égal d'éléments alors 
 la médiane sera la moitié de la somme de l'élément max du segment Max et de l'élément min du segment Min. 
 Sinon, la médiane sera l'élément max de la première 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();
    }
}
4
Harshit

Si vous avez la possibilité de référencer des valeurs en fonction de points dans le temps, vous pouvez échantillonner des valeurs avec remplacement, en appliquant bootstrapping pour générer une valeur médiane amorcée dans des intervalles de confiance. Cela peut vous permettre de calculer une médiane approximative avec une plus grande efficacité que de trier constamment les valeurs entrantes dans une structure de données.

1
Alex Reynolds

Il convient peut-être de souligner qu'il existe un cas particulier qui a une solution exacte simple: lorsque toutes les valeurs dans le flux sont des entiers dans une plage définie (relativement) petite. Par exemple, supposons qu'ils doivent tous se situer entre 0 et 1023. Dans ce cas, définissez simplement un tableau de 1024 éléments et un nombre, et effacez toutes ces valeurs. Pour chaque valeur dans le flux, incrémentez le bac correspondant et le nombre. Une fois le flux terminé, trouvez le bac qui contient la valeur la plus élevée de count/2 - facilement accompli en ajoutant des bacs successifs à partir de 0. En utilisant la même méthode, la valeur d'un ordre de classement arbitraire peut être trouvée. (Il y a une complication mineure si la détection de la saturation des bacs et la "mise à niveau" de la taille des bacs de stockage vers un type plus grand pendant une exécution seront nécessaires.)

Ce cas particulier peut sembler artificiel, mais en pratique, il est très courant. Elle peut également être appliquée comme approximation de nombres réels s'ils se situent dans une plage et si un niveau de précision "suffisamment bon" est connu. Cela valait à peu près n'importe quel ensemble de mesures sur un groupe d'objets "du monde réel". Par exemple, les hauteurs ou les poids d'un groupe de personnes. Pas un ensemble assez grand? Cela fonctionnerait tout aussi bien pour les longueurs ou les poids de toutes les bactéries (individuelles) de la planète - en supposant que quelqu'un pourrait fournir les données!

Il semble que j'ai mal lu l'original - ce qui semble vouloir une médiane de fenêtre coulissante au lieu de la juste médiane d'un très long flux. Cette approche fonctionne toujours pour cela. Chargez les N premières valeurs de flux pour la fenêtre initiale, puis pour la N + 1ème valeur de flux, incrémentez le bac correspondant tout en décrémentant le bac correspondant à la 0ème valeur de flux. Il est nécessaire dans ce cas de conserver les N dernières valeurs pour permettre la décrémentation, ce qui peut être fait efficacement en adressant cycliquement un tableau de taille N. Puisque la position de la médiane ne peut changer que de -2, -1,0,1 , 2 à chaque étape de la fenêtre coulissante, il n'est pas nécessaire de faire la somme de tous les bacs jusqu'à la médiane à chaque étape, il suffit d'ajuster le "pointeur médian" en fonction du ou des bacs qui ont été modifiés. Par exemple, si la nouvelle valeur et celle qui est supprimée tombent en dessous de la médiane actuelle, alors elle ne change pas (offset = 0). La méthode tombe en panne lorsque N devient trop grand pour être facilement conservé en mémoire.

1
mathog

Pour ceux qui ont besoin d'une médiane en cours d'exécution en Java ... PriorityQueue est votre ami. O (log N) insérer, O(1) médiane actuelle, et O(N) supprimer. Si vous connaissez la distribution de vos données, vous pouvez le faire beaucoup mieux que ça.

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();
  }
}
1
Ross Judson

En voici une qui peut être utilisée lorsque la sortie exacte n'est pas importante (à des fins d'affichage, etc.). Vous avez besoin du totalcount et de lastmedian, plus la nouvelle valeur.

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

Produit des résultats assez précis pour des choses comme page_display_time.

Règles: le flux d'entrée doit être fluide dans l'ordre du temps d'affichage de la page, grand en nombre (> 30, etc.) et avoir une médiane non nulle.

Exemple: temps de chargement de la page, 800 éléments, 10 ms ... 3000 ms, moyenne 90 ms, médiane réelle: 11 ms

Après 30 entrées, l'erreur médiane est généralement <= 20% (9 ms à 12 ms) et devient de moins en moins. Après 800 entrées, l'erreur est de + -2%.

Un autre penseur avec une solution similaire est ici: mise en œuvre super efficace du filtre médian

0
Johan