Repository: deepchecks/deepchecks

Issue:
[FEAT] Enable ignoring Null values in the Is Single Value check
**Is your feature request related to a problem? Please describe.**
Currently, if the data has both a single value and NaNs in the same column, this column will be considered to have 2 unique values.

**Describe the solution you'd like**
Add a Boolean parameter telling the check to disregard NaN's

**Additional context**
Issue raised by @galsr in the [Deepchecks Community](https://join.slack.com/t/deepcheckscommunity/shared_invite/zt-1973c7twx-Ytb7_yJefTT4PmnMZj2p7A) support channel. 


Discussion:
By default: ignore Nan

Resolving patch:
diff --git a/deepchecks/tabular/checks/data_integrity/is_single_value.py b/deepchecks/tabular/checks/data_integrity/is_single_value.py
--- a/deepchecks/tabular/checks/data_integrity/is_single_value.py
+++ b/deepchecks/tabular/checks/data_integrity/is_single_value.py
@@ -11,6 +11,8 @@
 """Module contains is_single_value check."""
 from typing import List, Union
 
+import pandas as pd
+
 from deepchecks.core import CheckResult, ConditionCategory, ConditionResult
 from deepchecks.tabular import Context, SingleDatasetCheck
 from deepchecks.tabular.utils.messages import get_condition_passed_message
@@ -31,17 +33,21 @@ class IsSingleValue(SingleDatasetCheck):
     ignore_columns : Union[Hashable, List[Hashable]] , default: None
         Columns to ignore, if none given checks based
         on columns variable.
+    ignore_nan : bool, default True
+        Whether to ignore NaN values in a column when counting the number of unique values.
     """
 
     def __init__(
         self,
         columns: Union[Hashable, List[Hashable], None] = None,
         ignore_columns: Union[Hashable, List[Hashable], None] = None,
+        ignore_nan: bool = True,
         **kwargs
     ):
         super().__init__(**kwargs)
         self.columns = columns
         self.ignore_columns = ignore_columns
+        self.ignore_nan = ignore_nan
 
     def run_logic(self, context: Context, dataset_type: str = 'train') -> CheckResult:
         """Run check.
@@ -60,14 +66,17 @@ def run_logic(self, context: Context, dataset_type: str = 'train') -> CheckResul
 
         df = select_from_dataframe(df, self.columns, self.ignore_columns)
 
-        num_unique_per_col = df.nunique(dropna=False)
+        num_unique_per_col = df.nunique(dropna=self.ignore_nan)
         is_single_unique_value = (num_unique_per_col == 1)
 
         if is_single_unique_value.any():
             # get names of columns with one unique value
             # pylint: disable=unsubscriptable-object
             cols_with_single = is_single_unique_value[is_single_unique_value].index.to_list()
-            uniques = df.loc[:, cols_with_single].head(1)
+            uniques = pd.DataFrame({
+                column_name: [column.sort_values(kind='mergesort').values[0]]
+                for column_name, column in df.loc[:, cols_with_single].items()
+            })
             uniques.index = ['Single unique value']
             display = ['The following columns have only one unique value', uniques]
         else:
diff --git a/docs/source/checks/tabular/data_integrity/plot_is_single_value.py b/docs/source/checks/tabular/data_integrity/plot_is_single_value.py
--- a/docs/source/checks/tabular/data_integrity/plot_is_single_value.py
+++ b/docs/source/checks/tabular/data_integrity/plot_is_single_value.py
@@ -8,6 +8,7 @@
 # Imports
 # =======
 
+import numpy as np
 import pandas as pd
 from sklearn.datasets import load_iris
 
@@ -41,3 +42,14 @@
 
 sv_ignore = IsSingleValue(ignore_columns=['b','c'])
 sv_ignore.run(df)
+
+#%%
+
+# Ignoring NaN values
+
+IsSingleValue(ignore_nan=True).run(pd.DataFrame({
+    'a': [3, np.nan], 
+    'b': [2, 2],
+    'c': [None, np.nan], 
+    'd': ['a', 4]
+}))


Test patch:
diff --git a/tests/tabular/checks/integrity/is_single_value_test.py b/tests/tabular/checks/integrity/is_single_value_test.py
--- a/tests/tabular/checks/integrity/is_single_value_test.py
+++ b/tests/tabular/checks/integrity/is_single_value_test.py
@@ -18,9 +18,9 @@
 from tests.base.utils import equal_condition_result
 
 
-def helper_test_df_and_result(df, expected_result_value, ignore_columns=None):
+def helper_test_df_and_result(df, expected_result_value, ignore_columns=None, ignore_nan=True):
     # Act
-    result = IsSingleValue(ignore_columns=ignore_columns).run(df)
+    result = IsSingleValue(ignore_columns=ignore_columns, ignore_nan=ignore_nan).run(df)
 
     # Assert
     assert_that(result.value, equal_to(expected_result_value))
@@ -98,8 +98,14 @@ def test_wrong_input_single_value():
 
 def test_nans(df_with_fully_nan, df_with_single_nan_in_col):
     # Act & Assert
-    helper_test_df_and_result(df_with_fully_nan, {'col1': 1, 'col2': 1})
-    helper_test_df_and_result(df_with_single_nan_in_col, {'col1': 11, 'col2': 11})
+    helper_test_df_and_result(df_with_fully_nan, {'col1': 1, 'col2': 1}, ignore_nan=False)
+    helper_test_df_and_result(df_with_single_nan_in_col, {'col1': 11, 'col2': 11}, ignore_nan=False)
+
+
+def test_ignore_nans(df_with_fully_nan, df_with_single_nan_in_col):
+    # Act & Assert
+    helper_test_df_and_result(df_with_fully_nan, {'col1': 0, 'col2': 0}, ignore_nan=True)
+    helper_test_df_and_result(df_with_single_nan_in_col, {'col1': 10, 'col2': 11}, ignore_nan=True)
 
 
 def test_condition_fail():
@@ -120,7 +126,7 @@ def test_condition_fail():
 def test_condition_pass():
     # Arrange
     df = pd.DataFrame({'a': ['b', 'asadf', 'b'], 'bbb': ['a', 'a', np.nan], 'f': [1, 2, 3]})
-    check = IsSingleValue().add_condition_not_single_value()
+    check = IsSingleValue(ignore_nan=False).add_condition_not_single_value()
 
     # Act
     result = check.conditions_decision(check.run(df))
diff --git a/tests/vision/base/test_static_predictions.py b/tests/vision/base/test_static_predictions.py
--- a/tests/vision/base/test_static_predictions.py
+++ b/tests/vision/base/test_static_predictions.py
@@ -10,13 +10,13 @@
 #
 #
 import copy
-import numpy as np
 
+import numpy as np
 import torch
-from hamcrest import (assert_that, calling, close_to, equal_to, has_entries, has_items, has_length,
-                      has_properties, has_property, instance_of, is_, raises)
-from deepchecks.core.check_result import CheckResult
+from hamcrest import (assert_that, calling, close_to, equal_to, has_entries, has_items, has_length, has_properties,
+                      has_property, instance_of, is_, raises)
 
+from deepchecks.core.check_result import CheckResult
 from deepchecks.vision.base_checks import SingleDatasetCheck
 from deepchecks.vision.batch_wrapper import Batch
 from deepchecks.vision.checks.model_evaluation.class_performance import ClassPerformance