Partie IV - Calcul scientifique · Chapitre 16

Bibliothèques mathématiques

3 sections de cours · fiche d'exercices corrigee

1Modules, importation et le module math

Reecrire une racine carree ou un generateur aleatoire serait a la fois inutile et moins fiable que d'utiliser la bibliotheque standard. Savoir quel module contient quelle fonction, et connaitre leurs pieges, fait partie du bagage de base.

Definition — Module

Un module est un fichier Python contenant des definitions reutilisables. Un paquet est un ensemble de modules. La bibliotheque standard est l'ensemble des modules livres avec Python.

Les quatre formes d'importation

import math                       # 1. le module entier
math.sqrt(2)                      #    acces qualifie : math.sqrt

from math import sqrt, pi         # 2. quelques noms
sqrt(2)                           #    acces direct

import numpy as np                # 3. avec un alias
np.array([1, 2])

from math import *                # 4. TOUT : A EVITER
sqrt(2)
Pourquoi eviter from ... import *
from math import *
from numpy import *
sqrt([1, 4, 9])      # laquelle des deux sqrt ? La derniere importee ecrase la premiere.

Cette forme pollue l'espace de noms, masque silencieusement des fonctions et rend le code illisible : on ne sait plus d'ou vient un nom. Les formes 1 a 3 sont les seules acceptables.

Ou placer les imports

Toujours en tete de fichier, jamais dans une fonction (sauf cas particuliers). Un import est peu couteux apres le premier appel — Python conserve les modules charges dans sys.modules — mais le placer en tete documente les dependances.

Le module math

CategorieFonctions
Constantespi, e, tau, inf, nan
Puissancessqrt, pow, exp, log, log2, log10
Trigonometriesin, cos, tan, asin, acos, atan, atan2
Hyperboliquessinh, cosh, tanh
Arrondisfloor, ceil, trunc, fabs
Arithmetiquefactorial, gcd, lcm, comb, perm, isqrt
Anglesdegrees, radians, hypot
Testsisclose, isnan, isinf, isfinite
from math import *

sqrt(16)              # 4.0     (toujours un flottant)
16 ** 0.5             # 4.0     (equivalent)
isqrt(17)             # 4       racine ENTIERE, exacte meme sur de grands entiers
log(100, 10)          # 2.0     logarithme en base 10
log(e)                # 1.0     logarithme NEPERIEN par defaut
factorial(20)         # 2432902008176640000  (exact, entier)
gcd(12, 18)           # 6
comb(10, 3)           # 120     coefficient binomial exact
hypot(3, 4)           # 5.0     sqrt(x^2 + y^2), sans debordement
Les angles sont en RADIANS
sin(30)               # -0.988...  : 30 RADIANS, pas 30 degres !
sin(radians(30))      # 0.4999999999999999  ~ 1/2
degrees(pi)           # 180.0

Et noter que \(\sin(\pi/6)\) ne donne pas exactement 0,5 : les flottants approchent \(\pi\). Ne jamais tester sin(radians(30)) == 0.5, mais isclose(sin(radians(30)), 0.5).

Domaines de definition
sqrt(-1)              # ValueError: math domain error
log(0)                # ValueError: math domain error
1 / 0                 # ZeroDivisionError
1.0 / 0.0             # ZeroDivisionError  (Python, contrairement a IEEE 754 pur)
float("inf")          # inf : la valeur infinie existe et se manipule
inf - inf             # nan : "not a number"
nan == nan            # False !  nan n'est egal a rien, pas meme a lui-meme
isnan(nan)            # True : la SEULE facon de tester

Le module cmath fournit les memes fonctions sur les complexes : cmath.sqrt(-1) vaut 1j.

Precision et fonctions robustes

from math import expm1, log1p, fsum, isclose

exp(1e-10) - 1        # 1.000000082740371e-10   : perte de precision
expm1(1e-10)          # 1.00000000005e-10       : exact

log(1 + 1e-15)        # 1.1102230246251565e-15  : imprecis
log1p(1e-15)          # 9.999999999999995e-16   : exact

sum([0.1] * 10)       # 0.9999999999999999
fsum([0.1] * 10)      # 1.0  : sommation compensee exacte

isclose(0.1 + 0.2, 0.3)                    # True
isclose(1e10, 1e10 + 1, rel_tol=1e-9)      # True
0.1 + 0.2 == 0.3                           # False

expm1(x) calcule \(e^x - 1\) et log1p(x) calcule \(\ln(1+x)\) sans la cancellation catastrophique qui survient quand \(x\) est petit. Ces fonctions existent precisement pour cela.

2Aleatoire, simulation et methode de Monte-Carlo

Le module random

Generateur pseudo-aleatoire

Un ordinateur deterministe ne produit pas de hasard : il calcule une suite pseudo-aleatoire, entierement determinee par un etat initial appele graine (seed). Python utilise le Mersenne Twister, de periode \(2^{19937}-1\), dont les proprietes statistiques sont excellentes.

import random

random.random()                  # flottant uniforme dans [0, 1[
random.uniform(2, 5)             # flottant uniforme dans [2, 5]
random.randint(1, 6)             # entier dans [1, 6]  : les DEUX bornes incluses
random.randrange(6)              # entier dans [0, 5]  : borne haute EXCLUE
random.choice([1, 2, 3])         # un element au hasard
random.choices([1,2,3], k=5)     # 5 tirages AVEC remise
random.sample([1,2,3,4], 2)      # 2 tirages SANS remise
random.shuffle(t)                # melange t EN PLACE, renvoie None
random.gauss(0, 1)               # loi normale N(0, 1)
random.expovariate(2)            # loi exponentielle de parametre 2
Deux pieges d'intervalle

randint(a, b) inclut \(b\), randrange(a, b) l'exclut. C'est une incoherence historique de l'API : randint suit la convention mathematique, randrange celle de range. Verifier a chaque usage.

shuffle modifie en place et renvoie None : ecrire t = random.shuffle(t) detruit la liste.

La graine : reproductibilite
random.seed(42)
print([random.randint(1, 100) for _ in range(5)])   # [82, 15, 4, 95, 36]
random.seed(42)
print([random.randint(1, 100) for _ in range(5)])   # [82, 15, 4, 95, 36] : IDENTIQUE

Fixer la graine rend une simulation reproductible : indispensable pour deboguer, comparer deux algorithmes sur les memes donnees, ou publier un resultat verifiable. Sans seed, Python initialise a partir de l'horloge et de l'entropie systeme.

Ne jamais utiliser random pour la securite

Le Mersenne Twister n'est pas cryptographiquement sur : observer 624 sorties consecutives suffit a reconstituer l'etat interne et a predire toute la suite. Pour des mots de passe, des jetons ou des cles, utiliser le module secrets.

Simulation et methode de Monte-Carlo

Principe

Estimer une quantite deterministe (une aire, une probabilite, une integrale) en simulant un grand nombre d'experiences aleatoires et en utilisant la loi des grands nombres. L'erreur decroit en \(O(1/\sqrt N)\) : gagner une decimale demande 100 fois plus de tirages.

import random
from math import sqrt, pi

def estime_pi(n):
    """Estime pi par tirages uniformes dans le carre [0,1]^2."""
    dedans = 0
    for _ in range(n):
        x, y = random.random(), random.random()
        if x*x + y*y <= 1:
            dedans += 1
    return 4 * dedans / n

random.seed(0)
for n in (10**3, 10**4, 10**5, 10**6):
    e = estime_pi(n)
    print(f"n = {n:>8} : pi ~ {e:.5f}, erreur {abs(e - pi):.5f}, "
          f"1/sqrt(n) = {1/sqrt(n):.5f}")
n =     1000 : pi ~ 3.16800, erreur 0.02641, 1/sqrt(n) = 0.03162
n =    10000 : pi ~ 3.14840, erreur 0.00681, 1/sqrt(n) = 0.01000
n =   100000 : pi ~ 3.14172, erreur 0.00013, 1/sqrt(n) = 0.00316
n =  1000000 : pi ~ 3.14117, erreur 0.00042, 1/sqrt(n) = 0.00100

Justification. Le quart de disque a pour aire \(\pi/4\) et le carre unite pour aire 1. La probabilite qu'un point uniforme tombe dans le quart de disque vaut donc \(\pi/4\). Si \(X_i\) vaut 1 quand le \(i\)-ieme point est dedans, la loi des grands nombres donne \(\frac1n\sum X_i \to \pi/4\) presque surement. Le theoreme central limite precise l'erreur : \(\sqrt n\bigl(\frac{4}{n}\sum X_i - \pi\bigr)\) converge en loi vers une gaussienne, d'ou une erreur typique en \(\Theta(1/\sqrt n)\).

Monte-Carlo : estimation de \(\pi\)
0 point
Ajouter des points pour voir l'estimation converger.

3Autres modules et ecriture d'un module

Autres modules utiles

ModuleUsageExemple
statisticsstatistiques descriptives mean, median, stdev, mode
fractionsrationnels exacts Fraction(1,3) + Fraction(1,6)
decimaldecimaux a precision arbitraire calculs financiers
itertoolscombinatoire permutations, combinations, product
functoolsoutils fonctionnels reduce, lru_cache
collectionsstructures Counter, deque, defaultdict
timemesure de tempsperf_counter
cmathfonctions complexescmath.sqrt(-1)
import statistics as st
notes = [12, 15, 9, 18, 11, 15]
st.mean(notes)         # 13.333333333333334
st.median(notes)       # 13.5
st.mode(notes)         # 15
st.stdev(notes)        # 3.2659863237109041   (ecart-type d'ECHANTILLON, n-1)
st.pstdev(notes)       # 2.9814426348136544   (ecart-type de POPULATION, n)
Echantillon ou population ?

stdev divise par \(n-1\) (estimateur sans biais de la variance d'une population a partir d'un echantillon), pstdev divise par \(n\) (ecart-type de la population complete). Choisir selon que les donnees sont un echantillon ou la population entiere.

from fractions import Fraction

Fraction(1, 3) + Fraction(1, 6)      # Fraction(1, 2)  : EXACT
1/3 + 1/6                            # 0.49999999999999994  : approche
Fraction(0.1)                        # Fraction(3602879701896397, 36028797018963968)
Fraction("0.1")                      # Fraction(1, 10)  : depuis une CHAINE, exact
sum(Fraction(1, k) for k in range(1, 11))   # Fraction(7381, 2520) : H_10 exact
from itertools import permutations, combinations, product, accumulate

list(permutations([1,2,3]))           # les 6 permutations
list(combinations([1,2,3,4], 2))      # les 6 paires
list(product([0,1], repeat=3))        # les 8 triplets binaires
list(accumulate([1,2,3,4]))           # [1, 3, 6, 10] : sommes prefixes

Ecrire son propre module

# fichier geometrie.py
"""Fonctions de geometrie plane."""
from math import pi, hypot

def aire_disque(r):
    """Aire d'un disque de rayon r >= 0."""
    return pi * r * r

def distance(a, b):
    """Distance euclidienne entre deux points (x, y)."""
    return hypot(a[0] - b[0], a[1] - b[1])

if __name__ == "__main__":
    # ce bloc n'est execute QUE si le fichier est lance directement,
    # pas s'il est importe. Ideal pour les tests.
    assert abs(aire_disque(1) - pi) < 1e-12
    assert distance((0, 0), (3, 4)) == 5.0
    print("tests de geometrie : ok")
# dans un autre fichier
import geometrie
geometrie.aire_disque(2)

from geometrie import distance
distance((0,0), (1,1))
Le bloc if __name__ == "__main__":

Quand un fichier est execute directement, la variable __name__ vaut "__main__" ; quand il est importe, elle vaut le nom du module. Ce test permet de placer les tests et le programme principal dans le meme fichier que les fonctions, sans qu'ils s'executent a l'import. C'est une convention universelle en Python.

L'essentiel du chapitre

  • Importer avec import m, from m import f ou import m as alias ; jamais from m import *.
  • Les fonctions trigonometriques travaillent en radians.
  • math.isclose pour comparer des flottants, math.isnan pour tester nan.
  • randint(a,b) inclut \(b\), randrange(a,b) l'exclut ; shuffle modifie en place.
  • random.seed(k) rend une simulation reproductible.
  • Monte-Carlo : erreur en \(O(1/\sqrt N)\), donc convergence lente mais insensible a la dimension.
  • if __name__ == "__main__": pour separer bibliotheque et programme principal.
Fiche d'exercices — chapitre 16Enonces gradues, du calcul d'application au niveau concours, avec corriges detailles.
Ouvrir la fiche →