From 0b02599c71797168589809ce4f690ae02b645196 Mon Sep 17 00:00:00 2001 From: Theo <49311372+Advueu963@users.noreply.github.com> Date: Thu, 31 Oct 2024 14:05:53 +0100 Subject: [PATCH] Adds SV Notebook and Reworks Imputers (#257) * Added interpretations for Shapley properties and extend lore of the cooking example * updated stacked-bar default label and title * updated function signatures * updated sv notebook * updated sv notebook * updates games input validator makes the validator a bit more readable and reduces the lines of code * adds NotImplementedError to aggregate_interaction_values * reworks imputer and fixes #264 --------- Co-authored-by: Maximilian --- CHANGELOG.md | 3 +- docs/source/notebooks/sv_calculation.ipynb | 797 +++++++++++++++++- shapiq/approximator/_base.py | 5 + shapiq/datasets/_all.py | 46 +- shapiq/explainer/tabular.py | 14 +- shapiq/games/base.py | 99 +-- shapiq/games/imputer/base.py | 20 +- shapiq/games/imputer/baseline_imputer.py | 5 +- shapiq/games/imputer/conditional_imputer.py | 10 +- shapiq/games/imputer/marginal_imputer.py | 53 +- shapiq/plot/stacked_bar.py | 10 +- shapiq/utils/sets.py | 2 +- tests/test_abstract_classes.py | 2 +- .../tests_explainer/test_explainer_tabular.py | 9 +- tests/tests_games/test_base_game.py | 6 +- tests/tests_imputer/test_baseline_imputer.py | 6 +- .../tests_imputer/test_conditional_imputer.py | 2 +- tests/tests_imputer/test_marginal_imputer.py | 8 +- 18 files changed, 953 insertions(+), 144 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 853f9de7..79fc0bca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,8 @@ - computing metrics now tries to resolve not-matching interaction indices and will throw a warning instead of a ValueError [#179](https://github.com/mmschlk/shapiq/issues/179) - removed the `sample_replacements` parameter from `MarginalImputer` which is now handled by the `BaselineImputer`. Added a DeprecationWarning for the parameter, which will be removed in the next release. - adds `BaselineImputer` [#107](https://github.com/mmschlk/shapiq/issues/107) -- adds `joint_marginal_distribution` parameter to `MarginalImputer` [#261](https://github.com/mmschlk/shapiq/issues/261) +- adds `joint_marginal_distribution` parameter to `MarginalImputer` with default value `True` [#261](https://github.com/mmschlk/shapiq/issues/261) +- fixes a bug with SIs not adding to model prediciton because of wrong value in empty set [#264](https://github.com/mmschlk/shapiq/issues/264) ### v1.0.1 (2024-06-05) diff --git a/docs/source/notebooks/sv_calculation.ipynb b/docs/source/notebooks/sv_calculation.ipynb index a7969431..ba084e1c 100644 --- a/docs/source/notebooks/sv_calculation.ipynb +++ b/docs/source/notebooks/sv_calculation.ipynb @@ -1,63 +1,808 @@ { "cells": [ { + "metadata": {}, "cell_type": "markdown", "source": [ - "# Shapley Value Calculation\n", - "A popular approach to tackle the problem of XAI is to use concepts from game theory in particular cooperative game theory.\n", - "The most popular method is to use the **Shapley Values** named after Lloyd Shapley, who introduced it in 1951 with his work *\"II: The Value of an n-Person Game\"*.\n", + "# Computing Shapley Values with `shapiq`\n", + "A popular approach to tackle the problem of XAI is to use concepts from _cooperative game theory_.\n", + "The most popular method is to use the **Shapley Values** named after Lloyd Shapley, who introduced it in 1951 with his work *\"The Value of an n-Person Game\"* ([Link](https://www.rand.org/content/dam/rand/pubs/papers/2021/P295.pdf)).\n", "\n", - "## Cooperative Game Theory\n", - "Cooperative game theory deals with the study of games in which players/participants can form groups to achieve a collective payoff. More formally a cooperative game is defined as a tuple $(N,\\nu)$ where:\n", + "We divide this notebook into _two_ parts.\n", + "1. **Introduction to Cooperative Game Theory and Shapley Values:** The _first part_ introduces the mathematical background of cooperative game theory and the Shapley value. It further shows how to use `shapiq` to calculate Shapley values for any cooperative game.\n", + "\n", + "2. **Explainable AI with Shapley Value:** The _second part_ discusses how the to use the `shapiq` library for explainable AI (XAI) with Shapley values.\n", + "\n", + "For practitioners the second part might be most important, but we would highly encourage to read the first part to understand the mathematical implications of Shapley values." + ] + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-10-31T12:26:09.126958Z", + "start_time": "2024-10-31T12:26:07.773878Z" + } + }, + "cell_type": "code", + "source": [ + "import shapiq\n", + "\n", + "shapiq.__version__" + ], + "outputs": [ + { + "data": { + "text/plain": [ + "'1.0.1.9001'" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "execution_count": 1 + }, + { + "cell_type": "markdown", + "source": [ + "\n", + "\n", + "## Introduction to Cooperative Game Theory and Shapley Values\n", + "Cooperative game theory deals with the study of games in which players/participants can form groups (also known coalitions) to achieve a collective payoff. More formally a cooperative game is defined as a tuple $(N,\\nu)$ where:\n", "- $N$ is a finite set of players\n", "- $\\nu$ is a characteristic function that maps every coalition of players to a real number, i.e. $\\nu:2^N \\rightarrow \\mathbb{R}$\n", "\n", - "Of particular interest is to find a concept that distributes the payoff of $\\nu(N)$ among the players, as it is assumed that the *grand coalition* $N$ is formed.\n", + "### Example: The Cooking Game\n", + "To illustrate the concept of cooperative games, we consider a simple example of a _cooking game_ you might find in a restaurant.\n", + "The game consists of three cooks, _Alice_, _Bob_, and _Charlie_, who are preparing a meal _together_.\n", + "\n", + "The characteristic function $\\nu$ maps each coalition of players to the quality of the meal:\n", + "| Coalition | Quality |\n", + "|-----------------------|---------|\n", + "| {no cook} | 0 |\n", + "| {Alice} | 2 |\n", + "| {Bob} | 3 |\n", + "| {Charlie} | 4 |\n", + "| {Alice, Bob} | 7 |\n", + "| {Alice, Charlie} | 8 |\n", + "| {Bob, Charlie} | 9 |\n", + "| {Alice, Bob, Charlie} | 15 |\n", + "\n", + "For example, the coalition {Alice, Bob} has a quality of 7, while the coalition {Alice, Bob, Charlie} has a quality of 15.\n", + "If no cooks participate, the quality of the meal is 0 and no meal is prepared.\n", + "\n", + "We can easily model this general form of a cooperative game with `shapiq` by defining a class that inherits from the `shapiq.Game` class.\n", + "Note, a game does not necessarily have to be a subclass of `shapiq.Game` and can also be a simple function that defines the value function $\\nu:2^N \\rightarrow \\mathbb{R}$.\n", + "Methods in `shapiq` can also be used with such functions. However, using the `Game` class provides a more structured way to define the game and its properties.\n", + "It also comes equipped with handy helper methods.\n", + "\n", + "Below we define the `CookingGame` class that models the cooking game." + ], + "metadata": { + "collapsed": false + } + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-10-31T12:26:09.142883Z", + "start_time": "2024-10-31T12:26:09.129872Z" + } + }, + "cell_type": "code", + "source": [ + "import numpy as np\n", + "\n", + "\n", + "class CookingGame(shapiq.Game):\n", + " def __init__(self):\n", + " self.characteristic_function = {\n", + " (): 0,\n", + " (0,): 4,\n", + " (1,): 3,\n", + " (2,): 2,\n", + " (0, 1): 9,\n", + " (0, 2): 8,\n", + " (1, 2): 7,\n", + " (0, 1, 2): 15,\n", + " }\n", + " super().__init__(\n", + " n_players=3,\n", + " player_names=[\"Alice\", \"Bob\", \"Charlie\"], # Optional list of names\n", + " normalization_value=self.characteristic_function[()], # 0\n", + " )\n", + "\n", + " def value_function(self, coalitions: np.ndarray) -> np.ndarray:\n", + " \"\"\"Defines the worth of a coalition as a lookup in the characteristic function.\"\"\"\n", + " output = []\n", + " for coalition in coalitions:\n", + " output.append(self.characteristic_function[tuple(np.where(coalition)[0])])\n", + " return np.array(output)\n", + "\n", + "\n", + "cooking_game = CookingGame()\n", + "cooking_game" + ], + "outputs": [ + { + "data": { + "text/plain": [ + "CookingGame(3 players, normalize=False, normalization_value=0, precomputed=False)" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "execution_count": 2 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "We can now use this cooking game to see the quality of the meal for different coalitions:" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-10-31T12:26:09.158898Z", + "start_time": "2024-10-31T12:26:09.144874Z" + } + }, + "cell_type": "code", + "source": [ + "# query the value function of the game for different coalitions\n", + "coals = np.array([[0, 0, 0], [1, 1, 0], [1, 0, 1], [0, 1, 1], [1, 1, 1]])\n", + "cooking_game(coals)" + ], + "outputs": [ + { + "data": { + "text/plain": [ + "array([ 0, 9, 8, 7, 15])" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "execution_count": 3 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-10-31T12:26:09.174426Z", + "start_time": "2024-10-31T12:26:09.160895Z" + } + }, + "cell_type": "code", + "source": [ + "# query the value function with the names of the players\n", + "coals = [\n", + " (),\n", + " (\"Alice\", \"Bob\"),\n", + " (\"Alice\", \"Charlie\"),\n", + " (\"Bob\", \"Charlie\"),\n", + " (\"Alice\", \"Bob\", \"Charlie\"),\n", + "]\n", + "cooking_game(coals)" + ], + "outputs": [ + { + "data": { + "text/plain": [ + "array([ 0, 9, 8, 7, 15])" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "execution_count": 4 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-10-31T12:26:09.190426Z", + "start_time": "2024-10-31T12:26:09.175421Z" + } + }, + "cell_type": "code", + "source": [ + "# we can automatically get the value of the grand coalition\n", + "print(\"The quality of the meal for the grand coalition is:\", cooking_game.grand_coalition_value)\n", + "\n", + "# similarly we can get the value of the empty coalition\n", + "print(\"The quality of the meal for the empty coalition is:\", cooking_game.empty_coalition_value)" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The quality of the meal for the grand coalition is: 15.0\n", + "The quality of the meal for the empty coalition is: 0.0\n" + ] + } + ], + "execution_count": 5 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "The chef of the restaurant is now interested in, which cook is the most **talented** one and would like to **distribute a bonus** accordingly.\n", + "Of particular interest is to find a concept that _distributes_ the payoff of $\\nu(N)$ among the players, as it is assumed that the *grand coalition* $N$ is formed.\n", "The distribution of the payoff among the players is called a *solution concept*.\n", + "A fair way to distribute the bonus is to use the Shapley values, which we will discuss in the next section.\n", + "\n", "\n", - "## Shapley Values: A Unique Solution Concept\n", - "Given a cooperative game $(N,\\nu)$, the Shapley value is a payoff vector dividing the total payoff $\\nu(N)$ among the players. The Shapley value of player $i$ is denoted by $\\phi_i(\\nu)$ and is defined as:\n", + "### Shapley Values: A Unique Solution Concept\n", + "\n", + "Given a cooperative game $(N,\\nu)$, the Shapley value is a payoff vector dividing the total payoff $\\nu(N)$ among all relevant players.\n", + "The Shapley value of player $i$ is denoted by $\\phi_i(\\nu)$ and is defined as:\n", "$$\n", "\\phi_i(\\nu) := \\sum_{S \\subseteq N \\setminus \\{i\\}} \\frac{|S|!(|N|-|S|-1)!}{|N|!} [\\nu(S \\cup \\{i\\}) - \\nu(S)]\n", - "$$\n", - "and can be interpreted as the average marginal contribution of player $i$ across all possible permutations of the players.\n", + "$$.\n", + "The Shapley value can be interpreted as the average marginal contribution of player $i$ across all possible permutations/orderings of the players.\n", "Its popularity arises from uniquely satisfies the following properties:\n", - "- **Efficiency**: The sum of the Shapley values equals the total payoff, i.e. $\\sum_{i \\in N} \\phi_i(\\nu) = \\nu(N)$\n", - "- **Symmetry**: If two players $i$ and $j$ are such that for all coalitions $S \\subseteq N \\setminus \\{i,j\\}$, $\\nu(S \\cup \\{i\\}) = \\nu(S \\cup \\{j\\})$, then $\\phi_i(\\nu) = \\phi_j(\\nu)$\n", - "- **Additivity**: For a game $(N,\\nu + \\mu)$ based on two games $(N,\\nu)$ and $(N,\\mu)$, the Shapley value of the sum of the games is the sum of the Shapley values, i.e. $\\phi_i(\\nu + \\mu) = \\phi_i(\\nu) + \\phi_i(\\mu)$\n", - "- **Dummy Player**: If for a player $i$ is holds for all coalitions $S \\subseteq N \\setminus \\{i\\}$, $\\nu(S \\cup \\{i\\}) - \\nu(S) = \\nu(\\{i\\})$ then $\\phi_i(\\nu) = \\nu(\\{i\\})$\n", + "- **Efficiency**: The sum of the Shapley values equals the total payoff, i.e. $\\sum_{i \\in N} \\phi_i(\\nu) = \\nu(N)$.\n", + "This property ensures that the total payoff is distributed among all players. For the cooking game, this means that the total bonus is distributed among the cooks and no bonus is lost.\n", + "\n", + "- **Symmetry**: If two players $i$ and $j$ are such that for all coalitions $S \\subseteq N \\setminus \\{i,j\\}$, $\\nu(S \\cup \\{i\\}) = \\nu(S \\cup \\{j\\})$, then $\\phi_i(\\nu) = \\phi_j(\\nu)$.\n", + "Symmetry implies that players with equal contributions receive equal payoffs. If two cooks in the cooking game have the same talent, they should receive the same bonus.\n", + "\n", + "- **Additivity**: For a game $(N,\\nu + \\mu)$ based on two games $(N,\\nu)$ and $(N,\\mu)$, the Shapley value of the sum of the games is the sum of the Shapley values, i.e. $\\phi_i(\\nu + \\mu) = \\phi_i(\\nu) + \\phi_i(\\mu)$.\n", + "Through Additivity we gain the possibility of calculating the Shapley value for smaller games and summing them up to receive the Shapley value for the larger game. \n", + "For the cooking game, this means that the chef can calculate the Shapley values for each pair of cooks and sum them up to get the Shapley values for all three cooks.\n", + "\n", + "- **Dummy Player**: If for a player $i$ it holds that for all coalitions $S \\subseteq N \\setminus \\{i\\}$, $\\nu(S \\cup \\{i\\}) - \\nu(S) = \\nu(\\{i\\})$ then $\\phi_i(\\nu) = \\nu(\\{i\\})$.\n", + "Through the Dummy Player property, players that do not contribute at all receive a value of zero.\n", + "A cook that does not contribute to the meal preparation should not receive a bonus.\n", + "\n", + "#### Using `shapiq.ExactComputer` to Calculate Shapley Values Exactly\n", + "\n", + "With `shapiq` we can easily calculate the Shapley values for any cooperative game such as the cooking game.\n", + "Since the cooking game contains only three players, we will use the `ExactComputer` to calculate the Shapley values exactly." + ] + }, + { + "cell_type": "code", + "source": [ + "from shapiq import ExactComputer\n", + "\n", + "# create an ExactComputer object for the cooking game\n", + "exact_computer = ExactComputer(n_players=cooking_game.n_players, game_fun=cooking_game)\n", "\n", - "## Shapley Values: Cooking Game\n", - "To illustrate the concept of Shapley values, we consider a simple example of a cooking game.\n", - " The game consists of three players(cooks), Alice, Bob, and Charlie, who are cooking a meal together.\n", - " The characteristic function $\\nu$ maps each coalition of players to the quality of the meal." + "# compute the Shapley Values for the game\n", + "sv_exact = exact_computer(index=\"SV\")\n", + "print(sv_exact)" ], "metadata": { - "collapsed": false - } + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-10-31T12:26:09.206425Z", + "start_time": "2024-10-31T12:26:09.191429Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "InteractionValues(\n", + " index=SV, max_order=1, min_order=0, estimated=False, estimation_budget=None,\n", + " n_players=3, baseline_value=0.0,\n", + " Top 10 interactions:\n", + " (0,): 6.0\n", + " (1,): 5.0\n", + " (2,): 3.9999999999999996\n", + " (): 0.0\n", + ")\n" + ] + } + ], + "execution_count": 6 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "With the exact computer we see that the Shapley values for the cooking game are:\n", + "- Player 0 (Alice): 6.0\n", + "- Player 1 (Bob): 5.0\n", + "- Player 2 (Charlie): 4.0\n", + "\n", + "We can also visualize the Shapley values using plots from `shapiq` like the stacked bar plot:" + ] + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-10-31T12:26:09.317976Z", + "start_time": "2024-10-31T12:26:09.208427Z" + } + }, + "cell_type": "code", + "source": [ + "# visualize the Shapley Values\n", + "sv_exact.plot_stacked_bar(\n", + " xlabel=\"Cooks\", ylabel=\"Shapley Values\", feature_names=[\"Alice\", \"Bob\", \"Charlie\"]\n", + ")" + ], + "outputs": [ + { + "data": { + "text/plain": [ + "
" + ], + "image/png": "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qgoWLKjevXsrODhYaWlpunHjhg4cOKB33nlHixcv1ocffqjixYsb/TLgIK6xewL8/vYNFotF3333nXr27Kn58+erWbNm8vDwkCSFhobq1q1b6t+/v9577z0VLlxYycnJiomJUVBQkJEvAci2R93GpGXLlnJ1dVVERIRmzJghSWrTpo399JWXlxejdciXbDabIiMjdefOHRUqVEgdOnSQj4+PNmzYoG7dusnd3V2zZs1Snz59lJSUpGXLlqlcuXIKDAyUq6urfvzxRz399NNGvwxkgcXGv1pPlPT0dA0aNEiFCxfWnDlz7L/sfv9Lb//+/Tp9+rRKliypatWqqUyZMganBrLnwfs7JiZGn376qVJSUuTr66tp06apSJEi+uGHHxQREaHt27erYcOGKleunL777jtt2LBBtWvXNjo+4JAH7/f09HR17dpVFy5c0Lhx49ShQwd5eHgoOjpa7dq102uvvaZ//vOf8vT0VGpqqrZt26aAgAB5eXnJx8fH6JeBLOJUrIlNmTJFvXr1yrDOarVq//799uvl/rPU3b17V3Xq1FHPnj3VvHlzSh1MwWKx6KuvvlL79u1VqFAheXp6KiYmRnXq1NHu3bvVtGlTjRs3To0aNdLRo0dVpUoV7d+/n1KHfMlisSg1NVUuLi5at26dqlWrpunTp2vDhg1KTk5W69at9c033ygyMlJhYWGKi4uTm5ubmjdvrqeeeopSl89xKtaEbDab0tPTVaNGDb300ksPPVezZk1du3ZNN2/eVIkSJezl7uTJk5o3b57ee+89FStWzKD0QM6yWq2Kj4/Xe++9p4kTJ+rdd9+VJKWmpurFF19Uz549deTIETVp0kRJSUlauXKlevfurVKlShmcHMg6Nzc3LVu2TCdOnNCKFSvUsWNH++UGHTp0sJe7zp07KykpSf/85z8z3AoF+Rcjdib04A76HTp0UHBwsH744Qd16dJFkuTu7q7nnntOX331lVauXKkbN25I+u0vvNWrV2vr1q1KSUkxMj6QZVarVdL/faKEJDk5OSk9PV137tyxXzOUlpYmNzc3bdiwQZLsNyRu166dFi9erLJlyxqQHsg5Z8+e1dSpU+Xr6ysnJyetW7dOZcuW1YwZMzKM3K1evVrfffed/ecF+R8jdiazfv16de7cWVu3blWjRo2Unp6umzdvauvWrerWrZvWrFmjv//9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" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "execution_count": 7 }, { "cell_type": "markdown", - "source": [], + "source": [ + "Based on the Shapley Values we can interpret, that the total bonus of 15 should be distributed as follows:\n", + "- Alice should receive a bonus of 6.0\n", + "- Bob should receive a bonus of 5.0\n", + "- Charlie should receive a bonus of 4.0" + ], "metadata": { "collapsed": false } }, { + "metadata": {}, "cell_type": "markdown", - "source": [], + "source": [ + "#### Approximating Shapley Values with any Approximation Method\n", + "While the exact computation of Shapley values is feasible for small games, it becomes computationally expensive for larger games.\n", + "In such cases, we can use approximation methods to estimate the Shapley values.\n", + "`shapiq` provides various approximation methods to calculate Shapley values for larger games.\n", + "Approximators can be found in the `shapiq.approximator` module.\n", + "Here, let's use `shapiq.KernelSHAP` to approximate the Shapley values for the cooking game.\n", + "\n", + "Three players, however, are a bit boring, so let's consider a more interesting Restaurant Game with 10 cooks." + ] + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-10-31T12:26:09.333970Z", + "start_time": "2024-10-31T12:26:09.319971Z" + } + }, + "cell_type": "code", + "source": [ + "from shapiq import powerset\n", + "\n", + "# create a random number generator with a seed\n", + "rng = np.random.default_rng(42)\n", + "\n", + "# food quality is a random number times the number of cooks (more cooks, better quality)\n", + "# contrary to the saying \"too many cooks spoil the broth\"\n", + "quality_dict = {cooks: rng.random() * len(cooks) for cooks in powerset(range(10))}\n", + "\n", + "\n", + "# define the restaurant game as a function\n", + "def restaurant_value_function(coalitions: np.ndarray) -> np.ndarray:\n", + " \"\"\"Defines the worth of a coalition as a lookup in the characteristic function.\"\"\"\n", + " output = []\n", + " for coalition in coalitions:\n", + " output.append(quality_dict[tuple(np.where(coalition)[0])])\n", + " return np.array(output)\n", + "\n", + "\n", + "# we can query the value function for different coalitions\n", + "cooks_to_check = [\n", + " [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],\n", + " [1, 0, 1, 0, 1, 0, 1, 0, 1, 0],\n", + " [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],\n", + "]\n", + "restaurant_value_function(np.array(cooks_to_check))" + ], + "outputs": [ + { + "data": { + "text/plain": [ + "array([0. , 4.73012227, 5.31413812])" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "execution_count": 8 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "The restaurant game is defined as a function `restaurant_value_function` that maps each coalition of cooks to the quality of the meal.\n", + "The total payoff of the grand coalition is: 5.31413812\n", + "\n", + "Now let's approximate the Shapley values with KernelSHAP:" + ] + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-10-31T12:26:09.475046Z", + "start_time": "2024-10-31T12:26:09.335981Z" + } + }, + "cell_type": "code", + "source": [ + "from shapiq import KernelSHAP\n", + "\n", + "# create a KernelSHAP object for 10 players\n", + "approx = KernelSHAP(n=10, random_state=42)\n", + "\n", + "# we can now provide the value function to the KernelSHAP and approximate the Shapley values\n", + "sv_approx = approx(game=restaurant_value_function, budget=100)\n", + "print(sv_approx)\n", + "\n", + "# visualize the Shapley Values\n", + "sv_approx.plot_stacked_bar(\n", + " xlabel=\"Cooks\", ylabel=\"Shapley Values\", feature_names=[f\"Cook {i}\" for i in range(10)]\n", + ")" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "InteractionValues(\n", + " index=SV, max_order=1, min_order=0, estimated=True, estimation_budget=100,\n", + " n_players=10, baseline_value=0.0,\n", + " Top 10 interactions:\n", + " (4,): 1.287790373728892\n", + " (7,): 0.9352475133060242\n", + " (6,): 0.729962062803052\n", + " (3,): 0.7097257904661043\n", + " (2,): 0.6497116688567804\n", + " (8,): 0.6355807791292664\n", + " (1,): 0.34334542810807006\n", + " (5,): 0.13950816787183876\n", + " (9,): 0.02633091520392801\n", + " (0,): -0.14306457769455308\n", + ")\n" + ] + }, + { + "data": { + "text/plain": [ + "
" + ], + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAnYAAAHWCAYAAAD6oMSKAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8hTgPZAAAACXBIWXMAAA9hAAAPYQGoP6dpAABFlElEQVR4nO3de1xUdf7H8fcAAmqCd4HEwFS8JGqaiNpFw9B1zUumtpUuWe2uWhZdVs3ES2VX05Iy3czczVtldl3MKO3ibb2HqZu3QAW8IoIJCN/fH/2YbRKN4TYzx9fz8TiPnHO+c+bzaWbgzZnzPWMzxhgBAADA43m5ugAAAABUDIIdAACARRDsAAAALIJgBwAAYBEEOwAAAIsg2AEAAFgEwQ4AAMAiCHYAAAAW4ePqAtxRUVGRjhw5olq1aslms7m6HAAAcBkzxujMmTMKCQmRl9elj8kR7Epw5MgRhYaGuroMAAAAu7S0NDVu3PiSYwh2JahVq5akX/4HBgQEuLgaAABwOcvOzlZoaKg9n1wKwa4ExR+/BgQEEOwAAIBbKM3pYUyeAAAAsAiCHQAAgEUQ7AAAACyCc+wAuKWioiLl5+e7ugz8P19f39+9zAIA1yPYAXA7+fn5OnDggIqKilxdCv6fl5eXwsPD5evr6+pSAFwCwQ6AWzHGKD09Xd7e3goNDeUokRsovmh7enq6mjRpwoXbATdGsAPgVs6fP6+zZ88qJCRENWrUcHU5+H8NGjTQkSNHdP78eVWrVs3V5QC4CP4UBuBWCgsLJYmP/NxM8fNR/PwAcE8EOwBuiY/73AvPB+AZCHYAAAAWwTl2ADxCamqqjh8/XmWPV79+fTVp0qTKHq8kCxYs0EMPPaSsrCyX1gHAcxDsALi91NRUtYpoqbPnfq6yx6zhX1279ux2KtylpaUpISFBSUlJOn78uIKDgzVgwABNmjRJ9erVq8Rqnbd8+XLNmTNHmzdv1smTJ7V161a1b9/e1WUBKCeCHQC3d/z4cZ0997NeqHO9mvrUrvTH238+S4+d+kbHjx8vdbDbv3+/oqOj1aJFCy1evFjh4eHauXOnHnvsMf373//W+vXrVbdu3RLvm5+fX2mTRQoKCkqcxZqbm6vu3btryJAhuu+++yrlsQFUPYIdAI/R1Ke22vi615GvYqNHj5avr68+//xzVa9eXZLUpEkTdejQQVdffbWeeOIJvf7665KksLAwjRw5Uj/++KNWrFihQYMGacGCBVqwYIEmTZqk48ePKzY2Vt27d7/gcT788ENNmTJFP/zwg0JCQjRixAg98cQT8vH55ce5zWbTa6+9pn//+99KTk7WY489psmTJ1+wn7vvvluSdPDgwcr5HwLAJZg8AQDldPLkSa1cuVKjRo2yh7piQUFBuvPOO7V06VIZY+zrX3zxRbVr105bt27Vk08+qQ0bNmjkyJEaM2aMtm3bph49euipp55y2Nc333yj4cOHa+zYsfrhhx/0xhtvaMGCBXr66acdxk2ePFkDBw7U999/r3vuuafyGgfgdjhiBwDl9OOPP8oYo1atWpW4vVWrVjp16pSOHTumhg0bSpJ69uypRx55xD7mySefVO/evfX4449Lklq0aKG1a9cqKSnJPmbKlCkaN26cRowYIUlq2rSppk2bpscff1wJCQn2cX/6058UFxdX4X0CcH8csQOACvLrI3K/p1OnTg63d+3apaioKId10dHRDre3b9+uqVOn6oorrrAv9913n9LT03X27NmL7hvA5YMjdgBQTs2aNZPNZtOuXbs0cODAC7bv2rVLderUUYMGDezratas6fTj5OTkaMqUKRo0aNAF2/z9/cu1bwDWQLADgHKqV6+eevXqpddee00PP/yww3l2GRkZeueddzR8+PBLfntDq1attGHDBod169evd7h97bXXas+ePWrWrFnFNgDAMgh2AFABZs+era5duyo2NlZPPfWUw+VOrrzyygsmOPzWgw8+qG7duunFF19U//79tXLlSofz6yRp0qRJ+uMf/6gmTZpo8ODB8vLy0vbt25WSknLBRIvfc/LkSaWmpurIkSOSpD179kj6ZbJHUFCQU/sC4D4IdgA8xv7zWW77OM2bN9emTZuUkJCgIUOG6OTJkwoKCtKAAQOUkJBw0WvYFevSpYvmzZunhIQETZo0STExMZo4caKmTZtmHxMbG6tPPvlEU6dO1XPPPadq1aqpZcuWuvfee52u96OPPnKYYDFs2DBJUkJCQomXRwHgGWzGmbN9LxPZ2dkKDAzU6dOnFRAQ4OpygMvKuXPndODAAYWHh9vPG/OUb56wspKeFwBVw5lcwhE7AG6vSZMm2rVn92X3XbEA4CyCHQCP0KRJE4IWAPwOrmMHAABgEQQ7AAAAiyDYAXBLzOtyLzwf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KSsrS2fPnq3Y6gAAAFBqTk+euOGGG7Rq1Sq1bdtWt99+u8aOHasvv/xSq1at0s0331wZNQIAAKAUSh3sUlJSdM0112j27Nk6d+6cJOmJJ55QtWrVtHbtWt12222aOHFipRUKAACASyt1sIuMjNR1112ne++9V8OGDZMkeXl5ady4cZVWHAAAAEqv1OfYrVmzRm3atNEjjzyi4OBgjRgxQt98801l1gYAAAAnlDrYXX/99Zo/f77S09P16quv6uDBg7rxxhvVokULPffcc8rIyKjMOgEAAPA7nJ4VW7NmTcXFxWnNmjX673//q9tvv12JiYlq0qSJbr311sqoEQAAAKVQru+KbdasmSZMmKCJEyeqVq1a+vTTTyuqLgAAADipTN8VK0lff/215s+fr/fff19eXl4aMmSIRo4cWZG1AQAAwAlOBbsjR45owYIFWrBggfbu3auuXbvqlVde0ZAhQ1SzZs3KqhEAAAClUOpg16dPH33xxReqX7++hg8frnvuuUcRERGVWRsAAACcUOpgV61aNb333nv64x//KG9v78qsCQAAAGVQ6mD30UcfVWYdAAAAKKdyzYoFAACA+yDYAQAAWATBDgAAwCKcDna5ubmVUQcAAADKyelg16hRI91zzz369ttvK6MeAAAAlJHTwe5f//qXTp48qZ49e6pFixZ69tlndeTIkXIVkZiYqLCwMPn7+ysqKkobN2686NibbrpJNpvtgqVv3772MX/+858v2N67d+9y1QgAAODunA52AwYM0IoVK3T48GH99a9/1aJFi3TVVVfpj3/8o5YvX67z5887tb+lS5cqPj5eCQkJ2rJli9q1a6fY2FgdPXq0xPHLly9Xenq6fUlJSZG3t7duv/12h3G9e/d2GLd48WJnWwUAAPAoZZ480aBBA8XHx2vHjh2aMWOGvvjiCw0ePFghISGaNGmSzp49W6r9zJgxQ/fdd5/i4uLUunVrzZkzRzVq1ND8+fNLHF+3bl0FBQXZl1WrVqlGjRoXBDs/Pz+HcXXq1ClrqwAAAB6hzMEuMzNTzz//vFq3bq1x48Zp8ODBSk5O1ksvvaTly5drwIABv7uP/Px8bd68WTExMf8ryMtLMTExWrduXanqePPNNzVs2LALvqt29erVatiwoSIiIvS3v/1NJ06ccKo/AAAAT1Pqb54otnz5cr311ltauXKlWrdurVGjRumuu+5S7dq17WO6du2qVq1a/e6+jh8/rsLCQjVq1MhhfaNGjbR79+7fvf/GjRuVkpKiN99802F97969NWjQIIWHh2vfvn2aMGGC+vTpo3Xr1pX4dWh5eXnKy8uz387Ozv7dxwYAAHA3Tge7uLg4DRs2TN99952uu+66EseEhIToiSeeKHdxv+fNN99U27Zt1blzZ4f1w4YNs/+7bdu2ioyM1NVXX63Vq1fr5ptvvmA/06dP15QpUyq9XgAAgMrk9Eex6enpeuONNy4a6iSpevXqSkhI+N191a9fX97e3srMzHRYn5mZqaCgoEveNzc3V0uWLNHIkSN/93GaNm2q+vXra+/evSVuHz9+vE6fPm1f0tLSfnefAAAA7sbpYFejRg3t27dPEydO1B133GGfvfrvf/9bO3fudGpfvr6+6tixo5KTk+3rioqKlJycrOjo6Eve991331VeXp7uuuuu332cQ4cO6cSJEwoODi5xu5+fnwICAhwWAAAAT+N0sFuzZo3atm2rDRs2aPny5crJyZEkbd++vVRH6X4rPj5e8+bN09tvv61du3bpb3/7m3JzcxUXFydJGj58uMaPH3/B/d58800NGDBA9erVc1ifk5Ojxx57TOvXr9fBgweVnJys/v37q1mzZoqNjXW6PgAAAE/h9Dl248aN01NPPaX4+HjVqlXLvr5nz56aPXu20wUMHTpUx44d06RJk5SRkaH27dsrKSnJPqEiNTVVXl6O+XPPnj369ttv9fnnn1+wP29vb+3YsUNvv/22srKyFBISoltuuUXTpk2Tn5+f0/UBAJyzp3Gcq0twSsSht1xdAlBhnA5233//vRYtWnTB+oYNG+r48eNlKmLMmDEaM2ZMidtWr159wbqIiAgZY0ocX716da1cubJMdQAAAHgypz+KrV27ttLT0y9Yv3XrVl155ZUVUhQAAACc53SwGzZsmP7+978rIyNDNptNRUVF+u677/Too49q+PDhlVEjAAAASsHpj2KfeeYZjR49WqGhoSosLFTr1q1VWFioP/3pT5o4cWJl1AjgEjifCQBQzOlg5+vrq3nz5unJJ59USkqKcnJy1KFDBzVv3rwy6gMAAEApOR3sijVp0kRNmjSpyFoAAABQDqUKdvHx8aXe4YwZM8pcDAAAAMquVMFu69atpdqZzWYrVzEAAAAou1IFu6+++qqy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/vWvf5nQ0FCzevVqV5ZXIc6cOWN69uxpHnzwQfu6qVOnmgceeMAY47nvNWOMycrKMl27djWPP/64MeaX12RSUpKx2WwmPDzcbNmyxb7eU3DEroIUFhbq2LFjuu666yRJ3t7e6tmzp+bOnav27durR48eOnbsmEem/7Vr16pOnTpasWKF/vCHP2jlypWaNWuWxx+5Kygo0L/+9S916dJFjz76qPz9/VWtWjVNmDBBu3fvVmpqqqtLLLOCggItXbpUN998syZMmCBjjLy9vRUeHq6UlBQNGzZMjz76qI4ePerqUsslOjpaHTt21KhRo9SvXz8tWLBAc+bM0aOPPqqkpCQVFha6ukSnFBQUaNasWcrLy9OkSZMkSb1799Y999yjQYMGacCAAbr33nt18uRJF1fqvOKjVqmpqQoJCZH0y/P3+eefa//+/frPf/6jNm3a6Ntvv5XkOUfubDabMjMzNXToUI0dO1bTp0+XJHXs2FE+Pj5as2aNJM/ppyRnzpxRenq6YmNj7etOnTql5ORkdevWTd26ddP69esleV6fp06d0qlTpzRw4EBJvxxVvuGGG9S9e3dVr15dffv2tf/u9hguDpaW0rt3bzNo0KAL1n///fema9eu5s9//rMpKChwQWXlk5WVZZKSkuy3ExISTIcOHcy4cePMkSNHjDGeeeSusLDQvPDCC+a1115zWL97925zxRVXmJ07d7qosorx/fffm+3bt9tvT5s2zVSrVs08/vjj5v777zfdu3c3Xbp0MWfOnHFhleWzf/9+06JFC3PkyBFz+PBhM3XqVFOnTh1js9nMTz/9ZIzxrL+0jTFmz549pmfPniY0NNR069bN9O/f36xdu9ZkZGSY2bNnm6ioKDNmzBiP/FlizC+vw7Zt25pFixaZPn36mNOnT5uCggJz7tw5c/fdd5tGjRqZkydPurpMp/z8888mOTnZfrv45+H06dNN7dq1zb59+1xVWoU4e/as6dq1q7nlllvM9u3bzfjx442/v7955ZVXzMKFC82dd95p6tSpY9LS0lxdqtMOHDhgWrVqZebNm2dft3fvXnP11Veb999/33To0ME88sgjxhjP+T1HsKtAiYmJpmPHjuaf//ynOX/+vMO2Z555xkRGRprTp0+7qLqKNXnyZHu4K/5YdubMmebEiRMursw5586ds/+7OACcOHHCNG/e3Bw8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2sd98MEHGjt2rB555BGlpKToL3/5i+Li4vTVV19dsE9jjB544AEtXLhQ33zzjSIjI7Vp0yY9+OCDmjp1qvbs2aOkpCTdcMMNldo7AM9nM8YYVxcBAO5i48aNioqK0vLlyzVw4MASx6xatUp9+vTRgQMHFBoaKkn64Ycf1KZNG23cuFHXXXedunXrpjZt2mju3Ln2+w0ZMkS5ubn69NNPJf0yeeLdd9/VBx98oK1bt2rVqlW68sorJUnLly9XXFycDh06pFq1alVy1wCsgiN2APArpflbd9euXQoNDbWHOklq3bq1ateurV27dtnHdOvWzeF+3bp1s28v9vDDD2vDhg36+uuv7aFOknr16qWrrrpKTZs21d1336133nlHZ8+eLU9rAC4DBDsA+JXmzZvLZrNV2QSJXr166fDhw1q5cqXD+lq1amnLli1avHixgoODNWnSJLVr105ZWVlVUhcAz0SwA4BfqVu3rmJjY5WYmKjc3NwLtmdlZalVq1ZKS0tTWlqaff0PP/ygrKwstW7dWpLUqlUrfffddw73/e677+zbi916661atGiR7r33Xi1ZssRhm4+Pj2JiYvT8889rx44dOnjwoL788suKahWABXG5EwD4jcTERHXr1k2dO3fW1KlTFRkZqfPnz2vVqlV6/fXX9cMPP6ht27a68847NXPmTJ0/f16jRo3SjTfeqE6dOkmSHnvsMQ0ZMkQdOnRQTEyMPv74Yy1fvlxffPHFBY83cOBA/fOf/9Tdd98tHx8fDR48WJ988on279+vG264QXXq1NFnn32moqIiRUREVPX/DgAehGAHAL/RtGlTbdmyRU8//bQeeeQRpaenq0GDBurYsaNef/112Ww2ffjhh3rggQd0ww03yMvLS71799arr75q38eAAQM0a9Ysvfjiixo7dqzCw8P11ltv6aabbirxMQcPHqyioiLdfffd8vLyUsOGDbV8+XJNnjxZ586dU/PmzbV48WK1adOmiv4vAPBEzIoFAACwCM6xAwAAsAiCHQAAgEUQ7AAAACyCYAcAAGARBDsAAACLINgBAABYBMEOAADAIgh2AAAAFkGwAwAAsAiCHQAAgEUQ7AAAACyCYAcAAGAR/wccLjmVntAG2QAAAABJRU5ErkJggg==" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "execution_count": 9 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "From this, plot we can see that the Shapley value for cook number 4 is the highest, followed by cook 7, etc.\n", + "\n", + "This concludes the first part of the notebook.\n", + "In the next part, we will discuss how to use Shapley values for Explainable AI (XAI) with `shapiq`." + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Explainable AI with Shapley Values\n", + "In the previous section, we discussed the Shapley values and how to calculate them for cooperative games.\n", + "In this section, we will discuss how to use Shapley values for Explainable AI (XAI) with `shapiq`.\n", + "\n", + "### Shapley Values for Local Feature Attribution\n", + "In the case of machine learning models and local feature attribution, a cooperative game is usually defined as the tuple $(N,\\nu)$ where:\n", + "\n", + "- $N$ is a finite set of features $N = \\{1, 2, \\ldots, d\\}$\n", + "- $\\nu$ is the output of a model $f$ given a local input $x$ restricted on a subset of features $S \\subseteq N$, i.e. $\\nu(S) = f_S(x)$\n", + "\n", + "The goal in XAI here is to explain the output of a machine learning provided with access to all feature values $N$ of a data point $x$.\n", + "The Shapley values can be used to fairly distribute the output of the model among the features (similarly to the cooks in the cooking game in the previous section).\n", + "To do so, the model needs to be evaluated on partial inputs $S \\subseteq N$ to determine the contribution of each feature to the model output.\n", + "\n", + "However, naturally, machine learning models operate on a _complete_ feature space $f: \\mathcal{X}^d \\rightarrow \\mathbb{R}$ and not on subsets $S \\subseteq N$ of features.\n", + "Most machine learning models cannot be evaluated with missing features.\n", + "Therefore, we need to define a restricted model $f_S: \\mathcal{X}^{|S|} \\rightarrow \\mathbb{R}$ that operates only on the subset of features $S$.\n", + "In practice this is achieved by imputing missing features with values sampled from a background distribution.\n", + "\n", + "Different imputation methods exist and are implemented in `shapiq`:\n", + "- `shapiq.BaselineImputer`: Impute missing features with a constant value (e.g. 0) or a summary statistic (e.g. mean/mode).\n", + "\n", + "- `shapiq.MarginalImputer`: Impute missing features $X^{\\bar{S}}$ with the marginal feature distribution $X^{\\bar{S}} \\sim P(X^{\\bar{S}})$.\n", + "\n", + "- `shapiq.ConditionalImputer`: Impute missing features $X^{\\bar{S}}$ with the conditional feature distribution $X^{\\bar{S}} \\sim P(X^{\\bar{S}}|X^S)$. Note that estimating the conditional distribution can be challenging and often only approximate distributions are used.\n", + "\n", + "To illustrate how to use `shapiq` for XAI, we will first train and evaluate a simple Random Forest model on the California Housing dataset.\n", + "Then we will use the Shapley values to explain the model's predictions for a single data point.\n", + "Finally, we will visualize the Shapley values to interpret the model's decision." + ], "metadata": { "collapsed": false } }, { + "metadata": { + "ExecuteTime": { + "end_time": "2024-10-31T12:26:12.753958Z", + "start_time": "2024-10-31T12:26:09.478046Z" + } + }, "cell_type": "code", - "execution_count": null, + "source": [ + "from sklearn.ensemble import RandomForestRegressor\n", + "from sklearn.model_selection import train_test_split\n", + "\n", + "data, targets = shapiq.datasets.load_california_housing()\n", + "feature_names = list(data.columns)\n", + "n_features = len(feature_names)\n", + "print(\"Feature names:\", feature_names)\n", + "\n", + "# split the data into training and test set\n", + "x_train, x_test, y_train, y_test = train_test_split(\n", + " data.values, targets.values, test_size=0.2, random_state=42\n", + ")\n", + "\n", + "# train a Random Forest model\n", + "rf = RandomForestRegressor(n_estimators=30, random_state=42)\n", + "rf.fit(x_train, y_train)\n", + "\n", + "# evaluate the model\n", + "test_score = rf.score(x_test, y_test)\n", + "print(f\"Test R^2 score: {test_score:.4f}\")" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Feature names: ['MedInc', 'HouseAge', 'AveRooms', 'AveBedrms', 'Population', 'AveOccup', 'Latitude', 'Longitude']\n", + "Test R^2 score: 0.8001\n" + ] + } + ], + "execution_count": 10 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "The model achieves an $R^2$ score of about 0.80 on the test set.\n", + "\n", + "Let's select a single data point from the test set and explain the model's prediction using the Shapley values." + ] + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-10-31T12:26:12.799497Z", + "start_time": "2024-10-31T12:26:12.754950Z" + } + }, + "cell_type": "code", + "source": [ + "# select a single data point from the test set\n", + "instance_id = 2\n", + "x_explain = x_test[instance_id]\n", + "y_explain = rf.predict([x_explain])\n", + "print(f\"Data point to explain {instance_id} is predicted as {y_explain[0]}\")\n", + "avg_prediction = np.mean(rf.predict(x_test))\n", + "print(f\"The average prediction of the model on the test set is {avg_prediction}\")" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Data point to explain 2 is predicted as 4.955573000000001\n", + "The average prediction of the model on the test set is 2.0660426668281655\n" + ] + } + ], + "execution_count": 11 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "The data point we want to explain is predicted to have a value of around **4.9**.\n", + "This is a lot higher than the model's average prediction of around **2.1** for all data points in the test set.\n", + "\n", + "### Using `shapiq.TabularExplainer` to Compute Shapley Values\n", + "With the Shapley values, we can see why the model predicts such a high value in comparison to the average prediction. For this we will use the `shapiq.TabularExplainer` class:" + ] + }, + { "metadata": { - "collapsed": true + "ExecuteTime": { + "end_time": "2024-10-31T12:26:13.127714Z", + "start_time": "2024-10-31T12:26:12.801496Z" + } }, - "outputs": [], - "source": [] + "cell_type": "code", + "source": [ + "# create explainer\n", + "explainer = shapiq.TabularExplainer(\n", + " model=rf, # insert the model to be explained\n", + " data=x_test, # insert the background data\n", + " imputer=\"marginal\", # specify the imputation strategy\n", + " index=\"SV\", # define the index to be used SV for the Shapley Value\n", + " max_order=1, # define the maximum order of interactions (1 for SV)\n", + " sample_size=100, # define how well the background distribution is estimated\n", + " random_state=42, # optionally set a random state for reproducibility\n", + ")\n", + "sv = explainer.explain(x_explain, budget=2**n_features)\n", + "print(sv)\n", + "print(\"\\nSum of Shapley values:\", sum(sv.values) + sv.baseline_value)\n", + "print(\"Model prediction:\", y_explain[0])" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "InteractionValues(\n", + " index=SV, max_order=1, min_order=0, estimated=False, estimation_budget=256,\n", + " n_players=8, baseline_value=2.0660426668281655,\n", + " Top 10 interactions:\n", + " (5,): 0.9779035114535043\n", + " (7,): 0.736985282046726\n", + " (1,): 0.4837160431773961\n", + " (6,): 0.35934460935202045\n", + " (0,): 0.14960690717717098\n", + " (3,): 0.10402609944915396\n", + " (2,): 0.04175939017311222\n", + " (4,): 0.036188481328273014\n", + " (): 0.0\n", + ")\n", + "\n", + "Sum of Shapley values: 4.955572990985522\n", + "Model prediction: 4.955573000000001\n" + ] + } + ], + "execution_count": 12 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "The shapley values show that all features contribute positively to the model's prediction.\n", + "Notice that the sum of the Shapley values plus the baseline value equals the model's prediction, which is a property of the efficiency property of the Shapley values.\n", + "\n", + "We can also visualize the Shapley values using different kind of visualizations implemented in `shapiq`.\n", + "Here, let's use `plot_force` method of the `shapiq.InteractionValues` class to visualize the Shapley values:" + ] + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-10-31T12:26:13.786793Z", + "start_time": "2024-10-31T12:26:13.128705Z" + } + }, + "cell_type": "code", + "source": [ + "# visualize the Shapley Values\n", + "sv.plot_force(feature_names=feature_names, feature_values=x_explain, show=True)" + ], + "outputs": [ + { + "data": { + "text/plain": [ + "
" + ], + "image/png": "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PVqwYIGcTqdGjx4tyURv33jjDcXFxenSSy9VTEyMsrOz9fXXX7s838cff6wtW7Zo+PDhGjNmjLKzs7Vu3TplZmbqe9/7nsLDwz3mc+DAgfrss8+0c+dOTZgwweW+nTt3qlevXtWBjF27dqmsrEwjR45UXFycTpw4oXXr1ik3N1e33HJLs7xv+fn5+uc//ymHw6HRo0crPj5e+/bt09y5c1VSUqKxY8c2y/MAAAAAAAAgNKWnpysxMVErVqzQmDFj1LlzZ8XHx+vYsWNyOBzq2LGjy/bTp0/XoUOHNGfOHN11110KCwvThg0bdODAAd14443VQYgqHTt21O7du/35koA6CERAkpSSkqLZs2dLkkaPHq3o6GitX79e48ePV/v27SVJd955p8tIgtGjR+vNN9/U6tWrqwMRx44dU1FRkb7zne+oU6dO1dtefvnl1dePHj2qTZs26aabbtLgwYOr07t3764333xTu3btckmvLTk5WV26dKkTiDhx4oSysrJcRipMnTrVJb/p6elKTU3VkiVLlJOTo+Tk5Ka8VS6++OILOZ1O/fCHP1RcXJwkaeTIkfrggw+0dOlSpaenNzj6AgAAAAAAAC1b165dVVFRoRUrVigtLU0DBgyQJG3dulWxsbGKjo522T4mJkYzZszQG2+8oZUrV2rw4MFavHix+vfvryFDhtTZf6tWrVRYWKiCggLFx8f75TUB7lgjApJUHUioMmbMGEnSvn37qtNqN6gXFxersLBQaWlpysrKUnFxsaSaeez27t2riooKj8+1c+dOxcTEqGfPniosLKy+dOrUSVFRUTp06FCDeR04cKBOnjyp8+fPu+wzIiJC/fv395jf0tJSFRYWqmvXrnI6ncrIyGjwObzhdDq1a9cu9e3bV5JcXkvv3r1VXFzcLM8DAAAAAACAlqeoqMhlzYjaevXqpZEjR2rZsmX6z3/+o4iICF1//fUet62aPaSwsNCyvAKNYUQEJEmpqakut1u1aiWHw6Hs7OzqtKNHj2rp0qU6duyYysrKXLYvKSlRTExMddR26dKlWr16tbp3767+/ftr8ODB1etNnD9/XsXFxXr66ac95qVqcZ76DBw4UJ9++ql27typiRMnyul0aufOnerdu7dLhDgnJ0dffvml9uzZo6Kiojr5vViFhYUqLi7Wxo0btXHjxia9FgAAAAAAAKAprrrqKu3evVuZmZm66aab6h3t4HQ6/ZwzoC4CEfDI4XC43D5//rxef/11tWnTRtOmTVNycrLCw8O1b98+rV69urpCczgcuuWWW3T8+HHt2bNHBw4c0Ny5c7V69Wp9//vfV1RUlJxOp+Lj43XTTTd5fO6qKY7qk5iYqLS0tOpAxPHjx5WTk6Mrr7yyepvKykq9/vrrKioq0oQJE9SmTRtFRUUpNzdXc+bMaVIFXFlZ6XK7ah9Dhgyps6h1lapprQAAAAAAAABfxMbG1ulcW1tGRkZ1J9jTp0/Xu13VTCaNtbkBViIQAUkm0NCqVSuX206nUykpKZLMVEvl5eWaPXu2y9oK9U2j1KVLF3Xp0kVXXHGFtm/frg8//FA7duzQiBEj1KpVKx08eFBdu3Zt8voJAwcO1IIFC3T27Fnt3LlTkZGR1VMkSabyPXfunGbNmqWhQ4dWpx84cKDRfVcNV6uqpKvk5OS43I6Li1N0dLScTqd69uzZpNcBAAAAAAAAeNKmTRtt375dxcXFdaZoKi0t1dy5c9W2bVt17dpVX331lfr376/OnTvX2U9WVpbi4uJYHwIBxRoRkCStW7fO5fbatWslSb1795ZUM0Ki9kiC4uJibdmyxeVxRUVFdUYbdOjQQZJUXl4uyQQRKisrtXz58jr5qKysrBMA8GTAgAEKCwvTjh07tHPnTvXt21dRUVHV93vKr9PprH5dDYmOjlZcXJyOHDnikr5+/XqX22FhYbrkkku0a9cuj1FnpmUCAAAAAABAUzW01unnn3+unJwczZo1S9OmTVNKSormzJlT3f5WW0ZGhrp27eqPLAP1YkQEJEnZ2dl655131Lt3bx07dkzbtm3T4MGDq4MIvXr1Unh4uN555x2lp6ertLRUmzZtUnx8vPLy8qr3s3XrVq1fv179+/dXamqqSkpKtGnTJkVHR6tPnz6SpO7du2vkyJFasWKFMjMz1atXL4WFhen8+fPauXOnpk+frgEDBjSY3/j4eHXv3l2rV69WSUmJBg0a5HJ/mzZtlJqaqsWLFysvL0/R0dHatWuXV0EOSRoxYoRWrlypefPmqVOnTjpy5IjOnTtXZ7upU6fq8OHD+sc//qH09HS1bdtWRUVFysjI0MGDB/XII4949XwAAAAAAABAbd26dVNcXJwOHjyoHj16VKcfOnRI69ev1+TJk9WxY0dJ0owZM/Taa6/pyy+/dJm+vKCgQKdOndKoUaP8nn+gNkZEQJJ08803Kzw8XJ9//rn27dun0aNHa8aMGdX3t2nTRrfccoskafHixdqwYYPS09M1ZswYl/2kpaWpU6dO2rFjhxYtWqSvvvpKqampuuOOO1ymfrruuut0/fXXq6CgQEuWLNGSJUt06NAhDRkyxOsI7aBBg1RSUuIS5KgSHh6u2bNnq0OHDlqxYoWWLl2q1q1ba9asWV7te/LkyRoxYoR27dqlzz77TJWVlbrtttvqbJeQkKC7775bw4cP19dff62FCxdqzZo1Kioq0tSpU716LgAAAAAAAMBdeHi4Bg8erJ07d1anlZSUaO7cuerQoYMmTZpUnZ6WlqaxY8dq1apVOn78eHX6119/rfDwcA0cONCveQfcOZwsmw4AAAAAAAAAtpOVlaW///3vuu2225q0RulLL72k7t276+qrr7Ygd4D3GBEBAAAAAAAAADbUqlUrDR8+XCtXrvT5sfv379f58+c1ceJEC3IG+IYREQAAAAAAAAAAwDKMiAAAAAAAAAAAAJYhEAEAAAAAAAAAACxDIAIAAAAAAAAAAFiGQAQAAAAAAAAAALAMgQgAAAAAAAAAAGAZAhEAAAAAAAAAAMAyBCIAAAAAAAAAAIBlCEQAAAAAAAAAAADLEIgAAAAAAAAAAACWIRABAAAAAAAAAAAsQyACAAAAAAAAAABYhkAEAAAAAAAAAPjRjBkz5HA4qi9Tpkzx6fFOp1NvvfWWpk6dqtatWysqKkqdOnXSN77xDa1evbrBxxYXF+v555/XpEmT1KZNG0VFRalDhw4aN26cfvnLX6qwsPAiXhngmcPpdDoDnQkAAAAAAAAAaAneeOMN3X777S5pkydP1tKlS716fHl5uW655RZ99NFHHu93OBx6/vnn9ZOf/KTOffv379e1116rvXv31rv/Y8eOqUuXLl7lBfBWRKAzAAAAAAAAAAAtwcmTJ3X//fdf1D7+8pe/uAQhrrvuOo0aNUrLly/XkiVL5HQ69cADD2j06NEaM2ZM9XZ5eXmaPn269u/fL0lKTk7WrFmz1KNHD5WXl+vkyZPasGGDwsPDLyp/gCcEIgAAAAAAAADAD+655x5lZWWpW7duat26tTZv3uzzPv79739XX7/00kv18ccfS5IqKys1ePBg7dq1S5WVlXriiSc0d+7c6m2feeaZ6iDEwIEDtXz5cqWmpl7kKwK8wxoRAAAAAAAAAGCx1157TfPnz5fD4dCrr76qpKSkJu3n4MGD1deHDh1afT0sLEyDBg2qvr148WKVl5dX364dwJg8ebK+/e1vq1OnToqNjdXAgQP1+OOPq7i4uEl5AhpDIAIAAAAAAAAALHTixAk9+OCDkqQf/vCHuuKKK5q8r+Tk5Orr27dvr77udDq1c+fO6tvFxcXVIyBOnDiho0ePVt/3wgsvaNGiRcrIyFBxcbF27dql3/zmN7rssstUVFTU5LwB9SEQAQAAAAAAAAAWuvvuu5Wdna0ePXroqaeeuqh9XX/99dXXly9frhkzZuj3v/+9pk2b5hKIkKSsrCxJUkZGRp39jB49Wo8++qimTp1anbZmzRo9/vjjF5U/wBPWiAAAAAAAAAAAi7z66qtatGiRHA6H/v3vfyshIeGi9vfEE09o6dKl1VM0zZs3T/PmzfO4bVRUlCSptLTUJb1jx45avny5oqOj5XQ6NWrUKG3cuFGS9Oabb+oPf/jDReURcMeICAAAAAAAAACwQHFxsR566CFJ0n333afJkydf9D7bt2+vDRs26Oc//7n69OmjqKgotW/fXtdee60eeeQRl207deokSUpJSXFJHzdunKKjoyVJDofDJV9Hjx5VWVnZRecTqI1ABAAAAAAAAABYoLi4WDk5OZKkv/3tb3I4HNWXZcuWVW+3bNkyORwOTZkyxav9tmrVSk899ZT27t2rkpISZWZmav78+crPz6/eJi0tTR07dpQk9enTpzrw4InT6ay+HhERoYgIJtJB8yIQAQAAAAAAAAA28tprr7kELWqrrKxUdnZ2ncesWLFCr7zySvXt7373u9XXIyMjdeWVV1bfXrNmjct0TStWrKi+np6eXuc5gYtFaAsAAAAAAAAALBAVFaWbbrrJ433Lli3T2bNnJUlt2rTR5MmTNXDgwEb3WVhYqA4dOuiqq67SgAEDFB0dre3bt2vu3LmqrKyUJPXs2VMPPvigy+N+8YtfaOHChaqsrNTJkyc1adIkTZ8+XatWrdKGDRuqt/vpT3/a1JcL1MvhrD3uBrCxgoICOZ1OORwOxcfHBzo7AAAAAAAAQJNNmTKlenqmyZMna+nSpdX3vfbaay4jGmo34ebn5ysxMbHe/fbq1UsLFy5U375969z37LPPVq9Z4clPfvIT/fWvf/XlZQBeYWomBA2n01l9AQAAAAAAAFqimJgYPfLIIxo7dqzat2+vyMhItWrVShMmTNAzzzyj7du3ewxCSNKDDz6olStX6sYbb1T79u0VERGh1NRUTZs2TXPnziUIAcswIgJBIz8/v3pEREJCQqCzAwAAAAAAAADwAiMiAAAAAAAAAACAZQhEAAAAAAAAAAAAyxCIAAAAAAAAAAAAliEQAQAAAAAAAAAALEMgAgAAAAAAAAAAWIZABAAAAAAAAAAAsAyBCAAAAAAAAAAAYBkCEQAAAAAAAAAAwDIEIgAAAAAAAAAAgGUiAp0BAAAAAAAAAID3CgsL5XQ65XA4FBcXF+jsAI0iEAEAAAAAAAAAQaSysrI6EAEEA6ZmAgAAAAAAAAAAliEQAY+efPJJjRo1SomJiWrXrp1mzpypPXv2NPq4999/X/3791dMTIwGDx6shQsX+iG3gL01pTy99tprcjgcLpeYmBg/5RiwrxdffFFDhgxRUlKSkpKSNG7cOC1atKjBx3BuAurna5ni/AR4549//KMcDoceeOCBBrfjHAU0zpvyxPkJqN9jjz1Wp3z079+/wcdwfoIVCETAo2XLlunee+/VmjVr9Nlnn6msrExXXXWVCgoK6n3MqlWrNHv2bN11113avHmzZs6cqZkzZ2rHjh1+zDlgP00pT5KUlJSkjIyM6suRI0f8lGPAvrp06aI//vGP2rhxozZs2KDLL79cM2bM0M6dOz1uz7kJaJivZUri/AQ0Zv369Xr55Zc1ZMiQBrfjHAU0ztvyJHF+AhoycOBAl/KxcuXKerfl/ASrOJxOpzPQmYD9nTlzRu3atdOyZcs0adIkj9vceuutKigo0Pz586vTxo4dq2HDhumll1666Dzk5+dXz32XkJBw0fsDAsWb8vTaa6/pgQceUHZ2tn8zBwSh1NRUPf3007rrrrvq3Gf1uQkIRQ2VKc5PQMPy8/M1YsQIvfDCC3r88cc1bNgwPffccx635RwFNMyX8sT5CS2Rt+1kjz32mObMmaMtW7Z4tV/OT7AKIyLglZycHEnmh2l9Vq9eralTp7qkTZs2TatXr7Y0b0Cw8aY8SeZLRVpamrp27dpo71SgJaqoqNC7776rgoICjRs3zuM2nJsA73lTpiTOT0BD7r33Xl177bV1zj2ecI4CGuZLeZI4PwEN2bdvnzp16qSePXvqtttu09GjR+vdlvMTrBIR6AzA/iorK/XAAw9owoQJGjRoUL3bZWZmqn379i5p7du3V2ZmptVZBIKGt+WpX79+evXVVzVkyBDl5OToz3/+s8aPH6+dO3eqS5cufswxYD/bt2/XuHHjVFxcrISEBH300UcaMGCAx205NwGN86VMcX4C6vfuu+9q06ZNWr9+vVfbc44C6udreeL8BNRvzJgxeu2119SvXz9lZGTot7/9rSZOnKgdO3YoMTGxzvacn2AVAhFo1L333qsdO3Y0OH8cAO94W57GjRvn0ht1/PjxuuSSS/Tyyy/r97//vdXZBGytX79+2rJli3JycvTBBx/ojjvu0LJly+ptOAXQMF/KFOcnwLNjx47p/vvv12effcYCucBFakp54vwE1G/69OnV14cMGaIxY8YoLS1N7733nsepOAGrEIhAg+677z7Nnz9fy5cvb7QXQYcOHXTq1CmXtFOnTqlDhw5WZhEIGr6UJ3eRkZEaPny49u/fb1HugOARFRWl3r17S5LS09O1fv16Pf/883r55ZfrbMu5CWicL2XKHecnwNi4caNOnz6tESNGVKdVVFRo+fLl+vvf/66SkhKFh4e7PIZzFOBZU8qTO85PQP1SUlLUt2/fessH5ydYhTUi4JHT6dR9992njz76SF988YV69OjR6GPGjRunJUuWuKR99tlnDc4xDLQETSlP7ioqKrR9+3Z17NjRghwCwa2yslIlJSUe7+PcBPiuoTLljvMTYFxxxRXavn27tmzZUn0ZOXKkbrvtNm3ZssVjoynnKMCzppQnd5yfgPrl5+frwIED9ZYPzk+wCiMi4NG9996rt99+W3PnzlViYmL1PHDJycmKjY2VJN1+++3q3LmznnzySUnS/fffr8mTJ+uZZ57Rtddeq3fffVcbNmzQK6+8ErDXAdhBU8rT7373O40dO1a9e/dWdna2nn76aR05ckTf//73A/Y6ADv45S9/qenTp6tbt27Ky8vT22+/raVLl+rTTz+VxLkJ8JWvZYrzE+BZYmJinfW/4uPj1bp16+p0zlGAd5pSnjg/AfV7+OGHdf311ystLU0nT57Uo48+qvDwcM2ePVsS5yf4D4EIePTiiy9KkqZMmeKS/u9//1t33nmnJOno0aMKC6sZVDN+/Hi9/fbb+vWvf63/+Z//UZ8+fTRnzpwGF+QFWoKmlKesrCzdfffdyszMVKtWrZSenq5Vq1YxBz5avNOnT+v2229XRkaGkpOTNWTIEH366ae68sorJXFuAnzla5ni/AQ0HecooPlwfgK8d/z4cc2ePVvnzp1T27Ztdemll2rNmjVq27atJM5P8B+H0+l0BjoTgDfy8/PldDrlcDiUkJAQ6OwAAAAAAAAAAUE7GYINa0QAAAAAAAAAAADLEIgAAAAAAAAAAACWIRABAAAAAAAAAAAsQyACAAAAAAAAAABYhkAEAAAAAAAAAACwDIEIAAAAAAAAAABgGQIRaLKSkhI99thjKikpCXRWgKBHeQKaD+UJaD6UJ6B5UaaA5kN5ApoP5Qn+4HA6nc5AZwLBKTc3V8nJycrJyVFSUpLlz5efny+n0ymHw6GEhATLnw/wJ3+XJyCUUZ6A5kN5ApoXZQpoPpQntHTN2U5GeYI/MCICAAAAAAAAAABYhkAEAAAAAAAAAACwTIQ3GzmdTuXl5VmdFwSZ3Nxcl/9Wqz3krLKy0i/PCfiLv8sTEMooT0DzoTwBzYsyBTQfyhNauuZsJ6M84WIlJibK4XA0uI1Xa0RUzRMGAAAAAAAAAABQxZv1RbwKRDAiAnbAYtUAAAAAAAAA7WSwF29GRHg1NZPD4WDFdARcWFgYFSwAAAAAAABaPNrJEGxYrBoAAAAAAAAAAFiGQAQAAAAAAAAAALAMgQgAAAAAAAAAAGAZAhEAAAAAAAAAAMAyBCIAAAAAAAAAAIBlIgKdAcBbDofD5T8AAAAAAAAAwP4IRCBoxMfHBzoLAAAAAAAAAAAfMTUTAAAAAAAAAACwDIEIAAAAAAAAAABgGQIRAAAAAAAAAADAMgQiAAAAAAAAAACAZQhEAAAAAAAAAAAAyxCIAAAAAAAAAAAAliEQgeDjdJrLxTy+ufIRTIItv82pJb92+AfHGOAbygyAUEX9BgAA4FFEoDMAeK2yUtp8UCoqlcLCpDCH9491Os325ZXmdngTY3BOp+RwSOUVZn9hNo/lVeW3okKSo+mvO1g5ZD5zp9O8docPxwzQmGCrD4BAqz4Xt9BzEoDQVtVZqpLvnQAAP+nXXooID3QuAK8RiEBwyMySjpyWyirM7W2Hpf/3mneP7dJa+tlMqV9nc7uiUrrjOel0jm95mDhAuvsqqU2SuX0wU/rxy77tw5+GdJd+cq3UubW5nZMv3fG8VFoe0Gz5RZhDeniW+cyqGrqe/1hauDGw+ULouPQS6QfTauqDQ6ekH70U2DwBdtavs3T/9VL3duZ2UYl0+7NSQUlg8wUAzWH2ROmm8VJctLm9aKP03MeBzRMAIPRteUZqmxToXABeoysagkN4WE0QQpIGdZN6d/TusefypK5tXPd1/Wjf81BUWtPoKEnd20tDe/i+H3/Jyq8JQkhScrw0ZVDg8uNPlU4pNsq1t+0No80ICaA5FLrVB2ntpOE9A5cfwO7Ou52LY6Okq4YHLj8A0NyqghCSdNlgKTkucHkBAACwIQIRCA6tk6QotwE8M7wMJhSVSos3u6ZNGy7FRPqWh437peNnm5aHQDh2Vtp0wDVtxpjA5CUQ5q51vd2trTS8V2DygtCz6YB09Ixrmp3rAyDQzuRKq752TbthtG/TLAKAXS3cKJXVGnUcGSFdkx64/AAAANgQgQgEhzCH1LGVa9qkQVKrBO8eP2+96SVfJSFGmjrUtzw4Jc1d55o2uq/UKdW3/fjTHLfG+B7tpaHdA5IVv9tySDp82jVtZgsKxMB68zzUB51tXB8AgeZ+TmqfIo3tF5CsAECzyimUvtzumnbtSObtBgAAqIVABIJHh1auPScjw80XfG9kZklr97imzRjj+1Q9S7ZKeUU1t8Mc0vWjfNyJH3kcxdGCGuPdR0WM7G3WDAGaw5JtrvWBZHp4A/Ds6+PSnhOuaQSIAYQK9w5LqYnSpAGByQsAAIANEYhA8IiMkNomu6Zdk24CEt5w/3HQubWU3tu3PBSXSZ+6TfN05TDXOWHtxCnPvbbdR5eEqi+3mx5qtdFQjOZSUiZ9ssk1beowKd6m9QFgB+4B4kFpUq8OgckLADSnQ6ekrYdd01pSByAAAIBGEIhAcHGfBinFhwWYtx2WDma6pjWlJ+b89VJFZc3tuGjpqmG+78dfPt8q5RfX3A5ztJzG+NJyadFG17SpQ83UXEBzcK8PWIAXaNjKXdK5PNc0GuoAhIq5a1xv9+kkDegamLwAAADYDIEIBJf4GBN8qM2XBgz3npgjeplFjH1xOkdatds1zc4LbhZ76LVt51EczW3BBqm8ouZ2TJR09YjA5Qeh5Uyu9BUL8AJeK680AbzaJg+SWsV73h4Agsm6fVLGedc0pqADAACQRCACwch9VETPDtLgNO8eu3SHlF3gmtaU0QHuAY0OraQxfX3fj794GsVx5bCAZcevzuWZHri1XTeKhmI0H/f6gAV4gYYt2mRGrFWJDJeu8XLNJwCws0qnNM8t2Dquv9Qu2fP2AAAALQiBCASfVgmmV3tt3vY0KquQFrpN1XPFECkx1rc87Dom7TvpmmbnqSVO50irg2gUR3Ob49ZQ3C5ZGt8/MHlB6GEBXsA3uYXSl9tc03xZ8wkA7GzxZqmwpOZ2eJjpBAMAANDCEYhA8HE46o6KGNPPjErwxoINJiBRJTpSmt6EqXrce0EP6S71bO/7fvzFfbHujjYfxdGc9p40jcW1zRwbmLwgNLEAL+Ab93NSqwQzRRMABLuiUumzLa5pV4+QYiIDkh0AAAC7IBCB4NQu2fQuqhLmkK73sqdRVr60Yqdr2nWjXPfnjeW7pPNBtODmzqN1R3HcYOP8Njf3xQMHdDULCALNgQV4Ad8cPi1tOeSaNqMJUyUCgB3NW2emaaqSECNdMTRw+QEAALABAhEIThHhZh722qYNl2KjPG5eh/tUPW2SpAmX+JaH8gozuqK2KYPqLqZtJ+69tod2t/cojub01W7pTI5rGo1eaC4swAv4zv1c3KujGU0EAMEuI0tau9c1bcZoqYXMigoAAOAJgQgEL/fpmeKipauGeffY/RlmhEBtTZnTfeFGqaz2gpsRZp5ru1q+Szqf75rWUnptV1RK890CR5MGSqkJgckPQg8L8AK+Wb9XOnneNY31VQCEinluwdYubaT03oHJCwAAgA0QiEDwiomSWie6pl3vwwLM7j0x+3eR+nX2LQ85hdKX213TrhlpRmzYUX2jOJLjApMff/tkk1RcVnM7Ily6loZiNBMW4AV845SZvqS2sf2kDimByA0ANK+th6VDp1zTWkoHIAAAAA8IRCC4uY+K6JQqjerj3WNX75ZOZ7umNeXHgfuCm6kJpqe9XS3c4GEURwtpjM8rkr7Y6pp2TboUFRGY/CD0sAAv4JvPtkgFxTW3wxymUwEAhAL3jk/pvaRubQOTFwAAgAAjEIHglhQnxce4pnk7rUOlU/rYbU73iQPqjrJozKFTpsdTU/IQCDmF0tIdrmnX2ngUR3NzbyhOjjejQoDm4HEBXhvXB0CgFZVKi7e4pl01zPs1nwDAzpZul3IKXNNuINgKAABaJgIRCG4Oh9Q2yTVtaA/vF4xettP1dniYdKmPi1ZL0nK3hv3eHaXOqZ63tQP3152aIA1uIQuEHjsrHcx0TaPHOprTMrf6oFcHqUvrwOQFCAbL3c5J8THSSOZRBxACyiqk1Xtc0yYNZNFqAADQIhGIQPA7l+d6e9cxKbvA87buxvZzvV3plNbu9T0P7vs5ekY6cd7ztnYwtq/r7dxCaddRz9uGmg6tpO7tXdNW7w5MXhCaxnmqD84FJi9AMHA/hxaXSpsPBiYvANCcIsLqBlbX7DFr5AAAALQwBCIQ3PKKzKW2eWs9b+vOIWmG29DotXukzGzf8tCldd11KeZ6mYdASIiRpg51Tftkk1RS7nn7UHPDKNcFzfOLpc+31r894IvOqdJot0DfvHU0OAD1iY6Qpo9wTVuyzdTNABDsLh0gtXEbvW3n3wkAAAAWIhCB4HbSrZfxmRzpKy97t4/sI3V2my7FfUE5b7jP85pXJH2xzff9+Mu04VJMrbm3KyrrrpURqmKjpKuGu6Z9ulkqLgtMfhB63BfZzSsyjaoAPJsy2Kz3VNu8dZ63BYBg475O1I4j0oFMz9sCAACEOAIRCF4lZdLZXNe0j9ebhnVvuC8ofSBT2n7Etzx4Gl2waKN9RxeEOeo2lK7YVXd6q1B11TApLrrmdkWl9DENXmgm8dHSlcNc0z7ZZOoqAJ65n4s37Ddr+QBAsOvfRerX2TWtKZ2eAAAAQgSBCASvjCzX6U6Ky0yjnzfS2krDe7qmeTulU22eRhfM3+D7fvxlfH+pXbJrWksZHh7mqDt6Zc0e6XROYPKD0HPVcDPqpkpFpTS/hYw2AppieE8prZ1rWks5JwEIfe6B1lPZ5rsnAABAC0UgAsGpolLKzHJN+2Kr93NKuzdIZxdIS3f4locwh3T9KNe0lbvqjtKwE/fh4buPS3tOBCYv/jaqj9Qx1TWNXmloLp4CXau+ls7YuD4AAs19naajZ6RNBwKTFwBoTm2SpAmXuKbNWydVsmgUAABouQhEIDidyZHKK1zT5no5xU5SrHT5ENe0BRuksgrP29dnXH+pXYprmp0btvt0lAZ2c02zc36bm3uvtH0npZ1HA5MXhJ6x/aT2Ka5pLal8Ab5iYXcAoey6UVJ4rZ/aRaXS4s2Byw8AAIANEIhA8HE6pZPnXdM2HvB+TumrR0jRkTW3yyqkhU2YTsl9dMGeE/YeXeCe37O50ldfByYv/ta9nTS0h2uat4ErwBvu5WvvCenr44HJCxAMWNgdQKiKjpCmj3BN+3yLVFASkOwAAADYBYEIBJ+cAqnQ7Yu8t3NKh4eZHkq1Ld8hZRX4loc+HaVB7qML1vi2D39KTZAmDnRNm+/Dwt7Bzr2R+Hy+tHxnYPKC0NOrgzQ4zTWN0RBA/VjYHUAou3yIlBjrmjaPDjAAAAAEIhB8TriNhjh2Vtq437vHXnqJmbO1tqb0jPc0umCljUcXXDtSigyvuV1SJi3ycmHvYJcUJ1022DVtwYa6U3sBTeVeH5zLM+vFAPCMhd0BhDL37wXr9tb9/QIAANACEYhAcCkqkbLyXdN8mVPa/YfBjqPS/gzf8tDK0+iCDfYdXRAZLl2T7pr2xTYzDUZLcE26FBVRc7usXFq0MXD5QWhpFS9NdqsPFmyQym1aHwCBxsLuAELZ8J5St7auaUwHCgAAIIlABILNySzX23lF0pKt3j22X2epfxfXtLlNmE7J0+iCT2zcsD1lsJQc75rWUn4QRYRJ1410TVu6Q8r2cSouoD7XjJQiawW6SsulhTauD4BAY2F3AKFsplunpyOnpc0HA5MXAAAAmyEQgeBRXiGdznZN+3SzVOzlnNLuPwxOZUur9/iWh/pGF+TaeHSB++vedEA6eiYwefG3iQOl1ETXNG/XEwEaE+GhPvhym5RbGJj8AMHA/Zy0h4XdAYSILq2lUX1c0wi0AgAAVCMQgeBxKtt1+iNf5pRunShdOsA17eP1UqW3czpdMGWQlOI2usDOi88N6S71aO+a1lJGQ0jSDLfpP7Ydlg6eCkhWEIImDzRTtdXWksoX4KteHaRBbgu7ExwGECrcp53LLZSWbg9MXgAAAGyIQASCg9MpnXRb5G3Vbul0jnePv36UFF7rcC8qlRZv9j0f7mtMbD4oHbHx6AL3/J44J23YF5i8+NuArlLfzq5pNHihObn37N5ySDp8OjB5AYIBC7sDCFUJMdLUoa5pizZJJeWByQ8AAIANEYhAcDiXZ9ZiqM3bRuXoCGm62/QpS7ZK+cW+5WFId6lnh6blIRA6tJLG9HVNm+vDwt7Bzr3BKyNLWrs3MHlB6BmUJvXq6Jpm5/oACLRW8dLkQa5pLOwOIFRMGy7FRNXc9mXkNgAAQAtBIALBobRcCnPU3N53Utp1zLvHtm9lFrWurSnTp7RNMmsrHMw0t0+ck9bbeHRB51TpTLa096S5nV8sfb4lkDnynzCHFBtl1hU5n2f+f7zO96m4gPq0S5ZyCmqOr5Pn7V0fAIHWqbV0LremzLCwO4BQkhwvFZfW1HErdpmOVAAA+IOTtg4EB4fTydGKIFFeYXq0V1SYBarLKnx7fHSkFBFmRgQUljS+vdNpAhinc8z6FCfOmcclxkjfnCSdzZXO5DbllfjH6Rxp3wnTO2vWONMz68S5QOfKvyoqzHQ5M8ZIJ7OkSnreohnlF0k7j5kpmsorpTNeThUHtFR5RdLOo2Ye9dgoe09tCAC+KiqRdhyVZo2Vzud793sDAICLkH/rODnjo+WodCohOSnQ2QEaFRHoDABeiwiXJlxSE+l1OBrevj6VlVJYPYOB8oqkI6fNPO+7T5gRBcVlZt7XYT2k9ilmcVqn0yyA3TqxaXmwUlGpWbsi87zUr7M0opcUd2GoeP/ODT821GTlm0avsnKpb8fGtwd8kZVvRkhVVkqtE8wFQP2y8qVDmZJDJhDR0s5JAEJbVr4ZtV1WLnVrE+jcAABaAEdlpZRfLEdsdKCzAniFQASCT1MDEFVqByFKysx0S4dPmx8Ox86ahvyYSCk1SRqQJrVJkiLDL+45/cHpNEGUzYfM1ETjLpG6trn49yuYOcIkOcx/BzPRoZlxfAG+ocwACGXUcQAAP4vfeETaflj63tRAZwXwCoEItCwVlWYu98OnpAOZ5lJQbIITqQlS745S+2TXxeaCQUGxtGG/WZC5eztpeE8zFVVL56h14fcgmhvHF+AbygyAUEYdBwDwtxbc7xTBiUAEQpvTadZyOHTaTAex96SUW2gCEinxUpfWZrqlhJjgHDngdEr7MqRth6XICGnSQKlz60Dnyj4cDtcL0Jw4vgDfUGYAhDLqOACAv3G+QZAhEIHQk1NYs87DnuPS2TyptFxKipXaJkuXdJFSEsz0RcEst1Bat88skNunkzS0uwlGoEbVSZkfhLACxxfgG8oMgFBGHQcA8DfONwgytFoi+BWXmnUeDp2W9p+Ujp8zaz/ERkttk6ThPcw6DxFBsM6DNyoqpd3HpR1HpbhoaepQM6oDdYXJnJjDFPyBJ9gPxxfgG8oMgFBGHQcA8DfONwgyBCIQfMorpBPnzIiHA5lmyqXCUjMaoHWiNKCr1C7FLDgdas7nSWv3StkFUv8u0uC00AmwWMHhuDBXLz3TYAGOL8A3lBkAoYw6DgDgb5xuEGQIRMD+nE7pdI5ZYPrgKWnfSSm/2KSnJkg9OkgdUqSE2EDn1DoVldL2w9LXx6XkOGnaCBN0QcMcF1YM5AchrMDxBfiGMgMglFHHAQD8jfMNggyBCNhTdoEZ8XDolLT3hJSVL5VXmkb4Dq3MpVV8y6h0T+dIa/aY4MuQ7mbER3hYoHMVHMIu9EwLczBkEc2P4wvwDWUGQCijjgMA+BunGwQZAhGwh8KSC+s8nJL2ZUiZWWadh4QYM81Sn05mnYeW1ABfWi5tOWQCMW2SpMmDpJT4QOcquDgcrhegOXF8Ab6hzAAIZdRxAAB/43yDIEMgAoFRVm4WlT58WjqQIR05Yxadjo40je5DupsFmKNDcJ0Hb5w8J63Za4IxI3tL/TpLYS0oCNNsHDX/OUGj2XF8Ab6hzAAIZdRxAAAADSEQAf+orJROZZvAw8FMs8h0QbHkCJPaJEr9O5vpluJjAp3TwCoulTYcMO9Rx1bS2H5SYgivfWG1sAs/BBkiDytwfAG+ocwACGXUcQAAfyPwjSBDIALWcDqlrAKzwPShCwtM5xSaRZdbJUhd25iG9pQWss5DY5xOMzXV2r1SpVOa0F/q1YH35mI53C5Ac+L4AnxDmQEQyqjjAAD+RpsRggyBCDSfgmIzxdLhU9Lek9KZHLPOQeKFBaYHprW8dR68UVhiAhBHz0jd2kpj+kpx0YHOVWhwhMkMjw+7cB1oRhxfgG8oMwBCGXUcAMDfCEQgyBCIQNOVlkvHzprAw4FMc72kTIqLktomS+m9TAAiisPMI6dT2p8hrd9v1n+4bJCU1pYTSXO68HtQYRcuQHPi+AJ8Q5kBEMqo4wAA/sZUgAgytBDDe5WVUkZWzToPB09JRSVmhEO7ZGlwmpluid78jcsrklbtljLOS707mgWpY6ICnavQ43BcGB7PooGwAMcX4BvKDIBQRh0HAPA3TjcIMgQiUD+nUzqfJx06bdZ52H/SNKA7JaUmmjUMOqVKyXF82fZWZaX09XFp0wETeLhyuNSldaBzFbocF/5U/SgEmhPHF+AbygyAUEYdBwDwN843CDIEIuAqr0g6ctqMeth3UjqXJ5VVmEWlO7aSRvSS2iaZqYTgm+x8aeXX0ukc6ZKuZuoqpq2yVu2eaQxZRHPj+AJ8Q5kBEMqo4wAA/kYgAkGGVtCWrqTMLJJ8+LR0IEM6cd6s/ZAQI7VPkfp0kjqkSJEcKk1WUSltOyxtPSQlxkrXjTLvLaxXNTSeIfKwAscX4BvKDIBQRh0HAPA3zjcIMrQutzQVldLJ82aB6YOnTACiuNT0zG+XbEY8sM5D8zmbK63YZUZDDO4uDeshRYQHOlctR9VJmR+EsALHF+AbygyAUEYdBwDwN043CDIEIkKd02kaw6vWeTiQIRUUm/vaJkuXdDHrPCT5YZ2H/66WnpkjDegq/eO+uvdPeMT1dkyk1KGVdOUw6ZsTg2sx5/IKsw7EjiNSq0TphjFSm6RA58o3CzZIT7xvrr/wQ2loD9f7nU7pxifNVFPj+0tPf9ekT3hEunGc9LOZfs2uR/RM85+q4+WfPzH1SlMVl0pvLZOG9zSBUTvj+IKVGitT970sZRdIbz7kml5RKc16wkyt+OfvSuP6+ye/3qDMAKGrub4HBDPqOACwv8bapSQpM0t6/Utp7V7pXK4UHyMNTpO+NVka0t2fufUC5xsEFwIRoSi30Ix0qFrnISvfNEy0TpTS2pnAQ5tE/6/zsHizGW2x65h04pzUtU3dbUb3kaanm+uFJWY6o38slvZnSE98x7/5baqM82YUREGJNLKPOWGFB+GaGlVz20ZFSJ9vNQ3DtW06aIIQVetc1J4L1y5z44ZdmKs3zCb5CWVV72+YLu69Li2XXv1cumuqNLJ3s2TNMhxfsJI3Zcrh4b4NB0wQomMr6bMt0oRLLMykjygzQOhqru8BwYw6DgDsr7F2qa2HpZ+9aq7fMErq3l46n2cC7j9+SXrwBukbE/ye7XpxukGQIRARCopLzToPhy6s85CZZRaYToozIwoGp5n/gZwS6OR5afsR6Y+3S3/60FT+37+q7nbd2tYEIiTppvFSeaW0dLtpoIyO9F+efVVaLq3bK3193KyrMW2EWeQ72I3vL32xTXpohusxtHiL1L+L6ZHr3vOraqG+QHM4JNEzza8u9r2ufmwQfGYcX/CHxo4v9/s+3Sz16yxdM1J6aZFUXCbF2mREIWUGCH0tuXxTxwGAvTXWLpVbKP3qDSk6QnrlXqlLrSDF7MnSA/+Qnptn2kHsMjKC8w2CDIGIYFReYSK3h0+bdR6OnDaN4LEXpjIa00/qnGqvqYw+3SwlxUoTB0jr95nbP5hWdztPvTtbJ5rKNTLcvr2LjpyRVu4yn8Oll5hhfsF+Qqh6r6cNl5btlDbsk8Zf6FlbVi59uU367lTpvZWu20ueP8dAoGea/1S9v/WNhim7MNLhq6+lY+ekigqpXxfpnmk1Ix9OnpdmPmGu/+szc5Gk71/pub4INI4vWKmxMuVw204yQYdlO6TvTZWuHCo9P09asVO6eoTl2fUKZQYIXY3VWZK054T0wkJp22Gp0ikN7Cb9aLrpNFVl/nrpd/+R/nGv9MV2adFG0+lqTD/pf26WWiVY/lKajDoOAOytsXapuWvNyOLHZptOsrXFRUmPflO6+U/md+1f7/Zv3usT7O1OaHEIRAQDp9NMgVO1wPTBTKmo1Eyt1D7FLIDcubUZAWFXn26SLhssRUWakQIfrjZD4QZ2c92utFzKKTDXi0rNsLiFG0xjeKQND9fiUmnVbjN1VJc25oSWGBvoXDWvTq3ND8TFW6QJA0za6j1SfrH5XN5b6WEEhE16gjFXr//V914Xlkhz10lXDZdmjr1we630039Ir91venCnJki/uEn644fSlMGmzpCkPh3t+flxfMEfCoprzou1lVeqTl27YqdUWGrKWdtks87Kp5tdRxoGEmUGCH31le8DmdIP/tfMs/2dy8wo2/+uln70ovTyj6VBaa7b/3mO+U5991Wmo8K7K6SnP5KevN0vL6NJqOMAwN4aa5dascuMhrhymOd6vEsbs3bmhv1SSblZ1zTQON0gyNiwZReSzHQ3hy8sML0/wwwRczrNgsd9OktdWteMFLC7r4+Z1/LzG01+h/c0AZRPN9X90TF3nbnUNmWQ9Otb7fVanU7zg+qrr831ywZLfTrZK48Xq+q1OBymN+3/LpBKysxIm082mQaudilVG9d97XZ4L2q/BjvkJ5Q19l4nxUsf/9o1oDhrnHTzH00w6//7phQXI00dagIRfTpK1470T96biuMLVqo6pu59uf5tenZwPfY+2WSGiXdMNbenDTflKbvAHr2IKTNA6GqsfL+0yIzq/udPzO8YSbpulHTTk9Lf5kuv3Oe6n+R46X/vqbntlPSfFSY4m2DTTj/UcQBgX960Sx06ZdZVbWhK8L6dpE0HpBNnpd6d/JL1BjECD0GGQIRdFJZcWOfhlGngPpNjvqynJEidWkmjeptKMpDrPDTVJ5tM0GRU75qe81cOM0OtH5zhupDz5EHSrZea68WlZv6+t5dJv35DeupOe3ypzy820zAdOS316GCmYoqLDnSurOOQdNUw6Zk55nWP7296Cvx8lmv03f26DT4qSTX5sEt+Ql19n31EmMwKlpIqK6W8IjMtwyVdzVQN7o+x0zHUEI4vWO2Rm6S0tnXTn51rylDVsZddYEar/WxGTdoVQ6Q//Vf6fIt0y6V+ynAjKDNAaPN0/q6olNbsNaMdu7auSW+bZDq7fLTmQoAhpua+G8e6Nq4M72F+E2RmSX1sGoiQqOMAwK68aZcqLDFtOw3V4fEX2n4KSuxR19uhjQzwAYEIO9h1TPrgKzMtUXyM1DFF6jfABCDstM5DU1RUmmkhRvaWMs7XpA9Jk95catYdGNe/Jr1DijSuX83tywabBZ+fnWcawScP8lfOPSsrl/67ykyLNW2E1LN9YPNjpeq5fmVO2GP6ms+ypMw0JF81zHUblzUibDI3bpgu5EX2yE8oq+9YqG3eOlPuD502gdYqnVNrHuPNHNN2wfEFK1UdU4O71Z3GUJL+FWeCD1Xbfb7FlKv+XaTjZ2u2G9xNWrRJ+uZEy7PcKMoMELoa+h5wvsB0MOreru59PduboOqZbCmpY839nVJdt02JN//ziuxbf1DHAYA9edsuFRdtghEN1eGFJeZ/Qow96noCEQgyBCLs4Hye+QL+jQmht77A+v3S2VxT6X+6ue79CzfVLIBcxb0iHdPX/N900PSkCqTiMnO5dmTdxYtCVdXw8mvSzeKB53KlCZfUXZOEqZkg1f9eL9ggPfqOCS7ecblZDyIszCz0deys62dVe192xvEFf2js+Kq6b+Em8/+7f/W83YlzZl7bQKLMAKHPU/muLvvyUPZrDSGofV94WP31hF3rD+o4ALAnb9ulerQ3o/XLKqSoeppL92WYmUrS2tqjrrdBFgBfEIiwi/Awey823VSLNpoGx//5Rt37lmyVvtwmldxSa+SHhy/ulU7zv6g08BV91Q+LsAZ+HIUK9x9TVwyVHn9P2nbEdZosTw3ILFbd8jT24/vzrWZO6Gfvcr3/pU9cHxMW1vB+7ITjC1ZqrEzVvv/4OWnrITPqYWRv1+0qndKv3jCjIn4wzdo8N4YyA4Suhuqs1ETzXf/wmbr3HT5tepR2THV9rPt+gqGRnzoOAOzJ23apyQOlbYelz7aYdYzcnTgnbT5oOsvG2mV6bs43CC4EIuyioelMglVxqanUrxpuFsx01z7ZNIws32nmh5U8vw/Ld5r//TsH/j0Kc1zIowKfF6tVNww7zCUhRvr1LdLJ89Jlgxqeiskux3P15+WwR35Cmfvx4q5qLZjax8a2w9LWw1LHVjVpcReCkvkepl7IK5LO5Jo5pe0weozjC1ZqrExVbRPmMD+uJOmuqVKHVnW3+2i1tHCj9MOrrcmrtygzQOhqqM4KCzdrjC3dbqbE6HxhnYizuab+Gt5TSopteD9hHtLP5Eh5xVLXNlKkDdbRo44DAPvxpV3qlkult5abqcGH9zTnlyolZdKjb0tOp/Sjq+1TzxP4RpAhEGEXodhzZukOs4DPZYM9v7ahPUxUesEGaXq6STtyRpq/wVwvLjWNlPPWmmmQrh8d+PfI4ZCqVuELdF6s5qh1peq1zhzbwPZugQg7vD+OWp+VHfITyqre3o/WSF99Xff+UX3MqIgH/iVNGmh6cL+/UurVwcyzWfX5xEabtE82m7mkk+KlPh2lPp2kL7ZJv35Levy2ho9Ff+H4gpU81cEet3OY82j/LqZHsSeXDZae+ED6+rg0oGtz59R7lBkgdDX2PeDH06XVu6U7npduvdRMa/HehTXyfjbTdfqmqivuo23d05//WJq7Tvr0sZrgRiBRxwGA/fjaLvXs96Qfvyzd+rR04zjz2/RsrjR3rXT0rPSLm6Thvfz/OurD+QZBhkCEndglotpcFm6QoiPNegKeXltYuGmQnL9Byi0waat3m4tkelC3TZJuGi/99DrTIz/QWlJPp+oeaWr8tbqPgLDLQsMt6fMKtKrj5T8rPd+/5PdSUYlpdPjqa/OF7k93mDk61+1z/Xx+/y3pD+9Lf/rILBD/4+lSv86u0zLY4fPk+IKVGquDq5J2H5cOnWq4Z9ZlQ0wgYsF6aZCHha/9hTIDhK7GvgfMGiu98aD03Dzpn5+ZHqVDuktP3S4N61F3P+51X5jqpnszcsyfqOMAwH58bZca1Uea80vplU+lxZvNiPzEWHOuevzbUrqNghASMzMh6DicTqcz0Jlo8VbuMsPA7rg80DlBY3ILpde/lG4Y3XIWqw5mp3Ok91aaIZbtkgOdG4Qaji/AN5QZAKGMOg4A4G/n86WP10nfq2eKVsBmGBFhJwypsr9gWCgPNfi8YCWOL8A3lBkAoYw6DgDgb4zAQ5AhEGEXdplqBA0Lc9R8Vnxe9sfnBStxfAG+ocwACGXUcQAAf+N0gyBDIMJO6Dljf44Lc7+2hMWqQ0HV50XPNFiB4wvwDWUGQCijjgMA+BunGwQZAhF2wRfW4OBwqDoIwedlf3xesBLHF+AbygyAUEYdBwDwN843CDIEIuyEIbz2F3ahp1OY+LyCQZj4vGAdji/AN5QZAKGMOg4A4HecbxBcCETYxYUONLA5h9sF9sbnBStxfAG+ocwACGXUcQAAf2NEBIIMgQi7sPsQ3oJi6eVPpS2HpK2HpJxC6ek7pW9MaPyxp7OlV5eYx24/LBWUSO88LI3rV3fbykrp7eXSW8ukI2ekuChpYJr002ul9N7N/KKaIgiGXO89IT33sbT9iHQmV4qNkvp0lH4wTZo61Ld9/eJ16d0V0uWDpVd/6npfcZn0r8+kj9ZIx89JyXFSei/pgeulvp2b7/VcDIbINz9vj6/ud9e/j0svkd58qOHn4fhCqFi9R5r9Z8/3/fcX0oheUlGJ9P4q6bMt0u4TUmGxlNZOmj1J+tYkKTzMu+f6bIspn/tOSm2SpJvHSz+9TooIb65Xc3EoM0DL4O13f1/qvv0Z0vtfSct3SkfPSHEx0qBu0oM3SEO6+++1NYQ6DgD842Lap9bulf6xWNp5VDqXJyXFSQO6mu/MIz20OZWWS//4VPpwjXT8rJQUKw3uLj3xbaljanO/Mt9xukGQIRBhF1ULm9lVVoH01/lS51Tpkq7Smj3ef8k+eEp66ROpR3upXxdp04H6X+8TH0j//EyaNVa6/TIpt1B6a7l065+lDx+RhvVs9pfmk2BYhO7EeXNivnm81D5FKiqVFm2Uvv936cnvSN+a7N1+th2WPlglRUfK4+LcD/xT+nyr9M2J5ofg6Rzp9S+kG/8offpbqUvrZn5hTcHi4s3O2+Pr2bvqPnb7EenVz6WJAxv/PDi+ECqqDo3vXlG3saxHe3PsHDsnPfqONKG/9P0rpcRYadkO6TdvSVsOSn/xUJ7cfbld+sEL0th+0u++Je0+Lv19gXQ+T/rDd5r7VTURZQZoEbz97u9L3fefleYyfcSF3whF0tvLpFlPSq8/IF06wJ+vsB7UcQDgFxfTPnXolJk+77bJUttkE8SYs0a65Snp3/dLUwbVbFtWLn3vr9LGAyZI3r+LlFNgAiD5xfao6+2QB8AHBCLsxM5ziXZIkTb+RWqXLG09LF33+wtzoHqR56HdpW3PS60SpAUbpB++aB7n/tjyCunNZdK16dJfa/Wmvm6UNOEX0ty1pudoIIXV6uVk189r6tC6Ix++e4V0ze9MkOfbUxrfh9MpPfaOdNM46auv637WGVnSJ5uke6ZJv76lJn1MX+nWp6VPN0l3X9Ucr+bihNX6rOz6eQUbb4+vm8fXfezavebzmDWm4c+D4wuhpOrYGNNXunak523aJ0uf/VbqV2u0z3emSD97VXrvK+n+602DXkOeeF+6pIv09kM1IyASY6W/L5TuulLq3fGiX8pFo8wALYO33/19qftmjpF+NkOKj6nZdvZE6bJfS8/NkyYNtPpVNY46DgD842Lap26bbC613XmZaXN69XMzG0SVVz83v2E//IU0PMCdYutDIAJBhkCEXdi5h70kxUSZi1TTu9PbPCfG1brhqPnv/tjySqm4VGqT7Hpf22RzQomJCvx7VPX8dv+83EWES51SzbBFb/L94SppzwnpH/dJNzxu0mo/rrDE/G/r9lm1TzH/Y23wWUnB+3kFG2+Or5IyM3JibD+pUyOjGTi+EFJqnfcKis25zH2qpNZJ5uJuerppjDuQKfXsUP9T7D0h7T0p/eHbUmStr3Z3XC79bYG0cKNp0As0ygzQMnj73d+Xum9oj7rbpSZKo/tKq3fbo06hjgMA/7iY9ilP4mLMOSW3sGYflZXSvz6Xrh5hOsSWV5gRErHRF539ZsX5BkGGQISdBEvPmbBaX7J9zXPVVK+eegrFR0sjepr5X0f1Nj8scgtNL6fkeNNDKtDvUTD1dCosMdPm5BVJizebaTtuGN14vvOLzBRZP7nO9DSQ6n7WPdpJHVtJr3xqetkO6iadypYef0/q1laaOdYe708wfV7Bxtfja+l2M+z1Ri+ODY4vhJKqY+Nnr5pARHiYGR3xm1s8N6zVdjbX/G+d2PAxtuuY+T+sh+t2nVJNWdp51B7HKGUGaHka+u5fH2/rPkk6k2Maj+xQp1DHAYD/NbV9Kq/IrP9wPl/64CvTEfOn19XsY2+G+Q06oKv0yP+ZdqrScjMC+XffkiZc0uwvpUk43yDIEIiwi6DqOeOo+edrnh2NPPbv90j3vCDd90pNWlpbad6vpO6NTEvhDw7J9mtEVPntu9IbS831MId0Tbr0xHcaz/ez80zvgnumuW5b+3pUpPSvn0g/fkm68/ma9CHdpY9/JaXEN9OLuEjBsKZHsPL1+PrvGik6Qrp+VOOfBccXQklUhJmS6YohprFs7wnpxU/MvObzfi0NTvP8uNJys5Bet7ZmKHhDx9jpHPO/fUrd7dqnmB9RdjhGKTNAy9PYd393vtR9a/aYebsfuN4edQp1HAAEQBPbp+55QVq6w1yPijAdXx+8oWYfh06b/68sllrFS0/dYW7/db5021+kRY+aIEWgcbpBkCEQYSfB8oW1Op9N+ZLdyGMTY808sSN7SxMHmMaVvy0wCwTN+ZXpGRVIjguRiKYEYfztB9Ok60dLmVnSvHVSpdMMJ2wo3wcyzTz/L/6oZqhj9Ynd7XEp8dLAbqZhOb23WfTpb/PNYqn/+XmtxwdQ9efFD8Jm58vxlVckLdkqXTFUSknwbv8cXwgVo/uaS5WrR5iyc/mvpSc/kN552PPjfvWGmW7pzYdcp1vypLjM/I/2MG1ZTKSUZ6fF9CgzQMvi4+8Gb+u+M7nSvS9L3dpI911rjzqFOg4A/K+p7VO/vkX60XTpxHnpvZVSWYX5TVu1j6rpgguKpc9/J3W+ML3wpQOlcT+XXlgo/e8Pm+1lNBnnGwQZAhF2EQwN21Wq6/kmfMlu6LHlFdI3npLG95eevL0mffIgadIvpRcXSb+5tam5bh7B1NOpb2dzkaRbJ0q3PCV95znpk0frz/tv3pJG9TGNZFVqf2ZVcgulGU9I915jTt5VhvWUZj0h/WeldOcVzflqmiaYPq9g48vxtWCDaSi9abx3nwPHF0Jdzw7StBHSwg3mB094mOv9/7tAenOZ9MhN0pXDGt9f7IXAXFl53WOxuNwEI+xwjFJmgNBSWi5l57umtU5yrdN8+d3gbd1XUCJ951kpv9iMLEuIbUrumx91HAD4X1PbpwZ3r7n+jQnS1N9I9//TjMyXar5fj+ojdWlTs23XNqaT0fr9Nqnr7ZAHwHsEImzDETxzu1XPwSff81xVUXuaO3XtHmn3cen333K9r3dHqW8nad2+wL9HVXO/NmV9jEC7YZT0s3+bnuW9O9a9f8VO6Ytt0mv3S8fP1qSXV5iFho+flVolmFErCzaYOXmvHuH6Plx6ibl//T7pe1Otf02NcQTx5xVsGjq+/rtaSoqTpg337nPg+EJL0CXVNOIVl5rjuso7y6Xfvyfdebn08Ezv9lW1ns+ZHPPjqLbT2WZ6Ezsco5QZILRs3C/NfMIt7S9mWqUqDX33r83buq+03IyU/vqYGSE50AbTYlShjgMA/7uY9qkqMZHmt+df55u2j9gos86aJLVLrrvftknSjiP2qOttkAXAFwQi7CKYRkTUnqqnqWtEuF+XzBBryXU4XJWyCqmikWmF/MFxEa890Kqm7sgr8pz3E+fN/9pz8lfJyJLSH5Ie/7b0w6trFhF0un1WTqdUWSmVV9rj/ak1StMW+Qll9R1fmVnSyl3S7EneT6fE8YWW4MgZ86MnIabm+Fm4UXrwX9J1I6Wn7vT+uKrq0bX1kJnKrEpGlnTyvHT7ZfY4RikzQGgZlCZ9+AvXNPe1ahr67l/F27qvstJMx7Rip+mxeumAi8l986OOA4AAaKY2muIy83uzoFiKi5YGdJMiw833aff9ZmabEYB2qOttkAXAFwQibCOIGrYb+kGRmSXlFkk92nme17WhYXO9O5n/H62Rpg6rSd96SNqfId1+eeDfo9o9nQKdl/qcyZHaJrumlZWbeQ9jo6R+XUze3T+rSQOl1x+su7+H/mWGIj40wyzG5HBIvS70eP9ojRk+X2XRRjNcfkh3e7w/wfB5BRtvj68qc9aa4OLNEzx/BmXlZiGwpFipw4VeJxxfCCVnc6U2Sa5pO45In2wy66aEh5u0VV9LP/i7NK6/9PK9NenuPJWZS7pKfTpJr38p3Tm1ZlqU15aYY/OGMfY4RikzQGhplSBNGdzwNg1995e8r/sk6RevS3PWSH+5y3UaUbugjgMA//O1fcrT79mcAmn+erMORLsUk5YUZ9qlFm+W9mWYWTokac8JM0L/Dhu0T0n2yAPgAwIRdnExw8j85ZVPzdztGVnm9uLNpmKXpLunSclx0uPvmaHVW593HZb954/M/6+Pm//vfyWt22uuPzzL/B/RU7pssPTuCjPn62WDpVPZ5nljo6QfTw/8exTmqPmsAp2X+vzsVdMrfXx/qWOqmZbj/a/Mwn+P32Yar6S6n1XVxd2v3jDDEa8fVZN2TbrUv4v09EdmyqaRfaSDmdI/F5spQm6fYo/3Jxg+r2Dj7fFV5YOvzLDWSQM8fwanss1iX7MnSS9cWOyL4wuh5Pt/M6OBRvc1w7j3nJD+7wspNlp6bLY5do6ekW77i/khMWOMWQC+toHdpEHdzHVPZUaSfvct6VvPSDf/UbpxnJm25B+LTXm5pIu/Xm3DKDNAy+HNd39f6r4XF0mvfm7m6o6LNvur7bqRUnyMNa/FW9RxAOA/TW2fuvUpqVNrKb2XCUgcPyu9tcw89tWfutbf/9+tZhTerD9IP7j6wvN+YgLxP5tpj7qeQASCDIEIuwiGnjN/XyAdq7V2wMfrzUWSbpkopcTX2tjt9fzhfdd9vbm05vrPb6y5/vbD0t/mS/9dJS3ZaqLW4/pLv/pGzcK4AeVQ9Wuz6+d14zjpjS/Nj7Xz+Wbaj2E9TIPXNSM9PMCL1+I+xDw60ixK/NRH5oT/4WrzPNeOlH7zTalNcr278itHEHxewcaX42vfSWnLIbPodL09HD30YOH4Qii5dpT0/krphYUmiNcmUbpulPSLm8yi1ZJ09Kz5ISVJP/933X08cpM0OO3CjXp6fU1Pl954UPrTh9Ij/2ee56GZ0iM32uf4pMwALYc33/19qfu2HzH/1+8zF3db/xr4Raup4wDAf5raPvXty0x704uLpJxCs93I3tI/f2I629V2SVdp/v8nPfa29MxHUliYNHGgWde0c2tLX57X7BAMAXzgcDqdzkBnosVbuUvadED66fWBzgkak11gGpO+ObGmAQn2lZllGsy/N7VmChOguXB8Ab6hzAAIZdRxAAB/49yDIBMW6AwAAAAAAAAAAIDQRSACAAAAAAAAAABYhkAEAAAAAAAAAACwDIEIAAAAAAAAAABgGQIRAAAAAAAAAADAMgQiAAAAAAAAAACAZQhEAAAAAAAAAAAAyxCIAAAAAAAAAAAAliEQAQAAAAAAAAAALEMgwg7CHFIYH0XQCA+THI5A5wIAAAAAAAAAgoLD6XQ6A50JALBEWbl0Lk9qnShFRgQ6Nwg1HF+AbygzAEIZdRwAwN849yDIEIgAAAAAAAAAAACWYT4gAAAAAAAAAABgGQIRAAAAAAAAAADAMgQiAAAAAAAAAACAZQhEAAAAAAAAAAAAyxCIAAAAAAAAAAAAliEQAQAAAAAAAAAALEMgAgAAAAAAAAAAWIZABAAAAAAAAAAAsAyBCAAAAAAAAAAAYBkCEQAAAAAAAAAAwDIEIgAAAAAAAAAAgGUIRAAAAAAAAAAAAMsQiAAAAAAAAAAAAJYhEAEAAAAAAAAAACxDIAIAAAAAAAAAAFiGQAQAAAAAAAAAALAMgQgAAAAAAAAAAGAZAhEAAAAAAAAAAMAyBCIAAAAAAAAAAIBlCEQAAAAAAAAAAADLEIgAAAAAAAAAAACWIRABAAAAAAAAAAAsQyACAAAAAAAAAABYhkAEAAAAAAAAAACwDIEIAAAAAAAAAABgGQIRAAAAAAAAAADAMgQiAAAAAAAAAACAZQhEAAAAAAAAAAAAyxCIAAAAAAAAAAAAliEQAQAAAAAAAAAALEMgAgAAAAAAAAAAWIZABAAAAAAAAAAAsAyBCAAAAAAAAAAAYBkCEQAAAAAAAAAAwDIEIgAAAAAAAAAAgGUIRAAAAAAAAAAAAMsQiAAAAAAAAAAAAJYhEAEAAAAAAAAAACxDIAIAAAAAAAAAAFiGQAQAAAAAAAAAALAMgQgAAAAAAAAAAGAZAhEAAAAAAAAAAMAyBCIAAAAAAAAAAIBlCEQAAAAAAAAAAADLEIgAAAAAAAAAAACWIRABAAAAAAAAAAAsQyACAAAAAAAAAABYhkAEAAAAAAAAAACwDIEIAAAAAAAAAABgGQIRAAAAAAAAAADAMgQiAAAAAAAAAACAZQhEAAAAAAAAAAAAyxCIAAAAAAAAAAAAliEQAQAAAAAAAAAALEMgAgAAAAAAAAAAWIZABAAAAAAAAAAAsAyBCAAAAAAAAAAAYBkCEQAAAAAAAAAAwDIEIgAAAAAAAAAAgGUIRAAAAAAAAAAAAMsQiAAAAAAAAAAAAJYhEAEAAAAAAAAAACxDIAIAAAAAAAAAAFiGQAQAAAAAAAAAALAMgQgAAAAAAAAAAGAZAhEAAAAAAAAAAMAyBCIAAAAAAAAAAIBlCEQAAAAAAAAAAADLEIgAAAAAAAAAAACWIRABAAAAAAAAAAAsQyACAAAAAAAAAABYhkAEAAAAAAAAAACwDIEIAAAAAAAAAABgGQIRAAAAAAAAAADAMgQiAAAAAAAAAACAZQhEAAAAAAAAAAAAyxCIAAAAAAAAAAAAliEQAQAAAAAAAAAALEMgAgAAAAAAAAAAWIZABAAAAAAAAAAAsAyBCAAAAAAAAAAAYBkCEQAAAAAAAAAAwDIEIgAAAAAAAAAAgGUIRAAAAAAAAAAAAMsQiAAAAAAAAAAAAJYhEAEAAAAAAAAAACxDIAIAAAAAAAAAAFiGQAQAAAAAAAAAALAMgQgAAAAAAAAAAGAZAhEAAAAAAAAAAMAyBCIAAAAAAAAAAIBlCEQAAAAAAAAAAADLEIgAAAAAAAAAAACWIRABAAAAAAAAAAAsQyACAAAAAAAAAABYhkAEAAAAAAAAAACwDIEIAAAAAAAAAABgGQIRAAAAAAAAAADAMgQiAAAAAAAAAACAZQhEAAAAAAAAAAAAyxCIAAAAAAAAAAAAliEQAQAAAAAAAAAALEMgAgAAAAAAAAAAWIZABAAAAAAAAAAAsAyBCAAAAAAAAAAAYBkCEQAAAAAAAAAAwDIEIgAAAAAAAAAAgGUIRAAAAAAAAAAAAMsQiAAAAAAAAAAAAJYhEAEAAAAAAAAAACxDIAIAAAAAAAAAAFiGQAQAAAAAAAAAALAMgQgAAAAAAAAAAGAZAhEAAAAAAAAAAMAyBCIAAAAAAAAAAIBlCEQAAAAAAAAAAADLEIgAAAAAAAAAAACWIRABAAAAAAAAAAAsQyACAAAAAAAAAABYhkAEAAAAAAAAAACwDIEIAAAAAAAAAABgmf8fzbA8bkR5bxsAAAAASUVORK5CYII=" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "execution_count": 13 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "For higher number of features, computing the Shapley values exactly can be computationally expensive (exponential complexity).\n", + "In such cases, we can use approximation methods to estimate the Shapley values which are implemented in `shapiq.approximator` and under the hood of the `shapiq.TabularExplainer` class.\n", + "\n", + "### Using `shapiq.TreeExplainer` for exact Shapley Values with Tree-based Models\n", + "\n", + "For some models, the Shapley values can also be computed more efficiently using **model-specific** algorithms.\n", + "For example, tree-based models such as the random forest used here can be computed more efficiently using the `shapiq.TreeExplainer` class." + ] + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-10-31T12:27:06.820963Z", + "start_time": "2024-10-31T12:26:13.787785Z" + } + }, + "cell_type": "code", + "source": [ + "# create explainer with TreeExplainer\n", + "tree_explainer = shapiq.TreeExplainer(model=rf, index=\"SV\", max_order=1)\n", + "sv_tree = tree_explainer.explain(x_explain)\n", + "print(sv_tree)\n", + "print(\"\\nSum of Shapley values:\", sum(sv_tree.get_n_order_values(1)) + sv_tree.baseline_value)\n", + "print(\"Model prediction:\", y_explain[0])" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "InteractionValues(\n", + " index=SV, max_order=1, min_order=0, estimated=False, estimation_budget=None,\n", + " n_players=8, baseline_value=2.072542972807655,\n", + " Top 10 interactions:\n", + " (): 2.072542972807655\n", + " (5,): 1.0855435959797073\n", + " (7,): 0.6743221497016763\n", + " (1,): 0.5348654145349319\n", + " (6,): 0.24030709824004307\n", + " (0,): 0.2383984451389463\n", + " (3,): 0.06806156156611531\n", + " (2,): 0.026626899459955358\n", + " (4,): 0.014904862570882\n", + ")\n", + "\n", + "Sum of Shapley values: 4.955572999999912\n", + "Model prediction: 4.955573000000001\n" + ] + } + ], + "execution_count": 14 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-10-31T12:27:07.086169Z", + "start_time": "2024-10-31T12:27:06.822955Z" + } + }, + "cell_type": "code", + "source": [ + "# visualize the Shapley Values\n", + "sv_tree.plot_force(feature_names=feature_names, feature_values=x_explain, show=True)" + ], + "outputs": [ + { + "data": { + "text/plain": [ + "
" + ], + "image/png": "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a/rss8+8pj3PycnRt99+qyFDhgQ9/2h6GBHRhNx4440KDQ3V0qVLtWfPHg0dOlRTp04te75FixaaMWOGJGnx4sXauHGj0tLSNGzYMK/9pKSkqF27dtq+fbsWLlyozz//XMnJybrlllu8pn669tprNWXKFOXk5GjZsmVatmyZDhw4oH79+tU60tqnTx8VFBR4BTk8QkNDNXPmTLVp00arVq3S8uXL1bx5c02fPr1W+x47dqwGDRqkHTt2aMmSJSotLdVNN91Uabu4uDjdcccdGjhwoL7++mstWLBA69atU15eniZMmFCr9wIAAAAAAAACJTQ0VH379lV6enpZWkFBgebOnas2bdpozJgxZekpKSkaPny41qxZoyNHjpSlf/311woNDVVqampQ846myeVmaXQAAAAAAAAAaFQyMjL0j3/8QzfddFO91jZ94YUX1KlTJ1199dUW5A7wxogIAAAAAAAAAGhkmjVrpoEDB2r16tV1fu3evXt19uxZjR492oKcAZUxIgIAAAAAAAAAAFiGEREAAAAAAAAAAMAyBCIAAAAAAAAAAIBlCEQAAAAAAAAAAADLEIgAAAAAAAAAAACWIRABAAAAAAAAAAAsQyACAAAAAAAAAABYhkAEAAAAAAAAAACwDIEIAAAAAAAAAABgGQIRAAAAAAAAAADAMgQiAAAAAAAAAACAZQhEAAAAAAAAAAAAyxCIAAAAAAAAAACHmDp1qlwuV9lt3LhxdXq92+3WW2+9pQkTJqh58+aKiIhQu3bt9J3vfEdr166t9rX5+fl65plnNGbMGLVo0UIRERFq06aNRowYod/85jfKzc1twJGhKXO53W633ZkAAAAAAAAAgKbujTfe0M033+yVNnbsWC1fvrxWry8uLtaMGTM0e/Zsv8+7XC4988wz+sUvflHpub1792ry5MnavXt3lfs/fPiw2rdvX6u8ABWF2Z0BAAAAAAAAAGjqjh07prvvvrtB+/jb3/7mFYS49tprNWTIEK1cuVLLli2T2+3WPffco6FDh2rYsGFl250/f16TJk3S3r17JUmJiYmaPn26OnfurOLiYh07dkwbN25UaGhog/KHpotABAAAAAAAAADY7Kc//akyMjLUsWNHNW/eXFu2bKnzPl599dWy3y+77DJ9/PHHkqTS0lL17dtXO3bsUGlpqR555BHNnTu3bNsnn3yyLAiRmpqqlStXKjk5uYFHBJRjjQgAAAAAAAAAsNFrr72mefPmyeVy6ZVXXlFCQkK99rN///6y3/v371/2e0hIiPr06VP2ePHixSouLi57XDGAMXbsWP3gBz9Qu3btFB0drdTUVD388MPKz8+vV54AiUAEAAAAAAAAANjm6NGjuvfeeyVJP/vZzzR+/Ph67ysxMbHs923btpX97na7lZ6eXvY4Pz+/bATE0aNHdejQobLnnnvuOS1cuFDHjx9Xfn6+duzYod///ve6/PLLlZeXV++8oWkjEAEAAAAAAAAANrnjjjuUmZmpzp076/HHH2/QvqZMmVL2+8qVKzV16lT96U9/0sSJE70CEZKUkZEhSTp+/Hil/QwdOlQPPfSQJkyYUJa2bt06Pfzwww3KH5ou1ogAAAAAAAAAABu88sorWrhwoVwul1599VXFxcU1aH+PPPKIli9fXjZF00cffaSPPvrI77YRERGSpMLCQq/0tm3bauXKlYqMjJTb7daQIUO0adMmSdKbb76pP//5zw3KI5omRkQAAAAAAAAAQJDl5+frvvvukyTdddddGjt2bIP32bp1a23cuFG//vWv1b17d0VERKh169aaPHmyHnjgAa9t27VrJ0lKSkrySh8xYoQiIyMlSS6Xyytfhw4dUlFRUYPziaaHQAQAAAAAAAAABFl+fr6ysrIkSX//+9/lcrnKbitWrCjbbsWKFXK5XBo3blyt9tusWTM9/vjj2r17twoKCnTixAnNmzdP2dnZZdukpKSobdu2kqTu3buXBR78cbvdZb+HhYUpLIxJdlB3BCIAAAAAAAAAoJF47bXXvIIWFZWWliozM7PSa1atWqWXXnqp7PGPfvSjst/Dw8N15ZVXlj1et26d13RNq1atKvs9LS2t0nsCtUH4CgAAAAAAAACCLCIiQjfccIPf51asWKHTp09Lklq0aKGxY8cqNTW1xn3m5uaqTZs2uuqqq9S7d29FRkZq27Ztmjt3rkpLSyVJXbp00b333uv1ugcffFALFixQaWmpjh07pjFjxmjSpElas2aNNm7cWLbdL3/5y/oeLpo4l7vi2BrAZjk5OXK73XK5XIqNjbU7OwAAAAAAAEDQjRs3rmx6prFjx2r58uVlz7322mteIxoqXt7Nzs5WfHx8lfvt2rWrFixYoB49elR67qmnnipbs8KfX/ziF3r22WfrchhAGaZmgqO43e6yGwAAAAAAAIDai4qK0gMPPKDhw4erdevWCg8PV7NmzTRq1Cg9+eST2rZtm98ghCTde++9Wr16ta6//nq1bt1aYWFhSk5O1sSJEzV37lyCEGgQRkTAUbKzs8tGRMTFxdmdHQAAAAAAAABAAzEiAgAAAAAAAAAAWIZABAAAAAAAAAAAsAyBCAAAAAAAAAAAYBkCEQAAAAAAAAAAwDIEIgAAAAAAAAAAgGUIRAAAAAAAAAAAAMsQiAAAAAAAAAAAAJYhEAEAAAAAAAAAACxDIAIAAAAAAAAAAFgmzO4MAAAAAAAAAAACIzc3V263Wy6XSzExMXZnB5BEIAIAAAAAAAAALhqlpaVlgQjAKZiaCQAAAAAAAAAAWIZARBPz6KOPasiQIYqPj1erVq00bdo07dq1q8bXvf/+++rVq5eioqLUt29fLViwIAi5BRqP+tSt1157TS6Xy+sWFRUVpBwDjcPzzz+vfv36KSEhQQkJCRoxYoQWLlxY7Wtos4Ca1bVu0WYB9fPYY4/J5XLpnnvuqXY72i6gbmpTt2i7gNqZNWtWpbrSq1eval9Du4X6IBDRxKxYsUJ33nmn1q1bpyVLlqioqEhXXXWVcnJyqnzNmjVrNHPmTN1+++3asmWLpk2bpmnTpmn79u1BzDngbPWpW5KUkJCg48ePl90OHjwYpBwDjUP79u312GOPadOmTdq4caOuuOIKTZ06Venp6X63p80CaqeudUuizQLqasOGDXrxxRfVr1+/arej7QLqprZ1S6LtAmorNTXVq66sXr26ym1pt1BfLrfb7bY7E7DPqVOn1KpVK61YsUJjxozxu813v/td5eTkaN68eWVpw4cP14ABA/TCCy8END/Z2dllc9jFxcUFdN9AMNWmbr322mu65557lJmZGdzMAY1ccnKynnjiCd1+++2VngtmmwVcbKqrW7RZQN1kZ2dr0KBBeu655/Twww9rwIABevrpp/1uS9sF1F5d6hZtF5qyulxfmzVrlubMmaOtW7fWat+0W6gvRkQ0cVlZWZLMF8+qrF27VhMmTPBKmzhxotauXWtp3oDGrDZ1SzIfDlJSUtShQ4cae6ICTV1JSYneffdd5eTkaMSIEX63oc0C6q42dUuizQLq4s4779TkyZMrtUn+0HYBtVeXuiXRdgG1tWfPHrVr105dunTRTTfdpEOHDlW5Le0W6ivM7gzAPqWlpbrnnns0atQo9enTp8rtTpw4odatW3ultW7dWidOnLA6i0CjVNu61bNnT73yyivq16+fsrKy9Ne//lUjR45Uenq62rdvH8QcA862bds2jRgxQvn5+YqLi9Ps2bPVu3dvv9vSZgG1V5e6RZsF1N67776rzZs3a8OGDbXanrYLqJ261i3aLqB2hg0bptdee009e/bU8ePH9Yc//EGjR4/W9u3bFR8fX2l72i3UF4GIJuzOO+/U9u3bq533DUDd1bZujRgxwqvn6ciRI3XppZfqxRdf1J/+9Cerswk0Gj179tTWrVuVlZWlDz74QLfccotWrFhR5QVTALVTl7pFmwXUzuHDh3X33XdryZIlLIoLBFB96hZtF1A7kyZNKvu9X79+GjZsmFJSUvTee+/5nbITqC8CEU3UXXfdpXnz5mnlypU19gRo06aNvv32W6+0b7/9Vm3atLEyi0CjVJe65Ss8PFwDBw7U3r17Lcod0DhFRESoW7dukqS0tDRt2LBBzzzzjF588cVK29JmAbVXl7rlizYL8G/Tpk06efKkBg0aVJZWUlKilStX6h//+IcKCgoUGhrq9RraLqBm9albvmi7gNpJSkpSjx49qqwrtFuoL9aIaGLcbrfuuusuzZ49W59++qk6d+5c42tGjBihZcuWeaUtWbKk2jmEgaamPnXLV0lJibZt26a2bdtakEPg4lFaWqqCggK/z9FmAfVXXd3yRZsF+Dd+/Hht27ZNW7duLbsNHjxYN910k7Zu3er3QiltF1Cz+tQtX7RdQO1kZ2dr3759VdYV2i3UFyMimpg777xTb7/9tubOnav4+Piy+dsSExMVHR0tSbr55pt1ySWX6NFHH5Uk3X333Ro7dqyefPJJTZ48We+++642btyol156ybbjAJymPnXrj3/8o4YPH65u3bopMzNTTzzxhA4ePKgf//jHth0H4DS/+c1vNGnSJHXs2FHnz5/X22+/reXLl2vRokWSaLOA+qpr3aLNAmonPj6+0hphsbGxat68eVk6bRdQd/WpW7RdQO3cf//9mjJlilJSUnTs2DE99NBDCg0N1cyZMyXRbiFwCEQ0Mc8//7wkady4cV7pr776qm699VZJ0qFDhxQSUj5YZuTIkXr77bf1u9/9Tv/93/+t7t27a86cOdUuwgs0NfWpWxkZGbrjjjt04sQJ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RAAAAAAAAAAAAMsQiAAAAAAAAAAAAJYhEAEAAAAAAAAAACxDIAIAAAAAAAAAAFiGQAQAAAAAAAAAALAMgQgAAAAAAAAAAGAZAhEAAAAAAAAAAMAyBCIAAAAAAAAAAIBlCEQAAAAAAAAAAADLEIgAAAAAAAAAAACWIRABAAAAAAAAAAAsQyACAAAAAAAAAABYhkAEAAAAAAAAAACwDIEIAAAAAAAAAABgGQIRAAAAAAAAAADAMgQiAAAAAAAAAACAZQhEAAAAAAAAAAAAyxCIAAAAAAAAAAAAliEQAQAAAAAAAAAALEMgAgAAAAAAAAAAWIZABAAAAAAAAAAAsAyBCAAAAAAAAAAAYBkCEQAAAAAAAAAAwDIEIgAAAAAAAAAAgGUIRAAAAAAAAAAAAMsQiAAAAAAAAAAAAJYhEAEAAAAAAAAAACxDIAIAAAAAAAAAAFiGQAQAAAAAAAAAALAMgQgAAAAAAAAAAGAZAhEAAAAAAAAAAMAyBCIAAAAAAAAAAIBlCEQAAAAAAAAAAADLEIgAAAAAAAAAAACWIRABAAAAAAAAAAAsQyACAAAAAAAAAABYhkAEAAAAAAAAAACwDIEIAAAAAAAAAABgGQIRAAAAAAAAAADAMgQiAAAAAAAAAACAZQhEAAAAAAAAAAAAyxCIAAAAAAAAAAAAliEQAQAAAAAAAAAALEMgAgAAAAAAAAAAWIZABAAAAAAAAAAAsAyBCAAAAAAAAAAAYBkCEQAAAAAAAAAAwDIEIgAAAAAAAAAAgGUIRAAAAAAAAAAAAMsQiAAAAAAAAAAAAJYhEAEAAAAAAAAAACxDIAIAAAAAAAAAAFiGQAQAAAAAAAAAALAMgQgAAAAAAAAAAGCZ/w+D8Oyxiq4zWgAAAABJRU5ErkJggg==" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "execution_count": 15 } ], "metadata": { diff --git a/shapiq/approximator/_base.py b/shapiq/approximator/_base.py index 7e377bad..c6782402 100644 --- a/shapiq/approximator/_base.py +++ b/shapiq/approximator/_base.py @@ -320,6 +320,11 @@ def aggregate_interaction_values( """ from ..aggregation import aggregate_interaction_values + if player_set is not None: + raise NotImplementedError( + "Aggregating interaction values for a subset of players is not implemented." + ) + return aggregate_interaction_values(base_interactions, order=order) @staticmethod diff --git a/shapiq/datasets/_all.py b/shapiq/datasets/_all.py index 631f2157..993cb363 100644 --- a/shapiq/datasets/_all.py +++ b/shapiq/datasets/_all.py @@ -1,7 +1,9 @@ """This module contains functions to load datasets.""" import os +from typing import Union +import numpy as np import pandas as pd GITHUB_DATA_URL = "https://raw.githubusercontent.com/mmschlk/shapiq/main/data/" @@ -29,7 +31,9 @@ def _try_load(csv_file_name: str) -> pd.DataFrame: return data -def load_california_housing(to_numpy=False) -> tuple[pd.DataFrame, pd.Series]: +def load_california_housing( + to_numpy=False, +) -> Union[tuple[pd.DataFrame, pd.Series], tuple[np.ndarray, np.ndarray]]: """Load the California housing dataset. Args: @@ -37,6 +41,12 @@ def load_california_housing(to_numpy=False) -> tuple[pd.DataFrame, pd.Series]: Returns: The California housing dataset as a pandas DataFrame. + + Example: + >>> from shapiq.datasets import load_california_housing + >>> x_data, y_data = load_california_housing() + >>> print(x_data.shape, y_data.shape) + ((20640, 8), (20640,)) """ dataset = _try_load("california_housing.csv") class_label = "MedHouseVal" @@ -49,17 +59,25 @@ def load_california_housing(to_numpy=False) -> tuple[pd.DataFrame, pd.Series]: return x_data, y_data -def load_bike_sharing(to_numpy=False) -> tuple[pd.DataFrame, pd.Series]: - """Load the bike-sharing dataset from openml. - - Args: - to_numpy: Return numpy objects instead of pandas. ``Default is False.`` +def load_bike_sharing( + to_numpy=False, +) -> Union[tuple[pd.DataFrame, pd.Series], tuple[np.ndarray, np.ndarray]]: + """Load the bike-sharing dataset from openml and preprocess it. Note: The function requires the `sklearn` package to be installed. + Args: + to_numpy: Return numpy objects instead of pandas. ``Default is False.`` + Returns: The bike-sharing dataset as a pandas DataFrame. + + Example: + >>> from shapiq.datasets import load_bike_sharing + >>> x_data, y_data = load_bike_sharing() + >>> print(x_data.shape, y_data.shape) + ((17379, 12), (17379,)) """ from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline @@ -112,19 +130,27 @@ def load_bike_sharing(to_numpy=False) -> tuple[pd.DataFrame, pd.Series]: return x_data, y_data -def load_adult_census(to_numpy=False) -> tuple[pd.DataFrame, pd.Series]: +def load_adult_census( + to_numpy=False, +) -> Union[tuple[pd.DataFrame, pd.Series], tuple[np.ndarray, np.ndarray]]: """Load the adult census dataset from the UCI Machine Learning Repository. Original source: https://archive.ics.uci.edu/ml/datasets/adult - Args: - to_numpy: Return numpy objects instead of pandas. Default is ``False``. - Note: The function requires the `sklearn` package to be installed. + Args: + to_numpy: Return numpy objects instead of pandas. Default is ``False``. + Returns: The adult census dataset as a pandas DataFrame. + + Example: + >>> from shapiq.datasets import load_adult_census + >>> x_data, y_data = load_adult_census() + >>> print(x_data.shape, y_data.shape) + ((45222, 14), (45222,)) """ from sklearn.compose import ColumnTransformer from sklearn.impute import SimpleImputer diff --git a/shapiq/explainer/tabular.py b/shapiq/explainer/tabular.py index b103e189..85887ea7 100644 --- a/shapiq/explainer/tabular.py +++ b/shapiq/explainer/tabular.py @@ -86,7 +86,7 @@ def __init__( random_state: Optional[int] = None, **kwargs, ) -> None: - from shapiq.games.imputer import ConditionalImputer, MarginalImputer + from shapiq.games.imputer import BaselineImputer, ConditionalImputer, MarginalImputer if index not in AVAILABLE_INDICES: raise ValueError(f"Invalid index `{index}`. " f"Valid indices are {AVAILABLE_INDICES}.") @@ -102,7 +102,15 @@ def __init__( self._imputer = ConditionalImputer( self.predict, self.data, random_state=random_state, **kwargs ) - elif isinstance(imputer, MarginalImputer) or isinstance(imputer, ConditionalImputer): + elif imputer == "baseline": + self._imputer = BaselineImputer( + self.predict, self.data, random_state=random_state, **kwargs + ) + elif ( + isinstance(imputer, MarginalImputer) + or isinstance(imputer, ConditionalImputer) + or isinstance(imputer, BaselineImputer) + ): self._imputer = imputer else: raise ValueError( @@ -143,7 +151,7 @@ def explain( imputer = self._imputer.fit(x) # explain - interaction_values = self._approximator.approximate(budget=budget, game=imputer) + interaction_values = self._approximator(budget=budget, game=imputer) interaction_values.baseline_value = self.baseline_value return interaction_values diff --git a/shapiq/games/base.py b/shapiq/games/base.py index 67f491f0..5677f59f 100644 --- a/shapiq/games/base.py +++ b/shapiq/games/base.py @@ -157,7 +157,7 @@ def precomputed(self) -> bool: @property def normalize(self) -> bool: - """Indication whether the game values are normalized.""" + """Indication whether the game values are getting normalized.""" return self.normalization_value != 0 @property @@ -167,17 +167,28 @@ def is_normalized(self) -> bool: def _check_coalitions( self, - coalitions: Union[np.ndarray, list[Union[tuple[int], tuple[str]]]], + coalitions: Union[ + np.ndarray, + list[tuple[int, ...]], + list[tuple[str, ...]], + tuple[int, ...], + tuple[str, ...], + ], ) -> np.ndarray: - """ + """Validates the coalitions and convert them to one-hot encoding. + Check if the coalitions are in the correct format and convert them to one-hot encoding. - The format may either be a numpy array containg the coalitions in one-hot encoding or a list of tuples with integers or strings. + The format may either be a numpy array containg the coalitions in one-hot encoding or a + list of tuples with integers or strings. + Args: coalitions: The coalitions to convert to one-hot encoding. Returns: np.ndarray: The coalitions in the correct format + Raises: TypeError: If the coalitions are not in the correct format. + Examples: >>> coalitions = np.asarray([[1, 0, 0, 0], [0, 1, 1, 0]]) >>> coalitions = [(0, 1), (1, 2)] @@ -189,79 +200,63 @@ def _check_coalitions( >>> coalitions = [1, 0, 0, 0] >>> coalitions = [(1, "Alice")] >>> coalitions = np.array([1, -1, 2]) - - """ error_message = ( - "List may only contain tuples of integers or strings." - "The tuples are not allowed to have heterogeneous types." - "Reconcile the docs for correct format of coalitions." + "List may only contain tuples of integers or strings. The tuples are not allowed to " + "have heterogeneous types. See the docs for correct format of coalitions. If strings " + "are used, the player_name_lookup has to be provided during initialization." ) - + # check for array input and do validation if isinstance(coalitions, np.ndarray): - - # Check that coalition is contained in array - if len(coalitions) == 0: + if len(coalitions) == 0: # check that coalition is contained in array raise TypeError("The array of coalitions is empty.") - - # Check if single coalition is correctly given - if coalitions.ndim == 1: + if coalitions.ndim == 1: # check if single coalition is correctly given if len(coalitions) < self.n_players or len(coalitions) > self.n_players: raise TypeError( "The array of coalitions is not correctly formatted." f"It should have a length of {self.n_players}" ) coalitions = coalitions.reshape((1, self.n_players)) - - # Check that all coalitions have the correct number of players - if coalitions.shape[1] != self.n_players: + if coalitions.shape[1] != self.n_players: # check if players match raise TypeError( - f"The number of players in the coalitions ({coalitions.shape[1]}) does not match " + f"Number of players in the coalitions ({coalitions.shape[1]}) does not match " f"the number of players in the game ({self.n_players})." ) - # TODO maybe remove this, as it might increase runtime unnecessarily - # Check that values of numpy array are either 0 or 1 + # check that values of numpy array are either 0 or 1 if not np.all(np.logical_or(coalitions == 0, coalitions == 1)): raise TypeError("The values in the array of coalitions are not binary.") - return coalitions - - # We now assume to work with list of tuples + # try for list of tuples if isinstance(coalitions, tuple): - # if by any chance a tuple was given wrap into a list coalitions = [coalitions] - try: # convert list of tuples to one-hot encoding coalitions = transform_coalitions_to_array(coalitions, self.n_players) - return coalitions - except Exception as err: - # It may either be the tuples contain strings or wrong format - if self.player_name_lookup is not None: - # We now assume the tuples to contain strings - try: - coalitions = [ - ( - tuple(self.player_name_lookup[player] for player in coalition) - if coalition != tuple() - else tuple() - ) - for coalition in coalitions - ] - coalitions = transform_coalitions_to_array(coalitions, self.n_players) - - return coalitions - except Exception as err: - raise TypeError(error_message) from err - - raise TypeError(error_message) from err + except (IndexError, TypeError): + pass + # assuming str input + if self.player_name_lookup is None: + raise ValueError("Player names are not provided. Cannot convert string to integer.") + try: + coalitions_from_str = [] + for coalition in coalitions: + coal_indices = sorted([self.player_name_lookup[player] for player in coalition]) + coalitions_from_str.append(tuple(coal_indices)) + coalitions = transform_coalitions_to_array(coalitions_from_str, self.n_players) + return coalitions + except Exception as error: + raise TypeError(error_message) from error def __call__( self, coalitions: Union[ - np.ndarray, list[Union[tuple[int], tuple[str]]], tuple[Union[int, str]], str + np.ndarray, + list[tuple[int, ...]], + list[tuple[str, ...]], + tuple[int, ...], + tuple[str, ...], ], verbose: bool = False, ) -> np.ndarray: @@ -275,11 +270,8 @@ def __call__( Returns: The values of the coalitions. """ - # check if coalitions are correct format - coalitions = self._check_coalitions(coalitions) - + coalitions = self._check_coalitions(coalitions) # validate and convert input coalitions verbose = verbose or self.verbose - if not self.precomputed and not verbose: values = self.value_function(coalitions) elif not self.precomputed and verbose: @@ -291,7 +283,6 @@ def __call__( values[i] = self.value_function(coalition)[0] else: values = self._lookup_coalitions(coalitions) # lookup the values present in the storage - return values - self.normalization_value def _lookup_coalitions(self, coalitions: np.ndarray) -> np.ndarray: diff --git a/shapiq/games/imputer/base.py b/shapiq/games/imputer/base.py index b093a95e..00a268be 100644 --- a/shapiq/games/imputer/base.py +++ b/shapiq/games/imputer/base.py @@ -29,6 +29,8 @@ class Imputer(Game): data: The background data to use for the imputer. model: The model to impute missing values for as a callable function. sample_size: The number of samples to draw from the background data. + random_state: The random state to use for sampling. + empty_prediction: The model's prediction on an empty data point (all features missing). Properties: x: The explanation point to use the imputer on. @@ -54,10 +56,11 @@ def __init__( data = data.reshape(1, data.shape[0]) self.data = data self.sample_size = sample_size + self.empty_prediction: float = 0.0 # will be overwritten in the subclasses self.n_features = self.data.shape[1] self._cat_features: list = [] if categorical_features is None else categorical_features - self._random_state = random_state - self._rng = np.random.default_rng(self._random_state) + self.random_state = random_state + self._rng = np.random.default_rng(self.random_state) # fit x self._x: Optional[np.ndarray] = None # will be overwritten @ fit @@ -98,3 +101,16 @@ def fit(self, x: np.ndarray) -> "Imputer": if self._x.ndim == 1: self._x = self._x.reshape(1, x.shape[0]) return self + + def insert_empty_value(self, outputs: np.ndarray, coalitions: np.ndarray) -> np.ndarray: + """Inserts the empty value into the outputs. + + Args: + outputs: The model's predictions on the imputed data points. + coalitions: The coalitions for which the model's predictions were made. + + Returns: + The model's predictions with the empty value inserted for the empty coalitions. + """ + outputs[~np.any(coalitions, axis=1)] = self.empty_prediction + return outputs diff --git a/shapiq/games/imputer/baseline_imputer.py b/shapiq/games/imputer/baseline_imputer.py index 86cf19ab..b23c41b4 100644 --- a/shapiq/games/imputer/baseline_imputer.py +++ b/shapiq/games/imputer/baseline_imputer.py @@ -67,7 +67,6 @@ def __init__( self.init_background(self.data) # set empty value and normalization - self.empty_prediction: float = self._calc_empty_prediction() if normalize: self.normalization_value = self.empty_prediction @@ -135,9 +134,10 @@ def init_background(self, data: np.ndarray) -> "BaselineImputer": ) self._cat_features.append(feature) self.baseline_values[0, feature] = summarized_feature + self.calc_empty_prediction() # reset the empty prediction to the new baseline values return self - def _calc_empty_prediction(self) -> float: + def calc_empty_prediction(self) -> float: """Runs the model on empty data points (all features missing) to get the empty prediction. Returns: @@ -145,6 +145,7 @@ def _calc_empty_prediction(self) -> float: """ empty_predictions = self.predict(self.baseline_values) empty_prediction = float(empty_predictions[0]) + self.empty_prediction = empty_prediction if self.normalize: # reset the normalization value self.normalization_value = empty_prediction return empty_prediction diff --git a/shapiq/games/imputer/conditional_imputer.py b/shapiq/games/imputer/conditional_imputer.py index 063246c1..b8f42c6b 100644 --- a/shapiq/games/imputer/conditional_imputer.py +++ b/shapiq/games/imputer/conditional_imputer.py @@ -61,7 +61,7 @@ def __init__( self.init_background(data=data) # set empty value and normalization - self.empty_prediction: float = self._calc_empty_prediction() + self.empty_prediction: float = self.calc_empty_prediction() if normalize: self.normalization_value = self.empty_prediction @@ -88,7 +88,7 @@ def init_background(self, data: np.ndarray) -> "ConditionalImputer": coalition_sampler = CoalitionSampler( n_players=n_features, sampling_weights=np.array([1e-7 for _ in range(n_features + 1)]), - random_state=self._random_state, + random_state=self.random_state, ) coalitions_matrix = [] for _ in range(data.shape[0]): @@ -98,7 +98,7 @@ def init_background(self, data: np.ndarray) -> "ConditionalImputer": # (data.shape[0] * self.conditional_budget, n_features) X_masked = X_tiled.copy() X_masked[coalitions_matrix] = np.NaN - tree_embedder = xgboost.XGBRegressor(random_state=self._random_state) + tree_embedder = xgboost.XGBRegressor(random_state=self.random_state) tree_embedder.fit(X_masked, X_tiled) self._data_embedded = tree_embedder.apply(data) self._tree_embedder = tree_embedder @@ -125,6 +125,8 @@ def value_function(self, coalitions: np.ndarray[bool]) -> np.ndarray[float]: x_tiled[~coalitions_tiled] = background_data_tiled[~coalitions_tiled] predictions = self.predict(x_tiled) avg_predictions = predictions.reshape(n_coalitions, -1).mean(axis=1) + # insert the better approximate empty prediction for the empty coalitions + avg_predictions[~np.any(coalitions, axis=1)] = self.empty_prediction return avg_predictions def _sample_background_data(self) -> np.ndarray: @@ -144,7 +146,7 @@ def _sample_background_data(self) -> np.ndarray: return conditional_data[idc, :] return conditional_data - def _calc_empty_prediction(self) -> float: + def calc_empty_prediction(self) -> float: """Runs the model on empty data points (all features missing) to get the empty prediction. Returns: diff --git a/shapiq/games/imputer/marginal_imputer.py b/shapiq/games/imputer/marginal_imputer.py index c09d1f66..be13354f 100644 --- a/shapiq/games/imputer/marginal_imputer.py +++ b/shapiq/games/imputer/marginal_imputer.py @@ -32,9 +32,6 @@ class MarginalImputer(Imputer): sample_size: The number of samples to draw from the background data. Only used if ``sample_replacements`` is ``True``. Increasing this value will linearly increase the runtime of the explainer. Defaults to ``100``. - categorical_features: A list of indices of the categorical features in the background data. - If no categorical features are given, all features are assumed to be numerical or in - string format (where ``np.mean`` fails) features. Defaults to ``None``. joint_marginal_distribution: A flag to sample the replacement values from the joint marginal distribution. If ``False``, the replacement values are sampled independently for each feature. If ``True``, the replacement values are sampled from the joint marginal @@ -71,7 +68,7 @@ def __init__( sample_replacements: bool = True, sample_size: int = 100, categorical_features: list[int] = None, - joint_marginal_distribution: bool = False, + joint_marginal_distribution: bool = True, normalize: bool = True, random_state: Optional[int] = None, ) -> None: @@ -81,27 +78,27 @@ def __init__( # setup attributes self.joint_marginal_distribution: bool = joint_marginal_distribution - self.replacement_data: np.ndarray = np.zeros((1, self.n_features)) # will be overwritten + self.replacement_data: np.ndarray = np.zeros( + (1, self.n_features) + ) # overwritten at init_background self.init_background(self.data) - # set empty value and normalization - self.empty_prediction: float = self._calc_empty_prediction() - if normalize: + if normalize: # update normalization value self.normalization_value = self.empty_prediction def value_function(self, coalitions: np.ndarray) -> np.ndarray: """Imputes the missing values of a data point and calls the model. Args: - coalitions: A boolean array indicating which features are present (``True``) and which are - missing (``False``). The shape of the array must be ``(n_subsets, n_features)``. + coalitions: A boolean array indicating which features are present (``True``) and which + are missing (``False``). The shape of the array must be ``(n_subsets, n_features)``. Returns: The model's predictions on the imputed data points. The shape of the array is ``(n_subsets, n_outputs)``. """ n_coalitions = coalitions.shape[0] - replacement_data = self._sample_replacement_values(self.sample_size) + replacement_data = self._sample_replacement_data(self.sample_size) sample_size = replacement_data.shape[0] outputs = np.zeros((sample_size, n_coalitions)) imputed_data = np.tile(np.copy(self._x), (n_coalitions, 1)) @@ -111,6 +108,8 @@ def value_function(self, coalitions: np.ndarray) -> np.ndarray: predictions = self.predict(imputed_data) outputs[j] = predictions outputs = np.mean(outputs, axis=0) # average over the samples + # insert the better approximate empty prediction for the empty coalitions + outputs[~np.any(coalitions, axis=1)] = self.empty_prediction return outputs def init_background(self, data: np.ndarray) -> "MarginalImputer": @@ -133,36 +132,50 @@ def init_background(self, data: np.ndarray) -> "MarginalImputer": >>> new_data = np.random.rand(10, 3) >>> imputer.init_background(data=new_data) """ - self.replacement_data = data + self.replacement_data = np.copy(data) if self.sample_size > self.replacement_data.shape[0]: warnings.warn(UserWarning(_too_large_sample_size_warning)) self.sample_size = self.replacement_data.shape[0] + self.calc_empty_prediction() # reset the empty prediction to the new background data return self - def _sample_replacement_values(self, sample_size: int) -> np.ndarray: - """Samples replacement values from the background data.""" + def _sample_replacement_data(self, sample_size: Optional[int] = None) -> np.ndarray: + """Samples replacement values from the background data. + + Args: + sample_size: The number of replacement values to sample. If ``None``, all replacement + values are sampled. Defaults to ``None``. + + Returns: + The replacement values as a two-dimensional array with shape + ``(sample_size, n_features)``. + """ replacement_data = np.copy(self.replacement_data) - rng = np.random.default_rng(self._random_state) - if not self.joint_marginal_distribution: + rng = np.random.default_rng(self.random_state) + if not self.joint_marginal_distribution: # shuffle data to break joint marginal dist. for feature in range(self.n_features): rng.shuffle(replacement_data[:, feature]) - # sample replacement values n_samples = replacement_data.shape[0] + if sample_size is None or sample_size == n_samples: + return replacement_data if sample_size > n_samples: - sample_size = n_samples warnings.warn(UserWarning(_too_large_sample_size_warning)) + return replacement_data + # sample replacement values replacement_idx = rng.choice(n_samples, size=sample_size, replace=False) replacement_data = replacement_data[replacement_idx] return replacement_data - def _calc_empty_prediction(self) -> float: + def calc_empty_prediction(self) -> float: """Runs the model on empty data points (all features missing) to get the empty prediction. Returns: The empty prediction. """ - empty_predictions = self.predict(self.replacement_data) + background_data = self._sample_replacement_data() + empty_predictions = self.predict(background_data) empty_prediction = float(np.mean(empty_predictions)) + self.empty_prediction = empty_prediction if self.normalize: # reset the normalization value self.normalization_value = empty_prediction return empty_prediction diff --git a/shapiq/plot/stacked_bar.py b/shapiq/plot/stacked_bar.py index a782030d..08ca7140 100644 --- a/shapiq/plot/stacked_bar.py +++ b/shapiq/plot/stacked_bar.py @@ -137,15 +137,11 @@ def stacked_bar_plot( ) # set title and labels if not provided - - ( - axis.set_title(f"n-SII values up to order ${max_order}$") - if title is None - else axis.set_title(title) - ) + if title is not None: + axis.set_title(title) axis.set_xlabel("features") if xlabel is None else axis.set_xlabel(xlabel) - axis.set_ylabel("n-SII values") if ylabel is None else axis.set_ylabel(ylabel) + axis.set_ylabel("SI values") if ylabel is None else axis.set_ylabel(ylabel) plt.tight_layout() diff --git a/shapiq/utils/sets.py b/shapiq/utils/sets.py index a6500a6c..ea6fcc2e 100644 --- a/shapiq/utils/sets.py +++ b/shapiq/utils/sets.py @@ -218,7 +218,7 @@ def generate_interaction_lookup( def transform_coalitions_to_array( - coalitions: Collection[tuple[int]], n_players: Optional[int] = None + coalitions: Collection[tuple[int, ...]], n_players: Optional[int] = None ) -> np.ndarray: """Transforms a collection of coalitions to a binary array (one-hot encodings). diff --git a/tests/test_abstract_classes.py b/tests/test_abstract_classes.py index 2f5808de..100e3575 100644 --- a/tests/test_abstract_classes.py +++ b/tests/test_abstract_classes.py @@ -45,7 +45,7 @@ def model(x): assert np.all(imputer.data == data) assert imputer.n_features == 3 assert imputer._cat_features == [] - assert imputer._random_state is None + assert imputer.random_state is None assert imputer._rng is not None with pytest.raises(NotImplementedError): diff --git a/tests/tests_explainer/test_explainer_tabular.py b/tests/tests_explainer/test_explainer_tabular.py index 0cc403af..b765516c 100644 --- a/tests/tests_explainer/test_explainer_tabular.py +++ b/tests/tests_explainer/test_explainer_tabular.py @@ -27,7 +27,7 @@ def data(): INDICES = ["SII", "k-SII", "STII", "FSII"] MAX_ORDERS = [2, 3] -IMPUTER = ["marginal", "conditional"] +IMPUTER = ["marginal", "conditional", "baseline"] APPROXIMATOR = ["regression", "montecarlo", "permutation"] @@ -165,3 +165,10 @@ def test_explain(dt_model, data, index, budget, max_order, imputer): assert np.allclose( interaction_values0.get_n_order_values(2), interaction_values2.get_n_order_values(2) ) + + # test for efficiency + if index in ("FSII", "k-SII"): + prediction = float(model_function(x)[0]) + sum_of_values = float(np.sum(interaction_values.values) + interaction_values.baseline_value) + assert interaction_values[()] == 0.0 + assert pytest.approx(sum_of_values, 0.01) == prediction diff --git a/tests/tests_games/test_base_game.py b/tests/tests_games/test_base_game.py index 5e3ef654..4fa9226a 100644 --- a/tests/tests_games/test_base_game.py +++ b/tests/tests_games/test_base_game.py @@ -87,11 +87,9 @@ def value_function(self, coalition): # test string calls with missing player names test_game2 = TestGame(n=n_players) - with pytest.raises(TypeError): - assert test_game2("Alice") == 0.0 - with pytest.raises(TypeError): + with pytest.raises(ValueError): assert test_game2(("Bob",)) == 0.0 - with pytest.raises(TypeError): + with pytest.raises(ValueError): assert test_game2([("Charlie",)]) == 0.0 diff --git a/tests/tests_imputer/test_baseline_imputer.py b/tests/tests_imputer/test_baseline_imputer.py index 5bb9dab1..b3c8c976 100644 --- a/tests/tests_imputer/test_baseline_imputer.py +++ b/tests/tests_imputer/test_baseline_imputer.py @@ -49,7 +49,7 @@ def test_baseline_imputer_with_model(dt_reg_model, background_reg_dataset): ) assert np.array_equal(imputer.x[0], x) assert imputer.sample_size == 1 - assert imputer._random_state == 42 + assert imputer.random_state == 42 assert imputer.n_features == data.shape[1] imputed_values = imputer(coalitions) assert len(imputed_values) == 3 @@ -109,7 +109,7 @@ def model(x: np.ndarray) -> np.ndarray: random_state=42, ) assert imputer.sample_size == 1 # sample size is always 1 for baseline imputer - assert imputer._random_state == 42 + assert imputer.random_state == 42 assert imputer.n_features == 3 # call with two inputs @@ -129,6 +129,6 @@ def model(x: np.ndarray) -> np.ndarray: imputer.fit(x) assert np.array_equal(imputer.x, x) assert imputer.n_features == 3 - assert imputer._random_state == 42 + assert imputer.random_state == 42 imputer.fit(x=np.ones((n_features,))) # test with vector assert np.array_equal(imputer.x, np.ones((1, n_features))) diff --git a/tests/tests_imputer/test_conditional_imputer.py b/tests/tests_imputer/test_conditional_imputer.py index b61dc2b0..3eb5e5e2 100644 --- a/tests/tests_imputer/test_conditional_imputer.py +++ b/tests/tests_imputer/test_conditional_imputer.py @@ -25,7 +25,7 @@ def model(x: np.ndarray) -> np.ndarray: ) assert np.array_equal(imputer._x, x) assert imputer.sample_size == 9 - assert imputer._random_state == 42 + assert imputer.random_state == 42 assert imputer.n_features == 3 # test raise warning with non generative method diff --git a/tests/tests_imputer/test_marginal_imputer.py b/tests/tests_imputer/test_marginal_imputer.py index 293dc445..83357814 100644 --- a/tests/tests_imputer/test_marginal_imputer.py +++ b/tests/tests_imputer/test_marginal_imputer.py @@ -22,7 +22,7 @@ def model(x: np.ndarray) -> np.ndarray: random_state=42, ) assert imputer.sample_size == 10 - assert imputer._random_state == 42 + assert imputer.random_state == 42 assert imputer.n_features == 3 # test with x @@ -35,7 +35,7 @@ def model(x: np.ndarray) -> np.ndarray: ) assert np.array_equal(imputer._x, x) assert imputer.n_features == 3 - assert imputer._random_state == 42 + assert imputer.random_state == 42 # check with categorical features and a wrong numerical feature @@ -95,7 +95,7 @@ def model(x: np.ndarray) -> np.ndarray: random_state=42, joint_marginal_distribution=False, ) - replacement_data_independent = imputer._sample_replacement_values(3) + replacement_data_independent = imputer._sample_replacement_data(3) print(replacement_data_independent) imputer = MarginalImputer( @@ -106,7 +106,7 @@ def model(x: np.ndarray) -> np.ndarray: random_state=42, joint_marginal_distribution=True, ) - replacement_data_joint = imputer._sample_replacement_values(3) + replacement_data_joint = imputer._sample_replacement_data(3) for i in range(3): assert tuple(replacement_data_joint[i]) in data_as_tuples # the below only works because of the random seed (might break in future)