We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[BUG] Maximum feature cardinality in WholeDatasetDrift is not configurable
**Describe the bug**
When running the WholeDatasetDrift on a dataset with a feature that has cardinality greater then 255, the following error is thrown by the HistGradientBoostingClassifier:

```
/usr/local/lib/python3.9/dist-packages/sklearn/ensemble/_hist_gradient_boosting/gradient_boosting.py in _check_categories(self, X)
    186 
    187                 if categories.size > self.max_bins:
--> 188                     raise ValueError(
    189                         f"Categorical feature at index {f_idx} is "
    190                         "expected to have a "

ValueError: Categorical feature at index 30 is expected to have a cardinality <= 255
```

There is no way to adjust the max_bins parameter of HistGradientBoostingClassifier. Alternatively, the feature cardinality could be reduced before training the HistGradientBoostingClassifier using the max_num_categories parameter of WholeDatasetDrift

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `deepchecks/core/check_utils/whole_dataset_drift_utils.py`
Content:
```
1 # ----------------------------------------------------------------------------
2 # Copyright (C) 2021-2022 Deepchecks (https://www.deepchecks.com)
3 #
4 # This file is part of Deepchecks.
5 # Deepchecks is distributed under the terms of the GNU Affero General
6 # Public License (version 3 or later).
7 # You should have received a copy of the GNU Affero General Public License
8 # along with Deepchecks.  If not, see <http://www.gnu.org/licenses/>.
9 # ----------------------------------------------------------------------------
10 #
11 """Module containing common WholeDatasetDriftCheck (domain classifier drift) utils."""
12 
13 from typing import List, Optional
14 import warnings
15 
16 import numpy as np
17 import pandas as pd
18 
19 from sklearn.pipeline import Pipeline
20 from sklearn.compose import ColumnTransformer
21 
22 with warnings.catch_warnings():
23     warnings.simplefilter('ignore')
24     from sklearn.experimental import enable_hist_gradient_boosting  # noqa # pylint: disable=unused-import
25 
26 from sklearn.ensemble import HistGradientBoostingClassifier
27 from sklearn.metrics import roc_auc_score
28 from sklearn.preprocessing import OrdinalEncoder
29 from sklearn.model_selection import train_test_split
30 import plotly.graph_objects as go
31 
32 from deepchecks.tabular import Dataset
33 from deepchecks.utils.distribution.plot import feature_distribution_traces, drift_score_bar_traces
34 from deepchecks.utils.features import N_TOP_MESSAGE, calculate_feature_importance_or_none
35 from deepchecks.utils.function import run_available_kwargs
36 from deepchecks.utils.strings import format_percent
37 from deepchecks.utils.typing import Hashable
38 
39 
40 def run_whole_dataset_drift(train_dataframe: pd.DataFrame, test_dataframe: pd.DataFrame,
41                             numerical_features: List[Hashable], cat_features: List[Hashable], sample_size: int,
42                             random_state: int, test_size: float, n_top_columns: int, min_feature_importance: float,
43                             max_num_categories: Optional[int], min_meaningful_drift_score: float):
44     """Calculate whole dataset drift."""
45     domain_classifier = generate_model(numerical_features, cat_features, random_state)
46 
47     train_sample_df = train_dataframe.sample(sample_size, random_state=random_state)
48     test_sample_df = test_dataframe.sample(sample_size, random_state=random_state)
49 
50     # create new dataset, with label denoting whether sample belongs to test dataset
51     domain_class_df = pd.concat([train_sample_df, test_sample_df])
52     domain_class_labels = pd.Series([0] * len(train_sample_df) + [1] * len(test_sample_df))
53 
54     x_train, x_test, y_train, y_test = train_test_split(domain_class_df, domain_class_labels,
55                                                         stratify=domain_class_labels,
56                                                         random_state=random_state,
57                                                         test_size=test_size)
58 
59     domain_classifier = domain_classifier.fit(x_train, y_train)
60 
61     y_test.name = 'belongs_to_test'
62     domain_test_dataset = Dataset(pd.concat([x_test.reset_index(drop=True), y_test.reset_index(drop=True)], axis=1),
63                                   cat_features=cat_features, label='belongs_to_test')
64 
65     # calculate feature importance of domain_classifier, containing the information which features separate
66     # the dataset best.
67     fi, importance_type = calculate_feature_importance_or_none(
68         domain_classifier,
69         domain_test_dataset,
70         force_permutation=True,
71         permutation_kwargs={'n_repeats': 10, 'random_state': random_state, 'timeout': 120}
72     )
73 
74     fi = fi.sort_values(ascending=False) if fi is not None else None
75 
76     domain_classifier_auc = roc_auc_score(y_test, domain_classifier.predict_proba(x_test)[:, 1])
77     drift_score = auc_to_drift_score(domain_classifier_auc)
78 
79     values_dict = {
80         'domain_classifier_auc': domain_classifier_auc,
81         'domain_classifier_drift_score': drift_score,
82         'domain_classifier_feature_importance': fi.to_dict() if fi is not None else {},
83     }
84 
85     feature_importance_note = f"""
86     <span>
87     The percents of explained dataset difference are the importance values for the feature calculated
88     using `{importance_type}`.
89     </span><br><br>
90     """
91 
92     if fi is not None and drift_score > min_meaningful_drift_score:
93         top_fi = fi.head(n_top_columns)
94         top_fi = top_fi.loc[top_fi > min_feature_importance]
95     else:
96         top_fi = None
97 
98     if top_fi is not None and len(top_fi):
99         score = values_dict['domain_classifier_drift_score']
100 
101         displays = [feature_importance_note, build_drift_plot(score),
102                     '<h3>Main features contributing to drift</h3>',
103                     N_TOP_MESSAGE % n_top_columns]
104         displays += [display_dist(train_sample_df[feature], test_sample_df[feature], top_fi, cat_features,
105                                   max_num_categories)
106                      for feature in top_fi.index]
107     else:
108         displays = None
109 
110     return values_dict, displays
111 
112 
113 def generate_model(numerical_columns: List[Hashable], categorical_columns: List[Hashable],
114                    random_state: int = 42) -> Pipeline:
115     """Generate the unfitted Domain Classifier model."""
116     categorical_transformer = Pipeline(
117         steps=[('encoder', run_available_kwargs(OrdinalEncoder, handle_unknown='use_encoded_value',
118                                                 unknown_value=np.nan,
119                                                 dtype=np.float64))]
120     )
121 
122     preprocessor = ColumnTransformer(
123         transformers=[
124             ('num', 'passthrough', numerical_columns),
125             ('cat', categorical_transformer, categorical_columns),
126         ]
127     )
128 
129     return Pipeline(
130         steps=[('preprocessing', preprocessor),
131                ('model', HistGradientBoostingClassifier(
132                    max_depth=2, max_iter=10, random_state=random_state,
133                    categorical_features=[False] * len(numerical_columns) + [True] * len(categorical_columns)
134                ))])
135 
136 
137 def auc_to_drift_score(auc: float) -> float:
138     """Calculate the drift score, which is 2*auc - 1, with auc being the auc of the Domain Classifier.
139 
140     Parameters
141     ----------
142     auc : float
143         auc of the Domain Classifier
144     """
145     return max(2 * auc - 1, 0)
146 
147 
148 def build_drift_plot(score):
149     """Build traffic light drift plot."""
150     bar_traces, x_axis, y_axis = drift_score_bar_traces(score)
151     x_axis['title'] = 'Drift score'
152     drift_plot = go.Figure(layout=dict(
153         title='Drift Score - Whole Dataset Total',
154         xaxis=x_axis,
155         yaxis=y_axis,
156         width=700,
157         height=200
158 
159     ))
160 
161     drift_plot.add_traces(bar_traces)
162     return drift_plot
163 
164 
165 def display_dist(train_column: pd.Series, test_column: pd.Series, fi_ser: pd.Series, cat_features,
166                  max_num_categories: int = 10):
167     """Display a distribution comparison plot for the given columns."""
168     column_name = train_column.name
169 
170     title = f'Feature: {column_name} - Explains {format_percent(fi_ser.loc[column_name])} of dataset difference'
171     traces, xaxis_layout, yaxis_layout = \
172         feature_distribution_traces(train_column.dropna(),
173                                     test_column.dropna(),
174                                     column_name,
175                                     is_categorical=column_name in cat_features,
176                                     max_num_categories=max_num_categories)
177 
178     figure = go.Figure(layout=go.Layout(
179         title=title,
180         xaxis=xaxis_layout,
181         yaxis=yaxis_layout,
182         legend=dict(
183             title='Dataset',
184             yanchor='top',
185             y=0.9,
186             xanchor='left'),
187         width=700,
188         height=300
189     ))
190 
191     figure.add_traces(traces)
192 
193     return figure
194
```

Path: `deepchecks/utils/distribution/rare_category_encoder.py`
Content:
```
1 # ----------------------------------------------------------------------------
2 # Copyright (C) 2021-2022 Deepchecks (https://www.deepchecks.com)
3 #
4 # This file is part of Deepchecks.
5 # Deepchecks is distributed under the terms of the GNU Affero General
6 # Public License (version 3 or later).
7 # You should have received a copy of the GNU Affero General Public License
8 # along with Deepchecks.  If not, see <http://www.gnu.org/licenses/>.
9 # ----------------------------------------------------------------------------
10 #
11 """Module of RareCategoryEncoder."""
12 from typing import List, Optional
13 from collections import defaultdict
14 
15 import pandas as pd
16 
17 from deepchecks.utils.typing import Hashable
18 
19 
20 __all__ = ['RareCategoryEncoder']
21 
22 
23 class RareCategoryEncoder:
24     """Encodes rare categories into an "other" parameter.
25 
26     Note that this encoder assumes data is received as a DataFrame.
27 
28     Parameters
29     ----------
30     max_num_categories : int , default: 10
31         Indicates the maximum number of unique categories in a single categorical column
32         (rare categories will be changed to a form of "other")
33     cols : Optional[List[Hashable]] , default: None
34         Columns to limit the encoder to work on. If non are given will work on all columns given in `fit`
35     """
36 
37     DEFAULT_OTHER_VALUE = 'OTHER_RARE_CATEGORY'
38 
39     def __init__(
40         self,
41         max_num_categories: int = 10,
42         cols: Optional[List[Hashable]] = None
43     ):
44         self.max_num_categories = max_num_categories
45         self.cols = cols
46         self._col_mapping = None
47 
48     def fit(self, data: pd.DataFrame):
49         """Fit the encoder using given dataframe.
50 
51         Parameters
52         ----------
53         data : pd.DataFrame
54             data to fit from
55         """
56         if self.cols is not None:
57             self._col_mapping = data[self.cols].apply(self._fit_for_series, axis=0)
58         else:
59             self._col_mapping = data.apply(self._fit_for_series, axis=0)
60 
61     def transform(self, data: pd.DataFrame):
62         """Transform given data according to columns processed in `fit`.
63 
64         Parameters
65         ----------
66         data : pd.DataFrame
67             data to transform
68         Returns
69         -------
70         DataFrame
71             transformed data
72         """
73         if self._col_mapping is None:
74             raise RuntimeError('Cannot transform without fitting first')
75 
76         if self.cols is not None:
77             data = data.copy()
78             data[self.cols] = data[self.cols].apply(lambda s: s.map(self._col_mapping[s.name]))
79         else:
80             data = data.apply(lambda s: s.map(self._col_mapping[s.name]))
81         return data
82 
83     def fit_transform(self, data: pd.DataFrame):
84         """Run `fit` and `transform` on given data.
85 
86         Parameters
87         ----------
88         data : pd.DataFrame
89             data to fit on and transform
90         Returns
91         -------
92         DataFrame
93             transformed data
94         """
95         self.fit(data)
96         return self.transform(data)
97 
98     def _fit_for_series(self, series: pd.Series):
99         top_values = list(series.value_counts().head(self.max_num_categories).index)
100         other_value = self._get_unique_other_value(series)
101         mapper = pd.Series(defaultdict(lambda: other_value, {k: k for k in top_values}), name=series.name)
102         return mapper
103 
104     def _get_unique_other_value(self, series: pd.Series):
105         unique_values = list(series.unique())
106         other = self.DEFAULT_OTHER_VALUE
107         i = 0
108         while other in unique_values:
109             other = self.DEFAULT_OTHER_VALUE + str(i)
110             i += 1
111         return other
112
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/deepchecks/core/check_utils/whole_dataset_drift_utils.py b/deepchecks/core/check_utils/whole_dataset_drift_utils.py
--- a/deepchecks/core/check_utils/whole_dataset_drift_utils.py
+++ b/deepchecks/core/check_utils/whole_dataset_drift_utils.py
@@ -31,6 +31,7 @@
 
 from deepchecks.tabular import Dataset
 from deepchecks.utils.distribution.plot import feature_distribution_traces, drift_score_bar_traces
+from deepchecks.utils.distribution.rare_category_encoder import RareCategoryEncoder
 from deepchecks.utils.features import N_TOP_MESSAGE, calculate_feature_importance_or_none
 from deepchecks.utils.function import run_available_kwargs
 from deepchecks.utils.strings import format_percent
@@ -114,7 +115,8 @@
                    random_state: int = 42) -> Pipeline:
     """Generate the unfitted Domain Classifier model."""
     categorical_transformer = Pipeline(
-        steps=[('encoder', run_available_kwargs(OrdinalEncoder, handle_unknown='use_encoded_value',
+        steps=[('rare', RareCategoryEncoder(254)),
+               ('encoder', run_available_kwargs(OrdinalEncoder, handle_unknown='use_encoded_value',
                                                 unknown_value=np.nan,
                                                 dtype=np.float64))]
     )
diff --git a/deepchecks/utils/distribution/rare_category_encoder.py b/deepchecks/utils/distribution/rare_category_encoder.py
--- a/deepchecks/utils/distribution/rare_category_encoder.py
+++ b/deepchecks/utils/distribution/rare_category_encoder.py
@@ -45,18 +45,24 @@
         self.cols = cols
         self._col_mapping = None
 
-    def fit(self, data: pd.DataFrame):
+    def fit(self, data: pd.DataFrame, y=None):  # noqa # pylint: disable=unused-argument
         """Fit the encoder using given dataframe.
 
         Parameters
         ----------
         data : pd.DataFrame
             data to fit from
+        y :
+            Unused, but needed for sklearn pipeline
         """
+        self._col_mapping = {}
+
         if self.cols is not None:
-            self._col_mapping = data[self.cols].apply(self._fit_for_series, axis=0)
+            for col in self.cols:
+                self._col_mapping[col] = self._fit_for_series(data[col])
         else:
-            self._col_mapping = data.apply(self._fit_for_series, axis=0)
+            for col in data.columns:
+                self._col_mapping[col] = self._fit_for_series(data[col])
 
     def transform(self, data: pd.DataFrame):
         """Transform given data according to columns processed in `fit`.
@@ -78,15 +84,18 @@
             data[self.cols] = data[self.cols].apply(lambda s: s.map(self._col_mapping[s.name]))
         else:
             data = data.apply(lambda s: s.map(self._col_mapping[s.name]))
+
         return data
 
-    def fit_transform(self, data: pd.DataFrame):
+    def fit_transform(self, data: pd.DataFrame, y=None):  # noqa # pylint: disable=unused-argument
         """Run `fit` and `transform` on given data.
 
         Parameters
         ----------
         data : pd.DataFrame
             data to fit on and transform
+        y :
+            Unused, but needed for sklearn pipeline
         Returns
         -------
         DataFrame
@@ -98,7 +107,7 @@
     def _fit_for_series(self, series: pd.Series):
         top_values = list(series.value_counts().head(self.max_num_categories).index)
         other_value = self._get_unique_other_value(series)
-        mapper = pd.Series(defaultdict(lambda: other_value, {k: k for k in top_values}), name=series.name)
+        mapper = defaultdict(lambda: other_value, {k: k for k in top_values})
         return mapper
 
     def _get_unique_other_value(self, series: pd.Series):

{"golden_diff": "diff --git a/deepchecks/core/check_utils/whole_dataset_drift_utils.py b/deepchecks/core/check_utils/whole_dataset_drift_utils.py\n--- a/deepchecks/core/check_utils/whole_dataset_drift_utils.py\n+++ b/deepchecks/core/check_utils/whole_dataset_drift_utils.py\n@@ -31,6 +31,7 @@\n \n from deepchecks.tabular import Dataset\n from deepchecks.utils.distribution.plot import feature_distribution_traces, drift_score_bar_traces\n+from deepchecks.utils.distribution.rare_category_encoder import RareCategoryEncoder\n from deepchecks.utils.features import N_TOP_MESSAGE, calculate_feature_importance_or_none\n from deepchecks.utils.function import run_available_kwargs\n from deepchecks.utils.strings import format_percent\n@@ -114,7 +115,8 @@\n                    random_state: int = 42) -> Pipeline:\n     \"\"\"Generate the unfitted Domain Classifier model.\"\"\"\n     categorical_transformer = Pipeline(\n-        steps=[('encoder', run_available_kwargs(OrdinalEncoder, handle_unknown='use_encoded_value',\n+        steps=[('rare', RareCategoryEncoder(254)),\n+               ('encoder', run_available_kwargs(OrdinalEncoder, handle_unknown='use_encoded_value',\n                                                 unknown_value=np.nan,\n                                                 dtype=np.float64))]\n     )\ndiff --git a/deepchecks/utils/distribution/rare_category_encoder.py b/deepchecks/utils/distribution/rare_category_encoder.py\n--- a/deepchecks/utils/distribution/rare_category_encoder.py\n+++ b/deepchecks/utils/distribution/rare_category_encoder.py\n@@ -45,18 +45,24 @@\n         self.cols = cols\n         self._col_mapping = None\n \n-    def fit(self, data: pd.DataFrame):\n+    def fit(self, data: pd.DataFrame, y=None):  # noqa # pylint: disable=unused-argument\n         \"\"\"Fit the encoder using given dataframe.\n \n         Parameters\n         ----------\n         data : pd.DataFrame\n             data to fit from\n+        y :\n+            Unused, but needed for sklearn pipeline\n         \"\"\"\n+        self._col_mapping = {}\n+\n         if self.cols is not None:\n-            self._col_mapping = data[self.cols].apply(self._fit_for_series, axis=0)\n+            for col in self.cols:\n+                self._col_mapping[col] = self._fit_for_series(data[col])\n         else:\n-            self._col_mapping = data.apply(self._fit_for_series, axis=0)\n+            for col in data.columns:\n+                self._col_mapping[col] = self._fit_for_series(data[col])\n \n     def transform(self, data: pd.DataFrame):\n         \"\"\"Transform given data according to columns processed in `fit`.\n@@ -78,15 +84,18 @@\n             data[self.cols] = data[self.cols].apply(lambda s: s.map(self._col_mapping[s.name]))\n         else:\n             data = data.apply(lambda s: s.map(self._col_mapping[s.name]))\n+\n         return data\n \n-    def fit_transform(self, data: pd.DataFrame):\n+    def fit_transform(self, data: pd.DataFrame, y=None):  # noqa # pylint: disable=unused-argument\n         \"\"\"Run `fit` and `transform` on given data.\n \n         Parameters\n         ----------\n         data : pd.DataFrame\n             data to fit on and transform\n+        y :\n+            Unused, but needed for sklearn pipeline\n         Returns\n         -------\n         DataFrame\n@@ -98,7 +107,7 @@\n     def _fit_for_series(self, series: pd.Series):\n         top_values = list(series.value_counts().head(self.max_num_categories).index)\n         other_value = self._get_unique_other_value(series)\n-        mapper = pd.Series(defaultdict(lambda: other_value, {k: k for k in top_values}), name=series.name)\n+        mapper = defaultdict(lambda: other_value, {k: k for k in top_values})\n         return mapper\n \n     def _get_unique_other_value(self, series: pd.Series):\n", "issue": "[BUG] Maximum feature cardinality in WholeDatasetDrift is not configurable\n**Describe the bug**\r\nWhen running the WholeDatasetDrift on a dataset with a feature that has cardinality greater then 255, the following error is thrown by the HistGradientBoostingClassifier:\r\n\r\n```\r\n/usr/local/lib/python3.9/dist-packages/sklearn/ensemble/_hist_gradient_boosting/gradient_boosting.py in _check_categories(self, X)\r\n    186 \r\n    187                 if categories.size > self.max_bins:\r\n--> 188                     raise ValueError(\r\n    189                         f\"Categorical feature at index {f_idx} is \"\r\n    190                         \"expected to have a \"\r\n\r\nValueError: Categorical feature at index 30 is expected to have a cardinality <= 255\r\n```\r\n\r\nThere is no way to adjust the max_bins parameter of HistGradientBoostingClassifier. Alternatively, the feature cardinality could be reduced before training the HistGradientBoostingClassifier using the max_num_categories parameter of WholeDatasetDrift\n", "before_files": [{"content": "# ----------------------------------------------------------------------------\n# Copyright (C) 2021-2022 Deepchecks (https://www.deepchecks.com)\n#\n# This file is part of Deepchecks.\n# Deepchecks is distributed under the terms of the GNU Affero General\n# Public License (version 3 or later).\n# You should have received a copy of the GNU Affero General Public License\n# along with Deepchecks.  If not, see <http://www.gnu.org/licenses/>.\n# ----------------------------------------------------------------------------\n#\n\"\"\"Module containing common WholeDatasetDriftCheck (domain classifier drift) utils.\"\"\"\n\nfrom typing import List, Optional\nimport warnings\n\nimport numpy as np\nimport pandas as pd\n\nfrom sklearn.pipeline import Pipeline\nfrom sklearn.compose import ColumnTransformer\n\nwith warnings.catch_warnings():\n    warnings.simplefilter('ignore')\n    from sklearn.experimental import enable_hist_gradient_boosting  # noqa # pylint: disable=unused-import\n\nfrom sklearn.ensemble import HistGradientBoostingClassifier\nfrom sklearn.metrics import roc_auc_score\nfrom sklearn.preprocessing import OrdinalEncoder\nfrom sklearn.model_selection import train_test_split\nimport plotly.graph_objects as go\n\nfrom deepchecks.tabular import Dataset\nfrom deepchecks.utils.distribution.plot import feature_distribution_traces, drift_score_bar_traces\nfrom deepchecks.utils.features import N_TOP_MESSAGE, calculate_feature_importance_or_none\nfrom deepchecks.utils.function import run_available_kwargs\nfrom deepchecks.utils.strings import format_percent\nfrom deepchecks.utils.typing import Hashable\n\n\ndef run_whole_dataset_drift(train_dataframe: pd.DataFrame, test_dataframe: pd.DataFrame,\n                            numerical_features: List[Hashable], cat_features: List[Hashable], sample_size: int,\n                            random_state: int, test_size: float, n_top_columns: int, min_feature_importance: float,\n                            max_num_categories: Optional[int], min_meaningful_drift_score: float):\n    \"\"\"Calculate whole dataset drift.\"\"\"\n    domain_classifier = generate_model(numerical_features, cat_features, random_state)\n\n    train_sample_df = train_dataframe.sample(sample_size, random_state=random_state)\n    test_sample_df = test_dataframe.sample(sample_size, random_state=random_state)\n\n    # create new dataset, with label denoting whether sample belongs to test dataset\n    domain_class_df = pd.concat([train_sample_df, test_sample_df])\n    domain_class_labels = pd.Series([0] * len(train_sample_df) + [1] * len(test_sample_df))\n\n    x_train, x_test, y_train, y_test = train_test_split(domain_class_df, domain_class_labels,\n                                                        stratify=domain_class_labels,\n                                                        random_state=random_state,\n                                                        test_size=test_size)\n\n    domain_classifier = domain_classifier.fit(x_train, y_train)\n\n    y_test.name = 'belongs_to_test'\n    domain_test_dataset = Dataset(pd.concat([x_test.reset_index(drop=True), y_test.reset_index(drop=True)], axis=1),\n                                  cat_features=cat_features, label='belongs_to_test')\n\n    # calculate feature importance of domain_classifier, containing the information which features separate\n    # the dataset best.\n    fi, importance_type = calculate_feature_importance_or_none(\n        domain_classifier,\n        domain_test_dataset,\n        force_permutation=True,\n        permutation_kwargs={'n_repeats': 10, 'random_state': random_state, 'timeout': 120}\n    )\n\n    fi = fi.sort_values(ascending=False) if fi is not None else None\n\n    domain_classifier_auc = roc_auc_score(y_test, domain_classifier.predict_proba(x_test)[:, 1])\n    drift_score = auc_to_drift_score(domain_classifier_auc)\n\n    values_dict = {\n        'domain_classifier_auc': domain_classifier_auc,\n        'domain_classifier_drift_score': drift_score,\n        'domain_classifier_feature_importance': fi.to_dict() if fi is not None else {},\n    }\n\n    feature_importance_note = f\"\"\"\n    <span>\n    The percents of explained dataset difference are the importance values for the feature calculated\n    using `{importance_type}`.\n    </span><br><br>\n    \"\"\"\n\n    if fi is not None and drift_score > min_meaningful_drift_score:\n        top_fi = fi.head(n_top_columns)\n        top_fi = top_fi.loc[top_fi > min_feature_importance]\n    else:\n        top_fi = None\n\n    if top_fi is not None and len(top_fi):\n        score = values_dict['domain_classifier_drift_score']\n\n        displays = [feature_importance_note, build_drift_plot(score),\n                    '<h3>Main features contributing to drift</h3>',\n                    N_TOP_MESSAGE % n_top_columns]\n        displays += [display_dist(train_sample_df[feature], test_sample_df[feature], top_fi, cat_features,\n                                  max_num_categories)\n                     for feature in top_fi.index]\n    else:\n        displays = None\n\n    return values_dict, displays\n\n\ndef generate_model(numerical_columns: List[Hashable], categorical_columns: List[Hashable],\n                   random_state: int = 42) -> Pipeline:\n    \"\"\"Generate the unfitted Domain Classifier model.\"\"\"\n    categorical_transformer = Pipeline(\n        steps=[('encoder', run_available_kwargs(OrdinalEncoder, handle_unknown='use_encoded_value',\n                                                unknown_value=np.nan,\n                                                dtype=np.float64))]\n    )\n\n    preprocessor = ColumnTransformer(\n        transformers=[\n            ('num', 'passthrough', numerical_columns),\n            ('cat', categorical_transformer, categorical_columns),\n        ]\n    )\n\n    return Pipeline(\n        steps=[('preprocessing', preprocessor),\n               ('model', HistGradientBoostingClassifier(\n                   max_depth=2, max_iter=10, random_state=random_state,\n                   categorical_features=[False] * len(numerical_columns) + [True] * len(categorical_columns)\n               ))])\n\n\ndef auc_to_drift_score(auc: float) -> float:\n    \"\"\"Calculate the drift score, which is 2*auc - 1, with auc being the auc of the Domain Classifier.\n\n    Parameters\n    ----------\n    auc : float\n        auc of the Domain Classifier\n    \"\"\"\n    return max(2 * auc - 1, 0)\n\n\ndef build_drift_plot(score):\n    \"\"\"Build traffic light drift plot.\"\"\"\n    bar_traces, x_axis, y_axis = drift_score_bar_traces(score)\n    x_axis['title'] = 'Drift score'\n    drift_plot = go.Figure(layout=dict(\n        title='Drift Score - Whole Dataset Total',\n        xaxis=x_axis,\n        yaxis=y_axis,\n        width=700,\n        height=200\n\n    ))\n\n    drift_plot.add_traces(bar_traces)\n    return drift_plot\n\n\ndef display_dist(train_column: pd.Series, test_column: pd.Series, fi_ser: pd.Series, cat_features,\n                 max_num_categories: int = 10):\n    \"\"\"Display a distribution comparison plot for the given columns.\"\"\"\n    column_name = train_column.name\n\n    title = f'Feature: {column_name} - Explains {format_percent(fi_ser.loc[column_name])} of dataset difference'\n    traces, xaxis_layout, yaxis_layout = \\\n        feature_distribution_traces(train_column.dropna(),\n                                    test_column.dropna(),\n                                    column_name,\n                                    is_categorical=column_name in cat_features,\n                                    max_num_categories=max_num_categories)\n\n    figure = go.Figure(layout=go.Layout(\n        title=title,\n        xaxis=xaxis_layout,\n        yaxis=yaxis_layout,\n        legend=dict(\n            title='Dataset',\n            yanchor='top',\n            y=0.9,\n            xanchor='left'),\n        width=700,\n        height=300\n    ))\n\n    figure.add_traces(traces)\n\n    return figure\n", "path": "deepchecks/core/check_utils/whole_dataset_drift_utils.py"}, {"content": "# ----------------------------------------------------------------------------\n# Copyright (C) 2021-2022 Deepchecks (https://www.deepchecks.com)\n#\n# This file is part of Deepchecks.\n# Deepchecks is distributed under the terms of the GNU Affero General\n# Public License (version 3 or later).\n# You should have received a copy of the GNU Affero General Public License\n# along with Deepchecks.  If not, see <http://www.gnu.org/licenses/>.\n# ----------------------------------------------------------------------------\n#\n\"\"\"Module of RareCategoryEncoder.\"\"\"\nfrom typing import List, Optional\nfrom collections import defaultdict\n\nimport pandas as pd\n\nfrom deepchecks.utils.typing import Hashable\n\n\n__all__ = ['RareCategoryEncoder']\n\n\nclass RareCategoryEncoder:\n    \"\"\"Encodes rare categories into an \"other\" parameter.\n\n    Note that this encoder assumes data is received as a DataFrame.\n\n    Parameters\n    ----------\n    max_num_categories : int , default: 10\n        Indicates the maximum number of unique categories in a single categorical column\n        (rare categories will be changed to a form of \"other\")\n    cols : Optional[List[Hashable]] , default: None\n        Columns to limit the encoder to work on. If non are given will work on all columns given in `fit`\n    \"\"\"\n\n    DEFAULT_OTHER_VALUE = 'OTHER_RARE_CATEGORY'\n\n    def __init__(\n        self,\n        max_num_categories: int = 10,\n        cols: Optional[List[Hashable]] = None\n    ):\n        self.max_num_categories = max_num_categories\n        self.cols = cols\n        self._col_mapping = None\n\n    def fit(self, data: pd.DataFrame):\n        \"\"\"Fit the encoder using given dataframe.\n\n        Parameters\n        ----------\n        data : pd.DataFrame\n            data to fit from\n        \"\"\"\n        if self.cols is not None:\n            self._col_mapping = data[self.cols].apply(self._fit_for_series, axis=0)\n        else:\n            self._col_mapping = data.apply(self._fit_for_series, axis=0)\n\n    def transform(self, data: pd.DataFrame):\n        \"\"\"Transform given data according to columns processed in `fit`.\n\n        Parameters\n        ----------\n        data : pd.DataFrame\n            data to transform\n        Returns\n        -------\n        DataFrame\n            transformed data\n        \"\"\"\n        if self._col_mapping is None:\n            raise RuntimeError('Cannot transform without fitting first')\n\n        if self.cols is not None:\n            data = data.copy()\n            data[self.cols] = data[self.cols].apply(lambda s: s.map(self._col_mapping[s.name]))\n        else:\n            data = data.apply(lambda s: s.map(self._col_mapping[s.name]))\n        return data\n\n    def fit_transform(self, data: pd.DataFrame):\n        \"\"\"Run `fit` and `transform` on given data.\n\n        Parameters\n        ----------\n        data : pd.DataFrame\n            data to fit on and transform\n        Returns\n        -------\n        DataFrame\n            transformed data\n        \"\"\"\n        self.fit(data)\n        return self.transform(data)\n\n    def _fit_for_series(self, series: pd.Series):\n        top_values = list(series.value_counts().head(self.max_num_categories).index)\n        other_value = self._get_unique_other_value(series)\n        mapper = pd.Series(defaultdict(lambda: other_value, {k: k for k in top_values}), name=series.name)\n        return mapper\n\n    def _get_unique_other_value(self, series: pd.Series):\n        unique_values = list(series.unique())\n        other = self.DEFAULT_OTHER_VALUE\n        i = 0\n        while other in unique_values:\n            other = self.DEFAULT_OTHER_VALUE + str(i)\n            i += 1\n        return other\n", "path": "deepchecks/utils/distribution/rare_category_encoder.py"}], "after_files": [{"content": "# ----------------------------------------------------------------------------\n# Copyright (C) 2021-2022 Deepchecks (https://www.deepchecks.com)\n#\n# This file is part of Deepchecks.\n# Deepchecks is distributed under the terms of the GNU Affero General\n# Public License (version 3 or later).\n# You should have received a copy of the GNU Affero General Public License\n# along with Deepchecks.  If not, see <http://www.gnu.org/licenses/>.\n# ----------------------------------------------------------------------------\n#\n\"\"\"Module containing common WholeDatasetDriftCheck (domain classifier drift) utils.\"\"\"\n\nfrom typing import List, Optional\nimport warnings\n\nimport numpy as np\nimport pandas as pd\n\nfrom sklearn.pipeline import Pipeline\nfrom sklearn.compose import ColumnTransformer\n\nwith warnings.catch_warnings():\n    warnings.simplefilter('ignore')\n    from sklearn.experimental import enable_hist_gradient_boosting  # noqa # pylint: disable=unused-import\n\nfrom sklearn.ensemble import HistGradientBoostingClassifier\nfrom sklearn.metrics import roc_auc_score\nfrom sklearn.preprocessing import OrdinalEncoder\nfrom sklearn.model_selection import train_test_split\nimport plotly.graph_objects as go\n\nfrom deepchecks.tabular import Dataset\nfrom deepchecks.utils.distribution.plot import feature_distribution_traces, drift_score_bar_traces\nfrom deepchecks.utils.distribution.rare_category_encoder import RareCategoryEncoder\nfrom deepchecks.utils.features import N_TOP_MESSAGE, calculate_feature_importance_or_none\nfrom deepchecks.utils.function import run_available_kwargs\nfrom deepchecks.utils.strings import format_percent\nfrom deepchecks.utils.typing import Hashable\n\n\ndef run_whole_dataset_drift(train_dataframe: pd.DataFrame, test_dataframe: pd.DataFrame,\n                            numerical_features: List[Hashable], cat_features: List[Hashable], sample_size: int,\n                            random_state: int, test_size: float, n_top_columns: int, min_feature_importance: float,\n                            max_num_categories: Optional[int], min_meaningful_drift_score: float):\n    \"\"\"Calculate whole dataset drift.\"\"\"\n    domain_classifier = generate_model(numerical_features, cat_features, random_state)\n\n    train_sample_df = train_dataframe.sample(sample_size, random_state=random_state)\n    test_sample_df = test_dataframe.sample(sample_size, random_state=random_state)\n\n    # create new dataset, with label denoting whether sample belongs to test dataset\n    domain_class_df = pd.concat([train_sample_df, test_sample_df])\n    domain_class_labels = pd.Series([0] * len(train_sample_df) + [1] * len(test_sample_df))\n\n    x_train, x_test, y_train, y_test = train_test_split(domain_class_df, domain_class_labels,\n                                                        stratify=domain_class_labels,\n                                                        random_state=random_state,\n                                                        test_size=test_size)\n\n    domain_classifier = domain_classifier.fit(x_train, y_train)\n\n    y_test.name = 'belongs_to_test'\n    domain_test_dataset = Dataset(pd.concat([x_test.reset_index(drop=True), y_test.reset_index(drop=True)], axis=1),\n                                  cat_features=cat_features, label='belongs_to_test')\n\n    # calculate feature importance of domain_classifier, containing the information which features separate\n    # the dataset best.\n    fi, importance_type = calculate_feature_importance_or_none(\n        domain_classifier,\n        domain_test_dataset,\n        force_permutation=True,\n        permutation_kwargs={'n_repeats': 10, 'random_state': random_state, 'timeout': 120}\n    )\n\n    fi = fi.sort_values(ascending=False) if fi is not None else None\n\n    domain_classifier_auc = roc_auc_score(y_test, domain_classifier.predict_proba(x_test)[:, 1])\n    drift_score = auc_to_drift_score(domain_classifier_auc)\n\n    values_dict = {\n        'domain_classifier_auc': domain_classifier_auc,\n        'domain_classifier_drift_score': drift_score,\n        'domain_classifier_feature_importance': fi.to_dict() if fi is not None else {},\n    }\n\n    feature_importance_note = f\"\"\"\n    <span>\n    The percents of explained dataset difference are the importance values for the feature calculated\n    using `{importance_type}`.\n    </span><br><br>\n    \"\"\"\n\n    if fi is not None and drift_score > min_meaningful_drift_score:\n        top_fi = fi.head(n_top_columns)\n        top_fi = top_fi.loc[top_fi > min_feature_importance]\n    else:\n        top_fi = None\n\n    if top_fi is not None and len(top_fi):\n        score = values_dict['domain_classifier_drift_score']\n\n        displays = [feature_importance_note, build_drift_plot(score),\n                    '<h3>Main features contributing to drift</h3>',\n                    N_TOP_MESSAGE % n_top_columns]\n        displays += [display_dist(train_sample_df[feature], test_sample_df[feature], top_fi, cat_features,\n                                  max_num_categories)\n                     for feature in top_fi.index]\n    else:\n        displays = None\n\n    return values_dict, displays\n\n\ndef generate_model(numerical_columns: List[Hashable], categorical_columns: List[Hashable],\n                   random_state: int = 42) -> Pipeline:\n    \"\"\"Generate the unfitted Domain Classifier model.\"\"\"\n    categorical_transformer = Pipeline(\n        steps=[('rare', RareCategoryEncoder(254)),\n               ('encoder', run_available_kwargs(OrdinalEncoder, handle_unknown='use_encoded_value',\n                                                unknown_value=np.nan,\n                                                dtype=np.float64))]\n    )\n\n    preprocessor = ColumnTransformer(\n        transformers=[\n            ('num', 'passthrough', numerical_columns),\n            ('cat', categorical_transformer, categorical_columns),\n        ]\n    )\n\n    return Pipeline(\n        steps=[('preprocessing', preprocessor),\n               ('model', HistGradientBoostingClassifier(\n                   max_depth=2, max_iter=10, random_state=random_state,\n                   categorical_features=[False] * len(numerical_columns) + [True] * len(categorical_columns)\n               ))])\n\n\ndef auc_to_drift_score(auc: float) -> float:\n    \"\"\"Calculate the drift score, which is 2*auc - 1, with auc being the auc of the Domain Classifier.\n\n    Parameters\n    ----------\n    auc : float\n        auc of the Domain Classifier\n    \"\"\"\n    return max(2 * auc - 1, 0)\n\n\ndef build_drift_plot(score):\n    \"\"\"Build traffic light drift plot.\"\"\"\n    bar_traces, x_axis, y_axis = drift_score_bar_traces(score)\n    x_axis['title'] = 'Drift score'\n    drift_plot = go.Figure(layout=dict(\n        title='Drift Score - Whole Dataset Total',\n        xaxis=x_axis,\n        yaxis=y_axis,\n        width=700,\n        height=200\n\n    ))\n\n    drift_plot.add_traces(bar_traces)\n    return drift_plot\n\n\ndef display_dist(train_column: pd.Series, test_column: pd.Series, fi_ser: pd.Series, cat_features,\n                 max_num_categories: int = 10):\n    \"\"\"Display a distribution comparison plot for the given columns.\"\"\"\n    column_name = train_column.name\n\n    title = f'Feature: {column_name} - Explains {format_percent(fi_ser.loc[column_name])} of dataset difference'\n    traces, xaxis_layout, yaxis_layout = \\\n        feature_distribution_traces(train_column.dropna(),\n                                    test_column.dropna(),\n                                    column_name,\n                                    is_categorical=column_name in cat_features,\n                                    max_num_categories=max_num_categories)\n\n    figure = go.Figure(layout=go.Layout(\n        title=title,\n        xaxis=xaxis_layout,\n        yaxis=yaxis_layout,\n        legend=dict(\n            title='Dataset',\n            yanchor='top',\n            y=0.9,\n            xanchor='left'),\n        width=700,\n        height=300\n    ))\n\n    figure.add_traces(traces)\n\n    return figure\n", "path": "deepchecks/core/check_utils/whole_dataset_drift_utils.py"}, {"content": "# ----------------------------------------------------------------------------\n# Copyright (C) 2021-2022 Deepchecks (https://www.deepchecks.com)\n#\n# This file is part of Deepchecks.\n# Deepchecks is distributed under the terms of the GNU Affero General\n# Public License (version 3 or later).\n# You should have received a copy of the GNU Affero General Public License\n# along with Deepchecks.  If not, see <http://www.gnu.org/licenses/>.\n# ----------------------------------------------------------------------------\n#\n\"\"\"Module of RareCategoryEncoder.\"\"\"\nfrom typing import List, Optional\nfrom collections import defaultdict\n\nimport pandas as pd\n\nfrom deepchecks.utils.typing import Hashable\n\n\n__all__ = ['RareCategoryEncoder']\n\n\nclass RareCategoryEncoder:\n    \"\"\"Encodes rare categories into an \"other\" parameter.\n\n    Note that this encoder assumes data is received as a DataFrame.\n\n    Parameters\n    ----------\n    max_num_categories : int , default: 10\n        Indicates the maximum number of unique categories in a single categorical column\n        (rare categories will be changed to a form of \"other\")\n    cols : Optional[List[Hashable]] , default: None\n        Columns to limit the encoder to work on. If non are given will work on all columns given in `fit`\n    \"\"\"\n\n    DEFAULT_OTHER_VALUE = 'OTHER_RARE_CATEGORY'\n\n    def __init__(\n        self,\n        max_num_categories: int = 10,\n        cols: Optional[List[Hashable]] = None\n    ):\n        self.max_num_categories = max_num_categories\n        self.cols = cols\n        self._col_mapping = None\n\n    def fit(self, data: pd.DataFrame, y=None):  # noqa # pylint: disable=unused-argument\n        \"\"\"Fit the encoder using given dataframe.\n\n        Parameters\n        ----------\n        data : pd.DataFrame\n            data to fit from\n        y :\n            Unused, but needed for sklearn pipeline\n        \"\"\"\n        self._col_mapping = {}\n\n        if self.cols is not None:\n            for col in self.cols:\n                self._col_mapping[col] = self._fit_for_series(data[col])\n        else:\n            for col in data.columns:\n                self._col_mapping[col] = self._fit_for_series(data[col])\n\n    def transform(self, data: pd.DataFrame):\n        \"\"\"Transform given data according to columns processed in `fit`.\n\n        Parameters\n        ----------\n        data : pd.DataFrame\n            data to transform\n        Returns\n        -------\n        DataFrame\n            transformed data\n        \"\"\"\n        if self._col_mapping is None:\n            raise RuntimeError('Cannot transform without fitting first')\n\n        if self.cols is not None:\n            data = data.copy()\n            data[self.cols] = data[self.cols].apply(lambda s: s.map(self._col_mapping[s.name]))\n        else:\n            data = data.apply(lambda s: s.map(self._col_mapping[s.name]))\n\n        return data\n\n    def fit_transform(self, data: pd.DataFrame, y=None):  # noqa # pylint: disable=unused-argument\n        \"\"\"Run `fit` and `transform` on given data.\n\n        Parameters\n        ----------\n        data : pd.DataFrame\n            data to fit on and transform\n        y :\n            Unused, but needed for sklearn pipeline\n        Returns\n        -------\n        DataFrame\n            transformed data\n        \"\"\"\n        self.fit(data)\n        return self.transform(data)\n\n    def _fit_for_series(self, series: pd.Series):\n        top_values = list(series.value_counts().head(self.max_num_categories).index)\n        other_value = self._get_unique_other_value(series)\n        mapper = defaultdict(lambda: other_value, {k: k for k in top_values})\n        return mapper\n\n    def _get_unique_other_value(self, series: pd.Series):\n        unique_values = list(series.unique())\n        other = self.DEFAULT_OTHER_VALUE\n        i = 0\n        while other in unique_values:\n            other = self.DEFAULT_OTHER_VALUE + str(i)\n            i += 1\n        return other\n", "path": "deepchecks/utils/distribution/rare_category_encoder.py"}]}