Line lists and contacts
linelist gives you a DataFrame with one row per infected case. The core columns (id, parent_id, generation, chain_id, date_infection) are always there; anything else on the individual — typed fields or state dict entries — shows up as a column too. Keys ending in _time become date columns, so :onset_time becomes date_onset.
To get a new column, write the field during the simulation. Whatever ends up on state ends up in the DataFrame.
Line list
A simulation state is converted to a DataFrame with one row per infected case using linelist:
using EpiBranch
using Distributions
using DataFrames
using Dates
using StableRNGs
attrs = clinical_presentation(incubation_period = LogNormal(1.5, 0.5))
progression = [
Reporting(delay = Exponential(3.0)),
Hospitalisation(delay = Exponential(5.0), probability = 0.2),
Death(delay = Exponential(14.0), probability = 0.05),
Recovery(delay = Exponential(14.0)),
]
model = ModelSpec(BranchingProcess(NegBin(1.5, 0.5), LogNormal(1.6, 0.5));
progression = progression, attributes = attrs)
rng = StableRNG(42)
state = simulate(model; condition = 50:200, max_cases = 200, rng = rng)
ll = linelist(state; reference_date = Date(2024, 1, 1))
first(ll, 5)| Row | id | parent_id | generation | chain_id | date_infection | date_admission | date_death_candidate | date_onset | date_outcome | date_recovery_candidate | date_reporting | admitted | asymptomatic | incubation_period | outcome | reported |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Int64 | Int64 | Int64 | Int64 | Date | Date? | Date? | Date? | Date? | Date? | Date? | Bool | Bool | Float64 | String? | Bool | |
| 1 | 1 | 0 | 0 | 1 | 2024-01-01 | missing | missing | 2024-01-06 | 2024-01-10 | 2024-01-10 | 2024-01-12 | false | false | 5.96391 | recovered | true |
| 2 | 2 | 1 | 1 | 1 | 2024-01-02 | missing | missing | 2024-01-04 | 2024-01-13 | 2024-01-13 | 2024-01-04 | false | false | 1.86305 | recovered | true |
| 3 | 3 | 1 | 1 | 1 | 2024-01-09 | missing | missing | 2024-01-11 | 2024-01-27 | 2024-01-27 | 2024-01-24 | false | false | 2.51621 | recovered | true |
| 4 | 4 | 1 | 1 | 1 | 2024-01-10 | 2024-01-15 | missing | 2024-01-15 | 2024-02-01 | 2024-02-01 | 2024-01-21 | true | false | 4.74611 | recovered | true |
| 5 | 5 | 1 | 1 | 1 | 2024-01-05 | 2024-01-14 | missing | 2024-01-11 | 2024-01-11 | 2024-01-11 | 2024-01-21 | true | false | 5.58373 | recovered | true |
Columns appear only when the relevant state keys are set. Drop the Hospitalisation transition and date_admission disappears from the output. Drop clinical_presentation and date_onset, date_reporting, date_admission, date_outcome and outcome all disappear — the transitions can't anchor on a missing onset.
Demographics
Demographics are an attribute, set at simulation time via the demographics builder. They appear in the line list as age and sex columns:
attrs_demo = [
clinical_presentation(incubation_period = LogNormal(1.5, 0.5)),
demographics(age_distribution = Normal(40, 15), prob_female = 0.55),
]
model = ModelSpec(BranchingProcess(NegBin(1.5, 0.5), LogNormal(1.6, 0.5));
progression = progression, attributes = attrs_demo)
rng = StableRNG(42)
state = simulate(model; condition = 50:200, max_cases = 200, rng = rng)
ll = linelist(state; reference_date = Date(2024, 1, 1))
println("Age range: $(minimum(ll.age)) - $(maximum(ll.age))")
println("Female: $(round(count(==("female"), ll.sex) / nrow(ll) * 100, digits=1))%")Age range: 7 - 78
Female: 54.0%Age-stratified risks
Age-conditional case fatality risk is expressed as a closure on the Death transition's probability, reading ind.state[:age]:
attrs_demo = [
clinical_presentation(incubation_period = LogNormal(1.5, 0.5)),
demographics(age_distribution = Uniform(0, 90)),
]
cfr_by_age = ind -> begin
age = ind.state[:age]
age <= 14 ? 0.001 : age <= 64 ? 0.01 : 0.15
end
age_stratified = [
Death(delay = Exponential(14.0),
probability = (rng, ind) -> cfr_by_age(ind)),
Recovery(delay = Exponential(14.0)),
]
model = ModelSpec(BranchingProcess(NegBin(1.5, 0.5), LogNormal(1.6, 0.5));
progression = age_stratified, attributes = attrs_demo)
rng = StableRNG(42)
state = simulate(model; condition = 100:500, max_cases = 500, rng = rng)
ll = linelist(state; reference_date = Date(2024, 1, 1))
for (lo, hi) in [(0, 14), (15, 64), (65, 90)]
group = filter(r -> lo <= r.age <= hi, ll)
n_died = count(==("died"), group.outcome)
pct = nrow(group) > 0 ? round(n_died / nrow(group) * 100, digits=1) : 0.0
println("Age $lo-$hi: $(nrow(group)) cases, $n_died deaths ($pct%)")
endAge 0-14: 18 cases, 0 deaths (0.0%)
Age 15-64: 67 cases, 0 deaths (0.0%)
Age 65-90: 32 cases, 5 deaths (15.6%)The same closure pattern covers risk groups, comorbidities, or any state field set by your attributes function. See the transitions tutorial for the full menu.
The whole population
linelist gives cases only by default. Pass infected_only = false to get every individual in the population, as needed for a test-negative design, an attack rate by covariate, or an exposed/unexposed comparison. It is most useful for a structure-driven model such as HomogeneousProcess, NetworkProcess or HouseholdProcess, whose population exists in full from the start:
pool = ModelSpec(HomogeneousProcess(; transmission_rate = 0.6, population_size = 200);
progression = [Transition(:recovered; from = :infection, delay = Exponential(5.0),
terminal = true)],
attributes = attrs)
pool_state = simulate(pool; n_initial = 2, rng = StableRNG(1))
pop = linelist(pool_state; reference_date = Date(2024, 1, 1), infected_only = false)
println("Population: $(nrow(pop)), infected: $(count(pop.infected))")
first(pop, 5)| Row | id | parent_id | generation | chain_id | infected | date_infection | date_onset | date_outcome | date_recovered | asymptomatic | incubation_period | index | outcome | recovered |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Int64 | Int64 | Int64 | Int64 | Bool | Date? | Date? | Date? | Date? | Bool | Float64 | Bool? | String? | Bool? | |
| 1 | 1 | 121 | 10 | 2 | true | 2024-01-17 | 2024-01-22 | 2024-01-18 | 2024-01-18 | false | 4.70786 | false | recovered | true |
| 2 | 2 | 0 | 0 | 2 | true | 2024-01-01 | 2024-01-03 | 2024-01-05 | 2024-01-05 | false | 2.93948 | true | recovered | true |
| 3 | 3 | 135 | 4 | 125 | true | 2024-01-09 | 2024-01-12 | 2024-01-12 | 2024-01-12 | false | 3.9161 | false | recovered | true |
| 4 | 4 | 0 | 0 | 4 | false | missing | missing | missing | missing | false | 6.28259 | missing | missing | missing |
| 5 | 5 | 128 | 11 | 2 | true | 2024-01-15 | 2024-01-20 | 2024-01-29 | 2024-01-29 | false | 5.07062 | false | recovered | true |
On an offspring-driven model such as BranchingProcess the rows are the cases plus every contact they exposed who was not infected.
An uninfected row has missing for date_infection and for every date that follows from an infection: date_onset, reporting, admission and outcome dates, and any date from your own _time fields. The dates of events that happen to a person whether or not they are infected are kept:
date_trace, when the contact was traced;date_vaccinationanddate_immunity;date_isolation, when it is a quarantine on tracing. An isolation thatIsolationderived from the contact's provisional onset ismissing, and if it replaced an earlier quarantine the quarantine's date is shown.
Columns that are not dates, such as asymptomatic, traced or vaccinated, are reported as stored.
Contacts table
All contacts (infected and non-infected) are returned by contacts, with an infected flag:
ct = contacts(state; reference_date = Date(2024, 1, 1))
println("Total: $(nrow(ct)), Infected: $(count(ct.infected)), Not infected: $(count(.!ct.infected))")
first(ct, 5)| Row | from | to | infected | generation | infection_time | date_infection |
|---|---|---|---|---|---|---|
| Int64 | Int64 | Bool | Int64 | Float64 | Date | |
| 1 | 1 | 2 | true | 1 | 9.24427 | 2024-01-10 |
| 2 | 1 | 3 | true | 1 | 9.0023 | 2024-01-10 |
| 3 | 1 | 4 | true | 1 | 6.13182 | 2024-01-07 |
| 4 | 1 | 5 | true | 1 | 4.35452 | 2024-01-05 |
| 5 | 2 | 6 | true | 2 | 12.0085 | 2024-01-13 |
Conditioned simulation
Generate outbreaks of a specific size range:
plain = ModelSpec(BranchingProcess(NegBin(1.5, 0.5), LogNormal(1.6, 0.5)); attributes = attrs)
rng = StableRNG(42)
state = simulate(plain; condition = 100:150, max_cases = 200, rng = rng)
println("Outbreak size: $(state.cumulative_cases) (target: 100-150)")Outbreak size: 104 (target: 100-150)