Search⌘ K
AI Features

Tree Construction and Splitting Criteria

Explore how decision trees classify data by constructing splits that reduce impurity and uncertainty in labels. Learn about Gini impurity and entropy as criteria for choosing splits, and understand the process of building a tree that predicts outcomes by refining data groups through local questions.

A fraud label cannot always come from one global rule such as amount > 200. A $500 transaction at a trusted merchant may be legitimate, while a $90 transaction can be suspicious when the merchant risk score is high and the card has been used many times in one hour.

A decision tree handles these combinations with a sequence of yes-or-no questions. Each node is a group of transactions currently under consideration. A split divides one node using a rule such as amount >= 200. A leaf is an endpoint where the tree stops asking questions and makes its classification. Depth is the number of splits from the starting node to a particular node.

The tree begins with every transaction in one root node. It selects a split that makes the resulting groups less mixed in their fraud labels, then repeats that choice within each child group. The goal is not to find a single perfect threshold. The goal is to form leaves where nearly all transactions share the same label.

Measuring label mixes

Before scoring a split, we need a concrete way to inspect the labels in a node. The root node in the transaction table contains all 12 transactions. Its fraud proportion is the count of rows where fraud is 1, divided by 12. The candidate rule amount >= 200 creates two child nodes, one for higher-value transactions and one for lower-value transactions.

Python
import pandas as pd
def label_mix(df: pd.DataFrame, label_col: str = "fraud") -> pd.DataFrame:
counts = df[label_col].value_counts().reindex([0, 1], fill_value=0)
proportions = (counts / len(df)).rename("proportion")
out = pd.concat([counts.rename("count"), proportions], axis=1)
out.index = out.index.map({0: "non-fraud (0)", 1: "fraud (1)"}.get)
return out
transactions = pd.DataFrame(
{
"amount": [45, 75, 120, 180, 220, 260, 310, 500, 30, 95, 205, 420],
"risk_score": [0.10, 0.30, 0.40, 0.55, 0.60, 0.80, 0.75, 0.20, 0.90, 0.65, 0.85, 0.35],
"velocity_1h": [1, 2, 1, 3, 1, 6, 5, 1, 9, 4, 7, 2],
"fraud": [0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0],
}
)
root_mix = label_mix(transactions, "fraud")
print(f"Root node: n={len(transactions)}")
print(root_mix.to_string())
mask_high_amount = transactions["amount"] >= 200
left_child = transactions[mask_high_amount]
right_child = transactions[~mask_high_amount]
print("\nChild node mixes after split on amount >= 200")
print(f"Left child (amount >= 200): n={len(left_child)}")
print(label_mix(left_child, "fraud").to_string())
print(f"\nRight child (amount < 200): n={len(right_child)}")
print(label_mix(right_child, "fraud").to_string())
  • Lines 4–9: label_mix forces a consistent two-row summary (non-fraud then fraud) via reindex([0, 1]), so a child node that happens to contain only one class still prints both classes with a 0 count and a 0.0 proportion.

  • Lines 12–19: The example dataset is fixed at 12 rows and includes amount, risk_score, velocity_1h, and a binary ...