The serology assay measures antibody titers against a panel of influenza strains, and is a component of the CMI-Flu prediction challenge. An overview of all the data, including links to more detailed descriptions of the other data, may be found at CMI-x .

Experimental protocol

Antibody responses were primarily measured by serum hemagglutination inhibition (HAI) against a panel of influenza strains, with HAI titers reported as the reciprocal of the highest serum dilution that inhibits hemagglutination. We also include ELISA (IgG, IgA) and neutralization data when available that can be used as additional model input, although all prediction tasks will predict HAI titers.

Data Standardization

Serology data generated for CMI-Flu was combined with other publicly-available data:

  • Virus strain names were standardized across studies following the pattern [‘H1N1 A’ or ‘H3N2 A’ or ‘Vic B’ or ‘Yam B’] + / + [origin] + / + [identifier] + / + [year] + [optional: _Cell or _MCDK] (if the virus was propagated in cell culture).
  • Technical replicates were collapsed to the geometric mean.
  • Timepoints were standardized to five reference timepoints: −14, 0, 28, 90, and 365 days. For each participant, the measurement closest to each reference timepoint was retained and assigned to that timepoint; all other measurements were discarded.
  • Baseline (“pre-vacc”) values were established by averaging all values at pre-vaccination timepoints.

The Data

Data tables

The serology dataset consists of 2 tables : publicData_serology.tsv contains both pre- and post-vaccination measurements from previous studies. Train your model on these data, then apply it to 2025LJI_serology.tsv (which contains pre-vaccination data only) to predict the unseen post-vaccination values. See the Prediction Task section description below for details.

Serology Assay
Data dictionary
Column Description
participant_id Donor ID, one for each subject in each study, in the format studyID.subjectID (links to participants.tsv).
timepoint Days relative to influenza vaccination. 'Pre-vacc' is the mean of all pre-vaccination timepoints.
virus_strain Influenza strain against which the antibody titer was measured (e.g. H1N1 A/California/7/2009).
assay Serological assay: hai (hemagglutination inhibition), elisa_IgG, elisa_IgA, or neutralization.
value Measured titer. For HAI, the reciprocal of the highest inhibitory serum dilution (two-fold series).
virus_in_vaccine 1 represents a vaccine strain, otherwise 0.
study_accession Study identifier. Links to the investigations.tsv file.
subject subject ID.

Data exploration

The main serology readout is the HAI titer; the exploration below focuses on HAI. The analyses below provide a high-level overview of the HAI data that you can use to confirm that you have successfully downloaded the data.”Post-vaccination titers are measured at Day 28 (magnitude/breadth) and Day 365 (durability).

train <- read.delim("../datasets/260512/train/publicData_serology.tsv", sep = "\t", stringsAsFactors = FALSE)
challenge <- read.delim("../datasets/260512/train/2025LJI_serology.tsv", sep = "\t", stringsAsFactors = FALSE)

train$set <- "train"
challenge$set <- "challenge"

# focus on HAI; keep the baseline mean and the post-vaccination (Day 28) timepoint
train <- train %>% filter(assay == "hai", timepoint %in% c("Pre-vacc", "28"))
challenge <- challenge %>% filter(assay == "hai", timepoint %in% c("Pre-vacc", "28"))

tp_levels <- c("Pre-vacc", "28")
train$timepoint     <- factor(train$timepoint,     levels = tp_levels)
challenge$timepoint <- factor(challenge$timepoint, levels = tp_levels)

both <- bind_rows(train, challenge)

Data preview :

head(both)
##    participant_id timepoint             virus_strain assay value
## 1 2016_UGA.ID_001        28  H1N1 A/Beijing/262/1995   hai    40
## 2 2016_UGA.ID_001        28    H1N1 A/Brazil/11/1978   hai     5
## 3 2016_UGA.ID_001        28  H1N1 A/Brisbane/59/2007   hai    40
## 4 2016_UGA.ID_001        28 H1N1 A/California/7/2009   hai   320
## 5 2016_UGA.ID_001        28      H1N1 A/Chile/1/1983   hai    10
## 6 2016_UGA.ID_001        28     H1N1 A/Denver/1/1957   hai     5
##   virus_in_vaccine study_accession subject   set
## 1                0        2016_UGA  ID_001 train
## 2                0        2016_UGA  ID_001 train
## 3                0        2016_UGA  ID_001 train
## 4                1        2016_UGA  ID_001 train
## 5                0        2016_UGA  ID_001 train
## 6                0        2016_UGA  ID_001 train

The public HAI data spans many influenza vaccination studies. The number of distinct participants per study is shown below.

participants_per_study <- both %>%
  distinct(set, study_accession, participant_id) %>%
  count(set, study_accession, name = "n_participants")

p1 <- ggplot(participants_per_study,
             aes(x = reorder(study_accession, n_participants), y = n_participants,
                 fill = set)) +
  geom_col(position = position_dodge(width = 0.7), width = 0.6) +
  coord_flip() +
  scale_fill_manual(values = c(train = "#2C7FB8", challenge = "#E34A33")) +
  labs(title = "Participants per study (HAI)",
       x = "Study accession", y = "# distinct participants", fill = NULL)

p1

Below is the distribution of HAI titers in the training data at the Pre-vacc and Day 28 timepoints for the vaccine strains (virus_in_vaccine == 1). The 15 strains with the largest number of measurements are shown, ordered by their median titer at both timepoints. Models of HAI titers typically use log-scaled values, as shown in the plot.”

vaccine_strains_top <- train %>%
  filter(virus_in_vaccine == 1) %>%
  count(virus_strain, sort = TRUE) %>%
  slice_head(n = 15) %>%
  pull(virus_strain)

train_vac <- train %>%
  filter(virus_in_vaccine == 1, virus_strain %in% vaccine_strains_top) %>%
  mutate(timepoint = factor(timepoint, levels = c("Pre-vacc", "28"),
                            labels = c("Pre-vacc", "Day 28")))

strain_order <- train_vac %>%
  group_by(virus_strain) %>%
  summarise(med = median(value), .groups = "drop") %>%
  arrange(desc(med)) %>%
  pull(virus_strain)

train_plot <- train_vac %>% mutate(virus_strain = factor(virus_strain, levels = strain_order))

p4 <- ggplot(train_plot, aes(x = virus_strain, y = value, fill = timepoint)) +
  geom_boxplot(outlier.size = 0.5, outlier.alpha = 0.3, alpha = 0.7,
               position = position_dodge(width = 0.75)) +
  scale_y_log10(labels = label_number()) +
  scale_fill_manual(values = c("Pre-vacc" = "#31A354", "Day 28" = "#756BB1")) +
  coord_flip() +
  labs(title = "HAI titer by vaccine strain (train set)",
       subtitle = "Top 15 vaccine strains by number of measurements",
       x = NULL, y = "HAI titer (log scaling)", fill = NULL) +
  theme(axis.text.y = element_text(size = 9), legend.position = "top")
p4

Additional resources

The investigations.tsv table specifies the vaccine season for each study (where that information is available). Specific details about the strains for each season are available in the vaccine_strains_per_season reference file.

Hemagglutinin sequences for all virus strains with serological data are provided here. The distance between these sequences could help models estimate the difference in HAI titers expected when the vaccine strain changes. Sequences were either provided by the study authors or retrieved from public databases.

Prediction Task

Three serology tasks are based on HAI titers, for the 40 donors in the 2025LJI challenge dataset. Each task asks you to predict the geometric mean of HAI titers across a defined set of strains at a given timepoint.

  • Task 2.1 HAI magnitude (Day 28). Predict the Day-28 geometric-mean HAI titer across the three 2025-26 vaccine strains: H1N1 A/Victoria/4897/2022, H3N2 A/District Of Columbia/27/2023_MDCK, and Vic B/Austria/1359417/2021.
  • Task 2.2 HAI breadth (Day 28). Predict the Day-28 geometric-mean HAI titer across all 12 challenge strains (the three vaccine strains above plus H1N1 A/Brisbane/2/2018, H1N1 A/California/7/2009, H1N1 A/Victoria/2570/2019, H3N2 A/Darwin/9/2021, H3N2 A/Massachusetts/18/2022_MDCK, H3N2 A/Pennsylvania/525/2025, H3N2 A/Tasmania/503/2020, Vic B/Colorado/6/2017, and Vic B/Washington/2/2019).
  • Task 2.3 HAI durability (Day 365). As Task 2.2, but predict the geometric-mean HAI titer across all 12 challenge strains at Day 365.

Training coverage of the challenge strains. Not all strains are present in the public training data. For each strain whose HAI must be predicted in Tasks 2.1-2.3, the table below counts the number of distinct training participants with HAI titers measured against this virus at Day 28 and Day 365. Rows highlighted in red indicate that no training data are available for that strain at the corresponding timepoint. Models must predict the titers of these strains using information from antigenically related viruses.

Challenge-strain training coverage (HAI)
Distinct training participants per strain and timepoint
Virus strain In 2025-26 vaccine Studies Train participants (D28) Train participants (D365)
H1N1 A/Victoria/4897/2022 yes 2 238 0
H3N2 A/District Of Columbia/27/2023_MDCK yes 0 0 0
Vic B/Austria/1359417/2021 yes 5 903 439
H1N1 A/Brisbane/2/2018 6 1233 668
H1N1 A/California/7/2009 32 2628 906
H1N1 A/Victoria/2570/2019 5 903 439
H3N2 A/Darwin/9/2021 5 903 439
H3N2 A/Massachusetts/18/2022_MDCK 0 0 0
H3N2 A/Pennsylvania/525/2025 0 0 0
H3N2 A/Tasmania/503/2020 5 903 439
Vic B/Colorado/6/2017 10 1673 880
Vic B/Washington/2/2019 6 1233 668

The distribution of vaccine-strain HAI titers across the relevant timepoints in the training data is shown below (Day 28 is the magnitude/breadth window; Day 365 the durability window). The challenge cohort includes pre-vaccination measurements only.

sero_pub <- read.delim("../datasets/260512/train/publicData_serology.tsv",
                       stringsAsFactors = FALSE)

tp <- c("Pre-vacc", "28", "365")
vac <- sero_pub %>%
  filter(assay == "hai", virus_in_vaccine == 1, timepoint %in% tp, value > 0) %>%
  mutate(timepoint = factor(timepoint, levels = tp,
                            labels = c("Pre-vacc", "Day 28", "Day 365")))

# one value per donor/timepoint: geometric mean of vaccine-strain HAI (the task metric)
donor_geo <- vac %>%
  group_by(participant_id, timepoint) %>%
  summarise(geo = exp(mean(log(value))), .groups = "drop") %>%
  group_by(participant_id) %>%
  filter(n_distinct(timepoint) >= 2) %>%
  ungroup()

p5c <- ggplot(donor_geo, aes(x = timepoint, y = geo, group = participant_id)) +
  geom_line(alpha = 0.2) +
  geom_point(alpha = 0.3, size = 0.9) +
  stat_summary(aes(group = 1), fun = median, geom = "line",
               color = "red", linewidth = 1.1) +
  scale_y_log10(labels = label_number()) +
  labs(title = "Per-donor vaccine-strain HAI trajectory (train set)",
       subtitle = "Grey = individual donors (geometric mean across vaccine strains); red = median.",
       x = "Timepoint", y = "Geometric-mean HAI titer (log scaling)") +
  theme(legend.position = "none")

p5c