Suppose you want to know whether higher player engagement would have changed a player’s in-game purchases. A prediction from observed data is not enough: the question asks what would happen after changing engagement while retaining what is known about that player. In a structural causal model (SCM), engagement and purchases are endogenous variables—the quantities whose causal behavior is modeled explicitly. section 8.4.2 section 9.1.3
Each endogenous variable corresponds to a node in the causal directed acyclic graph (DAG) and has an assignment function. The function sets its value from its causal parents. Exogenous variables supply inputs representing causes left outside the explicit model. In a common formulation, each endogenous variable receives its own latent exogenous parent; that one-to-one pairing is a modeling choice, not a requirement of every SCM. section 6.1.3 section 9.2.2
An additive assignment
If enzyme amount X causes measured protein amount Y, an additive SCM can set y := f_y(x, n_y) = g(x) + n_y. Here g(x) represents the modeled effect of enzyme amount, while n_y represents unmodeled influences, such as measurement noise. Additivity is an assumption about *how* the causes act, not part of the definition of an endogenous variable. section 6.4.3
An endogenous variable is determined once its assignment function’s inputs are fixed, but it can still vary across generated cases. The SCM draws exogenous inputs from probability distributions, then computes endogenous values. This differs from drawing each endogenous value directly from a distribution conditional on its modeled causal parents: the SCM places randomness in the exogenous inputs. section 6.1.7
Listing 6.3 The rock-throwing example from chapter 2 is an SCM
import pandas as pd
import random
def true_dgp(
jenny_inclination, #1
brian_inclination, #1
window_strength): #1
jenny_throws_rock = jenny_inclination > 0.5 #2
brian_throws_rock = brian_inclination > 0.5 #2
if jenny_throws_rock and brian_throws_rock: #3
strength_of_impact = 0.8 #3
elif jenny_throws_rock or brian_throws_rock: #3
strength_of_impact = 0.6 #3
else: #3
strength_of_impact = 0.0 #3
window_breaks = window_strength < strength_of_impact #4
return jenny_throws_rock, brian_throws_rock, window_breaks
generated_outcome = true_dgp(
jenny_inclination=random.uniform(0, 1), #5
brian_inclination=random.uniform(0, 1), #5
window_strength=random.uniform(0, 1) #5
)
The Python rock-throwing process makes the distinction concrete: inclinations and window strength enter as exogenous inputs; throw decisions and whether the window breaks are computed endogenous values. section 6.1.4
For counterfactual inference, observations of endogenous variables help update beliefs about exogenous inputs. After an intervention changes an assignment in a hypothetical world, the resulting endogenous values are computed using those updated inputs. If every endogenous variable is observed and the assignments are invertible, the exogenous values can be recovered exactly; otherwise, inference may be needed. section 9.1.3 section 9.3.3
Choosing assignments requires causal assumptions. Even a known DAG and joint distribution of its endogenous variables can be consistent with multiple SCMs. Observed statistical patterns alone therefore need not reveal the ground-truth functions that set their values. section 6.2.2

