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:

Mental model for causal models

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.