## ----include = FALSE----------------------------------------------------------
knitr::opts_chunk$set(
  collapse = TRUE,
  comment  = "#>",
  fig.width = 6,
  fig.height = 4
)

## ----data---------------------------------------------------------------------
library(psvr)
library(ggplot2)

# Target: highway fuel economy (all > 0)
y_all <- mpg$hwy

# Predictors: engine displacement, model year, cylinder count.
#
# `cty` is deliberately EXCLUDED. City and highway fuel economy are two
# measurements of the same property of the same vehicle (they correlate at
# 0.96), so predicting one from the other is leakage rather than modelling.
X_raw <- as.matrix(mpg[, c("displ", "year", "cyl")])

stopifnot(all(y_all > 0))
cat("N =", nrow(X_raw), "  p =", ncol(X_raw),
    "  y range: [", min(y_all), ",", max(y_all), "]\n")

## ----split--------------------------------------------------------------------
set.seed(42)
n      <- nrow(X_raw)
tr_idx <- sample(n, floor(0.7 * n))

X_raw_tr <- X_raw[tr_idx, ];  y_tr <- y_all[tr_idx]
X_raw_te <- X_raw[-tr_idx, ]; y_te <- y_all[-tr_idx]

# Standardise: centre and scale by training mean/sd
col_mean <- colMeans(X_raw_tr)
col_sd   <- apply(X_raw_tr, 2, sd)
X_tr <- scale(X_raw_tr, center = col_mean, scale = col_sd)
X_te <- scale(X_raw_te, center = col_mean, scale = col_sd)

## ----metrics------------------------------------------------------------------
mape  <- function(y, yhat) mean(abs(y - yhat) / y) * 100
rmspe <- function(y, yhat) sqrt(mean(((y - yhat) / y)^2)) * 100
r2    <- function(y, yhat) 1 - sum((y - yhat)^2) / sum((y - mean(y))^2)

## ----baseline-----------------------------------------------------------------
lm_df_tr <- as.data.frame(X_tr)
lm_df_te <- as.data.frame(X_te)
lm_fit   <- lm(y_tr ~ ., data = lm_df_tr)
lm_pred  <- predict(lm_fit, newdata = lm_df_te)

cat(sprintf("Linear regression — MAPE: %.2f%%  RMSPE: %.2f%%  R²: %.4f\n",
            mape(y_te, lm_pred), rmspe(y_te, lm_pred), r2(y_te, lm_pred)))

## ----mape-svr-----------------------------------------------------------------
# make_kernel() returns a closure K(xi, xj) = exp(-||xi - xj||^2 / (2 sigma^2)).
# sigma is a LENGTH in the units of the preprocessed feature space, so it has
# to be set on that scale -- sigma_heuristic() reads it off the data instead of
# guessing. See "Hyperparameter search ranges" below.
K <- make_kernel("rbf", sigma = sigma_heuristic(X_tr))

# C = 10: per-sample box bound |beta_k| <= 100*C/y_k; eps = 1: tube width (% of y_k)
fit_ep <- psvr_mape(X_tr, y_tr, kernel = K, C = 10, eps = 1)
pred_ep <- predict(fit_ep, X_te)

cat(sprintf("ε-SVR MAPE    — MAPE: %.2f%%  RMSPE: %.2f%%  R²: %.4f\n",
            mape(y_te, pred_ep), rmspe(y_te, pred_ep), r2(y_te, pred_ep)))
cat(sprintf("Support vectors: %d / %d\n", length(fit_ep$beta), fit_ep$n_train))
print(fit_ep)

## ----mape-plot, echo = FALSE--------------------------------------------------
lim <- range(c(y_te, pred_ep))
data.frame(actual = y_te, predicted = pred_ep) |>
  ggplot(aes(actual, predicted)) +
  geom_point(colour = "#d7191c", alpha = 0.65, size = 1.8) +
  geom_abline(slope = 1, intercept = 0, linetype = "dashed", colour = "grey40") +
  coord_equal(xlim = lim, ylim = lim) +
  labs(x = "Actual hwy (mpg)", y = "Predicted hwy (mpg)",
       title = "Model 1: \u03b5-SVR with MAPE")

## ----mape-coef----------------------------------------------------------------
cf_ep <- coef(fit_ep)
# alpha, alpha_star: length-N dual variables (paired); the pre-pruning
#               solution.  Useful as a warm-start across CV folds; for
#               prediction use `beta` instead.
# beta:         beta_k = alpha_k - alpha_k* for each SUPPORT VECTOR only
#               (non-zero only for training points outside the
#               percentage-error ε-tube — sparse)
# b:            bias / intercept term
# support_data: training rows corresponding to support vectors only
cat(sprintf("b = %.4f  |  beta range: [%.4f, %.4f]\n",
            cf_ep$b, min(cf_ep$beta), max(cf_ep$beta)))

## ----rmspe-lssvr--------------------------------------------------------------
# gamma = 5000: regularisation; larger gamma -> smaller Y_Gamma diagonal -> tighter fit.
# This is roughly var(y_tr) * N, the scale cost_psvr_ls_data() computes -- and
# already five times the ceiling of the registered `cost` default. See below.
fit_ls <- psvr_rmspe(X_tr, y_tr, kernel = K, gamma = 5000)
pred_ls <- predict(fit_ls, X_te)

cat(sprintf("LS-SVR RMSPE  — MAPE: %.2f%%  RMSPE: %.2f%%  R²: %.4f\n",
            mape(y_te, pred_ls), rmspe(y_te, pred_ls), r2(y_te, pred_ls)))
print(fit_ls)

## ----rmspe-plot, echo = FALSE-------------------------------------------------
lim <- range(c(y_te, pred_ls))
data.frame(actual = y_te, predicted = pred_ls) |>
  ggplot(aes(actual, predicted)) +
  geom_point(colour = "#2c7bb6", alpha = 0.65, size = 1.8) +
  geom_abline(slope = 1, intercept = 0, linetype = "dashed", colour = "grey40") +
  coord_equal(xlim = lim, ylim = lim) +
  labs(x = "Actual hwy (mpg)", y = "Predicted hwy (mpg)",
       title = "Model 3: LS-SVR with RMSPE")

## ----rmspe-coef---------------------------------------------------------------
cf_ls <- coef(fit_ls)
# alpha:        N dual variables; weight each training point's kernel
#               contribution in f(x) = sum_k alpha_k K(x_k, x) + b
#               (all N points, no sparsity)
# b:            bias / intercept term
# support_data: all N training inputs stored for prediction
cat(sprintf("b = %.4f  |  alpha range: [%.4f, %.4f]\n",
            cf_ls$b, min(cf_ls$alpha), max(cf_ls$alpha)))

## ----comparison---------------------------------------------------------------
results <- data.frame(
  Model = c("Linear regression", "\u03b5-SVR MAPE (Model 1)",
            "LS-SVR RMSPE (Model 3)"),
  MAPE  = c(mape(y_te,  lm_pred),
            mape(y_te,  pred_ep),
            mape(y_te,  pred_ls)),
  RMSPE = c(rmspe(y_te, lm_pred),
            rmspe(y_te, pred_ep),
            rmspe(y_te, pred_ls)),
  R2    = c(r2(y_te,    lm_pred),
            r2(y_te,    pred_ep),
            r2(y_te,    pred_ls))
)
results[, 2:4] <- round(results[, 2:4], 2)
knitr::kable(results, col.names = c("Model", "MAPE (%)", "RMSPE (%)", "R²"),
             align = "lrrr",
             caption = paste("Test-set performance on ggplot2::mpg",
                             "(70/30 split, RBF kernel, single run,",
                             "untuned hyperparameters)."))

## ----ranges-defaults, message = FALSE-----------------------------------------
library(parsnip)
library(tune)

show_ranges <- function(ps) {
  for (i in seq_len(nrow(ps))) {
    ob <- ps$object[[i]]
    cat(sprintf("  %-10s %s\n", ps$id[i],
      if (inherits(ob, "quant_param"))
        sprintf("[%s] on the %s scale",
                paste(signif(unlist(ob$range), 4), collapse = ", "),
                if (is.null(ob$trans)) "identity" else ob$trans$name)
      else sprintf("{%s}", paste(ob$values, collapse = ", "))))
  }
  invisible(ps)
}

spec_mape <- psvr_mape_rbf(cost = tune(), margin = tune(),
                           rbf_sigma = tune(), sym_type = tune()) |>
  set_engine("psvr")

extract_parameter_set_dials(spec_mape) |> show_ranges()

## ----ranges-mape--------------------------------------------------------------
extract_parameter_set_dials(spec_mape) |>
  update(
    cost      = cost_psvr(),                      # [-2, 10] log2 — fine for C
    margin    = margin_percentage(),              # 1-20% of each target
    rbf_sigma = rbf_sigma_psvr_data(X_tr),        # data-driven; see below
    sym_type  = sym_type_param(c("even", "odd"))  # drops "none"; see below
  ) |>
  show_ranges()

## ----ranges-rmspe-------------------------------------------------------------
spec_ls <- psvr_rmspe_rbf(cost = tune(), rbf_sigma = tune(),
                          sym_type = tune()) |>
  set_engine("psvr")

extract_parameter_set_dials(spec_ls) |>
  update(
    cost      = cost_psvr_ls_data(y_tr),
    rbf_sigma = rbf_sigma_psvr_data(X_tr),
    sym_type  = sym_type_param(c("even", "odd"))
  ) |>
  show_ranges()

## ----ranges-sigma-------------------------------------------------------------
sigma_heuristic(X_tr)   # the geometric centre of the range printed above,
                        # and the value the fits at the top of this page used

