knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)

Functions in {zoomclass} package are divided in 2 categories which aims to analyse input from (mainly) Zoom's participants report .csv and Zoom's chat .txt file.

library(zoomclass)

Participants Report

In this category, High-level functions are design to analyse Zoom's participants report (.csv file), especially for checking student's attendance in Zoom classroom.

Zoom's Participant Report

The term I used "Zoom's participant report", basically, is a CSV log file that host can downloaded after Zoom meeting has ended. It contains the following columns:

This package comes with an example data of Zoom's participant report in .csv which I will use for demo in this tutorial.

# List all example data in `zoomclass` package
zoomclass_example()

# Get a path to specific example data
path_heroes <- zoomclass_example("participants_heroes.csv")
path_heroes

Path to all example files can be obtained by the following command.

# if (interactive() && requireNamespace("fs")) {
#   fs::file_show(system.file("extdata", package = "zoomclass"))
# }
system.file("extdata", package = "zoomclass")

Read Participants

read_participants() read the data in using readr::read_csv() and then performs the followings steps:

  1. Read Zoom's participant report into a tibble
  2. Clean column names, so that we can manipulate more easily in R
  3. Apply appropriate time formatting (POSIXct) for relevant columns
  4. Extract participant's current (displayed) name and original name into Name and Name_Original columns, respectively.

The output is a tibble with zoom_participants subclass. If metadata regarding Zoom meeting was found, it will be assigned to a meeting_overview attribute.

pp_heroes <- read_participants(path_heroes)

str(pp_heroes)

As you can see, almost all column names were cleaned with no spaces; Join_Time and Leave_Time were formatted as POSIXct object, correctly. Moreover, "015_Loki (Loki Laufeyson)" in original Name (Original Name) column is extracted into "015_Loki" in Name and "Loki Laufeyson" in Name_Original, respectively.

The output tibble pp_heroes contain sessions information of each participants. If we count names of each participants, we can see that some rows is duplicated.

library(dplyr)
pp_heroes %>% 
  count(`Name (Original Name)`, sort = TRUE) %>% 
  head()

For example, "Power Girl" has 2 rows because she had 2 sessions in Zoom (join and leave 2 times).

pp_heroes %>% 
  filter(Name == "003_Power Girl") %>% 
  select(Name, Email, Join_Time, Leave_Time, Duration_Minutes)

But why Join_Time and Leave_Time is overlapped in these 2 sessions? This might be the case that she's joined Zoom with 2 devices at the same time. As I will show you next, The upcoming functions in this package can detect this scenario.

Classroom in Zoom

Now that we have read Zoom's participants data in, It's time to analyse participants in a context of a classroom. From now on, I'll call a "participant" as "student".

Next, 3 class_* functions will be introduced:

  1. class_session() summarizes time information about individual sessions of each students (If student has multiple sessions, output will show ≥ 1 rows per that student)
  2. class_students() summarizes time information of each students (1 row per student)
  3. class_studentsID() summarizes time information of each student's ID extracted from student name (1 row per student's ID)

The first argument of these functions receives input from zoom_participants tibble as created by read_participants().

A typical academic classroom usually has an explicit start and end time. If students arrive to class later than certain cutoff time point, teacher can mark them as late.

In the class_* functions, you must provide class_start and class_end arguments by which they will used to compute 4 time intervals that student spent before, during, and after Zoom class:

For class_students() and class_studentsID(), you can compute late time period of each student by providing late_cutoff argument.

All of theses time intervals are Period object from {lubridate} package. (It can be converted to hours, minutes, or secound as well.)

Furthermore, class_* functions can tell you whether each students joined Zoom with multiple devices at the same time period. The use case of this might be when live-exam is conducted in Zoom, and you want to check that each students joined with 1 device only (no cheating).

Class Session

Suppose that our Zoom classroom was started at 10:00 and ended at 12:00, I will call class_session() as follows:

(Input class_start and class_end as 24-hours clock time with no AM or PM)

pp_heroes_session <- 
  class_session(pp_heroes, 
                class_start = "10:00", # Official class started at 10:00 AM
                class_end = "12:00" # Official class ended at 12:00 PM
                )

pp_heroes_session

As previously stated, classroom-related time intervals were computed.

pp_heroes_session %>% 
  select(Name, ends_with("Class"), Total_Time) %>% 
  head()

Let's see who spend time during class all the time

pp_heroes_session %>% 
  filter(During_Class == lubridate::hours(2)) %>% 
  select(Name, ends_with("Class"))

Or, may be Loki could cast illusion to attend Zoom till the end :-)

Now, let's see who attend Zoom using multiple device at the same time by filter Multi_Device = TRUE.

pp_heroes_session %>% 
  filter(Multi_Device  == TRUE) %>% 
  select(Name, Session, Join_Time, Leave_Time, Multi_Device)

Session column displays session number of each student as ranked by Join_Time. Let's see who joined Zoom more than 1 time.

As you can see, "Power Girl" and "Swarm" has session number = 2; however, be aware that these two joined Zoom only one time using 2 devices.

pp_heroes_session %>% 
  filter(Session  > 1) %>% 
  select(Name, Session, Join_Time, Leave_Time, Multi_Device)

Class Student

class_students() summarizes time information grouped by each students (grouping variables is Name (Original Name) and Email). The result will be a tibble with one row per student.

Most columns are the same as output from class_session(), except for the followings:

New optional argument is late_cutoff from which student will be considered late if first joined time is later than this cutoff. The late time period will be shown in Late_Time column.

pp_heroes_students <- 
  class_students(pp_heroes, 
               class_start = "10:00",
               class_end = "12:00",
               late_cutoff = "10:15" # If student joined later than 10:15 will considered late
              )

head(pp_heroes_students)

Let's see who joined class late > 10 minutes (later than 10:25).

pp_heroes_students %>% 
  filter(Late_Time > lubridate::minutes(10)) %>% 
  select(Name, First_Join_Time, Late_Time)

Class Student ID

Supposed that after you've checked class attendance of each student, perhaps you want to merge this data into a database by some key columns which is usually student's ID.

First, you informed students to put student's ID in their names, for example: "001_Megus".

class_studentsID() will help you summarizes time information grouped by each student's ID. Then, you can used these IDs to merge into a database.

The internal processes are that class_studentsID() extracts student's ID from Name (Original Name) column using regular expression as provided by id_regex. Then, time information will be summarized per student's ID (grouping variable is ID), and the rest of output columns is similar to class_students(). Finally, the result will be a tibble with one row per ID.

pp_heroes_studentsID <- 
  class_studentsID(pp_heroes, 
                   id_regex = "\\d+", # Extract digits from student name as student's ID
                   class_start = "10:00",
                   class_end = "12:00",
                   late_cutoff = "10:15" # If student joined later than 10:15 will considered late
                   )

head(pp_heroes_studentsID)

(If the same ID is founded in multiple names (e.g. "001_Magus", "001_magus"), the Name column will contain all combinations of names for that particular ID.)

Merge Data

Here, I will give an example of merging student data to a database.

This package provides heroes_students data frame that contain names and ID of student who enrolled the course in this semester.

head(heroes_students)

You can join heroes_students with pp_heroes_studentsID using dplyr::*_join functions by ID.

Check Students NOT in Zoom Class

To check whether students joined Zoom class room or not, it can be obtained by using a filtering join function: dplyr::anti_join().

These students below are in the heroes_students data frame, but not in pp_heroes_studentsID, which means that they didn't join Zoom classroom.

heroes_students %>% 
  anti_join(pp_heroes_studentsID, by = "ID")

Check Non-Student in Zoom Class

Likewise, You can also check participants who joined Zoom classroom but not in the heroes_students data frame (non-students). Again, dplyr::anti_join() can be used.

pp_heroes_studentsID %>% 
  anti_join(heroes_students, by = "ID")

Merge All

dplyr::full_join() is a mutating join function that merge 2 data frame without any rows lost from either one.

In this example, I fully joined 2 data frame pp_heroes_studentsID and heroes_students by ID. Suffix "_from_ID" and "_from_Zoom" represents unmatched rows from the list of students (heroes_students) and Zoom classroom, respectively.

# Merge to Zoom data by `ID`
heroes_students_joined <- heroes_students %>% 
  full_join(pp_heroes_studentsID, by = "ID", suffix = c("_from_ID", "_from_Zoom")) %>% 
  relocate(ID, starts_with("Name"))

head(heroes_students_joined)

Student who didn't joined Zoom classroom will have NA presented in the Name_from_Zoom column, whereas participants who joined Zoom classroom but not in the list of students will have NA presented in the Name_from_ID column.

heroes_students_joined %>% 
  filter(if_any(starts_with("Name"), is.na)) %>% 
  select(ID, starts_with("Name"), Comment)

Zoom Chat

You might want to parse Zoom chat file from .txt file to a tibble.

# Path to example Zoom Chat file
path <- zoomclass_example("zoom-chat-1.txt")

See that raw zoom-chat-1.txt looks like this.

readLines(path) %>% 
  glue::as_glue()

Use read_zoom_chat() to read it into a data frame.

# Read from Text to a Data Frame
read_zoom_chat(path)

Last updated: r Sys.Date()



Lightbridge-KS/readzoom documentation built on April 3, 2022, 5:59 p.m.