HK (Hegselmann and Krause) opinion dynamics model
This example showcases
- How to do synchronous updating of Agent properties (also know as Synchronous update schedule). In a Synchronous update schedule changes made to an agent are not seen by other agents until the next step, see also Wilensky 2015, p.286).
- How to terminate the system evolution on demand according to a boolean function.
- How to terminate the system evolution according to what happened on the previous step.
Model overview
This is an implementation of a simple version of the Hegselmann and Krause (2002) model. It is a model of opinion formation with the question: which parameters' values lead to consensus, polarization or fragmentation? It models interacting groups of agents (as opposed to interacting pairs, typical in the literature) in which it is assumed that if an agent disagrees too much with the opinion of a source of influence, the source can no longer influence the agent's opinion. There is then a "bound of confidence". The model shows that the systemic configuration is heavily dependent on this parameter's value.
The model has the following components:
- A set of n Agents with opinions xᵢ in the range [0,1] as attribute
- A parameter ϵ called "bound" in (0, 0.3]
- The update rule: at each step every agent adopts the mean of the opinions which are within the confidence bound ( |xᵢ - xⱼ| ≤ ϵ).
It is also available from the Models
module as Models.hk
.
Core structures
We start by defining the Agent type and initializing the model. The Agent type has two fields so that we can implement the synchronous update.
using Agents
using Statistics: mean
mutable struct HKAgent <: AbstractAgent
id::Int
old_opinion::Float64
new_opinion::Float64
previous_opinon::Float64
end
There is a reason the agent has three fields that are "the same". The old_opinion
is used for the synchronous agent update, since we require access to a property's value at the start of the step and the end of the step. The previous_opinion
is the opinion of the agent in the previous step, as the model termination requires access to a property's value at the end of the previous step, and the end of the current step.
We could, alternatively, make the three opinions a single field with vector value.
function hk_model(; numagents = 100, ϵ = 0.2)
model = ABM(HKAgent, scheduler = fastest, properties = Dict(:ϵ => ϵ))
for i in 1:numagents
o = rand()
add_agent!(model, o, o, -1)
end
return model
end
model = hk_model()
AgentBasedModel with 100 agents of type HKAgent no space scheduler: fastest properties: Dict(:ϵ => 0.2)
Add some helper functions for the update rule. As there is a filter in the rule we implement it outside the agent_step!
method. Notice that the filter is applied to the :old_opinion
field.
function boundfilter(agent, model)
filter(
j -> abs(agent.old_opinion - j) < model.ϵ,
[a.old_opinion for a in allagents(model)],
)
end
Now we implement the agent_step!
function agent_step!(agent, model)
agent.previous_opinon = agent.old_opinion
agent.new_opinion = mean(boundfilter(agent, model))
end
and model_step!
function model_step!(model)
for a in allagents(model)
a.old_opinion = a.new_opinion
end
end
From this implementation we see that to implement synchronous scheduling we define an Agent type with old
and new
fields for attributes that are changed via the synchronous update. In agent_step!
we use the old
field then, after updating all the agents new
fields, we use the model_step!
to update the model for the next iteration.
Running the model
The parameter of interest is now :new_opinion
, so we assign it to variable adata
and pass it to the run!
method to be collected in a DataFrame.
In addition, we want to run the model only until all agents have converged to an opinion. From the documentation of step!
one can see that instead of specifying the amount of steps we can specify a function instead.
function terminate(model, s)
if any(
!isapprox(a.previous_opinon, a.new_opinion; rtol = 1e-12) for a in allagents(model)
)
return false
else
return true
end
end
step!(model, agent_step!, model_step!, terminate)
model[1]
Main.ex-hk.HKAgent(1, 0.16864683489044416, 0.16864683489044416, 0.16864683489044416)
Alright, let's wrap everything in a function and do some data collection using run!
.
function model_run(; kwargs...)
model = hk_model(; kwargs...)
agent_data, _ = run!(model, agent_step!, model_step!, terminate; adata = [:new_opinion])
return agent_data
end
data = model_run(numagents = 100)
data[(end - 19):end, :]
20 rows × 3 columns
step | id | new_opinion | |
---|---|---|---|
Int64 | Int64 | Float64 | |
1 | 6 | 81 | 0.321694 |
2 | 6 | 82 | 0.666219 |
3 | 6 | 83 | 0.666219 |
4 | 6 | 84 | 0.666219 |
5 | 6 | 85 | 0.666219 |
6 | 6 | 86 | 0.666219 |
7 | 6 | 87 | 0.321694 |
8 | 6 | 88 | 0.666219 |
9 | 6 | 89 | 0.321694 |
10 | 6 | 90 | 0.321694 |
11 | 6 | 91 | 0.321694 |
12 | 6 | 92 | 0.666219 |
13 | 6 | 93 | 0.666219 |
14 | 6 | 94 | 0.321694 |
15 | 6 | 95 | 0.321694 |
16 | 6 | 96 | 0.666219 |
17 | 6 | 97 | 0.321694 |
18 | 6 | 98 | 0.666219 |
19 | 6 | 99 | 0.666219 |
20 | 6 | 100 | 0.666219 |
Notice that here we didn't speciy when to collect data, so this is done at every step. Instead, we could collect data only at the final step, by re-using the same function for the when
argument:
model = hk_model()
agent_data, _ = run!(
model,
agent_step!,
model_step!,
terminate;
adata = [:new_opinion],
when = terminate,
)
agent_data
100 rows × 3 columns
step | id | new_opinion | |
---|---|---|---|
Int64 | Int64 | Float64 | |
1 | 6 | 1 | 0.356515 |
2 | 6 | 2 | 0.356515 |
3 | 6 | 3 | 0.789107 |
4 | 6 | 4 | 0.789107 |
5 | 6 | 5 | 0.356515 |
6 | 6 | 6 | 0.789107 |
7 | 6 | 7 | 0.356515 |
8 | 6 | 8 | 0.356515 |
9 | 6 | 9 | 0.356515 |
10 | 6 | 10 | 0.356515 |
11 | 6 | 11 | 0.789107 |
12 | 6 | 12 | 0.356515 |
13 | 6 | 13 | 0.356515 |
14 | 6 | 14 | 0.356515 |
15 | 6 | 15 | 0.356515 |
16 | 6 | 16 | 0.356515 |
17 | 6 | 17 | 0.789107 |
18 | 6 | 18 | 0.356515 |
19 | 6 | 19 | 0.789107 |
20 | 6 | 20 | 0.356515 |
21 | 6 | 21 | 0.789107 |
22 | 6 | 22 | 0.356515 |
23 | 6 | 23 | 0.789107 |
24 | 6 | 24 | 0.789107 |
25 | 6 | 25 | 0.356515 |
26 | 6 | 26 | 0.356515 |
27 | 6 | 27 | 0.356515 |
28 | 6 | 28 | 0.356515 |
29 | 6 | 29 | 0.356515 |
30 | 6 | 30 | 0.356515 |
31 | 6 | 31 | 0.356515 |
32 | 6 | 32 | 0.356515 |
33 | 6 | 33 | 0.789107 |
34 | 6 | 34 | 0.356515 |
35 | 6 | 35 | 0.356515 |
36 | 6 | 36 | 0.789107 |
37 | 6 | 37 | 0.356515 |
38 | 6 | 38 | 0.356515 |
39 | 6 | 39 | 0.356515 |
40 | 6 | 40 | 0.356515 |
41 | 6 | 41 | 0.789107 |
42 | 6 | 42 | 0.356515 |
43 | 6 | 43 | 0.356515 |
44 | 6 | 44 | 0.356515 |
45 | 6 | 45 | 0.356515 |
46 | 6 | 46 | 0.356515 |
47 | 6 | 47 | 0.789107 |
48 | 6 | 48 | 0.789107 |
49 | 6 | 49 | 0.356515 |
50 | 6 | 50 | 0.789107 |
51 | 6 | 51 | 0.356515 |
52 | 6 | 52 | 0.356515 |
53 | 6 | 53 | 0.789107 |
54 | 6 | 54 | 0.356515 |
55 | 6 | 55 | 0.789107 |
56 | 6 | 56 | 0.356515 |
57 | 6 | 57 | 0.356515 |
58 | 6 | 58 | 0.356515 |
59 | 6 | 59 | 0.356515 |
60 | 6 | 60 | 0.356515 |
61 | 6 | 61 | 0.356515 |
62 | 6 | 62 | 0.356515 |
63 | 6 | 63 | 0.789107 |
64 | 6 | 64 | 0.356515 |
65 | 6 | 65 | 0.789107 |
66 | 6 | 66 | 0.356515 |
67 | 6 | 67 | 0.356515 |
68 | 6 | 68 | 0.789107 |
69 | 6 | 69 | 0.356515 |
70 | 6 | 70 | 0.356515 |
71 | 6 | 71 | 0.789107 |
72 | 6 | 72 | 0.789107 |
73 | 6 | 73 | 0.356515 |
74 | 6 | 74 | 0.789107 |
75 | 6 | 75 | 0.789107 |
76 | 6 | 76 | 0.789107 |
77 | 6 | 77 | 0.356515 |
78 | 6 | 78 | 0.356515 |
79 | 6 | 79 | 0.789107 |
80 | 6 | 80 | 0.789107 |
81 | 6 | 81 | 0.356515 |
82 | 6 | 82 | 0.789107 |
83 | 6 | 83 | 0.789107 |
84 | 6 | 84 | 0.789107 |
85 | 6 | 85 | 0.356515 |
86 | 6 | 86 | 0.356515 |
87 | 6 | 87 | 0.789107 |
88 | 6 | 88 | 0.356515 |
89 | 6 | 89 | 0.356515 |
90 | 6 | 90 | 0.356515 |
91 | 6 | 91 | 0.356515 |
92 | 6 | 92 | 0.356515 |
93 | 6 | 93 | 0.789107 |
94 | 6 | 94 | 0.356515 |
95 | 6 | 95 | 0.789107 |
96 | 6 | 96 | 0.356515 |
97 | 6 | 97 | 0.789107 |
98 | 6 | 98 | 0.789107 |
99 | 6 | 99 | 0.356515 |
100 | 6 | 100 | 0.356515 |
Finally we run three scenarios, collect the data and plot it.
using Plots
plotsim(data, ϵ) = plot(
data.step,
data.new_opinion,
leg = false,
group = data.id,
title = "epsilon = $(ϵ)",
)
plt001, plt015, plt03 =
map(e -> (model_run(ϵ = e), e) |> t -> plotsim(t[1], t[2]), [0.05, 0.15, 0.3])
plot(plt001, plt015, plt03, layout = (3, 1))