Repository: deepchecks/deepchecks

Issue:
[BUG] Date Train-Test Leakage displays datetimes as object, should be human readable
![image](https://user-images.githubusercontent.com/42312361/147881742-edd663f8-18c7-4a14-903c-77b21bf76d17.png)

output of DateTrainTestLeakage is currently `datetime.datetime(...)` should be `1992/02/13 13:23....` instead


Discussion:

Resolving patch:
diff --git a/deepchecks/utils/strings.py b/deepchecks/utils/strings.py
--- a/deepchecks/utils/strings.py
+++ b/deepchecks/utils/strings.py
@@ -10,9 +10,10 @@
 #
 """String functions."""
 import random
-from string import ascii_uppercase, digits
 import typing as t
 import re
+from datetime import datetime
+from string import ascii_uppercase, digits
 from collections import defaultdict
 from decimal import Decimal
 from copy import copy
@@ -35,7 +36,8 @@
     'format_number',
     'format_list',
     'format_columns_for_condition',
-    'get_random_string'
+    'get_random_string',
+    'format_datetime'
 ]
 
 
@@ -297,3 +299,27 @@ def format_columns_for_condition(
         return f'all columns ignoring: {",".join(map(str, ignore_columns))}'
     else:
         return 'all columns'
+
+
+def format_datetime(
+    value,
+    datetime_format: str = '%Y/%m/%d %H:%M:%S.%f %Z%z'  # # 1992/02/13 13:23:00 UTC+0000
+) -> str:
+    """Format datetime object or timestamp value.
+
+    Args:
+        value (Union[datetime, int, float]): datetime (timestamp) to format
+        format (str): format to use
+
+    Returns:
+        str: string representation of the provided value
+
+    Raises:
+        ValueError: if unexpected value type was passed to the function
+    """
+    if isinstance(value, datetime):
+        return value.strftime(datetime_format)
+    elif isinstance(value, (int, float)):
+        return datetime.fromtimestamp(value).strftime(datetime_format)
+    else:
+        raise ValueError(f'Unsupported value type - {type(value).__name__}')


Test patch:
diff --git a/deepchecks/checks/methodology/date_train_test_leakage_duplicates.py b/deepchecks/checks/methodology/date_train_test_leakage_duplicates.py
--- a/deepchecks/checks/methodology/date_train_test_leakage_duplicates.py
+++ b/deepchecks/checks/methodology/date_train_test_leakage_duplicates.py
@@ -12,7 +12,7 @@
 import pandas as pd
 
 from deepchecks import CheckResult, Dataset, TrainTestBaseCheck, ConditionResult
-from deepchecks.utils.strings import format_percent
+from deepchecks.utils.strings import format_percent, format_datetime
 
 
 __all__ = ['DateTrainTestLeakageDuplicates']
@@ -56,12 +56,15 @@ def _date_train_test_leakage_duplicates(self, train_dataset: Dataset, test_datas
         train_date = train_dataset.datetime_col
         val_date = test_dataset.datetime_col
 
-        date_intersection = set(train_date).intersection(val_date)
+        date_intersection = tuple(set(train_date).intersection(val_date))
+
         if len(date_intersection) > 0:
             leakage_ratio = len(date_intersection) / test_dataset.n_samples
             text = f'{format_percent(leakage_ratio)} of test data dates appear in training data'
-            table = pd.DataFrame([[list(date_intersection)[:self.n_to_show]]],
-                                 index=['Sample of test dates in train:'])
+            table = pd.DataFrame(
+                [[list(format_datetime(it) for it in date_intersection[:self.n_to_show])]],
+                index=['Sample of test dates in train:']
+            )
             display = [text, table]
             return_value = leakage_ratio
         else:
diff --git a/deepchecks/checks/methodology/date_train_test_leakage_overlap.py b/deepchecks/checks/methodology/date_train_test_leakage_overlap.py
--- a/deepchecks/checks/methodology/date_train_test_leakage_overlap.py
+++ b/deepchecks/checks/methodology/date_train_test_leakage_overlap.py
@@ -10,7 +10,7 @@
 #
 """The date_leakage check module."""
 from deepchecks import CheckResult, Dataset, TrainTestBaseCheck, ConditionResult
-from deepchecks.utils.strings import format_percent
+from deepchecks.utils.strings import format_percent, format_datetime
 
 
 __all__ = ['DateTrainTestLeakageOverlap']
@@ -52,7 +52,7 @@ def _date_train_test_leakage_overlap(self, train_dataset: Dataset, test_dataset:
         if dates_leaked > 0:
             leakage_ratio = dates_leaked / test_dataset.n_samples
             display = f'{format_percent(leakage_ratio)} of test data dates '\
-                      f'before last training data date ({max_train_date})'
+                      f'before last training data date ({format_datetime(max_train_date)})'
             return_value = leakage_ratio
         else:
             display = None
diff --git a/tests/utils/test_string_utils.py b/tests/utils/test_string_utils.py
new file mode 100644
--- /dev/null
+++ b/tests/utils/test_string_utils.py
@@ -0,0 +1,58 @@
+# ----------------------------------------------------------------------------
+# Copyright (C) 2021 Deepchecks (https://www.deepchecks.com)
+#
+# This file is part of Deepchecks.
+# Deepchecks is distributed under the terms of the GNU Affero General
+# Public License (version 3 or later).
+# You should have received a copy of the GNU Affero General Public License
+# along with Deepchecks.  If not, see <http://www.gnu.org/licenses/>.
+# ----------------------------------------------------------------------------
+#
+from datetime import datetime
+from hamcrest import assert_that, calling, raises, matches_regexp, instance_of
+from deepchecks.utils.strings import format_datetime
+
+
+def test_datetime_instance_format():
+    now = datetime.now()
+    result = format_datetime(now)
+    # %Y/%m/%d %H:%M:%S.%f %Z%z
+    pattern = rf"{now.year}/{now.month}/{now.day} {now.hour}:{now.minute}:{now.second}.{now.microsecond}"
+    assert_that(
+        result,
+        instance_of(str),
+        matches_regexp(pattern) # type: ignore
+    )
+
+
+def test_integer_timestamp_format():
+    now = datetime.now()
+    timestamp = int(now.timestamp())
+    result = format_datetime(timestamp)
+    # %Y/%m/%d %H:%M:%S.%f %Z%z
+    pattern = rf"{now.year}/{now.month}/{now.day} {now.hour}:{now.minute}:{now.second}.{now.microsecond}"
+    assert_that(
+        result,
+        instance_of(str),
+        matches_regexp(pattern) # type: ignore
+    )
+
+
+def test_float_timestamp_format():
+    now = datetime.now()
+    timestamp = now.timestamp()
+    result = format_datetime(timestamp)
+    # %Y/%m/%d %H:%M:%S.%f %Z%z
+    pattern = rf"{now.year}/{now.month}/{now.day} {now.hour}:{now.minute}:{now.second}.{now.microsecond}"
+    assert_that(
+        result,
+        instance_of(str),
+        matches_regexp(pattern) # type: ignore
+    )
+
+
+def test_format_datetime_with_unsuported_value_type():
+    assert_that(
+        calling(format_datetime).with_args("hello"),
+        raises(ValueError, r"Unsupported value type - str")
+    )