Author: Jonathan Rougier

# Quick-start for ‘loclm’

#### Jonathan Rougier

`loclm()` implements local linear regression, using high-level functions. It can handle both numerical and non-numerical variables. Non-numerical variables are converted to factors.

### Top stuff

    library(loclm)
    show(packageVersion("loclm"))
    #> [1] '1.0.0'
    show(date())
    #> [1] "Thu Jul 30 07:00:27 2026"

Here I’m just setting up a nicer plotting frame.

    oldpar <- par(mgp = c(2.5, 0.7, 0), mar = c(4, 4, 2, 0.5), family = "Hershey",
      cex.main = 0.8, cex.axis = 0.8, cex.lab = 0.8, las = 1)
    op <- par(no.readonly = TRUE)

## Create a dataset

Let’s use the multivariate Banana function variant B from

S. Kok and C. Sandrock, 2009, Locating and Characterizing the Stationary Points of the Extended Rosenbrock Function, Evolutionary Computing, 17(3), pages 437-453.

As the paper explains, variant B is pathological, especially in higher dimensions.

    #### d-dimensional Banana function

    ## (d >= 2 is implied by x)

    banana <- function(x) {
      stopifnot(is.vector(x, mode = "numeric"))
      d <- length(x)
      if (d == 1) {
        x <- c(x, 1)
        d <- 2L
      }
      robj <- 0
      for (j in seq_len(d - 1)) {
        robj <- robj + 100 * (x[j]^2 - x[j+1])^2 + (x[j] - 1)^2
      }
      robj
    }

Here is a dataset of 101 runs from a 5D banana function, plus a little noise. Feel free to play with `d` and `sigma`.

    ## make a dataset

    set.seed(1234) # for reproducibility, feel free to change

    d <- 3
    sigma <- 1
    X <- matrix(runif(d * 101), ncol = d)
    y <- apply(X, 1, banana) + rnorm(nrow(X), sd = sigma)

    ## here's where we'll predict

    newX <- matrix(runif(d * 23), ncol = d)
    newy <- apply(newX, 1, banana) # leave off the noise

#### All numeric inputs

First, fit the model with the default span.

    ## default span

    fit <- loclm(X, y)
    pp <- predict(fit, newX)
    cex <- 0.6
    col <- c("darkblue", "forestgreen")
    par(op)
    matplot(newX[, 2], cbind(newy, pp), pch = c(1, 2), cex = cex,
      col = col,
      main = "Default span")

Not great.

## LOO cross-validation to set the span

Now use Leave-One-Out (LOO) cross-validation to set the span. We need a general-purpose LOO function.

    ## LOO cross-validation

    LOO <- function(X, y, reg, ...) {
      n <- length(y)
      yhat <- vapply(seq.int(n), \(i) {
        fit <- reg(X[-i, , drop=FALSE], y[-i], ...)
        predict(fit, X[i, , drop=FALSE])
      }, FUN.VALUE = 0)
      list(yhat = yhat, res = y - yhat)
    }

Now call this with `loclm` as the regression method, and `span` as the parameter.

    ## try different spans

    span_vals <- c(seq(0.1, 0.2, 0.01), 0.75)
    rmse <- vapply(span_vals, \(span) {
        loo <- LOO(X, y, reg = loclm, span = span)
        sqrt(mean(loo$res^2))
      }, FUN.VALUE = 0)
    show(cbind(span = span_vals, rmse = rmse))
    #>       span      rmse
    #>  [1,] 0.10  8.302191
    #>  [2,] 0.11  8.136132
    #>  [3,] 0.12  8.008744
    #>  [4,] 0.13  7.910766
    #>  [5,] 0.14  7.944608
    #>  [6,] 0.15  7.262508
    #>  [7,] 0.16  7.284905
    #>  [8,] 0.17  7.398039
    #>  [9,] 0.18  7.391820
    #> [10,] 0.19  7.454959
    #> [11,] 0.20  7.617551
    #> [12,] 0.75 14.358125
    spanhat <- span_vals[which.min(rmse)]
    show(c(span = spanhat))
    #> span
    #> 0.15

Try with the optimal span.

    fit <- loclm(X, y, span = spanhat)
    pp <- predict(fit, newX)
    par(op)
    matplot(newX[, 2], cbind(newy, pp), pch = c(1, 2), cex = cex,
      col = col,
      main = sprintf("span = %s (optimal)", format(spanhat)))

That’s a big improvement.

## Factor inputs

Let’s make the first input into a factor.

    ## first input is a factor

    breaks <- seq(0, 1, 0.2)
    X <- data.frame(X)
    X[[1]] <- factor(LETTERS[cut(X[[1]], breaks)])
    show(head(X, 10))
    #>    X1         X2        X3
    #> 1   A 0.56507611 0.3174938
    #> 2   D 0.28025778 0.7678555
    #> 3   D 0.20419632 0.5263085
    #> 4   D 0.13373890 0.7323019
    #> 5   E 0.32568192 0.3076657
    #> 6   D 0.15506197 0.4041733
    #> 7   A 0.12996214 0.2044024
    #> 8   B 0.43553106 0.9856331
    #> 9   D 0.03864265 0.5663108
    #> 10  C 0.71330156 0.2803751

    newX <- data.frame(newX)
    newX[[1]] <- factor(LETTERS[cut(newX[[1]], breaks)])

    ## OK, here is the fit

    fit <- loclm(X, y, span = spanhat)
    pp <- predict(fit, newX)
    par(op)
    matplot(newX[, 2], cbind(newy, pp), pch = c(1, 2), cex = cex,
      col = col,
      main = sprintf("First input is a factor"))

Not a disaster.

    ## reset the session

    par(oldpar)