web-dev-qa-db-fra.com

Calcul de la moyenne mobile d'une liste

Ce week-end, j'ai décidé de m'essayer à certains Scala et Clojure. Je suis compétent en programmation orientée objet, et donc Scala était facile à prendre comme un langage, mais voulait essayer la programmation fonctionnelle. C'est là que ça a été difficile.

Je n'arrive tout simplement pas à mettre la tête dans un mode d'écriture de fonctions. En tant que programmeur fonctionnel expert, comment abordez-vous un problème?

Étant donné une liste de valeurs et une période de sommation définie, comment généreriez-vous une nouvelle liste de la moyenne mobile simple de la liste?

Par exemple: étant donné la liste values (2.0, 4.0, 7.0, 6.0, 3.0, 8.0, 12.0, 9.0, 4.0, 1.0) et le period 4, la fonction doit retourner: ( 0,0, 0,0, 0,0, 4,75, 5,0, 6,0, 7,25, 8,0, 8,25, 6,5)

Après avoir passé une journée à réfléchir, le mieux que j'ai pu trouver en Scala était le suivant:

def simpleMovingAverage(values: List[Double], period: Int): List[Double] = {
  (for (i <- 1 to values.length)
    yield
    if (i < period) 0.00
    else values.slice(i - period, i).reduceLeft(_ + _) / period).toList
}

Je sais que c'est horriblement inefficace, je préfère de loin faire quelque chose comme:

where n < period: ma(n) = 0
where n = period: ma(n) = sum(value(1) to value(n)) / period
where n > period: man(n) = ma(n -1) - (value(n-period) / period) + (value(n) / period)

Maintenant, cela se ferait facilement dans un style impératif, mais je ne peux pas pour la vie de moi trouver comment l'exprimer fonctionnellement.

56
James P

Problème intéressant. Je peux penser à de nombreuses solutions, avec différents degrés d'efficacité. Devoir ajouter des choses à plusieurs reprises n'est pas vraiment un problème de performances, mais supposons que oui. De plus, les zéros au début peuvent être ajoutés plus tard, alors ne nous inquiétons pas de les produire. Si l'algorithme les fournit naturellement, très bien; sinon, nous le corrigeons plus tard.

En commençant par Scala 2.8, ce qui suit donnerait le résultat pour n >= period En utilisant sliding pour obtenir une fenêtre glissante de la liste:

def simpleMovingAverage(values: List[Double], period: Int): List[Double] =
  List.fill(period - 1)(0.0) ::: (values sliding period map (_.sum) map (_ / period))

Néanmoins, bien que ce soit plutôt élégant, il n'a pas les meilleures performances possibles, car il ne tire pas parti des ajouts déjà calculés. Donc, en parlant d'eux, comment pouvons-nous les obtenir?

Disons que nous écrivons ceci:

values sliding 2 map sum

Nous avons une liste de la somme de chacune des deux paires. Essayons d'utiliser ce résultat pour calculer la moyenne mobile de 4 éléments. La formule ci-dessus a fait le calcul suivant:

from d1, d2, d3, d4, d5, d6, ...
to (d1+d2), (d2+d3), (d3+d4), (d4+d5), (d5+d6), ...

Donc, si nous prenons chaque élément et l'ajoutons au deuxième élément suivant, nous obtenons la moyenne mobile pour 4 éléments:

(d1+d2)+(d3+d4), (d2+d3)+(d4+d5), (d3+d4)+(d5+d6), ...

Nous pouvons le faire comme ceci:

res Zip (res drop 2) map Function.tupled(_+_)

Nous pourrions alors calculer la moyenne mobile pour 8 éléments, et ainsi de suite. Eh bien, il existe un algorithme bien connu pour calculer les choses qui suivent un tel modèle. Il est surtout connu pour son utilisation sur le calcul de la puissance d'un nombre. Ça va comme ça:

def power(n: Int, e: Int): Int = e match {
  case 0 => 1
  case 1 => n
  case 2 => n * n
  case odd if odd % 2 == 1 => power(n, (odd - 1)) * n
  case even => power(power(n, even / 2), 2)
}

Alors, appliquons-le ici:

def movingSum(values: List[Double], period: Int): List[Double] = period match {
  case 0 => throw new IllegalArgumentException
  case 1 => values
  case 2 => values sliding 2 map (_.sum)
  case odd if odd % 2 == 1 => 
    values Zip movingSum(values drop 1, (odd - 1)) map Function.tupled(_+_)
  case even =>
    val half = even / 2
    val partialResult = movingSum(values, half)
    partialResult Zip (partialResult drop half) map Function.tupled(_+_)
}

Alors, voici la logique. La période 0 n'est pas valide, la période 1 est égale à l'entrée, la période 2 est une fenêtre coulissante de taille 2. Si elle est supérieure, elle peut être paire ou impaire.

S'il est impair, nous ajoutons chaque élément aux movingSum des éléments (odd - 1) Suivants. Par exemple, si 3, nous ajoutons chaque élément au movingSum des 2 éléments suivants.

Si même, nous calculons le movingSum pour n / 2, Puis ajoutons chaque élément aux étapes n / 2 Par la suite.

Avec cette définition, nous pouvons ensuite revenir au problème et faire ceci:

def simpleMovingAverage(values: List[Double], period: Int): List[Double] =
  List.fill(period - 1)(0.0) ::: (movingSum(values, period) map (_ / period))

Il y a une légère inefficacité en ce qui concerne l'utilisation de :::, Mais c'est O (point), pas O (valeurs.taille). Il peut être rendu plus efficace avec une fonction récursive de queue. Et, bien sûr, la définition de "glissement" que j'ai fournie est horrible en termes de performances, mais il y aura une bien meilleure définition sur Scala 2.8. Notez que nous ne pouvons pas faire un efficace sliding sur un List, mais nous pouvons le faire sur un Iterable.

Cela dit, j'irais avec la toute première définition et n'optimiserais que si une analyse du chemin critique identifiait cela comme un gros problème.

Pour conclure, considérons comment j'ai abordé le problème. Nous avons un problème de moyenne mobile. Une moyenne mobile est la somme d'une "fenêtre" mobile sur une liste, divisée par la taille de cette fenêtre. Donc, d'abord, j'essaie d'obtenir une fenêtre coulissante, de tout additionner, puis de diviser par la taille.

Le problème suivant était d'éviter la répétition des ajouts déjà calculés. Dans ce cas, je suis allé au plus petit ajout possible et j'ai essayé de comprendre comment calculer des sommes plus importantes en réutilisant ces résultats.

Enfin, essayons de résoudre le problème comme vous l'avez pensé, en ajoutant et en soustrayant du résultat précédent. Obtenir la première moyenne est facile:

 def movingAverage(values: List[Double], period: Int): List[Double] = {
   val first = (values take period).sum / period

Maintenant, nous faisons deux listes. Tout d'abord, la liste des éléments à soustraire. Ensuite, la liste des éléments à ajouter:

   val subtract = values map (_ / period)
   val add = subtract drop period

Nous pouvons ajouter ces deux listes en utilisant Zip. Cette méthode ne produira que autant d'éléments que la liste plus petite, ce qui évite que le problème de subtract soit plus grand que nécessaire:

   val addAndSubtract = add Zip subtract map Function.tupled(_ - _)

On termine en composant le résultat avec un pli:

   val res = (addAndSubtract.foldLeft(first :: List.fill(period - 1)(0.0)) { 
     (acc, add) => (add + acc.head) :: acc 
   }).reverse

qui est la réponse à retourner. La fonction entière ressemble à ceci:

 def movingAverage(values: List[Double], period: Int): List[Double] = {
   val first = (values take period).sum / period
   val subtract = values map (_ / period)
   val add = subtract drop period
   val addAndSubtract = add Zip subtract map Function.tupled(_ - _)
   val res = (addAndSubtract.foldLeft(first :: List.fill(period - 1)(0.0)) { 
     (acc, add) => (add + acc.head) :: acc 
   }).reverse
   res
 }
49
Daniel C. Sobral

Je connais Clojure mieux que Scala, alors voilà. Au moment où j'écris ceci, l'autre entrée de Clojure ici est impérative; ce n'est pas vraiment ce que vous recherchez (et ce n'est pas Clojure idiomatique). Le premier algorithme qui me vient à l'esprit est de prendre à plusieurs reprises le nombre d'éléments requis de la séquence, de supprimer le premier élément et de le répéter.

Ce qui suit fonctionne sur tout type de séquence (vecteur ou liste, paresseux ou non) et donne une séquence paresseuse de moyennes --- ce qui pourrait être utile si vous travaillez sur une liste de taille indéfinie. Notez qu'il prend en charge le cas de base en renvoyant implicitement nil s'il n'y a pas assez d'éléments dans la liste à consommer.

(defn moving-average [values period]
  (let [first (take period values)]
    (if (= (count first) period)
      (lazy-seq 
        (cons (/ (reduce + first) period)
              (moving-average (rest values) period))))))

L'exécution de ceci sur vos données de test donne

user> (moving-average '(2.0, 4.0, 7.0, 6.0, 3.0, 8.0, 12.0, 9.0, 4.0, 1.0) 4)
(4.75 5.0 6.0 7.25 8.0 8.25 6.5)

Il ne donne pas "0" pour les premiers éléments de la séquence, bien que cela puisse être facilement géré (quelque peu artificiellement).

La chose la plus simple est de voir le motif et de pouvoir penser à une fonction disponible qui correspond à la facture. partition donne une vue paresseuse de portions d'une séquence, que nous pouvons ensuite cartographier:

(defn moving-average [values period]
  (map #(/ (reduce + %) period) (partition period 1 values))

Quelqu'un a demandé une version récursive de queue; récursivité de la queue contre la paresse est un peu un compromis. Lorsque votre travail consiste à créer une liste, rendre la fonction récursive de votre fonction est généralement assez simple, et cela ne fait pas exception - créez simplement la liste comme argument d'une sous-fonction. Nous accumulerons dans un vecteur au lieu d'une liste, sinon la liste sera construite à l'envers et devra être inversée à la fin.

(defn moving-average [values period]
  (loop [values values, period period, acc []]
    (let [first (take period values)]
      (if (= (count first) period)
        (recur (rest values) period (conj acc (/ (reduce + first) period)))
        acc))))

loop est un moyen de créer une fonction interne anonyme (un peu comme le let nommé de Scheme); recur doit être utilisé dans Clojure pour éliminer les appels de queue. conj est un cons généralisé, ajoutant de la manière naturelle pour la collection --- le début des listes et la fin des vecteurs.

29
James Cunningham

Voici une autre solution (fonctionnelle) de Clojure:

 (defn moyenne [coll] 
 (/ (réduire + coll) 
 (compter coll))) 
 
 (defn ma [période coll ] 
 (carte moyenne (période de partition 1 coll))) 

Les zéros au début de la séquence doivent toujours être ajoutés si c'est une exigence.

15
Jonas

Voici une solution purement fonctionnelle dans Clojure. Plus complexe que ceux déjà fournis, mais ce sont paresseux et ajuste uniquement la moyenne à chaque étape, au lieu de la recalculer à partir de zéro. C'est en fait plus lent qu'une solution simple qui calcule une nouvelle moyenne à chaque étape si la période est petite; pendant de plus longues périodes, cependant, il ne connaît pratiquement aucun ralentissement, alors que quelque chose qui fait (/ (take period ...) period) fonctionnera moins bien pendant de plus longues périodes.

(defn moving-average
  "Calculates the moving average of values with the given period.
  Returns a lazy seq, works with infinite input sequences.
  Does not include initial zeros in the output."
  [period values]
  (let [gen (fn gen [last-sum values-old values-new]
              (if (empty? values-new)
                nil
                (let [num-out (first values-old)
                      num-in  (first values-new)
                      new-sum (+ last-sum (- num-out) num-in)]
                  (lazy-seq
                    (cons new-sum
                          (gen new-sum
                               (next values-old)
                               (next values-new)))))))]
    (if (< (count (take period values)) period)
      nil
      (map #(/ % period)
           (gen (apply + (take (dec period) values))
                (cons 0 values)
                (drop (dec period) values))))))
13
Michał Marczyk

Voici une solution Haskell partiellement sans point d'une ligne:

ma p = reverse . map ((/ (fromIntegral p)) . sum . take p) . (drop p) . reverse . tails

Tout d'abord, il applique tails à la liste pour obtenir les listes "tails", donc:

Prelude List> tails [2.0, 4.0, 7.0, 6.0, 3.0]
[[2.0,4.0,7.0,6.0,3.0],[4.0,7.0,6.0,3.0],[7.0,6.0,3.0],[6.0,3.0],[3.0],[]]

Inverse et supprime les premières entrées 'p' (en prenant p comme 2 ici):

Prelude List> (drop 2 . reverse . tails) [2.0, 4.0, 7.0, 6.0, 3.0]
[[6.0,3.0],[7.0,6.0,3.0],[4.0,7.0,6.0,3.0],[2.0,4.0,7.0,6.0,3.0]]

Dans le cas où vous n'êtes pas familier avec le symbole (.) Dot/nipple , il s'agit de l'opérateur de 'composition fonctionnelle', ce qui signifie qu'il passe la sortie d'une fonction comme entrée d'une autre, "composition "les en une seule fonction. (g. f) signifie "exécuter f sur une valeur puis passer la sortie à g", donc ((f. g) x) est identique à (g (f x)). En général, son utilisation conduit à un style de programmation plus clair.

Il mappe ensuite la fonction ((/ (fromIntegral p)). Sum. Take p) sur la liste. Ainsi, pour chaque liste de la liste, il prend les premiers éléments "p", les additionne, puis les divise par "p". Ensuite, nous retournons simplement la liste avec "reverse".

Prelude List> map ((/ (fromIntegral 2)) . sum . take 2) [[6.0,3.0],[7.0,6.0,3.0]
,[4.0,7.0,6.0,3.0],[2.0,4.0,7.0,6.0,3.0]]
[4.5,6.5,5.5,3.0]

Tout cela semble beaucoup plus inefficace qu'il ne l'est; "reverse" n'inverse pas physiquement l'ordre d'une liste jusqu'à ce que la liste soit évaluée, il la met simplement sur la pile (bon vieux Haskell paresseux). "tails" ne crée pas non plus toutes ces listes séparées, il fait simplement référence à différentes sections de la liste d'origine. Ce n'est toujours pas une excellente solution, mais c'est une ligne de long :)

Voici une solution légèrement plus agréable mais plus longue qui utilise mapAccum pour effectuer une soustraction et une addition glissantes:

ma p l = snd $ mapAccumL ma' a l'
    where
        (h, t) = splitAt p l
        a = sum h
        l' = (0, 0) : (Zip l t)
        ma' s (x, y) = let s' = (s - x) + y in (s', s' / (fromIntegral p))

Nous avons d'abord divisé la liste en deux parties en "p", donc:

Prelude List> splitAt 2 [2.0, 4.0, 7.0, 6.0, 3.0]
([2.0,4.0],[7.0,6.0,3.0])

Additionnez le premier bit:

Prelude List> sum [2.0, 4.0]
6.0

Compressez le deuxième bit avec la liste d'origine (cela associe simplement les éléments dans l'ordre des deux listes). La liste d'origine est évidemment plus longue, mais nous perdons ce petit plus:

Prelude List> Zip [2.0, 4.0, 7.0, 6.0, 3.0] [7.0,6.0,3.0]
[(2.0,7.0),(4.0,6.0),(7.0,3.0)]

Maintenant, nous définissons une fonction pour notre mapAccum (ulator). mapAccumL est identique à "map", mais avec un paramètre d'état/accumulateur supplémentaire, qui est passé du "mapping" précédent au suivant lorsque la carte parcourt la liste. Nous utilisons l'accumulateur comme moyenne mobile, et comme notre liste est formée de l'élément qui vient de quitter la fenêtre glissante et de l'élément qui vient de l'entrer (la liste que nous venons de zipper), notre fonction glissante prend le premier nombre 'x' loin de la moyenne et ajoute le deuxième nombre "y". On passe ensuite le nouveau 's' et on retourne le 's' divisé par 'p'. "snd" (second) prend simplement le deuxième membre d'une paire (Tuple), qui est utilisé pour prendre la deuxième valeur de retour de mapAccumL, car mapAccumL renverra l'accumulateur ainsi que la liste mappée.

Pour ceux d'entre vous qui ne connaissent pas le $ symbol , c'est "l'opérateur d'application". Il ne fait vraiment rien mais il a une "priorité de liaison basse associative à droite", donc cela signifie que vous pouvez laisser de côté les crochets (notez LISPers), c'est-à-dire que (f x) est identique à f $ x

L'exécution (ma 4 [2.0, 4.0, 7.0, 6.0, 3.0, 8.0, 12.0, 9.0, 4.0, 1.0]) donne [4.75, 5.0, 6.0, 7.25, 8.0, 8.25, 6.5] pour l'une ou l'autre solution.

Oh et vous devrez importer le module "Liste" pour compiler l'une ou l'autre solution.

9
Will

Voici 2 autres façons de faire la moyenne mobile dans Scala 2.8.0 (une stricte et une paresseuse). Les deux supposent qu'il y a au moins ( p Double en vs .

// strict moving average
def sma(vs: List[Double], p: Int): List[Double] =
  ((vs.take(p).sum / p :: List.fill(p - 1)(0.0), vs) /: vs.drop(p)) {(a, v) =>
    ((a._1.head - a._2.head / p + v / p) :: a._1, a._2.tail)
  }._1.reverse

// lazy moving average
def lma(vs: Stream[Double], p: Int): Stream[Double] = {
  def _lma(a: => Double, vs1: Stream[Double], vs2: Stream[Double]): Stream[Double] = {
    val _a = a // caches value of a
    _a #:: _lma(_a - vs2.head / p + vs1.head / p, vs1.tail, vs2.tail)
  }
  Stream.fill(p - 1)(0.0) #::: _lma(vs.take(p).sum / p, vs.drop(p), vs)
}

scala> sma(List(2.0, 4.0, 7.0, 6.0, 3.0, 8.0, 12.0, 9.0, 4.0, 1.0), 4)
res29: List[Double] = List(0.0, 0.0, 0.0, 4.75, 5.0, 6.0, 7.25, 8.0, 8.25, 6.5)

scala> lma(Stream(2.0, 4.0, 7.0, 6.0, 3.0, 8.0, 12.0, 9.0, 4.0, 1.0), 4).take(10).force
res30: scala.collection.immutable.Stream[Double] = Stream(0.0, 0.0, 0.0, 4.75, 5.0, 6.0, 7.25, 8.0, 8.25, 6.5)
7
Walter Chang

Le langage de programmation J facilite des programmes tels que la moyenne mobile. En effet, il y a moins de caractères dans (+/ % #)\ que dans leur étiquette, "moyenne mobile".

Pour les valeurs spécifiées dans cette question (y compris le nom "valeurs"), voici un moyen simple de coder ceci:

   values=: 2 4 7 6 3 8 12 9 4 1
   4 (+/ % #)\ values
4.75 5 6 7.25 8 8.25 6.5

Nous pouvons décrire cela en utilisant des étiquettes pour les composants.

   periods=: 4
   average=: +/ % #
   moving=: \

   periods average moving values
4.75 5 6 7.25 8 8.25 6.5

Les deux exemples utilisent exactement le même programme. La seule différence est l'utilisation de plusieurs noms dans le deuxième formulaire. De tels noms peuvent aider les lecteurs qui ne connaissent pas les primaires J.

Regardons un peu plus loin ce qui se passe dans le sous-programme, average. +/ désigne la somme (Σ) et % désigne la division (comme le signe classique ÷). Le calcul d'un décompte (nombre) d'éléments se fait par #. Le programme global est donc la somme des valeurs divisée par le décompte des valeurs: +/ % #

Le résultat du calcul de la moyenne mobile écrit ici n'inclut pas les zéros de tête attendus dans la question d'origine. Ces zéros ne font sans doute pas partie du calcul prévu.

La technique utilisée ici est appelée programmation tacite. C'est à peu près la même chose que le style de programmation fonctionnelle sans point.

6
kaleidic

Voici Clojure qui prétend être un langage plus fonctionnel. Ceci est entièrement récursif, btw, et inclut des zéros non significatifs.

(defn moving-average [period values]
  (loop [[x & xs]  values
         window    []
         ys        []]

    (if (and (nil? x) (nil? xs))
      ;; base case
      ys

      ;; inductive case
      (if (< (count window) (dec period))
        (recur xs (conj window x) (conj ys 0.0))
        (recur xs
               (conj (vec (rest window)) x)
               (conj ys (/ (reduce + x window) period)))))))

(deftest test-moving-average
  (is (= [0.0 0.0 0.0 4.75 5.0 6.0 7.25 8.0 8.25 6.5]
         (moving-average 4 [2.0 4.0 7.0 6.0 3.0 8.0 12.0 9.0 4.0 1.0]))))

Habituellement, je mets le paramètre collection ou list en dernier pour rendre la fonction plus facile à gérer. Mais à Clojure ...

(partial moving-average 4)

... est tellement encombrant, je finis généralement par faire ça ...

#(moving-average 4 %)

... dans ce cas, peu importe l'ordre des paramètres.

5
Jonathan Tran

Une version courte de Clojure qui a l'avantage d'être O (longueur de liste) quelle que soit votre période:

(defn moving-average [list period]
  (let [accums (let [acc (atom 0)] (map #(do (reset! acc (+ @acc %1 ))) (cons 0 list)))
        zeros (repeat (dec period) 0)]
     (concat zeros (map #(/ (- %1 %2) period) (drop period accums) accums))))

Cela exploite le fait que vous pouvez calculer la somme d'une plage de nombres en créant une somme cumulée de la séquence (par exemple [1 2 3 4 5] -> [0 1 3 6 10 15]), puis en soustrayant les deux nombres avec un décalage égal à votre période.

2
mikera

Cette solution se trouve dans Haskell, qui m'est plus familier:

slidingSums :: Num t => Int -> [t] -> [t]
slidingSums n list = case (splitAt (n - 1) list) of
                      (window, []) -> [] -- list contains less than n elements
                      (window, rest) -> slidingSums' list rest (sum window)
  where
    slidingSums' _ [] _ = []
    slidingSums' (hl : tl) (hr : tr) sumLastNm1 = sumLastN : slidingSums' tl tr (sumLastN - hl)
      where sumLastN = sumLastNm1 + hr

movingAverage :: Fractional t => Int -> [t] -> [t]
movingAverage n list = map (/ (fromIntegral n)) (slidingSums n list)

paddedMovingAverage :: Fractional t => Int -> [t] -> [t]
paddedMovingAverage n list = replicate (n - 1) 0 ++ movingAverage n list

Traduction scala:

def slidingSums1(list: List[Double], rest: List[Double], n: Int, sumLastNm1: Double): List[Double] = rest match {
    case Nil => Nil
    case hr :: tr => {
        val sumLastN = sumLastNm1 + hr
        sumLastN :: slidingSums1(list.tail, tr, n, sumLastN - list.head)
    }
}

def slidingSums(list: List[Double], n: Int): List[Double] = list.splitAt(n - 1) match {
    case (_, Nil) => Nil
    case (firstNm1, rest) => slidingSums1(list, rest, n, firstNm1.reduceLeft(_ + _))
}

def movingAverage(list: List[Double], n: Int): List[Double] = slidingSums(list, n).map(_ / n)

def paddedMovingAverage(list: List[Double], n: Int): List[Double] = List.make(n - 1, 0.0) ++ movingAverage(list, n)
2
Alexey Romanov

Cet exemple utilise l'état, car pour moi, c'est une solution pragmatique dans ce cas, et une fermeture pour créer la fonction de moyenne de fenêtrage:

(defn make-averager [#^Integer period]
  (let [buff (atom (vec (repeat period nil)))
        pos (atom 0)]
    (fn [nextval]
      (reset! buff (assoc @buff @pos nextval))
      (reset! pos (mod (+ 1 @pos) period))
      (if (some nil? @buff)
        0
        (/ (reduce + @buff)
           (count @buff))))))

(map (make-averager 4)
     [2.0, 4.0, 7.0, 6.0, 3.0, 8.0, 12.0, 9.0, 4.0, 1.0])
;; yields =>
(0 0 0 4.75 5.0 6.0 7.25 8.0 8.25 6.5)

Il est toujours fonctionnel dans le sens où il utilise des fonctions de première classe, bien qu'il ne soit pas exempt d'effets secondaires. Les deux langues que vous avez mentionnées s'exécutent toutes les deux au-dessus de la machine virtuelle Java et permettent donc toutes deux une gestion des états si nécessaire.

2
Kyle Burton

Je sais comment je le ferais en python (note: les 3 premiers éléments avec les valeurs 0.0 ne sont pas retournés car ce n'est en fait pas la manière appropriée de représenter une moyenne mobile). J'imagine des techniques similaires seront possibles à Scala. Voici plusieurs façons de le faire.

data = (2.0, 4.0, 7.0, 6.0, 3.0, 8.0, 12.0, 9.0, 4.0, 1.0)
terms = 4
expected = (4.75, 5.0, 6.0, 7.25, 8.0, 8.25, 6.5)

# Method 1 : Simple. Uses slices
assert expected == \
    Tuple((sum(data[i:i+terms])/terms for i in range(len(data)-terms+1)))

# Method 2 : Tracks slots each of terms elements
# Note: slot, and block mean the same thing.
# Block is the internal tracking deque, slot is the final output
from collections import deque
def slots(data, terms):
    block = deque()
    for datum in data :
        block.append(datum)
        if len(block) > terms : block.popleft()
        if len(block) == terms :
            yield block

assert expected == \
    Tuple(sum(slot)/terms for slot in slots(data, terms))

# Method 3 : Reads value one at a time, computes the sums and throws away read values
def moving_average((avgs, sums),val):
    sums = Tuple((sum + val) for sum in sums)
    return (avgs + ((sums[0] / terms),), sums[1:] + (val,))

assert expected == reduce(
    moving_average,
    Tuple(data[terms-1:]),
    ((),Tuple(sum(data[i:terms-1]) for i in range(terms-1))))[0]

# Method 4 : Semantically same as method 3, intentionally obfuscates just to fit in a lambda
assert expected == \
    reduce(
        lambda (avgs, sums),val: Tuple((avgs + ((nsum[0] / terms),), nsum[1:] + (val,)) \
                                for nsum in (Tuple((sum + val) for sum in sums),))[0], \
           Tuple(data[terms-1:]),
           ((),Tuple(sum(data[i:terms-1]) for i in range(terms-1))))[0]
1
Dhananjay Nene

Il semble que vous recherchiez une solution récursive. Dans ce cas, je suggère de modifier légèrement le problème et de viser à obtenir (4.75, 5.0, 6.0, 7.25, 8.0, 8.25, 6.5, 0.0, 0.0, 0.0) comme solution.

Dans ce cas, vous pouvez écrire l'élégante solution récursive ci-dessous dans Scala:

def mavg(values: List[Double], period: Int): List[Double] = {
  if (values.size < period) List.fill(values.size)(0.0) else
    if (values.size == period) (values.sum / values.size) :: List.fill(period - 1)(0.0) else {
      val rest: List[Double] = mavg(values.tail, period)
      (rest.head + ((values.head - values(period))/period)):: rest
  }
}
1
Tibor Gergely

Utilisation de Haskell:

movingAverage :: Int -> [Double] -> [Double]
movingAverage n xs = catMaybes . (fmap avg . take n) . tails $ xs
  where avg list = case (length list == n) -> Just . (/ (fromIntegral n)) . (foldl (+) 0) $ list
                        _                  -> Nothing

La clé est la fonction tails, qui mappe une liste à une liste de copies de la liste d'origine, avec la propriété que le n-ième élément du résultat manque les n-1 premiers éléments.

Donc

[1,2,3,4,5] -> [[1,2,3,4,5], [2,3,4,5], [3,4,5], [4,5], [5], []]

Nous appliquons fmap (moy. Prendre n) au résultat, ce qui signifie que nous prenons le préfixe de longueur n de la sous-liste et calculons sa moyenne. Si la longueur de la liste que nous avançons n'est pas n, alors nous ne calculons pas la moyenne (car elle n'est pas définie). Dans ce cas, nous ne renvoyons rien. Si c'est le cas, nous le faisons, et enveloppons-le dans "Just". Enfin, nous exécutons "catMaybes" sur le résultat de fmap (moy. Prendre n), pour nous débarrasser du type Maybe.

0
nomen

Dans le pseudocode Haskell:

group4 (a:b:c:d:xs) = [a,b,c,d] : group4 (b:c:d:xs)
group4 _ = []

avg4 xs = sum xs / 4

running4avg nums = (map avg4 (group4 nums))

ou sans point

runnig4avg = map avg4 . group4

(Maintenant, il faut vraiment résumer les 4 sur ....)

0
Ingo

Étant en retard sur la fête, et nouveau dans la programmation fonctionnelle aussi, je suis arrivé à cette solution avec une fonction interne:

def slidingAvg (ixs: List [Double], len: Int) = {
    val dxs = ixs.map (_ / len) 
    val start = (0.0 /: dxs.take (len)) (_ + _)
    val head = List.make (len - 1, 0.0)

    def addAndSub (sofar: Double, from: Int, to: Int) : List [Double] =  
        if (to >= dxs.length) Nil else {
            val current = sofar - dxs (from) + dxs (to) 
            current :: addAndSub (current, from + 1, to + 1) 
        }

    head ::: start :: addAndSub (start, 0, len)
}

val xs = List(2, 4, 7, 6, 3, 8, 12, 9, 4, 1)
slidingAvg (xs.map (1.0 * _), 4)

J'ai adopté l'idée, de diviser la liste entière par la période (len) à l'avance. Ensuite, je génère la somme pour commencer pour les len-first-elements. Et je génère les premiers éléments invalides (0.0, 0.0, ...).

Ensuite, je soustrais récursivement la première et j'ajoute la dernière valeur. En fin de compte, j'écris le tout.

0
user unknown