---
title: "Restricted regression with vimpute"
author: "VIM contributors"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Restricted regression with vimpute}
  %\VignetteEncoding{UTF-8}
  %\VignetteEngine{knitr::rmarkdown}
---

```{r, include=FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>",
  fig.width = 7,
  fig.height = 5,
  fig.align = "center"
)

has_restricted_backend <- requireNamespace("validate", quietly = TRUE) &&
  requireNamespace("ECOSolveR", quietly = TRUE)
```

`vimpute(method = "restricted")` imputes numeric variables with a
least-squares model whose predictions are constrained by edit rules. The rules
are written with `validate::validator()` and solved with `ECOSolveR`.

This is useful when the completed data must satisfy accounting identities,
bounded totals, or row-wise consistency rules.

```{r backend-message, echo=FALSE, results='asis'}
if (!has_restricted_backend) {
  cat("This vignette requires the packages `validate` and `ECOSolveR`; ",
      "the runnable examples are skipped because at least one is unavailable.")
}
```

## Minimal Example

The smallest useful example is a lower bound. Here the observed data suggest a
line through `(x, y)`, but the missing value is constrained to be at least 4.
The imputation indicator `y_imp` marks the cell filled by `vimpute()`.

```{r minimal-example, eval=has_restricted_backend, message=FALSE, warning=FALSE}
library(VIM)

small_data <- data.frame(
  y = c(1, 2, NA),
  x = c(1, 2, 3)
)

rules <- validate::validator(y >= 4)

small_imp <- vimpute(
  small_data,
  method = list(y = "restricted"),
  pmm = FALSE,
  sequential = FALSE,
  learner_params = list(
    restricted = list(
      rules = rules,
      save_optimization_problem = TRUE
    )
  )
)

small_imp
```

The completed value satisfies the rule, and the saved optimization problem can
be inspected when `save_optimization_problem = TRUE`.

```{r minimal-check, eval=has_restricted_backend}
all(validate::values(validate::confront(small_imp, rules)))

names(attr(small_imp, "restricted_optimization_problems"))
```

## Interval Rules

Restricted regression is most natural when variables have row-wise lower and
upper bounds. In this example `amount` is imputed while every completed value
must remain between its corresponding `lower` and `upper` columns.

```{r interval-rules, eval=has_restricted_backend, message=FALSE, warning=FALSE}
set.seed(42)
n <- 100L

bounded_data <- data.frame(
  x1 = stats::runif(n, 0, 10),
  x2 = stats::runif(n, -2, 2)
)
bounded_data$amount <- 20 + 1.5 * bounded_data$x1 - 0.75 * bounded_data$x2
bounded_data$lower <- bounded_data$amount - 0.5
bounded_data$upper <- bounded_data$amount + 0.5
bounded_data <- bounded_data[, c("amount", "x1", "x2", "lower", "upper")]

missing_idx <- sample.int(n, 20L)
bounded_missing <- bounded_data
bounded_missing$amount[missing_idx] <- NA_real_

bounded_rules <- validate::validator(
  amount >= lower,
  amount <= upper,
  amount >= 0,
  lower <= upper
)

bounded_imp <- vimpute(
  bounded_missing,
  method = list(amount = "restricted"),
  pmm = FALSE,
  sequential = FALSE,
  learner_params = list(amount = list(rules = bounded_rules))
)

head(bounded_imp[bounded_imp$amount_imp, c("amount", "lower", "upper")])
```

```{r interval-check, eval=has_restricted_backend}
completed <- as.data.frame(bounded_imp)[, names(bounded_data), drop = FALSE]

sum(bounded_imp$amount_imp)
all(validate::values(validate::confront(completed, bounded_rules)))
```

Rules can also involve categorical predictors and conditional logic. The
restricted learner uses the edit rules only to constrain the imputed target; the
remaining columns can be numeric or categorical predictors.

```{r conditional-rules, eval=has_restricted_backend, message=FALSE, warning=FALSE}
categorical_data <- bounded_missing
categorical_data$c1 <- factor(rep(c("A", "B", "C"), length.out = n))
categorical_data$c2 <- factor(rep(c("B", "C", "A"), length.out = n))
categorical_data$c3 <- factor(rep(c("C", "A", "B"), length.out = n))

conditional_rules <- validate::validator(
  amount >= lower,
  amount <= upper,
  amount >= 0,
  lower <= upper,
  (c1 == "A") + (c2 == "A") + (c3 == "A") <= 2,
  if (c1 == "A") amount >= 2
)

categorical_imp <- vimpute(
  categorical_data,
  method = list(amount = "restricted"),
  pmm = FALSE,
  sequential = FALSE,
  learner_params = list(amount = list(rules = conditional_rules))
)

all(validate::values(validate::confront(categorical_imp, conditional_rules)))
```

## Formula Control

`formula` can be used with `method = "restricted"` to choose the predictors in
the regression model while the edit rules still constrain the final
predictions. The left-hand side must be the untransformed target variable.

```{r formula-control, eval=has_restricted_backend, message=FALSE, warning=FALSE}
n_formula <- 80L
formula_data <- data.frame(
  y = 10 + 2 * seq_len(n_formula),
  x_signal = seq_len(n_formula),
  x_noise = 10 + 2 * seq_len(n_formula),
  lower = 0,
  upper = 200
)

formula_missing_idx <- c(12L, 24L, 36L)
expected_y <- formula_data$y[formula_missing_idx]
formula_data$y[formula_missing_idx] <- NA_real_
formula_data$x_noise[formula_missing_idx] <- -1000

formula_rules <- validate::validator(y >= lower, y <= upper, lower <= upper)

formula_imp <- vimpute(
  formula_data,
  method = list(y = "restricted"),
  formula = list(y = y ~ x_signal),
  pmm = FALSE,
  sequential = FALSE,
  learner_params = list(y = list(rules = formula_rules))
)

formula_imp[formula_missing_idx, c("y", "x_signal", "x_noise", "y_imp")]
max(abs(formula_imp$y[formula_missing_idx] - expected_y))
```

## Robust Restricted Regression

Set `robust = TRUE` to use a Huber loss instead of ordinary least squares. This
is helpful when the observed data contain influential outliers but the final
imputed values must still obey the edit rules.

```{r robust-restricted, eval=has_restricted_backend, message=FALSE, warning=FALSE}
robust_rules <- validate::validator(y >= 0)

outlier_data <- data.frame(
  x = 0:21,
  y = 1 + 2 * (0:21)
)
outlier_data$y[21L] <- 500
outlier_data$y[22L] <- NA_real_

ordinary_imp <- vimpute(
  outlier_data,
  method = list(y = "restricted"),
  formula = list(y = y ~ x),
  pmm = FALSE,
  sequential = FALSE,
  learner_params = list(y = list(rules = robust_rules))
)

robust_imp <- vimpute(
  outlier_data,
  method = list(y = "restricted"),
  formula = list(y = y ~ x),
  pmm = FALSE,
  sequential = FALSE,
  learner_params = list(y = list(
    rules = robust_rules,
    robust = TRUE,
    huber_k = 1.345
  ))
)

c(
  ordinary = ordinary_imp$y[22L],
  robust = robust_imp$y[22L],
  expected_without_outlier = 43
)
```

## Synthetic LSE Data

The package includes `lse_synthetic` and `lse_synthetic_rules`, a compact
synthetic business-statistics example with many linear edit rules. The rules
ship as plain data frames of rule text (so the data set does not depend on
`validate`); `validate::validator(.data = )` turns a rule set into a validator.
The test suite uses this data at larger scale; the vignette uses a smaller
slice so it can be knitted quickly.

```{r lse-example, eval=has_restricted_backend, message=FALSE, warning=FALSE}
data("lse_synthetic", package = "VIM")
data("lse_synthetic_rules", package = "VIM")

lse_rules <- validate::validator(.data = lse_synthetic_rules$edit)

numeric_cols <- c(
  "persons_employed",
  "employees_paid",
  "self_employed",
  "employees_male",
  "employees_female",
  "employees_blue_collar",
  "employees_white_collar",
  "apprentices",
  "marginal_employees",
  "turnover_total",
  "turnover_domestic",
  "turnover_exports",
  "e_commerce_turnover",
  "material_costs",
  "purchased_services",
  "rents_leasing",
  "other_operating_expense",
  "intermediate_consumption",
  "gross_value_added",
  "personnel_costs",
  "wages_salaries",
  "social_security_costs",
  "other_personnel_costs",
  "gross_operating_surplus",
  "investments_tangible",
  "investment_machinery",
  "investment_buildings",
  "investment_software"
)

edit_cols <- c(
  "reporting_year",
  "onace_section",
  "onace_group",
  "nuts2",
  "data_source",
  "survey_mode",
  "employment_size_class",
  "turnover_size_class",
  numeric_cols
)

lse_complete <- lse_synthetic[seq_len(80L), edit_cols]

missing_map <- list(
  turnover_total = c(2L, 15L),
  intermediate_consumption = c(13L, 18L),
  gross_value_added = c(24L, 31L),
  personnel_costs = c(39L, 45L),
  investments_tangible = c(52L, 60L)
)

lse_missing <- lse_complete
for (var in names(missing_map)) {
  lse_missing[missing_map[[var]], var] <- NA_real_
}

restricted_formulas <- setNames(
  lapply(names(missing_map), function(var) {
    stats::reformulate(setdiff(numeric_cols, var), response = var)
  }),
  names(missing_map)
)

lse_imp <- vimpute(
  lse_missing,
  method = "restricted",
  formula = restricted_formulas,
  pmm = FALSE,
  sequential = FALSE,
  learner_params = list(
    restricted = list(
      rules = lse_rules,
      save_optimization_problem = TRUE
    )
  )
)

lse_completed <- as.data.frame(lse_imp)[, edit_cols, drop = FALSE]

sum(as.data.frame(lse_imp)[paste0(names(missing_map), "_imp")])
all(validate::values(validate::confront(lse_completed, lse_rules)))
sort(names(attr(lse_imp, "restricted_optimization_problems")))
```

The same pattern scales to many variables: define a `validate::validator`
object for the edits, provide one formula per imputed target when you need
predictor control, and pass the rules through `learner_params` for either the
method name (`restricted`) or the individual variables.
