Repository: deepchecks/deepchecks

Issue:
[FEAT] Add printable representation of deepchecks Dataset
**Is your feature request related to a problem? Please describe.**
When just printing a dataset object, there is not output that gives any information about it's content
![image](https://user-images.githubusercontent.com/17730502/168469916-481832d4-c89a-4653-a8d4-da30696ba41a.png)

**Describe the solution you'd like**
Design a default display (for jupyter notebook and for python console) that is similar to that of a pandas df, but different enough so people won't get confused and also contains info about column roles. 


Discussion:

Resolving patch:
diff --git a/deepchecks/tabular/dataset.py b/deepchecks/tabular/dataset.py
--- a/deepchecks/tabular/dataset.py
+++ b/deepchecks/tabular/dataset.py
@@ -16,8 +16,11 @@
 
 import numpy as np
 import pandas as pd
+from IPython.display import HTML, display_html
+from pandas.api.types import infer_dtype
 from pandas.core.dtypes.common import is_numeric_dtype
 from sklearn.model_selection import train_test_split
+from typing_extensions import Literal as L
 
 from deepchecks.core.errors import DatasetValidationError, DeepchecksNotSupportedError, DeepchecksValueError
 from deepchecks.utils.dataframes import select_from_dataframe
@@ -29,6 +32,7 @@
 
 
 TDataset = t.TypeVar('TDataset', bound='Dataset')
+DatasetReprFmt = t.Union[L['string'], L['html']]  # noqa: F821
 
 
 class Dataset:
@@ -258,9 +262,9 @@ def __init__(
 
         if cat_features is not None:
             if set(cat_features).intersection(set(self._features)) != set(cat_features):
-                raise DeepchecksValueError(f'Categorical features must be a subset of features. '
+                raise DeepchecksValueError('Categorical features must be a subset of features. '
                                            f'Categorical features {set(cat_features) - set(self._features)} '
-                                           f'have not been found in feature list.')
+                                           'have not been found in feature list.')
             self._cat_features = list(cat_features)
         else:
             self._cat_features = self._infer_categorical_features(
@@ -669,12 +673,13 @@ def datetime_col(self) -> t.Optional[pd.Series]:
         t.Optional[pd.Series]
             Series of the datetime column
         """
-        if self._set_datetime_from_dataframe_index is True:
+        if self._set_datetime_from_dataframe_index is True and self._datetime_column is not None:
             return self._datetime_column
         elif self._datetime_name is not None:
             return self.data[self._datetime_name]
-        else:  # No meaningful Datetime to use: Datetime column not configured, and _set_datetime_from_dataframe_index
-            # is False
+        else:
+            # No meaningful Datetime to use:
+            # Datetime column not configured, and _set_datetime_from_dataframe_index is False
             return
 
     @property
@@ -1061,6 +1066,114 @@ def datasets_share_date(cls, *datasets: 'Dataset') -> bool:
 
         return True
 
+    def _dataset_description(self) -> pd.DataFrame:
+        data = self.data
+        features = self.features
+        categorical_features = self.cat_features
+        numerical_features = self.numerical_features
+
+        label_column = t.cast(pd.Series, data[self.label_name]) if self.label_name else None
+        index_column = self.index_col
+        datetime_column = self.datetime_col
+
+        label_name = None
+        index_name = None
+        datetime_name = None
+
+        dataset_columns_info = []
+
+        if index_column is not None:
+            index_name = index_column.name
+            dataset_columns_info.append([
+                index_name,
+                infer_dtype(index_column, skipna=True),
+                'Index',
+                'set from dataframe index' if self._set_index_from_dataframe_index is True else ''
+            ])
+
+        if datetime_column is not None:
+            datetime_name = datetime_column.name
+            dataset_columns_info.append([
+                datetime_name,
+                infer_dtype(datetime_column, skipna=True),
+                'Datetime',
+                'set from DataFrame index' if self._set_datetime_from_dataframe_index is True else ''
+            ])
+
+        if label_column is not None:
+            label_name = label_column.name
+            dataset_columns_info.append([
+                label_name,
+                infer_dtype(label_column, skipna=True),
+                t.cast(str, self.label_type).replace('_', ' ').capitalize(),
+                ''
+            ])
+
+        all_columns = pd.Series(features + list(self.data.columns)).unique()
+
+        for feature_name in t.cast(t.Iterable[str], all_columns):
+            if feature_name in (index_name, datetime_name, label_name):
+                continue
+
+            feature_dtype = infer_dtype(data[feature_name], skipna=True)
+
+            if feature_name in categorical_features:
+                kind = 'Categorical Feature'
+            elif feature_name in numerical_features:
+                kind = 'Numerical Feature'
+            elif feature_name in features:
+                kind = 'Other Feature'
+            else:
+                kind = 'Dataset Column'
+
+            dataset_columns_info.append([feature_name, feature_dtype, kind, ''])
+
+        return pd.DataFrame(
+            data=dataset_columns_info,
+            columns=['Column', 'DType', 'Kind', 'Additional Info'],
+        )
+
+    def __repr__(
+        self,
+        max_cols: int = 8,
+        max_rows: int = 10,
+        fmt: DatasetReprFmt = 'string'
+    ) -> str:
+        """Represent a dataset instance."""
+        info = self._dataset_description()
+        columns = list(info[info['Additional Info'] == '']['Column'])
+        data = self.data.loc[:, columns]  # Sorting horizontally
+        kwargs = dict(max_cols=max_cols, col_space=15)
+
+        if fmt == 'string':
+            features_info = info.to_string(max_rows=50, **kwargs)
+            data_to_show = data.to_string(show_dimensions=True, max_rows=max_rows, **kwargs)
+            title_template = '{:-^40}\n\n'
+            return ''.join((
+                title_template.format(' Dataset Description '),
+                f'{features_info}\n\n\n',
+                title_template.format(' Dataset Content '),
+                f'{data_to_show}\n\n',
+            ))
+
+        elif fmt == 'html':
+            features_info = info.to_html(notebook=True, max_rows=50, **kwargs)
+            data_to_show = data.to_html(notebook=True, max_rows=max_rows, **kwargs)
+            return ''.join([
+                '<h4><b>Dataset Description</b></h4>',
+                features_info,
+                '<h4><b>Dataset Content</b></h4>',
+                data_to_show
+            ])
+
+        else:
+            raise ValueError(
+                '"fmt" parameter supports only next values [string, html]'
+            )
+
+    def _ipython_display_(self):
+        display_html(HTML(self.__repr__(fmt='html')))
+
     def __len__(self) -> int:
         """Return number of samples in the member dataframe.
 


Test patch:
diff --git a/tests/tabular/test_dummy_model.py b/tests/tabular/test_dummy_model.py
--- a/tests/tabular/test_dummy_model.py
+++ b/tests/tabular/test_dummy_model.py
@@ -24,11 +24,10 @@
 from deepchecks.tabular.checks.model_evaluation.unused_features import UnusedFeatures
 from deepchecks.tabular.context import Context
 from deepchecks.tabular.dataset import Dataset
-from tests.base.utils import equal_condition_result
-from tests.tabular.checks.model_evaluation.simple_model_comparison_test import assert_regression
-
 from deepchecks.tabular.suites.default_suites import full_suite
+from tests.base.utils import equal_condition_result
 from tests.conftest import get_expected_results_length, validate_suite_result
+from tests.tabular.checks.model_evaluation.simple_model_comparison_test import assert_regression
 
 
 def _dummify_model(train, test, model):
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
@@ -16,7 +16,6 @@
 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.vision.suites.default_suites import full_suite
 from deepchecks.core.check_result import CheckResult
 from deepchecks.vision.base_checks import SingleDatasetCheck
 from deepchecks.vision.batch_wrapper import Batch
@@ -24,10 +23,10 @@
 from deepchecks.vision.checks.model_evaluation.image_segment_performance import ImageSegmentPerformance
 from deepchecks.vision.checks.model_evaluation.train_test_prediction_drift import TrainTestPredictionDrift
 from deepchecks.vision.context import Context
+from deepchecks.vision.suites.default_suites import full_suite
 from deepchecks.vision.task_type import TaskType
 from deepchecks.vision.vision_data import VisionData
 from tests.base.utils import equal_condition_result
-
 from tests.conftest import get_expected_results_length, validate_suite_result
 
 
diff --git a/tests/vision/checks/train_test_validation/new_labels_test.py b/tests/vision/checks/train_test_validation/new_labels_test.py
--- a/tests/vision/checks/train_test_validation/new_labels_test.py
+++ b/tests/vision/checks/train_test_validation/new_labels_test.py
@@ -11,7 +11,7 @@
 """New Labels check tests"""
 from copy import copy
 
-from hamcrest import assert_that, close_to, has_entries, has_items, has_length, equal_to
+from hamcrest import assert_that, close_to, equal_to, has_entries, has_items, has_length
 
 from deepchecks.vision.checks import NewLabels
 from deepchecks.vision.utils.test_utils import get_modified_dataloader