Le calcul de la Moyenne mobile d'une Liste
Ce week-end j'ai décidé de m'essayer à quelques Scala et Clojure. Je suis compétent avec la programmation orientée objet, et Scala est facile à ramasser comme une langue, mais je voulais essayer de la programmation fonctionnelle. C'est là que c'est devenu dur.
J'ai juste ne peut pas sembler obtenir ma tête dans un mode d'écriture de fonctions. Comme un expert programmeur fonctionnel, comment abordez-vous un problème?
Donné une liste de valeurs et d'une période définie de la sommation, comment voulez-vous générer une nouvelle liste de la moyenne mobile simple de la liste?
Par exemple: compte tenu de 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 une journée de ressasser sur, le mieux que je pouvais venir avec dans le Scala est ceci:
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 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, qui serait facile à faire dans un impératif de style, mais je ne peux pas pour la vie de moi de travailler sur la façon d'exprimer que fonctionnellement.
Vous devez vous connecter pour publier un commentaire.
Problème intéressant. Je peux penser à de nombreuses solutions, avec des degrés variables d'efficacité. Avoir à ajouter des trucs à plusieurs reprises n'est pas vraiment un problème de performance, mais imaginons qu'il est. Aussi, l'ensemble des zéros au début peut être ajouté plus tard, afin de ne pas laisser s'inquiéter de leur production. Si l'algorithme fournit naturellement, l'amende; si non, nous corriger plus tard.
De départ avec Scala 2.8, le tableau suivant donne le résultat pour
n >= period
en utilisantsliding
pour obtenir une fenêtre coulissante de la Liste:Néanmoins, bien que ce soit plutôt élégant, il n'a pas la meilleure performance possible, car elle ne prend pas avantage de s'être déjà calculé ajouts. Si, en parlant d'eux, comment pouvons-nous obtenir?
Disons-nous écrire ceci:
Nous avons une liste de la somme de chacune des deux paires. Nous allons essayer d'utiliser ce résultat pour calculer la moyenne mobile de 4 éléments. La formule ci-dessus fait le calcul suivant:
Donc, si nous prenons chaque élément et l'ajouter à la deuxième élément suivant, nous obtenons la moyenne mobile de 4 éléments:
On peut faire comme ceci:
On pourrait alors calculer la moyenne mobile de 8 éléments, et ainsi de suite. Eh bien, il est bien connu de l'algorithme pour calculer les choses qui suivent ce schéma. Il est plus connu pour son utilisation sur le calcul de la puissance d'un nombre. Il va comme ceci:
Donc, nous allons appliquer ici:
Donc, voilà la logique. La période 0 est nulle, la période 1 est égale à l'entrée, de la période 2 est de fenêtre glissante de taille 2. Si grand que cela, il peut être pair ou impair.
S'il est impair, nous ajoutons chaque élément de la
movingSum
de la prochaine(odd - 1)
éléments. Par exemple, si 3, nous ajoutons chaque élément de lamovingSum
des 2 éléments.Si même, on calcule la
movingSum
pourn /2
, puis ajouter chaque élément de l'unn /2
étapes par la suite.Avec cette définition, on peut alors revenir au problème et pour ce faire:
Il y a une légère inefficacité en ce qui concerne l'utilisation de
:::
, mais il est O(période), pas de O(valeurs.taille). Il peut être rendue plus efficace grâce à une queue de fonction récursive. Et, bien sûr, la définition de "glissement" j'ai fourni, c'est une horreur en terme de performance, mais il y en aura une bien meilleure définition sur Scala 2.8. Notez que nous ne pouvons pas faire un efficacesliding
méthode sur unList
, mais nous pouvons le faire sur unIterable
.Après avoir dit tout cela, j'irais avec la première définition, et d'optimiser uniquement si une analyse du chemin critique identifié cela comme une grosse affaire.
Pour conclure, nous examinerons la façon dont je suis allé sur le problème. Nous avons une moyenne mobile de problème. Une moyenne mobile est la somme d'un mouvement de la "fenêtre" sur une liste, divisé par la taille de cette fenêtre. Alors, tout d'abord, j'essaie d'obtenir une fenêtre coulissante, la somme de tout sur elle, et puis la diviser par la taille.
Le problème suivant a été, pour éviter la répétition du déjà calculés ajouts. Dans ce cas, je suis allé à la plus petite plus possible, et essayé de comprendre comment calculer plus sommes la réutilisation de ces résultats.
Enfin, nous allons essayer de résoudre le problème de la manière dont vous avez compris, en ajoutant et en soustrayant du résultat précédent. L'obtention de la première moyenne est facile:
Maintenant nous faire deux listes. Tout d'abord, la liste des éléments à retrancher. Ensuite, la liste des éléments à ajouter:
Nous pouvons ajouter à ces deux listes à l'aide de
zip
. Cette méthode ne pourra produire autant d'éléments que le plus petit de la liste a, qui permet d'éviter le problème desubtract
étant plus grand que nécessaire:Nous avons fini par composer le résultat avec un pli:
qui est la réponse à renvoyer. L'ensemble de la fonction ressemble à ceci:
period
éléments de tous les temps. Je crois que vous avez manqué le point.make
est obsolète et:::
ne peut pas être trouvé.Je sais Clojure mieux que Scala, donc voilà. Comme je l'ai écrit l'autre Clojure entrée ici est impératif; ce n'est pas vraiment ce que vous êtes après (et n'est pas idiomatique Clojure). Le premier algorithme qui me vient à l'esprit est à plusieurs reprises de prendre le nombre d'éléments de la séquence, la suppression du premier élément, et récurrents.
La suite à des travaux sur tout type de séquence (vecteur ou d'une liste, paresseux ou pas) et donne un paresseux séquence de moyennes---ce qui pourrait être utile si vous travaillez sur une liste de taille indéfinie. Notez qu'il prend soin de la base de cas par implicitement le retour nul si il n'y a pas assez d'éléments dans la liste de consommer.
L'exécution de ce sur vos données de test rendements
Il ne donne pas de "0" pour les quelques premiers éléments de la séquence, mais qui pourrait facilement être manipulé (un peu artificiellement).
La chose la plus simple de toutes, c'est de voir le modèle et être en mesure d'apporter à l'esprit une fonction disponible qui correspond à la facture.
partition
donne un paresseux vue d'une partie d'une séquence, que nous pouvons ensuite la carte sur:Quelqu'un a demandé pour une queue une version récursive; la queue de la récursivité contre la paresse est un peu un compromis. Lorsque votre travail est de construire une liste, puis faire de votre fonction de queue récursive est généralement assez simple, et ce n'est pas une exception---il suffit de construire la liste comme argument à une sous-fonction. Nous allons accumuler à un vecteur au lieu d'une liste parce que sinon la liste va être construit à l'envers et devra être inversée à la fin.
loop
est une manière de rendre anonyme fonction interne (comme un Système nommé let);recur
doit être utilisé en Clojure pour éliminer les appels tail.conj
est généraliséecons
, en ajoutant de la manière naturelle pour la collecte---le début de listes et de la fin de vecteurs.Voici un autre (fonctionnelle) Clojure solution:
Les zéros au début de la séquence doit encore être ajouté si c'est une exigence.
(defn ma [period coll] (lazy-cat (repeat period 0) (map avarage (partition period 1 (repeat 0) coll))))
Voici un point de vue purement fonctionnel de la solution en Clojure. Plus complexes que ceux déjà fournis, mais il est paresseux et seulement ajuste la moyenne à chaque étape, au lieu de recalculer à partir de zéro. C'est en fait plus lentement que d'une solution simple qui calcule une nouvelle moyenne à chaque étape si la période est petite; pour les grandes périodes, cependant, il connaît pratiquement pas de ralentissement, alors que quelque chose à faire
(/(take period ...) period)
effectuera pire pour de plus longues périodes.Voici une partie point-libre une ligne Haskell solution:
D'abord elle s'applique queues de la liste pour obtenir les "queues" des listes, donc:
L'inverse et les gouttes de la première 'p' entrées (en prenant p 2 ici):
Dans le cas où vous n'êtes pas familier avec le (.) dot/tétine symbole, c'est l'opérateur pour le fonctionnement de la composition", ce qui signifie qu'il passe à la sortie d'une fonction, comme l'entrée d'une autre, la "composition" dans une seule fonction. (g . f) des moyens de "f run sur une valeur, puis passer la sortie à g", donc (f . g) x) est la même que (g(f x)). Généralement son utilisation conduit à une meilleure style de programmation.
Il alors des cartes de la fonction ((/(fromIntegral p)) . la somme . prendre p) sur la liste. Ainsi, pour chaque liste dans la liste, il prend le premier " p " éléments, les résume, puis divise par 'p'. Alors que nous venons de retourner la liste de retour avec "reverse".
Tout cela semble beaucoup plus inefficace qu'il est; "inverse" n'est pas physiquement inverser l'ordre de la liste jusqu'à ce que la liste est évalué, c'est juste la pose sur la pile (good ol' lazy Haskell). les "queues" aussi ne pas créer autant de listes séparées, il vient de références différentes sections de la liste d'origine. C'est toujours pas une bonne solution, mais c'est une ligne longue 🙂
Ici est un peu plus agréable mais plus solution qui utilise mapAccum faire glisser la soustraction et l'addition:
Nous avons d'abord diviser la liste en deux parties à "p", donc:
Somme le premier bit:
Zip le deuxième bit avec l'original de la liste (ce seulement les paires d'objets dans l'ordre, les deux listes). L'original de la liste n'est évidemment plus de temps, mais nous perdons ce bit supplémentaire:
Maintenant, nous définissons une fonction pour notre mapAccum(ulator). mapAccumL est le même que "la carte", mais avec un supplément de l'exécution de l'état/de l'accumulateur paramètre, qui est passé de la précédente "cartographie" à la suivante que la carte fonctionne par le biais de la liste. Nous utilisons l'accumulateur que notre moyenne mobile, et notre liste est constituée de l'élément qui a juste à gauche de la fenêtre coulissante et l'élément qui vient d'entrer (la liste nous venons de zippée), notre fonction coulissante prend le premier numéro de " x "à l'écart de la moyenne et ajoute le deuxième numéro de "y". On passe ensuite à la nouvelle 's' le long et le retour d'un " s "divisé par "p". "snd" (deuxième) prend juste le deuxième membre d'une paire (n-uplet), qui est utilisé pour prendre la deuxième valeur de retour de mapAccumL, comme mapAccumL sera de retour l'accumulateur ainsi que la mappé liste.
Pour ceux d'entre vous qui ne connaissent pas le symbole $ , c'est l'application "opérateur". Il n'a pas vraiment faire quelque chose, mais il a un a "faible, associatifs droit priorité de liaison", donc, cela signifie que vous pouvez laisser de côté les crochets (prendre note LISPers), c'est à dire (f x) est le même que $ f x
En cours d'exécution (ma 4 [2.0, 4.0, 7.0, 6.0, 3.0, 8.0, 12.0, 9.0, 4.0, 1.0]) les rendements [4.75, 5.0, 6.0, 7.25, 8.0, 8.25, 6.5] pour l'une ou l'autre solution.
Oh, et vous aurez besoin d'importer le module "Liste" pour compiler une ou l'autre solution.
Voici 2 façons de plus pour faire la moyenne mobile à Scala 2.8.0(une stricte et un paresseux). Les deux supposons qu'il y ait au moins p Double vs.
#
" n'est pas un marqueur de commentaire, mais une partie des opérateurs "#::
" et "#:::
", qui sont des Flux de trésorerie de "::
" et ":::
".La J langage de programmation facilite les programmes tels que le déplacement de la moyenne. En effet, il y a moins de caractères dans
(+/% #)\
que dans leur libellé, " moyenne mobile.'Pour les valeurs spécifiées dans cette question (y compris le nom des "valeurs"), voici un moyen simple pour code ce:
On peut le décrire en utilisant des étiquettes pour les composants.
Deux exemples utilisent exactement le même programme. La seule différence est l'utilisation de plusieurs noms dans la deuxième forme. De tels noms peuvent aider les lecteurs qui ne connaissent pas la J primaires.
Regardons un peu plus loin dans ce qui se passe dans le sous-programme,
average
.+/
dénote de sommation (Σ) et%
désigne la division (comme le classique signe ÷). Calcul de comptage (count) des éléments est effectuée par#
. L'ensemble du programme, alors, est la somme des valeurs divisée par le nombre de valeurs:+/% #
Le résultat de la moyenne mobile des valeurs de calcul écrit ici ne comprennent pas les zéros non significatifs attendus dans la question d'origine. Ces zéros sont sans doute pas partie de la destinée de calcul.
La technique utilisée ici est appelé tacite de la programmation. Il est à peu près le même que le style libre de la programmation fonctionnelle.
Ici est Clojure de faire semblant d'être plus fonctionnelle de la langue. C'est entièrement la queue-récursive, btw, et comprend des zéros à gauche.
D'habitude je mets de la collection ou de la liste de paramètres dernier pour rendre la fonction plus facile de curry. Mais en Clojure...
... c'est tellement lourd, j'ai l'habitude de faire cela ...
... auquel cas, il n'importe pas vraiment ce que l'ordre des paramètres.
if
déclaration, où que soit l'option retenue est basée surrecur
. Cela permettra de calculer tous les paramètres d'abord, et ensuite seulement de manière récursive. La réponse sera le résultat derecur
. Comme le résultat est le même résultat retourné par la récursivité, n'ayant pas d'autres calculs, c'est la queue récursive.recur
retourne, il n'y a rien d'autre à faire. Le "stack frame" n'est plus nécessaire, et leloop
les variables peuvent être re-lié.recur
est un spécial construire en Clojure; le compilateur fait vérifie qu'elle est dans une queue de position.Voici un clojure version:
En raison de la paresse-seq, il est tout à fait général et ne souffle pas de pile
;; Pour vous aider à voir ce qu'il fait:
;; Exemple
Cet exemple utilise de l'etat, puisque, pour moi, c'est une solution pragmatique dans ce cas, et d'une fermeture à créer le fenêtrage fonction de la moyenne:
Il est encore fonctionnel dans le sens de rendre l'utilisation de la première classe de fonctions, si elle n'est pas sans effets secondaires. Les deux langues que vous avez mentionné tous les deux sur le dessus de la JVM et donc à la fois permettre de gestion de l'état lorsque cela est nécessaire.
Cette solution est en Haskell, qui est plus familier pour moi:
Scala de traduction:
Un court Clojure version qui a l'avantage d'être en O(liste de longueur), quelle que soit votre période:
Ce exploite le fait que vous pouvez calculer la somme d'une plage de numéros en créant une somme cumulative de la séquence (par exemple, [1 2 3 4 5] -> [0 1 3 6 10 15]) et puis en soustrayant les deux nombres avec un offset égal à votre période.
Je sais comment j'allais le faire en python (note: les 3 premiers éléments avec les valeurs 0.0 ne sont pas retournés depuis n'est en réalité pas le moyen le plus approprié pour représenter une moyenne mobile). J'imagine des techniques similaires sera possible dans Scala. Voici plusieurs façons de le faire.
On dirait que vous êtes la recherche d'une solution récursive. Dans ce cas, je suggère de changer un peu le problème et le but pour obtenir (4.75, 5.0, 6.0, 7.25, 8.0, 8.25, 6.5, 0.0, 0.0, 0.0) comme une solution.
Dans ce cas, vous pouvez écrire ci-dessous élégante solution récursive en Scala:
Être en retard sur la partie, et de nouveau à la programmation fonctionnelle aussi, j'en suis venu à cette solution avec un intérieur fonction:
J'ai adopté l'idée de diviser l'ensemble de la liste par la période (len) à l'avance.
Puis-je générer la somme de commencer avec le len-premier-éléments.
Et je générer le premier, les éléments invalides (0.0, 0.0, ...) .
Puis-je de manière récursive, il suffit de soustraire la première et ajouter la dernière valeur.
À la fin, je listify l'ensemble de la chose.
En Haskell pseudocode:
ou pointfree
(Maintenant on devrait vraiment abstraction de la 4 ....)
À L'Aide De Haskell:
La clé est la queue de la fonction, qui correspond à une liste d'une liste de copies de la liste d'origine, avec la propriété que le n-ième élément de la suite est à côté de la première à n-1 éléments.
Donc
Nous appliquons fmap (avg . prendre la n) pour le résultat, ce qui signifie que nous prenons la n-longueur du préfixe de la sous-liste, et de calculer sa moyenne. Si la longueur de la liste nous sommes avg require n'est pas n, alors nous n'avons pas à calculer la moyenne (car il n'est pas défini). Dans ce cas, nous ne retournent Rien. Si c'est, nous le faisons, et l'envelopper dans de la "Juste". Enfin, nous courons "catMaybes" sur le résultat de fmap (avg . prendre la n), pour se débarrasser de la Peut-être de type.
J'ai été (surpris et été déçu par les performances de ce qui me semblait le plus idiomatique Clojure solutions, @JamesCunningham 's
lazy-seq
solutions.Voici donc une combinaison de James solution avec @DanielC.Sobral 's idée de s'adapter rapide-exponentiation de déplacement sommes :
Edit: cette fonction sur @mikera 's solution- est encore plus rapide.