Public Documentation
Documentation for ForecastEnsembles's public interface.
Contents
Index
ForecastEnsembles.BLPForecastEnsembles.CRPSStackingForecastEnsembles.EnsembleMethodForecastEnsembles.EnsembleWeightsForecastEnsembles.FittedBLPForecastEnsembles.FittedCRPSStackingForecastEnsembles.FittedHedgeForecastEnsembles.FittedInverseScoreForecastEnsembles.FittedPartialPoolingForecastEnsembles.FittedQRAForecastEnsembles.FittedStackingForecastEnsembles.ForecastTableForecastEnsembles.HedgeForecastEnsembles.InverseScoreForecastEnsembles.LinearPoolForecastEnsembles.MixtureEnsembleForecastEnsembles.PartialPoolingForecastEnsembles.QRAForecastEnsembles.QuantileEnsembleForecastEnsembles.StackingForecastEnsembles.TrainedMethodForecastEnsembles.TrimmedMeanForecastEnsembles.UnfittedMethodForecastEnsembles.WindowedDataFrames.combineForecastEnsembles.backtestForecastEnsembles.effective_num_modelsForecastEnsembles.model_idsForecastEnsembles.output_typeForecastEnsembles.task_id_colsForecastEnsembles.weight_stabilityForecastEnsembles.weightsStatsAPI.fit
Public API
ForecastEnsembles.BLP Type
BLP(; weights = nothing)Beta-transformed linear pool (Gneiting & Ranjan 2013): a recalibrated mixture that corrects the linear opinion pool's underdispersion by passing the mixture CDF through a fitted Beta CDF.
fit(BLP(), training, observations) fits the Beta to the linear pool's PIT values on the training set (maximum likelihood); combine(ft, fitted) applies it by evaluating the linear pool's quantile function at Beta-remapped levels. weights are the underlying per-model pool weights (equal by default, or any per-model EnsembleWeights / fitted method); the Beta parameters are learned on top. Quantile forecasts only.
Because the fit is a Beta on PIT values rather than a full weight regression, BLP is a recalibration of the pool: weights returns nothing (the level remap is not expressible as model weights), so apply it with combine(ft, fitted).
Fields
weights: per-model pool weights, ornothingfor equal weights.
ForecastEnsembles.CRPSStacking Type
CRPSStacking(; dirichlet_alpha = 1.001,
lambda = nothing, time_col = nothing,
task_weights = nothing)CRPS-stacked linear opinion pool. Mirrors lopensemble::crps_weights, including its time weighting.
By default every training task contributes equally to the objective. Two ways to change that:
task_weights: aDataFramewith the training table's task-id columns plus a:weightcolumn — one non-negative weight per task. The general mechanism; covers recency, per-region weighting (lopensemble'sgamma), down-weighting anomalous reporting weeks, and so on.lambdawithtime_col: convenience for recency weighting.time_colnames the task column that orders tasks in time;lambdais one ofa scalar
φ ∈ (0, 1]: exponential decay, weightφ^(T − t)for the t-th of T ordered unique time values (the common forecasting- literature choice;φ = 1recovers equal weights),:lopensemble: the quadratic ramp2 − (1 − t/T)²thatlopensemble::crps_weightsuses by default (oldest ≈ 1, newest 2),a
Vector{Float64}with one weight per ordered unique time value (lopensemble's vector form),a function of the normalised time rank
t/T ∈ (0, 1]returning a weight.
lambda and task_weights are mutually exclusive. The Dirichlet prior strength scales with the effective sample size (Σλ)²/Σλ² rather than the raw task count, so heavy down-weighting of history does not quietly strengthen the prior relative to the data.
ForecastEnsembles.EnsembleMethod Type
EnsembleMethodTop of the method-type hierarchy. Subtypes split into
UnfittedMethod— can be passed tocombinedirectly.TrainedMethod— must be passed throughfitfirst to obtain a fitted counterpart, which is itself anUnfittedMethodand can then be passed tocombine.
ForecastEnsembles.EnsembleWeights Type
EnsembleWeights(data; shape = :auto)A typed container for per-model or per-quantile ensemble weights. The underlying data::DataFrame always has columns :model_id and :weight, and optionally :output_type_id. Two shapes are supported:
:per_model— columns:model_id, :weight. A single weight per model applied at every quantile/sample/etc.:per_quantile— columns:model_id, :output_type_id, :weight. Weights vary across quantile levels.
Construct from a DataFrame (shape inferred from columns by default) or by calling weights(m) on a fitted method. MixtureEnsemble and QuantileEnsemble accept any of: a raw DataFrame, an EnsembleWeights, or any fitted method for which weights(m) returns one. The conversion happens at construction of the method, not at combine time, so invalid inputs fail fast.
ForecastEnsembles.FittedBLP Type
FittedBLP(alpha, beta, weights)Output of fit(::BLP, …). Stores the fitted Beta shape parameters alpha and beta and the underlying pool weights. alpha = beta = 1 means the linear pool was already calibrated (no transform). Apply with combine(ft, fitted); weights returns nothing.
ForecastEnsembles.FittedCRPSStacking Type
FittedCRPSStacking(weights, models)Output of fit(::CRPSStacking, …). Stores the simplex-constrained ensemble weights as a DataFrame with columns model_id and weight, the list of models these weights refer to, and crps, the fitted objective value (the mean CRPS achieved at the optimum). Plug into combine(ft, fitted) (sample inputs) — internally a LinearPool with these weights.
ForecastEnsembles.FittedHedge Type
FittedHedge(weights, models, trajectory)Output of fit(::Hedge, …). Stores the final simplex weights (a DataFrame with columns model_id and weight), the component models in weight order, and the trajectory — a long DataFrame (time_col, model_id, weight) of the weights after each update, for diagnosing weight stability over time. Plug into combine(ft, fitted) — internally a LinearPool with the final weights.
ForecastEnsembles.FittedInverseScore Type
FittedInverseScore(weights, models, scores)Output of fit(::InverseScore, …). Stores the simplex weights (a DataFrame with columns model_id and weight), the component models in weight order, and the per-member mean scores they were derived from. Plug into combine(ft, fitted) — internally a LinearPool with these weights.
ForecastEnsembles.FittedPartialPooling Type
FittedPartialPooling(weights, global_weights, strata, models, score_value)Output of fit(::PartialPooling, …). Stores the per-stratum weights (a DataFrame with the strata columns plus model_id and weight), the pooled global_weights (model_id, weight) used for strata not seen in training, the strata columns, the component models, and the mean score_value at the optimum. Plug into combine(ft, fitted) — it applies each stratum's weights, falling back to global_weights for an unseen stratum. weights(fitted) returns the pooled global vector as an EnsembleWeights.
ForecastEnsembles.FittedQRA Type
FittedQRA(coefs, intercepts, models, levels, group_cols, per_quantile_weights, enforce_normalisation, has_intercept)Output of fit(::QRA, …). coefs is a Dict{NamedTuple => Vector{Float64}} mapping a (group..., quantile_level) key to a vector of model coefficients in the order models. When per_quantile_weights is false, all keys sharing the same group share the same coefficients (and have the same key under a sentinel :any quantile_level).
Fields
coefs:Dictmapping each(group, quantile_level)key to its vector of model coefficients (ordered asmodels).intercepts:Dictmapping each(group, quantile_level)key to its intercept; empty (all zero) when the fit has no intercept.models: component model ids, giving the order of the coefficient vectors.levels: the quantile levels the fit was trained on.group_cols: the grouping task columns used when fitting; may be empty.per_quantile_weights:Bool, whether coefficients vary across quantile levels.enforce_normalisation:Bool, whether the simplex constraint (non-negative coefficients summing to one) was imposed.has_intercept:Bool, whether an intercept was estimated.
ForecastEnsembles.FittedStacking Type
FittedStacking(weights, models, score_value)Output of fit(::Stacking, …). Stores the simplex ensemble weights (a DataFrame with columns model_id and weight), the component models in weight order, and the mean score_value achieved at the optimum. Plug into combine(ft, fitted) — internally a LinearPool with these weights.
ForecastEnsembles.ForecastTable Type
ForecastTableA long-format probabilistic-forecast table aligned with the hubverse model_out_tbl schema.
Required columns
the model identifier column (default
:model_id):output_type— symbol, one ofForecastEnsembles.KNOWN_OUTPUT_TYPES:output_type_id— quantile level, sample index, threshold, …:value— the forecast valueone or more task-id columns (e.g.
:location,:horizon,:target_date)
Construction
ForecastTable(df; task_id_cols, model_id_col = :model_id)task_id_cols may be omitted, in which case it is inferred as every column that is not one of the required columns.
Fields
data: the underlying long-formatDataFrameholding the forecasts.task_id_cols: the task-id columns identifying a forecast target.model_id_col: the column naming the model that produced each forecast.
ForecastEnsembles.Hedge Type
Hedge(score; eta = 1.0, time_col)Online ensemble weighting by the Hedge / exponentiated-gradient rule.
fit(Hedge(score; time_col), training, observations) walks the distinct time_col values in order and, at each step, multiplies every member's weight by exp(-eta · sₜ) — where sₜ is that member's mean score on the current step (negatively oriented, so a lower score keeps more weight) — then renormalises to the simplex. A member absent at a step keeps its weight (a "sleeping expert"). The final weights plug into combine; the full trajectory is kept for weight-stability diagnostics.
Unlike InverseScore (one pooled score per member) this adapts to when members did well, so it tracks regime change; unlike Stacking it needs no optimiser and updates incrementally. score is any negatively-oriented weighted-sample rule from ScoringRules (e.g. ScoringRules.crps), which must be loaded — it is a weak dependency.
Fields
score: the scoring-rule function (negatively oriented).eta: learning rate. Larger values adapt faster and concentrate weight more aggressively on recent winners; as it tends to0the weights stay uniform. Scale it to the magnitude of your score. Must be positive.time_col: the task-id column defining the update order.
ForecastEnsembles.InverseScore Type
InverseScore(score; temperature = 1.0)Performance weighting: score each member independently and weight the better ones more heavily — wᵢ ∝ exp(−temperature · sᵢ), where sᵢ is member i's mean score over the training set (negatively oriented, so a lower score earns more weight). No optimisation, so it is fast and robust with few observations; but — unlike Stacking — it scores each member in isolation and never sees how they combine, so it is blind to redundancy between them.
fit(InverseScore(score), training, observations) returns a FittedInverseScore that plugs into combine / weights. score is any negatively-oriented weighted-sample rule from ScoringRules (e.g. ScoringRules.crps), which must be loaded — it is a weak dependency.
Fields
score: the scoring-rule function (negatively oriented).temperature: sharpness of the softmax over member scores. As it tends to0the weights approach equal; large values approach winner-take-all. Must be positive.
ForecastEnsembles.LinearPool Type
MixtureEnsemble(; weights = nothing, n_samples = 10_000)Mixture (linear-opinion-pool) ensemble: the ensemble distribution is the (weighted) mixture of the component distributions, F = Σᵢ wᵢ Fᵢ. The algorithm path depends on the forecast output_type:
:sample— weighted resample from per-model samples.:cdf— pointwise weighted average of CDFs.:quantile— reconstruct each model's CDF from its quantiles, drawn_samples, pool, and re-extract quantiles at the original levels.
Mixture pooling is fundamentally a per-model operation; per-quantile weights aren't meaningful here (use QuantileEnsemble for that).
ForecastEnsembles.MixtureEnsemble Type
MixtureEnsemble(; weights = nothing, n_samples = 10_000)Mixture (linear-opinion-pool) ensemble: the ensemble distribution is the (weighted) mixture of the component distributions, F = Σᵢ wᵢ Fᵢ. The algorithm path depends on the forecast output_type:
:sample— weighted resample from per-model samples.:cdf— pointwise weighted average of CDFs.:quantile— reconstruct each model's CDF from its quantiles, drawn_samples, pool, and re-extract quantiles at the original levels.
Mixture pooling is fundamentally a per-model operation; per-quantile weights aren't meaningful here (use QuantileEnsemble for that).
ForecastEnsembles.PartialPooling Type
PartialPooling(score; strata, lambda = 1.0, dirichlet_alpha = 1.0)Hierarchical (partially pooled) stacking: learn a weight vector per stratum that shrinks toward a shared global vector, so a data-sparse stratum borrows strength from the rest.
fit(PartialPooling(score; strata), training, observations) jointly optimises, in softmax space, one logit vector per distinct combination of the strata columns plus a global logit vector, minimising the mean score of each stratum's linearly-pooled forecast plus a shrinkage penalty pulling every stratum toward the global vector. The FittedPartialPooling result plugs into combine, which applies each stratum's own weights (an unseen stratum falls back to the global vector).
score is any negatively-oriented weighted-sample rule from ScoringRules (e.g. ScoringRules.crps), which must be loaded — it is a weak dependency. Generalises Stacking: a single stratum, or lambda → ∞, recovers global stacking.
Fields
score: the scoring-rule function to minimise (negatively oriented).strata: task-id columns whose value combinations define the strata (e.g.[:location],[:location, :age_group]).lambda: shrinkage strength toward the global vector.0fits each stratum independently; large values pool them toward one shared vector. Must be≥ 0.dirichlet_alpha: strength of a symmetric-Dirichlet prior on each stratum's weights, pulling them toward the simplex centre;1.0applies no prior.
ForecastEnsembles.QRA Type
QRA(; per_quantile_weights = false, intercept = true,
enforce_normalisation = false, noncross = false,
group = Symbol[])Quantile Regression Averaging. group lists task dimensions over which a separate regression is fitted. Mirrors qrensemble::qra.
ForecastEnsembles.QuantileEnsemble Type
QuantileEnsemble(agg = :mean; weights = nothing)Per-quantile weighted aggregation of quantile forecasts. At each task and quantile level τ, take a weighted mean (agg = :mean, also called Vincentization) or weighted median (agg = :median) of the per-model quantile values. weights may be:
nothing— equal weights (the "simple ensemble" of the hubverse).a per-model
EnsembleWeights— same weights at every τ.a per-quantile
EnsembleWeights— different weights per τ (e.g. from a per-τQRA fit, or supplied externally).any fitted method whose
weights(m)returns one of the above (FittedCRPSStacking,FittedQRAin the right configuration, etc.).
ForecastEnsembles.Stacking Type
Stacking(score; dirichlet_alpha = 1.0)Score-optimal stacking against a user-supplied proper scoring rule.
fit(Stacking(score), training, observations) learns simplex ensemble weights that minimise the mean score of the linearly-pooled forecast, where score is any negatively-oriented rule from ScoringRules — e.g. ScoringRules.crps. The FittedStacking result plugs into combine, or into LinearPool/QuantileEnsemble via weights.
ScoringRules must be loaded (using ScoringRules) for fit to work. It is a weak dependency, so the MIT core does not pull in its GPL code unless you opt in. CRPSStacking and QRA remain the dependency-free closed-form specialisations for CRPS and WIS respectively.
Fields
score: the scoring-rule function to minimise (negatively oriented).dirichlet_alpha: strength of a symmetric-Dirichlet prior on the weights, pulling them toward the simplex centre;1.0applies no prior.
ForecastEnsembles.TrainedMethod Type
TrainedMethodAn ensemble method whose weights or coefficients are learned from past performance before use, for example QRA or CRPSStacking. Passing one through fit returns a fitted object (itself an UnfittedMethod) that can then be passed to combine.
ForecastEnsembles.TrimmedMean Type
TrimmedMean(; fraction = 0.1, mode = :trim)Robust ensemble mean. At each task and output_type_id (e.g. a quantile level τ), order the per-model values and either trim — drop the lowest and highest fraction of them, then average the rest — or winsorise — clamp those extremes to the surviving boundary values, then average all of them.
k = round(fraction · n) models are trimmed/clamped from each end, capped so at least one value always survives (so fraction → 0.5 degenerates to the median, fraction = 0 to the plain mean). This is the robust cousin of QuantileEnsemble(:mean): cheaper than a full median ensemble to reason about, and tunable in how much of the tail it discards. It aggregates values that are comparable across models at a shared output_type_id, so it supports :quantile and :cdf forecasts, not :sample (sample indices are not aligned across models — use MixtureEnsemble there).
Fields
fraction: proportion trimmed/clamped from each end, in[0, 0.5).mode::trim(drop the extremes) or:winsorise(clamp them).
Example
using ForecastEnsembles, DataFrames
df = DataFrame(
model_id = string.("m", 1:5),
output_type = "quantile",
output_type_id = 0.5,
location = "A",
value = [1.0, 2.0, 3.0, 4.0, 100.0]
)
ft = ForecastTable(df; task_id_cols = [:location])
combine(ft, TrimmedMean(; fraction = 0.2))ForecastEnsembles.UnfittedMethod Type
UnfittedMethodAn ensemble method applied directly to forecasts with no training, for example QuantileEnsemble(:mean) or MixtureEnsemble(). It carries any fixed configuration (aggregation rule, supplied weights) and can be passed straight to combine without a prior call to fit.
ForecastEnsembles.Windowed Type
Windowed(method, window; time_col)Wrap a TrainedMethod so it trains on only the most recent window values of time_col.
fit(Windowed(method, window; time_col), training, observations) keeps the last window distinct time_col values of training (and the matching observations), fits method on that subset, and returns method's own fitted result — so it drops straight into combine.
Useful for a rolling-window scheme in backtest: compare an expanding-window CRPSStacking() against a rolling Windowed(CRPSStacking(), 8; time_col = :date).
Fields
method: the innerTrainedMethodto fit on the window.window: number of most-recenttime_colvalues to train on (must be≥ 1).time_col: the task-id column defining time order.
ForecastEnsembles.backtest Function
backtest(ft, observations, schemes; time_col, min_train = 1,
rng = default_rng(), score_fn = <ScoringRules default>) -> DataFrameExpanding-window backtest of ensemble schemes. The unique values of time_col are ordered; for each test time after the first min_train, every scheme is trained on the earlier times and scored on the test time out-of-sample. Returns one row per (scheme, test time) with columns scheme, the time_col, and :score (mean over that time's tasks).
schemes maps names to EnsembleMethods (a Dict or a vector of name => method pairs):
a
TrainedMethod—CRPSStacking(),QRA(...)— is fitted on the training window each fold, then applied to the test time;an
UnfittedMethod—QuantileEnsemble(:mean),MixtureEnsemble()— is applied directly.
Each scheme must match the table's output_type: CRPSStacking and sample combiners need :sample data, QRA and quantile combiners need :quantile.
Scoring is injected via score_fn(forecast::ForecastTable, observations) -> Real (the mean score of one fold). Load ScoringRules (using ScoringRules) for a sensible default — CRPS for sample forecasts, the quantile score for quantile forecasts — or pass your own score_fn.
Aggregate across folds yourself, e.g.
using DataFrames
res = backtest(ft, obs, schemes; time_col = :target_date)
combine(groupby(res, :scheme), :score => mean => :mean_score)Arguments
ft: aForecastTable.observations: aDataFramewith the table's task-id columns and an:observedcolumn.schemes: aDictor vector ofname => EnsembleMethodpairs to compare.
Keyword Arguments
time_col: the column giving the time index to expand the window over.min_train: number of initial times used only for training (default1).rng: RNG used by sample-based schemes (defaultdefault_rng()).score_fn: a scorer(forecast, observations) -> Real; defaults to the ScoringRules-based rule for the table'soutput_type(needsusing ScoringRules).
Examples
using ForecastEnsembles, DataFrames, Random, Statistics
rng = MersenneTwister(1)
T = 12; K = 40
obs = DataFrame(t = 1:T, observed = randn(rng, T))
rows = DataFrame[]
for (mid, s) in (("m1", (y, r) -> y .+ randn(r, K)), ("m2", (y, r) -> 2 .* randn(r, K)))
for t in 1:T
push!(rows, DataFrame(model_id = mid, output_type = "sample",
output_type_id = 1:K, t = t, value = s(obs.observed[t], rng)))
end
end
ft = ForecastTable(reduce(vcat, rows); task_id_cols = [:t])
schemes = ["equal" => MixtureEnsemble(), "stack" => CRPSStacking()]
# Inject a scorer — here mean absolute error of the ensemble mean; with
# `using ScoringRules` you would instead pass a proper rule such as ScoringRules.crps.
function mae(ens, o)
d = innerjoin(DataFrame(ens), o; on = :t)
per = combine(groupby(d, :t),
[:value, :observed] => ((v, y) -> abs(mean(v) - first(y))) => :e)
return mean(per.e)
end
backtest(ft, obs, schemes; time_col = :t, min_train = 6, score_fn = mae)DataFrames.combine Function
combine(ft::ForecastTable, m::QuantileEnsemble) -> ForecastTableHub-style simple/weighted ensemble. Aggregates value across model_id within each (task, output_type, output_type_id) group, using m.agg (:mean or :median) and optional per-model weights.
Mirrors hubEnsembles::simple_ensemble. The output model_id is set to "hub-ensemble", matching the R package default.
combine(ft::ForecastTable, m::MixtureEnsemble; rng = default_rng()) -> ForecastTableLinear opinion pool. The kernel is dispatched on the table's output_type:
:sample→ weighted resampling of per-model samples to give a single pooled sample set per task (the only path that usesrng).:cdf→ weighted pointwise average of CDFs.:quantile→ reconstruct a continuous distribution per model viaQuantileDistribution, then invert the mixture CDF Σᵢ wᵢ Fᵢ exactly by bisection at each input level. Deterministic — no Monte Carlo error, which matters for the extreme levels (τ = 0.01, 0.99) hubs request.
combine(ft::ForecastTable, m::TrimmedMean; rng = default_rng()) -> ForecastTableApply a trimmed or winsorised cross-model mean at each (task, output_type_id). See TrimmedMean. The output model_id is "hub-ensemble".
combine(ft::ForecastTable, m::FittedQRA) -> ForecastTableApply fitted QRA weights to a new set of forecasts. The output has output_type = :quantile and one row per (task, quantile_level).
combine(ft::ForecastTable, m::FittedCRPSStacking; rng = default_rng()) -> ForecastTableApply CRPS-stacked weights to a (sample-typed) forecast table. Equivalent to combine(ft, LinearPool(weights = m.weights)).
Arguments
ft: aForecastTableof sample forecasts.m: aFittedCRPSStackingholding the fitted ensemble weights.
Keyword Arguments
rng: random number generator used for resampling; defaults todefault_rng().
Example
using ForecastEnsembles, DataFrames, Random
rng = MersenneTwister(1)
T = 20; K = 50
obs = DataFrame(t = 1:T, observed = randn(rng, T))
rows = DataFrame[]
for (mid, s) in (("m1", (y, r) -> y .+ randn(r, K)), ("m2", (y, r) -> 3 .* randn(r, K)))
for t in 1:T
push!(rows, DataFrame(model_id = mid, output_type = "sample",
output_type_id = 1:K, t = t, value = s(obs.observed[t], rng)))
end
end
ft = ForecastTable(reduce(vcat, rows); task_id_cols = [:t])
fitted = fit(CRPSStacking(), ft, obs)
combine(ft, fitted)combine(df::AbstractDataFrame, args...;
renamecols::Bool=true, threads::Bool=true)
combine(f::Callable, df::AbstractDataFrame;
renamecols::Bool=true, threads::Bool=true)
combine(gd::GroupedDataFrame, args...;
keepkeys::Bool=true, ungroup::Bool=true,
renamecols::Bool=true, threads::Bool=true)
combine(f::Base.Callable, gd::GroupedDataFrame;
keepkeys::Bool=true, ungroup::Bool=true,
renamecols::Bool=true, threads::Bool=true)Create a new data frame that contains columns from df or gd specified by args and return it. The result can have any number of rows that is determined by the values returned by passed transformations.
Below detailed common rules for all transformation functions supported by DataFrames.jl are explained and compared.
All these operations are supported both for AbstractDataFrame (when split and combine steps are skipped) and GroupedDataFrame. Technically, AbstractDataFrame is just considered as being grouped on no columns (meaning it has a single group, or zero groups if it is empty). The only difference is that in this case the keepkeys and ungroup keyword arguments (described below) are not supported and a data frame is always returned, as there are no split and combine steps in this case.
In order to perform operations by groups you first need to create a GroupedDataFrame object from your data frame using the groupby function that takes two arguments: (1) a data frame to be grouped, and (2) a set of columns to group by.
Operations can then be applied on each group using one of the following functions:
combine: does not put restrictions on number of rows returned per group; the returned values are vertically concatenated following order of groups inGroupedDataFrame; it is typically used to compute summary statistics by group; forGroupedDataFrameif grouping columns are kept they are put as first columns in the result;select: return a data frame with the number and order of rows exactly the same as the source data frame, including only new calculated columns;select!is an in-place version ofselect; forGroupedDataFrameif grouping columns are kept they are put as first columns in the result;transform: return a data frame with the number and order of rows exactly the same as the source data frame, including all columns from the source and new calculated columns;transform!is an in-place version oftransform; existing columns in the source data frame are put as first columns in the result;
As a special case, if a GroupedDataFrame that has zero groups is passed then the result of the operation is determined by performing a single call to the transformation function with a 0-row argument passed to it. The output of this operation is only used to identify the number and type of produced columns, but the result has zero rows.
All these functions take a specification of one or more functions to apply to each subset of the DataFrame. This specification can be of the following forms: 2. standard column selectors (integers, Symbols, strings, vectors of integers, vectors of Symbols, vectors of strings, All, Cols, :, Between, Not and regular expressions)
a
cols => functionpair indicating thatfunctionshould be called with positional arguments holding columnscols, which can be any valid column selector; in this case target column name is automatically generated and it is assumed thatfunctionreturns a single value or a vector; the generated name is created by concatenating source column name andfunctionname by default (see examples below).a
cols => function => target_colsform additionally explicitly specifying the target column or columns, which must be a single name (as aSymbolor a string), a vector of names orAsTable. Additionally it can be aFunctionwhich takes a string or a vector of strings as an argument containing names of columns selected bycols, and returns the target columns names (all accepted types exceptAsTableare allowed).a
col => target_colspair, which renames the columncoltotarget_cols, which must be single name (as aSymbolor a string), a vector of names orAsTable.column-independent operations
function => target_colsor justfunctionfor specificfunctions where the input columns are omitted; withouttarget_colsthe new column has the same name asfunction, otherwise it must be single name (as aSymbolor a string). Supportedfunctions are:
nrowto efficiently compute the number of rows in each group.proprowto efficiently compute the proportion of rows in each group.eachindexto return a vector holding the number of each row within each group.groupindicesto return the group number.
vectors or matrices containing transformations specified by the
Pairsyntax described in points 2 to 5a function which will be called with a
SubDataFramecorresponding to each group if aGroupedDataFrameis processed, or with the data frame itself if anAbstractDataFrameis processed; this form should be avoided due to its poor performance unless the number of groups is small or a very large number of columns are processed (in which caseSubDataFrameavoids excessive compilation)
Note! If the expression of the form x => y is passed then except for the special convenience form nrow => target_cols it is always interpreted as cols => function. In particular the following expression function => target_cols is not a valid transformation specification.
Note! If cols or target_cols are one of All, Cols, Between, or Not, broadcasting using .=> is supported and is equivalent to broadcasting the result of names(df, cols) or names(df, target_cols). This behaves as if broadcasting happened after replacing the selector with selected column names within the data frame scope.
All functions have two types of signatures. One of them takes a GroupedDataFrame as the first argument and an arbitrary number of transformations described above as following arguments. The second type of signature is when a Function or a Type is passed as the first argument and a GroupedDataFrame as the second argument (similar to map).
As a special rule, with the cols => function and cols => function => target_cols syntaxes, if cols is wrapped in an AsTable object then a NamedTuple containing columns selected by cols is passed to function. The documentation of DataFrames.table_transformation provides more information about this functionality, in particular covering performance considerations.
What is allowed for function to return is determined by the target_cols value: 2. If both cols and target_cols are omitted (so only a function is passed), then returning a data frame, a matrix, a NamedTuple, a Tables.AbstractRow or a DataFrameRow will produce multiple columns in the result. Returning any other value produces a single column.
If
target_colsis aSymbolor a string then the function is assumed to return a single column. In this case returning a data frame, a matrix, aNamedTuple, aTables.AbstractRow, or aDataFrameRowraises an error.If
target_colsis a vector ofSymbols or strings orAsTableit is assumed thatfunctionreturns multiple columns. Iffunctionreturns one ofAbstractDataFrame,NamedTuple,DataFrameRow,Tables.AbstractRow,AbstractMatrixthen rules described in point 1 above apply. Iffunctionreturns anAbstractVectorthen each element of this vector must support thekeysfunction, which must return a collection ofSymbols, strings or integers; the return value ofkeysmust be identical for all elements. Then as many columns are created as there are elements in the return value of thekeysfunction. Iftarget_colsisAsTablethen their names are set to be equal to the key names except ifkeysreturns integers, in which case they are prefixed byx(so the column names are e.g.x1,x2, ...). Iftarget_colsis a vector ofSymbols or strings then column names produced using the rules above are ignored and replaced bytarget_cols(the number of columns must be the same as the length oftarget_colsin this case). Iffunreturns a value of any other type then it is assumed that it is a table conforming to the Tables.jl API and theTables.columntablefunction is called on it to get the resulting columns and their names. The names are retained whentarget_colsisAsTableand are replaced iftarget_colsis a vector ofSymbols or strings.
In all of these cases, function can return either a single row or multiple rows. As a particular rule, values wrapped in a Ref or a 0-dimensional AbstractArray are unwrapped and then treated as a single row.
select/select! and transform/transform! always return a data frame with the same number and order of rows as the source (even if GroupedDataFrame had its groups reordered), except when selection results in zero columns in the resulting data frame (in which case the result has zero rows).
For combine, rows in the returned object appear in the order of groups in the GroupedDataFrame. The functions can return an arbitrary number of rows for each group, but the kind of returned object and the number and names of columns must be the same for all groups, except when a DataFrame() or NamedTuple() is returned, in which case a given group is skipped.
It is allowed to mix single values and vectors if multiple transformations are requested. In this case single value will be repeated to match the length of columns specified by returned vectors.
To apply function to each row instead of whole columns, it can be wrapped in a ByRow struct. cols can be any column indexing syntax, in which case function will be passed one argument for each of the columns specified by cols or a NamedTuple of them if specified columns are wrapped in AsTable. If ByRow is used it is allowed for cols to select an empty set of columns, in which case function is called for each row without any arguments and an empty NamedTuple is passed if empty set of columns is wrapped in AsTable.
If a collection of column names is passed then requesting duplicate column names in target data frame are accepted (e.g. select!(df, [:a], :, r"a") is allowed) and only the first occurrence is used. In particular a syntax to move column :col to the first position in the data frame is select!(df, :col, :). On the contrary, output column names of renaming, transformation and single column selection operations must be unique, so e.g. select!(df, :a, :a => :a) or select!(df, :a, :a => ByRow(sin) => :a) are not allowed.
In general columns returned by transformations are stored in the target data frame without copying. An exception to this rule is when columns from the source data frame are reused in the target data frame. This can happen via expressions like: :x1, [:x1, :x2], :x1 => :x2, :x1 => identity => :x2, or :x1 => (x -> @view x[inds]) (note that in the last case the source column is reused indirectly via a view). In such cases the behavior depends on the value of the copycols keyword argument:
if
copycols=truethen results of such transformations always perform a copy of the source column or its view;if
copycols=falsethen copies are only performed to avoid storing the same column several times in the target data frame; more precisely, no copy is made the first time a column is used, but each subsequent reuse of a source column (when compared using===, which excludes views of source columns) performs a copy;
Note that performing transform! or select! assumes that copycols=false.
If df is a SubDataFrame and copycols=true then a DataFrame is returned and the same copying rules apply as for a DataFrame input: this means in particular that selected columns will be copied. If copycols=false, a SubDataFrame is returned without copying columns and in this case transforming or renaming columns is not allowed.
If a GroupedDataFrame is passed and threads=true (the default), a separate task is spawned for each specified transformation; each transformation then spawns as many tasks as Julia threads, and splits processing of groups across them (however, currently transformations with optimized implementations like sum and transformations that return multiple rows use a single task for all groups). This allows for parallel operation when Julia was started with more than one thread. Passed transformation functions must therefore not modify global variables (i.e. they must be pure), use locks to control parallel accesses, or threads=false must be passed to disable multithreading. In the future, parallelism may be extended to other cases, so this requirement also holds for DataFrame inputs.
In order to improve the performance of the operations some transformations invoke optimized implementation, see DataFrames.table_transformation for details.
Keyword arguments
renamecols::Bool=true: whether in thecols => functionform automatically generated column names should include the name of transformation functions or not.keepkeys::Bool=true: whether grouping columns ofgdshould be kept in the returned data frame.ungroup::Bool=true: whether the return value of the operation ongdshould be a data frame or aGroupedDataFrame.threads::Bool=true: whether transformations may be run in separate tasks which can execute in parallel (possibly being applied to multiple rows or groups at the same time). Whether or not tasks are actually spawned and their number are determined automatically. Set tofalseif some transformations require serial execution or are not thread-safe.
Metadata: this function propagates table-level :note-style metadata. Column-level :note-style metadata is propagated if: a) a single column is transformed to a single column and the name of the column does not change (this includes all column selection operations), or b) a single column is transformed with identity or copy to a single column even if column name is changed (this includes column renaming). As a special case for GroupedDataFrame if the output has the same name as a grouping column and keepkeys=true, metadata is taken from original grouping column.
Examples
julia> df = DataFrame(a=1:3, b=4:6)
3×2 DataFrame
Row │ a b
│ Int64 Int64
─────┼──────────────
1 │ 1 4
2 │ 2 5
3 │ 3 6
julia> combine(df, :a => sum, nrow, renamecols=false)
1×2 DataFrame
Row │ a nrow
│ Int64 Int64
─────┼──────────────
1 │ 6 3
julia> combine(df, :a => ByRow(sin) => :c, :b)
3×2 DataFrame
Row │ c b
│ Float64 Int64
─────┼─────────────────
1 │ 0.841471 4
2 │ 0.909297 5
3 │ 0.14112 6
julia> combine(df, :, [:a, :b] => (a, b) -> a .+ b .- sum(b)/length(b))
3×3 DataFrame
Row │ a b a_b_function
│ Int64 Int64 Float64
─────┼────────────────────────────
1 │ 1 4 0.0
2 │ 2 5 2.0
3 │ 3 6 4.0
julia> combine(df, All() .=> [minimum maximum])
1×4 DataFrame
Row │ a_minimum b_minimum a_maximum b_maximum
│ Int64 Int64 Int64 Int64
─────┼────────────────────────────────────────────
1 │ 1 4 3 6
julia> using Statistics
julia> combine(df, AsTable(:) => ByRow(mean), renamecols=false)
3×1 DataFrame
Row │ a_b
│ Float64
─────┼─────────
1 │ 2.5
2 │ 3.5
3 │ 4.5
julia> combine(df, AsTable(:) => ByRow(mean) => x -> join(x, "_"))
3×1 DataFrame
Row │ a_b
│ Float64
─────┼─────────
1 │ 2.5
2 │ 3.5
3 │ 4.5
julia> combine(first, df)
1×2 DataFrame
Row │ a b
│ Int64 Int64
─────┼──────────────
1 │ 1 4
julia> df = DataFrame(a=1:3, b=4:6, c=7:9)
3×3 DataFrame
Row │ a b c
│ Int64 Int64 Int64
─────┼─────────────────────
1 │ 1 4 7
2 │ 2 5 8
3 │ 3 6 9
julia> combine(df, AsTable(:) => ByRow(x -> (mean=mean(x), std=std(x))) => :stats,
AsTable(:) => ByRow(x -> (mean=mean(x), std=std(x))) => AsTable)
3×3 DataFrame
Row │ stats mean std
│ NamedTup… Float64 Float64
─────┼───────────────────────────────────────────
1 │ (mean = 4.0, std = 3.0) 4.0 3.0
2 │ (mean = 5.0, std = 3.0) 5.0 3.0
3 │ (mean = 6.0, std = 3.0) 6.0 3.0
julia> df = DataFrame(a=repeat([1, 2, 3, 4], outer=[2]),
b=repeat([2, 1], outer=[4]),
c=1:8);
julia> gd = groupby(df, :a);
julia> combine(gd, :c => sum, nrow)
4×3 DataFrame
Row │ a c_sum nrow
│ Int64 Int64 Int64
─────┼─────────────────────
1 │ 1 6 2
2 │ 2 8 2
3 │ 3 10 2
4 │ 4 12 2
julia> combine(gd, :c => sum, nrow, ungroup=false)
GroupedDataFrame with 4 groups based on key: a
First Group (1 row): a = 1
Row │ a c_sum nrow
│ Int64 Int64 Int64
─────┼─────────────────────
1 │ 1 6 2
⋮
Last Group (1 row): a = 4
Row │ a c_sum nrow
│ Int64 Int64 Int64
─────┼─────────────────────
1 │ 4 12 2
julia> combine(gd) do d # do syntax for the slower variant
sum(d.c)
end
4×2 DataFrame
Row │ a x1
│ Int64 Int64
─────┼──────────────
1 │ 1 6
2 │ 2 8
3 │ 3 10
4 │ 4 12
julia> combine(gd, :c => (x -> sum(log, x)) => :sum_log_c) # specifying a name for target column
4×2 DataFrame
Row │ a sum_log_c
│ Int64 Float64
─────┼──────────────────
1 │ 1 1.60944
2 │ 2 2.48491
3 │ 3 3.04452
4 │ 4 3.46574
julia> combine(gd, [:b, :c] .=> sum) # passing a vector of pairs
4×3 DataFrame
Row │ a b_sum c_sum
│ Int64 Int64 Int64
─────┼─────────────────────
1 │ 1 4 6
2 │ 2 2 8
3 │ 3 4 10
4 │ 4 2 12
julia> combine(gd) do sdf # dropping group when DataFrame() is returned
sdf.c[1] != 1 ? sdf : DataFrame()
end
6×3 DataFrame
Row │ a b c
│ Int64 Int64 Int64
─────┼─────────────────────
1 │ 2 1 2
2 │ 2 1 6
3 │ 3 2 3
4 │ 3 2 7
5 │ 4 1 4
6 │ 4 1 8auto-splatting, renaming and keepkeys
julia> df = DataFrame(a=repeat([1, 2, 3, 4], outer=[2]),
b=repeat([2, 1], outer=[4]),
c=1:8);
julia> gd = groupby(df, :a);
julia> combine(gd, :b => :b1, :c => :c1, [:b, :c] => +, keepkeys=false)
8×3 DataFrame
Row │ b1 c1 b_c_+
│ Int64 Int64 Int64
─────┼─────────────────────
1 │ 2 1 3
2 │ 2 5 7
3 │ 1 2 3
4 │ 1 6 7
5 │ 2 3 5
6 │ 2 7 9
7 │ 1 4 5
8 │ 1 8 9broadcasting and column expansion
julia> df = DataFrame(a=repeat([1, 2, 3, 4], outer=[2]),
b=repeat([2, 1], outer=[4]),
c=1:8);
julia> gd = groupby(df, :a);
julia> combine(gd, :b, AsTable([:b, :c]) => ByRow(extrema) => [:min, :max])
8×4 DataFrame
Row │ a b min max
│ Int64 Int64 Int64 Int64
─────┼────────────────────────────
1 │ 1 2 1 2
2 │ 1 2 2 5
3 │ 2 1 1 2
4 │ 2 1 1 6
5 │ 3 2 2 3
6 │ 3 2 2 7
7 │ 4 1 1 4
8 │ 4 1 1 8
julia> combine(gd, [:b, :c] .=> Ref) # preventing vector from being spread across multiple rows
4×3 DataFrame
Row │ a b_Ref c_Ref
│ Int64 SubArray… SubArray…
─────┼─────────────────────────────
1 │ 1 [2, 2] [1, 5]
2 │ 2 [1, 1] [2, 6]
3 │ 3 [2, 2] [3, 7]
4 │ 4 [1, 1] [4, 8]
julia> combine(gd, AsTable(Not(:a)) => Ref) # protecting result
4×2 DataFrame
Row │ a b_c_Ref
│ Int64 NamedTup…
─────┼─────────────────────────────────
1 │ 1 (b = [2, 2], c = [1, 5])
2 │ 2 (b = [1, 1], c = [2, 6])
3 │ 3 (b = [2, 2], c = [3, 7])
4 │ 4 (b = [1, 1], c = [4, 8])
julia> combine(gd, :, AsTable(Not(:a)) => sum, renamecols=false)
8×4 DataFrame
Row │ a b c b_c
│ Int64 Int64 Int64 Int64
─────┼────────────────────────────
1 │ 1 2 1 3
2 │ 1 2 5 7
3 │ 2 1 2 3
4 │ 2 1 6 7
5 │ 3 2 3 5
6 │ 3 2 7 9
7 │ 4 1 4 5
8 │ 4 1 8 9protecting returned vectors from being expanded into multiple rows
julia> df = DataFrame(a=[1, 1, 2, 2],
b=[[1, 2], [2, 3], [3, 4], [4, 5]]);
julia> gd = groupby(df, :a);
julia> combine(gd, :b => Ref∘sum)
2×2 DataFrame
Row │ a b_Ref_sum
│ Int64 Array…
─────┼──────────────────
1 │ 1 [3, 5]
2 │ 2 [7, 9]ForecastEnsembles.effective_num_models Function
effective_num_models(m) -> Float64 or DataFrameThe effective number of models in a weight vector: the participation ratio p. It is 1 when all weight sits on one model and equals the model count M when the weights are equal, so it reads as "how many models is this ensemble really using".
m may be a fitted method exposing weights (e.g. FittedCRPSStacking, FittedStacking, FittedHedge; FittedPartialPooling uses its pooled global vector), an EnsembleWeights, a weights DataFrame, or a raw weight vector. For per-quantile weights it returns a DataFrame (output_type_id, effective_num_models) — one value per quantile level; otherwise a scalar.
Example
using ForecastEnsembles
effective_num_models([0.6, 0.3, 0.1])ForecastEnsembles.model_ids Function
model_ids(ft::ForecastTable) -> VectorThe distinct model identifiers present in ft, taken from its model-id column.
Arguments
ft: aForecastTable.
Example
using ForecastEnsembles, DataFrames
df = DataFrame(
location = "A", horizon = 1,
model_id = repeat(["m1", "m2", "m3"], inner = 2),
output_type = "quantile",
output_type_id = repeat([0.25, 0.75], 3),
value = [1.0, 3.0, 2.0, 4.0, 0.5, 2.5]
)
ft = ForecastTable(df; task_id_cols = [:location, :horizon])
model_ids(ft)ForecastEnsembles.output_type Function
output_type(ft::ForecastTable) -> SymbolThe single output_type present in ft. Throws if the table mixes output types, since most ensemble methods are defined on a single type at a time.
Arguments
ft: aForecastTable.
Examples
using ForecastEnsembles, DataFrames
df = DataFrame(
location = "A", horizon = 1,
model_id = repeat(["m1", "m2", "m3"], inner = 2),
output_type = "quantile",
output_type_id = repeat([0.25, 0.75], 3),
value = [1.0, 3.0, 2.0, 4.0, 0.5, 2.5]
)
ft = ForecastTable(df; task_id_cols = [:location, :horizon])
output_type(ft)ForecastEnsembles.task_id_cols Function
task_id_cols(ft::ForecastTable) -> Vector{Symbol}The task-id columns of ft, i.e. the columns that together identify a forecast target (e.g. :location, :horizon, :target_date).
Arguments
ft: aForecastTable.
Example
using ForecastEnsembles, DataFrames
df = DataFrame(
location = "A", horizon = 1,
model_id = repeat(["m1", "m2", "m3"], inner = 2),
output_type = "quantile",
output_type_id = repeat([0.25, 0.75], 3),
value = [1.0, 3.0, 2.0, 4.0, 0.5, 2.5]
)
ft = ForecastTable(df; task_id_cols = [:location, :horizon])
task_id_cols(ft)ForecastEnsembles.weight_stability Function
weight_stability(m::FittedHedge) -> DataFrameHow much each model's Hedge weight moved over the training run: the total variation trajectory. A large value flags a model whose weight swung across the history (regime change or noise); a small one flags a stable contribution. Returns a DataFrame with columns model_id and total_variation.
Example
using ForecastEnsembles, DataFrames
trajectory = DataFrame(
model_id = repeat(["m1", "m2"], inner = 2),
weight = [0.5, 0.7, 0.5, 0.3],
t = [1, 2, 1, 2]
)
fitted = FittedHedge(
DataFrame(model_id = ["m1", "m2"], weight = [0.7, 0.3]), ["m1", "m2"], trajectory)
weight_stability(fitted)ForecastEnsembles.weights Function
weights(m) -> Union{DataFrame, Nothing}Per-model weights estimated by a fitted method, as a DataFrame with columns :model_id and :weight. Returns nothing when the fit does not correspond to a single weight vector on the simplex (e.g. unconstrained QRA, per-quantile QRA, QRA with a non-zero intercept).
When weights(m) !== nothing, m can be passed in place of an explicit weights frame to any method that accepts one — for example MixtureEnsemble(weights = m) or QuantileEnsemble(:mean; weights = m). This is the composition path between trained and untrained methods.
Arguments
m: a fitted method (such asFittedCRPSStackingorFittedQRA) from which per-model weights are extracted.
Examples
using ForecastEnsembles, DataFrames, Random
rng = MersenneTwister(1)
T = 20; K = 50
obs = DataFrame(t = 1:T, observed = randn(rng, T))
rows = DataFrame[]
for (mid, s) in (("m1", (y, r) -> y .+ randn(r, K)), ("m2", (y, r) -> 3 .* randn(r, K)))
for t in 1:T
push!(rows, DataFrame(model_id = mid, output_type = "sample",
output_type_id = 1:K, t = t, value = s(obs.observed[t], rng)))
end
end
ft = ForecastTable(reduce(vcat, rows); task_id_cols = [:t])
weights(fit(CRPSStacking(), ft, obs))