Repository: modin-project/modin

Issue:
Incorrect output of dt properties
### System information
- **OS Platform and Distribution (e.g., Linux Ubuntu 16.04)**: Windows 10
- **Modin version** (`modin.__version__`): 0.7.3+198.ge4e3fecb
- **Python version**: 3.7.5
- **Code we can use to reproduce**:
```python
if __name__ == "__main__":
    import modin.pandas as pd
    import pandas
    import numpy as np

    data = pd.to_timedelta(np.arange(5), unit="d")
    md_series, pd_series = pd.Series(data), pandas.Series(data)
    
    print("pandas:\n", pd_series.dt.components)
    print("\nmodin:\n", md_series.dt.components)

```
Output:
```
pandas:
    days  hours  minutes  seconds  milliseconds  microseconds  nanoseconds
0     0      0        0        0             0             0            0
1     1      0        0        0             0             0            0
2     2      0        0        0             0             0            0
3     3      0        0        0             0             0            0
4     4      0        0        0             0             0            0

modin:
    days
0     0
1     1
2     2
3     3
4     4
```
<!--
You can obtain the Modin version with

python -c "import modin; print(modin.__version__)"
-->

### Describe the problem
The output of functions is different because result modin dataframe contains incorrect metadata. If we would try to convert `md_series` result to pandas, we will see that it equals to pandas, that's because `to_pandas` consider only partitions (which contains correct result) ignoring all meta information (that is also the reason why test don't fail for that).
```
(Pdb) md_series.dt.components._to_pandas()
   days  hours  minutes  seconds  milliseconds  microseconds  nanoseconds
0     0      0        0        0             0             0            0
1     1      0        0        0             0             0            0
2     2      0        0        0             0             0            0
3     3      0        0        0             0             0            0
4     4      0        0        0             0             0            0
(Pdb) md_series.dt.components.columns
Index(['__reduced__'], dtype='object')
```
That happened because in [`dt.components` implementation we use `MapFunction`](https://github.com/modin-project/modin/blob/master/modin/backends/pandas/query_compiler.py#L779) which is not suitable in that case because partitions grows in size (first partitions contain `Series`, but after applying map functions it becomes `DataFrame`) so it is necessary to updates information about columns, manually or by calling `apply_full_axis(1, lambda df: df.dt.components)` 

@prutskov, it seems that you familiar with that code, have any thoughts at which methods of `DatetimeProperties` similar problem could be?



Discussion:
@dchigarev According to documentation of pandas `DataFrame` will return only for `dt.components`. I'll fix it using `apply_full_axis`.

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
@@ -1135,8 +1135,12 @@ def unique(self):
     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_tz = MapReduceFunction.register(
+        _dt_prop_map("tz"), lambda df: pandas.DataFrame(df.iloc[0]), axis=0
+    )
+    dt_freq = MapReduceFunction.register(
+        _dt_prop_map("freq"), lambda df: pandas.DataFrame(df.iloc[0]), axis=0
+    )
     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"))
@@ -1154,7 +1158,9 @@ def unique(self):
     dt_days = MapFunction.register(_dt_prop_map("days"))
     dt_microseconds = MapFunction.register(_dt_prop_map("microseconds"))
     dt_nanoseconds = MapFunction.register(_dt_prop_map("nanoseconds"))
-    dt_components = MapFunction.register(_dt_prop_map("components"))
+    dt_components = MapFunction.register(
+        _dt_prop_map("components"), validate_columns=True
+    )
     dt_qyear = MapFunction.register(_dt_prop_map("qyear"))
     dt_start_time = MapFunction.register(_dt_prop_map("start_time"))
     dt_end_time = MapFunction.register(_dt_prop_map("end_time"))
diff --git a/modin/engines/base/frame/data.py b/modin/engines/base/frame/data.py
--- a/modin/engines/base/frame/data.py
+++ b/modin/engines/base/frame/data.py
@@ -1021,7 +1021,7 @@ def _map_reduce(self, axis, map_func, reduce_func=None, preserve_index=True):
                 reduce_parts, new_index, new_columns, validate_axes="reduced"
             )
 
-    def _map(self, func, dtypes=None, validate_index=False):
+    def _map(self, func, dtypes=None, validate_index=False, validate_columns=False):
         """Perform a function that maps across the entire dataset.
 
         Pamareters
@@ -1055,12 +1055,23 @@ def _map(self, func, dtypes=None, validate_index=False):
             new_row_lengths = None
         else:
             new_row_lengths = self._row_lengths
+
+        if validate_columns:
+            new_columns = self._frame_mgr_cls.get_indices(
+                1, new_partitions, lambda df: df.columns
+            )
+        else:
+            new_columns = self.columns
+        if len(new_columns) != len(self.columns):
+            new_column_widths = None
+        else:
+            new_column_widths = self._column_widths
         return self.__constructor__(
             new_partitions,
             new_index,
-            self.columns,
+            new_columns,
             new_row_lengths,
-            self._column_widths,
+            new_column_widths,
             dtypes=dtypes,
         )
 


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
@@ -1362,11 +1362,8 @@ def test_dtype(data):
     df_equals(modin_series.dtype, pandas_series.dtypes)
 
 
-@pytest.mark.xfail(
-    reason="Datetime properties is broken for now, see #1729 for details"
-)
 def test_dt():
-    data = pd.date_range("2016-12-31", "2017-01-08", freq="D", tz="Europe/Berlin")
+    data = pd.date_range("2016-12-31", periods=128, freq="D", tz="Europe/Berlin")
     modin_series = pd.Series(data)
     pandas_series = pandas.Series(data)
 
@@ -1421,8 +1418,8 @@ def test_dt():
     df_equals(modin_series.dt.month_name(), pandas_series.dt.month_name())
     df_equals(modin_series.dt.day_name(), pandas_series.dt.day_name())
 
-    modin_series = pd.Series(pd.to_timedelta(np.arange(5), unit="d"))
-    pandas_series = pandas.Series(pandas.to_timedelta(np.arange(5), unit="d"))
+    modin_series = pd.Series(pd.to_timedelta(np.arange(128), unit="d"))
+    pandas_series = pandas.Series(pandas.to_timedelta(np.arange(128), unit="d"))
 
     assert_array_equal(
         modin_series.dt.to_pytimedelta(), pandas_series.dt.to_pytimedelta()
@@ -1434,7 +1431,7 @@ def test_dt():
     df_equals(modin_series.dt.nanoseconds, pandas_series.dt.nanoseconds)
     df_equals(modin_series.dt.components, pandas_series.dt.components)
 
-    data_per = pd.date_range("1/1/2012", periods=5, freq="M")
+    data_per = pd.date_range("1/1/2012", periods=128, freq="M")
     pandas_series = pandas.Series(data_per, index=data_per).dt.to_period()
     modin_series = pd.Series(data_per, index=data_per).dt.to_period()