# Migrating from pense version 1.x to 2.x

#### David Kepplinger

#### 2023-02-06

Version 2.x released September 2020 offers many new features and improved speed. These changes make pense 2.x incompatible with code written for pense 1.x and results will not be identical!

The most visible changes are to functions `pense()` and `pensem()`, which now only fit models but do not evaluate prediction performance anymore. Instead, the new functions `pense_cv()` and `pensem_cv()` are now used for fitting models and estimating their prediction performance.

First we need to load the new version of the pense package:

    library(pense)
    packageVersion("pense")
    #> [1] '2.2.0'

This guide uses the following simulated data:

    set.seed(1234)
    x <- matrix(rt(50 * 10, df = 5), ncol = 10)
    y <- 0.5 * x[, 1] - 2 * x[, 2] + 1.5 * x[, 3] + rt(nrow(x), df = 3)

# The new `_cv()` family of functions

The most basic usage in old versions was to call function `pense()` to fit models with `nlambda` different penalization levels and evaluate each model’s prediction performance with K-fold cross-validation. In the new version, this will now raise an error:

    set.seed(1234)
    fitted_with_cv <- pense(x, y, alpha = 0.6, nlambda = 40, warm_reset = 5L, cv_k = 5)
    #> Error:
    #> ! The `cv_k` argument of `pense()` was deprecated in pense 2.0.0 and is
    #>   now defunct.
    #> ℹ Please use `pense_cv()` instead.

As the error message says, if model fitting and cross-validation is required, use `pense_cv()` instead. The simple solution is to replace `pense()` with `pense_cv()`:

    set.seed(1234)
    fitted_with_cv <- pense_cv(x, y, alpha = 0.6, nlambda = 40, warm_reset = 5L, cv_k = 5)
    #> Error:
    #> ! The `warm_reset` argument of `pense()` was deprecated in pense 2.0.0
    #>   and is now defunct.
    #> ℹ Please use the `nlambda_enpy` argument instead.

However, this raises a warning that the argument `warm_reset=` is also deprecated in favor of argument `nlambda_enpy=`. The new version uses more consistent and self-explaining naming for arguments. Therefore, the most basic way for computing regularized S-estimates of linear regression and estimating their prediction performance via CV is the following:

    set.seed(1234)
    fitted_with_cv <- pense_cv(x, y, alpha = 0.6, nlambda = 40, nlambda_enpy = 5L, cv_k = 5)

To only fit the models, without estimating prediction performance, use `pense()` (note the absence of the `cv_k=` argument):

    fitted_no_cv <- pense(x, y, alpha = 0.6, nlambda = 40, nlambda_enpy = 5L)

## Structure of returned objects

The structure of the returned objects is different from pense versions 1.x. In old versions, the estimated coefficients were stored as a sparse matrix object, with one column per fit (i.e., per penalization level). In new versions, estimates are stored as a list with one entry per penalization level. For fitted models with prediction performance, as in `fitted_with_cv`, the returned object additionally contains information on the prediction performance of all fitted models:

    str(fitted_no_cv, max.level = 1)
    #> List of 6
    #>  $ call     : language pense(x = x, y = y, alpha = 0.6, nlambda = 40, nlambda_enpy = 5L)
    #>  $ bdp      : num 0.255
    #>  $ lambda   :List of 1
    #>  $ metrics  :List of 1
    #>  $ estimates:List of 40
    #>  $ alpha    : num 0.6
    #>  - attr(*, "class")= chr [1:2] "pense" "pense_fit"
    str(fitted_with_cv, max.level = 1)
    #> List of 9
    #>  $ call      : language pense_cv(x = x, y = y, cv_k = 5, alpha = 0.6, nlambda = 40, nlambda_enpy = 5L)
    #>  $ bdp       : num 0.255
    #>  $ lambda    :List of 1
    #>  $ alpha     : num 0.6
    #>  $ cvres     :'data.frame':  40 obs. of  4 variables:
    #>  $ cv_measure: chr "tau_size"
    #>  $ cv_repl   : int 1
    #>  $ metrics   :List of 1
    #>  $ estimates :List of 40
    #>  - attr(*, "class")= chr [1:2] "pense" "pense_cvfit"

# Extracting coefficient estimates

As in previous versions, the coefficient estimates are best accessed via the `coefficients()` method. For fits with estimated prediction performance, the method returns the coefficients of the model yielding the lowest prediction error:

    coefficients(fitted_with_cv)
    #>   (Intercept)            X1            X2            X3            X4
    #>  0.0764169254  0.5424968148 -1.7469201595  1.4314931674 -0.1059407235
    #>            X5            X6            X7            X8            X9
    #>  0.1410239524  0.0470255357 -0.0456642474  0.0005307165  0.2026268551
    #>           X10
    #>  0.0733546445

Fits computed with `pense()`, however, do not have information about prediction performance, hence `coefficients(fitted_no_cv)` would not know what penalization level to use. In this case, the desired penalization level must be specified manually:

    coefficients(fitted_no_cv, lambda = fitted_no_cv$lambda[10])
    #> Error in lambda[[1L]]: subscript out of bounds

An important difference to previous versions is that new versions do not correct for the bias introduced by the Ridge penalty. The correction, which was adopted from the original LS elastic net paper (Zou and Hastie 2005), was dropped as it does not adequately counter the effects of the Ridge penalty on the S-estimates. Specifying the `correction=` argument results in an error in new versions of the package:

    coefficients(fitted_with_cv, correction = TRUE)
    #> Error:
    #> ! The `correction` argument of `coef()` was deprecated in pense 2.0.0
    #>   and is now defunct.

The same applies to functions `residuals()` and `predict()` for extracting residuals of the fits and using the estimated coefficients for prediction, respectively.

# Specifying advanced options

Many of the optional arguments to `pense()` and `pensem()` have been renamed. Options to control the algorithms have been regrouped to remove ambiguity and redundancies. For example, previous versions had several options to specify the numerical tolerance used in different parts of the algorithm. This could have lead to unstable algorithms and undesired results. In new versions, the numerical tolerance is only specified on the top level in the call to `pense()` and friends.

## Controlling the PENSE algorithm

In previous versions of the package, all options for the PENSE algorithm have been set via the `pense(options=)` argument and the `pense_options()` function.

In new versions, the options are either given directly to the `pense()` function, or supplied via arguments `algorithm_opts` or `mscale_opts`:

pense versions 1.x | pense versions 2.x and above
--- | ---
`pense_options(eps=)` | `pense(eps=)`
`pense_options(delta=)` | `pense(bdp=)`
`pense_options(cc=)` | `pense(cc=)`
`pense_options(maxit=)` | `mm_algorithm_options(max_it=)`
`pense_options(mscale_maxit=)` | `mscale_algorithm_options(max_it=)`
`pense_options(mscale_eps=)` | `mscale_algorithm_options(eps=)`
all other options | unsupported

For example, in previous versions the breakdown point of the PENSE estimate was set with `pense_options(delta=)`:

    pense(x, y, alpha = 0.6, nlambda = 40, nlambda_enpy = 5L, options = pense_options(delta = 0.33))

In the new versions, the breakdown point is set directly in the call to `pense()`:

    pense(x, y, alpha = 0.6, nlambda = 40, nlambda_enpy = 5L, bdp = 0.33)

## Controlling the PENSEM algorithm

As with `pense()`, options for controlling the algorithm used by `pensem_cv()` are moved from `mstep_options()` to:

pense versions 1.x | pense versions 2.x and above
--- | ---
`mstep_options(cc=)` | `pensem_cv(cc=)`
`mstep_options(eps=)` | `pensem_cv(eps=)`
`mstep_options(maxit=)` | `mm_algorithm_options(max_it=)`
all other options | unsupported

## Selecting the EN algorithm

In previous versions, the EN algorithm (the workhorse to compute PENSE estimates), was specified via `pense(en_options=)`. In new versions, the user can select the EN algorithm separately for computing PENSE estimates and for computing starting points via ENPY separately. Therefore, the algorithm and it’s options are now set through arguments `algorithm_opts=` and `enpy_opts=`.

    pense(x, y, alpha = 0.6, nlambda = 40, nlambda_enpy = 5L,
          algorithm_opts = mm_algorithm_options(en_algorithm_opts = en_lars_options()),
          enpy_opts = enpy_options(en_algorithm_opts = en_admm_options()))

Moreover, functions `en_options_aug_lars()` and `en_options_dal()` are replaced by `en_lars_options()` and `en_dal_options()`, respectively. The new functions accept similar arguments, but with slightly different names. They do not accept the numerical tolerance `eps` anymore, as this is now set directly in `pense()` and friends. The LARS algorithm now always uses the Gram matrix, afforded by a more efficient implementation (argument `use_gram` is now ignored). Similarly, the DAL algorithm always uses a conjugate gradient pre-conditioner, doesn’t print status information, and adaptively chooses the initial step size as either `eta_start_conservative` or `eta_start_aggressive`, based on the change in penalization level. Hence, arguments `verbosity`, `preconditioner`, and `eta_start` are now defunct:

pense versions 1.x | pense versions 2.x and above
--- | ---
`en_options_dal(eps=)` | `pense(eps=)`
`en_options_dal(maxit=)` | `en_dal_options(max_it=)`
`en_options_dal(eta_mult=)` | `en_dal_options(eta_multiplier=)`
`en_options_dal(eta_start=)` | `en_dal_options(eta_start_conservative=, eta_start_aggressive=)`
all other options | unsupported

# References

Zou, Hui, and Trevor Hastie. 2005. “Regularization and Variable Selection via the Elastic Net.” Journal of the Royal Statistical Society. Series B (Statistical Methodology) 67: 301–20. http://www.jstor.org/stable/3647580.