Learning Objectives:
One of the most useful ways to plan out studies is to generate a dataset to simulate what your study results might look like. In this workshop, we will learn how to create fake/simulated datasets in R, starting with a simple dataset that shows a main effect.
First, we need a way to “randomly” generate values to generate our columns. The most basic one is to randomly pull numbers from a pre-defined range:
## If you want linearly distributed data
ranged_data <- runif(n = 100, # generate 100 numbers
min = 0, # lower limit
max = 1) # upper limit
head(ranged_data)
## [1] 0.6243274 0.5910085 0.1564275 0.7399310 0.2745761 0.3073877
hist(ranged_data)
Here in the histogram, we see that it’s not quite linear because there is still a lot of random variation if you just generate n=100 values. As n approaches infinity, we will start to see the true shape of the distribution we drew from (in this case, because it is truly random, it should be flat):
ranged_data <- runif(n = 100000, # generate 100000 numbers
min = 0, # lower limit
max = 1) # upper limit
hist(ranged_data)
Linear, i.e. truly random distributions, are very uncommon in a real dataset. A more common distribution to draw your data from is a normal distribution, as many common measures will naturally resemble a normal distribution. Normal distributions require two parameters: the group mean, and the standard deviation. Let’s pretend we are generating some midterm test scores of a PSYC217 class, where the class average was 67% with a standard deviation of 10%:
# Normal distribution
norm <- rnorm(n = 200, # Generate 100 numbers
mean = 67, # Class average/mean
sd = 10) # standard deviation
hist(norm)
We get something pretty close to a normal distribution (or,
colloquially, a bell curve).
One way we can streamline our data generation workflow is to “set” our variables instead of putting the actual number in the function. For example, let’s start by rewriting our function above as follows:
n <- 200
class_average <- 67
class_sd <- 10
norm <- rnorm(n = n, # Generate 100 numbers
mean = class_average, # Class average/mean
sd = class_sd) # standard deviation
What we are doing here is asking the function to refer to some values
we can change around more easily, and use over and over again in
other functions. For example, instead of saying
mean = 67, we tell the function to refer to a value we
named class_average to find the mean (which we set in an
earlier code as 67). Now, if we ever want to adjust this simulation,
instead of going into the actual function, we can just adjust these
values. For instance, say I instead want to simulate a dataset with a
class average of 70:
n <- 200
class_average <- 70 # I just need to change this
class_sd <- 10
norm <- rnorm(n = n,
mean = class_average, # Notice this part doesn't change
sd = class_sd)
Of course, sometimes we want to randomly generate values that are not
numbers. We can use the function sample():
sex <- sample(c("M", "F"), # these are the values to randomly sample from
size = 200, # This is how many samples you want to draw
replace = T) # This specifies that the same value can be used more than once
Now we know how to make random sets of values and numbers. How do we
put it together into a dataset? The simplest way is to think of these
values as columns in a dataset, and all we are doing is joining them
together with the data.frame() function. Let’s start simple
with a dataset of two columns: subject_id and whether the subject is M
or F:
data <- data.frame(
id = 1:200, # This just means make a list of numbers from 1 to 200
sex = sample(c("M", "F"),
size = 200,
replace = T)
)
head(data)
Before we start adding more columns, let’s make sure we use what we learned before. Specifically, we want to make sure that any variables that our functions/columns have in common are referred to as a value and not a number. For instance, instead of writing the number 200 every time we want to specify how many generations we want, let’s create a new variable called sample_size:
sample_size <- 200
data <- data.frame(
id = 1:sample_size, # This just means make a list of numbers from 1 to 200
sex = sample(c("M", "F"),
size = sample_size,
replace = T)
)
head(data)
Let’s continue and make more columns. Suppose we want to simulate a study to test if a drug improves sleep quality. What columns will we need?
sample_size <- 200
mean_before <- 5
sd_before <- 1
mean_after <- 6
sd_after <- 1
data <- data.frame(
id = 1:sample_size,
sex = sample(c("M", "F"),
size = sample_size,
replace = T),
condition = sample(c("drug", "placebo"),
size = sample_size,
replace = T),
sleep_before = rnorm(n = sample_size,
mean = mean_before,
sd = sd_before) |>
round(), # Here we are just piping the output into the round() function to get rid of decimals
sleep_after = rnorm(n = sample_size,
mean = mean_after,
sd = sd_after) |>
round() # Same here
)
Great! We now have a dataset. But notice there is a problem: our data generation is NOT contingent on our condition at all. In other words, regardless of whether the participant is in the drug or placebo condition, they are pulled from the exact same distribution. How do we solve that?
The easiest way to do this is to simply generate your conditions separately. In other words, let’s make two different datasets: one for drug, and one for placebo. The drug one could look something like this:
drug_sample_size <- 100
drug_mean_before <- 5
drug_mean_after <- 6
drug_sd <- 1
drug <- data.frame(
id = 1:drug_sample_size,
sex = sample(c("M", "F"),
size = drug_sample_size,
replace = T),
condition = sample(c("drug"),
size = drug_sample_size,
replace = T),
sleep_before = rnorm(n = drug_sample_size,
mean = drug_mean_before,
sd = drug_sd) |>
round(),
sleep_after = rnorm(n = drug_sample_size,
mean = drug_mean_after,
sd = drug_sd) |>
round()
)
Before we move on, try to generate the placebo one yourself! Suppose the placebo sleep quality before has a mean of 5, and after placebo it stays at 5 (since we wouldn’t expect an improvement). Look at the answer below when you’re ready:
placebo_sample_size <- 100
placebo_mean_before <- 5
placebo_mean_after <- 5
placebo_sd <- 1
placebo <- data.frame(
id = 1:placebo_sample_size,
sex = sample(c("M", "F"),
size = placebo_sample_size,
replace = T),
condition = sample(c("placebo"),
size = placebo_sample_size,
replace = T),
sleep_before = rnorm(n = placebo_sample_size,
mean = placebo_mean_before,
sd = placebo_sd) |>
round(),
sleep_after = rnorm(n = placebo_sample_size,
mean = placebo_mean_after,
sd = placebo_sd) |>
round()
)
Now, we can simply put the two datasets together with
rbind(). This function essentially stacks two or more
datasets with the same column names on top of each other:
data <- rbind(drug, placebo)
Using what we learned about computing descriptive statistics, verify that our dataset reflects what we want to illustrate (i.e. drug sleep quality improved from 5 to 6, on average, but placebo stayed at 5)
descriptives <- data |>
group_by(condition) |>
summarize(mean_before = mean(sleep_before),
mean_after = mean(sleep_after))
descriptives # Note: It might not be perfectly at 5 and 6 since our sample is only 100 per condition
# But is it close enough?