Data updates: See the change log.

2026-10-20: timpeoint post-vaccination timepoints added to 2025LJI.

The ex vivo flow cytometry assay (which measures the frequency of immune cell populations in the blood) 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

We performed spectral cytometry on PBMCs, staining for surface antibodies listed in Table 3. Gating strategy for all immune cell populations is available in Figure 3. For a given cell population, frequencies represent the proportion of cells within the indicated parent population. Samples with either less than 50% live PBMC at thawing or less than 30,000 live singlet cells recorded were excluded from the dataset.

Data processing and standardization

  • Cell population names were standardized according to the name_map .
  • Technical replicates were collapsed by taking the mean.
  • Baseline (“pre-vaccination”) values were established by averaging all values at pre-vaccination timepoints.
  • Publicly-available data were collated from multiple studies; note that these use a mix of reporting units (percentages, absolute counts, counts/µl).

The Data

Change log

2026-10-20 Post-vaccination timepoints added for Part 2 of the challenge

Data tables

The ex vivo flow data consists of 2 tables : publicData_ex_vivo_flow.tsv and 2025LJI_ex_vivo_flow.tsv contain both pre- and post-vaccination measurements collated from public studies and for the “prediction/challenge” cohort respectively.

Ex vivo flow cytometry 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, or 'Pre-vacc' for the mean of the pre-vaccination timepoints.
name Immune cell population measured (e.g. Classical_monocytes, Antibody-secreting_cells_(ASC), CD4_T_cells).
value Frequency of the population.
unit Unit of measurement. 'percentage' throughout the 2025LJI cohort; public data additionally include absolute counts (cells, cells/ul) and other units.
parent_population Denominator population for value (e.g. CD45plus_cells, CD4_T_cells, B_cells).
population_definition Gating strategy defining the population (e.g. CD3-CD19-CD56-HLADR+CD14+CD16- for classical monocytes).
material Sample type: PBMCs (2025LJI); public data also include Whole blood, Serum, and Other.
comments For 2025LJI, the batch/round in which the sample was acquired.
study_accession Study identifier. Links to the investigations.tsv file.
subject subject ID.

Data exploration

Data preview :

library(tidyverse)
flow = read_tsv('../datasets/260512/train_part2//2025LJI_ex_vivo_flow.tsv')
head(flow)
## # A tibble: 6 × 11
##   participant_id  timepoint name         value unit       parent_population
##   <chr>           <chr>     <chr>        <dbl> <chr>      <chr>            
## 1 2025LJI.SUB7666 Pre-vacc  Neutrophils 0.0006 percentage CD45plus_cells   
## 2 2025LJI.SUB7666 -14       Neutrophils 0.0013 percentage CD45plus_cells   
## 3 2025LJI.SUB7742 0         Neutrophils 0.0016 percentage CD45plus_cells   
## 4 2025LJI.SUB5670 -14       Neutrophils 0.0019 percentage CD45plus_cells   
## 5 2025LJI.SUB5670 Pre-vacc  Neutrophils 0.0023 percentage CD45plus_cells   
## 6 2025LJI.SUB7742 Pre-vacc  Neutrophils 0.0023 percentage CD45plus_cells   
## # ℹ 5 more variables: population_definition <chr>, material <chr>,
## #   comments <chr>, study_accession <chr>, subject <chr>
table(flow$timepoint)
## 
##      -14        0        1        7 Pre-vacc 
##     1478     1477     1479     1480     1478

The 2025LJI challenge cohort is measured uniformly as a percentage of parent. The plot below shows the landscape of all populations at baseline (Pre-vacc), ordered by median frequency.

lji <- read.delim("../datasets/260512/train_part2//2025LJI_ex_vivo_flow.tsv",
                  stringsAsFactors = FALSE)

base <- lji %>%
  filter(timepoint == "Pre-vacc", value > 0)

ord <- base %>%
  group_by(name) %>%
  summarise(med = median(value), .groups = "drop") %>%
  arrange(med) %>%
  pull(name)

base <- base %>% mutate(name = factor(name, levels = ord))

p1 <- ggplot(base, aes(name, value)) +
  geom_boxplot(outlier.shape = NA, alpha = 0.55, width = 0.6, fill = "#1b7837") +
  geom_jitter(width = 0.15, size = 0.7, alpha = 0.4) +
  scale_y_log10(labels = label_number()) +
  coord_flip() +
  labs(title = "Immune cell populations at baseline (2025LJI, Pre-vacc)",
       x = NULL, y = "% of parent population (log10)") +
  theme_bw(base_size = 11)

p1

Timpeoints:

library(patchwork)

pub <- read.delim("../datasets/260512/train_part2/publicData_ex_vivo_flow.tsv",
                  stringsAsFactors = FALSE)

table(pub$timepoint)
## 
##      -21       -5       -7        0        1        7 Pre-vacc 
##       92      112      677     6901     1148     5130     6911
# Kinetics of 2 cell populations from the public data

paired_traj <- function(pop, tps, use_unit) {
  d <- pub %>%
    filter(name == pop, unit == use_unit, timepoint %in% tps, value > 0) %>%
    mutate(timepoint = factor(timepoint, levels = c("Pre-vacc", "1", "7"),
                              labels = c("Pre-vacc", "Day 1", "Day 7"))) %>%
    droplevels() %>%
    # collapse technical replicates to one mean value per participant/timepoint
    group_by(participant_id, timepoint) %>%
    summarise(value = mean(value), .groups = "drop") %>%
    # keep only participants measured at both timepoints, so every point is
    # part of a connecting line (drops single-timepoint participants)
    group_by(participant_id) %>%
    filter(n_distinct(timepoint) == 2) %>%
    ungroup()

  # One line per participant (participant_id = studyID.subjectID, unique across
  # public studies); red line is the median.
  ggplot(d, aes(timepoint, value, group = participant_id)) +
    geom_line(alpha = 0.3) +
    geom_point(alpha = 0.4, 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 = pop, x = NULL,
         y = paste0(use_unit, " (log10)")) +
    theme_bw(base_size = 12)
}

p_mono <- paired_traj("Classical_monocytes",
                      c("Pre-vacc", "1"), use_unit = "cells/ul")
p_asc  <- paired_traj("Antibody-secreting_cells_(ASC)",
                      c("Pre-vacc", "7"), use_unit = "percentage")

p3 <- p_mono + p_asc +
  plot_annotation(
    title = "Post-vaccination kinetics (public data)"
    )

p3