Modifying data

While FlexiChain and FlexiSummary are intended to be immutable, there are scenarios where you may want to modify the data that they contain.

FlexiChains provides generic, high-level functions that allow you to modify either the keys or the values stored inside a FlexiChain or FlexiSummary.

All of these functions return a new chain (or summary) and avoid mutating the original chain (or summary).

Specialised applications

These functions are meant to be general, in that you can use them to perform any transformation you like. This means that for 'simple' applications such as renaming a single key they can be quite verbose. If you have a specific use case that would benefit from a more specialised function, please do feel free to open an issue or submit a pull request!

Modifying keys

Let's load our example chain first:

using FlexiChainschain = FlexiChains.EXAMPLE_CHAIN
╭─FlexiChain (10 iterations, 2 chains) ────────────────────────────────────────╮
│ ↓ iter  = 101:110                                                            │
│ → chain = 1:2                                                                │
│                                                                              │
│ Parameters (3) ── VarName                                                    │
│  Float64          mu, tau                                                    │
│  Vector{Float64}  theta (8,)                                                 │
│                                                                              │
│ Extras (2)                                                                   │
│  Float64  logjoint                                                           │
│  Bool     is_divergent                                                       │
╰──────────────────────────────────────────────────────────────────────────────╯

If you want to change the keys stored inside a FlexiChain or FlexiSummary, you can use the FlexiChains.map_keys function.

using FlexiChains: get_name, map_keys, Parameter, Extra, VarNameusing VarNames: getsym, getoptic# Define a function that takes the old key and returns a new key.function f(p::Parameter{<:VarName})    # Get the name of the parameter (e.g. @vn(x))    name = get_name(p)    # Get the components of that VarName.    sym, optic = getsym(name), getoptic(name)    # Add a prefix to the symbol.    new_sym = Symbol("new_", sym)    # Return a new Parameter with the new name    return Parameter{VarName}(VarName{new_sym}(optic))end# Don't change extras!f(p::Extra) = pmap_keys(f, chain)
╭─FlexiChain (10 iterations, 2 chains) ────────────────────────────────────────╮
│ ↓ iter  = 101:110                                                            │
│ → chain = 1:2                                                                │
│                                                                              │
│ Parameters (3) ── VarName                                                    │
│  Float64          new_mu, new_tau                                            │
│  Vector{Float64}  new_theta (8,)                                             │
│                                                                              │
│ Extras (2)                                                                   │
│  Float64  logjoint                                                           │
│  Bool     is_divergent                                                       │
╰──────────────────────────────────────────────────────────────────────────────╯

This example is admittedly quite contrived. However, it is intended to be general enough to cover any use case you might have. For example, if you want to rename a single key, f can simply be a function that looks it up in a dictionary:

mapping = Dict(old_key => new_key)f(p) = get(mapping, p, p)  # Return the new key if it exists, otherwise return the old key.

Often for the most part you will only want to modify Parameters, in which case you can use FlexiChains.map_parameters. This has the same behaviour as map_keys, except that you can skip the wrapping/unwrapping in Parameter.

Note that both map_keys and map_parameters work with FlexiSummary as well.

Modifying values

It is also possible to modify the values stored inside a FlexiChain (but not a FlexiSummary). This is done with the transform_values function:

using FlexiChains: transform_valueschain2 = transform_values(    chain,    @vn(mu) => (i -> i + 1),    @vn(tau) => (i -> i * 2) => @vn(new_tau))
╭─FlexiChain (10 iterations, 2 chains) ────────────────────────────────────────╮
│ ↓ iter  = 101:110                                                            │
│ → chain = 1:2                                                                │
│                                                                              │
│ Parameters (4) ── VarName                                                    │
│  Float64          mu, tau, new_tau                                           │
│  Vector{Float64}  theta (8,)                                                 │
│                                                                              │
│ Extras (2)                                                                   │
│  Float64  logjoint                                                           │
│  Bool     is_divergent                                                       │
╰──────────────────────────────────────────────────────────────────────────────╯

The above example:

  • adds 1 to each element of chain[@vn(mu)]; and
  • multiplies each element of chain[@vn(tau)] by 2, and stores the result in a new key @vn(new_tau).
chain2[@vn(mu)] .- chain[@vn(mu)]   # Should be all 1.
┌ 10×2 DimArray{Float64, 2}  ┐
├────────────────────────────┴──────────────────────────── dims ┐
  ↓ iter Sampled{Int64} 101:110 ForwardOrdered Regular Points,
  → chain Sampled{Int64} 1:2 ForwardOrdered Regular Points
└───────────────────────────────────────────────────────────────┘
   ↓ →  1    2
 101    1.0  1.0
 102    1.0  1.0
 103    1.0  1.0
 104    1.0  1.0
   ⋮         
 107    1.0  1.0
 108    1.0  1.0
 109    1.0  1.0
 110    1.0  1.0

You can pass as many transformations as you like. The syntax is designed to be similar to that of DataFrames.transform, but has some slight differences: notably, the function being applied acts on individual draws from chain[@vn(x)] rather than the matrix of all draws. (In DataFrames, the functions act on entire columns of data.)

You can also pass binary (or n-ary) functions to transform_values to combine multiple keys. Again, this is similar to DataFrames.transform, but the function combines individual draws from chain[@vn(mu)] and chain[@vn(tau)] rather than the matrices themselves.

chain3 = transform_values(chain, [@vn(mu), @vn(tau)] => (+) => @vn(mu_plus_tau))
╭─FlexiChain (10 iterations, 2 chains) ────────────────────────────────────────╮
│ ↓ iter  = 101:110                                                            │
│ → chain = 1:2                                                                │
│                                                                              │
│ Parameters (4) ── VarName                                                    │
│  Float64          mu, tau, mu_plus_tau                                       │
│  Vector{Float64}  theta (8,)                                                 │
│                                                                              │
│ Extras (2)                                                                   │
│  Float64  logjoint                                                           │
│  Bool     is_divergent                                                       │
╰──────────────────────────────────────────────────────────────────────────────╯
chain3[@vn(mu_plus_tau)] == chain[@vn(mu)] .+ chain[@vn(tau)]
true

Attaching labels to data

A common use case for transform_values is to attach labels to data, for example, converting a Vector of parameters into a DimVector.

In our example chain above, theta has one entry per school, but is stored as a plain Vector. While this is fine for many applications, attaching labels can help make it easier to analyse the data.

using DimensionalData: DimArray, Dimschool_names = [    "Choate",    "Deerfield",    "Phillips Andover",    "Phillips Exeter",    "Hotchkiss",    "Lawrenceville",    "St. Paul's",    "Mt. Hermon",]add_labels(v::Vector{<:Real}) = DimArray(v, Dim{:school}(school_names))labelled_chain = transform_values(chain, @vn(theta) => add_labels)
╭─FlexiChain (10 iterations, 2 chains) ────────────────────────────────────────╮
│ ↓ iter  = 101:110                                                            │
│ → chain = 1:2                                                                │
│                                                                              │
│ Parameters (3) ── VarName                                                    │
│  Float64                                   mu, tau                           │
│  DimensionalData.DimVector{Float64, Tupl…  theta (8,)                        │
│                                                                              │
│ Extras (2)                                                                   │
│  Float64  logjoint                                                           │
│  Bool     is_divergent                                                       │
╰──────────────────────────────────────────────────────────────────────────────╯

For example, DimVector parameters get special labels when plotting:

using CairoMakieFlexiChains.Makie.traceplot(labelled_chain, @vn(theta); layout=(4, 2))
Example block output

Having the labels also benefits downstream analysis of any data extracted from the chain. For example, you can obtain the same functionality as tidybayes' spread_draws:

using DataFrames# Subset to the first three iterations, or else the output will be very long!labelled_chain = labelled_chain[iter=1:3]# Create a 3D DimArray (iter x chain x school)dimarr = labelled_chain[@vn(theta), stack=true]# Convert it to a DataFrame (long-format by default)df = DataFrame(dimarr)
48×4 DataFrame
RowiterchainschoolParameter(theta)
Int64Int64StringFloat64
11011Choate10.4929
21021Choate10.4929
31031Choate10.4929
41012Choate5.06589
51022Choate5.82169
61032Choate9.76186
71011Deerfield11.4664
81021Deerfield11.4664
91031Deerfield11.4664
101012Deerfield7.0214
111022Deerfield6.69599
121032Deerfield8.79631
131011Phillips Andover11.5705
141021Phillips Andover11.5705
151031Phillips Andover11.5705
161012Phillips Andover5.71944
171022Phillips Andover5.35547
181032Phillips Andover6.02066
191011Phillips Exeter12.131
201021Phillips Exeter12.131
211031Phillips Exeter12.131
221012Phillips Exeter6.71881
231022Phillips Exeter6.51507
241032Phillips Exeter8.1514
251011Hotchkiss10.9676
261021Hotchkiss10.9676
271031Hotchkiss10.9676
281012Hotchkiss2.90095
291022Hotchkiss2.97696
301032Hotchkiss2.35677
311011Lawrenceville12.0114
321021Lawrenceville12.0114
331031Lawrenceville12.0114
341012Lawrenceville-2.12359
351022Lawrenceville-1.63556
361032Lawrenceville4.94412
371011St. Paul's10.794
381021St. Paul's10.794
391031St. Paul's10.794
401012St. Paul's3.62712
411022St. Paul's3.65501
421032St. Paul's1.58788
431011Mt. Hermon11.582
441021Mt. Hermon11.582
451031Mt. Hermon11.582
461012Mt. Hermon5.59417
471022Mt. Hermon6.79247
481032Mt. Hermon6.26805

If you want a wide-format table, you can tap into the functionality in DimensionalData.jl (please see their docs for full info):

using DimensionalData: DimTabledf = DataFrame(DimTable(dimarr; layersfrom=:school))
6×10 DataFrame
Rowiterchainschool_Choateschool_Deerfieldschool_Phillips Andoverschool_Phillips Exeterschool_Hotchkissschool_Lawrencevilleschool_St. Paul'sschool_Mt. Hermon
Int64Int64Float64Float64Float64Float64Float64Float64Float64Float64
1101110.492911.466411.570512.13110.967612.011410.79411.582
2102110.492911.466411.570512.13110.967612.011410.79411.582
3103110.492911.466411.570512.13110.967612.011410.79411.582
410125.065897.02145.719446.718812.90095-2.123593.627125.59417
510225.821696.695995.355476.515072.97696-1.635563.655016.79247
610329.761868.796316.020668.15142.356774.944121.587886.26805

Standardisation

Another use case for transforming values is to (un)standardise parameter values.

For example, consider a case where we have some model data y which is standardised before being passed to a model:

y = randn(100) .* 4.0 .+ 2.0using StatsBase: fit, ZScoreTransform, transformzs = fit(ZScoreTransform, y)y_scaled = transform(zs, y)mean(y_scaled), std(y_scaled)  # ≈ 0 and 1
(6.88338275267597e-17, 0.9999999999999998)

Then, suppose that we pass y_scaled to a model and sample a parameter m_scaled which is the mean of the standardised y. We'll construct one such chain for demonstration purposes:

using FlexiChains: FlexiChain, VarName, Parameterniters, nchains = 1000, 2fake_m_data = [randn(100) .+ 1 for _ in 1:niters, _ in 1:nchains]fake_chain =    FlexiChain{VarName}(niters, nchains, Dict(Parameter(@vn(m_scaled)) => fake_m_data))
╭─FlexiChain (1000 iterations, 2 chains) ──────────────────────────────────────╮
│ ↓ iter  = 1:1000                                                             │
│ → chain = 1:2                                                                │
│                                                                              │
│ Parameters (1) ── VarName                                                    │
│  Vector{Float64}  m_scaled (100,)                                            │
│                                                                              │
│ Extras (0)                                                                   │
│  (none)                                                                      │
╰──────────────────────────────────────────────────────────────────────────────╯

The mean of this m_scaled should be approximately 1:

mean(fake_chain[@vn(m_scaled), stack=true])
0.9963935660290663

We can unstandardise the m_scaled values by applying the inverse transformation:

using StatsBase: reconstructfake_chain =    transform_values(fake_chain, @vn(m_scaled) => (i -> reconstruct(zs, i)) => @vn(m))
╭─FlexiChain (1000 iterations, 2 chains) ──────────────────────────────────────╮
│ ↓ iter  = 1:1000                                                             │
│ → chain = 1:2                                                                │
│                                                                              │
│ Parameters (2) ── VarName                                                    │
│  Vector{Float64}  m_scaled (100,), m (100,)                                  │
│                                                                              │
│ Extras (0)                                                                   │
│  (none)                                                                      │
╰──────────────────────────────────────────────────────────────────────────────╯

The mean of the reconstructed m should be approximately (4 * 1) + 2 = 6:

mean(fake_chain[@vn(m), stack=true])
6.116315588145866

Docstrings

FlexiChains.map_keys — Function
FlexiChains.map_keys(f, cs::ChainOrSummary{T})::ChainOrSummary{S} where {T,S}

Change the keys of a FlexiChain or FlexiSummary by applying the function f to each key.

f must have the signature f(::ParameterOrExtra{<:T}) -> ParameterOrExtra{<:S}. It must return a unique key for each input key.

source
FlexiChains.map_parameters — Function
FlexiChains.map_parameters(f, cs::ChainOrSummary{T})::ChainOrSummary{S} where {T,S}

Change the parameters of a FlexiChain or FlexiSummary by applying the function f to each parameter name.

f must have the signature f(::T) -> S. It must return a unique parameter for each input parameter.

source
FlexiChains.transform_values — Function
FlexiChains.transform_values(chn::FlexiChain{T}, args...)

Perform one or more transformations on the values inside a FlexiChain.

args can be one or more of the following:

  • key => f => new_key

    Applies f to each draw of key and stores the result in new_key.

  • key => f

    Shorthand for {key} => f => {key}, i.e., applies f to each draw of key and stores the result back in key.

  • [key1, key2, ..., keyN] => f => new_key

    Calculates f(key1, key2, ..., keyN) for each draw and stores the result in new_key. Note that the LHS must be an AbstractVector; other iterables like Tuples are not accepted.

key accepts any value that can be used to index into a FlexiChain, including Symbols when unambiguous. However, new_key is more restricted: it must be either FlexiChains.Extra, FlexiChains.Parameter{<:T}, or just a T (in which case it is assumed to be a parameter).

source