The data are downloaded from Strava and Garmin Connect and are a record of cycling trips over a period of a few years.
To start with I collected the overall distance and moving time for each trip as well as the average power. Data with null values and distances less than 5m traveled (mostly recorded by accident while setting the bike up) are discarded. The remainder represent the good data for both training and family outings.
I have shaded the power data to bring out the relationship between effort and speed, as well as the trend for lower mean power on longer rides.
raw_activities <- read.csv("C:/Users/A101632/Downloads/export_48385647/activities.csv")
basic_data <- raw_activities %>%
select('Activity.Date','Elapsed.Time.1','Moving.Time',"Distance.1",
"Average.Watts")
# throw away nulls and tiny distances (false trips)
basic_data <- na.omit(basic_data)
basic_data <- subset(basic_data, Distance.1 >= 5)
# may want normalised power at some stage, and convert seconds to hours,
# m to km
basic_data <- mutate(basic_data, power_norm = Average.Watts / max
(Average.Watts), hours = Moving.Time / 3600
,distance_km = Distance.1 / 1000)
# rename the Average.Watts column to make the legend better in the plot
names(basic_data)[5]<-paste("Avg_Power_W")
ggplot(data = basic_data) +
aes(x = hours, y = distance_km, colour = Avg_Power_W) +
geom_point() +
labs(x= "Moving Time / hours", y = "Total distance / km", title = "Valid Ride Data 2022-4")
## Including Plots Family trips are slower and lower power, so it’s
possible to separate them from the training rides.
By splitting the data into those with a power less than a chosen number I can divide into two datasets. I can then try different values of the number to get a good split.
power_divide = 70
distance_divide =90
basic_data <- mutate(basic_data, is_low_power_dist =
(Avg_Power_W <= power_divide &
distance_km <= distance_divide))
facet_names_1 <- as_labeller(
c('FALSE' = "Training", 'TRUE' = "Family"))
ggplot(data = basic_data) +
aes(x = hours, y = distance_km, colour = Avg_Power_W) +
geom_point() +
labs(x= "Moving Time / hours", y = "Total distance / km", title =
"Valid Ride Data 2022-4") +
facet_wrap(~is_low_power_dist, labeller = facet_names_1)
These look right. The couple of outliers from the trend in the Training data are from a trip to Peru where the hills are just a bit bigger than Singapore - hence the higher average powers at low distance. Equally, the power estimate may be off for those as they were a Strava estimate, not a direct measurement.
Let’s create a new subset of the data for Training rides only. Other than the two outlier (Peru) points the maxima show an interesting decline. It also shows up that my normal routes fall into a few “standard”distances that I do faster or slower (the lines all correspond to one idtance - slower at low power so time is longer).
training_data <- subset(basic_data, is_low_power_dist == 'FALSE')
# scatter plot and colour by distance
ggplot(data=training_data) +
aes(x = hours, y = Avg_Power_W, colour = distance_km) +
geom_point() +
labs(title = "Average power for each ride", y = "Average power / W", x = "Moving time / hours")
The stray point at the bottom I checked - it refers to a ride where
there was a mechanical failure (broken gear change cable).
Now let’s have a look at ride spacing and recovery.Does this have any impact on performance? First we need to create a recovery_time variable which is the days since last training ride.
#convert to strings dates in mdy format
# noting that not all the dates are the same length so need to trim a different amount off the string end to give a uniform date format - and the motif ", 2" is a fixed point in every string so find the number of characters in each case and put in to_remove column
training_data$to_remove <- str_length(str_extract(training_data$Activity.Date, ", 2.*"))-6
# put the trimmed dates in the new Date column
training_data$Date <- str_sub(training_data$Activity.Date, 1,
(str_length(training_data$Activity.Date)
- training_data$to_remove))
# let's set up a vector of the next date in a second column - which will involved a lagged copy
# Calculate the starting and ending indices for copying
n <- length(training_data$Date) # Assuming 'n' is the total number of elements
#print(n)
start_copy <- 1 # Start index for copying (n)
end_copy <- n-1 # End index for copying (last element)
start_paste <- 2 # Start index for pasting (first element of new column)
end_paste <- n # End index for pasting (n-1th element of new column)
# Create the new column of shifted dates and copy elements, including a copy of the first date in the original column as the first in the shifted column
training_data$shifted_Date <- rep(NA, n) # Pre-allocate the new column with NAs
training_data$shifted_Date[start_paste:end_paste] <- training_data$Date[start_copy:end_copy]
training_data$shifted_Date[1] <- training_data$Date[1]
#now convert the date strings to dates (would have been better to do the conversion first but
# not really worth fixing now)
training_data$date_col <- as.Date(training_data$Date, format = "%b %d, %Y")
training_data$shifted_date_col <- as.Date(training_data$shifted_Date, format = "%b %d, %Y")
training_data$recovery_days <- as.numeric(difftime(ymd(training_data$date_col),
ymd(training_data$shifted_date_col), units = "days"))
Now I have the recovery days available as a numeric column. Let’s look at a a scatter of average power against recovery time - who knows, we may see something!
ggplot(data = training_data) +
aes(x = recovery_days, y = Avg_Power_W, colour = distance_km) +
geom_point() +
scale_color_gradient(low = "blue", high = "red", aesthetics = "colour") +
geom_quantile() +
labs(x= "Recovery / days", y = "Average Power / W", title = "Power vs recovery time")
## Warning in .recacheSubclasses(def@className, def, env): undefined subclass
## "ndiMatrix" of class "replValueSp"; definition not updated
## Smoothing formula not specified. Using: y ~ x
## Warning: The following aesthetics were dropped during statistical transformation:
## colour.
## ℹ This can happen when ggplot fails to infer the correct grouping structure in
## the data.
## ℹ Did you forget to specify a `group` aesthetic or to convert a numerical
## variable into a factor?
Broadly this is consistent with the view that there is some drop-off in
average power with longer gaps between sessions. It’s interesting to
note that the highest mean power for a given recovery time rises over
the first 3 or so days then drops For further analysis I could look at -
whether the length of the preceding journey is important (and whether
longer journeys have required longer recovery times) - whether the
assertion looks valid at different lengths of journey - whether the
analysis is modified if the rather gentler Family rides are considered
as well.
Looking at the “best fit line”(Loess) we find
ggplot(data = training_data) +
aes(x = recovery_days, y = Avg_Power_W, colour = distance_km) +
geom_point() +
scale_color_gradient(low = "blue", high = "red", aesthetics = "colour") +
geom_smooth()+
labs(x= "Recovery / days", y = "Average Power / W", title = "Power vs recovery time")
## `geom_smooth()` using method = 'loess' and formula = 'y ~ x'
## Warning: The following aesthetics were dropped during statistical transformation:
## colour.
## ℹ This can happen when ggplot fails to infer the correct grouping structure in
## the data.
## ℹ Did you forget to specify a `group` aesthetic or to convert a numerical
## variable into a factor?
This suggests that perhaps some fitness level persists for about 20 days
before dropping away, though the long gap data are fewer which is not
encouraging to make firm conclusions.