Repository: modin-project/modin

Issue:
dt function implementation



Discussion:

Resolving patch:
diff --git a/modin/backends/pandas/query_compiler.py b/modin/backends/pandas/query_compiler.py
--- a/modin/backends/pandas/query_compiler.py
+++ b/modin/backends/pandas/query_compiler.py
@@ -60,6 +60,61 @@ def str_op_builder(df, *args, **kwargs):
     return str_op_builder
 
 
+def _dt_prop_map(property_name):
+    """
+    Create a function that call property of property `dt` of the series.
+
+    Parameters
+    ----------
+    property_name
+        The property of `dt`, which will be applied.
+
+    Returns
+    -------
+        A callable function to be applied in the partitions
+
+    Notes
+    -----
+    This applies non-callable properties of `Series.dt`.
+    """
+
+    def dt_op_builder(df, *args, **kwargs):
+        prop_val = getattr(df.squeeze().dt, property_name)
+        if isinstance(prop_val, pandas.Series):
+            return prop_val.to_frame()
+        else:
+            return pandas.DataFrame([prop_val])
+
+    return dt_op_builder
+
+
+def _dt_func_map(func_name):
+    """
+    Create a function that call method of property `dt` of the series.
+
+    Parameters
+    ----------
+    func_name
+        The method of `dt`, which will be applied.
+
+    Returns
+    -------
+        A callable function to be applied in the partitions
+
+    Notes
+    -----
+    This applies callable methods of `Series.dt`.
+    """
+
+    def dt_op_builder(df, *args, **kwargs):
+        dt_s = df.squeeze().dt
+        return pandas.DataFrame(
+            getattr(pandas.Series.dt, func_name)(dt_s, *args, **kwargs)
+        )
+
+    return dt_op_builder
+
+
 def copy_df_for_func(func):
     """Create a function that copies the dataframe, likely because `func` is inplace.
 
@@ -493,6 +548,50 @@ def unique(self):
         )
         return self.__constructor__(new_modin_frame)
 
+    # Dt map partitions operations
+
+    dt_date = MapFunction.register(_dt_prop_map("date"))
+    dt_time = MapFunction.register(_dt_prop_map("time"))
+    dt_timetz = MapFunction.register(_dt_prop_map("timetz"))
+    dt_year = MapFunction.register(_dt_prop_map("year"))
+    dt_month = MapFunction.register(_dt_prop_map("month"))
+    dt_day = MapFunction.register(_dt_prop_map("day"))
+    dt_hour = MapFunction.register(_dt_prop_map("hour"))
+    dt_minute = MapFunction.register(_dt_prop_map("minute"))
+    dt_second = MapFunction.register(_dt_prop_map("second"))
+    dt_microsecond = MapFunction.register(_dt_prop_map("microsecond"))
+    dt_nanosecond = MapFunction.register(_dt_prop_map("nanosecond"))
+    dt_week = MapFunction.register(_dt_prop_map("week"))
+    dt_weekofyear = MapFunction.register(_dt_prop_map("weekofyear"))
+    dt_dayofweek = MapFunction.register(_dt_prop_map("dayofweek"))
+    dt_weekday = MapFunction.register(_dt_prop_map("weekday"))
+    dt_dayofyear = MapFunction.register(_dt_prop_map("dayofyear"))
+    dt_quarter = MapFunction.register(_dt_prop_map("quarter"))
+    dt_is_month_start = MapFunction.register(_dt_prop_map("is_month_start"))
+    dt_is_month_end = MapFunction.register(_dt_prop_map("is_month_end"))
+    dt_is_quarter_start = MapFunction.register(_dt_prop_map("is_quarter_start"))
+    dt_is_quarter_end = MapFunction.register(_dt_prop_map("is_quarter_end"))
+    dt_is_year_start = MapFunction.register(_dt_prop_map("is_year_start"))
+    dt_is_year_end = MapFunction.register(_dt_prop_map("is_year_end"))
+    dt_is_leap_year = MapFunction.register(_dt_prop_map("is_leap_year"))
+    dt_daysinmonth = MapFunction.register(_dt_prop_map("daysinmonth"))
+    dt_days_in_month = MapFunction.register(_dt_prop_map("days_in_month"))
+    dt_tz = MapFunction.register(_dt_prop_map("tz"))
+    dt_freq = MapFunction.register(_dt_prop_map("freq"))
+    dt_to_period = MapFunction.register(_dt_func_map("to_period"))
+    dt_to_pydatetime = MapFunction.register(_dt_func_map("to_pydatetime"))
+    dt_tz_localize = MapFunction.register(_dt_func_map("tz_localize"))
+    dt_tz_convert = MapFunction.register(_dt_func_map("tz_convert"))
+    dt_normalize = MapFunction.register(_dt_func_map("normalize"))
+    dt_strftime = MapFunction.register(_dt_func_map("strftime"))
+    dt_round = MapFunction.register(_dt_func_map("round"))
+    dt_floor = MapFunction.register(_dt_func_map("floor"))
+    dt_ceil = MapFunction.register(_dt_func_map("ceil"))
+    dt_month_name = MapFunction.register(_dt_func_map("month_name"))
+    dt_day_name = MapFunction.register(_dt_func_map("day_name"))
+
+    # END Dt map partitions operations
+
     def astype(self, col_dtypes, **kwargs):
         """Converts columns dtypes to given dtypes.
 
diff --git a/modin/pandas/series.py b/modin/pandas/series.py
--- a/modin/pandas/series.py
+++ b/modin/pandas/series.py
@@ -1311,7 +1311,7 @@ def cat(self):
 
     @property
     def dt(self):
-        return self._default_to_pandas(pandas.Series.dt)
+        return DatetimeProperties(self)
 
     @property
     def dtype(self):
@@ -1399,6 +1399,163 @@ def _to_pandas(self):
         return series
 
 
+class DatetimeProperties(object):
+    def __init__(self, series):
+        self._series = series
+        self._query_compiler = series._query_compiler
+
+    @property
+    def date(self):
+        return Series(query_compiler=self._query_compiler.dt_date())
+
+    @property
+    def time(self):
+        return Series(query_compiler=self._query_compiler.dt_time())
+
+    @property
+    def timetz(self):
+        return Series(query_compiler=self._query_compiler.dt_timetz())
+
+    @property
+    def year(self):
+        return Series(query_compiler=self._query_compiler.dt_year())
+
+    @property
+    def month(self):
+        return Series(query_compiler=self._query_compiler.dt_month())
+
+    @property
+    def day(self):
+        return Series(query_compiler=self._query_compiler.dt_day())
+
+    @property
+    def hour(self):
+        return Series(query_compiler=self._query_compiler.dt_hour())
+
+    @property
+    def minute(self):
+        return Series(query_compiler=self._query_compiler.dt_minute())
+
+    @property
+    def second(self):
+        return Series(query_compiler=self._query_compiler.dt_second())
+
+    @property
+    def microsecond(self):
+        return Series(query_compiler=self._query_compiler.dt_microsecond())
+
+    @property
+    def nanosecond(self):
+        return Series(query_compiler=self._query_compiler.dt_nanosecond())
+
+    @property
+    def week(self):
+        return Series(query_compiler=self._query_compiler.dt_week())
+
+    @property
+    def weekofyear(self):
+        return Series(query_compiler=self._query_compiler.dt_weekofyear())
+
+    @property
+    def dayofweek(self):
+        return Series(query_compiler=self._query_compiler.dt_dayofweek())
+
+    @property
+    def weekday(self):
+        return Series(query_compiler=self._query_compiler.dt_weekday())
+
+    @property
+    def dayofyear(self):
+        return Series(query_compiler=self._query_compiler.dt_dayofyear())
+
+    @property
+    def quarter(self):
+        return Series(query_compiler=self._query_compiler.dt_quarter())
+
+    @property
+    def is_month_start(self):
+        return Series(query_compiler=self._query_compiler.dt_is_month_start())
+
+    @property
+    def is_month_end(self):
+        return Series(query_compiler=self._query_compiler.dt_is_month_end())
+
+    @property
+    def is_quarter_start(self):
+        return Series(query_compiler=self._query_compiler.dt_is_quarter_start())
+
+    @property
+    def is_quarter_end(self):
+        return Series(query_compiler=self._query_compiler.dt_is_quarter_end())
+
+    @property
+    def is_year_start(self):
+        return Series(query_compiler=self._query_compiler.dt_is_year_start())
+
+    @property
+    def is_year_end(self):
+        return Series(query_compiler=self._query_compiler.dt_is_year_end())
+
+    @property
+    def is_leap_year(self):
+        return Series(query_compiler=self._query_compiler.dt_is_leap_year())
+
+    @property
+    def daysinmonth(self):
+        return Series(query_compiler=self._query_compiler.dt_daysinmonth())
+
+    @property
+    def days_in_month(self):
+        return Series(query_compiler=self._query_compiler.dt_days_in_month())
+
+    @property
+    def tz(self):
+        return self._query_compiler.dt_tz().to_pandas().squeeze()
+
+    @property
+    def freq(self):
+        return self._query_compiler.dt_freq().to_pandas().squeeze()
+
+    def to_period(self, *args, **kwargs):
+        return Series(query_compiler=self._query_compiler.dt_to_period(*args, **kwargs))
+
+    def to_pydatetime(self):
+        return Series(query_compiler=self._query_compiler.dt_to_pydatetime()).to_numpy()
+
+    def tz_localize(self, *args, **kwargs):
+        return Series(
+            query_compiler=self._query_compiler.dt_tz_localize(*args, **kwargs)
+        )
+
+    def tz_convert(self, *args, **kwargs):
+        return Series(
+            query_compiler=self._query_compiler.dt_tz_convert(*args, **kwargs)
+        )
+
+    def normalize(self, *args, **kwargs):
+        return Series(query_compiler=self._query_compiler.dt_normalize(*args, **kwargs))
+
+    def strftime(self, *args, **kwargs):
+        return Series(query_compiler=self._query_compiler.dt_strftime(*args, **kwargs))
+
+    def round(self, *args, **kwargs):
+        return Series(query_compiler=self._query_compiler.dt_round(*args, **kwargs))
+
+    def floor(self, *args, **kwargs):
+        return Series(query_compiler=self._query_compiler.dt_floor(*args, **kwargs))
+
+    def ceil(self, *args, **kwargs):
+        return Series(query_compiler=self._query_compiler.dt_ceil(*args, **kwargs))
+
+    def month_name(self, *args, **kwargs):
+        return Series(
+            query_compiler=self._query_compiler.dt_month_name(*args, **kwargs)
+        )
+
+    def day_name(self, *args, **kwargs):
+        return Series(query_compiler=self._query_compiler.dt_day_name(*args, **kwargs))
+
+
 class StringMethods(object):
     def __init__(self, series):
         # Check if dtypes is objects


Test patch:
diff --git a/modin/pandas/test/test_series.py b/modin/pandas/test/test_series.py
--- a/modin/pandas/test/test_series.py
+++ b/modin/pandas/test/test_series.py
@@ -1338,6 +1338,63 @@ def test_dtype(data):
     df_equals(modin_series.dtype, pandas_series.dtypes)
 
 
+def test_dt():
+    data = pd.date_range("2016-12-31", "2017-01-08", freq="D", tz="Europe/Berlin")
+    modin_series = pd.Series(data)
+    pandas_series = pandas.Series(data)
+
+    df_equals(modin_series.dt.date, pandas_series.dt.date)
+    df_equals(modin_series.dt.time, pandas_series.dt.time)
+    df_equals(modin_series.dt.timetz, pandas_series.dt.timetz)
+    df_equals(modin_series.dt.year, pandas_series.dt.year)
+    df_equals(modin_series.dt.month, pandas_series.dt.month)
+    df_equals(modin_series.dt.day, pandas_series.dt.day)
+    df_equals(modin_series.dt.hour, pandas_series.dt.hour)
+    df_equals(modin_series.dt.minute, pandas_series.dt.minute)
+    df_equals(modin_series.dt.second, pandas_series.dt.second)
+    df_equals(modin_series.dt.microsecond, pandas_series.dt.microsecond)
+    df_equals(modin_series.dt.nanosecond, pandas_series.dt.nanosecond)
+    df_equals(modin_series.dt.week, pandas_series.dt.week)
+    df_equals(modin_series.dt.weekofyear, pandas_series.dt.weekofyear)
+    df_equals(modin_series.dt.dayofweek, pandas_series.dt.dayofweek)
+    df_equals(modin_series.dt.weekday, pandas_series.dt.weekday)
+    df_equals(modin_series.dt.dayofyear, pandas_series.dt.dayofyear)
+    df_equals(modin_series.dt.quarter, pandas_series.dt.quarter)
+    df_equals(modin_series.dt.is_month_start, pandas_series.dt.is_month_start)
+    df_equals(modin_series.dt.is_month_end, pandas_series.dt.is_month_end)
+    df_equals(modin_series.dt.is_quarter_start, pandas_series.dt.is_quarter_start)
+    df_equals(modin_series.dt.is_quarter_end, pandas_series.dt.is_quarter_end)
+    df_equals(modin_series.dt.is_year_start, pandas_series.dt.is_year_start)
+    df_equals(modin_series.dt.is_year_end, pandas_series.dt.is_year_end)
+    df_equals(modin_series.dt.is_leap_year, pandas_series.dt.is_leap_year)
+    df_equals(modin_series.dt.daysinmonth, pandas_series.dt.daysinmonth)
+    df_equals(modin_series.dt.days_in_month, pandas_series.dt.days_in_month)
+    assert modin_series.dt.tz == pandas_series.dt.tz
+    assert modin_series.dt.freq == pandas_series.dt.freq
+    df_equals(modin_series.dt.to_period("W"), pandas_series.dt.to_period("W"))
+    assert_array_equal(
+        modin_series.dt.to_pydatetime(), pandas_series.dt.to_pydatetime()
+    )
+    df_equals(
+        modin_series.dt.tz_localize(None), pandas_series.dt.tz_localize(None),
+    )
+    df_equals(
+        modin_series.dt.tz_convert(tz="Europe/Berlin"),
+        pandas_series.dt.tz_convert(tz="Europe/Berlin"),
+    )
+
+    df_equals(modin_series.dt.normalize(), pandas_series.dt.normalize())
+    df_equals(
+        modin_series.dt.strftime("%B %d, %Y, %r"),
+        pandas_series.dt.strftime("%B %d, %Y, %r"),
+    )
+    df_equals(modin_series.dt.round("H"), pandas_series.dt.round("H"))
+    df_equals(modin_series.dt.floor("H"), pandas_series.dt.floor("H"))
+    df_equals(modin_series.dt.ceil("H"), pandas_series.dt.ceil("H"))
+    df_equals(modin_series.dt.month_name(), pandas_series.dt.month_name())
+    df_equals(modin_series.dt.day_name(), pandas_series.dt.day_name())
+
+
 @pytest.mark.parametrize("data", test_data_values, ids=test_data_keys)
 @pytest.mark.parametrize(
     "keep", ["last", "first", False], ids=["last", "first", "False"]