options(cfb.lib_only = TRUE)
source("../R/02_team_game_metrics.R")
source("../R/05_viz_style.R")
TEAM <- "Syracuse"; OPP <- "Clemson"
SEASON <- params$season; WEEK <- params$week
FIG_DIR <- sprintf("figures/%d_wk%02d_story", SEASON, WEEK)
SCOPE <- sprintf("%d Week %d | %s %d, %s %d", SEASON, WEEK, OPP, 18, TEAM, 13)cur <- read_parquet(sprintf("~/cfb-data/processed/team_game_metrics_%d.parquet", SEASON))
fbs <- readRDS(sprintf("~/cfb-data/fbs_teams_%d.rds", SEASON))
p <- read_parquet(sprintf("~/cfb-data/raw/pbp_inseason/pbp_%d_wk%02d.parquet",
SEASON, WEEK))
su <- cur |> filter(team == TEAM, week == WEEK)
gid <- su$game_id[1]
g <- p |> filter(game_id == gid, !is.na(pos_team))
# Drive-level trips into scoring range, computed directly so the exhibits match
# the narrative rather than relying on the pipeline's own opportunity counter.
drives <- g |>
group_by(drive_id) |>
summarise(team = first(pos_team),
closest = suppressWarnings(min(yards_to_goal, na.rm = TRUE)),
result = first(drive_result),
pts = first(drive_pts),
plays = n(),
.groups = "drop") |>
filter(is.finite(closest))
trips <- drives |> filter(closest <= 40)
trip_summary <- trips |>
group_by(team) |>
summarise(Trips = n(), Points = sum(pts, na.rm = TRUE),
`Points per trip` = Points / Trips, .groups = "drop")trip_summary |>
mutate(team = factor(team, levels = c(OPP, TEAM))) |>
arrange(team) |>
rename(Team = team) |>
gt() |>
tab_header(
title = md(sprintf("**Neither offense could finish — but %s needed to**", TEAM)),
subtitle = md(sprintf(
"Trips inside the opponent's 40-yard line and the points they produced | %s won 18-13",
OPP))
) |>
fmt_number(columns = `Points per trip`, decimals = 2) |>
cols_align("center", columns = -Team) |>
tab_style(style = cell_text(weight = "bold"), locations = cells_body(columns = Team)) |>
tab_style(style = list(cell_fill(color = "#FFF3E6"), cell_text(weight = "bold")),
locations = cells_body(rows = Team == TEAM)) |>
tab_source_note(md(paste0("**", su_caption(SCOPE),
"** — a trip is any drive reaching the opponent's 40."))) |>
gt_theme_su()| Neither offense could finish — but Syracuse needed to | |||
| Trips inside the opponent’s 40-yard line and the points they produced | Clemson won 18-13 | |||
| Team | Trips | Points | Points per trip |
|---|---|---|---|
| Clemson | 8 | 12 | 1.50 |
| Syracuse | 6 | 13 | 2.17 |
| Data: CollegeFootballData via cfbfastR | 2026 Week 10 | Clemson 18, Syracuse 13 — a trip is any drive reaching the opponent’s 40. | |||
trips |>
filter(team == OPP) |>
arrange(closest) |>
transmute(`Got as close as` = sprintf("the %s %.0f", TEAM, closest),
Plays = plays, Result = result, Points = pts) |>
gt() |>
tab_header(
title = md(sprintf("**%s reached the %s 40 eight times and never scored a touchdown**",
OPP, TEAM)),
subtitle = md(sprintf(
"Four field goals, two punts, a missed field goal and a drive that ran out the half | %s succeeded on %.1f%% of its plays all afternoon",
OPP, 100 * su$def_success_rate))
) |>
cols_align("center", columns = c(Plays, Points)) |>
cols_align("left", columns = c(`Got as close as`, Result)) |>
tab_style(style = list(cell_fill(color = "#FFF3E6"), cell_text(weight = "bold")),
locations = cells_body(rows = Points == 0)) |>
tab_source_note(md(paste0("**", su_caption(SCOPE), "**"))) |>
gt_theme_su()| Clemson reached the Syracuse 40 eight times and never scored a touchdown | |||
| Four field goals, two punts, a missed field goal and a drive that ran out the half | Clemson succeeded on 28.6% of its plays all afternoon | |||
| Got as close as | Plays | Result | Points |
|---|---|---|---|
| the Syracuse 8 | 10 | FG | 3 |
| the Syracuse 14 | 6 | FG | 3 |
| the Syracuse 18 | 6 | FG | 3 |
| the Syracuse 18 | 6 | PUNT | 0 |
| the Syracuse 20 | 14 | END OF HALF | 0 |
| the Syracuse 27 | 10 | PUNT | 0 |
| the Syracuse 36 | 12 | FG | 3 |
| the Syracuse 39 | 10 | MISSED FG | 0 |
| Data: CollegeFootballData via cfbfastR | 2026 Week 10 | Clemson 18, Syracuse 13 | |||
trips |>
filter(team == TEAM) |>
arrange(closest) |>
transmute(`Got as close as` = sprintf("the %s %.0f", OPP, closest),
Plays = plays, Result = result, Points = pts) |>
gt() |>
tab_header(
title = md(sprintf("**%s got two fewer chances and scored one more point**", TEAM)),
subtitle = md(sprintf("Two field goals, a touchdown, a missed field goal, a drive that ran out the half — and an interception thrown from the %s 3-yard line",
OPP))
) |>
cols_align("center", columns = c(Plays, Points)) |>
cols_align("left", columns = c(`Got as close as`, Result)) |>
tab_style(style = list(cell_fill(color = "#FFF3E6"), cell_text(weight = "bold")),
locations = cells_body(rows = Points == 0)) |>
tab_source_note(md(paste0("**", su_caption(SCOPE), "**"))) |>
gt_theme_su()| Syracuse got two fewer chances and scored one more point | |||
| Two field goals, a touchdown, a missed field goal, a drive that ran out the half — and an interception thrown from the Clemson 3-yard line | |||
| Got as close as | Plays | Result | Points |
|---|---|---|---|
| the Clemson 3 | 9 | INT | 0 |
| the Clemson 8 | 15 | FG | 3 |
| the Clemson 14 | 11 | MISSED FG | 0 |
| the Clemson 28 | 8 | FG | 3 |
| the Clemson 33 | 8 | END OF HALF | 0 |
| the Clemson 33 | 5 | TD | 7 |
| Data: CollegeFootballData via cfbfastR | 2026 Week 10 | Clemson 18, Syracuse 13 | |||
zones <- p |>
select(all_of(intersect(NEEDED, names(p)))) |>
filter(pos_team == TEAM) |>
classify_plays() |>
filter(is_scrimmage) |>
mutate(zone = factor(ifelse(yards_to_goal <= 40,
sprintf("Inside the %s 40", OPP),
sprintf("Outside the %s 40", OPP)),
levels = c(sprintf("Outside the %s 40", OPP),
sprintf("Inside the %s 40", OPP))),
type = ifelse(pass == 1, "Pass", "Rush"))
zone_sr <- zones |>
group_by(zone, type) |>
summarise(plays = n(), sr = mean(success, na.rm = TRUE), .groups = "drop")
p_zone <- ggplot(zone_sr, aes(x = zone, y = sr, fill = type)) +
geom_col(position = position_dodge(width = 0.72), width = 0.62) +
geom_text(aes(label = sprintf("%.0f%%\n%d plays", 100 * sr, plays)),
position = position_dodge(width = 0.72), vjust = -0.25,
size = 3.3, fontface = "bold", lineheight = 0.95, color = SU$ink) +
scale_fill_manual(values = c(Pass = SU$orange, Rush = SU$blue)) +
scale_y_continuous(labels = percent, expand = expansion(mult = c(0, 0.22))) +
labs(
title = sprintf("%s's passing game fell apart once the field got short", TEAM),
subtitle = "Success rate by field zone — the share of plays gaining enough to stay on schedule",
x = NULL, y = "Success rate", fill = NULL,
caption = su_caption(SCOPE)
) +
theme_su(legend_pos = "top")
save_fig(p_zone, "01_success_rate_by_zone", height = 5)zones |>
group_by(zone) |>
summarise(Plays = n(), Successes = sum(success, na.rm = TRUE),
`Success rate` = mean(success, na.rm = TRUE), .groups = "drop") |>
rename(Zone = zone) |>
gt() |>
tab_header(
title = md("**All plays, by zone**"),
subtitle = md("The passing-only figures appear in the chart above; these are every snap")
) |>
fmt_percent(columns = `Success rate`, decimals = 1) |>
cols_align("center", columns = -Zone) |>
tab_style(style = cell_text(weight = "bold"), locations = cells_body(columns = Zone)) |>
tab_source_note(md(paste0("**", su_caption(SCOPE), "**"))) |>
gt_theme_su()| All plays, by zone | |||
| The passing-only figures appear in the chart above; these are every snap | |||
| Zone | Plays | Successes | Success rate |
|---|---|---|---|
| Outside the Clemson 40 | 51 | 22 | 43.1% |
| Inside the Clemson 40 | 22 | 7 | 31.8% |
| Data: CollegeFootballData via cfbfastR | 2026 Week 10 | Clemson 18, Syracuse 13 | |||
sched <- suppressWarnings(cfbd_game_info(SEASON, team = TEAM)) |>
filter(!is.na(home_points)) |> arrange(week) |>
mutate(opp = ifelse(home_team == TEAM, away_team, home_team),
su_pts = ifelse(home_team == TEAM, home_points, away_points),
op_pts = ifelse(home_team == TEAM, away_points, home_points),
margin = su_pts - op_pts,
one_score = abs(margin) <= 8,
lab = sprintf("Wk %d %s", week, opp),
outcome = ifelse(margin > 0, "Win", "Loss")) |>
mutate(lab = factor(lab, levels = rev(lab)))
p_margin <- ggplot(sched, aes(x = margin, y = lab)) +
annotate("rect", xmin = -8, xmax = 8, ymin = -Inf, ymax = Inf,
fill = SU$orange, alpha = 0.08) +
geom_vline(xintercept = 0, color = SU$muted, linewidth = 0.5) +
geom_segment(aes(x = 0, xend = margin, yend = lab, color = outcome), linewidth = 1.6) +
geom_point(aes(color = outcome), size = 3.4) +
geom_text(aes(label = sprintf("%+d", margin),
hjust = ifelse(margin > 0, -0.45, 1.45)),
size = 3.3, fontface = "bold", color = SU$ink) +
scale_color_manual(values = c(Win = SU$orange, Loss = SU$blue), name = NULL) +
scale_x_continuous(expand = expansion(mult = c(0.14, 0.14))) +
labs(
title = sprintf("%d of Syracuse's %d games have been decided by one score",
sum(sched$one_score), nrow(sched)),
subtitle = sprintf(
"Final margin by game | Shaded band: within 8 points | Syracuse is %d-%d in those games and %d-%d overall",
sum(sched$one_score & sched$margin > 0), sum(sched$one_score & sched$margin < 0),
sum(sched$margin > 0), sum(sched$margin < 0)),
x = "Final margin", y = NULL,
caption = su_caption("2026 season to date")
) +
theme_su(legend_pos = "top")
save_fig(p_margin, "02_one_score_games", height = 5.5)figs <- list.files(FIG_DIR, pattern = "\\.png$", full.names = TRUE)
if (length(figs) == 0) cat("No figures written.\n") else {
cat("300-dpi PNGs ready for the piece:\n\n")
for (f in figs) cat(" ", f, " (", round(file.info(f)$size/1024), " KB)\n", sep = "")
cat("\nCopy the gt tables from the browser into Google Docs — they paste as\n")
cat("native editable tables.\n")
}## 300-dpi PNGs ready for the piece:
##
## figures/2026_wk10_story/01_success_rate_by_zone.png (122 KB)
## figures/2026_wk10_story/02_one_score_games.png (149 KB)
##
## Copy the gt tables from the browser into Google Docs — they paste as
## native editable tables.