Journal Cohérence des fonctions d'arrondi

Posté par  (site web personnel) . Licence CC By‑SA.
44
29
nov.
2016

Étant très inspiré par un récent journal, je me souviens de différences de comportements entre les langages de programmation.

J'ai moins de détails techniques croustillants à mentionner, mais pour ceux qui manipulent des chiffres et des lettres, ça peut être intéressant.

Supposons que l'on veuille arrondir une valeur. -0.5. Facile ? Et bien en fait, pas du tout !

round(-0.5) =

  • Python: -1
  • WolframAlpha: 0
  • PHP: -1
  • JavaScript: -0
  • Matlab: -1
  • Java: 0

Si l'on tient compte du zéro négatif de JS, on a 3 valeurs différentes, et aucune n'est aberrante vu qu'en fait, c'est très louche, la notion d'arrondi. Une petite recherche sur Wikipédia nous donne en fait cinq solutions d'arrondis pour lever l’ambiguïté ou diminuer les erreurs accumulées d'arrondis en arrondis. Plutôt que de se perdre dans les détails, on va regarder les arrondis uniquement dans le domaine de l'informatique :

  • Arrondi vers moins l'infini
  • Arrondi vers plus l'infini
  • Arrondi vers zéro
  • Arrondi au plus loin de zéro
  • Arrondi au nombre pair : si à 0.5 près alors arrondi selon le bit de poids faible donc statistiquement 50% du temps au supérieur et 50% à l'inférieur

Le comportement recommandé est le dernier car statistiquement, les erreurs d'arrondis des .5 ne se cumulent pas mais s'annulent. En pratique, on obtient :

  • round(11.5) = 12
  • round(12.5) = 12
  • round(-11.5) = -12
  • round(-12.5) = -12

On comprend donc mieux le nom d'arrondi vers le nombre pair. Cet arrondi s'appelle aussi arrondi bancaire.

J'espère que vous avez appris quelque chose et qu'un bête arrondi n'a en fait rien de bête même si c'est utilisé quotidiennement à grande échelle.

Dans la même veine, vous pouvez découvrir en anglais les innombrables subtilités des nombres flottants.

Je laisse la main à quelqu'un d'autre dans la série "Cohérences" :)

  • # Merci

    Posté par  . Évalué à 3.

    Merci pour ces curiosités.
    En règle générale, je préfère les arrondis de Matlab; pour les impôts, je préfère Java.
    D'ailleurs, complètement hs, sur ma taxe d'habitation, je n'ai pas réussi à trouver la règle des arrondis: en fonction des colonnes, les règles me semblaient différer, soit les calculs utilisaient les arrondis de certains résultats intermédiaires, soit les valeurs sans arrondis. Je n'ai pas creusé plus.

    Le sujet des flottants/float est un sujet assez récurrent dans les forums en fonction des langages et librairies.

    • [^] # Re: Merci

      Posté par  . Évalué à 3.

      Pour tout ce qui est impôt, la loi privilégie toujours le contribuable. Si ça peut t'aider dans ta compréhension du bouzin…

      • [^] # Re: Merci

        Posté par  . Évalué à 4.

        Si je comprends correctement la notion d’« arrondi bancaire » c’est justement la méthode qui permet de ne privilégier ni le trésor ni le contribuable… (ni la banque ni le client, etc…)

    • [^] # Re: Merci

      Posté par  (site web personnel, Mastodon) . Évalué à 3.

      Pour tout ce qui est calcul monétaire, l'usage des nombres flottants devrait être illégal. Seul l'emploi d'une bibliothèque éprouvée pour les calculs monétaires devrait être considéré. J'ai travaillé sur quelques IS où PHP était utilisé, et les développeurs m'ayant précédé avaient opté pour des flottants, quand il s'agissait du prix des prestations… Évidement, au final, cela se traduisait par une comptabilité incohérente et des fausses factures, des faux bilans, etc. Finalement, l'usage des fonctions bc_* s'est généralisé pour travailler sur les nombres décimaux, alors que malheureusement, le web bouge vers le JS, qui est encore pire en matière de gestion de l'approximation de la représentation des nombres décimaux en nombres flottants, misère !

  • # Python(s)

    Posté par  . Évalué à 10.

    En passant, Python2 renvoie -1, mais Python3 renvoie 0

    • [^] # Re: Python(s)

      Posté par  . Évalué à 3. Dernière modification le 30 novembre 2016 à 09:50.

      >>>>import numpy as np
      >>>>np.round(-0.5)
      -0.0
      

      oO

  • # -0

    Posté par  . Évalué à 4.

    En pratique, la différence entre 0 et -0 est faible. Elle n'a d'importance que quand 0 se retrouve au dénominateur, ce qui ne devrait de toute façon pas arriver.

    Un exemple en R :

    a = c(1, 0, -0)
    1/a
    [1]    1  Inf -Inf
    as.logical(a)
    [1]  TRUE FALSE FALSE
    

    Ça, ce sont les sources. Le mouton que tu veux est dedans.

    • [^] # Re: -0

      Posté par  . Évalué à 1.

      Quelques subtilités des numeric en R, type qui englobe les integers et les doubles :

      a <- c(1L, 0L, -0L)
      1/a
      all(a == c(1, 0, -0))
      identical(a, c(1, 0, -0))
      [1]   1 Inf Inf
      [1] TRUE
      [1] FALSE
      
    • [^] # Re: -0

      Posté par  (site web personnel) . Évalué à 6.

      En pratique, la différence entre 0 et -0 est faible.

      Je me souviens cependant d'un exposé d'un chercheur bossant sur la preuve de programmes flottants qui nous avait expliqué qu'un avion de chasse avait failli partir dans le décor à cause de cette différence…

  • # Haute précision

    Posté par  . Évalué à 1.

    Quid de bibliothèques de haute précision ? Par exemple, GNU MPFR implémente les cinq modes d'arrondis d'IEEE754-2008.

    /* linuxfr.c */
    #include <stdio.h>
    #include <stdlib.h>
    #include <math.h>
    #include <mpfr.h>
    
    void exemplifier(mpfr_t, mpfr_rnd_t, char **, size_t);
    
    int main(int argc, char* argv[]) {
        mpfr_t x;
        mpfr_init2(x, 64);
        char *eleven[] = {"11.5", "-11.5"};
        char *twelve[] = {"12.5", "-12.5"};
    
        exemplifier(x, MPFR_RNDN, eleven, 2);
        exemplifier(x, MPFR_RNDN, twelve, 2);
    
        exemplifier(x, MPFR_RNDZ, eleven, 2);
        exemplifier(x, MPFR_RNDZ, twelve, 2);
    
        exemplifier(x, MPFR_RNDU, eleven, 2);
        exemplifier(x, MPFR_RNDU, twelve, 2);
    
        exemplifier(x, MPFR_RNDD, eleven, 2);
        exemplifier(x, MPFR_RNDD, twelve, 2);
    
        exemplifier(x, MPFR_RNDA, eleven, 2);
        exemplifier(x, MPFR_RNDA, twelve, 2);
    
        mpfr_clear(x);
        mpfr_free_cache();
        return 0;
    }
    
    void exemplifier(mpfr_t val, mpfr_rnd_t rnd, char **lit, size_t size) {
        size_t i = 0;
        fprintf(stdout, "\n");
        for(i = 0; i < size; ++i) {
            mpfr_set_str(val, lit[i], 10, rnd);
            mpfr_out_str(stdout, 10, 10, val, rnd);
            fprintf(stdout, "\t");
        }
        fprintf(stdout, "\n");
    }

    Disclaimer: n'ayant pas de grande aisance en C, je me fie au compilateur pour m'indiquer les éventuelles bourdes de ce que j'aurais commises.

    gcc -lmpfr -lgmp -std=c11 -g -O3 -Wall -pedantic -Wextra -ggdb -fomit-frame-pointer -fstack-protector-all -ftrapv -pipe -flto linuxfr.c -o linuxfr
    1.150000000e1   -1.150000000e1  
    
    1.250000000e1   -1.250000000e1  
    
    1.150000000e1   -1.150000000e1  
    
    1.250000000e1   -1.250000000e1  
    
    1.150000000e1   -1.150000000e1  
    
    1.250000000e1   -1.250000000e1  
    
    1.150000000e1   -1.150000000e1  
    
    1.250000000e1   -1.250000000e1  
    
    1.150000000e1   -1.150000000e1  
    
    1.250000000e1   -1.250000000e1
    
  • # Java c'est plus fort que toi

    Posté par  . Évalué à 5.

    A noter que la classe BigDecimal de Java supporte tous les types d'arrondis possibles, y compris le Round Half Uneven que je n'ai pas vu cité dans les commentaires.

    Il y a une page Wikipédia dédiée au sujet pour ceux que ça passionne : https://en.m.wikipedia.org/wiki/Rounding.

  • # Oui mais...

    Posté par  . Évalué à 1.

    … n'y a t-il pas un arrondi "légal" (celui qu'on apprend à l'école) ?
    De mémoire 0.5 → 1 et -0.5 → -1
    (j'ai un léger doute sur le dernier, 0 ou -1 ?)

    • [^] # Re: Oui mais...

      Posté par  . Évalué à 2.

      De mémoire en math la fonction int est définie et prend le nombre entier inférieur… donc -1,5 --> -2, pour l’arrondi, je ne sais pas s’il y a une définition mathématique.

      • [^] # Re: Oui mais...

        Posté par  . Évalué à 1.

        Les langages de type C/C++/Java (implicitement, Scala/Clojure aussi du coup) utilisent l'arrondi mathématique classique. Par exemple:

        /* rnd.c -- build with LDFLAGS=-lm make rnd */
        #include <stdio.h>
        #include <math.h>
        int main(void) {
            printf("round(%.1f) = %d (%.1f)\n", 0.5, (int)round(0.5), round(0.5));
            return 0;
        }

        … imprimera round(0.5) = 1 (1.0).

        En Scala:

        (math round 0.5)

        … imprimera res0: Long = 1 dans l'interpréteur.

    • [^] # Re: Oui mais...

      Posté par  (site web personnel) . Évalué à 4.

      Ce n'est pas un arrondi "légal" mais un arrondi "logique" au plus près pour les décimaux (https://en.wikipedia.org/wiki/Rounding#Round_half_away_from_zero). L'idée est que si le premier chiffre après la troncature dans un nombre décimal est 5, les éventuels chiffres suivants ne peuvent que le faire s'éloigner de 0 donc 0.5 → 1 et -0.5 → -1.

      Mais c'est logique pour les nombres décimaux, pas pour les flottants, doubles et tout ça.

      • [^] # Re: Oui mais...

        Posté par  . Évalué à 1.

        D'après mes cours de physique, c'est l'arrondi utilisé pour obtenir le nombre de chiffres significatifs.

  • # Virgule flottante

    Posté par  . Évalué à 7.

    Personnellement je considère les nombres à virgule flottante comme les dates : dès qu'il y en a un dans un programme, il y a (au moins) un bug :)

    Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

    • [^] # Re: Virgule flottante

      Posté par  . Évalué à 4.

      Y'a un gang de physiciens et mathématiciens appliqués qui viennent de sentir quelqu'un marcher sur leur tombe. Je leur file ton adresse ? :-)

  • # Comment arrondir une date ?

    Posté par  (site web personnel) . Évalué à 2.

    Je me suis frotté aux erreurs d'arrondi en concevant une API C pour manipuler des dates : https://haypo.github.io/pytime.html

    J'en garde un drôle de souvenir. Avec le langage C, l'arrondit est implicite et les erreurs faciles à commettre.

    L'arrondit Python de la fonction round() est l'arrondit par défaut du très sérieux standard IEEE 754 : https://en.wikipedia.org/wiki/Rounding#Round_half_to_even

    Cet arrondit est celui qui produit le moins d'erreur d'un point de vue statistique :

    "This method treats positive and negative values symmetrically, and is therefore free of sign bias. More importantly, for reasonable distributions of y values, the average value of the rounded numbers is the same as that of the original numbers."

  • # Paramètre

    Posté par  . Évalué à 2.

    Personne n'a eu l'idée d'introduire un paramètre obligatoire dans ces fonctions d'arrondi pour dire quel type d'arrondi on souhaite avoir..?

  • # En Ada :)

    Posté par  (site web personnel, Mastodon) . Évalué à 2.

    Vous auriez été déçu si j'avais réagi pour promouvoir ce langage :)
    En Ada, il y a 3 attributs pour faire ça :
    - S'Rounding qui renvoie l'entier le plus loin de zéro
    - S'Unbiased_Rounding qui ramène au plus proche entier pair
    - S'Machine_Rounding dont la norme précise que le retour est spécifique à la machine cible

    Merci pour ce journal rafraichissant en tout cas

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.