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
_ = sklearnThe 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)
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)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()
# 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)
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