Repository: modin-project/modin

Issue:
`groupby.skew` on Linux produces different from pandas results in case of incorrect input
Recently (most likely after this commit: #5318) a single groupby test started to randomly fail in CI ([[1]](https://github.com/modin-project/modin/actions/runs/3941841701/jobs/6744761165) [[2]](https://github.com/modin-project/modin/actions/runs/3740484239/jobs/6348913987) [[3]](https://github.com/modin-project/modin/actions/runs/3839267007/jobs/6536796805) [[4]](https://github.com/modin-project/modin/actions/runs/3885019812/jobs/6628351545) [[5]](https://github.com/modin-project/modin/actions/runs/3934781302/jobs/6729830024)) with the following traceback:

<details><summary>Traceback</summary>

```python
=================================== FAILURES ===================================
_______________________ test_mixed_dtypes_groupby[True] ________________________
[gw0] linux -- Python 3.8.15 /usr/share/miniconda3/envs/modin/bin/python

as_index = True

    @pytest.mark.parametrize("as_index", [True, False])
    def test_mixed_dtypes_groupby(as_index):
        frame_data = np.random.randint(97, 198, size=(2**6, 2**4))
        pandas_df = pandas.DataFrame(frame_data).add_prefix("col")
        # Convert every other column to string
        for col in pandas_df.iloc[
            :, [i for i in range(len(pandas_df.columns)) if i % 2 == 0]
        ]:
            pandas_df[col] = [str(chr(i)) for i in pandas_df[col]]
        modin_df = from_pandas(pandas_df)
    
        n = 1
    
        by_values = [
            ("col1",),
            (lambda x: x % 2,),
            (modin_df["col0"].copy(), pandas_df["col0"].copy()),
            ("col3",),
        ]
    
        for by in by_values:
            if isinstance(by[0], str) and by[0] == "col3":
                modin_groupby = modin_df.set_index(by[0]).groupby(
                    by=by[0], as_index=as_index
                )
                pandas_groupby = pandas_df.set_index(by[0]).groupby(
                    by=by[-1], as_index=as_index
                )
            else:
                modin_groupby = modin_df.groupby(by=by[0], as_index=as_index)
                pandas_groupby = pandas_df.groupby(by=by[-1], as_index=as_index)
    
            modin_groupby_equals_pandas(modin_groupby, pandas_groupby)
            eval_ngroups(modin_groupby, pandas_groupby)
            eval_general(modin_groupby, pandas_groupby, lambda df: df.ffill())
            eval_general(
                modin_groupby,
                pandas_groupby,
                lambda df: df.sem(),
                modin_df_almost_equals_pandas,
            )
            eval_shift(modin_groupby, pandas_groupby)
            eval_mean(modin_groupby, pandas_groupby)
            eval_any(modin_groupby, pandas_groupby)
            eval_min(modin_groupby, pandas_groupby)
            eval_general(modin_groupby, pandas_groupby, lambda df: df.idxmax())
            eval_ndim(modin_groupby, pandas_groupby)
            eval_cumsum(modin_groupby, pandas_groupby)
            eval_general(
                modin_groupby,
                pandas_groupby,
                lambda df: df.pct_change(),
                modin_df_almost_equals_pandas,
            )
            eval_cummax(modin_groupby, pandas_groupby)
    
            # TODO Add more apply functions
            apply_functions = [lambda df: df.sum(), min]
            for func in apply_functions:
                eval_apply(modin_groupby, pandas_groupby, func)
    
            eval_dtypes(modin_groupby, pandas_groupby)
            eval_general(modin_groupby, pandas_groupby, lambda df: df.first())
            eval_general(modin_groupby, pandas_groupby, lambda df: df.backfill())
            eval_cummin(modin_groupby, pandas_groupby)
            eval_general(modin_groupby, pandas_groupby, lambda df: df.bfill())
            eval_general(modin_groupby, pandas_groupby, lambda df: df.idxmin())
            eval_prod(modin_groupby, pandas_groupby)
            if as_index:
                eval_std(modin_groupby, pandas_groupby)
                eval_var(modin_groupby, pandas_groupby)
>               eval_skew(modin_groupby, pandas_groupby)

modin/pandas/test/test_groupby.py:1[68](https://github.com/modin-project/modin/actions/runs/3941841701/jobs/6744761165#step:28:69): 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
modin/pandas/test/test_groupby.py:1003: in eval_skew
    modin_df_almost_equals_pandas(modin_groupby.skew(), pandas_groupby.skew())
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

modin_df =       col1  col3  col5  col7  col9  col11  col13  col15
col0                                                   
d     ...    NaN
Ã      NaN   NaN   NaN   NaN   NaN    NaN    NaN    NaN
Ä      NaN   NaN   NaN   NaN   NaN    NaN    NaN    NaN
pandas_df =       col1  col3  col5  col7  col9  col11  col13  col15
col0                                                   
d     ...    NaN
Ã      NaN   NaN   NaN   NaN   NaN    NaN    NaN    NaN
Ä      NaN   NaN   NaN   NaN   NaN    NaN    NaN    NaN

    def modin_df_almost_equals_pandas(modin_df, pandas_df):
        df_categories_equals(modin_df._to_pandas(), pandas_df)
    
        modin_df = to_pandas(modin_df)
    
        if hasattr(modin_df, "select_dtypes"):
            modin_df = modin_df.select_dtypes(exclude=["category"])
        if hasattr(pandas_df, "select_dtypes"):
            pandas_df = pandas_df.select_dtypes(exclude=["category"])
    
        difference = modin_df - pandas_df
        diff_max = difference.max()
        if isinstance(diff_max, pandas.Series):
            diff_max = diff_max.max()
>       assert (
            modin_df.equals(pandas_df)
            or diff_max < 0.0001
            or (all(modin_df.isna().all()) and all(pandas_df.isna().all()))
        )
E       AssertionError

modin/pandas/test/utils.py:6[86](https://github.com/modin-project/modin/actions/runs/3941841701/jobs/6744761165#step:28:87): AssertionError
```

</details>

If you restart the job the failure goes away. The failure so far occurred only on Ubuntu jobs and with no dependency on the engine. The problem is reproducible locally by running the test a lot of times sequentially (see the reproducer below).

If you'll get lucky enough to reproduce the problem you'll see that the frames are indeed unequal. The pandas frame consists full of NaNs and the modin has 'inf' in the [176, 'col11'] position instead:
```python
(Pdb) modin_df
      col11  col13  col15  col3  col5  col7  col9
col1
97      NaN    NaN    NaN   NaN   NaN   NaN   NaN
98      NaN    NaN    NaN   NaN   NaN   NaN   NaN
...
170     NaN    NaN    NaN   NaN   NaN   NaN   NaN
176     inf    NaN    NaN   NaN   NaN   NaN   NaN
         ^--
177     NaN    NaN    NaN   NaN   NaN   NaN   NaN
...

(Pdb) pandas_df
      col3  col5  col7  col9  col11  col13  col15
col1
97     NaN   NaN   NaN   NaN    NaN    NaN    NaN
98     NaN   NaN   NaN   NaN    NaN    NaN    NaN
...
170    NaN   NaN   NaN   NaN    NaN    NaN    NaN
176    NaN   NaN   NaN   NaN    NaN    NaN    NaN
        ^--
177    NaN   NaN   NaN   NaN    NaN    NaN    NaN
...
```
And that's it. For some reason, Modin's `.skew` implementation randomly throws an 'inf' instead of 'NaN' sometimes.

Modin uses a different from pandas formula for the 'skew', so probably some floating-precision errors come into play somehow? IDK. If this will be confirmed, we may probably want to equal 'inf' to 'nan' in this particular test.

**Steps to reproduce**
1. Apply the following patch so you'll enter into debug mode once you have caught the issue:
```diff
diff --git a/modin/pandas/test/utils.py b/modin/pandas/test/utils.py
index 6c7d7f91..957c53ca 100644
--- a/modin/pandas/test/utils.py
+++ b/modin/pandas/test/utils.py
@@ -683,11 +683,14 @@ def modin_df_almost_equals_pandas(modin_df, pandas_df):
     diff_max = difference.max()
     if isinstance(diff_max, pandas.Series):
         diff_max = diff_max.max()
-    assert (
-        modin_df.equals(pandas_df)
-        or diff_max < 0.0001
-        or (all(modin_df.isna().all()) and all(pandas_df.isna().all()))
-    )
+    try:
+        assert (
+            modin_df.equals(pandas_df)
+            or diff_max < 0.0001
+            or (all(modin_df.isna().all()) and all(pandas_df.isna().all()))
+        )
+    except:
+        breakpoint()
 
 
 def try_modin_df_almost_equals_compare(df1, df2):
```
2. Run the following bash script and hope you'll lucky enough to meet the problem. If no success run the script again (for me it's usually enough to run 10 iterations to reproduce the issue, takes ~5 minutes):
```bash
for i in {1..10}
do
    echo "RUN $i"
    python -m pytest modin/pandas/test/test_groupby.py -k "test_mixed_dtype" -s
done

```


Discussion:
Okay, the problem occurs randomly because there is literally a random statement in the test :D, although I strongly believed that random in our test was configured in a way to produce the same results every run...

Anyway, I've been able to capture the state of NumPy's random generator when the failure occurs and come up with a 100% reproducer. Also, I found out that the problem is not reproducible on Windows.

```python
import pandas
import modin.pandas as pd

md_df = pd.DataFrame({"col0": [148, 148], "col1": [171, 137]})
pd_df = md_df._to_pandas()

md_res = md_df.groupby("col0").skew()
pd_res = pd_df.groupby("col0").skew()

print(f"modin result:\n{md_res}")
print(f"\npandas result:\n{pd_res}")
```
Output:
```
modin result:
      col1
col0
148    inf

pandas result:
      col1
col0
148    NaN
```


Resolving patch:
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
@@ -2733,7 +2733,12 @@ def reduce_skew(dfgb, *args, **kwargs):
 
             # The equation for the 'skew' was taken directly from pandas:
             # https://github.com/pandas-dev/pandas/blob/8dab54d6573f7186ff0c3b6364d5e4dd635ff3e7/pandas/core/nanops.py#L1226
-            skew_res = (count * (count - 1) ** 0.5 / (count - 2)) * (m3 / m2**1.5)
+            with np.errstate(invalid="ignore", divide="ignore"):
+                skew_res = (count * (count - 1) ** 0.5 / (count - 2)) * (m3 / m2**1.5)
+
+            # Setting dummy values for invalid results in accordance with pandas
+            skew_res[m2 == 0] = 0
+            skew_res[count < 3] = np.nan
             return skew_res
 
         result = GroupByReduce.register(


Test patch:
diff --git a/modin/pandas/test/test_groupby.py b/modin/pandas/test/test_groupby.py
--- a/modin/pandas/test/test_groupby.py
+++ b/modin/pandas/test/test_groupby.py
@@ -2216,3 +2216,27 @@ def run_test(eval_function, *args, **kwargs):
     # https://github.com/modin-project/modin/issues/5441
     # run_test(eval_transform, func=lambda df: df.mean())
     # run_test(eval_std)
+
+
+def test_skew_corner_cases():
+    """
+    This test was inspired by https://github.com/modin-project/modin/issues/5545.
+
+    The test verifies that modin acts exactly as pandas when the input data is
+    bad for the 'skew' and so some components of the 'skew' formula appears to be invalid:
+        ``(count * (count - 1) ** 0.5 / (count - 2)) * (m3 / m2**1.5)``
+    """
+    # When 'm2 == m3 == 0' thus causing 0 / 0 division in the second multiplier.
+    # Note: mX = 'sum((col - mean(col)) ^ x)'
+    modin_df, pandas_df = create_test_dfs({"col0": [1, 1, 1], "col1": [10, 10, 10]})
+    eval_general(modin_df, pandas_df, lambda df: df.groupby("col0").skew())
+
+    # When 'count < 3' thus causing dividing by zero in the first multiplier
+    # Note: count = group_size
+    modin_df, pandas_df = create_test_dfs({"col0": [1, 1], "col1": [1, 2]})
+    eval_general(modin_df, pandas_df, lambda df: df.groupby("col0").skew())
+
+    # When 'count < 3' and 'm3 / m2 != 0'. The case comes from:
+    # https://github.com/modin-project/modin/issues/5545
+    modin_df, pandas_df = create_test_dfs({"col0": [1, 1], "col1": [171, 137]})
+    eval_general(modin_df, pandas_df, lambda df: df.groupby("col0").skew())