Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

CausalNex API Tutorial

This notebook explores the CausalNex library for causal inference using Bayesian Networks. It demonstrates the complete workflow from structure learning to inference and causal interventions.

References:

%load_ext autoreload
%autoreload 2
%matplotlib inline

# System libraries.
import logging

# Third-party libraries.
import pandas as pd
import matplotlib.pyplot as plt
from IPython.display import display

# Note: we need to import sklearn first to avoid conflicts with libgomp-d22c30c5.so.1.0.0: cannot allocate memory in static TLS block
import sklearn

_ = sklearn
The autoreload extension is already loaded. To reload it, use:
  %reload_ext autoreload
# import helpers.hmodule as hmodule
# hmodule.install_module_if_not_present(
#     [""],
#     use_activate=True,
#     use_sudo=False,
#     venv_path="/opt/venv",
# )
# Helpers packages.

# Tutorial-specific packages.
import tutorials.causalnex.causalnex_utils as tcnut
import causalnex

print(causalnex.__version__)

_LOG = logging.getLogger(__name__)
tcnut.init_loggers(_LOG)

_LOG.info("Test _LOG.info")
0.12.1
WARNING: Logger already initialized: skipping
Test _LOG.info

Cell 1: Load Data

# Load the student performance dataset from https://archive.ics.uci.edu/dataset/320/student+performance
df = tcnut.load_student_performance_data(data_dir="data")
_LOG.info("Dataset shape: %s", df.shape)
display(df.head())
File already exists at '/git_root/tutorials/causalnex/data/student_performance.zip', skipping download
Loading student performance data from '/git_root/tutorials/causalnex/data/student_performance/student-mat.csv'
Data loaded: 395 rows, 33 columns
Dataset shape: (395, 33)
Loading...
print(df.columns)
Index(['school', 'sex', 'age', 'address', 'famsize', 'Pstatus', 'Medu', 'Fedu', 'Mjob', 'Fjob', 'reason', 'guardian', 'traveltime', 'studytime', 'failures', 'schoolsup', 'famsup', 'paid', 'activities', 'nursery', 'higher', 'internet', 'romantic', 'famrel', 'freetime', 'goout', 'Dalc', 'Walc', 'health', 'absences', 'G1', 'G2', 'G3'], dtype='object')
!cat /git_root/tutorials/causalnex/data/info.txt 
- This data approach student achievement in secondary education of two Portuguese
  schools

- The data attributes include student grades, demographic, social and school
  related features) and it was collected by using school reports and
  questionnaires

- Two datasets are provided regarding the performance in two distinct subjects:
  Mathematics (mat) and Portuguese language (por). In [Cortez and Silva, 2008],
  the two datasets were modeled under binary/five-level classification and
  regression tasks.

- Important note: the target attribute G3 has a strong correlation with
  attributes G2 and G1. This occurs because G3 is the final year grade (issued at
  the 3rd period), while G1 and G2 correspond to the 1st and 2nd period grades.
  It is more difficult to predict G3 without G2 and G1, but such prediction is
  much more useful 
!cat /git_root/tutorials/causalnex/data/student.txt
# Attributes for both student-mat.csv (Math course) and student-por.csv (Portuguese language course) datasets:
1 school - student's school (binary: "GP" - Gabriel Pereira or "MS" - Mousinho da Silveira)
2 sex - student's sex (binary: "F" - female or "M" - male)
3 age - student's age (numeric: from 15 to 22)
4 address - student's home address type (binary: "U" - urban or "R" - rural)
5 famsize - family size (binary: "LE3" - less or equal to 3 or "GT3" - greater than 3)
6 Pstatus - parent's cohabitation status (binary: "T" - living together or "A" - apart)
7 Medu - mother's education (numeric: 0 - none,  1 - primary education (4th grade), 2 – 5th to 9th grade, 3 – secondary education or 4 – higher education)
8 Fedu - father's education (numeric: 0 - none,  1 - primary education (4th grade), 2 – 5th to 9th grade, 3 – secondary education or 4 – higher education)
9 Mjob - mother's job (nominal: "teacher", "health" care related, civil "services" (e.g. administrative or police), "at_home" or "other")
10 Fjob - father's job (nominal: "teacher", "health" care related, civil "services" (e.g. administrative or police), "at_home" or "other")
11 reason - reason to choose this school (nominal: close to "home", school "reputation", "course" preference or "other")
12 guardian - student's guardian (nominal: "mother", "father" or "other")
13 traveltime - home to school travel time (numeric: 1 - <15 min., 2 - 15 to 30 min., 3 - 30 min. to 1 hour, or 4 - >1 hour)
14 studytime - weekly study time (numeric: 1 - <2 hours, 2 - 2 to 5 hours, 3 - 5 to 10 hours, or 4 - >10 hours)
15 failures - number of past class failures (numeric: n if 1<=n<3, else 4)
16 schoolsup - extra educational support (binary: yes or no)
17 famsup - family educational support (binary: yes or no)
18 paid - extra paid classes within the course subject (Math or Portuguese) (binary: yes or no)
19 activities - extra-curricular activities (binary: yes or no)
20 nursery - attended nursery school (binary: yes or no)
21 higher - wants to take higher education (binary: yes or no)
22 internet - Internet access at home (binary: yes or no)
23 romantic - with a romantic relationship (binary: yes or no)
24 famrel - quality of family relationships (numeric: from 1 - very bad to 5 - excellent)
25 freetime - free time after school (numeric: from 1 - very low to 5 - very high)
26 goout - going out with friends (numeric: from 1 - very low to 5 - very high)
27 Dalc - workday alcohol consumption (numeric: from 1 - very low to 5 - very high)
28 Walc - weekend alcohol consumption (numeric: from 1 - very low to 5 - very high)
29 health - current health status (numeric: from 1 - very bad to 5 - very good)
30 absences - number of school absences (numeric: from 0 to 93)

# these grades are related with the course subject, Math or Portuguese:
31 G1 - first period grade (numeric: from 0 to 20)
31 G2 - second period grade (numeric: from 0 to 20)
32 G3 - final grade (numeric: from 0 to 20, output target)

Additional note: there are several (382) students that belong to both datasets . 
These students can be identified by searching for identical attributes
that characterize each student, as shown in the annexed R file.
metadata_df = pd.read_csv("/git_root/tutorials/causalnex/data/metadata.csv")
display(metadata_df)
Loading...

Cell 2: Structure Learning

Define the causal structure of the Bayesian Network by specifying relationships between variables.

  • Manual definition via domain expertise
  • Algorithmic learning using NOTEARS algorithm
  • Hybrid approach combining both methods
from causalnex.structure import StructureModel

# Create a StructureModel instance to define causal relationships.
sm = StructureModel()
# Add edges representing causal relationships between variables.
sm.add_edges_from(
    [
        ("health", "absences"),
        ("health", "G1"),
        ("studytime", "G1"),
        ("studytime", "G2"),
        ("G1", "G2"),
        ("absences", "G1"),
        ("absences", "G2"),
    ]
)
print(f"Nodes: {list(sm.nodes)}")
print(f"Edges: {list(sm.edges)}")
Structure nodes: ['health', 'absences', 'G1', 'studytime', 'G2']
Structure edges: [('health', 'absences'), ('health', 'G1'), ('absences', 'G1'), ('absences', 'G2'), ('G1', 'G2'), ('studytime', 'G1'), ('studytime', 'G2')]
Nodes: ['health', 'absences', 'G1', 'studytime', 'G2']
Edges: [('health', 'absences'), ('health', 'G1'), ('absences', 'G1'), ('absences', 'G2'), ('G1', 'G2'), ('studytime', 'G1'), ('studytime', 'G2')]
# Plot the relationships using networkx.
if False:
    from causalnex.plots import plot_structure, NODE_STYLE, EDGE_STYLE

    viz = plot_structure(
        sm,
        all_node_attributes=NODE_STYLE.WEAK,
        all_edge_attributes=EDGE_STYLE.WEAK,
    )
    # viz.save_graph("graph.html")
    # display(HTML("graph.html"))
# Plot the relationships using networkx.
G = nx.DiGraph(sm)
plt.figure(figsize=(6, 4))

pos = nx.spring_layout(G, seed=42)

nx.draw(
    G,
    pos,
    with_labels=True,
    node_color="lightblue",
    node_size=2000,
    font_size=10,
    arrows=True,
)

plt.title("Graph Visualization")

plt.show()
<Figure size 600x400 with 1 Axes>
# Plot using pygraphviz.
# from networkx.drawing.nx_pydot import graphviz_layout
# import matplotlib.pyplot as plt
# pos = graphviz_layout(G, prog="dot")
# nx.draw(
#     G,
#     pos,
#     with_labels=True,
#     node_color="lightgreen",
#     node_size=2000,
#     arrows=True,
# )
# plt.show()

Cell 3: Data Discretization

Convert continuous features into categorical buckets with meaningful labels. Bayesian Networks require discrete distributions for probability estimation.

# Create a copy of the dataframe for discretization.
df_discrete = df.copy()

# Discretize continuous variables into categorical buckets.
# Replace originals so column names match the structure model nodes.
df_discrete["studytime"] = pd.cut(
    df["studytime"],
    bins=[0, 1, 2, 3, 4],
    labels=["very_low", "low", "medium", "high"],
).astype(str)
df_discrete["absences"] = pd.cut(
    df["absences"],
    bins=[-1, 5, 10, 20, 100],
    labels=["low", "medium", "high", "very_high"],
).astype(str)
df_discrete["G1"] = pd.cut(
    df["G1"], bins=[-1, 10, 20], labels=["fail", "pass"]
).astype(str)
df_discrete["G2"] = pd.cut(
    df["G2"], bins=[-1, 10, 20], labels=["fail", "pass"]
).astype(str)
df_discrete["health"] = df["health"].astype(str)
_LOG.info("Discretized data shape: %s", df_discrete.shape)

display(df_discrete[["health", "studytime", "absences", "G1", "G2"]].head())
Discretized data shape: (395, 33)
Loading...

Cell 4: CPD Fitting

Learn conditional probability distributions (CPDs) from the training data. CPDs represent the probability of each variable given its parents in the network.

from causalnex.network import BayesianNetwork

# Create a Bayesian Network from the structure model.
bn = BayesianNetwork(sm)
# Select only the columns needed for the network.
cols = ["health", "studytime", "absences", "G1", "G2"]
df_fit = df_discrete[cols].copy()
# Learn the categorical states for each node before fitting the CPDs.
bn = bn.fit_node_states(df_fit)
# Fit the network to the data.
bn.fit_cpds(
    df_fit,
    method="BayesianEstimator",
    bayes_prior="BDeu",
    equivalent_sample_size=10,
)
_LOG.info("CPDs fitted successfully")
_LOG.info("Network CPDs: %s", list(bn.cpds.keys()))
print(f"CPDs: {list(bn.cpds.keys())}")
CPDs fitted successfully
Network CPDs: ['health', 'absences', 'G1', 'G2', 'studytime']
CPDs: ['health', 'absences', 'G1', 'G2', 'studytime']

Cell 5: Model Validation

Evaluate the model using classification metrics on test data. Validate that the learned network makes accurate predictions.

# Split data into training and test sets.
train_size = int(0.8 * len(df_discrete))
train_data = df_discrete[:train_size]
test_data = df_discrete[train_size:]
# Get predictions from the Bayesian Network on test data.
input_cols = [c for c in cols if c != "G2"]
predictions_df = bn.predict(test_data[cols], "G2")
_LOG.info("Test set size: %s", len(test_data))
_LOG.info("Predictions made: %s", len(predictions_df))
print(f"Test set size: {len(test_data)}")
print(f"Predictions made: {len(predictions_df)}")
print(predictions_df.head())
Test set size: 79
Predictions made: 79
Test set size: 79
Predictions made: 79
    G2_prediction
316          fail
317          fail
318          pass
319          pass
320          pass

Cell 6: Inference & Querying

Extract insights through conditional probability queries. Compute marginal and conditional probabilities given observations.

# Extract the CPD for G2 (second period grade) as a DataFrame.
cpd_g2 = bn.cpds.get("G2")
if cpd_g2 is not None:
    _LOG.info("CPD for G2 shape: %s", cpd_g2.shape)
    _LOG.info("G2 states: %s", list(cpd_g2.index))
    print(f"CPD shape: {cpd_g2.shape}")
    print(f"G2 states: {list(cpd_g2.index)}")
    print(cpd_g2.head())
# Perform inference with observations.
_ = bn.fit_cpds(
    train_data[cols],
    method="BayesianEstimator",
    bayes_prior="BDeu",
    equivalent_sample_size=10,
)
_LOG.info("Inference complete on training data")
CPD for G2 shape: (2, 32)
G2 states: ['fail', 'pass']
CPD shape: (2, 32)
G2 states: ['fail', 'pass']
G1        fail                                                                                                                                                      pass                                                                                                                                                    
absences  high                                 low                                 medium                               very_high                                   high                                   low                                  medium                               very_high                              
studytime high      low medium  very_low      high       low   medium  very_low      high       low    medium  very_low      high       low    medium very_low      high      low   medium  very_low      high       low    medium  very_low      high       low    medium  very_low      high       low    medium  very_low
G2                                                                                                                                                                                                                                                                                                                          
fail       0.5  0.92449    0.5  0.875839  0.978632  0.902764  0.81769  0.905253  0.594118  0.893846  0.963768  0.809392       0.5  0.970588  0.880952  0.95283  0.119048  0.28157  0.04717  0.217647  0.018797  0.049852  0.146799  0.038144  0.024752  0.112121  0.190608  0.086854       0.5  0.268116  0.119048  0.119048
pass       0.5  0.07551    0.5  0.124161  0.021368  0.097236  0.18231  0.094747  0.405882  0.106154  0.036232  0.190608       0.5  0.029412  0.119048  0.04717  0.880952  0.71843  0.95283  0.782353  0.981203  0.950148  0.853201  0.961856  0.975248  0.887879  0.809392  0.913146       0.5  0.731884  0.880952  0.880952
INFO  Replacing existing CPD for health
INFO  Replacing existing CPD for absences
INFO  Replacing existing CPD for G1
INFO  Replacing existing CPD for G2
INFO  Replacing existing CPD for studytime
Inference complete on training data

Cell 7: Causal Interventions

Apply “do” operators to simulate policy changes. Estimate the causal effect of interventions on outcomes.

# Create counterfactual scenarios by intervention.
df_intervention = df_discrete.copy()
# Intervene: set studytime to 'high' for all students.
df_intervention["studytime"] = "high"
# Predict G2 outcomes under the intervention using the fitted network.
intervention_preds = bn.predict(df_intervention[cols], "G2")
pred_col = intervention_preds.columns[0]
# Compare outcomes before and after intervention.
pass_rate_before = (df_discrete["G2"] == "pass").sum() / len(df_discrete)
pass_rate_after = (intervention_preds[pred_col] == "pass").sum() / len(
    intervention_preds
)
improvement = (pass_rate_after - pass_rate_before) * 100
_LOG.info("Pass rate before intervention: %.1f%%", pass_rate_before * 100)
_LOG.info(
    "Pass rate after intervention (hypothetical): %.1f%%", pass_rate_after * 100
)
_LOG.info("Intervention effect: %.1f%% improvement", improvement)
print(f"Pass rate before: {pass_rate_before * 100:.1f}%")
print(f"Pass rate after: {pass_rate_after * 100:.1f}%")
print(f"Improvement: {improvement:.1f}%")
Pass rate before intervention: 51.4%
Pass rate after intervention (hypothetical): 49.6%
Intervention effect: -1.8% improvement
Pass rate before: 51.4%
Pass rate after: 49.6%
Improvement: -1.8%

Cell 8: Network Visualization

Visualize the causal structure and relationships.

import networkx as nx

# Create a figure to display the causal graph.
fig, ax = plt.subplots(figsize=(10, 8))
# Draw the structure model with nodes and edges using networkx.
pos = nx.spring_layout(sm, seed=42)
nx.draw(
    sm,
    pos=pos,
    with_labels=True,
    node_color="lightblue",
    node_size=3000,
    font_size=10,
    arrows=True,
    arrowsize=20,
    ax=ax,
)
ax.set_title(
    "Causal Structure: Student Performance", fontsize=14, fontweight="bold"
)
plt.tight_layout()
# Save the figure since the script runs headless without a display.
fig.savefig("/git_root/tutorials/causalnex/causal_structure.png")
plt.close(fig)
_LOG.info("Network visualization saved to causal_structure.png")
Network visualization saved to causal_structure.png