Cet article s’appuie sur un projet d’épidémiologie publié sur GitHub, et qui est directement lié à la crise du Covid-19 ayant débutée en 2020. On découvrira dans cet article comment certains paramètres, propres à une maladie et à une population, influent sur la propagation et la dangerosité de l’épidémie, tels que la distanciation des personnes, le taux d’infection, etc. Pour se faire, nous allons créer un modèle statistique à l’aide de python, pour étudier tous les scénarios possibles, et les analyser. On tentera ensuite de valider ce modèle par plusieurs modèles mathématiques utilisés en épidémiologie, à savoir les modèles SIR et SIDR. L’étude de ces deux modèles est développée dans le ReadMe du repo GitHub. Quant à la comparaison de ces modèles avec le modèle statistique, elle sera détaillée sur cette article, et sur le GitHub.
Ce projet a tout d’abord été implémenté avec le module Matplotlib, puis a été retranscrit avec la librairie Plotly qui offre une personnalisation et interactivité plus intéressante. C’est donc avec Plotly, que cet article détaillera l’implémentation de ce modèle épidémiologique et étudiera ses paramètres.
Création du modèle statistique
Cadre de l’étude et du modèle python
Pour ce projet, on prendra une population dont l’effectif variera entre 1 et 20000 individus. On supposera que la natalité compense la mortalité liée à une cause autre que l’épidémie, et de ce fait la population restera constante. Le principe de base de cette simulation repose sur l’aspect spatial de la propagation, en effet, lors de la transmission de la maladie, chaque individu infecté pourra infecter un individu sain si et seulement si ce dernier est dans son rayon d’infection et que ses chances d’infection sont suffisantes. Ensuite, dans les individus infectés, certains vont devenir immunisés et d’autres vont décéder, avec des probabilités associées aux deux cas. Finalement, on tracera l’évolution du nombre de personnes dans les 4 états différents au cours des jours, on affichera la population répartie dans l’espace ainsi que la répartition des individus dans chaque catégorie avec un diagramme circulaire. Au niveau des structures de données, on utilisera un dictionnaire de listes, qui offre une performance optimale pour ce projet en particulier.
Création de la population
Afin de créer notre population, nous utiliserons la fonction make_blobs() de la librairie sklearn.datasets, avec le paramètre « n_samples » qui correspond au nombre d’individus, le paramètre « centers » qui correspond au nombre de cluster dans la population et un paramètre « cluster_std » qui nous aidera à faire varier la distance moyenne entre les individus.
Par exemple, après importation des fonctions de Plotly :
[code lang=”python”]
x, y = make_blobs(n_samples=10000, centers=1, cluster_std=2)
fig = go.Figure()
fig.add_trace(
go.Scattergl(
x = x[:, 0],
y= x[:, 1],
mode = ‘markers’,
marker = dict(
line = dict(
width = 1,
color = ‘#636EFA’)
)
,marker_line=dict(width=1)
)
)
plot(fig)
[/code]
Ce qui nous donne la population suivante :
Ainsi, en utilisant la fonction make_blobs, nous pouvons récupérer les coordonnées de tous les individus, et les stocker dans un dataframe à 2 colonnes pour avoir les positions x et y de chacun. Pour les récupérer on procède comme suit :
[code lang=”python”]
df = pd.DataFrame(dict(x=x[:,0],y=x[:,1]))
[/code]
Fonctions annexes
Avant de commencer l’implémentation du modèle en tant que tel, nous aurons besoin de quelques fonctions supplémentaires.
1. Une fonction permettant de calculer la distance euclidienne entre deux points x et y, en l’occurrence entre un individu infecté et un individu sain. Afin d’optimiser le plus possible l’algorithme, nous utiliserons la fonction euclidean de la librairie fastdist :
[code lang=”python”]
def distance_e(x, y):
"""
Distance entre 2 points
"""
return fastdist.euclidean(np.array(x), np.array(y))
[/code]
2. Une fonction qui va simplement supprimer les éléments d’une liste « a » qui appartiennent à une liste « L ». Cela va nous aider à gérer les individus lorsqu’ils changeront d’état.
[code lang=”python”]
def remove_(a, l):
"""
Enlever les éléments de l dans a
"""
for i in range(len(l)):
a.remove(l[i])
return a
[/code]
3. Une fonction qui va retourner True ou False pour déterminer si in individu change d’état avec une probabilité notée p. Le principe est de tirer un nombre au hasard entre 0 et 100 et si ce nombre est supérieur à la probabilité p alors la fonction retourne True et l’individu change d’état.
[code lang=”python”]
def chance_infecte(p):
"""
Return True s'il devient infecté avec une proba p
"""
return rd.randint(0, 100) < int(p * 100)
[/code]
4. Une fonction qui va, à partir de la liste des individus infectés, de la liste des individus immunisés au jour précédent et du taux d’infection p, retourner la liste des individus infectés après l’immunisation et la liste des immunisés après l’ajout des nouveaux individus. La variable interne drop sert à gérer le décalage d’indice lorsque nous supprimons une valeur dans les listes.
[code lang=”python”]
def immuniser(l, l2, p):
"""
l: infectés;
l2: immunisés précédents
"""
drop = 0
for i in range(len(l)):
if rd.randint(0, 100) < int(p * 100):
l2.append(l[i – drop])
l.remove(l[i – drop])
drop += 1
return l, l2
[/code]
5. Une fonction qui va, à partir de la liste des individus infectés, des individus décédés précédemment, des individus immunisés et du taux d’infection p, retourner la liste des infectés mise à jour, et la liste des décès après l’ajout des personnes qui viennent de décéder.
[code lang=”python”]
def deces(l, l2, l3, p):
"""
l: infectés;
l2: décès précédents;
l3: immunisés
"""
l_p = l[:] # création d'une copie pour éviter d'erreur d'indice
for i in range(len(l_p)):
if rd.randint(0, 100) < int(p * 100) and not any(list == l_p[i] for list in l3):
l2.append(l_p[i])
l.remove(l_p[i])
return l, l2
[/code]
Première contamination et initialisation des données
Nous voici maintenant armé pour lancer notre épidémie. Mais pour commencer, nous allons infecter notre premier individu, qui est notre patient 0. Pour cela, nous choisissons au hasard un numéro compris entre 0 et la taille de notre population (auquel on soustrait 1 pour gérer les indices) et nous extrayions les coordonnées comme ci-dessous :
[code lang=”python”]
numero_infecte_1 = rd.randint(0, nb_individu – 1)
coord_1er_infecte = [df['x'][numero_infecte_1], df['y'][numero_infecte_1]]
[/code]
On crée maintenant nos structures de données. On peut initialiser ces dernières comme ceci à l’aide d’un dictionnaire de listes, les listes courbe_xxxx correspondent aux nombres jour par jour des individus par catégories, et les listes coord_xxxx contiendront à la fin les coordonnées des individus dans chaque état :
[code lang=”python”]
data = dict(courbe_sains = [],
courbe_infectes = [],
courbe_immunises = [],
courbe_deces = [],
courbe_removed = [],
coord_infectes=[],
coord_sains=[],
coord_immunises=[],
coord_deces=[])
[/code]
Puis on affecte nos données, aux listes. L’individu infecté est copié dans la liste des coord_infectes et les autres, qui sont sains, sont copiés dans la liste coord_sains :
[code lang=”python”]
for k in range(nb_individu):
if k==numero_infecte_1 :
data['coord_infectes'].append(coord_1er_infecte)
else:
data['coord_sains'].append([df['x'][k], df['y'][k]])
data['courbe_sains'].append(nb_individu-1)
data['courbe_infectes'].append(1)
data['courbe_immunises'].append(0)
data['courbe_deces'].append(0)
data['courbe_removed'].append(0)
[/code]
On a donc notre premier individu infecté :
Lancement de l’épidémie
À partir d’ici, nous avons nos individus rangés dans les listes qui correspondent à leurs états. On peut maintenant lancer notre épidémie. On définit premièrement une condition d’arrêt de notre modèle : lorsque le nombre d’infectés est inférieur à 8% de la population totale et que le nombre de jour est supérieur à 10. Ensuite, à chaque début de vague de contaminations, nos individus infectés passent dans les fonctions immuniser() et deces() et on sélectionne ainsi ceux qui deviennent immunisés et ceux qui décèdent, les probabilités sont « p » pour le taux d’immunité et « d » pour la létalité. Puis, pour chaque individu infecté, on infecte ou non les individus sains à proximité s’ils ne le sont pas déjà par un autre infecté. Et on déplace les coordonnées des personnes nouvellement infectés dans la liste coord_infectes. Enfin, on ajoute dans les listes courbe_xxxx le nombre de personnes dans chaque catégorie à la fin de la journée de contaminations.
[code lang=”python”]
while len(data['coord_infectes']) > 0.08 * nb_individu or len(data['courbe_sains']) < 10: # condition d'arrêt
for k in range(len(data['coord_infectes'])):
non_sains = 0
for j in range(len(data['coord_sains'])):
if distance_e(data['coord_infectes'][k], data['coord_sains'][j – non_sains]) < rayon_contamination and not any(list == data['coord_sains'][j – non_sains] for list in data['coord_infectes']) and chance_infecte(infectiosite):
buf = data['coord_sains'][j – non_sains]
data['coord_infectes'].append(buf)
data['coord_sains'].remove(buf)
non_sains += 1
coord_infectes1, data['coord_immunises'] = immuniser(data['coord_infectes'], data['coord_immunises'], p)
data['coord_infectes'], data['coord_deces'] = deces(coord_infectes1, data['coord_deces'], data['coord_immunises'], d)
# pour les courbes finales
data['courbe_sains'].append(len(data['coord_sains']))
data['courbe_infectes'].append(len(data['coord_infectes']))
data['courbe_immunises'].append(len(data['coord_immunises']))
data['courbe_deces'].append(len(data['coord_deces']))
data['courbe_removed'].append(len(data['coord_immunises']) + len(data['coord_deces']))
[/code]
Affichage des résultats
Une fois nos données complètent, on peut tracer tout ça. Dans la fonction disponible sur GitHub, qui s’appelle vague_seuil_px_opti2(), on va utiliser un subplot pour afficher la population, le diagramme circulaire des états des individus, et les courbes représentatives de l’épidémie. Par exemple, pour afficher les individus sains dans la population :
[code lang=”python”]
if data['coord_sains']:
fig.add_trace(
go.Scatter(
x=np.array(data['coord_sains'])[:, 0],
y=np.array(data['coord_sains'])[:, 1],
name="sain",
mode="markers",
marker=dict(
color='#636EFA',
size=5,
line=dict(
width=0.4,
color='#636EFA'
)
),
marker_line=dict(width=1),
showlegend=False), 1, 1)
[/code]
Pour afficher le diagramme circulaire :
[code lang=”python”]
labels = ["sains", "infectés", "immunisés", "décédés"]
fig.add_trace(go.Pie(
values=[len(data['coord_sains']), len(data['coord_infectes']), len(data['coord_immunises']), len(data['coord_deces'])],
labels=labels,
sort=False), 1, 2)
[/code]
Pour tracer la courbe d’évolution des individus sains :
[code lang=”python”]
x_courbe = list(np.arange(0, len(data['courbe_sains'])))
fig.add_trace(
go.Scatter(
x=x_courbe,
y=data['courbe_sains'],
marker=dict(color='#636EFA'),
marker_line=dict(width=2),
showlegend=False,
name="sains",
yaxis="y"), 2, 1)
[/code]
Finalement, on obtient une figure qui ressemble par exemple à celle-ci :
Étude du modèle python
Pour étudier le modèle python que nous venons de créer nous allons faire varier les paramètres du virus et de la population. Tout d’abord, définissons le taux de reproduction de base Ro de la maladie, qui est définit comme le quotient du taux d’infection β, sur le taux de retirement λ, où le taux de retirement correspond à la probabilité qu’a un individu de passer d’un état infecté à un état immunisé ou décédé. Ce taux est très important dans la gestion d’une épidémie, il représente le nombre moyen de nouveaux cas d’infection engendrés par un individu infecté, et permet de se rendre compte de la vitesse de propagation du virus. Pour modéliser ce paramètre dans notre modèle, nous prendrons un taux d’immunité p=0 et une létalité d=λ. En prenant différentes valeurs de λ pour obtenir plusieurs valeurs de Ro , on obtient :
On remarque ainsi que pour des valeurs de Ro < 1 on observe une disparition rapide des individus infectés, voire instantanée, ici entre 0 et environ une dizaine de jours, et que pour un Ro > 1 l’épidémie se propage à une grande partie de la population, et sur une période plus longue, ici une trentaine de jours. Ce Ro apparait donc comme un seuil, pour les apparitions d’épidémie. En effet, Ro=1 est la frontière entre un système stable (Ro < 1) et instable (Ro > 1) dans le cas où le nombre d’infectés au départ dans la population est très faible par rapport à l’effectif total (Pour aller plus loin dans l’étude de ce seuil et cet équilibre : pdf).
Faisons maintenant varier le paramètre « cluster_std » pour voir l’influence de l’espacement des individus sur la propagation de l’épidémie.
Plus la variance est grande, plus les individus sont espacés, et plus on remarque que le pic d’infectés est petit. En effet, les individus étant plus rapprochés avec une petite variance, chaque individu infecté peut ainsi potentiellement infecter plus de personne. De plus, même si l’épidémie touche moins de personnes avec une disparité élevée, elle dure plus longtemps. Ainsi, l’espacement des individus est un élément clé dans la lutte contre une épidémie.
Comparaison des modèles mathématiques avec le modèle python
En épidémiologie, les modèles de modélisation d’épidémies sont des modèles dit à compartiments. Le principe est le même pour tous, on découpe la population en plusieurs catégories.
Ici on s’intéressera aux modèles SIR et SIDR qui correspondent parfaitement au modèle python précédemment crée. Dans cet article, on détaillera brièvement l’approche mathématique de ces modèles, pour se focaliser sur la comparaison avec le modèle python. Dans le cas où vous voudriez en savoir plus sur la mise en place de ses modèles et sur l’implémentation de ces derniers, je vous invite à vous rendre sur la page GitHub référencée au début.
Le modèle SIR
Ce modèle divise la population en 3 compartiments, notés S, I et R qui correspondent respectivement au nombre d’individus sains, infectés et remis de la population. On considérera que la population reste constante tout au long de l’épidémie.
Les paramètres qui entrent en jeu ici, sont :
- \beta -> le taux d’infection
- \lambda -> le taux de retirement
On peut ainsi représenter le modèle comme ceci :
Cependant, notre modèle python est un peu différent, car nous distinguons les individus remis en 2 catégories, les individus immunisés et décédés. Pour pallier cette petite différence on fixe nos paramètres de notre modèle python à un taux d’immunité p=0 et de décès d=λ, pour qu’ainsi notre liste d’individus décédés correspondent ici aux individus remis.
Comparons maintenant les modèles avec des valeurs de Ro différentes :
On observe que globalement la tendance des courbes est similaire pour des Ro égaux. Avec un Ro très proche de 0, l’épidémie est inexistante. Avec un Ro égale à 1, nous sommes à l’état d’équilibre, c’est-à-dire que le nombre d’individus remis ne dépassera jamais le nombre d’individus sains. Et avec un Ro très grand, le pic d’infectés est atteint très rapidement, dans les 15 premiers jours après la première infection, et atteint environ 75% de la population. Globalement, le modèle python est assez conforme avec le modèle SIR.
Le modèle SIDR
Ce modèle, quant à lui, décompose la population en 4 compartiments, comme notre modèle python. Ces 4 compartiments sont notés S, I, D et R qui correspondent respectivement au nombre d’individus sains, infectés, décédés et rétablis dans la population. On considèrera ici aussi que le nombre d’individus reste constant dans la population durant l’épidémie.
Les paramètres qui régissent ce modèle sont :
- \beta -> le taux d’infection
- \theta -> le taux de mortalité
- \mu -> le taux de guérison
On représente ainsi le modèle comme ceci :
On prendra donc dans le modèle python, les paramètres infectiosite=b, p=m et d=q. Comparons maintenant les modèles SIDR et python pour des paramètres différents :
On remarque que les courbes sont similaires pour des paramètres égaux, excepté l’allure de la courbe des individus infectés, qui est légèrement « volatile » et qui peut s’expliquer par le fait que le modèle python crée et le modèle SIDR ont des représentations temporelles différentes. Mais globalement, notre modèle python reste assez conforme au modèle.
Conclusion
Finalement, la modélisation d’un phénomène tel que la propagation d’une épidémie, reste compliqué, dans la mesure où elle dépend aussi énormément du mouvement de la population. Malgré ça, notre modèle python que nous venons de créer à une certaine validité car il est semblable aux modèles mathématiques dont il partage les caractéristiques, à savoir les modèles SIR et SIDR.
À partir de là, l’étude des paramètres liés à la population et à l’épidémie nous a montré que les interactions entre les individus jouaient un rôle majeur dans sa propagation, et que dès lors que notre indice Ro dépassait le seuil de 1, l’épidémie allait se propager dans quasiment toute la population. C’est donc cette valeur de Ro qui joue un rôle crucial dans la gestion d’une crise sanitaire, car à partir des paramètres de l’épidémie, qui sont calculés statistiquement, on peut prévoir si oui ou non il faut appliquer des mesures fortes pour contrer l’épidémie, tel qu’un confinement strict par exemple.