9  Usage guide

This chapter covers how to prepare the inputs, run the model, find the results and check one run against another. The model is controlled through 00_config.R and the files in user_inputs/, so different services and scenarios can be run without editing the engine scripts.

9.1 Running everything in order

00_setup_and_run.R, at the top of the repository, runs the whole workflow in order and then checks that nothing has been missed. Set RUN_PRESET at the top and run it from the repository root, in RStudio with source("00_setup_and_run.R", echo = TRUE) or from a terminal with Rscript 00_setup_and_run.R. Nothing in 00_config.R needs changing: each step runs in its own R session and gets its area, scenarios and run mode on the command line, and the first step to fail stops the run with its name.

RUN_PRESET When Steps
full_vm A full run on the virtual machine packages; the data preparation scripts that need running; the three calibrations; England with every scenario; every ICB and Trust; the report inputs and results; the check
prepared_inputs Given prepared inputs, away from the virtual machine as full_vm, without the data preparation and report inputs
after_pathway_change After changing the basecase pathway, quit rates or effects of quitting the three calibrations, every run, the report results, the check
after_other_change After changing costs, another scenario, a coverage panel or the report code every run, the report results, the check
report_only After changing only figures, tables or report text the report results, the check
check_only To see what is done and what isn’t the check

The steps run in this order, each after the ones before it:

Step What it does What it makes
packages renv::restore() the R packages at the versions in renv.lock
prepare_inputs the data preparation scripts (below) data/real/derived/ and data/base/derived/
calibrate_pathway, calibrate_history, calibrate_follow_up the three calibrations, in that order (“Calibrating the model” below) values in scenarios_control_panel.csv and macro_trends_panel.csv; output/calibration_record.csv
england main.R for England with every scenario, then its scenario comparison output/national/england/; files in output/for_report/
areas run_batch.R: every ICB and every Trust, each with AREA_SCENARIOS (00_config.R), its comparison, briefing and slides output/icb/, output/trust/; files in output/for_report/; output/batch_logs/
(after england or areas) the England briefing and slides, which compare the ICBs, so they are built after every ICB output/for_report/briefings/, output/for_report/slides/
report_inputs the report’s chapters on hospital patients and smoking, and the appendix of conditions report_ch1.rds, report_smoking.rds, reports/_tbl_ch1_conditions.qmd
report_results the comparison with Ottawa, the results file with disclosure control, every figure and table output/for_report/
check check_run_complete.R (below) output/run_status.csv
render quarto render reports/report.qmd, after output checking; not in any preset reports/report.docx, .html

With PREPARE_MODE “as_needed” (the default), the data preparation runs only the scripts whose outputs are missing or older than the outputs of the scripts they follow, and every script that follows one being run. For example, running the HES script again also runs the catchment weights, the smoking base grids and the deaths by cause, which read its outputs. Which script makes what, and which it follows, is listed in PREPARE_STEPS in R/run_records.R. A change to a data preparation script itself is not detected: after changing one, run it, or set PREPARE_MODE to “all”.

The areas step runs 36 ICBs and 129 Trusts, each with seven scenarios, so it takes some hours. It carries on past an area that fails and lists every failure at the end, with the logs in output/batch_logs/; run the step again after fixing the cause (to run only some areas, list them in BATCH_RUNS in run_batch.R).

9.1.1 Checking that everything is complete

check_run_complete.R is the last step of every preset, and can be run on its own at any time (Rscript check_run_complete.R, or RUN_PRESET <- "check_only"). It checks each stage in turn, says for each check OK, MISSING, OUT OF DATE or NOTE, and names the steps to run:

  1. Data preparation. Every output of every script exists and was made after the outputs it is made from (prepare_plan()). Files in data/real/derived/ that no script makes, left from earlier versions, are listed as a NOTE.
  2. Calibrations. All three have been run, in order, and no input or code file has changed since. Each calibration records the checksum of every input and code file in output/calibration_record.csv. A later change to the control panel or the macro trends panel is a NOTE: it needs the calibrations again only if it changes the basecase pathway, quit rates or effects of quitting.
  3. England. The latest run included every scenario in the control panel, every scenario has results, no input or code file has changed since the run (from its run_record.csv), and the scenario comparison, briefing and slides were made after it.
  4. ICBs and Trusts. The same for every ICB and every Trust in geo_areas.csv, with AREA_SCENARIOS.
  5. Report. The report inputs are newer than the hospital and smoking inputs they are made from, the results file, figures and tables are newer than the latest England and ICB runs, and the disclosure log exists. A report not rendered since the tables were built is a NOTE.

Every check, with what to run, is written to output/run_status.csv. The check stops with an error, listing the steps to run in order, if anything is MISSING or OUT OF DATE, so a run of 00_setup_and_run.R that ends with “Everything is complete and up to date” has done everything.

9.2 Data and where it can be processed

The Hospital Episode Statistics (HES) extract is held under data sharing agreement NIC-366216 and may only be processed on the heta_study virtual machine. Every file made from it, and every other file excluded on information governance grounds, lives under data/real/ and is never committed. Non-sensitive inputs live under data/base/. There is no synthetic or dummy version of the HES data: the model runs only where the real inputs are.

9.3 Preparing the inputs

The data preparation scripts in scripts/01_data_prep_real/ are run once, in this order, and again only when a source changes:

Order Script Makes
1 00_construct_lags.R monthly recovery curves after quitting (data/base/derived/monthly_lags.csv)
2 01_process_hes_data.R the hospital inputs for England, every ICB and every trust (data/real/derived/hes_inputs/), and, for England, the patients with each condition and those who died in their first admission (hes_condition_patients.csv, hes_index_deaths.csv), and the IHD patients by admission type, for the IHD smoking target (hes_ihd_admission_type.csv)
3 02_process_population.R population by age, sex and IMD quintile
4 03a_collapse_synthetic_population.R, 03b_build_catchment_weights.R, 03c_build_smoking_base_grids.R smoking prevalence in each hospital’s catchment, before calibration (data/real/derived/smoking_inputs/)
5 04_process_mortality.R annual mortality rates (data/real/derived/ons_mortality_rates.csv)
6 05_build_lifetime_values.R the life years, QALYs and NHS savings of each person who quits for life, by age band and sex, central and lower (data/base/derived/lifetime_values_per_quitter.csv), from user_inputs/lifetime_benefits_panel.csv; it runs again whenever the panel, its sources or the control panel change
7 07_prep_relative_risks.R relative risks by condition, age band and sex
8 08_process_pld_referrals.R referrals to the service by month and year from NHS England’s patient-level data, and the calibration targets (data/real/derived/pld_referrals_*.csv, pld_referral_targets.csv)
9 09_process_mff.R (for the MFF option and the report’s illustration of it) the market forces factor payment index for every trust in trusts_in_scope.csv (data/real/derived/mff_by_trust.csv), from NHS England’s allocations spreadsheet in data/real/raw/; see “Local costs: the market forces factor”
10 10_process_cause_deaths.R (after 2 and 5) each condition’s deaths a year per patient who has it, and the death rate from other causes (data/real/derived/condition_excess_mortality.csv, other_cause_mortality.csv), from the ONS deaths by cause file in data/real/raw/ (chapter 5, “Deaths after discharge”)

main.R checks every input as it reads it (R/check_inputs.R). If a file is missing, or a column the engine needs is missing, blank or out of range, the run stops and the message names the file, the problem and the script that makes it.

9.4 Running the model with prepared inputs

Most users will run the model with inputs prepared on the virtual machine and passed to them after output checking, without access to the HES or patient level data themselves. They skip the data preparation and the calibration, and need, in the same folders as on the virtual machine:

Files What they hold
data/real/derived/hes_inputs/<level>/ (admissions_stratified.csv, outcome_multipliers.csv, input_comorbidities.csv, geo_areas.csv) for national and each level they will run patients, admissions, comorbidities, unit costs and lengths of stay by area, condition, age group, sex and IMD quintile
data/real/derived/smoking_inputs/<level>/smoking_base_grid.csv, for national and each level smoking prevalence in each hospital’s catchment before calibration
data/real/derived/hes_ihd_admission_type.csv patients first admitted for ischaemic heart disease, elective and non-elective, which weight the IHD smoking target (chapter 3)
data/real/derived/condition_excess_mortality.csv and other_cause_mortality.csv death rates after discharge, by condition and from other causes (chapter 5, “Deaths after discharge”)
data/real/derived/pld_referrals_annual.csv and pld_referral_targets.csv NHS England’s referrals by year, shown against the model’s in Tab_X and Tab_X2, and the targets for the coverage calibration. Made from NHS England’s extract 67_017_tobacco_referrals_20260617.xlsx, put in data/real/raw/, by scripts/01_data_prep_real/08_process_pld_referrals.R (the prepare_referrals step of 00_setup_and_run.R), which needs nothing else
user_inputs/ and data/base/ as in the repository, with the calibrated coverage and follow-up admissions factors already written into macro_trends_panel.csv and scenarios_control_panel.csv

main.R checks every one of these as it reads it and names any that is missing. With them in place, set the area and scenarios and run main.R, or list areas in run_batch.R; then build the briefings, slides and report as below. Changing a scenario’s parameters, coverage rules or costs needs no recalibration. Recalibrating (after a change to the clinical pathway, the quit rates or recovery after quitting) needs only the prepared inputs and the referrals extract, so it can be done anywhere; a change to the inputs themselves needs the HES data and is done on the virtual machine. Changing a coverage rule constant needs the coverage calibration (calibrate_history) before the run.

9.5 Changing an assumption

To change Where Then
What the service adds over ward care (each step’s value with and without the service) the treatment and control rows of user_inputs/scenarios_control_panel.csv (chapter 4, “What the service adds”) as for pathway probabilities, below
Pathway probabilities and quit rates user_inputs/scenarios_control_panel.csv, one column per scenario for the basecase, the three calibrations then main.R; for another scenario, main.R
NRT, staffing, salaries, unit costs, cash-releasing share user_inputs/scenarios_control_panel.csv, in 2024/25 prices (chapter 6, “Prices”) run main.R
The discount rate discount_rate in the control panel, the same in every scenario 05_build_lifetime_values.R, then main.R
The lifetime value of quitting (Tier 2): life years by age at quitting, when they arrive, the share who would quit later anyway, QALYs per life year, central and lower user_inputs/lifetime_benefits_panel.csv (chapter 6, “Changing the lifetime assumptions”) 05_build_lifetime_values.R, then main.R
The price year MODEL_PRICE_YEAR in 00_config.R, with the year’s index in data/base/raw/nhs_cost_inflation_index.csv and the control panel’s money values in that year’s prices 05_build_lifetime_values.R, then main.R
Differences by age group and IMD quintile user_inputs/equity_multipliers_panel.csv run main.R
Coverage rules for the scenarios (full ambition, disinvestment), and the MSF path for quit attempts COVERAGE_* and MSF_METRIC_POINTS in 00_config.R the history calibration (MAIN_RUN_MODE <- "calibrate_history"), then main.R
An area’s own coverage user_inputs/area_coverage_panel.csv run main.R for the area
Admissions growth, prevalence trend, relapse user_inputs/macro_trends_panel.csv run main.R
Follow-up admissions follow_up_admissions_factor_* in the control panel, set by the follow-up calibration the follow-up calibration, then main.R
The effect of quitting on a condition, and the evidence for it user_inputs/post_diagnosis_effects.csv (chapter 5, “Effects of quitting in people who already have a disease”); effect_borrowing in the control panel the follow-up calibration, then main.R
Readmissions per smoker without the service that k1 and k2 are calibrated to user_inputs/readmission_targets.csv (today the Ottawa control group) the follow-up calibration, then main.R
When deaths from a condition happen after discharge, and the most that can die of it in year 1 user_inputs/excess_death_timing.csv (chapter 5, “Deaths after discharge”) the follow-up calibration, then main.R
The ONS deaths by cause a new file in data/real/raw/, with FILE_CAUSE_DEATHS_RAW in 00_config.R pointed at it 10_process_cause_deaths.R on the virtual machine, the follow-up calibration, then main.R
Thresholds and cash-releasing shares shown in the report REPORT_WTP_THRESHOLDS, REPORT_CASHABLE_STEP in 00_config.R 01_build_report_results.R
Disclosure control SDC_* in 00_config.R every step that writes to output/for_report/

Every parameter in the control panel has a definition and a source in its last two columns. A change to the basecase should keep these up to date, and a new scenario needs a column in both panels and a coverage rule (see “Adding a scenario” below).

9.6 Settings for a run

These are set in 00_config.R:

Setting What it does
GEO_LEVEL "national", "icb" or "trust"
ACTIVE_REGION the area within that level: "england", an ICB name such as "SOUTH YORKSHIRE", or a trust code such as "RHQ". The areas available are listed in geo_areas.csv in each level’s folder of data/real/derived/hes_inputs/
SCENARIOS_TO_RUN "all" for every scenario in scenarios_control_panel.csv, or a list of names such as c("Basecase"). A name that isn’t in the panel stops the run
MAIN_RUN_MODE "full" to run the scenarios; "calibrate_pathway", "calibrate_history" or "calibrate_follow_up" to calibrate (see “Calibrating the model” below)
MFF_MODE "off" (national costs), "relative" or "payment" (see “Local costs: the market forces factor” below)
SMOKING_HEADLINE_SOURCE and the other settings in section 7c the smoking calibration (see chapter 3)

9.7 Running the model

From the repository root:

Code
source("main.R", echo = TRUE)

or Rscript main.R from a terminal. Don’t run it by sending lines to the console: if anything asks a question part way through, the console takes the lines that follow as the answer.

Each scenario takes a few minutes. The time taken by each stage is printed and saved in the run record.

9.8 What a run produces

Results go in one folder per area and scenario, so runs never overwrite each other:

File Contents
output/<level>/<area>/<scenario>/results.rds every result table, as R objects. The figures, tables, slides and briefings are built from this
output/<level>/<area>/<scenario>/<scenario>_HybridReport.xlsx the same tables, one sheet each, for reading
output/<level>/<area>/<scenario>/<scenario>_VFM_Subgroups.csv subgroup economics
output/<level>/<area>/<scenario>/run_check.csv the headline metrics, rounded to 3 significant figures, for comparing runs
output/<level>/<area>/run_record.csv when the run happened, its settings, the time each stage took, and a checksum of every input and code file
output/calibration_record.csv when each calibration finished, with a checksum of every input and code file at that moment
output/run_status.csv the result of check_run_complete.R: every check, its status and what to run

The calibrated smoking prevalence and the calibration factors are also written, each run, to data/real/derived/smoking_inputs/<level>/.

Everything under output/ stays on the virtual machine unless it has been through output checking. Results for ICBs and trusts rest on smaller numbers of patients than national results and need disclosure control before they leave.

9.9 Calibrating the model

Three things are calibrated, in this order (with the run itself as a fourth step), on a national run (GEO_LEVEL “national”, ACTIVE_REGION “england”). Each is a run of main.R with MAIN_RUN_MODE set in 00_config.R. The calibration modes prepare the same inputs as a full run, write their results to user_inputs/ and save no model results. Area runs use the national values.

  1. Quit success, to the national service data. Set MAIN_RUN_MODE <- "calibrate_pathway" and run main.R. R/calibrate_pathway.R sets quit_rate_scale so that supported quit attempts succeed as often as they did nationally in 2024 (20.2%, Agrawal et al.), and checks the share seen (60.2%) and supported attempts per identified smoker (22.3%) against the same data (chapter 4). It runs the pathway once and prints both.
  2. Coverage, to the referral data. Set MAIN_RUN_MODE <- "calibrate_history" and run main.R. R/calibrate_history.R solves within-trust coverage for 2023, 2024 and 2025 so the model’s referrals match NHS England’s patient-level data (chapter 7), then sets coverage for every year and scenario from the rules in 00_config.R (COVERAGE_RULE_BY_SCENARIO; see “The national scenarios” below) and writes both coverage columns of macro_trends_panel.csv. It runs the pathway for the historical years only, a few times, and prints the fit.
  3. Follow-up admissions, to the readmission targets. Set MAIN_RUN_MODE <- "calibrate_follow_up" and run main.R. R/calibrate_follow_up.R sets the follow-up admissions factors \(k_1\) and \(k_2\) so that readmissions without the service match the targets in readmission_targets.csv, today Ottawa’s control group (chapter 7), for the base case and for each scenario in FOLLOW_UP_OWN_FACTOR_SCENARIOS, and writes them into scenarios_control_panel.csv. Scenarios in FOLLOW_UP_NO_DECLINE_SCENARIOS get \(k_2\) equal to \(k_1\). It runs the 2026 cohort once for each of those scenarios and prints the factors.
  4. Run. Set MAIN_RUN_MODE <- "full" and run main.R. Tab_X shows referrals against the targets and Tab_14b the model’s and Ottawa’s readmission rates (equal in years 1 and 2). Two checks follow, with nothing calibrated to them: Tab_14d the model’s deaths after discharge in each arm next to Ottawa’s, and Tab_14e the model’s quitters next to Ottawa’s and London’s, for admissions and deaths, with whether each lies inside the study’s interval.

Repeat steps 1 to 3 after any change to the HES inputs, smoking calibration, relative risks, recovery curves, effects of quitting, quit rates or clinical pathway parameters. Each step depends on the ones before it through the quitters carried forward. 00_setup_and_run.R runs all three in order. scripts/04_reports/main_report/build_ottawa_report_inputs.R then writes the comparison with Ottawa and the sensitivity analyses for the report to output/for_report/report_ottawa.rds, with disclosure control applied.

9.10 Checking one run against another

model_development/compare_runs.R compares two result workbooks sheet by sheet and cell by cell. Set the two paths at the top of the script and source it. It prints, for each sheet, whether it is the same, the number of cells that differ and the largest difference, and writes the same table to output/compare_runs.csv. It reports counts and differences only, never values.

It is used in two ways. After a change that shouldn’t move any result, every sheet should be the same. After a deliberate change, it shows which tabs moved and by how much. The two run records show whether any input or code file differed between the runs.

9.11 The national scenarios and comparing them

Four scenarios tell the national story, following the ambitions of the CVD modernising services framework (MSF). Each is the same as the basecase from the index year (2019) to 2025 and differs only from the policy effect year (2026) to the time horizon (2045).

Scenarios diverge at the policy effect year. follows_basecase_before_policy_year in the control panel says, for each scenario, whether it uses the basecase’s parameters before the policy effect year. It is 1 for the service scenarios (the roll-out, improvement and minimum viable service scenarios), so they differ from the basecase only from 2026, and 0 for the sensitivity analyses, which change an assumption shared by every scenario (effects of quitting, readmissions, deaths, usual care, background quitting, prevalence, costs) and so apply it from the index year. For a scenario set to 1, main.R gives the engine the basecase’s clinical and quit success parameters and the yearly cost parameters (COST_PARAMS_BY_YEAR in 03_econ_engine.R) for the years before 2026. Every other global parameter applies to the whole run, so the run stops if such a scenario changes one (the MSF scenario’s quit_attempt_follows_msf acts only from 2026 and is allowed), or if its macro trends before 2026 differ from the basecase’s. Three differ only in coverage, set by rule from the current level, which is NHS England’s 92% of trusts with a service (COVERAGE_TRUST_CURRENT) and the calibrated 2025 within-trust coverage. The rules and their constants are in 00_config.R and R/coverage_paths.R.

Scenario Rule What changes from 2026
Hold_Constant (the reference) hold coverage held at the current level
Basecase (full ambition) full_ambition trust coverage rises to 95% (COVERAGE_TRUST_MAX) and within-trust coverage to 100% in equal steps to 2030, so 95% of smokers are reached from 2030
MSF_Planned_Increase full_ambition coverage as the base case, and quit attempts among patients seen rise in proportion to the MSF metric (below)
Disinvest disinvest reach falls to half the current level in equal steps to 2028, then held: about 10% of the current trusts stop their service each year (COVERAGE_DISINVEST_TRUST_LOSS) and the rest of the fall is within trusts

The MSF metric is supported quit attempts per inpatient who smokes: 22% now, 44% by 2029 and 54% by 2036 (MSF_METRIC_POINTS, straight lines between, then held). Its 22% is the national figure in the patient level data (22.3% in 2024), which counts the smokers each service records, so the metric is per smoker the service reaches and more coverage does not move it. In MSF_Planned_Increase (quit_attempt_follows_msf = 1 in the control panel) the national average of pr_quit_attempt_seen rises in proportion to the metric, keeping the differences by age, sex and IMD quintile on the odds scale (msf_attempt_params()), so supported attempts per smoker reached follow the ambition. Nothing else changes: assessments and the staff they need are as in the base case, so the scenario shows what meeting the ambition would deliver, not what it would cost to meet it.

The other scenarios use the full ambition rule. The single-lever improvement scenarios (MVS, SCREENING_BEST, ASSESSMENT_BEST, REFERRAL_BEST, PHARMA_BEST, BEST_ALL) change the clinical pathway. The sensitivity analyses are the range, Sens_Effects_Lower (lower: no condition borrows the effect of its nearest evidence) and Sens_FollowUp_NoDecline (higher: readmissions do not fall after the first year), with Sens_Effects_Upper (every capped condition borrows) beside them (chapter 5), nine on costs, Sens_Productivity_Low, Sens_Productivity_High, Sens_NRT_Low, Sens_NRT_High, Sens_Overhead_Low, Sens_Overhead_High, Sens_Consultant_Low, Sens_Consultant_High and Sens_Advisor_Band3 (chapter 6), and two on the comparator, Sens_Screening_Equal (usual care screens as many patients as the service) and Sens_Screening_Advice_Equal (and gives as much very brief advice) (chapter 4), two on background quitting, Sens_Background_Low (1% at 4 weeks) and Sens_Background_High (4.8%, which is 2% at 12 months) (chapter 4), two on the smoking prevalence trend from 2026, Sens_Prevalence_Static (no fall) and Sens_Prevalence_Faster (2.8% a year), set in macro_trends_panel.csv, four on the evidence for the effects of quitting, Sens_Evidence_Weaker and Sens_Evidence_Stronger (every study at its confidence limit nearer and further from no effect) and Sens_Stroke_Weaker and Sens_Stroke_Stronger (the stroke study alone), and one on death rates after discharge, Sens_Death_Rates_Lower (deaths from each condition halved) (chapter 5, “Sensitivity analyses of the effects of quitting”). Run them with Basecase for England; the report’s sensitivity table is built from them. To add a sensitivity analysis, add a column to scenarios_control_panel.csv and equity_multipliers_panel.csv, its rows to macro_trends_panel.csv (copy Basecase), its coverage rule to COVERAGE_RULE_BY_SCENARIO in 00_config.R, and its name to the list in scripts/04_reports/main_report/build_results_for_report.R.

9.12 Area briefings

scripts/04_reports/briefings/build_area_outputs.R makes, for the area in 00_config.R (or given with --level= and --area=), a Word briefing (about five pages for England, about four for an ICB or a Trust) and a slide deck in the University template (reports/assets/uos_slide_template.pptx). It first collects the area’s results with disclosure control applied (R/report_results.R) into output/for_report/results_<level>_<area>.rds, and builds both documents from that alone, so every number in them is already rounded; they go to output/for_report/briefings/ and output/for_report/slides/ for output checking. It needs Basecase, the lower and higher estimates (SCENARIO_RANGE_LOW, SCENARIO_RANGE_HIGH), the roll-out scenarios and the improvement scenario in AREA_BRIEFING_IMPROVEMENT (BEST_ALL) run for the area. What each leads on depends on the audience. For England (NHS England and DHSC, about five pages and 16 slides): one year of the service; value for money from the Trusts’ side (Tier 1) and the health system’s (Tier 2), judged against NICE’s threshold, with the figure of how much of the lifetime benefit must be realised; whether the value is cash for Trusts, ICBs and the NHS as a whole; the roll-out choices and the cash flow under each; deaths under 75; the extra quits and admissions avoided by IMD quintile; the lowest, median and highest ICB on four measures beside England; support after discharge; the pathway rates; and what is not counted (build_national_briefing(), tbl_national_perspectives() and tbl_national_icb_range() in tbl_system.R). The England briefing reads every ICB’s results file (output/for_report/results_icb_<code>.rds), so it is built after the ICBs and stops if one is missing. For an ICB, the local system (the ICB and local authorities, about four pages and 14 slides, with no table by Trust): one year of the service with its cost by who pays it (Trusts, specialist support after discharge, ward staff time), the admissions and bed days avoided in two and five years, the deaths avoided (all and under 75) and the share of admissions avoided for heart disease and stroke, the extra quits and admissions avoided by IMD quintile (Core20PLUS5), lifetime savings and QALYs, the roll-out from 2026 to 2030 year by year and under each roll-out scenario, support after discharge, the result at best practice, the pathway rates the results rest on, and what is not counted (build_system_briefing(), scripts/04_reports/tables/tbl_system.R). A Trust’s briefing and slides keep to the Trust’s own budget (Tier 1): what the service costs the Trust and the staff it needs, what it does, the admissions and bed days avoided in two and five years (net of the admissions of patients alive because they quit, with both parts shown), the deaths avoided within two and five years, the value of those admissions against the cost and the share that would need to release cash to cover it, the cost per unit counted three ways (per quit with specialist support after discharge, per quit with the service, per extra quit), the result with every stage at best practice (the best published results for assessment and registration, the targets for screening and NRT), the patients treated from 2026 to 2030 as the service grows and what they avoid by the end of 2030, the model’s pathway rates for the Trust, and what the results do not count, including specialist support after discharge, which may be a cost to the Trust but isn’t included (R/report_results.R report_trust_view_for(), scripts/04_reports/tables/tbl_trust.R). The key messages, figures and headings are set in the script (national_key_messages(), system_key_messages(), trust_key_messages(), area_figures()). The notes say whether the area’s own coverage was used (see “Coverage for an ICB or trust”).

After running the scenarios for an area, scripts/04_reports/compare_scenarios.R writes output/<level>/<area>/scenario_comparison.xlsx, unrounded, for use on the virtual machine, and the same tables with disclosure control applied to output/for_report/comparison_<level>_<area>.rds. It shows, for the forward programme from 2026, each scenario’s activity, admissions avoided (over 3 and 10 years and in total, all conditions and cardiovascular, kidney and metabolic conditions), deaths avoided within five years of treatment (all ages and under 75), trust view, system view and supported quit attempt rates, then each scenario’s difference from Hold_Constant, extra admissions avoided per 1,000 extra supported attempts, and the cost per extra QALY against the reference. The “Annual budget” sheet gives each scenario’s cost to trusts in each year from 2026, in today’s prices and not discounted, with reach and specialist assessments: the yearly budget at current reach (Hold_Constant), at the MSF level and at full ambition (Basecase from 2030).

Supported quit attempts are quit attempts by patients seen by a tobacco dependence advisor (Agrawal et al.’s outcome 1). They are reported two ways, by year, in Tab_MSF_Supported_Attempts: among identified smokers the service reaches, which is comparable with the patient level data (22.3% in 2024, which only cover patients the service records) and is the MSF metric, and among all identified smokers in the area’s hospitals, which rises with coverage.

9.13 Running a batch of areas

run_batch.R runs main.R for a list of areas, one after another, then the scenario comparison and the briefing for each. Set the areas in BATCH_RUNS (level and area code, one row per area; all_areas("icb") gives every ICB) and the scenarios in BATCH_SCENARIOS at the top of the script, then run it with source("run_batch.R", echo = TRUE). Each step runs in its own R session with Rscript, because main.R clears the workspace when it starts, and writes a log to output/batch_logs/. An area that fails does not stop the others; the batch stops with an error at the end that lists every failure, and batch_summary.csv shows what ran.

The batch passes the area and scenarios to main.R on the command line, which replaces GEO_LEVEL, ACTIVE_REGION and SCENARIOS_TO_RUN in 00_config.R. One area can be run the same way from a terminal:

Rscript main.R --level=icb "--area=SOUTH YORKSHIRE" --scenarios=Basecase,Hold_Constant

Anything else on the command line stops the run. Calibration is not run in a batch: calibrate first, on England, with MAIN_RUN_MODE set in 00_config.R.

9.14 Local costs: the market forces factor

9.14.1 What the market forces factor is

It costs more to provide the same NHS care in some places than in others, mainly because of differences in pay, land and buildings. NHS England measures these unavoidable differences with the market forces factor (MFF), one value for each NHS trust. It comes in two versions that differ only in scale:

  • the payment index, which the NHS Payment Scheme uses: each trust is paid its national prices multiplied by its payment index. The trust with the lowest costs has an index of 1 and the highest are around 1.2, so every trust’s index is 1 or more;
  • the underlying index, used in ICB allocations, which has the same differences between trusts but averages 1 across England.

The model’s costs are national: admissions avoided are costed at national Payment Scheme prices and staff at national pay rates (PSSRU). The MFF option lets you cost an ICB or trust at its local costs instead.

9.14.2 What the model does with it

An area’s MFF is the average of its trusts’ payment indices, weighted by their admissions (hes_inputs/trust/geo_areas.csv). England’s MFF is the same average over every trust. MFF_MODE in 00_config.R then says how it is used:

MFF_MODE Staff pay and ward staff time Unit costs of admissions avoided Use it for
"off" (default) national national England, and any comparison between areas on the same footing
"relative" x area MFF / England MFF x area MFF / England MFF a Trust’s or ICB’s business case: its budget and savings in local terms
"payment" x area MFF / England MFF x area MFF valuing avoided admissions at what the trust is actually paid for them

With "relative", the area’s costs are scaled by how much more or less expensive it is than England as a whole. This works like the underlying index, with England’s average (over the admissions in the model) set to 1, so England’s own results don’t change. An ICB whose MFF is 4.5% above England’s has its staff pay and the cost of the admissions it avoids 4.5% above the national values.

With "payment", admissions avoided are valued at the trust’s own price, national price x payment index, as the trust is paid. Because every payment index is 1 or more, this raises the savings everywhere, including England’s (by England’s average index). Pay is still scaled relative to England, because PSSRU pay is already a national average.

Some values are never adjusted: NRT (a national price), specialist support after discharge, lifetime NHS savings and QALYs. Numbers of patients, quits, admissions avoided and bed days don’t change either: the MFF changes what things cost, not what happens.

9.14.3 A worked example

These numbers are made up to show the arithmetic. An ICB has two trusts: trust A has a payment index of 1.15 and 30,000 admissions, trust B 1.09 and 70,000. The ICB’s MFF is (30,000 x 1.15 + 70,000 x 1.09) / 100,000 = 1.108. If England’s MFF is 1.060, the relative factor is 1.108 / 1.060 = 1.045.

At national costs, one year of the service costs the ICB’s trusts GBP 2.0m, of which GBP 0.3m is NRT, and saves GBP 1.5m in avoided admissions over five years.

  • "relative": cost = (2.0 - 0.3) x 1.045 + 0.3 = GBP 2.08m; savings = 1.5 x 1.045 = GBP 1.57m. Savings per pound go from 0.75 to 0.76.
  • "payment": cost as for "relative", GBP 2.08m; savings = 1.5 x 1.108 = GBP 1.66m.

So local costs change the size of the budget and the savings more than the balance between them. Savings per pound rise slightly in high-cost areas because NRT, which costs the same everywhere, is a smaller part of the cost.

9.14.4 Running it, step by step

  1. The MFF values. The model reads NHS England’s allocations spreadsheet “H - Market forces factor (MFF) 2026/27 to 2028/29”, version 1, saved as data/real/raw/h-market-forces-factor-2627-to-2829-version-1.xlsx (from www.england.nhs.uk/allocations/). It uses the provider_mff sheet, which has a row for each trust: its code (Trust), name and Payment MFF index. If NHS England publishes a newer version, save it in the same folder and change FILE_MFF_RAW (and, if the layout has changed, SHEET_MFF_RAW, MFF_RAW_HEADER_ROW, MFF_RAW_CODE_COL and MFF_RAW_VALUE_COL) in 00_config.R, and MFF_SOURCE_LABEL to say what it is.
  2. Prepare them. Run Rscript scripts/01_data_prep_real/09_process_mff.R (it is also part of the prepare_inputs step of 00_setup_and_run.R). It writes data/real/derived/mff_by_trust.csv, one payment index for each trust in trusts_in_scope.csv, and prints the range of values. If a trust in scope isn’t in the spreadsheet, usually because it has merged, the script stops and names it. Add a row for it to data/real/raw/mff_code_map.csv (columns trust_code, mff_code, note), giving the code to use instead, for example the trust it merged into, and run the script again.
  3. Choose the mode. Set MFF_MODE <- "relative" (or "payment") in 00_config.R. For a single run you can give it on the command line instead: Rscript main.R --level=icb "--area=SOUTH YORKSHIRE" --mff=relative. run_batch.R uses the setting in 00_config.R, so set it there for a batch, and run every ICB with the same mode.
  4. Run the areas as usual. Calibration always runs at national costs: 00_setup_and_run.R passes --mff=off to the calibration steps, and main.R stops if a calibration mode is run with the MFF on.
  5. Set it back to "off" before running England for the report, unless you mean England’s savings to be at payment index prices.

Runs with the MFF on write to the same folders as runs without it, so a run replaces the previous run for that area and scenario. Keep a copy of the earlier results if you want both.

9.14.5 How to tell what a run used

  • The console prints the area’s MFF, England’s and the two factors applied, for example MFF (relative): SOUTH YORKSHIRE MFF 1.0234, England 1.0612; pay x 0.9644, unit costs of admissions x 0.9644.
  • Tab_Area_MFF in the results (and the Excel workbook) records the mode, both MFFs, the two factors, the number of trusts and the source.
  • The briefing’s notes say whether local costs were used, and the report’s ICB table adds a column with each ICB’s factor when they were.

9.14.6 The illustration in the report

The report’s chapter on Trusts and ICBs shows each ICB’s MFF relative to England’s (fig_mff) and one year of the service for each region at national and at local costs (tbl_mff). build_results_for_report.R works these out from the ICB results and mff_by_trust.csv, whatever mode the ICBs were run with: it takes the run’s own factors off to get national costs, then puts the relative factor on. This is exact, because the factors multiply only pay (all of the cost to Trusts except NRT) and the unit costs of admissions (all of the savings from admissions avoided). It needs 09_process_mff.R to have been run and every ICB to have Basecase results; otherwise the report says it is not yet available.

9.15 Disclosure control

Everything the model produces is built from HES, so every output is treated as disclosive, modelled or not, and only output/for_report/ leaves the virtual machine, after output checking. R/disclosure_control.R holds the rules, from the HES Analysis Guide: counts of 1 to 7 are suppressed and other counts rounded to the nearest 5 (SDC_SUPPRESS_MAX, SDC_ROUND_TO); rates and means need at least 10 records behind them (SDC_MIN_N_FOR_MEAN); money is rounded to the nearest 1,000 GBP (SDC_MONEY_ROUND_TO). A modelled value counts as 1 to 7 if it rounds to one. QALYs and life-years are treated as counts.

Every table written to output/for_report/ goes through sdc_table() or sdc_metric_rows(), which stop if any column or metric has no rule, so a new output can’t leave without one. Each file written or copied there adds a line to output/for_report/disclosure_log.csv: when, what, how many cells were suppressed, the rules and the results it came from, with blank checked_by and checked_on columns for the output checker. No secondary suppression is applied, because every count is rounded to 5; the checker looks for totals that would let a suppressed cell be worked out.

9.16 Building the report

The report (reports/report.qmd) reads only files in output/for_report/, so it can be rendered away from the virtual machine once they have been checked. They are made in two steps:

  1. scripts/04_reports/main_report/00_build_report_inputs.R, after the data preparation: the chapters on hospital patients and smoking, and the appendix of conditions.
  2. scripts/04_reports/main_report/01_build_report_results.R, after main.R has been run for England with every scenario (and run_batch.R for every ICB with Basecase, for the ICB chapter): the scenario comparison, the comparison with Ottawa, the results file (build_results_for_report.R, which applies disclosure control), then every figure (scripts/04_reports/figures/make_figures.R) and table (scripts/04_reports/tables/make_tables.R) from it.

Each figure is a function in its own fig_*.R file in scripts/04_reports/figures/, drawn in the house style in figure_style.R (colours for each condition group, fonts, fixed sizes) and saved as PNG and SVG. Each table is a function in a tbl_*.R file in scripts/04_reports/tables/, formatted as text with the shared number formats in table_style.R; suppressed values show as *. To change a figure or table, change its function and rerun make_figures.R or make_tables.R; to add one, write a new function and add it to the list in the wrapper.

“One year of the service” in the report and briefings is the patients treated in 2026: that year’s cost to Trusts and the admissions they avoid in the five years after treatment (Tab_20_Cohort_Condition_Matrix and Tab_21_Cohort_Pathway in the results). The forward programme is the service from 2026 to 2045.

Set require_results <- TRUE (and require_ch1, require_smk) at the top of the report for the final render, so that a missing file stops it rather than showing “not yet available”.

Before a final render:

  1. Run main.R for England with every scenario, and run_batch.R for every ICB, with the same code and inputs (compare the run records with model_development/compare_runs.R if in doubt).
  2. Run 00_build_report_inputs.R and 01_build_report_results.R.
  3. Have output/for_report/ checked against disclosure_log.csv and fill in its checked_by and checked_on columns.
  4. Set the three require_ flags to TRUE and render with quarto render reports/report.qmd.
  5. Search the Word file for “CHECK” and “not yet available”; neither should be left.
  6. Run Rscript scripts/04_reports/check_doc_extracts.R from the repository root. It stops if a code extract in the documentation or the report no longer matches the code it quotes.

9.17 Scenarios

A scenario is a column of user_inputs/scenarios_control_panel.csv: every column other than Category, Parameter_Name, Arm, Definition and Source is a scenario, and each row is a parameter of the clinical pathway, quit success or costs. scripts/00_build_scenarios.R turns the panel, with the odds ratios in equity_multipliers_panel.csv (chapter 7a), into the parameters each scenario runs with. Coverage by year is in user_inputs/macro_trends_panel.csv, set by a rule for each scenario (below).