Search⌘ K
AI Features

Interpreting a Decision Tree

Explore how to interpret decision tree models in the context of loan approval predictions. Learn to translate the tree's splits into clear if-then rules, trace how individual applications follow specific decision paths, and understand the significance of feature importance. This lesson helps you explain model predictions to non-technical audiences while recognizing the limits of what these interpretations can claim, ensuring accurate and responsible explanations.

A declined loan application needs an explanation that a non-technical audience can inspect. A single decision tree supports that explanation because its prediction follows a visible sequence of threshold questions. With a validated tree selected, we can translate those questions into statements such as: if debt_to_income <= 0.36 and delinquencies <= 1, predict approval.

A path is the sequence of splits one applicant follows from the root node to a leaf. A rule is the combined set of conditions along that path. A leaf prediction is the class or class probability stored at the final leaf. These objects describe what the fitted model did for its training pattern. They do not establish that a feature causes an approval or decline, and a readable tree is not automatically fair.

Reading the tree as rules

The tree first exposes its global structure as indented text, so we can turn each root-to-leaf route into a clear if-then rule. The synthetic loan data uses income, debt_to_income, delinquencies, and credit_history_years, which makes each split readable without decoding feature numbers.

Python
import numpy as np
import pandas as pd
from sklearn.tree import DecisionTreeClassifier, export_text
def make_synthetic_loan_data(n: int = 600, random_state: int = 7) -> pd.DataFrame:
rng = np.random.default_rng(random_state)
income = rng.lognormal(mean=np.log(65000), sigma=0.45, size=n)
income = np.clip(income, 20000, 200000)
debt_to_income = rng.beta(a=2.2, b=3.8, size=n) * 0.8
debt_to_income = np.clip(debt_to_income, 0.02, 0.75)
delinquencies = rng.poisson(lam=0.6, size=n)
delinquencies = np.clip(delinquencies, 0, 6)
credit_history_years = rng.gamma(shape=3.0, scale=2.2, size=n)
credit_history_years = np.clip(credit_history_years, 0.0, 25.0)
# A simple synthetic approval process that creates learnable thresholds.
score = (
0.000045 * income
- 4.3 * debt_to_income
- 0.9 * delinquencies
+ 0.11 * credit_history_years
- 0.2
)
p_approved = 1 / (1 + np.exp(-score))
approved = (rng.random(n) < p_approved).astype(int)
return pd.DataFrame(
{
"income": income,
"debt_to_income": debt_to_income,
"delinquencies": delinquencies,
"credit_history_years": credit_history_years,
"approved": approved,
}
)
def leaf_prediction_label(tree: DecisionTreeClassifier, node_id: int) -> int:
counts = tree.tree_.value[node_id][0]
return int(np.argmax(counts))
def pick_two_leaf_paths(tree: DecisionTreeClassifier, feature_names: list[str]) -> list[tuple[list[str], int]]:
t = tree.tree_
def recurse(node_id: int, conditions: list[str], out: list[tuple[list[str], int]]) -> None:
is_leaf = t.children_left[node_id] == t.children_right[node_id]
if is_leaf:
out.append((conditions.copy(), leaf_prediction_label(tree, node_id)))
return
feature = feature_names[t.feature[node_id]]
threshold = t.threshold[node_id]
recurse(
t.children_left[node_id],
conditions + [f"{feature} <= {threshold:.3f}"],
out,
)
recurse(
t.children_right[node_id],
conditions + [f"{feature} > {threshold:.3f}"],
out,
)
all_paths: list[tuple[list[str], int]] = []
recurse(0, [], all_paths)
# Prefer distinct predicted classes if possible; otherwise take the first two.
by_class: dict[int, list[tuple[list[str], int]]] = {}
for conds, pred in all_paths:
by_class.setdefault(pred, []).append((conds, pred))
if len(by_class) >= 2:
classes = sorted(by_class.keys())
return [by_class[classes[0]][0], by_class[classes[1]][0]]
return all_paths[:2]
def format_rule(conditions: list[str], predicted_class: int) -> str:
outcome = "approved" if predicted_class == 1 else "declined"
if not conditions:
return f"IF (no conditions) THEN predict {outcome}"
joined = " AND ".join(conditions)
return f"IF {joined} THEN predict {outcome}"
def main() -> None:
df = make_synthetic_loan_data(n=600, random_state=7)
feature_names = ["income", "debt_to_income", "delinquencies", "credit_history_years"]
X = df[feature_names]
y = df["approved"]
model = DecisionTreeClassifier(max_depth=3, random_state=7)
model.fit(X, y)
tree_text = export_text(model, feature_names=feature_names, decimals=3)
print(tree_text)
rule_paths = pick_two_leaf_paths(model, feature_names)
print("\nTwo example root-to-leaf rules:")
for i, (conditions, pred_class) in enumerate(rule_paths, start=1):
print(f"Rule {i}: {format_rule(conditions, pred_class)}")
if __name__ == "__main__":
main()
  • Lines 6–40: The code generates a self-contained synthetic loan dataset with the lesson’s four feature names and an approved target, using a probability model that creates threshold-like structure a depth-3 tree can learn.

  • Lines 48–83: The ...