Getting started¶
This notebook takes one shock from an external model and turns it into transformed EUROMOD input, using nothing but a local model and its own bundled training data.
It runs top to bottom against any EUROMOD model. Which methodologies it can demonstrate depends on the model’s release — the first thing we do is ask.
Install¶
pip install euromod-linking # or: pip install -e .
pip install euromod-linking[excel] # to ingest .xlsx/.xlsm/.xls model output
Python 3.11+, plus a local EUROMOD model and installation. The model is distributed
by the JRC and can be requested at
https://euromod-web.jrc.ec.europa.eu/download-euromod; point MODEL_PATH at the
folder you unpack.
This targets the JRC’s EUROMOD model for the 27 EU member states. Models built on the same engine — SOUTHMOD, SWISSMOD, and national models outside the EU — use different variable and income-list conventions and will not work here.
import os
import pandas as pd
# Point this at your own EUROMOD model folder, or set EUROMOD_MODEL_PATH.
MODEL_PATH = os.environ.get("EUROMOD_MODEL_PATH", r"C:\EUROMOD_RELEASES")
DATA_DIR = os.path.join(MODEL_PATH, "Input")
# Keep DataFrame previews compact in the rendered docs.
pd.set_option("display.max_columns", 10)
pd.set_option("display.width", 110)
1. What can this model run?¶
A methodology needs things from the model — lma_labour_alignment needs the LMA
add-on and the LMA_trans extension switch. The engine does not complain when they
are missing; it just quietly does nothing. So ask first.
from euromod import Model
from euromod_linking import compatibility_matrix
from euromod_linking.compat import model_release
model = Model(MODEL_PATH)
print("engine :", model.software_version)
print("release:", model_release(MODEL_PATH))
engine : 3.8.8
release: ('J2.54', 'folder-name')
matrix = compatibility_matrix(model, "BE")
matrix[matrix["system"] == "BE_2025"][
["methodology", "ok", "model_release", "min_model_release"]
]
| methodology | ok | model_release | min_model_release | |
|---|---|---|---|---|
| 42 | lma_labour_alignment | True | J2.54 | J2.54 |
| 43 | scale_variables | True | J2.54 |
The release is read from the folder name, which is a best-effort guess — see Model compatibility for the full detection ladder and why a missing release is never treated as a failure.
What actually decides is the capability check: whether the model exposes the add-ons and extension switches a methodology declares. Ask it about one methodology and it lists them:
from euromod_linking import check_compatibility
system = model["BE"]["BE_2025"]
report = check_compatibility(system, "lma_labour_alignment")
print(report)
for r in report.requirements:
print(f" {r.kind:10} {r.name:12} satisfied={r.satisfied}")
lma_labour_alignment on BE/BE_2025: ok
note: Could not verify the LMA_trans extension: this model's extension names could not be read.
addon LMA satisfied=True
extension LMA_trans satisfied=None
2. Load the microdata¶
The model ships training data for every country, which is enough to demonstrate the transform. A real study uses the survey microdata for the country.
data = pd.read_csv(os.path.join(DATA_DIR, "BE_training_data.txt"), sep="\t")
print(f"{len(data):,} rows, {data['dwt'].sum():,.0f} weighted people")
data[["idhh", "idperson", "dwt", "dag", "dgn", "deh", "yem"]].head()
7,482 rows, 11,322,391 weighted people
| idhh | idperson | dwt | dag | dgn | deh | yem | |
|---|---|---|---|---|---|---|---|
| 0 | 1 | 101 | 42302.102 | 40 | 0 | 3 | 0.00000 |
| 1 | 2 | 201 | 50581.000 | 40 | 0 | 3 | 0.00000 |
| 2 | 3 | 301 | 23908.301 | 40 | 0 | 3 | 178.41209 |
| 3 | 4 | 401 | 23908.301 | 40 | 0 | 3 | 713.64838 |
| 4 | 5 | 501 | 14644.400 | 40 | 0 | 3 | 1248.88500 |
3. Write a scenario¶
A scenario binds shocks to a country and system. This one raises employment income
by 3% for people with medium education — a scale shock, which selects the
scale_variables methodology.
Note what is not here: the methodology. It is resolved from the shock’s channel and echoed back, so a scenario cannot silently be handled by a different method than the one that produced an earlier result.
scenario = {
"country_code": "BE",
"system_name": "BE_2025",
"shocks": {"inline": [
{"channel": "scale", "metric": "yem", "group": "deh=3-4",
"period": "1", "op": "grow", "value": 0.03},
]},
"params": {"period": "1"},
}
4. Look before you leap¶
validate_only=True stops before the expensive work and reports what the scenario
would do: which people each cell resolves to, and what the methodology would target.
This is where you find out a shock is the wrong size, while it is still cheap to fix.
from euromod_linking import apply_scenario
plan = apply_scenario(system, data, scenario, validate_only=True)
print("methodology:", plan["methodology"])
print("add-ons :", plan["addons"], "| extensions:", plan["extensions"])
plan["diagnostics"]["cell_population"]
methodology: scale_variables
add-ons : [] | extensions: []
{'deh=3-4': {'n_rows': 5460, 'n_weighted_all_ages': 8813907.8}}
5. Apply it¶
Now the real transform. It returns the counterfactual input and, when the methodology
restructures rows, a matching baseline built on the same rows. scale_variables
adds and removes no rows, so the baseline here is the untouched data.
plan = apply_scenario(system, data, scenario)
counterfactual = plan["counterfactual"]
cell = data["deh"].between(3, 4)
moved = counterfactual["yem"] != data["yem"]
pd.DataFrame({
"rows": [cell.sum(), (~cell).sum()],
"rows whose yem changed": [(moved & cell).sum(), (moved & ~cell).sum()],
"mean yem before": [data.loc[cell, "yem"].mean(), data.loc[~cell, "yem"].mean()],
"mean yem after": [counterfactual.loc[cell, "yem"].mean(),
counterfactual.loc[~cell, "yem"].mean()],
}, index=["deh=3-4 (shocked)", "outside the cell"])
| rows | rows whose yem changed | mean yem before | mean yem after | |
|---|---|---|---|---|
| deh=3-4 (shocked) | 5460 | 2744 | 1838.102132 | 1893.245196 |
| outside the cell | 2022 | 0 | 0.000000 | 0.000000 |
Inside the cell, mean employment income is up 3%. Outside it, not a single
row changed. That is the whole contract of a scale shock.
(The model’s bundled training data is a simplified extract — every earner in it
happens to sit at deh=3 — so the second row has no earnings to move. Real survey
microdata spreads across the education range.)
6. Run both halves¶
run_scenario does everything above and then runs both simulations through the
engine, so the difference between them isolates the shock:
from euromod_linking import run_scenario
out = run_scenario(system, scenario, input_path=DATA_DIR)
out["baseline_output"], out["counterfactual_output"]
It is not executed here because a full simulation takes minutes and its output is large. Everything up to this point is pure pandas.
Where to go next¶
The shock table — the one format every external model is normalised into
Population cells — how
groupselects peopleScenario documents — the constants distinction that is easy to get wrong
Adapters — what each methodology does and how to use it
Examples — ingesting a model output file, income lists, and what an unsupported methodology looks like