{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Quick introduction to generating counterfactual explanations using DiCE\n",
    "\n",
    "Exploring \"what-if\" scenarios is an important way to inspect a machine learning (ML) model. The DiCE library helps you to understand an ML model by generating \"what-if\" data points that lead to the desired model output. Formally, such \"what-if\" data points are known as *counterfactuals*, described by the following question:\n",
    "> Given that the model's output for input $x$ is $y$, what would be the output if input $x$ is changed to $x'$?\n",
    "\n",
    "The answer to the above question can be obtained by simply inputting $x'$ to the ML model. However, in many cases, we are interested in the reverse question: what changes to $x$ would lead to a desired change in model's output? When inspecting a classifier, for instance, we are often interested in knowing the changes to $x$ that will lead to a desired predicted class. For a regressor, we may be interested in the changes to $x$ that lead to a desired output range. Ideally, these changes should be *proximal* to bring out the local decision logic of the classifier, *sparse* to highlight a limited set of features, and *diverse* to show the different ways in which the same outcome can be achieved. The DiCE library provides an easy interface to generate such counterfactual examples for any ML model.\n",
    "\n",
    "In addition to proximity (minimal changes) and diversity, another important metric for counterfactual examples is their **feasibility**. If the changes in a counterfactual example are not feasible (e.g., outside the possible range of a particular feature), then the example is less useful. Specifying the possible ranges for continuous features (and possible values for categorical features) is one of the simplest forms of feasibility. DiCE allows you to customize the permitted values for features through the `permitted_range` parameter. In general, feasibility is a complex concept based on whether the [*changes* indicated by a counterfactual are feasible](https://arxiv.org/abs/1912.03277) for the individual, not just that certain values are feasible (e.g., 20 years may be a feasible value for Age feature, but changing a person's age from 22 to 20 is not feasible). DiCE will support such constraints in a future release.\n",
    "\n",
    "Counterfactual examples generated by DiCE are closely related to **necessity** and **sufficiency** conditions for causing a given model output. Necessity and sufficiency provide an intuitive way to explain a model's output. Given an input $x$ and the model output $y$, a feature value $x_i$ is *necessary* for causing the model output $y$ if changing $x_i$ changes the model output, while keeping every other feature constant. Similarly, a feature value $x_i$ is *sufficient* for causing the model output $y$ if it is impossible to change the model output while keeping $x_i$ constant. DiCE allows inspecting necessity and sufficiency by setting the `features_to_vary` parameter. If `features_to_vary` is set to $x_i$, then the generated counterfactuals demonstrate the necessity of the feature. If `features_to_vary` is set to all features except $x_i$, then the absence of any counterfactuals demonstrates the sufficiency of the feature. In practice, a single feature may be neither sufficient nor necessary---the formal definitions and the `features_to_vary` parameter translate easily to sets of features.\n",
    "\n",
    "Finally, the counterfactuals for an input data point can be used to derive a **local importance score** for each feature. The local feature importance score ranks features by their frequency of being changed in the generated counterfactuals. Among all the features, necessary features are likely to be changed more often to generate proximal counterfactuals and therefore will receive a higher score. You can use `features_to_vary` and `permitted_range` parameteres to refine the search space for the counterfactuals and consequently, the local importance score. Given a set of input points, the local importance score can be aggregated to provide a global importance score. Compared to explanation methods like LIME or SHAP, feature importance scores generated by DiCE tend to give importance to a larger number of features; more details are in this [paper](https://arxiv.org/abs/2011.04917).\n",
    "\n",
    "To generate counterfactuals, DiCE implements two kinds of methods: model-agnostic and gradient-based. \n",
    "\n",
    "* **Model-Agnostic**: These methods apply to any black-box classifier or regressor. They are based on sampling nearby points to an input point, while optimizing a loss function based on proximity (and optionally, sparsity, diversity and feasibility). Use this class of methods for sklearn models. Currently supported methods are:\n",
    "    * Randomized Search\n",
    "    * Genetic Search\n",
    "    * KD Tree Search (for counterfactuals from a given training dataset)\n",
    "* **Gradient-Based**: These methods apply to differentiable models, such as those returned by deep learning libraries like tensorflow and pytorch. They are based on an explicit loss minimization based on proximity, diversity and feasibility. The method is described in this [paper](https://arxiv.org/abs/1905.07697)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "![DiCE API](images/dice_getting_started_api.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "DiCE requires two inputs: a training dataset and a pre-trained ML model. When the training dataset is unknown (e.g., for privacy reasons), it can also work without access to the full dataset (see this [notebook](DiCE_with_private_data.ipynb) for an example). Below we show a simple example. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Sklearn imports\n",
    "from sklearn.compose import ColumnTransformer\n",
    "from sklearn.model_selection import train_test_split\n",
    "from sklearn.pipeline import Pipeline\n",
    "from sklearn.preprocessing import OneHotEncoder\n",
    "from sklearn.ensemble import RandomForestClassifier\n",
    "\n",
    "# Tensorflow import\n",
    "import tensorflow as tf\n",
    "\n",
    "\n",
    "# DiCE imports\n",
    "import dice_ml\n",
    "from dice_ml.utils import helpers  # helper functions"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%load_ext autoreload\n",
    "%autoreload 2"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Preliminaries: Loading a dataset and a ML model trained over it\n",
    "### Loading the `Adult` dataset"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We use the \"adult\" income dataset from UCI Machine Learning Repository (https://archive.ics.uci.edu/ml/datasets/adult). For demonstration purposes, we transform the data as described in **dice_ml.utils.helpers** module. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "dataset = helpers.load_adult_income_dataset()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This dataset has 8 features. The outcome is income which is binarized to 0 (low-income, <=50K) or 1 (high-income, >50K). "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "dataset.head()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# description of transformed features\n",
    "adult_info = helpers.get_adult_data_info()\n",
    "adult_info"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Split the dataset into train and test sets."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "target = dataset[\"income\"]\n",
    "train_dataset, test_dataset, y_train, y_test = train_test_split(dataset,\n",
    "                                                                target,\n",
    "                                                                test_size=0.2,\n",
    "                                                                random_state=0,\n",
    "                                                                stratify=target)\n",
    "x_train = train_dataset.drop('income', axis=1)\n",
    "x_test = test_dataset.drop('income', axis=1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Given the train dataset, we construct a data object for DiCE. Since continuous and discrete features have different ways of perturbation, we need to specify the names of the continuous features. DiCE also requires the name of the output variable that the ML model will predict."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Step 1: dice_ml.Data\n",
    "d = dice_ml.Data(dataframe=train_dataset, continuous_features=['age', 'hours_per_week'], outcome_name='income')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Loading the ML model"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "DiCE supports sklearn, tensorflow and pytorch models. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The variable *backend* below indicates the implementation type of DiCE we want to use. Four backends are supported: sklearn, TensorFlow 1.x with backend='TF1', Tensorflow 2.x with backend='TF2', and PyTorch with backend='PYT'. \n",
    "\n",
    "Below we show use a trained classification model using sklearn. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "numerical = [\"age\", \"hours_per_week\"]\n",
    "categorical = x_train.columns.difference(numerical)\n",
    "\n",
    "categorical_transformer = Pipeline(steps=[\n",
    "    ('onehot', OneHotEncoder(handle_unknown='ignore'))])\n",
    "\n",
    "transformations = ColumnTransformer(\n",
    "    transformers=[\n",
    "        ('cat', categorical_transformer, categorical)])\n",
    "\n",
    "# Append classifier to preprocessing pipeline.\n",
    "# Now we have a full prediction pipeline.\n",
    "clf = Pipeline(steps=[('preprocessor', transformations),\n",
    "                      ('classifier', RandomForestClassifier())])\n",
    "model = clf.fit(x_train, y_train)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Generating counterfactual examples using DiCE\n",
    "\n",
    "We now initialize the DiCE explainer, which needs a dataset and a model. DiCE provides local explanation for the model *m* and requires an query input whose outcome needs to be explained. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Using sklearn backend\n",
    "m = dice_ml.Model(model=model, backend=\"sklearn\")\n",
    "# Using method=random for generating CFs\n",
    "exp = dice_ml.Dice(d, m, method=\"random\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The `method` parameter specifies the explanation method. DiCE supports three methods for sklearn models: random sampling, genetic algorithm search, and kd-tree based generation.  \n",
    "\n",
    "The next code snippet shows how to generate and visualize counterfactuals. The first argument of the `generate_counterfactuals` method is the _query instances_ on which counterfactuals are desired. This can be a dataframe with one or more rows. \n",
    "\n",
    "Below we provide a sample input whose outcome is 0 (low-income) as per the ML model object *m*. Given the query input, we can now generate counterfactual explanations to show perturbed inputs from the original input where the ML model outputs class 1 (high-income). The last column shows the output of the classifier: `income-output` >=0.5 is class 1 and `income-output`<0.5 is class 0. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "e1 = exp.generate_counterfactuals(x_test[0:1], total_CFs=2, desired_class=\"opposite\")\n",
    "e1.visualize_as_dataframe(show_only_changes=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The `show_only_changes` parameter highlights the changes from the query instance. If you would like to see the full feature values for the counterfactuals, set it to False."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "e1.visualize_as_dataframe(show_only_changes=False)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "That's it! You can try generating counterfactual explanations for other examples using the same code. \n",
    "It is also possible to restrict the features to vary while generating the counterfactuals, and to specify permitted range of features within which the counterfactual should be generated. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Changing only age and education\n",
    "e2 = exp.generate_counterfactuals(x_test[0:1],\n",
    "                                  total_CFs=2,\n",
    "                                  desired_class=\"opposite\",\n",
    "                                  features_to_vary=[\"age\", \"education\", \"race\"]\n",
    "                                  )\n",
    "e2.visualize_as_dataframe(show_only_changes=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Restricting age to be between [20,30] and Education to be either {'Doctorate', 'Prof-school'}.\n",
    "e3 = exp.generate_counterfactuals(x_test[0:1],\n",
    "                                  total_CFs=2,\n",
    "                                  desired_class=\"opposite\",\n",
    "                                  permitted_range={'age': [20, 30], 'education': ['Doctorate', 'Prof-school']})\n",
    "e3.visualize_as_dataframe(show_only_changes=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Generating feature attributions (local and global) using DiCE\n",
    "\n",
    "DiCE can generate feature importance scores using a summary of the counterfactuals generated. Intuitively, a feature that is changed more often when generating proximal counterfactuals for an input is locally important for causing the model's prediction at the input. Formally, counterfactuals operationalize the __necessity__ criterion for a model explanation: _is the feature value necessary for the given model output?_ \n",
    "\n",
    "For more details, refer to the paper, [Towards Unifying Feature Attribution and Counterfactual Explanations: Different Means to the Same End](https://arxiv.org/abs/2011.04917)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Local feature importance scores\n",
    "\n",
    "These scores are computed for a given query instance (input point) by summarizing a set of counterfactual examples around the point. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "query_instance = x_test[0:1]\n",
    "imp = exp.local_feature_importance(query_instance, total_CFs=10)\n",
    "print(imp.local_importance)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The `total_CFs` parameter denotes the number of counterfactuals that are used to create the local importance. More the better."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Global feature importance scores\n",
    "\n",
    "A global importance score per feature can be estimated by aggregating the scores over individual inputs. The more the inputs, the better the estimate for global importance of a feature."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "query_instances = x_test[0:20]\n",
    "imp = exp.global_feature_importance(query_instances)\n",
    "print(imp.summary_importance)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Working with deep learning models (TensorFlow and PyTorch)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We now show examples of gradient-based methods with Tensorflow and Pytorch models. Since the gradient-based methods _optimize_ the loss rather than simply sampling some points, they can be slower to generate counterfactuals. The loss is defined by three component: **validity** (does the CF have the desired model output), **proximity** (distance of CF from original point should be low), and **diversity** (multiple CFs should change different features). The DiCE loss formulation is described in the paper, [Explaining Machine Learning Classifiers through Diverse Counterfactual Explanations](https://arxiv.org/abs/1905.07697).\n",
    "\n",
    "Below, we use a pre-trained ML model which produces high accuracy comparable to other baselines. For convenience, we include the sample trained model with the DiCE package."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Explaining a Tensorflow model"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# supress deprecation warnings from TF\n",
    "tf.compat.v1.logging.set_verbosity(tf.compat.v1.logging.ERROR)\n",
    "\n",
    "backend = 'TF'+tf.__version__[0]  # TF1\n",
    "ML_modelpath = helpers.get_adult_income_modelpath(backend=backend)\n",
    "# Step 2: dice_ml.Model\n",
    "m = dice_ml.Model(model_path=ML_modelpath, backend=backend)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We want to note that the time required to find counterfactuals with Tensorflow 2.x's eager style of execution is significantly greater than that with TensorFlow 1.x's graph execution.\n",
    "\n",
    "Based on the data object *d* and the model object *m*, we can now instantiate the DiCE class for generating explanations. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Step 3: initiate DiCE\n",
    "exp = dice_ml.Dice(d, m)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Below we provide query instance as a dict."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# query instance in the form of a dictionary or a dataframe; keys: feature name, values: feature value\n",
    "query_instance = {'age': 22,\n",
    "                  'workclass': 'Private',\n",
    "                  'education': 'HS-grad',\n",
    "                  'marital_status': 'Single',\n",
    "                  'occupation': 'Service',\n",
    "                  'race': 'White',\n",
    "                  'gender': 'Female',\n",
    "                  'hours_per_week': 45}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# generate counterfactuals\n",
    "dice_exp = exp.generate_counterfactuals(query_instance, total_CFs=4, desired_class=\"opposite\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "# visualize the result, highlight only the changes\n",
    "dice_exp.visualize_as_dataframe(show_only_changes=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The counterfactuals generated above are slightly different from those shown in [our paper](https://arxiv.org/pdf/1905.07697.pdf), where the loss convergence condition was made more conservative for rigorous experimentation. To replicate the results in the paper, add an argument *loss_converge_maxiter=2* (the default value is 1) in the *exp.generate_counterfactuals()* method above. For more info, see *generate_counterfactuals()* method in [dice_ml.dice_interfaces.dice_tensorflow.py](https://github.com/interpretml/DiCE/blob/master/dice_ml/dice_interfaces/dice_tensorflow1.py)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Explaining a Pytorch model"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Just change the backend variable to 'PYT' to use DiCE with PyTorch. Below, we use a pre-trained ML model in PyTorch which produces high accuracy comparable to other baselines. For convenience, we include the sample trained model with the DiCE package."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "backend = 'PYT'\n",
    "ML_modelpath = helpers.get_adult_income_modelpath(backend=backend)\n",
    "m = dice_ml.Model(model_path=ML_modelpath, backend=backend)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Instantiate the DiCE class with the new PyTorch model object *m*. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "exp = dice_ml.Dice(d, m)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# query instance in the form of a dictionary; keys: feature name, values: feature value\n",
    "query_instance = {'age': 22,\n",
    "                  'workclass': 'Private',\n",
    "                  'education': 'HS-grad',\n",
    "                  'marital_status': 'Single',\n",
    "                  'occupation': 'Service',\n",
    "                  'race': 'White',\n",
    "                  'gender': 'Female',\n",
    "                  'hours_per_week': 45}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# generate counterfactuals\n",
    "dice_exp = exp.generate_counterfactuals(query_instance, total_CFs=4, desired_class=\"opposite\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# highlight only the changes\n",
    "dice_exp.visualize_as_dataframe(show_only_changes=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## More resources: What's next?\n",
    "\n",
    "DiCE has multiple configurable options and support for different kinds of models. Follow these notebooks to learn more.\n",
    "\n",
    "1. You can constrain the features to vary, weigh the relative importance of different features for computing distance, or specify permitted ranges for each feature. Check out the [Customizing Counterfactuals Notebook](DiCE_with_advanced_options.ipynb). \n",
    "2. You can use it for multi-class classification or regression models. [Counterfactuals for Multi-class Classification and Regression Models Notebook](DiCE_multiclass_classification_and_regression.ipynb).\n",
    "3. Explore the different model-agnostic explanation methods in DiCE. [Model-agnostic Counterfactual Generation Methods](DiCE_model_agnostic_CFs.ipynb).\n",
    "4. You can generate CFs even without access to training data (e.g., for privacy-sensitive data). [DiCE with Private Data Notebook](DiCE_with_private_data.ipynb).\n",
    "5. Feasibility of counterfactuals is an important consideration. You can try out this VAE-based method that adds a CF likelihood term to the loss. [VAE-based Counterfactuals (Pytorch only)](DiCE_getting_started_feasible.ipynb).\n",
    "\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.12"
  },
  "toc": {
   "base_numbering": 1,
   "nav_menu": {},
   "number_sections": true,
   "sideBar": true,
   "skip_h1_title": false,
   "title_cell": "Table of Contents",
   "title_sidebar": "Contents",
   "toc_cell": false,
   "toc_position": {},
   "toc_section_display": true,
   "toc_window_display": true
  },
  "varInspector": {
   "cols": {
    "lenName": 16,
    "lenType": 16,
    "lenVar": 40
   },
   "kernels_config": {
    "python": {
     "delete_cmd_postfix": "",
     "delete_cmd_prefix": "del ",
     "library": "var_list.py",
     "varRefreshCmd": "print(var_dic_list())"
    },
    "r": {
     "delete_cmd_postfix": ") ",
     "delete_cmd_prefix": "rm(",
     "library": "var_list.r",
     "varRefreshCmd": "cat(var_dic_list()) "
    }
   },
   "types_to_exclude": [
    "module",
    "function",
    "builtin_function_or_method",
    "instance",
    "_Feature"
   ],
   "window_display": false
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
