read_csv("data/my_csv_data.csv")
write_csv(my_r_data, "data/my_NEW_csv_data.csv")Packages for Quantitative (and some Qualitative) Evaluations in HE
— THIS RESOURCE IS CURRENTLY IN DEVELOPMENT —
This document has a list of packages that may be useful to someone doing quantitative (or qualitative) evaluations in R. It is separated broadly into functions for data wrangling, descriptive statistics, inferential statistics, and working with qualitative data.
It is designed as a reference guide, so while you are welcome to read through in order, you might find it more useful to refer to the sections on the right as-and-when you need to do various processes in R.
Data Wrangling
Read and Write Data
The most important thing to remember when reading and writing data is to know where you’re reading and writing to/from. The easiest way to do this is by using rprojects. Using rprojects means that you don’t have to worry about the directory, and also makes it easier to work collaboratively with colleagues.
csv:
readrfrom thetidyverseallows us to read and write to csv (comma separated values), e.g.Excel:
readxlto read Excel files. It allows you to read in from different worksheets within an Excel file, and gives various other read options.writexlthen allows you to write data to an Excel file, e.g.library(readxl) library(writexl) read_excel("data/my_excel_data.xlsx", sheet = "my important sheet") write_xlsx(my_data, "data/my_NEW_excel_data.xlsx")SPSS, Stata, SAS:
havenfor SPSS, Stata, and SAS data.Havenallows us to read and write to these file typeslibrary(haven) path <- system.file("data", "my_spss_data.sps", package = "haven") read_sav(path)googlesheets:
googlesheets4for Google Sheets via the Sheets API v4.read_sheet()can either read from the full URL, or just from the sheet IDlibrary(googlesheets4) read_sheet("https://docs.google.com/spreadsheets/d/1UDiI4Uue3epJS1gZWbWXadVbMbB12VWFQLdgcWG8BEY/edit#gid=0") read_sheet("1UDiI4Uue3epJS1gZWbWXadVbMbB12VWFQLdgcWG8BEY")Data from more advanced sources can also be read into R. The most common of these include:
If you want to store some data to be used again in R, you can use the save() function to save it as an .RData file. .RData formats are the most convenient if you know you will be (only) using the data again in R.
Rename variables
Sometimes (read: usually) real data is messy. And a big part of this is having variable names that may a) not mean anything, b) contain “illegal” characters, and/or c) don’t follow a standard naming convention. We can therefore use the rename() function to take variable names and give them a more sensible name.
For example:
library(dplyr)
my_data <- my_data %>%
rename(
student_id = `Student ID`,
course_name = `CAH name 1`,
cohort = Cohort,
programme_year = `Year of programme`,
ethnicity = Ethnicity,
polar4 = `POLAR4 quintile`,
sex = Sex,
disability_status = `Disability status`,
entry_quals = `Entry qualifications`,
tariff_points = `Tariff points`,
status = Status
)
glimpse(my_data)Rows: 23,046
Columns: 22
$ student_id <chr> "SU00001", "SU00002", "SU00002", "SU000…
$ cohort <chr> "2022/23 entry", "2019/20 entry", "2019…
$ `Academic year` <dbl> 2022, 2019, 2020, 2021, 2019, 2020, 202…
$ `Entrant?` <chr> "Entrant", "Entrant", "Continuer", "Con…
$ sex <chr> "Female", "Female", "Female", "Female",…
$ ethnicity <chr> "White", "White", "White", "White", "Wh…
$ polar4 <chr> "4", "1", "1", "1", "1", "1", "1", "1",…
$ disability_status <chr> "N", "N", "N", "N", "N", "N", "N", "N",…
$ `Disability type` <chr> "No disability reported", "No disabilit…
$ `Age group` <chr> "Young", "Young", "Young", "Young", "Yo…
$ `Commuter flag` <chr> "N", "N", "N", "N", "N", "N", "N", "N",…
$ Domicile <chr> "England", "England", "England", "Engla…
$ Region <chr> "West Midlands", "East of England", "Ea…
$ tariff_points <dbl> 100, 276, 276, 276, 124, 124, 124, 124,…
$ `Level of qualification` <chr> "Bachelors' degrees, graduate certifica…
$ `CAH code 1` <chr> "CAH17-01-01", "CAH19-01-03", "CAH19-01…
$ course_name <chr> "Business and management (non-specific)…
$ status <chr> "Active registration", "Active registra…
$ programme_year <dbl> 1, 1, 2, 3, 1, 1, 2, 3, 1, 2, 1, 1, 2, …
$ `Degree classification` <chr> "NA", "NA", "NA", "FIRST", "NA", "NA", …
$ `Degree classification detail` <chr> "Classification NA/unknown", "Classific…
$ entry_quals <chr> "A levels only", "A levels only", "A le…
Separate each variable name with a comma (,). I tend to do each variable on a different line so it’s easier to read at a glance, but you don’t have to.
If you want to rename variables and want to get rid of unwanted variables, you can also treat the select() function like rename() (see the `Remove unwanted variables (columns)](#sec-removevars) section. Using this function you can include all the variables you want to keep, and rename only the ones that you want to rename.
For example, if we only wanted to keep student_id and cohort, and we want to rename cohort as entry year, we could use the following:
library(dplyr)
my_data %>%
select(
student_id,
entry_year = cohort) %>%
glimpse()Rows: 23,046
Columns: 2
$ student_id <chr> "SU00001", "SU00002", "SU00002", "SU00002", "SU00003", "SU0…
$ entry_year <chr> "2022/23 entry", "2019/20 entry", "2019/20 entry", "2019/20…
This time, we have got rid of all variables except student_id and cohort, and we’ve renamed cohort to entry_year as this is more easily understandable. (not that we didn’t assign this change to the my_data object, so that code hasn’t changed any of our data).
Remove unwanted variables (columns)
Sometimes we have a dataset with 50+ variables. We might want these later, so we do need them in the dataset, but we don’t need them for what we’re working on right now. the select() function from dplyr allows us to choose only those variables we want to keep to hand.
Remember, nothing we’re doing here overwrites the original datafile that we read in, so the original data still remains, untouched and unharmed in its folder.
we can use select() a few different ways, the easiest is to explicitly say which variables (columns) should be kept. We can separate each variable we want to keep with a comma (,). I tend to put each variable on a new row for readability, but this is personal preference. In this example, we want to keep all variables except for status, which we don’t need for these (hypothetical) analyses.
We can also use select to rename our variables with more meaningful names (see Rename variables section). As we have already done this for most variables, we can just change our cohort variable to entry_year, which we will now assign to the my_data object.
library(dplyr)
my_data <- my_data %>%
select(
student_id,
course_name,
entry_year = cohort,
programme_year,
ethnicity,
sex,
disability_status,
polar4,
entry_quals,
tariff_points,
status
) %>%
glimpse()Rows: 23,046
Columns: 11
$ student_id <chr> "SU00001", "SU00002", "SU00002", "SU00002", "SU00003…
$ course_name <chr> "Business and management (non-specific)", "Literatur…
$ entry_year <chr> "2022/23 entry", "2019/20 entry", "2019/20 entry", "…
$ programme_year <dbl> 1, 1, 2, 3, 1, 1, 2, 3, 1, 2, 1, 1, 2, 3, 1, 2, 3, 1…
$ ethnicity <chr> "White", "White", "White", "White", "White", "White"…
$ sex <chr> "Female", "Female", "Female", "Female", "Male", "Mal…
$ disability_status <chr> "N", "N", "N", "N", "N", "N", "N", "N", "N", "N", "Y…
$ polar4 <chr> "4", "1", "1", "1", "1", "1", "1", "1", "3", "2", "4…
$ entry_quals <chr> "A levels only", "A levels only", "A levels only", "…
$ tariff_points <dbl> 100, 276, 276, 276, 124, 124, 124, 124, 148, 124, 10…
$ status <chr> "Active registration", "Active registration", "Activ…
If we only want to remove a couple of variables, there’s an easier way, however! Instead of listing everything we want to keep, we can just tell select() what we want to remove. We can indicate that we want to remove it by prefixing with a minus (-) symbol. For example if we now decided that we also want to remove status, we could simply run the following code:
library(dplyr)
my_data <- my_data %>%
select(-status)
glimpse(my_data)Rows: 23,046
Columns: 10
$ student_id <chr> "SU00001", "SU00002", "SU00002", "SU00002", "SU00003…
$ course_name <chr> "Business and management (non-specific)", "Literatur…
$ entry_year <chr> "2022/23 entry", "2019/20 entry", "2019/20 entry", "…
$ programme_year <dbl> 1, 1, 2, 3, 1, 1, 2, 3, 1, 2, 1, 1, 2, 3, 1, 2, 3, 1…
$ ethnicity <chr> "White", "White", "White", "White", "White", "White"…
$ sex <chr> "Female", "Female", "Female", "Female", "Male", "Mal…
$ disability_status <chr> "N", "N", "N", "N", "N", "N", "N", "N", "N", "N", "Y…
$ polar4 <chr> "4", "1", "1", "1", "1", "1", "1", "1", "3", "2", "4…
$ entry_quals <chr> "A levels only", "A levels only", "A levels only", "…
$ tariff_points <dbl> 100, 276, 276, 276, 124, 124, 124, 124, 148, 124, 10…
Filter out unwanted data (rows)
filter()from thedplyrpackage intidyverseis the most useful function here. You will likely use this function in nearly all of your scripts (I do!). To use it there are some operators that are useful to know.
The most useful are:
==: “equal to”>/>=: “greater than” / “equal to or greater than”</<=: “less than” / “equal to or less than”!=: exclamation marks mean ‘NOT’, so this means “not equal to”&: “AND” - useful if something needs to meet multiple conditions|: “OR” - useful if you want to filter if any of multiple conditions are metgrepl()- “regular expressions” - checks if a particular text pattern exists in a string (e.g. we could find all courses with “Business” in the name)
In a nutshell, filter() will only keep rows the meet a certain condition (i.e. if something is TRUE), e.g. let’s say we want to include only students from POLAR 4 Quintile 1 who are Black or Asian:
library(dplyr)
my_data %>%
filter((ethnicity=="Black"|ethnicity=="Asian")&polar4==1) %>%
glimpse()Rows: 995
Columns: 10
$ student_id <chr> "SU00008", "SU00008", "SU00008", "SU00010", "SU00031…
$ course_name <chr> "Biomedical sciences (non-specific)", "Biomedical sc…
$ entry_year <chr> "2019/20 entry", "2019/20 entry", "2019/20 entry", "…
$ programme_year <dbl> 1, 2, 3, 1, 1, 1, 2, 1, 2, 3, 1, 1, 2, 3, 1, 2, 3, 1…
$ ethnicity <chr> "Asian", "Asian", "Asian", "Asian", "Asian", "Asian"…
$ sex <chr> "Female", "Female", "Female", "Male", "Male", "Male"…
$ disability_status <chr> "Y", "Y", "Y", "N", "N", "N", "N", "N", "N", "N", "N…
$ polar4 <chr> "1", "1", "1", "1", "1", "1", "1", "1", "1", "1", "1…
$ entry_quals <chr> "Mix with A levels, no BTEC", "Mix with A levels, no…
$ tariff_points <dbl> 148, 148, 148, 100, 116, 132, 132, 110, 110, 110, 12…
This code can be interpreted as telling R: “keep any data where the student’s ethnicity is Black or Asian and where their POLAR4 Quintile is 1”.
Notice the brackets around (ethnicity=="Black"|ethnicity=="Asian"). Remember back to when you did BODMAS1. First we evaluate the brackets and find all the students who are Black or Asian… and then we do the rest of the statement.
Calculate a New Variable
The mutate() function from the dplyr package is your friend here. Using mutate() we can either create a new variable or overwrite an existing variable. We can create new variables from: - mathematical equations using existing data - an example might be if we have grades for each module and we want to calculate the weighted average (mean) / overall grade for the year - pattern matching in existing text - an example might be if we have granular detail entry level qualifications, but we want to categorise these as simply A-Levels and BTEC+ (students doing either all BTECs or a combination of BTEC and other qualification(s))
In the above example, we are mindful that the university does not know the entry qualifications for some students (these are categorised as Unknown). Some further students entered with non-BTEC and non-A-level qualifications. We don’t want to categorise these as either A-levels or BTEC+, as they could be either, so we will instead remove them later. Using unique() we can see what entry qualifications exist:
unique(my_data$entry_quals) [1] "A levels only" "Mix with A levels, no BTEC"
[3] "Mix with BTEC, no A levels" "Mix with BTEC and A levels"
[5] "BTEC only" "IB only"
[7] "Unknown" "Mix with BTEC, no A level"
[9] "Other quals only" "Mix no BTEC or A levels"
[11] "hello"
We can therefore use the case_when() function from dplyr to find students whose entry qualifications meet our criteria. We can read this as “in the case when X=Y, assign Z value)
library(dplyr)
my_data %>%
mutate(entry_quals = case_when(
entry_quals=="A levels only" ~ "A-levels",
grepl("only BTEC|Mix with BTEC", entry_quals) ~ "BTEC+",
.default = "Other"
)) %>%
glimpse()Rows: 23,046
Columns: 10
$ student_id <chr> "SU00001", "SU00002", "SU00002", "SU00002", "SU00003…
$ course_name <chr> "Business and management (non-specific)", "Literatur…
$ entry_year <chr> "2022/23 entry", "2019/20 entry", "2019/20 entry", "…
$ programme_year <dbl> 1, 1, 2, 3, 1, 1, 2, 3, 1, 2, 1, 1, 2, 3, 1, 2, 3, 1…
$ ethnicity <chr> "White", "White", "White", "White", "White", "White"…
$ sex <chr> "Female", "Female", "Female", "Female", "Male", "Mal…
$ disability_status <chr> "N", "N", "N", "N", "N", "N", "N", "N", "N", "N", "Y…
$ polar4 <chr> "4", "1", "1", "1", "1", "1", "1", "1", "3", "2", "4…
$ entry_quals <chr> "A-levels", "A-levels", "A-levels", "A-levels", "A-l…
$ tariff_points <dbl> 100, 276, 276, 276, 124, 124, 124, 124, 148, 124, 10…
Edit a Variable
As with creating a new variable, we can use the mutate function here.
For example, using the unique() function, we can see that each entry_year value has ‘entry’ at the end:
unique(my_data$entry_year)[1] "2022/23 entry" "2019/20 entry" "2021/22 entry" "2020/21 entry"
We might decide this is extra text that isn’t useful. We can use mutate() to edit the variable, and then str_remove() (part of the stringr package in tidyverse to remove the unwanted part of the string, e.g.
library(dplyr)
library(stringr)
my_data %>%
mutate(entry_year = str_remove(entry_year, " entry")) %>%
glimpse()Rows: 23,046
Columns: 10
$ student_id <chr> "SU00001", "SU00002", "SU00002", "SU00002", "SU00003…
$ course_name <chr> "Business and management (non-specific)", "Literatur…
$ entry_year <chr> "2022/23", "2019/20", "2019/20", "2019/20", "2019/20…
$ programme_year <dbl> 1, 1, 2, 3, 1, 1, 2, 3, 1, 2, 1, 1, 2, 3, 1, 2, 3, 1…
$ ethnicity <chr> "White", "White", "White", "White", "White", "White"…
$ sex <chr> "Female", "Female", "Female", "Female", "Male", "Mal…
$ disability_status <chr> "N", "N", "N", "N", "N", "N", "N", "N", "N", "N", "Y…
$ polar4 <chr> "4", "1", "1", "1", "1", "1", "1", "1", "3", "2", "4…
$ entry_quals <chr> "A levels only", "A levels only", "A levels only", "…
$ tariff_points <dbl> 100, 276, 276, 276, 124, 124, 124, 124, 148, 124, 10…
Join two datasets together
…with the same variables but additional students
Let’s say we have 3 spreadsheets of data; one from 2020/21, one from 2021/22, and one from 2022/23. These datasets have the same data, but new students, so we just want to append the most recent years to the earliest year.
We can do this using the rbind() function.
A couple of cautions with this, though.
First, you want to make sure that your variables in each dataset are the same, and in the same order, otherwise your join won’t make sense. We can force this using select() from dplyr.
Second, if you need to know which data is from 2020/21 (etc.) and you don’t already have a variable (column) that does this, you may want to create one before you join the data. We can do this using mutate() from dplyr. For example:
library(dplyr)
my_data_202021 <- my_data_202021 %>%
select(student_id, course_id, dob, sex, ethnicity, imd, tundra, commuter) %>%
mutate(academic_year = "2020/21")
my_data_202122 <- my_data_202122 %>%
select(student_id, course_id, dob, sex, ethnicity, imd, tundra, commuter) %>%
mutate(academic_year = "2021/22")
my_data_202223 <- my_data_202223 %>%
select(student_id, course_id, dob, sex, ethnicity, imd, tundra, commuter) %>%
mutate(academic_year = "2022/23")
my_data_all <- rbind(my_data_202021, my_data_202122, my_data_202223)…with the same students but additional variables
Data Visualisations
ggplot2 is the package that is most useful for data viz. Part of the tidyverse package, so you don’t need to install it separately.
The ggplot() is infinitely tailor-able, but in my opinion the easiest way to get started is to tell R what values we want on the X and Y axes, and whether we want to group by any other groups. These are the aesthetics - represented in ggplot() as aes()).
in ggplot(), we add (+) layers (And we can add as many as we want!), so once you’ve specified your aesthetics, you can then tell R what you want it to do with those aesthetics. Plotting data on the graph almost always uses functions that begin with geom_. Here, we’re asking R to add violin plots (geom_violin()), and then jitter points (geom_jitter()).
We finish by telling R what theme we want to use. I find that theme_bw() is an uncluttered layout for quick-and-dirty plotting). you can tailor the theme to your heart’s content, and in fact the BBC, Financial Times, and The Economist all use ggplot2 for their data visualisations and have created their own themes using ggplot2.
library(ggplot2)
my_data %>%
ggplot(aes(x = ethnicity, y = tariff_points, colour = ethnicity)) +
geom_violin(fill = "grey") +
geom_jitter(alpha = .1, width = .2) +
theme_bw() +
labs(
x = "Ethnicity",
y = "Tariff Points"
)Descriptive Statistics
to be completed…
The tidyverse is a package of packages that make data wrangling in R (relatively) easy! It’s made up of:
dplyr
Solves most data manipulation challenges. The function in this - filter() - remove certain rows from the dataset that we don’t want to include - glimpse() - take a quick look at our dataset - mutate() - either edit the values of an existing variable (column) or create a new one - select() - make your dataset more manageable by removing the rows you don’t need - group_by() - group specific columns together, which is often used with… - summarise() - we we can crunch some numbers on those groups we created (a bit like a pivot table) - join_by() - join two datasets together using a common variable (e.g. student id).
Inferential Statistics
Base R has a lot of inferential statistics built in, but thousands of other packages exist that allow us to do inferential statistics beyond our wildest dreams. We’re going to focus here on frequentist inferential statistics, which are most often used by social scientists and social evaluators.
This section will not explain the theory behind different statistical tests, nor how to interpret them. Its aim is to give an insight into the functions and packages that can do the analyses, but I encourage evaluators to ensure they are fully conversant in the assumptions, uses, and interpretation of these tests before using them. Failure to use statistics appropriately can be more harmful than not using them at all.
Pearson’s Correlation
to be completed…
t-test
to be completed…
Chi-Square
to be completed…
Linear Regression
also known as the General Linear Model, or simply, the Linear Model, Base R has an inbuilt function that allows us to do this simply - lm().
Say we want to predict students tariff points from their sex and whether they have disclosed a disability.
Remember our figure earlier in Data Visualisations - we had a lot of students whose tariff points were entered as zero. These are in fact students who did not enter using the traditional route, so using these zero values would be misleading. We also know from inspecting our data that some students did not wish to disclose their gender, and some identified as ‘Other’ gender. We therefore first want to remove those values.
After that, we need to supply to R is the data we want to use, and the formula.
The formula is written as: dependant_variable ~ predictor1 + predictor2
We can read the tilde (~) as ‘is predicted by’, and the plus (+) as ‘and’. We would therefore read this formula as: dependent_variable is predicted by predictor1 and predictor2
In this example the code would look like this:
library(dplyr)
my_data_filtered <- my_data %>%
filter(tariff_points>0) %>%
filter(sex!="Information refused") %>%
filter(sex!="Other")
lm(data = my_data_filtered,
formula = tariff_points ~ sex + disability_status)
Call:
lm(formula = tariff_points ~ sex + disability_status, data = my_data_filtered)
Coefficients:
(Intercept) sexMale disability_statusY
131.819 -5.138 -1.065
We can see that this gives us an output, but it’s not the usual regression output that we expect. Instead, we can use the summary() function (also from Base R) to convert this into language that we understand!
library(dplyr)
tariff_model <- lm(data = my_data_filtered,
formula = tariff_points ~ sex + disability_status)
summary(tariff_model)
Call:
lm(formula = tariff_points ~ sex + disability_status, data = my_data_filtered)
Residuals:
Min 1Q Median 3Q Max
-111.819 -18.681 -2.681 16.181 188.181
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 131.8186 0.2909 453.074 <2e-16 ***
sexMale -5.1378 0.3859 -13.313 <2e-16 ***
disability_statusY -1.0653 0.4775 -2.231 0.0257 *
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 28.72 on 22419 degrees of freedom
Multiple R-squared: 0.007876, Adjusted R-squared: 0.007787
F-statistic: 88.99 on 2 and 22419 DF, p-value: < 2.2e-16
Logistic Regression
to be completed…
ANOVA
psychhas all manner of inferential statistical tests to be completed…
Assumption Checking
- The
performancepackage2 can be used to check the assumptions of Linear Regression:
library(performance)
check_model(tariff_model)- The
carpackage is useful for…
Power calculations
pwrto be completed…
Propensity Score Matching
Matchitto be completed…
Regression Discontinuity
didto be completed…
Instrumental Variables
AERto be completed…
Footnotes
Brackets, Of, Division, Multiplication, Addition, and Subtraction - to indicate the order of operations. Other acronyms exist, and include BIDMAS and PEDMAS, but which mean the exact same thing.↩︎
this package requires the package
seeto be installed as well. You don’t need to load it, but it does need to be installed on your computer.↩︎