Infoforall

Python-Physique 07 : Gérer les chiffres significatifs

Cette partie vous montrera comment parvenir à gérer vos chiffres significatifs ainsi que la difficulté de réaliser des calculs avec des nombres à virgules en informatique.

Une grande majorité des parties sont optionnelles. Mais regardez (au moins) ce qu'on y présente, cela vous permettra de savoir où aller voir lorsque vous rencontrerez ce type de problème. C'est dense mais cela vous servira vraiment à comprendre certaines "erreurs" que vous pourriez voir dans l'exécution de vos programmes. Et encore, beaucoup de choses ont été cachées sous le tapis...

Si vous zappez une grande partie de cette activité, allez voir la partie 7 : on vous montre un module maison basique qui vous permettra de gérer les chiffres significatifs dans vos propres programmes.

1 - Lecture des listes

Pour l'instant, nous avons vu deux façons de créer des listes de données :

Nous allons maintenant voir qu'on peut stocker bien d'autres choses que des nombres et qu'il existe bien des façons de les créer et les remplir après coup.

Qu'est qu'une liste ? C'est une suite ordonnée et modifiable (mutable) d'objets ou variables quelconques.

Les éléments d'une liste sont séparés par des virgules et la définition de la liste commence avec [ et s'arrête avec ].

 maListe = [1,'a',45.2,"bonjour",'b']  crée une liste de 5 éléments contenant l'integer 1, le char a, le float 45.2, le string bonjour et le char b.

Si on veut afficher une liste dans la console Python, print(maListe) fonctionne.

>>> maListe = [1,'a',45.2,"bonjour",'b']

>>> print(maListe)

[1,'a',45.2,'bonjour','b']

Si on veut connaitre le nombre d'éléments dans une liste, len(maListe) fonctionne.

>>> maListe = [1,'a',45.2,"bonjour",'b']

>>> len(maListe)

5

Si on veut accéder à l'un des éléments d'une liste, on tapera maListe[2], mais attention le premier élément est l'élément 0.

Si on demande print(maListe[2]) avec maListe = [1,'a',45.2,"bonjour",'b'], on obtient :

>>> maListe = [1,'a',45.2,"bonjour",'b']

>>> maListe = [2]

45.2

Si on veut accéder à un ensemble d'éléments d'une liste, on tapera ma_liste[1:4], et on aura les éléments 1,2 et 3 (car cela veut dire qu'on commence à l'élément 1 et qu'on s'arrête avant le 4.).

Si on demande print(ma_liste[1:4]) avec ma_liste = [1, 'a', 45.2,"bonjour", 'b'], on obtient :

>>> maListe = [1,'a',45.2,"bonjour",'b']

>>> print( ma_liste[1:4] )

['a',45.2,'bonjour']

Pour parcourir une liste, il suffit d'utiliser un for.

for element in maListe :

    print(element)

Et on obtient alors

1

a

45.2

bonjour

b

Nous avons également vu qu'on peut utiliser plutôt une boucle for numérique :

for numero in range(len(maListe)) :

    print(maListe[numero])

Voyons trois façons de gérer des mesures expérimentales avec des listes. Reprenons les données de l'activité 1 :

listeI = [ 0.0, 10.2, 19.8, 38.9 ]

listeU = [ 0.0, 1.0, 2.0, 3.9 ]

01° Utiliser le code suivant qui permet d'afficher le contenu des listes I et U. A quoi servent les caractères \n ?

#!/usr/bin/env python

# -*-coding:Utf-8 -*


listeI = [ 0.0, 10.2, 19.8, 38.9 ]

listeU = [ 0.0, 1.0, 2.0, 3.9 ]


print("\nAffichage des données I(mA)")

for I in listeI :

    print(I)


print("\nAffichage des données U(V)")

for U in listeU :

    print(U)

...CORRECTION...

On insère \n pour forcer un passage à la ligne supplémentaire et ainsi créer un affichage clair.

Nous obtenons donc ceci :

Affichage des données I(mA)

0.0

10.2

19.8

38.9


Affichage des données U(V)

0.0

1.0

2.0

3.9

Le problème est qu'on ne lit pas en même temps les deux listes. Pour lire en même temps plusieurs listes ayant le même nombre d'éléments, il faut utiliser un boucle FOR de ce type :

for numero in range(len(maListe)) :

    print(maListe[numero])

02° Créer un programme permettant d'afficher ceci à partir des deux listes précédentes :

Index	U(V)	I(mA)
0	0.0	0.0
1	1.0	10.2
2	2.0	19.8
3	3.9	38.9
					

Rappel : la tabulation s'obtient à l'aide de \t, à insérer directement dans la chaîne de caractères.

...CORRECTION...

#!/usr/bin/env python

# -*-coding:Utf-8 -*


listeI = [ 0.0, 10.2, 19.8, 38.9 ]

listeU = [ 0.0, 1.0, 2.0, 3.9 ]


if len(listeI)==len(listeU) :

    print("Index\tU(V)\tI(mA)")

    for index in range(len(listeI)):

        print("{0}\t{1}\t{2}".format(index, listeU[index], listeI[index]))

Dans la solution proposée, nous utilisons la méthode format qui permet d'écrire sa chaîne de caractères en laissant des 'blancs' caractérisés par les numéros entre accolades :

        "{0}\t{1}\t{2}" . format(index, listeU[index], listeI[index])

L'interpréteur Python remplace alors {0} par l'élément d'index 0 fourni dans format. Avec quelques couleurs pour voir les liaisons :

        "{0}\t{1}\t{2}" . format(index, listeU[index], listeI[index])

03° Créer un programme permettant d'afficher ceci à partir des deux listes précédentes. Il suffira de calculer P à chaque fois, inutile de chercher à stocker les valeurs.

Index	U(V)	I(mA)	P(W)
0	0.0	0.0	0.0
1	1.0	10.2	0.010199999999999999
2	2.0	19.8	0.0396
3	3.9	38.9	0.15170999999999998
					

...CORRECTION...

#!/usr/bin/env python

# -*-coding:Utf-8 -*


listeI = [ 0.0, 10.2, 19.8, 38.9 ]

listeU = [ 0.0, 1.0, 2.0, 3.9 ]


if len(listeI)==len(listeU) :

    print("Index\tU(V)\tI(mA)\tP(W)")

    for index in range(len(listeI)):

        U = listeU[index] # unité : V

        I = listeI[index] # unité : mA

        P = U*I/1000 # unité : W

        print("{0}\t{1}\t{2}\t{3}".format(index, U, I, P))

Comme sur toute calculatrice, Python ne gère pas les chiffres significatifs.

La méthode format permet néanmoins de faire pas mal de réglage sur le résultat obtenu. Voyons comment dans la partie suivante.

2 - Méthode format

Une partie totalement optionnelle. Mais, autant savoir comment gérer les chiffres significatifs. Ou savoir où on peut trouver l'information lorsqu'on veut les gérer.

On peut formater de façon assez précise l'affichage en plaçant des caractères codant le type d'affichage : il suffit pour cela de taper  :  après votre numéro d'index.

Voyons d'abord comment préciser plus clairement la façon dont on veut afficher notre nombre à virgule :

nombre = 1234.123456789

print("En ne plaçant rien : {0}".format(nombre))

print("En plaçant :f : {0:f}".format(nombre))

print("En plaçant :e : {0:e}".format(nombre))

print("En plaçant :E : {0:E}".format(nombre))

print("En plaçant :g : {0:g}".format(nombre))

print("En plaçant :G : {0:G}".format(nombre))

Ce code vous affiche ceci dans la console :

En ne plaçant rien : 1234.123456789

f permet d'avoir un nombre à virgule, 6 chiffres au maximum après la virgule :

En plaçant :f : 1234.123457

e ou E permettent d'avoir l'écriture scientifique, avec une mantisse possèdant 6 chiffres après la virgule au maximum.

En plaçant :e : 1.234123e+03

En plaçant :E : 1.234123E+03

g ou G permet de laisser Python choisir entre les deux écritures. Par contre, il y aura toujours 6 chiffres significatifs. Pas 6 chiffres après la virgule.

En plaçant :g : 1234.12

En plaçant :G : 1234.12

Nous allons détailler un peu plus le cas du float.

04° Utiliser le code proposé ci-dessous pour comprendre comment on peut imposer le nombre de chiffres après la virgule.

On peut changer le nombre de chiffre après la virgule : il suffit de taper .2f et on affichera deux flottants arrondis à deux chiffres après la virgule uniquement.

nombre = 1234.123456789

print("En plaçant :.1f : {0:.1f}".format(nombre))

print("En plaçant :.2f : {0:.2f}".format(nombre))

print("En plaçant :.3f : {0:.3f}".format(nombre))

print("En plaçant :.4f : {0:.4f}".format(nombre))

print("En plaçant :.5f : {0:.5f}".format(nombre))

On obtient alors :

En plaçant :.1f : 1234.1

En plaçant :.2f : 1234.12

En plaçant :.3f : 1234.123

En plaçant :.4f : 1234.1235

En plaçant :.5f : 1234.12346

Ce n'est pas encore de la gestion de chiffres significatifs, mais on pourra au moins faire comme sur les calculatrices. On peut encore faire mieux : on peut imposer un nombre minimum de caractères. Le mot caractère inclu les chiffres mais également le point et le signe.

05° Vérifier si le code suivant correspond bien à l'affichage :

nombre = 123.123456789

print("En plaçant 6.0f : {0:6.0f}".format(nombre))

print("En plaçant 6.1f : {0:6.1f}".format(nombre))

print("En plaçant 6.2f : {0:6.2f}".format(nombre))

print("En plaçant 6.3f : {0:6.3f}".format(nombre))


nombre = - 123.123456789

print("\nEn plaçant 6.0f : {0:6.0f}".format(nombre))

print("En plaçant 6.1f : {0:6.1f}".format(nombre))

print("En plaçant 6.2f : {0:6.2f}".format(nombre))

print("En plaçant 6.3f : {0:6.3f}".format(nombre))

Ca donne :

En plaçant 6.0f :    123
En plaçant 6.1f :  123.1
En plaçant 6.2f : 123.12
En plaçant 6.3f : 123.123

En plaçant 6.0f :   -123
En plaçant 6.1f : -123.1
En plaçant 6.2f : -123.12
En plaçant 6.3f : -123.123						
						

Si on regarde cela en détails, on voit bien qu'on a toujours au moins 6 caractères, des blancs s'il le faut :

En plaçant 6.0f : ...123
En plaçant 6.1f : .123.1
En plaçant 6.2f : 123.12
En plaçant 6.3f : 123.123

En plaçant 6.0f : ..-123
En plaçant 6.1f : -123.1
En plaçant 6.2f : -123.12
En plaçant 6.3f : -123.123						
							

06° Utiliser ces nouvelles connaissances pour obtenir un affichage de ce type :

     Index      U(V)     I(mA)      P(W)
         0       0.0       0.0     0.000
         1       1.0      10.2     0.010
         2       2.0      19.8     0.040
         3       3.9      38.9     0.152
					

Remarque : ce n'est toujours pas ça au niveau des chiffres significatifs puisqu'on doit imposer un nombre fixe de chiffre après la virgule sur toute la colonne.

...CORRECTION...

#!/usr/bin/env python

# -*-coding:Utf-8 -*


listeI = [ 0.0, 10.2, 19.8, 38.9 ]

listeU = [ 0.0, 1.0, 2.0, 3.9 ]


if len(listeI)==len(listeU) :

    print("{0:>10s}{1:>10s}{2:>10s}{3:>10s}".format("Index", "U(V)", "I(mA)", "P(W)"))

    for index in range(len(listeI)):

        U = listeU[index] # unité : V

        I = listeI[index] # unité : mA

        P = U*I/1000 # unité : W

        print("{0:>10d}{1:>10.1f}{2:>10.1f}{3:>10.3f}".format(index, U, I, P))

Une dernière remarque :

On peut changer le calage de l'affichage vers la droite : on peut caler à gauche ou même centrer :

nombre = 123.123456789

print("En plaçant <8.1f : {0:<8.1f}".format(nombre))

print("En plaçant ^8.1f : {0:^8.1f}".format(nombre))

print("En plaçant >8.1f : {0:>8.1f}".format(nombre))

En plaçant <8.1f : 123.1   
En plaçant ^8.1f :  123.1  
En plaçant >8.1f :    123.1						
						

07° Modifiez votre dernier programme pour tout mettre à gauche ou centrer :

Index     U(V)      I(mA)     P(W)      
0         0.0       0.0       0.000     
1         1.0       10.2      0.010     
2         2.0       19.8      0.040     
3         3.9       38.9      0.152
					
  Index      U(V)     I(mA)      P(W)   
    0        0.0       0.0      0.000   
    1        1.0       10.2     0.010   
    2        2.0       19.8     0.040   
    3        3.9       38.9     0.152
					

...CORRECTION...

Il suffit de remplacer dans les accolades les signes supérieurs > par des signes < ou ^.

Alors, comment peut-on utiliser tout ceci pour afficher correctement les chiffres significatifs ? C'est dans la partie suivante.

3 - Gestion des chiffres significatifs avec format

Il faut revenir à l'écriture scientifique : 0,152 = 1,52.10-1.

Si on parvient à trouver que l'unité (1) ici est à une position derrière le zéro, on pourra ainsi facilement trouver combien de chiffres on doit afficher derrière le zéro.

Voyons comment :

Retrouver la position du chiffre de l'unité du nombre en écriture scientifique

Il faut commencer par appliquer le logarithme décimal du nombre :

>>> import math

>>> math.log10(0.753)

-0.12320502379929942

Il faudrait alors trouver le nombre entier juste inférieur avec la fonction floor, comme sol.

>>> import math

>>> math.floor(7.53)

7

>>> math.floor(0.53)

0

>>> math.floor(-0.53)

-1

En conclusion, on récupère facilement la position de notre unité en associant les deux fonctions :

>>> import math

>>> position = math.log10(0.753)

>>> position = math.floor(position)

>>> position

-1

Ou encore plus simple : en une ligne :

>>> import math

>>> position = math.floor(math.log10(0.753))

>>> position

-1

08° Utiliser le code suivant pour qu'il gère correctement l'affichage des données comme ci-dessous. Il faudra modifier la façon dont on génére la variable decalage dans la fonction ag (pour affichage de la grandeur):

On veut obtenir ceci :

     Index      U(V)     I(mA)      P(W)
         0       0.0       0.0     0.000
         1       1.0      10.2     0.010
         2       2.0      19.8     0.040
         3       3.9      38.9      0.15
					

#!/usr/bin/env python

# -*-coding:Utf-8 -*


import math


# - - - - - - - - - - - - - - - - - -

# Déclarations des fonctions

# - - - - - - - - - - - - - - - - - -


def pos(nombre) :

    """

    Renvoie la valeur de l'exposant en puissance de 10 en écriture scientifique

    n = 120, la fonction renvoie 2

     n = 1,2 la fonction renvoie 1

    n = 0.012, la fonction renvoie -2

    Pour un nombre inférieur à 1, cela correspond à la position après la virgule.

    """

    if nombre > 0 :

        return( math.floor( math.log10(nombre)))

    elif nombre < 0 :

        return( math.floor( math.log10(-nombre)))

    else :

        return(0)


def ag(nombre,cs=1,largeur=10) :

    """ Renvoie un string représentant le nombre avec le bon nombre de CS"""

    # On récupère la valeur de l'exposant en écriture scientifique

    valExp = pos(nombre)

    # On calcule jusqu'à quel décalage on garde après la virgule

    decalage = -valExp+cs

    # On génére la chaîne de commande du format

    commande = ":>" + str(largeur) + "." + str(decalage) + "f"

    # On crée la chaîne non formatée

    chaine = "{" + commande + "}"

    # On renvoie le string formaté

    return(chaine.format(nombre))


...CORRECTION...

Il suffit de rajouter -1 si valExp est négatif :


    decalage = -valExp+cs-1

Cela semble bien fonctionner mais en réalité, le script n'est fonctionnel qu'avec des nombres possèdant au moins un chiffre après la virgule. Nous allons voir plus bas comment gérer le cas des entiers.

Deux remarques :

Paramètres par défaut

Une nouveauté apparaît dans l'une des fonctions :

def ag(nombre,cs=1,largeur=10) :

On voit qu'on impose dans la déclaration de la fonction des valeurs à certains paramètres. Il s'agit de valeurs par défaut : si on ne transmet pas ces paramètres, la fonction utilisera les valeurs par défaut.

Regardons l'appel suivant :

ag(P,2)

On ne donne que deux arguments qui seront affectés à nombre et cs.

largeur ne reçoit rien : le programme y placera 10 par défaut.

Création de module

Le code des fonctions n'a rien à voir avec le programme de physique et d'informatique destiné aux élèves. Il s'agit juste d'obtenir un affichage des données cohérent en terme de chiffres significatifs. Nous allons voir qu'on peut cacher ces fonctions et les importer en toute discrétion.

Le code à transmettre deviendrait donc quelque chose comme :

#!/usr/bin/env python

# -*-coding:Utf-8 -*


import math

from phytonphysique import *


listeI = [ 0.0, 10.2, 19.8, 38.9 ]

listeU = [ 0.0, 1.0, 2.0, 3.9 ]


if len(listeI)==len(listeU) :

    print("{0:>10s}{1:>10s}{2:>10s}{3:>10s}".format("Index", "U(V)", "I(mA)", "P(W)"))

    for index in range(len(listeI)):

        U = listeU[index] # unité : V

        I = listeI[index] # unité : mA

        P = U*I/1000 # unité : W

        print("{0:>10d}{1}{2}{3}".format(index, ag(U,2), ag(I,3), ag(P,2)))

Ce sera déjà un peu mieux. On pourra même cacher encore plus de chose. Nous le verrons en fin d'activité.

4 - Le problème des flottants

Les calculs informatiques basés sur les nombres flottants provoquent parfois des erreurs. Voyons pourquoi !

09° Ouvrir la console Python et taper ceci. Est-ce étonnant ?

>>> 0.1+0.1+0.1-0.3

>>> 0.1+0.1+0.1+0.3

...CORRECTION...

On constate que les langages du programmation ont du mal à faire des calculs exacts avec les flottants. C'est un problème connu de tous provenant de la façon dont les réels à virgule sont encodés sous forme de bits.


Ainsi :  0.1+0.1+0.1-0.3  donne  5.551115123125783e-17 .


De la même façon :  0.1+0.1+0.1+0.3  donne  0.6000000000000001 .

Il y a la fonction fsum du module math qui permet de faire mieux :

>>> import math

>>> math.fsum([0.1,0.1,0.1,0.3])

0.6

>>> math.fsum([0.1,0.1,0.1])

0.30000000000000004

>>> math.fsum([0.1,0.1,0.1,-0.3])

2.7755575615628914e-17

Ca fonctionne bien sur la première addition, un peu moins si le résultat tend vers 0...

C'est un peu technique non ?

Oui, un peu mais ...

Cela vous permet de comprendre qu'il faut éviter de faire un test d'égalité sur deux nombres si l'un des deux est un float.

Imaginons une variable distance contenant la distance entre un capteur et un obstacle. Cette variable est un float.

>>> distance = 40.00001

>>> distance == 40

False

>>> distance == 40.0

False

>>> (0.1+0.1-0.2) == 0

False

Si vraiment vous devez faire un tel test, il existe la fonction isclose du module math. On doit fournir les deux nombres à comparer et fournir la tolérance absolue sur cette comparaison :

>>> distance = 40.00001

>>> consigne == 40

>>> math.isclose(distance, consigne, abs_tol=1e-3)

True

>>> math.isclose(distance, consigne, abs_tol=1e-6)

False

Sachez qu'on peut aussi lui demander de comparer les deux avec une tolérance relative. Voici deux exemples avec une comparaison à 5% près et 1% prés.

>>> distance = 40.00001

>>> consigne == 40

>>> math.isclose(distance, consigne, rel_tol=0.05)

True

>>> math.isclose(distance, consigne, rel_tol=0.01)

False

Si vous ne fournissez aucune tolérance, la tolérance relative par défaut est de 10-9 et la tolérance absolue de 0.

D'où vient le problème ? De la façon dont on encode les flottants : on les enregistre sous la forme nombre = mantisse . 2 exposant.

Bien entendu, ma mantisse n'est pas enregistrée sur un nombre infini de bits et il y a nécéssairement des différences entre le nombre voulu et le nombre réellement stocké. L'erreur est minime mais non négligeable dans certains cas.

Module decimal

Si vous avez VRAIMENT besoin d'une précision absolue dans vos projets, il va falloir utiliser un module particulier de Python : le module decimal. Mais ca ne réglera pas notre problème de chiffres significatifs : 14.20 s'affichera que 14.2. C'est pourquoi je ne fais qu'en parler ici.

from decimal import *

py_getcontext().prec = 3

a = Decimal(0.1)

b = Decimal(0.1)

c = Decimal(0.1)

d = a+b+c

e = a-b

print(d)

print(e)

0.300

0E-55

LIEN VERS LA DOCUMENTATION DE PYTHON 3.7

5 - Arrondir les nombres avec round

Nous venons de voir comment modifier l'affichage et comment comparer deux nombres flottants. Mais comment réellement arrondir un nombre ? De cette façon, 0.1+0.1+0.1 donnerait bien 0.3.

Une réponse simple : utiliser une fonction native de Python : round.

Si vous voulez obtenir un résultat plus présentable, il suffit alors de préciser la position à partir de laquelle vous voulez arrondir votre nombre :

Exemples avec quelques arrondis après la virgule :

>>> n = 1234.56789

>>> round(n,9)     1234.56789

>>> round(n,5)     1234.56789

>>> round(n,4)     1234.5679

>>> round(n,3)     1234.568

>>> round(n,2)     1234.57

>>> round(n,1)     1234.6

>>> round(n)       1235

>>> round(n,0)     1235.0

On voit qu'il y a une différence entre utiliser round(n) et round(n,0) : si on ne place pas 0, la fonction renvoie un integer. Sinon, c'est un float.

Autre remarque : imposer 9 ne va pas ici jusq'à rajouter des zéros jusqu'à la 9e position après la virgule.

Pour arrondir avant la virgule, il suffit de fournir un argument négatif : -1 pour la dizaine, -2 pour la centaine ... :

>>> n = 1234.56789

>>> round(n,-1)     1230.0

>>> round(n,-2)     1200.0

>>> round(n,-3)     1000.0

Voilà. C'est tout. Quelques derniers exemples ?

>>> round(12.5)     12

>>> round(13.5)     14

10° Voyez-vous un "bug" dans la façon dont Python arrondit ?

...CORRECTION...

L'arrondi de 12.5 aurait dû donner 13 !

Je vous rassure, il ne s'agit pas d'un simple bug mais d'une programmation volontaire. Les langages informatiques n'arrondissent pas comme nous pour éviter les biais statistiques.

Regardons comment fonctionne notre façon 'humaine' d'arrondir :

>>> arrondiHumain(5.6)    6

>>> arrondiHumain(5.5)    6

>>> arrondiHumain(5.4)    5

>>> arrondiHumain(4.6)    5

>>> arrondiHumain(4.5)    5

>>> arrondiHumain(4.4)    4

La moyenne des trois valeurs est (5.6+5.5+5.4+4.6+4.5+4.4)/6, soit 5.0.

La moyenne des valeurs arrondies est (6+6+5+5+5+4)/6, soit 5.2.

Pourquoi un écart positif ? Simplement parce qu'on arrondit toujours au supérieur lorsqu'on tombe sur un 5.


Si vous aviez un grand nombre de mesures positives et négatives en proportion égale, cela ne posserait pas de problème puisque les deux arrondis se compenserait :

>>> arrondiHumain(7.5)    8

>>> arrondiHumain(-5.5)   -6

Dans le premier arrondi, on rajoute 0.5. Dans le second, on rajoute -0.5. La moyenne ne serait donc quasiment pas modifiée avec un grand nombre de données, si on a bien du 50/50.

C'est pour cela que Python n'arrondit pas comme vous le voulez : dans la plupart des cas, vous n'allez pas avoir cette proportion 50/50 de valeurs positives et négatives qui compenserait naturellement les arrondis sur un 5.

Pour régler ce problème sur les moyennes effectuées à partir d'arrondis, le standard IEEE-754 propose une solution astucieuse :

11° Utiliser le programme fourni pour tenter de comprendre comment Python effectue les arrondis :

#!/usr/bin/env python

# -*-coding:Utf-8 -*


for nombre in range(100) :

    nombre = nombre/10

    print(round(nombre))

La solution est un peu plus bas. En entendant, voici le résultat fourni par le programme. Alors ?

0.0  est arrondi en  0
0.1  est arrondi en  0
0.2  est arrondi en  0
0.3  est arrondi en  0
0.4  est arrondi en  0
0.5  est arrondi en  0
0.6  est arrondi en  1
0.7  est arrondi en  1
0.8  est arrondi en  1
0.9  est arrondi en  1
1.0  est arrondi en  1
1.1  est arrondi en  1
1.2  est arrondi en  1
1.3  est arrondi en  1
1.4  est arrondi en  1
1.5  est arrondi en  2
1.6  est arrondi en  2
1.7  est arrondi en  2
1.8  est arrondi en  2
1.9  est arrondi en  2
2.0  est arrondi en  2
2.1  est arrondi en  2
2.2  est arrondi en  2
2.3  est arrondi en  2
2.4  est arrondi en  2
2.5  est arrondi en  2
2.6  est arrondi en  3
2.7  est arrondi en  3
2.8  est arrondi en  3
2.9  est arrondi en  3
3.0  est arrondi en  3
3.1  est arrondi en  3
3.2  est arrondi en  3
3.3  est arrondi en  3
3.4  est arrondi en  3
3.5  est arrondi en  4
3.6  est arrondi en  4
3.7  est arrondi en  4
3.8  est arrondi en  4
3.9  est arrondi en  4
4.0  est arrondi en  4
4.1  est arrondi en  4
4.2  est arrondi en  4
4.3  est arrondi en  4
4.4  est arrondi en  4
4.5  est arrondi en  4
4.6  est arrondi en  5
4.7  est arrondi en  5
4.8  est arrondi en  5
4.9  est arrondi en  5
5.0  est arrondi en  5
5.1  est arrondi en  5
5.2  est arrondi en  5
5.3  est arrondi en  5
5.4  est arrondi en  5
5.5  est arrondi en  6
5.6  est arrondi en  6
5.7  est arrondi en  6
5.8  est arrondi en  6
5.9  est arrondi en  6
6.0  est arrondi en  6
6.1  est arrondi en  6
6.2  est arrondi en  6
6.3  est arrondi en  6
6.4  est arrondi en  6
6.5  est arrondi en  6
6.6  est arrondi en  7
6.7  est arrondi en  7
6.8  est arrondi en  7
6.9  est arrondi en  7
7.0  est arrondi en  7
7.1  est arrondi en  7
7.2  est arrondi en  7
7.3  est arrondi en  7
7.4  est arrondi en  7
7.5  est arrondi en  8
7.6  est arrondi en  8
7.7  est arrondi en  8
7.8  est arrondi en  8
7.9  est arrondi en  8
8.0  est arrondi en  8
8.1  est arrondi en  8
8.2  est arrondi en  8
8.3  est arrondi en  8
8.4  est arrondi en  8
8.5  est arrondi en  8
8.6  est arrondi en  9
8.7  est arrondi en  9
8.8  est arrondi en  9
8.9  est arrondi en  9
9.0  est arrondi en  9
9.1  est arrondi en  9
9.2  est arrondi en  9
9.3  est arrondi en  9
9.4  est arrondi en  9
9.5  est arrondi en  10
9.6  est arrondi en  10
9.7  est arrondi en  10
9.8  est arrondi en  10
9.9  est arrondi en  10

Si vous n'avez pas trouvé, voici un indice : il faut regarder uniquement ce qui se passe lorsqu'on arrondi un 5. Dans les autres cas, Python se comporte normalement.

0.5  est arrondi en  0
1.5  est arrondi en  2
2.5  est arrondi en  2
3.5  est arrondi en  4
4.5  est arrondi en  4
5.5  est arrondi en  6
6.5  est arrondi en  6
7.5  est arrondi en  8
8.5  est arrondi en  8
9.5  est arrondi en  10

Et voilà : pour réduire les biais statistiques dus à l'arrondi en 0.5, Python arrondit parfois au supérieur, parfois à l'inférieur. Comment ? Simplement en tenant compte du chiffre devant le 5 :

On utilise le même scénario avec de rapprochement vers 0 pour les chiffres pairs et d'éloignement pour les chiffres impairs :

-0.5  est arrondi à l'inférieur en  0
-2.5  est arrondi à l'inférieur en  -2
-3.5  est arrondi à l'inférieur en  -4
-5.5  est arrondi à l'inférieur en  -6

12° Conclusion : est-il donc normal que l'arrondi à la dizaine de  125  donne  120  avec la fonction round ? Vous pouvez tester en tapant round(125,-1).

...CORRECTION...

 125  devient  120  car il y a un 2 (pair) devant le 5 : on arrondit donc à l'inférieur pour se rapprocher de 0.

Le mystère est résolu.

On retiendra donc que la fonction native round fonctionne très bien, merci pour elle. Si vous avez besoin de faire des arrondis sur un grand échantillon de données, autant l'utiliser.

Par contre, si vous voulez faire des arrondis humains (pour une présentation avec le bon nombre de chiffres significatifs par exemple), soit vous acceptez d'avoir parfois des arrondis étranges, soit vous créez votre propre fonction.

6 - Afficher correctement les chiffres significatifs

Voyons maintenant comment utiliser la fonction native round dans la fonction que nous avions créé pour renvoyer une chaîne de caractères affichant correctement nos nombres :

def ag(nombre,cs=1,largeur=10) :

    """ Renvoie un string représentant le nombre avec le bon nombre de CS"""

    # On récupère la valeur de l'exposant en écriture scientifique

    valExp = pos(nombre)

    # On calcule jusqu'à quel décalage on garde après la virgule

    decalage = -valExp+cs-1


    # DEBUT MODIF

    # On arrondit avec round

    nombre = round(nombre, decalage)

    valExp = pos(nombre)

    decalage = -valExp+cs-1

    # On génére la chaîne de commande en fonction du type

    if decalage <=0 : # on doit afficher un nombre entier, round commandée avec decalage négatif

        nombre = int(nombre)

        commande = ":>" + str(largeur) + "d"

    else :

        commande = ":>" + str(largeur) + "." + str(decalage) + "f"

    # FIN MODIF


    # On crée la chaîne non formatée

    chaine = "{" + commande + "}"

    # On renvoie le string formaté

    return(chaine.format(nombre))

Voyons un peu comment cela fonctionne :

Imaginons qu'on utiilse ag(12437,3).

def ag(nombre,cs=1,largeur=10) :

On rentre dans la fonction et on affecte nombre à 12437.3, cs à 3 et largeur à 10 par défaut.

    valExp = pos(nombre)

La variable valExp contient alors 4 car 12437,3 = 1.24373.104.

    decalage = -valExp+cs-1

La variable decalage est affectée à -4+3-1 = -2.

    nombre = round(nombre, decalage)

    valExp = pos(nombre)

    decalage = -valExp+cs-1

C'est ici que commence le rajout : la variable nombre est arrondi à la centaine (-2) et on y affecte le résultat : 12400. On refait le calcul de décalage pour le cas (rare) où 09 devient 10.

    if decalage <=0 : # on doit afficher un nombre entier, round commandée avec decalage négatif

        nombre = int(nombre)

        commande = ":>" + str(largeur) + "d"

On rentre bien dans la condition et on affiche donc le tout comme un entier ( la commande "d" signifie decimal integer, un entier sous forme décimal.

13° Utilisez le code fourni et rajouter quelques nombres à afficher pour voir si cela fonctionne bien.

#!/usr/bin/env python

# -*-coding:Utf-8 -*


import math


# - - - - - - - - - - - - - - - - - -

# Déclarations des fonctions

# - - - - - - - - - - - - - - - - - -


def pos(nombre) :

    """

    Renvoie la valeur de l'exposant en puissance de 10 en écriture scientifique

    n = 120, la fonction renvoie 2

     n = 1,2 la fonction renvoie 1

    n = 0.012, la fonction renvoie -2

    Pour un nombre inférieur à 1, cela correspond à la position après la virgule.

    """

    if nombre > 0 :

        return( math.floor( math.log10(nombre)))

    elif nombre < 0 :

        return( math.floor( math.log10(-nombre)))

    else :

        return(0)


def ag(nombre,cs=1,largeur=10) :

    """ Renvoie un string représentant le nombre avec le bon nombre de CS"""

    # On récupère la valeur de l'exposant en écriture scientifique

    valExp = pos(nombre)

    # On calcule jusqu'à quel décalage on garde après la virgule

    decalage = -valExp+cs-1


    # DEBUT MODIF

    # On arrondit avec round

    nombre = round(nombre, decalage)

    valExp = pos(nombre)

    decalage = -valExp+cs-1

    # On génére la chaîne de commande en fonction du type

    if decalage <=0 : # on doit afficher un nombre entier, round commandée avec decalage négatif

        nombre = int(nombre)

        commande = ":>" + str(largeur) + "d"

    else :

        commande = ":>" + str(largeur) + "." + str(decalage) + "f"

    # FIN MODIF


    # On crée la chaîne non formatée

    chaine = "{" + commande + "}"

    # On renvoie le string formaté

    return(chaine.format(nombre))


# - - - - - - - - - - - - - - - - - -

# Corps du programme

# - - - - - - - - - - - - - - - - - -


print(ag(12437,1))

print(ag(12437,3))

print(ag(12437,6))

print(ag(0.9874,1))

print(ag(0.9874,3))

print(ag(0.9874,6))

Cela fonctionne bien mais on ne passe jamais en notation scientifique puisqu'on impose un affichage entier ou flottant. Voici une dernière version qui passe en notation scientifique si le nombre est supérieur à un million ou inférieure à un micro.

J'ai donc rajouté un test pour savoir si le nombre est trop grand ou petit. Dans ce cas, on passe à un affichage en écriture scientifique en utilisant la valeur contenue dans cs pour définir le nombre de chiffre après la virgule.

#!/usr/bin/env python

# -*-coding:Utf-8 -*


import math


# - - - - - - - - - - - - - - - - - -

# Déclarations des fonctions

# - - - - - - - - - - - - - - - - - -


def pos(nombre) :

    """

    Renvoie la valeur de l'exposant en puissance de 10 en écriture scientifique

    n = 120, la fonction renvoie 2

     n = 1,2 la fonction renvoie 1

    n = 0.012, la fonction renvoie -2

    Pour un nombre inférieur à 1, cela correspond à la position après la virgule.

    """

    if nombre > 0 :

        return( math.floor( math.log10(nombre)))

    elif nombre < 0 :

        return( math.floor( math.log10(-nombre)))

    else :

        return(0)


def ag(nombre,cs=1,largeur=10) :

    """ Renvoie un string représentant le nombre avec le bon nombre de CS"""

    # On récupère la valeur de l'exposant en écriture scientifique

    valExp = pos(nombre)

    # On calcule jusqu'à quel décalage on garde après la virgule

    decalage = -valExp+cs-1


    # DEBUT MODIF

    # On arrondit avec round

    nombre = round(nombre, decalage)

    valExp = pos(nombre)

    decalage = -valExp+cs-1

    # On génére la chaîne de commande en fonction du type

    if (nombre < 1E-6 or nombre > 1E6) and (nombre !=0) : # on passe en écriture scientifique

        commande = ":>" + str(largeur) + "." + str(cs) + "e"

    elif decalage <=0 : # on doit afficher un nombre entier, round commandée avec decalage négatif

        nombre = int(nombre)

        commande = ":>" + str(largeur) + "d"

    else :

        commande = ":>" + str(largeur) + "." + str(decalage) + "f"

    # FIN MODIF


    # On crée la chaîne non formatée

    chaine = "{" + commande + "}"

    # On renvoie le string formaté

    return(chaine.format(nombre))


# - - - - - - - - - - - - - - - - - -

# Corps du programme

# - - - - - - - - - - - - - - - - - -


print(ag(12437,1))

print(ag(12437,3))

print(ag(12437,6))

print(ag(0.9874,1))

print(ag(0.9874,3))

print(ag(0.9874,6))

print(ag(12437000,4))

print(ag(0.0000009874,4))

14° Utilisez ce dernier code et tenter d'y trouver des limites.

La limite est simple : la largeur de 10 est la largeur minimale. Pour des affichages en colonne, il aurait fallu pouvoir prévoir la largeur à rajouter si l'un des termes prend plus de place que prévu.

Ca fait néanmoins beaucoup de codes impressionnant pour les élèves, tout ça pour gérer les chiffres significatifs. Nous allons maintenant cacher tout cela de façon à rendre le code plus facile à utiliser.

7 - Le module gestion des chiffres significatifs

Nos fonctions ne sont pas parfaites mais vont permettre d'afficher correctement nos résultats expérimentaux. Nous allons donc les enregistrer dans un fichier qu'on devra placer au même endroit que notre programme Python.

15° Créer un fichier Python nommé gestionCS.py contenant ceci :

#!/usr/bin/env python

# -*-coding:Utf-8 -*


import math


# - - - - - - - - - - - - - - - - - -

# Déclarations des fonctions

# - - - - - - - - - - - - - - - - - -


def pos(nombre) :

    """

    Renvoie la valeur de l'exposant en puissance de 10 en écriture scientifique

    n = 120, la fonction renvoie 2

     n = 1,2 la fonction renvoie 1

    n = 0.012, la fonction renvoie -2

    Pour un nombre inférieur à 1, cela correspond à la position après la virgule.

    """

    if nombre > 0 :

        return( math.floor( math.log10(nombre)))

    elif nombre < 0 :

        return( math.floor( math.log10(-nombre)))

    else :

        return(0)


def ag(nombre,cs=1,largeur=10) :

    """ Renvoie un string représentant le nombre avec le bon nombre de CS"""

    # On récupère la valeur de l'exposant en écriture scientifique

    valExp = pos(nombre)

    # On calcule jusqu'à quel décalage on garde après la virgule

    decalage = -valExp+cs-1


    # DEBUT MODIF

    # On arrondit avec round

    nombre = round(nombre, decalage)

    valExp = pos(nombre)

    decalage = -valExp+cs-1

    # On génére la chaîne de commande en fonction du type

    if (nombre < 1E-6 or nombre > 1E6) and (nombre !=0) : # on passe en écriture scientifique

        commande = ":>" + str(largeur) + "." + str(cs) + "e"

    elif decalage <=0 : # on doit afficher un nombre entier, round commandée avec decalage négatif

        nombre = int(nombre)

        commande = ":>" + str(largeur) + "d"

    else :

        commande = ":>" + str(largeur) + "." + str(decalage) + "f"

    # FIN MODIF


    # On crée la chaîne non formatée

    chaine = "{" + commande + "}"

    # On renvoie le string formaté

    return(chaine.format(nombre))

16° Créer un fichier Python nommé comme vous le voulez, sauf gestionCS.py contenant le code suivant. Veuillez à bien le placer dans le même dossier que votre module précédent.

#!/usr/bin/env python

# -*-coding:Utf-8 -*


from gestioncs import *


# - - - - - - - - - - - - - - - - - -

# Corps du programme

# - - - - - - - - - - - - - - - - - -


print(ag(12437,1))

print(ag(12437,3))

print(ag(12437,6))

print(ag(0.9874,1))

print(ag(0.9874,3))

print(ag(0.9874,6))

print(ag(12437000,4))

print(ag(0.0000009874,4))

Si vous lancez, vous devriez avoir ceci :

     10000
     12400
   12437.0
         1
     0.987
  0.987400
1.2440e+07
9.8740e-07

17° Utiliser le module pour afficher correctement les résultats qu'on avait obtenu avec le code suivant :

# - - - - - - - - - - - - - - - - - -

# Corps du programme

# - - - - - - - - - - - - - - - - - -


listeI = [ 0.0, 10.2, 19.8, 38.9 ]

listeU = [ 0.0, 1.0, 2.0, 3.9 ]


if len(listeI)==len(listeU) :

    print("{0:>10s}{1:>10s}{2:>10s}{3:>10s}".format("Index", "U(V)", "I(mA)", "P(W)"))

    for index in range(len(listeI)):

        U = listeU[index] # unité : V

        I = listeI[index] # unité : mA

        P = U*I/1000 # unité : W

        print("{0:>10d}{1}{2}{3}".format(index, ag(U,2), ag(I,3), ag(P,2)))

     Index      U(V)     I(mA)      P(W)
         0       0.0      0.00       0.0
         1       1.0      10.2     0.010
         2       2.0      19.8     0.040
         3       3.9      38.9      0.15

...CORRECTION...

#!/usr/bin/env python

# -*-coding:Utf-8 -*


from gestioncs import *


# - - - - - - - - - - - - - - - - - -

# Corps du programme

# - - - - - - - - - - - - - - - - - -


listeI = [ 0.0, 10.2, 19.8, 38.9 ]

listeU = [ 0.0, 1.0, 2.0, 3.9 ]


if len(listeI)==len(listeU) :

    print("{0:>10s}{1:>10s}{2:>10s}{3:>10s}".format("Index", "U(V)", "I(mA)", "P(W)"))

    for index in range(len(listeI)):

        U = listeU[index] # unité : V

        I = listeI[index] # unité : mA

        P = U*I/1000 # unité : W

        print("{0:>10d}{1}{2}{3}".format(index, ag(U,2), ag(I,3), ag(P,2)))

8 - Arrondir avec ceil et floor

Et nous voici enfin dans cette dernière partie : comment parvenir à arrondir correctement les données et les afficher ensuite avec le bon nombre de chiffres significatifs ? C'est totalement optionnel. Autant garder la fonction native round.

Pour cela, nous allons avoir besoin de plusieurs fonctions du module math :

Il faudra donc d'abord commencer par importer le module :

>>> import math

Alors, comment arrondir comme un humain avec cela ? C'est simple :

  • Si le nombre est positif, on va rajouter 0.5 à notre nombre et arrondir le tout à l'inférieur (floor):
  • >>> math.floor(4.4+0.5)     4

    >>> math.floor(4.5+0.5)     5

    >>> math.floor(4.6+0.5)     5

  • Si le nombre est négatif, on va rajouter -0.5 à notre nombre et arrondir le tout au supérieur (ceil):
  • >>> math.ceil(-4.4-0.5)     -4

    >>> math.ceil(-4.5-0.5)     -5

    >>> math.ceil(-4.6-0.5)     -5

18° Utiliser la fonction arrondir ci-dessous. Rajouter d'autres tests pour vérifier qu'elle renvoie bien l'arrondi attendu par un être humain.

#!/usr/bin/env python

# -*-coding:Utf-8 -*


import math


def arrondir(nombre) :

    """ Renvoie un arrondi à l'entier"""

    if nombre > 0 :

        reponse = math.floor(nombre+0.5)

    elif nombre < 0 :

        reponse = math.ceil(nombre-0.5)

    else :

        reponse = 0

    return(reponse)


print(arrondir(45.5))

Ca fonctionne bien mais on ne peut arrondir avec qu'à l'unité. Comment faire pour arrondir où on veut ?

Il faut travailler en trois étapes. Voici un exemple avec la centaine :

  1. On divise par une centaine
  2. >>> nombre = 14860.2

    >>> nombre = nombre /100

    >>> nombre = math.floor(nombre+0.5)

    >>> nombre

    148.602

  3. On arrondit le nouveau nombre à l'unité
  4. >>> nombre = math.floor(nombre+0.5)

    >>> nombre

    149

  5. On multiplie par la centaine
  6. >>> nombre = nombre*100

    >>> nombre

    14900

Un deuxième exemple avec le centième :

  1. On divise par un centième
  2. >>> nombre = 1.48602

    >>> nombre = nombre /0.01

    >>> nombre = math.floor(nombre+0.5)

    >>> nombre

    148.60199999999998

  3. On arrondit le nouveau nombre à l'unité
  4. >>> nombre = math.floor(nombre+0.5)

    >>> nombre

    149

  5. On multiplie par un centième
  6. >>> nombre = nombre*0.01

    >>> nombre

    1.49

19° En vous basant sur les exemples ci-dessus, créer une fonction arrondir à laquelle on doit fournir le nombre à arrondir et la puissance de 10 à laquelle on doit arrondir.

20° Il vous reste maintenant à créer un second module de gestion des chiffres significatifs dans lequel vous ferez appel à votre fonction et pas à round.. Pensez à rajouter votre fonction maison de le module lui-même !