library(dplyr)
answers <- c(4, 3, 5, 99, 2)
mean(answers)[1] 22.6
mean(na_if(answers, 99), na.rm = TRUE)[1] 3.5
Raw data is never ready for analysis. Survey tools record test responses alongside real ones, participants submit the same form twice, one person types “female” and another “F”, missing answers are stored as 99, and a slip of the finger turns an age of 25 into 250. None of this is unusual, and none of it is harmless: a single 99 among answers from 1 to 5 can shift an average noticeably, and a duplicated row counts one participant twice. Preparing data for analysis, usually called data cleaning, often takes longer than the analysis itself, and its decisions shape every result that follows.
Cleaning is therefore part of the analysis, not a chore before it, and it deserves the same care. This chapter first sets out what clean data means and the principles that guide cleaning decisions. It then introduces the tools for working with tables in R: choosing cases and variables, creating new variables, summarising by group, reshaping, and combining tables. Finally, it applies them, step by step, to the raw export of the wellbeing survey that Chapter 2 opened: the file with test responses, duplicated rows, the same answer spelled four ways, ages stored as text, and missing values coded as 99.
|>.Teaching & Practice: Interactive Lecture Slides · Browser Playground Exercises
Data is clean when it can be trusted to represent what was measured. That broad idea can be broken into five qualities, each of which the survey export fails in some way. Clean data is valid: every value is possible for its variable, so there are no ages of 250 or 26 hours of sleep a night. It is accurate: values record what the participant actually meant, so a missing-answer code is not mistaken for a real answer. It is complete as far as possible, and where values are missing, they are marked as missing rather than hidden behind codes. It is consistent: the same answer is always recorded in the same way, so “F”, “female”, and “Female” become one category. And it is unique: each case appears exactly once, with no test entries and no duplicates.
Each failure distorts results in a different way. A missing-answer code that stays in the data is the easiest to see. Suppose five students answered a question on a 1-to-5 scale, and one of them skipped it, which the survey tool recorded as 99:
[1] 22.6
[1] 3.5
With the code left in, the average is 22.6, a value that is impossible on a 1-to-5 scale. Marked as missing with na_if(), the code drops out, and the average of the four real answers is 3.5. In a real dataset the distortion is usually smaller and therefore harder to notice, which is exactly what makes it dangerous. Duplicates work in a similar way, but on the sample size and the balance of the sample: a participant who appears twice counts twice.
Three principles follow for every cleaning decision. The raw file is never edited; it stays exactly as it was received, and all changes are made by code that starts from it. Every decision is recorded, in the script and, where it affects the results, in the thesis: how many cases were removed and why, which values were set to missing, how scale scores were built. And the result is checked after every step, because many cleaning mistakes produce no error message at all. The final section of this chapter shows how to report the cleaning in a thesis.
Clean data should also be arranged in a way that makes analysis straightforward. The arrangement used throughout this book is called tidy data (Wickham 2014). In tidy data, each variable forms a column, each observation forms a row, and each value sits in its own cell. The idea is easiest to see in a small example. Table 3.1 stores two semesters of wellbeing for three students side by side, as a spreadsheet often would.
| student | wellbeing_s1 | wellbeing_s2 |
|---|---|---|
| S1 | 58 | 63 |
| S2 | 71 | 70 |
| S3 | 64 | 69 |
The table looks neat, but one variable, wellbeing, is spread over two columns, and a second variable, the semester, is hidden inside the column names. Table 3.2 holds the same six measurements in tidy form.
| student | semester | wellbeing |
|---|---|---|
| S1 | 1 | 58 |
| S1 | 2 | 63 |
| S2 | 1 | 71 |
| S2 | 2 | 70 |
| S3 | 1 | 64 |
| S3 | 2 | 69 |
Now each of the three variables has its own column, and each row is one observation: one student in one semester. Questions such as “average wellbeing in each semester” become a simple grouped summary. The students table in the package is tidy, with one row per student and one column per variable. The survey export is not: it squeezes four semesters of measurements side by side into one row per student. Much of cleaning consists of making data tidy, because once it is tidy, every tool in this chapter works on it.
The tools in this chapter come from the tidyverse, a collection of packages that share one way of working and are designed for tidy data. This chapter uses four of them: dplyr for working with rows and columns, tidyr for reshaping data, stringr for working with text, and readr for turning text into numbers (and for reading and writing files). The whole collection can be installed at once with install.packages("tidyverse") and loaded with library(tidyverse), but this book loads only the packages each chapter needs:
Analysis is a series of steps: take the data, keep some rows, calculate something, round the result. In Chapter 1, such steps were written by putting functions inside each other:
This reads from the inside out, which gets hard to follow as the steps multiply. The pipe, written |>, writes the steps in the order they happen. It takes the result on its left and passes it as the first argument to the function on its right:
The pipe reads as “and then”: take sleep, and then take the mean, and then round it to one decimal place. In RStudio, Ctrl+Shift+M (Cmd+Shift+M on a Mac) types the pipe for you.
Before R had its own pipe, the tidyverse used %>%, from the magrittr package. You will see it in many tutorials and older scripts. For everything in this book, it does the same job as |>.
The dplyr package provides a small set of verbs, each doing one job. Every verb takes a data frame first and returns a data frame, so verbs can be chained with the pipe. The verbs are easiest to understand on a tiny table. The function tibble() builds one; a tibble is the tidyverse’s version of a data frame, which prints more neatly:
# A tibble: 5 × 4
student faculty sleep stress
<chr> <chr> <dbl> <dbl>
1 S1 Education 6.5 3.2
2 S2 Humanities 7.5 2.1
3 S3 Education 5.5 4.5
4 S4 Health Sciences 8 1.8
5 S5 Humanities 6 3.9
Most analyses use only some of the cases: the students of one faculty, the first semester, the participants who completed the study. The verb filter() keeps the rows that meet a condition:
# A tibble: 3 × 4
student faculty sleep stress
<chr> <chr> <dbl> <dbl>
1 S1 Education 6.5 3.2
2 S3 Education 5.5 4.5
3 S5 Humanities 6 3.9
Several conditions separated by commas must all be true:
# A tibble: 2 × 4
student faculty sleep stress
<chr> <chr> <dbl> <dbl>
1 S1 Education 6.5 3.2
2 S3 Education 5.5 4.5
To keep rows matching any of several values, use %in%:
# A tibble: 4 × 4
student faculty sleep stress
<chr> <chr> <dbl> <dbl>
1 S1 Education 6.5 3.2
2 S2 Humanities 7.5 2.1
3 S3 Education 5.5 4.5
4 S5 Humanities 6 3.9
Every filter is a decision about the sample, and should be reported: an analysis of “students who completed all four semesters” answers a different question from an analysis of all students.
Large datasets have far more variables than any one analysis needs. The verb select() keeps the columns you name, in the order you name them, and a minus sign drops a column instead:
Sorting helps when looking at data, for example to see the most extreme values first. The verb arrange() sorts rows by one or more columns, and desc() sorts from largest to smallest:
Many variables used in analysis are calculated from others: a total score, a conversion to other units, a category derived from a number. The verb mutate() adds new columns calculated from existing ones, or changes existing ones:
# A tibble: 5 × 6
student faculty sleep stress sleep_minutes short_sleep
<chr> <chr> <dbl> <dbl> <dbl> <lgl>
1 S1 Education 6.5 3.2 390 TRUE
2 S2 Humanities 7.5 2.1 450 FALSE
3 S3 Education 5.5 4.5 330 TRUE
4 S4 Health Sciences 8 1.8 480 FALSE
5 S5 Humanities 6 3.9 360 TRUE
To sort values into categories, case_when() checks conditions in order and uses the first one that is true, and .default catches everything else:
# A tibble: 5 × 5
student faculty sleep stress stress_level
<chr> <chr> <dbl> <dbl> <chr>
1 S1 Education 6.5 3.2 Medium
2 S2 Humanities 7.5 2.1 Low
3 S3 Education 5.5 4.5 High
4 S4 Health Sciences 8 1.8 Low
5 S5 Humanities 6 3.9 Medium
Turning a number into categories in this way loses information, since a stress score of 3.9 and one of 2.6 become the same “Medium”. It is useful for description, but analyses should normally keep the original number.
Summaries reduce many values to a few numbers. The verb summarise() reduces a table to one row of summaries, and n() counts the rows:
# A tibble: 1 × 2
mean_sleep students
<dbl> <int>
1 6.7 5
The real power comes with groups. The .by argument calculates the summary separately for each group:
# A tibble: 3 × 3
faculty mean_sleep students
<chr> <dbl> <int>
1 Education 6 2
2 Humanities 6.75 2
3 Health Sciences 8 1
For simple counts, count() is a shortcut:
# A tibble: 3 × 2
faculty n
<chr> <int>
1 Education 2
2 Health Sciences 1
3 Humanities 2
In many tutorials you will see group_by(faculty) |> summarise(...) instead of the .by argument. Both give the same result. The .by argument is newer and keeps each step self-contained, so this book uses it.
Each verb is simple, but chained together they answer real questions. One such question concerns how average wellbeing changed over the four semesters, separately for students who were and were not invited to the workshop. The semester records are in semesters, and the workshop group is in students, so the workshop group is first added to the semester records. (The left_join() in the first line is explained in Section 3.6.)
semester workshop mean_wellbeing students
1 1 Invited 60.64667 300
2 1 Not invited 60.26667 300
3 2 Invited 65.47000 300
4 2 Not invited 60.22000 300
5 3 Invited 63.22261 283
6 3 Not invited 59.22857 280
7 4 Invited 60.93286 283
8 4 Not invited 58.42500 280
Read aloud, the code says: take the semester records, and then add each student’s workshop group, and then calculate the average wellbeing and the number of students for each semester and workshop group, and then sort the result. In semester 1, before the workshop, the two groups are almost level. From semester 2, the invited students are ahead. Chapter 7 tests whether that difference is larger than chance.
The number of students also drops in semesters 3 and 4. Some students left the study, which is worth remembering: it returns later in this chapter and in Chapter 6.
The same data can be laid out in two ways, as the section on tidy data showed. In wide format, repeated measurements sit side by side, one column per occasion. In long format, each measurement has its own row. Here is the wide table from Table 3.1 in R:
# A tibble: 3 × 3
student wellbeing_1 wellbeing_2
<chr> <dbl> <dbl>
1 S1 58 63
2 S2 71 70
3 S3 64 69
Spreadsheets and SPSS often use wide format. For analysis in R, long format is usually easier, because each variable (student, semester, wellbeing) is then one column. The function pivot_longer() turns wide into long:
# A tibble: 6 × 3
student semester wellbeing
<chr> <chr> <dbl>
1 S1 1 58
2 S1 2 63
3 S2 1 71
4 S2 2 70
5 S3 1 64
6 S3 2 69
The arguments say which columns to stack (cols), where the old column names should go (names_to), which part of the names to drop (names_prefix), and where the values should go (values_to). The result is Table 3.2.
The opposite function, pivot_wider(), is especially useful for presenting results, because a wide table is easier to read:
# A tibble: 3 × 3
student `1` `2`
<chr> <dbl> <dbl>
1 S1 58 63
2 S2 71 70
3 S3 64 69
The average wellbeing table from the previous section, for example, reads more easily with one column per workshop group:
# A tibble: 4 × 3
semester `Not invited` Invited
<int> <dbl> <dbl>
1 1 60.3 60.6
2 2 60.2 65.5
3 3 59.2 63.2
4 4 58.4 60.9
Research data is often spread over several tables that share an identifier. The wellbeing study keeps students in one table, their semester records in another, and their supervisors in a third. Joins combine tables by matching the identifier. Two small tables show how:
The function left_join() keeps every row of the first table and adds the matching information from the second:
# A tibble: 4 × 3
student wellbeing faculty
<chr> <dbl> <chr>
1 S1 58 Education
2 S1 63 Education
3 S2 71 Humanities
4 S4 66 <NA>
The argument join_by(student) names the column that links the two tables. Student S4 has a score but no entry in people, so their faculty is NA.
The function anti_join() does the opposite: it keeps the rows of the first table that have no match in the second. It is the quickest way to find what is missing, here the students without any scores:
# A tibble: 1 × 2
student faculty
<chr> <chr>
1 S3 Education
On the wellbeing data, joins answer questions that no single table can. Each student’s supervisor has an academic rank, stored in supervisors, and a join brings it next to each student’s answer about dropping out:
rank considering_dropout students
1 Lecturer 0.1367521 234
2 Professor 0.1217391 115
3 Assistant Professor 0.1752988 251
The shares differ a little, from about 12% to about 18%. Differences this small could easily be chance; Chapter 7 shows how to test that. A second join finds the students who have no records for semester 3:
[1] 37
considering_dropout n
1 No 15
2 Yes 22
In all, 37 students have no semester 3 record: they left the study after the first year, and 22 of them had said they were considering dropping out. The students who left are not a random selection, which matters for any analysis of the later semesters. Chapter 6 returns to this.
The survey export can now be cleaned. The steps below are the ones needed for almost any survey data, and each is preceded by the problem it solves. Each step is short. What matters is doing them in a sensible order, and checking the result after each one.
Cleaning starts with looking, because problems that have not been seen cannot be fixed:
The file has 615 rows, although there are only 600 students. A few lines of code show what needs fixing:
# A tibble: 7 × 2
Q3_Gender n
<chr> <int>
1 F 28
2 Female 233
3 M 46
4 Male 173
5 female 56
6 male 78
7 <NA> 1
# A tibble: 3 × 4
`Response ID` `Q1_Student ID` Q2_Age Q3_Gender
<chr> <chr> <chr> <chr>
1 R0001 TEST 99 Female
2 R0002 test <NA> <NA>
3 R0003 TEST2 30 Male
Gender is spelled six ways (F, female, Female, and so on), plus a blank from a test response, and there are test responses from when the survey was set up. (The raw file also contains "Female " with a trailing space; read_excel() removes such spaces automatically, but other import functions do not, which is why the cleaning below still uses str_trim().) Column names such as Q1_Student ID contain spaces, so they must be written inside backticks (`) in code. And as Chapter 2 showed, many numeric columns arrived as text.
Test responses are not data about students, and duplicated submissions would count some students twice, so both must go before anything else. Test responses have a student ID starting with “TEST”, in upper or lower case. The function str_to_upper() makes the case irrelevant, and str_detect() checks for the pattern. The function distinct() then removes rows that are exact copies of another row. The response ID is different for each submission, even for duplicates, so it is dropped first:
[1] 600
In the pattern, ! means “not” and ^ means “at the start”. Exactly 600 rows are left: one per student.
Names such as Q10_How worried are you about money? (1-5) are awkward to type and easy to get wrong. Short, consistent names make the code readable and reduce errors. The function rename() changes names, new name first, and the 22 questionnaire items, Q11_1 to Q11_22, are renamed in one go with rename_with(), using the scale each item belongs to:
item_names <- c(paste0("stress_", 1:6), paste0("burnout_", 1:6),
paste0("support_", 1:6), paste0("satisfaction_", 1:4))
responses <- responses |>
rename(
student_id = `Q1_Student ID`,
age = Q2_Age,
gender = Q3_Gender,
faculty = Q4_Faculty,
programme = Q5_Programme,
study_mode = `Q6_Study mode`,
employment = `Q7_Paid work`,
has_children = Q8_Children,
lives_away = `Q9_Moved away from family`,
financial_worry = `Q10_How worried are you about money? (1-5)`,
workshop = `Workshop group`,
workshop_sessions = `Workshop sessions attended`,
considering_dropout = `Y1_Considered leaving?`
) |>
rename_with(~ item_names, .cols = Q11_1:Q11_22)The call paste0("stress_", 1:6) builds the names stress_1 to stress_6, which saves typing them all. The questionnaire’s wording is not lost: it belongs in the codebook (Chapter 2).
If “F”, “female”, and “Female” stay separate, every table and every test treats them as three different groups. Categories are fixed by looking for a pattern rather than listing every spelling, after str_to_lower() and str_trim() have removed differences in case and stray spaces:
responses <- responses |>
mutate(
gender = if_else(str_starts(str_to_lower(str_trim(gender)), "f"), "Female", "Male"),
faculty = case_when(
str_detect(str_to_lower(faculty), "educ") ~ "Education",
str_detect(str_to_lower(faculty), "health") ~ "Health Sciences",
str_detect(str_to_lower(faculty), "humanit") ~ "Humanities",
str_detect(str_to_lower(faculty), "social") ~ "Social Sciences",
str_detect(str_to_lower(faculty), "natural|^sciences$") ~ "Natural Sciences"
),
programme = if_else(str_detect(str_to_lower(programme), "ph"), "PhD", "Master's"),
study_mode = if_else(str_detect(str_to_lower(study_mode), "part"), "Part-time", "Full-time"),
employment = if_else(str_to_lower(employment) %in% c("none", "no job"), "None", employment),
across(c(has_children, lives_away, considering_dropout),
~ if_else(str_starts(str_to_lower(.x), "y"), "Yes", "No"))
)
responses |> count(gender)# A tibble: 2 × 2
gender n
<chr> <int>
1 Female 312
2 Male 288
# A tibble: 5 × 2
faculty n
<chr> <int>
1 Education 148
2 Health Sciences 154
3 Humanities 95
4 Natural Sciences 87
5 Social Sciences 116
Two details deserve attention. The order of conditions in case_when() matters: “social sciences” also contains “sciences”, so Social Sciences is checked before Natural Sciences. And across() applies the same change to several columns at once, here turning Yes, yes, and Y into Yes.
After each fix, the categories should be counted again, as above. If a spelling was missed, it shows up immediately.
Survey tools often mark missing answers with codes such as 99 or -9. As the opening example showed, these codes must become NA before any calculation, or R will treat them as real values. The function na_if() turns a code into NA, and as.integer() then turns the text into whole numbers:
99 everywhere
It is tempting to turn every 99 in the file into NA in one go. But a 99 is not always a code. In a wellbeing score from 0 to 100, it is a perfectly real value. Replace missing codes only in the columns where you know they are codes.
The function parse_number() from readr reads the number out of a text value, ignoring extra text around it. It is useful for the sleep columns, where some students typed "7 hrs". A decimal comma, however, defeats it:
The value "6,5" becomes 65, not 6.5, because in English a comma separates thousands. The comma has to be changed to a point first:
Mistakes like this produce no error message. The only protection is to check the data after each step.
Some values cannot be true, such as an age of 250 or 26 hours of sleep a night. Left in the data, a single one can dominate an average or a regression. They are typing mistakes, and since the true value cannot be known, the honest fix is to set them to NA rather than to guess (an age of 250 might have been 25, or 50):
# A tibble: 1 × 2
student_id age
<chr> <dbl>
1 S0007 250
This is where the missing age in Chapter 1 came from. Unusual but possible values, such as a student who sleeps 4 hours a night, are a different matter: they are real data and are kept. Chapter 6 discusses how to handle them.
A questionnaire scale is scored as the average of its items, and the items must all point in the same direction before they are averaged. One stress item, stress_4 (“I feel confident handling problems in my studies”), is worded in the opposite direction to the others: agreeing with it means less stress. Averaged as it stands, it would cancel part of what the other five items measure. It must first be reversed, so that 1 becomes 5, 2 becomes 4, and so on. On a 1-to-5 scale, subtracting from 6 does exactly that:
questionnaire_clean <- responses |>
select(student_id, all_of(item_names))
scores <- questionnaire_clean |>
mutate(
stress_4 = 6 - stress_4,
stress_score = rowMeans(pick(stress_1:stress_6), na.rm = TRUE),
burnout_score = rowMeans(pick(burnout_1:burnout_6), na.rm = TRUE),
support_score = rowMeans(pick(support_1:support_6), na.rm = TRUE),
satisfaction_score = rowMeans(pick(satisfaction_1:satisfaction_4), na.rm = TRUE)
) |>
select(student_id, ends_with("_score"))
head(scores)# A tibble: 6 × 5
student_id stress_score burnout_score support_score satisfaction_score
<chr> <dbl> <dbl> <dbl> <dbl>
1 S0191 2.83 1.5 3.83 3.33
2 S0015 4.5 3.83 1.67 3
3 S0474 3.5 4.5 1.4 2.75
4 S0418 3.17 3 1.67 3.75
5 S0049 3 3.5 3.17 3.33
6 S0537 2.83 2.5 3.2 4.75
The function pick() selects the columns of one scale, and rowMeans() averages each student’s answers across them. With na.rm = TRUE, a student who skipped one item still gets a score from the items they answered. Whether that is acceptable, and how many skipped items is too many, is a decision to report in the thesis. Chapter 9 checks that the items really do measure four separate scales.
The original items should stay in the data, with the scores stored separately as here, because the items are needed again for checking the scales.
The semester measurements sit side by side: GPA_S1, Sleep_S1, …, Wellbeing_S4, 28 columns in all. The data is not tidy, since each row holds four observations, and analyses of change over time need one row per student per semester. This is a pivot_longer() with one extra idea: each column name holds two pieces of information, the variable (GPA) and the semester (1), separated by _S. The special name .value tells R to use the first piece as a column name:
# A tibble: 6 × 9
student_id semester GPA Sleep Study Exercise Caffeine Meetings Wellbeing
<chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr>
1 S0191 1 3.07 5.6 31 3 140 2 64
2 S0191 2 3.29 5,7 30 4 140 7 65
3 S0191 3 3.35 5.5 32 3 130 5 60
4 S0191 4 3.58 6.6 29 2 155 7 69
5 S0015 1 2.8 6.5 15 2 210 2 34
6 S0015 2 3.2 6.5 18 0 275 5 38
The selection matches("_S[1-4]$") picks the columns whose names end in _S1 to _S4. The rest is cleaning already familiar from the steps above: tidy names, text to numbers (with the decimal-comma fix for sleep), impossible values to NA, and finally removing the empty rows of students who had left the study:
semesters_clean <- semesters_clean |>
rename(gpa = GPA, sleep_hours = Sleep, study_hours = Study,
exercise_days = Exercise, caffeine_mg = Caffeine,
supervisor_meetings = Meetings, wellbeing = Wellbeing) |>
mutate(
semester = as.integer(semester),
sleep_hours = parse_number(str_replace(sleep_hours, ",", ".")),
across(c(gpa, study_hours, exercise_days, caffeine_mg, supervisor_meetings, wellbeing),
parse_number),
sleep_hours = if_else(sleep_hours > 24, NA, sleep_hours),
study_hours = if_else(study_hours > 168, NA, study_hours),
gpa = if_else(gpa > 4, NA, gpa)
) |>
filter(!if_all(gpa:wellbeing, is.na))
nrow(semesters_clean)[1] 2326
The condition if_all(gpa:wellbeing, is.na) is TRUE for rows where every measurement is missing, and ! keeps the others.
The last step checks what the cleaning has produced. The amount of missing data comes first, and across() inside summarise() counts the NAs in every column at once:
# A tibble: 1 × 9
student_id semester gpa sleep_hours study_hours exercise_days caffeine_mg
<int> <int> <int> <int> <int> <int> <int>
1 0 0 1 21 23 20 25
# ℹ 2 more variables: supervisor_meetings <int>, wellbeing <int>
Up to 25 values per measurement are still missing, about 1% of the records: answers students really did not give, and the impossible values set to NA. These are genuine gaps, and Chapter 6 discusses how to handle them.
The most satisfying check of all is a comparison with a trusted result. The clean tables in the data2thesis package were produced from this same export, so the cleaned data should match them exactly:
[1] TRUE
[1] TRUE
Both are TRUE. The cleaning is complete, and from here on the book uses the clean tables from the package.
A thesis reports the cleaning briefly, usually in the methods chapter, so that a reader can judge what was done to the data. For the survey export, with the numbers filled in by the code:
The survey export contained 615 responses. Test responses (3) and duplicate submissions (12) were removed, leaving 600 students. Inconsistent spellings of categories were standardised, and missing-answer codes (99 and −9) were recoded as missing. Impossible values (4 in all), such as an age of 250 years or 26 hours of sleep a night, were set to missing, because the true values could not be known. The reverse-worded item stress_4 was recoded before scale scores were computed as the mean of each scale’s items, using all items a student had answered.
Put all of these steps in one script, for example 01-clean-data.R, that starts from the raw file and ends by saving the clean tables:
Never edit the raw file by hand. If you find a new problem next month, you fix it in the script and run it again, and every step stays on record.
airquality is a real dataset that comes with R: daily air measurements in New York from May to September 1973. Like most real data, it has gaps. The same verbs summarise it by month:
Month mean_ozone missing_days
1 5 23.61538 5
2 6 29.44444 21
3 7 59.11538 5
4 8 59.96154 5
5 9 31.44828 1
Ozone levels peak in July and August, and June has the most missing days, a pattern worth knowing before any analysis: if the missing days were mostly hot ones, the June average would be too low.
|> passes a result to the next function: read it as “and then”.filter() chooses cases, select() chooses variables, arrange() sorts, mutate() creates variables, summarise() with .by summarises by group, and count() counts. case_when() sorts values into categories.pivot_longer() and pivot_wider() reshape data between wide and long formats.left_join() combines tables by an identifier; anti_join() finds rows without a match.NA column by column, convert text to numbers (watch decimal commas), set impossible values to NA, reverse reversed items before computing scale scores, and check the result.Data cleaning, tidyverse, tidy data, tibble, pipe, verb, grouped summary, wide format, long format, join, identifier, missing code, reversed item, scale score.
The playground has these and more, with hints and solutions.
students, find the average age of students in each faculty, sorted from oldest to youngest. (Remember na.rm = TRUE.)semesters that is "Short" when sleep_hours is under 6, "Recommended" when it is 7 or more, and "Moderate" otherwise. Count the semester records in each category.semesters, make a table of average GPA with one row per student and one column per semester, using pivot_wider(). Show the first rows.semesters to students and compare the average semester GPA of full-time and part-time students.stress_4 item must be reversed before the stress score is calculated, and describe what would happen to the scores if it were not.