Introduction

A campus gym has asked for their attendance data to be analyzed. There expectations are actionable insights that would lead to an increase in profits. Before looking at the data a key to a successful result would be to acquire domain knowledge.

Librarys Used

library(tidyverse)
library(dplyr)

Import and Display Code

gym_data <- read_csv("gym.csv")
## Rows: 62184 Columns: 11
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## dbl  (10): number_people, timestamp, day_of_week, is_weekend, is_holiday, te...
## dttm  (1): date
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
head(gym_data)
## # A tibble: 6 × 11
##   number_people date                timestamp day_of_week is_weekend is_holiday
##           <dbl> <dttm>                  <dbl>       <dbl>      <dbl>      <dbl>
## 1            37 2015-08-15 00:00:11     61211           4          0          0
## 2            45 2015-08-15 00:20:14     62414           4          0          0
## 3            40 2015-08-15 00:30:15     63015           4          0          0
## 4            44 2015-08-15 00:40:16     63616           4          0          0
## 5            45 2015-08-15 00:50:17     64217           4          0          0
## 6            46 2015-08-15 01:00:18     64818           4          0          0
## # ℹ 5 more variables: temperature <dbl>, is_start_of_semester <dbl>,
## #   is_during_semester <dbl>, month <dbl>, hour <dbl>
view(gym_data)

CLEANING

Removing Duplicates

new_gym_data <- distinct(gym_data)
glimpse(new_gym_data)
## Rows: 62,184
## Columns: 11
## $ number_people        <dbl> 37, 45, 40, 44, 45, 46, 43, 53, 54, 43, 39, 38, 4…
## $ date                 <dttm> 2015-08-15 00:00:11, 2015-08-15 00:20:14, 2015-0…
## $ timestamp            <dbl> 61211, 62414, 63015, 63616, 64217, 64818, 66008, …
## $ day_of_week          <dbl> 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4…
## $ is_weekend           <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…
## $ is_holiday           <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…
## $ temperature          <dbl> 71.76, 71.76, 71.76, 71.76, 71.76, 72.15, 72.15, …
## $ is_start_of_semester <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…
## $ is_during_semester   <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…
## $ month                <dbl> 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8…
## $ hour                 <dbl> 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 19, 19, 1…

Adjusting Varialbe Types

Switched data from 0 and 1s to Boolean

new_gym_data$is_weekend <- as.logical(new_gym_data$is_weekend)
  new_gym_data$is_holiday <- as.logical(new_gym_data$is_holiday)
  new_gym_data$is_start_of_semester <- as.logical(new_gym_data$is_start_of_semester)
  new_gym_data$is_during_semester <- as.logical(new_gym_data$is_during_semester)
glimpse(new_gym_data)
## Rows: 62,184
## Columns: 11
## $ number_people        <dbl> 37, 45, 40, 44, 45, 46, 43, 53, 54, 43, 39, 38, 4…
## $ date                 <dttm> 2015-08-15 00:00:11, 2015-08-15 00:20:14, 2015-0…
## $ timestamp            <dbl> 61211, 62414, 63015, 63616, 64217, 64818, 66008, …
## $ day_of_week          <dbl> 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4…
## $ is_weekend           <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, …
## $ is_holiday           <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, …
## $ temperature          <dbl> 71.76, 71.76, 71.76, 71.76, 71.76, 72.15, 72.15, …
## $ is_start_of_semester <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, …
## $ is_during_semester   <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, …
## $ month                <dbl> 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8…
## $ hour                 <dbl> 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 19, 19, 1…

Looking for irrelavent/out of place entries

unique(new_gym_data$day_of_week)
unique(new_gym_data$month)
unique(new_gym_data$hour)

Analysis

Average attendance per month

att_month  <- (new_gym_data %>% group_by(month) %>% summarise(mean=mean(number_people)))
att_month2<- att_month %>% mutate(month.abb[att_month$month])
colnames(att_month2) = c("month_num", "avg_attendance", "month")
att_month2$month <-factor(att_month2$month, 
                       levels = c("Jan", "Feb", "Mar", "Apr", "May", "Jun",
                                  "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"))
head(att_month2)
## # A tibble: 6 × 3
##   month_num avg_attendance month
##       <dbl>          <dbl> <fct>
## 1         1           33.5 Jan  
## 2         2           39.2 Feb  
## 3         3           33.2 Mar  
## 4         4           38.5 Apr  
## 5         5           19.8 May  
## 6         6           20.3 Jun

Average attendance per hour

att_hour_during <- (new_gym_data %>% 
                      filter(is_during_semester == TRUE) %>%  
                      group_by(hour) %>%
                      summarise(mean=mean(number_people)))
colnames(att_hour_during) = c("hour","avg_during")
att_hour_break <- (new_gym_data %>% 
                     filter(is_during_semester == FALSE) %>%  
                     group_by(hour) %>%
                     summarise(mean=mean(number_people)))
colnames(att_hour_break) = c("hour","avg_break")

att_hour <- inner_join(att_hour_during, att_hour_break, by = c("hour"))
head(att_hour)
## # A tibble: 6 × 3
##    hour avg_during avg_break
##   <dbl>      <dbl>     <dbl>
## 1     0     18.7       6.96 
## 2     1      3.52      0.875
## 3     2      0.235     0.114
## 4     3      0.182     0.103
## 5     4      0.302     0.256
## 6     5      1.13      0.884

Attendance during semester vs break

att_semester <- (new_gym_data %>% group_by(is_during_semester) %>% summarise(mean=mean(number_people)))
head(att_semester)
## # A tibble: 2 × 2
##   is_during_semester  mean
##   <lgl>              <dbl>
## 1 FALSE               18.5
## 2 TRUE                34.5

Holidays affects on attendance

att_holiday <- (new_gym_data %>% group_by(is_holiday) %>% summarise(mean=mean(number_people)))
head(att_holiday)
## # A tibble: 2 × 2
##   is_holiday  mean
##   <lgl>      <dbl>
## 1 FALSE      29.1 
## 2 TRUE        7.52

Busiest days of the week

att_week  <- (new_gym_data %>% group_by(day_of_week) %>% 
                summarise(mean=mean(number_people))) %>% 
  mutate_if(is.numeric, ~round(., 2))

x <- data.frame(wday=rep(0:6, 2))
att_week$day_of_week <- factor(att_week$day_of_week, levels=0:6,
                      labels=c("Sun", "Mon", "Tues", "Wed",
                               "Thurs", "Fri", "Sat"))
head(att_week)
## # A tibble: 6 × 2
##   day_of_week  mean
##   <fct>       <dbl>
## 1 Sun          32.6
## 2 Mon          32.4
## 3 Tues         32.4
## 4 Wed          30.2
## 5 Thurs        30.0
## 6 Fri          23.0

Creating Vizualizations

Including Plots

plot(ggplot(data=att_month2, aes(x=month, y=avg_attendance, group = 1))+
  geom_line()+
  geom_point()+
  labs(title = 'Daily Average Per Month', y = 'attendance'))

As expected attendance drops during the periods between the spring and fall semesters. An option that was conceived is to open the gym to the general public with paid membership fees. Stakeholders however, decided this was not viable for safety reasons.

The percentage of students who commute is 81%. This means a majority of the students who use the facility are still able to travel to the gym during break, but the major drop in attendance reveals a 45% decrease in attendance.

The fall semester (Sep-Dec) saw a progressive decrease all the way into the end of the of the semester which cannot be pinned to just the finals test taking period as attendance is on the decline from the very beginning.

  • Action:
    • Without addressing attendance retention, there is potential for immediate revenue increases by implementing merchandise or juice bar sales during peak months of the year.
    • A cost efficient step would be to post weekly gym reminders on the school app during the break periods. The drop in attendance suggests students might not be aware that many of the schools facilities are still operating during break.
    • The fall semester contains multiple holidays potentially drawing students away from the gym as they go home to see family. However, hosting holiday themed events and even selling holiday themed university spirit wear has great potential for attendance retention and an increase in revenue.
plot(ggplot(att_week, aes(x = day_of_week, y = mean)) +
  geom_col() +
  geom_text(aes(label = mean), vjust = 1.5, colour = "white")+
  labs(title="Gym volume", x="Day of the Week", y="Average"))

As addressed above, knowing when peak times are provides many benefits. This trend shows that students visit the gym the most at the beginning of the week and attendance tapers off towards the end.

  • Action:
    • Designate an adequate amount of staff to handle the peak days and adjust staffing as needed.
    • Run sales when attendance is at its highest throughout the week.
    • Implement classes, personal training, and events towards the end of the week to draw more students to the gym.
matplot(att_hour$hour, cbind(att_hour$avg_during, att_hour$avg_break), type = "l", lty = 1, 
        col = c("red", "blue"), xlab = "hour", 
        ylab = "average attendance", main = "Hourly attendance")

During Semester During Break

legend("topleft", legend = c("during semester", "during break"), 
       cex = 0.75,col = c("red", "blue"), 
       lty = 1)

The hourly attendance during the break and during the semester trend in similar fashion, with the break trend showing smaller averages as students are away but peaks remain fairly consistent. Hourly attendance for this gym is standard; efforts to look for revenue increase would prove negligible results.

  • Action:
    • Adjust shift staff numbers based on traffic throughout the day.