Package: scmodels
OSStatusLinuxWindowsMacA Python package implementing Structural Causal Models (SCM).The library uses the CAS librarySymPyto allow the user to state arbitrary assignment functions and noise distributions as supported by SymPy and builds the DAG withnetworkx.It supports the features:SamplingInterveningPlottingPrintingand by extension all methods on a DAG provided by networkx after accessing the member variable dagInstallationEither install via pippip install scmodelsor via cloning the repository and running the installation locallygit clone https://github.com/maichmueller/scmodels
cd scmodels
pip install .Building an SCMIn order to declare the SCM$X ~ \leftarrow ~ Z ~ \rightarrow ~ V ~ \rightarrow ~ W$$\downarrow ~ ~ \swarrow$$Y$with the assignments$Z \sim \text{LogLogistic} (\alpha=1,\beta=1)$$X=3Z^2 \cdot N \quad [N \sim \text{LogNormal}(\mu=1,\sigma=1)]$$Y = 2Z + \sqrt{X} + N \quad [N \sim \text{Normal}(\mu=2,\sigma=1)]$$V = Z + N^P \quad [N \sim \text{Normal}(\mu=0,\sigma=1), P \sim \text{Ber}(0.5)]$$W = V + \exp(T) - \log(M) * N \quad [N \sim \text{Normal}(\mu=0,\sigma=1), T \sim \text{StudentT}(\nu = 0.5), M \sim \text{Exp}(\lambda = 1)]$there are 2 different ways implemented.1. List Of StringsDescribe the assignments as strings of the form:'VAR = FUNC(Noise, parent1, parent2, ...), Noise ~ DistributionXYZ'Note that in this case, one must not restate the noise symbol string in the distribution (as would otherwise be necessary in constructing sympy distributions).fromscmodelsimportSCMassignment_seq=["Z = M, M ~ LogLogistic(alpha=1, beta=1)","X = N * 3 * Z ** 2, N ~ LogNormal(mean=1, std=1)","Y = P + 2 * Z + sqrt(X), P ~ Normal(mean=2, std=1)","V = N**P + Z, N ~ Normal(0,1) / P ~ Bernoulli(0.5)","W = exp(T) - log(M) * N + V, M ~ Exponential(1) / T ~ StudentT(0.5) / N ~ Normal(0, 2)",]myscm=SCM(assignment_seq)Agreements:The name of the noise variable in the distribution specification
(e.g.P ~ Normal(mean=2, std=1)) has to align with the noise variable
name (P) of the assignment string.Multiple noise models can be composited as done for variableWin the above example.
The noise model string segment must specify all individual noise models separated by a '/'2. Assignment MapOne can construct the SCM via an assignment map with the variables as keys
and a tuple defining the assignment and the noise.2-Tuple: Assignments via SymPy parsingTo refer to SymPy's string parsing capability (this includes numpy functions) provide a dict entry
with a 2-tuples as values of the form:'var': ('assignment name string', noise)fromsympy.statsimportLogLogistic,LogNormal,Normal,Bernoulli,StudentT,Exponentialassignment_map={"Z":("M",LogLogistic("M",alpha=1,beta=1)),"X":("N * 3 * Z ** 2",LogNormal("N",mean=1,std=1),),"Y":("P + 2 * Z + sqrt(X)",Normal("P",mean=2,std=1),),"V":("N**P + Z",[Normal("N",0,1),Bernoulli("P",0.5)]),"W":("exp(T) - log(M) * N + V",[Normal("N",0,1),StudentT("T",0.5),Exponential("M",1)])}myscm2=SCM(assignment_map)Agreements:the name of the noise distribution provided in its constructor
(e.g.Normal("N", mean=2, std=1)) has to align with the noise variable
name (N) of the assignment string.Multiple noise models in the assignment must be wrapped in an iterable (e.g. a list[], or tuple())3-Tuple: Assignments with arbitrary callablesOne can also declare the SCM via specifying the variable assignment in a dictionary with the
variables as keys and as values a sequence of length 3 of the form:'var': (['parent1', 'parent2', ...], Callable, Noise)This allows the user to supply complex functions outside the space of analytical functions.importnumpyasnpdefY_assignment(p,z,x):returnp+2*z+np.sqrt(x)functional_map={"Z":([],lambdam:m,LogLogistic("M",alpha=1,beta=1)),"X":(["Z"],lambdan,z:n*3*z**2,LogNormal("N",mean=1,std=1),),"Y":(["Z","X"],Y_assignment,Normal("P",mean=2,std=1),),"V":(["Z"],lambdan,p,z:n**p+z,[Normal("N",mean=0,std=1),Bernoulli("P",p=0.5)]),"W":(["V"],lambdan,t,m,v:np.exp(m)-np.log(m)*n+v,[Normal("N",mean=0,std=1),StudentT("T",nu=0.5),Exponential("M",rate=1)])}myscm3=SCM(functional_map)Agreements:The callable's first parameter MUST be the noise input (unless the noise distribution isNone).The order of variables in the parents list determines the semantic order of input for parameters in the functional
(left to right).FeaturesPrettyprintThe SCM supports a form of informative printing of its current setup,
which includes mentioning active interventions and the assignments.print(myscm)Structural Causal Model of 5 variables: Z, X, V, Y, W
Variables with active interventions: []
Assignments:
Z := f(M) = M	 [ M ~ LogLogistic(alpha=1, beta=1) ]
X := f(N, Z) = N * 3 * Z ** 2	 [ N ~ LogNormal(mean=1, std=1) ]
Y := f(P, Z, X) = P + 2 * Z + sqrt(X)	 [ P ~ Normal(mean=2, std=1) ]
V := f(N, P, Z) = N**P + Z	 [ N ~ Normal(mean=0, std=1), P ~ Bernoulli(p=0.5, succ=1, fail=0) ]
W := f(M, T, N, V) = exp(T) - log(M) * N + V	 [ M ~ Exponential(rate=1), T ~ StudentT(nu=0.5), N ~ Normal(mean=0, std=2) ]In the case of custom callable assignments, the output is less informativeprint(myscm3)Structural Causal Model of 5 variables: Z, X, V, Y, W
Variables with active interventions: []
Assignments:
Z := f(M) = __unknown__	 [ M ~ LogLogistic(alpha=1, beta=1) ]
X := f(N, Z) = __unknown__	 [ N ~ LogNormal(mean=1, std=1) ]
Y := f(P, Z, X) = __unknown__	 [ P ~ Normal(mean=2, std=1) ]
V := f(N, P, Z) = __unknown__	 [ N ~ Normal(mean=0, std=1), P ~ Bernoulli(p=0.5, succ=1, fail=0) ]
W := f(N, T, M, V) = __unknown__	 [ N ~ Normal(mean=0, std=1), T ~ StudentT(nu=0.5), M ~ Exponential(rate=1) ]InterventionsOne can easily perform interventions on the variables,
e.g. a Do-intervention or also general interventions, which remodel the connections, assignments, and noise distributions.
For general interventions, the passing structure is dict of the following form:{var: (New Parents (Optional), New Assignment (optional), New Noise (optional))}Any part of the original variable state, that is meant to be left unchanged, has to be passed asNone.
E.g. to assign a new callable assignment to variableXwithout changing parents or noise, one would call:my_new_callable=lambdan,z:n+zmyscm.intervention({"X":(None,my_new_callable,None)})For the example of the do-intervention $\text{do}(X=1)$,
one can use the helper methoddo_intervention. The pendant for noise interventions is calledsoft_intervention:myscm.do_intervention([("X",1)])fromsympy.statsimportFiniteRVmyscm.soft_intervention([("X",FiniteRV(str(myscm["X"].noise),density={-1:.5,1:.5}))])Callingundo_interventionrestores the original state of all variables from construction time, that have been passed. One can optionally specify,
If no variables are specified (variables=None), all interventions are undone.myscm.undo_intervention(variables=["X"])SamplingThe SCM allows drawing as many samples as needed through the methodmyscm.sample(n).n=5samples=myscm.sample(n)display_data(samples)ZVXWY00.4589710.46860315.97738.690916.4105510.9386291.9386319.70123.420968.3979920.3721481.372150.1066421.740834.1158631.24888-0.18783113.90630.1593017.6686240.641981.6419815.8582-2.544856.30742If infinite sampling is desired, one can also receive a sampling generator throughcontainer={var:[]forvarinmyscm}sampler=myscm.sample_iter(container)containeris an optional target dictionary to store the computed samples in.foriinrange(n):next(sampler)display_data(container)ZVXWY00.553284-0.3276673.26078-0.5020144.5957210.6879761.687982.286241.820784.2611120.06252121.062520.008143410.1548922.1143233.818344.81834143.2746.59465e+0821.358940.204415-2.329990.863289-0.6374282.62001If the target container is not provided, the generator returns a newdictfor every sample.sample=next(myscm.sample_iter())display_data(sample)ZVXWY00.07943890.09274720.1322140.04711773.73759PlottingIf you have graphviz installed, you can plot the DAG by callingmyscm.plot(node_size=1000,alpha=1)