mandarin <- read.csv("/Users/Thomas/Library/CloudStorage/OneDrive-YorkUniversity/LING 3300/2025-26 Winter/Datasets/mandarin_f0.csv", header = TRUE)
Remember - here you should change the path that’s between the ” ” so that it points to where on YOUR computer the .csv is. If you don’t know how to do this easily, Google “how to find the path name of a file” + your operating system (Mac, Windows 10, etc.). This will be a very valuable skill going forward, so master it now!
Also remember that the reason we put header = TRUE is so that R knows that the first line of our csv is the names of our columns, not data itself.
In general, it’s useful to know how to install and update packages. Packages are bundles of code that you can import into R. They’re sort of like software applications that you install and then open up when needed. They give you way more coding options and power than just using the stuff that R comes pre-loaded with - imagine buying an iPad and then not downloading any new apps to it! That wouldn’t be very fun.
One package we’ll be using today is called tidyverse. Actually, the tidyverse contains several R packages that are incredibly useful for data analysis and visualization. Think of it like “Microsoft Suite” (which I guess is what we used to call “Microsft Office”) – it’s a collection of pieces of software to do certain specific things, like Word, PowerPoint, Excel, etc. By installing and loading tidyverse, you’re availing yourself of a bunch of individual specialized packages including ggplot2, dplyr, tidyr, readr, and some others. The tidyverse is also a philosophy for maintaining datasets and general data and code hygiene; for instance, it allows us to construct our code in a different way from the default R language (which we call “Base R”).
If you’re connected to the internet, you can install/update a package with the following code:
install.packages("tidyverse")
You only have to run this code once on your computer, and then the package is installed. It’s just like when you install an app on your computer – you don’t have to re-download it every time you use it! So I don’t advise keeping this line of code in your scripts or re-running it every time you open R.
However you’re not done yet! You now “own” the package but you still need to pull the package “off the shelf” to use the code. You can import the package with the library() function. The function require() does the same thing.
library(tidyverse)
# or
require(tidyverse)
Yes, you do have to open each package every time you restart R and want to run your code again! That means that most of your scripts will begin with two bits of code: reading in the data you want to work with and reading in the packages you need using library().
Alternatively, you can click the “Packages” tab in the lower righthand corner of RStudio, and check the box for the packages you want to use.
Don’t worry if some warnings come up when you load in packages or run other code. They just contain information like the version of R that they were built in. You usually won’t have to worry about this. “Warnings” are often fine, while “errors” are almost always bad.
One of the most useful packages within the tidyverse is dplyr. If you’ve already loaded in the whole tidyverse using library(tidyverse), then dplyr has been loaded and you’re ready to go! In the future, though, if you’d like you you can load the individual packages within tidyverse instead of everything at once. In the case of dplyr, you’d run:
library(dplyr)
dplyr has a few very useful functions:
Note that there are also ways to do all of these things in Base R, but dplyr code is sometimes a little more logical and streamlined.
You can select just a subset of the rows in the dataset using the filter() function. This works really similarly to the Base R subset() function; in fact, in this simple case, they look basically identical. Let’s create a new dataframe (which will show up in our Environment at upper right) with just the Mandarin F0 observations from the vowel marked “i4” – that is, the vowel /i/ in Tone 4.
mandarin_i4 <- filter(mandarin, vowel == "i4")
View(mandarin_i4)
Dplyr lets you use what’s called a pipe, which is written like this: %>%. Well, actually, |> is also a way of typing the pipe using Base R, so you can feel free to use either of them, and some people prefer |>. Whatever floats your boat.
The pipe allows for an assembly line of functions: you start with the original dataset, then apply a function to that dataset. Using %>% or |> allows you to think of your code as a sequence of actions.
For instance, the two sets of code below do the exact same thing:
mandarin_i4 <- filter(mandarin, vowel == "i4")
mandarin_i4 <- mandarin %>%
filter(vowel == "i4")
In the first set of code, we type out the “verb” – in this case, filter() – then within the parentheses we define the dataframe of interest as mandarin, followed by an operation on the variable Level. In the second set, we type out the “topic” – in this case, our dataframe mandarin – and then after the pipe we tell it what “verb” we want to operate on the data – in this case, filtering based on a particular variable called vowel. In both cases, the leftmost part of the code will be the name we want our new dataframe to be called, plus the assign <- function.
You may find that you sometimes prefer using Base R syntax and functions for some things and prefer to use dplyr’s functions for other things. It’s fine to mix and match!
Note: %>% or |> should always have a space before it, and should usually be followed by a new line. After the first step, each line should be indented by two spaces. This structure makes it easier to add new steps (or rearrange existing steps) and harder to overlook a step.
Typing %>% over and over can be tedious! Thankfully, RStudio provides a keyboard shortcut for inserting the pipe operator into your R code.
On Mac type shift + command + m.
On Windows type shift + control + m.
It may not seem totally intuitive at first, but this shortcut can handy once you get used to it.
In this dataset, the column “file” is quite rich in information. It looks like maybe the three numbers refer to a participant number. The “M” looks like it could be a gender marker. The final three letters look like they could be some sort of condition. What if we could have those all as their own columns? With the str_sub() function, they can!
We’ll make a new column “participant” that will be all letters from the 5th to the 7th character in the “file” column…
mandarin$participant <- str_sub(mandarin$file, 5, 7)
…and a new column “condition” that will be all letters from 7th-to-last to the 5th-to-last character in the “file” column…
mandarin$condition <- str_sub(mandarin$file, -7, -5)
…and a new column “gender” that will be just the 9th character in the “file” column.
mandarin$gender <- str_sub(mandarin$file, 9, 9)
It also looks like the data in the vowel column is really a vowel plus a tone number. Let’s make a tone column from just the last character of the vowel column…
mandarin$tone <- str_sub(mandarin$vowel, -1,-1)
…and let’s overwrite the vowel column with just its first to the second-to-last character – in other words, we’ll delete the final character.
mandarin$vowel <- str_sub(mandarin$vowel, 1,-2)
Nice, now we have some more rational columns.
Allowing multiple pipes brings us to one of the most useful sequences of functions: group_by() %>% summarize()
SUPER USEFUL!
Remember the summary() function from the first tutorial? Well, that was useful, but let’s say we wanted to get the mean duration rating for each of the vowels in the Mandarin F0. In Base R, we would have to create a ton of separate subsets of the data, and run the mean() (or summary()) function on each subset. That’s effortful. Instead, we can use the group_by() function to create subsets, and then derive the mean of the dur column using the summarize() function (or summarise(), which also works).
In this example, we’ll store these means in a new dataset called duration_means:
duration_means <- mandarin %>%
group_by(vowel) %>%
summarise(mean_dur = mean(dur))
View(duration_means)
The above line of code takes the mandarin dataset, groups by the vowel category, then gets the mean of the dur column and fills that value into a column we’re calling “mean_dur” in duration_means dataframe. This is great, but wouldn’t it be even better if we could also distinguish between the various following sounds, which could conceivably have an effect on the duration too?
duration_means <- mandarin %>%
group_by(vowel, foll) %>%
summarise(mean_dur = mean(dur))
View(duration_means)
We could also reverse the order of the grouping; note how the calculations of the means don’t change, but we might prefer either seeing the rows clustered by following sound or by vowel, depending on what our research questions are.
duration_means <- mandarin %>%
group_by(foll, vowel) %>%
summarise(mean_dur = mean(dur))
View(duration_means)
You can have all sorts of functions embedded in the summarise function. These will be useful to find means, medians, standard deviations, etc.
duration_means <- mandarin %>%
group_by(foll, vowel) %>%
summarise(mean_dur = mean(dur),
sd_dur = sd(dur))
duration_means <- mandarin %>%
group_by(foll, vowel) %>%
summarise(mean_dur = mean(dur),
sd_dur = sd(dur),
median_dur = median(dur))
duration_means <- mandarin %>%
group_by(foll, vowel) %>%
summarise(mean_dur = mean(dur),
sd_dur = sd(dur),
median_dur = median(dur),
max_dur = max(dur),
min_dur = min(dur),
count = length(dur))
View(duration_means)
Note that within the summarize() function, on the left side of the = we specify a new name that we make up for each column in our output, and on the right side of the = we specify the function to run on a column in the input. There’s nothing special about the word “mean_dur” – we could cal this column “meandur” or “mean_dur” or “Rumpelstiltskin” or anything we wanted. It’s best if we choose a clear, unique, descriptive name that’s short enough to easily type.
Use the unique() function to see the individual categories in a column
unique(mandarin$vowel)
unique(mandarin$participant)
You can use length(unique()) to get the number of unique categories in a column. The length() function is simply wrapped around the unique() function.
length(unique(mandarin$vowel))
This will tell us the number of vowel categories in the dataset. You could alternatively write two lines of code to get this:
unique_items <- unique(mandarin$vowel)
length(unique_items)
Or even use the pipe %>% :
unique(mandarin$vowel) %>%
length()
If-else statements are a staple of almost all programming languages. As their name suggests, these are statements that make a certain change to an element if a condition is met, otherwise (or else), it makes a different change. There are a few ways to write if-else statements in R. One way is using the ifelse() function.
For example, in this dataset, f0_1 represents the F0 of the token measured at the first timepoint, f0_2 at the second timepoint, all the way up to f0_10 at the tenth and final timepoint. We might be interested in sorting out tokens where the final point (f0_10) had a higher pitch than the starting point (f0_1), thus giving us a rough measure of “rising” vs. “not rising” tones. We could thus create a new column in the dataset called “rising” that puts “rising” if f0_1 is less than f0_10, and “not rising” otherwise.
ifelse() takes three arguments: the condition that needs to be met
(mandarin$f0_1 < mandarin$f0_10), what to do if that
condition is TRUE (indicate “rising”), what to do if the condition is
FALSE (indicate “not rising”). Here we create a new column called
rising (on the left side of the line) using an if-else
statement:
mandarin$rising <- ifelse(mandarin$f0_1 < mandarin$f0_10, "rising", "not rising")
View(mandarin)
Notice that if either f0_1 or f0_10 are “NA” and have no measurement, we can’t evaluate the condition and we need to put “NA” in the resulting cell of the “rising” column.
You can get help using a question mark preceding the function that you want more information on. You can also get help via Google or the Help tab in the lower righthand corner.
?mean
?max
?filter
?subset
Something important we haven’t covered yet is the AND and OR operators. The AND operator (typed as &, called “ampersand”) takes two logical values and returns TRUE only if both values are TRUE themselves. For instance, suppose we have a variable x that is equal to 12, and another variable y equal to 17. We can then check if each of these variables is greater than 5 and less than 15:
x <- 12
x > 5 & x < 15
## [1] TRUE
y <- 17
y > 5 & y < 15
## [1] FALSE
The OR operator (typed as |, called “pike”) takes two logical values and returns TRUE if either one (or both) values are TRUE themselves. If we replace the ANDs in the previous example with ORs, here’s what we get:
x <- 12
x > 5 | x < 15
## [1] TRUE
y <- 17
y > 5 | y < 15
## [1] TRUE
Since y equals 17, and since 17 is greater than 5, the answer is TRUE to the question “Is y greater than 5 or less than 15?”.
These operators are very useful when you’re filtering/subsetting your data, too. You can combine subset conditions using AND or OR. For instance, here we can subset out a dataframe of just the tokens where preceding the vowel was “d” and following it was “ng”.
d_ng <- subset(mandarin, prec == "d" & foll == "ng")
…or maybe we want to revise the “rising” column so that it marks as “not rising” any token where f0_10 is greater than f0_1 OR where f0_9 is greater than f0_2, with others marked “not rising”.
mandarin$rising <- ifelse(mandarin$f0_10 > mandarin$f0_1 | mandarin$f0_9 > mandarin$f0_2, "not rising", "rising")
For character vectors, we can get the overall number of cells per category using the xtabs() function. xtabs() tabulates, or counts up, the number of cells with each unique label. Here we can look at the number of observations by vowel. Note the use of the tilde (~) in this formulation. The use of the tilde is a bit idiosyncratic in R, and frequently means “as a function of”, but just go with it for now. Like natural languages, computer languages sometimes also have idiosyncratic features.
xtabs(~vowel, mandarin)
We can get further breakdowns of the counts using the plus sign. The following example returns a table of the vowel + following sound pairs.
xtabs(~vowel + foll, mandarin)
# or, if we wanted our table flipped around the other way:
xtabs(~foll + vowel, mandarin)
You can also obtain counts and proportions of vectors with categorical variables with tidyverse code.
counts <- mandarin %>%
group_by(vowel) %>%
summarize(count = n())
# This should give you an ERROR because length() requires you to type out an argument, unlike n()
counts <- mandarin %>%
group_by(vowel) %>%
summarize(count = length())
# This code should work. Note you can replace the argument inside length() with *any* of the column names (file, vowel, prec, foll, etc.) and it will generate the same by-vowel row count.
counts <- mandarin %>%
group_by(vowel) %>%
summarize(count = length(vowel))
# If you want to group by two variables, you just put them both in group_by(), separated by a comma:
counts <- mandarin %>%
group_by(vowel, foll) %>%
summarize(count = n())
If we run the following code, what we see is the number of tokens collected in the experiment for each vowel and tone category – that is, how many times each vowel was uttered with each tone in the files under investigation.
counts <- mandarin %>%
group_by(vowel, tone) %>%
summarize(count = n())
Suppose instead we wanted to find out how many participants said each vowel/tone combo instead of how many observations we have per vowel/tone combo. (Ideally, every participant should have said every combo at some point, but this kind of thing is always good to double-check!) If we add “participant” to the group_by() argument, that doesn’t exactly give us what we want – it’ll just tell us how many tokens were uttered by each participant for each vowel/tone combo:
counts <- mandarin %>%
group_by(vowel, tone, participant) %>%
summarize(count = n())
One solution is to instead first collapse the “participant” column such that we only have one row per participant per vowel per tone, which we can then sum up. In this case, it’s useful to select just the columns we’re interested in: we can ignore all the other columns for now. If we wanted to make a new dataframe with just these columns, we can use the select() function to do so:
participant_voweltone <- mandarin %>%
select(participant, vowel, tone)
If you view this resulting dataframe, you can see that we now have lots of duplicate identical rows. We don’t want to stop there! We can then use the unique() function to delete all the duplicate rows, keeping only one version of each unique row:
participant_voweltone <- mandarin %>%
select(participant, vowel, tone) %>%
unique()
We can now add our group_by() and summarize() functions to group by vowel and tone and then get the count for how many individual participants uttered each vowel/tone combo:
participant_voweltone <- mandarin %>%
select(participant, vowel, tone) %>%
unique() %>%
group_by(vowel, tone) %>%
summarize(count = n())
Looks like we mostly have evidence from all 13 speakers, but there are a handful of vowel/tone combos for which we’re missing some evidence! It is always good to know when we have some unbalanced or missing data.
Suppose we want to find the mean of all the f0 measurements at timepoint 5 (that is, f0_5), grouped by file, so that we get a sense of the average f0 within each file at about the midpoint of each vowel.
mandarin %>%
group_by(file) %>%
summarise(mean_f0 = mean(f0_5))
If you look at the resulting table, everything is NA! That’s not good. The reason is that if you have “NA” values in your vector (column), R will be unable to take the mean or perform other standard mathematical functions on it. You’ll know if this happens to you because the returned value (e.g., for the mean) will be “NA”. One way to omit NA values is to wrap the object within the function na.omit():
mandarin %>%
group_by(file) %>%
summarise(mean_f0 = mean(na.omit(f0_5)))
Another strategy that does the same thing is using the optional argument “na.rm = TRUE”, where na.rm means “remove the NA values”.
mandarin %>%
group_by(file) %>%
summarise(mean_f0 = mean(f0_5, na.rm = TRUE))
If we don’t want to type na.omit() or na.rm = TRUE over and over again, we can potentially use the drop_na() function. What this does is it removes all rows that contain any NA values – not just rows where there are NA values for f0_5, but for any variable. If our data contained rows that contained a number in the f0_5 column but an NA in the f0_4 column, we probably wouldn’t want to use this drop_na() function because it would remove too many rows.
na_dropped <- mandarin %>%
drop_na()
nrow(mandarin)
nrow(na_dropped)
Here you can see that we’ve eliminated about half the rows, because so many of them have one or more NAs in them! Supposing that was something we wanted to do, we could have drop_na() as our first step, after which we use the group and summarize functions.
mandarin %>%
drop_na() %>%
group_by(file) %>%
summarise(mean_f0 = mean(f0_5))
You might want to save a table of values as a .csv or text file for ease of then copying it into a report/paper. You can use the write.table() function to create a space-delimited .txt file, or write.csv() to create a comma-delimited .csv file. Within each of these functions, you first tell it the name of the dataframe you want to save as a table, then within “” quotes you put the path name to where you want to save it, ending in the name you want to give the new file and the .csv or .txt extension it should have.
When using write.table(), it’ll default to writing as a space-delimited .text file – that is, there will be a space between each of the columns. You can also make it a tab-delimited file by adding the argument sep = “\\t” which tells R to separate the columns with a tab.
You can also set row.names to FALSE; this is recommended to prevent R from saving the row numbers as an extra column in your file, which can cause issues if you read the data back into R later. You can see in the following two examples that when row.names = FALSE is not included, you end up with an initial column with row numbers that is in most cases extraneous/unnecessary.
write.table(participant_voweltone, "/Users/Thomas/Library/CloudStorage/OneDrive-YorkUniversity/LING 3300/Datasets/participant_voweltone.txt", sep = "\\t", row.names = FALSE)
write.csv(participant_voweltone, "/Users/Thomas/Library/CloudStorage/OneDrive-YorkUniversity/LING 3300/Datasets/participant_voweltone.txt")
Create an R script to save the answers to these questions.