Repository: deepchecks/deepchecks

Issue:
[BUG] [CV] Image segment performance visualization
**Describe the bug**
Unable to read the text

![image](https://user-images.githubusercontent.com/20860465/161699240-cd2b9ece-0200-4e82-8ebf-db5c97767de1.png)



Discussion:
Actually, this check display is in need of a general remake. 
Things to remake:

1. Different name and wording (segments is confusing for vision)
2. When there is only a single value we should understand that.
3. Try to think of a clearer way to display percentile segments.
4. Also, nicer looking plots.
we should add x-axis label and fix the overlapping
Suggestion:
1. Use only a single metric (AP for detection, f1 for classification)
2. Show 2 property plots per line (so they have proper width)
3. Show only n_top most "variable" properties, sorted by variance or by max_performance - min_performance. 
4. Don't show properties with a single value
5. Extra suggestion that people may not like - color the bars according to their height, so that low bars (weak segments) are redder and high bars (good segments) are greener.
> Suggestion:
> 
> 1. Use only a single metric (AP for detection, f1 for classification)
> 2. Show 2 property plots per line (so they have proper width)
> 3. Show only n_top most "variable" properties, sorted by variance or by max_performance - min_performance.
> 4. Don't show properties with a single value
> 5. Extra suggestion that people may not like - color the bars according to their height, so that low bars (weak segments) are redder and high bars (good segments) are greener.

5 is indeed too much
Also let's rename AP to Average Precision...
> Also let's rename AP to Average Precision...

it's a really big change

Resolving patch:
diff --git a/deepchecks/vision/checks/performance/image_segment_performance.py b/deepchecks/vision/checks/performance/image_segment_performance.py
--- a/deepchecks/vision/checks/performance/image_segment_performance.py
+++ b/deepchecks/vision/checks/performance/image_segment_performance.py
@@ -48,6 +48,8 @@ class ImageSegmentPerformance(SingleDatasetCheck):
         Maximum number of bins to segment a single property into.
     number_of_samples_to_infer_bins : int, default : 1000
         Minimum number of samples to use to infer the bounds of the segments' bins
+    n_show_top : int , default: 3
+        number of properties to show (shows by top diffrence by first metric)
     """
 
     def __init__(
@@ -56,6 +58,7 @@ def __init__(
         alternative_metrics: t.Optional[t.Dict[str, Metric]] = None,
         number_of_bins: int = 5,
         number_of_samples_to_infer_bins: int = 1000,
+        n_to_show: int = 3,
         **kwargs
     ):
         super().__init__(**kwargs)
@@ -67,9 +70,9 @@ def __init__(
 
         self.alternative_metrics = alternative_metrics
         self.number_of_bins = number_of_bins
+        self.n_to_show = n_to_show
         self.number_of_samples_to_infer_bins = number_of_samples_to_infer_bins
         self._state = None
-        self._metrics = None
 
     def initialize_run(self, context: Context, dataset_kind: DatasetKind):
         """Initialize run before starting updating on batches."""
@@ -132,12 +135,10 @@ def compute(self, context: Context, dataset_kind: DatasetKind) -> CheckResult:
         for property_name, prop_bins in bins.items():
             # Calculate scale for the numbers formatting in the display of range
             bins_scale = max([_get_range_scale(b['start'], b['stop']) for b in prop_bins])
+            # If we have a low number of unique values for a property, the first bin (-inf, x) might be empty so
+            # check the count, and if empty filter out the bin
+            prop_bins = list(filter(lambda x: x['count'] > 0, prop_bins))
             for single_bin in prop_bins:
-                # If we have a low number of unique values for a property, the first bin (-inf, x) might be empty so
-                # check the count, and if empty filter out the bin
-                if single_bin['count'] == 0:
-                    continue
-
                 display_range = _range_string(single_bin['start'], single_bin['stop'], bins_scale)
                 bin_data = {
                     'Range': display_range,
@@ -148,26 +149,39 @@ def compute(self, context: Context, dataset_kind: DatasetKind) -> CheckResult:
                 # in order to return the bins object as the check result value
                 single_bin['metrics'] = _calculate_metrics(single_bin['metrics'], dataset)
                 single_bin['display_range'] = display_range
-                # For the plotly display need row per metric in the dataframe
-                for metric, val in single_bin['metrics'].items():
-                    display_data.append({'Metric': metric, 'Value': val, **bin_data})
+                # we don't show single columns in the display
+                if len(prop_bins) > 1:
+                    # For the plotly display need row per metric in the dataframe
+                    for metric, val in single_bin['metrics'].items():
+                        display_data.append({'Metric': metric, 'Value': val, **bin_data})
                 # Save for result
                 result_value[property_name].append(single_bin)
 
         display_df = pd.DataFrame(display_data)
-
+        if display_df.empty:
+            return CheckResult(value=dict(result_value))
+        first_metric = display_df['Metric'][0]
+        if self.alternative_metrics is None:
+            display_df = display_df[display_df['Metric'] == first_metric]
+        top_properties = display_df[display_df['Metric'] == first_metric] \
+            .groupby('Property')[['Value']] \
+            .agg(np.ptp).sort_values('Value', ascending=False).head(self.n_to_show) \
+            .reset_index()['Property']
+        display_df = display_df[display_df['Property'].isin(top_properties)]
         fig = px.bar(
             display_df,
             x='Range',
             y='Value',
             color='Metric',
+            facet_row='Metric',
+            facet_row_spacing=0.05,
             color_discrete_sequence=plot.metric_colors,
             barmode='group',
             facet_col='Property',
-            facet_row='Metric',
             facet_col_spacing=0.05,
-            facet_row_spacing=0.05,
-            hover_data=['Number of samples']
+            hover_data=['Number of samples'],
+            title='Metric Score Per Property Value Segment '
+                  f'(showing top {self.n_to_show} properties by diffrence in segments)'
         )
 
         bar_width = 0.2


Test patch:
diff --git a/tests/vision/checks/performance/image_segment_performance_test.py b/tests/vision/checks/performance/image_segment_performance_test.py
--- a/tests/vision/checks/performance/image_segment_performance_test.py
+++ b/tests/vision/checks/performance/image_segment_performance_test.py
@@ -8,12 +8,13 @@
 # along with Deepchecks.  If not, see <http://www.gnu.org/licenses/>.
 # ----------------------------------------------------------------------------
 #
-from tests.checks.utils import equal_condition_result
-from deepchecks.vision.checks.performance import ImageSegmentPerformance
-
+from ignite.metrics import Precision, Recall
 import numpy as np
-from hamcrest import assert_that, has_length, has_entries, has_items, close_to
+from hamcrest import assert_that, equal_to, has_length, has_entries, has_items, close_to, matches_regexp
+
 from tests.vision.vision_conftest import *
+from tests.checks.utils import equal_condition_result
+from deepchecks.vision.checks.performance import ImageSegmentPerformance
 
 
 def test_mnist(mnist_dataset_train, mock_trained_mnist):
@@ -30,6 +31,52 @@ def test_mnist(mnist_dataset_train, mock_trained_mnist):
     }))
 
 
+def test_mnist_no_display(mnist_dataset_train, mock_trained_mnist):
+    # Act
+    result = ImageSegmentPerformance().run(mnist_dataset_train, mock_trained_mnist, n_samples=1)
+    # Assert
+    assert_that(result.value, has_entries({
+        'Brightness': has_length(1),
+        'Area': has_length(1),
+        'Aspect Ratio': has_length(1),
+    }))
+    assert_that(result.display, has_length(1))
+    assert_that(result.display[0], matches_regexp('<i>Note'))  #
+
+def test_mnist_top_display(mnist_dataset_train, mock_trained_mnist):
+    # Act
+    result = ImageSegmentPerformance(n_to_show=1).run(mnist_dataset_train, mock_trained_mnist, n_samples=None)
+    # Assert
+    assert_that(result.value, has_entries({
+        'Brightness': has_length(5),
+        'Area': has_length(1),
+        'Aspect Ratio': has_items(has_entries({
+            'start': 1.0, 'stop': np.inf, 'count': 60000, 'display_range': '[1, inf)',
+            'metrics': has_entries({'Precision': close_to(0.982, 0.001), 'Recall': close_to(0.979, 0.001)})
+        })),
+    }))
+    assert_that(result.display, has_length(1))
+    assert_that(result.display[0].data, has_length(1))
+    assert_that(result.display[0].data[0].name, equal_to('Precision'))
+
+def test_mnist_alt_metrics(mnist_dataset_train, mock_trained_mnist):
+    # Act
+    result = ImageSegmentPerformance(alternative_metrics={'p': Precision(), 'r': Recall()}) \
+        .run(mnist_dataset_train, mock_trained_mnist, n_samples=None)
+    # Assert
+    assert_that(result.value, has_entries({
+        'Brightness': has_length(5),
+        'Area': has_length(1),
+        'Aspect Ratio': has_items(has_entries({
+            'start': 1.0, 'stop': np.inf, 'count': 60000, 'display_range': '[1, inf)',
+            'metrics': has_entries({'p': close_to(0.982, 0.001), 'r': close_to(0.979, 0.001)})
+        })),
+    }))
+    assert_that(result.display, has_length(1))
+    assert_that(result.display[0].data, has_length(4))
+    assert_that(result.display[0].data[0].name, equal_to('p'))
+    assert_that(result.display[0].data[3].name, equal_to('r'))
+
 def test_coco_and_condition(coco_train_visiondata, mock_trained_yolov5_object_detection):
     # Arrange
     check = ImageSegmentPerformance().add_condition_score_from_mean_ratio_not_less_than(0.5)\