ecart_type() – Exemple

Ce qui suit présume que vous avez compris les techniques derrière le calcul de la moyenne générique à partir de traits et d'algorithmes standards. À titre de rappel, voici un exemple de calcul de la moyenne d'une séquence numérique à partir de cette combinaison de techniques.

#include <iterator>
#include <numeric>
#include <iostream>
#include <algorithm>
using namespace std;

template <class>
   struct reel_traits;
template <>
   struct reel_traits<int> {
      using type = float;
   };
//
// ...
//
template <class T>
   using reel_type_t = typename reel_traits<T>::type;

template <class>
   struct cumul_traits;
template <>
   struct cumul_traits<int> {
      using type = long;
   };
//
// ...
//
template <class T>
   using cumul_type_t = typename cumul_traits<T>::type;

//
// Pour alléger l'écriture
//
template <class T>
   using it_val_t = typename std::iterator_traits<T>::value_type;
template <class It, class C>
   auto moyenne(It debut, It fin, C cumul) {
      return static_cast<reel_type_t<it_val_t<It>>>(
         accumulate(debut, fin, cumul)
      ) / distance(debut, fin);
   }
template <class It>
   auto moyenne(It debut, It fin) {
      return moyenne(debut, fin, cumul_type_t<it_val_t<It>>{});
   }
int main() {
   int tab[] { 2, 3, 5, 7, 11 };
   cout << moyenne(begin(tab), end(tab)) << endl;
   cout << moyenne(begin(tab), end(tab), 0.0) << endl;
}

Comment réaliserons-nous l'écart type? À partir de la formule générale, soit , où la sommation peut être exprimée par un appel à std::accumulate(), où est la moyenne des éléments de la séquence et où est le nombre d'éléments dans la séquence (ce que nous pouvons évaluer par std::distance()). Notez que l'algorithme doit échouer si pour éviter une division par zéro.

La partie lourde du travail, si nous choisissons d'exprimer les termes dans la sommation à l'aide d'un foncteur écrit manuellement, est justement la rédaction du foncteur en question. Notez le recours à CTAD pour alléger la syntaxe dans le code client.

Le calcul de l'écart type lui même se résout en une seule expression, comme le montre l'exemple à droite, et le programme de test est à la fois simple et exact.

#include <cmath>
#include <cassert>
#include <numeric>
#include <iostream>
#include <algorithm>
#include <iterator>
using namespace std;

//
// moyenne() etc., omis par souci de simplicité
//

template <class R>
   class eval_terme_impl {
   public:
      using reel_type = R;
   private:
      reel_type moy;
   public:
      eval_terme(reel_type moy) : moy{ moy } {
      }
      template <class T>
         reel_type operator()(reel_type total_partiel, T x) const {
            return total_partiel + pow(static_cast<reel_type>(x) - moy, 2);
         }
   };
template <class It>
   reel_type_t<it_val_t<It>> ecart_type(It debut, It fin) {
      assert(distance(debut,fin) > 1);
      return sqrt(
         accumulate(
            debut, fin, reel_type_t<it_val_t<It>>{},
            eval_terme(moyenne(debut, fin))
         ) / (distance(debut, fin)-1)
      );
   }
int main() {
   int tab[] { 2, 3, 5, 7, 11 };
   copy(begin(tab), end(tab), ostream_iterator<int>{ cout, " " });
   cout << endl
        << "Moyenne: " << moyenne(begin(tab), end(tab)) << endl
        << "Ecart type: " << ecart_type(begin(tab), end(tab)) << endl;
}

Il est bien sûr possible de réduire la complexité de l'expression de l'écart type en passant par une λ-expression plutôt que par un foncteur écrit à la main.

Examinons l'expression en détail :

  • Nous déterminons la moyenne par la constante mu puisqu'elle ne changera pas une fois évaluée pour un calcul d'écart type donné
  • Nous exprimons ensuite l'équation mathématique attendue, littéralement, soit :
    • la racine carrée de
    • la somme des termes sur
    • le nombre d'éléments dans la séquence moins un – attention aux parenthèses ici pour que la soustraction s'applique au dénominateur, pas à la somme toute entière;
  • Le calcul d'un terme est une λ-expression qui :
    • capture la moyenne mu par le bloc de capture [&] qui ne capture que cette constante du fait que, de toutes les variables et de toutes les constantes disponibles localement, nous n'utilisons qu'elle;
    • prend à chaque appel de l'opérateur () le cumul partiel (un reel_type puisque l'appel à std::accumulate() initialise le cumul avec reel_type{}) et la nouvelle valeur (un value_type puisque c'est ce que contient la séquence d'origine);
    • lors de chaque appel, réalise le calcul attendu et retourne un reel_type (ce qui est indiqué par la clause optionnelle -> reel_type de la signature de l'expression). C'est un cas d'application de la signature unifiée des fonctions.

Voilà!

#include <cmath>
#include <cassert>
#include <numeric>
#include <iostream>
#include <algorithm>
#include <iterator>
using namespace std;
//
// ... voir plus haut pour reel_traits,
// cumul_traits et moyenne()...
//
template <class It>
   reel_type_t<it_val_t<It>> ecart_type(It debut, It fin) {
      assert(distance(debut,fin) > 1);
      const auto mu = moyenne(debut, fin);
      return sqrt(
         accumulate(
            debut, fin, reel_type_t<it_val_t<It>>{},
            [&](reel_type_t<it_val_t<It>> avant, it_val_t<It> maintenant) -> reel_type_t<it_val_t<It>> {
               return avant + pow(static_cast<reel_type_t<it_val_t<It>>>(maintenant) - mu, 2);
            }
         ) / (distance(debut, fin)-1)
      );
   }
int main() {
   int tab[] { 2, 3, 5, 7, 11 };
   copy(begin(tab), end(tab), ostream_iterator<int>{ cout, " " });
   cout << endl
        << "Moyenne: " << moyenne(begin(tab), end(tab)) << endl
        << "Ecart type: " << ecart_type(begin(tab), end(tab)) << endl;
}

Qu'en pensez-vous? Il reste une optimisation possible, si vous en avez envie soit celle qui réduirait le nombre de parcours de la séquence à un seul (plutôt que deux) si les itérateurs sont de catégorie forward_iterator_tag ou bidirectional_iterator_tag. Amusez-vous bien!


Valid XHTML 1.0 Transitional

CSS Valide !