Learning Analytics — Data Collection and Preparation (Required)
Author
Cassidy Newby
Published
September 12, 2026
Learning objectives
By the end of this file, you will be able to:
Load a CSV dataset into R using read_csv()
Inspect a data frame using glimpse(), head(), str(), and skim()
Handle missing values appropriately — not just with na.omit()
Select and filter data using select() and filter()
Create and modify variables using mutate() and ifelse()
Sort data using arrange()
Chain multiple operations using the pipe |>
Dataset: sci-online-classes
This file uses sci-online-classes.csv — a dataset from a real study of science students in online courses. Each row represents one student in one course section.
Data dictionary — know your variables before you code
Variable
Type
What it means
student_id
numeric
Unique student identifier
subject
character
Course subject (e.g., “OcnA”, “PhysA”)
semester
character
Semester the course was taken
section
character
Course section identifier
FinalGradeCEMS
numeric
Final course grade (0–100) — key outcome variable
TimeSpent_hours
numeric
Total hours spent on the LMS
percentage_earned
numeric
Percentage of points earned
Gender
character
Student gender (“M” or “F”)
total_points_possible
numeric
Total points available in the course
Grade_Category
character
Pass/fail category
Note
Knowing your variables before writing any code prevents the most common beginner mistake: writing code that references a column that does not exist or is the wrong type.
Part 1 · Setup and loading data
# Load the dataset from the data folderdata <-read_csv("data/sci-online-classes.csv")# Always inspect immediately after loadingglimpse(data)
Question: What do you notice from the first look at this dataset? Share one thing that stands out — a variable type that surprised you, an unexpected value range, or anything else.
[The thing that stood out to me was q1-q10. I am not sure what that variable really means. The other variables match up to the ones listed in the code, so I was able to track where those variables in-puts and out-puts.]
Part 2 · Exploring your data
Summary statistics
# summary() gives min, max, median, mean for numeric columnssummary(data)
student_id course_id total_points_possible total_points_earned
Min. :43146 Length:603 Min. : 840 Min. : 651
1st Qu.:85612 Class :character 1st Qu.: 2810 1st Qu.: 2050
Median :88340 Mode :character Median : 3583 Median : 2757
Mean :86070 Mean : 4274 Mean : 3245
3rd Qu.:92730 3rd Qu.: 5069 3rd Qu.: 3875
Max. :97441 Max. :15552 Max. :12208
percentage_earned subject semester section
Min. :0.3384 Length:603 Length:603 Length:603
1st Qu.:0.7047 Class :character Class :character Class :character
Median :0.7770 Mode :character Mode :character Mode :character
Mean :0.7577
3rd Qu.:0.8262
Max. :0.9106
Gradebook_Item Grade_Category FinalGradeCEMS Points_Possible
Length:603 Mode:logical Min. : 0.00 Min. : 5.00
Class :character NA's:603 1st Qu.: 71.25 1st Qu.: 10.00
Mode :character Median : 84.57 Median : 10.00
Mean : 77.20 Mean : 76.87
3rd Qu.: 92.10 3rd Qu.: 30.00
Max. :100.00 Max. :935.00
NA's :30
Points_Earned Gender q1 q2
Min. : 0.00 Length:603 Min. :1.000 Min. :1.000
1st Qu.: 7.00 Class :character 1st Qu.:4.000 1st Qu.:3.000
Median : 10.00 Mode :character Median :4.000 Median :4.000
Mean : 68.63 Mean :4.296 Mean :3.629
3rd Qu.: 26.12 3rd Qu.:5.000 3rd Qu.:4.000
Max. :828.20 Max. :5.000 Max. :5.000
NA's :92 NA's :123 NA's :126
q3 q4 q5 q6
Min. :1.000 Min. :1.000 Min. :2.000 Min. :1.000
1st Qu.:3.000 1st Qu.:4.000 1st Qu.:4.000 1st Qu.:4.000
Median :3.000 Median :4.000 Median :4.000 Median :4.000
Mean :3.327 Mean :4.268 Mean :4.191 Mean :4.008
3rd Qu.:4.000 3rd Qu.:5.000 3rd Qu.:5.000 3rd Qu.:5.000
Max. :5.000 Max. :5.000 Max. :5.000 Max. :5.000
NA's :123 NA's :125 NA's :127 NA's :127
q7 q8 q9 q10
Min. :1.000 Min. :1.000 Min. :1.000 Min. :1.000
1st Qu.:3.000 1st Qu.:4.000 1st Qu.:3.000 1st Qu.:4.000
Median :4.000 Median :4.000 Median :4.000 Median :4.000
Mean :3.907 Mean :4.289 Mean :3.487 Mean :4.101
3rd Qu.:4.750 3rd Qu.:5.000 3rd Qu.:4.000 3rd Qu.:5.000
Max. :5.000 Max. :5.000 Max. :5.000 Max. :5.000
NA's :129 NA's :129 NA's :129 NA's :129
TimeSpent TimeSpent_hours TimeSpent_std int
Min. : 0.45 Min. : 0.0075 Min. :-1.3280 Min. :2.000
1st Qu.: 851.90 1st Qu.: 14.1983 1st Qu.:-0.6996 1st Qu.:3.900
Median :1550.91 Median : 25.8485 Median :-0.1837 Median :4.200
Mean :1799.75 Mean : 29.9959 Mean : 0.0000 Mean :4.219
3rd Qu.:2426.09 3rd Qu.: 40.4348 3rd Qu.: 0.4623 3rd Qu.:4.700
Max. :8870.88 Max. :147.8481 Max. : 5.2188 Max. :5.000
NA's :5 NA's :5 NA's :5 NA's :76
pc uv
Min. :1.500 Min. :1.000
1st Qu.:3.000 1st Qu.:3.333
Median :3.500 Median :3.667
Mean :3.608 Mean :3.719
3rd Qu.:4.000 3rd Qu.:4.167
Max. :5.000 Max. :5.000
NA's :75 NA's :75
# skim() gives a richer view including missing counts and distributionsskim(data)
Data summary
Name
data
Number of rows
603
Number of columns
30
_______________________
Column type frequency:
character
6
logical
1
numeric
23
________________________
Group variables
None
Variable type: character
skim_variable
n_missing
complete_rate
min
max
empty
n_unique
whitespace
course_id
0
1
12
13
0
26
0
subject
0
1
4
5
0
5
0
semester
0
1
4
4
0
3
0
section
0
1
2
2
0
4
0
Gradebook_Item
0
1
9
35
0
3
0
Gender
0
1
1
1
0
2
0
Variable type: logical
skim_variable
n_missing
complete_rate
mean
count
Grade_Category
603
0
NaN
:
Variable type: numeric
skim_variable
n_missing
complete_rate
mean
sd
p0
p25
p50
p75
p100
hist
student_id
0
1.00
86069.54
10548.60
43146.00
85612.50
88340.00
92730.50
97441.00
▁▁▁▃▇
total_points_possible
0
1.00
4274.41
2312.74
840.00
2809.50
3583.00
5069.00
15552.00
▇▅▂▁▁
total_points_earned
0
1.00
3244.69
1832.00
651.00
2050.50
2757.00
3875.00
12208.00
▇▅▁▁▁
percentage_earned
0
1.00
0.76
0.09
0.34
0.70
0.78
0.83
0.91
▁▁▃▇▇
FinalGradeCEMS
30
0.95
77.20
22.23
0.00
71.25
84.57
92.10
100.00
▁▁▁▃▇
Points_Possible
0
1.00
76.87
167.51
5.00
10.00
10.00
30.00
935.00
▇▁▁▁▁
Points_Earned
92
0.85
68.63
145.26
0.00
7.00
10.00
26.12
828.20
▇▁▁▁▁
q1
123
0.80
4.30
0.68
1.00
4.00
4.00
5.00
5.00
▁▁▂▇▇
q2
126
0.79
3.63
0.93
1.00
3.00
4.00
4.00
5.00
▁▂▆▇▃
q3
123
0.80
3.33
0.91
1.00
3.00
3.00
4.00
5.00
▁▃▇▅▂
q4
125
0.79
4.27
0.85
1.00
4.00
4.00
5.00
5.00
▁▁▂▇▇
q5
127
0.79
4.19
0.68
2.00
4.00
4.00
5.00
5.00
▁▂▁▇▅
q6
127
0.79
4.01
0.80
1.00
4.00
4.00
5.00
5.00
▁▁▃▇▅
q7
129
0.79
3.91
0.82
1.00
3.00
4.00
4.75
5.00
▁▁▅▇▅
q8
129
0.79
4.29
0.68
1.00
4.00
4.00
5.00
5.00
▁▁▂▇▆
q9
129
0.79
3.49
0.98
1.00
3.00
4.00
4.00
5.00
▁▃▇▇▃
q10
129
0.79
4.10
0.93
1.00
4.00
4.00
5.00
5.00
▁▂▃▇▇
TimeSpent
5
0.99
1799.75
1354.93
0.45
851.90
1550.91
2426.09
8870.88
▇▅▁▁▁
TimeSpent_hours
5
0.99
30.00
22.58
0.01
14.20
25.85
40.43
147.85
▇▅▁▁▁
TimeSpent_std
5
0.99
0.00
1.00
-1.33
-0.70
-0.18
0.46
5.22
▇▅▁▁▁
int
76
0.87
4.22
0.59
2.00
3.90
4.20
4.70
5.00
▁▁▃▇▇
pc
75
0.88
3.61
0.64
1.50
3.00
3.50
4.00
5.00
▁▁▇▅▂
uv
75
0.88
3.72
0.70
1.00
3.33
3.67
4.17
5.00
▁▁▆▇▅
Question: What did you discover from the skim() output? Mention at least two things — for example: variables with missing data, the range of a numeric variable, or anything that caught your attention.
[The first thing that got my attention is in skim_df 23 x 11 that most of the variable means are massive decimal points for all the data in that section. FinalGradeCEMS, Points_Earned, and q1-3 are all missing data. It is almost like that data was just was not there; however, in the first 6x8 skim there is no missing data or crazy mean scores. ]
Part 3 · Cleaning data
Handling missing values — the right way
A common mistake is using na.omit() to remove ALL rows with any missing value. Let’s see why this is usually a bad idea:
# This removes every row that has ANY missing value in ANY columndata_naive <-na.omit(data)nrow(data) # how many rows originally?
[1] 603
nrow(data_naive) # how many rows after na.omit?
[1] 0
Question: What happened to the dataset? Why do you think removing all rows with any missing value is not the right approach for this data?
[It got rid of 603 rows of missing data, which would explain why all the data was missing from before in the data sets. Delete all is never the right approach to anything because what if you accidently delete something you never orignally met to. ]
A better approach: target specific variables
# Remove rows where FinalGradeCEMS is missing — our key outcome variabledata_clean <- data |>filter(!is.na(FinalGradeCEMS))nrow(data_clean)
[1] 573
# Confirm FinalGradeCEMS no longer has missing valuessum(is.na(data_clean$FinalGradeCEMS))
[1] 0
# Inspect the cleaned datahead(data_clean)
Standardize column names (optional but good practice)
# clean_names() from the janitor package standardizes column names:# removes spaces, converts to lowercase, fixes special charactersdata_clean <- data_clean |>clean_names()glimpse(data_clean)
After clean_names(), all column names become lowercase_with_underscores. If column names changed, update your code below to match the new names. Check names(data_clean) if you are not sure what the columns are called.
# Always confirm your column names after cleaningnames(data_clean)
# Use - to drop columns you will not use# grade_category is a derived variable we will not need for this analysisdata_clean <- data_clean |>select(-grade_category)glimpse(data_clean)
# Select specific columns by nameselected_data <- data_clean |>select(student_id, subject, semester, final_grade_cems)head(selected_data)
Task: Select all columns exceptsubject and section. Save the result as reduced_data and inspect it.
# Hint: use - before column names to exclude them.# Example: select(data_clean, -column_name)# You have seen select() used once above — try writing this one yourself.# YOUR CODE HEREreduced_data <- data_clean |>select(-subject, -section)glimpse(reduced_data)
# Keep only students in "OcnA" coursesocna_students <- data_clean |>filter(subject =="OcnA")nrow(ocna_students)
[1] 105
head(ocna_students)
Task: Filter to remove rows where total_points_possible is NA. Save as no_na_points and inspect with glimpse().
# Hint: !is.na(column_name) means "where column_name is NOT missing"no_na_points <- data_clean |>filter(!is.na(total_points_possible))glimpse(no_na_points)
Question: (In your own words,) What does the ! symbol mean in R?
[Typically in normal coding platforms ! means not or not equal, but according to R it means that the value is not missing rather than equal.]
Task: Filter to keep only students with final_grade_cems above 85. Save as data_over85. Then separately filter for time_spent_hours greater than 50. Save as data_timeover50.
# Now filter for time_spent_hours > 50data_timeover50 <- data_clean |>filter(time_spent_hours >50)nrow(data_timeover50)
[1] 96
Part 5 · Creating and modifying variables
mutate() and ifelse()
mutate() adds or changes a column. ifelse() lets you create a variable whose value depends on a condition.
# Create 'added_gender' — spell out "Female" / "Male" from the "F"/"M" codesdata_gender <- data_clean |>mutate(added_gender =ifelse(gender =="F", "Female", "Male"))# Inspect the new columndata_gender |>select(student_id, gender, added_gender) |>head()
Question: In your own words, how does ifelse() work? What are its three parts?
[It works has a logic gate. If something is true then it works, and if it is false then it does not. So, the three parts are: it checks to see if true/false, if true, or if false.]
Task: Create a new variable called pass_fail — “Pass” if final_grade_cems is 70 or above, “Fail” if below 70.
# YOUR CODE HERE — use mutate() and ifelse() to create a pass_fail column,# then use count() to see how many students passed vs. failed.data_clean <- data_clean |>mutate(pass_fail =ifelse(final_grade_cems >=70, "Fail", "Pass"))count(data_clean, pass_fail)
Part 6 · Sorting data
arrange()
# Sort by subject, then by final grade descendingarranged_data <- data_clean |>arrange(subject, desc(final_grade_cems))head(arranged_data)
Part 7 · Chaining with the pipe |>
The pipe |> chains operations together so you can do multiple steps in one readable flow. Read each |> as “then.”
# All in one pipeline:# start with data → select columns → filter subject → arrange by gradefinal_data <- data_clean |>select(student_id, subject, semester, final_grade_cems) |>filter(subject =="OcnA") |>arrange(desc(final_grade_cems))print(final_data)
Task: Build your own pipeline. Start with data_clean, select any four columns of your choice, filter for gender == "M", and arrange by time_spent_hours descending. Save as my_pipeline and inspect the result.
TipWriting this yourself
By this point you have seen select(), filter(), arrange(), and |> used multiple times. This chunk has no hints — write the full pipeline from memory. If you need to check a column name, run names(data_clean) in the Console first.
# YOUR CODE HERE — write the full pipeline without looking at the examples abovemy_pipeline <- data_clean |>select(student_id, time_spent_hours, gender , subject, final_grade_cems) |>filter(gender =="M") |>arrange(desc(time_spent_hours))glimpse(my_pipeline)
Before running the scatter plot below, write your prediction in the space provided. Students who commit to a prediction before seeing the output retain the interpretation better than students who just read the result.
Prediction: Before running the next chunk, write one sentence: do you expect students who spend more time on the LMS to earn higher or lower grades? How strong do you expect the relationship to be — weak, moderate, or strong?
[I expect that students who spend more time on LMS will have higher grades than those who spent less time. I think it has a moderate relationship.]
# Scatter plot of time spent vs. final gradeggplot(data_clean, aes(x = time_spent_hours, y = final_grade_cems)) +geom_point(color ="#378ADD", size =2, alpha =0.6) +geom_smooth(method ="lm", color ="#0F6E56", se =TRUE) +labs(title ="Time Spent vs. Final Grade",x ="Time Spent on LMS (hours)",y ="Final Grade" ) +theme_minimal()
Relationship between time spent on the LMS and final grade
Question: Based on this scatter plot, what do you expect the relationship between time spent and final grades to be? Why?
[It appears to be scattered, but the highly dense area is on the students who spent more than 0 hours but less than 50. So, I believe there is a relationship on spending too much time on LMS could be harmful because the one lone student who spent 150 hours scored less than a student, who had maybe 1-10 hours.]
::: {.cell}
```{.r .cell-code}
# Use filter() to create one meaningful subset of this data.
# Choose a filter condition that makes instructional sense.
risky <- data_clean |>
filter(final_grade_cems < 70)
glimpse(risky)
# Use arrange() to sort the data in a way that would be useful to a teacher# or instructional designer reviewing student performance.data <- data_clean |>arrange(final_grade_cems)glimpse(data)
Change the author: field in the YAML header at the very top of this file to your name. The YAML header controls the document’s metadata and formatting — it does not display in the final rendered output.
Step 2 — Render
Click the Render button in the toolbar above. A formatted HTML page will appear in your Viewer tab or open in a new browser window. If you see any error messages, check the Console pane below for the exact error text — copy it and post in the course discussion board if you are stuck.
Step 3 — Publish
Share a link to your published document with your instructor. Choose any publishing method below:
If you are building a professional e-portfolio, this document demonstrates your ability to load, clean, filter, and visualize real educational data in R. Consider publishing to Quarto Pub or GitHub Pages for a shareable, permanent link you can include in a resume or portfolio site.
Once your instructor has reviewed your published link, you will be notified and the module will be marked complete.
If you have any questions or run into technical issues, contact your instructor or post in the course discussion board with the exact error message from the Console.