2025-12-12
Answer: -136.7
Define a variable for each construct* of the VAST display. These can be measured variables or unmeasured (mediating) variables.
You can either refer to an actual measurement procedure or simply define a variable. In both cases you should explicitly define the following properties:
* only uni-dimensional constructs, not higher-order concepts
In the Google doc (below your Construct Table) fill in 2-3 exemplary variables from your bystander model in the Variables Table:
Example:
| Construct in VAST display | Shortname | Type | Range/values | Anchors |
|---|---|---|---|---|
| Affective tone of instruction | aff_tone | Continuous | [-1; 1] | -1 = maximally negative 0 = neutral +1 = maximally positive |
| Anxiety | anxiety | Continuous | [0; 1] | 0 = no anxiety 1 = maximal anxiety |
| Kohlberg’s Stages of Moral Development | moral_stage | Ordinal | {1; 2; 3} | 1=Pre-conventional 2=Conventional 3=Post-Conventional |
| … | … | … | … |
Note: This resembles a codebook; but for theoretical variables, not only for measured variables. As a heuristic, list all concepts that are not higher-order concepts (because these usually have no single numerical representation).
We want to model the following phenomenon (a specific version of the bystander effect):
Task: Sketch a first functional relationship that could model this phenomenon. Use the variables you defined in the previous step (including their labels and ranges).
Every causal path needs to be implemented as a mathematical function, where the dependent variable/output \(y\) is a function of the input variable(s) \(x_i\).
\(y = f(x_1, x_2, ..., x_i)\)
This can, for example, be a linear function, \(y = \beta_0 + \beta_1x_1\).
\(\color{red} y = \color{forestgreen} \beta_0 \color{black} + \color{forestgreen} \beta_1 \color{blue} x\)
→ \(\color{red} y\) = output variable, \(\color{forestgreen} \beta\)s = parameters, \(\color{blue} x\) = input variable.
Two types of parameters:
Note
Virtually all parameters (except natural constants) could be imagined as being free. It is a choice to fix some of them in order to simplify the model. In our course, we do not attempt to estimate parameters from data. Instead, we will explore the behavior of our models under different parameter settings (i.e., a sensitivity analysis).
Fixing a parameter: \(\color{forestgreen} \beta_0 \color{black} = 1 \rightarrow \color{red} y = \color{forestgreen} 1 \color{black} + \color{forestgreen} \beta_1 \color{blue} x\)
That means, the slope \(\color{forestgreen} \beta_1\) still can vary, but the intercept is fixed to 1.
Free parameters give flexibility to your function: If you are unsure about the exact relationship between two variables, you can estimate the best-fitting parameters from the data.
For example, sometimes a theory specifies the general functional form of a relationship (e.g., “With increasing \(x\), \(y\) is monotonously decreasing”), but does not tell how fast this decrease happens, where \(y\) starts when \(x\) is minimal, etc. These latter decisions are then made by the free parameters.
As a linear function is unbounded, it can easily happen that the computed \(y\) exceeds the possible range of values.
If \(y\) has defined boundaries (e.g., \([0; 1]\)), a logistic function can bound the values between a lower and an upper limit (in the basic logistic function, between 0 and 1):
\(y = \frac{1}{1 + e^{-x}}\)
With the 4PL* model from IRT, you can adjust the functional form to your needs, by:
\(y = \text{min} + \frac{\text{max}-\text{min}}{1 + e^{a(d-x)}}\)
*4PL = 4-parameter logistic model
\(y = \text{min} + \frac{\text{max}-\text{min}}{1 + e^{a(d-x)}}\) (basic logistic function as dotted grey line)
The d, a, min, and max parameters can be used to “squeeze” the S-shaped curve into the range of your variables. For example, if your \(x\) variable is defined on the range \([0; 1]\), the following function parameters lead to a reasonable shift:
Of course, the logistic function is just a start - you can use the full toolbox of mathematical functions to implement your model!
Note
These considerations about functional forms, however, are typically not substantiated by psychological theory or background knowledge - at least at the start of a modeling project. We choose them, because we are (a) acquainted to it, and/or (b) they are mathematically convenient and tractable.
Empirical evidence can inform both your choice of the functional form, and, in a model-fitting step, the values of the parameters.
my_functionarg1, arg2return(return_variable). If no explicit return() statement is given, the last evaluated expression is returned by default.Tips:
roxygen2 documentation standard)R function implements exactly one functional relationship of your model.For our simulations, we need to ensure that our functions can handle vector inputs^*^. (Typically, all functions in R work are vectorized). This simply means, that arithmetic and logical operations are applied element-wise to all elements of a vector:
^*^ Parameters of the functions typically are passed as a single value, not a vector.
#' Compute the updated expected anxiety
#'
#' The expected anxiety at any given moment is a weighted average of
#' the momentary anxiety and the previous expected anxiety.
#'
#' @param momentary_anxiety The momentary anxiety, on a scale from 0 to 1
#' @param previous_expected_anxiety The previous expected anxiety, on a scale from 0 to 1
#' @param alpha A factor that shifts the weight between the momentary anxiety (alpha=1)
#' and the previous expected anxiety (alpha=0).
#' @return The updated expected anxiety, as a scalar on a scale from 0 to 1
get_expected_anxiety <- function(momentary_anxiety, previous_expected_anxiety, alpha=0.5) {
momentary_anxiety*alpha + previous_expected_anxiety*(1-alpha)
}roxygen2 comments start with #' and are placed directly above the function definition.
@param parameter_name Description. Provide the range of possible values if applicable.@return DescriptionCheck out roxygen2 and document our exponential decay function with:
Solution
#' Compute the probability to help in an emergency situation
#'
#' Calculate the probability of a bystander to help in a critical situation
#' as a function of the number of passive bystanders.
#'
#' @param NOPB Number of passive bystanders. Ranges from 0 to infinity (discrete).
#' @param y_0 Probability to help when no other bystanders are present (function intercept).
#' Ranges from 0 to 1 (continuous).
#' @param y_final Probability to help when number of other bystanders goes to infinity.
#' Ranges from 0 to 1 (continuous).
#' @param BSE_strength Strength of the bystander effect (function slope).
#' Ranges from -inf to +inf (continuous); the classical
#' bystander effect (i.e., decreasing probability with more bystanders)
#' occurs for positive values.
#'
#' @return Probability to help for the given parameters. Ranges from 0 to 1 (continuous).
get_p_help <- function(NOPB, y_0, y_final, BSE_strength) {
p_help <- y_final + (y_0 - y_final) * exp(-BSE_strength * NOPB)
return(p_help)
}Many psychological theories ultimately predict dichotomous outcomes (e.g., help vs. no help). In this case, often continuous latent variables are transformed into dichotomous manifest outcomes:
Behavior occurs if feltResp exceeds some threshold \(\theta\):
\[ \begin{equation} \text{helping} = \begin{cases} 1 & \text{if } \text{feltResp} \ge \theta\\ 0 & \text{if } \text{feltResp} \lt \theta \end{cases} \end{equation} \]
Any value > threshold \(\theta\) is mapped to outcome 1, all other values to outcome 0. Any input value will deterministically lead to the respective output value.
More compact:
Latent input variables in the [0; 1] range can be treated as a probability that is fed into a Bernoulli trial*:
Here, each run of the function will produce a different output, even for the same input value. Make your simulation reproducible by setting a seed with set.seed().
* Bernoulli trial: A random experiment with exactly two possible outcomes (e.g., success/failure, help/no help) where the probability of success is constant.
#' @param trait A latent trait on a *z* scale (ranging from -Inf to +Inf)
get_helping <- function(trait) {
# first convert trait to probability with a logistic transformation
# Use the 4PL logistic function to shift the midpoint etc.
prob <- 1 / (1 + exp(-trait))
# Transform probability to a random dichotomous outcome
helping <- rbinom(n=1, size=1, prob=prob)
return(helping)
}This is similar to the item response theory (IRT) approach to model dichotomous item responses based on a latent trait.
R with proper roxygen2 documentation in your group’s subfolder.Plot main input variable on the x-axis, output variable on the y-axis.
Moderating input variables and parameters can be represented by faceting (small multiples), color coding, or shapes.
seq(from, to, by), can be fine-grainedc(level1, level2, level3), typically only a few levels
Use the expand.grid() function to create all possible combination of a set of variables (the first varies fastest, the last varies slowest):
Do this for all relevant input variables and parameters of your function.
NOPB y_0 y_final BSE_strength
1 0 0.7 0 0.3
2 1 0.7 0 0.3
3 2 0.7 0 0.3
4 3 0.7 0 0.3
5 4 0.7 0 0.3
6 5 0.7 0 0.3
7 6 0.7 0 0.3
8 7 0.7 0 0.3
9 8 0.7 0 0.3
10 9 0.7 0 0.3
11 10 0.7 0 0.3
12 0 0.8 0 0.3
13 1 0.8 0 0.3
df <- expand.grid(
NOPB = seq(0, 10, by=1),
y_0 = c(0.7, 0.8, 0.9),
y_final = c(0, 0.1, 0.2),
BSE_strength = c(0.3, 0.5)
)
# compute the output variable
# (vectorized function application)
df$p_help <- get_p_help(
NOPB = df$NOPB,
y_0 = df$y_0,
y_final = df$y_final,
BSE_strength = df$BSE_strength
)
print(head(df, 20)) NOPB y_0 y_final BSE_strength p_help
1 0 0.7 0 0.3 0.70000000
2 1 0.7 0 0.3 0.51857275
3 2 0.7 0 0.3 0.38416815
4 3 0.7 0 0.3 0.28459876
5 4 0.7 0 0.3 0.21083595
6 5 0.7 0 0.3 0.15619111
7 6 0.7 0 0.3 0.11570922
8 7 0.7 0 0.3 0.08571950
9 8 0.7 0 0.3 0.06350257
10 9 0.7 0 0.3 0.04704386
11 10 0.7 0 0.3 0.03485095
12 0 0.8 0 0.3 0.80000000
13 1 0.8 0 0.3 0.59265458
14 2 0.8 0 0.3 0.43904931
15 3 0.8 0 0.3 0.32525573
16 4 0.8 0 0.3 0.24095537
17 5 0.8 0 0.3 0.17850413
18 6 0.8 0 0.3 0.13223911
19 7 0.8 0 0.3 0.09796514
20 8 0.8 0 0.3 0.07257436
library(ggplot2)
ggplot() +
geom_line(data=df, aes(x=NOPB, y=p_help, linetype=factor(BSE_strength), color=factor(y_0))) +
facet_grid(~ y_final) +
labs(
x = "Number of passive bystanders (NOPB)",
y = "Probability to help",
color = "y_0 (help when no bystanders)",
linetype = "BSE strength"
) + theme_minimal()Done as live demo, with ggplot2: Plot the atomic functions in the reasonable range of parameters to explore and understand their behavior.
Guiding questions:
Connect all functions to one “super-function” (which we will call the \(\Psi\) function), which takes all exogenous variables as input and computes the focal output variable(s):
# Pseudo-code:
get_x <- function(a, b) {a*2 + b}
get_y <- function(x) {x^2}
get_o <- function(y) {sqrt(y) + 3}
psi <- function(a, b) {
x <- get_x(a, b)
y <- get_y(x)
o <- get_o(y)
# return all relevant outputs as a list
# (e.g, the final output o, but also intermediate output y)
return(list(
y = y,
o = o
))
}Plot the \(\Psi\)-function: Does it produce the phenomenon you intended to model?
We can tune our free parameters to fit the model as good as possible to empirical data. This is called model fitting.
See the Find-a-fit app for an example of a simple linear regression with two parameters (intercept and slope) that are fitted by an optimization algorithm:
(Live demo how our exponential fit function could be fitted to empirical data)
To create a virtual experiment, we again use the expand.grid function to construct a design matrix of our experimental factors:
Now add one additional variable with a participant ID (this also determines the size of your sample):
pID valence speed
1 1 pos slow
2 2 pos slow
3 3 pos slow
4 1 neg slow
5 2 neg slow
6 3 neg slow
7 1 pos fast
8 2 pos fast
9 3 pos fast
10 1 neg fast
11 2 neg fast
12 3 neg fast
We have 12 participants overall: 3 participants in the pos/slow condition, 3 in the neg/slow condition, and so forth. Note that, although the participant ID repeats within each condition, these could be different (independent) participants if we assume a between-person design.
This approach always creates balanced designs where all conditions have the same number of participants.
In your own R project, create a new file simulation.R.
In case you had multiple versions of the functions within your group: For today, each group uses only one consensus version. One person should do the coding, the others observe and comment in a “pair-programming” style.
Create a design matrix with expand.grid() and the following fully crossed experimental factors (\(n=30\) participants per condition):
DoI) with the levels 0.2 and 0.8NOPB) with the levels 0, 1, and 4Hint
The resulting data frame should have 180 rows (2 x 3 x 30).
Add observed interindividual differences or situational variables. These are not experimentally fixed at specific levels, but vary randomly between participants. You can draw them from a random distribution, e.g. a normal distribution with rnorm():
pID valence speed age extra
1 1 pos slow 33 1.5
2 2 pos slow 26 -1.6
3 3 pos slow 27 0.2
4 1 neg slow 22 -0.7
5 2 neg slow 16 -0.2
6 3 neg slow 23 -1.9
7 1 pos fast 25 -1.9
8 2 pos fast 29 1.7
9 3 pos fast 29 1.4
10 1 neg fast 28 -1.0
11 2 neg fast 25 0.9
12 3 neg fast 27 -0.9
Assume you virtual participants differ in their base responsibility, which you previously defined as a probability on the range \([0; 1]\).
How can you generate random values that roughly look like a normal distribution, but are bounded to the defined range?
A handy distribution for drawing random values in the \([0; 1]\) range is the beta distribution. With its two parameters \(\alpha\) (also called \(a\) or shape1) and \(\beta\) (also called \(b\) or shape2), it can take many different forms:
How to choose \(\alpha\) and \(\beta\)? Asking ChatGPT for assistance:
ChatGPT prompt
Calibrate a beta distribution so that the peak is around 0.8 and the sd is around 0.1. Show a plot of the resulting distribution.
Answer
Here’s the calibrated beta distribution: α ≈ 12.99, β ≈ 4.00. It has a mode ≈ 0.80 and standard deviation ≈ 0.10, as targeted.
You can generate random values in R with the rbeta function. Here’s a comparison of a normal distribution and a matched beta distribution that respects the boundaries \([0; 1]\):
For simulations, it is good to know some basic distributions. Here are three interactive resources for choosing your distribution:
When we visually plotted and explored our atomic functions before, we tried out different parameter settings. When we simulate a concrete virtual sample, we need to fix them to one specific value or assign random values to them for each participant (e.g., if the treatment effect is different for each person).
If you do the latter, add the parameters as additional random observed variables to your data frame.
Add random variables for all exogenous* variables of your model that are not experimentally manipulated. For variables ranging from 0 to 1, use the beta distribution with appropriate parameters.
baseResp (if that is in your model).neuro), age (age), and gender (gender). (Note: We don’t need them for our model, just for practice.)Hint
The resulting data frame should still have 180 rows and additional columns for obnserved variables, including neuro, age, gender and all observed variables in your model.
* Reminder: Exogenous variables are those variables in your model that have no arrows pointing towards them.
Once all input variables have been simulated, submit them to your model function and compute the outcome variable \(y\).
pID valence speed age extra y
1 1 pos slow 33 1.5 10.7
2 2 pos slow 26 -1.6 14.2
3 3 pos slow 27 0.2 8.6
4 1 neg slow 22 -0.7 9.8
5 2 neg slow 16 -0.2 10.5
6 3 neg slow 23 -1.9 7.7
7 1 pos fast 25 -1.9 9.3
8 2 pos fast 29 1.7 10.4
9 3 pos fast 29 1.4 7.9
10 1 neg fast 28 -1.0 11.1
11 2 neg fast 25 0.9 9.5
12 3 neg fast 27 -0.9 4.8
This only works if the psi() function can handle vectors as input (i.e., you can submit the entire data frame of input variables to the function).
Not every person is identical, so in reality there probably are interindividual differences at several places:
We can simulate these interindividual differences - or we assume that some of them are constant for all participants.
Furthermore, our models are always simplifications of reality. We can never model all relevant variables; and measurement error adds further noise. Consequently there is some unexplained variability (aka. random noise) in the system.
All additional sources of variation could be modeled as a single random error term pointing to the final outcome variable. This summarizes all additional sources of variation that are not explicitly modeled:
Add additional (summative) error variance to output variable:
pID valence speed age extra y y_obs
1 1 pos slow 33 1.5 10.7 10.0
2 2 pos slow 26 -1.6 14.2 12.5
3 3 pos slow 27 0.2 8.6 6.3
4 1 neg slow 22 -0.7 9.8 12.6
5 2 neg slow 16 -0.2 10.5 10.9
6 3 neg slow 23 -1.9 7.7 6.8
7 1 pos fast 25 -1.9 9.3 12.3
8 2 pos fast 29 1.7 10.4 11.7
9 3 pos fast 29 1.4 7.9 10.2
10 1 neg fast 28 -1.0 11.1 8.9
11 2 neg fast 25 0.9 9.5 9.1
12 3 neg fast 27 -0.9 4.8 1.8
The size of the error variance (in combination with upstream sources of interindividual variance) determines the effect size that can be observed in a simulated study. The more error variance, the lower the effect size.
ggplot2. Create a plot that clearly shows the statistical pattern of the phenomenon you intended to model - like a plot that could be shown in a publication.Hint
For testing, set the number of participants to 10,000 to get a clear signal without noise. Then you can clearly see whether your model produces the expected pattern.
Simulate this design, analog to Fischer et al. (2006): “A 2 (bystander: yes vs. no) x 2 (danger: low vs high) factorial design was employed.”
“If the formal model and statistical pattern are appropriately anchored, and the formal model produces the statistical pattern, the theory can be said to explain the phenomenon. This does not mean that the explaining theory is true, or that explanatory power is a binary concept: rather, it comes in degrees and depends on the strength of the anchoring and production relations.” (van Dongen, 2025, p. 315)
Van Dongen et al. (2025). Productive explanation: A framework for evaluating explanations in psychological science. Psychological Review, 132(2), 311–329. https://doi.org/10.1037/rev0000479
These slides are part of the course Formal modeling in psychology at LMU Munich