The modelling workflow in this folder starts from a mix of raw downloads in
raw_data/ and a few already-curated inputs in clean_data/. The cleaned
tables used by the INLA scripts were produced in two stages: first, each raw
source was reduced to a planning-area-level or coordinate-level analysis file;
second, prepare_inla_data.R merged those cleaned files into a single spatial
modelling dataset.
extract_planning_area_resident_count.pyreads the June 2025 resident Excel file, keeps only planning-area totals where subzone, age group, and floor area are all reported as totals, and writesclean_data/planning_area_total_residents_2025.csv.extract_planning_area _worker_counts.pyidentifies planning areas whose 2025 resident count is missing, then attaches 2020 workplace counts for those areas and writesclean_data/planning_areas_worker_counts_2020.csv.extract_planning_area_visitor_counts.pycleans attraction names, joins them to a place-to-planning-area lookup table, sums annual visitor counts by planning area, divides those totals by 2 to approximate the second half of 2024, and writesclean_data/planning_area_visitors_2024_halved.csv.aggregate_weekly_rainfall.pyreads the extracted rainfall CSV, groups rows by station coordinates, aggregates rainfall into consecutive 7-day bins, and writes a complete location-by-week table toclean_data/rainfall_weekly_by_location.csv, including zero-rainfall weeks when a station has no positive readings in a week.extract_flood_alert_locations.pyreads the saved flood-alert JSON, keeps only records wheremsgType == "Alert", extracts the alert-circle coordinates, removes duplicates, and writesclean_data/flood_alert_locations.csv.filter_landuse_exposure_areas.pyfilters the Master Plan 2025 land-use layer to the exposure-related classes used in this analysis (BEACH AREA,WATERBODY,AGRICULTURE,PARK,OPEN SPACE,CEMETERY) and writesclean_data/HighExposureLandUse2025.geojson.
Not every modelling input is generated by a script in this folder. The case
coordinates in clean_data/Case.txt are used as the observed outcome input,
and clean_data/Non-resident_population.csv is a small curated fallback table
used for planning areas that do not have resident counts but still need an
exposure proxy.
prepare_inla_data.R performs the last integration step before model fitting.
It standardizes planning-area names, spatially joins case points and flood
alerts to planning areas, averages weekly rainfall to a station-level mean and
then to a planning-area mean, intersects the filtered land-use polygons with
planning-area boundaries, and builds a single population_proxy using the
resident, worker, non-resident, and visitor sources in that order. It also
applies nearest-feature fallback rules when points or rainfall stations do not
fall cleanly inside a planning area, computes expected counts E, and creates
the adjacency graph used by the BYM2 model.
This folder contains R scripts for planning-area-level spatial modelling using
INLA.
prepare_inla_data.R is the shared data-preparation module. It reads the
cleaned inputs, aggregates cases to planning areas, builds the population proxy
and covariates, computes the expected counts E, creates the adjacency graph,
and returns the prepared objects used by the modelling scripts. It also
contains the shared fit_poisson_inla() helper used to fit Poisson INLA models
with the common settings for this project.
compare_inla_models.R is the model-comparison script. It sources
prepare_inla_data.R, then fits a greedy sequence of candidate Poisson INLA
models, starting from an intercept-only model, adding fixed covariates when
WAIC improves, and adding BYM2 only when the spatial term further improves
WAIC. It saves model-level comparison metrics to results/model_comparison.csv
and combined fixed-effect summaries to
results/model_fixed_effects_comparison.csv.
build_inla_model.R is the main analysis script. It sources
prepare_inla_data.R, fits the Poisson spatial model with the best WAIC in results/model_comparison.csv, which is with rainfall,
high-exposure land-use share, and a BYM2 random effect, then
saves the detailed outputs, tables, and maps used for the main analysis.
Together, these files are used to:
- read the cleaned inputs from
clean_data/ - aggregate case points to planning areas
- create planning-area covariates
- build a proxy population offset
- fit the full Poisson BYM2 model
- compare simpler candidate models against the full spatial specification
- save model outputs, tables, and maps into
results/
prepare_inla_data.R: shared data preparation and common INLA fitting helperbuild_inla_model.Rcompare_inla_models.R
The modelling script uses these R packages:
sfdplyrspdepINLAggplot2
If INLA is not installed yet, install it from the INLA repository before
running the script.
The workflow expects these files inside clean_data/:
2025PlanningAreaBoundary.geojsonCase.txtrainfall_weekly_by_location.csvHighExposureLandUse2025.geojsonflood_alert_locations.csvplanning_area_total_residents_2025.csvplanning_areas_worker_counts_2020.csvplanning_area_visitors_2024_halved.csvNon-resident_population.csv
If any of these files are missing, the shared preparation step stops with an error before fitting any model.
The planning-area boundary file is read as an sf object and standardized to a
single planning-area key using uppercase names. A projected version is also
created for area-based calculations.
The case coordinates in Case.txt are converted into points and spatially
joined to planning areas.
If a case point falls on a boundary and is not matched by st_within, the
script assigns it to the nearest planning area using st_nearest_feature.
The final response variable is:
cases: number of observed case points per planning area
Because some planning areas do not have resident population counts, the script
constructs a single population_proxy variable using this order:
total_residentsworkplace_count_2020non_resident_populationannual_visitors_2024_halved0
The script also records a proxy_source column so it is clear which source was
used for each planning area.
Planning areas with:
population_proxy = 0- and
cases = 0
are kept in the exported tables and maps, but they are excluded from the model fit itself because a zero exposure cannot be used as a stable Poisson offset.
If any planning area has:
population_proxy = 0- and
cases > 0
the script stops and reports that problem.
The script derives three planning-area covariates.
rainfall_weekly_by_location.csv contains weekly rainfall totals by station
location. The script:
- averages weekly rainfall over time for each station
- assigns stations to planning areas
- computes the mean rainfall for each planning area from stations inside it
- uses the nearest station as a fallback when an area has no station inside it
The final rainfall covariate is:
rainfall_mm
HighExposureLandUse2025.geojson contains polygons representing filtered land
uses such as waterbody, park, open space, agriculture, beach area, and
cemetery.
The script intersects those polygons with planning areas and computes:
high_exposure_area_m2high_exposure_share
where high_exposure_share is:
high_exposure_area_m2 / planning_area_area_m2flood_alert_locations.csv is converted into spatial points and joined to
planning areas.
The script counts how many flood-alert locations fall inside each planning area. If a point does not fall cleanly inside an area, it is assigned to the nearest planning area.
The final flood covariate is:
flood_alert_count
For planning areas included in the model, the script computes:
E = total_cases * population_proxy / sum(population_proxy)This is the expected number of cases under a baseline where cases are distributed proportionally to the proxy population.
The model then estimates whether each planning area has higher or lower risk than expected after accounting for the covariates and spatial structure.
The fitted model is the selected rainfall_landuse_bym2 Poisson BYM2 model:
cases ~ rainfall_z + high_exposure_share_z +
f(region_id, model = "bym2", graph = graph)Where:
rainfall_zandhigh_exposure_share_zare standardized covariatesregion_idis the planning-area index used by INLAgraphis the neighborhood graph built from shared planning-area borders
The response is cases, and the exposure term is E.
The script also requests:
- fitted values
- DIC
- WAIC
- CPO
After fitting, the script adds:
fitted_meanfitted_lowerfitted_upperrelative_riskrr_lowerrr_upperpp_rr_gt_1
relative_risk is computed as:
relative_risk = fitted_mean / Epp_rr_gt_1 is the posterior probability that relative risk is greater than
1.
When the script runs successfully, it writes these files into results/:
model_data.csvfixed_effects.csvhyperparameters.csvfit_metrics.csvrisk_ranking.csvinla_model.rdsadj.graphplanning_area_risk.geojsoncases_map.pngrelative_risk_map.pngposterior_prob_map.png
Planning-area table containing:
- case counts
- population proxy information
- covariates
- expected counts
- fitted summaries
- relative risk summaries
Posterior summaries of the fixed-effect coefficients.
Posterior summaries of the BYM2 hyperparameters.
Model fit statistics such as:
- DIC
- WAIC
- mean negative log CPO
Planning areas sorted from highest to lowest estimated relative risk.
Spatial output for mapping in GIS tools or other spatial software.
cases_map.png: observed case locations over planning-area boundariesrelative_risk_map.png: modelled relative risk by planning areaposterior_prob_map.png: posterior probability that relative risk exceeds 1
- The case file represents the observed outcome being modeled.
- Rainfall is treated as a spatial covariate by averaging the available weekly station records in the cleaned file.
- Flood-alert locations are treated as a static environmental covariate, not as the response.
- Planning areas with no positive proxy population and no cases remain in the exports, but not in the fitted model.
- The script is designed for the cleaned inputs currently present in this project.
- If you change column names or file names in
clean_data/, the script will need to be updated. - The script does not modify the input data; it only reads from
clean_data/and writes new outputs intoresults/.