vignettes/hierarchical-forecasting.Rmd
hierarchical-forecasting.RmdThe finnts package leverages the great work of the hts package. It’s currently retired but we think old dogs can still learn new tricks! It’s still of great use to finnts and allows for both standard and grouped hierarchical forecasting.
Use detect_hierarchy() before a non-agentic forecast to
choose a structurally valid approach. A unique bottom-level ID alone
does not prove nesting: intermediate labels can still occur under
multiple parents. Detection checks every adjacent parent-child
relationship in the same cardinality-based order used by the engine.
decision <- finnts::detect_hierarchy(
input_data = data,
combo_variables = c("Region", "Country", "Site"),
target_variable = "Revenue",
combo_cleanup_date = as.Date("2025-08-01"),
hist_end_date = as.Date("2026-08-01")
)
decision$forecast_approach
decision$conflicts
# Pass decision$forecast_approach as forecast_approach to prep_data(),
# using the same input data and cleanup settings.The function returns diagnostics in memory and does not save files or
change the caller’s input. Without a cleanup date it considers all
supplied tuples; with a cleanup date it uses the existing target-sum
cleanup policy and excludes future targets from activity. One retained
series selects bottoms_up, and no retained series produces
an error. Character-like labels are trimmed consistently with
preprocessing; invalid labels and ambiguous combo identities fail
explicitly. Collect Spark data frames before detection.
Detection describes observed data, not unobserved business
relationships. Inspect hierarchy_order and
conflicts when validating the result. Existing explicit
prep_data() settings are not overridden. Agent setup and
EDA share the structural analysis, with an intentional single-column
bottoms-up policy. Adopting a different approach changes the prepared
aggregates and hierarchy metadata; use a new run rather than reusing
artifacts from a different hierarchy.
New Agent versions with only one retained series use
bottoms_up, even with multiple combo columns. EDA reports
none, and global reasoning allows only bottoms-up for this
scope. Pre-expanded hierarchy-level ID inputs also use
bottoms-up internally, but retain their configured outer
reconciliation.
Resuming an existing Agent version preserves its saved forecast
approach and previously uploaded inputs; a package upgrade does not
reclassify that version. API request-ID retries select the version
belonging to that request, even if a newer request has since created
another version. Logged settings such as dates, horizon, regressors, and
forecast flags must still match. Supplied replacement data values are
not uploaded on a resume; create a new version to use changed history,
drivers, or series. Missing required saved artifacts remain errors
rather than triggering a hierarchy migration. Legacy bottoms-up run logs
without allow_hierarchical_forecast cannot reveal whether
expansion was disabled or the single-column policy selected bottoms-up;
these retain the existing flag-comparison fallback. A saved hierarchical
approach proves expansion was enabled. No new log columns are added.
A new version uses current detection on its cleaned population. If
its outer approach differs from the latest completed predecessor (for
example, standard to grouped, or hierarchical to single-series
bottoms-up), update_forecast() rejects reuse and directs
the user to iterate_forecast(). The
allow_iterate_forecast performance fallback does not bypass
this compatibility check. Existing predecessor artifacts are not
rewritten.
Hierarchical external regressors retain the existing additive convention. A level-mapped driver must have at most one nonmissing value for each source tuple and date. Inference first checks single columns, then progressively wider tuples only if no narrower grain is valid. At the first valid width, it selects the grain with the fewest distinct grain/date/value tuples; equally coarse candidates must identify equivalent source partitions. Inference checks at most 1,024 candidate grains per driver. Each candidate-width batch is evaluated completely or rejected before scanning. If the search budget is exhausted, preparation fails before hierarchy artifacts are written; it does not silently fall back to bottom-level summation. Reduce redundant combo columns or prepare the driver at its known source grain before retrying. Aggregates count each source tuple once within the node’s actual bottom-series membership, including separate tuples with equal numeric values. Original bottom observations are preserved; missing-only level aggregates stay missing. Date-global drivers are copied, while full-bottom drivers retain the existing bottom-level summation and missing-value handling. Equally coarse candidate levels must describe the same partition or preparation fails with an ambiguity error. Observed equality cannot establish business meaning: validate the source grain and additive assumption before preparing a production forecast. These corrections can change newly prepared driver values. Use a new run or Agent version; existing saved inputs and fitted models are not migrated.
A standard hierarchy of data is pretty straight forward. Each combo variable can be aggregated into the next combo variable when producing forecasts at higher aggregations of the data. This structure resembles a pyramid, with the bottom being the lowest granularity of time series and the top being a single time series of the grand total of the data. Below is a good example with geographical combo variables that can be aggregated into one another when building a standard hierarchy forecast.
#> Standard Hierarchical Time Series Data
#> # A tibble: 9 × 5
#> Continent Country City Date Target
#> <chr> <chr> <chr> <date> <dbl>
#> 1 North America United States Kansas City 2020-01-01 100
#> 2 North America United States Kansas City 2020-02-01 250
#> 3 North America United States Kansas City 2020-03-01 320
#> 4 North America United States Seattle 2020-01-01 80
#> 5 North America United States Seattle 2020-02-01 200
#> 6 North America United States Seattle 2020-03-01 270
#> 7 North America Mexico Mexico City 2020-01-01 50
#> 8 North America Mexico Mexico City 2020-02-01 80
#> 9 North America Mexico Mexico City 2020-03-01 120
In the above example, “City” was the lowest level of the hierarchy,
which feeds into “Country”, which then feeds into “Continent”. For a
non-agentic run, Finn forecasts each model at the prepared hierarchy
levels, selects the best forecast at each level, and reconciles both the
selected combination and the individual models back down to the lowest
level. get_forecast_data(run_info) returns every
successfully saved per-model reconciled forecast plus
Best-Model. Models that fail to produce a reconciled
artifact are not fabricated in the result. Filter
Best_Model == "Yes" to retain only the reconciled selected
forecast.
Agentic runs use a different output contract. During
iterate_forecast(), different series can run and select
different model sets, recipes, or averages. Finn therefore reconciles
the selected forecasts together, and
get_agent_forecast(agent_info) returns only that final
reconciled Best-Model forecast, not a reconciled comparison
table for every model. Best-Model can combine different
winners across series; it does not mean that the same model family won
everywhere. This distinction applies to both standard and grouped
hierarchies and to Agent forecast updates.
Grouped hierarchies are very different than the traditional hierarchy approach described above. There are some data sets that can be aggregated in various ways, meaning they need to follow another approach the hts package calls “grouped”. A good example is a data set that contains historical time series by geography, customer segment, and product.
#> Grouped Hierarchical Time Series Data
#> # A tibble: 12 × 5
#> Country Segment Product Date Target
#> <chr> <chr> <chr> <date> <dbl>
#> 1 United States Enterprise Coffee 2020-01-01 10
#> 2 United States Enterprise Coffee 2020-02-01 20
#> 3 United States Enterprise Coffee 2020-03-01 30
#> 4 United States Public Sector Coffee 2020-01-01 5
#> 5 United States Public Sector Coffee 2020-02-01 8
#> 6 United States Public Sector Coffee 2020-03-01 11
#> 7 Mexico Enterprise Coffee 2020-01-01 20
#> 8 Mexico Enterprise Coffee 2020-02-01 23
#> 9 Mexico Enterprise Coffee 2020-03-01 27
#> 10 Mexico Enterprise Tea 2020-01-01 50
#> 11 Mexico Enterprise Tea 2020-02-01 55
#> 12 Mexico Enterprise Tea 2020-03-01 60
It would be hard to aggregate the above data in a traditional hierarchy. The same products are found in different segments and countries, also the same segments are found in multiple countries. Finn will follow a similar modeling process as the one described for a traditional hierarchy, but instead will create forecasts at the below levels.
Standalone hierarchy runs can apply a reviewed custom rule independently to every prepared node, including aggregates, then use FinnTS’s regular hts reconciliation to publish the lowest level. Review must explicitly permit this adjustment: reconciled values are not guaranteed to equal the rule’s original base predictions.
An HTS forecast using custom models requires a shared, explicitly
enrolled mixed custom/FinnTS candidate pool. Custom-only requests must
use bottoms_up with a compatible bottom-up definition. Finn
does not silently add competitors or switch approaches. Eligibility
depends on requested candidates, not winners; custom models may win
everywhere. Existing built-in-only hierarchy behavior is unchanged.
custom <- create_custom_model(
instructions = paste(
"For each hierarchy node, forecast the mean of all historical targets.",
"Allow normal FinnTS reconciliation to adjust base forecasts."
),
llm = llm, input_data = data_tbl,
combo_variables = c("Region", "Product"), target_variable = "value",
date_type = "month", forecast_horizon = 3,
model_type = "local", name = "business_mean",
control = custom_model_control(forecast_approach = "standard_hierarchy")
)
run_info <- set_run_info(data_output = "csv", object_output = "rds")
forecast_time_series(
run_info, data_tbl, c("Region", "Product"), "value", "month", 3,
forecast_approach = "standard_hierarchy",
custom_models = list(business_mean = custom),
models_to_run = c("business_mean", "meanf"),
recipes_to_run = c("R1", "C1"), stationary = FALSE,
clean_missing_values = FALSE, clean_outliers = FALSE,
run_local_models = TRUE, run_global_models = FALSE,
run_ensemble_models = FALSE, average_models = TRUE,
feature_selection = FALSE, inner_parallel = FALSE,
negative_forecast = TRUE, weekly_to_daily = FALSE
)The input must contain a complete finite leaf panel over the requested historical window. This custom profile does not silently zero-fill missing observations. Raw authoring samples up to three deterministic leaf series and keeps all their derived nodes; prepared authoring reads the whole existing hierarchy without modifying it. Both enforce a 10000 historical-node-row bound and validate every captured node. Manual and automatic approval use the same gates. Individual authoring validation does not enroll a custom-only HTS run.
Old bottom-up definitions and hashes remain valid but do not acquire hierarchy permission. New hierarchy definitions bind one approach, per-node application and permission for reconciliation. The approved envelope and registry format are unchanged. Run manifests additionally bind topology and input artifacts. Changed context or missing base candidates cannot be treated as a completed restart.
Custom hierarchy requires average_models = FALSE, no
external regressors, no target transformations or cleaning, and no
pooled/global fitting. Weekly output stays weekly. Every enrolled
candidate must have complete finite node/fold/date coverage before
reconciliation; missing nodes are not filled from snaive
and failed candidates are not silently skipped. Normal weights and
selection policy remain unchanged. Per-model results retain their
readable aliases and full version IDs; the selected mixture is published
as Best-Model.
The following matrix distinguishes implementation from qualification evidence. It does not imply every cross-product of formats, cadences and workers was tested.
| Capability | Current Evidence or Boundary |
|---|---|
| Standard/grouped, local R1, monthly CSV | Implemented-tested: real mixed forecasts, strict coverage and exact-version restarts |
| Grouped monthly CSV, two local-machine workers | Implemented-tested: sequential parity, two worker PIDs and exact installed namespace |
| Year/quarter/month/week/day at native cadence | Implemented-tested: independent reconciliation projection and date-key checks; complete nonmonthly pipeline qualification remains open |
| RDS and Parquet data, RDS objects | Implemented-tested: preparation, context capture and exact-path reload; full forecasting/worker combinations remain unverified |
| Raw/prepared hierarchy review | Implemented-tested: real installed TEST/APPROVE children, complete-node evidence and authored mixed forecast |
| Other installed compatible local R1 FinnTS competitors | Inert workflow/dependency checks implemented; individual model-family qualification remains open |
| Custom-only HTS | Disallowed by explainability policy; use compatible bottom-up definitions |
| Foundation-model competitors | Not qualified for this custom hierarchy profile; rejected rather than counted as token competitors |
| Drivers, pooling, R2, transformed targets, hierarchy averages, weekly expansion | Remaining work; rejected by the current profile |
| Agent hierarchy and hierarchical new-data updates | Remaining work; current custom Agent behavior remains bottom-up only |
| Remote/Spark, other platforms, minimum dependency versions and live providers | Not qualified here; separate approval and validation required |
The per-series routing interface is deferred. Future user-specific
model restrictions will require bottoms_up; ordinary
node-level winner differences and internal worker partitions are not
routing restrictions. Users requiring exact single-rule bottom-up output
must also explicitly disable averaging. Trusted source and RDS are not
sandboxed or authenticated approval. Offline provider fixtures verify
control flow, not live generation quality.
The aggregation process is automatically calculated for each external regressor, depending on how the regressor maps to the combo variables. If a regressor is unique for each time series, then the standard aggregation process is implemented (same as the process for the target variable). If the regressor repeats the same values across all time series, then only one global copy is retained and applied to all new aggregated time series. If the regressor specifically maps to one or more combo variables, then the relationship is respected while still being able to aggregate to the total level. Aggregations of drivers are always summed.
Intermediate standard-hierarchy nodes use their actual bottom-series
membership, even when hts assigns synthetic node names.
Each node sums only its own source entities, rather than inheriting a
grand-total driver value.
Explore the final results of the aggregations by seeing the end
result using get_prepped_data().
There is also a small limitation when doing hierarchical forecasting using spark as the parallel computing back-end. The hts package finnts uses cannot handle spark data frames. So behind the scenes finnts has to bring all of the data into memory on the spark cluster driver node in order to create the hierarchies. Please keep that in mind. If you have a lot of historical data that is greater than the RAM on the spark driver node, please consider using a different VM size with more memory. The same issue holds true when reconciling the hierarchical forecast, where all of the forecasts across all time series for a specific model is loaded into the same executor node to be reconciled. Because of these limitations, we are now exploring other options outside of the hts package, including the building of our own hierarchical forecasting package. Stay tuned.
Some forecasts can be widely off from the target value. This can cause issues when finnts tries to use historical back testing residuals as the initial weights in hts to reconcile the forecast at various aggregations of your data down to the lowest level. To prevent reconciliation issues, finnts will shrink these residuals to more appropriate numbers that still convey they are considerably off from the initial target values, but not astronomical. For example a residual value of 1,000,000,000,000 compared to a target value of 100 might get shrunk down to 900, since keeping the initial residual will throw the weighted reconciliation process out of whack and cause issues. Most forecasts in finnts will not have this problem, but if your data is very noisy then this fix will help prevent any issues when trying to run hierarchical forecasts in finnts.