2 == 1+1[1] TRUE
1/2 == .5[1] TRUE
2 == 3[1] FALSE
2/3 == .6666[1] FALSE
Course textbook for F2026 under active development
📄 Download the R code from this chapter
This course is designed for people who already have taken PSCI1800 or have a basic familiarity with R. This chapter serves as a refresher on specific use cases that will be important for the way we will use R in this course.
The textbook for my intro to R class, PSCI 1800, is available here if you feel you need additional reminders about anything I don’t cover here.
There are a few main things that we will be doing in R that are particular to this class that I want to refresh: working with Boolean variables, subsetting, generating simulations, and writing our own functions.
A very helpful feature of R is how it makes use of True or False, or “Boolean”, variables. R can easily test a large number of objects for some sort of logical condition and return a bunch of trues and falses. Further, R very helpfully treats “True” as the number 1, and “False” as the number 0, which will be critical for using R to simulate probability.
If we give R some expressions that can be True or False, it will evaluate them:
2 == 1+1[1] TRUE
1/2 == .5[1] TRUE
2 == 3[1] FALSE
2/3 == .6666[1] FALSE
A couple of things to note here. First, we use a double equals sign for conditional logic statements. A single equals sign is used for assignment (we use<- as our assignment operator, but you can also use =. I like using the arrow so I don’t get confused about what I’m doing). Second, while TRUE and FALSE are words, they are not in quotation marks. This is not the words “True” and “False”, R is treating these as special values, not just characters.
When we have a vector of information that we create with concatenate (c()) we can also use the double equals sign (or any other boolean operator):
vec <- c(1,2,3,4,5)
vec == 3[1] FALSE FALSE TRUE FALSE FALSE
We can see that R went through and evaluated each of the items in vec as true or false.
Other common boolean operators are greater than and less than:
2 > 3[1] FALSE
2 < 3[1] TRUE
vec > 3[1] FALSE FALSE FALSE TRUE TRUE
vec >= 3[1] FALSE FALSE TRUE TRUE TRUE
vec <= 3[1] TRUE TRUE TRUE FALSE FALSE
As I mentioned above, something that ends up being very helpful down the road for us is that R treats Trues as being equal to 1 and Falses be equal to 0. That means that we can quickly figure out how many items in a vector are true:
sum(vec >= 3)[1] 3
Moreso, it lets us take advantage of the fact that the mean of a vector of 1’s and 0’s is equal to the probability that the vector is equal to 1.
mean(vec >= 3)[1] 0.6
This is going to keep coming up so let’s take a second to prove to ourselves that taking the mean of a variable that is 0 or 1 returns the proportion of 1s.
Consider this vector:
\[ v = \lbrace 0,1,1\rbrace \]
We want to take the mean of this vector. The formula for the mean is:
\[ \bar{v} = \frac{\sum v_i}{n} \]
Which just means: add up all of the values of the vector and divide by the number of values. So for this variable:
\[ \begin{aligned} \bar{v} = \frac{0 + 1+ 1}{3}\\ \bar{v} = \frac{2}{3} \end{aligned} \]
We can see really clearly that what is going to happen here is that the numerator will equal the number of 1s and the denominator will be the total length of the vector. So the mean will return the proportion of entries that equal 1. Or, if we have a boolean variable (which treats T=1 and F=0) the proportion of the entries that are true. We are going to make use of this fact a lot.
Another key boolean operator is the exclamation point, which we use in two distinct ways. First, we use it simply to say “not equal”:
2 == 3 #FALSE[1] FALSE
2 != 3 #TRUE[1] TRUE
We can also use it in front of any logical statement to negate the whole thing, turning all TRUE to FALSE and vice versa:
!(2 == 3) #TRUE. essentially the same as the one above[1] TRUE
vec == 3[1] FALSE FALSE TRUE FALSE FALSE
!(vec == 3)[1] TRUE TRUE FALSE TRUE TRUE
Why is this helpful? Imagine a situation where you have data on all counties in the US and you want to look at all the values except those in Alaska. It is way easier to say “not Alaska” than it is to say “Arizona, Alabama, Arkansas, Connecticut….”.
An important extension to this is the ability to have more than one condition. For this we have both an AND and OR function.
For the AND function we use the ampersand:
(2 == 2) & (3 == 3) #TRUE[1] TRUE
(2 == 2) & (3 == 2) #FALSE[1] FALSE
The ‘OR’ operator is denoted by the pipe:
(2 == 2) | (3 == 2) #TRUE[1] TRUE
(2 == 8) | (3 == 2) #FALSE[1] FALSE
A helpful shortcut for separating AND and OR functions is:
The final boolean operator we will make use of is %in%, which is maybe the most helpful one. This will test whether things on the left hand side are present in the right hand side. The logical test is being performed on the things on the left hand side.
So consider the confusing and overlapping band memberships of Buffalo Springfield, Crosby Stills and Nash, and Crosby Stills Nash and Young.
buffalo.springfield <- c("stills", "martin","palmer","furay","young")
csn <- c("crosby","stills","nash")
csny <- c("crosby","stills","nash","young")The %in% operator will tell me: which members of Buffalo Springfield were in CSN?
buffalo.springfield %in% csn[1] TRUE FALSE FALSE FALSE FALSE
buffalo.springfield %in% csny[1] TRUE FALSE FALSE FALSE TRUE
How do we use these boolean variables to subset? The key insight is that if we put a vector of TRUE and FALSE inside square brackets, R will return the index positions of the TRUE values and not return the index positions of the FALSE values.
Let’s pull in some data on all US counties to demonstrate:
acs <- rio::import("https://github.com/marctrussler/IIS-Data/raw/main/ACSCountyData.csv")
head(acs[,c("county.name","state.abbr","median.income","percent.college")]) county.name state.abbr median.income percent.college
1 Etowah County AL 44023 17.73318
2 Winston County AL 38504 13.53585
3 Escambia County AL 35000 12.65935
4 Autauga County AL 58786 27.68929
5 Baldwin County AL 55962 31.34588
6 Barbour County AL 34186 12.21592
If we want the median incomes of counties where more than 40% of the population has a college degree:
head(acs$median.income[acs$percent.college > 40])[1] 63417 75761 84196 116178 78041 93712
I would read that expression as: give me the values of median income where percent college is greater than 40.
We can similarly use conditional logic to return rows of the full data frame that meet a certain condition. If I want to return the rows of counties in Pennsylvania:
head(acs[acs$state.abbr == "PA", ]) V1 county.fips county.name state.full state.abbr state.alpha
2245 2245 42015 Bradford County Pennsylvania PA 38
2246 2246 42017 Bucks County Pennsylvania PA 38
2247 2247 42013 Blair County Pennsylvania PA 38
2248 2248 42083 McKean County Pennsylvania PA 38
2249 2249 42085 Mercer County Pennsylvania PA 38
2250 2250 42087 Mifflin County Pennsylvania PA 38
state.icpsr census.region population population.density percent.senior
2245 14 northeast 61304 53.42781 20.26295
2246 14 northeast 626370 1036.30200 17.61275
2247 14 northeast 123842 235.53000 19.79781
2248 14 northeast 41806 42.69485 18.51409
2249 14 northeast 112630 167.45910 20.70674
2250 14 northeast 46362 112.79050 20.77779
percent.white percent.black percent.asian percent.amerindian
2245 96.92353 0.4404280 0.6769542 0.06361738
2246 88.01555 4.0017881 4.6973833 0.16475885
2247 95.50799 1.6214208 0.6992781 0.03391418
2248 94.48405 2.4876812 0.4449122 0.06697603
2249 91.22969 5.6698926 0.6889816 0.10920714
2250 96.88322 0.7915966 0.1898106 0.15745654
percent.less.hs percent.college unemployment.rate median.income gini
2245 11.085675 17.84021 4.808069 51457 0.4310
2246 6.144487 40.46496 4.524462 86055 0.4480
2247 9.070472 20.80933 4.961147 47969 0.4448
2248 8.908716 18.14864 6.829542 46953 0.4475
2249 10.161785 22.55270 5.271703 48768 0.4381
2250 14.792334 12.47843 3.598866 47526 0.4144
median.rent percent.child.poverty percent.adult.poverty
2245 711 17.012323 11.132679
2246 1207 7.358998 5.743422
2247 711 19.575034 14.600937
2248 644 27.077236 16.563244
2249 679 24.827619 13.507259
2250 665 23.594624 13.347636
percent.car.commute percent.transit.commute percent.bicycle.commute
2245 90.12223 0.2475153 0.27036290
2246 88.75924 3.4039931 0.16052502
2247 91.58161 1.1913318 0.18328182
2248 91.10273 0.9152430 0.17750166
2249 90.04547 0.4175279 0.08887970
2250 90.67850 0.3038048 0.05786758
percent.walk.commute average.commute.time percent.no.insurance
2245 3.625148 23 6.692875
2246 1.717094 30 4.212366
2247 3.056494 20 5.240548
2248 3.372532 22 4.568722
2249 3.577925 21 6.158217
2250 2.956069 24 12.339847
Remember the use of the comma here! What we are subsetting is a data frame with 2 dimensions, so we have to give information on which columns we want to return. By putting a comma and leaving the second part blank, we are saying “give me all the columns”.
We can string together booleans with & and | to subset on multiple conditions:
#Counties in Pennsylvania with over 40% college educated:
head(acs[acs$state.abbr == "PA" & acs$percent.college > 40, ]) V1 county.fips county.name state.full state.abbr state.alpha
2246 2246 42017 Bucks County Pennsylvania PA 38
2252 2252 42091 Montgomery County Pennsylvania PA 38
2260 2260 42027 Centre County Pennsylvania PA 38
2261 2261 42029 Chester County Pennsylvania PA 38
2289 2289 42003 Allegheny County Pennsylvania PA 38
state.icpsr census.region population population.density percent.senior
2246 14 northeast 626370 1036.3020 17.61275
2252 14 northeast 821301 1700.5640 17.00095
2260 14 northeast 161443 145.4622 13.26970
2261 14 northeast 517156 689.0596 15.44138
2289 14 northeast 1225561 1678.5810 18.11171
percent.white percent.black percent.asian percent.amerindian
2246 88.01555 4.001788 4.697383 0.1647588
2252 79.41607 9.079375 7.476552 0.1271154
2260 87.51200 3.961770 6.345893 0.0953897
2261 85.33711 5.937473 5.177741 0.1077044
2289 80.11409 12.883161 3.654734 0.1104800
percent.less.hs percent.college unemployment.rate median.income gini
2246 6.144487 40.46496 4.524462 86055 0.4480
2252 5.781227 48.70617 4.619781 88166 0.4667
2260 5.673250 44.73959 4.253275 58055 0.4696
2261 6.749068 51.81124 4.356723 96726 0.4572
2289 5.712555 40.70367 5.286755 58383 0.4869
median.rent percent.child.poverty percent.adult.poverty
2246 1207 7.358998 5.743422
2252 1253 7.293578 6.084560
2260 966 11.803944 22.887424
2261 1287 8.224267 6.776429
2289 865 16.381297 11.940936
percent.car.commute percent.transit.commute percent.bicycle.commute
2246 88.75924 3.403993 0.1605250
2252 85.07758 5.457351 0.2106967
2260 77.86436 5.472074 2.0904255
2261 86.12955 2.732424 0.1857280
2289 79.94947 9.523755 0.5728954
percent.walk.commute average.commute.time percent.no.insurance
2246 1.717094 30 4.212366
2252 2.276138 29 3.767316
2260 8.864362 20 5.295987
2261 2.747870 28 5.890486
2289 4.109690 27 4.278286
The %in% operator is often the cleanest way to subset to multiple categories. If I want counties in any of the northeastern states:
northeast <- c("PA","NY","NJ","CT","MA","VT","NH","ME","RI")
head(acs[acs$state.abbr %in% northeast, ]) V1 county.fips county.name state.full state.abbr state.alpha
309 309 9005 Litchfield County Connecticut CT 7
310 310 9007 Middlesex County Connecticut CT 7
311 311 9003 Hartford County Connecticut CT 7
312 312 9001 Fairfield County Connecticut CT 7
313 313 9013 Tolland County Connecticut CT 7
314 314 9015 Windham County Connecticut CT 7
state.icpsr census.region population population.density percent.senior
309 1 northeast 183031 198.8359 19.91794
310 1 northeast 163368 442.3669 19.00739
311 1 northeast 894730 1217.3820 16.45860
312 1 northeast 944348 1511.0420 15.18709
313 1 northeast 151269 368.6320 14.81136
314 1 northeast 116538 227.2013 15.66613
percent.white percent.black percent.asian percent.amerindian
309 92.92579 1.927542 1.854331 0.19450257
310 88.66118 5.297243 3.072817 0.16159835
311 71.47721 13.646910 5.205705 0.32914958
312 73.24016 11.353336 5.286928 0.24757822
313 88.35650 3.054823 4.651978 0.04429196
314 88.86972 2.268788 1.384956 0.61010143
percent.less.hs percent.college unemployment.rate median.income gini
309 7.410651 35.07926 5.234069 78314 0.4479
310 5.823624 41.52384 4.693489 84761 0.4427
311 10.469985 37.75315 6.543712 72321 0.4716
312 10.110784 47.35474 6.955179 92969 0.5432
313 5.439658 41.15365 5.248147 84916 0.4315
314 11.350250 24.14521 6.283623 64774 0.4151
median.rent percent.child.poverty percent.adult.poverty percent.car.commute
309 1036 7.701885 7.096809 89.94674
310 1162 8.338651 7.000711 88.49398
311 1076 15.441200 10.286984 89.51368
312 1470 10.954530 8.405622 80.34434
313 1119 4.802281 8.191013 86.55236
314 895 13.548696 10.178451 91.43660
percent.transit.commute percent.bicycle.commute percent.walk.commute
309 1.3545533 0.13105198 2.250949
310 1.4582524 0.06971269 2.621654
311 3.2843624 0.22519651 1.936371
312 10.2409910 0.18379396 2.528293
313 1.9894721 0.22062239 4.602106
314 0.5409203 0.21250439 2.736214
average.commute.time percent.no.insurance
309 28 4.172517
310 26 3.284609
311 23 4.285092
312 31 8.589842
313 26 3.009870
314 26 3.872557
Now all of these examples have been super obvious. Things we could eyeball. Try to think about how helpful these tools are going to be when we have a dataset with thousands of rows.
One of the most flexible tools in all coding languages is for() loops. In short: for() loops allow us to repeat the same code many, many times. There are two main use cases in this course. The first is when we need to perform the same operation on different clusters in the data (below, for example, we’ll compute the mean population for the counties in each state). The second is when we want to use R to simulate probability, which we’ll get to in the next section.
Previously we saw that we can use square brackets to give us conditional means. For example we can easily get the mean population for all counties:
mean(acs$population)[1] 102769.9
And if we want the mean population for just the counties in Alabama:
mean(acs$population[acs$state.abbr=="AL"])[1] 72607.16
OK, but what if we wanted to do the same thing for all 50 states?
We could think about brute forcing it, subbing in the right two letter abbreviation and repeating the code 50 times:
mean(acs$population[acs$state.abbr=="AK"])[1] 25466.07
mean(acs$population[acs$state.abbr=="AZ"])[1] 463112.3
mean(acs$population[acs$state.abbr=="AR"])[1] 39875.61
mean(acs$population[acs$state.abbr=="CA"])[1] 674978.6
#
#
#
mean(acs$population[acs$state.abbr=="WI"])[1] 80255.47
mean(acs$population[acs$state.abbr=="WY"])[1] 25297.22
This is unwieldy! And I didn’t even do all of the states!
But notice something about the above block of code: most of the command on each line is exactly the same. All that we are changing in every line is what is in the quotations. We are going from AL, to AZ, to AR…. all the way to WY.
So all we need is a method that allows us to run mean(acs$population[acs$state.abbr=="XX"]) 50 times, each time through changing XX to the next two-letter state abbreviation.
The way that we will accomplish this is with a for() loop.
Here is the basic setup for a for() loop:
for(i in 1:3){
#CODE HERE
}We can look at the for loop as having two parts. Everything from the start to the opening squiggly bracket are the instructions for the loop. It basically tells the loop how many times to repeat the same thing. “i” in this case is the variable that is going to change its value each time through the loop. Having this variable is what allows the loop to do something slightly different each time through the loop. The second part is whatever you put in between the squiggly brackets. This is the code that will be repeated again, and again, the specified number of times.
Let’s put a simple bit of code in the loop to see how these work. I’m just going to put the command print(i) in the loop so that all the loop does is print the current value of i.
for(i in 1:3){
print(i)
}[1] 1
[1] 2
[1] 3
What a loop does is the following: on the first time through the loop it sets our variable (i) to the number 1. Literally. Anywhere we have the variable i, R will replace it with the integer 1. Once all the code has been executed and R reaches the closing squiggly bracket it goes back up to the start. Now it will set i equal to 2. Anywhere in our code where there is currently the variable “i”, R will literally treat this as the integer 2. After it reaches the end of the code again, it will go back to the beginning and run the code again, this time making “i” equal to 3.
So the result of running this code is to simply print out the numbers 1, then 2, then 3.
To make this crystal clear, here is what code is literally being executed by R here. It is just the print command 3 times, the first time changing the variable i to the integer 1, the second time changing it to 2, and the third time changing it to 3.
print(1)[1] 1
print(2)[1] 2
print(3)[1] 3
There is nothing special about “i” — we can name the variable that changes every time through the script anything we want. So we could edit this same code to be for(batman in 1:3), and have the variable be called batman. And if we run this we get the exact same result:
for(batman in 1:3){
print(batman)
}[1] 1
[1] 2
[1] 3
While we can use anything, it is convention to use the letter “i” as the indexing variable in a for loop. This is true across coding platforms so it’s a good idea to just use that.
Now let’s use our knowledge of for() loops to complete the task we set out: calculating the mean population for the counties in every state.
The way we are going to do that is to first create and save a vector of all the state abbreviations. Using the unique() command, I can create a vector of all the state abbreviations. If we look at this, we now have a vector of all the possible state abbreviations.
states <- unique(acs$state.abbr)
head(states)[1] "AL" "AK" "AZ" "AR" "CA" "CO"
length(states)[1] 51
Remember that by using square brackets we can select a certain value out of a vector. So the first entry in states is AL, the 13th entry is ID, and the 27th entry is Montana.
states[1][1] "AL"
states[13][1] "ID"
states[27][1] "MT"
So instead of subsetting to a particular two letter abbreviation, now we can insert this vector and have the loop sequentially select each of the items in it each time through the loop.
for(i in 1:3){
mean(acs$population[acs$state.abbr==states[i]])
}So instead of acs$state.abbr=="AL", we now have acs$state.abbr==states[i].
Again, let’s think about what R is going to do in this code. The first time through the loop it will change i to the integer 1, then the integer 2, then the integer 3. So this successively subsets to the first state, the second state, then the third state.
#What it will literally do
mean(acs$population[acs$state.abbr==states[1]])[1] 72607.16
mean(acs$population[acs$state.abbr==states[2]])[1] 25466.07
mean(acs$population[acs$state.abbr==states[3]])[1] 463112.3
But hold on, right now we are only going from 1 to 3, but there are 50 states + DC. We could just write in 1:51, but a better coding practice is to set the end point of the loop to be the length of the vector states, regardless of what the length is. That way if we ever change the vector in the future our loop still works.
for(i in 1:length(states)){
mean(acs$population[acs$state.abbr==states[i]])
}Now let’s run it and see what happens: nothing!
R, in fact, did run the command in the brackets 51 times, each time changing i from 1 to 2 to 3, effectively looping over the 50 states + DC. But unless we explicitly tell R to save the results of what it is doing, it will do all of this in the background when running code inside a loop.
So the last step in this process is to create an empty vector to store the results. We need to create a vector of NAs that is the same length as states.
state.pop.means <- rep(NA, length(states))
for(i in 1:length(states)){
state.pop.means <- mean(acs$population[acs$state.abbr==states[i]])
}
state.pop.means[1] 25297.22
Huh. Well that didn’t work. We were expecting to get 51 state means in this vector, but instead we got just one. Why did that happen?
Well, the first time through the loop we told R: assign the mean of Alabama to the object state.pop.means, and it did. Then we said, OK, nevermind, assign the mean of Alaska to state.pop.means. Then we said, OK, nevermind, assign the mean of Arkansas to state.pop.means. So this last number is just the last mean we calculated (the average population of counties in Wyoming).
The last step here is to tell R to save the results in the right “slot” of state.pop.means. We do this by using our variable i to index the results vector as well. Each time through the loop we save the results to the first slot in state.pop.means, and then the next time in the second slot, etc.
state.pop.means <- rep(NA, length(states))
for(i in 1:length(states)){
state.pop.means[i] <- mean(acs$population[acs$state.abbr==states[i]])
}
head(cbind(states, state.pop.means)) states state.pop.means
[1,] "AL" "72607.1641791045"
[2,] "AK" "25466.0689655172"
[3,] "AZ" "463112.333333333"
[4,] "AR" "39875.6133333333"
[5,] "CA" "674978.620689655"
[6,] "CO" "86424.078125"
Each time through the loop our variable i gets changed to a new value. The first time through the loop, changing i to the integer 1 accomplishes two tasks. First, it tells R which slot in the empty vector state.pop.means to save the answer. Second, it tells R which entry in the vector states to access in order to subset our data.
So always try to remember that nothing more complicated than this is happening inside of a loop. And you can always just enter the integers yourself to get a sense of what is going on.
The other major use case for for() loops is to simulate probability. A helpful definition of the probability of an event is the frequency that an outcome occurs if the event is repeated a large number of times.
So, for example, the probability of a coin coming up heads on a fair coin is 50%. That does not mean that if we flip a tail we will get heads next. It means that if we flip a coin a large number of times, the number of heads will approach 50%.
We can write code that simulates one coin flip:
coin <- c("H","T")
sample(coin, 1)[1] "T"
The sample() command is a random command. We say what we want to sample in the first argument, and how many times we want to sample it in the second argument. Every time we run the command we get something different. (Well, here we may not because there are only two things we are sampling. But every time we run it is a new random draw.)
sample(coin, 1)[1] "H"
sample(coin, 1)[1] "H"
sample(coin, 1)[1] "T"
sample(coin, 1)[1] "H"
sample(coin, 1)[1] "H"
Because sample() produces a different result each time we run it, we’ll frequently want to make our code reproducible. Setting a seed at the top of a simulation makes sure everyone (including future-you) gets the same “random” numbers each time the code is run. Throughout this course I set the seed to Penn’s zip code:
set.seed(19104)
sample(coin, 1)[1] "T"
Above we defined probability as the frequency of an outcome when we repeat an event a large number of times. With R we can just repeat an event a large number of times using a loop:
for(i in 1:1000){
sample(coin, 1)
}Like with all loops, this code did indeed run that sample() command 1000 times, but if we don’t save what we are doing we don’t see any result. Instead, let’s save the outcome of each loop to a results vector. The best practice for this is to pre-allocate a vector of NAs of the length we want, and then fill in each position as we go:
result <- rep(NA, 1000)
for(i in 1:1000){
result[i] <- sample(coin, 1)
}
head(result)[1] "T" "T" "H" "T" "H" "T"
What percentage of these results are Heads? We can figure that out, but I’m going to propose that we edit our code slightly to make better use of what we just learned about boolean variables. All we want to know is if the coin is heads or not, so let’s evaluate that each time we sample:
result <- rep(NA, 1000)
for(i in 1:1000){
result[i] <- sample(coin, 1) == "H"
}
head(result)[1] FALSE TRUE FALSE FALSE FALSE FALSE
We now have 1000 trues or falses. Because R treats True as 1 and False as 0, we can determine the proportion of Trues by taking the mean of this variable:
mean(result)[1] 0.503
As expected it’s approximately 50%.
Now that was a pretty obvious example. The nice thing about this method is that it can answer probability questions as long as we can generate one answer. We just need to put that code in a loop and determine how frequently a certain answer occurs in the long run.
So: if I roll 3 dice and flip 2 coins, how often does the dice add up to a number greater than 12 and we get two heads?
Let’s write code to do this event once:
dice <- c(1,2,3,4,5,6)
#Using the replace=T option because each time we sample from the dice
#we want all sides to be available
sum(sample(dice, 3, replace=T))[1] 9
#Same with the coin
sample(coin, 2, replace=T)[1] "H" "H"
This code accomplished this task, but we want to know if the conditions we set have been met.
#This one is easy: is the sum greater than 12
sum(sample(dice, 3, replace=T))>=12[1] TRUE
#For the coin we want to see if each entry is equal to "H", but only return one true
#if both of them are H. We can do that with all()
sample(coin,2, replace=T)=="H"[1] TRUE TRUE
all(sample(coin,2, replace=T)=="H")[1] FALSE
#And then we want to know if both conditions are met:
sum(sample(dice, 3, replace=T))>=12 & all(sample(coin,2, replace=T)=="H")[1] FALSE
OK, we’ve written code to do this once. All we need to do is repeat this a large number of times to determine the probability of this compound event:
result <- rep(NA, 1000)
for(i in 1:1000){
result[i] <- sum(sample(dice, 3, replace=T))>=12 & all(sample(coin,2, replace=T)=="H")
}
mean(result)[1] 0.103
Around 10% of the time do we get a sum over 12 and two heads. Not sure what to do with that information tbh.
This method can be applied to any sort of probability problem, but where we are particularly interested in it will be helping us to understand the principles of sampling. When we take a survey we get one sample of many possible samples we could get. Each of those samples will be a bit different, and statistics helps us understand just how different those samples will be. Much more on this to come!
One principle of sampling that we will learn is that confidence intervals contain the true population parameter 95% of the time. If the true proportion we are measuring with our sample is 30%, if we repeatedly take samples and form confidence intervals, 95% of them will contain 30%.
Here is simulating that once:
coin <- c(0,1)
samp <- sample(coin, 1000, replace=T, prob=c(.7,.3))
#Lower bound less than .3 and upper bound greater than .3?
t.test(samp)$conf.int[1] < .3 & t.test(samp)$conf.int[2] > .3[1] TRUE
To prove this axiom of probability, we can repeat this process many times and store the results:
result <- rep(NA, 1000)
for(i in 1:1000){
samp <- sample(coin, 1000, replace=T, prob=c(.7,.3))
#Lower bound less than .3 and upper bound greater than .3?
result[i] <- t.test(samp)$conf.int[1] < .3 & t.test(samp)$conf.int[2] > .3
}
mean(result)[1] 0.957
95% of confidence intervals contained the truth (30%). This is where we are headed! For right now, don’t worry too much about what a confidence interval is or how t.test() works. The point is the pattern — simulate an event once, put tha code in a loop, and take the mean of the boolean result to get a probability.
table() and prop.table()When we look at categorical variables we frequently want to know the frequency and proportion of each category. This is what table() and prop.table() are for.
Let’s load some data from the American National Election Study (ANES):
anes <- read.csv("https://raw.githubusercontent.com/marctrussler/IIS-Data/main/ANES2020Clean.csv")If we want the raw counts of a variable we use table():
table(anes$race)
Asian/Hawaiian/Pacific-Islander Black, non-Hispanic
284 726
Hispanic Multiple races, non-Hispanic
762 271
Native American White, non-Hispanic
172 5963
Wrapping that in prop.table() gives us relative frequencies (proportions) instead of counts:
prop.table(table(anes$race))
Asian/Hawaiian/Pacific-Islander Black, non-Hispanic
0.03472732 0.08877476
Hispanic Multiple races, non-Hispanic
0.09317682 0.03313769
Native American White, non-Hispanic
0.02103204 0.72915138
We can also make two-way tables by giving table() two variables. I like to label the variables in the command to make things more clear:
table(race = anes$race, gender = anes$gender) gender
race Female Male
Asian/Hawaiian/Pacific-Islander 133 151
Black, non-Hispanic 466 255
Hispanic 404 357
Multiple races, non-Hispanic 156 114
Native American 78 90
White, non-Hispanic 3177 2763
And we can pass that to prop.table() too. By default, this gives us the percentage of the total sample in each cell:
prop.table(table(race = anes$race, gender = anes$gender)) gender
race Female Male
Asian/Hawaiian/Pacific-Islander 0.016331041 0.018541257
Black, non-Hispanic 0.057220039 0.031311395
Hispanic 0.049607073 0.043835953
Multiple races, non-Hispanic 0.019155206 0.013998035
Native American 0.009577603 0.011051081
White, non-Hispanic 0.390103143 0.339268173
We can add an option to prop.table to calculate either row or column percentages. 1 will calculate row percentages and 2 will calculate column percentages. (This is just like square brackets where the first spot is for row information and the second spot is for column information!)
Row percentages — of people of each race, what percent are Female vs Male?
prop.table(table(race = anes$race, gender = anes$gender), 1) gender
race Female Male
Asian/Hawaiian/Pacific-Islander 0.4683099 0.5316901
Black, non-Hispanic 0.6463245 0.3536755
Hispanic 0.5308804 0.4691196
Multiple races, non-Hispanic 0.5777778 0.4222222
Native American 0.4642857 0.5357143
White, non-Hispanic 0.5348485 0.4651515
Column percentages — of people of each gender, what percent are each race?
prop.table(table(race = anes$race, gender = anes$gender), 2) gender
race Female Male
Asian/Hawaiian/Pacific-Islander 0.03013140 0.04048257
Black, non-Hispanic 0.10557318 0.06836461
Hispanic 0.09152696 0.09571046
Multiple races, non-Hispanic 0.03534209 0.03056300
Native American 0.01767105 0.02412869
White, non-Hispanic 0.71975532 0.74075067
These three tables answer three genuinely different questions. When we get to conditional probability in the next chapter you will see that these are actually the joint, and two different conditional, probabilities.
Functions take a specific set of inputs and produce a specific set of outputs.
That might sound confusing, but you have already been using functions constantly. Every time you write mean(), sum(), length(), table(), or head() you are calling a function. Someone, at some point, wrote the code that makes mean() work, and packaged it up so that all you have to do is give it a vector (an input) and it gives you back the mean (the output).
In this section we are going to learn how to write our own functions. Here’s the basic idea: if you find yourself copy-pasting the same chunk of code and just changing one or two things each time, you should probably write a function instead.
A function in R has three parts: a name, arguments (the inputs), and a body (the code that runs). Here is the simplest possible function:
say.hello <- function(){
print("Hello!")
}Let’s break this down. say.hello is the name – this is what we will type to call the function, just like we type mean to call the mean function. The function() part tells R “I am defining a function.” The curly braces {} contain the body – the code that runs when you call the function. In this case, all it does is print “Hello!”.
To call the function:
say.hello()[1] "Hello!"
Note the parentheses. If you forget them, R doesn’t run the function – it just tells you that the function exists:
say.hellofunction ()
{
print("Hello!")
}
That’s not what we want. Always include the parentheses when you want to actually run a function.
A function that does the same thing every time isn’t very useful. The real power comes from giving functions arguments – inputs that change what the function does.
To continue our example, let’s write our own function for calculating the mean:
new.mean <- function(vec){
sum(vec)/length(vec)
}The input into this function is vec, which will be a vector of numbers. The function sums that vector and divides by its length, giving us the mean.
Now we can set vec equal to any vector of numbers and it will calculate the mean:
x <- 1:175
new.mean(vec=x)[1] 88
R will automatically treat what you put in parentheses as the first argument, so we can shortcut this to:
new.mean(x)[1] 88
#To show it works with other things
y <- 4:17
new.mean(y)[1] 10.5
We can put as many arguments as we want into a function. Let’s do a more useful example. I have a locker in Pottruck and the code has 3 numbers picked from a dial of 40. Not every locker is like this though: some dials have 30 numbers, some have 50, and codes can be shorter or longer. Let’s write a function that generates a random combination for any size dial and any length of code.
random.combo <- function(dial, entries){
sample(1:dial, entries, replace=F)
}The function takes two arguments: dial is the number of positions on the dial, and entries is how many numbers are in the combination. Inside, we just use sample() to draw entries numbers from 1:dial, without replacement (because a real lock code doesn’t repeat numbers).
Now we can generate a combination for any lock we like:
set.seed(19104)
#My Pottruck locker
random.combo(dial=40, entries=3)[1] 28 21 37
#Some fancy safe
random.combo(dial=100, entries=5)[1] 51 78 20 97 86
#The tiniest lock in the world
random.combo(dial=10, entries=2)[1] 7 2
Every time we call the function we get a new random combination, because sample() is a random command.
We can use this function to estimate the probability of correctly guessing a locker combination by pure chance. What are the odds that someone who has one guess at my Pottruck locker gets in?
We can first set a true combination:
set.seed(19104)
truth <- random.combo(dial=40, entries=3)
truth[1] 28 21 37
And then generate guesses to see whether each position matches the truth:
guess <- random.combo(dial=40, entries=3)
guess == truth[1] FALSE FALSE FALSE
The all() function returns TRUE only if every entry of a vector is true. That is exactly the condition we want – every position of the guess has to match every position of the truth:
all(guess == truth)[1] FALSE
Now we do this a large number of times, saving whether each attempt matches, and take the mean of the result vector to get the probability:
set.seed(19104)
truth <- random.combo(dial=40, entries=3)
result <- rep(NA, 100000)
for(i in 1:100000){
guess <- random.combo(dial=40, entries=3)
result[i] <- all(guess == truth)
}
mean(result)[1] 1e-05
We get a tiny number, meaning correctly guessing a lock code is very unlikely. Reassuring! We can also confirm this with math: there are \(40 \times 39 \times 38 = 59{,}280\) possible combinations, and only one is correct, so \(P(\text{correct guess}) = 1/59{,}280 \approx 0.0000169\). Note that even doing 100,000 simulations, we only expect around 1 or 2 matches, so simulation values here are going to jump around a bit.
And because we wrote the function generally, we can immediately answer the same question for a lock of any size, or a code of any length, just by changing the arguments.
if() StatementsSometimes we want to say: if a certain condition holds, then run this code. We can do this via if statements. Similar to when we do conditional logic for a single line of code, we start with a logical statement – but in this case a statement that produces a single T or F, and then R will only run the code if the statement is True.
If statements look a lot like for loops, but in the opening bracket we put a conditional statement that is either T or F. If the statement is T R will run what is in the squiggly brackets. If it is false it won’t:
if(2+2==4){
print("I ran code chunk 1")
}[1] "I ran code chunk 1"
if(2+2==5){
print("I ran code chunk 2")
}When we run the first bit of code it runs what is in the squiggly brackets, when we run the second chunk of code it does not run what is in the squiggly brackets.
Right now our code just says: if the statement is true run this code, if it is not, move on. But many times, we want to say: if this statement is true run this code, if it is not, run this other code. In that case we can add an else to our code:
if(2+2==5){
print("I ran code chunk 2")
} else {
print("I ran code chunk 3")
}[1] "I ran code chunk 3"
Because the logical statement at the start is false, R runs the code following the else.
We aren’t limited to just two conditions, we can use else if to add a second logical statement:
if(2+2==5){
print("I ran code chunk 1")
} else if(2+2==4){
print("I ran code chunk 2")
} else {
print("I ran code chunk 3")
}[1] "I ran code chunk 2"
Where this becomes useful in this course is inside a loop, when the code we want to run depends on the outcome of the current iteration. For example, when we simulate the Monty Hall problem in the probability chapter, what a player gets from “switching” depends on whether they initially picked a car or a goat. An if()/else inside the loop lets us handle those two cases differently. We’ll return to this then.