<jupyter_start><jupyter_text>Model UnderstandingSimply examining a model's performance metrics is not enough to select a model and promote it for use in a production setting. While developing an ML algorithm, it is important to understand how the model behaves on the data, to examine the key factors influencing its predictions and to consider where it may be deficient. Determination of what "success" may mean for an ML project depends first and foremost on the user's domain expertise.blocktorch includes a variety of tools for understanding models, from graphing utilities to methods for explaining predictions.** Graphing methods on Jupyter Notebook and Jupyter Lab require [ipywidgets](https://ipywidgets.readthedocs.io/en/latest/user_install.html) to be installed.** If graphing on Jupyter Lab, [jupyterlab-plotly](https://plotly.com/python/getting-started/jupyterlab-support-python-35) required. To download this, make sure you have [npm](https://nodejs.org/en/download/) installed.  Graphing Utilities First, let's train a pipeline on some data.<jupyter_code>import blocktorch
from blocktorch.pipelines import BinaryClassificationPipeline
X, y = blocktorch.demos.load_breast_cancer()

X_train, X_holdout, y_train, y_holdout = blocktorch.preprocessing.split_data(X, y, problem_type='binary',
                                                                         test_size=0.2, random_seed=0)


pipeline_binary = BinaryClassificationPipeline(['Simple Imputer', 'Random Forest Classifier'])
pipeline_binary.fit(X_train, y_train)
print(pipeline_binary.score(X_holdout, y_holdout, objectives=['log loss binary']))<jupyter_output><empty_output><jupyter_text>Feature ImportanceWe can get the importance associated with each feature of the resulting pipeline<jupyter_code>pipeline_binary.feature_importance<jupyter_output><empty_output><jupyter_text>We can also create a bar plot of the feature importances<jupyter_code>pipeline_binary.graph_feature_importance()<jupyter_output><empty_output><jupyter_text>Permutation ImportanceWe can also compute and plot [the permutation importance](https://scikit-learn.org/stable/modules/permutation_importance.html) of the pipeline.<jupyter_code>from blocktorch.model_understanding import calculate_permutation_importance
calculate_permutation_importance(pipeline_binary, X_holdout, y_holdout, 'log loss binary')
from blocktorch.model_understanding import graph_permutation_importance
graph_permutation_importance(pipeline_binary, X_holdout, y_holdout, 'log loss binary')<jupyter_output><empty_output><jupyter_text>Partial Dependence PlotsWe can calculate the one-way [partial dependence plots](https://christophm.github.io/interpretable-ml-book/pdp.html) for a feature.<jupyter_code>from blocktorch.model_understanding.graphs import partial_dependence
partial_dependence(pipeline_binary, X_holdout, features='mean radius', grid_resolution=5)
from blocktorch.model_understanding.graphs import graph_partial_dependence
graph_partial_dependence(pipeline_binary, X_holdout, features='mean radius', grid_resolution=5)<jupyter_output><empty_output><jupyter_text>You can also compute the partial dependence for a categorical feature. We will demonstrate this on the fraud dataset.<jupyter_code>X_fraud, y_fraud = blocktorch.demos.load_fraud(100, verbose=False)
X_fraud.ww.init(logical_types={"provider": "Categorical", 'region': "Categorical",
                               "currency": "Categorical", "expiration_date": "Categorical"})

fraud_pipeline = BinaryClassificationPipeline(["DateTime Featurization Component","One Hot Encoder", "Random Forest Classifier"])
fraud_pipeline.fit(X_fraud, y_fraud)

graph_partial_dependence(fraud_pipeline, X_fraud, features='provider')<jupyter_output><empty_output><jupyter_text>Two-way partial dependence plots are also possible and invoke the same API.<jupyter_code>partial_dependence(pipeline_binary, X_holdout, features=('worst perimeter', 'worst radius'), grid_resolution=5)
graph_partial_dependence(pipeline_binary, X_holdout, features=('worst perimeter', 'worst radius'), grid_resolution=5)<jupyter_output><empty_output><jupyter_text>Confusion MatrixFor binary or multiclass classification, we can view a [confusion matrix](https://en.wikipedia.org/wiki/Confusion_matrix) of the classifier's predictions. In the DataFrame output of `confusion_matrix()`, the column header represents the predicted labels while row header represents the actual labels.<jupyter_code>from blocktorch.model_understanding.graphs import confusion_matrix
y_pred = pipeline_binary.predict(X_holdout)
confusion_matrix(y_holdout, y_pred)
from blocktorch.model_understanding.graphs import graph_confusion_matrix
y_pred = pipeline_binary.predict(X_holdout)
graph_confusion_matrix(y_holdout, y_pred)<jupyter_output><empty_output><jupyter_text>Precision-Recall CurveFor binary classification, we can view the precision-recall curve of the pipeline.<jupyter_code>from blocktorch.model_understanding.graphs import graph_precision_recall_curve
# get the predicted probabilities associated with the "true" label
import woodwork as ww
y_encoded = y_holdout.ww.map({'benign': 0, 'malignant': 1})
y_pred_proba = pipeline_binary.predict_proba(X_holdout)["malignant"]
graph_precision_recall_curve(y_encoded, y_pred_proba)<jupyter_output><empty_output><jupyter_text>ROC CurveFor binary and multiclass classification, we can view the [Receiver Operating Characteristic (ROC) curve](https://en.wikipedia.org/wiki/Receiver_operating_characteristic) of the pipeline.<jupyter_code>from blocktorch.model_understanding.graphs import graph_roc_curve
# get the predicted probabilities associated with the "malignant" label
y_pred_proba = pipeline_binary.predict_proba(X_holdout)["malignant"]
graph_roc_curve(y_encoded, y_pred_proba)<jupyter_output><empty_output><jupyter_text>The ROC curve can also be generated for multiclass classification problems. For multiclass problems, the graph will show a one-vs-many ROC curve for each class.<jupyter_code>from blocktorch.pipelines import MulticlassClassificationPipeline
X_multi, y_multi = blocktorch.demos.load_wine()

pipeline_multi = MulticlassClassificationPipeline(['Simple Imputer', 'Random Forest Classifier'])
pipeline_multi.fit(X_multi, y_multi)

y_pred_proba = pipeline_multi.predict_proba(X_multi)
graph_roc_curve(y_multi, y_pred_proba)<jupyter_output><empty_output><jupyter_text>Binary Objective Score vs. Threshold Graph[Some binary classification objectives](./objectives.ipynb) (objectives that have `score_needs_proba` set to False) are sensitive to a decision threshold. For those objectives, we can obtain and graph the scores for thresholds from zero to one, calculated at evenly-spaced intervals determined by `steps`.<jupyter_code>from blocktorch.model_understanding.graphs import binary_objective_vs_threshold
binary_objective_vs_threshold(pipeline_binary, X_holdout, y_holdout, 'f1', steps=10)
from blocktorch.model_understanding.graphs import graph_binary_objective_vs_threshold
graph_binary_objective_vs_threshold(pipeline_binary, X_holdout, y_holdout, 'f1', steps=100)<jupyter_output><empty_output><jupyter_text>Predicted Vs Actual Values Graph for Regression ProblemsWe can also create a scatterplot comparing predicted vs actual values for regression problems. We can specify an `outlier_threshold` to color values differently if the absolute difference between the actual and predicted values are outside of a given threshold.<jupyter_code>from blocktorch.model_understanding.graphs import graph_prediction_vs_actual
from blocktorch.pipelines import RegressionPipeline

X_regress, y_regress = blocktorch.demos.load_diabetes()
X_train, X_test, y_train, y_test = blocktorch.preprocessing.split_data(X_regress, y_regress, problem_type='regression')

pipeline_regress = RegressionPipeline(['One Hot Encoder', 'Linear Regressor'])
pipeline_regress.fit(X_train, y_train)

y_pred = pipeline_regress.predict(X_test)
graph_prediction_vs_actual(y_test, y_pred, outlier_threshold=50)<jupyter_output><empty_output><jupyter_text>Now let's train a decision tree on some data.<jupyter_code>pipeline_dt = BinaryClassificationPipeline(['Simple Imputer', 'Decision Tree Classifier'])
pipeline_dt.fit(X_train, y_train)<jupyter_output><empty_output><jupyter_text>Tree VisualizationWe can visualize the structure of the Decision Tree that was fit to that data, and save it if necessary.<jupyter_code>from blocktorch.model_understanding.graphs import visualize_decision_tree

visualize_decision_tree(pipeline_dt.estimator, max_depth=2, rotate=False, filled=True, filepath=None)<jupyter_output><empty_output><jupyter_text>Explaining Predictions We can explain why the model made certain predictions with the [explain_predictions](../autoapi/blocktorch/model_understanding/prediction_explanations/explainers/index.rstblocktorch.model_understanding.prediction_explanations.explainers.explain_predictions) function. This will use the [Shapley Additive Explanations (SHAP)](https://github.com/slundberg/shap) algorithm to identify the top features that explain the predicted value. This function can explain both classification and regression models - all you need to do is provide the pipeline, the input features, and a list of rows corresponding to the indices of the input features you want to explain. The function will return a table that you can print summarizing the top 3 most positive and negative contributing features to the predicted value.In the example below, we explain the prediction for the third data point in the data set. We see that the `worst concave points` feature increased the estimated probability that the tumor is malignant by 20% while the `worst radius` feature decreased the probability the tumor is malignant by 5%.<jupyter_code>from blocktorch.model_understanding.prediction_explanations import explain_predictions

table = explain_predictions(pipeline=pipeline_binary, input_features=X_holdout, y=None, indices_to_explain=[3],
                           top_k_features=6, include_shap_values=True)
print(table)<jupyter_output><empty_output><jupyter_text>The interpretation of the table is the same for regression problems - but the SHAP value now corresponds to the change in the estimated value of the dependent variable rather than a change in probability. For multiclass classification problems, a table will be output for each possible class.Below is an example of how you would explain three predictions with [explain_predictions](../autoapi/blocktorch/model_understanding/prediction_explanations/explainers/index.rstblocktorch.model_understanding.prediction_explanations.explainers.explain_predictions).<jupyter_code>from blocktorch.model_understanding.prediction_explanations import explain_predictions

report = explain_predictions(pipeline=pipeline_binary,
                             input_features=X_holdout, y=y_holdout, indices_to_explain=[0, 4, 9], include_shap_values=True,
                             output_format='text')
print(report)<jupyter_output><empty_output><jupyter_text>Explaining Best and Worst PredictionsWhen debugging machine learning models, it is often useful to analyze the best and worst predictions the model made. The [explain_predictions_best_worst](../autoapi/blocktorch/model_understanding/prediction_explanations/explainers/index.rstblocktorch.model_understanding.prediction_explanations.explainers.explain_predictions_best_worst) function can help us with this.This function will display the output of [explain_predictions](../autoapi/blocktorch/model_understanding/prediction_explanations/explainers/index.rstblocktorch.model_understanding.prediction_explanations.explainers.explain_predictions) for the best 2 and worst 2 predictions. By default, the best and worst predictions are determined by the absolute error for regression problems and [cross entropy](https://en.wikipedia.org/wiki/Cross_entropy) for classification problems.We can specify our own ranking function by passing in a function to the `metric` parameter. This function will be called on `y_true` and `y_pred`. By convention, lower scores are better.At the top of each table, we can see the predicted probabilities, target value, error, and row index for that prediction. For a regression problem, we would see the predicted value instead of predicted probabilities.<jupyter_code>from blocktorch.model_understanding.prediction_explanations import explain_predictions_best_worst

report = explain_predictions_best_worst(pipeline=pipeline_binary, input_features=X_holdout, y_true=y_holdout,
                                        include_shap_values=True, top_k_features=6, num_to_explain=2)

print(report)<jupyter_output><empty_output><jupyter_text>We use a custom metric ([hinge loss](https://en.wikipedia.org/wiki/Hinge_loss)) for selecting the best and worst predictions. See this example:```pythonimport numpy as npdef hinge_loss(y_true, y_pred_proba):        probabilities = np.clip(y_pred_proba.iloc[:, 1], 0.001, 0.999)    y_true[y_true == 0] = -1        return np.clip(1 - y_true * np.log(probabilities / (1 - probabilities)), a_min=0, a_max=None)report = explain_predictions_best_worst(pipeline=pipeline, input_features=X, y_true=y,                                        include_shap_values=True, num_to_explain=5, metric=hinge_loss)print(report)```  Changing Output FormatsInstead of getting the prediction explanations as text, you can get the report as a python dictionary or pandas dataframe. All you have to do is pass `output_format="dict"` or `output_format="dataframe"` to either `explain_prediction`, `explain_predictions`, or `explain_predictions_best_worst`.  Single prediction as a dictionary<jupyter_code>import json
single_prediction_report = explain_predictions(pipeline=pipeline_binary, input_features=X_holdout, indices_to_explain=[3],
                                               y=y_holdout, top_k_features=6, include_shap_values=True,
                                               output_format="dict")
print(json.dumps(single_prediction_report, indent=2))<jupyter_output><empty_output><jupyter_text>Single prediction as a dataframe<jupyter_code>single_prediction_report = explain_predictions(pipeline=pipeline_binary, input_features=X_holdout,
                                               indices_to_explain=[3],
                                               y=y_holdout, top_k_features=6, include_shap_values=True,
                                               output_format="dataframe")
single_prediction_report<jupyter_output><empty_output><jupyter_text>Best and worst predictions as a dictionary<jupyter_code>report = explain_predictions_best_worst(pipeline=pipeline_binary, input_features=X, y_true=y,
                                        num_to_explain=1, top_k_features=6,
                                        include_shap_values=True, output_format="dict")
print(json.dumps(report, indent=2))<jupyter_output><empty_output><jupyter_text>Best and worst predictions as a dataframe<jupyter_code>report = explain_predictions_best_worst(pipeline=pipeline_binary, input_features=X_holdout, y_true=y_holdout,
                                        num_to_explain=1, top_k_features=6,
                                        include_shap_values=True, output_format="dataframe")
report<jupyter_output><empty_output><jupyter_text>Force PlotsForce plots can be generated to predict single or multiple rows for binary, multiclass and regression problem types.  Here's an example of predicting a single row on a binary classification dataset.  The force plots show the predictive power of each of the features in making the negative ("Class: 0") prediction and the positive ("Class: 1") prediction.<jupyter_code>import shap

from blocktorch.model_understanding.force_plots import graph_force_plot

rows_to_explain = [0] # Should be a list of integer indices of the rows to explain.

results = graph_force_plot(pipeline_binary, rows_to_explain=rows_to_explain, 
                           training_data=X_holdout, y=y_holdout)

for result in results:
    for cls in result:
        print("Class:", cls)
        display(result[cls]["plot"])<jupyter_output><empty_output><jupyter_text>Here's an example of a force plot explaining multiple predictions on a multiclass problem.  These plots show the force plots for each row arranged as consecutive columns that can be ordered by the dropdown above.  Clicking the column indicates which row explanation is underneath.<jupyter_code>rows_to_explain = [0,1,2,3,4] # Should be a list of integer indices of the rows to explain.

results = graph_force_plot(pipeline_multi, 
                           rows_to_explain=rows_to_explain, 
                           training_data=X_multi, y=y_multi)

for idx, result in enumerate(results):
    print("Row:", idx)
    for cls in result:
        print("Class:", cls)
        display(result[cls]["plot"])<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.07159353348729793, 'fraction_numerical': 0.004145200450050335, 'mean_word_length': 5.840016200891049, 'pattern_counts': {'":': 3, '<': 97, '<?xml version=': 0, '>': 97, 'https://': 10, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 21, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '24', 'dir': 'jupyter-structured-clean-dedup', 'id': '19649', 'license': 'MIT', 'n_tokens_mistral': 4830, 'n_tokens_neox': 4710, 'n_words': 1323, 'path': 'ml_source/src/blocktorch/docs/source/user_guide/model_understanding.ipynb', 'repo_name': 'blocktorch/blocktorch', 'size': '16887'}