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 group selects people

  • Scenario 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