2015-10-20 21:39:34 +08:00
|
|
|
"""
|
|
|
|
|
=========================================
|
|
|
|
|
Understanding the decision tree structure
|
|
|
|
|
=========================================
|
|
|
|
|
|
2015-10-21 00:02:09 +08:00
|
|
|
The decision tree structure can be analysed to gain further insight on the
|
2015-10-20 21:39:34 +08:00
|
|
|
relation between the features and the target to predict. In this example, we
|
|
|
|
|
show how to retrieve:
|
2015-10-21 23:05:00 +08:00
|
|
|
|
|
|
|
|
- the binary tree structure;
|
|
|
|
|
- the depth of each node and whether or not it's a leaf;
|
|
|
|
|
- the nodes that were reached by a sample using the ``decision_path`` method;
|
|
|
|
|
- the leaf that was reached by a sample using the apply method;
|
|
|
|
|
- the rules that were used to predict a sample;
|
|
|
|
|
- the decision path shared by a group of samples.
|
2015-10-20 21:39:34 +08:00
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
import numpy as np
|
|
|
|
|
|
2015-11-02 05:22:37 +08:00
|
|
|
from sklearn.model_selection import train_test_split
|
2015-10-20 21:39:34 +08:00
|
|
|
from sklearn.datasets import load_iris
|
2015-10-21 16:33:34 +08:00
|
|
|
from sklearn.tree import DecisionTreeClassifier
|
2015-10-20 21:39:34 +08:00
|
|
|
|
|
|
|
|
iris = load_iris()
|
|
|
|
|
X = iris.data
|
|
|
|
|
y = iris.target
|
|
|
|
|
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
|
|
|
|
|
|
2015-10-21 16:33:34 +08:00
|
|
|
estimator = DecisionTreeClassifier(max_leaf_nodes=3, random_state=0)
|
2015-10-20 21:39:34 +08:00
|
|
|
estimator.fit(X_train, y_train)
|
|
|
|
|
|
|
|
|
|
# The decision estimator has an attribute called tree_ which stores the entire
|
2015-10-21 00:02:09 +08:00
|
|
|
# tree structure and allows access to low level attributes. The binary tree
|
2015-10-20 21:39:34 +08:00
|
|
|
# tree_ is represented as a number of parallel arrays. The i-th element of each
|
|
|
|
|
# array holds information about the node `i`. Node 0 is the tree's root. NOTE:
|
|
|
|
|
# Some of the arrays only apply to either leaves or split nodes, resp. In this
|
|
|
|
|
# case the values of nodes of the other type are arbitrary!
|
|
|
|
|
#
|
|
|
|
|
# Among those arrays, we have:
|
|
|
|
|
# - left_child, id of the left child of the node
|
|
|
|
|
# - right_child, id of the right child of the node
|
|
|
|
|
# - feature, feature used for splitting the node
|
|
|
|
|
# - threshold, threshold value at the node
|
|
|
|
|
#
|
|
|
|
|
|
2015-10-21 00:02:09 +08:00
|
|
|
# Using those arrays, we can parse the tree structure:
|
2015-10-20 21:39:34 +08:00
|
|
|
|
2015-10-21 16:33:34 +08:00
|
|
|
n_nodes = estimator.tree_.node_count
|
|
|
|
|
children_left = estimator.tree_.children_left
|
|
|
|
|
children_right = estimator.tree_.children_right
|
|
|
|
|
feature = estimator.tree_.feature
|
|
|
|
|
threshold = estimator.tree_.threshold
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# The tree structure can be traversed to compute various properties such
|
|
|
|
|
# as the depth of each node and whether or not it is a leaf.
|
2017-02-03 22:21:03 +08:00
|
|
|
node_depth = np.zeros(shape=n_nodes, dtype=np.int64)
|
2015-10-21 16:33:34 +08:00
|
|
|
is_leaves = np.zeros(shape=n_nodes, dtype=bool)
|
|
|
|
|
stack = [(0, -1)] # seed is the root node id and its parent depth
|
|
|
|
|
while len(stack) > 0:
|
|
|
|
|
node_id, parent_depth = stack.pop()
|
|
|
|
|
node_depth[node_id] = parent_depth + 1
|
|
|
|
|
|
|
|
|
|
# If we have a test node
|
|
|
|
|
if (children_left[node_id] != children_right[node_id]):
|
|
|
|
|
stack.append((children_left[node_id], parent_depth + 1))
|
|
|
|
|
stack.append((children_right[node_id], parent_depth + 1))
|
|
|
|
|
else:
|
|
|
|
|
is_leaves[node_id] = True
|
|
|
|
|
|
2015-10-20 21:39:34 +08:00
|
|
|
print("The binary tree structure has %s nodes and has "
|
|
|
|
|
"the following tree structure:"
|
2015-10-21 16:33:34 +08:00
|
|
|
% n_nodes)
|
|
|
|
|
for i in range(n_nodes):
|
|
|
|
|
if is_leaves[i]:
|
|
|
|
|
print("%snode=%s leaf node." % (node_depth[i] * "\t", i))
|
2015-10-20 21:39:34 +08:00
|
|
|
else:
|
2016-12-06 09:13:10 +08:00
|
|
|
print("%snode=%s test node: go to node %s if X[:, %s] <= %s else to "
|
2015-10-21 00:02:09 +08:00
|
|
|
"node %s."
|
2015-10-21 16:33:34 +08:00
|
|
|
% (node_depth[i] * "\t",
|
|
|
|
|
i,
|
|
|
|
|
children_left[i],
|
|
|
|
|
feature[i],
|
|
|
|
|
threshold[i],
|
|
|
|
|
children_right[i],
|
2015-10-20 21:39:34 +08:00
|
|
|
))
|
|
|
|
|
print()
|
|
|
|
|
|
2015-10-21 17:46:16 +08:00
|
|
|
# First let's retrieve the decision path of each sample. The decision_path
|
2015-10-22 15:51:14 +08:00
|
|
|
# method allows to retrieve the node indicator functions. A non zero element of
|
|
|
|
|
# indicator matrix at the position (i, j) indicates that the sample i goes
|
|
|
|
|
# through the node j.
|
2015-10-20 21:39:34 +08:00
|
|
|
|
2015-10-21 16:37:55 +08:00
|
|
|
node_indicator = estimator.decision_path(X_test)
|
2015-10-20 21:39:34 +08:00
|
|
|
|
2015-10-21 16:44:35 +08:00
|
|
|
# Similarly, we can also have the leaves ids reached by each sample.
|
2015-10-20 21:39:34 +08:00
|
|
|
|
2015-10-21 16:33:34 +08:00
|
|
|
leave_id = estimator.apply(X_test)
|
2015-10-20 21:39:34 +08:00
|
|
|
|
|
|
|
|
# Now, it's possible to get the tests that were used to predict a sample or
|
2015-10-22 15:51:14 +08:00
|
|
|
# a group of samples. First, let's make it for the sample.
|
2015-10-20 21:39:34 +08:00
|
|
|
|
|
|
|
|
sample_id = 0
|
|
|
|
|
node_index = node_indicator.indices[node_indicator.indptr[sample_id]:
|
|
|
|
|
node_indicator.indptr[sample_id + 1]]
|
|
|
|
|
|
|
|
|
|
print('Rules used to predict sample %s: ' % sample_id)
|
2015-10-21 23:09:38 +08:00
|
|
|
for node_id in node_index:
|
2015-10-22 15:51:14 +08:00
|
|
|
if leave_id[sample_id] != node_id:
|
2015-10-20 21:39:34 +08:00
|
|
|
continue
|
|
|
|
|
|
2015-10-21 17:46:16 +08:00
|
|
|
if (X_test[sample_id, feature[node_id]] <= threshold[node_id]):
|
2015-10-20 21:39:34 +08:00
|
|
|
threshold_sign = "<="
|
|
|
|
|
else:
|
|
|
|
|
threshold_sign = ">"
|
|
|
|
|
|
2017-03-28 00:54:52 +08:00
|
|
|
print("decision id node %s : (X_test[%s, %s] (= %s) %s %s)"
|
2015-10-21 23:09:38 +08:00
|
|
|
% (node_id,
|
2015-10-20 21:39:34 +08:00
|
|
|
sample_id,
|
2015-10-21 16:33:34 +08:00
|
|
|
feature[node_id],
|
2017-03-28 00:54:52 +08:00
|
|
|
X_test[sample_id, feature[node_id]],
|
2015-10-20 21:39:34 +08:00
|
|
|
threshold_sign,
|
2015-10-21 16:33:34 +08:00
|
|
|
threshold[node_id]))
|
2015-10-20 21:39:34 +08:00
|
|
|
|
|
|
|
|
# For a group of samples, we have the following common node.
|
|
|
|
|
sample_ids = [0, 1]
|
|
|
|
|
common_nodes = (node_indicator.toarray()[sample_ids].sum(axis=0) ==
|
|
|
|
|
len(sample_ids))
|
|
|
|
|
|
2015-10-21 16:33:34 +08:00
|
|
|
common_node_id = np.arange(n_nodes)[common_nodes]
|
2015-10-20 21:39:34 +08:00
|
|
|
|
2015-10-22 15:51:14 +08:00
|
|
|
print("\nThe following samples %s share the node %s in the tree"
|
2015-10-20 21:39:34 +08:00
|
|
|
% (sample_ids, common_node_id))
|
2015-10-22 00:08:57 +08:00
|
|
|
print("It is %s %% of all nodes." % (100 * len(common_node_id) / n_nodes,))
|