How to model immunity

In this tutorial, we are going to simulate the spread of Covid-19 in the case of a virus variant that is potentially resistant to immunity. We will first introduce the relevant immunity related variables that govern the model. Afterwards we will look at a simulation comparing how immunity and virus strains that are very resistant to it, interact in the model.

[1]:
%matplotlib inline

import warnings

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

import sid
from sid.config import INDEX_NAMES

warnings.filterwarnings("ignore")
np.random.seed(0)

Preparation

For the simulation we need to prepare several objects which are identical to the ones from the tutorial on simulating multiple virus strain.

Initial states

[2]:
available_ages = [
    "0-9",
    "10-19",
    "20-29",
    "30-39",
    "40-49",
    "50-59",
    "60-69",
    "70-79",
    "80-100",
]

ages = np.random.choice(available_ages, size=10_000)
regions = np.random.choice(["North", "South"], size=10_000)

initial_states = pd.DataFrame({"age_group": ages, "region": regions}).astype("category")
initial_states.head()
[2]:
age_group region
0 50-59 North
1 0-9 South
2 30-39 South
3 30-39 North
4 70-79 South

Contact Models

[3]:
def meet_distant(states, params, seed):
    possible_nr_contacts = np.arange(10)
    contacts = np.random.choice(possible_nr_contacts, size=len(states))
    return pd.Series(contacts, index=states.index)


def meet_close(states, params, seed):
    possible_nr_contacts = np.arange(5)
    contacts = np.random.choice(possible_nr_contacts, size=len(states))
    return pd.Series(contacts, index=states.index)


assort_by = ["age_group", "region"]

contact_models = {
    "distant": {"model": meet_distant, "assort_by": assort_by, "is_recurrent": False},
    "close": {"model": meet_close, "assort_by": assort_by, "is_recurrent": False},
}

Initial conditions

For the initial conditions, we assume a two-day burn-in period. On the first day, 50 people are infected with the base virus, on the second day one halve of 50 people has the old and the other halve the new variant.

Each column in the DataFrame is a categorical. Infected individuals have a code for the variant, all others have NaNs.

[4]:
infected_first_day = set(np.random.choice(10_000, size=50, replace=False))
first_day = pd.Series([pd.NA] * 10_000)
first_day.iloc[list(infected_first_day)] = "base"

infected_second_day_old_variant = set(
    np.random.choice(
        list(set(range(10_000)) - infected_first_day), size=25, replace=False
    )
)
infected_second_day_new_variant = set(
    np.random.choice(
        list(set(range(10_000)) - infected_first_day - infected_second_day_old_variant),
        size=25,
        replace=False,
    )
)

second_day = pd.Series([pd.NA] * 10_000)
second_day.iloc[list(infected_second_day_old_variant)] = "base"
second_day.iloc[list(infected_second_day_new_variant)] = "b117"

initial_infections = pd.DataFrame(
    {
        pd.Timestamp("2020-02-25"): pd.Categorical(
            first_day, categories=["base", "b117"]
        ),
        pd.Timestamp("2020-02-26"): pd.Categorical(
            second_day, categories=["base", "b117"]
        ),
    }
)

initial_conditions = {"initial_infections": initial_infections, "initial_immunity": 50}

Parameters

[5]:
epidemiological_parameters = pd.read_csv("infection_probs.csv", index_col=INDEX_NAMES)

assort_probs = pd.read_csv("assort_by_params.csv", index_col=INDEX_NAMES)

disease_params = sid.load_epidemiological_parameters()

params = pd.concat([disease_params, epidemiological_parameters, assort_probs])
[6]:
params
[6]:
value note source
category subcategory name
health_system icu_limit_relative icu_limit_relative 50.00 NaN NaN
cd_infectious_true all 1 0.39 NaN NaN
2 0.35 NaN NaN
3 0.22 NaN NaN
5 0.04 NaN NaN
... ... ... ... ... ...
infection_prob household household 0.20 NaN NaN
assortative_matching close age_group 0.50 NaN NaN
region 0.90 NaN NaN
distant age_group 0.50 NaN NaN
region 0.90 NaN NaN

136 rows × 3 columns

Additional objects to model immunity

In the model, immunity is assumed to be a factor between 0 and 1. A non-zero immunity level can arise either through infection or vaccination. If an individual has contact with an infectious person state-dependent infection probability, say \(p\), is multiplied with the factor \((1 - \text{immunity})\). But we know that the effect of immunity is heterogeneous dependent on the virus strain. To model this phenomenon we include another virus dependent factor, which we call immunity_resistance_factor. The updated infection probability is then

:nbsphinx-math:`begin{align}

p leftarrow p times left(1 - text{immunity_resistance} times text{immunity} right) ,.

end{align}`

From this expression it is clear that, for the immunity resistance factor, values close to 0 imply that immunity barely affects the infection probability, while values close to one imply that the immunity level influences the infection probability directly. Now lets return to the simulation.

[11]:
for virus, cf, irf in [("base", 1, 1), ("b117", 1.3, 0.5)]:
    params.loc[("virus_strain", virus, "contagiousness_factor"), "value"] = cf
    params.loc[("virus_strain", virus, "immunity_resistance_factor"), "value"] = irf

Run the simulation

Now we will simulate this population over 200 periods. To compare the effects of the immunity resistance parameter we run the simulation for three parameter values: 0, 0.5 and 1.

[12]:
resistance_factors = [0, 0.5, 1]

df_dict = {}
for value in resistance_factors:

    # update the immunity resistance factor
    params.loc[("virus_strain", "b117", "immunity_resistance_factor"), "value"] = value

    # simulate
    simulate = sid.get_simulate_func(
        initial_states=initial_states,
        contact_models=contact_models,
        params=params,
        initial_conditions=initial_conditions,
        duration={"start": "2020-02-27", "periods": 365},
        virus_strains=["base", "b117"],
        seed=2,
    )

    result = simulate(params=params)
    df_dict[value] = result["time_series"].compute()
Start the simulation...
2021-02-25: 100%|██████████| 365/365 [00:44<00:00,  8.17it/s]
Start the simulation...
2021-02-25: 100%|██████████| 365/365 [00:37<00:00,  9.69it/s]
Start the simulation...
2021-02-25: 100%|██████████| 365/365 [00:35<00:00, 10.38it/s]

Let us take a look at various statistics of the sample.

[13]:
fig, axs = plt.subplots(4, 2, figsize=(14, 12))
fig.subplots_adjust(bottom=0.15, wspace=0.2, hspace=0.5)

axs = axs.flatten()

value_to_ax = {0: 5, 0.5: 6, 1: 7}

for value, color in [(0, "tab:blue"), (0.5, "tab:green"), (1, "tab:orange")]:

    df_dict[value].resample("D", on="date")["ever_infected"].mean().plot(
        ax=axs[0], color=color, label=value
    )
    df_dict[value].resample("D", on="date")["infectious"].mean().plot(
        ax=axs[1], color=color
    )
    df_dict[value].resample("D", on="date")["dead"].sum().plot(ax=axs[4], color=color)

    r_zero = sid.statistics.calculate_r_zero(df_dict[value], window_length=7)
    r_zero.plot(ax=axs[2], color=color)

    r_effective = sid.statistics.calculate_r_effective(df_dict[value], window_length=7)
    r_effective.plot(ax=axs[3], color=color)

    _df = (
        df_dict[value]
        .query("newly_infected")
        .groupby([pd.Grouper(key="date", freq="D"), "virus_strain"])["newly_infected"]
        .count()
        .unstack()
    )
    _df["base"].plot(ax=axs[value_to_ax[value]], color="grey")
    _df["b117"].plot(ax=axs[value_to_ax[value]], color=color)

for ax in axs:
    ax.set_xlabel("")
    ax.spines["right"].set_visible(False)
    ax.spines["top"].set_visible(False)

axs[0].legend(
    ncol=3,
    bbox_to_anchor=(1.3, 1.5),
    title="Immunity resistance factor",
)
axs[5].legend(loc="upper center", bbox_to_anchor=(0.70, 0.95), ncol=2)
axs[6].legend(loc="upper center", bbox_to_anchor=(0.75, 0.70), ncol=2)
axs[7].legend(loc="upper center", bbox_to_anchor=(0.75, 0.70), ncol=2)

axs[0].set_title("Share of Infected People")
axs[1].set_title("Share of Infectious People in the Population")
axs[2].set_title("$R_0$ (Basic Reproduction Number)")
axs[3].set_title("$R_t$ (Effective Reproduction Number)")
axs[4].set_title("Total Number of Deaths")
axs[5].set_title("Distribution of virus strains among newly infected (resistance: 0)")
axs[6].set_title("Distribution of virus strains among newly infected (resistance: 0.5)")
axs[7].set_title("Distribution of virus strains among newly infected (resistance: 1)")

plt.show()
../_images/tutorials_how_to_model_immunity_24_0.png