using UnfoldDecode
using UnfoldSim
using UnfoldMakie
using CairoMakie
using Unfold

Overlap-corrected decoding

We will try to introduce as many fancy features as possible Please read the "tutorial" first

Simulation

multi-event

dat, evt = UnfoldSim.predef_eeg()
evt.event = rand(["eventA", "eventB"], size(evt, 1)) # add random events
dat = repeat(dat', 5)
dat .= dat .+ 20 .* rand(size(dat)...)
5×120199 Matrix{Float64}:
 9.50901  13.7884   19.6804   15.9948   …  14.3846   11.1383    7.87808
 7.93281   9.31424  16.7044    4.37156     12.4072   12.7546    3.5393
 6.78216  13.4509    8.10916   3.9423       5.22966  14.2837   13.5129
 7.14976   3.34022   8.09905   7.42073     17.6824   14.1518    4.83414
 3.77254   2.53089  14.6006   15.1018      11.8468    2.95192   4.43227

Overlap-model Definition

We have two basis functions now, with two different timewindows. Let's see if it works!

des = [
    "eventA" => (@formula(0 ~ 1 + condition + continuous), firbasis((-0.1, 1.0), 100)),
    "eventB" => (@formula(0 ~ 1 + continuous), firbasis((-0.3, 0.5), 100)),
]
2-element Vector{Pair{String, Tuple{StatsModels.FormulaTerm{StatsModels.ConstantTerm{Int64}}, Unfold.FIRBasis}}}: "eventA" => (0 ~ 1 + condition + continuous, ╭──────────────────────────────────────────────────────────────────────────────╮111×111 SparseArrays.SparseMatrixCSC with 111 stored entries: │ ::BasisFunction │⎡⠑⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎤ │ name:  │⎢⠀⠀⠑⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥ │ kerneltype: Unfold.FIRBasis │⎢⠀⠀⠀⠀⠑⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥ │ width: 111 │⎢⠀⠀⠀⠀⠀⠀⠑⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥ │ height: 111 │⎢⠀⠀⠀⠀⠀⠀⠀⠀⠑⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥ │ colnames: [-0.1, -0.09 ... 1.0] │⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⢄⠀⠀⠀⠀⠀⠀⠀⠀⎥ │ times: [-0.1, -0.09 ... 1.0] │⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⢄⠀⠀⠀⠀⠀⠀⎥ │ collabel: time │⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⢄⠀⠀⠀⠀⎥ │ shift_onset: -10 │⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⢄⠀⠀⎥ │ │⎣⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⢄⎦ ╰──────────────────────────────────────────────────────────────────────────────╯ ) "eventB" => (0 ~ 1 + continuous, ╭──────────────────────────────────────────────────────────────────────────────╮81×81 SparseArrays.SparseMatrixCSC with 81 stored entries: │ ::BasisFunction │⎡⠑⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎤ │ name:  │⎢⠀⠀⠑⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥ │ kerneltype: Unfold.FIRBasis │⎢⠀⠀⠀⠀⠑⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥ │ width: 81 │⎢⠀⠀⠀⠀⠀⠀⠑⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥ │ height: 81 │⎢⠀⠀⠀⠀⠀⠀⠀⠀⠑⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥ │ colnames: [-0.3, -0.29 ... 0.5] │⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⢄⠀⠀⠀⠀⠀⠀⠀⠀⎥ │ times: [-0.3, -0.29 ... 0.5] │⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⢄⠀⠀⠀⠀⠀⠀⎥ │ collabel: time │⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⢄⠀⠀⠀⠀⎥ │ shift_onset: -30 │⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⢄⠀⠀⎥ │ │⎣⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⢄⎦ ╰──────────────────────────────────────────────────────────────────────────────╯ )

To show that it is possible, we explicitly specify the solver

customsolver = (x, y) -> Unfold.solver_default(x, y)
uf = Unfold.fit(UnfoldModel, des, evt, dat[1, :]; solver = customsolver);
plot_erp(coeftable(uf); mapping = (; col = :eventname))
Example block output

Fitting the Overlap-corrected LDA model

using MLJ, MultivariateStats, MLJMultivariateStatsInterface
LDA = @load LDA pkg = MultivariateStats
MLJMultivariateStatsInterface.LDA

you could use other parameters, check out ?LDA

ldaModel = LDA(
    method = :whiten,
    cov_w = SimpleCovariance(),
    cov_b = SimpleCovariance(),
    regcoef = 1e-3,
)

uf_lda = UnfoldDecode.fit(
    UnfoldDecodingModel,
    des,
    evt,
    dat,
    ldaModel,
    "eventA" => :condition;
    nfolds = 2,# only 2 folds to speed up computation
    unfold_fit_options = (; solver = customsolver), #customer solver for fun
    eventcolumn = :event, # actually the default, but maybe your event dataframe has a different name?
    multithreading = false,
) # who needs speed anyway :shrug:

plot_erp(coeftable(uf_lda))

Voila, the model classified the correct period at the correct event


This page was generated using Literate.jl.