---
title: "02c — Item mapping: observed codes and routing codes"
author: "Pierpaolo Massoli"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{02c — Item mapping: observed codes and routing codes}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include=FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
```

# Purpose and place in the workflow

A categorical variable may have three observed codes, such as `10`, `21`
and `99`. These are three categories. The largest code is not the number
of categories. SSPICE represents their outgoing transitions with three
modal positions, while routing metadata describe an equidistant structural
domain. Item mapping connects the observed codes to that domain.

Read [Building the routing file](routing-02b.html) first. This guide explains
which codes belong in each input, how the correspondence is used, and how
to check it before running the application pipeline. Its executable example
uses synthetic records and a temporary CSV; it needs the patched SSPICE
package, but no questionnaire data or external files.

# Three representations of the same category

| Meaning | Observed dataset code | Structural routing code | Dummy position |
|:--|--:|--:|--:|
| First category | 10 | 10 | 1 |
| Second category | 21 | 20 | 2 |
| Third category | 99 | 30 | 3 |

The mapping file is a comma-separated CSV with a header:

```text
varname,item_route,item_dataset
CHOICE,10,10
CHOICE,20,21
CHOICE,30,99
```

Each row pairs one routing code with one observed code **for the variable
named in that row**. Keep this column order. Codes are numeric; use separate
rows for each category and repeat the variable name. Different variables
can reuse the same numeric codes.

For this mapping, the beginning of the routing row is:

```text
CHOICE;3;10;30;0;...
```

The fields declare the variable name, three modes, minimum structural code
10 and maximum structural code 30. The implied structural codes are
`10, 20, 30`. The row `CHOICE;3;10;99;0;...` would instead imply
`10, 54.5, 99`: it does not describe the mapping above. Do not use the
observed minimum and maximum when they disagree with the chosen structural
domain, and do not declare 99 modes merely because one observed code is 99.

An equally valid choice is structural codes `1, 2, 3`. It requires mapping
rows `(CHOICE,1,10)`, `(CHOICE,2,21)`, `(CHOICE,3,99)` and routing bounds 1
and 3. Either structural coding produces the same three dummy positions.

For a structural domain with `n > 1` modes, minimum `a` and maximum `b`,
valid structural codes have step `(b - a)/(n - 1)`. After lookup, their dummy
position is `1 + (item_route - a)/step`. A code of 20 in the first example
therefore activates position 2, not position 20.

# Which codes to use where

| Input or output | Coding convention |
|:--|:--|
| Questionnaire and donor records | Observed dataset codes |
| Routing domain bounds and jump conditions | Structural routing codes |
| Mapping column 2, `item_route` | Structural routing codes |
| Mapping column 3, `item_dataset` | Observed dataset codes |
| VAUX/LEDIT predicates on the mapped input variable | Observed dataset codes |
| Corrected records and routing report values | Observed dataset codes |
| Generated RULES and VARDOM, when supplied with this mapping | Observed dataset codes |
| Internal categorical dummy representation | Positions `1, ..., n` |

`build_DAG()` receives routing metadata, not the mapping CSV. Thus the DAG
must already specify structural domains and jumps consistently.
`prepare_data()` does not apply this categorical mapping; its other
normalizations, such as logical-group processing, are described in
[Data preparation and checks](controlli-04.html).

The canonical checker and correction oracle translate observed codes to
routing codes in a working representation, then to dummy positions. They
do not recode the input dataset or rewrite the routing table. Lookup uses
the original value once. For example, observed codes `2, 3, 4` mapped to
structural codes `1, 2, 3` do not trigger successive substitutions when the
two sets overlap. Missing values remain missing and activate no categorical
mode; whether a missing value is a violation depends on the routing path.

The correction engine uses observed codes for mapped variable domains,
donor values and proposed record values. It translates a proposed value
when identifying an active predecessor or testing a jump. Explicit
`domains` overrides and `imputation_priors` for a mapped variable must
therefore also refer to its observed codes.

# A jump, six records, and a correction

Only the second category skips `DETAIL`. With structural codes `10, 20, 30`,
the routing jump condition is **20**, although the corresponding observed
value is **21**:

```text
CHOICE;3;10;30;0;1;20;END
```

The example below checks an independently specified expectation: records
1–3 are correct; records 4 and 6 omit a required DETAIL, and record 5 fills
DETAIL although it should be blank. Correction keeps CHOICE and END fixed
and repairs DETAIL. The same experiment is run with both structural domains.

```{r standalone-item-mapping, eval=TRUE}
local({
  folder <- tempfile("sspice-mapping-guide-")
  dir.create(folder)
  on.exit(unlink(folder, recursive = TRUE), add = TRUE)
  mapping_file <- file.path(folder, "items_mapping.csv")
  observed <- c(10, 21, 99)
  expected <- sort(c("4|DETAIL|-1", "5|DETAIL|1", "6|DETAIL|-1"))

  for (structural in list(c(10, 20, 30), 1:3)) {
    mapping <- data.frame(
      varname = "CHOICE", item_route = structural,
      item_dataset = observed
    )
    utils::write.csv(mapping, mapping_file, row.names = FALSE)

    routes <- data.frame(
      V1 = c("ID", "CHOICE", "DETAIL", "END"),
      V2 = c(1, 3, 1, 1),
      V3 = c(1, min(structural), 1, 1),
      V4 = c(9999, max(structural), 1, 1),
      V5 = 0, V6 = c(0, 1, 0, 0),
      V7 = c(NA, structural[2], NA, NA),
      V8 = c(NA, "END", NA, NA)
    )
    dag <- SSPICE::build_DAG(routes, analyze = FALSE)
    original <- data.frame(
      ID = 1:6, CHOICE = rep(observed, 2),
      DETAIL = c(1, NA, 1, NA, 1, NA), END = 1
    )
    prepared <- SSPICE::prepare_data(original, dag)$data
    stopifnot(isTRUE(all.equal(prepared$CHOICE, original$CHOICE)))

    checked <- SSPICE::routing_check(
      prepared, dag$routes_numeric, dag$Bt, "ID",
      basename(mapping_file), dirname(mapping_file)
    )
    report <- checked$full_report
    keys <- sort(paste(report$ID, report$varname_label,
                       report$errcode, sep = "|"))
    stopifnot(identical(keys, expected))
    cat("\nStructural codes:", structural, "\n")
    print(report[, c("ID", "varname_label", "errcode", "routeval")])

    corrected <- SSPICE::correct_data(
      prepared[4:6, , drop = FALSE], dag, record_id = "ID",
      donor_data = prepared[1:3, , drop = FALSE],
      immutable_variables = c("CHOICE", "END"),
      items_mapping_file = mapping_file,
      control = SSPICE::correction_control(
        seed = 123L, max_iterations = 12L, use_parallel = FALSE
      )
    )
    stopifnot(
      isTRUE(all.equal(corrected$corrected_data$DETAIL, c(1, NA, 1))),
      isTRUE(all.equal(corrected$corrected_data$CHOICE, observed)),
      all(corrected$record_summary$resolved)
    )
    print(corrected$corrected_data)

    # These internal generators are also used by the operational scripts.
    # getFromNamespace is explicit: they are not exported public functions.
    rules <- getFromNamespace("create_rules", "SSPICE")(
      dag$Bt, dag$routes_numeric,
      basename(mapping_file), dirname(mapping_file)
    )
    expected_rules <- c(
      "CHOICE(10) DETAIL< )",
      "CHOICE(21) DETAIL() END< )",
      "CHOICE(99) DETAIL< )"
    )
    stopifnot(setequal(rules[startsWith(rules, "CHOICE(")], expected_rules))
    print(rules[startsWith(rules, "CHOICE(")])
    vardom <- getFromNamespace("create_vardom", "SSPICE")(
      dag$routes_numeric, dag$Bt,
      basename(mapping_file), dirname(mapping_file)
    )$vardom
    choice_domain <- vardom[vardom$varname == "CHOICE", , drop = FALSE]
    stopifnot(identical(as.character(choice_domain$intervalli),
      paste0(sprintf("%04d%04d", observed, observed), collapse = "")))
    print(choice_domain)
  }
})
```

`routeval` in the error report refers to the observed value of the variable
responsible for the transition. In the record that incorrectly fills DETAIL
under the second category, this value is 21. The generated rule also refers
to `CHOICE(21)`. Mapping is an internal correspondence, not a request to
rewrite the questionnaire records with codes 1–3 or 10–30.

# VAUX and LEDIT retain the meaning of the original values

A predicate referring to the second observed category of CHOICE must test
`CHOICE = 21`. Writing `CHOICE = 20` would test a different observed value;
the mapping file does not automatically rewrite VAUX or LEDIT statements.
Likewise, a sum or other arithmetic expression involving CHOICE uses its
observed values. Mapping category labels does not redefine arithmetic.

VAUX and LEDIT are evaluated on the records using their own metadata.
Their resulting variables have their own domains and routing nodes. A mapping
for CHOICE does not implicitly map a derived VAUX or LEDIT variable. For
example, a VAUX may assign category 1 when CHOICE is 21 and category 2 when
CHOICE is 10 or 99; those output codes belong to the VAUX variable.

The regression script `scripts/13_verify_item_mapping.R` includes matched
VAUX and LEDIT examples in observed and normalized coding. The comparison
translates the predicates as well as the data. It also tests routing,
correction, exports, missing values and overlapping codes. See
[Metadata and external sources](metadati-02.html) for the statement format,
assignment order and missing-operand rules; mapping does not change them.

# The bundled FASE variable

FASE is a categorical variable with ten observed modalities. The bundled
mapping specifies:

| Observed FASE | Structural code | Dummy position |
|--:|--:|--:|
| 10 | 10 | 1 |
| 20 | 20 | 2 |
| 21 | 30 | 3 |
| 25 | 40 | 4 |
| 50 | 50 | 5 |
| 55 | 60 | 6 |
| 60 | 70 | 7 |
| 70 | 80 | 8 |
| 80 | 90 | 9 |
| 100 | 100 | 10 |

Its structural declaration starts with `FASE;10;10;100`. The verification
script checks all ten positions in both the canonical checker and the
correction oracle. This is a controlled encoding test, not a rerun of
correction on the application dataset. The separate CHOICE example tests a
category-dependent jump, which an encoding-only example cannot establish.

# Supplying the file consistently

For the project scripts, set `config$items_mapping_file` in
`scripts/00_config.R`. The working example path is
`data-raw/metadata/items_mapping.csv`; the packaged copy is under
`inst/extdata/SSPICE/metadata/`. Editing one copy does not update the other.
Check that the workflow is reading the intended file.

| Function or workflow | Mapping input |
|:--|:--|
| `build_DAG()` | None; routing metadata already use structural codes |
| `prepare_data()` | No categorical recoding through this CSV |
| `routing_check()` | `items.maps = basename(file)`, `path.items.maps = dirname(file)` |
| `correct_data()` | `items_mapping_file = file` |
| Step 04, RULES/VARDOM generation | The same mapping supplied to the generators |
| VAUX/LEDIT evaluation | Predicates already use observed codes; no implicit predicate translation |

For a workflow without recoding, a CSV containing only
`varname,item_route,item_dataset` is a consistent way to supply an empty
mapping, including to functions that expect a file. Unmapped categorical
variables still need codes consistent with their structural domains.

# Validation and common mistakes

For each mapped variable in the DAG, the patched checker requires a complete
one-to-one correspondence with the structural domain. The shared encoding
validates the first three columns, numeric codes, unique codes on each side,
and agreement with the declared number of modes and structural bounds.
A valid file may include mappings for variables outside a particular DAG;
those variables are not encoded by that DAG.

| Symptom or mistake | What to check |
|:--|:--|
| Mapping file not found | Full path, or the filename/directory pair supplied to the checker |
| Duplicate codes | Each observed code and structural code must occur once per mapped variable |
| Routing domain disagreement | Complete mapping; number of modes; structural minimum, maximum and spacing |
| Observed code absent from mapping | Input values and missing-value conventions; add a legitimate category consistently to routing and mapping |
| Jump follows the wrong category | Jump conditions must use structural codes, not irregular observed codes |
| VAUX/LEDIT predicate does not match | Predicates must use observed values; mapping does not rewrite statements |
| A mapping seems to be ignored | Exact variable name, active file path, and mapping supplied to the function |

Use actual `NA` for missing data. A numeric sentinel such as 99 remains an
observed category when it is declared as one; it is not automatically missing.
Do not recode the input data yourself and then pass a mapping that expects
the original codes: overlapping values make such double conversion ambiguous.

The validation above is not a universal data-domain check. In particular,
the existing scaling convention for unmapped categories is not a guarantee
that every out-of-domain numeric value is rejected. This guide concerns
numeric categorical mappings; string fields, quantitative presence states
and OR/XOR normalization follow the separate routing conventions.

When the mapping or routing specification changes, regenerate dependent
artifacts and recheck records with the updated inputs before interpreting
correction results. The mapping patch invalidates the earlier cache schema
and correction checkpoint fingerprint; that does not recompute or validate
previously exported results automatically.

From the source-project root, run:

```{r verify-mapping-project, eval=FALSE}
source("scripts/13_verify_item_mapping.R")
```

Read [Domains and rules](regole-05.html) and [Record correction](correzione-06.html)
for the next workflow steps.
