The plasma cytokine assay 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

Plasma samples were analyzed in duplicate using two 12-plex Legendplex cytokine bead panels on a Cytek Aurora analyzer. Following automated bead gating and manual quality control – which led to the exclusion of one outlier replicate plate – mean fluorescence intensities were converted to concentrations using a 5-PL standard curve, with values below a cytokine- and experiment-specific limit of detection (LODHigh) thresholded to that limit.

Data Standardization

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

  • Analyte names were standardized according to a cytokine name map.
  • Technical replicates were collapsed to the mean.
  • Baseline (“pre-vaccination”) values were established by averaging all values at pre-vaccination timepoints.

The Data

Data tables

The cytokine data consists of 2 tables : publicData_cytokine.tsv contains both pre- and post-vaccination measurements. Train your model on this data, then apply it to 2025LJI_cytokine.tsv (which contains pre-vaccination data only) to predict the unseen post-vaccination values. See the Task description below for details.

Cytokine 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.
analyte Cytokine or chemokine measured.
value Measured concentration.
unit Unit of measurement (pg/ml).
measurementTechnique Assay platform: Luminex xMAP or Legendplex (challenge).
material Sample type: Plasma, Serum, or Protein.
study_accession Study identifier. Links to the investigations.tsv file.
subject subject ID.

Data exploration

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

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

train = train %>% filter(!timepoint %in% c(-14,0))
challenge = challenge %>% filter(!timepoint %in% c(-14,0))

#table(train$timepoint)
#table(challenge$timepoint)

tp_levels_train    <- c("Pre-vacc", "1")
tp_levels_challenge<- c("Pre-vacc", "1")

train$timepoint    <- factor(train$timepoint,    levels = tp_levels_train)
challenge$timepoint<- factor(challenge$timepoint, levels = tp_levels_challenge)

both <- bind_rows(train, challenge)

Data preview :

head(both)
##     participant_id timepoint analyte  value  unit measurementTechnique material
## 1 SDY180.SUB119292         1     EGF   2.50 pg/ml         Luminex xMAP  Protein
## 2 SDY180.SUB119292         1 EOTAXIN  23.67 pg/ml         Luminex xMAP  Protein
## 3 SDY180.SUB119292         1    FGFB   7.50 pg/ml         Luminex xMAP  Protein
## 4 SDY180.SUB119292         1   GMCSF  12.85 pg/ml         Luminex xMAP  Protein
## 5 SDY180.SUB119292         1     GRO 625.19 pg/ml         Luminex xMAP  Protein
## 6 SDY180.SUB119292         1     IL8   3.55 pg/ml         Luminex xMAP  Protein
##   study_accession   subject   set
## 1          SDY180 SUB119292 train
## 2          SDY180 SUB119292 train
## 3          SDY180 SUB119292 train
## 4          SDY180 SUB119292 train
## 5          SDY180 SUB119292 train
## 6          SDY180 SUB119292 train

The training data consists of 4 studies which measured cytokines in response to influenza vaccination. These studies have a mean of 32 participants per study.

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) +
  geom_text(aes(label = n_participants),
            position = position_dodge(width = 0.7), hjust = -0.2, size = 3.5) +
  coord_flip() +
  scale_fill_manual(values = c(train = "#2C7FB8", challenge = "#E34A33")) +
  labs(title = "Participants per study",
       x = "Study accession", y = "# distinct participants", fill = NULL) +
  expand_limits(y = max(participants_per_study$n_participants) * 1.15)

p1

Below is the distribution of cytokine measures found in the training data at Days 0 (pre-vaccination) and 1.

shared_analytes <- intersect(unique(train$analyte), unique(challenge$analyte))

# Restrict to the two timepoints of interest: Pre-vacc and Day 1
train_shared <- train %>%
  filter(analyte %in% shared_analytes, timepoint %in% c("Pre-vacc", "1")) %>%
  mutate(timepoint = factor(timepoint, levels = c("Pre-vacc", "1"),
                             labels = c("Pre-vacc", "Day 1")))

# Order analytes by their overall median (across both timepoints combined)
train_order <- train_shared %>%
  group_by(analyte) %>%
  summarise(med = median(value), .groups = "drop") %>%
  arrange(desc(med)) %>%
  pull(analyte)

train_plot <- train_shared %>% mutate(analyte = factor(analyte, levels = train_order))

p4 <- ggplot(train_plot, aes(x = analyte, 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 1" = "#756BB1")) +
  coord_flip() +
  labs(title = "Distribution of each cytokine (train set)",
       subtitle = paste0("Cytokine measures shared across challenge and train data"),
       x = NULL, y = "pg/ml (log10)", fill = NULL) +
  theme(axis.text.y = element_text(size = 9), legend.position = "top")
p4

Prediction Task

Task 1.1 asks contestants to predict the fold change of IP10 (CXCL10) from Pre-vacc to Day 1, for the 40 donors found in the 2025LJI challenge dataset.

The distribution of IP10 in the different studies is shown below.

ip10 <- both %>% filter(analyte == "IP10")

p5b <- ggplot(ip10, aes(x = timepoint, y = value, fill = set)) +
  geom_boxplot(alpha = 0.7) +
  scale_y_log10(labels = label_number()) +
  facet_wrap(~set, scales = "free_x") +
  scale_fill_manual(values = c(train = "#2C7FB8", challenge = "#E34A33")) +
  labs(title = "IP10 by timepoint", x = "Timepoint", y = "IP10 (pg/ml, log10 scale)",
       fill = NULL) +
  theme(legend.position = "none")

p5b