Course contents

Part IChapter 4R foundations and data

Vectors, Missing Values, and Conditions

On this page
  1. 1. A vector is a column-shaped collection
  2. 2. Build vectors in other ways
  3. 3. Find a particular element
  4. 4. Vectorized calculations
  5. 5. Find, sort, and test values
  6. 6. Missing means “not known”
  7. 7. Text vectors
  8. 8. Types, categories, and conversion
  9. 9. Common vector traps
  10. Try it
  11. Checkpoint

1. A vector is a column-shaped collection

Imagine measuring five patients’ pulse rates. Five separate named objects are inconvenient. The function c() (“combine”) places values together.

pulse <- c(70, 82, 90, 68, 77)
pulse

Output

[1] 70 82 90 68 77
length(pulse)

Output

[1] 5
mean(pulse)

Output

[1] 77.4
median(pulse)

Output

[1] 77
min(pulse)

Output

[1] 68
max(pulse)

Output

[1] 90

The vector has 5 values. The mean is 77.4, the median is 77, and the smallest and largest readings are 68 and 90.

All values in an ordinary atomic vector share a basic type. Combining numbers and text usually converts the numbers into text. That is why we use tables with differently typed columns in the next chapter.

2. Build vectors in other ways

c() is fine for a few typed values, but there are shortcuts for patterns. : makes a run of whole numbers, seq() makes a sequence with a step (by) or a chosen number of points (length.out), and rep() repeats values. c() also joins vectors you already have.

1:5

Output

[1] 1 2 3 4 5
10:6

Output

[1] 10  9  8  7  6
seq(0, 100, by = 25)

Output

[1]   0  25  50  75 100
seq(0, 1, length.out = 5)

Output

[1] 0.00 0.25 0.50 0.75 1.00
rep("North", times = 3)

Output

[1] "North" "North" "North"
rep(c("North", "South"), times = 2)

Output

[1] "North" "South" "North" "South"
rep(c("North", "South"), each = 2)

Output

[1] "North" "North" "South" "South"
more_pulse <- c(pulse, 85, 64)    # join an old vector and new values
more_pulse

Output

[1] 70 82 90 68 77 85 64

times = 2 repeats the whole vector (North, South, North, South), while each = 2 repeats each element in turn (North, North, South, South). Use rep() to build a column of labels, and seq() to make axis breaks or a grid of ages.

3. Find a particular element

R counts positions beginning at 1, not 0. Square brackets select positions.

pulse[1]             # First pulse

Output

[1] 70
pulse[3]             # Third pulse

Output

[1] 90
pulse[c(1, 5)]       # First and fifth

Output

[1] 70 77
pulse[-2]            # All except the second

Output

[1] 70 90 68 77

These print 70, then 90, then 70 and 77, then the four values 70, 90, 68, 77 with the second one (82) left out. A negative position means “leave this one out”.

Brackets mean take part of something. Parentheses after a function name mean run the function.

4. Vectorized calculations

Most R calculations operate on every value in the vector.

pulse + 5

Output

[1] 75 87 95 73 82
pulse >= 80

Output

[1] FALSE  TRUE  TRUE FALSE FALSE
pulse[pulse >= 80]     # Only measurements >= 80

Output

[1] 82 90
sum(pulse >= 80)       # TRUE counts as one; FALSE as zero

Output

[1] 2

pulse + 5 adds 5 to each reading (75, 87, 95, 73, 82). pulse >= 80 gives FALSE TRUE TRUE FALSE FALSE. Putting that logical vector inside the brackets keeps 82 and 90, and sum() of the same logical vector is 2.

The final line counts how many readings meet a condition. It does not count unique patients unless each element belongs to a different patient.

Two vectors can also be combined element by element. When one is shorter, R recycles it, starting over from its first value. That is intended when the shorter vector has length 1 (as in pulse + 5). When the lengths do not divide evenly, R still gives a result but prints a warning, which usually signals a mistake.

pulse - c(60, 70, 80, 60, 70)       # same length: matched position by position

Output

[1] 10 12 10  8  7
c(10, 20, 30, 40) + c(1, 2)         # recycled: 1, 2, 1, 2

Output

[1] 11 22 31 42
c(10, 20, 30, 40) + c(1, 2, 3)      # lengths do not divide: warning

Output

Warning in c(10, 20, 30, 40) + c(1, 2, 3): longer object length is not a
multiple of shorter object length

[1] 11 22 33 41

The second line returns 11, 22, 31, 42. The third still returns four numbers, but R warns that the longer length is not a multiple of the shorter one. Treat that warning as a bug to fix, not noise.

Two more summaries describe spread: var() is the variance (the square of the standard deviation, sd()) and range() returns the smallest and largest values.

var(pulse)

Output

[1] 80.8
sd(pulse)

Output

[1] 8.988882
range(pulse)

Output

[1] 68 90
A vector of five made-up pulse readings at positions 1 to 5. pulse[2] selects 82, and pulse[pulse >= 80] keeps the readings 82 and 90.
A vector of five made-up pulse readings at positions 1 to 5. pulse[2] selects 82, and pulse[pulse >= 80] keeps the readings 82 and 90.

pulse[2] picks the second reading, 82, and pulse[pulse >= 80] keeps the readings 82 and 90.

5. Find, sort, and test values

which() returns the positions where a condition is TRUE; which.max() and which.min() return the position of the largest and smallest value. Use them when you need to know which patient, not just the value.

which(pulse >= 80)

Output

[1] 2 3
which.max(pulse)

Output

[1] 3
which.min(pulse)

Output

[1] 4

The readings at or above 80 are in positions 2 and 3, the highest is in position 3, and the lowest is in position 4.

Three functions deal with order, and beginners often mix them up:

  • sort() returns the values in order (decreasing = TRUE for largest first).
  • order() returns the positions that would put the values in order. It is what you use to reorder a table later.
  • rank() returns each value’s rank: 1 for the smallest.
sort(pulse)

Output

[1] 68 70 77 82 90
sort(pulse, decreasing = TRUE)

Output

[1] 90 82 77 70 68
order(pulse)

Output

[1] 4 1 5 2 3
rank(pulse)

Output

[1] 2 4 5 1 3
pulse[order(pulse)]      # same as sort(pulse)

Output

[1] 68 70 77 82 90

order(pulse) starts with 4 because the smallest pulse is the fourth value. rank(pulse) starts with 2 because the first reading, 70, is the second smallest. Tied values get the average of their ranks.

For yes/no questions about a whole vector, any() asks whether at least one value is TRUE and all() asks whether every value is. ifelse() chooses between two results for each element; the tidyverse version, if_else(), appears in chapter 6.

any(pulse > 100)

Output

[1] FALSE
all(pulse > 60)

Output

[1] TRUE
ifelse(pulse >= 80, "high", "ok")

Output

[1] "ok"   "high" "high" "ok"   "ok"  

No reading exceeds 100, so any() is FALSE; all are above 60, so all() is TRUE; and ifelse() returns one label per reading.

6. Missing means “not known”

In an EHR, a blank field may mean not measured, unavailable, or entered elsewhere. NA is R’s marker for missing; it is not zero.

bmi <- c(24.5, NA, 28.1, 31.0, NA)
is.na(bmi)

Output

[1] FALSE  TRUE FALSE FALSE  TRUE
sum(is.na(bmi))

Output

[1] 2
mean(bmi)                  # Returns NA

Output

[1] NA
mean(bmi, na.rm = TRUE)    # Average of observed values only

Output

[1] 27.86667

The option na.rm = TRUE removes missing observations from this calculation; it does not fill missing records in the dataset. Deleting patients with missing information can bias an analysis. Later, we learn to impute using training data only.

na.omit() returns the vector without its missing values (it also attaches a note of which positions were dropped). is.nan() checks for NaN, the “not a number” result of calculations like 0 / 0; it is FALSE for an ordinary NA.

na.omit(bmi)

Output

[1] 24.5 28.1 31.0
attr(,"na.action")
[1] 2 5
attr(,"class")
[1] "omit"
is.nan(c(1, NA, 0 / 0))

Output

[1] FALSE FALSE  TRUE

The kept values are 24.5, 28.1, and 31, followed by the attribute note. is.nan() flags only the third value.

7. Text vectors

Names, IDs, and clinic sites are text, and a few functions cover most cleaning work. Here are some made-up patient IDs and site names.

ids <- c("S0001", "S0012", "N0045", "S0100")
sites <- c("North clinic", "south clinic", "Mobile unit")

paste("Patient", ids)          # join with a space

Output

[1] "Patient S0001" "Patient S0012" "Patient N0045" "Patient S0100"
paste0(ids, "_v1")             # join with no separator

Output

[1] "S0001_v1" "S0012_v1" "N0045_v1" "S0100_v1"
nchar(ids)                     # number of characters

Output

[1] 5 5 5 5
toupper(sites)

Output

[1] "NORTH CLINIC" "SOUTH CLINIC" "MOBILE UNIT" 
tolower(sites)

Output

[1] "north clinic" "south clinic" "mobile unit" 
substr(ids, 2, 5)              # characters 2 to 5: the number part

Output

[1] "0001" "0012" "0045" "0100"

paste0() is paste() with no separator. substr() keeps the positions you ask for, so substr(ids, 2, 5) drops the leading letter.

strsplit() cuts text at a separator and returns a list, with one element per input string. sub() replaces the first match in each string and gsub() replaces every match.

strsplit("North;Mobile;South", split = ";")

Output

[[1]]
[1] "North"  "Mobile" "South" 
sub("clinic", "site", sites)

Output

[1] "North site"  "south site"  "Mobile unit"
gsub("0", "", ids)             # remove every zero

Output

[1] "S1"  "S12" "N45" "S1" 

gsub("0", "", ids) removes every zero, so "S0100" becomes "S1", while sub() would remove only the first one.

grepl() asks whether each string matches a pattern and returns TRUE/FALSE; grep() returns the positions (or the matching strings, with value = TRUE). A leading ^ means “at the start”.

grepl("^S", ids)                    # IDs that start with S

Output

[1]  TRUE  TRUE FALSE  TRUE
ids[grepl("^S", ids)]

Output

[1] "S0001" "S0012" "S0100"
grep("clinic", sites)

Output

[1] 1 2
grep("clinic", sites, value = TRUE)

Output

[1] "North clinic" "south clinic"

Matching is case-sensitive, so a search for "South" would miss the lower-case "south clinic". Use tolower() first when capitalization is inconsistent. The stringr package offers friendlier versions of these functions with names such as str_detect().

8. Types, categories, and conversion

clinic_site <- c("North", "Mobile", "North", "South")
unique(clinic_site)

Output

[1] "North"  "Mobile" "South" 
table(clinic_site)

Output

clinic_site
Mobile  North  South 
     1      2      1 
sex <- factor(c("F", "M", "F", "F"), levels = c("F", "M"))
levels(sex)

Output

[1] "F" "M"

unique() lists the sites once each (North, Mobile, South), table() counts them (Mobile 1, North 2, South 1), and levels(sex) prints "F" and "M".

A factor means a categorical variable whose allowable values are represented as levels. A factor is useful when a model needs to treat a code as a category rather than quantity.

Some categories have a natural order, such as severity. Add ordered = TRUE and list the levels from lowest to highest, and R can compare them. For an unordered factor, relevel() chooses which level comes first, which matters later because models treat the first level as the reference group.

severity <- factor(c("mild", "severe", "moderate", "mild"),
                   levels = c("mild", "moderate", "severe"),
                   ordered = TRUE)
severity

Output

[1] mild     severe   moderate mild    
Levels: mild < moderate < severe
severity < "severe"

Output

[1]  TRUE FALSE  TRUE  TRUE
site <- factor(c("North", "Mobile", "South", "North"))
levels(site)

Output

[1] "Mobile" "North"  "South" 
levels(relevel(site, ref = "South"))

Output

[1] "South"  "Mobile" "North" 

The printout shows mild < moderate < severe, and severity < "severe" is TRUE for every value except the severe one. Without relevel(), the levels are in alphabetical order; with it, "South" comes first.

Converting between types uses as.numeric(), as.integer(), and as.character(). When text cannot be read as a number, R returns NA and warns.

as.numeric(c("12", "7.5", "high"))

Output

Warning: NAs introduced by coercion

[1] 12.0  7.5   NA
as.integer(7.9)                # drops the decimals

Output

[1] 7
as.character(c(10, 20))

Output

[1] "10" "20"

"high" becomes NA with the warning “NAs introduced by coercion”, and as.integer(7.9) is 7, not 8. When you see that warning in real data, look for entries such as "unknown" or ">200" typed into a numeric column.

9. Common vector traps

  • mean("high") returns NA with a warning: the data are text rather than numeric.
  • NA == NA is NA, not TRUE: use is.na() to check missingness.
  • A numeric code is not automatically a meaningful number; site 1 + site 2 is not site 3.
  • c(1, "two") becomes a character vector; inspect class() when unsure.

Example: run each trap and read what R says.

mean("high")

Output

Warning in mean.default("high"): argument is not numeric or logical: returning
NA

[1] NA
NA == NA

Output

[1] NA
is.na(NA)

Output

[1] TRUE
class(c(1, "two"))

Output

[1] "character"

mean("high") returns NA and warns that the argument is not numeric or logical. NA == NA prints NA, while is.na(NA) prints TRUE, which is the correct test. class(c(1, "two")) is "character", so the 1 became the text "1".

Try it

  1. Make a vector with six systolic readings, one of them NA. Calculate the average while skipping NA.
Show solution to question 1

Answer: The average of the five observed readings is 134.4 mmHg.

Step by step

  1. Build the vector with c(), writing the missing value as NA.
  2. Without na.rm, one NA makes the mean NA. Add na.rm = TRUE to skip it.
sbp <- c(115, 143, NA, 136, 151, 127)
mean(sbp)

Output

[1] NA
mean(sbp, na.rm = TRUE)

Output

[1] 134.4

3. The first line prints NA and the second prints 134.4, the mean of 115, 143, 136, 151 and 127.

  1. Count how many observed readings are at least 140.
Show solution to question 2

Answer: 2 readings (143 and 151).

Step by step

  1. sbp >= 140 returns NA for the missing reading, so first keep only values that are not missing with !is.na(sbp).
  2. Join the two conditions with & and add them up with sum(), since TRUE counts as 1.
sbp <- c(115, 143, NA, 136, 151, 127)
sum(!is.na(sbp) & sbp >= 140)

Output

[1] 2

3. The result is 2. Of the five observed readings, only 143 and 151 reach 140.

  1. Use brackets to print the last two readings.
Show solution to question 3

Answer: The last two readings are 151 and 127.

Step by step

  1. Positions count from 1, and the vector has length(sbp) entries.
  2. Ask for the last two positions with brackets, or use tail().
sbp <- c(115, 143, NA, 136, 151, 127)
sbp[c(length(sbp) - 1, length(sbp))]

Output

[1] 151 127
tail(sbp, 2)

Output

[1] 151 127

3. Both print 151 and 127. Position 6 is the last value, and position 5 is the one before it.

  1. Explain why replacing every missing blood pressure with zero would be a bad idea.
Show solution to question 4

Answer: Missing means “not known”, not zero. A blood pressure of 0 is impossible, and the zero would drag the average down and distort every summary.

Step by step

  1. Replace the NA in the vector with 0 and compare the means.
sbp <- c(115, 143, NA, 136, 151, 127)
sbp_zero <- sbp
sbp_zero[is.na(sbp_zero)] <- 0
mean(sbp_zero)

Output

[1] 112
mean(sbp, na.rm = TRUE)

Output

[1] 134.4

2. The zero-filled mean is 112, while the mean of the observed values is 134.4. The zero pulled it down by over 22 mmHg. 3. The measurement was not taken, so we do not know that patient’s value. Treat it as missing and say so, rather than inventing a number.

  1. Make a vector of ages from 20 to 60 in steps of 10 with seq(). Then use rep() with each to make a vector in which "North" and "South" each appear three times in a row.
Show solution to question 5

Answer: seq(20, 60, by = 10) gives 20, 30, 40, 50, 60, and rep(c("North", "South"), each = 3) gives North three times, then South three times.

Step by step

  1. seq() takes a start, an end and a step by.
  2. rep() with each = 3 repeats each element in turn.
seq(20, 60, by = 10)

Output

[1] 20 30 40 50 60
rep(c("North", "South"), each = 3)

Output

[1] "North" "North" "North" "South" "South" "South"

3. The ages print five values. The labels print six values: three “North”, then three “South”. With times = 3 the whole pair would repeat instead.

  1. Add c(1, 2, 3) to a vector of length 6, then to a vector of length 5. Describe what R does each time.
Show solution to question 6

Answer: With length 6, the shorter vector is reused twice and there is no warning. With length 5 it does not fit evenly, so R still returns a result but warns.

Step by step

  1. Add to the length-6 vector.
c(1, 2, 3, 4, 5, 6) + c(1, 2, 3)

Output

[1] 2 4 6 5 7 9

2. This prints 2, 4, 6, 5, 7, 9: the values 1, 2, 3 are recycled twice. 3. Add to the length-5 vector.

c(1, 2, 3, 4, 5) + c(1, 2, 3)

Output

Warning in c(1, 2, 3, 4, 5) + c(1, 2, 3): longer object length is not a
multiple of shorter object length

[1] 2 4 6 5 7

4. R returns five values (2, 4, 6, 5, 7) and warns that the longer length is not a multiple of the shorter one. Treat that warning as a bug to fix.

  1. For c(120, 135, 128, 150, 118): find the positions of readings above 130, the position of the largest, and the variance and range. Then show the order and the rank of the values and explain how they differ.
Show solution to question 7

Answer: Readings above 130 are in positions 2 and 4, the largest is in position 4, the variance is 168.2, and the range is 118 to 150. order() gives 5, 1, 3, 2, 4 (positions that sort the values) and rank() gives 2, 4, 3, 5, 1 (each value’s rank).

Step by step

  1. Store the vector and use which(), which.max(), var() and range().
v <- c(120, 135, 128, 150, 118)
which(v > 130)

Output

[1] 2 4
which.max(v)

Output

[1] 4
var(v)

Output

[1] 168.2
range(v)

Output

[1] 118 150

2. Positions 2 (135) and 4 (150) exceed 130, and 150 is the largest. The variance is 168.2, and range() returns the smallest and largest values. 3. Now the two ordering functions.

order(v)

Output

[1] 5 1 3 2 4
rank(v)

Output

[1] 2 4 3 5 1

4. order(v) starts with 5 because the smallest value, 118, is in position 5. rank(v) starts with 2 because the first value, 120, is the second smallest. order() answers “which positions, in sorted order?” and rank() answers “what place did each value get?”.

  1. Use any() and all() to test whether any reading is over 140 and whether all are over 100. Label each reading "high" or "normal" at 130 using ifelse().
Show solution to question 8

Answer: any(v > 140) is TRUE (150), all(v > 100) is TRUE, and the labels are normal, high, normal, high, normal.

Step by step

  1. Use the vector v from the previous item.
v <- c(120, 135, 128, 150, 118)
any(v > 140)

Output

[1] TRUE
all(v > 100)

Output

[1] TRUE
ifelse(v >= 130, "high", "normal")

Output

[1] "normal" "high"   "normal" "high"   "normal"

2. Only 150 exceeds 140, which is enough for any() to be TRUE. Every value is above 100, so all() is TRUE. 3. ifelse() returns one label per reading: 135 and 150 meet the 130 cut-off, so they are “high”.

  1. With ids <- c("S0001", "N0045", "S0100"): count the characters, keep the digits with substr(), find which IDs start with "S" using grepl(), and replace "S" with "Site" using sub().
Show solution to question 9

Answer: Every ID has 5 characters; the digit parts are “0001”, “0045”, “0100”; the IDs starting with “S” are the first and third; and sub() gives “Site0001”, “N0045”, “Site0100”.

Step by step

  1. Run the four functions.
ids <- c("S0001", "N0045", "S0100")
nchar(ids)

Output

[1] 5 5 5
substr(ids, 2, 5)

Output

[1] "0001" "0045" "0100"
grepl("^S", ids)

Output

[1]  TRUE FALSE  TRUE
sub("S", "Site", ids)

Output

[1] "Site0001" "N0045"    "Site0100"

2. nchar() counts 5 for each. substr(ids, 2, 5) keeps characters 2 to 5 and drops the first letter. 3. grepl("^S", ids) prints TRUE FALSE TRUE; the ^ means “at the start”. sub() changes only the matching IDs, so “N0045” is unchanged.

  1. Convert c("5", "8", "n/a") to numbers and explain the warning. Then make an ordered factor for c("low", "high", "medium") with levels low < medium < high, and make "South" the first level of an unordered site factor with relevel().
Show solution to question 10

Answer: The conversion gives 5, 8 and NA, with the warning “NAs introduced by coercion”. The ordered factor prints with low < medium < high, and the releveled site factor starts with "South".

Step by step

  1. Convert the text.
as.numeric(c("5", "8", "n/a"))

Output

Warning: NAs introduced by coercion

[1]  5  8 NA

2. "n/a" is not a number, so R returns NA and warns. In real data, look for entries like “unknown” typed into a numeric column. 3. Build the ordered factor and relevel the site factor.

urgency <- factor(c("low", "high", "medium"),
                  levels = c("low", "medium", "high"), ordered = TRUE)
urgency

Output

[1] low    high   medium
Levels: low < medium < high
site <- factor(c("North", "Mobile", "South"))
levels(site)

Output

[1] "Mobile" "North"  "South" 
levels(relevel(site, ref = "South"))

Output

[1] "South"  "Mobile" "North" 

4. urgency prints the three values and the order of its levels. Without relevel(), site has alphabetical levels with Mobile first; with it, "South" is first, which matters later because models use the first level as the reference group.

Checkpoint

You can create vectors, subset them, calculate summaries, recognize categories, and count missing values correctly.