Repository: deepchecks/deepchecks

Issue:
`utils.features._built_in_importance` misuses `model.coef_`
In some `sklearn` linear models, the `coef_` attribute of a fitted estimator might be either a 1d array or a 2d one (e.g. in the case of `LinearRegression` it depends on the number of targets passed). In others, it is guaranteed to always be 2d, like for `LogisticRegression`:

```
coef_ : ndarray of shape (1, n_features) or (n_classes, n_features)
 Coefficient of the features in the decision function.

 coef_ is of shape (1, n_features) when the given problem is binary. In particular, when multi_class='multinomial', coef_ corresponds to outcome 1 (True) and -coef_ corresponds to outcome 0 (False).
```

The `utils.features. _built_in_importance` method  - used by several checks, including at the very least `TrainTestFeatureDrift`, `DominantFrequencyChange` and `StringMismatchComparison` - misuses this attribute by assuming it's always 1d in the case of Linear Models. See the line after the `elif` here:

```python
def _built_in_importance(model: t.Any, dataset: 'base.Dataset') -> t.Optional[pd.Series]:
    """Get feature importance member if present in model."""
    if 'feature_importances_' in dir(model):  # Ensembles
        normalized_feature_importance_values = model.feature_importances_/model.feature_importances_.sum()
        return pd.Series(normalized_feature_importance_values, index=dataset.features)
    elif 'coef_' in dir(model):  # Linear models
        coef = np.abs(model.coef_)
        coef = coef / coef.sum()
        return pd.Series(coef, index=dataset.features)
    else:
        return
```

In case of a 2d array, this should use `model.coef_[0]`, of course.

Minimal example to reproduce:
```python
import pandas as pd; import deepchecks;
from sklearn.linear_model import LogisticRegression
from deepchecks.utils.features import _built_in_importance

train_df = pd.DataFrame([[23, True], [19, False], [15, False], [5, True]], columns=['age', 'smoking'], index=[0, 1, 2, 3])
train_y = pd.Series([1, 1, 0, 0])
test_df = pd.DataFrame([[21, True], [40, False], [12, False], [50, True]], columns=['age', 'smoking'], index=[0, 1, 2, 3])
test_y = pd.Series([1, 0, 1, 0])

logreg = LogisticRegression()
logreg.fit(train_df, train_y)

ds_train = deepchecks.Dataset(df=train_df, label=train_y)
ds_test = deepchecks.Dataset(df=test_df, label=test_y)

_built_in_importance(logreg, ds_train)
```

This will yield:
```python
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
/var/folders/2q/pxh7dn6s1nq4fb8hw_x4lqd80000gn/T/ipykernel_42799/3322691940.py in <module>
----> 1 _built_in_importance(logreg, ds_train)

~/clones/deepchecks/deepchecks/utils/features.py in _built_in_importance(model, dataset)
    113         coef = np.abs(model.coef_)
    114         coef = coef / coef.sum()
--> 115         return pd.Series(coef, index=dataset.features)
    116     else:
    117         return

~/.pyenv/versions/3.8.5/envs/py3/lib/python3.8/site-packages/pandas/core/series.py in __init__(self, data, index, dtype, name, copy, fastpath)
    428                 index = ibase.default_index(len(data))
    429             elif is_list_like(data):
--> 430                 com.require_length_match(data, index)
    431 
    432             # create/copy the manager

~/.pyenv/versions/3.8.5/envs/py3/lib/python3.8/site-packages/pandas/core/common.py in require_length_match(data, index)
    529     """
    530     if len(data) != len(index):
--> 531         raise ValueError(
    532             "Length of values "
    533             f"({len(data)}) "

ValueError: Length of values (1) does not match length of index (2)
```


Discussion:

Resolving patch:
diff --git a/deepchecks/utils/features.py b/deepchecks/utils/features.py
--- a/deepchecks/utils/features.py
+++ b/deepchecks/utils/features.py
@@ -110,7 +110,7 @@ def _built_in_importance(model: t.Any, dataset: 'base.Dataset') -> t.Optional[pd
         normalized_feature_importance_values = model.feature_importances_/model.feature_importances_.sum()
         return pd.Series(normalized_feature_importance_values, index=dataset.features)
     elif 'coef_' in dir(model):  # Linear models
-        coef = np.abs(model.coef_)
+        coef = np.abs(model.coef_.flatten())
         coef = coef / coef.sum()
         return pd.Series(coef, index=dataset.features)
     else:


Test patch:
diff --git a/tests/base/feature_importance_utils_test.py b/tests/base/feature_importance_utils_test.py
--- a/tests/base/feature_importance_utils_test.py
+++ b/tests/base/feature_importance_utils_test.py
@@ -12,12 +12,13 @@
 import pandas as pd
 from hamcrest import equal_to, assert_that, calling, raises, close_to, not_none, none
 from sklearn.ensemble import AdaBoostClassifier
-from sklearn.linear_model import LinearRegression
+from sklearn.linear_model import LinearRegression, LogisticRegression
 from sklearn.neural_network import MLPClassifier
 
 from deepchecks.utils.features import calculate_feature_importance, calculate_feature_importance_or_null, \
     column_importance_sorter_df, column_importance_sorter_dict
 from deepchecks.errors import DeepchecksValueError
+from deepchecks.base import Dataset
 
 
 def test_adaboost(iris_split_dataset_and_model):
@@ -43,6 +44,20 @@ def test_linear_regression(diabetes):
     assert_that(feature_importances.sum(), close_to(1, 0.000001))
 
 
+def test_logistic_regression():
+    train_df = pd.DataFrame([[23, True], [19, False], [15, False], [5, True]], columns=['age', 'smoking'],
+                            index=[0, 1, 2, 3])
+    train_y = pd.Series([1, 1, 0, 0])
+
+    logreg = LogisticRegression()
+    logreg.fit(train_df, train_y)
+
+    ds_train = Dataset(df=train_df, label=train_y)
+
+    feature_importances = calculate_feature_importance(logreg, ds_train)
+    assert_that(feature_importances.sum(), close_to(1, 0.000001))
+
+
 def test_calculate_importance(iris_labeled_dataset):
     clf = MLPClassifier(hidden_layer_sizes=(10,), random_state=42)
     clf.fit(iris_labeled_dataset.features_columns, iris_labeled_dataset.label_col)