Skip to content
This repository has been archived by the owner on Jun 21, 2024. It is now read-only.

visualizing unaggregated agent data per step #88

Closed
marcelovmaciel opened this issue Jan 24, 2022 · 9 comments
Closed

visualizing unaggregated agent data per step #88

marcelovmaciel opened this issue Jan 24, 2022 · 9 comments

Comments

@marcelovmaciel
Copy link

Right now the adata keyword in abm_data_exploration requires an aggregating function. I'm interested in how to visualize unaggregated agent data per step, as done for instance in the HK example. More specifically, I'm interested in when collecting the agent-wise data applying a function and plot the evolution of the output of that function for each agent (so it is not only capturing a field value, but also doing some transformation on it and then visualizing it). I suppose I could do so in a custom plot, but I haven't been able to come up with such a code (and this may then be actually a Makie question).

@Datseris
Copy link
Member

Yeah okay, we can make it so that if you don't have an aggregating function, then multiple lines are plotted in one panel, one for each agent. Yes?

My difficulty: what do you do when agents are deleted or more agents are added? It will lead to large and messy code :(

cc @fbanning


for now, custom plot is the way to go.

@fbanning
Copy link
Member

I'm interested in how to visualize unaggregated agent data per step, as done for instance in the HK example.

If that's what you want to do, then the functions provided by InteractiveDynamics are currently not helpful for you at all.

for now, custom plot is the way to go.

I agree. It would probably be best to just use some custom plots (like we did in the HK example) to get the desired result.

Yeah okay, we can make it so that if you don't have an aggregating function, then multiple lines are plotted in one panel, one for each agent.

Sure, that should be doable.

what do you do when agents are deleted or more agents are added?

Adding/deleting new agents will definitely make the resulting plot look quite messy. Not so sure if it's really insightful to look at such a plot at all. But that's not really our concern, to be quite honest, because what you do with the framework we provide with this package is absolutely up to you. :)

Alternative approach

Here's an alternative approach that would leaves more choice and responsibility to the users:

As I've already hinted at on Slack, I think it might be a good idea to make data collection in our interactive applications behave similar to the regular non-GUI run function. For example, currently adata collects unaggregated agent data in the non-GUI version but collects aggregated agent data in the interactive application. This is a bit unintuitive.

That's why I propose that we unify the behaviour of adata in both cases to collect unaggregated agent data. Collecting aggregated agent data can and should be done via mdata anyways, so imho there's no need to confuzzle these two.

Neither mdata nor adata plots will be automatically added to the interactive app. So how to tell the interactive app to plot any of the collected data then? I think it might be nice if users can add pdata as a kwarg to provide a Tuple consisting of a Makie plotting function (scatter, lines, etc.) and which column data of the provided DataFrame should be plotted after each step (mdata[1], adata[3], etc.).

If pdata is empty, we don't plot anything. If it's not empty, we can iterate over this tuple and add the desired plots to the interactive app. What's cool about this is, that now we don't have to make any assumptions about the appropriate format of the data to be plotted but instead pass this responsibility to the users.

Or we just do what George has proposed above (i.e. if adata doesn't provide an aggregation function, plot one line per agent). I don't really have a strong tendency towards one option or the other, so I'd like to hear your thoughts on the proposed alternative approach above. :)

@marcelovmaciel
Copy link
Author

My two cents as an user is that the gain in flexibility is very exciting. The gain in consistency is a good bonus too. (However, the proposal of @Datseris does solve my problem right away hahahaah).

@marcelovmaciel
Copy link
Author

marcelovmaciel commented Jan 25, 2022

I almost got it via a custom plot! The only thinking I'm missing is rescaling it at each step, as done in the adata and mdata plots. Is it done here through the autolimits! procedure? And if it is how could, for instance, I take the custom example plot from the docs and adapt to use this procedure?

@fbanning
Copy link
Member

Is it done here through the autolimits! procedure?

Yes.

And if it is how could, for instance, I take the custom example plot from the docs and adapt to use this procedure?

Probably best to post a MWE of what you're doing so that we can better understand what you've already tried to do. You could for example take the custom plots example from the docs and alter the plots to reflect your use case. If you post something like that, we can have a look and give you a tip how to rescale the axes.

@marcelovmaciel
Copy link
Author

Here is an working example that is very close to what I'm already doing.

using Agents
using Distributions
using GLMakie
using InteractiveDynamics

mutable struct DummyAgent{n} <: AbstractAgent
    id::Int
    pos::NTuple{n,Float64}
    am_I_plus::Bool
    var_to_plot::Float64
end


function initialize_model(nagents=500, n=2)

    space = ContinuousSpace(ntuple(x -> float(10),n), periodic = false)
    model = ABM(DummyAgent{n}, space)
    for i in 1:nagents
        pos = Tuple(rand(Uniform(0,10), n))
        add_agent_pos!(DummyAgent{n}(i, pos, false, rand(Uniform(0,10))), model)
    end

    for i in rand(allids(model), 250)
        model[i].am_I_plus = true
    end
    return(model)
end

function var_increase(a)
    a.am_I_plus ? a.var_to_plot += 1 : a.var_to_plot -= 1
end


function model_step!(model)
    for i in allids(model)
        var_increase(model[i])
    end
end


function visualize_m(m)
    fig,adf,mdf = abm_data_exploration(m,
                                       dummystep,
                                       model_step!)
    var_wanna_observe = [Observable([m[i].var_to_plot * 10]) for i in  allids(m)]
    nsteps = Observable([0.])
    function newstep(m, var_wanna_observe = var_wanna_observe, nsteps = nsteps)
        model_step!(m)
        var_values = [m[i].var_to_plot * 10 for i in  allids(m)]
        for (i,v) in enumerate(var_values)
            var_wanna_observe[i][] = [v]
        end
        nsteps[] = push!(nsteps.val,nsteps.val[end]+ 1.)
    end
    fig,adf,mdf = abm_data_exploration(m, dummystep, newstep)
    scatter(fig[1,2], nsteps, var_wanna_observe[1])
    lines!(fig[1,2],nsteps, var_wanna_observe[1])
    for i in var_wanna_observe[2:end]
        scatter!(fig[1,2], nsteps, i)
        lines!(fig[1,2], nsteps, i)
    end
end


m = initialize_model()
visualize_m(m)

@Datseris
Copy link
Member

Hi, I'm finally back here

As I've already hinted at on Slack, I think it might be a good idea to make data collection in our interactive applications behave similar to the regular non-GUI run function. For example, currently adata collects unaggregated agent data in the non-GUI version but collects aggregated agent data in the interactive application. This is a bit unintuitive.

But run! collects either aggregated data or not, the user decides this. The reason for the interactive app here to work only for aggregated data is, well, because that's the only thing that makes sense in a general, model-agnostic environment. As I said in the original post, what do you do with deleted agents, or newly created ones? This is depends on the context of the model, while the purpose of all of these apps (and in fact all of InteractiveDynamics.jl) is to be agnostic to the model context.

So how to tell the interactive app to plot any of the collected data then?

If a user wants to plot data, they use the interactive app and hence they must pass either adata or mdata. If not, they just use abm_play. We really shouldn't be complicating further things that are already possible.

Neither mdata nor adata plots will be automatically added to the interactive app. So how to tell the interactive app to plot any of the collected data then? I think it might be nice if users can add pdata as a kwarg to provide a Tuple consisting of a Makie plotting function (scatter, lines, etc.) and which column data of the provided DataFrame should be plotted after each step (mdata[1], adata[3], etc.).

I wonder if all this complexity is necessary given how straightforward it is for users to be adding new plots to the app... I mean, I understand that it is difficult for the user @marcelovmaciel , hence this issue being open, but this doesn't necessarily mean that it is in general difficult. Furthermore, this is more about having familiarity with Makie.jl, not Agents.jl. So, we have to be careful about significantly increasing the complexity of the code, and the complexity of using the app, to counteract users not being able to use the already existing abilities of Makie.jl and InteractiveDynamics.jl on their own end.

After the refactoring, where the interactive apps will just return an Observable{ABM} and the users can just do

x = lift(model) do model
    return whatever collected data in whatever form
end
ax = Axis in whatever location, even existing location
scatter!(ax, x) # or lines(x) or surface or whatever

it is just too simple. It is simpler than making the decision of whether the interactive apps should scatter plot or not.

(I guess we should also return the "time vector" observable that the current timeseries are plotted against...)

My two cents as an user is that the gain in flexibility is very exciting.

We need to be clear here. All flexibility you wish for, you already have. You can add arbitrary plots to any app, that show anything arbitrary of your choice, and is updated with the rest of the interactive app by using step!(abmstepper). And personally I'd argue that it's not even hard. So, there is no more flexibility you can attain, you already have the maximum possible. But what we could do, and will do, is make it easier for you to do some things. For example, #87 will make lifting new observables easier.

@marcelovmaciel
Copy link
Author

marcelovmaciel commented Jan 27, 2022

The snippet below has the same behavior as the previous one I posted (with less clutter), though I feel like I'm getting closer to something that would work. I truly don't understand why limits are not being update here.

function visualize_m(m)
    fig,adf,mdf = abm_data_exploration(m,
                                       dummystep,
                                       model_step!)
    var_wanna_observe = [Observable([m[i].var_to_plot * 10]) for i in  allids(m)]
    nsteps = Observable([0.])
    function newstep(m, var_wanna_observe = var_wanna_observe, nsteps = nsteps)
        model_step!(m)
        var_values = [m[i].var_to_plot * 10 for i in  allids(m)]
        for (i,v) in enumerate(var_values)
            push!(var_wanna_observe[i].val, v)
        end
        nsteps[] = push!(nsteps.val,nsteps.val[end]+ 1.)
    end
    fig,adf,mdf = abm_data_exploration(m, dummystep, newstep)
    ax = Axis(fig[1,2])
    for i in var_wanna_observe[1:end]
        lines!(ax, nsteps, i, linewidth = 0.5)
        autolimits!(ax)
    end

end

@fbanning
Copy link
Member

@marcelovmaciel Version 0.20.0 has been released. Please have a look at the updated docs section on custom plots and apply the new approach to your problem described here. It should now be possible to do whatever you want to do (and possibly even more). :)

# for free to subscribe to this conversation on GitHub. Already have an account? #.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants