itsdfish / SequentialSamplingModels.jl

A unified interface for simulating and evaluating sequential sampling models in Julia.
https://itsdfish.github.io/SequentialSamplingModels.jl/dev/
MIT License
27 stars 4 forks source link

Easy way of extracting "traces" of evidence accumulation process #84

Closed DominiqueMakowski closed 4 months ago

DominiqueMakowski commented 4 months ago

The plotting of DDMs allows to visualize n traces, but I was wondering if there was a way (I looked at the plotting code but didn't find) to extract the traces as for instance a DataFrame of n traces n* time points?

If not maybe it would be worth adding such a method, to 1) modularize/streamline the plotting code and 2) allow users to extract and flexibly plot (and in my case, animate) the bits and pieces they want

itsdfish commented 4 months ago

I don't know if you need to use a DataFrame for the purposes of plotting, but I think simulate is the method you are looking for.

using DataFrames 
using SequentialSamplingModels

model = RDM()
_,trace = simulate(model)
df = DataFrame(trace, :auto)
itsdfish commented 4 months ago

In terms of plotting, you can do the following:

using Plots 
using SequentialSamplingModels

model = RDM()
times,trace = simulate(model)
plot(times, trace)

If you want to plot the accumulation process incrementally, you can do something like this:

using GLMakie
using SequentialSamplingModels 

model = RDM()
times, evidence = simulate(model)

idx = Observable(1)

ys_1 = @lift(evidence[$idx,1])
ys_2 = @lift(evidence[$idx,2])

fig = lines(times, ys_1, color = :blue, linewidth = 4,
    axis = (title = @lift("t = $(round(times[$idx], digits = 1))"),))
lines!(times, ys_2, color = :red, linewidth = 4)

indices = 1:length(times)
record(fig, "time_animation.mp4", indices;
        framerate = length(times)) do i
    println("i $i")
    idx[] = i
end

update

This code is closer, but it throws a type error. I'm not familiar with Makie, so its not immediately clear how to fix the problem. Another improvement would be to generate the lines programatically based on the number of columns in evidence.

DominiqueMakowski commented 4 months ago

I managed to animate the way I wanted :) Thanks a lot!

See here the result

itsdfish commented 4 months ago

No problem. If the animation code was written in Julia, do you mind sharing it?

DominiqueMakowski commented 4 months ago

https://github.com/DominiqueMakowski/CognitiveModels/blob/main/content/media/animations_rt.jl#L351

itsdfish commented 4 months ago

Thanks. Do you know how to generalize to the case with an arbitrary number of options? That was one of the problems I had above.

itsdfish commented 4 months ago

Maybe I need to make a vector of points similar to points = [(i, j) for (i, j) in zip(x, trace)] in your code, but adapt it for multiple accumulators rather than simulations

DominiqueMakowski commented 4 months ago

Thanks. Do you know how to generalize to the case with an arbitrary number of options?

Not at the moment, I spent my day on it fighting Makie and my brain is fried 😅

The trick bit was to figure out when to use @lift() and Observable()

itsdfish commented 4 months ago

fighting Makie and my brain is fried

Haha. I feel the same.

DominiqueMakowski commented 4 months ago

Perhaps I would consider adding an arguments like n_traces=1 in the simulate() methods that would return a matrix of n traces, of the length of the longest and potentially filled with missing.

itsdfish commented 4 months ago

Unfortunately, that approach doesn't generalize well for SSMs with more than 1 accumulators.

There are some alternatives that I considered, but their design tradeoffs do not result in a clear net gain. For example, simulate could return a matrix with time in the first column and samples from the other accumulators in the other columns. With this approach, its not possible to use plot(simulate(model)) because the matrix needs to be sliced. I could stack the matrices following map, but the simulation id would be lost. A similar alternative would be to add a simulation id and throw it into a dataframe, but that still doesn't integrate well with Plots or StatsPlots because it requires explicitly calling column names in the plotting function. Although not perfect, the current set up is simple and integrates well with Plots. If I want to plot multiple traces of the RDM and color code the accumulators, I can run this:

p = plot(); foreach(_ -> plot!(p, simulate(RDM()), color = [:red :black], leg = false), 1:4); p

No need to worry about padding with Missing.

The integration with Makie is poor, but as far as I can tell, the alternatives do not integrate any better. Here, you have to iterate over each accumulator (as far as I can tell):

using Makie 
fig = Figure()
ax = Axis(fig[1, 1], yautolimitmargin = (0.2, 0.2))
times, evidence = simulate(RDM())
map(e -> lines!(ax, times, e), eachcol(evidence))

If you wrap that in a function, it is possible to iterate over multiple traces without need for padding.