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 variable — yem, 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 |
|---|---|---|
|
Employment income |
Scales the recorded input variables |
|
Self-employment income |
Scales the recorded input variables |
|
Investment income |
Scales the recorded input variables |
|
Property income |
Scales the recorded input variables |
|
Private pension income |
Scales the recorded input variables |
|
Private transfers received |
Scales the recorded input variables |
|
Other income |
Scales the recorded input variables |
|
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 |
|---|---|---|
|
Unemployment benefits |
Mostly recomputed — see below |
|
Social assistance |
Mostly recomputed — see below |
|
Housing benefits |
Mostly recomputed — see below |
|
Health-related benefits |
Mostly recomputed — see below |
|
Education benefits |
Mostly recomputed — see below |
|
Family benefits |
Mostly recomputed — see below |
|
Old-age pensions |
Mostly recomputed — see below |
|
Survivors’ benefits |
Mostly recomputed — see below |
|
Disability benefits |
Mostly recomputed — see below |
|
Income tax and social insurance contributions |
Mostly recomputed — see below |
|
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:
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.A partial list warns.
ils_udb_bunresolves tobun,bun_s,byrand 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.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.
multandgrow— proportionalCommute with each other. Both multiply, so
x × m × (1+g)is the same either way round.add— additiveCommutes with
add. Does not commute withmult/grow:(x + a) × mis notx × m + a.set— absoluteCommutes 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.