---
title: "Auditable joins and comparable maps"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Auditable joins and comparable maps}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>", fig.width = 7, fig.height = 4.5)
```

```{r setup}
library(mongolmaps)
```

## Keep a record of every join decision

Use codes where possible. When names repeat, supply the parent in a column
or restrict the candidate universe with `within`.

```{r scoped}
district <- mn_join(
  data.frame(place = "Bayangol", cases = 12),
  "place", level = "soum", within = "Ulaanbaatar"
)
mn_join_audit(district)[c("row", "x", "pcode", "status", "action")]
```

`mn_match(details = TRUE)` exposes candidates, normalized keys, aliases and
edit distances. A short edit distance is a spelling rule, not a probability
that a match is correct. Strict joins reject fuzzy/unresolved keys,
missing boundaries and duplicate place/stratum keys.

```{r strict}
observations <- data.frame(
  place = c("Uvs", "Uvs", "Khovd", "Khovd"),
  Year = c(2024, 2025, 2024, 2025),
  cases = c(10, 12, 20, 15)
)
joined <- mn_join(observations, "place", level = "aimag",
                  strata = "Year", strict = TRUE, fuzzy = FALSE)
mn_join_audit(joined)[c("row", "status", "action")]
```

The audit has one record per **original input row**. Its row numbers do not
refer to rows of a subsequently filtered map. It distinguishes unmatched
keys from known units without geometry, geographic exclusions and other
administrative levels. Audit/provenance attributes can be lost if an
object is rebuilt; save them explicitly for an analysis record.

## Reconcile Ulaanbaatar within statistical strata

NSO codes 5 and 511 sometimes hold complementary capital observations.
Declare all statistical dimensions and the measure columns. Check table
metadata first: this geographic equivalence is not valid for every table.

```{r ub}
table <- data.frame(
  Region = c("5", "511", "5", "511"),
  Month = c("July", "July", "August", "August"),
  value = c(10, NA, NA, 20)
)
capital <- mn_join(table, "Region", level = "aimag",
                   strata = "Month", value_cols = "value")
sf::st_drop_geometry(capital[capital$pcode == "MN11", c("Month", "value")])
mn_join_audit(capital)[c("row", "x", "action")]
```

The default prefers a nonmissing capital value and fills missing values from
the region. Conflicts produce a warning. Use `ub_policy = "error"` to stop
on conflicts, `"prefer_region"` to reverse precedence, or `"none"` to disable
this reconciliation. Never sum mixed-level NSO rows to calculate a retained
percentage: totals and their components overlap.

## Separate statistical years from boundary vintages

```{r provenance}
mn_vintages()[c("id", "data_version", "geometry_vintage", "authority")]
mn_provenance(mn_khoroos())[c("id", "source_revision", "effective_date")]
mn_citation(mn_settlements())
```

Only `data-v1` geometry is currently available. Passing another
`boundary_version` to an administrative getter errors instead of relabeling
the available map as a historical boundary. Khoroo geometry is unofficial
and reconciled; its source snapshot date does not establish its effective
date. Historical comparisons need compatible codes and actual historical
geometry or an explicit reallocation procedure.

## Compare maps using common scales

```{r comparison, eval=rlang::is_installed("ggplot2")}
mn_map(joined, fill = cases, limits = c(0, 25), breaks = c(0, 10, 20)) +
  ggplot2::facet_wrap(~Year)
```

`bins` accepts interval counts or explicit cut points. Use explicit cut
points for comparisons. `midpoint = 0` gives a diverging scale for
differences. Grey means missing values; zero and missing observations
should be coded separately. Preserve suppression and uncertainty variables
when preparing health statistics; a map does not establish rate stability.

## Enlarge Ulaanbaatar without moving analytical geometry

```{r inset, eval=rlang::is_installed(c("ggplot2","patchwork"))}
pop <- mn_example_population[mn_example_population$Year == 2025, ]
aimags <- mn_join(pop, "Region", level = "aimag")
mn_inset_ub(aimags, fill = value, trans = "log10",
            title = "Population, 2025")
```

For finer urban data, pass an `ub` map with a fill expression in the same
units as the national data. Limits, breaks and bin cut points are shared.
The panels use independent geographic scales. The UB layout separates
Baganuur and Bagakhangai; its main panel includes Nalaikh.

```{r urban, eval=rlang::is_installed(c("ggplot2","patchwork"))}
mn_map_ub(fill = area_km2, title = "District areas")
```

## Small multiples and population-aware views

Use `mn_aimag_grid` for aimags and `mn_ub_grid` for the nine districts.
Tile positions are approximate geographic layouts, not coordinates.
Per-aimag soum grids are usually more readable than hundreds of national
soum panels.

```{r grid, eval=rlang::is_installed(c("ggplot2","geofacet"))}
grid <- mn_ub_grid
grid$value <- seq_len(nrow(grid))
ggplot2::ggplot(grid, ggplot2::aes(1, value)) +
  ggplot2::geom_col() +
  geofacet::facet_geo(~code, grid = mn_ub_grid, label = "name")
```

A Dorling cartogram is an optional alternative: size circles by population
and use colour for an analytically defensible rate. Retain the original
geometry for analysis. Check missing/zero weights, geographic vintage,
and any instability or suppression in the rate before using this recipe.

```{r cartogram, eval=FALSE}
# Optional package: install.packages("cartogram")
# For health rates, prepare compatible numerators and denominators separately.
analysis_map <- sf::st_transform(aimags, mn_crs("albers"))
analysis_map$people_per_km2 <- analysis_map$value / analysis_map$area_km2
display_map <- cartogram::cartogram_dorling(analysis_map, weight = "value")
mn_map(display_map, fill = people_per_km2)
```

## Raster summaries and offline work

Population grids contain **people per cell**. The default
`mn_zonal(r, polygons, fun = "sum")` weights each count by its covered cell
fraction. It assumes a uniform allocation inside each cell. A 1 km grid
cannot recover building-level population. For means, coverage fractions
are not physical area weights on a longitude/latitude grid. Median
summaries require `exact = FALSE`.

Prefer analysis on the native population grid. If `mn_population(crs = ...)`
is used for export, weighted-sum remapping is normalized to preserve the
retained native-grid total. This does **not** validate local allocations;
inspect the `mongolmaps_remapping` attribute. Raster map resampling is for
display only.

```{r assets}
assets <- mn_assets()
assets[assets$id == "worldpop_2025_1km", c("id", "year", "resolution", "url")]
```

```{r offline, eval=FALSE}
mn_download(c("admin_high", "worldpop_2025_1km"))
mn_cache_verify()
options(mongolmaps.offline = TRUE)
```

WorldPop files are opt-in, so `mn_download("all")` does not fetch every
year and resolution. Cached downloads are checked against their checksum
by default; old data versions remain until explicitly cleared.
Automatic protected-area refresh keeps a valid stale file if refreshing
fails. An explicit `refresh = TRUE` requires a successful refresh and
preserves the old file on failure.

For cartographic water removal, `mn_erase_water()` returns a new geometry
and `land_area_km2`, leaving the input and its `area_km2` unchanged.
Land is not the same as inhabited land. When composing additional layers,
combine their credits explicitly, for example
`mn_citation(c("admin", "osm_roads", "osm_water"))`.
