PSYC 70510 · Statistical Methods in Psych Lab I · Section 53950
Lab Instructor: Kaile Smith
This page is meant to stay open while you work in RStudio. Add the code to a Week 2 script, run one block at a time, and compare your Console output with the output shown here.
psych to calculate descriptive statistics.tidyverse steps with the pipe.Open the PSYC-70510 Project, then create a new R script:
StatsLab_Week2_FactorsPackages.R inside the Project’s scripts folder.Add a title comment:
# Lab 2: Factors and PackagesWeek 1 introduced numeric, character, and logical values.
| Type | Examples | Used for |
|---|---|---|
| numeric | 7, 3.5, -2 |
calculations |
| character | "male", "college" |
text and names |
| logical | TRUE, FALSE |
conditions |
When logical values appear with numbers, R converts TRUE to 1 and FALSE to 0:
c(10, 7, 9, 3, TRUE, FALSE)[1] 10 7 9 3 1 0
When text appears in the same vector, R converts every value to character text:
c("hi", "hey", 7, TRUE)[1] "hi" "hey" "7" "TRUE"
This matters because a data-frame column is a vector. Every value in one column follows one common data type.
A factor represents a categorical variable with a known set of possible values. Those possible values are called levels.
| Character vector | Factor |
|---|---|
| stores text | stores categories |
| has no declared set of possible values | records a fixed set of levels |
| has no category order | can record a level order |
Create a character vector containing the days of the week:
days_of_week <- c(
"Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday"
)
class(days_of_week)[1] "character"
factor()factor(days_of_week)[1] Sunday Monday Tuesday Wednesday Thursday Friday Saturday
Levels: Friday Monday Saturday Sunday Thursday Tuesday Wednesday
R created and printed a factor, but the original object was not changed. Because the result was not assigned with <-, days_of_week is still a character vector:
is.factor(days_of_week)[1] FALSE
is.factor() is a quick check: it returns TRUE for a factor and FALSE for another type of object.
To keep the converted factor, assign the result of factor() to an object. By default, R arranges the factor levels alphabetically:
Use levels() to inspect the stored level order:
days_default <- factor(days_of_week)
is.factor(days_default)
levels(days_default)[1] TRUE
[1] "Friday" "Monday" "Saturday" "Sunday"
[5] "Thursday" "Tuesday" "Wednesday"
The first argument to factor() is the vector to convert. The levels argument states the order of the possible values:
days_factor <- factor(
days_of_week,
levels = days_of_week
)
levels(days_factor)
is.factor(days_factor)[1] "Sunday" "Monday" "Tuesday" "Wednesday"
[5] "Thursday" "Friday" "Saturday"
[1] TRUE
The levels now follow the order supplied in days_of_week.
Some categories have a meaningful progression. Add ordered = TRUE when comparisons such as earlier/later or low/medium/high make sense:
days_factor <- factor(
days_of_week,
levels = days_of_week,
ordered = TRUE
)
class(days_factor)
is.ordered(days_factor)[1] "ordered" "factor"
[1] TRUE
Setting the level order and declaring an ordered factor are related but separate choices. Level order controls how categories are stored and displayed; ordered = TRUE additionally tells R that the categories have a meaningful ranking.
Sometimes categories arrive as numeric codes. Here, the values 1 through 7 stand for days:
day_codes <- 1:7
day_codes[1] 1 2 3 4 5 6 7
Use levels to identify the original codes and labels to supply the readable names:
labeled_days <- factor(
day_codes,
levels = 1:7,
labels = days_of_week,
ordered = TRUE
)
labeled_days[1] Sunday Monday Tuesday Wednesday Thursday Friday Saturday
7 Levels: Sunday < Monday < Tuesday < Wednesday < ... < Saturday
Because the call begins with labeled_days <-, the factor is already saved as a new object. To use another name, change the name on the left side of <- when you create it.
If you get stuck or want more information about factors, open the Help page with either form:
?factor
help("factor")The Help page lists the function’s arguments, which arguments are required, and examples of valid calls.
mtcarsmtcars is a dataset included with R. In Week 1, we used a CSV copy so you could practice importing a file. Here, we use the built-in version directly.
Assign it to an object so the original built-in dataset remains unchanged:
cars <- mtcarsOpen the data viewer and inspect the structure:
View(cars)
str(cars)View() opens a spreadsheet-style viewer in RStudio. str() prints the data-frame structure in the Console.
The cyl column records each car’s number of cylinders:
unique(cars$cyl)
table(cars$cyl)[1] 6 4 8
4 6 8
11 7 14
unique() returns the distinct values. table() counts how often each value appears.
The values 4, 6, and 8 identify categories of cars. Convert them to a factor and give each category a readable label:
cars$cyl_factor <- factor(
cars$cyl,
levels = c(4, 6, 8),
labels = c("4 cylinder", "6 cylinder", "8 cylinder")
)
table(cars$cyl_factor)4 cylinder 6 cylinder 8 cylinder
11 7 14
Inspect the structure again after adding the factor column:
str(cars)In the output, cyl_factor is identified as a factor with three levels. This confirms that the new column is stored differently from the original numeric cyl column.
The original numeric cyl column is still available for numeric operations. The new cyl_factor column tells R to treat the values as three groups.
You can also create the factor without supplying new labels:
cars$cyl_factor2 <- factor(cars$cyl)
levels(cars$cyl_factor2)[1] "4" "6" "8"
Both factor columns contain the same three categories. The first uses labels that are easier to read in output.
The original numeric column can be averaged:
mean(cars$cyl)[1] 6.1875
For most research questions, cylinders are more useful as groups. For example, we may compare mean mpg across 4-, 6-, and 8-cylinder cars.
relevel()Many statistical models compare every factor level with one reference level. Check the current order:
levels(cars$cyl_factor)[1] "4 cylinder" "6 cylinder" "8 cylinder"
Set "8 cylinder" as the reference:
cars$cyl_factor <- relevel(
cars$cyl_factor,
ref = "8 cylinder"
)
levels(cars$cyl_factor)[1] "8 cylinder" "4 cylinder" "6 cylinder"
To see why a reference level matters, we will briefly fit a simple linear model. You do not need to interpret the full model yet.
The function lm() fits a linear model. Inside it, mpg ~ cyl_factor is a formula:
~, mpg, is the outcome we want to compare;cyl_factor, defines the groups being compared.Here, the model compares mean mpg across the cylinder groups.
cylinder_model <- lm(
mpg ~ cyl_factor,
data = cars
)
coef(cylinder_model) (Intercept) cyl_factor4 cylinder cyl_factor6 cylinder
15.100000 11.563636 4.642857
The intercept is the mean mpg for the 8-cylinder reference group. The other coefficients describe how the 4- and 6-cylinder means differ from that reference.
Use summary(cylinder_model) when you need the full model output:
summary(cylinder_model)Base R provides the functions available when R first opens. A package adds functions, datasets, or documentation created for a particular purpose.
This week we will use three packages:
| Package | What we use it for |
|---|---|
psych |
descriptive statistics |
tidyverse |
filtering, selecting, grouping, and piping |
datarium |
the jobsatisfaction practice dataset |
The psych package contains tools developed for psychological research, including the descriptive-statistics functions used here.
tidyverse is a collection of related packages. The functions filter(), select(), group_by(), and summarize() come from its dplyr package. Running library(tidyverse) loads dplyr, so a separate library(dplyr) call is not required for this lesson.
Installation downloads a package onto the computer. Run these commands in the Console if the packages are not already installed:
install.packages("psych")
install.packages("tidyverse")
install.packages("datarium")Package installation may print many messages while dependencies are installed. Those messages are expected. Do not place install.packages() in a script that will run every session.
Loading attaches an installed package to the current R session. Put these lines near the top of the Week 2 script:
library(psych)
library(tidyverse)
library(datarium)Run library() again after restarting RStudio. Installing again is usually unnecessary.
help(package = "psych")
help("describe", package = "psych")The first line opens the package index. The second opens the Help page for one function.
package::function()Two colons identify exactly which package should supply a function:
psych::describe(cars$mpg)Read this as: use describe() from the psych package.
This notation is helpful when two loaded packages contain functions with the same name. For example, tidyverse startup messages may say that dplyr::filter() masks stats::filter(). An unqualified call to filter() will then use the version from dplyr.
psychUse describe() for a compact set of descriptive statistics:
psych::describe(cars$mpg) vars n mean sd median trimmed mad min max range skew kurtosis se
X1 1 32 20.09 6.03 19.2 19.95 5.41 10.4 33.9 23.5 0.61 -0.37 1.07
The output includes sample size, mean, standard deviation, median, minimum, maximum, range, skew, kurtosis, and standard error.
Use describeBy() when descriptive statistics should be calculated separately for each group:
psych::describeBy(
cars$mpg,
group = cars$cyl_factor,
mat = TRUE
) group1 n mean sd median min max
1 4 cylinder 11 26.66 4.51 26.0 21.4 33.9
2 6 cylinder 7 19.74 1.45 19.7 17.8 21.4
3 8 cylinder 14 15.10 2.56 15.2 10.4 19.2
The first argument is the numeric variable to summarize. The group argument identifies the grouping factor. mat = TRUE returns one rectangular table.
Use list() to supply more than one grouping variable:
psych::describeBy(
cars$mpg,
group = list(
cylinders = cars$cyl_factor,
gears = cars$gear
),
mat = TRUE
)list() keeps the grouping variables together. The output contains one row for each observed combination of cylinder group and gear value.
filter()filter() keeps rows that satisfy a condition. The first argument is the data frame. The remaining arguments are conditions.
Keep cars with six-cylinder engines:
six_cyl_cars <- filter(cars, cyl == 6)
View(six_cyl_cars)Read filter(cars, cyl == 6) as: within cars, check the cyl value in every row and keep the rows where it equals 6. The comparison cyl == 6 produces TRUE for rows to keep and FALSE for rows to remove.
Use the double equals sign == to ask whether two values are equal. A single equals sign is used for named function arguments, such as group = cars$cyl_factor, rather than for this comparison.
| Operator | Meaning |
|---|---|
== |
equal to |
!= |
not equal to |
> |
greater than |
< |
less than |
>= |
greater than or equal to |
<= |
less than or equal to |
Type and run these examples:
not_six_cyl_cars <- filter(cars, cyl != 6)
over_20_mpg_cars <- filter(cars, mpg > 20)
at_most_20_mpg_cars <- filter(cars, mpg <= 20)Use range() to check the smallest and largest values in a numeric vector:
range(over_20_mpg_cars$mpg)[1] 21.0 33.9
Separate conditions with commas when every condition must be true:
six_cyl_20_mpg <- filter(
cars,
cyl == 6,
mpg > 20
)
rownames(six_cyl_20_mpg)[1] "Mazda RX4" "Mazda RX4 Wag" "Hornet 4 Drive"
A comma inside filter() works like the AND operator &. This equivalent form returns the same rows:
filter(cars, cyl == 6 & mpg > 20)Join two comparisons with the OR operator | when either condition can be true:
filter(cars, gear == 4 | gear == 5)Here, a row is kept when gear equals 4 or when gear equals 5.
The operator %in% provides a shorter way to check whether a value appears in a vector. This code returns the same rows:
four_or_five_gears <- filter(
cars,
gear %in% c(4, 5)
)
four_or_five_gearsUse %in% when one variable can match several named categories. It is usually easier to read than repeating the column name.
The pipe %>% takes the object or result on its left and supplies it as the first input to the function on the next line. This lets you write a sequence of steps from top to bottom.
cars %>%
filter(mpg > 20)Read the code from top to bottom: begin with cars, then pass it into filter() and keep rows where mpg is greater than 20.
The code above does the same thing as filter(cars, mpg > 20). The data-frame name moves outside filter() because the pipe supplies cars as the first argument.
six_cyl_20_mpg <- cars %>%
filter(cyl == 6) %>%
filter(mpg > 20)The first filter() returns six-cylinder cars. The second receives those rows and keeps the cars above 20 mpg.
The same result can be written with both conditions in one call:
six_cyl_20_mpg <- cars %>%
filter(cyl == 6, mpg > 20)select() keeps columns. This example keeps wt, mpg, and cyl:
selected_cars <- cars %>%
select(wt, mpg, cyl)group_by() records the groups to use in the next calculation. summarize() produces one summary row per group:
cylinder_weights <- cars %>%
select(wt, mpg, cyl) %>% # keep the three columns we need
group_by(cyl) %>% # make one group for each cyl value
summarize(
mean_wt = mean(wt), # calculate mean wt within each group
.groups = "drop" # remove the grouping when finished
)
cylinder_weights# A tibble: 3 × 2
cyl mean_wt
<dbl> <dbl>
1 4 2.29
2 6 3.12
3 8 4.00
group_by(cyl) temporarily marks the rows as belonging to cylinder groups. .groups = "drop" removes that grouping instruction after the means are calculated, so cylinder_weights behaves like a regular, ungrouped data frame. It does not remove any rows from the result.
Every part of this pipeline has been introduced above. Type it into the script, read each line aloud, and then run it:
practice_cars <- cars %>%
filter(cyl != 8, gear %in% c(4, 5)) %>%
select(mpg, cyl, gear)
practice_carsjobsatisfaction dataThe Week 2 assignment uses a dataset included in the datarium package. Assign it to df:
df <- datarium::jobsatisfactionInspect the data before using it:
head(df)
names(df)
str(df)[1] "id" "gender" "education_level" "score"
The dataset contains 58 observations and four variables:
| Variable | Type | Meaning |
|---|---|---|
id |
factor | participant identifier |
gender |
factor | male or female |
education_level |
factor | school, college, or university |
score |
numeric | job-satisfaction score |
Check the available factor levels before filtering or grouping:
levels(df$gender)
levels(df$education_level)Filtering a data frame does not automatically remove factor levels that no longer appear. Use droplevels() when later output should show only the remaining categories:
university_example <- filter(
df,
education_level == "university"
)
university_example <- droplevels(university_example)
levels(university_example$education_level)[1] "university"
The Week 2 practice is on a separate page so you can complete each task before seeing more worked code.
Keep this lesson open in another tab and return to the examples whenever you need a code pattern.
When a line does not work, bring these four pieces of information:
Use the remaining class time to complete the Week 2 group work and ask questions as they come up.
| Task | Code pattern |
|---|---|
| Convert a vector to a factor | factor(x) |
| Set factor levels | factor(x, levels = ...) |
| Replace codes with labels | factor(x, levels = ..., labels = ...) |
| Mark a factor as ordered | factor(x, ordered = TRUE) |
| Change the reference level | relevel(x, ref = "...") |
| Install a package once | install.packages("package") |
| Load a package each session | library(package) |
| Use a specific package function | package::function() |
| Calculate descriptive statistics | psych::describe(x) |
| Describe by one group | psych::describeBy(x, group = g) |
| Describe by two groups | psych::describeBy(x, group = list(g1, g2)) |
| Keep rows meeting a condition | filter(data, condition) |
| Require several conditions | filter(data, condition1, condition2) |
| Accept several values | x %in% c(value1, value2) |
| Remove unused factor levels | droplevels(data) |
| Send output to the next step | data %>% function() |
| Keep selected columns | select(data, column1, column2) |
| Calculate by group | group_by() %>% summarize() |