R Basics & The Weighted Dice

Your first hour in R — everything runs in this page, nothing to install

Welcome

Today you are going to build a pair of dice in R — and then quietly weight them so they roll 12 more often than they should.

Along the way you will meet almost everything you need to start using R: arithmetic and objects, vectors, functions and their arguments, writing a function of your own, and repeating a simulation ten thousand times.

How this page works

Every grey box below is a real R console running inside your browser. Type in it, then click Run Code. There is nothing to install and nothing you can break — if a box gets into a mess, reload the page.

The first box may take a few seconds to wake up while R loads. After that it is instant.

Boxes with a Hint will nudge you; Solution shows the answer. Try it yourself before you peek.

1. R is a calculator

At its simplest, R evaluates what you type and prints the answer. Run this box as-is first:

R has the operators you would expect — + - * / ^ — and some you may not. %% gives the remainder after division:

Your turn. Work out how many minutes there are in a week.

Hint

Minutes in an hour, times hours in a day, times days in a week. Start with:

60 * 24

Solution:

60 * 24 * 7

10,080 minutes.

An incomplete command

Run the box below exactly as it is:

You get an error: unexpected end of input. R got to the end of what you gave it and the command still wasn’t finished — you promised it something to subtract and never delivered.

This looks different in RStudio — and you need to recognise both

This page runs each box as a complete chunk, so an unfinished command comes back as an error.

The RStudio console works line by line, so it does something else: it shows you a + prompt and just sits there. That + is R saying “I’m still waiting — you haven’t finished.” It is not an error and nothing has crashed.

When you see a + where you expected an answer, press Escape to cancel, then retype the whole command.

Same cause, two different faces. This is the single most common thing that confuses people in their first week, and now it will not confuse you.

2. Objects

You can store a result in an object with the arrow <-. RStudio’s shortcut for the arrow is Alt + - on Windows, Option + - on a Mac.

Notice that assigning does not print anything. To see an object, type its name.

Once an object exists, you can use it anywhere you would use its value:

Your turn. Create an object called die that holds the numbers 1 through 6, then print it. The colon operator makes a sequence.

Hint

Use the arrow to assign, then type the object’s name on the next line to see it.

die <- ______
die

Solution:

die <- 1:6
die

R is case sensitive: die and Die are two different objects. Names cannot begin with a number, and cannot contain spaces or most symbols.

my_die, die2 and .die are all fine.

2die is not — names cannot start with a number. my die is not — names cannot contain spaces.

3. Vectors do maths all at once

die is not a single number — it is a vector of six numbers. When you do arithmetic on a vector, R does it to every element.

Predict what each line prints before you run it:

That last one may have surprised you. die * die did not do matrix multiplication — it multiplied each element by the element in the same position: 1×1, 2×2, 3×3, and so on. This is called element-wise execution and it is everywhere in R.

Recycling

What if the two vectors are different lengths? R recycles the shorter one until it is long enough.

1:2 became 1 2 1 2 1 2. Useful, and occasionally a trap — if the lengths don’t divide evenly R warns you, but still returns an answer.

Your turn. Use die to produce the numbers 2, 4, 6, 8, 10, 12 without typing them out.

Solution:

die <- 1:6
die * 2

4. Functions

A function is a name followed by parentheses. Whatever you put inside the parentheses is what the function works on.

Functions can be nested — R works from the inside out:

sample()

The function that makes dice possible is sample(). It draws at random from a vector.

Run it several times. You get a different number each time — you have just rolled a die.

Arguments

x and size are the function’s arguments. To find out what arguments a function takes, use args():

round(x, digits = 0) shows that digits has a default value of 0. You only need to supply an argument if you want something other than the default:

If you name your arguments, order does not matter. If you don’t name them, R matches them by position.

round(3.1415, 2), round(3.1415, digits = 2) and round(digits = 2, x = 3.1415) all work.

round(2, 3.1415) does not — unnamed arguments are matched by position, so that rounds the number 2 to 3 decimal places.

5. Rolling two dice

Now roll a pair of dice. Change size to 2 and run it about ten times.

Do you ever see a double — two of the same number?

You won’t. By default sample() draws without replacement: once it has taken the 3, the 3 is gone and cannot come up again. That is not how dice work. Real dice sample with replacement.

Run that several times until you see a double.

Now sum the two dice to get the roll:

Without it the same number cannot be drawn twice, so doubles would be impossible — and a pair of dice that can never roll snake eyes is not a pair of dice.

It has nothing to do with replacing the object die; it is about whether a drawn value goes back in the bag before the next draw.

6. Write your own function

Typing three lines every time you want to roll is tedious. Wrap it in a function of your own. The recipe is always the same:

name <- function(arguments) {
  body of code
}

Here it is for our dice:

Run roll() a few times. You have just built a tool.

Two things to notice:

  • A function returns its last line. That is why sum(dice) comes last.
  • die and dice live inside the function. They do not appear in your environment.

Your turn. Right now roll() can only roll a six-sided die. Rewrite it so the die is an argument with a default of 1:6 — then it can roll anything.

Hint

The argument goes inside the parentheses, with its default value after an = sign. Then use that argument’s name inside sample().

roll2 <- function(die = ______) {
  ...
}

Solution:

roll2 <- function(die = 1:6) {
  dice <- sample(die, size = 2, replace = TRUE)
  sum(dice)
}

roll2()
roll2(die = 1:20)

7. Roll it 10,000 times

One roll tells you nothing. replicate() repeats an expression as many times as you like and collects the results.

Now do it ten thousand times and look at the shape of the results:

A pyramid, peaking at 7. That is what a fair pair of dice looks like: there are six ways to make a 7 and only one way to make a 12.

You can also count the outcomes directly:

8. Weight the dice

Time to cheat.

sample() has one more argument we haven’t used: prob. It takes a vector of probabilities — one for each value — saying how likely each is to be drawn.

A fair die gives every side the same chance, 1/6 each:

Your turn. Load the dice. Make the 6 come up half the time, and split the rest evenly across 1 through 5 — 1/10 each. The six numbers must add up to 1.

Hint

Five sides at 1/10, and the six at 5/10. Check they sum to 1:

sum(c(1/10, 1/10, 1/10, 1/10, 1/10, 5/10))

Solution:

roll <- function() {
  die <- 1:6
  dice <- sample(die, size = 2, replace = TRUE,
                 prob = c(1/10, 1/10, 1/10, 1/10, 1/10, 5/10))
  sum(dice)
}

roll()

Does it work?

Ten thousand rolls of the loaded dice. Compare this histogram with the fair one from Section 7.

The peak has slid to the right and 12 is now common. The mean roll is close to 9, well above the fair 7. If you played this at a casino table, the casino would notice.

12. A 12 requires two sixes, and sixes are now the most common face by far. 7 wins for fair dice, but not for these. Run table(rolls) and see.

9. Your turn

No hints on these. Everything you need is somewhere above.

Challenge 1. Write roll_n(), a function that rolls n fair dice (default 2) and returns their sum.

One solution:

roll_n <- function(n = 2) {
  sum(sample(1:6, size = n, replace = TRUE))
}

roll_n()
roll_n(5)

Challenge 2. Weight a die so that 1 never comes up at all, and the other five faces are equally likely. Roll it 10,000 times and check with table() that no 1s appear.

One solution:

weird <- replicate(10000,
  sample(1:6, size = 1, replace = TRUE,
         prob = c(0, 0.2, 0.2, 0.2, 0.2, 0.2)))

table(weird)

A probability of exactly 0 means that face can never be drawn.

Challenge 3. The weighted dice in Section 8 average about 9. Can you push the average roll all the way to 10? Change the prob values, run 10,000 rolls, check the mean(), and try again. How high could the average possibly go?

What you learned

In under an hour you used:

Idea Looks like
Arithmetic 1 + 1, 2^10, 17 %% 5
Objects die <- 1:6
Vectors 1:6, element-wise math, recycling
Functions sum(), mean(), round(), sample()
Arguments & defaults args(round), round(x, digits = 2)
Writing functions roll <- function() { ... }
Repetition replicate(10000, roll())
Summarising table(), hist(), mean()

That is the core of the language. Everything else is more functions.

Next step: do it again in real R

This page runs a sandboxed R inside your browser. To do this on your own machine — which is where you’ll work from here on — download dice-fallback.R, open it in RStudio, and run it line by line with Ctrl+Enter (Cmd+Enter on a Mac).

Adapted from Garrett Grolemund, Hands-On Programming with R, Chapters 1–3 — free online at rstudio-education.github.io/hopr. Runs on webR via quarto-live.