Course contents
Part IChapter 4R foundations and data
Vectors, Missing Values, and Conditions
On this page
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 77length(pulse)
Output
[1] 5mean(pulse)
Output
[1] 77.4median(pulse)
Output
[1] 77min(pulse)
Output
[1] 68max(pulse)
Output
[1] 90The 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 510:6
Output
[1] 10 9 8 7 6seq(0, 100, by = 25)
Output
[1] 0 25 50 75 100seq(0, 1, length.out = 5)
Output
[1] 0.00 0.25 0.50 0.75 1.00rep("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 64times = 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] 70pulse[3] # Third pulse
Output
[1] 90pulse[c(1, 5)] # First and fifth
Output
[1] 70 77pulse[-2] # All except the second
Output
[1] 70 90 68 77These 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 82pulse >= 80
Output
[1] FALSE TRUE TRUE FALSE FALSEpulse[pulse >= 80] # Only measurements >= 80
Output
[1] 82 90sum(pulse >= 80) # TRUE counts as one; FALSE as zero
Output
[1] 2pulse + 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 7c(10, 20, 30, 40) + c(1, 2) # recycled: 1, 2, 1, 2
Output
[1] 11 22 31 42c(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 41The 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.8sd(pulse)
Output
[1] 8.988882range(pulse)
Output
[1] 68 90pulse[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 3which.max(pulse)
Output
[1] 3which.min(pulse)
Output
[1] 4The 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 = TRUEfor 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 90sort(pulse, decreasing = TRUE)
Output
[1] 90 82 77 70 68order(pulse)
Output
[1] 4 1 5 2 3rank(pulse)
Output
[1] 2 4 5 1 3pulse[order(pulse)] # same as sort(pulse)
Output
[1] 68 70 77 82 90order(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] FALSEall(pulse > 60)
Output
[1] TRUEifelse(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 TRUEsum(is.na(bmi))
Output
[1] 2mean(bmi) # Returns NA
Output
[1] NAmean(bmi, na.rm = TRUE) # Average of observed values only
Output
[1] 27.86667The 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 TRUEThe 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 5toupper(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 TRUEids[grepl("^S", ids)]
Output
[1] "S0001" "S0012" "S0100"grep("clinic", sites)
Output
[1] 1 2grep("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 < severeseverity < "severe"
Output
[1] TRUE FALSE TRUE TRUEsite <- 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 NAas.integer(7.9) # drops the decimals
Output
[1] 7as.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")returnsNAwith a warning: the data are text rather than numeric.NA == NAisNA, notTRUE: useis.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; inspectclass()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] NANA == NA
Output
[1] NAis.na(NA)
Output
[1] TRUEclass(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
- Make a vector with six systolic readings, one of them
NA. Calculate the average while skippingNA.
Show solution to question 1
Answer: The average of the five observed readings is 134.4 mmHg.
Step by step
- Build the vector with
c(), writing the missing value asNA. - Without
na.rm, oneNAmakes the meanNA. Addna.rm = TRUEto skip it.
sbp <- c(115, 143, NA, 136, 151, 127)
mean(sbp)Output
[1] NAmean(sbp, na.rm = TRUE)Output
[1] 134.43. The first line prints NA and the second prints 134.4, the mean of 115, 143, 136, 151 and 127.
- Count how many observed readings are at least 140.
Show solution to question 2
Answer: 2 readings (143 and 151).
Step by step
sbp >= 140returnsNAfor the missing reading, so first keep only values that are not missing with!is.na(sbp).- Join the two conditions with
&and add them up withsum(), sinceTRUEcounts as 1.
sbp <- c(115, 143, NA, 136, 151, 127)
sum(!is.na(sbp) & sbp >= 140)Output
[1] 23. The result is 2. Of the five observed readings, only 143 and 151 reach 140.
- 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
- Positions count from 1, and the vector has
length(sbp)entries. - 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 127tail(sbp, 2)Output
[1] 151 1273. Both print 151 and 127. Position 6 is the last value, and position 5 is the one before it.
- 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
- Replace the
NAin 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] 112mean(sbp, na.rm = TRUE)Output
[1] 134.42. 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.
- Make a vector of ages from 20 to 60 in steps of 10 with
seq(). Then userep()witheachto 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
seq()takes a start, an end and a stepby.rep()witheach = 3repeats each element in turn.
seq(20, 60, by = 10)Output
[1] 20 30 40 50 60rep(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.
- 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
- Add to the length-6 vector.
c(1, 2, 3, 4, 5, 6) + c(1, 2, 3)Output
[1] 2 4 6 5 7 92. 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 74. 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.
- 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
- Store the vector and use
which(),which.max(),var()andrange().
v <- c(120, 135, 128, 150, 118)
which(v > 130)Output
[1] 2 4which.max(v)Output
[1] 4var(v)Output
[1] 168.2range(v)Output
[1] 118 1502. 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 4rank(v)Output
[1] 2 4 3 5 14. 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?”.
- Use
any()andall()to test whether any reading is over 140 and whether all are over 100. Label each reading"high"or"normal"at 130 usingifelse().
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
- Use the vector
vfrom the previous item.
v <- c(120, 135, 128, 150, 118)
any(v > 140)Output
[1] TRUEall(v > 100)Output
[1] TRUEifelse(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”.
- With
ids <- c("S0001", "N0045", "S0100"): count the characters, keep the digits withsubstr(), find which IDs start with"S"usinggrepl(), and replace"S"with"Site"usingsub().
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
- Run the four functions.
ids <- c("S0001", "N0045", "S0100")
nchar(ids)Output
[1] 5 5 5substr(ids, 2, 5)Output
[1] "0001" "0045" "0100"grepl("^S", ids)Output
[1] TRUE FALSE TRUEsub("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.
- Convert
c("5", "8", "n/a")to numbers and explain the warning. Then make an ordered factor forc("low", "high", "medium")with levels low < medium < high, and make"South"the first level of an unordered site factor withrelevel().
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
- Convert the text.
as.numeric(c("5", "8", "n/a"))Output
Warning: NAs introduced by coercion
[1] 5 8 NA2. "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)
urgencyOutput
[1] low high medium
Levels: low < medium < highsite <- 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.