What is if and else?

Label a return with if, else if and else, then label a whole vector at once with ifelse.

if runs a block of code when a condition is TRUE. else catches everything the tests missed, and else if adds another test in between.

In this lesson I label one return as up, flat or down, then swap to ifelse() to label a whole vector of returns at once, and I check for NA before I compare anything.

Step 1. One return, three labels

The condition sits in the round brackets and the code that runs sits in the curly brackets. R reads the tests from the top down and stops at the first one that is TRUE.

r <- 0.014                # one day's return, up 1.4 percent

if (r > 0.005) {
  label <- "up"
} else if (r < -0.005) {  # only reached when the first test is FALSE
  label <- "down"
} else {                  # everything the two tests missed
  label <- "flat"
}

print(label)              # -> [1] "up"
[1] "up"

Change the return and a different branch runs. -0.021 fails the first test, passes the second, and never reaches the else.

r <- -0.021               # a 2.1 percent fall

if (r > 0.005) {
  label <- "up"
} else if (r < -0.005) {
  label <- "down"
} else {
  label <- "flat"
}

print(label)              # -> [1] "down"
[1] "down"

First match wins, so the order you write the tests in decides the answer. Here both tests are true and only the top one runs.

r <- 0.030                # up 3 percent

if (r > 0.005) {
  size <- "up"
} else if (r > 0.02) {    # true as well, but R never gets here
  size <- "big up"
}

print(size)               # -> [1] "up"
[1] "up"

Step 2. if takes one value, not a vector

if wants a single TRUE or a single FALSE. Hand it a vector of three and R stops. try() here just lets the page carry on past the error.

rets <- c(0.014, -0.021, 0.001)      # three returns in one vector

try(if (rets > 0.005) print("up"))   # three answers, and if() can only use one
Error in if (rets > 0.005) print("up") : the condition has length > 1
# -> Error in if (rets > 0.005) print("up") : the condition has length > 1

rets > 0.005 is TRUE FALSE FALSE. R cannot pick which of the three to act on, so it refuses.

Step 3. ifelse() labels the whole vector

ifelse(test, yes, no) takes three arguments: the test, the value to use where the test is TRUE, and the value to use where it is FALSE. It runs element by element and gives you back a vector the same length as the test.

rets   <- c(0.014, -0.021, 0.001, 0.008, -0.002)  # five daily returns
labels <- ifelse(rets > 0, "up", "down")          # test, value if TRUE, value if FALSE

print(labels)                                     # -> [1] "up"   "down" "up"   "up"   "down"
[1] "up"   "down" "up"   "up"   "down"
print(length(labels))                             # -> [1] 5
[1] 5

Five returns in, five labels out. No loop and no error.

Step 4. Three labels from two tests

ifelse() gives two answers. For three, put a second ifelse() in the no slot: it only sees the elements the first test rejected.

Below are six daily returns. I call a move above 2 percent big up, a move below -2 percent big down, and everything else quiet.

rets <- c(0.022, 0.004, -0.031, 0.020, -0.025, -0.006)  # six daily returns

move <- ifelse(rets >  0.02, "big up",     # above 2 percent
        ifelse(rets < -0.02, "big down",   # below -2 percent
                             "quiet"))     # everything in between

print(move)
[1] "big up"   "quiet"    "big down" "quiet"    "big down" "quiet"   
# -> [1] "big up"   "quiet"    "big down" "quiet"    "big down" "quiet"
print(table(move))                         # count each label
move
big down   big up    quiet 
       2        1        3 
# -> move
# -> big down   big up    quiet
# ->        2        1        3

The day of exactly 0.020 came out quiet. > is strict, so 0.02 is not above 0.02. Move the boundary with >= if you want it counted as a big up.

Step 5. Check for NA before you compare

A missing value in R is NA. Comparing anything with NA gives NA, not TRUE and not FALSE.

print(NA > 0)      # -> [1] NA
[1] NA
print(NA + 1)      # -> [1] NA
[1] NA

So an NA in the vector produces an NA in the labels. ifelse() has nothing to choose from and passes the gap through.

rets <- c(0.014, NA, -0.021, 0.001)     # the second day never arrived

print(ifelse(rets > 0, "up", "down"))   # -> [1] "up"   NA     "down" "up"
[1] "up"   NA     "down" "up"  

is.na(x) is the test that does work on NA, and it returns TRUE or FALSE. Put it first, so the missing day is caught before any comparison runs.

lab <- ifelse(is.na(rets), "missing",   # catch the gap first
       ifelse(rets > 0,    "up",        # then the ordinary tests
                           "down"))

print(lab)                              # -> [1] "up"      "missing" "down"    "up"
[1] "up"      "missing" "down"    "up"     

The same holds for if. An NA condition is neither TRUE nor FALSE, so R stops rather than guess.

r <- NA                      # today's return did not arrive

try(if (r > 0) print("up"))  # the condition is NA, not TRUE or FALSE
Error in if (r > 0) print("up") : missing value where TRUE/FALSE needed
# -> Error in if (r > 0) print("up") : missing value where TRUE/FALSE needed

if (is.na(r)) {              # the guard goes on top
  label <- "missing"
} else if (r > 0) {
  label <- "up"
} else {
  label <- "down"
}

print(label)                 # -> [1] "missing"
[1] "missing"

Your turn

Take vals <- c(-0.04, 0.0, NA, 0.03, -0.012). Label each one "big up" above 0.01, "big down" below -0.01, "small" in between, and "missing" where the value is NA. Then count the missing ones.

vals <- c(-0.04, 0.0, NA, 0.03, -0.012)

out <- ifelse(is.na(vals), "missing",        # the NA test comes first
       ifelse(vals >  0.01, "big up",
       ifelse(vals < -0.01, "big down",
                            "small")))

print(out)                     # -> [1] "big down" "small"    "missing"  "big up"   "big down"
print(sum(out == "missing"))   # -> [1] 1