This vignette takes one object apart. The example is a small fuel classification with every awkward feature: a code reused at two frames, a frame that cross-cuts another, an atom with no code at one frame, and attributes stored at three different levels.
lt <- data.frame(
group = c("Fossil", "Fossil", "Fossil", "Fossil", "Fossil",
"Bio", "Bio", "Bio", "Power"),
phase = c("solid", "solid", "liquid", "liquid", "gas",
"solid", "liquid", "gas", NA),
carrier = c("Coal", "Coal", "Oil", "Oil", "Gas",
"Biomass", "Biomass", "Biomass", "Electricity"),
fuel = c("Anthracite", "Lignite", "Diesel", "Gasoline", "NaturalGas",
"Wood", "Biodiesel", "Biogas", "Electricity")
)
at <- data.frame(
frame = c("fuel", "fuel", "carrier", "group", "fuel", "carrier"),
unit = c("Anthracite", "Lignite", "Oil", "Bio", "Anthracite", "Gas"),
attribute = c("lhv", "lhv", "lhv", "lhv", "ef_co2", "ef_co2"),
kind = c(rep("property", 4), "factor", "factor"),
value = c(26.7, 11.9, 42.5, 15.0, 98300, 56100),
unit_of_measure = c(rep("GJ/t", 4), "kg/TJ", "kg/TJ"),
source = "example"
)
al <- data.frame(
frame = c("fuel", "carrier", "fuel", "carrier", "carrier"),
unit = c("Anthracite", "Coal", "Electricity", "Electricity", "Electricity"),
alias = c("hard coal", "coal", "elc", "elc", "power"),
source = "example"
)
x <- scale_from_leaftable(
lt, frames = c("group", "phase", "carrier", "fuel"), key = "fuel",
attributes = at, aliases = al, name = "fuels"
)
x
#> NestedScale: fuels
#> Frames (4, coarsest first):
#> - group (3)
#> - phase (3) [1 unit unassigned]
#> - carrier (5)
#> - fuel (9)
#> Atoms: 9
#> Attributes: lhv, ef_co2 (sources: example) -- 1 at group, 2 at carrier, 3 at fuel
#> Aliases: 5 (example)A NestedScale is an S7 object with seven properties:
| property | type | holds |
|---|---|---|
leaftable |
data.frame |
one row per atom: one column per frame, the key column, weight columns |
frames |
character |
the hierarchy, ordered coarsest first |
members |
named list |
the ordered code vocabulary at each frame |
key |
character |
the name of the atom column ("unit" by default) |
meta |
list |
name, desc, weights,
default_weight, and whatever the builder adds |
attributes |
data.frame |
values keyed by (frame, unit): attribute,
kind, value, lower,
upper, unit_of_measure, source,
note |
aliases |
data.frame |
alternative labels keyed by (frame, unit):
alias, source |
Everything else – parent/child tables, ancestry, shares, resolved attributes – is derived on demand; nothing is cached on the object. The two tables are empty unless the builder fills them; a scale saved by an earlier version, which had no such properties, loads and validates unchanged.
scale_leaftable(x)
#> group phase carrier fuel
#> 1 Fossil solid Coal Anthracite
#> 2 Fossil solid Coal Lignite
#> 3 Fossil liquid Oil Diesel
#> 4 Fossil liquid Oil Gasoline
#> 5 Fossil gas Gas NaturalGas
#> 6 Bio solid Biomass Wood
#> 7 Bio liquid Biomass Biodiesel
#> 8 Bio gas Biomass Biogas
#> 9 Power <NA> Electricity ElectricityEach row is one atom. The key column (fuel here) is
unique and complete. Frame columns may hold NA:
Electricity has no physical state, so it does not
participate at the phase frame, and na_action=
decides what recasts do about it. The key may share its name with the
finest frame, as here and in the shipped carriers: there
the frame column is the key column.
scale_frames(x)
#> [1] "group" "phase" "carrier" "fuel"
scale_atom_level(x) # the finest frame, or the key
#> [1] "fuel"
scale_units(x, "carrier") # the members at one frame
#> [1] "Coal" "Oil" "Gas" "Biomass" "Electricity"
scale_units(x) # the atoms
#> [1] "Anthracite" "Lignite" "Diesel" "Gasoline" "NaturalGas"
#> [6] "Wood" "Biodiesel" "Biogas" "Electricity"
scale_rank(x, "carrier")
#> [1] 3Codes are not unique across frames:
Electricity is a group and a carrier here, and
Nuclear, Heat, Storage,
Other are each a group and a carrier in
carriers. Every function that takes a code therefore takes
its frame as well.
Navigation is derived from the leaftable and routed through the atoms, so a frame that cross-cuts another reports overlap rather than invented containment:
scale_nests(x, "carrier", "fuel") # a tree
#> [1] TRUE
scale_nests(x, "phase", "carrier") # Biomass is solid, liquid and gas
#> [1] FALSE
#> attr(,"offenders")
#> [1] "Biomass"
scale_parents(x, "carrier", "Biomass", target = "phase")
#> [1] "solid" "liquid" "gas"
scale_ancestors(x, "fuel", "Biogas")
#> frame unit
#> 1 group Bio
#> 2 phase gas
#> 3 carrier Biomass
scale_descendants(x, "group", "Fossil")
#> frame unit
#> 1 phase solid
#> 2 phase liquid
#> 3 phase gas
#> 4 carrier Coal
#> 5 carrier Oil
#> 6 carrier Gas
#> 7 fuel Anthracite
#> 8 fuel Lignite
#> 9 fuel Diesel
#> 10 fuel Gasoline
#> 11 fuel NaturalGas
head(scale_ancestry(x), 4)
#> parent_frame parent child_frame child
#> 1 group Bio phase gas
#> 2 group Bio phase liquid
#> 3 group Bio phase solid
#> 4 group Bio carrier BiomassPhysical attributes live in one long table, and a row may sit on any member, not only on an atom:
scale_attributes(x)
#> frame unit attribute kind value lower upper unit_of_measure
#> 1 fuel Anthracite lhv property 26.7 NA NA GJ/t
#> 2 fuel Lignite lhv property 11.9 NA NA GJ/t
#> 3 carrier Oil lhv property 42.5 NA NA GJ/t
#> 4 group Bio lhv property 15.0 NA NA GJ/t
#> 5 fuel Anthracite ef_co2 factor 98300.0 NA NA kg/TJ
#> 6 carrier Gas ef_co2 factor 56100.0 NA NA kg/TJ
#> source note
#> 1 example <NA>
#> 2 example <NA>
#> 3 example <NA>
#> 4 example <NA>
#> 5 example <NA>
#> 6 example <NA>Reading with a frame resolves one value per member,
walking to the nearest coarser frame when a member has none of its own,
and says where the value came from:
scale_attributes(x, "lhv", frame = "fuel")[
c("unit", "value", "unit_of_measure", "origin_frame", "origin_unit")]
#> unit value unit_of_measure origin_frame origin_unit
#> 1 Anthracite 26.7 GJ/t fuel Anthracite
#> 2 Lignite 11.9 GJ/t fuel Lignite
#> 3 Diesel 42.5 GJ/t carrier Oil
#> 4 Gasoline 42.5 GJ/t carrier Oil
#> 5 NaturalGas NA <NA> <NA> <NA>
#> 6 Wood 15.0 GJ/t group Bio
#> 7 Biodiesel 15.0 GJ/t group Bio
#> 8 Biogas 15.0 GJ/t group Bio
#> 9 Electricity NA <NA> <NA> <NA>Attributes are not weights. A weight says how big an
atom is and drives aggregation; an attribute is a per-member conversion
factor and is consumed only by convert_scale(). The
constructor promotes numeric leaftable columns to weights, so a
calorific value must be passed as attributes=, never as a
leaftable column. Every attribute name carries a kind:
property (an invertible ratio) or factor
(one-way).
vignette("attributes") has the reasoning and the full
account of inheritance, sources and conversion.
scale_aliases(x)
#> frame unit alias source
#> 1 fuel Anthracite hard coal example
#> 2 carrier Coal coal example
#> 3 fuel Electricity elc example
#> 4 carrier Electricity elc example
#> 5 carrier Electricity power example
scale_match(x, c("hard coal", "elc", "Lignite"))
#> label frame unit method candidates
#> 1 hard coal fuel Anthracite alias <NA>
#> 2 elc <NA> <NA> ambiguous fuel/Electricity; carrier/Electricity
#> 3 Lignite fuel Lignite exact <NA>One alias may point at several members (elc above);
matching reports that as ambiguous unless frame= narrows
it. Matching is exact and case-insensitive, with no fuzzy stage.
scale_weights(x) # none declared here
#> character(0)
S7::prop(x, "meta")[c("name", "desc")]
#> $name
#> [1] "fuels"
#>
#> $desc
#> [1] ""
carriers <- read_scale("carriers")
scale_weights(carriers)
#> character(0)
S7::prop(carriers, "meta")[c("name", "source", "palette_version")]
#> $name
#> [1] "carriers"
#>
#> $source
#> [1] "energypal carriers.yml"
#>
#> $palette_version
#> [1] "0.1.1"
head(S7::prop(carriers, "meta")$labels, 4)
#> frame unit label_short label_long
#> 1 group FossilFuels Fossil Fossil Fuels
#> 2 carrier FossilCoal Coal Coal
#> 3 carrier FossilOil Oil Oil
#> 4 carrier FossilGas Gas Natural Gasmeta$name is load-bearing: join_scale()
prefixes the columns it attaches with "<name>.",
scale_map_between() names its label columns after the
objects, and write_scale() needs a name for the definition.
The shipped carriers keeps energypal’s short and long
labels in meta$labels; colours are not stored.
scale_from_leaftable() and every operation that rebuilds
a scale enforce:
members are exactly its
non-NA values, in a stable order;(frame, unit)
that is a member; value is finite;
lower <= value <= upper where given;
kind is one of SCALE_ATTRIBUTE_KINDS and one
attribute name has one kind; unit_of_measure and
source are non-empty; no duplicate
(frame, unit, attribute, source) rows and no duplicate
alias rows.The order of frames is a convention, coarsest first; the
direction-dependent rules (share, residuals,
reconciliation) follow it.
Subsetting keeps the tables consistent: filter_scale()
and [ keep the attribute and alias rows of every surviving
member at every frame, and prune_scale() keeps the rows at
the frames that survive the collapse.
fossil <- x["group", "Fossil"]
scale_attributes(fossil)[c("frame", "unit", "attribute", "value")]
#> frame unit attribute value
#> 1 fuel Anthracite lhv 26.7
#> 2 fuel Lignite lhv 11.9
#> 3 carrier Oil lhv 42.5
#> 4 fuel Anthracite ef_co2 98300.0
#> 5 carrier Gas ef_co2 56100.0prune_scale() needs the frames above the new atom level
to nest, so the cross-cutting phase has to go first:
tree <- scale_from_leaftable(
lt[c("group", "carrier", "fuel")], frames = c("group", "carrier", "fuel"),
key = "fuel", attributes = at, aliases = al, name = "tree"
)
scale_aliases(prune_scale(tree, "carrier"))
#> frame unit alias source
#> 1 carrier Coal coal example
#> 2 carrier Electricity elc example
#> 3 carrier Electricity power exampleA scale is stored as a definition, not as an R object: a JSON header
with the three tables inline, or, for large scales, a
<name>.scale/ bundle directory holding the header and
the tables as Arrow IPC or CSV files. The Python binding reads and
writes the same files.
path <- file.path(tempdir(), "fuels.json")
write_scale(x, path)
cat(head(readLines(path), 12), sep = "\n")
#> {
#> "schema_version": 1,
#> "nestedscales": "0.2.0",
#> "class": "nestedscales::NestedScale",
#> "name": "fuels",
#> "key": "fuel",
#> "frames": ["group", "phase", "carrier", "fuel"],
#> "members": {
#> "group": ["Fossil", "Bio", "Power"],
#> "phase": ["solid", "liquid", "gas"],
#> "carrier": ["Coal", "Oil", "Gas", "Biomass", "Electricity"],
#> "fuel": ["Anthracite", "Lignite", "Diesel", "Gasoline", "NaturalGas", "Wood", "Biodiesel", "Biogas", "Electricity"]
back <- read_scale(path)
identical(scale_frames(back), scale_frames(x))
#> [1] TRUE
identical(scale_attributes(back), scale_attributes(x))
#> [1] TRUEwrite_scale(format = "yaml") writes the same header as
YAML, and read_scale() accepts either. Installed packages
register directories of definitions; scale_catalog() lists
them and read_scale(id) opens one by its id:
One package-level registry changes behaviour explicitly.
Rules map value-column names to their aggregation rule;
there is no fallback, and an unregistered column without
rule= is an error:
SCALE_RULES
#> [1] "sum" "weighted_mean" "mean" "copy"
#> [5] "sd" "share" "logshare"
register_scale_rule("tj", "sum")
register_scale_rule("price", "weighted_mean", weight = "tj")
get_scale_rule("price")
#> $rule
#> [1] "weighted_mean"
#>
#> $weight
#> [1] "tj"
#>
#> $scope
#> NULL
list_scale_rules()
#> param rule weight scope
#> 1 price weighted_mean tj <NA>
#> 2 tj sum <NA> <NA>
clear_scale_rules()A rule may be scoped to one dimension with scope=, so a
model’s tj column aggregates the same way over fuels,
regions and months without being registered three times – or
differently, when it should.
vignette("nestedscales") – the tour of the main
functions.vignette("attributes") – attributes, aliases and
conversion on the shipped carriers.