nestedscales represents a discrete data dimension at multiple resolutions and recasts tabular data between them. A nested scale stores one row per finest-grained unit, or atom, and one column for each coarser grouping, or frame. Each frame assigns atoms to units; frames may form a hierarchy or cross-cut one another, including product-like overlays. All mappings are derived from membership at the atom level.
The package defines:
sum, weighted_mean, mean, and copy; sd for aggregation; and share or logshare for within-parent shares.The package is dimension-agnostic. Time and space are provided by two packages built on it, designed for optimization and simulation models, where labeled time slices and regions serve as the index sets of variables and constraints:
Calendar, labeled discrete time slices (hours, days, seasons, representative days).Geoscale, labeled geographical regions with geometry.Both classes are subclasses of NestedScale. Any other dimension (industries, income brackets, technology vintages) is declared directly from a table. vignette("nestedscales") introduces the functions; the package website includes a roadmap of what is available and what is planned.
Until the first CRAN release, install the development version from GitHub:
pak::pkg_install("optimal2050/nestedscales")The package is pure R; its non-base runtime dependencies are dplyr, rlang and S7.
Declare the levels, coarsest first:
library(nestedscales)
industries <- data.frame(
section = c("C", "C", "C", "D", "D"),
division = c("C10", "C10", "C11", "D35", "D35"),
class = c("C101", "C102", "C110", "D351", "D352"),
gva = c(120, 80, 45, 300, 150)
)
ind <- scale_from_leaftable(
industries,
frames = c("section", "division", "class"),
weights = "gva",
name = "nace"
)
ind
#> NestedScale: nace
#> Frames (3, coarsest first):
#> - section (2)
#> - division (3)
#> - class (5)
#> Atoms: 5
#> Weights: gva (default: gva)Every structural question is answered from the atoms, never from an assumed tree:
scale_frames(ind)
#> [1] "section" "division" "class"
scale_units(ind, "division")
#> [1] "C10" "C11" "D35"
scale_nests(ind, "section", "division")
#> [1] TRUEHierarchies that cross-cut are normal and supported – one detailed code may belong to several aggregates. scale_nests(x, parent, child) tests whether every child code belongs to a single parent code; conversion never depends on that relationship.
summary(ind)
#> <summary of NestedScale 'nace'>
#> frames: section (2) / division (3) / class (5)
#> atoms: 5
#> weight totals: gva = 695 (default: gva)
#> nesting: section > division: nested
#> nesting: division > class: nestedData keyed at one level is recast to another with a rule per value column. With complete coverage, "sum" conserves totals; intensive quantities use "weighted_mean" with a declared weight:
x <- data.frame(
class = c("C101", "C102", "C110", "D351", "D352"),
jobs = c(12, 8, 5, 30, 20),
wage = c(31, 29, 27, 45, 40)
)
recast_scale(x, ind, from = "class", to = "section",
rule = c(jobs = "sum", wage = "weighted_mean"),
weight = "gva")
#> section jobs wage
#> 1 C 25 29.61224
#> 2 D 50 43.33333gva, employment and count side by side, with one declared default.NA, not an error, and a subset records what fraction of its parent it keeps.data.frame, a data.table, or an arrow dataset; lazy inputs stay lazy.reconcile_scale() here compares data aggregated with sum or weighted_mean against independent totals and can balance discrepancies.