Repository: sktime/sktime
Pull request: [ENH] Add DropNA transformer
<!--
Thanks for contributing a pull request! Please ensure you have taken a look
at our contribution guide: https://github.com/sktime/sktime/blob/main/CONTRIBUTING.md
-->

#### Reference Issues/PRs
Fixes #4977.

#### What does this implement/fix? Explain your changes.
Implements a `DropNA` transformer that saves the dropped index/column names from `fit`.

#### Does your contribution introduce a new dependency? If yes, which one?
No.

#### What should a reviewer concentrate their feedback on?
Any feedback is appreciated. This is mostly a thin wrapper around `pd.DataFrame.dropna`, accepting the arguments `axis`, `how`, and `thresh`, defaulting to `axis=0` and `how=any`. With univariate series, only `axis=0` will work. 

I am validating the inputs to some degree and I've extended the `thresh` argument to also accept a fraction of non-NA values (pandas only accepts counts). I've always found the pandas default of specifying the threshold in terms of non-NA values strange (as opposed to specifying NA values) , but I've left it as is to be consistent. Thoughts on this?

#### Did you add any tests for the change?
I've added a couple of tests for the functionality and to make sure the parameter validation works. I haven't added any tests with a univariate series so far.

#### Any other comments?
Any feedback is appreciated.

#### PR checklist

##### For all contributions
- [x] I've added myself to the [list of contributors](https://github.com/sktime/sktime/blob/main/CONTRIBUTORS.md) with any new badges I've earned :-)
  How to: add yourself to the [all-contributors file](https://github.com/sktime/sktime/blob/main/.all-contributorsrc) in the `sktime` root directory (not the `CONTRIBUTORS.md`). Common badges: `code` - fixing a bug, or adding code logic. `doc` - writing or improving documentation or docstrings. `bug` - reporting or diagnosing a bug (get this plus `code` if you also fixed the bug in the PR).`maintenance` - CI, test framework, release.
  See here for [full badge reference](https://allcontributors.org/docs/en/emoji-key)
- [x] The PR title starts with either [ENH], [MNT], [DOC], or [BUG]. [BUG] - bugfix, [MNT] - CI, test framework, [ENH] - adding or improving code, [DOC] - writing or improving documentation or docstrings.



Problem statement:
[BUG] Using IndexSubset together with PandasTransformAdaptor(method="dropna") in a pipeline fails
**Describe the bug**
Using `IndexSubset` and `PandasTransformAdaptor(method="dropna")` together in a pipeline fails in some orderings. `PandasTransformAdaptor` appears to be operating on the untransformed/not subset `X`. 

**To Reproduce**
The example below fails. It works with the `PandasTransformAdaptor` commented out.  

The example differences `y`, lags `X`, then sets `y` and `X` to the same index (effectively removing the missing values from `X`). Dropping `nan` values should not have any effect. 

```python
import numpy as np
from sklearn.ensemble import RandomForestRegressor
from sktime.datasets import load_longley
from sktime.forecasting.base import ForecastingHorizon
from sktime.forecasting.compose import (
    ForecastingPipeline,
    TransformedTargetForecaster,
    RecursiveTabularRegressionForecaster,
)
from sklearn.preprocessing import MinMaxScaler
from sktime.transformations.series.adapt import TabularToSeriesAdaptor, PandasTransformAdaptor
from sktime.transformations.series.boxcox import LogTransformer
from sktime.transformations.series.difference import Differencer
from sktime.transformations.series.subset import IndexSubset
from sktime.transformations.series.lag import Lag

y, X = load_longley()
# X.loc["1955", "GNPDEFL"] = np.nan
# X.loc["1947", "POP"] = np.nan

horizon = ForecastingHorizon(np.arange(1, 4), is_relative=True)

pipe = TransformedTargetForecaster(
    [
        LogTransformer(),
        Differencer(lags=1, na_handling="drop_na"),
        ForecastingPipeline(
            [
                Lag(lags=[0, 1], index_out="original"),
                IndexSubset(index_treatment="remove"),
                PandasTransformAdaptor(method="dropna", kwargs={"axis": "columns", "how": "any"}),
                TabularToSeriesAdaptor(MinMaxScaler()),
                RecursiveTabularRegressionForecaster(RandomForestRegressor(), window_length=2),
            ],
        ),
    ]
)
pipe.fit(y, X, fh=horizon)
pipe.predict(fh=horizon, X=X)

pipe._X
pipe.forecaster_.forecaster_._X
```

**Expected behavior**
Drop only features in `X` that are actually missing at this step in the pipeline.

**Versions**
<details>
System:
    python: 3.10.6 (main, May 29 2023, 11:10:38) [GCC 11.3.0]
executable: /usr/bin/python3.10
   machine: Linux-5.19.0-50-generic-x86_64-with-glibc2.35
Python dependencies:
          pip: 22.0.2
       sktime: 0.21.0
      sklearn: 1.1.2
       skbase: 0.4.6
        numpy: 1.22.4
        scipy: 1.8.1
       pandas: 1.4.4
   matplotlib: 3.5.3
       joblib: 1.1.0
  statsmodels: 0.13.2
        numba: 0.56.0
     pmdarima: 2.0.1
      tsfresh: 0.19.0
   tensorflow: None
tensorflow_probability: None
</details>

Hints from issue discussion:
I see - the problem is that the `PandasTransformAdaptor` drops exactly the columns with NAs that it sees at the moment, and these can differ between `fit` and `predict`.

So, in a sense, `PandasTransformAdaptor` does what it's told to do, but the implied behaviour is problematic and does not match the user expectation.

Question, what should the end state be here? Not sure if `PandasTransformAdaptor` can or should be changed.

If it should be changed, how? If not, then what should be the way to get what you want?

Assuming that what you want is dropping exactly the rows that have at least NA in the `fit` data, in both `fit` and `predict`?

Should this be a `DropNA` transformer perhaps? Which can be applied to rows and columns, and for columns remembers the drops from `fit`?
(re status I'm not sure yet whether this is a bug?)
> I see - the problem is that the `PandasTransformAdaptor` drops exactly the columns with NAs that it sees at the moment, and these can differ between `fit` and `predict`.

But in the example, the X in `fit` and `predict` are the same? And the example below (not relying on `IndexSubset`) works.

```python
import numpy as np
from sklearn.ensemble import RandomForestRegressor
from sktime.datasets import load_longley
from sktime.forecasting.base import ForecastingHorizon
from sktime.forecasting.compose import ForecastingPipeline, RecursiveTabularRegressionForecaster
from sklearn.preprocessing import MinMaxScaler
from sktime.transformations.series.adapt import TabularToSeriesAdaptor, PandasTransformAdaptor

y, X = load_longley()
X.loc["1955", "GNPDEFL"] = np.nan
X.loc["1947", "POP"] = np.nan

horizon = ForecastingHorizon(np.arange(1, 4), is_relative=True)

pipe = ForecastingPipeline(
    [
        PandasTransformAdaptor(method="dropna", kwargs={"axis": "columns", "how": "any"}),
        TabularToSeriesAdaptor(MinMaxScaler()),
        RecursiveTabularRegressionForecaster(RandomForestRegressor(), window_length=2),
    ],
)

pipe.fit(y, X, fh=horizon)
pipe.predict(fh=horizon, X=X)

pipe._X
pipe.forecaster_._X
```

> Assuming that what you want is dropping exactly the rows that have at least NA in the `fit` data, in both `fit` and `predict`?

Yes, that's what I had in mind (assuming you meant columns, not rows).

> Should this be a `DropNA` transformer perhaps? Which can be applied to rows and columns, and for columns remembers the drops from `fit`

Perhaps - having this functionality would be nice (not a bug but a feature request then.) 

If it is not overly complex and somewhat beginner friendly, I'd be happy to start working on a `DropNA` transformer. I'd probably need some guidance on where to begin though. 
Ah, thanks! That might be a useful addition!

The guide for adding estimators is here: https://www.sktime.net/en/stable/developer_guide/add_estimators.html

In your case, I would start with the extension template `extension_templates/transformer_supersimple`, since the "drop NA" logic should be a few-liner on top of the simplest template:

* in `_fit`, drop the columns and remember which columns were dropped (write to `self.dropped_cols_` or similar)
* in `_transform`, remember the dropped columns and drop them
* throughout, care should be taken not to mutate the inputs by accidents
* not sure whether we need any parameters for this. What would be useful is to define an axis perhaps? Since some users may like to use it to drop rows, not columns. In that case, nothing needs to be remembered.

Commit message: Update API reference

Patch to review:
diff --git a/.all-contributorsrc b/.all-contributorsrc
index 8a63a0b9424..c0ecd44499f 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -2286,6 +2286,16 @@
         "bug",
         "code"
       ]
+    },
+    {
+      "login": "hliebert",
+      "name": "Helge Liebert",
+      "avatar_url": "https://avatars.githubusercontent.com/u/20834265",
+      "profile": "https://github.com/hliebert",
+      "contributions": [
+        "bug",
+        "code"
+      ]
     }
   ]
 }
diff --git a/docs/source/api_reference/transformations.rst b/docs/source/api_reference/transformations.rst
index a5b739560fa..2ecd69bbe2a 100644
--- a/docs/source/api_reference/transformations.rst
+++ b/docs/source/api_reference/transformations.rst
@@ -418,7 +418,7 @@ Binning and segmentation
     IntervalSegmenter
     RandomIntervalSegmenter
 
-Missing value imputation
+Missing value treatment
 ~~~~~~~~~~~~~~~~~~~~~~~~
 
 .. currentmodule:: sktime.transformations.series.impute
@@ -427,6 +427,7 @@ Missing value imputation
     :toctree: auto_generated/
     :template: class.rst
 
+    DropNA
     Imputer
 
 Seasonality and Date-Time Features
diff --git a/sktime/transformations/series/dropna.py b/sktime/transformations/series/dropna.py
new file mode 100644
index 00000000000..a0444dcf504
--- /dev/null
+++ b/sktime/transformations/series/dropna.py
@@ -0,0 +1,239 @@
+# copyright: sktime developers, BSD-3-Clause License (see LICENSE file)
+"""Transformer to drop rows or columns containing missing values."""
+
+__author__ = ["hliebert"]
+
+from sktime.transformations.base import BaseTransformer
+
+
+class DropNA(BaseTransformer):
+    """Drop missing values transformation.
+
+    Drops rows or columns with missing values from X. Mostly wraps
+    pandas.DataFrame.dropna, but allows specifying thresh as a fraction of
+    non-missing observations.
+
+    Parameters
+    ----------
+    axis : {0 or 'index', 1 or 'columns'}, default 0
+        Determine if rows or columns which contain missing values are removed.
+        Must be 0 or 'index' for univariate input.
+
+        * 0, or 'index' : Drop rows which contain missing values.
+        * 1, or 'columns' : Drop columns which contain missing value.
+
+    how : {'any', 'all'}, default 'any'
+        Determine if row or column is removed from DataFrame, when we have
+        at least one NA or all NA.
+
+        * 'any' : If any NA values are present, drop that row or column.
+        * 'all' : If all values are NA, drop that row or column.
+
+    thresh : int or float, optional
+         If int, require that many non-NA values (as in pandas.dropna). If
+         float, share of non-NA values for rows or columns to be retained.
+         Cannot be combined with how.
+
+    remember : bool, default False if axis==0, True if axis==1
+        If True, drops the same rows/columns in transform as in fit. If false,
+        drops rows/columns according to the NAs seen in transform (equivalent
+        to PandasTransformAdaptor(method="dropna")).
+    """
+
+    _tags = {
+        "univariate-only": False,
+        "scitype:transform-input": "Series",
+        "scitype:transform-output": "Series",
+        "scitype:instancewise": True,
+        "scitype:transform-labels": "None",
+        "X_inner_mtype": ["pd.DataFrame", "pd-multiindex", "pd_multiindex_hier"],
+        "fit_is_empty": False,
+        "capability:inverse_transform": False,
+        "capability:unequal_length": True,
+        "handles-missing-data": False,
+    }
+
+    VALID_AXIS_VALUES = [0, "index", 1, "columns"]
+    VALID_HOW_VALUES = [None, "any", "all"]
+    VALID_THRESH_TYPES = (type(None), int, float)
+    VALID_REMEMBER_TYPES = (type(None), bool)
+
+    def __init__(self, axis=0, how=None, thresh=None, remember=None):
+        self.axis = self._check_axis(axis)
+        self.how = self._check_how(how)
+        self.thresh = self._check_thresh(thresh, how)
+        self.remember = self._check_remember(remember)
+        super().__init__()
+
+        # axis, use numeric axis internally, default rows/index
+        if self.axis == "index":
+            self._axis = 0
+        elif self.axis == "columns":
+            self._axis = 1
+        else:
+            self._axis = self.axis
+
+        # criterion (how/thresh), default to "any" if neither how nor thresh passed
+        if (self.how is None) and (self.thresh is None):
+            self._how = "any"
+        else:
+            self._how = how
+        self._thresh = self.thresh
+
+        # remember, default to remember dropped columns but not rows
+        if self.remember is None:
+            self._remember = bool(self._axis)
+        else:
+            self._remember = self.remember
+
+    def _check_axis(self, axis):
+        """Check axis parameter, should be a valid string as per docstring."""
+        if axis not in self.VALID_AXIS_VALUES:
+            raise ValueError(
+                f'invalid axis parameter value encountered: "{axis}", '
+                f"axis must be one of: {self.VALID_AXIS_VALUES}"
+            )
+
+        return axis
+
+    def _check_how(self, how):
+        """Check how parameter, should be a valid string as per docstring."""
+        if how not in self.VALID_HOW_VALUES:
+            raise ValueError(
+                f'invalid how parameter value encountered: "{how}", '
+                f"how must be one of: {self.VALID_HOW_VALUES}"
+            )
+
+        return how
+
+    def _check_thresh(self, thresh, how):
+        """Check thresh parameter, should be a valid value as per docstring."""
+        if not isinstance(thresh, self.VALID_THRESH_TYPES):
+            raise ValueError(
+                f'invalid thresh parameter value encountered: "{thresh}", '
+                f"thresh must be of type: {self.VALID_HOW_VALUES}"
+            )
+        if isinstance(thresh, float) and not (0 < thresh < 1):
+            raise ValueError("thresh must be a fraction between zero and one")
+        if (how is not None) and (thresh is not None):
+            raise TypeError("thresh cannot be set together with how")
+
+        return thresh
+
+    def _check_remember(self, remember):
+        """Check remember parameter, should be a valid type as per docstring."""
+        if not isinstance(remember, self.VALID_REMEMBER_TYPES):
+            raise ValueError(
+                f'invalid remember parameter value encountered: "{remember}", '
+                f"remember must be of type: {self.VALID_REMEMBER_TYPES}"
+            )
+
+        return remember
+
+    def _fit(self, X, y=None):
+        """Fit transformer to X and y.
+
+        private _fit containing the core logic, called from fit
+
+        Parameters
+        ----------
+        X : pd.DataFrame
+            if self.get_tag("univariate-only")==True:
+                guaranteed to have a single column
+            if self.get_tag("univariate-only")==False: no restrictions apply
+        y : None, present only for interface compatibility
+
+        Returns
+        -------
+        self: reference to self
+        """
+        self.dropped_index_values_ = None
+        self._agg_axis = 1 - self._axis
+
+        mask = None
+        if self._how == "any":
+            mask = X.isna().any(axis=self._agg_axis)
+        elif self._how == "all":
+            mask = X.isna().all(axis=self._agg_axis)
+        elif isinstance(self._thresh, int):
+            mask = X.count(axis=self._agg_axis) < self._thresh
+        elif isinstance(self._thresh, float):
+            mask = (
+                X.count(axis=self._agg_axis).div(X.shape[self._agg_axis]) < self._thresh
+            )
+
+        if mask is not None:
+            self.dropped_index_values_ = mask.index[mask].to_list()
+        else:
+            self.dropped_index_values_ = None
+
+        return self
+
+    def _transform(self, X, y=None):
+        """Transform X and return a transformed version.
+
+        private _transform containing core logic, called from transform
+
+        Parameters
+        ----------
+        X : pd.DataFrame
+            if self.get_tag("univariate-only")==True:
+                guaranteed to have a single column
+            if self.get_tag("univariate-only")==False: no restrictions apply
+        y : None, present only for interface compatibility
+
+        Returns
+        -------
+        transformed version of X
+        """
+        dropped_index_values = self.dropped_index_values_
+        agg_axis = self._agg_axis
+        axis = self._axis
+        how = self._how
+        thresh = self._thresh
+        remember = self._remember
+
+        if remember:
+            if dropped_index_values is not None:
+                return X.drop(labels=dropped_index_values, axis=axis)
+            else:
+                return X
+        else:
+            if isinstance(thresh, float):
+                mask = X.count(axis=agg_axis).div(X.shape[agg_axis]) < thresh
+                index_to_drop = mask.index[mask]
+                return X.drop(labels=index_to_drop, axis=axis)
+            elif isinstance(thresh, int):
+                return X.dropna(axis=axis, thresh=thresh)
+            else:
+                return X.dropna(axis=axis, how=how)
+
+    @classmethod
+    def get_test_params(cls, parameter_set="default"):
+        """Return testing parameter settings for the estimator.
+
+        Parameters
+        ----------
+        parameter_set : str, default="default"
+            Name of the set of test parameters to return, for use in tests. If no
+            special parameters are defined for a value, will return `"default"` set.
+            There are currently no reserved values for transformers.
+
+        Returns
+        -------
+        params : dict or list of dict, default = {}
+            Parameters to create testing instances of the class
+            Each dict are parameters to construct an "interesting" test instance, i.e.,
+            `MyClass(**params)` or `MyClass(**params[i])` creates a valid test instance.
+            `create_test_instance` uses the first (or only) dictionary in `params`
+        """
+        params = [
+            {"axis": 0, "how": "any", "thresh": None},
+            {"axis": 1, "how": "any", "thresh": None},
+            {"axis": 0, "how": "all", "thresh": None},
+            {"axis": 1, "how": "all", "thresh": None},
+            {"axis": 0, "how": None, "thresh": 0.9},
+            {"axis": 1, "how": None, "thresh": 3},
+        ]
+
+        return params
diff --git a/sktime/transformations/series/tests/test_dropna.py b/sktime/transformations/series/tests/test_dropna.py
new file mode 100644
index 00000000000..2e1c85d506c
--- /dev/null
+++ b/sktime/transformations/series/tests/test_dropna.py
@@ -0,0 +1,172 @@
+#!/usr/bin/env python3 -u
+# copyright: sktime developers, BSD-3-Clause License (see LICENSE file)
+"""Unit tests of DropNA functionality."""
+
+__author__ = ["hliebert"]
+__all__ = []
+
+import numpy as np
+import pytest
+
+from sktime.datasets import load_longley
+from sktime.transformations.series.dropna import DropNA
+from sktime.utils._testing.estimator_checks import _assert_array_almost_equal
+
+y_few_na, X_few_na = load_longley()
+X_few_na.loc["1947", "GNPDEFL"] = np.nan
+X_few_na.loc["1950", "GNPDEFL"] = np.nan
+X_few_na.loc["1950", "GNP"] = np.nan
+
+X_few_na_expected = {
+    "0": {
+        "None": X_few_na.drop(
+            labels=[
+                "1947",
+                "1950",
+            ],
+            axis=0,
+        ),
+        "any": X_few_na.drop(
+            labels=[
+                "1947",
+                "1950",
+            ],
+            axis=0,
+        ),
+        "all": X_few_na,
+    },
+    "index": {
+        "None": X_few_na.drop(
+            labels=[
+                "1947",
+                "1950",
+            ],
+            axis=0,
+        ),
+        "any": X_few_na.drop(
+            labels=[
+                "1947",
+                "1950",
+            ],
+            axis=0,
+        ),
+        "all": X_few_na,
+    },
+    "1": {
+        "None": X_few_na.drop(
+            labels=[
+                "GNPDEFL",
+                "GNP",
+            ],
+            axis=1,
+        ),
+        "any": X_few_na.drop(
+            labels=[
+                "GNPDEFL",
+                "GNP",
+            ],
+            axis=1,
+        ),
+        "all": X_few_na,
+    },
+    "columns": {
+        "None": X_few_na.drop(
+            labels=[
+                "GNPDEFL",
+                "GNP",
+            ],
+            axis=1,
+        ),
+        "any": X_few_na.drop(
+            labels=[
+                "GNPDEFL",
+                "GNP",
+            ],
+            axis=1,
+        ),
+        "all": X_few_na,
+    },
+}
+
+y_many_na, X_many_na = load_longley()
+X_many_na.loc["1947", "GNPDEFL"] = np.nan
+X_many_na.loc["1950", "GNPDEFL"] = np.nan
+X_many_na.loc["1950", "GNP"] = np.nan
+X_many_na.loc[:, "ARMED"] = np.nan
+X_many_na.loc[:, "POP"] = np.nan
+X_many_na.loc["1960", :] = np.nan
+X_many_na.loc["1962", :] = np.nan
+X_many_na.dropna(axis=1)
+
+X_many_na_expected = {
+    "0": {
+        "None": X_many_na.drop(labels=X_many_na.index, axis=0),
+        "any": X_many_na.drop(labels=X_many_na.index, axis=0),
+        "all": X_many_na.drop(labels=["1960", "1962"]),
+    },
+    "index": {
+        "None": X_many_na.drop(labels=X_many_na.index, axis=0),
+        "any": X_many_na.drop(labels=X_many_na.index, axis=0),
+        "all": X_many_na.drop(labels=["1960", "1962"]),
+    },
+    "1": {
+        "None": X_many_na.drop(labels=X_many_na.columns, axis=1),
+        "any": X_many_na.drop(labels=X_many_na.columns, axis=1),
+        "all": X_many_na.drop(
+            labels=[
+                "ARMED",
+                "POP",
+            ],
+            axis=1,
+        ),
+    },
+    "columns": {
+        "None": X_many_na.drop(labels=X_many_na.columns, axis=1),
+        "any": X_many_na.drop(labels=X_many_na.columns, axis=1),
+        "all": X_many_na.drop(
+            labels=[
+                "ARMED",
+                "POP",
+            ],
+            axis=1,
+        ),
+    },
+}
+
+
+@pytest.mark.parametrize("axis", DropNA.VALID_AXIS_VALUES)
+@pytest.mark.parametrize("how", DropNA.VALID_HOW_VALUES)
+def test_dropna_few_na(axis, how):
+    """Test expected results on a DataFrame with occasional missings."""
+    transformer = DropNA(axis=axis, how=how, thresh=None)
+    X_transformed = transformer.fit_transform(X_few_na)
+    X_expected = X_few_na_expected[str(axis)][str(how)]
+
+    _assert_array_almost_equal(X_transformed, X_expected)
+
+
+@pytest.mark.parametrize("axis", DropNA.VALID_AXIS_VALUES)
+@pytest.mark.parametrize("how", DropNA.VALID_HOW_VALUES)
+def test_dropna_many_na(axis, how):
+    """Test expected results on a DataFrame with complete columns/rows missing."""
+    transformer = DropNA(axis=axis, how=how, thresh=None)
+    X_transformed = transformer.fit_transform(X_many_na)
+    X_expected = X_many_na_expected[str(axis)][str(how)]
+
+    _assert_array_almost_equal(X_transformed, X_expected)
+
+
+@pytest.mark.parametrize("how", ["any", "all"])
+@pytest.mark.parametrize("thresh", [10, 0.5])
+def test_dropna_incompatible_arguments(how, thresh):
+    """Test that how and thresh arguments cannot be both set."""
+    with pytest.raises(TypeError):
+        DropNA(axis=0, how=str(how), thresh=thresh)
+
+
+@pytest.mark.parametrize("how", ["any", "all", "invalid_value"])
+@pytest.mark.parametrize("thresh", [3, 0.9, True, False, "invalid_value", [1, 2, 3]])
+def test_dropna_invalid_arguments(how, thresh):
+    """Test that invalid arguments or combinations thereof are not accepted."""
+    with pytest.raises((TypeError, ValueError)):
+        DropNA(axis=0, how=str(how), thresh=thresh)


Code review comments:
diff_hunk: @@ -0,0 +1,239 @@
+# copyright: sktime developers, BSD-3-Clause License (see LICENSE file)
+"""Transformer to drop rows or columns containing missing values."""
+
+__author__ = ["hliebert"]
+
+from sktime.transformations.base import BaseTransformer
+
+
+class DropNA(BaseTransformer):
+    """Drop missing values transformation.
+
+    Drops rows or columns with missing values from X. Mostly wraps
+    pandas.DataFrame.dropna, but allows specifying thresh as a fraction of
+    non-missing observations.
+
+    Parameters
+    ----------
+    axis : {0 or 'index', 1 or 'columns'}, default 0
+        Determine if rows or columns which contain missing values are removed.
+        Must be 0 or 'index' for univariate input.
+
+        * 0, or 'index' : Drop rows which contain missing values.
+        * 1, or 'columns' : Drop columns which contain missing value.
+
+    how : {'any', 'all'}, default 'any'
+        Determine if row or column is removed from DataFrame, when we have
+        at least one NA or all NA.
+
+        * 'any' : If any NA values are present, drop that row or column.
+        * 'all' : If all values are NA, drop that row or column.
+
+    thresh : int or float, optional
+         If int, require that many non-NA values (as in pandas.dropna). If
+         float, share of non-NA values for rows or columns to be retained.
+         Cannot be combined with how.
+
+    remember : bool, default False if axis==0, True if axis==1
+        If True, drops the same rows/columns in transform as in fit. If false,
+        drops rows/columns according to the NAs seen in transform (equivalent
+        to PandasTransformAdaptor(method="dropna")).
+    """
+
+    _tags = {
+        "univariate-only": False,
+        "scitype:transform-input": "Series",
+        "scitype:transform-output": "Series",
+        "scitype:instancewise": True,
+        "scitype:transform-labels": "None",
+        "X_inner_mtype": ["pd.DataFrame", "pd-multiindex", "pd_multiindex_hier"],
+        "fit_is_empty": False,
+        "capability:inverse_transform": False,
+        "capability:unequal_length": True,
+        "handles-missing-data": False,
+    }
+
+    VALID_AXIS_VALUES = [0, "index", 1, "columns"]
+    VALID_HOW_VALUES = [None, "any", "all"]
+    VALID_THRESH_TYPES = (type(None), int, float)
+    VALID_REMEMBER_TYPES = (type(None), bool)
+
+    def __init__(self, axis=0, how=None, thresh=None, remember=None):
+        self.axis = self._check_axis(axis)
+        self.how = self._check_how(how)
+        self.thresh = self._check_thresh(thresh, how)
+        self.remember = self._check_remember(remember)
+        super().__init__()
+
+        # axis, use numeric axis internally, default rows/index
+        if self.axis == "index":
+            self._axis = 0
+        elif self.axis == "columns":
+            self._axis = 1
+        else:
+            self._axis = self.axis
+
+        # criterion (how/thresh), default to "any" if neither how nor thresh passed
+        if (self.how is None) and (self.thresh is None):
+            self._how = "any"
+        else:
+            self._how = how
+        self._thresh = self.thresh
+
+        # remember, default to remember dropped columns but not rows
+        if self.remember is None:
+            self._remember = bool(self._axis)
+        else:
+            self._remember = self.remember
+
+    def _check_axis(self, axis):
+        """Check axis parameter, should be a valid string as per docstring."""
+        if axis not in self.VALID_AXIS_VALUES:
+            raise ValueError(
+                f'invalid axis parameter value encountered: "{axis}", '
+                f"axis must be one of: {self.VALID_AXIS_VALUES}"
+            )
+
+        return axis
+
+    def _check_how(self, how):
+        """Check how parameter, should be a valid string as per docstring."""
+        if how not in self.VALID_HOW_VALUES:
+            raise ValueError(
+                f'invalid how parameter value encountered: "{how}", '
+                f"how must be one of: {self.VALID_HOW_VALUES}"
+            )
+
+        return how
+
+    def _check_thresh(self, thresh, how):
+        """Check thresh parameter, should be a valid value as per docstring."""
+        if not isinstance(thresh, self.VALID_THRESH_TYPES):
+            raise ValueError(
+                f'invalid thresh parameter value encountered: "{thresh}", '
+                f"thresh must be of type: {self.VALID_HOW_VALUES}"
+            )
+        if isinstance(thresh, float) and not (0 < thresh < 1):
path: sktime/transformations/series/dropna.py
text: @user1:
Curiuous why `1.0` is not allowed? Can a user not want to ensure to have zero null values?

@author:
I thought about it, but `thresh=1.0` is just the same as `how="any`". (Similarly, `thresh=0.0` corresponds to `how="all"` and is also excluded). Happy to add it if you think this should be possible.

@user1:
> `thresh=1.0` is just the same as `how="any"`

This was not immediately clear to me, may be it's worth adding in `Notes` section of docstring?

Unfortunately, I do not follow the equivalence of `thresh=0.0` and `how="all"` cases. Can you please explain that part?

For `how="all"` and `axis="index"`, it means drop a row (index) if all columns is null in it. How do I even interpret `axis="index"` and `thresh=0.0`? Proportion of columns with non-null values has to be at least 0, can not be negative, so the condition to drop will never hold good. And hence I interpreted that `(0, 1.0]` is the valid range for `thresh`, assuming I'm not making a super silly mistake. Please correct my understanding, sorry if I'm missing something obvious.

@author:
I've added this to the docstring. Sorry for the confusion, the `thresh="0.0"` equivalence was a mistake, I responded too late in the evening. I've thought about it and I think you're right, allowing the interval for thresh to be right-open makes sense. I've adjusted the logic and the docstring accordingly.

diff_hunk: @@ -0,0 +1,239 @@
+# copyright: sktime developers, BSD-3-Clause License (see LICENSE file)
+"""Transformer to drop rows or columns containing missing values."""
+
+__author__ = ["hliebert"]
+
+from sktime.transformations.base import BaseTransformer
+
+
+class DropNA(BaseTransformer):
+    """Drop missing values transformation.
+
+    Drops rows or columns with missing values from X. Mostly wraps
+    pandas.DataFrame.dropna, but allows specifying thresh as a fraction of
+    non-missing observations.
+
+    Parameters
+    ----------
+    axis : {0 or 'index', 1 or 'columns'}, default 0
+        Determine if rows or columns which contain missing values are removed.
+        Must be 0 or 'index' for univariate input.
+
+        * 0, or 'index' : Drop rows which contain missing values.
+        * 1, or 'columns' : Drop columns which contain missing value.
+
+    how : {'any', 'all'}, default 'any'
+        Determine if row or column is removed from DataFrame, when we have
+        at least one NA or all NA.
+
+        * 'any' : If any NA values are present, drop that row or column.
+        * 'all' : If all values are NA, drop that row or column.
+
+    thresh : int or float, optional
+         If int, require that many non-NA values (as in pandas.dropna). If
+         float, share of non-NA values for rows or columns to be retained.
+         Cannot be combined with how.
+
+    remember : bool, default False if axis==0, True if axis==1
+        If True, drops the same rows/columns in transform as in fit. If false,
+        drops rows/columns according to the NAs seen in transform (equivalent
+        to PandasTransformAdaptor(method="dropna")).
+    """
+
+    _tags = {
+        "univariate-only": False,
+        "scitype:transform-input": "Series",
+        "scitype:transform-output": "Series",
+        "scitype:instancewise": True,
+        "scitype:transform-labels": "None",
+        "X_inner_mtype": ["pd.DataFrame", "pd-multiindex", "pd_multiindex_hier"],
+        "fit_is_empty": False,
+        "capability:inverse_transform": False,
+        "capability:unequal_length": True,
+        "handles-missing-data": False,
+    }
+
+    VALID_AXIS_VALUES = [0, "index", 1, "columns"]
+    VALID_HOW_VALUES = [None, "any", "all"]
+    VALID_THRESH_TYPES = (type(None), int, float)
+    VALID_REMEMBER_TYPES = (type(None), bool)
+
+    def __init__(self, axis=0, how=None, thresh=None, remember=None):
+        self.axis = self._check_axis(axis)
+        self.how = self._check_how(how)
+        self.thresh = self._check_thresh(thresh, how)
+        self.remember = self._check_remember(remember)
+        super().__init__()
+
+        # axis, use numeric axis internally, default rows/index
+        if self.axis == "index":
+            self._axis = 0
+        elif self.axis == "columns":
+            self._axis = 1
+        else:
+            self._axis = self.axis
+
+        # criterion (how/thresh), default to "any" if neither how nor thresh passed
+        if (self.how is None) and (self.thresh is None):
+            self._how = "any"
+        else:
+            self._how = how
+        self._thresh = self.thresh
+
+        # remember, default to remember dropped columns but not rows
+        if self.remember is None:
+            self._remember = bool(self._axis)
+        else:
+            self._remember = self.remember
+
+    def _check_axis(self, axis):
+        """Check axis parameter, should be a valid string as per docstring."""
+        if axis not in self.VALID_AXIS_VALUES:
+            raise ValueError(
+                f'invalid axis parameter value encountered: "{axis}", '
+                f"axis must be one of: {self.VALID_AXIS_VALUES}"
+            )
+
+        return axis
+
+    def _check_how(self, how):
+        """Check how parameter, should be a valid string as per docstring."""
+        if how not in self.VALID_HOW_VALUES:
+            raise ValueError(
+                f'invalid how parameter value encountered: "{how}", '
+                f"how must be one of: {self.VALID_HOW_VALUES}"
+            )
+
+        return how
+
+    def _check_thresh(self, thresh, how):
+        """Check thresh parameter, should be a valid value as per docstring."""
+        if not isinstance(thresh, self.VALID_THRESH_TYPES):
+            raise ValueError(
+                f'invalid thresh parameter value encountered: "{thresh}", '
+                f"thresh must be of type: {self.VALID_HOW_VALUES}"
+            )
+        if isinstance(thresh, float) and not (0 < thresh < 1):
+            raise ValueError("thresh must be a fraction between zero and one")
+        if (how is not None) and (thresh is not None):
+            raise TypeError("thresh cannot be set together with how")
+
+        return thresh
+
+    def _check_remember(self, remember):
+        """Check remember parameter, should be a valid type as per docstring."""
+        if not isinstance(remember, self.VALID_REMEMBER_TYPES):
+            raise ValueError(
path: sktime/transformations/series/dropna.py
text: @user1:
```suggestion
            raise TypeError(
```

diff_hunk: @@ -0,0 +1,239 @@
+# copyright: sktime developers, BSD-3-Clause License (see LICENSE file)
+"""Transformer to drop rows or columns containing missing values."""
+
+__author__ = ["hliebert"]
+
+from sktime.transformations.base import BaseTransformer
+
+
+class DropNA(BaseTransformer):
+    """Drop missing values transformation.
+
+    Drops rows or columns with missing values from X. Mostly wraps
+    pandas.DataFrame.dropna, but allows specifying thresh as a fraction of
+    non-missing observations.
+
+    Parameters
+    ----------
+    axis : {0 or 'index', 1 or 'columns'}, default 0
+        Determine if rows or columns which contain missing values are removed.
+        Must be 0 or 'index' for univariate input.
+
+        * 0, or 'index' : Drop rows which contain missing values.
+        * 1, or 'columns' : Drop columns which contain missing value.
+
+    how : {'any', 'all'}, default 'any'
+        Determine if row or column is removed from DataFrame, when we have
+        at least one NA or all NA.
+
+        * 'any' : If any NA values are present, drop that row or column.
+        * 'all' : If all values are NA, drop that row or column.
+
+    thresh : int or float, optional
+         If int, require that many non-NA values (as in pandas.dropna). If
+         float, share of non-NA values for rows or columns to be retained.
path: sktime/transformations/series/dropna.py
text: @user1:
Will it make sense to add "at least" for `int` case? Just in case someone misinterpretes it to be exact value.

For `float` case, I found this line a bit difficult to follow. Is `retained` the intended word, because it sounds like `thresh=0.3` mean 30% non-null values will be kept and rest will be removed?

@author:
I'll add the at least, it makes it clearer (I partly took the original docstrings from `pandas.dropna`).

How about the following for the `float` case?

`If float, minimum share of non-NA values required for rows or columns to be retained.`

@user1:
Yes, that seems much better to me.

Can you please also mention about being strict proper fractions, 0/1 exclusive part?

@author:
I've adjusted it as discussed.

diff_hunk: @@ -0,0 +1,239 @@
+# copyright: sktime developers, BSD-3-Clause License (see LICENSE file)
+"""Transformer to drop rows or columns containing missing values."""
+
+__author__ = ["hliebert"]
+
+from sktime.transformations.base import BaseTransformer
+
+
+class DropNA(BaseTransformer):
+    """Drop missing values transformation.
+
+    Drops rows or columns with missing values from X. Mostly wraps
+    pandas.DataFrame.dropna, but allows specifying thresh as a fraction of
+    non-missing observations.
+
+    Parameters
+    ----------
+    axis : {0 or 'index', 1 or 'columns'}, default 0
+        Determine if rows or columns which contain missing values are removed.
+        Must be 0 or 'index' for univariate input.
+
+        * 0, or 'index' : Drop rows which contain missing values.
+        * 1, or 'columns' : Drop columns which contain missing value.
+
+    how : {'any', 'all'}, default 'any'
+        Determine if row or column is removed from DataFrame, when we have
+        at least one NA or all NA.
+
+        * 'any' : If any NA values are present, drop that row or column.
+        * 'all' : If all values are NA, drop that row or column.
+
+    thresh : int or float, optional
+         If int, require that many non-NA values (as in pandas.dropna). If
+         float, share of non-NA values for rows or columns to be retained.
+         Cannot be combined with how.
+
+    remember : bool, default False if axis==0, True if axis==1
+        If True, drops the same rows/columns in transform as in fit. If false,
+        drops rows/columns according to the NAs seen in transform (equivalent
+        to PandasTransformAdaptor(method="dropna")).
+    """
+
+    _tags = {
+        "univariate-only": False,
+        "scitype:transform-input": "Series",
+        "scitype:transform-output": "Series",
+        "scitype:instancewise": True,
+        "scitype:transform-labels": "None",
+        "X_inner_mtype": ["pd.DataFrame", "pd-multiindex", "pd_multiindex_hier"],
+        "fit_is_empty": False,
+        "capability:inverse_transform": False,
+        "capability:unequal_length": True,
+        "handles-missing-data": False,
+    }
+
+    VALID_AXIS_VALUES = [0, "index", 1, "columns"]
+    VALID_HOW_VALUES = [None, "any", "all"]
+    VALID_THRESH_TYPES = (type(None), int, float)
+    VALID_REMEMBER_TYPES = (type(None), bool)
+
+    def __init__(self, axis=0, how=None, thresh=None, remember=None):
+        self.axis = self._check_axis(axis)
+        self.how = self._check_how(how)
+        self.thresh = self._check_thresh(thresh, how)
+        self.remember = self._check_remember(remember)
+        super().__init__()
+
+        # axis, use numeric axis internally, default rows/index
+        if self.axis == "index":
+            self._axis = 0
+        elif self.axis == "columns":
+            self._axis = 1
+        else:
+            self._axis = self.axis
+
+        # criterion (how/thresh), default to "any" if neither how nor thresh passed
+        if (self.how is None) and (self.thresh is None):
+            self._how = "any"
+        else:
+            self._how = how
+        self._thresh = self.thresh
+
+        # remember, default to remember dropped columns but not rows
+        if self.remember is None:
+            self._remember = bool(self._axis)
+        else:
+            self._remember = self.remember
+
+    def _check_axis(self, axis):
+        """Check axis parameter, should be a valid string as per docstring."""
+        if axis not in self.VALID_AXIS_VALUES:
+            raise ValueError(
+                f'invalid axis parameter value encountered: "{axis}", '
+                f"axis must be one of: {self.VALID_AXIS_VALUES}"
+            )
+
+        return axis
+
+    def _check_how(self, how):
+        """Check how parameter, should be a valid string as per docstring."""
+        if how not in self.VALID_HOW_VALUES:
+            raise ValueError(
+                f'invalid how parameter value encountered: "{how}", '
+                f"how must be one of: {self.VALID_HOW_VALUES}"
+            )
+
+        return how
+
+    def _check_thresh(self, thresh, how):
+        """Check thresh parameter, should be a valid value as per docstring."""
+        if not isinstance(thresh, self.VALID_THRESH_TYPES):
+            raise ValueError(
+                f'invalid thresh parameter value encountered: "{thresh}", '
+                f"thresh must be of type: {self.VALID_HOW_VALUES}"
+            )
+        if isinstance(thresh, float) and not (0 < thresh < 1):
+            raise ValueError("thresh must be a fraction between zero and one")
+        if (how is not None) and (thresh is not None):
+            raise TypeError("thresh cannot be set together with how")
+
+        return thresh
+
+    def _check_remember(self, remember):
+        """Check remember parameter, should be a valid type as per docstring."""
+        if not isinstance(remember, self.VALID_REMEMBER_TYPES):
+            raise ValueError(
+                f'invalid remember parameter value encountered: "{remember}", '
+                f"remember must be of type: {self.VALID_REMEMBER_TYPES}"
+            )
+
+        return remember
+
+    def _fit(self, X, y=None):
+        """Fit transformer to X and y.
+
+        private _fit containing the core logic, called from fit
+
+        Parameters
+        ----------
+        X : pd.DataFrame
+            if self.get_tag("univariate-only")==True:
+                guaranteed to have a single column
+            if self.get_tag("univariate-only")==False: no restrictions apply
+        y : None, present only for interface compatibility
+
+        Returns
+        -------
+        self: reference to self
+        """
+        self.dropped_index_values_ = None
+        self._agg_axis = 1 - self._axis
+
+        mask = None
+        if self._how == "any":
+            mask = X.isna().any(axis=self._agg_axis)
+        elif self._how == "all":
+            mask = X.isna().all(axis=self._agg_axis)
+        elif isinstance(self._thresh, int):
+            mask = X.count(axis=self._agg_axis) < self._thresh
+        elif isinstance(self._thresh, float):
+            mask = (
+                X.count(axis=self._agg_axis).div(X.shape[self._agg_axis]) < self._thresh
+            )
path: sktime/transformations/series/dropna.py
text: @user1:
Why not pass this task to `pandas`?

```suggestion
        mask = X.dropna(axis=self._agg_axis, how=self._how, thresh=self._thresh)
```

@author:
Not sure what you are suggesting, `pandas` doesn't support fractions for `thresh`, and `dropna` doesn't help in getting the names of the dropped columns?

diff_hunk: @@ -0,0 +1,239 @@
+# copyright: sktime developers, BSD-3-Clause License (see LICENSE file)
+"""Transformer to drop rows or columns containing missing values."""
+
+__author__ = ["hliebert"]
+
+from sktime.transformations.base import BaseTransformer
+
+
+class DropNA(BaseTransformer):
+    """Drop missing values transformation.
+
+    Drops rows or columns with missing values from X. Mostly wraps
+    pandas.DataFrame.dropna, but allows specifying thresh as a fraction of
+    non-missing observations.
+
+    Parameters
+    ----------
+    axis : {0 or 'index', 1 or 'columns'}, default 0
+        Determine if rows or columns which contain missing values are removed.
+        Must be 0 or 'index' for univariate input.
+
+        * 0, or 'index' : Drop rows which contain missing values.
+        * 1, or 'columns' : Drop columns which contain missing value.
+
+    how : {'any', 'all'}, default 'any'
+        Determine if row or column is removed from DataFrame, when we have
+        at least one NA or all NA.
+
+        * 'any' : If any NA values are present, drop that row or column.
+        * 'all' : If all values are NA, drop that row or column.
+
+    thresh : int or float, optional
+         If int, require that many non-NA values (as in pandas.dropna). If
+         float, share of non-NA values for rows or columns to be retained.
+         Cannot be combined with how.
+
+    remember : bool, default False if axis==0, True if axis==1
+        If True, drops the same rows/columns in transform as in fit. If false,
+        drops rows/columns according to the NAs seen in transform (equivalent
+        to PandasTransformAdaptor(method="dropna")).
+    """
+
+    _tags = {
+        "univariate-only": False,
+        "scitype:transform-input": "Series",
+        "scitype:transform-output": "Series",
+        "scitype:instancewise": True,
+        "scitype:transform-labels": "None",
+        "X_inner_mtype": ["pd.DataFrame", "pd-multiindex", "pd_multiindex_hier"],
+        "fit_is_empty": False,
+        "capability:inverse_transform": False,
+        "capability:unequal_length": True,
+        "handles-missing-data": False,
+    }
+
+    VALID_AXIS_VALUES = [0, "index", 1, "columns"]
+    VALID_HOW_VALUES = [None, "any", "all"]
+    VALID_THRESH_TYPES = (type(None), int, float)
+    VALID_REMEMBER_TYPES = (type(None), bool)
+
+    def __init__(self, axis=0, how=None, thresh=None, remember=None):
+        self.axis = self._check_axis(axis)
+        self.how = self._check_how(how)
+        self.thresh = self._check_thresh(thresh, how)
+        self.remember = self._check_remember(remember)
+        super().__init__()
+
+        # axis, use numeric axis internally, default rows/index
+        if self.axis == "index":
+            self._axis = 0
+        elif self.axis == "columns":
+            self._axis = 1
+        else:
+            self._axis = self.axis
+
+        # criterion (how/thresh), default to "any" if neither how nor thresh passed
+        if (self.how is None) and (self.thresh is None):
+            self._how = "any"
+        else:
+            self._how = how
+        self._thresh = self.thresh
+
+        # remember, default to remember dropped columns but not rows
+        if self.remember is None:
+            self._remember = bool(self._axis)
+        else:
+            self._remember = self.remember
+
+    def _check_axis(self, axis):
+        """Check axis parameter, should be a valid string as per docstring."""
+        if axis not in self.VALID_AXIS_VALUES:
+            raise ValueError(
+                f'invalid axis parameter value encountered: "{axis}", '
+                f"axis must be one of: {self.VALID_AXIS_VALUES}"
+            )
+
+        return axis
+
+    def _check_how(self, how):
+        """Check how parameter, should be a valid string as per docstring."""
+        if how not in self.VALID_HOW_VALUES:
+            raise ValueError(
+                f'invalid how parameter value encountered: "{how}", '
+                f"how must be one of: {self.VALID_HOW_VALUES}"
+            )
+
+        return how
+
+    def _check_thresh(self, thresh, how):
+        """Check thresh parameter, should be a valid value as per docstring."""
+        if not isinstance(thresh, self.VALID_THRESH_TYPES):
+            raise ValueError(
path: sktime/transformations/series/dropna.py
text: @user1:
```suggestion
            raise TypeError(
```

diff_hunk: @@ -0,0 +1,239 @@
+# copyright: sktime developers, BSD-3-Clause License (see LICENSE file)
+"""Transformer to drop rows or columns containing missing values."""
+
+__author__ = ["hliebert"]
+
+from sktime.transformations.base import BaseTransformer
+
+
+class DropNA(BaseTransformer):
+    """Drop missing values transformation.
+
+    Drops rows or columns with missing values from X. Mostly wraps
+    pandas.DataFrame.dropna, but allows specifying thresh as a fraction of
+    non-missing observations.
+
+    Parameters
+    ----------
+    axis : {0 or 'index', 1 or 'columns'}, default 0
+        Determine if rows or columns which contain missing values are removed.
+        Must be 0 or 'index' for univariate input.
+
+        * 0, or 'index' : Drop rows which contain missing values.
+        * 1, or 'columns' : Drop columns which contain missing value.
+
+    how : {'any', 'all'}, default 'any'
+        Determine if row or column is removed from DataFrame, when we have
+        at least one NA or all NA.
+
+        * 'any' : If any NA values are present, drop that row or column.
+        * 'all' : If all values are NA, drop that row or column.
+
+    thresh : int or float, optional
+         If int, require that many non-NA values (as in pandas.dropna). If
+         float, share of non-NA values for rows or columns to be retained.
+         Cannot be combined with how.
+
+    remember : bool, default False if axis==0, True if axis==1
+        If True, drops the same rows/columns in transform as in fit. If false,
+        drops rows/columns according to the NAs seen in transform (equivalent
+        to PandasTransformAdaptor(method="dropna")).
+    """
+
+    _tags = {
+        "univariate-only": False,
+        "scitype:transform-input": "Series",
+        "scitype:transform-output": "Series",
+        "scitype:instancewise": True,
+        "scitype:transform-labels": "None",
+        "X_inner_mtype": ["pd.DataFrame", "pd-multiindex", "pd_multiindex_hier"],
+        "fit_is_empty": False,
+        "capability:inverse_transform": False,
+        "capability:unequal_length": True,
+        "handles-missing-data": False,
+    }
+
+    VALID_AXIS_VALUES = [0, "index", 1, "columns"]
+    VALID_HOW_VALUES = [None, "any", "all"]
+    VALID_THRESH_TYPES = (type(None), int, float)
+    VALID_REMEMBER_TYPES = (type(None), bool)
+
+    def __init__(self, axis=0, how=None, thresh=None, remember=None):
+        self.axis = self._check_axis(axis)
+        self.how = self._check_how(how)
+        self.thresh = self._check_thresh(thresh, how)
+        self.remember = self._check_remember(remember)
+        super().__init__()
+
+        # axis, use numeric axis internally, default rows/index
+        if self.axis == "index":
+            self._axis = 0
+        elif self.axis == "columns":
+            self._axis = 1
+        else:
+            self._axis = self.axis
+
+        # criterion (how/thresh), default to "any" if neither how nor thresh passed
+        if (self.how is None) and (self.thresh is None):
+            self._how = "any"
+        else:
+            self._how = how
+        self._thresh = self.thresh
+
+        # remember, default to remember dropped columns but not rows
+        if self.remember is None:
+            self._remember = bool(self._axis)
+        else:
+            self._remember = self.remember
+
+    def _check_axis(self, axis):
+        """Check axis parameter, should be a valid string as per docstring."""
+        if axis not in self.VALID_AXIS_VALUES:
+            raise ValueError(
+                f'invalid axis parameter value encountered: "{axis}", '
+                f"axis must be one of: {self.VALID_AXIS_VALUES}"
+            )
+
+        return axis
+
+    def _check_how(self, how):
+        """Check how parameter, should be a valid string as per docstring."""
+        if how not in self.VALID_HOW_VALUES:
+            raise ValueError(
+                f'invalid how parameter value encountered: "{how}", '
+                f"how must be one of: {self.VALID_HOW_VALUES}"
+            )
+
+        return how
+
+    def _check_thresh(self, thresh, how):
+        """Check thresh parameter, should be a valid value as per docstring."""
+        if not isinstance(thresh, self.VALID_THRESH_TYPES):
+            raise ValueError(
+                f'invalid thresh parameter value encountered: "{thresh}", '
+                f"thresh must be of type: {self.VALID_HOW_VALUES}"
+            )
+        if isinstance(thresh, float) and not (0 < thresh < 1):
+            raise ValueError("thresh must be a fraction between zero and one")
+        if (how is not None) and (thresh is not None):
+            raise TypeError("thresh cannot be set together with how")
+
+        return thresh
+
+    def _check_remember(self, remember):
+        """Check remember parameter, should be a valid type as per docstring."""
+        if not isinstance(remember, self.VALID_REMEMBER_TYPES):
+            raise ValueError(
+                f'invalid remember parameter value encountered: "{remember}", '
+                f"remember must be of type: {self.VALID_REMEMBER_TYPES}"
+            )
+
+        return remember
+
+    def _fit(self, X, y=None):
+        """Fit transformer to X and y.
+
+        private _fit containing the core logic, called from fit
+
+        Parameters
+        ----------
+        X : pd.DataFrame
+            if self.get_tag("univariate-only")==True:
+                guaranteed to have a single column
+            if self.get_tag("univariate-only")==False: no restrictions apply
+        y : None, present only for interface compatibility
+
+        Returns
+        -------
+        self: reference to self
+        """
+        self.dropped_index_values_ = None
+        self._agg_axis = 1 - self._axis
+
+        mask = None
+        if self._how == "any":
+            mask = X.isna().any(axis=self._agg_axis)
+        elif self._how == "all":
+            mask = X.isna().all(axis=self._agg_axis)
+        elif isinstance(self._thresh, int):
+            mask = X.count(axis=self._agg_axis) < self._thresh
+        elif isinstance(self._thresh, float):
+            mask = (
+                X.count(axis=self._agg_axis).div(X.shape[self._agg_axis]) < self._thresh
+            )
+
+        if mask is not None:
+            self.dropped_index_values_ = mask.index[mask].to_list()
+        else:
+            self.dropped_index_values_ = None
+
+        return self
+
+    def _transform(self, X, y=None):
+        """Transform X and return a transformed version.
+
+        private _transform containing core logic, called from transform
+
+        Parameters
+        ----------
+        X : pd.DataFrame
+            if self.get_tag("univariate-only")==True:
+                guaranteed to have a single column
+            if self.get_tag("univariate-only")==False: no restrictions apply
+        y : None, present only for interface compatibility
+
+        Returns
+        -------
+        transformed version of X
+        """
+        dropped_index_values = self.dropped_index_values_
+        agg_axis = self._agg_axis
+        axis = self._axis
+        how = self._how
+        thresh = self._thresh
+        remember = self._remember
+
+        if remember:
+            if dropped_index_values is not None:
+                return X.drop(labels=dropped_index_values, axis=axis)
+            else:
+                return X
+        else:
+            if isinstance(thresh, float):
+                mask = X.count(axis=agg_axis).div(X.shape[agg_axis]) < thresh
path: sktime/transformations/series/dropna.py
text: @user1:
```suggestion
                mask = X.notna().mean(axis=agg_axis) < thresh
```

diff_hunk: @@ -0,0 +1,239 @@
+# copyright: sktime developers, BSD-3-Clause License (see LICENSE file)
+"""Transformer to drop rows or columns containing missing values."""
+
+__author__ = ["hliebert"]
+
+from sktime.transformations.base import BaseTransformer
+
+
+class DropNA(BaseTransformer):
+    """Drop missing values transformation.
+
+    Drops rows or columns with missing values from X. Mostly wraps
+    pandas.DataFrame.dropna, but allows specifying thresh as a fraction of
+    non-missing observations.
+
+    Parameters
+    ----------
+    axis : {0 or 'index', 1 or 'columns'}, default 0
+        Determine if rows or columns which contain missing values are removed.
+        Must be 0 or 'index' for univariate input.
+
+        * 0, or 'index' : Drop rows which contain missing values.
+        * 1, or 'columns' : Drop columns which contain missing value.
+
+    how : {'any', 'all'}, default 'any'
+        Determine if row or column is removed from DataFrame, when we have
+        at least one NA or all NA.
+
+        * 'any' : If any NA values are present, drop that row or column.
+        * 'all' : If all values are NA, drop that row or column.
+
+    thresh : int or float, optional
+         If int, require that many non-NA values (as in pandas.dropna). If
+         float, share of non-NA values for rows or columns to be retained.
+         Cannot be combined with how.
+
+    remember : bool, default False if axis==0, True if axis==1
+        If True, drops the same rows/columns in transform as in fit. If false,
+        drops rows/columns according to the NAs seen in transform (equivalent
+        to PandasTransformAdaptor(method="dropna")).
+    """
+
+    _tags = {
+        "univariate-only": False,
+        "scitype:transform-input": "Series",
+        "scitype:transform-output": "Series",
+        "scitype:instancewise": True,
+        "scitype:transform-labels": "None",
+        "X_inner_mtype": ["pd.DataFrame", "pd-multiindex", "pd_multiindex_hier"],
+        "fit_is_empty": False,
+        "capability:inverse_transform": False,
+        "capability:unequal_length": True,
+        "handles-missing-data": False,
+    }
+
+    VALID_AXIS_VALUES = [0, "index", 1, "columns"]
+    VALID_HOW_VALUES = [None, "any", "all"]
+    VALID_THRESH_TYPES = (type(None), int, float)
+    VALID_REMEMBER_TYPES = (type(None), bool)
+
+    def __init__(self, axis=0, how=None, thresh=None, remember=None):
+        self.axis = self._check_axis(axis)
+        self.how = self._check_how(how)
+        self.thresh = self._check_thresh(thresh, how)
+        self.remember = self._check_remember(remember)
+        super().__init__()
+
+        # axis, use numeric axis internally, default rows/index
+        if self.axis == "index":
+            self._axis = 0
+        elif self.axis == "columns":
+            self._axis = 1
+        else:
+            self._axis = self.axis
+
+        # criterion (how/thresh), default to "any" if neither how nor thresh passed
+        if (self.how is None) and (self.thresh is None):
+            self._how = "any"
+        else:
+            self._how = how
+        self._thresh = self.thresh
+
+        # remember, default to remember dropped columns but not rows
+        if self.remember is None:
+            self._remember = bool(self._axis)
+        else:
+            self._remember = self.remember
+
+    def _check_axis(self, axis):
+        """Check axis parameter, should be a valid string as per docstring."""
+        if axis not in self.VALID_AXIS_VALUES:
+            raise ValueError(
+                f'invalid axis parameter value encountered: "{axis}", '
+                f"axis must be one of: {self.VALID_AXIS_VALUES}"
+            )
+
+        return axis
+
+    def _check_how(self, how):
+        """Check how parameter, should be a valid string as per docstring."""
+        if how not in self.VALID_HOW_VALUES:
+            raise ValueError(
+                f'invalid how parameter value encountered: "{how}", '
+                f"how must be one of: {self.VALID_HOW_VALUES}"
+            )
+
+        return how
+
+    def _check_thresh(self, thresh, how):
+        """Check thresh parameter, should be a valid value as per docstring."""
+        if not isinstance(thresh, self.VALID_THRESH_TYPES):
+            raise ValueError(
+                f'invalid thresh parameter value encountered: "{thresh}", '
+                f"thresh must be of type: {self.VALID_HOW_VALUES}"
+            )
+        if isinstance(thresh, float) and not (0 < thresh < 1):
+            raise ValueError("thresh must be a fraction between zero and one")
+        if (how is not None) and (thresh is not None):
+            raise TypeError("thresh cannot be set together with how")
+
+        return thresh
+
+    def _check_remember(self, remember):
+        """Check remember parameter, should be a valid type as per docstring."""
+        if not isinstance(remember, self.VALID_REMEMBER_TYPES):
+            raise ValueError(
+                f'invalid remember parameter value encountered: "{remember}", '
+                f"remember must be of type: {self.VALID_REMEMBER_TYPES}"
+            )
+
+        return remember
+
+    def _fit(self, X, y=None):
+        """Fit transformer to X and y.
+
+        private _fit containing the core logic, called from fit
+
+        Parameters
+        ----------
+        X : pd.DataFrame
+            if self.get_tag("univariate-only")==True:
+                guaranteed to have a single column
+            if self.get_tag("univariate-only")==False: no restrictions apply
+        y : None, present only for interface compatibility
+
+        Returns
+        -------
+        self: reference to self
+        """
+        self.dropped_index_values_ = None
+        self._agg_axis = 1 - self._axis
+
+        mask = None
+        if self._how == "any":
+            mask = X.isna().any(axis=self._agg_axis)
+        elif self._how == "all":
+            mask = X.isna().all(axis=self._agg_axis)
+        elif isinstance(self._thresh, int):
+            mask = X.count(axis=self._agg_axis) < self._thresh
+        elif isinstance(self._thresh, float):
+            mask = (
+                X.count(axis=self._agg_axis).div(X.shape[self._agg_axis]) < self._thresh
path: sktime/transformations/series/dropna.py
text: @user1:
```suggestion
                X.notna().mean(axis=self._agg_axis) < self._thresh
```

Will it be simpler?

@user1:
Btw, I wasn't aware of `div` method od dataframes, thanks.

@author:
This is simpler, thanks.