Efficiency Factor measures how much work you perform per unit of cardiovascular effort. Higher EF indicates better aerobic fitness - you're able to maintain faster pace or higher power at the same heart rate. Tracking EF over time helps monitor aerobic base development and training effectiveness.
EF Metrics:
speed_hr (for running): Speed (m/s) / Average HR
Higher values = faster speed at same HR = better fitness
power_hr (for cycling): Average Power (watts) / Average HR
Higher values = more power at same HR = better fitness
What Improves EF?
Aerobic base building (Zone 2 training)
Improved running/cycling economy
Enhanced cardiovascular efficiency
Increased mitochondrial density
Usage
calculate_ef(
activities_data,
activity_type = c("Run", "Ride"),
ef_metric = c("speed_hr", "gap_hr", "power_hr"),
start_date = NULL,
end_date = Sys.Date(),
min_duration_mins = 20,
min_steady_minutes = 20,
steady_cv_threshold = 0.08,
min_hr_coverage = 0.9,
quality_control = c("off", "flag", "filter"),
export_dir = NULL
)Arguments
- activities_data
A data frame of activities from
load_local_activities(). Must contain columns:date,type,moving_time,distance,average_heartrate, andaverage_watts(for power_hr metric).- activity_type
Character vector or single string specifying activity type(s) to analyze. Common values:
"Run","Ride", orc("Run", "Ride"). Default:c("Run", "Ride").- ef_metric
Character string specifying the efficiency metric:
"speed_hr": Speed-based efficiency (for running). Formula: speed (m/s) / avg_HR. Units: m/s/bpm (higher = better fitness)"gap_hr": Grade Adjusted Speed efficiency (for running on hilly terrain). Formula: GAP speed (m/s) / avg_HR. Accounts for elevation changes. Units: m/s/bpm"power_hr": Power-based efficiency (for cycling). Formula: avg_watts / avg_HR. Units: W/bpm (higher = better fitness) Default:c("speed_hr", "power_hr")(uses first matching metric for activity type). Note:"pace_hr"is accepted as a deprecated alias for"speed_hr"for backward compatibility.
- start_date
Optional. Analysis start date (YYYY-MM-DD string, Date, or POSIXct). Defaults to one year before
end_date.- end_date
Optional. Analysis end date (YYYY-MM-DD string, Date, or POSIXct). Defaults to current date (Sys.Date()).
- min_duration_mins
Numeric. Minimum activity duration in minutes to include in analysis (default: 20). Filters out very short activities that may not represent steady-state aerobic efforts.
- min_steady_minutes
Numeric. Minimum duration (minutes) for steady-state segment (default: 20). Activities shorter than this are automatically rejected for EF calculation.
- steady_cv_threshold
Numeric. Coefficient of variation threshold for steady-state (default: 0.08 = 8%). Activities with higher variability are rejected as non-steady-state.
- min_hr_coverage
Numeric. Minimum HR data coverage threshold (default: 0.9 = 90%). Activities with lower HR coverage are rejected as insufficient data quality.
- quality_control
Character. Quality control mode: "off" (no filtering), "flag" (mark issues), or "filter" (exclude flagged data). Default "filter" for scientific rigor.
- export_dir
Optional. Path to Strava export directory containing activity files. When provided, enables stream data analysis for more accurate steady-state detection.
Value
A tibble with the following columns:
- date
Activity date (Date class)
- activity_type
Activity type (character: "Run" or "Ride")
- ef_value
Efficiency Factor value (numeric). Higher = better fitness. Units: m/s/bpm for speed_hr, W/bpm for power_hr.
- status
Character status code describing the outcome of the calculation. See Status vocabulary below.
- ef_metric_requested
The metric the user asked for (
"speed_hr","gap_hr", or"power_hr"). Mirrors theef_metricargument.- ef_metric_used
The metric that was actually computed. Usually matches
ef_metric_requested; forgap_hrinputs processed through the stream path (where no GAP channel is available) this is"speed_hr"and the row is also marked with status"gap_stream_unavailable_fallback_to_speed".- quality_score
Numeric in [0, 1]. Fraction of stream samples that passed quality-control range checks (HR, velocity, watts).
NAwhen no stream was parsed or when the activity was rejected before QC.- hr_coverage
Numeric in [0, 1]. Time-weighted fraction of the stream that carried a valid heart-rate sample.
NAfor activity-level paths (status = "no_streams").- steady_duration_minutes
Wall-clock duration of the contiguous steady-state block the EF value was derived from.
NAfor activity-level paths or when no qualifying block existed.- n_steady_blocks
Number of contiguous steady-state blocks that met the
min_steady_minutesthreshold.NAfor activity-level paths.- sampling_interval_seconds
Observed median sampling interval of the stream (seconds).
NAfor activity-level paths.
Details
Computes Efficiency Factor (EF) for endurance activities, quantifying the relationship between performance output (speed or power) and heart rate. EF is a key indicator of aerobic fitness and training adaptation (Allen et al., 2019).
Algorithm:
Filter activities by type, date range, and minimum duration
For each activity, calculate:
speed_hr: (distance / moving_time) / average_heartrate
power_hr: average_watts / average_heartrate
Return one EF value per activity
Steady-State Detection Method:
When stream data is available (via export_dir), the function applies a rolling
coefficient of variation (CV) approach to identify steady-state periods and
then enforces contiguous block duration so scattered "steady" islands are
not averaged together:
Sampling-interval awareness: The observed median sampling interval is estimated via
diff(time)so the rolling window targets a fixed number of seconds regardless of recording frequency (1 Hz, 0.5 Hz smart-recording, multi-Hz). This removes the implicit 1 Hz assumption from earlier versions.Rolling window: A sliding window targeting 300 seconds (capped by
nrow / 4, floor at 60 seconds' worth of samples) computes rolling mean and SD of the output metric.CV calculation: CV = rolling SD / rolling mean at each time point.
Continuous blocks: All time points with CV <
steady_cv_thresholdare marked steady, then grouped into contiguous runs viarle(). A block qualifies only if its wall-clock span is >=min_steady_minutesAND it has >= 100 samples. If no block qualifies, status is"insufficient_steady_duration"(activities with no steady CV window at all are marked"non_steady").EF computation: The longest qualifying block is selected; EF is the median output/HR ratio across that block.
steady_duration_minutes,n_steady_blocksandsampling_interval_secondsare returned alongside the EF value for auditability.
This mirrors the block-based steady-state logic used in
calculate_decoupling() and follows the principle that valid EF comparisons
require quasi-constant output intensity, as outlined by Coyle &
González-Alonso (2001), who demonstrated that cardiovascular drift is
meaningful only under steady-state exercise conditions. The rolling CV
method is a common signal-processing technique for detecting stationarity
in physiological time series.
Data Quality Considerations:
Requires heart rate data (activities without HR are excluded)
power_hr requires power meter data (cycling with power)
Best for steady-state endurance efforts (tempo runs, long rides)
Interval workouts may give misleading EF values
Environmental factors (heat, altitude) can affect EF
Interpretation:
Upward trend: Improving aerobic fitness
Stable: Maintenance phase
Downward trend: Possible overtraining, fatigue, or environmental stress
Sudden drop: Check for illness, equipment change, or data quality
Typical EF Ranges (speed_hr for running):
Beginner: 0.01 - 0.015 (m/s per bpm)
Intermediate: 0.015 - 0.020
Advanced: 0.020 - 0.025
Elite: > 0.025
Note: EF values are relative to individual baseline. Focus on personal trends rather than absolute comparisons with other athletes.
Status vocabulary
"ok": Full steady-state analysis succeeded on stream data."no_streams": No stream file was available;ef_valuewas computed from activity-level averages. QC and HR-coverage metrics areNA."gap_stream_unavailable_fallback_to_speed": Caller requestedef_metric = "gap_hr"but the stream does not expose a grade-adjusted channel, soef_valuewas computed from plain speed/HR.ef_metric_usedis"speed_hr"on these rows so downstream code can filter them out."missing_velocity_data": Stream lacked bothdistanceandvelocity_smoothwhenef_metric = "speed_hr"."missing_power_data": Stream lackedwattswhenef_metric = "power_hr"."missing_hr_data": Stream lacked aheartrate/heart_ratecolumn."insufficient_hr_data": Time-weighted HR coverage <min_hr_coverage."insufficient_data_points": Fewer than 100 non-NA stream samples."insufficient_valid_data": Fewer than 100 samples survived the basic positivity filter (velocity > 0orwatts > 0)."insufficient_data_after_quality_filter":quality_control = "filter"removed enough out-of-range samples to drop the stream below 100 rows."poor_hr_quality": Activity-level path with out-of-rangeaverage_heartratewhilequality_control = "filter"."too_short": Activity or steady-state window duration is belowmin_steady_minutes."non_steady": No rolling-CV window clearedsteady_cv_threshold."insufficient_steady_duration": No contiguous steady-state block lasted at leastmin_steady_minutes."calculation_failed": Median ratio was non-positive or not finite.
References
Allen, H., Coggan, A. R., & McGregor, S. (2019). Training and Racing with a Power Meter (3rd ed.). VeloPress.
Coyle, E. F., & González-Alonso, J. (2001). Cardiovascular drift during prolonged exercise: New perspectives. Exercise and Sport Sciences Reviews, 29(2), 88-92. doi:10.1097/00003677-200104000-00009
See also
plot_ef() for visualization with trend lines,
calculate_decoupling() for within-activity efficiency analysis,
load_local_activities() for data loading
Examples
# Example using simulated data
data(sample_ef)
print(head(sample_ef))
#> # A tibble: 6 × 3
#> date activity_type ef_value
#> <date> <chr> <dbl>
#> 1 2023-01-01 Run 1.16
#> 2 2023-01-01 Ride 1.86
#> 3 2023-01-04 Run 1.19
#> 4 2023-01-06 Run 1.21
#> 5 2023-01-06 Ride 1.94
#> 6 2023-01-08 Run 1.31
# Runnable example with dummy data:
end <- Sys.Date()
dates <- seq(end - 29, end, by = "day")
dummy_activities <- data.frame(
date = dates,
type = "Run",
moving_time = rep(3600, length(dates)), # 1 hour
distance = rep(10000, length(dates)), # 10 km
average_heartrate = rep(140, length(dates)),
average_watts = rep(200, length(dates)),
weighted_average_watts = rep(210, length(dates)),
filename = "",
stringsAsFactors = FALSE
)
# Calculate EF (Speed/HR)
ef_result <- calculate_ef(
activities_data = dummy_activities,
activity_type = "Run",
ef_metric = "speed_hr",
end_date = end
)
print(head(ef_result))
#> date activity_type ef_value status ef_metric_requested
#> 1 2026-03-22 Run 0.01984127 no_streams speed_hr
#> 2 2026-03-23 Run 0.01984127 no_streams speed_hr
#> 3 2026-03-24 Run 0.01984127 no_streams speed_hr
#> 4 2026-03-25 Run 0.01984127 no_streams speed_hr
#> 5 2026-03-26 Run 0.01984127 no_streams speed_hr
#> 6 2026-03-27 Run 0.01984127 no_streams speed_hr
#> ef_metric_used quality_score hr_coverage steady_duration_minutes
#> 1 speed_hr NA NA NA
#> 2 speed_hr NA NA NA
#> 3 speed_hr NA NA NA
#> 4 speed_hr NA NA NA
#> 5 speed_hr NA NA NA
#> 6 speed_hr NA NA NA
#> n_steady_blocks sampling_interval_seconds
#> 1 NA NA
#> 2 NA NA
#> 3 NA NA
#> 4 NA NA
#> 5 NA NA
#> 6 NA NA
if (FALSE) { # \dontrun{
# Example using local Strava export data
activities <- load_local_activities("strava_export_data/activities.csv")
# Calculate Speed/HR efficiency factor for Runs
ef_data_run <- calculate_ef(
activities_data = activities,
activity_type = "Run",
ef_metric = "speed_hr"
)
print(tail(ef_data_run))
# Calculate Power/HR efficiency factor for Rides
ef_data_ride <- calculate_ef(
activities_data = activities,
activity_type = "Ride",
ef_metric = "power_hr"
)
print(tail(ef_data_ride))
} # }
