---
title: "Introduction to Reason Codes"
author: "Colin Priest, Sergey Yurgenson, Thakur Raj Anand, Peter Hurford"
date: "`r Sys.Date()`"
output:
  rmarkdown::html_vignette:
    fig_caption: yes  
vignette: >
  %\VignetteIndexEntry{Using Reason Codes}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

A few questions always asked by business leaders after seeing the results of highly accurate machine learning models are as follows  
- Are machine learning models interpretable and transparent?  
- How can the results of the model be used to develop a business strategy?  
- Can the predictions from the model be used to explain to the regulators why something was rejected or accepted based on model prediction?  

DataRobot does provide many diagnostics like partial dependence, feature impact, reason codes to answer the above questions and using those diagnostics predictions can be converted to prescriptions for the business. In this vignette we would be covering reason codes. Partial dependence has been covered in detail in the companion vignette "Interpreting Predictive Models Using Partial Dependence Plots".


## Introduction
The DataRobot modeling engine is a commercial product that supports the rapid development and evaluation of a large number of different predictive models from a single data source. The open-source R package datarobot allows users of the DataRobot modeling engine to interact with it from R, creating new modeling projects, examining model characteristics, and generating predictions from any of these models for a specified dataset. This vignette illustrates how to interact with DataRobot using **datarobot** package, build models, make prediction using a model and then use reason codes to explain why a model is predicting high or low. Reason codes can be used to answer the questions mentioned earlier.


## Load the useful libraries
Let's load **datarobot** and other useful packages 
```{r results = "asis", message = FALSE, warning = FALSE}
library(httr)
library(knitr)
library(data.table)
```

## Connecting to DataRobot
To access the DataRobot modeling engine, it is necessary to establish an authenticated connection, which can be done in one of two ways. In both cases, the necessary information is an **endpoint** - the URL address of the specific DataRobot server being used - and a **token**, a previously validated access token.

**token** is unique for each DataRobot modeling engine account and can be accessed using the DataRobot webapp in the account profile section.

**endpoint** depends on DataRobot modeling engine installation (cloud-based, on-prem...) you are using. Contact your DataRobot admin for endpoint to use. The **endpoint** for DataRobot cloud accounts is ```https://app.datarobot.com/api/v2```

The first access method uses a YAML configuration file with these two elements - labeled **token** and **endpoint** - located at $HOME/.config/datarobot/drconfig.yaml. If this file exists when the datarobot package is loaded, a connection to the DataRobot modeling engine is automatically established. It is also possible to establish a connection using this YAML file via the ConnectToDataRobot function, by specifying the configPath parameter.

The second method of establishing a connection to the DataRobot modeling engine is to call the function ConnectToDataRobot with the **endpoint** and **token** parameters.

```{r results = 'asis', message = FALSE, warning = FALSE, eval = FALSE}
library(datarobot)
endpoint <- "https://<YOUR ENDPOINT HERE>/api/v2"
apiToken <- "<YOUR API TOKEN HERE>"
ConnectToDataRobot(endpoint = endpoint, token = apiToken)
```

## Data
We would be using a sample dataset related to credit scoring open sourced by LendingClub (https://www.lendingclub.com/). Below is the information related to the variables.

```{r echo = FALSE, results = "asis", message = FALSE, warning = FALSE}
Lending <- fread("lendingClub.csv")
EDA <- t(summary(Lending))
kable(EDA, longtable = TRUE, booktabs = TRUE, row.names = TRUE)
```

## Divide data into train and test and setup the project
Let's divide our data in train and test. We can use train data to create a datarobot project using **SetupProject** function and
test data to make predictions and generate reason codes. Detailed explanation about creating projects was described in the vignette , “Introduction to the DataRobot R Package.” The specific sequence used here was:

```{r results = "asis", message = FALSE, warning = FALSE, eval = FALSE}
target <- "is_bad"
projectName <- "Credit Scoring"
numWorkers <- 10

set.seed(1111)
split <- sample(nrow(Lending), round(0.9 * nrow(Lending)), replace = FALSE)
train <- Lending[split,]
test <- Lending[-split,]

project <- SetupProject(dataSource = train, 
                       projectName = projectName)
SetTarget(project = project, 
          target = target)
```

Once the modeling process has completed, the ListModels function returns an S3 object of class “listOfModels” that characterizes all of the models in a specified DataRobot project. Calling this function before the modeling process is complete causes a partial result to be returned, with a warning; to avoid this problem, the WaitForAutopilot function is used before calling ListModels:

```{r results = "asis", message = FALSE, warning = FALSE, eval = FALSE}
# increase the number of workers used by this project
UpdateProject(project = project$projectId, 
              workerCount = numWorkers)
WaitForAutopilot(project, verbosity = 1, timeout = 999999)

results <- as.data.frame(ListModels(project))
kable(head(results), longtable = TRUE, booktabs = TRUE, row.names = TRUE)
```

```{r echo = FALSE, results = "asis", message = FALSE, warning = FALSE}
results <- readRDS("results.rds")
kable(head(results), longtable = TRUE, booktabs = TRUE, row.names = TRUE)
```

## Generating Model Predictions
The generation of model predictions is a three-step process:  
  1. Upload dataset for prediction using **UploadPredictionDataset**.   
  2. Create a predict job using **RequestPredictionsForDataset** function, which returns the predictJobId.   
  3. Pass the predictJobId to GetPredictions along with the projectId for the DataRobot project containing the model. The result returned by this function is a vector of predicted responses; in the case of binary classification projects, the optional type parameter may be used to request a vector of probabilities instead of binary responses; refer to the help files for details.   

As a specific example, the following code sequence identifies the model with the best performance, extracts it as bestModel, and generates predictions for it from the **test** dataframe we created earlier:   

```{r results = "asis", message = FALSE, warning = FALSE, eval = FALSE}
allModels <- ListModels(project)
modelFrame <- as.data.frame(allModels)
metric <- modelFrame$validationMetric
bestIndex <- which.min(metric)
bestModel <- allModels[[bestIndex]]
dataset <- UploadPredictionDataset(project, test, maxWait = 1200)
bestPredictJobId <- RequestPredictionsForDataset(project, bestModel$modelId, dataset$id)
bestPredictions <- GetPredictions(project, bestPredictJobId, type="probability")
testPredictions <- data.frame(original = test$is_bad, prediction = bestPredictions)
kable(head(testPredictions), longtable = TRUE, booktabs = TRUE, row.names = TRUE)
```

```{r echo = FALSE, results = "asis", message = FALSE, warning = FALSE}
testPredictions <- readRDS("testPredictions.rds")
kable(head(testPredictions), longtable = TRUE, booktabs = TRUE, row.names = TRUE)
```

## Calculate Feature Impact
We need to generate **Feature Impact** for the model before we can get reason codes using that model. **Feature Impact**, which is available for all model types, works by altering input data and observing the effect on a model's score. It is an on-demand feature, meaning that you must initiate a calculation to see the results. Once you have had DataRobot compute the feature impact for a model, that information is saved with the project (you do not need to recalculate feature impact each time you re-open the project or each time you request reason codes in new data).

Feature Impact for a given column measures how much worse a model's error score would be if DataRobot made predictions after randomly shuffling that column (while leaving other columns unchanged). This technique is sometimes called Permutation Importance.

```{r results = "asis", message = FALSE, warning = FALSE, eval = FALSE}
featureImpactJobId <- RequestFeatureImpact(bestModel)
featureImpact <- GetFeatureImpactForJobId(project, featureImpactJobId, maxWait = 1200)
#Print top 10 features
kable(featureImpact[1:10, ], longtable = TRUE, booktabs = TRUE, row.names = TRUE)
```


```{r echo = FALSE, results = 'asis',message=F, warning=F}
featureImpact <- readRDS('featureImpact.rds')
kable(featureImpact[1:10, ], longtable = TRUE, booktabs = TRUE, row.names = TRUE)
```


## Calculate Reason Codes
For each prediction, DataRobot provides an ordered list of reasons; the number of reasons is based on the setting. Each reason is a feature from the dataset and its corresponding value, accompanied by a qualitative indicator of the reason’s strength—strong (+++), medium (++), or weak (+) positive or negative (-) influence. 

There are three main inputs you can set for DataRobot to use when computing reason codes  
  1. `maxCodes`: the Number of reasons for each predictions. Default is 3.   
  2. `thresholdLow`: Probability threshold below which DataRobot should calculate reason codes.   
  3. `thresholdHigh`: Probability threshold above which DataRobot should calculate reason codes.    
 
```{r results = "asis", message = FALSE, warning = FALSE, eval = FALSE}
# Calculate reason codes
reasonCodeJobID <- RequestReasonCodesInitialization(bestModel)
reasonCodeJobIDInitialization <- GetReasonCodesInitializationFromJobId(project,reasonCodeJobID)
# Calculate reason codes for our dataset
reasonCodeRequest <- RequestReasonCodes(bestModel, dataset$id,
                                        maxCodes = 3, thresholdLow = 0.25, thresholdHigh = 0.75)
# Get the reason codes we calculated
reasonCodeRequestMetaData <- GetReasonCodesMetadataFromJobId(project, reasonCodeRequest,
                                                             maxWait = 1800)
reasonCodeMetadata <- GetReasonCodesMetadata(project, reasonCodeRequestMetaData$id)
reasonCodeDF <- GetAllReasonCodesRowsAsDataFrame(project, reasonCodeRequestMetaData$id)
reasonCodeDF$rowId <- NULL
#subset top 3 and bottom 3 predictions
rcDataFrameFull <- rbind(reasonCodeDF[order(reasonCodeDF$class1Probability), ][1:3, ],
                                    reasonCodeDF[order(reasonCodeDF$class2Probability), ][1:3, ])
kable(head(rcDataFrameFull), longtable = TRUE, booktabs = TRUE, row.names = TRUE)
```

```{r echo = FALSE, results = "asis", message = FALSE, warning = FALSE}
rcDataFrameFull <- readRDS("reasonCodeAsDataFrameTopBottom.rds")
kable(head(rcDataFrameFull), longtable = TRUE, booktabs = TRUE, row.names = TRUE)
```


From the example above, you could answer "Why did the model give one of the customers a 97% probability of defaulting?" Top reason explains that **purpose_cat** of loan was "credit card small business"" and we can also see in above example that whenever model is predicting high probability of default, **purpose_cat** is related to small business. 

Some notes on reasons:  
  - If the data points are very similar, the reasons can list the same rounded up values.  
  - It is possible to have a reason state of MISSING if a “missing value” was important in making the prediction.  
  - Typically, the top reasons for a prediction have the same direction as the outcome, but it’s possible that with interaction
    effects or correlations among variables a reason could, for instance, have a strong positive impact on a negative
    prediction.  


## Adjusted Predictions in Reason Codes

In some projects -- such as insurance projects -- the prediction adjusted by exposure is more useful to look at than just raw prediction. For example, the raw prediction (e.g. claim counts) is divided by exposure (e.g. time) in the project with exposure column. The adjusted prediction provides insights with regard to the predicted claim counts per unit of time. To include that information, set `excludeAdjustedPredictions` to False in correspondent method calls.

```{r results = "asis", message = FALSE, warning = FALSE, eval = FALSE}
reasonCodeDFWithExposure <- GetAllReasonCodesRowsAsDataFrame(project,
                                                             reasonCodeRequestMetaData$id,
                                                             excludeAdjustedPredictions = FALSE)
kable(head(reasonCodeDFWithExposure), longtable = TRUE, booktabs = TRUE, row.names = TRUE)

```{r echo = FALSE, results = "asis", message = FALSE, warning = FALSE}
reasonCodeDFWithExposure <- readRDS("reasonCodeAsDataFrameWithExposure.rds")
kable(head(reasonCodeDFWithExposure), longtable = TRUE, booktabs = TRUE, row.names = TRUE)
```


## Summary
This note has described the **Reason Codes** which are useful for understanding why model is predicting high or low predictions for a specific case. DataRobot also provides qualitative stregth of each reason. **Reason Codes** can be used in developing good business strategy by taking prescriptions based on the reasons which are responsible for high or low predictions. They are also useful in explaining the actions taken based on the model predictions to regulatory or compliance department within an organiation.

