This is a quick review of the different ways to organize tabular data in R. We'll look at matrices, data frames, tables, and tidyverse data tables.
A matrix is a rectangular array of data. All the data in a matrix must be the same type.
You create a matrix using the matrix function, and specifying some data and a number of rows and/or columns. By default, the data will fill the matrix by columns (i.e. it will fill the first column, then the second column, etc.) You can force it to fill by rows using the byrow parameter.
Here's an example using the Pfizer vaccine trial data:
vactrialMatrix <- matrix(c(162, 18163, 8, 18190), nrow=2)
vactrialMatrix
## [,1] [,2]
## [1,] 162 8
## [2,] 18163 18190
We can add names to the rows and columns:
rownames(vactrialMatrix) <- c("Infected", "Not infected")
colnames(vactrialMatrix) <- c("Placebo", "Vaccine")
vactrialMatrix
## Placebo Vaccine
## Infected 162 8
## Not infected 18163 18190
We can access individual elements of a matrix using the [ ] syntax. Since there are two dimensions (rows and columns), we need to provide two values here; the row first, and then the column. To get the value in the first row and second column, we can do
vactrialMatrix[1,2]
## [1] 8
If we omit one of these values, it will give us the whole row:
vactrialMatrix[1, ]
## Placebo Vaccine
## 162 8
or the whole column:
vactrialMatrix[ , 2]
## Infected Not infected
## 8 18190
What do you think happens if you omit both?
vactrialMatrix[ , ]
You can also access elements by the row and column names, instead of indexes:
vactrialMatrix["Infected", "Vaccine"]
## [1] 8
vactrialMatrix["Infected", ]
## Placebo Vaccine
## 162 8
vactrialMatrix[ , "Vaccine"]
## Infected Not infected
## 8 18190
To reiterate, everything in a matrix must be the same type. If you try to create a matrix of mixed types it will force everything to be the same type:
mixedMatrix <- matrix(c("B6","B6","TH","TH", 1.5, 2.6, 4.1, 3.8), ncol=2)
mixedMatrix
## [,1] [,2]
## [1,] "B6" "1.5"
## [2,] "B6" "2.6"
## [3,] "TH" "4.1"
## [4,] "TH" "3.8"
Note how the numbers in the second column are quoted; they are treated as char types. If you try
mean(mixedMatrix[,2])
## Warning in mean.default(mixedMatrix[, 2]): argument is not numeric or logical:
## returning NA
## [1] NA
it will just give NA, because you can't compute the mean of character data.
A Data Frame also represents a rectangular array of data, but it is organized differently. The data frame stores its data internally as a list of lists, with each individual list representing a column. This means different columns can be different types.
Because the data are represented differently, data frames behave very differently to matrices. The intention of a data frame is that each row should represent an observation, and each row should represent a variable. Data frames are good for representing experimental data.
We can create a data frame "by hand" using the data.frame function:
met <- data.frame(
Strain=c("B6", "B6", "B6", "B6", "TH", "TH", "TH", "TH"),
Diet=c("Chow", "Chow", "HF", "HF", "Chow", "Chow", "HF", "HF"),
Cholesterol=c(37.73, 28.53, 102.81, 100.31, 107.98, 97.55, 206.13, 169.63)
)
met
More usually, we read a data frame from an external file, such as a CSV file. For plain data frames (not using tidyverse), we can use the read.csv function. Note this is not the tidyverse read_csv function:
met <- read.csv("https://denvirlab.marshall.edu/BMR617-2024/data/TH-B6-metabolic.csv")
met
We can access portions of a data frame in the same way we did a matrix:
met[1, 5]
## [1] 37.73
met[1, "Cholesterol"]
## [1] 37.73
met[1:3, ]
met[1:3, c("Cholesterol", "FatMass")]
Because a data frame is represented as a list of its columns (each of which is also a list), and the columns have names, we can also reference individual columns by their names. The way to get an element of something by name in general in R is to use the $ operator. So
met$Cholesterol
## [1] 37.73 28.53 53.99 51.53 102.81 100.31 80.00 132.31 160.00 116.15
## [11] 110.00 117.69 19.23 115.34 11.96 107.98 97.55 89.57 99.23 102.31
## [21] 110.77 206.13 169.63 171.17 149.69 135.89 137.73 153.07 159.82
will extract the Cholesterol column. And then of course we can do things like
mean(met$Cholesterol)
## [1] 107.8662
sd(met$Cholesterol)
## [1] 48.16128
"Tidy" data refers to data that are organized well for statistical analyses. In particular:
Our mouse metabolic data currently meets the first requirement; however it fails on the second requirement. The MouseID column contains two (or three) variables; the mouse Strain and the Diet (and a unique mouse ID).
While the base R data.frame is fairly well-suited to organized data, base R has limited capability for "Data Wrangling" (the practice of organizing data so that it is tidy). We've seen easier ways to do this with the tidyverse package, but here are a couple of ways to do this using plain R.
Try
splitID <- strsplit(met$MouseID, split='-')
splitID
and see what you get.
This splits each value of the MouseID column into pieces, splitting each one at a -. The problem is that the resulting value is not easy to use for what we want: it's a list, each element of which is a char vector containing the three pieces. We need to extract the first of these pieces for the strain, and the second of each pieces for the diet.
The R function sapply(list, function) will apply the specified function to each element of a list, creating a new list containing all the results. It will then simplify the resulting list, if possible (that's the "s" in "sapply"). So
splitID <- strsplit(met$MouseID, split='-')
sapply(splitID, function(x) x[[1]])
## [1] "B6" "B6" "B6" "B6" "B6" "B6" "B6" "B6" "B6" "B6" "B6" "B6" "B6" "B6" "B6"
## [16] "TH" "TH" "TH" "TH" "TH" "TH" "TH" "TH" "TH" "TH" "TH" "TH" "TH" "TH"
will give the first element of each of the splits from the MouseID column. So we can do this:
splitID <- strsplit(met$MouseID, split='-')
met$Strain <- sapply(splitID, function(x) x[[1]])
met$Diet <- sapply(splitID, function(x) x[[2]])
met
Finally, we can use [ ] to reorder the columns (and drop the MouseID column). Make sure you understand how this works:
met <- met[ , c(8, 9, 2:7)]
Let's go back to the contingency table we created as a matrix, for the vaccine trial data. We previously created this as a matrix:
vactrialMatrix <- matrix(c(162, 18163, 8, 18190), nrow=2)
rownames(vactrialMatrix) <- c("Infected", "Not infected")
colnames(vactrialMatrix) <- c("Placebo", "Vaccine")
vactrialMatrix
## Placebo Vaccine
## Infected 162 8
## Not infected 18163 18190
The important thing to note here is that this is not tidy data. In fact, it is not even the raw data set; this is just a summary or a visualization of the data. (You've already seen in this course (and may see more) that this summary is still sufficiently detailed enough to do quite a lot of analysis, even if we don't have the raw data.)
What would this look like as "tidy data"? There are two variables: Treatment (either Placebo or Vaccine) and Infection (either "Infected" or "Not infected", which we could also label as "Yes" or "No", having named the variable.) So there should be two columns.
(Note that in the real data set, there would be more variables, because the study needed to look at whether the vaccine was equally effective across gender, race, age, etc.)
There should also be one row for each observation. Here each observation is a study subject (patient), so there would be a total of 36523 rows.
We could actually reconstruct this as a data frame. There are a total of 18325 subjects who received the placebo, and 18198 who received the vaccine. So our "Treatment" column could just have 18325 values of "Placebo", followed by 18198 values of "Vaccine". We can do this with
Treatment = c(rep("Placebo", 18325), rep("Vaccine", 18198))
Of those receiving the placebo (the first rows in the table), 162 were infected and 18163 were not infected. Of those receiving the vaccine (the last rows in the table), 8 were infected and 18190 were not infected. So our "Infection" column could look like
Infection = c(rep("Yes", 162), rep("No", 18163), rep("Yes", 8), rep("No", 18190))
Put together, we have:
vactrialFrame <- data.frame(
Treatment = c(rep("Placebo", 18325), rep("Vaccine", 18198)),
Infection = c(rep("Yes", 162), rep("No", 18163), rep("Yes", 8), rep("No", 18190))
)
It's not a good idea to view this in the console, but we can look at the frst few rows:
vactrialFrame[1:20, ]
So while this looks like a strange thing to do, this is actually the kind of data we would start with in real life, if we were working for Pfizer analyzing the trial data. Again, the table would have many more columns, but it would include these.
What we would then want to do is create a contingency table. In R, where a data.frame represents a data table, the table type represents a contingency table. We can create a contingency table from the vactrialFrame data frame (which, again, in real life is our starting point) with
vactrialContingency <- table(vactrialFrame)
vactrialContingency
## Infection
## Treatment No Yes
## Placebo 18163 162
## Vaccine 18190 8
We can add "margins" (i.e. row and column sums) to this if we want:
addmargins(vactrialContingency)
## Infection
## Treatment No Yes Sum
## Placebo 18163 162 18325
## Vaccine 18190 8 18198
## Sum 36353 170 36523
Note that you usually don't want to include the margins in any analysis.