using ClusterDepth
using Random
using CairoMakie
using UnfoldSim
using StatsBase
using Distributions
using DataFrames
using Unfold
using UnfoldMakie
How to use clusterDepth multiple comparison correction
This tutorial focuses on single-channel data. For multichannel data, see the tutorial "Further EEG Example".
Let's setup an EEG simulation using UnfoldSim.jl. We simulate a simple 1x2 design with 20 subjects, each with 40 trials
n_subjects = 20
design = MultiSubjectDesign(n_subjects=n_subjects, n_items=40, items_between=Dict(:condition => ["car", "face"]))
first(generate_events(design), 5)
Row | subject | item | condition |
---|---|---|---|
String | String | String | |
1 | S01 | I01 | car |
2 | S01 | I02 | face |
3 | S01 | I03 | car |
4 | S01 | I04 | face |
5 | S01 | I05 | car |
next we define a ground-truth signal + relation to events/design with Wilkinson Formulas let's simulate a P100, a N170 and a P300 - but an effect only on the N170
p1 = MixedModelComponent(;
basis=UnfoldSim.p100(; sfreq=250),
formula=@formula(0 ~ 1 + (1 | subject)),
β=[1.0],
σs=Dict(:subject => [1]),
);
n170 = MixedModelComponent(;
basis=UnfoldSim.n170(; sfreq=250),
formula=@formula(0 ~ 1 + condition + (1 + condition | subject)),
β=[1.0, -0.5], # condition effect - faces are more negative than cars
σs=Dict(:subject => [1, 0.2]), # random slope yes please!
);
p300 = MixedModelComponent(;
basis=UnfoldSim.p300(; sfreq=250),
formula=@formula(0 ~ 1 + condition + (1 + condition | subject)),
β=[1.0, 0], ## no p300 condition effect
σs=Dict(:subject => [1, 1.0]), # but a random slope for condition
);
# Start the simulation
data, events = simulate(MersenneTwister(1), design, [p1, n170, p300], UniformOnset(; offset=500, width=100), RedNoise(noiselevel=1); return_epoched=true)
times = range(0, stop=size(data, 1) / 250, length=size(data, 1));
let's fit an Unfold Model for each subject
In principle, we do not need Unfold here - we could simply calculate (subjectwise) means of the conditions, and their time-resolved difference. Using Unfold.jl here simply generalizes it to more complex design, e.g. with continuous predictors etc.
models = map((d, ev) -> (fit(UnfoldModel, @formula(0 ~ 1 + condition), DataFrame(ev), d, times), ev.subject[1]),
eachslice(data; dims=3),
groupby(events, :subject))
20-element Vector{Tuple{Unfold.UnfoldLinearModel, String}}:
(Unfold-Type: Unfold.UnfoldLinearModel
formula: Dict{DataType, Tuple{StatsModels.FormulaTerm{StatsModels.ConstantTerm{Int64}, Tuple{StatsModels.ConstantTerm{Int64}, StatsModels.Term}}, StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}}}(Any => (0 ~ 1 + condition, 0.0:0.004035714285714286:0.452))
Useful functions:
design(uf) (returns Dict of event => (formula,times/basis))
designmatrix(uf) (returns DesignMatrix with events)
modelfit(uf) (returns modelfit object)
coeftable(uf) (returns tidy result dataframe)
, "S01")
(Unfold-Type: Unfold.UnfoldLinearModel
formula: Dict{DataType, Tuple{StatsModels.FormulaTerm{StatsModels.ConstantTerm{Int64}, Tuple{StatsModels.ConstantTerm{Int64}, StatsModels.Term}}, StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}}}(Any => (0 ~ 1 + condition, 0.0:0.004035714285714286:0.452))
Useful functions:
design(uf) (returns Dict of event => (formula,times/basis))
designmatrix(uf) (returns DesignMatrix with events)
modelfit(uf) (returns modelfit object)
coeftable(uf) (returns tidy result dataframe)
, "S02")
(Unfold-Type: Unfold.UnfoldLinearModel
formula: Dict{DataType, Tuple{StatsModels.FormulaTerm{StatsModels.ConstantTerm{Int64}, Tuple{StatsModels.ConstantTerm{Int64}, StatsModels.Term}}, StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}}}(Any => (0 ~ 1 + condition, 0.0:0.004035714285714286:0.452))
Useful functions:
design(uf) (returns Dict of event => (formula,times/basis))
designmatrix(uf) (returns DesignMatrix with events)
modelfit(uf) (returns modelfit object)
coeftable(uf) (returns tidy result dataframe)
, "S03")
(Unfold-Type: Unfold.UnfoldLinearModel
formula: Dict{DataType, Tuple{StatsModels.FormulaTerm{StatsModels.ConstantTerm{Int64}, Tuple{StatsModels.ConstantTerm{Int64}, StatsModels.Term}}, StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}}}(Any => (0 ~ 1 + condition, 0.0:0.004035714285714286:0.452))
Useful functions:
design(uf) (returns Dict of event => (formula,times/basis))
designmatrix(uf) (returns DesignMatrix with events)
modelfit(uf) (returns modelfit object)
coeftable(uf) (returns tidy result dataframe)
, "S04")
(Unfold-Type: Unfold.UnfoldLinearModel
formula: Dict{DataType, Tuple{StatsModels.FormulaTerm{StatsModels.ConstantTerm{Int64}, Tuple{StatsModels.ConstantTerm{Int64}, StatsModels.Term}}, StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}}}(Any => (0 ~ 1 + condition, 0.0:0.004035714285714286:0.452))
Useful functions:
design(uf) (returns Dict of event => (formula,times/basis))
designmatrix(uf) (returns DesignMatrix with events)
modelfit(uf) (returns modelfit object)
coeftable(uf) (returns tidy result dataframe)
, "S05")
(Unfold-Type: Unfold.UnfoldLinearModel
formula: Dict{DataType, Tuple{StatsModels.FormulaTerm{StatsModels.ConstantTerm{Int64}, Tuple{StatsModels.ConstantTerm{Int64}, StatsModels.Term}}, StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}}}(Any => (0 ~ 1 + condition, 0.0:0.004035714285714286:0.452))
Useful functions:
design(uf) (returns Dict of event => (formula,times/basis))
designmatrix(uf) (returns DesignMatrix with events)
modelfit(uf) (returns modelfit object)
coeftable(uf) (returns tidy result dataframe)
, "S06")
(Unfold-Type: Unfold.UnfoldLinearModel
formula: Dict{DataType, Tuple{StatsModels.FormulaTerm{StatsModels.ConstantTerm{Int64}, Tuple{StatsModels.ConstantTerm{Int64}, StatsModels.Term}}, StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}}}(Any => (0 ~ 1 + condition, 0.0:0.004035714285714286:0.452))
Useful functions:
design(uf) (returns Dict of event => (formula,times/basis))
designmatrix(uf) (returns DesignMatrix with events)
modelfit(uf) (returns modelfit object)
coeftable(uf) (returns tidy result dataframe)
, "S07")
(Unfold-Type: Unfold.UnfoldLinearModel
formula: Dict{DataType, Tuple{StatsModels.FormulaTerm{StatsModels.ConstantTerm{Int64}, Tuple{StatsModels.ConstantTerm{Int64}, StatsModels.Term}}, StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}}}(Any => (0 ~ 1 + condition, 0.0:0.004035714285714286:0.452))
Useful functions:
design(uf) (returns Dict of event => (formula,times/basis))
designmatrix(uf) (returns DesignMatrix with events)
modelfit(uf) (returns modelfit object)
coeftable(uf) (returns tidy result dataframe)
, "S08")
(Unfold-Type: Unfold.UnfoldLinearModel
formula: Dict{DataType, Tuple{StatsModels.FormulaTerm{StatsModels.ConstantTerm{Int64}, Tuple{StatsModels.ConstantTerm{Int64}, StatsModels.Term}}, StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}}}(Any => (0 ~ 1 + condition, 0.0:0.004035714285714286:0.452))
Useful functions:
design(uf) (returns Dict of event => (formula,times/basis))
designmatrix(uf) (returns DesignMatrix with events)
modelfit(uf) (returns modelfit object)
coeftable(uf) (returns tidy result dataframe)
, "S09")
(Unfold-Type: Unfold.UnfoldLinearModel
formula: Dict{DataType, Tuple{StatsModels.FormulaTerm{StatsModels.ConstantTerm{Int64}, Tuple{StatsModels.ConstantTerm{Int64}, StatsModels.Term}}, StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}}}(Any => (0 ~ 1 + condition, 0.0:0.004035714285714286:0.452))
Useful functions:
design(uf) (returns Dict of event => (formula,times/basis))
designmatrix(uf) (returns DesignMatrix with events)
modelfit(uf) (returns modelfit object)
coeftable(uf) (returns tidy result dataframe)
, "S10")
(Unfold-Type: Unfold.UnfoldLinearModel
formula: Dict{DataType, Tuple{StatsModels.FormulaTerm{StatsModels.ConstantTerm{Int64}, Tuple{StatsModels.ConstantTerm{Int64}, StatsModels.Term}}, StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}}}(Any => (0 ~ 1 + condition, 0.0:0.004035714285714286:0.452))
Useful functions:
design(uf) (returns Dict of event => (formula,times/basis))
designmatrix(uf) (returns DesignMatrix with events)
modelfit(uf) (returns modelfit object)
coeftable(uf) (returns tidy result dataframe)
, "S11")
(Unfold-Type: Unfold.UnfoldLinearModel
formula: Dict{DataType, Tuple{StatsModels.FormulaTerm{StatsModels.ConstantTerm{Int64}, Tuple{StatsModels.ConstantTerm{Int64}, StatsModels.Term}}, StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}}}(Any => (0 ~ 1 + condition, 0.0:0.004035714285714286:0.452))
Useful functions:
design(uf) (returns Dict of event => (formula,times/basis))
designmatrix(uf) (returns DesignMatrix with events)
modelfit(uf) (returns modelfit object)
coeftable(uf) (returns tidy result dataframe)
, "S12")
(Unfold-Type: Unfold.UnfoldLinearModel
formula: Dict{DataType, Tuple{StatsModels.FormulaTerm{StatsModels.ConstantTerm{Int64}, Tuple{StatsModels.ConstantTerm{Int64}, StatsModels.Term}}, StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}}}(Any => (0 ~ 1 + condition, 0.0:0.004035714285714286:0.452))
Useful functions:
design(uf) (returns Dict of event => (formula,times/basis))
designmatrix(uf) (returns DesignMatrix with events)
modelfit(uf) (returns modelfit object)
coeftable(uf) (returns tidy result dataframe)
, "S13")
(Unfold-Type: Unfold.UnfoldLinearModel
formula: Dict{DataType, Tuple{StatsModels.FormulaTerm{StatsModels.ConstantTerm{Int64}, Tuple{StatsModels.ConstantTerm{Int64}, StatsModels.Term}}, StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}}}(Any => (0 ~ 1 + condition, 0.0:0.004035714285714286:0.452))
Useful functions:
design(uf) (returns Dict of event => (formula,times/basis))
designmatrix(uf) (returns DesignMatrix with events)
modelfit(uf) (returns modelfit object)
coeftable(uf) (returns tidy result dataframe)
, "S14")
(Unfold-Type: Unfold.UnfoldLinearModel
formula: Dict{DataType, Tuple{StatsModels.FormulaTerm{StatsModels.ConstantTerm{Int64}, Tuple{StatsModels.ConstantTerm{Int64}, StatsModels.Term}}, StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}}}(Any => (0 ~ 1 + condition, 0.0:0.004035714285714286:0.452))
Useful functions:
design(uf) (returns Dict of event => (formula,times/basis))
designmatrix(uf) (returns DesignMatrix with events)
modelfit(uf) (returns modelfit object)
coeftable(uf) (returns tidy result dataframe)
, "S15")
(Unfold-Type: Unfold.UnfoldLinearModel
formula: Dict{DataType, Tuple{StatsModels.FormulaTerm{StatsModels.ConstantTerm{Int64}, Tuple{StatsModels.ConstantTerm{Int64}, StatsModels.Term}}, StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}}}(Any => (0 ~ 1 + condition, 0.0:0.004035714285714286:0.452))
Useful functions:
design(uf) (returns Dict of event => (formula,times/basis))
designmatrix(uf) (returns DesignMatrix with events)
modelfit(uf) (returns modelfit object)
coeftable(uf) (returns tidy result dataframe)
, "S16")
(Unfold-Type: Unfold.UnfoldLinearModel
formula: Dict{DataType, Tuple{StatsModels.FormulaTerm{StatsModels.ConstantTerm{Int64}, Tuple{StatsModels.ConstantTerm{Int64}, StatsModels.Term}}, StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}}}(Any => (0 ~ 1 + condition, 0.0:0.004035714285714286:0.452))
Useful functions:
design(uf) (returns Dict of event => (formula,times/basis))
designmatrix(uf) (returns DesignMatrix with events)
modelfit(uf) (returns modelfit object)
coeftable(uf) (returns tidy result dataframe)
, "S17")
(Unfold-Type: Unfold.UnfoldLinearModel
formula: Dict{DataType, Tuple{StatsModels.FormulaTerm{StatsModels.ConstantTerm{Int64}, Tuple{StatsModels.ConstantTerm{Int64}, StatsModels.Term}}, StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}}}(Any => (0 ~ 1 + condition, 0.0:0.004035714285714286:0.452))
Useful functions:
design(uf) (returns Dict of event => (formula,times/basis))
designmatrix(uf) (returns DesignMatrix with events)
modelfit(uf) (returns modelfit object)
coeftable(uf) (returns tidy result dataframe)
, "S18")
(Unfold-Type: Unfold.UnfoldLinearModel
formula: Dict{DataType, Tuple{StatsModels.FormulaTerm{StatsModels.ConstantTerm{Int64}, Tuple{StatsModels.ConstantTerm{Int64}, StatsModels.Term}}, StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}}}(Any => (0 ~ 1 + condition, 0.0:0.004035714285714286:0.452))
Useful functions:
design(uf) (returns Dict of event => (formula,times/basis))
designmatrix(uf) (returns DesignMatrix with events)
modelfit(uf) (returns modelfit object)
coeftable(uf) (returns tidy result dataframe)
, "S19")
(Unfold-Type: Unfold.UnfoldLinearModel
formula: Dict{DataType, Tuple{StatsModels.FormulaTerm{StatsModels.ConstantTerm{Int64}, Tuple{StatsModels.ConstantTerm{Int64}, StatsModels.Term}}, StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}}}(Any => (0 ~ 1 + condition, 0.0:0.004035714285714286:0.452))
Useful functions:
design(uf) (returns Dict of event => (formula,times/basis))
designmatrix(uf) (returns DesignMatrix with events)
modelfit(uf) (returns modelfit object)
coeftable(uf) (returns tidy result dataframe)
, "S20")
now we can inspect the data easily, and extract the face-effect
function add_subject!(df, s)
df[!, :subject] .= s
return df
end
allEffects = map((x) -> (effects(Dict(:condition => ["car", "face"]), x[1]), x[2]) |> (x) -> add_subject!(x[1], x[2]), models) |> e -> reduce(vcat, e)
plot_erp(allEffects; mapping=(color=:condition, group=:subject))
extract the face-coefficient from the linear model
allCoefs = map(m -> (coeftable(m[1]), m[2]) |> (x) -> add_subject!(x[1], x[2]), models) |> e -> reduce(vcat, e)
plot_erp(allCoefs; mapping=(group=:subject, col=:coefname))
let's unstack the tidy-coef table into a matrix and put it to clusterdepth for clusterpermutation testing
faceCoefs = allCoefs |> x -> subset(x, :coefname => x -> x .== "condition: face")
erpMatrix = unstack(faceCoefs, :subject, :time, :estimate) |> x -> Matrix(x[:, 2:end])' |> collect
summary(erpMatrix)
"113×20 Matrix{Union{Missing, Float64}}"
Clusterdepth
pvals = clusterdepth(erpMatrix; τ=quantile(TDist(n_subjects - 1), 0.95), nperm=5000);
well - that was fast, less than a second for a cluster permutation test. not bad at all!
Plotting
Some plotting, and we add the identified cluster
first calculate the ERP
faceERP = groupby(faceCoefs, [:time, :coefname]) |>
x -> combine(x, :estimate => mean => :estimate,
:estimate => std => :stderror);
put the pvalues into a nicer format
pvalDF = ClusterDepth.cluster(pvals .<= 0.05) |> x -> DataFrame(:from => x[1] ./ 250, :to => (x[1] .+ x[2]) ./ 250, :coefname => "condition: face")
plot_erp(faceERP; stderror=true, pvalue=pvalDF)
Looks good to me! We identified the cluster :-)
old unused code to use extra=(;pvalue=pvalDF) in the plotting function, but didnt work.
This page was generated using Literate.jl.