Représentation du R0 par départements

Cet exemple reprend des données de tests par gouvernement pour calculer le coefficient R0 de l’épidémie. La méthode de Cori est utilisée avec la formule proposée dans covidtracker :

\[R = \frac{\sum_{i=t-6}^{t}C_i}{\sum_{i=t-13}^{t-7}C_i}\]

\(C_i\) est le nombre de cas positifs du jour i. Cette méthode est implémentée dans le package R EpiEstim ou epyestim pour le langage python. Dans cet exemple, il sera calculé directement à partir des données.

Sources de données:

Récupération des données

import warnings
from pandas import Timedelta, DataFrame
from geopandas import GeoDataFrame
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import make_axes_locatable
from aftercovid.data import (
    data_covid_france_departments_tests,
    data_france_departments)


def valid(df):
    "Checks that a dataframe is not empty."
    if 0 in df.shape:
        raise ValueError(
            f"One dataframe is empty with shape={df.shape!r}.")
    return df


case = data_covid_france_departments_tests(metropole=True)
case.tail()
somewhereaftercovid_39_std/aftercovid/aftercovid/data/pandas_cache.py:24: DtypeWarning: Columns (0) have mixed types. Specify dtype option on import or set low_memory=False.
  df = pandas.read_csv(url, **kwargs)  # pragma: no cover
dep jour cage10 cl_age90 pop P T Ti Tp Td
1178040 95 2023-06-23 [90-Inf) 90 10176.0 0.0 2.0 0.00 0.0 19.65
1178041 95 2023-06-24 [90-Inf) 90 10176.0 0.0 5.0 0.00 0.0 49.14
1178042 95 2023-06-25 [90-Inf) 90 10176.0 0.0 4.0 0.00 0.0 39.31
1178043 95 2023-06-26 [90-Inf) 90 10176.0 0.0 8.0 0.00 0.0 78.62
1178044 95 2023-06-27 [90-Inf) 90 10176.0 1.0 5.0 9.83 20.0 49.14


Aggrégation par départements et par jour.

case = valid(
    case[case.cl_age90 != 0].groupby(['dep', 'jour'], as_index=False).sum()
)
case.tail()
dep jour cage10 cl_age90 pop P T Ti Tp Td
109531 95 2023-06-23 [00-10)[10-20)[20-30)[30-40)[40-50)[50-60)[60-... 531 1276534.0 15.0 246.0 12.28 50.65 241.91
109532 95 2023-06-24 [00-10)[10-20)[20-30)[30-40)[40-50)[50-60)[60-... 531 1276534.0 10.0 218.0 8.16 33.51 219.56
109533 95 2023-06-25 [00-10)[10-20)[20-30)[30-40)[40-50)[50-60)[60-... 531 1276534.0 2.0 68.0 1.28 28.57 95.36
109534 95 2023-06-26 [00-10)[10-20)[20-30)[30-40)[40-50)[50-60)[60-... 531 1276534.0 20.0 346.0 18.06 51.18 348.35
109535 95 2023-06-27 [00-10)[10-20)[20-30)[30-40)[40-50)[50-60)[60-... 531 1276534.0 16.0 264.0 20.01 70.38 267.95


Quelques aggrégations, par département.

deps = valid(case.groupby(["dep", "jour"], as_index=False).sum())
deps.tail(n=10)
dep jour cage10 cl_age90 pop P T Ti Tp Td
109526 95 2023-06-18 [00-10)[10-20)[20-30)[30-40)[40-50)[50-60)[60-... 531 1276534.0 4.0 92.0 2.55 31.94 126.94
109527 95 2023-06-19 [00-10)[10-20)[20-30)[30-40)[40-50)[50-60)[60-... 531 1276534.0 22.0 407.0 29.09 67.27 387.99
109528 95 2023-06-20 [00-10)[10-20)[20-30)[30-40)[40-50)[50-60)[60-... 531 1276534.0 13.0 339.0 9.32 32.87 325.92
109529 95 2023-06-21 [00-10)[10-20)[20-30)[30-40)[40-50)[50-60)[60-... 531 1276534.0 14.0 302.0 14.96 41.79 323.51
109530 95 2023-06-22 [00-10)[10-20)[20-30)[30-40)[40-50)[50-60)[60-... 531 1276534.0 20.0 251.0 15.73 67.11 243.76
109531 95 2023-06-23 [00-10)[10-20)[20-30)[30-40)[40-50)[50-60)[60-... 531 1276534.0 15.0 246.0 12.28 50.65 241.91
109532 95 2023-06-24 [00-10)[10-20)[20-30)[30-40)[40-50)[50-60)[60-... 531 1276534.0 10.0 218.0 8.16 33.51 219.56
109533 95 2023-06-25 [00-10)[10-20)[20-30)[30-40)[40-50)[50-60)[60-... 531 1276534.0 2.0 68.0 1.28 28.57 95.36
109534 95 2023-06-26 [00-10)[10-20)[20-30)[30-40)[40-50)[50-60)[60-... 531 1276534.0 20.0 346.0 18.06 51.18 348.35
109535 95 2023-06-27 [00-10)[10-20)[20-30)[30-40)[40-50)[50-60)[60-... 531 1276534.0 16.0 264.0 20.01 70.38 267.95


Sur tout le territoire.

france = case.groupby(["jour"], as_index=False).sum()
france.tail(n=10)
jour dep cage10 cl_age90 pop P T Ti Tp Td
1131 2023-06-18 0102030405060708091011121314151617181921222324... [00-10)[10-20)[20-30)[30-40)[40-50)[50-60)[60-... 50976 65627454.0 230.0 4134.0 396.55 3839.41 8312.51
1132 2023-06-19 0102030405060708091011121314151617181921222324... [00-10)[10-20)[20-30)[30-40)[40-50)[50-60)[60-... 50938 65613544.0 1796.0 21846.0 2604.81 7639.19 35226.84
1133 2023-06-20 0102030405060708091011121314151617181921222324... [00-10)[10-20)[20-30)[30-40)[40-50)[50-60)[60-... 50938 65613544.0 1378.0 17154.0 2365.26 7736.85 28971.69
1134 2023-06-21 0102030405060708091011121314151617181921222324... [00-10)[10-20)[20-30)[30-40)[40-50)[50-60)[60-... 50948 65609313.0 1160.0 14704.0 1877.49 7682.57 24634.95
1135 2023-06-22 0102030405060708091011121314151617181921222324... [00-10)[10-20)[20-30)[30-40)[40-50)[50-60)[60-... 50967 65620566.0 1001.0 11556.0 1631.53 7796.87 19556.14
1136 2023-06-23 0102030405060708091011121314151617181921222324... [00-10)[10-20)[20-30)[30-40)[40-50)[50-60)[60-... 50967 65620566.0 1011.0 11962.0 1627.93 7392.79 19878.85
1137 2023-06-24 0102030405060708091011121314151617181921222324... [00-10)[10-20)[20-30)[30-40)[40-50)[50-60)[60-... 50967 65620566.0 558.0 11145.0 964.71 4932.64 18726.24
1138 2023-06-25 0102030405060708091011121314151617181921222324... [00-10)[10-20)[20-30)[30-40)[40-50)[50-60)[60-... 50976 65627454.0 184.0 3495.0 319.92 3441.66 7484.84
1139 2023-06-26 0102030405060708091011121314151617181921222324... [00-10)[10-20)[20-30)[30-40)[40-50)[50-60)[60-... 50887 65622747.0 1434.0 18864.0 2295.85 7380.86 30830.98
1140 2023-06-27 0102030405060708091011121314151617181921222324... [00-10)[10-20)[20-30)[30-40)[40-50)[50-60)[60-... 50887 65622747.0 972.0 13827.0 1606.70 6494.00 23537.51


Calcul du R

def compute_r(df, col_day='jour', col='P', last_day=None):
    if last_day is None:
        last_day = df.jour.max()
    v1 = last_day - Timedelta(days=6)
    v2 = last_day
    p1 = last_day - Timedelta(days=13)
    p2 = last_day - Timedelta(days=7)
    w1 = df[(df[col_day] >= p1) & (df[col_day] <= p2)]
    w2 = df[(df[col_day] >= v1) & (df[col_day] <= v2)]
    return w2[col].sum() / w1[col].sum()


compute_r(france)
0.7627323195751871

On regarde quelle tête ça a sur six mois. Ce n’est pas le code le plus efficace mais c’est rapide à écrire. Dans le cas idéal, il faudra s’assurer que toutes les dates sont présents et les compléter le cas échéants puis calculer l’estimateur sur des fenêtres glissantes.

last_day = france.jour.max()
obs = []
for i in range(0, 180):
    ld = last_day - Timedelta(days=i)
    obs.append({'jour': ld, 'R': compute_r(france, last_day=ld)})

gr = DataFrame(obs).sort_values("jour")
gr.tail()
jour R
4 2023-06-23 0.791458
3 2023-06-24 0.778821
2 2023-06-25 0.778816
1 2023-06-26 0.783825
0 2023-06-27 0.762732


Et on dessine.

gr.set_index('jour').plot(title="Evolution de R en Métropole")
Evolution de R en Métropole

Carte du R par département

On calcule les R par départements.

obs = []
for d in set(deps.dep):
    r = compute_r(deps[deps.dep == d])
    obs.append({'dep': d, 'R': r})

depdf = DataFrame(obs).sort_values("dep")
depdf.tail()
dep R
46 91 0.829268
51 92 0.825893
49 93 0.741722
95 94 0.742105
25 95 0.906542


On récupère les contours des départements français.

loc = data_france_departments(metropole=True)
loc.tail()
ancienne_re code_ancien code_depart code_region departement region geometry
96 Limousin 74.0 87 75.0 Haute-Vienne Nouvelle-Aquitaine POLYGON ((0.92278 45.94610, 0.92856 45.94808, ...
97 Auvergne 83.0 63 84.0 Puy-de-Dôme Auvergne-Rhône-Alpes POLYGON ((2.45492 45.76131, 2.45169 45.76137, ...
98 Basse-Normandie 25.0 14 28.0 Calvados Normandie MULTIPOLYGON (((-0.42990 48.86405, -0.42881 48...
99 Rhône-Alpes 82.0 07 84.0 Ardèche Auvergne-Rhône-Alpes POLYGON ((4.30746 44.98596, 4.30412 44.98837, ...
100 Midi-Pyrénées 73.0 32 76.0 Gers Occitanie POLYGON ((-0.24307 43.66404, -0.24328 43.66476...


On fusionne avec les R.

locdep = GeoDataFrame(depdf.merge(loc, left_on='dep', right_on='code_depart'))
locdep.tail()
dep R ancienne_re code_ancien code_depart code_region departement region geometry
91 91 0.829268 Île-de-France 11.0 91 11.0 Essonne Île-de-France POLYGON ((2.04261 48.62638, 2.04045 48.62754, ...
92 92 0.825893 Île-de-France 11.0 92 11.0 Hauts-de-Seine Île-de-France POLYGON ((2.22040 48.92061, 2.23114 48.92773, ...
93 93 0.741722 Île-de-France 11.0 93 11.0 Seine-Saint-Denis Île-de-France POLYGON ((2.45949 48.95505, 2.46072 48.95840, ...
94 94 0.742105 Île-de-France 11.0 94 11.0 Val-de-Marne Île-de-France POLYGON ((2.32906 48.81378, 2.33190 48.81701, ...
95 95 0.906542 Île-de-France 11.0 95 11.0 Val-d'Oise Île-de-France POLYGON ((2.43548 49.13411, 2.44084 49.13419, ...


Et on dessine. Les départements en vert sont ceux pour lequel le R est > 1.

with warnings.catch_warnings():
    warnings.filterwarnings("ignore", category=UserWarning)
    warnings.filterwarnings("ignore", category=DeprecationWarning)
    fig, axs = plt.subplots(
        1, 2, figsize=(16, 10),
        gridspec_kw={'width_ratios': [2, 1]})

    # métropole
    ax = axs[0]
    cax = make_axes_locatable(ax).append_axes("right", size="5%", pad=0.1)
    locdep.plot(
        column="R", ax=ax, edgecolor='black',
        legend=True, cax=cax, cmap="OrRd")
    if (locdep.R < 1).sum() > 0:
        locdep[locdep.R < 1].geometry.boundary.plot(
            color=None, edgecolor='g', linewidth=2, ax=ax, label="R<1")
    ax.set_title(f"R par départments de la métropole\n{deps.jour.max()!r}")

    for _, row in locdep.iterrows():
        p = row['geometry'].representative_point()
        ax.annotate(f"{row['R']:1.1f}", xy=(p.x, p.y),
                    horizontalalignment='center', color="black", fontsize=8)

    ax.legend()

    # Paris et sa région
    idf = set(['75', '77', '78', '91', '92', '93', '94', '95'])
    ax = axs[1]
    locdep2 = locdep[locdep.dep.isin(idf)]
    cax = make_axes_locatable(ax).append_axes("right", size="5%", pad=0.1)
    locdep2.plot(
        column="R", ax=ax, edgecolor='black',
        legend=True, cax=cax, cmap="OrRd")
    ax.set_title(f"R par départments de la métropole\n{deps.jour.max()!r}")

    for _, row in locdep2.iterrows():
        p = row['geometry'].representative_point()
        ax.annotate(f"{row['R']:1.1f}", xy=(p.x, p.y),
                    horizontalalignment='center', color="black", fontsize=8)

plt.show()
R par départments de la métropole Timestamp('2023-06-27 00:00:00'), R par départments de la métropole Timestamp('2023-06-27 00:00:00')
/usr/local/lib/python3.9/site-packages/geopandas/plotting.py:48: ShapelyDeprecationWarning: The 'type' attribute is deprecated, and will be removed in the future. You can use the 'geom_type' attribute instead.
  if geom is not None and geom.type.startswith(prefix) and not geom.is_empty:
/usr/local/lib/python3.9/site-packages/geopandas/plotting.py:48: ShapelyDeprecationWarning: The 'type' attribute is deprecated, and will be removed in the future. You can use the 'geom_type' attribute instead.
  if geom is not None and geom.type.startswith(prefix) and not geom.is_empty:

Total running time of the script: ( 0 minutes 46.073 seconds)

Gallery generated by Sphinx-Gallery