Case Study (R): analisi dati servizio di bike-sharing

Strumenti utilizzati:
R

Come fa un servizio di bike-sharing a progettare una strategia di marketing per convertire i ciclisti occasionali in abbonati annuali

Case Study, linguaggio R, analisi dati servizio bike sharing

Scenario

Sei un analista di dati che lavora nel marketing analyst team di Cyclistic, una società di bike-sharing di Chicago. Il direttore del marketing ritiene che il successo futuro dell’azienda dipenda dalla massimizzazione del numero di abbonamenti annuali. Pertanto, il vostro team vuole capire in che modo i ciclisti occasionali e quelli annuali utilizzano le biciclette di Cyclistic in modo diverso. Sulla base di queste informazioni, il team progetterà una nuova strategia di marketing per convertire i ciclisti occasionali in abbonati annuali. Ma prima i dirigenti di Cyclistic devono valutare le vostre raccomandazioni, che devono quindi essere supportate da dati convincenti e visualizzazioni professionali.

Personale coinvolto:
  • Cyclistic: un programma di bike-sharing con più di 5800 biciclette e 600 docking station. Cyclistic si distingue per l’offerta di biciclette reclinabili, tricicli a mano e cargo bike, che rendono il bike-sharing più inclusivo per le persone con disabilità e per i ciclisti che non possono utilizzare le due ruote standard. La maggior parte dei ciclisti opta per le biciclette tradizionali; circa l’8% dei ciclisti utilizza le opzioni di assistenza. Gli utenti del bike-sharing sono più propensi a pedalare per svago, ma circa il 30% li usa per recarsi al lavoro ogni giorno.
  • Lily Moreno: il direttore del marketing e manager della società. Moreno è responsabile dello sviluppo di campagne e iniziative per promuovere il programma di bike sharing. Queste possono includere email, social media e altri canali.
  • Cyclistic marketing analytics team: un team di analisti di dati che è responsabile della raccolta, dell’analisi e del reporting dei dati che aiutano a guidare la strategia di marketing di Cyclistic. Siete entrati a far parte di questo team sei mesi fa e siete stati impegnati a conoscere la missione e gli obiettivi aziendali di Cyclistic, nonché il modo in cui voi, come analisti di dati junior, potete aiutare Cyclistic a raggiungerli.
  • Cyclistic executive team: il team esecutivo, da tradizione orientato ai dettagli, deciderà se approvare il programma di marketing raccomandato.
Linee guida:
  • In che modo gli abbonati annuali e i ciclisti occasionali differiscono nell’utilizzo delle biciclette di Cyclistic?
  • Perché i ciclisti occasionali dovrebbero acquistare l’abbonamento annuale di Cyclistic?
  • Come può Cyclistic utilizzare i media digitali per influenzare i ciclisti occasionali a fare un abbonamento annuale?


[1-6] Chiedere (Ask)

Domande:
  • Qual è il problema che stai cercando di risolvere?
    Creare un profilo delle due tipologie di clientela, in maniera tale da poter individuare tutte le loro caratteristiche comportamentali più importanti.
  • In che modo le tue intuizioni possono guidare le decisioni aziendali di Cyclistic?
    Il mio lavoro può aiutare il team di marketing ad elaborare una strategia per convertire il maggior numero di ciclisti occasionali in abbonati.
Compiti principali da svolgere:
  • Identificare il compito da svolgere
  • Identificare le principali parti coinvolte nel progetto (stakeholder)
Obiettivi:
  • Identificare il compito da svolgere
  • Identificare le principali parti coinvolte nel progetto (stakeholder)

[2-6] Preparare (Prepare)

Introduzione:

Per analizzare e identificare i comportamenti, vengono utilizzati i dati storici relative alle corse dei clienti degli ultimi 12 mesi raccolti direttamente da Cyclistic.

Domande:
  • Dove si trovano i dati?
    I dati si trovano raggruppati in una pagina accessibile tramite un link pubblico. I set di dati hanno un nome diverso perché Cyclistic è una fictional company.
  • Come sono organizzati i dati?
    I dati sono disponibili in singoli file .csv suddivisi per mensilità.
  • Ci sono problemi di pregiudizi o attendibilità in questi dati? I vostri dati sono ROCCC?
    Non ho individuato problemi di pregiudizi o attendibilità nella fase di preparazione, poiché sono stati raccolti direttamente dall’azienda e la popolazione è costituita dall’intera base di clienti. I miei dati sono affidabili, originali, completi, attuali e citati (ROCCC).
  • Come vengono gestite le tematiche sulla licenza d’uso, la privacy, la sicurezza e l’accessibilità dei dati trattati?
    Per quanto riguarda la privacy, non includono dati sensibili (es. carte di credito, numeri di telefono, etc.) rendendo impossibile risalire all’identità del singolo ciclista.
    I dati sono stati messi a disposizione da Motivate International Inc. con il presente documento di licenza. L’utilizzo è riservato esclusivamente per scopi non commerciali. Si tratta di dati pubblici che possono essere utilizzati per esplorare il modo in cui i diversi tipi di clienti utilizzano le biciclette Cyclistic. Tuttavia, le questioni relative alla privacy dei dati vietano di utilizzare le informazioni personali dei ciclisti. Ciò significa che non sarà possibile collegare gli acquisti di pass ai numeri di carta di credito per determinare se i ciclisti occasionali vivono nell’area di servizio di Cyclistic o se hanno acquistato più pass singoli.
    Ai fini di questo caso di studio, i set di dati sono appropriati e permettono di rispondere alle domande assegnate.
  • Come avete verificato l’integrità dei dati?
    Ogni set di dati ha colonne etichettate di facile identificazione e i dati sono popolati correttamente in base alla specifica tipologia.
  • La procedura eseguita come vi può aiutare nello svolgimento dell’analisi?
    La procedura seguita durante la fase di preparazione permetterà di rispondere alla domanda principale posta dal cliente, quindi nel dare un’idea precisa del modello di comportamento del ciclista che usa i servizi di Cyclistic.
  • Sono state individuate delle problematiche con i dati ricevuti?
    Sono state individuate delle celle con valori vuoti o nulli.
Compiti principali da svolgere:
  • Scaricare i dati e archiviarli in modo appropriato.
  • Identificare come è organizzato.
  • Ordinare e filtrare i dati.
  • Determinare la credibilità dei dati.
Obiettivi:
  • Descrizione di tutte le fonti di dati utilizzate.


[3-6] Processare (Process)

Introduzione:

Verranno caricati i dati degli ultimi 12 mesi e create alcune nuove colonne etichettate con una nomenclatura di facile comprensione, come “ride_length” e “day_of_the_week”.

  • Per i dataset verrà utilizzato il prefisso “ds_”;
  • Con “member” ci si riferirà agli abbonati annuali;
  • Con “casual” ci si riferirà agli utenti occasionali che noleggiano di volta in volta;
  • L’operazione di noleggio “occasionale” delle biciclette si ipotizza possa essere effettuata sul sito web della società, tramite un’applicazione mobile oppure direttamente presso le stazioni.
Domande:
  • Quali strumenti scegliere e perché?
    Per ordinare e organizzare i dati ho scelto di usare il linguaggio R con RStudio, in quanto l’ho ritenuto adatto per svolgere tutti i compiti richiesti dal case study, oltre a poter eseguire tutte le operazioni in maniera centralizzata così da permetterne anche una facile rielaborazione in caso di modifiche/integrazioni.
  • Avete garantito l’integrità dei dati?
    Sì, i dati sono coerenti in tutte le colonne.
  • Quali sono le misure adottate per garantire la pulizia dei dati?
    Innanzitutto, le colonne sono state formattate con il tipo di dati corretto e successivamente sono stati rimossi i valori Na e i duplicati.
  • Come si può verificare che i dati siano puliti e pronti per essere analizzati?
    È possibile verificarlo tramite questo file R markdown.
  • Avete documentato il vostro processo di pulizia in modo da poter rivedere e condividere i risultati?
    Si, confermo che il tutto è stato documentato dettagliatamente in questo file R markdown.
Compiti principali da svolgere:
  • Controllare che i dati non contengano errori;
  • Scegliere gli strumenti più idonei;
  • Trasformare i dati in modo da poterli utilizzare efficacemente;
  • Documentare il processo di pulizia.
Obiettivi:
  • Documentazione di qualsiasi attività di pulizia o manipolazione dei dati

Setup Librerie

Prima di caricare le librerie, tutti i relativi pacchetti devono essere già stati installati in precedenza.
In caso contrario eseguire il primo code chunk qui di seguito altrimenti passare direttamente al caricamento delle librerie.

install.packages("tidyverse")
install.packages("lubridate")
install.packages("ggplot2")
install.packages("janitor")
install.packages("dplyr")
install.packages("skimr")
install.packages("scales")
library(tidyverse) #helps wrangle data
library(lubridate) #helps wrangle data attributes
library(ggplot2) #helps visualize data
library(janitor) # simply tools for examining and cleaning dirty data
library(dplyr) # data manipulations
library(skimr) # compact and flexible summaries of data
library(scales) # scale functions for visualization
getwd() #your working directory
Step 1-5: raccolta dati

Caricare i set di dati in R:

ds_2021_011 <- read_csv("202111-divvy-tripdata.csv")
ds_2021_012 <- read_csv("202112-divvy-tripdata.csv")
ds_2022_001 <- read_csv("202201-divvy-tripdata.csv")
ds_2022_002 <- read_csv("202202-divvy-tripdata.csv")
ds_2022_003 <- read_csv("202203-divvy-tripdata.csv")
ds_2022_004 <- read_csv("202204-divvy-tripdata.csv")
ds_2022_005 <- read_csv("202205-divvy-tripdata.csv")
ds_2022_006 <- read_csv("202206-divvy-tripdata.csv")
ds_2022_007 <- read_csv("202207-divvy-tripdata.csv")
ds_2022_008 <- read_csv("202208-divvy-tripdata.csv")
ds_2022_009 <- read_csv("202209-divvy-publictripdata.csv")
ds_2022_010 <- read_csv("202210-divvy-tripdata.csv")
Step 2-5: elaborare i dati e combinarli in un unico file

Verificare la corrispondenza dei campi tra i vari set di dati e combinarli insieme.
Utilizzare come riferimento i nomi delle colonne del più recente set di dati caricato.

colnames(ds_2021_011)
colnames(ds_2021_012)
colnames(ds_2022_001)
colnames(ds_2022_002)
colnames(ds_2022_003)
colnames(ds_2022_004)
colnames(ds_2022_005)
colnames(ds_2022_006)
colnames(ds_2022_007)
colnames(ds_2022_008)
colnames(ds_2022_009)
colnames(ds_2022_010)

Assicurarsi che le colonne siano dello stesso tipo:

compare_df_cols(ds_2021_011,ds_2021_012,ds_2022_001,ds_2022_002,ds_2022_003,ds_2022_004,ds_2022_005,ds_2022_006,ds_2022_007,ds_2022_008,ds_2022_009,ds_2022_010, return = "mismatch")

Combinare i singoli set di dati in un unico data frame e rimuovere le righe e le colonne vuote, se presenti. Al termine eliminare tutti i singoli set di dati precedenti in quanto non più necessari:

ds_all_trips <- rbind(ds_2021_011, ds_2021_012, ds_2022_001, ds_2022_002, ds_2022_003, ds_2022_004, ds_2022_005, ds_2022_006, ds_2022_007, ds_2022_008, ds_2022_009, ds_2022_010)
dim(ds_all_trips)
ds_all_trips <- janitor::remove_empty(ds_all_trips,which = c("cols"))
ds_all_trips <- janitor::remove_empty(ds_all_trips,which = c("rows"))
dim(ds_all_trips)
rm(ds_2021_011,ds_2021_012,ds_2022_001,ds_2022_002,ds_2022_003,ds_2022_004,ds_2022_005,ds_2022_006,ds_2022_007,ds_2022_008,ds_2022_009,ds_2022_010)

Riepilogo della struttura dei dati:

summary(ds_all_trips)
Step 3-5: ripulire e aggiungere dati per preparare l’analisi

Esaminare il nuovo dataset creato

Elenco dei nomi delle colonne:

colnames(ds_all_trips)

Numero di righe presenti:

nrow(ds_all_trips)

Dimensioni della struttura dei dati:

dim(ds_all_trips)

Vedere le prime 6 righe del frame di dati.
Verificare che le date corrispondano all’intervallo temporale di partenza richiesto per l’analisi:

head(ds_all_trips)

Vedere le ultime 6 righe del frame di dati.
Verificare che le date corrispondano all’intervallo temporale di conclusione richiesto per l’analisi:

tail(ds_all_trips)

Vedere l’elenco delle colonne e dei tipi di dati (numerici, caratteri, etc.):

str(ds_all_trips)

Riassunto statistico dei dati.
Verificare in particolare tutti i dati numerici per individuare eventuali anomalie:

summary(ds_all_trips)

Ci sono alcuni problemi da risolvere
  1. PROBLEMA 1
    (PRESENTE SOLO SE si stanno usando set di dati precedenti al 2020)
    Nella colonna “member_casual” i membri vengono indicati in due maniere differenti (“member” e “Subscriber”), stessa cosa per i ciclisti occasionali (“Customer” e “casual”). Dovremo consolidare queste etichette da quattro a due.
  2. PROBLEMA 2
    I dati possono essere aggregati solo a livello di corsa, il che è troppo granulare. Si dovranno aggiungere altre colonne come giorno, mese e anno, in maniera tale da permettere ulteriori opportunità di aggregazione dei dati.
  3. PROBLEMA 3
    Si dovrà aggiungere un campo per il calcolo della durata della corsa. Aggiungeremo “ride_length” all’intero dataframe, con una suddivisione basata su ore, minuti e secondi.
  4. PROBLEMA 4
    Ci sono alcune corse in cui la durata risulta negativa oppure quelle in cui Divvy ha tolto le bici dalla circolazione per motivi di controllo qualità (identificabili in “start_station_name” come “HQ QR”). Si dovranno eliminare queste corse qualora presenti nell’intervallo temporale preso per l’analisi.
Problema 1-4

(PRESENTE SOLO SE si stanno usando set di dati precedenti al 2020)

Nella colonna “member_casual”, sostituire “Subscriber” con “member” e “Customer” con “casual”.

Prima del 2020, Divvy utilizzava etichette diverse per queste due tipologie di clientela; dobbiamo rendere il nostro dataframe coerente con la nomenclatura attuale/recente.

1 – Iniziare a vedere quante osservazioni rientrano in ciascuna tipologia di cliente:

table(ds_all_trips$member_casual)

2 – Eseguire il seguente code chunk solo se nel precedente sono stati segnalati valori differenti rispetto a quelle corrette “casual” e “member”, cambiandole con le nomenclature ufficiali (noi utilizzeremo le etichette usate dal 2020, “member” e “casual”).

ds_all_trips <-  ds_all_trips %>%
mutate(member_casual = recode(member_casual ,"Subscriber" = "member" ,"Customer" = "casual"))

3 – Una volta eseguita la precedente operazione (opzionale), verificare che sia stato riassegnato il numero corretto di osservazioni.

table(ds_all_trips$member_casual)
Problema 2-4

1 – Elaborazione del datetime (tramite libreria lubridate):

ds_all_trips$started_at <- lubridate::ymd_hms(ds_all_trips$started_at)
ds_all_trips$ended_at <- lubridate::ymd_hms(ds_all_trips$ended_at)

2 – Creazione di due nuovi campi per l’ora iniziale e finale di ciascuna corsa:

ds_all_trips$start_hour <- lubridate::hour(ds_all_trips$started_at)
ds_all_trips$end_hour <- lubridate::hour(ds_all_trips$ended_at)

3 – Creazione di due nuovi campi che contengano rispettivamente la lettera iniziale e il numero del giorno della settimana.
L’opzione “lubridate.week.start” serve per evitare che il sistema legga “domenica” come primo giorno della settimana, impostandolo invece in base al fuso orario locale (nel mio caso quello italiano):

ds_all_trips$day_of_week_letter <- lubridate::wday(ds_all_trips$started_at,abbr = TRUE,label = TRUE)
ds_all_trips$day_of_week_number <- lubridate::wday(ds_all_trips$started_at, week_start = getOption("lubridate.week.start", 1),locale = Sys.getlocale("LC_TIME"))

4 – Creazione di nuove colonne con valori utili per le misurazioni successive.
Aggiungere le colonne con la data, il mese, il giorno (sia numerico che testuale) e l’anno di ogni corsa.
Questo ci permetterà di aggregare i dati delle corse per ogni mese, giorno o anno; senza questi dati sarebbe possibile aggregare solo a livello di corsa:

ds_all_trips$date <- as.Date(ds_all_trips$started_at) #default format yyyy-mm-dd
ds_all_trips$month <- format(as.Date(ds_all_trips$date), "%m")
ds_all_trips$day <- format(as.Date(ds_all_trips$date), "%d")
ds_all_trips$year <- format(as.Date(ds_all_trips$date), "%Y")
ds_all_trips$day_of_week <- format(as.Date(ds_all_trips$date), "%A")

5 -Creazione di in un nuovo campo che unisca ANNO-Mese (year_month):

ds_all_trips$year_month <- paste(ds_all_trips$year, ds_all_trips$month, sep= " - ")

6 – Aggiunta di un nuovo campo con il giorno della settimana, utile per determinare i modelli di spostamento durante la settimana (weekday):

ds_all_trips$weekday <- paste(ds_all_trips$day_of_week_number, ds_all_trips$day_of_week_letter, sep= " - ")
Problema 3-4

1 – Creazione del campo durata corsa in ore, minuti, secondi:

ds_all_trips$ride_length_hours <- difftime(ds_all_trips$ended_at,ds_all_trips$started_at,units="hours")
ds_all_trips$ride_length_mins <- difftime(ds_all_trips$ended_at,ds_all_trips$started_at,units="mins")
ds_all_trips$ride_length_secs <- difftime(ds_all_trips$ended_at,ds_all_trips$started_at,units="secs")

2 – Convertire i valori di “ride_length_…” da fattore a numerico, in modo tale da poter eseguire correttamente i calcoli per le visualizzazioni successive:

is.factor(ds_all_trips$ride_length_hours)
is.factor(ds_all_trips$ride_length_mins)
is.factor(ds_all_trips$ride_length_secs)
ds_all_trips$ride_length_hours <- as.numeric(as.character(ds_all_trips$ride_length_hours))
is.numeric(ds_all_trips$ride_length_hours) # se TRUE operazione avvenuta correttamente
ds_all_trips$ride_length_mins <- as.numeric(as.character(ds_all_trips$ride_length_mins))
is.numeric(ds_all_trips$ride_length_mins) # se TRUE operazione avvenuta correttamente
ds_all_trips$ride_length_secs <- as.numeric(as.character(ds_all_trips$ride_length_secs))
is.numeric(ds_all_trips$ride_length_secs) # se TRUE operazione avvenuta correttamente

3 – Riepilogo dei dati:

summary(ds_all_trips)
Problema 4-4

1 – Rimozione valori “Na”

ds_all_trips_clean  <- drop_na(ds_all_trips)
print(paste("Rimossi", nrow(ds_all_trips) - nrow(ds_all_trips_clean), "valori Na"))
rm(ds_all_trips)

2 – Rimozione dei duplicati:

ds_all_trips_clean_no_dups <- distinct(ds_all_trips_clean)
print(paste("Rimosse", nrow(ds_all_trips_clean) - nrow(ds_all_trips_clean_no_dups), "righe duplicate"))
rm(ds_all_trips_clean) #rimozione del precedente dataset

3 – Rimozione delle righe con i valori negativi relativi alla durata della corsa (generati quando le biciclette vengono prelevate dalle banchine e controllate da Divvy per verificarne la qualità o la durata della corsa), con salvataggio su un nuovo data frame (ds_all_trips_clean_length_correct) ed eliminazione di quello precedente:

ds_all_trips_clean_length_correct <- ds_all_trips_clean_no_dups %>% filter(ride_length_secs>0)
print(paste("Rimosse", nrow(ds_all_trips_clean_no_dups) - nrow(ds_all_trips_clean_length_correct), "righe con valori negativi"))
rm(ds_all_trips_clean_no_dups) #rimozione del precedente dataset

4 – Riepilogo dati del nuovo data frame:

summary(ds_all_trips_clean_length_correct)


[4-6] Analizzare (Analyze)

Introduzione:

In questa fase si procederà con la costruzione di un profilo delle due tipologie di clientela (occasionali e abbonati) e di come si differenziano tra loro.

Verranno create nuove variabili utili per l’individuazione di caratteristiche specifiche.

Domande:
  • Come organizzare i dati per poterli analizzare?
    Assicurandoci che tutte le colonne siano coerenti con la tipologia di dato presente.
  • I dati sono stati formattati correttamente?
    Tutti i dati sono stati formattati correttamente.
  • Quali sorprese hai scoperto analizzando i dati?
    Non ho trovato particolari sorprese, probabilmente grazie al fatto che negli anni la società ha migliorato costantemente la gestione di questi set di dati (ad esempio, confrontando una dataset precedente al 2020 si trovano molte più discrepanze).
  • Quali tendenze o relazioni hai riscontrato nei dati?
    Ho scoperto che il numero di corse sembra essere influenzato dalle condizioni climatiche e che la differenza di proporzione tra abbonati annuali e occasionali è minore nei mesi “caldi”.
  • In che modo questi approfondimenti (insight) aiutano a rispondere alle domande cardine di questo caso studio?
    Questi approfondimenti ci aiutano a capire come i clienti di Cyclist utilizzano il servizio di noleggio biciclette.
Compiti principali da svolgere:
  • Aggregare i dati in modo che siano utili e accessibili;
  • Organizzare e formattare i dati;
  • Eseguire i calcoli;
  • Identificare tendenze e relazioni.
Obiettivi:
  • Una sintesi dell’analisi svolta
Step 4-5: condurre un’analisi descrittiva

Analisi

1 – Analisi descrittiva sulla durata della corsa (valori sono espressi in ore, minuti e secondi).

MEAN
mean(ds_all_trips_clean_length_correct$ride_length_hours) #hours straight average (total ride length hours / rides)
mean(ds_all_trips_clean_length_correct$ride_length_mins) #mins straight average (total ride length minutes / rides)
mean(ds_all_trips_clean_length_correct$ride_length_secs) #secs straight average (total ride length seconds / rides)
MEDIAN
median(ds_all_trips_clean_length_correct$ride_length_hours) #midpoint number in the ascending array of hour ride lengths
median(ds_all_trips_clean_length_correct$ride_length_mins) #midpoint number in the ascending array of mins ride lengths
median(ds_all_trips_clean_length_correct$ride_length_secs) #midpoint number in the ascending array of secs ride lengths
MAX
max(ds_all_trips_clean_length_correct$ride_length_hours) #hours longest ride
max(ds_all_trips_clean_length_correct$ride_length_mins) #mins longest ride
max(ds_all_trips_clean_length_correct$ride_length_secs) #secs longest ride
MIN
min(ds_all_trips_clean_length_correct$ride_length_hours) #hours shortest ride
min(ds_all_trips_clean_length_correct$ride_length_mins) #mins shortest ride
min(ds_all_trips_clean_length_correct$ride_length_secs) #secs shortest ride

2 – È possibile raggruppare le analisi precedenti per “hours”, “mins” e “secs” utilizzando summary():

Istruzioni:
summary(ds_all_trips_clean_length_correct$ride_length_hours)
summary(ds_all_trips_clean_length_correct$ride_length_mins)
summary(ds_all_trips_clean_length_correct$ride_length_secs)

3 – Confronto tra abbonati e utenti occasionali per durata delle corse in ore, minuti e secondi.

Introduzione

N.B. Tra media e mediana è preferibile prendere in considerazione la mediana.
La media è solitamente la misura più appropriata per determinare una posizione. Questo perché tiene conto di ogni valore nel set di dati. Tuttavia, i valori anomali nel set di dati possono influenzare la media portandola a non rappresentare accuratamente tutti i punteggi (come succede nel dataset usato in questo case study). Quindi, la mediana è una misura migliore poiché i valori anomali non la influenzano.

3.1 – “mean” calcolo della media (durata media di una corsa)
setNames(aggregate(ds_all_trips_clean_length_correct$ride_length_hours ~ ds_all_trips_clean_length_correct$member_casual, FUN = mean), c("clientela", "mean hours"))
setNames(aggregate(ds_all_trips_clean_length_correct$ride_length_mins ~ ds_all_trips_clean_length_correct$member_casual, FUN = mean), c("clientela", "mean mins"))
setNames(aggregate(ds_all_trips_clean_length_correct$ride_length_secs ~ ds_all_trips_clean_length_correct$member_casual, FUN = mean), c("clientela", "mean secs"))
3.2 – “median” calcolo della mediana (calore numerico centrale relativo alla durata delle corse)
setNames(aggregate(ds_all_trips_clean_length_correct$ride_length_hours ~ ds_all_trips_clean_length_correct$member_casual, FUN = median), c("clientela", "median hours"))
setNames(aggregate(ds_all_trips_clean_length_correct$ride_length_mins ~ ds_all_trips_clean_length_correct$member_casual, FUN = median), c("clientela", "median mins"))
setNames(aggregate(ds_all_trips_clean_length_correct$ride_length_secs ~ ds_all_trips_clean_length_correct$member_casual, FUN = median), c("clientela", "median secs"))
3.3 – “max” calcolo della durata massima di una corsa
setNames(aggregate(ds_all_trips_clean_length_correct$ride_length_hours ~ ds_all_trips_clean_length_correct$member_casual, FUN = max), c("clientela", "max hours"))
setNames(aggregate(ds_all_trips_clean_length_correct$ride_length_mins ~ ds_all_trips_clean_length_correct$member_casual, FUN = max), c("clientela", "max mins"))
setNames(aggregate(ds_all_trips_clean_length_correct$ride_length_secs ~ ds_all_trips_clean_length_correct$member_casual, FUN = max), c("clientela", "max secs"))
3.4 – “min” calcolo della duarata minima di una corsa
setNames(aggregate(ds_all_trips_clean_length_correct$ride_length_hours ~ ds_all_trips_clean_length_correct$member_casual, FUN = min), c("clientela", "min hours"))
setNames(aggregate(ds_all_trips_clean_length_correct$ride_length_mins ~ ds_all_trips_clean_length_correct$member_casual, FUN = min), c("clientela", "min mins"))
setNames(aggregate(ds_all_trips_clean_length_correct$ride_length_secs ~ ds_all_trips_clean_length_correct$member_casual, FUN = min), c("clientela", "min secs"))

4 – Vedere il tempo medio di percorrenza per ogni giorno tra abbonati e utenti occasionali:

Istruzioni
setNames(aggregate(ds_all_trips_clean_length_correct$ride_length_secs ~ ds_all_trips_clean_length_correct$member_casual + ds_all_trips_clean_length_correct$day_of_week, FUN = mean), c("clientela", "giorno settimana", "durata in secondi"))

4.1 – Si noti che i giorni della settimana non sono in ordine; risolviamo il problema.
Indicare i nomi dei giorni in italiano se il sistema operativo è in questa lingua (sempre che si stia utilizzando per l’elaborazione di questo lavoro un applicativo installato sul proprio computer, esempio RStudio Desktop), altrimenti usare la relativa traduzione (sulle piattaforme online lasciare i nomi dei giorni in inglese):

ds_all_trips_clean_length_correct$day_of_week <- ordered(ds_all_trips_clean_length_correct$day_of_week, levels=c("Lunedì", "Martedì", "Mercoledì", "Giovedì", "Venerdì", "Sabato", "Domenica"))

4.2 – Analizziamo nuovamente il tempo medio di percorrenza per ogni giorno tra abbonati e utenti occasionali:

setNames(aggregate(ds_all_trips_clean_length_correct$ride_length_secs ~ ds_all_trips_clean_length_correct$member_casual + ds_all_trips_clean_length_correct$day_of_week, FUN = mean), c("clientela", "giorno settimana", "durata in secondi"))

5 – Analizzare i dati sulle corse per tipo e giorno della settimana.

Istruzioni

Per evitare il messaggio `summarise()` ha raggruppato l’output per ‘member_casual’ possiamo nasconderlo utilizzando l’argomento `.summarise`e impostandolo a “FALSE“.

options(dplyr.summarise.inform = FALSE)

ATTENZIONE: su tutti i grafici la funzione “lubridate” è stata utilizzata per ordinare i dati con il giorno della settimana nella lingua in base al fuso orario locale, che nel mio caso parte da lunedì (altrimenti l’elenco partirebbe da domenica). Se è necessario rispettare l’ordinamento originale rimuovere lubridate (in pratica, rimuovere la singola istruzione con lubridate e riattivare quella successiva disabilitata con il simbolo #).

ds_all_trips_clean_length_correct %>% 
  mutate(weekday = lubridate::wday(ds_all_trips_clean_length_correct$started_at, label = TRUE, week_start = getOption("lubridate.week.start", 1),locale = Sys.getlocale("LC_TIME"))) %>% #creates weekday field using wday()
  #mutate(weekday = wday(started_at, label = TRUE)) %>%
  group_by(member_casual, weekday) %>%  #groups by usertype and weekday
  dplyr::summarise(number_of_rides = n() #calculates the number of rides and average duration
  ,average_duration_hours = mean(ride_length_hours), average_duration_mins = mean(ride_length_mins), average_duration_secs = mean(ride_length_secs)) %>%  #calculates the average duration
  arrange(member_casual, weekday)  #sorts

6 – Visualizziamo il numero di corse per tipo di ciclista

Istruzioni
# This function help to resize the plots
function_help_resize_plots <- function(width, heigth){options(repr.plot.width = width, repr.plot.height = heigth)}
ds_all_trips_clean_length_correct %>% 
  mutate(weekday = lubridate::wday(ds_all_trips_clean_length_correct$started_at, label = TRUE, week_start = getOption("lubridate.week.start", 1),locale = Sys.getlocale("LC_TIME"))) %>%
  #mutate(weekday = wday(started_at, label = TRUE)) %>% 
  group_by(member_casual, weekday) %>% 
  dplyr::summarise(number_of_rides = n(), average_duration = mean(ride_length_secs)) %>% 
  arrange(member_casual, weekday)  %>% 
  ggplot(aes(x = weekday, y = number_of_rides, fill = member_casual)) +
  guides(fill = guide_legend(title = "Categoria utenti")) +
  scale_y_continuous("numero corse", labels = scales::comma) +
  scale_x_discrete(name = "giorno settimana") +
  scale_fill_discrete(labels = c("Occasionali", "Abbonati")) +
  geom_col(position = "dodge")
Numero corse giorno settimana
Considerazioni su quanto elaborato fino a questo punto

Dai grafici appena elaborati si possono trarre alcune considerazioni:

  • durante il weekend (sabato e domenica) gli utenti occasionali aumentano notevolmente il numero di corse rispetto agli altri singoli giorni della settimana.
  • gli abbonati annuali effettuano un maggior numero di corse durante la settimana lavorativa rispetto al weekend, quindi è presumibile un comportamento legato ad un uso dell’abbonamento per scopo lavorativo (spostamento casa/lavoro).

7 – Creiamo una visualizzazione per la durata media

7.1 – Ordinamento in ore:
ds_all_trips_clean_length_correct %>% 
  mutate(weekday = lubridate::wday(ds_all_trips_clean_length_correct$started_at, label = TRUE, week_start = getOption("lubridate.week.start", 1),locale = Sys.getlocale("LC_TIME"))) %>%
  #mutate(weekday = wday(started_at, label = TRUE)) %>% 
  group_by(member_casual, weekday) %>% 
  dplyr::summarise(number_of_rides = n(), average_duration = mean(ride_length_hours)) %>% 
  arrange(member_casual, weekday)  %>% 
  ggplot(aes(x = weekday, y = average_duration, fill = member_casual)) +
  guides(fill = guide_legend(title = "Categoria utenti")) +
  scale_y_continuous("durata media in ore") +
  scale_x_discrete(name = "giorno settimana") +
  scale_fill_discrete(labels = c("Occasionali", "Abbonati")) +
  geom_col(position = "dodge")
Durata media corsa in ore
7.2 – Ordinamento in minuti:
ds_all_trips_clean_length_correct %>% 
  mutate(weekday = lubridate::wday(ds_all_trips_clean_length_correct$started_at, label = TRUE, week_start = getOption("lubridate.week.start", 1),locale = Sys.getlocale("LC_TIME"))) %>%
  #mutate(weekday = wday(started_at, label = TRUE)) %>% 
  group_by(member_casual, weekday) %>% 
  dplyr::summarise(number_of_rides = n(), average_duration = mean(ride_length_mins)) %>% 
  arrange(member_casual, weekday)  %>% 
  ggplot(aes(x = weekday, y = average_duration, fill = member_casual)) +
  guides(fill = guide_legend(title = "Categoria utenti")) +
  scale_y_continuous("durata media in minuti") +
  scale_x_discrete(name = "giorno settimana") +
  scale_fill_discrete(labels = c("Occasionali", "Abbonati")) +
  geom_col(position = "dodge")
Durata media corsa in minuti
7.3 – Ordinamento in secondi:
ds_all_trips_clean_length_correct %>% 
  mutate(weekday = lubridate::wday(ds_all_trips_clean_length_correct$started_at, label = TRUE, week_start = getOption("lubridate.week.start", 1),locale = Sys.getlocale("LC_TIME"))) %>%
  #mutate(weekday = wday(started_at, label = TRUE)) %>% 
  group_by(member_casual, weekday) %>% 
  dplyr::summarise(number_of_rides = n(), average_duration = mean(ride_length_secs)) %>% 
  arrange(member_casual, weekday)  %>% 
  ggplot(aes(x = weekday, y = average_duration, fill = member_casual)) +
  guides(fill = guide_legend(title = "Categoria utenti")) +
  scale_y_continuous("durata media in secondi") +
  scale_x_discrete(name = "giorno settimana") +
  scale_fill_discrete(labels = c("Occasionali", "Abbonati")) +
  geom_col(position = "dodge")
Durata media corsa in secondi
Considerazioni:

Da questi grafici si possono trarre alcune considerazioni:

  • gli utenti occasionali hanno un tempo di utilizzo delle biciclette doppio rispetto agli abbonati annuali;
  • gli abbonati annuali hanno un tempo di percorrenza distribuito egualmente in tutta la settimana;
  • quelli occasionali vedono un utilizzo significativamente più elevato (rispetto agli altri giorni della settimana) durante il fine settimana e il lunedì.


Distribuzione dei dati

In questa fase vogliamo cercare di rispondere alle domande più elementari sulla distribuzione dei dati.

Occasionali vs Abbonati

Quanti dati riguardano gli abbonati annuali (member) e quanti gli occasionali (casual)?

ds_all_trips_clean_length_correct %>% 
    group_by(member_casual) %>% 
    summarise(count = length(ride_id),
              '%' = (length(ride_id) / nrow(ds_all_trips_clean_length_correct)) * 100)

Visualizzazione dei dati con un grafico a barre:

function_help_resize_plots(16,8)
ggplot(ds_all_trips_clean_length_correct, aes(member_casual, fill=member_casual)) +
  guides(fill = guide_legend(title = "Categoria utenti")) +
  geom_bar() +
  scale_y_continuous("numero corse", labels = scales::comma) +
  scale_x_discrete(name = "Occasionali vs Abbonati") +
  scale_fill_discrete(labels = c("Occasionali", "Abbonati")) +
  labs(title="Grafico 1 - Occasionali vs Abbonati")
Grafico 1 - Occasionali vs Abbonati

Visualizzazione su grafico a torta:

ds_all_trips_clean_length_correct %>% 
    group_by(member_casual) %>% 
    summarise(count = length(ride_id),
              percent_cat_users = round((length(ride_id) / nrow(ds_all_trips_clean_length_correct)) * 100, digits = 2)) %>% 
  #arrange(percent_cat_users) %>%
  #mutate(percent_cat_users_labels = scales::percent(percent_cat_users, accuracy = 0.01)) %>% 
  ggplot(aes(x = "", y = count, fill = member_casual)) +
  geom_col(color = "black") +
  geom_label(aes(label = paste0(percent_cat_users, "%")),	position = position_stack(vjust = 0.5), show.legend = FALSE) +
  guides(fill = guide_legend(title = "Categoria utenti")) +
  scale_fill_discrete(labels = c("Occasionali", "Abbonati")) +
  labs(title="Occasionali vs Abbonati (in %)",
      subtitle = "novembre 2021 - ottobre 2022",
      x="",
      y="") + 
  #scale_fill_viridis_d() +
  coord_polar(theta = "y") +
  theme_void() # remove default theme
Occasionali vs Abbonati

Come si può vedere nella tabella “Occasionali vs Abbonati”, gli abbonati hanno una proporzione maggiore del dataset, con una percentuale del ~60%, ~20% in più rispetto al conteggio dei ciclisti occasionali.

Mese

Come sono distribuiti i dati per mese?

ds_all_trips_clean_length_correct %>%
    group_by(year_month) %>%
    summarise(count = length(ride_id),
              '%' = (length(ride_id) / nrow(ds_all_trips_clean_length_correct)) * 100,
              'members_p' = (sum(member_casual == "member") / length(ride_id)) * 100,
              'casual_p' = (sum(member_casual == "casual") / length(ride_id)) * 100,
              'dif_members_casuals' = members_p - casual_p)
ds_all_trips_clean_length_correct %>%
  ggplot(aes(year_month, fill=member_casual)) +
  geom_bar() +
  labs(x="Mese", title="Grafico 2 - Distribuzione per mese") +
  guides(fill = guide_legend(title = "Categoria utenti")) +
  scale_y_continuous("numero corse", labels = scales::comma) +
  scale_fill_discrete(labels = c("Occasionali", "Abbonati")) +
  theme(axis.text.x=element_text(angle=45,hjust=1))
Grafico 2 - Distribuzione per mese

Da questo grafico si possono trarre alcune considerazioni:

  • il numero di corse sembra essere influenzato dalle condizioni climatiche;
  • il mese con il maggior numero di punti dati è stato luglio 2022, con ~14,5% del dataset;
  • in tutti i mesi abbiamo più corse di abbonati che di occasionali;
  • la differenza di proporzione tra abbonati annuali e occasionali è minore nei mesi “caldi”;
  • la distribuzione sembra ciclica.
Verificare la correlazione con le condizioni climatiche

Per verificare la correlazione con le condizioni climatiche, confrontiamo il numero delle corse con i dati climatici di Chicago nel periodo oggetto del case study.

Fonte: Climate-Data.org (Media giornaliera °C, novembre 2021 — ottobre 2022).
Attenzione: qualora cambi l’intervallo mensile di riferimento, risulta necessario cambiare manualmente i valori delle temperature nel seguente dataset “ds_chicago_temperature“.

ds_stats_n_races <- ds_all_trips_clean_length_correct %>%
  group_by(year_month) %>%
  summarise(count = length(ride_id),
            '%' = (length(ride_id) / nrow(ds_all_trips_clean_length_correct)) * 100,
            'members_p' = (sum(member_casual == "member") / length(ride_id)) * 100,
            'casual_p' = (sum(member_casual == "casual") / length(ride_id)) * 100,
            'dif_members_casuals' = members_p - casual_p)
ds_chicago_temperature <- data.frame(temp_mean=c(5.9, -0.5, -3.8, -3.1, 1.6, 7.8, 14.5, 20.7, 23.8, 23.2, 19.6, 12.7),month=c("2021 - 11", "2021 - 12", "2022 - 01", "2022 - 02", "2022 - 03", "2022 - 04", "2022 - 05", "2022 - 06", "2022 - 07", "2022 - 08", "2022 - 09", "2022 - 10"))
ds_chicago_temperature_races <- data.frame(ds_stats_n_races, ds_chicago_temperature)

ds_all_trips_clean_length_correct %>%
  ggplot(aes(year_month, fill=member_casual)) +
  geom_bar() +
  labs(x="Mese", title="Grafico 2 - Distribuzione corse per mese") +
  guides(fill = guide_legend(title = "Categoria utenti")) +
  scale_y_continuous("numero corse", labels = scales::comma) +
  scale_fill_discrete(labels = c("Occasionali", "Abbonati")) +
  theme(axis.text.x=element_text(angle=45,hjust=1))
Grafico 2 - Distribuzione per mese
ggplot(ds_chicago_temperature_races, aes(x = year_month, y = count)) +                            # bar plot
  geom_col(linewidth = 1, color = "darkblue", fill = "white") +
  scale_x_discrete(name = "anno - mese") +
  scale_y_continuous("numero corse", labels = scales::comma) +
  scale_fill_discrete(labels = c("Occasionali", "Abbonati")) +
  theme(axis.text.x=element_text(angle=45,hjust=1))
Numero corse
ggplot(ds_chicago_temperature_races, aes(x = month, y = temp_mean)) +                            # line plot
  geom_line(linewidth = 1.5, color="red", group = 1) +
  scale_x_discrete(name = "anno - mese") +
  scale_y_continuous(name = "temperaratura media") +
  theme(axis.text.x=element_text(angle=45,hjust=1))
Temperatura media
ggplot(ds_chicago_temperature_races, aes(x = year_month)) + 
  geom_col(aes(y = count), size = 1, color = "darkblue", fill = "white") +
  #scale_y_continuous("numero corse", labels = scales::comma) +
  geom_line(aes(y = 30000*temp_mean), linewidth = 1.5, color="red", group = 1) + 
  scale_x_discrete(name = "anno - mese") +
  scale_y_continuous(sec.axis = sec_axis(~./300000, name = "temp mean")) +
  theme(axis.text.x=element_text(angle=45,hjust=1),axis.text.y=element_blank(),axis.ticks.y=element_blank()) +
  labs(title = "Confronto tra numero corse e temperatura", subtitle = "la linea rossa indica la temperatura media nel mese corrispondente")
Confronto tra numero corse e temperatura
rm(ds_stats_n_races) # remove temporary dataset used to sync temperatures

Il risultato principale è:

  • la temperatura influenza il volume di corse mensile, in particolare in quelli invernali/freddi;
  • quando la temparatura scende sotto lo zero, i clienti occasionali tendono ad evitare l’utilizzo della bicicletta mentre nelle stesse condizioni gli abbonati annuali mantengono una certa frequenza di utilizzo probabilmente legata ad uno spostamento obbligatorio (casa/lavoro).
Giorni della settimana

Come sono distribuiti i dati sui giorni della settimana?

ds_all_trips_clean_length_correct %>%
    group_by(weekday) %>% 
    summarise(count = length(ride_id),
              '%' = (length(ride_id) / nrow(ds_all_trips_clean_length_correct)) * 100,
              'members_p' = (sum(member_casual == "member") / length(ride_id)) * 100,
              'casuals_p' = (sum(member_casual == "casual") / length(ride_id)) * 100,
              'dif_perc_members_casuals' = members_p - casuals_p)

Visualizziamo i dati tramite un grafico (ingrandirlo per una migliore lettura)

ggplot(ds_all_trips_clean_length_correct, aes(weekday, fill=member_casual)) +
  geom_bar(position='dodge2') +
  #geom_text(stat = "count", aes(label = ..count..)) +
  geom_label(stat='count',
             aes(label=after_stat(count)),
             position=position_dodge2(width=0.5),
             size=3,
             show.legend = FALSE # rimuove la lettera all'interno dell'icona
             ) +
  labs(x="giorni della settimana", title="Grafico 3.1 - Distribuzione per giorno della settimana") +
  guides(fill = guide_legend(title = "Categoria utenti")) +
  scale_y_continuous("numero corse", labels = scales::comma) +
  scale_fill_discrete(labels = c("Occasionali", "Abbonati"))
  #theme(legend.position = "none")
Grafico 3.1 - Distribuzione per giorno della settimana

Medesimi dati ma visualizzati in maniera differente:

ggplot(ds_all_trips_clean_length_correct, aes(weekday, fill=member_casual)) +
  geom_bar() +
  #geom_text(stat = "count", aes(label = ..count..)) +
  geom_text(stat = "count", 
            aes(label= after_stat(count)), 
            position = position_stack(vjust = 0.5),
            size = 3
            ) +
  labs(x="giorni della settimana", title="Grafico 3.2 - Distribuzione per giorno della settimana") +
  guides(fill = guide_legend(title = "Categoria utenti")) +
  scale_y_continuous("numero corse", labels = scales::comma) +
  scale_fill_discrete(labels = c("Occasionali", "Abbonati"))
Grafico 3.2 - Distribuzione per giorno della settimana

È interessante vedere:

  • il volume di dati è distribuito sostanzialmente in maniera eguale in tutti i giorni della settimana tranne il sabato;
  • il sabato ha il maggior numero di punti dati;
  • gli abbonati annuali hanno il maggior volume di dati, eccetto il sabato. In questo giorno della settimana gli occasionali hanno il maggior numero di punti dati;
  • nei fine settimana a partire dal venerdì si registrano il maggior volume di corse per gli occasionali, con un aumento fino al 20%.
Ore del giorno
ds_all_trips_clean_length_correct %>%
    group_by(start_hour) %>% 
    summarise(count = length(ride_id),
          '%' = (length(ride_id) / nrow(ds_all_trips_clean_length_correct)) * 100,
          'members_p' = (sum(member_casual == "member") / length(ride_id)) * 100,
          'casuals_p' = (sum(member_casual == "casual") / length(ride_id)) * 100,
          'dif_perc_members_casuals' = members_p - casuals_p)
ds_all_trips_clean_length_correct %>%
  ggplot(aes(start_hour, fill=member_casual)) +
  scale_y_continuous("numero corse", labels = scales::comma) +
  labs(x="Ora del giorno", title="Grafico 4.1 - Distribuzione per ora del giorno") +
  guides(fill = guide_legend(title = "Categoria utenti")) +
  scale_fill_discrete(labels = c("Occasionali", "Abbonati")) +
  geom_bar()
Grafico 4.1 - Distribuzione per ora del giorno

medesimi risultati mostrati in maniera differente (ingrandire il grafico)

ggplot(ds_all_trips_clean_length_correct, aes(start_hour, fill=member_casual)) +
  geom_bar(position='dodge2') +
  #geom_text(stat = "count", aes(label = ..count..)) +
  geom_label(stat='count',
             aes(label=after_stat(count)),
             position=position_dodge2(width=0.5),
             size=3,
             show.legend = FALSE # rimuove la lettera all'interno dell'icona
             ) +
  labs(x="ore del giorno", title="Grafico 4.2 - Distribuzione per ora del giorno") +
  guides(fill = guide_legend(title = "Categoria utenti")) +
  scale_y_continuous("numero corse", labels = scales::comma) +
  scale_fill_discrete(labels = c("Occasionali", "Abbonati")) +
  theme(legend.position = c(0.2, 0.8))
  # coord_flip()
  #theme(legend.position = "none")
Grafico 4.2 - Distribuzione per ora del giorno

Da questo grafico, possiamo vedere:

  • nel pomeriggio c’è una maggiore affluenza di ciclisti;
  • il numero di corse degli abbonati annuali sono significativamente maggiori rispetto a quelli occasionali nella fascia oraria dalle 6 alle 10 del mattino e tra le 15 e le 19 di sera;
  • gli occasionali sono maggiori rispetto agli abbonati tra le 24.00 e le 4.00 del mattino.

Questo grafico può essere ampliato se lo si divide per ciascun giorno della settimana.

ds_all_trips_clean_length_correct %>%
  ggplot(aes(start_hour, fill=member_casual)) +
  geom_bar() +
  labs(x="Ora del giorno", y="numero corse", title="Grafico 5 - Distribuzione per ora del giorno e giorno della settimana") +
  guides(fill = guide_legend(title = "Categoria utenti")) +
  scale_fill_discrete(labels = c("Occasionali", "Abbonati")) +
  facet_wrap(~ weekday)
Grafico 5 - Distribuzione per ora del giorno e giorno della settimana

C’è una chiara differenza tra i giorni infrasettimanali e i fine settimana.

Generiamo i grafici per queste due suddivisioni settimanali.

ds_all_trips_clean_length_correct %>%
  mutate(type_of_weekday = ifelse(weekday == '6 - Sat' | weekday == '7 - Sun', 'weekend', 'midweek')) %>%
  ggplot(aes(start_hour, fill=member_casual)) +
  guides(fill = guide_legend(title = "Categoria utenti")) +
  scale_fill_discrete(labels = c("Occasionali", "Abbonati")) +
  scale_y_continuous("numero corse", labels = scales::comma) +
  labs(x="Ora del giorno", title="Grafico 6 - Distribuzione per ora del giorno infrasettimanale-weekend") +
  geom_bar() +
  facet_wrap(~ type_of_weekday)
Grafico 6 - Distribuzione per ora del giorno infrasettimanale-weekend

I due plot differiscono per alcuni aspetti fondamentali:

  • mentre i fine settimana hanno un flusso regolare di punti dati, i giorni infrasettimanali hanno un flusso di dati più ripido/irregolare;
  • il conteggio dei punti di dati non ha molto significato, dato che ogni grafico rappresenta un numero diverso di giorni;
  • per i giorni infrasettimanali c’è un grande aumento di punti dati tra le 6 e le 8 del mattino (in particolare di abbonati) per poi calare nelle due ore successive (9 e 10) e poi riprendere con un costante aumento dalle 11 del mattino alle 18 di sera;
  • un altro grande aumento si ha dalle 16.00 alle 18.00, in particolare di abbonati;
  • durante il fine settimana abbiamo un flusso maggiore di utenti occasionali tra le 11:00 del mattino e le 18:00 di sera.


Riassumento quanto sopra:
È fondamentale distinguere le due tipologie di ciclisti (abbonati e occasionali) che utilizzano le biciclette nei vari periodi della giornata. Possiamo ipotizzare alcuni fattori, uno dei quali è che gli abbonati possono essere persone che utilizzano le biciclette durante le loro attività quotidiane di routine, come andare al lavoro (tra le 5 e le 8 del mattino in un giorno infrasettimanale) e tornare dal lavoro (tra le 16 e le 18 infrasettimanali).



Tipologia di bicicletta
ds_all_trips_clean_length_correct %>%
group_by(rideable_type) %>%
summarise(count = length(ride_id),
'%' = (length(ride_id) / nrow(ds_all_trips_clean_length_correct)) * 100,
'members_p' = (sum(member_casual == "member") / length(ride_id)) * 100,
'casual_p' = (sum(member_casual == "casual") / length(ride_id)) * 100,
'member_casual_perc_difer' = members_p - casual_p)
ggplot(ds_all_trips_clean_length_correct, aes(rideable_type, fill=member_casual)) +
guides(fill = guide_legend(title = "Categoria utenti")) +
scale_fill_discrete(labels = c("Occasionali", "Abbonati")) +
scale_y_continuous("numero corse", labels = scales::comma) +
labs(x="Tipologia bicicletta", title="Grafico 7 - Distribuzione per tipologia di biciclette") +
geom_bar()
Grafico 7 - Distribuzione per tipologia di biciclette

È importante notare che:

  • le biciclette “docked” hanno un volume di corse di scarso rilievo e vengono utilizzate solo dagli utenti occasionali;
  • gli abbonati hanno una maggiore preferenza per le biciclette classiche, il 66% rispetto al 34% degli occasionali;
  • anche per le biciclette elettriche gli abbonati hanno una preferenza maggiore (57%) rispetto a quelli occasionali (43%).


Stazioni
Introduzione

Per le azioni di marketing sarà utile sapere quali sono le stazioni più popolari. Per scoprire quali sono, utilizzeremo il nome della stazione di partenza e quella di arrivo e conteremo il numero di corse che iniziano o terminano su di esse.

Stazioni di partenza

Stazioni di partenza —> ordinare a partire da quelle con il più alto numero di corse:

ds_all_trips_clean_length_correct %>% 
  group_by(start_station_name) %>%
  summarise(
    count = length(ride_id), 
    #ride_id = n(),
    ) %>%
  slice_max(count, n = 10)

Stazioni di partenza —> quelle più utilizzate dagli abbonati:

ds_all_trips_clean_length_correct %>% 
  filter(member_casual=='member') %>%
  group_by(start_station_name) %>%
  summarise(count = length(ride_id)) %>%
  arrange(desc(count), desc(start_station_name)) %>% 
  slice_max(count, n = 10)

Stazioni di partenza —> quelle più utilizzate dagli occasionali:

ds_all_trips_clean_length_correct %>% 
  filter(member_casual=='casual') %>%
  group_by(start_station_name) %>%
  summarise(count = length(ride_id)) %>%
  arrange(desc(count), desc(start_station_name)) %>% 
  slice_max(count, n = 10)

Stazioni di partenza —> ora raffrontiamo l’utilizzo delle stationi di partenza con le due tipologie di utenza (abbonati e occasionali) per verificare le percentuali di utilizzo e l’orario di maggior frequenza e salviamo il tutto in un dataset dedicato così da poterlo utilizzare per tutte le operazioni di filtraggio e visualizzazione:

colnames(ds_all_trips_clean_length_correct)
ds_all_trips_clean_length_correct_station_start <- ds_all_trips_clean_length_correct %>%
  group_by(start_station_name) %>% 
  summarise(count = length(ride_id),
            '%' = (length(ride_id) / nrow(ds_all_trips_clean_length_correct)) * 100,
            'members_p' = (sum(member_casual == "member") / length(ride_id)) * 100,
            'casual_p' = (sum(member_casual == "casual") / length(ride_id)) * 100,
            'member_casual_perc_difer' = members_p - casual_p,
            'members' = (sum(member_casual == "member")),
            'casual' = (sum(member_casual == "casual")),
            'member_casual_difer' = members - casual,
            'hours_first_race' = min(start_hour, na.rm = TRUE),
            'hours_last_race' = max(start_hour, na.rm = TRUE),
            'data_ds_start_at' = min(started_at, na.rm = TRUE),
            'data_ds_end_at' = max(ended_at, na.rm = TRUE),
            'hours_mean' = mean(start_hour, na.rm = TRUE),
            'hours_median' = median(start_hour, na.rm = TRUE)) %>% 
  arrange(desc(count), desc(start_station_name))
  #slice(1:10) 
ds_all_trips_clean_length_correct_station_start
Stazioni di arrivo

Stazioni di arrivo —> ordinare a partire da quelle con il più alto numero di corse:

ds_all_trips_clean_length_correct %>% 
  group_by(end_station_name) %>%
  summarise(
    count = length(ride_id), 
    #ride_id = n(),
    ) %>%
  slice_max(count, n = 10)

Stazioni di arrivo —>quelle più utilizzate dagli abbonati:

ds_all_trips_clean_length_correct %>% 
  filter(member_casual=='member') %>%
  group_by(end_station_name) %>%
  summarise(count = length(ride_id)) %>%
  arrange(desc(count), desc(end_station_name)) %>% 
  slice_max(count, n = 10)

Stazioni di arrivo —>quelle più utilizzate dagli occasionali:

ds_all_trips_clean_length_correct %>% 
  filter(member_casual=='casual') %>%
  group_by(end_station_name) %>%
  summarise(count = length(ride_id)) %>%
  arrange(desc(count), desc(end_station_name)) %>% 
  slice_max(count, n = 10)

Stazioni di arrivo —> ora raffrontiamo l’utilizzo delle stationi di arrivo con le due tipologie di utenza (abbonati e occasionali) per verificare le percentuali di utilizzo e l’orario di maggior frequenza e salviamo il tutto in un dataset dedicato così da poterlo utilizzare per tutte le operazioni di filtraggio e visualizzazione:

ds_all_trips_clean_length_correct_station_end <- ds_all_trips_clean_length_correct %>%
  group_by(end_station_name) %>%
  summarise(count = length(ride_id),
            '%' = (length(ride_id) / nrow(ds_all_trips_clean_length_correct)) * 100,
            'members_p' = (sum(member_casual == "member") / length(ride_id)) * 100,
            'casual_p' = (sum(member_casual == "casual") / length(ride_id)) * 100,
            'member_casual_perc_difer' = members_p - casual_p,
            'members' = (sum(member_casual == "member")),
            'casual' = (sum(member_casual == "casual")),
            'member_casual_difer' = members - casual,
            'hours_first_race' = min(start_hour, na.rm = TRUE),
            'hours_last_race' = max(start_hour, na.rm = TRUE),
            'data_ds_start_at' = min(started_at, na.rm = TRUE),
            'data_ds_end_at' = max(ended_at, na.rm = TRUE),
            'hours_mean' = mean(start_hour, na.rm = TRUE),
            'hours_median' = median(start_hour, na.rm = TRUE)) %>% 
  arrange(desc(count), desc(end_station_name))
  #slice(1:10) 
ds_all_trips_clean_length_correct_station_end

Considerazioni sulle stazioni di partenza/arrivo:

Si può notare che le stazioni più utilizzate sono diverse a seconda del tipo di utente. Ciò può essere correlato al tipo di utilizzo della bicicletta; essendo l’uso degli utenti occasionali più legato al tempo libero, le loro stazioni potrebbero essere più vicine ai luoghi di svago, mentre gli abbonati sembrano usare le biciclette principalmente per recarsi al lavoro.

Il traffico maggiore sia in partenza che in arrivo si concentra presso la stazione “Streeter Dr & Grand Ave“, dove però il 77% degli utenti in partenza e l’80% di quelli in arrivo appartiene alla categoria “occasionali“, con l’ora di punta mediamente (su base settimanale) verso le 14 del pomeriggio.

Il traffico per gli abbonati annuali si concentra invece sulla stazione di “Kingsbury St & Kinzie St” con una percentuale di circa il 75% in partenza e il 77% in arrivo, rispetto a quelli occasionali che frequentano questa stazione. In questa stazione mediamente (su base settimanale) l’ora di punta è verso le 13 del pomeriggio.

Le stazioni più utilizzate dagli abbonati sono:
“Kingsbury St & Kinzie St”, “Clark St & Elm St” e “Wells St & Concord Ln”.

Le stazioni più utilizzate dagli utenti occasionali sono:
“Streeter Dr & Grand Ave”, “DuSable Lake Shore Dr & Monroe St” e “Millennium Park”.



Analisi senza valori outline (fase 1 di 2)

Ora diamo un’occhiata ad alcune variabili del set di dati.

Per prima cosa visualizziamo alcune statistiche riassuntive dall’insieme di dati

summary(ds_all_trips_clean_length_correct$ride_length_mins)

I valori minimi e massimi possono essere un problema per tracciare alcuni grafici. Come mai il tempo di percorrenza per alcune biciclette ha un valore massimo così elevato? Forse c’è qualche malfunzionamento delle stazioni che restituiscono date errate.

Per prima cosa dividiamo la popolazione in più parti uguali per verificare in quale intervallo è presente l’anomalia (ventili):

ventiles = quantile(ds_all_trips_clean_length_correct$ride_length_mins, seq(0, 1, by=0.05))
ventiles

Possiamo vedere che:

  • la differenza tra 0% e 100% è di 34354,07 minuti (oltre 572 ore, quasi 24 giorni);
  • la differenza tra il 0% e il 95% è di 47,5 minuti;
  • anche i valori sotto il 5% risultano troppo brevi per essere considerati una corsa reale (meno di 3 minuti).

Vediamo quali sono le corse con i tempi di percorrenza più alti per vedere se troviamo qualche riferimento che possa fornirci indicazioni utili sul motivo di questo utilizzo anomalo:

ds_all_trips_clean_length_correct %>% arrange(desc(ride_length_mins))

Qui invece le corse elencate a partire dalla durata minima:

ds_all_trips_clean_length_correct %>% arrange((ride_length_mins))

Controllando le varie voci (es. stazioni di partenza e di arrivo) non sembrerebbe esserci alcun problema però notiamo che tutti i valori anomali per le corse con tempi elevati sono circoscritti alle bici “docked”.
Maggiori informazioni su questi dati verranno richieste agli stakeholder di riferimento per verificare se l’azienda li considera normali.

Verifichiamo anche filtrando per l’intervallo > 95%:

ds_all_trips_clean_length_correct_outliners_max <- ds_all_trips_clean_length_correct %>%
  filter(ride_length_mins > as.numeric(ventiles['95%'])) %>%
  arrange(desc(ride_length_mins))
ds_all_trips_clean_length_correct_outliners_max

Una volta fatte le opportune verifiche, cancellare l’ultimo dataset appena creato (…_outliners_max):

rm(ds_all_trips_clean_length_correct_outliners_max)
Analisi senza valori outline (fase 2 di 2)

Attenzione: l’analisi prosegue creando un dataset senza outliner (“…_outliners_without“), con un sottoinsieme di dati che escluda gli estremi anomali minimi/massimi individuati in precedenza, quindi con un intervallo ottimale individuato tra il 5 e il 95%.
Tale scelta è dettata anche dall’esigenza di effettuare delle verifiche che risulterebbero difficili da interpretare nei grafici con la presenza di questi valori anomali.

ds_all_trips_clean_length_correct_outliners_without <- ds_all_trips_clean_length_correct %>% 
    filter(ride_length_mins > as.numeric(ventiles['5%'])) %>%
    filter(ride_length_mins < as.numeric(ventiles['95%']))
print(paste("Rimosse", nrow(ds_all_trips_clean_length_correct) - nrow(ds_all_trips_clean_length_correct_outliners_without), "righe come outliner" ))

Una delle prime interazioni tra le colonne e la durata della corsa è un box plot, con plot sottostanti basati sulla colonna casual_members. Anche per i dati riassunti.

ds_all_trips_clean_length_correct_outliners_without %>% 
    group_by(member_casual) %>% 
    summarise(mean = mean(ride_length_mins),
              'first_quarter' = as.numeric(quantile(ride_length_mins, .25)),
              'median' = median(ride_length_mins),
              'third_quarter' = as.numeric(quantile(ride_length_mins, .75)),
              'IR' = third_quarter - first_quarter)
ggplot(ds_all_trips_clean_length_correct_outliners_without, aes(x=member_casual, y=ride_length_mins, fill=member_casual)) +
  guides(fill = guide_legend(title = "Categoria utenti")) +
  scale_fill_discrete(labels = c("Occasionali", "Abbonati")) +
  labs(x="Abbonati vs Occasionali", y="Tempo di percorrenza", title="Grafico 8 - Distribuzione tempo di percorrenza per Occasionali/Abbonati") +
  geom_boxplot()
Grafico 8 - Distribuzione tempo di percorrenza per Occasionali/Abbonati

È importante notare che:

  • gli occasionali hanno più tempo per pedalare rispetto agli abbonati;
  • anche la media e l’IQR sono più grandi per i clienti occasionali.

Vediamo se è possibile trovare altre informazioni utili quando si elabora con il giorno della settimana.

ggplot(ds_all_trips_clean_length_correct_outliners_without, aes(x=weekday, y=ride_length_mins, fill=member_casual)) +
  geom_boxplot() +
  facet_wrap(~ member_casual) +
  guides(fill = guide_legend(title = "Categoria utenti")) +
  scale_fill_discrete(labels = c("Occasionali", "Abbonati")) +
  labs(x="Giorno", y="Tempo di percorrenza", title="Grafico 9 var.a - Distribuzione del tempo di noleggio per giorno della settimana") +
  theme(axis.text.x=element_text(angle=45,hjust=1))
Grafico 9 var.a - Distribuzione del tempo di noleggio per giorno della settimana

Alcune considerazioni:

  • il tempo di pedalata per gli abbonati rimane invariato durante l’infrasettimanale, mentre aumenta durante i fine settimana;
  • gli occasionali seguono una distribuzione più curva, con un picco il sabato/domenica e i valori più bassi il martedì/mercoledì/giovedì.

Infine, elaboriamo in base alla tipologia di bicicletta.

ggplot(ds_all_trips_clean_length_correct_outliners_without, aes(x=rideable_type, y=ride_length_mins, fill=member_casual)) +
  geom_boxplot() +
  facet_wrap(~ member_casual) +
  guides(fill = guide_legend(title = "Categoria utenti")) +
  scale_fill_discrete(labels = c("Occasionali", "Abbonati")) +
  labs(x="Tipologia bicipletta", y="Tempo di percorrenza", title="Grafico 10 - Distribuzione del tempo di noleggio per tipo di bicicletta")
  #coord_flip()
Grafico 10 - Distribuzione del tempo di noleggio per tipo di bicicletta

Alcune considerazioni:

  • le bici elettriche hanno un tempo di guida inferiore rispetto alle altre bici, sia per gli abbonati che per gli occasionali;
  • le biciclette “docked” hanno più tempo di percorrenza (dati che risulterebbero “fuori scala” nel grafico se utilizzassimo il dataset comprensivo degli outliner rimossi in precedenza, facenti riferimento all’intervallo > 95%) e vengono utilizzate solo dai clienti occasionali.

Step 5-5: esportare i file di riepilogo per ulteriori analisi

Creare un file csv che verrà utilizzato in Excel, Tableau, SQL oppure in altri software di analisi/presentazione.

Salvare il frame di dati finale con e senza outliner (oltre a quelli utilizzati per le condizioni meteo e le stationi di partenza e arrivo), in un file csv utilizzando il seguente code chunk:

write_csv(ds_all_trips_clean_length_correct,"divvy_tripdata.csv")
write_csv(ds_all_trips_clean_length_correct_outliners_without,"divvy_tripdata_without_outliners.csv")
write_csv(ds_chicago_temperature_races,"divvy_tripdata_chicago_temperature_races.csv")
write_csv(ds_all_trips_clean_length_correct_station_start,"divvy_tripdata_station_start.csv")
write_csv(ds_all_trips_clean_length_correct_station_end,"divvy_tripdata_station_end.csv")


[5-6] Condividere (Share)

Domande
  • Siete stati in grado di rispondere alla domanda su come gli abbonati e i ciclisti occasionali utilizzino le biciclette Cyclistic in modo diverso?
    Sì, sono riusciuto a individuare il modello comportamentale delle tue tipologie di clientela.
  • Quale storia raccontano i vostri dati?
    Gli utenti occasionali e gli abbonati utilizzano il servizio Cyclistic in modo molto diverso, ma ci sono le possibilità per convertire una buona parte degli attuali clienti occasionali in abbonati.
  • In che modo i risultati ottenuti si riferiscono alla domanda iniziale?
    I miei risultati si riferiscono alla domanda iniziale costruendo un profilo delle due tipologie di clientela , trovando le differenze chiave tra gli occasionali e gli abbonati, oltre al motivo per cui ogni gruppo di utenza utilizza le biciclette.
  • Chi sono gli interlocutori per questo case study? Qual è il modo migliore per comunicare con loro?
    I miei interlocutori sono il Cyclistic Executive Team.
  • Le visualizzazioni dei dati possono aiutarvi a condividere i vostri risultati?
    Le visualizzazioni sono fondamentali per presentare dati complessi in maniera chiara, intuitiva e immediata.
  • La presentazione è accessibile al pubblico?
    La presentazione dei risultati è accessibile a tutti.
Compiti da svolgere
  • Determinare il modo migliore per condividere i risultati;
  • Creare visualizzazioni di dati efficaci;
  • Presentare i risultati;
  • Assicurarsi che il lavoro sia accessibile.
Obiettivi
  • Visualizzazioni di supporto all’analisi e indicazione dei risultati ottenuti.


[6-6] Agire (Act)

Domande
  • Qual è la conclusione finale della vostra analisi?
    Gli abbonati e i ciclisti occasionali hanno profili di utilizzo differenti. Per maggiori dettagli, fare riferimento alle mie considerazioni personali che ho indicato dettagliatamente in tutti i vari passaggi dell’analisi.
  • In che modo il vostro team e la vostra azienda potrebbero applicare le vostre intuizioni?
    Il team e l’azienda potrebbero applicare le linee guida scaturite da questa analisi sviluppando una campagna di marketing per trasformare i ciclisti occasionali in abbonati.
  • Quali sono i prossimi passi che tu o i vostri interlocutori fareste sulla base delle vostre conclusioni?
    Approfondire ulteriormente le varie metriche di analisi allo scopo di trovare ulteriori informazioni utili. Informazioni che il team di marketing potrebbe utilizzare per migliorare la campagna digitale della società.
  • Ci sono altri dati che potreste utilizzare per approfondire i vostri risultati?
    Potrebbe risultare utile aggiornare costantemente i dati presenti in questo case study con quelli più recenti così da mostrare l’andamento delle eventuali azioni intraprese, verificando l’evoluzione del comportamento degli abbonati e dei ciclisti occasionali.
Raccomandazioni finali
  • Le tre principali raccomandazioni basate sulla tua analisi.
    Consiglierei al team di marketing di creare una campagna digitale indirizzandola verso le località più frequentate dai ciclisti occasionali che evidenzi le potenzialità e i vantaggi del noleggio delle biciclette rispetto all’uso dell’auto, mostrando l’impatto positivo sull’ambiente, l’esercizio fisico e la riduzione della congestione del traffico.
    Inoltre:
    • evidenziare la disponibilità delle biciclette in ogni fascia oraria della giornata;
    • creare abbonamenti legati al numero di corse, da “consumare” entro i 12 mesi dal loro acquisto;
    • offrire uno sconto nei giorni più freddi per incoraggiare le corse dei clienti occasionali.
Compiti da svolgere

Redigere un report riassuntivo del case study.

Il report riassuntivo del case study avrà lo scopo di presentare i risultati dell’analisi in modo chiaro, conciso e accessibile a un pubblico non tecnico, come i dirigenti e i membri del team di marketing di Cyclistic. Il report seguirà una struttura logica che includerà i seguenti elementi chiave:

  • Introduzione: questa sezione introdurrà lo scenario del case study, l’obiettivo dell’analisi e la metodologia adottata. Presenterà inoltre brevemente il contesto dell’azienda Cyclistic e la sua posizione nel mercato del bike-sharing.
  • Metodologia: in questa sezione, descriveremo in dettaglio la metodologia basata sui sei passaggi “Chiedere, Preparare, Processare, Analizzare, Condividere e Agire”. Illustreremo come questi passaggi sono stati applicati al case study e come hanno contribuito al processo decisionale.
  • Analisi dei dati e risultati: qui, presenteremo i risultati dell’analisi dei dati, evidenziando le differenze tra i ciclisti occasionali e gli abbonati annuali in termini di comportamenti e preferenze. Includeremo visualizzazioni dei dati chiare e comprensibili per supportare le nostre conclusioni e raccomandazioni.
  • Raccomandazioni: basandoci sui risultati dell’analisi, proporremo le tre principali raccomandazioni per il team di marketing di Cyclistic, spiegando come queste strategie possono contribuire a convertire i ciclisti occasionali in abbonati annuali.
  • Implementazione e monitoraggio: in questa sezione, suggeriremo come il team di marketing possa implementare le raccomandazioni proposte e discuteremo l’importanza del monitoraggio delle metriche di successo per valutare l’efficacia delle strategie adottate.
  • Conclusione: concluderemo il report riassumendo i risultati principali dell’analisi, le raccomandazioni proposte e il valore aggiunto che l’applicazione della metodologia usata e l’utilizzo degli strumenti R e RStudio hanno portato al case study.

Infine, il report sarà redatto in un linguaggio chiaro e accessibile, evitando eccessivi tecnicismi e focalizzandosi sulle informazioni essenziali per permettere ai decisori aziendali di comprendere facilmente i risultati dell’analisi e prendere decisioni informate sulla base delle raccomandazioni proposte.

Conclusione

In questo case study, abbiamo esaminato come un servizio di bike-sharing può progettare una strategia di marketing per convertire i ciclisti occasionali in abbonati annuali. Le tre principali raccomandazioni emerse dall’analisi includono: lanciare una campagna digitale mirata, creare abbonamenti flessibili legati al numero di corse e offrire sconti nei giorni più freddi per incoraggiare l’utilizzo delle biciclette.

Per elaborare il case study, ho applicato la metodologia appresa nel corso di specializzazione “Google Data Analytics Professional Certificate“. Questa metodologia segue sei passaggi chiave: 1. Chiedere (Ask), 2. Preparare (Prepare), 3. Processare (Process), 4. Analizzare (Analyze), 5. Condividere (Share) e 6. Agire (Act). Questo approccio strutturato ha permesso di formulare domande pertinenti, preparare e analizzare i dati, creare visualizzazioni professionali e infine proporre raccomandazioni basate su dati solidi.

Il case study è stato elaborato utilizzando il linguaggio di programmazione R e l’ambiente di sviluppo integrato (IDE) offerto da RStudio Desktop. Questi strumenti hanno fornito la base per un’analisi efficiente e approfondita dei dati raccolti.

In conclusione, grazie all’applicazione della metodologia imparata nel corso di specializzazione “Google Data Analytics Professional Certificate” e all’utilizzo degli strumenti R e RStudio, siamo riusciti a proporre raccomandazioni concrete e basate su dati per aiutare una società che fornisce servizi di bike-sharing a massimizzare il numero di abbonamenti annuali e a sviluppare una strategia di marketing efficace.

Se siete interessati ad approfondire ulteriormente l’argomento o avete domande specifiche da porre, non esitate a contattarmi utilizzando i riferimenti presenti nella mia pagina contatti. Sarò felice di rispondere alle vostre domande e di fornirvi ulteriori informazioni sulla mia attività di Data Analyst. Grazie per aver visitato il mio sito e per l’interesse dimostrato nei confronti del mio lavoro.

News tag:
Torna in alto