Click to expand
using Unfold
using UnfoldMakie
using UnfoldSim
using DataFrames
using CairoMakie
using TopoPlots
using Statistics
using Random
using Animations

Representing uncertainty is one of the most difficult tasks in visualization. It is especially difficult for heatmaps and topoplots. Here we will present new ways to show uncertainty for topoplots series.

Uncertainty in EEG data usually comes from subjects and trials:

  1. Subjects can vary in phisological or behavioral characteristics;
  2. Something can change between trials (electrode connection can get worse, etc.).

Data input

include("../../../example_data.jl")
dat, positions = TopoPlots.example_data()
df = UnfoldMakie.eeg_array_to_dataframe(dat[:, :, 1], string.(1:length(positions)));
df_uncert = UnfoldMakie.eeg_array_to_dataframe(dat[:, :, 2], string.(1:length(positions)));

Generate data with 227 channels, 50 trials, 500 mseconds for bootstrapping noiselevel is important for adding variability it your data

df_toposeries, pos_toposeries = example_data("bootstrap_toposeries"; noiselevel = 7);
df_toposeries = df_toposeries[df_toposeries.trial.<=15, :];
rng = MersenneTwister(1)
Random.MersenneTwister(1)

Uncertainty via additional row

In this case we alread have two datasets: df with mean estimates and df_uncert with variability estimation.

f = Figure()
plot_topoplotseries!(
    f[1, 1],
    df;
    bin_num = 5,
    positions = positions,
    axis = (; xlabel = ""),
    colorbar = (; label = "Voltage estimate"),
)
plot_topoplotseries!(
    f[2, 1],
    df_uncert;
    bin_num = 5,
    positions = positions,
    visual = (; colormap = :viridis),
    axis = (; xlabel = "50 ms"),
    colorbar = (; label = "Voltage uncertainty"),
)
f
Example block output

Uncertainty via animation

In this case, we need to boostrap the data, so we'll use raw data with single trials.

To show the uncertainty of the estimate, we will compute 10 different means of the boostrapped data. More specifically: 1) create N boostrapped data sets using random sampling with replacement across trials; 2) compute their means; 3) do a toposeries animation iterating over these means.

Click to expand for supportive functions

With this function we will bootstrap the data. rng - random number generated. Be sure to send the same rng from outside the function.

bootstrap_toposeries(df; kwargs...) = bootstrap_toposeries(MersenneTwister(), df; kwargs...)
function bootstrap_toposeries(rng::AbstractRNG, df)
    df1 = groupby(df, [:time, :channel])
    len_estimate = length(df1[1].estimate)
    bootstrap_ix = rand(rng, 1:len_estimate, len_estimate) # random sample with replacement
    tmp = vcat([d.estimate[bootstrap_ix] for d in df1]...)
    df1 = DataFrame(df1)

    df1.estimate .= tmp
    return df1
end
bootstrap_toposeries (generic function with 2 methods)

function for easing - smooth transition between frames in animation. update_ratio - transition ratio between time1 and time2. at - create animation object: 0 and 1 are time points, old and new are data vectors.

function ease_between(old, new, update_ratio; easing_function = sineio())
    anim = Animation(0, old, 1, new; defaulteasing = easing_function)
    return at(anim, update_ratio)
end
ease_between (generic function with 1 method)
dat_obs = Observable(df_toposeries)
f = Figure()
plot_topoplotseries!(
    f[1, 1],
    dat_obs;
    bin_num = 5,
    nrows = 2,
    positions = pos_toposeries,
    axis = (; xlabel = "Time [msec]"),
)
GridPosition(GridLayout[1, 1] (1 children), GridLayoutBase.Span(1:1, 1:1), GridLayoutBase.Inner())

Basic toposeries

record(f, "bootstrap_toposeries.mp4"; framerate = 2) do io
    for i = 1:10
        dat_obs[] = bootstrap_toposeries(rng, df_toposeries)
        recordframe!(io)
    end
end;

Toposeries without contour

dat_obs = Observable(df_toposeries)
f = Figure()
plot_topoplotseries!(
    f[1, 1],
    dat_obs;
    bin_num = 5,
    nrows = 2,
    positions = pos_toposeries,
    visual = (; contours = false),
    axis = (; xlabel = "Time [msec]"),
)
record(f, "bootstrap_toposeries_nocontours.mp4"; framerate = 2) do io
    for i = 1:10
        dat_obs[] = bootstrap_toposeries(rng, df_toposeries)
        recordframe!(io)
    end
end;

Toposeries with easing (smooth transition between frames)

dat_obs = Observable(bootstrap_toposeries(rng, df_toposeries))
f = Figure()
plot_topoplotseries!(
    f[1, 1],
    dat_obs;
    bin_num = 5,
    nrows = 2,
    positions = pos_toposeries,
    visual = (; contours = false),
    axis = (; xlabel = "Time [msec]"),
)
record(f, "bootstrap_toposeries_easing.mp4"; framerate = 10) do io
    for n_bootstrapping = 1:10
        recordframe!(io)
        new_df = bootstrap_toposeries(rng, df_toposeries)
        old_estimate = deepcopy(dat_obs.val.estimate)
        for update_ratio in range(0, 1, length = 8)

            dat_obs.val.estimate .=
                ease_between(old_estimate, new_df.estimate, update_ratio)
            notify(dat_obs)
            recordframe!(io)
        end
    end
end;


This page was generated using Literate.jl.