Repository: modin-project/modin

Issue:
Series.dt accessor doesn't have day_of_week, day_of_year, isocalendar, asfreq methods



Discussion:

Resolving patch:
diff --git a/modin/core/storage_formats/base/query_compiler.py b/modin/core/storage_formats/base/query_compiler.py
--- a/modin/core/storage_formats/base/query_compiler.py
+++ b/modin/core/storage_formats/base/query_compiler.py
@@ -3796,6 +3796,13 @@ def dt_freq(self):
         """
         return DateTimeDefault.register(pandas.Series.dt.freq)(self)
 
+    @doc_utils.doc_dt_timestamp(
+        prop="Calculate year, week, and day according to the ISO 8601 standard.",
+        refer_to="isocalendar",
+    )
+    def dt_isocalendar(self):
+        return DateTimeDefault.register(pandas.Series.dt.isocalendar)(self)
+
     @doc_utils.doc_dt_timestamp(prop="hour", refer_to="hour")
     def dt_hour(self):
         return DateTimeDefault.register(pandas.Series.dt.hour)(self)
@@ -3944,6 +3951,30 @@ def dt_time(self):
     def dt_timetz(self):
         return DateTimeDefault.register(pandas.Series.dt.timetz)(self)
 
+    @doc_utils.add_refer_to("Series.dt.asfreq")
+    def dt_asfreq(self, freq=None, how: str = "E"):
+        """
+        Convert the PeriodArray to the specified frequency `freq`.
+
+        Equivalent to applying pandas.Period.asfreq() with the given arguments to each Period in this PeriodArray.
+
+        Parameters
+        ----------
+        freq : str, optional
+            A frequency.
+        how : str {'E', 'S'}, default: 'E'
+            Whether the elements should be aligned to the end or start within pa period.
+            * 'E', "END", or "FINISH" for end,
+            * 'S', "START", or "BEGIN" for start.
+            January 31st ("END") vs. January 1st ("START") for example.
+
+        Returns
+        -------
+        BaseQueryCompiler
+            New QueryCompiler containing period data.
+        """
+        return DateTimeDefault.register(pandas.Series.dt.asfreq)(self, freq, how)
+
     @doc_utils.add_one_column_warning
     @doc_utils.add_refer_to("Series.dt.to_period")
     def dt_to_period(self, freq=None):
diff --git a/modin/core/storage_formats/pandas/query_compiler.py b/modin/core/storage_formats/pandas/query_compiler.py
--- a/modin/core/storage_formats/pandas/query_compiler.py
+++ b/modin/core/storage_formats/pandas/query_compiler.py
@@ -1662,6 +1662,7 @@ def datetime_freq(df):
 
         return self.default_to_pandas(datetime_freq)
 
+    dt_asfreq = Map.register(_dt_func_map("asfreq"))
     dt_to_period = Map.register(_dt_func_map("to_period"))
     dt_to_pydatetime = Map.register(_dt_func_map("to_pydatetime"), dtypes=np.object_)
     dt_tz_localize = Map.register(_dt_func_map("tz_localize"))
diff --git a/modin/pandas/series_utils.py b/modin/pandas/series_utils.py
--- a/modin/pandas/series_utils.py
+++ b/modin/pandas/series_utils.py
@@ -543,6 +543,8 @@ def weekofyear(self):
     def dayofweek(self):
         return Series(query_compiler=self._query_compiler.dt_dayofweek())
 
+    day_of_week = dayofweek
+
     @property
     def weekday(self):
         return Series(query_compiler=self._query_compiler.dt_weekday())
@@ -551,6 +553,8 @@ def weekday(self):
     def dayofyear(self):
         return Series(query_compiler=self._query_compiler.dt_dayofyear())
 
+    day_of_year = dayofyear
+
     @property
     def quarter(self):
         return Series(query_compiler=self._query_compiler.dt_quarter())
@@ -605,6 +609,9 @@ def freq(self):
     def to_period(self, *args, **kwargs):
         return Series(query_compiler=self._query_compiler.dt_to_period(*args, **kwargs))
 
+    def asfreq(self, *args, **kwargs):
+        return Series(query_compiler=self._query_compiler.dt_asfreq(*args, **kwargs))
+
     def to_pydatetime(self):
         return Series(query_compiler=self._query_compiler.dt_to_pydatetime()).to_numpy()
 
@@ -672,6 +679,11 @@ def components(self):
 
         return DataFrame(query_compiler=self._query_compiler.dt_components())
 
+    def isocalendar(self):
+        from .dataframe import DataFrame
+
+        return DataFrame(query_compiler=self._query_compiler.dt_isocalendar())
+
     @property
     def qyear(self):
         return Series(query_compiler=self._query_compiler.dt_qyear())


Test patch:
diff --git a/modin/pandas/test/test_api.py b/modin/pandas/test/test_api.py
--- a/modin/pandas/test/test_api.py
+++ b/modin/pandas/test/test_api.py
@@ -249,6 +249,20 @@ def test_series_str_api_equality():
     )
 
 
+def test_series_dt_api_equality():
+    modin_dir = [obj for obj in dir(pd.Series().dt) if obj[0] != "_"]
+    pandas_dir = [obj for obj in dir(pandas.Series.dt) if obj[0] != "_"]
+
+    missing_from_modin = set(pandas_dir) - set(modin_dir)
+    assert not len(missing_from_modin), "Differences found in API: {}".format(
+        missing_from_modin
+    )
+    extra_in_modin = set(modin_dir) - set(pandas_dir)
+    assert not len(extra_in_modin), "Differences found in API: {}".format(
+        extra_in_modin
+    )
+
+
 def test_series_api_equality():
     modin_dir = [obj for obj in dir(pd.Series) if obj[0] != "_"]
     pandas_dir = [obj for obj in dir(pandas.Series) if obj[0] != "_"]
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
@@ -1677,8 +1677,11 @@ def test_dt(timezone):
     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.day_of_week, pandas_series.dt.day_of_week)
     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.day_of_year, pandas_series.dt.day_of_year)
+    df_equals(modin_series.dt.isocalendar(), pandas_series.dt.isocalendar())
     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)
@@ -1754,6 +1757,12 @@ def dt_with_empty_partition(lib):
 
     eval_general(pd, pandas, dt_with_empty_partition)
 
+    if timezone is None:
+        data = pd.period_range("2016-12-31", periods=128, freq="D")
+        modin_series = pd.Series(data)
+        pandas_series = pandas.Series(data)
+        df_equals(modin_series.dt.asfreq("T"), pandas_series.dt.asfreq("T"))
+
 
 @pytest.mark.parametrize(
     "data", test_data_with_duplicates_values, ids=test_data_with_duplicates_keys