Customizing Causal Mechanism Assignment
In GCM-based inference, fitting means, we learn the generative model of the variables in the graph from data. Before we can fit the variables to data, each node in a causal graph requires a generative causal model, or a “causal mechanism”. In this section, we’ll dive deeper into how to use this feature.
To understand this, let’s pull up the mental model for a probabilistic causal model (PCM) again:
On the left, it shows a trivial causal graph \(X \rightarrow Y\). \(X\) is a so-called root node (it has no parents), \(Y\) is a non-root node (it has parents). We fundamentally distinguishes between these two types of nodes.
For root nodes such as \(X\), the distribution \(P_x\) is modeled using a stochastic model.
Non-root nodes such as \(Y\) are modelled using a conditional stochastic model. DoWhy’s gcm package
defines corresponding interfaces for both, namely StochasticModel
and
ConditionalStochasticModel
.
The gcm package also provides ready-to-use implementations, such as ScipyDistribution
or BayesianGaussianMixtureDistribution
for
StochasticModel
, and AdditiveNoiseModel
for
ConditionalStochasticModel
.
Knowing that, we can now start to manually assign causal models to nodes according to our needs. Say, we know from domain knowledge, that our root node X follows a normal distribution. In this case, we can explicitly assign this:
>>> from scipy.stats import norm
>>> import networkx as nx
>>> from dowhy import gcm
>>>
>>> causal_model = gcm.ProbabilisticCausalModel(nx.DiGraph([('X', 'Y')]))
>>> causal_model.set_causal_mechanism('X', gcm.ScipyDistribution(norm))
For the non-root node Y, let’s use an additive noise model (ANM), represented by the
AdditiveNoiseModel
class. It has a
structural assignment of the form: \(Y := f(X) + N\). Here, f is a deterministic prediction
function, whereas N is a noise term. Let’s put all of this together:
>>> causal_model.set_causal_mechanism('Y',
>>> gcm.AdditiveNoiseModel(prediction_model=gcm.ml.create_linear_regressor(),
>>> noise_model=gcm.ScipyDistribution(norm)))
The rather interesting part here is the prediction_model
, which corresponds to our function
\(f\) above. This prediction model must satisfy the contract defined by
PredictionModel
, i.e. it must implement the methods:
def fit(self, X: np.ndarray, Y: np.ndarray) -> None: ...
def predict(self, X: np.ndarray) -> np.ndarray: ...
This interface is very analogous to model interfaces in many machine learning libraries, such as Scikit Learn. In fact the gcm package provides multiple adapter classes to make libraries such as Scikit Learn interoperable.
Now that we have associated a data-generating process to each node in the causal graph, let us prepare the training data.
>>> import numpy as np, pandas as pd
>>> X = np.random.normal(loc=0, scale=1, size=1000)
>>> Y = 2*X + np.random.normal(loc=0, scale=1, size=1000)
>>> data = pd.DataFrame(data=dict(X=X, Y=Y))
Finally, we can learn the parameters of those causal models from the training data.
>>> gcm.fit(causal_model, data)
causal_model
is now ready to be used for various types of causal queries as explained in
Performing Causal Tasks.
Note
As mentioned above, DoWhy has a wrapper class that supports scikit learn models out of the box. For instance
>>> from sklearn.ensemble import RandomForestRegressor
>>> causal_model.set_causal_mechanism('Y', gcm.AdditiveNoiseModel(gcm.ml.SklearnRegressionModel(RandomForestRegressor)))
would use a RandomForestRegressor instead of a LinearRegressor from the sklearn package.
Using ground truth models
In some scenarios the ground truth models might be known and should be used instead. Let’s
assume, we know that our relationship are linear with coefficients \(\alpha = 2\) and
\(\beta = 3\). Let’s make use of this knowledge by creating a custom prediction model that
implements the PredictionModel
interface:
>>> import dowhy.gcm.ml.prediction_model
>>>
>>> class MyCustomModel(gcm.ml.PredictionModel):
>>> def __init__(self, coefficient):
>>> self.coefficient = coefficient
>>>
>>> def fit(self, X, Y):
>>> # Nothing to fit here, since we know the ground truth.
>>> pass
>>>
>>> def predict(self, X):
>>> return self.coefficient * X
>>>
>>> def clone(self):
>>> return MyCustomModel(self.coefficient)
Now we can use this in our ANMs instead:
>>> causal_model.set_causal_mechanism('Y', gcm.AdditiveNoiseModel(MyCustomModel(2)))
>>> gcm.fit(causal_model, data)
Note
Important: When a function or algorithm is called that requires a causal graph, DoWhy GCM sorts the input features internally based on their alphabetical order. For instance, in case of the MyCustomModel above, if the names of the input features are ‘X2’ and ‘X1’, the model should expect ‘X1’ in the first input and ‘X2’ in the second column.