Logical Operations and Conditionals

Use logical operations and conditionals to express rules, flags, and QC logic in PMx workflows.
Tip

Big idea: Most PMx decisions are expressed as logical rules applied to entire vectors — not one value at a time.

Learning Objectives

By the end of this lesson, you will be able to:

  • Use comparison operators (==, !=, <, >, <=, >=).
  • Combine logical conditions with & and |.
  • Use %in% for multi-value checks.
  • Understand how NA behaves in logical expressions.
  • Use ifelse() for vectorized conditional logic.

Setup

library(tidyverse)

Logical Vectors

A logical vector contains TRUE, FALSE, or NA.

dv <- c(1.2, 3.5, 2.8)
dv > 2
[1] FALSE  TRUE  TRUE

Logical vectors are the backbone of:

  • filtering
  • QC flags
  • conditional transformations

Comparison Operators

dv == 2.8
[1] FALSE FALSE  TRUE
dv != 2.8
[1]  TRUE  TRUE FALSE
dv >= 2
[1] FALSE  TRUE  TRUE
dv < 3
[1]  TRUE FALSE  TRUE

These comparisons are vectorized.


Combining Conditions

AND (&)

dv > 2 & dv < 3
[1] FALSE FALSE  TRUE

OR (|)

dv < 2 | dv > 3
[1]  TRUE  TRUE FALSE
Note

Use & and | for vectors.
Avoid && and || (they only check the first value).


%in% for Membership Checks

ids <- c(101, 102, 103, 104)

ids %in% c(101, 103)
[1]  TRUE FALSE  TRUE FALSE

This is safer and clearer than chaining ==.


PMx Example: Flagging Observations

pk <- tibble(
  ID = c(1, 1, 1, 2, 2, 2),
  TIME = c(0.5, 1, 2, 0.5, 1, 2),
  DV = c(2.1, 3.8, 0.2, 1.6, 2.9, 0.1)
)

pk
# A tibble: 6 × 3
     ID  TIME    DV
  <dbl> <dbl> <dbl>
1     1   0.5   2.1
2     1   1     3.8
3     1   2     0.2
4     2   0.5   1.6
5     2   1     2.9
6     2   2     0.1

Flag low concentrations:

pk$low_conc <- pk$DV < 0.5
pk
# A tibble: 6 × 4
     ID  TIME    DV low_conc
  <dbl> <dbl> <dbl> <lgl>   
1     1   0.5   2.1 FALSE   
2     1   1     3.8 FALSE   
3     1   2     0.2 TRUE    
4     2   0.5   1.6 FALSE   
5     2   1     2.9 FALSE   
6     2   2     0.1 TRUE    
Note

In a data frame or tibble, pk$DV means:

“Take the DV column from pk and return it as a vector.”

The result is not a data frame — it is a single vector.


Missing Values in Logic (NA)

dv_na <- c(1.2, NA, 2.8)

dv_na > 2
[1] FALSE    NA  TRUE

NA propagates.

Safer logic:

!is.na(dv_na) & dv_na > 2
[1] FALSE FALSE  TRUE

Vectorized Conditionals with ifelse()

pk$BLQ <- ifelse(pk$DV < 0.5, TRUE, FALSE)
pk
# A tibble: 6 × 5
     ID  TIME    DV low_conc BLQ  
  <dbl> <dbl> <dbl> <lgl>    <lgl>
1     1   0.5   2.1 FALSE    FALSE
2     1   1     3.8 FALSE    FALSE
3     1   2     0.2 TRUE     TRUE 
4     2   0.5   1.6 FALSE    FALSE
5     2   1     2.9 FALSE    FALSE
6     2   2     0.1 TRUE     TRUE 

Better (handles NA explicitly):

pk$BLQ <- ifelse(!is.na(pk$DV) & pk$DV < 0.5, TRUE, FALSE)

Nested Logic (Keep It Simple)

Avoid deep nesting early on.

pk$flag <- ifelse(
  pk$DV < 0.5, "BLQ",
  ifelse(pk$DV > 3, "HIGH", "OK")
)

pk
# A tibble: 6 × 6
     ID  TIME    DV low_conc BLQ   flag 
  <dbl> <dbl> <dbl> <lgl>    <lgl> <chr>
1     1   0.5   2.1 FALSE    FALSE OK   
2     1   1     3.8 FALSE    FALSE HIGH 
3     1   2     0.2 TRUE     TRUE  BLQ  
4     2   0.5   1.6 FALSE    FALSE OK   
5     2   1     2.9 FALSE    FALSE OK   
6     2   2     0.1 TRUE     TRUE  BLQ  
Warning

If logic becomes hard to read, split it into steps or write a function.


PMx Rule Example: Dose vs Observation

ev <- tibble(
  EVID = c(1, 0, 0, 1, 0),
  AMT = c(100, NA, NA, 80, NA),
  DV = c(NA, 2.1, 3.8, NA, 1.6)
)

ev$logic_ok <- (ev$EVID == 1 & !is.na(ev$AMT) & is.na(ev$DV)) |
               (ev$EVID == 0 & is.na(ev$AMT) & !is.na(ev$DV))

ev
# A tibble: 5 × 4
   EVID   AMT    DV logic_ok
  <dbl> <dbl> <dbl> <lgl>   
1     1   100  NA   TRUE    
2     0    NA   2.1 TRUE    
3     0    NA   3.8 TRUE    
4     1    80  NA   TRUE    
5     0    NA   1.6 TRUE    

This kind of rule shows up everywhere in PMx QC.


Strategies

  • Think in terms of vectors, not single values.
  • Handle NA explicitly.
  • Use %in% for membership checks.
  • Keep logic readable; split into steps when needed.
  • Turn repeated logic into a function.

Practice Problems

  1. Create a logical vector identifying DV > 2.
  2. Combine conditions to identify DV > 2 and TIME <= 1.
  3. Use %in% to flag IDs in a given set.
  4. Create a BLQ flag using ifelse().
  5. Write one PMx-style logical rule and explain it.

pk$DV > 2
[1]  TRUE  TRUE FALSE FALSE  TRUE FALSE
pk$DV > 2 & pk$TIME <= 1
[1]  TRUE  TRUE FALSE FALSE  TRUE FALSE
pk$ID %in% c(1, 2)
[1] TRUE TRUE TRUE TRUE TRUE TRUE
pk$BLQ <- ifelse(!is.na(pk$DV) & pk$DV < 0.5, TRUE, FALSE)

ev$logic_ok
[1] TRUE TRUE TRUE TRUE TRUE

Summary

You now know how to:

  • express PMx rules as logical vectors
  • combine conditions safely
  • handle missing values explicitly
  • use ifelse() for vectorized conditionals

These skills underpin QC checks, data wrangling, and modeling logic.


  • Use & and | for vectors.
  • Always think about NA.
  • Vectorized logic beats loops in most PMx cases.
  • If logic feels messy, refactor it.