Del 2: En model over smittespredning

Du har nu din model, og dine agenter - men hvordan skal du simulere sygdommen? Du grubler meget længe, indtil at en anden kollega fortæller dig om SIR-modellen [#]_ : en matematisk model, som bruges til at modellere sygdomsspredning.

Modellen har tre kategorier, som den opdeler folk i:

  • Susceptible: Folk i denne gruppe er modtagelige, og kan blive smittet, hvis de kommer i kontakt med en, der bærer sygdommen.
  • Infectious: Folk i denne gruppe er blevet syge, og kan smitte folk, der er modtagelige.
  • Recovered: Folk i denne gruppe har haft sygdommen og er blevet raske og immune, og kan derfor ikke længere hverken smitte eller blive smittet.

En person kan altså kun være i én kategori ad gangen, og deres tilstand vil have mønsteret:

SusceptibleInfectiousRecovered

Du tænker, at dette er lige den model, du har brug for, og går straks i gang med at kode.

Fra agent til person

Lige nu er vores agenter “bare” agenter. Vi vil gerne gøre dem lidt mere avancerede, sådan at de blandt andet kan selv kan holde styr på, hvilken kategori af SIR-modellen, de er i.

Tilføj, over din model_setup-funktion (men under dine imports), følgende kode:

class Person(Agent):
    def setup(self,model):
        self.category = 0

    def step(self,model):
        self.direction += randint(-10,10)
        self.forward()

Ovenstående kode definerer en klasse, som har noget opførsel beskrevet i sine egne funktioner Person.setup og Person.step.

Ændr så model_setup-funktionen til:

def model_setup(model):
    model.reset()
    for person in range(100):
        model.add_agent(Person())

Nu tilføjer vi altså personer i stedet for “bare” normale agenter.

Bemærk, at indholdet i Person.step lidt ligner det, der står i model_step-funktionen i forvejen. Faktisk kan vi nu også ændre i model_step-funktionen, sådan at der i stedet står:

def model_step(model):
    for person in model.agents:
        person.step(model)

Prøv nu at køre modellen igen. Hvis du har gjort det rigtigt, burde den ikke se anderledes ud end før.

Kategorier

For ikke at skulle skrive navnene på kategorierne hele tiden, bruger vi i stedet tal, sådan at

Kategori #
Susceptible 0
Infectious 1
Recovered 2

Tilføj nu en infect-funktion til Person, som har følgende udseende:

def infect(self, model):
    self.color = (200, 0, 0)
    self.category = 1

Funktionen giver agenten en rød farve, og sætter den i kategori 1.

Omskriv så Person.setup til følgende:

def setup(self,model):
    self.category = 0
    self.color = (0, 200, 0)
    if randint(1,50) == 1:
        self.infect(model)

Vi gør her sådan, at de fleste agenter starter med at være raske og have en grøn farve, men en lille del (omkring 2%) starter med at være syge og have en rød farve.

../_images/epidemic-2.2.png

Smittespredning

Ideen med modellen er, at de syge agenter skal smitte de raske agenter. Vi gør det på den måde, at en syg agent smitter alle raske agenter, som er indenfor en bestemt afstand af den. Tilføj følgende kode i bunden af Person.step-funktionen:

if self.category == 1:
    for agent in self.agents_nearby(12):
        if agent.category == 0:
            agent.infect(model)

Koden siger, at hvis agenten er i kategori 1 (altså syg), så smitter den alle agenter indenfor en radius af 12 (agentens egen radius er på 4).

../_images/epidemic-2.3.png

Immunitet

Lige nu kan vores model vise 2 af de 3 kategorier, altså “susceptible” og “infectious”. Som det sidste led i modellen, skal agenter i “infectious” kategorien flyttes til “recovered” kategorien, når der er gået et stykke tid.

Tilføj først først denne funktion turn_immune til Person:

def turn_immune(self, model):
    self.color = (0,0,200)
    self.category = 2

Denne minder om Person.infect, men i stedet for at personen bliver rød og inficeret, bliver den blå og opnår immunitet.

Tilføj så denne linje til Person.infect:

self.infection_level = 600

Idéen med infection_level-variablen er, at den langsomt tæller ned, og, når den rammer 0, bliver den inficerede agent immun. Det gør vi ved at tilføje disse tre linjer i bunden af if-sætningen i Person.step:

self.infection_level -= 1
if self.infection_level == 0:
    self.turn_immune(model)

if-sætningen burde til slut gerne se således ud:

if self.category == 1:
    for agent in self.agents_nearby(12):
        if agent.category == 0:
            agent.infect(model)
    self.infection_level -= 1
    if self.infection_level == 0:
        self.turn_immune(model)

Når du kører programmet, burde du nu have en færdig implementation af SIR-modellen.

Grafer

Til slut vil vi gerne se, om vores model forløber på samme måde som SIR-modellen. Det gør vi ved at indsætte en graf, som viser fordelingen af agenter over tid.

Ideen med grafen kommer til at være, at vi optæller antallet af agenter i hver kategori, og så får grafen til at vise tre linjer, som viser antallene i hver kategori som funktion af tid.

Begynd først med at indsætte disse tre linjer i model_setup-funktionen, lige efter du har kaldt model.reset():

model.Susceptible = 0
model.Infectious = 0
model.Recovered = 0

Vi får agenterne selv til at tildele sig de forskellige kategorier, så vi lader alle tre starte med at være 0.

Tilføj øverst i Person.setup:

model.Susceptible += 1

Tilføj øverst i Person.infect:

model.Susceptible -= 1
model.Infectious += 1

Tilføj øverst i Person.turn_immune:

model.Infectious -= 1
model.Recovered += 1

Nu har vi styr på dataen til vores model. Programmet skal dog lige vide, at det skal opdatere grafen, imens Go-knappen holdes inde. Tilføj denne linje nederst i model_step-funktionen:

model.update_plots()

Det eneste, vi mangler nu, er at tilføje graferne. Indsæt disse linjer, lige efter der hvor du tilføjer knapperne til modellen:

epidemic_model.line_chart(["Susceptible","Infectious","Recovered"],[(0, 200, 0),(200, 0, 0),(0, 0, 200)])
epidemic_model.bar_chart(["Susceptible", "Infectious", "Recovered"], (200, 200, 200))

Prøv at køre modellen, indtil der ikke er flere inficerede agenter tilbage, og sammenlign så den graf du får med den, der er på Wikipedia-siden for SIR-modellen.

../_images/epidemic-2.4.png

Forskelle mellem SIR-modellen og den agent-baserede model

Du har nu udviklet en agent-baseret model, der approksimerer SIR-modellen. Men hvad er fordelene og ulemperne egentlig ved at bruge de to forskellige modeller?

SIR-modellen
  • Fordele: Da SIR-modellen er baseret på et sæt matematiske formler, er det nemmere at beregne direkte på modellens resultater, samt at kombinere og sammenligne den med andre matematiske modeller. Derudover er modellen også konsistent: hvis man giver det samme input flere gange til den samme model, vil man altid få det samme output.
  • Ulemper: SIR-modellen er en meget generaliserende model, da den antager, at alle individer opfører sig ens, for eksempel ved at alle har samme infektionsrate og sygdomsforløb. Man kan approksimere forskellige opførsler ved at justere på parametrene, men det er svært at sige, hvor meget det hænger sammen med virkeligheden.
Agent-baseret model
  • Fordele: Med den agent-baserede model er det nemmere at modellere både forskellige typer adfærd og karakteristika for både personer og virus, for eksempel superspredere, virusmutationer, social afstand, og så videre. Selvom modellen aldrig kan være helt præcis, kan den stadig give god indsigt i, hvilken indflydelse disse faktorer har på epidemien, og hvordan de interagerer med hinanden.
  • Ulemper: Den agent-baserede model har en stor tilfældighedsfaktor i sig. Agenterne starter tilfældige steder, bevæger sig tilfældigt, og smittes tilfældigt. Derfor er det nødvendigt at køre modellen mange gange for at fastlægge endelige resultater, og selv da kan man ikke garantere det. Derudover er det også svært at kombinere den agent-baserede model med andre eksisterende matematiske modeller.

Opgave 1

Tilføj en tilfældighed, så smitte ikke spredes med 100% sandsynlighed, men fx kun 20% sandsynlighed, når to agenter mødes.

Hint: Brug randint()-funktionen, som vi også har brugt tidligere.

Opgave 2

Overvej hvordan vi kan lave en type agent “Superspreder”, der enten:

  • Bevæger sig hurtigere (flere kontakter)
  • Smitter mere end andre agenter (høj smitterate)

Samlet kode

Her er den samlede kode du gerne skulle have nu:

from agents import Model, Agent, run
from random import randint


class Person(Agent):
    def setup(self, model):
        model.Susceptible += 1
        self.category = 0
        self.color = (0, 200, 0)
        if randint(1, 50) == 1:
          self.infect(model)

    def step(self, model):
        self.direction += randint(-10, 10)
        self.forward()
        if self.category == 1:
            for agent in self.agents_nearby(12):
                if agent.category == 0:
                    agent.infect(model)
            self.infection_level -= 1
            if self.infection_level == 0:
                self.turn_immune(model)

    def infect(self, model):
        model.Susceptible -= 1
        model.Infectious += 1
        self.color = (200, 0, 0)
        self.category = 1
        self.infection_level = 600

    def turn_immune(self, model):
        model.Infectious -= 1
        model.Recovered += 1
        self.color = (0, 0, 200)
        self.category = 2


def model_setup(model):
    model.reset()
    model.Susceptible = 0
    model.Infectious = 0
    model.Recovered = 0
    for person in range(100):
        model.add_agent(Person())


def model_step(model):
    for person in model.agents:
        person.step(model)
    model.update_plots()


epidemic_model = Model("Epidemimodel", 100, 100)

epidemic_model.add_button("Setup", model_setup)
epidemic_model.add_button("Go", model_step, toggle=True)
epidemic_model.line_chart(
    ["Susceptible", "Infectious", "Recovered"], [(0, 200, 0), (200, 0, 0), (0, 0, 200)]
)
epidemic_model.bar_chart(["Susceptible", "Infectious", "Recovered"], (200, 200, 200))

run(epidemic_model)