scale_variables

Cell-level arithmetic on numeric input variables: wage changes, hours shocks, any income component adjustment. It consumes the scale channel.

How it works

The price and income channel

A macro model projects nominal paths — wage growth by region, an hours adjustment, a change in investment income. This method applies them heterogeneously across population cells, which is the entire point of pushing them through a microsimulation.

A uniform +3% wage change does not have a uniform effect on disposable income. Tax brackets, benefit withdrawals and means tests bite differently along the distribution, so the same percentage at the top and the bottom produces quite different net outcomes. EUROMOD supplies exactly that pass-through when the scaled inputs are simulated.

What a shock names

The metric is either an input variableyem, lhw, yiy — or an EUROMOD income list such as ils_udb_yem.

Income lists matter because an economic concept like “employment income” is not one variable. Scaling the list scales every component the model itself counts under that concept, so the shock stays consistent with the model’s own accounting. Expansion is extension-aware: it walks the model’s DefIl definitions at run time, so a list stays correct when an extension changes what it contains.

Lists accept mult and grow only. A proportional factor distributes exactly over a sum; an absolute add or set on an aggregate has no unique per-component allocation, so it is rejected rather than resolved arbitrarily.

The income concepts you can name

These are EUROMOD’s User Database output lists. The names are standardised across the EU-27 countries; their membership is not — it is country- and extension-specific, which is why a list is resolved against the live model at run time rather than tabulated here. A name a given system does not define fails with the list of names it does.

Market income (scalable)

Income list

Concept

Scaling it

ils_udb_yem

Employment income

Scales the recorded input variables

ils_udb_yse

Self-employment income

Scales the recorded input variables

ils_udb_yiy

Investment income

Scales the recorded input variables

ils_udb_ypr

Property income

Scales the recorded input variables

ils_udb_ypp

Private pension income

Scales the recorded input variables

ils_udb_ypt

Private transfers received

Scales the recorded input variables

ils_udb_yot

Other income

Scales the recorded input variables

ils_udb_yds

Total market income — employment, self-employment, private pension, property, investment income and private transfers combined

Scales the recorded input variables

Benefits and taxes (mostly simulated — see notes)

Income list

Concept

Scaling it

ils_udb_bun

Unemployment benefits

Mostly recomputed — see below

ils_udb_bsa

Social assistance

Mostly recomputed — see below

ils_udb_bho

Housing benefits

Mostly recomputed — see below

ils_udb_bhl

Health-related benefits

Mostly recomputed — see below

ils_udb_bed

Education benefits

Mostly recomputed — see below

ils_udb_bfa

Family benefits

Mostly recomputed — see below

ils_udb_boa

Old-age pensions

Mostly recomputed — see below

ils_udb_bsu

Survivors’ benefits

Mostly recomputed — see below

ils_udb_bdi

Disability benefits

Mostly recomputed — see below

ils_udb_tis

Income tax and social insurance contributions

Mostly recomputed — see below

ils_udb_tpr

Property tax

Mostly recomputed — see below

Simulated components, and why they are skipped

A variable whose name ends in _s is a simulated output: EUROMOD computes it during the run from the input microdata and the policy rules. It is not a column of the input, so there is nothing to scale — and even if there were, the engine would overwrite it on the next run.

This is the difference between the two groups above. Market-income lists resolve to data-reported variables: ils_udb_yem is employment income as the survey recorded it, and scaling it changes what the model is given. Benefit and tax lists resolve largely to what the model produces: ils_udb_tis is income tax and social insurance contributions, every component of which EUROMOD calculates.

The package refuses to pretend otherwise, in three steps:

  1. The split is reported, not hidden. Expansion compares the list’s components against the input’s own columns and returns {"scaled": [...], "skipped_not_in_input": [...]}. What you see is what will move.

  2. A partial list warns. ils_udb_bun resolves to bun, bun_s, byr and more — a mixture. Scaling it shifts the recorded part while the simulated part is recomputed from unchanged rules, which is almost never what was meant, so the warning names every skipped component and says so.

  3. A fully simulated list is rejected. If nothing a list resolves to is in the input, the scenario fails rather than running a shock that provably does nothing.

The same split is computed by preview() and by apply(), from the same columns, so the validation report and the run cannot disagree about what was scaled.

To change what a benefit or tax pays out, shock its policy parameters through the constant channel instead. That changes the rules the engine applies, which is the thing that actually determines a simulated amount. Scaling the output of a calculation cannot change the calculation.

How shocks compose

Rows are matched per shock by its own group keys. Shocks of different granularity may coexist: a national wage shock and a regional one both apply to someone caught by both, and their effects compound.

Mixing ops in one table is fine. The rule is about overlap, and it is one line: two shocks may touch the same person on the same metric only when their ops commute.

mult and growproportional

Commute with each other. Both multiply, so x × m × (1+g) is the same either way round.

addadditive

Commutes with add. Does not commute with mult/grow: (x + a) × m is not x × m + a.

setabsolute

Commutes with nothing, not even another set — the later one would simply overwrite the earlier.

An overlap across families is rejected, naming both shocks. Without that rule the answer would depend on which cell sorted first alphabetically, which is no basis for an economic result. Non-overlapping cells may use any ops they like.

Region shocks finer than the dataset collapse to the level it supports, with growth rates averaged. See population cells.

What it does not do

No rows are added or removed and no weights change, so the baseline is simply the untouched input. The transform is purely arithmetic and deterministic — the same inputs always give the same output.

Using it

A scenario

import pandas as pd
from euromod_linking import apply_scenario
from euromod import Model

system = Model(MODEL_PATH)["BE"]["BE_2025"]
data = pd.read_csv(data_file, sep="\t")

scenario = {
    "country_code": "BE",
    "system_name": "BE_2025",
    "shocks": {"inline": [
        # Employment income up 3% for medium education.
        {"channel": "scale", "metric": "yem", "group": "deh=3-4",
         "period": "1", "op": "grow", "value": 0.03},
        # Investment income down 1% everywhere.
        {"channel": "scale", "metric": "yiy", "group": "",
         "period": "1", "op": "grow", "value": -0.01},
    ]},
    "params": {"period": "1"},
}

plan = apply_scenario(system, data, scenario)
counterfactual = plan["counterfactual"]

plan["methodology"] reads back "scale_variables" — the dispatch that was resolved, not one you asked for.

Scaling an income list

scenario["shocks"] = {"inline": [
    {"channel": "scale", "metric": "ils_udb_yem", "group": "region=21",
     "period": "1", "op": "grow", "value": 0.05},
]}

plan = apply_scenario(system, data, scenario, validate_only=True)
plan["diagnostics"]["income_list_expansions"]
# {'ils_udb_yem': {'scaled': ['yem'], 'skipped_not_in_input': []}}

The expansion report is worth reading before a full run: it names exactly which input variables the list resolved to for this country and system, and which components were skipped because the model simulates them.

To see the catalogue of standardised lists and what each covers:

from euromod_linking.methods.scale_variables import income_lists

income_lists()["market income (scalable)"]

Checking the size of a shock first

plan = apply_scenario(system, data, scenario, validate_only=True)
plan["diagnostics"]["cell_population"]
# {'deh=3-4': {'n_rows': 5460, 'n_weighted_all_ages': 8813907.8}}

That reports how many people each cell actually contains, so a group that matches nobody — a typo, or a variable this dataset does not carry — is visible before anything is transformed.

Running both halves

from euromod_linking import run_scenario

out = run_scenario(system, scenario, input_path=INPUT_DIR)
out["baseline_output"], out["counterfactual_output"]

Because this method leaves the row structure alone, the baseline is the untransformed input and the two outputs are directly comparable row for row.

What it needs

Nothing from the model beyond the system itself — no add-ons, no extension switches — so it runs against any EUROMOD release. Its dataset requirements are just the identifiers and the weight.

Shock channels:

scale

Metrics:

any input variable or income list

Population cells:

Any input variable (deh=3-4, dgn=1, dag=25-34) and/or ‘region’.

Required input columns:

idhh, idperson, dwt

Scenario params

period (optional)

External-model period label whose shocks to apply. Optional when the shock table has exactly one period.