This Level 1 CMJ Readiness Report is designed to answer one question:
Is the athlete neuromuscularly ready today?
It uses Typical Error (TE) and Smallest Worthwhile Change (SWC) rather than population SD to flag meaningful changes.
library(tidyverse)
library(lubridate)
library(formattable)
Edit these in the YAML header under params:
(recommended), or override when knitting.
file_path <- params$file_path
lookback_days <- params$lookback_days
min_tests <- params$min_tests
monitor_te <- params$monitor_te
flag_te <- params$flag_te
good_col <- "#11cc47"
warn_col <- "#e6a500"
bad_col <- "#aa1818"
Required columns (exact names): - Name
- Date (MM/DD/YYYY) -
Jump Height (Imp-Mom) [cm] -
RSI-modified [m/s] -
Concentric Impulse [N*s]
cmj <- read_csv(file_path, show_col_types = FALSE) %>%
mutate(Date = mdy(Date)) %>% # ForceDecks export typically MM/DD/YYYY
arrange(Name, Date) %>%
rename(
Jump_Height = `Jump Height (Imp-Mom) [cm]`,
RSI_Mod = `RSI-modified [m/s]`,
Con_Impulse = `Concentric Impulse [N*s]`
)
metrics <- c("Jump_Height", "Con_Impulse", "RSI_Mod")
missing_metrics <- setdiff(metrics, names(cmj))
if (length(missing_metrics) > 0) {
stop(paste("Missing required metric columns:", paste(missing_metrics, collapse = ", ")))
}
latest <- cmj %>%
group_by(Name) %>%
slice_max(Date, n = 1, with_ties = FALSE) %>%
ungroup() %>%
mutate(Days_Since_Last_Jump = as.integer(Sys.Date() - Date))
window <- cmj %>%
group_by(Name) %>%
mutate(Latest_Date = max(Date, na.rm = TRUE)) %>%
filter(
Date > Latest_Date - days(lookback_days),
Date < Latest_Date
) %>%
ungroup()
Typical Error (TE) is computed from successive test-to-test differences:
\[ TE = \frac{SD(\Delta)}{\sqrt{2}} \]
calc_te <- function(x) {
x <- x[!is.na(x)]
if (length(x) < 3) return(NA_real_) # need at least 3 tests for diff-based TE
sd(diff(x)) / sqrt(2)
}
# Baselines in WIDE form (mean/sd/te per metric)
baseline_wide <- window %>%
group_by(Name) %>%
summarise(
Tests_in_Window = n(),
across(
all_of(metrics),
list(
mean = ~mean(.x, na.rm = TRUE),
sd = ~sd(.x, na.rm = TRUE),
te = ~calc_te(.x)
),
.names = "{.col}_{.fn}"
),
.groups = "drop"
)
# Convert to LONG form for safe joins (one row per athlete x metric)
baseline_long <- baseline_wide %>%
pivot_longer(
cols = -c(Name, Tests_in_Window),
names_to = c("Metric", ".value"),
names_pattern = "^(.*)_(mean|sd|te)$"
)
SWC is set to:
\[ SWC = 0.2 \times SD \]
Severity tiers: - Stable: within SWC / within noise
- Monitor: ≥ monitor_te TE -
Flag: ≥ flag_te TE -
Insufficient: not enough baseline data
# Latest values in long form
latest_long <- latest %>%
select(Name, Date, Days_Since_Last_Jump, all_of(metrics)) %>%
pivot_longer(
cols = all_of(metrics),
names_to = "Metric",
values_to = "Latest"
)
# Join baselines and compute change + severity
report_long <- latest_long %>%
left_join(baseline_long, by = c("Name", "Metric")) %>%
mutate(
Change = Latest - mean,
SWC = 0.2 * sd,
TE_Z = Change / te,
TE_Abs = abs(TE_Z),
Severity = case_when(
is.na(Tests_in_Window) | Tests_in_Window < min_tests ~ "Insufficient",
is.na(te) | te <= 0 ~ "Insufficient",
is.na(SWC) | SWC <= 0 ~ "Insufficient",
abs(Change) < SWC ~ "Stable",
TE_Abs < monitor_te ~ "Stable",
TE_Abs < flag_te ~ "Monitor",
TRUE ~ "Flag"
)
)
final <- report_long %>%
mutate(
Metric = recode(
Metric,
Jump_Height = "Outcome — Jump Height (cm)",
Con_Impulse = "Capacity — Concentric Impulse (N·s)",
RSI_Mod = "Quality — RSI-mod"
)
) %>%
select(
Name, Date, Days_Since_Last_Jump, Tests_in_Window,
Metric, Latest, TE_Z, Severity
) %>%
pivot_wider(
names_from = Metric,
values_from = c(Latest, TE_Z, Severity),
names_glue = "{Metric} — { .value }"
) %>%
arrange(Days_Since_Last_Jump, Name)
# Round numeric columns for readability
num_cols <- names(final)[map_lgl(final, is.numeric)]
final[num_cols] <- lapply(final[num_cols], function(x) round(x, 2))
final
## # A tibble: 8 × 13
## Name Date Days_Since_Last_Jump Tests_in_Window Outcome — Jump Heigh…¹
## <chr> <date> <dbl> <dbl> <dbl>
## 1 Athlet… 2025-11-30 22 9 39.1
## 2 Athlet… 2025-11-30 22 9 46.9
## 3 Athlet… 2025-11-30 22 9 43.9
## 4 Athlet… 2025-11-30 22 9 58.0
## 5 Athlet… 2025-11-30 22 9 49.1
## 6 Athlet… 2025-11-30 22 9 42.9
## 7 Athlet… 2025-11-30 22 9 54.2
## 8 Athlet… 2025-11-30 22 9 55.0
## # ℹ abbreviated name: ¹`Outcome — Jump Height (cm) — Latest`
## # ℹ 8 more variables: `Capacity — Concentric Impulse (N·s) — Latest` <dbl>,
## # `Quality — RSI-mod — Latest` <dbl>,
## # `Outcome — Jump Height (cm) — TE_Z` <dbl>,
## # `Capacity — Concentric Impulse (N·s) — TE_Z` <dbl>,
## # `Quality — RSI-mod — TE_Z` <dbl>,
## # `Outcome — Jump Height (cm) — Severity` <chr>, …
severity_cols <- names(final)[stringr::str_detect(names(final), "Severity$")]
fmt <- lapply(severity_cols, function(col) {
formatter(
"span",
style = x ~ ifelse(
x == "Flag", style(color = bad_col, font.weight = "bold"),
ifelse(x == "Monitor", style(color = warn_col, font.weight = "bold"),
ifelse(x == "Stable", style(color = good_col), style(color = "gray50")))
)
)
})
names(fmt) <- severity_cols
formattable(
final,
align = "c",
row.names = FALSE,
fmt
)
| Name | Date | Days_Since_Last_Jump | Tests_in_Window | Outcome — Jump Height (cm) — Latest | Capacity — Concentric Impulse (N·s) — Latest | Quality — RSI-mod — Latest | Outcome — Jump Height (cm) — TE_Z | Capacity — Concentric Impulse (N·s) — TE_Z | Quality — RSI-mod — TE_Z | Outcome — Jump Height (cm) — Severity | Capacity — Concentric Impulse (N·s) — Severity | Quality — RSI-mod — Severity |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Athlete A | 2025-11-30 | 22 | 9 | 39.11 | 240.7 | 0.67 | -2.50 | -1.35 | -0.30 | Flag | Monitor | Stable |
| Athlete B | 2025-11-30 | 22 | 9 | 46.89 | 257.0 | 0.65 | -0.23 | 1.76 | 0.25 | Stable | Monitor | Stable |
| Athlete C | 2025-11-30 | 22 | 9 | 43.87 | 275.0 | 0.59 | -4.43 | -3.84 | -1.51 | Flag | Flag | Monitor |
| Athlete D | 2025-11-30 | 22 | 9 | 57.96 | 220.4 | 0.71 | 2.11 | -1.63 | -0.81 | Flag | Monitor | Stable |
| Athlete E | 2025-11-30 | 22 | 9 | 49.12 | 297.0 | 0.52 | 1.49 | -1.16 | 0.66 | Monitor | Monitor | Stable |
| Athlete F | 2025-11-30 | 22 | 9 | 42.86 | 272.5 | 0.66 | 0.32 | 0.23 | -0.84 | Stable | Stable | Stable |
| Athlete G | 2025-11-30 | 22 | 9 | 54.21 | 220.6 | 0.59 | -0.64 | 0.58 | -0.57 | Stable | Stable | Stable |
| Athlete H | 2025-11-30 | 22 | 9 | 55.02 | 297.1 | 0.43 | -0.88 | 1.25 | -1.28 | Stable | Monitor | Monitor |
lookback_days or ensure each athlete has at least
min_tests tests prior to the latest test.rename() step in section 3.