---
title: "Functionals in the modeling workflow"
output: rmarkdown::pdf_document
vignette: >
  %\VignetteIndexEntry{Functionals in the modeling workflow}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include = FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
library(functionals)
set.seed(1)
has_rsample <- requireNamespace("rsample", quietly = TRUE)
```

A model-building pipeline is a sequence of loops: fit one model per group, one
per resample, one per hyperparameter setting, one per bootstrap replicate. Each
loop is embarrassingly parallel, each benefits from a progress bar on a slow
run, and each should be reproducible and should not collapse because one fit
failed. That is exactly the surface `functionals` covers, with the same handful
of arguments throughout.

## Fit one model per group

`fmapg()` splits a data frame by one or more columns and hands each piece to
`.f`.

```{r}
by_species <- fmapg(iris, function(d) {
  fit <- lm(Sepal.Length ~ Petal.Length, data = d)
  c(slope = unname(coef(fit)[2]), r2 = summary(fit)$r.squared)
}, by = "Species")

do.call(rbind, by_species)
```

## Fit many model specifications

Row-wise over a table of formulas with `fmapr()`, where each row arrives as a named
list:

```{r}
specs <- data.frame(
  response  = "mpg",
  predictor = c("wt", "hp", "wt + hp", "wt * hp"),
  stringsAsFactors = FALSE
)

aic_tbl <- fmapr(specs, function(row) {
  f   <- reformulate(row$predictor, row$response)
  fit <- lm(f, data = mtcars)
  data.frame(model = deparse(f), aic = AIC(fit), adj_r2 = summary(fit)$adj.r.squared)
})
do.call(rbind, aic_tbl)
```

Or over a grid of tuning values with `fmapn()`, iterating aligned lists in
lockstep:

```{r}
grid <- expand.grid(df = c(2, 3, 4, 5), span = NA)
fits <- fmapn(list(df = grid$df), function(df) {
  fit <- smooth.spline(mtcars$wt, mtcars$mpg, df = df)
  c(df = df, gcv = fit$cv.crit)
})
do.call(rbind, fits)
```

## Cross-validation

`fcv()` is `fapply()` named for the job: map an evaluation function over a list
of resample splits. Collect the per-fold rows and `rbind()` once.

```{r eval = has_rsample}
set.seed(42)
folds <- rsample::vfold_cv(mtcars, v = 5)

cv_rows <- fcv(folds$splits, function(split) {
  tr <- rsample::analysis(split)
  te <- rsample::assessment(split)
  fit  <- lm(mpg ~ wt + hp, data = tr)
  pred <- predict(fit, newdata = te)
  data.frame(rmse = sqrt(mean((te$mpg - pred)^2)))
})

cv <- do.call(rbind, cv_rows)
c(mean_rmse = mean(cv$rmse), sd_rmse = sd(cv$rmse))
```

For a slow learner, add `ncores` and `pb = TRUE` and nothing else changes:

```{r eval = FALSE}
cv_rows <- fcv(folds$splits, evaluate_one, ncores = 5, pb = TRUE)
```

If a fold can fail (a degenerate resample, a non-converging fit), use
`.on_error = "pass"` and drop the failures afterwards:

```{r eval = has_rsample}
safe_rows <- fcv(folds$splits, function(split) {
  tr <- rsample::analysis(split)
  if (nrow(tr) < 3) stop("degenerate fold")
  data.frame(rmse = sd(rsample::assessment(split)$mpg))
}, .on_error = "pass")

ok <- !vapply(safe_rows, inherits, logical(1), "functionals_error")
do.call(rbind, safe_rows[ok])
```

## Bootstrap with reproducible streams

`frepeat()` evaluates an expression `times` times. With `.seed`, replicate `i`
draws from its own RNG stream, so the estimate is identical on a re-run and
identical whether you use one core or eight.

```{r}
boot_coef <- frepeat(times = 200, .seed = 2024, simplify = TRUE, expr = function() {
  idx <- sample(nrow(mtcars), replace = TRUE)
  coef(lm(mpg ~ wt, data = mtcars[idx, ]))[["wt"]]
})

c(
  estimate = coef(lm(mpg ~ wt, data = mtcars))[["wt"]],
  boot_se  = sd(boot_coef),
  ci_lo    = unname(quantile(boot_coef, 0.025)),
  ci_hi    = unname(quantile(boot_coef, 0.975))
)
```

```{r}
# same result, more cores; .seed is invariant to ncores
b2 <- frepeat(times = 200, .seed = 2024, simplify = TRUE, ncores = 2, expr = function() {
  idx <- sample(nrow(mtcars), replace = TRUE)
  coef(lm(mpg ~ wt, data = mtcars[idx, ]))[["wt"]]
})
identical(round(sd(boot_coef), 10), round(sd(b2), 10))
```

## A benchmark over learners

Put the pieces together: a list of fit/predict pairs, cross-validated on the
same folds, robust to a learner that errors, reproducible via `.seed`.

```{r eval = has_rsample}
learners <- list(
  lm_wt      = function(tr) lm(mpg ~ wt, data = tr),
  lm_wt_hp   = function(tr) lm(mpg ~ wt + hp, data = tr),
  lm_full    = function(tr) lm(mpg ~ ., data = tr)
)

cv_rmse <- function(fit_fun) {
  rows <- fcv(folds$splits, function(split) {
    tr <- rsample::analysis(split); te <- rsample::assessment(split)
    p  <- predict(fit_fun(tr), newdata = te)
    sqrt(mean((te$mpg - p)^2))
  })
  mean(unlist(rows))
}

bench_tbl <- fmap(learners, cv_rmse, .on_error = "fill", .fill = NA_real_)
data.frame(learner = names(bench_tbl), cv_rmse = round(unlist(bench_tbl), 3))
```

Swap `fmap(learners, cv_rmse, ...)` for
`fmap(learners, cv_rmse, ncores = 3, pb = TRUE)` to run the learners in
parallel with a progress bar; the rest of the code is unchanged.

## Note on non-forkable models

`torch`, `xgboost`, `keras`, database handles and similar hold external-pointer
state that does not survive a fork. Build such objects *inside* `.f` (so each
worker makes its own) rather than capturing them from the caller. When that is
not possible, keep that loop sequential (`ncores = NULL`) and parallelise a
different level: the folds, the learners, or the bootstrap.
