Opinion Pooling

PortfolioOptimisers.OpinionPoolingAlgorithmType
abstract type OpinionPoolingAlgorithm <: AbstractAlgorithm

Abstract supertype for opinion pooling algorithms.

OpinionPoolingAlgorithm is the base type for all algorithms that combine multiple prior estimations into a consensus prior using opinion pooling. All concrete opinion pooling algorithms should subtype this type to ensure a consistent interface for consensus formation in portfolio optimisation workflows.

Interfaces

In order to implement a new concrete type that works seamlessly with the library, subtype OpinionPoolingAlgorithm and implement the following method:

Required method name

  • compute_pooling(alg::OpinionPoolingAlgorithm, ow::VecNum, pw::MatNum) -> StatsBase.ProbabilityWeights: Aggregate the columns of pw into one consensus scenario-weight vector.

Arguments

  • alg: The concrete subtype instance.
  • ow: $K \times 1$ vector of opinion probabilities, summing to 1.
  • pw: $T \times K$ matrix whose column k holds expert k's scenario weights.

Returns

  • w::StatsBase.ProbabilityWeights: $T \times 1$ consensus scenario weights, summing to 1.

Examples

julia> struct MedianOpinionPooling <: PortfolioOptimisers.OpinionPoolingAlgorithm endjulia> function PortfolioOptimisers.compute_pooling(::MedianOpinionPooling, ow, pw)           w = vec(mapslices(PortfolioOptimisers.Statistics.median, pw; dims = 2))           return PortfolioOptimisers.StatsBase.pweights(w / sum(w))       endjulia> PortfolioOptimisers.compute_pooling(MedianOpinionPooling(), [0.5, 0.5],                                           [0.5 0.25; 0.25 0.25; 0.25 0.5])3-element ProbabilityWeights{Float64, Float64, Vector{Float64}}: 0.375 0.25 0.375

The incremental fit

This prior has no exact incremental recursion, so it takes the online step by refitting from a sample buffer: Online seeds cache, partial_fit! appends each observation to it verbatim, and the one-argument prior runs this estimator's own batch verb over the rows the buffer kept. The answer is therefore exactly a batch fit over those rows, and a max_history on the wrapper windows the whole fit.

cache travels the three propagation channels as every partial-fit state does: factory carries it unchanged, port_opt_view slices it to the selected assets, and obs_weights_view drops it, because no slice of a state exists on the observation axis. It is not rendered, because a running buffer is not the configuration a reader looks the type up for.

Related

References

  • [81] F. Dietrich and C. List. Probabilistic opinion pooling generalized. Part one: general agendas. Social Choice and Welfare 48, 747–786 (2017).
  • [82] C. Martini and J. Sprenger. Opinion Aggregation and Individual Expertise. In: Scientific Collaboration and Collective Knowledge (Oxford University Press, 2017).
source
PortfolioOptimisers.LinearOpinionPoolingType
struct LinearOpinionPooling <: OpinionPoolingAlgorithm

Pools the opinions as a weighted arithmetic mean of their scenario weights.

Each scenario's consensus weight is the opinion-weighted average of what the experts assign to it, so the pooled distribution keeps every scenario any one expert believes in. It suits opinions that are independent and additive.

Mathematical definition

\[\begin{align} p_t^{*} &= \sum_{k=1}^{K} \alpha_k\, p_{tk}\,. \end{align}\]

Where:

  • $p_t^{*}$: Pooled weight of scenario $t$.
  • $\alpha_k$: Opinion probability of expert $k$.
  • $p_{tk}$: Scenario weight for scenario $t$ from expert $k$.
  • $K$: Number of opinions.

A sum of non-negative terms vanishes only when every term does, so $p_t^{*} = 0$ requires that every opinion assigns scenario $t$ zero probability. The pooled distribution is a mixture of the opinions, so it is at least as dispersed as the most dispersed one.

Related

References

  • [81] F. Dietrich and C. List. Probabilistic opinion pooling generalized. Part one: general agendas. Social Choice and Welfare 48, 747–786 (2017).
source
PortfolioOptimisers.LogarithmicOpinionPoolingType
struct LogarithmicOpinionPooling <: OpinionPoolingAlgorithm

Pools the opinions as a weighted geometric mean of their scenario weights, renormalised.

The result is the distribution that minimises the opinion-weighted Kullback-Leibler divergence to the individual opinions, which makes it the information-theoretic consensus. It is robust to extremes, because it down-weights a scenario that any one opinion doubts.

Mathematical definition

\[\begin{align} p_t^{*} &= \frac{\exp\!\left(\sum_{k=1}^{K} \alpha_k \log p_{tk}\right)}{\sum_{s=1}^{T} \exp\!\left(\sum_{k=1}^{K} \alpha_k \log p_{sk}\right)}\,. \end{align}\]

Where:

  • $p_t^{*}$: Pooled weight of scenario $t$.
  • $\alpha_k$: Opinion probability of expert $k$.
  • $p_{tk}$: Scenario weight for scenario $t$ from expert $k$.
  • $K$: Number of opinions.
  • $T$: Number of observations.

A single $\log 0$ sends the exponent to $-\infty$, so $p_t^{*} = 0$ as soon as one opinion assigns scenario $t$ zero probability. The product $\alpha_k \log p_{tk}$ is undefined when both factors vanish, which is the one case the form above does not cover.

Related

References

  • [83] I. J. Good. Rational decisions. Journal of the Royal Statistical Society: Series B (Methodological) 14, 107–114 (1952).
  • [81] F. Dietrich and C. List. Probabilistic opinion pooling generalized. Part one: general agendas. Social Choice and Welfare 48, 747–786 (2017).
source
PortfolioOptimisers.OpinionPoolingPriorType
struct OpinionPoolingPrior{__T_pes, __T_pe1, __T_pe2, __T_p, __T_w, __T_alg, __T_ex, __T_cache} <: AbstractLowOrderPriorEstimator_AF

Opinion pooling prior estimator for asset returns.

OpinionPoolingPrior is a low order prior estimator that computes the mean and covariance of asset returns by combining multiple prior estimations into a consensus prior using opinion pooling algorithms. It supports both linear and logarithmic pooling, flexible weighting of opinions, and optional pre- and post-processing estimators.

The opinions contribute observation weights alone. Every moment of the result comes from refitting pe2 under the pooled weights, which is why pes is typed to the entropy-pooling estimators — they are the ones whose result carries a w. A w of nothing weights every opinion equally at 1/length(pes), and a p of nothing uses the opinion probabilities as given rather than adjusting them through robust_probabilities.

Fields

  • pes: Vector of prior estimators.
  • pe1: Pre-processing prior estimator.
  • pe2: Post-processing prior estimator.
  • p: Opinion pooling blending parameter.
  • w: Opinion pooling weights.
  • alg: Opinion pooling algorithm.
  • ex: Parallel execution strategy.
  • cache: Optional partial-fit state. It is nothing until partial_fit! writes one, and the estimator's read-out verb reads it when the caller gives no data matrix. Each propagation channel does one thing with it: factory carries it unchanged, because a factory call resolves configuration rather than the sample; port_opt_view slices it to the selected assets by index copy, so the viewed estimator answers over those assets alone; and obs_weights_view drops it, because no slice of a state exists on the observation axis. A family whose state has no exact asset slice drops it on both axes and names the reason.

Constructors

OpinionPoolingPrior(;    pes::VecEP,    pe1::Option{<:AbstractLowOrderPriorEstimator_A_F_AF} = nothing,    pe2::AbstractLowOrderPriorEstimator_A_F_AF = EmpiricalPrior(),    p::Option{<:Number} = nothing,    w::Option{<:VecNum} = nothing,    alg::OpinionPoolingAlgorithm = LinearOpinionPooling(),    ex::FLoops.Transducers.Executor = FLoops.Transducers.ThreadedEx(),    cache::Option{<:AbstractPartialFitState} = nothing) -> OpinionPoolingPrior

Keywords correspond to the struct's fields. All arguments are validated for type and value consistency.

Validation

  • pes must be a non-empty vector of prior estimators.
  • If w is not nothing, !isempty(w), length(w) == length(pes), all(x -> 0 <= x <= 1, w), and sum(w) <= 1.
  • The last is an inequality on purpose. When sum(w) < 1, prior gives the remaining weight to a uniform prior over the observations, which becomes an opinion in its own right: it takes a column of pw, and it is pooled and penalised alongside the others.
  • If p is not nothing, p > 0. The bound is strict, so p = 0 raises; p = nothing is how one asks for no penalty.

Propagated parameters

When factory is called on this type, the following @fprop-tagged fields are automatically propagated:

  • pes: Recursively updated via factory.
  • pe1: Recursively updated via factory.
  • pe2: Recursively updated via factory.

View parameters

When port_opt_view is called on this type, the following @vprop-tagged fields are automatically subset to the selected indices:

Examples

julia> sets = UniverseSets(; xkey = "nx", dict = Dict("nx" => ["A", "B", "C"]));julia> OpinionPoolingPrior(;                           pes = [EntropyPoolingPrior(; sets = sets,                                                      mu_views = LinearConstraintEstimator(;                                                                                           val = ["A == 0.03",                                                                                                  "B + C == 0.04"])),                                  EntropyPoolingPrior(; sets = sets,                                                      mu_views = LinearConstraintEstimator(;                                                                                           val = ["A == 0.05",                                                                                                  "B + C >= 0.06"]))])OpinionPoolingPrior  pes ┼ 2-element Vector{EntropyPoolingPrior}      │ EntropyPoolingPrior ⋯      │ EntropyPoolingPrior ⋯  pe1 ┼ nothing  pe2 ┼ EmpiricalPrior      │           ce ┼ PortfolioOptimisersCovariance      │              │   ce ┼ Covariance      │              │      │    me ┼ SimpleExpectedReturns      │              │      │       │   w ┴ nothing      │              │      │    ce ┼ GeneralCovariance      │              │      │       │   ce ┼ StatsBase.SimpleCovariance: StatsBase.SimpleCovariance(true)      │              │      │       │    w ┴ nothing      │              │      │   alg ┼ FullMoment()      │              │      │     w ┴ nothing      │              │   mp ┼ MatrixProcessing      │              │      │     pdm ┼ Posdef      │              │      │         │      alg ┼ UnionAll: NearestCorrelationMatrix.Newton      │              │      │         │   kwargs ┴ @NamedTuple{}: NamedTuple()      │              │      │      dn ┼ nothing      │              │      │      dt ┼ nothing      │              │      │     alg ┼ nothing      │              │      │   order ┴ NTuple{4, Symbol}: (:pdm, :dn, :dt, :alg)      │           me ┼ SimpleExpectedReturns      │              │   w ┴ nothing      │      horizon ┼ nothing      │   fill_limit ┴ nothing    p ┼ nothing    w ┼ nothing  alg ┼ LinearOpinionPooling()   ex ┴ Transducers.ThreadedEx{@NamedTuple{}}: Transducers.ThreadedEx()

Related

References

  • [81] F. Dietrich and C. List. Probabilistic opinion pooling generalized. Part one: general agendas. Social Choice and Welfare 48, 747–786 (2017).
  • [82] C. Martini and J. Sprenger. Opinion Aggregation and Individual Expertise. In: Scientific Collaboration and Collective Knowledge (Oxford University Press, 2017).
source
PortfolioOptimisers.compute_poolingFunction
compute_pooling(::LinearOpinionPooling, ow::VecNum, pw::MatNum)
compute_pooling(::LogarithmicOpinionPooling, ow::VecNum, pw::MatNum)

Compute the consensus posterior return distribution from individual prior distributions using opinion pooling.

compute_pooling aggregates multiple prior probability distributions (pw) into a single consensus posterior distribution according to the specified opinion pooling algorithm and opinion probabilities (ow). Supports both linear and logarithmic pooling.

Mathematical definition

Let $\boldsymbol{\alpha}$ be the opinion probabilities and $\mathbf{P}$ the $T \times K$ matrix of scenario weights for $K$ experts:

Linear (weighted arithmetic mean):

\[\begin{align} \boldsymbol{p}^* &= \mathbf{P} \boldsymbol{\alpha}\,. \end{align}\]

Logarithmic (weighted geometric mean, normalised):

\[\begin{align} p_t^* &= \frac{\exp\!\left(\sum_{k=1}^{K} \alpha_k \log p_{tk}\right)}{\sum_{s=1}^{T} \exp\!\left(\sum_{k=1}^{K} \alpha_k \log p_{sk}\right)}\,. \end{align}\]

Where:

  • $\boldsymbol{p}^*$: $T \times 1$ pooled posterior weight vector.
  • $\mathbf{P}$: $T \times K$ matrix of scenario weights for $K$ experts.
  • $\boldsymbol{\alpha}$: $K \times 1$ opinion probability vector (weights summing to 1).
  • $p_{tk}$: Scenario weight for scenario $t$ from expert $k$.
  • $T$: Number of observations.

Algorithm

Under LinearOpinionPooling:

  1. Multiply pw by ow, giving the consensus weights w.

Under LogarithmicOpinionPooling:

  1. Multiply the elementwise logarithm of pw by ow, giving the exponent vector u.
  2. Read LogExpFunctions.logsumexp(u) into lse. This shifts u by its own maximum before exponentiating it, so a very negative exponent does not underflow to a vector of zeros.
  3. Exponentiate u .- lse, giving the consensus weights w.

Arguments

  • alg: Opinion pooling algorithm (LinearOpinionPooling or LogarithmicOpinionPooling).
  • ow: Vector of opinion probabilities (length = number of opinions).
  • pw: Matrix of prior weights for each opinion (observations × opinions).

Validation

  • The result carries no Inf and no NaN, checked by StatsBase.pweights. Under LogarithmicOpinionPooling an opinion probability of exactly 0 against a scenario weight of exactly 0 makes the product 0 * log(0), which is NaN, and the call raises ArgumentError. A zero scenario weight alone is safe: it gives -Inf, which exponentiates to a consensus weight of 0.

Returns

  • w::StatsBase.ProbabilityWeights: Consensus posterior probability weights.

Related

source
PortfolioOptimisers.priorFunction
prior(pe::OpinionPoolingPrior, X::MatNum, F::Option{<:MatNum} = nothing,
      pnl::Option{<:AssetPanel} = nothing;
      dims::Int = 1, strict::Bool = false, kwargs...)

Compute opinion pooling prior moments for asset returns.

prior estimates the mean and covariance of asset returns by combining multiple prior estimations into a consensus prior using opinion pooling algorithms. Supports both linear and logarithmic pooling, robust opinion probability adjustment, and optional pre- and post-processing estimators.

No field of pe is modified, so calling prior twice on one estimator gives the same answer twice. Every moment of the result is pe.pe2's; the opinions contribute observation weights alone.

Algorithm

  1. Orient X and F by dims.
  2. When pe.pe1 is not nothing, replace X with the returns of that estimator's prior.
  3. Read the opinion probabilities ow, from pe.w when it is set and from a uniform range over length(pe.pes) when it is nothing.
  4. Take the remainder rw of ow against one. When rw exceeds eps, append it to ow and give pw a last column of 1/T, the uniform prior over the observations.
  5. Fit every estimator of pe.pes over the executor pe.ex. Check that each answered on the pool's own observation axis, and write its weights into a column of pw.
  6. Penalise ow through robust_probabilities, which is the identity when pe.p is nothing.
  7. Pool the columns of pw under pe.alg through compute_pooling, giving the consensus weights w.
  8. Refit pe.pe2 under w through factory, giving the moments of the result.
  9. Read ens as exp of the entropy of w, and kld as the divergence from w to each column of pw.

Arguments

  • pe: Opinion pooling prior estimator.
  • X: Asset returns matrix (observations × assets).
  • F: Optional factor matrix.
  • pnl: Optional AssetPanel, the panel the carrier held. A wrapping prior forwards it unchanged, so that it can compose an estimator that is fitted on a panel. An estimator that reads no panel ignores it. It reaches pe.pe1, every opinion in pe.pes, and the refit pe.pe2.
  • dims: Dimension along which to perform the computation.
  • strict: If true, throws error for missing assets; otherwise, issues warnings. Default is false.
  • kwargs...: Additional keyword arguments passed to underlying estimators and solvers.

Validation

  • dims in (1, 2).
  • Every opinion answers on the pool's observation axis, which is pe.pe1's result when pe.pe1 is set and X when it is nothing. An opinion whose own wrapped estimator drops rows answers on a shorter axis, and its probabilities are then over other scenarios. That raises a DimensionMismatch naming the opinion, both counts, and the fix: move the estimator that drops rows into pe1.

Returns

  • pr::LowOrderPrior: Result object containing asset returns, posterior mean vector, posterior covariance matrix, consensus weights, entropy, Kullback-Leibler divergence, opinion probabilities, and optional factor moments.
  • pr.ens: exp of the entropy of the consensus weights, so it runs from 1 (all mass on one observation) to T (uniform).
  • pr.kld[i]: $\mathrm{KL}(\boldsymbol{w} \,\|\, \boldsymbol{p}_i)$, from the consensus to opinion i. robust_probabilities reads the divergence in the other direction, so the two vectors are different numbers.
  • pr.ow: The penalised opinion probabilities, one entry per column of pw. It is one entry longer than pe.w when the uniform-prior remainder took a column.

Related

source

References

[81]
F. Dietrich and C. List. Probabilistic opinion pooling generalized. Part one: general agendas. Social Choice and Welfare 48, 747–786 (2017).
[82]
C. Martini and J. Sprenger. Opinion Aggregation and Individual Expertise. In: Scientific Collaboration and Collective Knowledge (Oxford University Press, 2017).
[83]