Looking at the table, let’s just start with the simplest model possible and just predict that no one will default.
So the output of our model is just to always predict “No”.
We see a 30% error rate since 3 out of 10 loans defaulted.
Let’s split the data based on the “Home Owner” field. (values = [# No, # Yes]).
We see that the left node (Home Owner == Yes) has a 0% error rate since all the samples are Defaulted == No. We don’t split this node since all the samples are of the same class. We call this node a leaf node and we’ll color it orange.
The right node (Home Owner == No) has a 43% error rate since 3 out of 7 loans defaulted.
Let’s split this node into two nodes based on the Marital Status field.
Let’s split on the “Marital Status” field.
We see that the 3 defaulted loans are all for single or divorced people. Since the node is all one class, we don’t split this node and we call it a leaf node.
We can list the subsets for the two criteria to calculate the error rate.
Table: Home Owner == No and Marital Status == Married –> Defaulted == No
Error rate for predicting Defaulted == No is 0%.
Let’s try to split on the “Annual Income” field.
We see that the person with income of 70K doesn’t default, so we split the node into two nodes based on the “Income” field.
We arbitrarily pick a threshold of $75K.
Evaluating the Model
We’ve dispositioned every data point by walking down the tree to a leaf node.
How do we know if this tree is good?
We arbitrarily picked the order of the fields to split on.
Is there a way to systematically pick the order of the fields to split on?
This is called the splitting criterion.
There’s also the question of when to stop splitting, or the stopping criterion.
So far, we stopped splitting when we reached a node of pure class but there are reasons to stop splitting even without pure classes, which we’ll see later.
Specifying the Test Condition
Before we continue, we should take a moment to consider how we specify a test condition of a node.
How we specify a test condition depends on the attribute type which can be:
Binary (Boolean)
Nominal (Categorical, e.g., cat, dog, bird)
Ordinal (e.g., Small, Medium, Large)
Continuous (e.g., 1.5, 2.1, 3.7)
And depends on the number of ways to split:
multi-way
binary
For a Nominal (Categorical) attribute:
In a Multi-way split we can use as many partitions as there are distinct values of the attribute:
For a Nominal (Categorical) attribute:
In a Binary split we divide the values into two groups.
In this case, we need to find an optimal partitioning of values into groups, which we discuss shortly.
For an Ordinal attribute, we can use a multi-way split with as many partitions as there are distinct values.
Or we can use a binary split as long we preserve the ordering of the values.
Warning
Be careful not to violate the ordering of values such as {Small, Large} and {Medium, X-Large}.
A Continuous attribute can be handled two ways:
It can be thresholded to form a binary split.
Or it can be split into contiguous ranges to form an ordinal categorical attribute.
Note that finding good partitions for \(k\) nominal attributes can be expensive, \(\mathcal{O}(2^k)\), possibly involving combinatorial searching of groupings.
However for ordinal or continuous attributes, sweeping through a range of \(n\) threshold values can be more efficient if \(n \approx k\). \(\mathcal{O}(n)\) for a sorted list.
Selecting Attribute and Test Condition
Ideally, we want to pick attributes and test conditions that maximize the homogeneity of the splits.
We can use an impurity index to measure the homogeneity in a node.
We’ll look at ways of measuring impurity of a node and then collective impurity of its child nodes.
Here we split \(N\) training instances into \(k\) child nodes, \(v_j\) for \(j=1, \ldots, k\).
\(N(v_j)\) is the number of training instances at child node \(v_j\) and \(I(v_j)\) is the impurity at child node \(v_j\).
Impurity Example
Let’s compute collective impurity on our loans dataset to see which feature to split on.
(a) Collective Entropy: 0.690
(b) Collective Entropy: 0.686
(c) Collective Entropy index: 0.00
Tip
Try calculating the collective Entropy for (a) and (b) and see if you get the same values.
Important
The collective entropy for (c) is 0. Why would we not want to use this node?
There are two ways to overcome this problem.
One way is to generate only binary decision trees, thus avoiding the difficulty of handling attributes with varying number of partitions. This strategy is employed by decision tree classifiers such as CART.
Another way is to modify the splitting criterion to take into account the number of partitions produced by the attribute. For example, in the C4.5 decision tree algorithm, a measure known as gain ratio is used to compensate for attributes that produce a large number of child nodes.
Having a low impurity value alone is insufficient to find a good attribute test condition for a node.
Having more child nodes can make a decision tree more complex and consequently more susceptible to overfitting.
Hence, the number of children produced by the splitting attribute should also be taken into consideration while deciding the best attribute test condition.
where \(N(v_i)\) is the number of instances assigned to node \(v_i\) and \(k\) is the total number of splits.
The split information measures the entropy of splitting a node into its child nodes and evaluates if the split results in a larger number of equally-sized child nodes or not.
Gain Ratio Example
Let’s compare the Gain Ratio for Home Owner and Marital Status attributes using the loans dataset.
Recall from the earlier example that the parent node has 3 Yes and 7 No defaulters (10 total instances).
Motivated around the idea that combining several noisy classifiers can result in a better prediction under certain conditions.
The base classifiers are independent
The base classifiers are noisy (high variance)
The base classifiers are low (ideally zero) bias
Bias and Variance
Recall bias and variance from Generalization: bias is error from a model too simple to capture the pattern (a shallow tree underfits); variance is error from a model so flexible that it changes with every training sample (a deep tree overfits).
A fully grown tree is low bias, high variance. Averaging many such trees keeps the low bias and cancels out the variance – ensembles reduce variance.
Random Forests
Random forests are an ensemble of decision trees that:
Construct a set of base classifiers from random sub-samples of the training data.
Train each base classifier to completion.
Take a majority vote of the base classifiers to form the final prediction.
Titanic Example
We’ll use the Titanic data set and excerpts of this Kaggle tutorial to illustrate the concepts of overfitting and random forests.
Code
import pandas as pdimport osimport urllib.request# Check if the directory exists, if not, create itifnot os.path.exists('data/titanic'): os.makedirs('data/titanic')ifnot os.path.exists('data/titanic/train.csv'): url ='https://raw.githubusercontent.com/tools4ds/DS701-Course-Notes/refs/heads/main/ds701_book/data/titanic/train.csv' urllib.request.urlretrieve(url, 'data/titanic/train.csv')df_train = pd.read_csv('data/titanic/train.csv', index_col='PassengerId')ifnot os.path.exists('data/titanic/test.csv'): url ='https://raw.githubusercontent.com/tools4ds/DS701-Course-Notes/refs/heads/main/ds701_book/data/titanic/test.csv' urllib.request.urlretrieve(url, 'data/titanic/test.csv')df_test = pd.read_csv('data/titanic/test.csv', index_col='PassengerId')
There are 418 entries in the test set with same fields except for ‘Survived’, which is what we need to predict.
We’ll do some data cleaning and preparation.
Code
import numpy as npdef proc_data(df): df['Fare'] = df.Fare.fillna(0) df.fillna(modes, inplace=True) # Fill missing values with the mode df['LogFare'] = np.log1p(df['Fare']) # Create a new column for the log of the fare + 1 df['Embarked'] = pd.Categorical(df.Embarked) # Convert to categorical df['Sex'] = pd.Categorical(df.Sex) # Convert to categoricalmodes = df_train.mode().iloc[0] # Get the mode for each columnproc_data(df_train)proc_data(df_test)
Let’s split the independent (input) variables from the dependent (output) variable.
def xs_y(df): xs = df[cats+conts].copy()return xs,df[dep] if dep in df elseNonetrn_xs,trn_y = xs_y(trn_df)val_xs,val_y = xs_y(val_df)
Here’s the predictions for our extremely simple model, where female is coded as 0:
preds = val_xs.Sex==0
We’ll use mean absolute error to measure how good this model is:
from sklearn.metrics import mean_absolute_errormean_absolute_error(val_y, preds)
0.21524663677130046
Alternatively, we could try splitting on a continuous column. We have to use a somewhat different chart to see how this might work – here’s an example of how we could look at LogFare: