1
\$\begingroup\$

I have heard that some patterns in R code are really anti-patterns and thus I am trying to imitate superior examples of R code.

I am confused about control flow.

I have been reviewing some R code from openintro.org; the original exercise is explained at:

http://www.openintro.org/download.php?file=os2_lab_02A&referrer=/stat/labs.php

One of the very early examples deals with counting "hit" or "miss" streaks within a vector of "H" and "M" characters. We consider a winning streak to be at least one H, and each streak ends in M. So H M M H H M counts as a streak of 1, a streak of 0, a streak of 2, etc.

So, for example, we start with a sequence like kobe$basket:

  [1] "H" "M" "M" "H" "H" "M" "M" "M" "M" "H" "H" "H" "M" "H" "H" "M" "M" "H"
 [19] "H" "H" "M" "M" "H" "M" "H" "H" "H" "M" "M" "M" "M" "M" "M" "H" "M" "H"
 [37] "M" "M" "H" "H" "H" "H" "M" "H" "M" "M" "H" "M" "M" "H" "M" "M" "H" "M"
 [55] "H" "H" "M" "M" "H" "M" "H" "H" "M" "H" "M" "M" "M" "H" "M" "M" "M" "M"
 [73] "H" "M" "H" "M" "M" "H" "M" "M" "H" "H" "M" "M" "M" "M" "H" "H" "H" "M"
 [91] "M" "H" "M" "M" "H" "M" "H" "H" "M" "H" "M" "M" "H" "M" "M" "M" "H" "M"
[109] "H" "H" "H" "M" "H" "H" "H" "M" "H" "M" "H" "M" "M" "M" "M" "M" "M" "H"
[127] "M" "H" "M" "M" "M" "M" "H"

and if we call calc_streak on that, we get:

 [1] 1 0 2 0 0 0 3 2 0 3 0 1 3 0 0 0 0 0 1 1 0 4 1 0 1 0 1 0 1 2 0 1 2 1 0 0 1 0
[39] 0 0 1 1 0 1 0 2 0 0 0 3 0 1 0 1 2 1 0 1 0 0 1 3 3 1 1 0 0 0 0 0 1 1 0 0 0 1

The "1 0 2" of the answer indicates the streak of 1, the streak of 0, the streak of 2 mentioned above.

The following function is referred to as "calc_streak" but apparently that name doesn't show up in the code listing.

function(x){
y <- rep(0,length(x))
y[x == "H"] <- 1
y <- c(0, y, 0)
wz <- which(y == 0)
streak <- diff(wz) - 1
return(streak)
}

I think the first line makes a big vector of "0" values, just as long as the input, so in this case it should be 133 entries long.

I think the second line goes through that vector and rewrites the "0" value to "1" if the input has an "H."

The third line might be putting an extra "0" at the front and back of the vector. I think it's in there to make the "diff" work correctly, so that the first winning streak shows up as a "1" value.

I think the object "wz" is a vector of integers. (I suspect this is a short vector, just containing the indices of the entries in x that satisfy the test.)

So first and foremost, if I am misunderstanding the way this example code works, please correct me.

But my primary question is - Should I regard the example R code as a good example of control flow and imitate it in my other programs?

Thanks.

\$\endgroup\$
2
\$\begingroup\$

Your interpretation of the code is correct, but I think that it could be written more idiomatically.

But before getting into that, the textual description of the output and the example output don't agree

We consider a winning streak to be at least one H, and each streak ends in M. So H M M H H M counts as a streak of 1, a streak of 0, a streak of 2, etc.

If a streak must have at least one H, how can you have a streak of 0? The example here (and the following example) are consistent with a definiton of streak of "the number of consecutive H's preceding each M." I'll continue with this interpretation so that it matches the existing code.

Now to improvements to the code.

The initialization of y can be simplified because 0 is the default numeric value, so the first line can be re-written as

y <- numeric(length(x))

The first two lines together create a 0/1 vector corresponding to whether the value in x was "H" (1) or "M" (0) (and uses the fact that it must be one or the other of those values). This can be done as one statement, using the fact that TRUE and FALSE become 1 and 0 when converted to numbers.

y <- as.numeric(x == "H")

The y vector has a 0 added to the beginning and the end which is equivalent to adding a "M" to the beginning and end of the baskets. This makes the first streak start at the beginning and the last streak stop at the end.

wz is a vector of indexes of where the misses ("M" in x, 0 in y) occur. The lengths of the streaks preceding them, then, is just the difference in successive indexes (minus 1, since you want to count the number of spaces between, which is one less than the difference of the positions).

Now, since the intermediate results are just used once on the next statement, they can be rolled up. Here are successive versions of doing that:

calc_streak <- function(x){
    y <- as.numeric(x == "H")
    y <- c(0, y, 0)
    wz <- which(y == 0)
    streak <- diff(wz) - 1
    return(streak)
}

calc_streak <- function(x){
    y <- c(0, as.numeric(x == "H"), 0)
    wz <- which(y == 0)
    streak <- diff(wz) - 1
    return(streak)
}

calc_streak <- function(x){
    wz <- which(c(0, as.numeric(x == "H"), 0) == 0)
    streak <- diff(wz) - 1
    return(streak)
}

calc_streak <- function(x){
    streak <- diff(which(c(0, as.numeric(x == "H"), 0) == 0)) - 1
    return(streak)
}

calc_streak <- function(x){
    diff(which(c(0, as.numeric(x == "H"), 0) == 0)) - 1
}

The last simplification uses the fact that a function returns the last evaluated expression by default.

I don't necessarily recommend doing that because with compactness comes a loss in being able to see what the computations are/mean. (To understand it, you must essentially reverse the steps and break out each transformation separately to see what it does and to figure out why.)

Looking at it again, the conversion to numeric is not a necessary step to determine wz; you can operate from x directly:

calc_streak <- function(x){
    wz <- which(c("M", x, "M") == "M")
    streak <- diff(wz) - 1
    return(streak)
}

For understandability, I'd use a different name than wz

calc_streak <- function(x){
    miss_indexes <- which(c("M", x, "M") == "M")
    streak <- diff(miss_indexes) - 1
    return(streak)
}

Then applying the default return value

calc_streak <- function(x){
    miss_indexes <- which(c("M", x, "M") == "M")
    diff(miss_indexes) - 1
}

It could be simplified once further to

calc_streak <- function(x){
    diff(which(c("M", x, "M") == "M")) - 1
}

but the loss in clarity (for you and someone else later) is (in my opinion) not worth it.


If length 0 streaks are not allowed, the approach I would use would be

calc_streak <- function(x) {
  r <- rle(x)
  r$lengths[r$values == "H"]
}
\$\endgroup\$
  • \$\begingroup\$ I may have been confused about whether streaks of zero length are well-defined in the original problem. Thanks for your code examples. I like the idea of avoiding the conversion to numeric values. \$\endgroup\$ – dataqeuerent Nov 11 '13 at 1:16
4
\$\begingroup\$

In my opinion, this function is a nice example of R code. Your interpretation of the code is correct. One unnecessary command is y == 0 since the information is redundant given that x == "H" is already known. In this case, y == 0 is identical to c(TRUE, !(x == "H"), TRUE).

The code can be simplified. The object wz can be created more easily:

calc_streak <- function(x){
  wz <- c(1, which(x == "M") + 1, length(x) + 2)
  streak <- diff(wz) - 1
  return(streak)
}

Alternatively and probabily even more efficiently, you can create a logical index indicating the positions of the "M"s in the vector and calculate streak more directly:

calc_streak <- function(x){
  idx <- x == "M"
  streak <- c(!idx[1], diff(which(idx)) - 1, !tail(idx, 1))
  return(streak)
}

The ! sign indicates logical not. If numeric and logical values are combined in one vector, the logical values are cast into numeric values. TRUE becomes 1, FALSE becomes 0.

\$\endgroup\$
  • \$\begingroup\$ Thank you for the demonstration of the which(idx) syntax. I'm still reading basic R textbooks to find good examples of which and how to extract elements from vectors. I think I need to retype your code in my rstudio environment and observe how it works. \$\endgroup\$ – dataqeuerent Nov 11 '13 at 1:14

Not the answer you're looking for? Browse other questions tagged or ask your own question.