A fuel-use table has three dimensions a model cares about: which fuel, where, when. Each is a nested scale – the fuel taxonomy the core ships, and the classes of two sibling packages built on the same core – and each attaches to a table under its own name:
es <- read_scale("carriers") # fuels: group > carrier > fuel
gs <- geoscale_example() # regions: country > state > zone > atom
cal <- calendar("m12") # time: MONTH
scale_units(es, "carrier")
#> [1] "FossilCoal" "FossilOil" "FossilGas"
#> [4] "Bioenergy" "Hydro" "Wind"
#> [7] "Solar" "Geothermal" "OtherRenewables"
#> [10] "Nuclear" "Electricity" "Heat"
#> [13] "Hydrogen" "HydrogenDerivatives" "SyntheticFuels"
#> [16] "Storage" "Other"
geoscale_regions(gs, "country")
#> [1] "N" "S"
calendar_timeslices(cal)
#> [1] "m01" "m02" "m03" "m04" "m05" "m06" "m07" "m08" "m09" "m10" "m11" "m12"Twelve months of fuel use in six regions for five fuels, in kilotonnes. The numbers are made up but shaped: coal and gas follow a heating season, the size of each region scales its use.
use <- expand.grid(
atom = geoscale_regions(gs)[geoscale_regions(gs) != "ROW"],
timeslice = calendar_timeslices(cal),
fuel = c("Anthracite", "Lignite", "DistillateFuelOil", "NaturalGas",
"Biodiesel"),
stringsAsFactors = FALSE
)
base <- c(Anthracite = 2, Lignite = 6, DistillateFuelOil = 1.5,
NaturalGas = 4, Biodiesel = 0.3)
seasonal <- c(Anthracite = 0.6, Lignite = 0.6, DistillateFuelOil = 0.1,
NaturalGas = 0.8, Biodiesel = 0)
size <- geoscale_leaftable(gs) |>
transmute(atom, size = km2 / mean(km2))
use <- use |>
left_join(size, by = "atom") |>
mutate(
month = match(timeslice, calendar_timeslices(cal)),
kt = base[fuel] * size *
(1 + seasonal[fuel] * cos(2 * pi * (month - 1) / 12))
) |>
select(atom, timeslice, fuel, kt)
head(use)
#> atom timeslice fuel kt
#> 1 A1 m01 Anthracite 0.7225806
#> 2 A2 m01 Anthracite 1.4451613
#> 3 A3 m01 Anthracite 2.1677419
#> 4 A4 m01 Anthracite 2.8903226
#> 5 A5 m01 Anthracite 3.6129032
#> 6 A6 m01 Anthracite 4.3354839Each join adds columns prefixed by the scale’s name, so the three never collide and a reader can see which scale a column came from:
labelled <- use |>
join_scale(es, frame = "fuel", attach = TRUE) |>
join_geoscale(gs = gs, geoframe = "atom", geoframes = TRUE) |>
join_calendar(calendar = cal, meta = TRUE)
names(labelled)
#> [1] "atom" "timeslice" "fuel" "kt"
#> [5] "carriers.group" "carriers.carrier" "carriers" "example.country"
#> [9] "example.state" "example.zone" "example" "m12"
#> [13] "m12.share" "m12.weight"
head(labelled, 3)
#> atom timeslice fuel kt carriers.group carriers.carrier
#> 1 A1 m01 Anthracite 0.7225806 FossilFuels FossilCoal
#> 2 A2 m01 Anthracite 1.4451613 FossilFuels FossilCoal
#> 3 A3 m01 Anthracite 2.1677419 FossilFuels FossilCoal
#> carriers example.country example.state example.zone example m12 m12.share
#> 1 Anthracite N N1 N1 A1 m01 0.08493151
#> 2 Anthracite N N1 N1 A2 m01 0.08493151
#> 3 Anthracite N N2 ZB A3 m01 0.08493151
#> m12.weight
#> 1 744
#> 2 744
#> 3 744carriers.carrier and carriers.group come
from the fuel scale, example.country and friends from the
GeoScale, m12.share and m12.weight (hours)
from the Calendar.
The fuel scale carries the IPCC 2006 net calorific value (GJ/t) and CO2 factor (kg/TJ) for these five fuels. Each conversion is one call, keyed by fuel, and the factor applied is reported alongside the result:
energy <- use |>
mutate(t = kt * 1e3) |>
select(atom, timeslice, fuel, t) |>
convert_scale(es, "lhv", from_unit = "t", to_unit = "GJ") |>
transmute(atom, timeslice, fuel, tj = t / 1e3)
co2 <- energy |>
convert_scale(es, "ef_co2", from_unit = "TJ", to_unit = "kg") |>
rename(kg_co2 = tj) |> # the converted column: kg CO2
transmute(atom, timeslice, fuel, tj = energy$tj, kt_co2 = kg_co2 / 1e6)
head(co2, 3)
#> atom timeslice fuel tj kt_co2
#> 1 A1 m01 Anthracite 19.29290 1.896492
#> 2 A2 m01 Anthracite 38.58581 3.792985
#> 3 A3 m01 Anthracite 57.87871 5.689477Every scale aggregates with the same verb and the same rules. Columns the scale does not know (the other dimensions’ keys) are carried through as identifiers. One registration covers all three dimensions:
register_scale_rule("tj", "sum")
register_scale_rule("kt_co2", "sum")By carrier and month – the fuel dimension collapses, region and month stay:
by_carrier <- co2 |>
recast_scale(es, source = "fuel", target = "carrier",
missing_sources = "ignore") |>
filter(!is.na(tj))
#> Warning: 66 source unit(s) present in the scale but missing from the data (e.g.
#> Ammonia, AssociatedGas, Battery, ... (66 total)); produced NAs
head(by_carrier, 3)
#> carrier atom timeslice tj kt_co2
#> 1 FossilCoal A1 m01 45.08903 4.501901
#> 2 FossilOil A1 m01 16.02097 1.187154
#> 3 FossilGas A1 m01 78.03871 4.377972
sum(by_carrier$tj) - sum(co2$tj)
#> [1] 0The table reports five of the 71 fuels. With
missing_sources = "ignore" the others count as unused
rather than unknown, and the total is conserved – the difference above
is zero.
By country – the pipeline verb recast()
dispatches on whichever scale it is handed, so the two aggregations
chain:
by_country <- co2 |>
recast(es, target = "carrier", missing_sources = "ignore") |>
filter(!is.na(tj)) |>
recast(gs, target = "country")
#> Warning: 66 source unit(s) present in the scale but missing from the data (e.g.
#> Ammonia, AssociatedGas, Battery, ... (66 total)); produced NAs
by_country |>
group_by(country) |>
summarise(tj = sum(tj), kt_co2 = sum(kt_co2))
#> # A tibble: 2 × 3
#> country tj kt_co2
#> <chr> <dbl> <dbl>
#> 1 N 10551. 775.
#> 2 S 11607. 852.By quarter – a calendar recast needs the year, to place the months on real dates:
by_quarter <- by_carrier |>
select(carrier, atom, timeslice, tj, kt_co2) |>
recast_calendar(source = cal, target = calendar("q4"), year = 2024)
head(by_quarter, 4)
#> timeslice carrier atom tj kt_co2
#> 1 Q1 FossilCoal A1 124.54761 12.435420
#> 2 Q2 FossilCoal A1 61.44465 6.134923
#> 3 Q3 FossilCoal A1 44.53626 4.446710
#> 4 Q4 FossilCoal A1 107.63922 10.747207
sum(by_quarter$tj) - sum(by_carrier$tj)
#> [1] 0energypal’s carriers palette is the same taxonomy, so
the carrier ids map straight onto its colours with no relabelling:
by_carrier |>
group_by(timeslice, carrier) |>
summarise(tj = sum(tj), .groups = "drop") |>
ggplot(aes(x = timeslice, y = tj, fill = carrier)) +
geom_col() +
energypal::scale_fill_energy(palette = "carriers",
label_style = "short") +
labs(x = NULL, y = "TJ", fill = NULL,
title = "Energy use by month and carrier") +
theme_minimal()
by_country |>
group_by(country, carrier) |>
summarise(kt_co2 = sum(kt_co2), .groups = "drop") |>
ggplot(aes(x = country, y = kt_co2, fill = carrier)) +
geom_col() +
energypal::scale_fill_energy(palette = "carriers",
label_style = "short") +
labs(x = NULL, y = "kt CO2", fill = NULL,
title = "CO2 by country and carrier") +
theme_minimal()
The GeoScale draws its own hierarchy with data attached – CO2 summed to every geoframe at once:
co2_by_atom <- co2 |>
group_by(atom) |>
summarise(kt_co2 = sum(kt_co2), .groups = "drop")
geoscale_autoplot(gs, data = co2_by_atom, z = "kt_co2", rule = "sum")
Real sources spell carriers their own way. energypal ships a slice of
Our World in Data’s electricity mix; its source labels
resolve against the fuel scale’s names and aliases exactly, with
frame = "carrier" settling the names that exist at two
frames (Nuclear, Other):
mix <- energypal::owid_energy_mix
matched <- scale_match(es, unique(mix$source), frame = "carrier")
matched
#> label frame unit method candidates
#> 1 Bioenergy carrier Bioenergy exact <NA>
#> 2 Coal carrier FossilCoal alias <NA>
#> 3 Hydro carrier Hydro exact <NA>
#> 4 Natural Gas carrier FossilGas alias <NA>
#> 5 Nuclear carrier Nuclear exact <NA>
#> 6 Oil carrier FossilOil alias <NA>
#> 7 Solar carrier Solar exact <NA>
#> 8 Wind carrier Wind exact <NA>
#> 9 Other carrier Other exact <NA>Join the resolved id on and the palette applies:
mix |>
filter(year == 2024) |>
left_join(matched |> select(source = label, carrier = unit),
by = "source") |>
ggplot(aes(x = country, y = generation_twh, fill = carrier)) +
geom_col(position = "fill") +
energypal::scale_fill_energy(palette = "carriers",
label_style = "short") +
labs(x = NULL, y = "share of generation", fill = NULL,
title = "Electricity generation mix, 2024 (OWID)") +
theme_minimal()
| package | contributed | |
|---|---|---|
| fuels | nestedscales (carriers) |
the taxonomy, calorific values, CO2 factors, label matching |
| regions | geoscales | the region hierarchy, aggregation to countries, the icicle |
| time | timescales | month shares and hours, aggregation to quarters |
| colours | energypal | one palette for every figure, by carrier id |
The core is what lets one table carry all three and lets
recast() chain across them.