knitr::opts_chunk$set( collapse = TRUE, comment = "#>", out.width = '100%', fig.align = "center", fig.width = 7, fig.height = 5, dpi = 200, warning = FALSE )
In dymium, a 'world' object contains everything from representations of entities (eg: individuals, households, residential building, firms, and so on) to the rules (eg: probabilistic models, rates, exogenous variables, etc) that the entities in your world follow.
Hence, to create your first dymium model you must first prepare your world
and
add your entities and rules that they will follow.dymiumCore
provides four toy
datasets which are individuals, households, residential buildings and zones. All
of them are linkable by the id columns. All individuals belong to households, all
households occupy residential buildings, all residential buildings are in the zones.
Let's create a world object.
library(dymiumCore) world <- World$new()
World$new()
is a R6::R6Class()
constructor, a constructor is a class method that
creates an object of that class. So the line above creates a World
object and assigns
it as world
to your global environment. Now, your newly created world is still emptied.
So we are going to add individuals into it using the toy_individuals
dataset from dymiumCore
.
knitr::kable(head(toy_individuals, n = 10), caption = "First 10 rows of the `toy_individuals` data")
To add individuals into world and you must first create an Individual object.
The Individual class is provided by dymiumCore
, it contains methods that help
you to access and modify microdata of individuals such as the toy_individuals
data.
To see what methods and fields are available put ?Individual
into your R console
and press enter.
Ind <- Individual$new(.data = toy_individuals, id_col = "pid") Ind
By calling Ind
it returns some useful information about the object such as the
number of entities of class Individual and the inheritance structure.
Now let's add Ind
into world
using world
's add
method.
world$add(Ind, name = "Individual")
You can see from the warning message above that name
is not needed when adding
an Entity class to world
since it will use the name of the class as the name
of the added object. However, when you add a model object the name
argument must
be specified. Currently, only one instance of each Entity class can be added to
world
since there is no obvious benefit to adding multiple instances of the same
class to world
since it is probably easier and clearer just to combine them as
one instance.
Now that your world
contains Individual agents we may want to introduce some
rules so that we can start to microsimulate something!
Let's microsimulate changes in marital status of the Individual agents.
Using toy_individuals
as the data to estimate a marital status model with the
caret
package. By default, caret
will fit a random forest model if the method
argument in train
is not specified.
library(caret) marital_status_model <- train(marital_status ~ age + sex, data = toy_individuals)
Add the model to world
and assign its key as "marital_status_model".
world$add(marital_status_model, name = "marital_status_model")
Create an event function for changes in marital status.
event_demography_maritalStatus <- function(world) { # get the stored Individual object from `world` Ind <- world$get("Individual") # get the stored marital status model object from `world` # unpack it with `$get()` model <- world$get("marital_status_model")$get() # create a TransitionClassification object to perform a simulation using the model # on the individual agents. TransMaritalStatus <- TransitionClassification$new(Ind, model) # update the `marital_status` column of the individual agents using the simulated # result TransMaritalStatus$update_agents(attr = "marital_status") # end of event must returns the input world object which now contains the updated # individual agents. return(world) }
Since the marital status model uses age of the individual as an explanatory variable we make all individuals aged one year more every iteration
event_demography_age <- function(world) { # get the stored Individual object from `world` Ind <- world$get("Individual") # increase age of all individuals by one # note that in R 1L is integer while 1 is numeric. Ind$get_data(copy = FALSE)[, age := age + 1L] return(world) }
When calling the get_data
method of an Entity class and specify copy
as FALSE
the reference to the data will be returned hence allows the data to be updated
in place/by reference. If copy
is TRUE
or not given a copy of the data will
be returned hence any mofification to that data wouldn't change the data of those
entities.
Now, build a microsimulation pipeline to run for 10 iterations and store some statistics at the end of each iteration for visualisation later.
count_ls <- list() for (i in 1:10) { world$start_iter(time_step = i, unit = "year") %>% event_demography_age() %>% event_demography_maritalStatus() # record the count of households by zone in each iteration count_ls[[i]] <- world$get("Individual")$get_data()[, .(count = .N, iteration = i), marital_status] }
Create a visualisation to show how the distribution of marital status changes over the course of the simulation.
library(data.table) library(ggplot2) count <- rbindlist(count_ls) ggplot(count, aes(iteration, count, group = marital_status)) + geom_line() + geom_point(size = 2) + theme_bw() + labs(title = "Marital status across 10 iterations") + facet_wrap(~ marital_status)
The reason why the number of people with marital status "not applicable" is in decline is because there were no births added to the simulated population. All "not applicable" individuals are those with age less than 16 year-old.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.