02 — Metadata and external sources

Pierpaolo Massoli

Operational examples assume that the SSPICE project root is the working directory. Operational chunks are not executed when building the vignette because they require the project data. Standalone chunks explicitly marked eval=TRUE are exceptions.

Input files

File Purpose Preliminary check
routing_file.csv Nodes, categories and routing dependencies Consistent identifiers and no cycles
vaux_rules.csv Auxiliary-variable definitions Consistent conditions and assigned categories
logical_edit_rules.csv Logical constraints between variables Consistent categories and operators
items_mapping.csv Correspondence between routing codes and observed codes for each variable See Item mapping for the coding contract and an executable example
data-raw/input/questionnaire_data.csv Responses to be checked Agreed record key and missing-value codes
data-raw/external/*.csv Additional information for exogenous auxiliary variables Linkage key and uniqueness at the required level of observation

The project’s routing, auxiliary-variable and logical-edit metadata use ; as the separator; items_mapping.csv uses commas. Configuration specifies these paths, the metadata separator, missing_codes and record_id (DB030 in the included example). Do not infer a file’s schema from its name: check its columns and key against the actual rules.

source(file.path("scripts", "00_config.R"))
stopifnot(all(file.exists(c(config$routing_file, config$vaux_file,
                            config$logical_edit_rules_file,
                            config$items_mapping_file))))
utils::head(utils::read.csv(config$items_mapping_file, check.names = FALSE))

Exogenous auxiliary variables and the observation unit

An auxiliary variable requiring information on individual household members cannot be inferred from a file containing only the household respondent. In this project, extfile3.csv contains DB030 and EXIST_MINOR; the latter indicates whether the household contains at least one individual with PRESEN_B = 1 and ETAINT < 15. Auxiliary rules must refer to the correct external information, linked through DB030.

Active exogenous auxiliary variables retain their source value during correction; if routing makes them inactive, they may be blanked to NA. Check missing source values before interpreting an error as an incorrect questionnaire response.

Defining auxiliary-variable categories

vaux_rules.csv has nine columns without a header, separated by ;. Each row contributes to a category definition; multiple rows with the same variable name and category form its condition.

Column Meaning
V1 Auxiliary-variable name
V2 Category assigned when the condition is true
V3 Variable used in this row
V4, V5 Optional external-source name without .csv, and linkage key
V6 Exact value; BLANK or NA denotes a missing value
V7, V8 Inclusive interval bounds
V9 Operator following the row, or comparison terminating a calculation

For logical conditions, the parser automatically opens a parenthesis at the start of each condition and closes it when it encounters NULL.

Logical token in V9 Effect
AND, OR Connect this row’s condition to the next one
) AND (, ) OR ( Close the current group and open the next one
NULL Close and evaluate the condition for the row’s category
IN(...), NOT IN(...) Test membership or non-membership; BLANK represents a missing value

For example, the purely logical rule A = 1 AND (D = 1 OR E = 3) is represented as follows:

V1 V2 V3 V6 V7 V8 V9
VAUX_ESEMPIO 1 A 1 . . ) AND (
VAUX_ESEMPIO 1 D 1 . . OR
VAUX_ESEMPIO 1 E 3 . . NULL

In the actual file, V4 and V5 are empty for variables already present in the data. If V4 specifies extfile1, extfile2 or extfile3, V5 specifies the key (DB030 in the project example).

Purely arithmetic rules

Operation Simple token in V9 Grouping token in V9
Addition + ) + (
Subtraction - ) - (
Multiplication * ) * (
Division / ) / (

The last row compares the result with a numeric constant: <n, <=n, >n, >=n, =n, ==n or !=n. Positive or negative constants can be used, for example <-2 or >=0.5. The comparison closes the expression and assigns the V2 category to data rows for which it is true.

Purely arithmetic example, (A+B)*(C-D) >= 10:

V1 V2 V3 V9
VAUX_ARIT 1 A +
VAUX_ARIT 1 B ) * (
VAUX_ARIT 1 C -
VAUX_ARIT 1 D >=10

V4–V5 and V6–V8 are empty in this example. Simple operators follow R’s arithmetic precedence; tokens containing parentheses close one group and open another.

Mixed rules

To continue after a comparison with a logical condition, write the comparison followed by a space and a connector: <10 AND, <10 ) AND (, >=5 ) OR (, or =100 NULL. OR and ) OR ( are also accepted.

Example: A < 10 AND (D = 1 OR E = 3). This is a strict comparison and also works when A contains decimal values.

V1 V2 V3 V6 V7 V8 V9
VAUX_MISTA 1 A . . . <10 ) AND (
VAUX_MISTA 1 D 1 . . OR
VAUX_MISTA 1 E 3 . . NULL

Example combining grouped arithmetic and logic: ((A + B) * (C - D) / F >= 5) OR (E = 1 AND A = 2).

V1 V2 V3 V6 V7 V8 V9
VAUX_CALCOLO 1 A . . . +
VAUX_CALCOLO 1 B . . . ) * (
VAUX_CALCOLO 1 C . . . -
VAUX_CALCOLO 1 D . . . ) / (
VAUX_CALCOLO 1 F . . . >=5 ) OR (
VAUX_CALCOLO 1 E 1 . . AND
VAUX_CALCOLO 1 A 2 . . NULL

Missing values in calculations produce conditions that are not true and leave the auxiliary variable at NA if no category applies. The available grouping tokens close one group and open another; they do not provide arbitrarily nested parentheses. NOT IN(...) supports set non-membership; general NOT and XOR between predicates are not reliably supported. Check overlapping categories and rows for which no condition is true before applying new rules to real data.

Application example: project auxiliary variables

In the project, VAUX7 is derived from the external file extfile3.csv. The DB030 key links each household to EXIST_MINOR, calculated over the whole household. Its two categories are defined by:

VAUX7;1;EXIST_MINOR;extfile3;DB030;1;.;.;NULL
VAUX7;2;EXIST_MINOR;extfile3;DB030;0;.;.;NULL

EXIST_MINOR cannot be reconstructed from the questionnaire respondent alone: it comes from the presence of at least one member with PRESEN_B = 1 and ETAINT < 15 in the individual-level file used to prepare the source. The auxiliary rule is applied after linkage on DB030; when linkage fails, the category remains missing.

A second actual example is VAUX_PROP2, which classifies the sum QPROP1 + QPROP2:

VAUX_PROP2;1;QPROP1;;;;;;+
VAUX_PROP2;1;QPROP2;;;;;;<100
VAUX_PROP2;2;QPROP1;;;;;;+
VAUX_PROP2;2;QPROP2;;;;;;=100

These rows use arithmetic only, without logical operators. They can be inspected in the project outputs without modifying the data:

# Run from the root of your project copy.
d <- readRDS("output/prepared-data/prepared_questionnaire_data.rds")
table(d$VAUX7, useNA = "ifany")
table(d$VAUX_PROP2, useNA = "ifany")

Dedicated routing guide

The complete routing_file.csv schema, variable types, categories, conditional and unconditional jumps, and OR/XOR groups are described in Building the routing file. Do not infer routing conventions from the auxiliary-variable file: the two formats have different purposes and fields.

Consistency of external sources

Before preparation, check key uniqueness, key types, coverage of questionnaire DB030 values and availability of the values required by the rules. A complete questionnaire file combined with reduced external sources can produce many missing categories even when the original responses have not changed. When extracting a sample, subset each source using the same keys and document links that were already absent from the source. After replacing sources, regenerate prepared data, checks and extended data, and verify cache validity before correction.

Defining semantic edits: logical_edit_rules.csv

This is a headerless nine-column file separated by semicolons. The columns have the same roles V1–V9 as the VAUX table, but the LEDIT evaluator has its own semantics. Each edit must define both accepted categories 1 and 2. Both are valid outcomes: 2 does not mean error. If no condition assigns an accepted category, the result remains NA and the edit is violated. Choose and document what the two categories mean for each edit.

For example, require A + B = 100 when amounts are reported, and allow both amounts to be blank when the edit is inapplicable. Define category 2 first for the all-blank case, then category 1 for the total:

LEDIT_TOTAL;2;A;;;BLANK;;;AND
LEDIT_TOTAL;2;B;;;BLANK;;;NULL
LEDIT_TOTAL;1;A;;;;;;+
LEDIT_TOTAL;1;B;;;;;;=100

V4 and V5 are empty because A and B are input columns. V6 specifies an exact value or BLANK; V7 and V8 specify inclusive bounds instead. Arithmetic rows leave V6–V8 empty. V9 joins rows and closes a predicate with NULL or an arithmetic expression with a comparison. Keep each condition contiguous and use the same V1 and V2 throughout that condition.

Missing arithmetic operands are replaced by zero in the LEDIT evaluator. Consequently A = 100 and B = NA satisfies the total in this example. If both amounts must be observed, define additional presence requirements. This differs from the VAUX arithmetic behavior described above. Logical conditions evaluating to NA do not assign a category. Conditions are processed in file order; the first matching condition wins. Later conditions cannot overwrite an assigned category. Avoid unintended overlaps.

local({
  edits <- read.table(text = paste(
    "LEDIT_TOTAL;2;A;;;BLANK;;;AND",
    "LEDIT_TOTAL;2;B;;;BLANK;;;NULL",
    "LEDIT_TOTAL;1;A;;;;;;+",
    "LEDIT_TOTAL;1;B;;;;;;=100", sep = "\n"),
    sep = ";", header = FALSE, colClasses = "character",
    na.strings = character(0))
  x <- data.frame(ID = 1:5, A = c(40, NA, 40, 100, NA),
                  B = c(60, NA, 50, NA, 20))
  create_edits <- getFromNamespace("create_logical_edit_variables", "SSPICE")
  out <- create_edits(x, edits)
  print(out)
  stopifnot(identical(out$LEDIT_TOTAL, c(1, 2, NA_real_, 1, NA_real_)))
  print(attr(out, "logical_edit_definitions"))
})
#>   ID   A  B LEDIT_TOTAL
#> 1  1  40 60           1
#> 2  2  NA NA           2
#> 3  3  40 50          NA
#> 4  4 100 NA           1
#> 5  5  NA 20          NA
#>   logical_edit assigned_value statement_number
#> 1  LEDIT_TOTAL              2                1
#> 2  LEDIT_TOTAL              1                2
#>                                                                                                                                                   expression
#> 1                                                                                                                                  (is.na(`A`) & is.na(`B`))
#> 2 ((ifelse(is.na(`A`), 0, suppressWarnings(as.numeric(as.character(`A`)))) + ifelse(is.na(`B`), 0, suppressWarnings(as.numeric(as.character(`B`))))) == 100)
#>   condition_true_records newly_assigned_records
#> 1                      1                      1
#> 2                      2                      2

The output categories are 1, 2, NA, 1, NA. The logical_edit_definitions attribute records the expressions and assignment counts. The internal evaluator is called explicitly here to illustrate the metadata; the operational entry point is Step 09, which also appends LEDIT nodes with accepted domain {1, 2} to the routing representation.

For membership expressions, IN(A,B) compares the current variable with other columns in the same record, not with a list of numeric constants. IN(BLANK) tests missingness. Logical tokens include AND, OR, XOR and NOT; NOT in a connector position means AND NOT of the following predicate. For mixed arithmetic and logical groups the comparison connector has the form >=5) AND (. Do not copy the more permissive VAUX token syntax without checking it against the LEDIT parser.

V4 specifies an external source basename and V5 its linkage key. External keys must be unique. Missing links remain missing, subject to the arithmetic zero convention above. If a referenced column already exists in the input, the evaluator retains that column; avoid conflicting source-column names.

Item mapping and semantic predicates

Item mapping explains the correspondence between observed categories and equidistant structural codes. Keep observed codes in the data and in predicates on those variables in VAUX/LEDIT metadata. Routing bounds and jump conditions use structural codes. Mapping does not rewrite semantic statements or change their arithmetic meaning.