Skip to content

← Data and analysis

How Does a Cold Spread Through a School?

Topic 16 · Grades 6–8 · Modeling a spread

The blocks from Grade 7 (variables and repeat); the Python parts from Grade 8 (loops, lists and functions).

What this builds on from class
In this unit 9 sections
  1. Try it
  2. How it works
  3. Program it
  4. What your program shows
  5. What the model leaves out
  6. Going further still
  7. Challenges
  8. Questions
  9. Where to go next

On Monday one student in Class 7 comes to school sneezing. By Thursday two of her friends have colds too. Sometimes that is the end of it. Other times, two weeks later, half the school is sneezing. Why does a cold fizzle out one time and sweep through the school the next?

Scientists answer questions like this with a model: a simple copy of the real thing, run on a computer. Here is a school of 300 students, one of them sick. Press Next day, or Play.

Try it

A school of 300

Each dot is a student; a red line shows who caught it from whom that day.

  • Healthy
  • Sick
  • Sick, at home
  • Better
Each sick student passes it to:
Day
0
Sick now
1
Caught so far
1
Sent home
0

Day 0: one student comes to school with a cold. Press Next day, or Play.

Play the school a few times with 1.5 (each sick student passes the cold to 1.5 others, on average), then a few times with 0.8. How different are they?

Pick an answer to see why.

You need: variables and repeat (Grade 7) for the blocks; loops, lists and functions (Grade 8) for the Python. This is a model to explore, not health advice: “What the model leaves out” says how a real school differs.

Try it

One school at a time

The number you choose is called R: how many others each sick student passes the cold to, on average, while nearly everyone is still healthy. Here a sick student has 4 meetings a day, with students picked at random, for 3 days. Each meeting passes the cold on with a small chance, set so that the 12 meetings make R new cases on average.

  • a.Choose 0.5 and play five schools. How many catch it, at most?
  • b.Choose 2 and play five schools. Does the cold ever die out early?
  • c.Choose 1.2. What happens? (Play it several times: it is the hardest to guess.)

Stay home!

A sick student who stays home passes the cold to nobody at school. In the widget, tap a red student to send them home. (A student who caught it today doesn’t feel ill yet: they spend one day at school first, and can be sent home after that.)

Choose 2. Press Next day, and send home every sick student you can. Can you stop the cold before it reaches 10 students?

You sent every sick student home on the day you saw them. What did that do to R?

Pick an answer to see why.

A hundred schools

One school can be lucky or unlucky. To see what usually happens, run many.

Try it

A hundred schools

Each bar counts the schools in which that many caught the cold. Blue bars: it died out with fewer than 30 cases.

0255075100060120180240300Students who caught it, in each school (of 300)Schools
Each sick student passes it to:
Died out under 30
–
Average caught
–
Most in one school
–

Press Run 100 schools.

Run 100 schools for each R in turn. Where does the picture change most?

Pick an answer to see why.

How it works

Rounds of passing it on

Think of the cold passing on in rounds. The first student passes it to R others, on average: round 1. Each of those passes it to R more, so round 2 has R × R new cases, and so on. Each round, the new cases are multiplied by R.

Round 0 1 2 3 4 5 6
New cases, R = 0.8 1 0.8 0.64 0.51 0.41 0.33 0.26
New cases, R = 1.5 1 1.5 2.25 3.38 5.06 7.59 11.39

(These are averages: over 100 schools, round 2 would have about 64 cases in all.)

Multiply again and again by a number below 1, and the rounds shrink to nothing; by a number above 1, and they grow, faster each round. That is the tipping point you saw in the hundred schools.

Why it stops before everyone

With R = 1.5 the table grows for ever, but a school has only 300 students. As more have had the cold, more meetings are with someone who can’t catch it again. Once half the school has had it, each sick student passes it to about 1.5 × ½ = 0.75 others, below 1. The cold grows until about a third has had it (1.5 × ⅔ = 1), then shrinks, and many never catch it.

Program it

With blocks: the rounds

Here are the rounds as a block program: it asks for R, then 10 times multiplies the new cases by R and adds them to the total.

Try it

The rounds of passing it on

Press Run and type 0.8; watch new and total in the trace table. Then try 1.5 and 2. Test runs five values of R at once.

What the program says and asks appears here.

R – no value
round – no value
new – no value
total – no value
StepBlockRroundnewtotal
TestShould sayIt said
R = 0.5After 10 rounds, caught it: 1.999023438
R = 0.8After 10 rounds, caught it: 4.57050327
R = 1After 10 rounds, caught it: 11
R = 1.2After 10 rounds, caught it: 32.150418534
R = 2After 10 rounds, caught it: 2047

Press Run to start the program, or Step to do one block at a time.

Loading the blocks…

With R = 0.8, the total after 10 rounds is about 4.6. What will it be after 100 rounds?

Pick an answer to see why.

In Python (from Grade 8)

Step 1: one school

The school is two lists, one place for each student, 0 to 299: days_left[s], how many more days student s will be sick (0 if not sick), and caught[s], whether s has ever had the cold. [0] * 300 makes a list of 300 zeros.

From the random module: random.randint(0, 299) picks a student, and random.random() gives a decimal between 0 and 1, so random.random() < CHANCE is true with exactly that chance. random.seed(5) makes the same random numbers come out every time, so your school matches the one here; change the 5 for another.

Today’s new cases only start passing it on tomorrow, so they go into a list new first. The program prints a bar for each day, one # for every two students sick. (days_left.count(0) counts the 0s; str(day).rjust(2) writes the day in two places, so the bars line up.)

import randomrandom.seed(5) STUDENTS = 300      # students in the schoolMEETS = 4           # students a sick student meets each dayDAYS_SICK = 3       # days a student can pass the cold onR = 1.5             # how many each sick student infects, on averageCHANCE = R / (MEETS * DAYS_SICK) days_left = [0] * STUDENTS      # days each student is still sick (0: not sick)caught = [False] * STUDENTS     # has each student had the cold?days_left[0] = DAYS_SICK        # student 0 comes to school with a coldcaught[0] = True day = 0while days_left.count(0) < STUDENTS:      # while anyone is sick    day = day + 1    new = []    for s in range(STUDENTS):        if days_left[s] > 0:              # s is sick today            for m in range(MEETS):                if random.random() < CHANCE:     # this meeting passes it on                    other = random.randint(0, STUDENTS - 1)                    if not caught[other]:                        caught[other] = True                        new.append(other)            days_left[s] = days_left[s] - 1    for s in new:                         # today's new cases start tomorrow        days_left[s] = DAYS_SICK    sick = STUDENTS - days_left.count(0)    print(str(day).rjust(2), '#' * (sick // 2), sick) print('Caught it:', caught.count(True), 'of', STUDENTS)

Output

 1  1 2 # 2 3 ## 5 4 ### 7 5 ##### 10 6 ###### 12 7 ####### 14 8 ####### 15 9 ####### 1410 ######## 1711 ########## 2112 ############# 2713 ############### 3014 ################## 3715 ###################### 4416 ######################## 4917 ######################## 4818 ##################### 4319 #################### 4020 ############## 2921 ########### 2322 ######## 1623 ####### 1424 #### 825 # 326 # 227 # 228 # 229  130  0Caught it: 179 of 300

The bars are the shape of an outbreak: slow at first, then a hump, then a tail. Try R = 0.8, and other seeds.

Step 2: the school as a film

Now the same outbreak as a film, a picture a day, played by show_frames from the page’s own pixels module (on your own computer you might use Pillow). picture() turns the lists into 15 rows of 20 seats: 0 healthy, 1 sick, 2 better.

import randomfrom pixels import show_framesrandom.seed(5) STUDENTS = 300MEETS = 4DAYS_SICK = 3R = 1.5CHANCE = R / (MEETS * DAYS_SICK) days_left = [0] * STUDENTScaught = [False] * STUDENTSdays_left[0] = DAYS_SICKcaught[0] = True def picture():    rows = []    for r in range(15):        row = []        for c in range(20):            s = r * 20 + c                # the student in row r, seat c            if days_left[s] > 0:                row.append(1)             # sick            elif caught[s]:                row.append(2)             # better            else:                row.append(0)             # healthy        rows.append(row)    return rows frames = [picture()]day = 0while days_left.count(0) < STUDENTS:    day = day + 1    new = []    for s in range(STUDENTS):        if days_left[s] > 0:            for m in range(MEETS):                if random.random() < CHANCE:     # this meeting passes it on                    other = random.randint(0, STUDENTS - 1)                    if not caught[other]:                        caught[other] = True                        new.append(other)            days_left[s] = days_left[s] - 1    for s in new:        days_left[s] = DAYS_SICK    frames.append(picture()) print('The cold lasted', day, 'days.')show_frames(frames, ['#f1ead0', '#d6332a', '#8fa3b8'])

Output

The cold lasted 30 days.

Picture

The school of 300 on the last day: a grid of 15 rows of 20 seats, more than half of them gray-blue for students who had the cold and are better, the rest cream for students who never caught it.
The last of its 31 steps: press Run to play them all.

Press Play under the picture, or move the slider.

Step 3: a hundred schools for each R

Step 1’s loop goes into a function, one_school(R), which gives back how many caught the cold, and the program runs a hundred schools for each R. Fewer than 30 cases counts as died out early.

import randomrandom.seed(2026) STUDENTS = 300MEETS = 4DAYS_SICK = 3 def one_school(R):    chance = R / (MEETS * DAYS_SICK)    days_left = [0] * STUDENTS    caught = [False] * STUDENTS    days_left[0] = DAYS_SICK    caught[0] = True    while days_left.count(0) < STUDENTS:        new = []        for s in range(STUDENTS):            if days_left[s] > 0:                for m in range(MEETS):                    if random.random() < chance:     # this meeting passes it on                        other = random.randint(0, STUDENTS - 1)                        if not caught[other]:                            caught[other] = True                            new.append(other)                days_left[s] = days_left[s] - 1        for s in new:            days_left[s] = DAYS_SICK    return caught.count(True) for R in [0.5, 0.8, 0.9, 1.1, 1.2, 1.5, 2, 3]:    total = 0    early = 0    for school in range(100):        n = one_school(R)        total = total + n        if n < 30:            early = early + 1    print('R', R, ' average caught:', total / 100, ' died out early:', early, 'of 100')

Output

R 0.5  average caught: 1.99  died out early: 100 of 100R 0.8  average caught: 4.26  died out early: 99 of 100R 0.9  average caught: 6.22  died out early: 95 of 100R 1.1  average caught: 19.88  died out early: 79 of 100R 1.2  average caught: 27.25  died out early: 74 of 100R 1.5  average caught: 105.17  died out early: 38 of 100R 2  average caught: 190.15  died out early: 21 of 100R 3  average caught: 270.18  died out early: 4 of 100

Step 4: sick students stay home

In this model a student feels ill from their second day. Suppose every sick student stays home from then on: they meet people only on their first day, when days_left[s] is still DAYS_SICK. One new if does it.

import randomrandom.seed(2026) STUDENTS = 300MEETS = 4DAYS_SICK = 3 def one_school(R, stay_home):    chance = R / (MEETS * DAYS_SICK)    days_left = [0] * STUDENTS    caught = [False] * STUDENTS    days_left[0] = DAYS_SICK    caught[0] = True    while days_left.count(0) < STUDENTS:        new = []        for s in range(STUDENTS):            if days_left[s] > 0:                first_day = days_left[s] == DAYS_SICK                if first_day or not stay_home:                    for m in range(MEETS):                        if random.random() < chance:     # this meeting passes it on                            other = random.randint(0, STUDENTS - 1)                            if not caught[other]:                                caught[other] = True                                new.append(other)                days_left[s] = days_left[s] - 1        for s in new:            days_left[s] = DAYS_SICK    return caught.count(True) for R in [1.5, 2, 3]:    for stay_home in [False, True]:        total = 0        for school in range(100):            total = total + one_school(R, stay_home)        print('R', R, ' stay home:', stay_home, ' average caught:', total / 100)

Output

R 1.5  stay home: False  average caught: 107.78R 1.5  stay home: True  average caught: 1.92R 2  stay home: False  average caught: 201.79R 2  stay home: True  average caught: 3.17R 3  stay home: False  average caught: 262.0R 3  stay home: True  average caught: 10.66

What your program shows

  • Below 1, a cold fizzles out. With R = 0.5 every school had fewer than 30 cases, and with 0.8 all but one, with 4.26 cases on average: close to the blocks’ 5.
  • Just above 1, it’s a gamble. At R = 1.1 and 1.2 most schools were lucky (79 and 74 of 100); the rest had big outbreaks.
  • Well above 1, it reaches most of the school: 105 on average at R = 1.5, 190 at 2, 270 at 3. A change from 0.8 to 1.5 turned a handful into a third of the school.
  • Changing what people do changes R. With sick students staying home after their first day, R = 3 became about 1: 11 cases on average instead of 262.

That is why health workers talk so much about R. They can’t change the virus, but they can change how many people each sick person meets. Push R below 1 and the outbreak dies out by itself: you needn’t stop every case, only enough of them.

What the model leaves out

Our school is much simpler than a real one:

  • Everyone meets everyone at random. In a real school you meet your own class and your friends far more than anyone else, so colds spread class by class.
  • Everyone is the same. Real students differ: some meet many people, some few.
  • The numbers are made up. 4 meetings a day and 3 days sick keep the program small. The NHS (Britain’s health service) says you can pass on a cold until all your symptoms have gone, usually 1 to 2 weeks, and from a few days before you feel ill, so staying home helps but can’t stop everything.
  • No homes, weekends or teachers, and one cold each. Real colds travel home and back, and you can catch another soon after: the CDC (the United States’ health agency) says more than 200 viruses cause colds.

A model like this shows the shape of what happens, and why: the tipping point at 1, the luck at the start, the cold stopping before everyone has it. For real numbers, scientists check models against real cases, as Grade 11’s Modelling and Simulation does.

Going further still

Half stay home. In Step 4, give each sick student a chance, say random.random() < 0.5, of staying home. How many must stay home to bring R = 2 below 1?

A vaccine. Before the first day, mark some students caught = True. With R = 3, how many in 100 must be immune for the cold to die out early in most schools? Compare “Your turn” above.

Challenges

★ Enough for a whole school (blocks)

Finish the program so that it says how many rounds it takes before the total reaches at least 300, a whole school. Say just the number (for R = 2, it says 8). Then press Test.

Try it

★ Enough for a whole school

What the program says and asks appears here.

R – no value
rounds – no value
new – no value
total – no value
StepBlockRroundsnewtotal
TestShould sayIt said
R = 28
R = 35
R = 1.512
R = 103

Press Run to start the program, or Step to do one block at a time.

Loading the blocks…

Hint
Answer

★★ Who passed it on most? (Python, from Grade 8)

The school nurse keeps a notebook: for every new case, the day, the student who passed the cold on, and the student who caught it, as [day, giver, taker]. The first student is 0, and the students of this class are numbered 0 to 39. Write top_spreader(log) that gives back [student, count]: the student who passed it to the most others, and how many. If two passed it on equally often, give the one with the smaller number.

def top_spreader(log):    # log is a list of [day, giver, taker]    return [0, 0] notebook = [[1, 0, 7], [1, 0, 12], [2, 7, 3], [3, 12, 5], [3, 12, 9], [4, 9, 2]]print(top_spreader(notebook))

Try it

★★ Who passed it on most?

  1. The nurse's first notebook top_spreader([[1, 0, 7], [1, 0, 12], [2, 7, 3], [3, 12, 5], [3, 12, 9], [4, 9, 2]]) Should give [0, 2] Yours —
  2. Only one passed on top_spreader([[1, 0, 5]]) Should give [0, 1] Yours —
Tests passed
— / 7
Challenge
★★ solved

Write your top_spreader in the program above (Edit), then check it.

A test your function gets wrong, anything it prints, and any error appear here.

Hint
Another hint
Answer

★★★ Generation by generation (Python, from Grade 8)

Student 0 is generation 0. The students who caught it from student 0 are generation 1, those who caught it from generation 1 are generation 2, and so on. Write generations(log) that gives back a list of how many students are in each generation: for the notebook below, [1, 2, 3, 1]. The log is in order of days, so a giver always appears as a taker (or is student 0) before they give it to anyone.

def generations(log):    # log is a list of [day, giver, taker], in order of days    return [1] notebook = [[1, 0, 7], [1, 0, 12], [2, 7, 3], [3, 12, 5], [3, 12, 9], [4, 9, 2]]print(generations(notebook))

Try it

★★★ Generation by generation

  1. The nurse's first notebook generations([[1, 0, 7], [1, 0, 12], [2, 7, 3], [3, 12, 5], [3, 12, 9], [4, 9, 2]]) Should give [1, 2, 3, 1] Yours —
  2. Only one passed on generations([[1, 0, 5]]) Should give [1, 1] Yours —
Tests passed
— / 7
Challenge
★★★ solved

Write your generations in the program above (Edit), then check it.

A test your function gets wrong, anything it prints, and any error appear here.

Hint
Another hint
Answer

Questions

What does R = 1.5 mean for a cold?

Pick an answer to see why.

A cold has R = 0.7 in a school. What will most probably happen?

Pick an answer to see why.

a.

Two schools have colds with R = 0.9 and R = 1.2. One friend says: “They’re almost the same, so the outbreaks will be about the same size.” What would you tell your friend?

Answer
b.

Why does an outbreak stop before every student has caught the cold, even when R is 3?

Answer
c.

Give two ways a real school is different from the model, and say for each whether you think it would make the cold spread faster or slower.

Answer

Where to go next