| Title: | Tools to Access and Map Spatial Datasets of Portugal |
| Version: | 0.1.0 |
| Description: | Tools to download, process, and visualize official spatial datasets for Portugal. Get administrative boundaries at all levels and other geographic data ready for mapping. Data from Statistics Portugal (INE), for example retrieved with the 'ineptr2' package, can be joined to the boundaries by place name or code, including names from earlier boundary reforms. |
| License: | MIT + file LICENSE |
| URL: | https://c-matos.github.io/geopt/, https://github.com/c-matos/geopt |
| BugReports: | https://github.com/c-matos/geopt/issues |
| Encoding: | UTF-8 |
| LazyData: | true |
| Depends: | R (≥ 4.1.0) |
| Imports: | R6, sf, httr2 (≥ 1.0.0), geojsonsf, digest |
| Suggests: | rmapshaper, stringdist, ggplot2, tmap, echarts4r, htmlwidgets, dplyr, plotly, knitr, rmarkdown, testthat (≥ 3.0.0), withr |
| VignetteBuilder: | knitr |
| Config/testthat/edition: | 3 |
| Collate: | 'utils.R' 'compose.R' 'layout.R' 'atoms.R' 'lookup.R' 'match.R' 'match-pool.R' 'join.R' 'derive.R' 'vintage.R' 'censos.R' 'get_map.R' 'plot.R' 'GeoClient.R' 'data.R' 'geopt-package.R' |
| Config/roxygen2/version: | 8.0.0 |
| NeedsCompilation: | no |
| Packaged: | 2026-10-12 12:18:49 UTC; CM |
| Author: | Carlos Matos [aut, cre, cph] |
| Maintainer: | Carlos Matos <carlosmdmatos@gmail.com> |
| Repository: | CRAN |
| Date/Publication: | 2026-10-21 16:50:18 UTC |
geopt: Tools to Access and Map Spatial Datasets of Portugal
Description
Tools to download, process, and visualize official spatial datasets for Portugal. Get administrative boundaries at all levels and other geographic data ready for mapping. Data from Statistics Portugal (INE), for example retrieved with the 'ineptr2' package, can be joined to the boundaries by place name or code, including names from earlier boundary reforms.
Author(s)
Maintainer: Carlos Matos carlosmdmatos@gmail.com [copyright holder]
Authors:
Carlos Matos carlosmdmatos@gmail.com [copyright holder]
See Also
Useful links:
Report bugs at https://github.com/c-matos/geopt/issues
GeoClient
Description
R6 class for downloading, processing, and caching Portuguese geographic
boundaries from CAOP (Carta Administrativa Oficial de Portugal), and for
filtered queries to DGT's OGC API. get_censos() and get_map() use one for
their downloads too.
Failed downloads
When a download fails, because a server can't be reached or a file has
moved or changed, the method stops with an error saying which and why. The
error has class geopt_download_error, so a script can catch download
failures alone with tryCatch(..., geopt_download_error = ...).
Usage
client <- GeoClient$new()
sf <- client$get_caop("continente", "municipio")
Active bindings
cache_dirCharacter. Path to the cache directory. Defaults to
tools::R_user_dir("geopt", "cache").use_cacheLogical. Whether to read and write cached results (default
TRUE). WithFALSE, cached results are neither read nor written: each call builds its result again, and the latest CAOP year is looked up online instead of taken from the cache. Downloaded files (CAOP and census GeoPackages) are kept and reused either way;$clear_cache("raw")removes them.crsInteger EPSG code for the default coordinate reference system. Defaults to 4326 (WGS84). Output can be transformed to any CRS via the
crsargument of$get_caop().
Methods
Public methods
GeoClient$new()
Create a new GeoClient.
Usage
GeoClient$new(cache_dir = NULL, use_cache = TRUE, crs = DEFAULT_CRS)
Arguments
cache_dirPath to cache directory. Defaults to user cache dir.
use_cacheWhether to read and write cached results. Downloaded files are kept either way; see the
use_cachefield.crsDefault EPSG code. Defaults to 4326 (WGS84).
GeoClient$get_caop()
Get geographic boundaries as an sf object.
Usage
GeoClient$get_caop(region, level, year = NULL, crs = NULL, detail = "full")
Arguments
regionOne of "continente", "raa", "ram".
levelOne of "freguesia", "municipio", "distrito", "nuts1", "nuts2", "nuts3".
yearCAOP year. If NULL, resolves to the latest available.
crsOutput EPSG code. If NULL, uses the client default.
detailSimplification level: "full", "high", "medium", "low" or "very_low".
Returns
An sf object.
GeoClient$get_ogc()
Get features from a DGT OGC API collection.
Queries https://ogcapi.dgterritorio.gov.pt/ and returns the response
as an sf object. Use this for filtered or bounded subsets (single
municipio, bbox around a city, COS land-cover by area). For bulk
country-wide downloads of CAOP, $get_caop() is faster.
Usage
GeoClient$get_ogc( collection, bbox = NULL, bbox_crs = 4326L, properties = NULL, cql_filter = NULL, crs = NULL, limit = OGC_DEFAULT_LIMIT, max_features = Inf )
Arguments
collectionCollection id, e.g. "freguesias", "municipios", "nuts3", "cos2023v1". See
$list_ogc_collections(); an id geopt doesn't know is an error that lists the ones it does. The large collections ("cos2018v3","cos2023v1","cadastro") need abbox,propertiesorcql_filter.bboxNumeric length-4 vector
c(xmin, ymin, xmax, ymax)or an sfbboxobject. If NULL, no spatial filter is applied.bbox_crsInteger EPSG code that
bboxis expressed in. Defaults to 4326. Only sent to the server when not 4326.propertiesNamed list of equality filters on attribute columns, e.g.
list(distrito = "Lisboa"). Usecql_filterfor more complex predicates.cql_filterOptional CQL-Text filter string, passed through verbatim. Example:
"distrito_ilha = 'Lisboa'". DGT's server accepts CQL1 syntax (cql-text), not CQL2.crsOutput EPSG code. If NULL, uses the client default. Requested server-side to avoid client-side reprojection.
limitPage size sent to the server. Default 1000.
max_featuresStop pagination after this many features. Defaults to
Inf(fetch everything).
Returns
An sf object, or NULL if the query returned zero features.
GeoClient$list_ogc_collections()
List collections reported live by the DGT OGC API.
Hits /collections?f=json. Used for discovery; the package itself
validates against the hardcoded OGC_COLLECTIONS registry.
Usage
GeoClient$list_ogc_collections()
Returns
A data.frame with columns id, title, item_type,
description.
GeoClient$download_caop()
Download boundaries to cache without loading into memory.
Usage
GeoClient$download_caop(region, year = NULL)
Arguments
regionOne of "continente", "raa", "ram".
yearCAOP year. If NULL, resolves to the latest available.
Returns
The paths to the downloaded GeoPackage files (invisibly).
GeoClient$simplify_boundaries()
Simplify boundary geometries.
Usage
GeoClient$simplify_boundaries(sf_obj, detail = "medium")
Arguments
sf_objAn sf object.
detailOne of "full", "high", "medium", "low" or "very_low".
Returns
A simplified sf object.
GeoClient$export_boundaries()
Export boundaries to a file format suitable for dashboards.
Usage
GeoClient$export_boundaries( sf_obj, path, format = "geojson", detail = NULL, crs = 4326L )
Arguments
sf_objAn sf object.
pathOutput file path.
formatOutput format: "geojson" or "gpkg".
detailOptional simplification level before export.
crsOutput EPSG code. Defaults to 4326 (WGS84) for web compatibility.
Returns
The output path (invisibly).
GeoClient$list_cached()
List cached data files.
Usage
GeoClient$list_cached()
Returns
A list with processed and raw character vectors.
GeoClient$clear_cache()
Clear cached data.
Usage
GeoClient$clear_cache(type = c("all", "processed", "raw"))
Arguments
typeOne of "all", "processed", or "raw".
GeoClient$print()
Print method.
Usage
GeoClient$print(...)
Arguments
...Ignored.
GeoClient$clone()
The objects of this class are cloneable with this method.
Usage
GeoClient$clone(deep = FALSE)
Arguments
deepWhether to make a deep clone.
Simplified Portuguese administrative boundaries
Description
Pre-simplified sf objects derived from CAOP (Carta Administrativa Oficial de
Portugal). All levels are topologically consistent: higher-level boundaries
are dissolved from simplified freguesias, guaranteeing aligned edges. Each
level carries its own identity columns (official codes and names) plus
aggregated area_ha, so every dataset joins directly against user data or
against pt_registry.
Usage
pt_freguesias
pt_municipios
pt_distritos
pt_nuts1
pt_nuts2
pt_nuts3
pt_uls
pt_reg_saude
pt_aces
pt_uls_municipio
pt_aces_municipio
Format
An sf data frame with MULTIPOLYGON geometry.
An object of class sf (inherits from data.frame) with 3259 rows and 16 columns.
An object of class sf (inherits from data.frame) with 308 rows and 13 columns.
An object of class sf (inherits from data.frame) with 29 rows and 8 columns.
An object of class sf (inherits from data.frame) with 3 rows and 4 columns.
An object of class sf (inherits from data.frame) with 9 rows and 6 columns.
An object of class sf (inherits from data.frame) with 26 rows and 9 columns.
An object of class sf (inherits from data.frame) with 55 rows and 7 columns.
An object of class sf (inherits from data.frame) with 7 rows and 6 columns.
An object of class sf (inherits from data.frame) with 71 rows and 8 columns.
An object of class sf (inherits from data.frame) with 50 rows and 7 columns.
An object of class sf (inherits from data.frame) with 66 rows and 8 columns.
Details
Common columns (present where applicable to the level):
- dtmnfr
6-digit freguesia code (distrito + municipio + freguesia), zero-padded character. Freguesias only.
- dtmn
4-digit municipality code (first 4 digits of
dtmnfr).- dt
2-digit distrito/ilha code.
- nuts1_cod, nuts2_cod, nuts3_cod
NUTS 2024 codes in full Eurostat form (
PT1,PT19,PT191, ...).- freguesia, municipio, distrito_ilha, nuts1, nuts2, nuts3
Official names.
- designacao_simplificada
The freguesia's name without the
"União das freguesias de"prefix. Freguesias only.- perimetro_km
Perimeter in kilometres, as CAOP gives it. Freguesias only.
- area_ha
Area in hectares, summed from the constituent freguesias.
- n_freguesias, n_municipios
Number of constituent units.
Geometries use WGS84 (EPSG:4326) and cover all of Portugal (Continente, Açores, and Madeira).
A multi-layer GeoPackage containing the same data is also bundled at
inst/extdata/caop_simplified.gpkg, with one layer per level:
gpkg <- system.file("extdata", "caop_simplified.gpkg", package = "geopt")
sf::st_layers(gpkg)
sf::st_read(gpkg, layer = "municipio")
The health maps are dissolved from the same simplified freguesias using the
pt_health_codes mappings, so their edges align with every administrative
level. Each reflects a single health vintage and exists on the current
freguesia map only. pt_uls, pt_reg_saude, and pt_aces are
freguesia-accurate: units that split municipalities are drawn correctly.
pt_uls has 55 units: 39 mainland ULS plus the island health units, which
are not ULS — the Açores have nine Unidades de Saúde de Ilha
("USI São Miguel", ...) and Madeira seven centros de saúde
("CS Funchal Zona I", ...). pt_reg_saude carries the seven regiões de
saúde. Both are the 2024 reform.
pt_aces has 71 Agrupamentos de Centros de Saúde — the organization the
2024 reform superseded. Its reg_saude is the region of that time (the
former ARS), not the 2024 região de saúde.
pt_uls_municipio (50 units) and pt_aces_municipio (66) are for data by
municipality, which can't be divided between the units that share a
municipality. Those units are merged into one, named after its members
("ULS Santo António + São João"), so every unit is a set of whole
municipalities. Lomba, the one freguesia of Gondomar in ULS Vila Nova de
Gaia/Espinho and ACES Espinho/Gaia, is drawn with the rest of Gondomar,
where data by municipality counts it. They hold the units of the
uls_municipio and aces_municipio columns that pt_join() adds.
All datasets carry nuts1_cod and nuts1, so a health map can be
filtered to the mainland, the Açores ("PT2") or Madeira ("PT3"),
and pt_aces and pt_aces_municipio carry their reg_saude.
They carry no other region column, because no other is the same throughout
every unit: ULS Médio Tejo, for example, is split between two regiões de saúde.
Source
Direção-Geral do Território (DGT), CAOP. https://www.dgterritorio.gov.pt/cartografia/cartografia-tematica/caop
See Also
pt_lookup() for the geometry-free lookup table, get_map() for
composed maps with island insets.
Add labels to an echarts map
Description
Pipe-able function that injects series-level label configuration into
an existing echarts widget, labelling the rectangles drawn for
islands = "box". Uses a JavaScript formatter that shows the
geopt_label text for those rectangles and an empty string for every other
feature.
Usage
box_labels_echarts(
e,
sf_obj,
font_size = 12,
color = "#333",
position = "right",
offset = c(92, 0),
...
)
Arguments
e |
An echarts widget created by |
sf_obj |
An sf object returned by |
font_size |
Font size for labels. Default |
color |
Color for labels. Default |
position |
Label position relative to the feature bounding box.
Default |
offset |
Numeric vector of length 2 ( |
... |
Additional echarts label properties (e.g. |
Value
The modified echarts widget.
Examples
m <- get_map("nuts3", islands = "box")
m$name <- dplyr::coalesce(m$geopt_label, m$nuts3)
geojson <- geojsonsf::sf_geojson(m)
m |>
echarts4r::e_charts(name) |>
echarts4r::e_map_register("pt", geojson) |>
echarts4r::e_map(area_ha, map = "pt", roam = TRUE) |>
box_labels_echarts(m) |>
echarts4r::e_visual_map(area_ha)
Build echarts frame styling data
Description
Creates a list of data items suitable for passing to
echarts4r::e_map(data = ...) to style the frames around the island
insets, with line widths based on geopt_frame_level. Because the data
parameter in echarts replaces the entire series data, this function returns
entries for all features — preserving the mapped values for real
regions and styling frames as transparent overlays.
Usage
frame_data_echarts(
sf_obj,
value_col,
level_widths = c(0.8, 0.4, 0.2),
border_color = "#333"
)
Arguments
sf_obj |
An sf object returned by |
value_col |
Name of the value column being mapped (as a string). |
level_widths |
Numeric vector mapping frame level to border width.
Position 1 corresponds to |
border_color |
Border color string. Default |
Value
A list of named lists, each with name, value, and (for frames)
itemStyle. Pass this to echarts4r::e_map(data = ...).
Examples
m <- get_map("municipio")
m$name <- dplyr::coalesce(m$municipio, m$geopt_label)
geojson <- geojsonsf::sf_geojson(m)
m |>
echarts4r::e_charts(name) |>
echarts4r::e_map_register("pt", geojson) |>
echarts4r::e_map(area_ha, map = "pt", roam = TRUE,
data = frame_data_echarts(m, "area_ha")) |>
echarts4r::e_visual_map(area_ha)
Get INE census geographies (BGRI, secções, GRID)
Description
Downloads and reads the census geographies that INE distributes at https://mapas.ine.pt/download/index2021.phtml: BGRI subsecções (city-block/place-part polygons) for the 1991, 2001, 2011, and 2021 censuses, statistical secções, and the 1 km grid — each with the census variables INE embeds in the files. Files are fetched at the smallest scope that covers the request (per-municipality files are typically 1–3 MB) and cached.
Usage
get_censos(
municipio = NULL,
year = 2021,
level = "subseccao",
region = NULL,
freguesia = NULL,
variables = NULL,
geometry = TRUE,
crs = NULL,
client = NULL
)
Arguments
municipio |
Municipality name(s) or DTMN code(s). See Scope. |
year |
Census year: 1991, 2001, 2011, or 2021 (default). |
level |
|
region |
Region keyword, NUTS2/NUTS3 name, or NUTS 2013 code. See Scope. |
freguesia |
Freguesia name(s) or DTMNFR code(s). See Scope. |
variables |
Optional character vector of census variables to keep (case-insensitive; identifier columns are always kept). See pt_censos_vars. |
geometry |
Logical. |
crs |
Output EPSG code. If |
client |
Optional GeoClient to use (for a custom cache directory
or CRS). Defaults to |
Value
An sf object (or a data.frame when geometry = FALSE) with one
row per subsecção, secção, or grid cell. A failed download stops with an
error of class geopt_download_error saying why (see GeoClient).
Scope
Exactly one scope must be given (there is no default: a whole-country request is ~250 MB and must be explicit):
-
municipio: one or more municipality names or 4-digit DTMN codes, resolved viapt_match()(qualifiers like"Lagoa (Açores)"work). Downloads the per-municipality file(s). -
freguesia: one or more freguesia names or 6-digit DTMNFR codes. Downloads the enclosing municipality file(s) and filters. May be combined withmunicipioto disambiguate names. -
region:"portugal","continente","raa"/"açores","ram"/"madeira", a NUTS2 or NUTS3 name, or a NUTS 2013 code ("PT16D"/"16D"). Census files follow the NUTS 2013 geography; NUTS 2024 names are accepted when they map unambiguously to it.
Levels and years
-
"subseccao"(default): BGRI subsecções, available for all four censuses. 2021 files embed 32 census variables, 2011 files 122, 1991/2001 files seven basic counts. -
"seccao": statistical secções. For 2021 these are INE's native files with ~180 variables (more detail than the subsecção set). For earlier censuses no secção product exists: subsecções are downloaded and dissolved, summing that year's variables (addsn_subseccoes). -
"grid": the 2021-only 1 km grid (EPSG:3035 cells crossing administrative boundaries),regionscope only.
See pt_censos_vars for every variable with its INE description.
Identifiers
INE's identifier columns are year-suffixed and inconsistent
(DTCC91, DTMN21, FR11, ...). Year-stable tidy columns are added
alongside the originals: censos_year, bgri (full subsecção code),
dtmn, dtmnfr, seccao, subseccao, and — where the file carries
them (1991–2011) — lugar/lugar_nome. GIS artifacts (OBJECTID,
SHAPE_*) and INE join metadata are dropped.
Freguesia codes are vintage-specific: each census uses the freguesia map
in force at the time. The dtmnfr codes of the 2021 census are the
"2013" vintage, those of 2011 the "2011" vintage and those of 2001 the
"2001" vintage, so pt_recode() converts them to today's map. The 1991
census used an earlier map (4208 freguesias in 305 municípios, before
Odivelas, Trofa and Vizela were created in 1998), which geopt has no
vintage for.
Name-based freguesia scoping resolves against the current registry and,
for 2021, also matches through pt_crosswalk(); for older censuses pass
that era's codes if a name does not resolve. In the 2001 files, two
freguesias still carry the code of the municipality they belonged to before
a transfer: Vale de Amoreira is 090756 (Guarda, now Manteigas) and Moita
100117 (Alcobaça, now Marinha Grande). pt_recode() and pt_match()
accept those codes for the "2001" vintage, and a freguesia filter finds
either form.
See Also
pt_censos_vars, GeoClient (for client), get_map() for
administrative boundaries.
Examples
# City blocks of Braga with the 2021 census variables (~2 MB download)
bgri <- get_censos("Braga")
# Population density by subsecção
bgri$dens <- bgri$N_INDIVIDUOS / sf::st_area(bgri)
# 2011 vintage, only the resident population variables
get_censos("Braga", year = 2011,
variables = c("N_INDIVIDUOS_RESIDENT", "N_INDIVIDUOS_RESIDENT_H"))
# Secção-level detail (native 2021 product, ~180 variables)
get_censos("Braga", level = "seccao")
# A whole NUTS3, and the 1 km grid for the mainland
get_censos(region = "Cávado")
get_censos(region = "continente", level = "grid")
# Just the data, no geometry
get_censos("Braga", geometry = FALSE)
Get a composed map of Portugal
Description
Returns an sf object with mainland Portugal and the islands moved next to it as insets, ready for plotting. Uses the pre-simplified bundled datasets.
Usage
get_map(
level = "municipio",
islands = "compact",
data = NULL,
by = NULL,
fuzzy = FALSE,
aliases = NULL,
admin_vintage = NULL,
nuts_vintage = NULL,
client = NULL
)
Arguments
level |
Administrative level: |
islands |
How to show the Açores and Madeira: |
data |
Optional data.frame to join onto the boundaries, producing a
map ready for a choropleth. Requires |
by |
How to join |
fuzzy |
Logical. Enable fuzzy name matching for the join (see
|
aliases |
Optional named character vector of extra name aliases for
the join (see |
admin_vintage |
Which freguesia map to draw. Available values are
|
nuts_vintage |
Which NUTS revision to use: |
client |
Optional GeoClient to download and cache with (for a custom cache directory). |
Details
Every result has the same columns, in the same order, whatever the
options: the level's own columns (and any joined data), then
geopt_type, geopt_label and geopt_frame_level, then the geometry.
geopt_type is "boundary" for the regions and "frame" for the frames
drawn around the insets (see island_layout()). geopt_label holds a
frame's name, or the label of an archipelago drawn as a rectangle
(islands = "box").
For a map of one region, filter the result: rows keep their codes and
names. For an archipelago, use islands = "in_place" first, so it is in its
true position rather than moved and enlarged.
Value
An sf object in WGS84 (EPSG:4326).
See Also
Examples
library(ggplot2)
m <- get_map("municipio")
ggplot(m) + geom_sf()
# Mainland only
m <- get_map("municipio", islands = "none")
# Regions and frames drawn separately
m <- get_map("nuts2")
ggplot() +
geom_sf(data = m[m$geopt_type == "boundary", ], aes(fill = nuts2)) +
geom_sf(data = m[m$geopt_type == "frame", ], fill = NA, linewidth = 0.5)
# The archipelagos as labelled rectangles
get_map("nuts3", islands = "box")
# The Açores in their three official groups, no frames
get_map("municipio", islands = island_layout(raa = "groups", frames = FALSE))
# Join your own data by municipality name
df <- data.frame(concelho = c("Porto", "Lisboa"), value = c(10, 20))
m <- get_map("municipio", data = df, by = c(municipio = "concelho"))
ggplot(m) + geom_sf(aes(fill = value))
# The NUTS 3 regions of the 2013 revision (25, against 26 today)
get_map("nuts3", nuts_vintage = "2013")
Lay out the islands
Description
The Açores are about 1,500 km west of the mainland and Madeira about
1,000 km to the south-west, so a map of Portugal drawn to scale is mostly
ocean. get_map() and plot_pt() move the islands next to the mainland,
as insets. island_layout() says how: which archipelagos to show, how to
split them into insets, and whether to draw frames around them.
Usage
island_layout(
both = "compact",
raa = both,
ram = both,
frames = TRUE,
box_position = "bottom_left"
)
Arguments
both |
How to show both archipelagos: |
raa, ram |
How to show the Açores ( |
frames |
Draw frames around the insets? Only applies to archipelagos drawn as insets. |
box_position |
Where to put the rectangles, for |
Details
Pass the result as islands to get_map() or plot_pt(). For the common
cases a string is enough: islands = "none" is the same as
islands = island_layout("none").
Value
An island layout, to pass as islands to get_map() or
plot_pt().
Choices
Each archipelago takes one of:
"compact"(The default.) Insets arranged to take the least room: the Açores in four insets (the western group, the central group, São Miguel, and Santa Maria on its own), and Porto Santo in its own small inset, moved next to Madeira.
"groups"The Açores in their three official groups (Ocidental, Central and Oriental). Madeira has no groups, so for Madeira this is the same as
"compact"."whole"Madeira and Porto Santo in one inset, at their true distance apart. Not available for the Açores: at a size that fits next to the mainland, the smaller islands would be too small to read.
"box"A labelled rectangle instead of the islands, stacked to the left of the mainland. Only works at levels where each archipelago is a single region:
"nuts1","nuts2","nuts3"and"reg_saude". Can't be combined with insets."in_place"The islands where they really are, without frames. The starting point for a map of one archipelago (see Examples).
"none"Leave the archipelago out.
Frames
With frames = TRUE, get_map() adds the frames drawn around the insets,
which show that the islands have been moved. They are extra rows with
geopt_type = "frame", their name in geopt_label, and
geopt_frame_level saying what they surround: 1 for a whole archipelago,
2 for a group of islands and 3 for a single island.
Moving and resizing the islands
island_layout() returns a list, which you can change before passing it
on. Each archipelago drawn as insets has:
anchorLongitude and latitude its insets are placed around.
labelThe name on its frame (and on its rectangle, for
"box").frame_bufferSpace between the islands and the frame around the whole archipelago, in degrees.
groupsOne entry per inset, each with:
cropThe part of the archipelago this inset shows, as
c(xmin =, ymin =, xmax =, ymax =)in longitude and latitude.NULLshows the whole archipelago.offsetWhere the inset goes, as
c(x, y)in degrees fromanchor.scaleHow much to enlarge it. Default 1.
labelOptional. Draws a frame around this inset alone, with this name.
frame_level,frame_bufferOptional. That frame's
geopt_frame_level(default 3) and its space around the islands.
sub_framesOptional. Frames around several insets at once, each with
groups(the inset names),label, and optionallyframe_bufferandframe_level(default 2).
Print island_layout()$raa to see a complete example. To lay out an
archipelago from scratch, pass a list with the same structure as raa or
ram.
See Also
Examples
island_layout()
island_layout(raa = "groups", ram = "whole")
get_map("municipio", islands = "none") # mainland only
get_map("nuts3", islands = "box")
get_map("nuts3", islands = island_layout("box", box_position = "top_left"))
get_map("municipio", islands = island_layout(frames = FALSE))
# The Açores alone, in their true position
m <- get_map("municipio", islands = "in_place")
acores <- m[m$nuts2_cod %in% "PT20", ]
# Move the Açores half a degree west, with an English label
lay <- island_layout()
lay$raa$anchor <- lay$raa$anchor + c(-0.5, 0)
lay$raa$label <- "Azores"
get_map("nuts3", islands = lay)
Quick map of Portugal
Description
One-liner choropleths on the composed Portugal map (mainland + island
insets), rendered with ggplot2 or echarts4r. Returns the renderer's native
object – a ggplot you can extend with +, or an echarts4r widget you
can pipe into e_*() functions – with deliberately light styling.
Usage
plot_pt(
data = NULL,
value = NULL,
level = "municipio",
by = NULL,
renderer = "ggplot",
palette = NULL,
na_color = "grey85",
legend = TRUE,
...
)
Arguments
data |
|
value |
Name of the column to fill by (a column of |
level |
Administrative level, as in |
by |
How to join a data.frame |
renderer |
|
palette |
Fill palette: a vector of colors, or a single string naming
a |
na_color |
Fill for boundaries without a data value. |
legend |
Logical. Show the fill legend? |
... |
Passed to |
Value
A ggplot object or an echarts4r widget, depending on
renderer.
Input forms
-
plot_pt()– blank administrative map. -
plot_pt(df, value = "cases", by = "dtmn")– joinsdfonto the boundaries viaget_map()and fills byvalue. -
plot_pt(m, value = "cases")wheremis an sf returned byget_map()– renders an already-composed map directly. Build the map once, render it with several renderers.
Renderers
-
"ggplot"(default): returns aggplotobject. Suited to static reports; style further with+ theme(...),+ labs(...), etc. -
"echarts": returns anecharts4rhtmlwidget with tooltip and roam enabled, suited to Shiny. Features are keyed internally by official code (not by name), so duplicate unit names (two "Lagoa" municipalities, 196 reused freguesia names) cannot mis-map values; tooltips show the display name. Style further with|> echarts4r::e_title(...), etc.
See Also
get_map(), island_layout(), pt_join()
Examples
# blank municipality map
plot_pt()
df <- data.frame(concelho = c("Porto", "Lisboa"), value = c(10, 20))
plot_pt(df, value = "value", by = c(municipio = "concelho"))
plot_pt(df, value = "value", by = c(municipio = "concelho"), islands = "none")
# Build once, render twice
m <- get_map("nuts3", islands = "box")
m$area_ha <- as.numeric(m$area_ha)
plot_pt(m, value = "area_ha")
plot_pt(m, value = "area_ha", renderer = "echarts")
Census variables available from get_censos()
Description
Dictionary of the census variables embedded in the INE geographies
returned by get_censos(), with INE's Portuguese descriptions. One row
per variable per census year and level, derived from the actual
GeoPackage columns (INE's description CSVs use spelling variants of the
column names, which are matched to them).
Usage
pt_censos_vars
Format
A data.frame with columns:
- year
Census year (1991, 2001, 2011, 2021).
- level
"subseccao","seccao", or"grid".- variable
Column name as it appears in the returned object.
- description
INE's description (Portuguese).
Details
The variable sets differ widely by year: 2021 subsecções carry 32 variables, 2011 subsecções 122, 1991/2001 subsecções seven basic counts, 2021 secções ~180 variables, and the 2021 grid ten.
Source
INE, Censos — importação de dados alfanuméricos e geográficos (BGRI, GRID e Lugares): https://mapas.ine.pt/download/index2021.phtml
See Also
Examples
# What is available for the 2011 census?
vars <- pt_censos_vars[pt_censos_vars$year == 2011, ]
head(vars[, c("variable", "description")])
Correspondence between two administrative vintages
Description
Which units of one vintage correspond to which of another, and how each changed. Derived from shared territory rather than stored, so any pair of vintages works and nothing goes stale when a new one is added.
Usage
pt_crosswalk(
from = "2013",
to = NULL,
level = c("freguesia", "municipio", "distrito")
)
Arguments
from, to |
Administrative vintages: |
level |
Which units to relate. Only |
Value
A data.frame with one row per corresponding pair: from, to,
name_from, name_to, relation.
Codes are vintage-local
A DTMNFR identifies a unit within its own vintage only. None of the 302 freguesias restored in 2025 reused its pre-2013 code, so this table — not string comparison — is the only safe way to relate codes across vintages.
Relations
relation is computed from the correspondence itself:
unchangedsame territory, same code, same name.
renamedsame territory and code, different name.
renumberedsame territory, different code. Colos kept its territory across the 2025 reform and changed only its code (
021118→021124).mergedseveral
fromunits became one — the 2013 uniões.splitone
fromunit became several — the 2025 reposição.
See Also
pt_recode() to carry values across vintages, pt_registry for the
units themselves.
Examples
x <- pt_crosswalk("2013", "2025")
table(x$relation)
# what became of the 2013 "030882" code
x[x$from == "030882", ]
Health geography of Portuguese freguesias, both vintages
Description
One row per freguesia per health vintage. Keyed on the freguesia rather than
the município because unit varies within a município in five of them —
the reason this cannot live alongside pt_nuts_codes, which is município
grained.
Usage
pt_health_codes
Format
A data.frame with 6518 rows and columns:
- health_vintage
"2022"or"2024".- dtmnfr
6-digit freguesia code, current vintage.
- reg_saude
Região de saúde (2024) or the former ARS (2022).
- scheme
"uls"for 2024,"aces"for 2022 — different concepts, not two names for one thing.- unit
The local unit: a ULS or an ACES.
- unit_municipio
Município-grained approximation of
unit.
Details
Mappings are recorded against the current (2025) freguesia set;
pt_lookup() reaches them from an earlier admin_vintage.
Vintages
"2024"The reform: 5 regiões de saúde on the mainland plus the island services, and ULS (Unidades Locais de Saúde) as the local unit.
"2022"What it superseded: the former ARS, and ACES (Agrupamentos de Centros de Saúde) as the local unit.
unit_municipio is the unit to use for data by município. Units that share
a município are merged into one pseudo-unit, and merging follows chains, so
it can differ from unit even in a município that lies whole in one unit.
For example, ULS São José also covers part of Loures, which merges ULS
Loures-Odivelas with Lisboa's three ULS, and Odivelas gets the merged unit
too. Everywhere else it equals unit.
Source
Decreto-Lei 102/2023 (ULS); prior ARS/ACES organization.
See Also
Examples
table(pt_health_codes$health_vintage, pt_health_codes$scheme)
Add official codes and administrative context to a data frame
Description
Matches a column of data against the pt_registry (via
pt_match()) and appends the administrative columns that are well-defined
at that level: codes, names of parent units, the NUTS regions of one
revision, and health mappings of both vintages. A column is only appended
when its value is constant within each matched unit. If you have data by
municipio, for example, pt_join will add uls_municipio and not uls.
Usage
pt_join(
data,
by,
fuzzy = FALSE,
aliases = NULL,
admin_vintage = NULL,
nuts_vintage = NULL,
quiet = FALSE
)
Arguments
data |
A data.frame. |
by |
Which column of
|
fuzzy, aliases, quiet |
Passed to |
admin_vintage |
Which administrative vintage |
nuts_vintage |
Which NUTS revision the NUTS columns are: |
Value
data with the matched code column and administrative columns
appended. Columns already present in data are not overwritten. The
match report is attached and retrievable with pt_match_report().
Vintages
admin_vintage says which freguesia map data is on, and the codes and
names appended are that map's. INE publishes freguesias on the 2013 map, so
its data joins with admin_vintage = "2013" (or "auto") and keeps its
2013 codes; on the default, the current map, the uniões split in 2025
would not match. Health mappings are recorded on the current map, so on an
earlier one they are reached through shared territory, as in pt_lookup():
a unit whose territory now lies in more than one ULS gets NA.
nuts_vintage says which NUTS revision the NUTS columns are, and at a NUTS
level which revision by is matched against.
See Also
pt_match(), get_map() (which joins directly onto geometry)
Examples
df <- data.frame(concelho = c("Porto", "Lagoa (Acores)"), value = 1:2)
pt_join(df, by = c(municipio = "concelho"))
# Data on the 2013 map, as INE publishes it: the uniao of Conde and
# Gandarela (Guimaraes) was split in 2025, so its code only exists there
ine <- data.frame(dtmnfr = "030882", value = 3)
pt_join(ine, by = "dtmnfr", admin_vintage = "2013")
# The NUTS regions of the 2013 revision, under which Lisboa was PT17
pt_join(data.frame(concelho = "Lisboa"), by = c(municipio = "concelho"),
nuts_vintage = "2013")
Administrative, NUTS and health codes for one choice of each vintage
Description
Assembles a wide lookup table from pt_registry, pt_nuts_codes and pt_health_codes. Each axis is chosen independently, because they are independent: the same freguesia set is published by INE under more than one NUTS revision, and a NUTS revision says nothing about which freguesias exist.
Usage
pt_lookup(
level = "freguesia",
admin_vintage = NULL,
nuts_vintage = NULL,
health_vintage = NULL
)
Arguments
level |
Grain of the result: |
admin_vintage |
Which administrative unit set: |
nuts_vintage |
Which NUTS revision: |
health_vintage |
Which health geography: |
Value
A data.frame, one row per unit of level.
Vintages
admin_vintage selects which administrative units exist:
-
"2025"— 3259 freguesias, the current set (Lei 25-A/2025, in force from the 2025 autárquicas, which restored units aggregated in 2013). -
"2013"— 3092 freguesias (Lei 11-A/2013). This is what INE still publishes, including on indicators labelled "NUTS - 2024". -
"2011"— 4260 freguesias, the map the 2011 census used, in force from 2004 until the 2013 reform. -
"2001"— 4241 freguesias, the map the 2001 census used. 22 freguesias were created and 3 dissolved between the two censuses.
The pre-2013 maps are named after the census that recorded them, because neither was made by a single law. The 1991 census used an earlier map again (4208 freguesias in 305 municípios, before Odivelas, Trofa and Vizela were created in 1998), which geopt does not have.
nuts_vintage selects the NUTS revision: "2024" (9 NUTS II, 26 NUTS III),
"2013" (7 / 25) or "2002" (7 / 30).
health_vintage selects the health geography: "2024" (regiões de saúde +
ULS) or "2022" (the former ARS + ACES). Health columns are omitted unless
asked for, or unless level is a health level: those read the geography
their units belong to, "2024" for "reg_saude", "uls" and
"uls_municipio", and "2022" for "aces" and "aces_municipio".
Codes are vintage-local
A DTMNFR identifies a unit within its own vintage only. None of the 302
freguesias restored in 2025 reused its pre-2013 code, so comparing a code
across vintages — or joining data of one vintage onto codes of another —
silently mismatches. Use pt_crosswalk() to convert, and pt_recode() to
carry values across.
See Also
pt_registry, pt_nuts_codes, pt_health_codes,
pt_crosswalk(), pt_match()
Examples
head(pt_lookup())
# the combination INE actually publishes: 2013 freguesias, 2024 NUTS
head(pt_lookup(admin_vintage = "2013", nuts_vintage = "2024"))
# one row per municipio, with its 2002 NUTS grouping
head(pt_lookup("municipio", nuts_vintage = "2002"))
# one row per ULS (the 2024 health geography, without asking)
head(pt_lookup("uls"))
Match place names to official codes
Description
Resolves a character vector of Portuguese administrative unit names to
their official codes using the pt_registry table. Matching is exact on
normalized names (see pt_normalize()), optionally preceded by a
user-supplied alias lookup and followed by an opt-in fuzzy pass.
Usage
pt_match(
x,
level = "municipio",
to = NULL,
context = NULL,
admin_vintage = NULL,
nuts_vintage = NULL,
health_vintage = NULL,
fuzzy = FALSE,
max_dist = 2,
aliases = NULL,
historic = TRUE,
quiet = FALSE
)
Arguments
x |
Character vector of names to match. Codes are automatically padded to the correct length, if needed. |
level |
Which units |
to |
Column of |
context |
Optional character vector (length 1 or |
admin_vintage |
Which administrative vintage to match against and return
codes for: |
nuts_vintage |
Which NUTS revision |
health_vintage |
Which health geography |
fuzzy |
Logical. Enable the fuzzy fallback pass? Default |
max_dist |
Maximum string distance for a fuzzy match. Default |
aliases |
Optional named character vector mapping variant spellings
to either canonical names or codes, e.g.
|
historic |
Logical. Also match names from the level's other vintages?
Default |
quiet |
Logical. Suppress warnings/messages about ambiguous,
unmatched, and fuzzy-matched entries? Default |
Value
Character vector of codes (same length as x), with a "report"
attribute retrievable via pt_match_report().
Ambiguity and context
Many names are not unique (~200 freguesia names are reused; the municipality
"Lagoa" exists in Faro and in Sao Miguel). Ambiguous names return NA with
a warning listing the candidates. To resolve them, either:
pass
context: a parent unit name per element (municipio for freguesias; distrito/ilha, NUTS region, or"Acores"/"Madeira"for municipios), orembed a qualifier in the name itself:
"Lagoa (Acores)","Lagoa (Faro)", INE-style"Calheta (R.A.M.)","Lagoa [R.A. Acores]"and dash-separated forms like"Calheta - RAA"or"Calheta-R.A.A"are all parsed automatically. A dash is only read as a separator when what follows it is a recognized region — ordinary names containing dashes ("Albergaria-a-Velha") are never split.
A qualifier is only read as one when it is not already part of an official
name. Many freguesias end in a parenthetical of their own —
"Bouro (Santa Maria)", "Uniao das freguesias de Braga (Maximinos, Se e Cividade)" — and those are matched whole, before any split is attempted.
Spelling tolerance
Matching ignores case, accents, and punctuation, and expands S./Sta./
Sto.. If that still does not match, a second pass ignores Portuguese
particles, so official designations and their working short forms both
resolve: "ULS da Guarda" → "ULS Guarda",
"ULS Trás-os-Montes Alto Douro" → "ULS Trás-os-Montes e Alto Douro".
Exact spellings always win; the report's method column shows "loose"
when this pass was needed.
A few units accept a second official spelling, so that data labelled either
way resolves: "Vila da Praia da Vitoria" for Praia da Vitória, and
"Calheta" for the Azorean Calheta (which pt_registry stores as
"Calheta de Sao Jorge"). Because Madeira also has a Calheta, a bare
"Calheta" is therefore ambiguous and returns NA with a warning —
qualify it as "Calheta (R.A.A.)" or "Calheta (Madeira)".
Historic names
Administrative reforms rename and dissolve units, so names in older census and
administrative files often have no current counterpart. With
historic = TRUE (the default) names from every vintage of the level are
matched, returning the code of the unit covering that territory in the
requested vintage; the report's method column shows "historic". A name
found in the 2001 map is only used when no later vintage has it: most 2001
names were still in use in 2011, and for the few freguesias that lost
territory in between, the 2011 sense is the one that matches a single unit
today. This is
one mechanism, not a freguesia special case — it is also what makes
"Minho-Lima" resolve at level = "nuts3", where the current name is
"Alto Minho".
Live names win outright. A name still in use resolves to the unit bearing it
now, whatever it also meant in the past. Where a predecessor was later split
between several current units the name is genuinely ambiguous and returns
NA — pass a context or use codes. Historic names are matched exactly and
particle-loosely, never fuzzily.
A freguesia renamed while its map was in force keeps its earlier names as
names of that vintage, not as historic ones. The 2013 freguesias are named as
they were last, in CAOP 2024 and in INE's tables, and 25 of them were renamed
by law before that. "Buarcos", the name of 060520 until 2015, therefore
matches it with admin_vintage = "2013" even when historic = FALSE.
Vintages
The three axes are independent and each has its own argument. admin_vintage
chooses which units exist and which vintage's codes come back;
nuts_vintage and health_vintage choose the nomenclature that NUTS and
health columns are expressed in. See pt_lookup().
admin_vintage defaults to the current set, which is what you want for
joining old data onto today's map. To keep the data on its own vintage
instead — to map it against boundaries of the era — name that vintage:
pt_match(c("Sé", "Maximinos"), level = "freguesia", context = "Braga")
#> [1] "030364" "030364" # the union covering both today
pt_match(c("Sé", "Maximinos"), level = "freguesia", context = "Braga",
admin_vintage = "2011")
#> [1] "030352" "030324" # the units they were
Codes are vintage-local. None of the 302 freguesias restored in 2025
reused its pre-2013 code, even if it has the same name and same boundaries,
so a code is only accepted against the vintage it belongs to.
pt_match("030882", level = "freguesia") is NA, because that is
a 2013 code; with admin_vintage = "2013" it resolves.
admin_vintage = "auto" infers the vintage from the input and says which it
chose. This is the setting for INE data, which is published on the 2013
freguesia set even under the 2024 NUTS revision — so 134 units that were
disaggregated in 2025 are ambiguous against the current registry and exact
against their own.
INE prefixes a city's pre-2013 parishes with the city ("Braga (Sé)") but
names the same kind of unit plainly elsewhere ("Sé" in Lisboa). All the
spellings resolve — "Braga (Sé)", "Sé (Braga)", and "Sé" with
context = "Braga" — because a bare name plus a context is recombined into
the parenthesised forms and retried, once the name as given has failed. A
trailing parenthetical never displaces a context you passed: where it is
part of the official name rather than a qualifier, the context still applies.
Fuzzy matching
With fuzzy = TRUE (requires the stringdist package), elements that
fail exact matching are compared against candidate names by optimal string
alignment distance. A fuzzy match is accepted only when the best candidate
is within max_dist and strictly better than the second-best candidate;
every accepted fuzzy match is reported via message. Fuzzy matching is
most reliable with a context, which shrinks the candidate pool.
See Also
pt_match_report(), pt_normalize(), pt_registry
Examples
pt_match(c("Porto", "lisboa", "S. Joao da Madeira"))
pt_match(c("Lagoa (Faro)", "Lagoa (Acores)"))
pt_match("Pinheiro", level = "freguesia", context = "Guimarães")
Retrieve the match report from a pt_match result
Description
Retrieve the match report from a pt_match result
Usage
pt_match_report(x)
Arguments
x |
The return value of |
Value
A data.frame with one row per input: input, matched_name,
code, method ("exact", "loose" — matched once Portuguese
particles were ignored, "alias", "historic" — matched a name from a
vintage other than the one requested, such as a pre-2013 name,
"fuzzy", "code", "ambiguous", "context_unrecognized" — the
context is not a unit geopt knows, "unmatched", "empty"), and dist
(fuzzy matches only).
Examples
m <- pt_match(c("Porto", "Lagoa"))
pt_match_report(m)
Normalize Portuguese place names for matching
Description
Lowercases, expands common abbreviations (S. -> Sao, Sta. -> Santa,
Sto. -> Santo), strips accents, and collapses punctuation and
whitespace, so that variant spellings of the same unit produce identical
keys: "S. Jorge", "são jorge", and "Sao-Jorge" all normalize to
"sao jorge".
Usage
pt_normalize(x)
Arguments
x |
Character vector of names. |
Details
This is the normalization used internally by pt_match(); it is exported
so the same rules can be applied to arbitrary user columns.
Value
Character vector of normalized names.
Examples
pt_normalize(c("S. Jorge", "SÃO JORGE", "Sao-Jorge"))
NUTS classification of Portuguese municípios, every revision
Description
One row per município per NUTS revision. NUTS revisions are set at European level and are independent of the administrative vintage: INE publishes the same 3092-freguesia set under both the 2013 and the 2024 revision.
Usage
pt_nuts_codes
Format
A data.frame with 924 rows and columns:
- nuts_vintage
"2002","2013"or"2024".- dtmn
4-digit município code.
- nuts1_cod, nuts1
NUTS I code and name.
- nuts2_cod, nuts2
NUTS II code and name.
- nuts3_cod, nuts3
NUTS III code and name.
Details
Keyed on dtmn because every NUTS revision Portugal has had is a regrouping
of the same 308 municípios — stable since the 1998 creation of Trofa, Vizela
and Odivelas. NUTS is therefore a function of (dtmn, nuts_vintage) and not a
freguesia-level attribute at all, which is why 308 rows per revision is the
whole fact.
Revisions
"2024"9 NUTS II, 26 NUTS III. Splits the former Lisboa region into Grande Lisboa, Península de Setúbal and Oeste e Vale do Tejo.
"2013"7 NUTS II, 25 NUTS III, the CIM/AM-based sub-regions.
"2002"7 NUTS II, 30 NUTS III.
PT17is"Lisboa"here and"Área Metropolitana de Lisboa"from 2013.
Source
INE / Eurostat NUTS nomenclature. NUTS II 2002 names come from INE's own indicator dimension labels.
See Also
Examples
# how a municipio's NUTS III changed across revisions
pt_nuts_codes[pt_nuts_codes$dtmn == "1106", c("nuts_vintage", "nuts3")]
Restore leading zeros in administrative codes
Description
Spreadsheet round-trips commonly convert codes like "0115" to the number
115. This pads digit codes back to their official width: 6 for DTMNFR
(freguesia), 4 for DTMN (municipio), 2 for DT (distrito/ilha).
Usage
pt_pad_code(x, level = "municipio")
Arguments
x |
Vector of codes (character or numeric). |
level |
One of |
Value
Character vector of zero-padded codes. Non-digit entries are returned unchanged.
Examples
pt_pad_code(c(115, 1106), "municipio")
Convert freguesia-level data between code vintages
Description
Portugal's freguesias were aggregated into "uniões de freguesias" in 2013
and partially de-aggregated in 2025; the maps before 2013 are finer still.
This function converts freguesia-level data between four vintages: "2001"
(the map the 2001 census used, 4241 freguesias), "2011" (the map the 2011
census used, 4260), "2013" (the uniões, 3092) and "2025" (the current
map, 3259). The correspondence is derived from shared territory rather than
stored, so any pair of vintages works; it handles the value semantics
explicitly:
Usage
pt_recode(
data,
by = "dtmnfr",
from,
to = NULL,
values = NULL,
aggregate = NULL,
split = c("error", "replicate", "weight"),
weights = NULL,
quiet = FALSE
)
Arguments
data |
A data.frame with freguesia-level rows. |
by |
Name of the column in |
from |
Which vintage the codes in |
to |
Target vintage, one of the same four. Leave it out for the newest
( |
values |
Character vector naming the numeric value columns to
convert. Other columns are carried along: replicated on splits, kept on
merges when constant within the group (otherwise set to |
aggregate |
Function (or named list of functions, one per value
column) used to combine values on merges, e.g. |
split |
How to distribute values on splits: |
weights |
For |
quiet |
Logical. Suppress informational warnings? Default |
Details
-
finer → coarser (merges), e.g.
2025→2013,2011→2025: freguesias collapse into a shared unit. Value columns must be aggregated, and only values for which aggregation is meaningful may be converted (raw counts can be summed; rates and medians cannot) — soaggregateis required whenever a merge occurs. If only some constituents of a target are present indata, the aggregate covers the present ones and a warning reports the partial groups. -
coarser → finer (splits), e.g.
2013→2025,2025→2011,2001→2011(22 freguesias created between the two censuses): a unit's value cannot be exactly disaggregated. You must opt in to a strategy:split = "replicate"copies the value to every constituent;split = "weight"distributes it proportionally toweights("area", only when splitting towards the newest vintage, or a named vector keyed by the targetdtmnfr, e.g. population). The default ("error") refuses.
Codes not present in the relevant crosswalk (including current freguesias
created after 2011, which have no pre-2013 predecessor) convert to NA
with a warning. For "2001", the codes the 2001 census areas use for Vale
de Amoreira (090756) and Moita (100117) are accepted alongside INE's
(090804, 101003).
Value
A data.frame with the target vintage's freguesia code (dtmnfr)
and name (freguesia), followed by the carried and converted columns. The
column names are the same whichever vintage you convert to, as in
pt_lookup().
See Also
pt_crosswalk(), pt_registry, pt_join()
Examples
# 2013-era data onto the current map: sum-able counts, split by area
old <- data.frame(dtmnfr = c("010121", "010109"), cases = c(30, 10))
new <- pt_recode(old, from = "2013", values = "cases",
split = "weight", weights = "area")
get_map("freguesia", data = new, by = "dtmnfr")
# Current data backwards to 2013 units (counts: aggregate = sum). Conde and
# Gandarela (Guimarães) were one união from 2013 until 2025.
now <- data.frame(dtmnfr = c("030895", "030897"), n = c(12, 7))
pt_recode(now, from = "2025", to = "2013", values = "n", aggregate = sum)
# 2011-census freguesia counts onto the current map: many old freguesias
# merged into today's uniões, so aggregate = sum
old <- data.frame(dtmnfr = c("010101", "010102"), pop = c(500, 800))
pt_recode(old, from = "2011", to = "2025", values = "pop",
aggregate = sum)
# a 2001-census rate onto the 2011 map: Agualva-Cacém became four
# freguesias, and a rate is copied to each
pt_recode(data.frame(dtmnfr = "111101", rate = 12.5), from = "2001",
to = "2011", values = "rate", split = "replicate")
Portuguese administrative units, every vintage
Description
One row per freguesia per administrative vintage. "Administrative vintage" means which units exist, which changes by Portuguese law and is unrelated to the NUTS revision (see pt_nuts_codes) or the health geography (see pt_health_codes).
Usage
pt_registry
Format
A data.frame with 14852 rows and columns:
- admin_vintage
"2001","2011","2013"or"2025".- dtmnfr
6-digit freguesia code, unique within the vintage.
- freguesia
Official designation. For 2013, the last one the freguesia had on that map.
- designacao_simplificada
Without the
"União das freguesias de"prefix. Official for 2025; derived by the same rule for 2013, because DGT's list for that map leaves 32 uniões unsimplified. Equal tofreguesiafor 2001 and 2011, which predate uniões.- dtmn
4-digit município code (the first four of
dtmnfr).- municipio
Município name.
- dt, distrito_ilha
District or island.
Details
dtmn is denormalised onto each row, so a vintage's set of municípios is the
distinct dtmn values within it.
Vintages
"2025"3259 freguesias. Lei 25-A/2025, in force from the 2025 autárquicas, which restored units aggregated in 2013.
"2013"3092 freguesias. Lei 11-A/2013. This is the set INE still publishes, including on indicators labelled "NUTS - 2024" — the label refers to the NUTS revision, not to the freguesia set. Each freguesia has the last name it had on that map, as in CAOP 2024 and INE's tables. 25 were renamed by law while the map was in force (
"Buarcos"became"Buarcos e São Julião"in 2015), andpt_match()recognizes the earlier names too."2011"4260 freguesias, the map the 2011 census used, in force from 2004 until the 2013 reform.
"2001"4241 freguesias, the map the 2001 census used. Between the two censuses 22 freguesias were created and 3 dissolved.
The pre-2013 maps are named after the census that recorded them, because neither was made by a single law; the later ones after their reform.
Codes are vintage-local
A DTMNFR identifies a unit within its own vintage only. None of the 302
freguesias restored in 2025 reused its pre-2013 code, so comparing codes
across vintages, or joining data of one vintage onto codes of another,
silently mismatches. Convert with pt_crosswalk(); carry values across with
pt_recode().
Source
Carta Administrativa Oficial de Portugal (DGT): CAOP 2025, and CAOP 2024 for the 2013 names, which INE's code list for that map matches name for name; Lei 11-A/2013 and Lei 25-A/2025; INE's freguesia code lists for the 2001 and 2011 censuses.
See Also
pt_lookup(), pt_nuts_codes, pt_health_codes, pt_crosswalk()
Examples
table(pt_registry$admin_vintage)
# the set INE publishes today
head(pt_registry[pt_registry$admin_vintage == "2013", ])