bnlearn
Purpose
Use this skill when working with Bayesian Networks, probabilistic graphical
models, structure learning, parameter learning, inference, prediction,
sampling, and causal discovery using the Python bnlearn library.
The goal of this skill is to help an AI agent:
- Understand the statistical problem.
- Select an appropriate Bayesian Network approach.
- Select an appropriate bnlearn algorithm, score, or test.
- Prepare and validate the data.
- Execute the analysis using the bnlearn API.
- Validate the resulting network.
- Interpret the results without making unsupported causal claims.
1. Core Concepts
A Bayesian Network (BN) is a directed acyclic graph (DAG) in which:
- Nodes represent random variables.
- Directed edges represent conditional dependencies.
- Each node has an associated conditional probability distribution (CPD).
- The graph encodes conditional independence relationships.
The main Bayesian Network workflow is:
Data
│
▼
Data inspection
│
▼
Structure learning
│
▼
Network structure
│
▼
Parameter learning
│
▼
Bayesian Network
│
├── Inference
├── Prediction
├── Sampling
├── Intervention
└── Visualization
Do not confuse:
association
≠
conditional dependence
≠
directed edge
≠
causal relationship
A learned DAG should not automatically be interpreted as a causal graph.
2. When to Use bnlearn skill
Use bnlearn when the task involves:
- Learning dependencies between variables.
- Learning a Bayesian Network from observational data.
- Learning a DAG structure.
- Estimating Bayesian Network parameters.
- Performing probabilistic inference.
- Predicting variables using a Bayesian Network.
- Sampling from a Bayesian Network.
- Finding conditional dependencies.
- Testing conditional independence.
- Comparing Bayesian Network structures.
- Discovering potentially causal structures.
- Performing interventions.
- Visualizing Bayesian Networks.
Do not automatically use bnlearn for:
- Ordinary regression.
- Ordinary clustering.
- Dimensionality reduction.
- Classification when a Bayesian Network is not required.
- Time-series forecasting when temporal structure is the primary problem.
- Deep learning problems.
3. Identify the User's Task
Before selecting a bnlearn function, determine what the user is trying to accomplish.
| User goal | bnlearn workflow |
|---|---|
| Discover relationships | Structure learning |
| Find a DAG | Structure learning |
| Test conditional independence | Independence testing |
| Estimate probabilities | Parameter learning |
| Ask probability questions | Inference |
| Predict a variable | Prediction |
| Generate synthetic observations | Sampling |
| Compare networks | Structure scoring |
| Ask whether X causes Y | Causal discovery / intervention |
| Visualize a network | Network visualization |
Do not select an algorithm before identifying the problem type.
4. Standard Workflow
For a new Bayesian Network problem, follow this workflow:
- Identify the user's goal.
- Select the appropriate method.
- Consult the relevant reference documentation.
- Prefer an existing example when one matches the task.
- Adapt the example to the user's data.
Now we move to the data set:
- Inspect the dataset.
- Identify variable types.
- Check missing values.
- Check categorical cardinalities.
- Inspect continuous-variable distributions.
- Determine whether preprocessing is required.
- Determine whether the data are discrete, continuous, or mixed.
- Select a structure-learning strategy.
- Select an appropriate score or independence test.
- Apply domain constraints when available.
- Learn the network structure.
- Inspect the learned structure.
- Validate the structure.
- Learn the network parameters.
- Validate the fitted model.
- Perform inference, prediction, sampling, or intervention.
- Interpret the results.
- Clearly distinguish statistical dependency from causal interpretation.
5. Data Preparation
Before structure learning, inspect:
- Number of observations.
- Number of variables.
- Variable types.
- Missing values.
- Constant variables.
- Duplicate variables.
- Categorical cardinality.
- Continuous-variable distributions.
- Outliers.
- Highly correlated variables.
- Sample size relative to network complexity.
Do not silently modify the dataset.
If preprocessing is required, explain why it is being performed.
5.1 Discrete Variables
Discrete variables may represent:
- Categories.
- Binary states.
- Ordinal states.
- Discretized measurements.
Check whether categorical variables are represented consistently.
Avoid unnecessarily creating a very large number of states because this can make parameter estimation unreliable.
High cardinality: If a categorical column has many rare levels (e.g. job
titles, product IDs, free-text codes), group rare levels into an "Other"
category (or a domain-sensible bin) before structure learning. Large
cardinality explodes CPD size and makes scores/CI tests unstable. See §18.10 D.
5.2 Continuous Variables
Do not automatically discretize continuous variables.
First determine whether a continuous Bayesian Network is appropriate.
Inspect:
- Distribution shape.
- Outliers.
- Transformations.
- Approximate Gaussianity.
- Sample size.
- Nonlinear relationships.
- Measurement scale.
When the assumptions are appropriate, prefer continuous structure learning
(scoretype='bic-g' / 'aic-g' / 'loglik-g', or LiNGAM methods) rather
than discarding information through discretization.
API limitation: parameter_learning.fit, inference.fit, and
sampling in bnlearn are built around discrete TabularCPDs. After
continuous structure learning you obtain a DAG structure; do not expect
the discrete parameter/inference/sampling pipeline to work unchanged on
raw continuous columns. If discrete inference is required, discretize
explicitly (see bn.discretize) and state that choice.
See:
references/continuous_models.md
5.3 Mixed Variables
When variables contain a mixture of discrete and continuous values:
- Identify the variable types.
- Determine whether the selected bnlearn method supports the combination.
- Select a compatible scoring or testing method.
- Do not silently convert variables without explaining the consequences.
See:
references/continuous_models.md
6. Structure Learning
Structure learning determines the dependency structure of the Bayesian Network.
Exact call (see §18 for full signature):
model = bn.structure_learning.fit(df, methodtype='hc', scoretype='bic')
The main approaches are:
Structure Learning
│
├── Score-based (methodtype='hc' | 'ex')
│ └── Search for a graph that optimizes a score
│
├── Constraint-based (methodtype='pc' | 'cs')
│ └── Infer structure using conditional independence tests
│
├── Tree / classification (methodtype='cl' | 'tan' | 'nb')
│
└── Causal discovery (methodtype='direct-lingam' | 'ica-lingam')
6.1 Score-Based Learning
Use score-based learning when the goal is to find a graph that optimizes a statistical score.
Typical approaches:
methodtype='hc'— Hill Climbing (default, recommended starting point)methodtype='ex'— Exhaustive Search (only for very small networks)
Typical scores:
- Discrete:
bic,aic,k2,bdeu,bds - Continuous (Gaussian):
bic-g,aic-g,loglik-g
See:
references/structure_learning.md
and:
references/scoring.md
6.2 Constraint-Based Learning
Constraint-based methods infer graph structure from conditional independence relationships.
model = bn.structure_learning.fit(
df,
methodtype='pc', # or 'cs'
params_pc={'ci_test': 'chi_square', 'alpha': 0.05},
)
Use this approach when conditional independence testing is central to the analysis.
Consider:
- Variable type
- Independence test (
params_pc['ci_test']) - Significance level (
params_pc['alpha']) - Sample size
- Multiple testing
- Causal assumptions
See:
references/structure_learning.md
6.3 Tree, classification and causal methods
- Tree-structured:
methodtype='cl'/'chow-liu'(requiresroot_node) - Tree-Augmented Naive Bayes:
methodtype='tan'(requiresroot_node+class_node) - Naive Bayes:
methodtype='nb'/'naivebayes' - Causal discovery (continuous):
methodtype='direct-lingam'or'ica-lingam'
See:
references/structure_learning.md and references/causal_discovery.md
7. Choosing a Structure-Learning Method
Use the following decision logic:
Is the network structure already known?
│
├── Yes → bn.make_DAG / bn.import_DAG → parameter_learning.fit
│
└── No
│
├── Continuous data + causal discovery goal?
│ └── methodtype='direct-lingam' or 'ica-lingam'
│
├── Conditional independence testing is central?
│ └── methodtype='pc' (or 'cs')
│
├── Tree / classification structure desired?
│ ├── methodtype='cl' / 'chow-liu' (need root_node)
│ ├── methodtype='tan' (need root_node + class_node)
│ └── methodtype='nb' / 'naivebayes'
│
├── Very small number of variables (≤ ~6) and exhaustive OK?
│ └── methodtype='ex'
│
└── Default / general score-based search
└── methodtype='hc' + appropriate scoretype
Do not choose an algorithm solely because it is the default.
Also consider:
- Data type (discrete scores vs
*-gGaussian scores) - Sample size vs. number of variables
- Expected graph complexity → use
max_indegreeor black/white lists - Computational budget
- Causal assumptions
- Available domain knowledge (
black_list,white_list,fixed_edges)
8. Structure Constraints
When domain knowledge is available, use it to constrain the search space.
DAG = bn.structure_learning.fit(
df,
methodtype='hc',
scoretype='bic',
black_list=[('A', 'B'), ('C', 'D')], # forbidden edges
white_list=[('X', 'Y')], # allowed edges (or nodes)
bw_list_method='edges', # REQUIRED with black/white lists
max_indegree=3, # max parents per node
fixed_edges=[('Temp', 'Pressure')], # edges that must be present
)
Required: if you pass black_list or white_list, you must also set
bw_list_method to 'edges' or 'nodes'. Omitting it raises an Exception.
start_dag: must include the same variables as the dataset. A partial
DAG that omits nodes present in df raises ValueError.
Possible constraints:
black_list/white_list(+ requiredbw_list_method)max_indegreefixed_edges- Starting structure via
start_dag(full variable set) - Temporal / domain ordering encoded as black/white lists
Constraints can substantially improve both computational efficiency and interpretability.
Do not invent domain constraints.
Only apply constraints supplied by the user or justified by the problem domain.
9. Parameter Learning
Parameter learning estimates the probability distributions associated with a known network structure.
model = bn.parameter_learning.fit(DAG, df, methodtype='bayes')
# methodtype: 'bayes' (recommended when sparse) | 'ml' | 'maximumlikelihood'
The typical workflow is:
Data
│
▼
Learn or define DAG
│
▼
Parameter learning
│
▼
Fitted Bayesian Network
Do not use parameter learning as a substitute for structure learning when the
network structure is unknown. Always run it after structure_learning.fit
(or after make_DAG / import_DAG) before inference, prediction, or sampling.
See:
references/parameter_learning.md
10. Bayesian Inference
Use inference when the user wants to calculate probabilities conditioned on
observed evidence, or (with the do argument) interventional probabilities.
query = bn.inference.fit(
model,
variables=['Y'],
evidence={'X': 1},
to_df=True,
)
# query.df has columns = variables + 'p'
Typical observational problem:
Given:
X = x
Calculate:
P(Y | X = x)
Interventional problem (Pearl's do-operator):
query = bn.inference.fit(
model,
variables=['Y'],
do={'X': 1},
)
# P(Y | do(X = 1))
do and evidence may be combined. A variable cannot appear in both.
General workflow:
Fitted Bayesian Network
│
▼
Evidence and/or interventions (do)
│
▼
Inference (Variable Elimination on the
possibly mutilated network)
│
▼
Posterior / interventional distribution
Clearly distinguish:
- Prior probability
- Likelihood
- Posterior probability
- Marginal probability
- Conditional probability
- Interventional probability
P(Y | do(X=x))
See:
references/inference.md
11. Prediction
Use prediction when the user wants to estimate an unknown variable from observed variables.
Before prediction:
- Verify that the target variable exists.
- Verify that the model has been fitted.
- Identify available evidence variables.
- Determine whether the target is discrete or continuous.
- Report uncertainty where available.
Do not describe prediction as causal intervention.
12. Sampling
Use sampling when the user wants to:
- Generate synthetic observations.
- Explore the model distribution.
- Perform simulation.
- Generate conditional samples.
samples = bn.sampling(model, n=1000, methodtype='bayes')
# Conditional: bn.sampling(model, n=100, methodtype='bayes', evidence={'Rain': 1})
# Gibbs does NOT support evidence
Sampling should use the fitted Bayesian Network rather than independently sampling each variable.
See:
references/sampling.md
13. Causal Discovery
Causal interpretation requires additional assumptions.
Do not automatically interpret:
X → Y
as:
X causes Y
Before making a causal claim, consider:
- Causal sufficiency.
- Hidden confounding.
- Measurement quality.
- Sampling assumptions.
- Temporal ordering.
- Faithfulness.
- Markov assumptions.
- Correct specification of the causal graph.
- Intervention semantics.
When assumptions are not established, describe the result as a dependency structure.
See:
references/causal_discovery.md
14. Intervention (do-calculus)
An intervention asks a different question from ordinary inference.
Observational question:
P(Y | X = x) # evidence={'X': x}
Interventional question:
P(Y | do(X = x)) # do={'X': x}
bnlearn implements Pearl's do-operator via the do argument of
bn.inference.fit. Internally the query runs Variable Elimination on the
mutilated network (model.do(...)): incoming edges of the intervened
variables are cut, then the intervened values are fixed as evidence. That is
exact and works together with ordinary evidence, elimination_order, and
joint.
model = bn.import_DAG('sprinkler')
# Observational
q_obs = bn.inference.fit(model, variables=['Wet_Grass'], evidence={'Sprinkler': 1})
# P(Wet_Grass=1 | Sprinkler=1) ≈ 0.927
# Interventional
q_do = bn.inference.fit(model, variables=['Wet_Grass'], do={'Sprinkler': 1})
# P(Wet_Grass=1 | do(Sprinkler=1)) ≈ 0.945
# Combined
q_mix = bn.inference.fit(
model,
variables=['Wet_Grass'],
do={'Sprinkler': 1},
evidence={'Rain': 1},
)
Rules:
- A variable cannot appear in both
doandevidence(raises). - Unknown node names in
doraise. - Interventions are labeled
do(X)=…inquery.text. dois the last parameter offit, so existing positional calls stay compatible.
Do not replace an intervention with ordinary conditioning.
When the user asks:
What happens if I force / set / change X to a particular value?
interpret this as an intervention problem and use do=.
Causal interpretation of the underlying DAG still requires the usual
assumptions (no hidden confounding, correct causal graph, etc.). See
references/causal_discovery.md and references/inference.md.
15. Model Scoring
Use structure scores to compare candidate Bayesian Network structures.
Common scores include:
- BIC.
- AIC.
- K2.
- BDeu.
- BDs.
- Gaussian scores for continuous data where supported.
When comparing scores:
- Use compatible data types.
- Consider model complexity.
- Consider the assumptions behind each score.
- Do not compare scores calculated under incompatible conditions without explaining the limitation.
See:
references/scoring.md
16. Model Validation
Do not assume that a learned network is correct merely because the algorithm successfully completed.
Evaluate:
- Structural plausibility.
- Stability.
- Predictive performance where appropriate.
- Parameter estimates.
- Conditional independence relationships.
- Sensitivity to preprocessing.
- Sensitivity to algorithm settings.
- Sensitivity to sample variation.
Where possible, assess whether important edges are stable under resampling.
17. Visualization
Use visualization to inspect:
- Nodes.
- Directed edges.
- Network structure.
- Parent-child relationships.
- Markov blankets.
- Edge strengths where available.
Visualization is an exploratory and communication tool.
Do not infer statistical significance merely from visual appearance.
18. Exact API Cheat Sheet (bnlearn ≥ 0.14)
Critical rule: Use the exact parameter names below. Never invent aliases
(method= is wrong; the correct name is methodtype=). Never borrow
signatures from pgmpy, DoWhy, CausalNex, or other libraries.
All signatures below were verified against the installed bnlearn API.
18.1 Structure learning
model = bn.structure_learning.fit(
df, # pandas DataFrame (required)
methodtype='hc', # see table below
scoretype='bic', # see table below
black_list=None, # list of edges or nodes to forbid
white_list=None, # list of edges or nodes to allow
bw_list_method=None, # 'edges' | 'nodes' | None
max_indegree=None, # int, limit number of parents
tabu_length=100, # Hill-Climbing tabu list length
epsilon=1e-4,
max_iter=1e6,
root_node=None, # required for 'cl' / 'chow-liu'
class_node=None, # required for 'tan'
fixed_edges=None, # iterable of edges that must be present
start_dag=None, # optional starting DAG
params_pc={'ci_test': 'chi_square', 'alpha': 0.05},
params_lingam={'random_state': None, 'prior_knowledge': None,
'apply_prior_knowledge_softly': False, 'measure': 'pwling'},
n_jobs=-1,
verbose=3,
)
Supported methodtype values
| methodtype (aliases) | Family | Typical data | Notes |
|---|---|---|---|
hc / hillclimbsearch |
Score-based | Discrete / continuous | Default. Hill Climbing |
ex / exhaustivesearch |
Score-based | Discrete (tiny nets) | Exhaustive search |
pc / cs / constraintsearch |
Constraint-based | Discrete / continuous | PC algorithm |
cl / chow-liu |
Tree | Discrete | Requires root_node |
tan |
Tree-Augmented NB | Discrete | Requires root_node + class_node |
nb / naivebayes |
Naive Bayes | Discrete | Classification |
direct-lingam |
Causal (LiNGAM) | Continuous / mixed | Causal discovery |
ica-lingam |
Causal (LiNGAM) | Continuous | Causal discovery |
Prefer the short forms (hc, ex, pc, cl) in new code. The long aliases
are accepted by bnlearn and appear in older examples/blogs.
Supported scoretype values
| scoretype | Data type | Notes |
|---|---|---|
bic |
Discrete | Default, good general choice |
aic |
Discrete | Less penalization than BIC |
k2 |
Discrete | Dirichlet prior |
bdeu |
Discrete | BDeu score |
bds |
Discrete | BDs score |
bic-g |
Continuous | Gaussian BIC |
aic-g |
Continuous | Gaussian AIC |
loglik-g |
Continuous | Gaussian log-likelihood |
Never pass a discrete score to continuous data (or vice-versa) without explicit conversion/discretization.
Return value (structure learning)
Common keys (always present for score-based methods):
model # pgmpy model
model_edges # list of edges
adjmat # adjacency matrix (DataFrame)
config # configuration dict
structure_scores # scores of the learned structure
Constraint-based (pc / cs) additionally returns:
undirected, undirected_edges, pdag, pdag_edges, dag, dag_edges
Do not assume attributes that are not in the returned dictionary.
18.2 Parameter learning
model = bn.parameter_learning.fit(
model, # output of structure_learning.fit or make_DAG / import_DAG
df, # same DataFrame (or compatible)
methodtype='bayes', # 'bayes' | 'ml' | 'maximumlikelihood'
scoretype='bdeu', # used by some estimators
smooth=None,
n_jobs=-1,
verbose=3,
)
methodtype='bayes'→ Bayesian parameter estimation (recommended when counts are low).methodtype='ml'/'maximumlikelihood'→ Maximum-likelihood estimates.
After this call the model contains CPDs and can be used for inference, prediction and sampling.
18.3 Inference (including do-calculus)
query = bn.inference.fit(
model, # fitted model (after parameter learning) or DAG with CPDs
variables=['Target'], # variables to query (list)
evidence={'A': 1, 'B': 0}, # observational evidence (state values)
to_df=True, # return a DataFrame-friendly object
elimination_order='greedy',
joint=True,
groupby=None,
plot=False,
verbose=3,
do=None, # interventional assignment, e.g. {'Sprinkler': 1}
)
- Evidence values must be valid states of the variables.
- For a pure marginal (no evidence), pass
evidence={}— do not omit the argument or passNone(current bnlearn callsevidence.keys()and will raiseAttributeError). - When
to_df=Truethe result has a.dfattribute with columns = variables +p. - Use
bn.query2df(query, variables=[...])to reshape. do: Pearl's do-operator. Cuts incoming edges of the intervened nodes (mutilated network) and runs Variable Elimination with those values fixed. Combines freely withevidence. A variable must not appear in bothdoandevidence. Interventions appear asdo(X)=…inquery.text.
Observational vs interventional (sprinkler)
model = bn.import_DAG('sprinkler')
# P(Wet_Grass | Sprinkler=1) ≈ 0.927
bn.inference.fit(model, variables=['Wet_Grass'], evidence={'Sprinkler': 1})
# P(Wet_Grass | do(Sprinkler=1)) ≈ 0.945
bn.inference.fit(model, variables=['Wet_Grass'], do={'Sprinkler': 1})
Pattern: inference on a hand-built DAG with placeholder CPDs
edges = [
('Cloudy', 'Sprinkler'),
('Cloudy', 'Rain'),
('Sprinkler', 'Wet_Grass'),
('Rain', 'Wet_Grass'),
]
# Generate placeholder CPDs (uniform by default)
CPD = bn.build_cpts_from_structure(edges, variable_card=2)
# Create DAG with those CPDs
DAG = bn.make_DAG(edges, CPD=CPD)
query = bn.inference.fit(
DAG,
variables=['Wet_Grass'],
evidence={'Rain': 1, 'Sprinkler': 0, 'Cloudy': 1},
)
18.4 Sampling
df_samples = bn.sampling(
DAG, # fitted model or DAG dict
n=1000,
methodtype='bayes', # 'bayes' | 'gibbs'
evidence=None, # optional conditioning dict
verbose=0,
)
methodtype='gibbs'does not supportevidence.- Evidence that has zero probability under the model raises
ValueError. - Evidence variable names and states are validated; unknown names raise
ValueError.
18.5 Prediction
yhat = bn.predict(
model, # fitted model
df, # DataFrame with evidence columns
variables, # target variable name(s) but must be in of df.columns
to_df=True,
method='max', # 'max' (MAP) or probability
verbose=3,
)
18.6 Building / loading a DAG by hand
# From edge list (optionally with CPDs)
DAG = bn.make_DAG(
edges, # list of (parent, child) tuples
CPD=None, # optional list of TabularCPD
methodtype='bayes', # 'bayes' | 'naivebayes' | 'DBN' | 'markov'
checkmodel=True,
verbose=3,
)
# Generate uniform placeholder CPDs for a structure
CPDs = bn.build_cpts_from_structure(edges, variable_card=2)
DAG = bn.make_DAG(edges, CPD=CPDs)
# Single-node CPT (optionally with a rulebook)
cpt = bn.generate_cpt('Y', parents=['X'], variable_card=2)
# From built-in example network
DAG = bn.import_DAG('sprinkler') # also: 'asia', 'alarm', ...
df = bn.import_example('sprinkler')
18.7 Independence tests, discretization, one-hot
# Conditional independence test on a learned model
# prune=True removes edges that fail the significance test (common post-processing step)
model = bn.independence_test(
model, df,
test='chi_square', # also: 'pearsonr', 'g_sq', 'log_likelihood', ...
alpha=0.05,
prune=True,
)
# Discretize continuous columns given a structure
# continuous_columns must be a list of column names that are continuous
df_disc = bn.discretize(
df,
edges,
continuous_columns=['Age', 'Income'],
max_iterations=8,
)
# One-hot / numeric encoding helper
dfhot, dfnum = bn.df2onehot(df)
18.8 Visualization, CPD inspection, scores, I/O
bn.plot(model) # static matplotlib plot
bn.plot_graphviz(model) # graphviz
bn.print_CPD(model) # print all CPDs
bn.structure_scores(model, df, scoring_method=['bic', 'k2', 'bdeu', 'bds'])
bn.save(model, filepath='bn')
model = bn.load(filepath='bn')
18.9 Minimal end-to-end pattern (discrete)
import bnlearn as bn
df = bn.import_example('sprinkler')
# 1. Structure
DAG = bn.structure_learning.fit(df, methodtype='hc', scoretype='bic')
# 2. Parameters
model = bn.parameter_learning.fit(DAG, df, methodtype='bayes')
# 3. Inference
q = bn.inference.fit(model, variables=['Wet_Grass'],
evidence={'Rain': 1, 'Sprinkler': 0})
# 4. Sampling
samples = bn.sampling(model, n=1000)
Always keep structure learning and parameter learning as two distinct steps unless the user already supplies a fully specified DAG with CPDs.
18.10 Canonical workflows from practice (blog patterns)
These patterns appear repeatedly in real bnlearn tutorials and should be preferred when they match the user’s goal.
A. Expert-knowledge-first (no structure learning)
When the user (or domain expert) already knows the causal / dependency structure:
edges = [
('Cloudy', 'Sprinkler'),
('Cloudy', 'Rain'),
('Sprinkler', 'Wet_Grass'),
('Rain', 'Wet_Grass'),
]
DAG = bn.make_DAG(edges)
model = bn.parameter_learning.fit(DAG, df, methodtype='bayes')
bn.print_CPD(model)
q = bn.inference.fit(model, variables=['Wet_Grass'],
evidence={'Rain': 1, 'Sprinkler': 0})
Do not run structure_learning.fit when the structure is already given.
B. Data-driven structure → prune → parameters → “chat with the data”
model = bn.structure_learning.fit(df, methodtype='hc', scoretype='bic')
# Optional but recommended: drop edges that fail a CI test
model = bn.independence_test(model, df, test='chi_square', alpha=0.05, prune=True)
model = bn.parameter_learning.fit(model, df, methodtype='bayes')
# Ask probability questions (the “chat with your dataset” pattern)
q1 = bn.inference.fit(model, variables=['salary'], evidence={'education': 'Doctorate'})
q2 = bn.inference.fit(model, variables=['salary'], evidence={'education': 'HS-grad'})
C. Compare several structure learners / scores
When the user asks which method or score is better, or wants robustness:
models = {
'hc_bic': bn.structure_learning.fit(df, methodtype='hc', scoretype='bic'),
'hc_k2': bn.structure_learning.fit(df, methodtype='hc', scoretype='k2'),
'hc_bdeu': bn.structure_learning.fit(df, methodtype='hc', scoretype='bdeu'),
'cs_bic': bn.structure_learning.fit(df, methodtype='cs', scoretype='bic'),
'cl': bn.structure_learning.fit(df, methodtype='cl', root_node='Cloudy'),
}
for name, m in models.items():
print(name, m['model_edges'])
D. High-cardinality / messy categoricals before structure learning
Structure learning degrades when categorical variables have dozens of rare levels. From practice:
- Inspect
df[col].value_counts(). - Group rare levels into an
"Other"(or domain-sensible) category. - Drop or bin continuous columns that should not enter a discrete BN (or switch to continuous scores / LiNGAM instead of discretizing blindly).
- Only then call
structure_learning.fit.
# Example: collapse rare job titles
counts = df['job_title'].value_counts()
rare = counts[counts < 30].index
df['job_title'] = df['job_title'].where(~df['job_title'].isin(rare), 'Other')
E. Prediction vs intervention (prescriptive framing)
- Prediction / inference:
P(Y | X = x)→bn.inference.fit(..., evidence={'X': x})orbn.predict - Intervention:
P(Y | do(X = x))→bn.inference.fit(..., do={'X': x})
If the user asks “what happens if we force / set / change X?”, use the
do= argument. The underlying DAG still needs a causal interpretation
(no unmeasured confounding, correct structure, etc.); state those assumptions
(see §13–14 and references/causal_discovery.md).
19. Common Mistakes (agent failure modes)
These are the mistakes that appear most frequently when an agent uses bnlearn.
Mistake: Wrong parameter name (method instead of methodtype)
Incorrect:
bn.structure_learning.fit(df, method='hc', scoretype='bic')
Correct:
bn.structure_learning.fit(df, methodtype='hc', scoretype='bic')
The same rule applies to parameter_learning.fit, make_DAG, sampling, etc.
Always use methodtype=.
Mistake: Treating every edge as causal
Incorrect:
X → Y
therefore X causes Y
Correct:
X → Y
is evidence of a learned dependency structure.
Causal interpretation requires additional assumptions (see §13 and
references/causal_discovery.md).
Mistake: Discretizing continuous variables automatically
Do not discretize continuous variables simply because discrete Bayesian Networks are easier to use.
First consider whether a continuous/Gaussian model is appropriate
(scoretype='bic-g', methodtype='hc' or LiNGAM methods).
Only call bn.discretize(...) when the user explicitly requests
discretization or when a continuous method is demonstrably unsuitable.
Mistake: Confusing inference and intervention
These are different operations:
P(Y | X = x) # evidence={'X': x}
versus:
P(Y | do(X = x)) # do={'X': x}
Use the do argument of bn.inference.fit for interventions. Do not answer
an intervention question with ordinary evidence. A variable cannot appear in
both do and evidence.
Mistake: Ignoring variable types
Do not select a score or independence test without considering whether the variables are:
- Discrete.
- Continuous.
- Mixed.
Discrete scores (bic, k2, bdeu, …) on continuous data, or Gaussian
scores (bic-g, …) on discrete data, produce incorrect or failing results.
Mistake: Calling parameter learning before structure learning
When the structure is unknown:
structure_learning.fit → parameter_learning.fit → inference / sampling
Do not call parameter_learning.fit on a raw DataFrame; it expects a model
dict that already contains a DAG.
Mistake: Inventing function names or arguments
Never invent:
bn.learn_structure,bn.fit_bn,bn.build_network, …- Arguments such as
algorithm=,scoring=,ci_test=at the top level ofstructure_learning.fit(CI-test settings belong insideparams_pc).
Stick to the functions listed in §18 and the reference files.
Mistake: Passing invalid evidence
- Variable names that do not exist in the model →
ValueError. - State values outside the variable’s cardinality →
ValueError. - Evidence with zero probability under the model →
ValueError(sampling). methodtype='gibbs'together withevidence=→ValueError.
Always validate evidence against the model’s nodes and CPDs.
Mistake: Ignoring sample size
A network with many parameters can be poorly estimated even when structure learning succeeds computationally.
Prefer simpler structures (max_indegree, stronger scores, domain constraints)
when the available data cannot support a highly complex model.
Mistake: Treating a single learned DAG as ground truth
Structure learning is sensitive to:
- Data
- Algorithm (
methodtype) - Score / test (
scoretype,params_pc) - Hyperparameters
- Sampling variation
- Model assumptions
Consider stability (bootstrap / resampling) and uncertainty when the user asks for confidence in the structure.
Mistake: Using the wrong return-value keys
After structure_learning.fit:
- Always present:
model,model_edges,adjmat,config,structure_scores - Only for
pc/cs: alsoundirected,pdag,dag, …
Do not write model.edges or model.cpds on the raw return dict; use
model['model_edges'] and, after parameter learning, model['model'].get_cpds().
20. Troubleshooting
Unexpected network structure
Check:
- Variable types.
- Missing values.
- Score selection.
- Independence test / significance level.
- Search constraints (
black_list,white_list,max_indegree). - Maximum parent count.
- Sample size.
- Outliers / transformations.
- Algorithm assumptions.
- High-cardinality categoricals (group rare levels).
Too many edges after structure learning: run
model = bn.independence_test(model, df, test='chi_square', alpha=0.05, prune=True)
before parameter learning. This is a standard post-processing step in practice.
Poor continuous-variable results
Check:
- Gaussian assumptions.
- Outliers.
- Transformations.
- Nonlinear relationships.
- Sample size.
- Score selection.
- Whether discretization is appropriate.
Too many edges
Consider:
- Stronger model-selection criteria.
- Search constraints.
- Maximum parent count.
- Domain knowledge.
- Larger sample size.
- Stability analysis.
Do not arbitrarily delete edges merely to make the graph look simpler.
Too few edges
Check:
- Independence-test threshold.
- Score selection.
- Sample size.
- Variable preprocessing.
- Search constraints.
- Whether the underlying relationships are detectable with the available data.
21. Reproducibility
When applicable:
- Set random seeds.
- Record algorithm settings.
- Record score/test settings.
- Record preprocessing steps.
- Record bnlearn and dependency versions.
- Record structural constraints.
- Record the input variables.
- Save the learned network.
A Bayesian Network result should be reproducible from the documented configuration and input data.
22. Performance
For large datasets or many variables:
- Reduce unnecessary variables.
- Remove constant variables.
- Use domain constraints where justified.
- Limit graph complexity where appropriate.
- Select computationally appropriate algorithms.
- Avoid unnecessarily expensive exhaustive searches.
- Validate the result after optimization.
Do not sacrifice statistical validity solely for speed.
23. Recommended Response Pattern
When solving a bnlearn problem, structure the response as:
1. Problem identification
Explain what Bayesian Network task is being solved.
2. Data assessment
Identify:
- Variable types.
- Sample size.
- Missing values.
- Relevant assumptions.
3. Method selection
Explain:
- Structure-learning algorithm.
- Score or independence test.
- Constraints.
- Continuous/discrete strategy.
4. Implementation
Provide the relevant bnlearn code.
5. Validation
Explain how to evaluate the learned model.
6. Interpretation
Explain what the network means and explicitly state limitations.
24. Reference Documentation
Use the following references for detailed guidance:
references/structure_learning.mdreferences/continuous_models.mdreferences/scoring.mdreferences/parameter_learning.mdreferences/inference.mdreferences/sampling.mdreferences/causal_discovery.mdreferences/troubleshooting.md
Examples:
examples/discrete_bn.pyexamples/continuous_bn.pyexamples/hill_climbing.pyexamples/pc_algorithm.pyexamples/inference.pyexamples/sampling.py
25. API Accuracy (non-negotiable rules)
The bnlearn API can evolve. When implementing a solution:
- Prefer the API exposed by the installed bnlearn version (currently documented for ≥ 0.14).
- Do not assume that APIs from other Bayesian Network libraries are interchangeable.
- Use only functions that exist in bnlearn. Do not invent new function names.
- Do not invent function arguments.
- The structure-learning keyword is
methodtype, nevermethod,algorithm, oralgo. - Verify estimator, score, test, and inference arguments before using them.
- Prefer the current bnlearn documentation, the unit tests under
tests/, and the reference files over outdated blog snippets or memory of other libraries. - If an API differs between versions, explicitly state the version-specific behavior.
Never silently substitute APIs from pgmpy, pomegranate, DoWhy, CausalNex, Lingam (standalone), or any other library. bnlearn already wraps the needed functionality; call the bnlearn surface.
When in doubt, open the corresponding file in references/ or the matching
example in examples/ before writing code.
26. Final Decision Checklist
Before returning a bnlearn solution, verify:
- The Bayesian Network task has been identified (structure / parameters / inference / sampling / causal).
- Variable types (discrete / continuous / mixed) have been determined.
- Missing values have been considered.
- Continuous variables have not been discretized unnecessarily.
-
methodtype(notmethod) is used for structure / parameter / sampling calls. - The chosen
methodtypeandscoretypeare compatible with the data type. - Domain constraints (
black_list/white_list/max_indegree/fixed_edges) have been considered when knowledge exists. - Sample size versus model complexity has been considered.
- Structure learning and parameter learning are performed as separate steps when the DAG is unknown.
- Return-value keys (
model,model_edges,adjmat, …) are used correctly. - Evidence passed to inference / sampling uses valid variable names and st
…(truncated)