<jupyter_start><jupyter_text>FastTreeSHAP in Crop Mapping Data This notebook contains usages and detailed comparisons of FastTreeSHAP v1, FastTreeSHAP v2 and the original TreeSHAP in **multiclass classification** problems using scikit-learn, XGBoost and LightGBM. It may take a few minutes to run through all code in this notebook. The source of crop mapping data is https://archive.ics.uci.edu/ml/datasets/Crop+mapping+using+fused+optical-radar+data+set.  Load Python libraries<jupyter_code>import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import roc_auc_score, accuracy_score
import xgboost as xgb
import lightgbm as lgb
import fasttreeshap
import os
import time<jupyter_output><empty_output><jupyter_text>Pre-process training and testing data<jupyter_code># run the following statement if download the dataset directly from
# https://archive.ics.uci.edu/ml/datasets/Crop+mapping+using+fused+optical-radar+data+set
#data = pd.read_csv("WinnipegDataset.txt", sep = ",", engine = "python")
#data, _ = train_test_split(data, test_size = 0.9, random_state = 0)
# source of data: https://archive.ics.uci.edu/ml/datasets/Crop+mapping+using+fused+optical-radar+data+set
data = pd.read_csv("../data/crop_train.csv")
train, test = train_test_split(data, test_size = 0.33, random_state = 0)
label_train = train["label"] - 1
label_test = test["label"] - 1
train = train.iloc[:, 1:]
test = test.iloc[:, 1:]
print("Training data has {} rows and {} columns.".format(train.shape[0], train.shape[1])) 
print("Testing data has {} rows and {} columns.".format(test.shape[0], test.shape[1]))<jupyter_output><empty_output><jupyter_text>Train a random forest model using scikit-learn and compute SHAP values<jupyter_code>n_estimators = 200  # number of trees in random forest model
max_depth = 8  # maximum depth of any trees in random forest model
# train a random forest model
rf_model = RandomForestClassifier(n_estimators = n_estimators, max_depth = max_depth, random_state = 0)
rf_model.fit(train, label_train)
print("Accuracy on testing set is {:.2f}.".format(accuracy_score(label_test, rf_model.predict(test))))
# obtain total number of leaves
shap_explainer = fasttreeshap.TreeExplainer(rf_model)
num_leaves = sum(shap_explainer.model.num_nodes) - sum(sum(shap_explainer.model.children_left > 0))
print("Total number of leaves is {}.".format(num_leaves))
# estimate memory usage of FastTreeSHAP v2 since FastTreeSHAP v2 has a stricter memory constraint than
# TreeSHAP and FastTreeSHAP v1
# derivation of the memory estimation can be found in Deep Dive Section in FastTreeSHAP_Census_Income notebook
def memory_estimate_v2(shap_explainer, num_sample, num_feature, n_jobs):
    max_node = max(shap_explainer.model.num_nodes)
    max_leaves = (max_node + 1) // 2
    max_combinations = 2**int(shap_explainer.model.max_depth)
    phi_dim = num_sample * (num_feature + 1) * shap_explainer.model.num_outputs
    n_jobs = os.cpu_count() if n_jobs == -1 else n_jobs
    memory_1 = (max_leaves * max_combinations + phi_dim) * 8 * n_jobs
    memory_2 = max_leaves * max_combinations * shap_explainer.model.values.shape[0] * 8
    memory = min(memory_1, memory_2)
    if memory < 1024:
        print("Memory usage of FastTreeSHAP v2 is around {:.2f}B.".format(memory))
    elif memory / 1024 < 1024:
        print("Memory usage of FastTreeSHAP v2 is around {:.2f}KB.".format(memory / 1024))
    elif memory / 1024**2 < 1024:
        print("Memory usage of FastTreeSHAP v2 is around {:.2f}MB.".format(memory / 1024**2))
    else:
        print("Memory usage of FastTreeSHAP v2 is around {:.2f}GB.".format(memory / 1024**3))<jupyter_output><empty_output><jupyter_text>Compute SHAP values via different versions of TreeSHAP<jupyter_code>num_sample = 10000  # number of samples to be explained
n_jobs = -1  # number of parallel threads (-1 means utilizing all available cores)
# compute SHAP values via FastTreeSHAP v0 (i.e., original TreeSHAP)
# parallel computing is not enabled in original TreeSHAP in SHAP package, but here we enable it for a fair comparison
# on execution time
shap_explainer = fasttreeshap.TreeExplainer(rf_model, algorithm = "v0", n_jobs = n_jobs)
shap_values_v0 = shap_explainer(test.iloc[:num_sample]).values
shap_values_v0.shape
# compute SHAP values via FastTreeSHAP v1
shap_explainer = fasttreeshap.TreeExplainer(rf_model, algorithm = "v1", n_jobs = n_jobs)
shap_values_v1 = shap_explainer(test.iloc[:num_sample]).values
shap_values_v1.shape
# justify the correctness of FastTreeSHAP v1
print("Maximum difference of SHAP values between v1 and v0 is {:.2e}.".format(
    np.max(abs(shap_values_v1 - shap_values_v0))))
# estimate memory usage of FastTreeSHAP v2 since FastTreeSHAP v2 has a stricter memory constraint than
# TreeSHAP and FastTreeSHAP v1
memory_estimate_v2(shap_explainer, num_sample, test.shape[1], n_jobs)
# compute SHAP values via FastTreeSHAP v2
shap_explainer = fasttreeshap.TreeExplainer(rf_model, algorithm = "v2", n_jobs = n_jobs)
shap_values_v2 = shap_explainer(test.iloc[:num_sample]).values
shap_values_v2.shape
# justify the correctness of FastTreeSHAP v2
print("Maximum difference of SHAP values between v2 and v0 is {:.2e}.".format(
    np.max(abs(shap_values_v2 - shap_values_v0))))
# compute SHAP values via automatic TreeSHAP algorithm selection
shap_explainer = fasttreeshap.TreeExplainer(rf_model, algorithm = "auto", n_jobs = n_jobs)
shap_values_auto = shap_explainer(test.iloc[:num_sample]).values
shap_values_auto.shape
# justify the correctness of automatically selected TreeSHAP algorithm
# it turns out that "auto" selects "v2" as the most appropriate TreeSHAP algorithm
print("Maximum difference of SHAP values between auto and v0 is {:.2e}.".format(
    np.max(abs(shap_values_auto - shap_values_v0))))<jupyter_output><empty_output><jupyter_text>Compare running times of different versions of TreeSHAP in computing SHAP values<jupyter_code># compute SHAP values/SHAP interaction values via TreeSHAP algorithm with version "algorithm_version"
# (parallel on "n_jobs" threads)
def run_fasttreeshap(model, sample, interactions, algorithm_version, n_jobs, num_round, num_sample, shortcut = False):
    shap_explainer = fasttreeshap.TreeExplainer(
        model, algorithm = algorithm_version, n_jobs = n_jobs, shortcut = shortcut)
    run_time = np.zeros(num_round)
    for i in range(num_round):
        start = time.time()
        shap_values = shap_explainer(sample.iloc[:num_sample], interactions = interactions).values
        run_time[i] = time.time() - start
        print("Round {} takes {:.3f} sec.".format(i + 1, run_time[i]))
    print("Average running time of {} is {:.3f} sec (std {:.3f} sec){}.".format(
        algorithm_version, np.mean(run_time), np.std(run_time), " (with shortcut)" if shortcut else ""))
num_sample = 10000  # number of samples to be explained
num_round = 3  # number of rounds to record mean and standard deviation of running time
n_jobs = -1  # number of parallel threads (-1 means utilizing all available cores)
# run FastTreeSHAP v0 (i.e., original TreeSHAP) multiple times and record its average running time
# parallel computing is not enabled in original TreeSHAP in SHAP package, but here we enable it for a fair comparison
# on execution time
run_fasttreeshap(
    model = rf_model, sample = test, interactions = False, algorithm_version = "v0", n_jobs = n_jobs,
    num_round = num_round, num_sample = num_sample)
# run FastTreeSHAP v1 multiple times and record its average running time
run_fasttreeshap(
    model = rf_model, sample = test, interactions = False, algorithm_version = "v1", n_jobs = n_jobs, 
    num_round = num_round, num_sample = num_sample)
# run FastTreeSHAP v2 multiple times and record its average running time
run_fasttreeshap(
    model = rf_model, sample = test, interactions = False, algorithm_version = "v2", n_jobs = n_jobs, 
    num_round = num_round, num_sample = num_sample)
# run automatically selected TreeSHAP algorithm multiple times and record its average running time
# it turns out that "auto" selects "v2" as the most appropriate TreeSHAP algorithm
run_fasttreeshap(
    model = rf_model, sample = test, interactions = False, algorithm_version = "auto", n_jobs = n_jobs, 
    num_round = num_round, num_sample = num_sample)<jupyter_output><empty_output><jupyter_text>Compute SHAP interaction values via different versions of TreeSHAP<jupyter_code>num_sample = 100  # number of samples to be explained
n_jobs = -1  # number of parallel threads (-1 means utilizing all available cores)
# compute SHAP interaction values via FastTreeSHAP v0 (i.e., original TreeSHAP)
# parallel computing is not enabled in original TreeSHAP in SHAP package, but here we enable it for a fair comparison
# on execution time
shap_explainer = fasttreeshap.TreeExplainer(rf_model, algorithm = "v0", n_jobs = n_jobs)
shap_interaction_values_v0 = shap_explainer(test.iloc[:num_sample], interactions = True).values
shap_interaction_values_v0.shape
# compute SHAP interaction values via FastTreeSHAP v1
shap_explainer = fasttreeshap.TreeExplainer(rf_model, algorithm = "v1", n_jobs = n_jobs)
shap_interaction_values_v1 = shap_explainer(test.iloc[:num_sample], interactions = True).values
shap_interaction_values_v1.shape
# justify the correctness of FastTreeSHAP v1
print("Maximum difference of SHAP interaction values between v1 and v0 is {:.2e}.".format(
    np.max(abs(shap_interaction_values_v1 - shap_interaction_values_v0))))
# compute SHAP interaction values via automatic TreeSHAP algorithm selection
# v1 is always preferred to v0 in any use cases, and v2 does not support interactions
shap_explainer = fasttreeshap.TreeExplainer(rf_model, algorithm = "auto", n_jobs = n_jobs)
shap_interaction_values_auto = shap_explainer(test.iloc[:num_sample], interactions = True).values
shap_interaction_values_auto.shape
# justify the correctness of automatically selected TreeSHAP algorithm
print("Maximum difference of SHAP interaction values between auto and v0 is {:.2e}.".format(
    np.max(abs(shap_interaction_values_auto - shap_interaction_values_v0))))<jupyter_output><empty_output><jupyter_text>Compare running times of different versions of TreeSHAP in computing SHAP interaction values<jupyter_code>num_sample = 100  # number of samples to be explained
num_round = 3  # number of rounds to record mean and standard deviation of running time
n_jobs = -1  # number of parallel threads (-1 means utilizing all available cores)
# run FastTreeSHAP v0 (i.e., original TreeSHAP) multiple times and record its average running time
# parallel computing is not enabled in original TreeSHAP in SHAP package, but here we enable it for a fair comparison
# on execution time
run_fasttreeshap(
    model = rf_model, sample = test, interactions = True, algorithm_version = "v0", n_jobs = n_jobs, 
    num_round = num_round, num_sample = num_sample)
# run FastTreeSHAP v1 multiple times and record its average running time
run_fasttreeshap(
    model = rf_model, sample = test, interactions = True, algorithm_version = "v1", n_jobs = n_jobs, 
    num_round = num_round, num_sample = num_sample)
# run automatically selected TreeSHAP algorithm multiple times and record its average running time
# v1 is always preferred to v0 in any use cases, and v2 does not support interactions
run_fasttreeshap(
    model = rf_model, sample = test, interactions = True, algorithm_version = "auto", n_jobs = n_jobs, 
    num_round = num_round, num_sample = num_sample)<jupyter_output><empty_output><jupyter_text>Train an XGBoost model and compute SHAP values<jupyter_code>n_estimators = 200  # number of trees in XGBoost model
max_depth = 8  # maximum depth of any trees in XGBoost model
# train an XGBoost model
xgb_model = xgb.XGBClassifier(
    max_depth = max_depth, n_estimators = n_estimators, learning_rate = 0.1, n_jobs = -1,
    use_label_encoder = False, eval_metric = "logloss", random_state = 0)
xgb_model.fit(train, label_train)
print("Accuracy on testing set is {:.2f}.".format(accuracy_score(label_test, xgb_model.predict(test))))
# obtain total number of leaves
shap_explainer = fasttreeshap.TreeExplainer(xgb_model)
num_leaves = sum(shap_explainer.model.num_nodes) - sum(sum(shap_explainer.model.children_left > 0))
print("Total number of leaves is {}.".format(num_leaves))<jupyter_output><empty_output><jupyter_text>Compute SHAP values via different versions of TreeSHAP<jupyter_code>num_sample = 10000  # number of samples to be explained
n_jobs = -1  # number of parallel threads (-1 means utilizing all available cores)
# compute SHAP values via "shortcut" (i.e., original TreeSHAP in XGBoost package)
# by default, parallel computing on all available cores is enabled in "shortcut"
shap_explainer = fasttreeshap.TreeExplainer(xgb_model, algorithm = "v0", n_jobs = n_jobs, shortcut = True)
shap_values_shortcut = shap_explainer(test.iloc[:num_sample]).values
shap_values_shortcut.shape
# compute SHAP values via FastTreeSHAP v0 (i.e., original TreeSHAP in SHAP package)
# parallel computing is not enabled in original TreeSHAP in SHAP package, but here we enable it for a fair comparison
# on execution time
shap_explainer = fasttreeshap.TreeExplainer(xgb_model, algorithm = "v0", n_jobs = n_jobs, shortcut = False)
shap_values_v0 = shap_explainer(test.iloc[:num_sample]).values
shap_values_v0.shape
# justify the correctness of FastTreeSHAP v0
print("Mean and maximum differences of SHAP values between v0 and shortcut is {:.2e} and {:.2e}.".format(
    np.mean(abs(shap_values_v0 - shap_values_shortcut)), np.max(abs(shap_values_v0 - shap_values_shortcut))))
# compute SHAP values via FastTreeSHAP v1
shap_explainer = fasttreeshap.TreeExplainer(xgb_model, algorithm = "v1", n_jobs = n_jobs, shortcut = False)
shap_values_v1 = shap_explainer(test.iloc[:num_sample]).values
shap_values_v1.shape
# justify the correctness of FastTreeSHAP v1
print("Maximum difference of SHAP values between v1 and v0 is {:.2e}.".format(
    np.max(abs(shap_values_v1 - shap_values_v0))))
# estimate memory usage of FastTreeSHAP v2 since FastTreeSHAP v2 has a stricter memory constraint than
# TreeSHAP and FastTreeSHAP v1
memory_estimate_v2(shap_explainer, num_sample, test.shape[1], n_jobs)
# compute SHAP values via FastTreeSHAP v2
shap_explainer = fasttreeshap.TreeExplainer(xgb_model, algorithm = "v2", n_jobs = n_jobs, shortcut = False)
shap_values_v2 = shap_explainer(test.iloc[:num_sample]).values
shap_values_v2.shape
# justify the correctness of FastTreeSHAP v2
print("Maximum difference of SHAP values between v2 and v0 is {:.2e}.".format(
    np.max(abs(shap_values_v2 - shap_values_v0))))
# compute SHAP values via automatic TreeSHAP algorithm selection
shap_explainer = fasttreeshap.TreeExplainer(xgb_model, algorithm = "auto", n_jobs = n_jobs, shortcut = False)
shap_values_auto = shap_explainer(test.iloc[:num_sample]).values
shap_values_auto.shape
# justify the correctness of automatically selected TreeSHAP algorithm
# it turns out that "auto" selects "v2" as the most appropriate TreeSHAP algorithm
print("Maximum difference of SHAP values between auto and v0 is {:.2e}.".format(
    np.max(abs(shap_values_auto - shap_values_v0))))<jupyter_output><empty_output><jupyter_text>Compare running times of different versions of TreeSHAP in computing SHAP values<jupyter_code>num_sample = 10000  # number of samples to be explained
num_round = 3  # number of rounds to record mean and standard deviation of running time
n_jobs = -1  # number of parallel threads (-1 means utilizing all available cores)
# run "shortcut" version of TreeSHAP multiple times and record its average running time
# by default, parallel computing on all available cores is enabled in "shortcut"
run_fasttreeshap(
    model = xgb_model, sample = test, interactions = False, algorithm_version = "v0", n_jobs = n_jobs, 
    num_round = num_round, num_sample = num_sample, shortcut = True)
# run FastTreeSHAP v0 (i.e., original TreeSHAP) multiple times and record its average running time
# parallel computing is not enabled in original TreeSHAP in SHAP package, but here we enable it for a fair comparison
# on execution time
run_fasttreeshap(
    model = xgb_model, sample = test, interactions = False, algorithm_version = "v0", n_jobs = n_jobs, 
    num_round = num_round, num_sample = num_sample, shortcut = False)
# run FastTreeSHAP v1 multiple times and record its average running time
run_fasttreeshap(
    model = xgb_model, sample = test, interactions = False, algorithm_version = "v1", n_jobs = n_jobs, 
    num_round = num_round, num_sample = num_sample, shortcut = False)
# run FastTreeSHAP v2 multiple times and record its average running time
run_fasttreeshap(
    model = xgb_model, sample = test, interactions = False, algorithm_version = "v2", n_jobs = n_jobs, 
    num_round = num_round, num_sample = num_sample, shortcut = False)
# run automatically selected TreeSHAP algorithm multiple times and record its average running time
# it turns out that "auto" selects "v2" as the most appropriate TreeSHAP algorithm
run_fasttreeshap(
    model = xgb_model, sample = test, interactions = False, algorithm_version = "auto", n_jobs = n_jobs, 
    num_round = num_round, num_sample = num_sample, shortcut = False)<jupyter_output><empty_output><jupyter_text>Compute SHAP interaction values via different versions of TreeSHAP<jupyter_code>num_sample = 100  # number of samples to be explained
n_jobs = -1  # number of parallel threads (-1 means utilizing all available cores)
# compute SHAP interaction values via "shortcut" (i.e., original TreeSHAP in XGBoost package)
# by default, parallel computing on all available cores is enabled in "shortcut"
shap_explainer = fasttreeshap.TreeExplainer(xgb_model, algorithm = "v0", n_jobs = n_jobs, shortcut = True)
shap_interaction_values_shortcut = shap_explainer(test.iloc[:num_sample], interactions = True).values
shap_interaction_values_shortcut.shape
# compute SHAP interaction values via FastTreeSHAP v0 (i.e., original TreeSHAP in SHAP package)
# parallel computing is not enabled in original TreeSHAP in SHAP package, but here we enable it for a fair comparison
# on execution time
shap_explainer = fasttreeshap.TreeExplainer(xgb_model, algorithm = "v0", n_jobs = n_jobs, shortcut = False)
shap_interaction_values_v0 = shap_explainer(test.iloc[:num_sample], interactions = True).values
shap_interaction_values_v0.shape
# justify the correctness of FastTreeSHAP v0
print("Mean and maximum differences of SHAP values between v0 and shortcut is {:.2e} and {:.2e}.".format(
    np.mean(abs(shap_interaction_values_v0 - shap_interaction_values_shortcut)), 
    np.max(abs(shap_interaction_values_v0 - shap_interaction_values_shortcut))))
# compute SHAP interaction values via FastTreeSHAP v1
shap_explainer = fasttreeshap.TreeExplainer(xgb_model, algorithm = "v1", n_jobs = n_jobs, shortcut = False)
shap_interaction_values_v1 = shap_explainer(test.iloc[:num_sample], interactions = True).values
shap_interaction_values_v1.shape
# justify the correctness of FastTreeSHAP v1
print("Maximum difference of SHAP interaction values between v1 and v0 is {:.2e}.".format(
    np.max(abs(shap_interaction_values_v1 - shap_interaction_values_v0))))
# compute SHAP interaction values via automatic TreeSHAP algorithm selection
# v1 is always preferred to v0 in any use cases, and v2 does not support interactions
shap_explainer = fasttreeshap.TreeExplainer(xgb_model, algorithm = "auto", n_jobs = n_jobs, shortcut = False)
shap_interaction_values_auto = shap_explainer(test.iloc[:num_sample], interactions = True).values
shap_interaction_values_auto.shape
# justify the correctness of automatically selected TreeSHAP algorithm
print("Maximum difference of SHAP interaction values between auto and v0 is {:.2e}.".format(
    np.max(abs(shap_interaction_values_auto - shap_interaction_values_v0))))<jupyter_output><empty_output><jupyter_text>Compare running times of different versions of TreeSHAP in computing SHAP interaction values<jupyter_code>num_sample = 100  # number of samples to be explained
num_round = 3  # number of rounds to record mean and standard deviation of running time
n_jobs = -1  # number of parallel threads (-1 means utilizing all available cores)
# run "shortcut" version of TreeSHAP multiple times and record its average running time
# by default, parallel computing on all available cores is enabled in "shortcut"
run_fasttreeshap(
    model = xgb_model, sample = test, interactions = True, algorithm_version = "v0", n_jobs = n_jobs, 
    num_round = num_round, num_sample = num_sample, shortcut = True)
# run FastTreeSHAP v0 (i.e., original TreeSHAP) multiple times and record its average running time
# parallel computing is not enabled in original TreeSHAP in SHAP package, but here we enable it for a fair comparison
# on execution time
run_fasttreeshap(
    model = xgb_model, sample = test, interactions = True, algorithm_version = "v0", n_jobs = n_jobs, 
    num_round = num_round, num_sample = num_sample, shortcut = False)
# run FastTreeSHAP v1 multiple times and record its average running time
run_fasttreeshap(
    model = xgb_model, sample = test, interactions = True, algorithm_version = "v1", n_jobs = n_jobs, 
    num_round = num_round, num_sample = num_sample, shortcut = False)
# run automatically selected TreeSHAP algorithm multiple times and record its average running time
# v1 is always preferred to v0 in any use cases, and v2 does not support interactions
run_fasttreeshap(
    model = xgb_model, sample = test, interactions = True, algorithm_version = "auto", n_jobs = n_jobs, 
    num_round = num_round, num_sample = num_sample, shortcut = False)<jupyter_output><empty_output><jupyter_text>Train a LightGBM model and compute SHAP values<jupyter_code>n_estimators = 500  # number of trees in LightGBM model
max_depth = 8  # maximum depth of any trees in LightGBM model
# train a LightGBM model
lgb_model = lgb.LGBMClassifier(
    max_depth = max_depth, n_estimators = n_estimators, learning_rate = 0.1, n_jobs = -1, random_state = 0)
lgb_model.fit(train, label_train)
print("Accuracy on testing set is {:.2f}.".format(accuracy_score(label_test, lgb_model.predict(test))))
# obtain total number of leaves
shap_explainer = fasttreeshap.TreeExplainer(lgb_model)
num_leaves = sum(shap_explainer.model.num_nodes) - sum(sum(shap_explainer.model.children_left > 0))
print("Total number of leaves is {}.".format(num_leaves))<jupyter_output><empty_output><jupyter_text>Compute SHAP values via different versions of TreeSHAP<jupyter_code>num_sample = 10000  # number of samples to be explained
n_jobs = -1  # number of parallel threads (-1 means utilizing all available cores)
# compute SHAP values via "shortcut" (i.e., original TreeSHAP in LightGBM package)
# by default, parallel computing on all available cores is enabled in "shortcut"
shap_explainer = fasttreeshap.TreeExplainer(lgb_model, algorithm = "v0", n_jobs = n_jobs, shortcut = True)
shap_values_shortcut = shap_explainer(test.iloc[:num_sample]).values
shap_values_shortcut.shape
# compute SHAP values via FastTreeSHAP v0 (i.e., original TreeSHAP in SHAP package)
# parallel computing is not enabled in original TreeSHAP in SHAP package, but here we enable it for a fair comparison
# on execution time
shap_explainer = fasttreeshap.TreeExplainer(lgb_model, algorithm = "v0", n_jobs = n_jobs, shortcut = False)
shap_values_v0 = shap_explainer(test.iloc[:num_sample]).values
shap_values_v0.shape
# justify the correctness of FastTreeSHAP v0
print("Mean and maximum differences of SHAP values between v0 and shortcut is {:.2e} and {:.2e}.".format(
    np.mean(abs(shap_values_v0 - shap_values_shortcut)), np.max(abs(shap_values_v0 - shap_values_shortcut))))
# compute SHAP values via FastTreeSHAP v1
shap_explainer = fasttreeshap.TreeExplainer(lgb_model, algorithm = "v1", n_jobs = n_jobs, shortcut = False)
shap_values_v1 = shap_explainer(test.iloc[:num_sample]).values
shap_values_v1.shape
# justify the correctness of FastTreeSHAP v1
print("Maximum difference of SHAP values between v1 and v0 is {:.2e}.".format(
    np.max(abs(shap_values_v1 - shap_values_v0))))
# estimate memory usage of FastTreeSHAP v2 since FastTreeSHAP v2 has a stricter memory constraint than
# TreeSHAP and FastTreeSHAP v1
memory_estimate_v2(shap_explainer, num_sample, test.shape[1], n_jobs)
# compute SHAP values via FastTreeSHAP v2
shap_explainer = fasttreeshap.TreeExplainer(lgb_model, algorithm = "v2", n_jobs = n_jobs, shortcut = False)
shap_values_v2 = shap_explainer(test.iloc[:num_sample]).values
shap_values_v2.shape
# justify the correctness of FastTreeSHAP v2
print("Maximum difference of SHAP values between v2 and v0 is {:.2e}.".format(
    np.max(abs(shap_values_v2 - shap_values_v0))))
# compute SHAP values via automatic TreeSHAP algorithm selection
shap_explainer = fasttreeshap.TreeExplainer(lgb_model, algorithm = "auto", n_jobs = n_jobs, shortcut = False)
shap_values_auto = shap_explainer(test.iloc[:num_sample]).values
shap_values_auto.shape
# justify the correctness of automatically selected TreeSHAP algorithm
# it turns out that "auto" selects "v2" as the most appropriate TreeSHAP algorithm
print("Maximum difference of SHAP values between auto and v0 is {:.2e}.".format(
    np.max(abs(shap_values_auto - shap_values_v0))))<jupyter_output><empty_output><jupyter_text>Compare running times of different versions of TreeSHAP in computing SHAP values<jupyter_code>num_sample = 10000  # number of samples to be explained
num_round = 3  # number of rounds to record mean and standard deviation of running time
n_jobs = -1  # number of parallel threads (-1 means utilizing all available cores)
# run "shortcut" version of TreeSHAP multiple times and record its average running time
# by default, parallel computing on all available cores is enabled in "shortcut"
run_fasttreeshap(
    model = lgb_model, sample = test, interactions = False, algorithm_version = "v0", n_jobs = n_jobs, 
    num_round = num_round, num_sample = num_sample, shortcut = True)
# run FastTreeSHAP v0 (i.e., original TreeSHAP) multiple times and record its average running time
# parallel computing is not enabled in original TreeSHAP in SHAP package, but here we enable it for a fair comparison
# on execution time
run_fasttreeshap(
    model = lgb_model, sample = test, interactions = False, algorithm_version = "v0", n_jobs = n_jobs, 
    num_round = num_round, num_sample = num_sample, shortcut = False)
# run FastTreeSHAP v1 multiple times and record its average running time
run_fasttreeshap(
    model = lgb_model, sample = test, interactions = False, algorithm_version = "v1", n_jobs = n_jobs, 
    num_round = num_round, num_sample = num_sample, shortcut = False)
# run FastTreeSHAP v2 multiple times and record its average running time
run_fasttreeshap(
    model = lgb_model, sample = test, interactions = False, algorithm_version = "v2", n_jobs = n_jobs, 
    num_round = num_round, num_sample = num_sample, shortcut = False)
# run automatically selected TreeSHAP algorithm multiple times and record its average running time
# it turns out that "auto" selects "v2" as the most appropriate TreeSHAP algorithm
run_fasttreeshap(
    model = lgb_model, sample = test, interactions = False, algorithm_version = "auto", n_jobs = n_jobs, 
    num_round = num_round, num_sample = num_sample, shortcut = False)<jupyter_output><empty_output><jupyter_text>Compute SHAP interaction values via different versions of TreeSHAP<jupyter_code>num_sample = 100  # number of samples to be explained
n_jobs = -1  # number of parallel threads (-1 means utilizing all available cores)
# compute SHAP interaction values via FastTreeSHAP v0 (i.e., original TreeSHAP in SHAP package)
# parallel computing is not enabled in original TreeSHAP in SHAP package, but here we enable it for a fair comparison
# on execution time
# "shortcut" of SHAP interaction values is not enabled for LightGBM in SHAP package
shap_explainer = fasttreeshap.TreeExplainer(lgb_model, algorithm = "v0", n_jobs = n_jobs, shortcut = False)
shap_interaction_values_v0 = shap_explainer(test.iloc[:num_sample], interactions = True).values
shap_interaction_values_v0.shape
# compute SHAP interaction values via FastTreeSHAP v1
shap_explainer = fasttreeshap.TreeExplainer(lgb_model, algorithm = "v1", n_jobs = n_jobs, shortcut = False)
shap_interaction_values_v1 = shap_explainer(test.iloc[:num_sample], interactions = True).values
shap_interaction_values_v1.shape
# justify the correctness of FastTreeSHAP v1
print("Maximum difference of SHAP interaction values between v1 and v0 is {:.2e}.".format(
    np.max(abs(shap_interaction_values_v1 - shap_interaction_values_v0))))
# compute SHAP interaction values via automatic TreeSHAP algorithm selection
# v1 is always preferred to v0 in any use cases, and v2 does not support interactions
shap_explainer = fasttreeshap.TreeExplainer(lgb_model, algorithm = "auto", n_jobs = n_jobs, shortcut = False)
shap_interaction_values_auto = shap_explainer(test.iloc[:num_sample], interactions = True).values
shap_interaction_values_auto.shape
# justify the correctness of automatically selected TreeSHAP algorithm
print("Maximum difference of SHAP interaction values between auto and v0 is {:.2e}.".format(
    np.max(abs(shap_interaction_values_auto - shap_interaction_values_v0))))<jupyter_output><empty_output><jupyter_text>Compare running times of different versions of TreeSHAP in computing SHAP interaction values<jupyter_code>num_sample = 100  # number of samples to be explained
num_round = 3  # number of rounds to record mean and standard deviation of running time
n_jobs = -1  # number of parallel threads (-1 means utilizing all available cores)
# run FastTreeSHAP v0 (i.e., original TreeSHAP) multiple times and record its average running time
# parallel computing is not enabled in original TreeSHAP in SHAP package, but here we enable it for a fair comparison
# on execution time
# "shortcut" of SHAP interaction values is not enabled for LightGBM in SHAP package
run_fasttreeshap(
    model = lgb_model, sample = test, interactions = True, algorithm_version = "v0", n_jobs = n_jobs, 
    num_round = num_round, num_sample = num_sample, shortcut = False)
# run FastTreeSHAP v1 multiple times and record its average running time
run_fasttreeshap(
    model = lgb_model, sample = test, interactions = True, algorithm_version = "v1", n_jobs = n_jobs, 
    num_round = num_round, num_sample = num_sample, shortcut = False)
# run automatically selected TreeSHAP algorithm multiple times and record its average running time
# v1 is always preferred to v0 in any use cases, and v2 does not support interactions
run_fasttreeshap(
    model = lgb_model, sample = test, interactions = True, algorithm_version = "auto", n_jobs = n_jobs, 
    num_round = num_round, num_sample = num_sample, shortcut = False)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.0800012929501891, 'fraction_numerical': 0.013252739438213142, 'mean_word_length': 5.637631409568762, 'pattern_counts': {'":': 0, '<': 72, '<?xml version=': 0, '>': 72, 'https://': 3, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 1, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '17', 'dir': 'jupyter-structured-clean-dedup', 'id': '200165', 'license': 'BSD-2-Clause', 'n_tokens_mistral': 9992, 'n_tokens_neox': 9533, 'n_words': 3147, 'path': 'notebooks/FastTreeSHAP_Crop_Mapping.ipynb', 'repo_name': 'linkedin/FastTreeSHAP', 'size': '30937'}