Skip to content

Conditional Logic

Turning numbers into labels is a constant task: bin an expression value into low, medium, or high; flag a gene as housekeeping or a target; classify a result as up, down, or unchanged. This is multi-branch conditional logic. dplyr writes it as case_when, Polars as chained when().then() clauses ending in otherwise. Both test conditions top to bottom and take the first that matches.

Every code block runs in a container in the companion repository, and the two languages produce the same labels and the same group sizes.

Classify each measurement by its count: below 300 is low, below 1000 is medium, the rest high. The order matters, since the first matching branch wins.

expr <- expr |>
mutate(level = case_when(
counts < 300 ~ "low",
counts < 1000 ~ "medium",
TRUE ~ "high"
))
count(expr, level)
sample_id condition gene counts level
1 S01 control TP53 393 medium
2 S01 control EGFR 294 low
3 S01 control MYC 247 low
4 S01 control BRCA1 195 low
5 S01 control GAPDH 3939 high
# count(expr, level)
level n
1 high 16
2 low 15
3 medium 17

The TRUE ~ branch in dplyr and .otherwise(...) in Polars are the catch-all: anything not matched above falls through to it. Forgetting the catch-all leaves unmatched rows as missing, a common source of stray NAs.

The condition can be any test, including “is this value in a set”. Here, mark the two housekeeping genes and call everything else a target.

expr <- expr |>
mutate(role = case_when(
gene %in% c("GAPDH", "ACTB") ~ "housekeeping",
TRUE ~ "target"
))
count(expr, role)
role n
1 housekeeping 16
2 target 32

Sixteen housekeeping rows, two genes across eight samples, and thirty-two targets. Both languages agree. Next, Missing Values handles the gaps these tests silently create.