Repository: feature-engine/feature_engine
Pull request #392 (2022-03-23 23:41:22)

adds expanding window transformer
This PR adds an expanding window transformer to close https://github.com/feature-engine/feature_engine/issues/386

KishManani: The following are now complete:
- [ x]  add class to Readme
- [ x]  add class to docs/index.rst
- [ x]  add class to user_guide/ and api
- [ x]  add rst file with some examples on how to use the class
KishManani: Hi @solegalli. I've made the changes we discussed and resolved the conversations. Let me know if you spot anything else that needs changing! Thanks!

Patch:
diff --git a/README.md b/README.md
index c76661a..68a2cde 100644
--- a/README.md
+++ b/README.md
@@ -127,6 +127,7 @@ transforming parameters from the data and then transform it.
 ### Time Series
  * LagFeatures
  * WindowFeatures
+ * ExpandingWindowFeatures
  
 ### Preprocessing
  * MatchVariables
diff --git a/docs/api_doc/timeseries/forecasting/ExpandingWindowFeatures.rst b/docs/api_doc/timeseries/forecasting/ExpandingWindowFeatures.rst
new file mode 100644
index 0000000..d8742de
--- /dev/null
+++ b/docs/api_doc/timeseries/forecasting/ExpandingWindowFeatures.rst
@@ -0,0 +1,5 @@
+ExpandingWindowFeatures
+=======================
+
+.. autoclass:: feature_engine.timeseries.forecasting.ExpandingWindowFeatures
+    :members:
\ No newline at end of file
diff --git a/docs/api_doc/timeseries/forecasting/index.rst b/docs/api_doc/timeseries/forecasting/index.rst
index cdeac76..8670589 100644
--- a/docs/api_doc/timeseries/forecasting/index.rst
+++ b/docs/api_doc/timeseries/forecasting/index.rst
@@ -12,5 +12,6 @@ past.
 
    LagFeatures
    WindowFeatures
+   ExpandingWindowFeatures
 
 
diff --git a/docs/index.rst b/docs/index.rst
index 47f05dc..a5b79d8 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -187,6 +187,7 @@ Forecasting:
 
 - :doc:`api_doc/timeseries/forecasting/LagFeatures`: extract lag features
 - :doc:`api_doc/timeseries/forecasting/WindowFeatures`: create window features
+- :doc:`api_doc/timeseries/forecasting/ExpandingWindowFeatures`: create expanding window features
 
 Preprocessing:
 --------------
diff --git a/docs/user_guide/datetime/DatetimeFeatures.rst b/docs/user_guide/datetime/DatetimeFeatures.rst
index c548d1c..139968f 100644
--- a/docs/user_guide/datetime/DatetimeFeatures.rst
+++ b/docs/user_guide/datetime/DatetimeFeatures.rst
@@ -5,11 +5,18 @@
 DatetimeFeatures
 ================
 
-:class:`DatetimeFeatures()` extracts several datetime features from datetime
+:class:`DatetimeFeatures()` extracts several date and time features from datetime
 variables. It works with variables whose original dtype is datetime, and also with
 object-like and categorical variables, provided that they can be parsed into datetime
 format. It *cannot* extract features from numerical variables.
 
+In time series data, the index of the dataframe often contains date and time
+information. :class:`DatetimeFeatures()` can also extract date and time variables from
+the dataframe index.
+
+Tabular data
+------------
+
 Oftentimes, datasets contain information related to dates and/or times at which an event
 occurred. In pandas dataframes, these datetime variables can be cast as datetime or,
 more generically, as object.
@@ -17,18 +24,16 @@ more generically, as object.
 Datetime variables, in their raw format, are generally not suitable to train machine
 learning models. Yet, an enormous amount of information can be extracted from them.
 
-:class:`DatetimeFeatures()` is able to extract many numerical and binary date and
-time features from these datetime variables. Among these features we can find the month
-in which an event occurred, the day of the week, or whether that day was a weekend day.
+:class:`DatetimeFeatures()` extracts many numerical and binary date and time features
+from these datetime variables. Among these features, we find the month in which an
+event occurred, the day of the week, or whether that day was a weekend day.
 
-With :class:`DatetimeFeatures()` you can choose which date and time features
-to extract from your datetime variables. You can also extract date and time features
-from a subset of datetime variables in your data.
+With :class:`DatetimeFeatures()` we can choose which date and time features
+to extract from the datetime variables. We can also extract date and time features
+from a subset of datetime variables in the data.
 
-Examples
---------
 Through the following examples we highlight the functionality and versatility of
-:class:`DatetimeFeatures()`.
+:class:`DatetimeFeatures()` for tabular data.
 
 Extract date features
 ~~~~~~~~~~~~~~~~~~~~~
@@ -62,7 +67,7 @@ second datetime variable in our dataset.
 
     df_transf = dtfs.fit_transform(toy_df)
 
-    print(df_transf)
+    df_transf
 
 .. code:: python
 
@@ -80,6 +85,29 @@ By default, :class:`DatetimeFeatures()` drops the variable from which the date a
 features were extracted, in this case, *var_date2*. To keep the variable, we just need
 to indicate `drop_original=False` when initializing the transformer.
 
+Finally, we can obtain the name of the variables in the returned data as follows:
+
+.. code:: python
+
+    dtfs.get_feature_names_out()
+
+.. code:: python
+
+    ['var_date1',
+     'var_date2_month',
+     'var_date2_month_end',
+     'var_date2_day_of_year']
+
+Or we can obtain the names of the features created from a certain variable as follows:
+
+.. code:: python
+
+    dtfs.get_feature_names_out(["var_date2"])
+
+.. code:: python
+
+    ['var_date2_month', 'var_date2_month_end', 'var_date2_day_of_year']
+
 Extract time features
 ~~~~~~~~~~~~~~~~~~~~~
 
@@ -109,7 +137,7 @@ don't need to specify them.
 
     df_transf = dfts.fit_transform(toy_df)
 
-    print(df_transf)
+    df_transf
 
 .. code:: python
 
@@ -138,6 +166,37 @@ Again, the original datetime variables are dropped from the data by default. If
 want to keep them, we just need to indicate `drop_original=False` when initializing
 the transformer.
 
+Finally, if we want to obtain the names of the variables in the output data, we can use:
+
+.. code:: python
+
+    dfts.get_feature_names_out()
+
+.. code:: python
+
+    ['not_a_dt', 'var_time1_minute', 'var_time2_minute']
+
+We can obtain the date and time features derived from both our datetime variables as
+follows:
+
+.. code:: python
+
+    dfts.get_feature_names_out(['var_time1', 'var_time2'])
+
+.. code:: python
+
+    ['var_time1_minute', 'var_time2_minute']
+
+Or, we can obtain the features derived from just one of the variables like this:
+
+.. code:: python
+
+    dfts.get_feature_names_out(['var_time1'])
+
+.. code:: python
+
+    ['var_time1_minute']
+
 Extract date and time features
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -187,8 +246,113 @@ the features.
 
 And that is it. The additional features are now added in the dataframe.
 
+Time series
+-----------
+
+Time series data consist of datapoints indexed in time order. The time is usually in
+the index of the dataframe. With :class:`DatetimeFeatures` we can also create date and
+time features from the dataframe index.
+
+Let's create a toy dataframe with datetime in the index.
+
+.. code:: python
+
+    import pandas as pd
+
+    X = {"ambient_temp": [31.31, 31.51, 32.15, 32.39, 32.62, 32.5, 32.52, 32.68],
+         "module_temp": [49.18, 49.84, 52.35, 50.63, 49.61, 47.01, 46.67, 47.52],
+         "irradiation": [0.51, 0.79, 0.65, 0.76, 0.42, 0.49, 0.57, 0.56],
+         "color": ["green"] * 4 + ["blue"] * 4,
+         }
+
+    X = pd.DataFrame(X)
+    X.index = pd.date_range("2020-05-15 12:00:00", periods=8, freq="15min")
+
+    X.head()
+
+Below we see the output of our toy dataframe:
+
+.. code:: python
+
+                         ambient_temp  module_temp  irradiation  color
+    2020-05-15 12:00:00         31.31        49.18         0.51  green
+    2020-05-15 12:15:00         31.51        49.84         0.79  green
+    2020-05-15 12:30:00         32.15        52.35         0.65  green
+    2020-05-15 12:45:00         32.39        50.63         0.76  green
+    2020-05-15 13:00:00         32.62        49.61         0.42   blue
+
+We can extract features from the index as follows:
+
+..  code:: python
+
+    from feature_engine.datetime import DatetimeFeatures
+
+    dtf = DatetimeFeatures(variables="index")
+
+    Xtr = dtf.fit_transform(X)
+
+    Xtr
+
+We can see that the transformer created the default time features and added them at
+the end of the dataframe.
+
+.. code:: python
+
+                         ambient_temp  module_temp  irradiation  color  month  \
+    2020-05-15 12:00:00         31.31        49.18         0.51  green      5
+    2020-05-15 12:15:00         31.51        49.84         0.79  green      5
+    2020-05-15 12:30:00         32.15        52.35         0.65  green      5
+    2020-05-15 12:45:00         32.39        50.63         0.76  green      5
+    2020-05-15 13:00:00         32.62        49.61         0.42   blue      5
+    2020-05-15 13:15:00         32.50        47.01         0.49   blue      5
+    2020-05-15 13:30:00         32.52        46.67         0.57   blue      5
+    2020-05-15 13:45:00         32.68        47.52         0.56   blue      5
+
+                         year  day_of_week  day_of_month  hour  minute  second
+    2020-05-15 12:00:00  2020            4            15    12       0       0
+    2020-05-15 12:15:00  2020            4            15    12      15       0
+    2020-05-15 12:30:00  2020            4            15    12      30       0
+    2020-05-15 12:45:00  2020            4            15    12      45       0
+    2020-05-15 13:00:00  2020            4            15    13       0       0
+    2020-05-15 13:15:00  2020            4            15    13      15       0
+    2020-05-15 13:30:00  2020            4            15    13      30       0
+    2020-05-15 13:45:00  2020            4            15    13      45       0
+
+We can obtain the name of all the variables in the output dataframe as follows:
+
+.. code:: python
+
+    dtf.get_feature_names_out()
+
+.. code:: python
+
+    ['ambient_temp',
+     'module_temp',
+     'irradiation',
+     'color',
+     'month',
+     'year',
+     'day_of_week',
+     'day_of_month',
+     'hour',
+     'minute',
+     'second']
+
+
+Alternatively, we can obtain the name of the features created from the index as
+follows:
+
+.. code:: python
+
+    dtf.get_feature_names_out("index")
+
+.. code:: python
+
+    ['month', 'year', 'day_of_week', 'day_of_month', 'hour', 'minute', 'second']
+
+
 Important
-~~~~~~~~~
+---------
 
 We highly recommend specifying the date and time features that you would like to extract
 from your datetime variables. If you have too many time variables, this might not be
@@ -271,7 +435,7 @@ And mistakenly extract the hour and the minute.
 The new features will contain the value 0.
 
 Automating feature extraction
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+-----------------------------
 
 We can indicate which features we want to extract from the datetime variables as we did
 in the previous examples, passing the feature names in lists. Alternatively,
@@ -418,7 +582,7 @@ We can find the group of features extracted by the transformer in its attribute.
      'second']
 
 Extract and select features automatically
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+-----------------------------------------
 
 If we have a dataframe with a date variables, time variables and date and time variables,
 we can extract all features, or the most common features from all the variables, and then
@@ -494,7 +658,7 @@ from the dataset.
 As you can see, we do not have the constant features in the transformed dataset.
 
 Extract features from time-aware variables
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+------------------------------------------
 
 Time-aware datetime variables can be particularly cumbersome to work with as far
 as the format goes. We will briefly show how :class:`DatetimeFeatures()` deals
@@ -606,8 +770,9 @@ is the default option.
 Note that the hour extracted from the variable differ in this dataframe respect to the
 one obtained in **Case 2**.
 
+
 More details
-^^^^^^^^^^^^
+~~~~~~~~~~~~
 
 You can find additional examples with a real dataset on how to use
 :class:`DatetimeFeatures()` in the following Jupyter notebook.
diff --git a/docs/user_guide/timeseries/forecasting/ExpandingWindowFeatures.rst b/docs/user_guide/timeseries/forecasting/ExpandingWindowFeatures.rst
new file mode 100644
index 0000000..1a0a1fb
--- /dev/null
+++ b/docs/user_guide/timeseries/forecasting/ExpandingWindowFeatures.rst
@@ -0,0 +1,302 @@
+.. _expanding_window_features:
+
+.. currentmodule:: feature_engine.timeseries.forecasting
+
+ExpandingWindowFeatures
+=======================
+
+:class:`ExpandingWindowFeatures` adds expanding window features to the dataframe.
+Window features are the result of applying an aggregation operation (e.g., mean,
+min, max, etc.) to a variable over a past window. For an expanding window feature,
+the window spans from the start of the data up to, but not including, the
+time point at which the feature is being computed.
+
+For example, the mean value of all the data prior to the current value is an
+expanding window feature. The maximum value of all the rows prior to the
+current row is another expanding window feature.
+
+When forecasting the future values of a variable, the past values of that variable are
+likely to be predictive. To capitalize on the past values of a variable, we can simply
+lag features with :class:`LagFeatures`. We can also create features that summarise all
+the past values into a single quantity utilising :class:`ExpandingWindowFeatures`.
+
+:class:`ExpandingWindowFeatures` works on top of `pandas.expanding`, `pandas.aggregate`
+and `pandas.shift`.
+
+:class:`ExpandingWindowFeatures` uses `pandas.aggregate` to perform the mathematical
+operations over the expanding window. Therefore, you can use any operation supported
+by pandas. For supported aggregation functions, see Expanding Window
+`Functions <https://pandas.pydata.org/docs/reference/window.html>`_.
+
+With `pandas.shift`, :class:`ExpandingWindowFeatures` lags the result of the expanding
+window operation. This is useful to ensure that only the information known at predict
+time is used to compute the window feature. So if at predict time we only know
+the value of a feature at the previous time period and before that, then we should lag the
+the window feature by 1 period. If at predict time we only know the value of a feature
+from 2 weeks ago and before that, then we should lag the window feature column by 2 weeks.
+:class:`ExpandingWindowFeatures` uses a default lag of one period.
+
+:class:`ExpandingWindowFeatures` will add the new variables with a representative
+name to the original dataframe. It also has the methods `fit()` and `transform()`
+that make it compatible with the Scikit-learn's `Pipeline` and cross-validation
+functions.
+
+Note that to be compatible with :class:`ExpandingWindowFeatures` the dataframe's
+index must have unique values and no NaN.
+
+Examples
+--------
+
+Let's create a toy dataset to demonstrate the functionality of :class:`ExpandingWindowFeatures`.
+The dataframe contains 3 numerical variables, a categorical variable, and a datetime
+index.
+
+.. code:: python
+
+    import pandas as pd
+
+    X = {"ambient_temp": [31.31, 31.51, 32.15, 32.39, 32.62, 32.5, 32.52, 32.68],
+         "module_temp": [49.18, 49.84, 52.35, 50.63, 49.61, 47.01, 46.67, 47.52],
+         "irradiation": [0.51, 0.79, 0.65, 0.76, 0.42, 0.49, 0.57, 0.56],
+         "color": ["green"] * 4 + ["blue"] * 4,
+         }
+
+    X = pd.DataFrame(X)
+    X.index = pd.date_range("2020-05-15 12:00:00", periods=8, freq="15min")
+
+    X.head()
+
+Below we see the output of our toy dataframe:
+
+.. code:: python
+
+                         ambient_temp  module_temp  irradiation  color
+    2020-05-15 12:00:00         31.31        49.18         0.51  green
+    2020-05-15 12:15:00         31.51        49.84         0.79  green
+    2020-05-15 12:30:00         32.15        52.35         0.65  green
+    2020-05-15 12:45:00         32.39        50.63         0.76  green
+    2020-05-15 13:00:00         32.62        49.61         0.42   blue
+
+
+
+Now we will create expanding window features from the numerical variables. In `functions`,
+we indicate all the operations that we want to perform over those windows. In
+our example below, we want to calculate the mean and the standard deviation of the
+data within those windows and also find the maximum value within the windows.
+
+.. code:: python
+
+    from feature_engine.timeseries.forecasting import ExpandingWindowFeatures
+
+    win_f = ExpandingWindowFeatures(functions=["mean", "max", "std"])
+
+    X_tr = win_f.fit_transform(X)
+
+    X_tr.head()
+
+We can find the window features on the right side of the dataframe.
+
+.. code:: python
+
+                         ambient_temp  module_temp  irradiation  color  \
+    2020-05-15 12:00:00         31.31        49.18         0.51  green
+    2020-05-15 12:15:00         31.51        49.84         0.79  green
+    2020-05-15 12:30:00         32.15        52.35         0.65  green
+    2020-05-15 12:45:00         32.39        50.63         0.76  green
+    2020-05-15 13:00:00         32.62        49.61         0.42   blue
+
+                         ambient_temp_expanding_mean  ambient_temp_expanding_max  \
+    2020-05-15 12:00:00                          NaN                         NaN
+    2020-05-15 12:15:00                    31.310000                       31.31
+    2020-05-15 12:30:00                    31.410000                       31.51
+    2020-05-15 12:45:00                    31.656667                       32.15
+    2020-05-15 13:00:00                    31.840000                       32.39
+
+                         ambient_temp_expanding_std  module_temp_expanding_mean  \
+    2020-05-15 12:00:00                         NaN                         NaN
+    2020-05-15 12:15:00                         NaN                   49.180000
+    2020-05-15 12:30:00                    0.141421                   49.510000
+    2020-05-15 12:45:00                    0.438786                   50.456667
+    2020-05-15 13:00:00                    0.512640                   50.500000
+
+                         module_temp_expanding_max  module_temp_expanding_std  \
+    2020-05-15 12:00:00                        NaN                        NaN
+    2020-05-15 12:15:00                      49.18                        NaN
+    2020-05-15 12:30:00                      49.84                   0.466690
+    2020-05-15 12:45:00                      52.35                   1.672553
+    2020-05-15 13:00:00                      52.35                   1.368381
+
+                         irradiation_expanding_mean  irradiation_expanding_max  \
+    2020-05-15 12:00:00                         NaN                        NaN
+    2020-05-15 12:15:00                      0.5100                       0.51
+    2020-05-15 12:30:00                      0.6500                       0.79
+    2020-05-15 12:45:00                      0.6500                       0.79
+    2020-05-15 13:00:00                      0.6775                       0.79
+
+                         irradiation_expanding_std
+    2020-05-15 12:00:00                        NaN
+    2020-05-15 12:15:00                        NaN
+    2020-05-15 12:30:00                   0.197990
+    2020-05-15 12:45:00                   0.140000
+    2020-05-15 13:00:00                   0.126853
+
+
+The variables used as input for the window features are stored in the `variables_`
+attribute of the :class:`ExpandingWindowFeatures`.
+
+.. code:: python
+
+    win_f.variables_
+
+.. code:: python
+
+    ['ambient_temp', 'module_temp', 'irradiation']
+
+We can obtain the names of the variables in the returned dataframe using the
+get_feature_names_out() method:
+
+.. code:: python
+
+    win_f.get_feature_names_out()
+
+.. code:: python
+
+    ['ambient_temp',
+     'module_temp',
+     'irradiation',
+     'color',
+     'ambient_temp_expanding_mean',
+     'ambient_temp_expanding_max',
+     'ambient_temp_expanding_std',
+     'module_temp_expanding_mean',
+     'module_temp_expanding_max',
+     'module_temp_expanding_std',
+     'irradiation_expanding_mean',
+     'irradiation_expanding_max',
+     'irradiation_expanding_std']
+
+We can get the names of the expanding window features created for the variable
+"irradiation" as follows:
+
+.. code:: python
+
+    win_f.get_feature_names_out(["irradiation"])
+
+.. code:: python
+
+    ['irradiation_expanding_mean',
+     'irradiation_expanding_max',
+     'irradiation_expanding_std']
+
+
+Working with pandas series
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If your time series is a pandas Series instead of a pandas Dataframe, you need to
+transform it into a dataframe before using :class:`ExpandingWindowFeatures`.
+
+The following is a pandas Series:
+
+.. code:: python
+
+    X['ambient_temp']
+
+.. code:: python
+
+    2020-05-15 12:00:00    31.31
+    2020-05-15 12:15:00    31.51
+    2020-05-15 12:30:00    32.15
+    2020-05-15 12:45:00    32.39
+    2020-05-15 13:00:00    32.62
+    2020-05-15 13:15:00    32.50
+    2020-05-15 13:30:00    32.52
+    2020-05-15 13:45:00    32.68
+    Freq: 15T, Name: ambient_temp, dtype: float64
+
+We can use :class:`ExpandingWindowFeatures` to create, for example, 2 new expanding window
+features by finding the mean and maximum value of a pandas Series if we convert
+it to a pandas Dataframe using the method `to_frame()`:
+
+.. code:: python
+
+    win_f = ExpandingWindowFeatures(functions=["mean", "max"])
+
+    X_tr = win_f.fit_transform(X['ambient_temp'].to_frame())
+
+    X_tr.head()
+
+.. code:: python
+
+                         ambient_temp  ambient_temp_expanding_mean  \
+    2020-05-15 12:00:00         31.31                          NaN
+    2020-05-15 12:15:00         31.51                    31.310000
+    2020-05-15 12:30:00         32.15                    31.410000
+    2020-05-15 12:45:00         32.39                    31.656667
+    2020-05-15 13:00:00         32.62                    31.840000
+
+                         ambient_temp_expanding_max
+    2020-05-15 12:00:00                         NaN
+    2020-05-15 12:15:00                       31.31
+    2020-05-15 12:30:00                       31.51
+    2020-05-15 12:45:00                       32.15
+    2020-05-15 13:00:00                       32.39
+
+
+And if we do not want the original values of time series in the returned dataframe, we
+just need to remember to drop the original series after the transformation:
+
+.. code:: python
+
+    win_f = WindowFeatures(
+        functions=["mean", "max"],
+        drop_original=True,
+    )
+
+    X_tr = win_f.fit_transform(X['ambient_temp'].to_frame())
+
+    X_tr.head()
+
+.. code:: python
+
+                         ambient_temp_expanding_mean  ambient_temp_expanding_max
+    2020-05-15 12:00:00                          NaN                         NaN
+    2020-05-15 12:15:00                    31.310000                       31.31
+    2020-05-15 12:30:00                    31.410000                       31.51
+    2020-05-15 12:45:00                    31.656667                       32.15
+    2020-05-15 13:00:00                    31.840000                       32.39
+
+Getting the name of the variables
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+We can easily obtain the name of the original and new variables with the method
+`get_feature_names_out`. By using the method with the default parameters, we obtain
+all the features in the output dataframe.
+
+.. code:: python
+
+    win_f = ExpandingWindowFeatures()
+
+    win_f.fit(X)
+
+    win_f.get_feature_names_out()
+
+.. code:: python
+
+    ['ambient_temp',
+     'module_temp',
+     'irradiation',
+     'color',
+     'ambient_temp_expanding_mean',
+     'module_temp_expanding_mean',
+     'irradiation_expanding_mean']
+
+Alternatively, we can obtain the names of the expanding window features created from one or more
+input features as follows:
+
+.. code:: python
+
+    win_f.get_feature_names_out(input_features=["irradiation"])
+
+.. code:: python
+
+    ['irradiation_expanding_mean']
diff --git a/docs/user_guide/timeseries/forecasting/index.rst b/docs/user_guide/timeseries/forecasting/index.rst
index 6680da4..a12f648 100644
--- a/docs/user_guide/timeseries/forecasting/index.rst
+++ b/docs/user_guide/timeseries/forecasting/index.rst
@@ -15,5 +15,6 @@ the past.
 
    LagFeatures
    WindowFeatures
+   ExpandingWindowFeatures
 
 
diff --git a/feature_engine/_prediction/base_predictor.py b/feature_engine/_prediction/base_predictor.py
index 35a9e52..243e164 100644
--- a/feature_engine/_prediction/base_predictor.py
+++ b/feature_engine/_prediction/base_predictor.py
@@ -9,15 +9,15 @@ from sklearn.utils.validation import check_is_fitted
 from feature_engine.dataframe_checks import (
     _check_contains_inf,
     _check_contains_na,
-    _check_input_matches_training_df,
-    _is_dataframe,
+    _check_X_matches_training_df,
+    check_X,
 )
 from feature_engine.discretisation import (
     EqualFrequencyDiscretiser,
     EqualWidthDiscretiser,
 )
 from feature_engine.encoding import MeanEncoder
-from feature_engine.validation import _return_tags
+from feature_engine.tags import _return_tags
 from feature_engine.variable_manipulation import (
     _check_input_parameter_variables,
     _find_categorical_and_numerical_variables,
@@ -112,7 +112,7 @@ class BaseTargetMeanEstimator(BaseEstimator):
             The target variable.
         """
         # check if 'X' is a dataframe
-        X = _is_dataframe(X)
+        X = check_X(X)
 
         # Check X and y for consistent length
         if len(X) != len(y):
@@ -258,10 +258,10 @@ class BaseTargetMeanEstimator(BaseEstimator):
         check_is_fitted(self)
 
         # check that input is a dataframe
-        X = _is_dataframe(X)
+        X = check_X(X)
 
         # Check input data contains same number of columns as df used to fit
-        _check_input_matches_training_df(X, self.n_features_in_)
+        _check_X_matches_training_df(X, self.n_features_in_)
 
         # check for missing values
         _check_contains_na(X, self.variables_numerical_)
diff --git a/feature_engine/base_transformers.py b/feature_engine/base_transformers.py
index d2240d5..5b553da 100644
--- a/feature_engine/base_transformers.py
+++ b/feature_engine/base_transformers.py
@@ -11,10 +11,10 @@ from sklearn.utils.validation import check_is_fitted
 from feature_engine.dataframe_checks import (
     _check_contains_inf,
     _check_contains_na,
-    _check_input_matches_training_df,
-    _is_dataframe,
+    _check_X_matches_training_df,
+    check_X,
 )
-from feature_engine.validation import _return_tags
+from feature_engine.tags import _return_tags
 from feature_engine.variable_manipulation import _find_or_check_numerical_variables
 
 
@@ -50,7 +50,7 @@ class BaseNumericalTransformer(BaseEstimator, TransformerMixin):
             The same dataframe entered as parameter
         """
         # check input dataframe
-        X = _is_dataframe(X)
+        X = check_X(X)
 
         # find or check for numerical variables
         variables = [x for x in user_dict_.keys()]
@@ -96,7 +96,7 @@ class BaseNumericalTransformer(BaseEstimator, TransformerMixin):
         """
 
         # check input dataframe
-        X = _is_dataframe(X)
+        X = check_X(X)
 
         # find or check for numerical variables
         self.variables_ = _find_or_check_numerical_variables(X, self.variables)
@@ -140,10 +140,10 @@ class BaseNumericalTransformer(BaseEstimator, TransformerMixin):
         check_is_fitted(self)
 
         # check that input is a dataframe
-        X = _is_dataframe(X)
+        X = check_X(X)
 
         # Check if input data contains same number of columns as dataframe used to fit.
-        _check_input_matches_training_df(X, self.n_features_in_)
+        _check_X_matches_training_df(X, self.n_features_in_)
 
         # check if dataset contains na or inf
         _check_contains_na(X, self.variables_)
diff --git a/feature_engine/creation/base_creation.py b/feature_engine/creation/base_creation.py
index 9ed2a84..6391637 100644
--- a/feature_engine/creation/base_creation.py
+++ b/feature_engine/creation/base_creation.py
@@ -7,10 +7,10 @@ from sklearn.utils.validation import check_is_fitted
 from feature_engine.dataframe_checks import (
     _check_contains_inf,
     _check_contains_na,
-    _check_input_matches_training_df,
-    _is_dataframe,
+    _check_X_matches_training_df,
+    check_X,
 )
-from feature_engine.validation import _return_tags
+from feature_engine.tags import _return_tags
 from feature_engine.variable_manipulation import _find_or_check_numerical_variables
 
 
@@ -56,7 +56,7 @@ class BaseCreation(BaseEstimator, TransformerMixin):
         """
 
         # check input dataframe
-        X = _is_dataframe(X)
+        X = check_X(X)
 
         # check variables are numerical
         self.variables: List[Union[str, int]] = _find_or_check_numerical_variables(
@@ -95,10 +95,10 @@ class BaseCreation(BaseEstimator, TransformerMixin):
         check_is_fitted(self)
 
         # check that input is a dataframe
-        X = _is_dataframe(X)
+        X = check_X(X)
 
         # Check if input data contains same number of columns as dataframe used to fit.
-        _check_input_matches_training_df(X, self.n_features_in_)
+        _check_X_matches_training_df(X, self.n_features_in_)
 
         # check if dataset contains na
         if self.missing_values == "raise":
diff --git a/feature_engine/creation/combine_with_reference_feature.py b/feature_engine/creation/combine_with_reference_feature.py
index ac4bed2..7f720e3 100644
--- a/feature_engine/creation/combine_with_reference_feature.py
+++ b/feature_engine/creation/combine_with_reference_feature.py
@@ -8,8 +8,8 @@ from sklearn.utils.validation import check_is_fitted
 from feature_engine.dataframe_checks import (
     _check_contains_inf,
     _check_contains_na,
-    _check_input_matches_training_df,
-    _is_dataframe,
+    _check_X_matches_training_df,
+    check_X,
 )
 from feature_engine.docstrings import (
     Substitution,
@@ -20,7 +20,7 @@ from feature_engine.docstrings import (
     _missing_values_docstring,
     _n_features_in_docstring,
 )
-from feature_engine.validation import _return_tags
+from feature_engine.tags import _return_tags
 from feature_engine.variable_manipulation import _find_or_check_numerical_variables
 
 
@@ -210,7 +210,7 @@ class CombineWithReferenceFeature(BaseEstimator, TransformerMixin):
         """
 
         # check input dataframe
-        X = _is_dataframe(X)
+        X = check_X(X)
 
         # check variables to combine are numerical
         self.variables_to_combine = _find_or_check_numerical_variables(
@@ -265,10 +265,10 @@ class CombineWithReferenceFeature(BaseEstimator, TransformerMixin):
         check_is_fitted(self)
 
         # check that input is a dataframe
-        X = _is_dataframe(X)
+        X = check_X(X)
 
         # Check if input data contains same number of columns as dataframe used to fit.
-        _check_input_matches_training_df(X, self.n_features_in_)
+        _check_X_matches_training_df(X, self.n_features_in_)
 
         # check if dataset contains na
         if self.missing_values == "raise":
diff --git a/feature_engine/creation/mathematical_combination.py b/feature_engine/creation/mathematical_combination.py
index dbe7f81..4a54538 100644
--- a/feature_engine/creation/mathematical_combination.py
+++ b/feature_engine/creation/mathematical_combination.py
@@ -8,8 +8,8 @@ from sklearn.utils.validation import check_is_fitted
 from feature_engine.dataframe_checks import (
     _check_contains_inf,
     _check_contains_na,
-    _check_input_matches_training_df,
-    _is_dataframe,
+    _check_X_matches_training_df,
+    check_X,
 )
 from feature_engine.docstrings import (
     Substitution,
@@ -20,7 +20,7 @@ from feature_engine.docstrings import (
     _missing_values_docstring,
     _n_features_in_docstring,
 )
-from feature_engine.validation import _return_tags
+from feature_engine.tags import _return_tags
 from feature_engine.variable_manipulation import _find_or_check_numerical_variables
 
 
@@ -213,7 +213,7 @@ class MathematicalCombination(BaseEstimator, TransformerMixin):
         """
 
         # check input dataframe
-        X = _is_dataframe(X)
+        X = check_X(X)
 
         # check variables to combine are numerical
         self.variables_to_combine = _find_or_check_numerical_variables(
@@ -273,10 +273,10 @@ class MathematicalCombination(BaseEstimator, TransformerMixin):
         check_is_fitted(self)
 
         # check that input is a dataframe
-        X = _is_dataframe(X)
+        X = check_X(X)
 
         # Check if input data contains same number of columns as dataframe used to fit.
-        _check_input_matches_training_df(X, self.n_features_in_)
+        _check_X_matches_training_df(X, self.n_features_in_)
 
         # check if dataset contains na
         if self.missing_values == "raise":
diff --git a/feature_engine/dataframe_checks.py b/feature_engine/dataframe_checks.py
index efb210a..b85d415 100644
--- a/feature_engine/dataframe_checks.py
+++ b/feature_engine/dataframe_checks.py
@@ -9,52 +9,83 @@ import pandas as pd
 from scipy.sparse import issparse
 
 
-def _is_dataframe(X: pd.DataFrame) -> pd.DataFrame:
+def check_X(X: Union[np.generic, np.ndarray, pd.DataFrame]) -> pd.DataFrame:
     """
-    Checks if the input is a DataFrame and then creates a copy.
+    Checks if the input is a DataFrame and then creates a copy. This is an important
+    step not to accidentally transform the original dataset entered by the user.
 
-    If the input is a numpy array, it converts it to a pandas Dataframe. This is mostly
-    so that we can add the check_estimator checks for compatibility with sklearn.
+    If the input is a numpy array, it converts it to a pandas Dataframe. The column
+    names are strings representing the column index starting at 0.
+
+    Feature-engine was originally designed to work with pandas dataframes. However,
+    allowing numpy arrays as input allows 2 things:
+
+    We can use the Scikit-learn tests for transformers provided by the
+    `check_estimator` function to test the compatibility of our transformers with
+    sklearn functionality.
+
+    Feature-engine transformers can be used within a Scikit-learn Pipeline together
+    with Scikit-learn transformers like the `SimpleImputer`, which return by default
+    Numpy arrays.
 
     Parameters
     ----------
-    X : pandas Dataframe or numpy array. The one that will be checked and copied.
+    X : pandas Dataframe or numpy array.
+        The input to check and copy or transform.
 
     Raises
     ------
     TypeError
-        If the input is not a Pandas DataFrame or a numpy array
+        If the input is not a Pandas DataFrame or a numpy array.
+    ValueError
+        If the input is an empty dataframe.
 
     Returns
     -------
     X : pandas Dataframe.
-        A copy of original DataFrame. Important step not to accidentally transform the
-        original dataset entered by the user.
+        A copy of original DataFrame or a converted Numpy array.
     """
-    # check_estimator uses numpy arrays for its checks.
-    # Thus, we need to allow np arrays
-    if isinstance(X, (np.generic, np.ndarray)):
-        col_names = [str(i) for i in range(X.shape[1])]
-        X = pd.DataFrame(X, columns=col_names)
-
-    if issparse(X):
-        raise ValueError("This transformer does not support sparse matrices.")
-
-    if not isinstance(X, pd.DataFrame):
+    if isinstance(X, pd.DataFrame):
+        X = X.copy()
+
+    elif isinstance(X, (np.generic, np.ndarray)):
+        # If input is scalar raise error
+        if X.ndim == 0:
+            raise ValueError(
+                "Expected 2D array, got scalar array instead:\narray={}.\n"
+                "Reshape your data either using array.reshape(-1, 1) if "
+                "your data has a single feature or array.reshape(1, -1) "
+                "if it contains a single sample.".format(X)
+            )
+        # If input is 1D raise error
+        if X.ndim == 1:
+            raise ValueError(
+                "Expected 2D array, got 1D array instead:\narray={}.\n"
+                "Reshape your data either using array.reshape(-1, 1) if "
+                "your data has a single feature or array.reshape(1, -1) "
+                "if it contains a single sample.".format(X)
+            )
+
+        X = pd.DataFrame(X)
+        X.columns = [str(i) for i in range(X.shape[1])]
+
+    elif issparse(X):
+        raise TypeError("This transformer does not support sparse matrices.")
+
+    else:
         raise TypeError(
-            "X is not a pandas dataframe. The dataset should be a pandas dataframe."
+            f"X must be a numpy array or pandas dataframe. Got {type(X)} instead."
         )
 
     if X.empty:
         raise ValueError(
-            "0 feature(s) (shape=%s) while a minimum of %d is "
-            "required." % (X.shape, 1)
+            "0 feature(s) (shape=%s) while a minimum of %d is required." % (X.shape, 1)
         )
 
-    return X.copy()
+    return X
 
 
-def _check_input_matches_training_df(X: pd.DataFrame, reference: int) -> None:
+def _check_X_matches_training_df(X: pd.DataFrame, reference: int) -> None:
     """
     Checks that DataFrame to transform has the same number of columns that the
     DataFrame used with the fit() method.
diff --git a/feature_engine/datetime/_datetime_constants.py b/feature_engine/datetime/_datetime_constants.py
index 273247f..f7ef69a 100644
--- a/feature_engine/datetime/_datetime_constants.py
+++ b/feature_engine/datetime/_datetime_constants.py
@@ -23,6 +23,16 @@ FEATURES_SUPPORTED = [
     "second",
 ]
 
+FEATURES_DEFAULT = [
+    "month",
+    "year",
+    "day_of_week",
+    "day_of_month",
+    "hour",
+    "minute",
+    "second",
+]
+
 FEATURES_SUFFIXES = {
     "month": "_month",
     "quarter": "_quarter",
@@ -68,13 +78,3 @@ FEATURES_FUNCTIONS = {
     "minute": lambda x: x.dt.minute,
     "second": lambda x: x.dt.second,
 }
-
-FEATURES_DEFAULT = [
-    "month",
-    "year",
-    "day_of_week",
-    "day_of_month",
-    "hour",
-    "minute",
-    "second",
-]
diff --git a/feature_engine/datetime/datetime.py b/feature_engine/datetime/datetime.py
index eafa8d0..93d3499 100644
--- a/feature_engine/datetime/datetime.py
+++ b/feature_engine/datetime/datetime.py
@@ -4,13 +4,14 @@ from typing import List, Optional, Union
 
 import pandas as pd
 from pandas.api.types import is_datetime64_any_dtype as is_datetime
+from pandas.api.types import is_numeric_dtype as is_numeric
 from sklearn.base import BaseEstimator, TransformerMixin
 from sklearn.utils.validation import check_is_fitted
 
 from feature_engine.dataframe_checks import (
     _check_contains_na,
-    _check_input_matches_training_df,
-    _is_dataframe,
+    _check_X_matches_training_df,
+    check_X,
 )
 from feature_engine.datetime._datetime_constants import (
     FEATURES_DEFAULT,
@@ -28,6 +29,7 @@ from feature_engine.docstrings import (
 from feature_engine.variable_manipulation import (
     _check_input_parameter_variables,
     _find_or_check_datetime_variables,
+    _is_categorical_and_is_datetime,
 )
 
 
@@ -40,10 +42,10 @@ from feature_engine.variable_manipulation import (
 class DatetimeFeatures(BaseEstimator, TransformerMixin):
     """
     DatetimeFeatures extracts date and time features from datetime variables, adding
-    new columns to the dataset. DatetimeFeatures is able to extract datetime
-    information from existing datetime or object-like variables.
+    new columns to the dataset. DatetimeFeatures can extract datetime information from
+    existing datetime or object-like variables or from the dataframe index.
 
-    DatetimeFeatures uses pandas.to_datetime to convert object variables to datetime
+    DatetimeFeatures uses `pandas.to_datetime` to convert object variables to datetime
     and pandas.dt to extract the features from datetime.
 
     The transformer supports the extraction of the following features:
@@ -73,17 +75,19 @@ class DatetimeFeatures(BaseEstimator, TransformerMixin):
 
     Parameters
     ----------
-    variables: list, default=None
-        The list of variables to extract date and time features from.
+    variables: str, list, default=None
+        List with the variables from which date and time information will be extracted.
         If None, the transformer will find and select all datetime variables,
         including variables of type object that can be converted to datetime.
+        If "index", the transformer will extract datetime features from the
+        index of the dataframe.
 
     features_to_extract: list, default=None
         The list of date features to extract. If None, the following features will be
         extracted: "month", "year", "day_of_week", "day_of_month", "hour",
         "minute" and "second". If "all", all supported features will be extracted.
-        Alternatively, you can pass a list with the names of the supported features
-        you want to extract.
+        Alternatively, you can pass a list with the names of the features you want to
+        extract.
 
     drop_original: bool, default="True"
         If True, the original datetime variables will be dropped from the dataframe.
@@ -92,29 +96,37 @@ class DatetimeFeatures(BaseEstimator, TransformerMixin):
         Indicates if missing values should be ignored or raised. If 'raise' the
         transformer will return an error if the the datasets to `fit` or `transform`
         contain missing values. If 'ignore', missing data will be ignored when
-        performing the feature extraction.
+        performing the feature extraction. Missing data is only evaluated in the
+        variables that will be used to derive the date and time features. If features
+        are derived from the dataframe index, missing data will be checked in the
+        index.
 
     dayfirst: bool, default="False"
-        Specify a date parse order if arg is str or its list-likes. If True, parses
-        dates with the day first, eg 10/11/12 is parsed as 2012-11-10.
+        Specify a date parse order if arg is str or is list-like. If True, parses
+        dates with the day first, e.g. 10/11/12 is parsed as 2012-11-10. Same as in
+        `pandas.to_datetime`.
 
     yearfirst: bool, default="False"
-        Specify a date parse order if arg is str or its list-likes.
+        Specify a date parse order if arg is str or is list-like.
+        Same as in `pandas.to_datetime`.
 
-        - If True parses dates with the year first, eg 10/11/12 is parsed as 2010-11-12.
+        - If True parses dates with the year first, e.g. 10/11/12 is parsed as
+          2010-11-12.
         - If both dayfirst and yearfirst are True, yearfirst is preceded.
 
     utc: bool, default=None
         Return UTC DatetimeIndex if True (converting any tz-aware datetime.datetime
-        objects as well).
+        objects as well). Same as in `pandas.to_datetime`.
 
     Attributes
     ----------
     variables_:
-        List of variables from which date and time features will be extracted.
+        List of variables from which date and time features will be extracted. If None,
+        features will be extracted from the dataframe index.
 
     features_to_extract_:
-        The date and time features that will be extracted from each variable.
+        The date and time features that will be extracted from each variable or the
+        index.
 
     {feature_names_in_}
 
@@ -202,14 +214,31 @@ class DatetimeFeatures(BaseEstimator, TransformerMixin):
             It is not needed in this transformer. You can pass y or None.
         """
         # check input dataframe
-        X = _is_dataframe(X)
+        X = check_X(X)
 
-        # find or check for datetime variables
-        self.variables_ = _find_or_check_datetime_variables(X, self.variables)
+        # special case index
+        if self.variables == "index":
 
-        # check if datetime variables contains na
-        if self.missing_values == "raise":
-            _check_contains_na(X, self.variables_)
+            if not (
+                is_datetime(X.index)
+                or (
+                    not is_numeric(X.index) and _is_categorical_and_is_datetime(X.index)
+                )
+            ):
+                raise TypeError("The dataframe index is not datetime.")
+
+            if self.missing_values == "raise":
+                self._check_index_contains_na(X.index)
+
+            self.variables_ = None
+
+        else:
+            # find or check for datetime variables
+            self.variables_ = _find_or_check_datetime_variables(X, self.variables)
+
+            # check if datetime variables contains na
+            if self.missing_values == "raise":
+                _check_contains_na(X, self.variables_)
 
         if self.features_to_extract is None:
             self.features_to_extract_ = FEATURES_DEFAULT
@@ -245,62 +274,88 @@ class DatetimeFeatures(BaseEstimator, TransformerMixin):
         check_is_fitted(self)
 
         # check that input is a dataframe
-        X = _is_dataframe(X)
+        X = check_X(X)
 
         # Check if input data contains same number of columns as dataframe used to fit.
-        _check_input_matches_training_df(X, self.n_features_in_)
-
-        # check if dataset contains na
-        if self.missing_values == "raise":
-            _check_contains_na(X, self.variables_)
+        _check_X_matches_training_df(X, self.n_features_in_)
 
         # reorder variables to match train set
         X = X[self.feature_names_in_]
 
-        # convert datetime variables
-        datetime_df = pd.concat(
-            [
+        # special case index
+        if self.variables_ is None:
+            # check if dataset contains na
+            if self.missing_values == "raise":
+                self._check_index_contains_na(X.index)
+
+            # convert index to a datetime series
+            idx_datetime = pd.Series(
                 pd.to_datetime(
-                    X[variable],
+                    X.index,
                     dayfirst=self.dayfirst,
                     yearfirst=self.yearfirst,
                     utc=self.utc,
-                )
-                for variable in self.variables_
-            ],
-            axis=1,
-        )
-
-        non_dt_columns = datetime_df.columns[~datetime_df.apply(is_datetime)].tolist()
-        if non_dt_columns:
-            raise ValueError(
-                "ValueError: variable(s) "
-                + (len(non_dt_columns) * "{} ").format(*non_dt_columns)
-                + "could not be converted to datetime. Try setting utc=True"
+                ),
+                index=X.index,
             )
 
-        # create new features
-        for var in self.variables_:
+            # create new features
             for feat in self.features_to_extract_:
-                X[str(var) + FEATURES_SUFFIXES[feat]] = FEATURES_FUNCTIONS[feat](
-                    datetime_df[var]
+                X[FEATURES_SUFFIXES[feat][1:]] = FEATURES_FUNCTIONS[feat](idx_datetime)
+
+        else:
+            # check if dataset contains na
+            if self.missing_values == "raise":
+                _check_contains_na(X, self.variables_)
+
+            # convert datetime variables
+            datetime_df = pd.concat(
+                [
+                    pd.to_datetime(
+                        X[variable],
+                        dayfirst=self.dayfirst,
+                        yearfirst=self.yearfirst,
+                        utc=self.utc,
+                    )
+                    for variable in self.variables_
+                ],
+                axis=1,
+            )
+
+            non_dt_columns = datetime_df.columns[
+                ~datetime_df.apply(is_datetime)
+            ].tolist()
+            if non_dt_columns:
+                raise ValueError(
+                    "ValueError: variable(s) "
+                    + (len(non_dt_columns) * "{} ").format(*non_dt_columns)
+                    + "could not be converted to datetime. Try setting utc=True"
                 )
 
-        if self.drop_original:
-            X.drop(self.variables_, axis=1, inplace=True)
+            # create new features
+            for var in self.variables_:
+                for feat in self.features_to_extract_:
+                    X[str(var) + FEATURES_SUFFIXES[feat]] = FEATURES_FUNCTIONS[feat](
+                        datetime_df[var]
+                    )
+            if self.drop_original:
+                X.drop(self.variables_, axis=1, inplace=True)
 
         return X
 
-    def get_feature_names_out(self, input_features: Optional[List] = None) -> List:
+    def get_feature_names_out(
+        self, input_features: Optional[Union[List, str]] = None
+    ) -> List:
         """Get output feature names for transformation.
 
         Parameters
         ----------
-        input_features: list, default=None
-            Input features. If `input_features` is `None`, then the names of all the
-            variables in the transformed dataset (original + new variables) is returned.
-            Alternatively, only the names for the datetime features derived from
-            input_features will be returned.
+        input_features: str, list, default=None
+            Input features. If `None`, then the names of all the variables in the
+            transformed dataset (original + new variables) is returned. If 'index',
+            then the name of the features created from the index is returned. If list
+            with feature names, the names for the datetime features derived from
+            the variables in the list will be returned."
 
         Returns
         -------
@@ -309,44 +364,80 @@ class DatetimeFeatures(BaseEstimator, TransformerMixin):
         """
         check_is_fitted(self)
 
-        # Create names for all features or just the indicated ones.
-        if input_features is None:
-            # Create all features.
-            input_features_ = self.variables_
-        else:
-            if not isinstance(input_features, list):
-                raise ValueError(
-                    f"input_features must be a list. Got {input_features} instead."
+        # special case index
+        if self.variables_ is None:
+
+            if input_features is None:
+                feature_names = (
+                    self.feature_names_in_ + self._get_datetime_feature_names("index")
                 )
-            if any([f for f in input_features if f not in self.variables_]):
+
+            elif input_features == "index":
+                feature_names = self._get_datetime_feature_names("index")
+
+            else:
                 raise ValueError(
-                    "Some features in input_features were not used to extract new "
-                    "variables. You can only get the names of the extracted features "
-                    "with this function."
+                    "The dataframe index was used to extract date and time variables."
+                    "Thus, input_features can only be None or 'index'. "
+                    f"Got {input_features} instead."
                 )
-            # Create just indicated features.
-            input_features_ = input_features
-
-        # create the names for the lag features
-        feature_names = [
-            str(var) + FEATURES_SUFFIXES[feat]
-            for var in input_features_
-            for feat in self.features_to_extract_
-        ]
-
-        # Return names of all variables if input_features is None.
-        if input_features is None:
-            if self.drop_original is True:
-                # Remove names of variables to drop.
-                original = [
-                    f for f in self.feature_names_in_ if f not in self.variables_
-                ]
-                feature_names = original + feature_names
+
+        else:
+            # Return all features in transformed output.
+            if input_features is None:
+                # All datetime features.
+                feature_names = self._get_datetime_feature_names(self.variables_)
+
+                if self.drop_original is True:
+                    # Remove names of variables to drop.
+                    original = [
+                        f for f in self.feature_names_in_ if f not in self.variables_
+                    ]
+                    feature_names = original + feature_names
+                else:
+                    feature_names = self.feature_names_in_ + feature_names
+
             else:
-                feature_names = self.feature_names_in_ + feature_names
+                # Return features requested by user.
+                if not isinstance(input_features, list):
+                    raise ValueError(
+                        f"input_features must be a list. Got {input_features} instead."
+                    )
+                if any([f for f in input_features if f not in self.variables_]):
+                    raise ValueError(
+                        "Some features in input_features were not used to extract new "
+                        "variables. Pass either None, or a list with the features that "
+                        "were used to create date and time features."
+                    )
+                # Create just indicated features.
+                feature_names = self._get_datetime_feature_names(input_features)
 
         return feature_names
 
+    def _get_datetime_feature_names(self, input_features: Union[List, str]):
+        """create the names for the datetime features."""
+
+        if input_features == "index":
+            feature_names = [
+                FEATURES_SUFFIXES[feat][1:] for feat in self.features_to_extract_
+            ]
+        else:
+            feature_names = [
+                str(var) + FEATURES_SUFFIXES[feat]
+                for var in input_features
+                for feat in self.features_to_extract_
+            ]
+
+        return feature_names
+
+    def _check_index_contains_na(self, index: pd.Index):
+        if index.isnull().any():
+            raise ValueError(
+                "The dataframe index contains missing data. "
+                "Check and remove those before using this transformer "
+                "or set missing_values to False."
+            )
+
     def _more_tags(self):
         tags_dict = {"variables": "datetime"}
         return tags_dict
diff --git a/feature_engine/discretisation/arbitrary.py b/feature_engine/discretisation/arbitrary.py
index 569f3e3..791e288 100644
--- a/feature_engine/discretisation/arbitrary.py
+++ b/feature_engine/discretisation/arbitrary.py
@@ -15,7 +15,7 @@ from feature_engine.docstrings import (
     _n_features_in_docstring,
     _variables_attribute_docstring,
 )
-from feature_engine.validation import _return_tags
+from feature_engine.tags import _return_tags
 
 
 @Substitution(
diff --git a/feature_engine/discretisation/decision_tree.py b/feature_engine/discretisation/decision_tree.py
index d2b8b6f..b4fb9ad 100644
--- a/feature_engine/discretisation/decision_tree.py
+++ b/feature_engine/discretisation/decision_tree.py
@@ -17,7 +17,7 @@ from feature_engine.docstrings import (
     _variables_attribute_docstring,
     _variables_numerical_docstring,
 )
-from feature_engine.validation import _return_tags
+from feature_engine.tags import _return_tags
 from feature_engine.variable_manipulation import _check_input_parameter_variables
 
 
diff --git a/feature_engine/encoding/base_encoder.py b/feature_engine/encoding/base_encoder.py
index 2645a56..2bb15a6 100644
--- a/feature_engine/encoding/base_encoder.py
+++ b/feature_engine/encoding/base_encoder.py
@@ -7,8 +7,8 @@ from sklearn.utils.validation import check_is_fitted
 
 from feature_engine.dataframe_checks import (
     _check_contains_na,
-    _check_input_matches_training_df,
-    _is_dataframe,
+    _check_X_matches_training_df,
+    check_X,
 )
 from feature_engine.docstrings import Substitution
 from feature_engine.encoding._docstrings import (
@@ -16,7 +16,7 @@ from feature_engine.encoding._docstrings import (
     _ignore_format_docstring,
     _variables_docstring,
 )
-from feature_engine.validation import _return_tags
+from feature_engine.tags import _return_tags
 from feature_engine.variable_manipulation import (
     _check_input_parameter_variables,
     _find_all_variables,
@@ -81,7 +81,7 @@ class BaseCategoricalTransformer(BaseEstimator, TransformerMixin):
         """
 
         # check input dataframe
-        X = _is_dataframe(X)
+        X = check_X(X)
 
         if not self.ignore_format:
             # find categorical variables or check variables entered by user are object
@@ -130,10 +130,10 @@ class BaseCategoricalTransformer(BaseEstimator, TransformerMixin):
         check_is_fitted(self)
 
         # check that input is a dataframe
-        X = _is_dataframe(X)
+        X = check_X(X)
 
         # Check input data contains same number of columns as df used to fit
-        _check_input_matches_training_df(X, self.n_features_in_)
+        _check_X_matches_training_df(X, self.n_features_in_)
 
         # check if dataset contains na
         _check_contains_na(X, self.variables_)
diff --git a/feature_engine/encoding/decision_tree.py b/feature_engine/encoding/decision_tree.py
index d728a95..82e4604 100644
--- a/feature_engine/encoding/decision_tree.py
+++ b/feature_engine/encoding/decision_tree.py
@@ -21,7 +21,7 @@ from feature_engine.encoding._docstrings import (
 )
 from feature_engine.encoding.base_encoder import BaseCategoricalTransformer
 from feature_engine.encoding.ordinal import OrdinalEncoder
-from feature_engine.validation import _return_tags
+from feature_engine.tags import _return_tags
 
 
 @Substitution(
diff --git a/feature_engine/encoding/probability_ratio.py b/feature_engine/encoding/probability_ratio.py
index 6caaa83..bfc2669 100644
--- a/feature_engine/encoding/probability_ratio.py
+++ b/feature_engine/encoding/probability_ratio.py
@@ -21,7 +21,7 @@ from feature_engine.encoding._docstrings import (
     _variables_docstring,
 )
 from feature_engine.encoding.base_encoder import BaseCategorical
-from feature_engine.validation import _return_tags
+from feature_engine.tags import _return_tags
 
 
 @Substitution(
diff --git a/feature_engine/encoding/woe.py b/feature_engine/encoding/woe.py
index e996bb7..fd32421 100644
--- a/feature_engine/encoding/woe.py
+++ b/feature_engine/encoding/woe.py
@@ -21,7 +21,7 @@ from feature_engine.encoding._docstrings import (
     _variables_docstring,
 )
 from feature_engine.encoding.base_encoder import BaseCategorical
-from feature_engine.validation import _return_tags
+from feature_engine.tags import _return_tags
 
 
 @Substitution(
diff --git a/feature_engine/estimator_checks.py b/feature_engine/estimator_checks.py
index ffcfebd..6588918 100644
--- a/feature_engine/estimator_checks.py
+++ b/feature_engine/estimator_checks.py
@@ -198,6 +198,7 @@ def check_get_feature_names_out(estimator):
         "AddMissingIndicator",
         "LagFeatures",
         "WindowFeatures",
+        "ExpandingWindowFeatures",
         "MathFeatures",
         "CyclicalFeatures",
         "RelativeFeatures",
diff --git a/feature_engine/imputation/arbitrary_number.py b/feature_engine/imputation/arbitrary_number.py
index ec08874..67974c1 100644
--- a/feature_engine/imputation/arbitrary_number.py
+++ b/feature_engine/imputation/arbitrary_number.py
@@ -5,7 +5,7 @@ from typing import List, Optional, Union
 
 import pandas as pd
 
-from feature_engine.dataframe_checks import _is_dataframe
+from feature_engine.dataframe_checks import check_X
 from feature_engine.docstrings import (
     Substitution,
     _feature_names_in_docstring,
@@ -113,7 +113,7 @@ class ArbitraryNumberImputer(BaseImputer):
         """
 
         # check input dataframe
-        X = _is_dataframe(X)
+        X = check_X(X)
 
         # find or check for numerical variables
         # create the imputer dictionary
diff --git a/feature_engine/imputation/base_imputer.py b/feature_engine/imputation/base_imputer.py
index 69e3201..6200155 100644
--- a/feature_engine/imputation/base_imputer.py
+++ b/feature_engine/imputation/base_imputer.py
@@ -4,11 +4,8 @@ import pandas as pd
 from sklearn.base import BaseEstimator, TransformerMixin
 from sklearn.utils.validation import check_is_fitted
 
-from feature_engine.dataframe_checks import (
-    _check_input_matches_training_df,
-    _is_dataframe,
-)
-from feature_engine.validation import _return_tags
+from feature_engine.dataframe_checks import _check_X_matches_training_df, check_X
+from feature_engine.tags import _return_tags
 
 
 class BaseImputer(BaseEstimator, TransformerMixin):
@@ -49,10 +46,10 @@ class BaseImputer(BaseEstimator, TransformerMixin):
         check_is_fitted(self)
 
         # check that input is a dataframe
-        X = _is_dataframe(X)
+        X = check_X(X)
 
         # Check that input df contains same number of columns as df used to fit
-        _check_input_matches_training_df(X, self.n_features_in_)
+        _check_X_matches_training_df(X, self.n_features_in_)
 
         # reorder df to match train set
         X = X[self.feature_names_in_]
diff --git a/feature_engine/imputation/categorical.py b/feature_engine/imputation/categorical.py
index 0c6e0c1..6789444 100644
--- a/feature_engine/imputation/categorical.py
+++ b/feature_engine/imputation/categorical.py
@@ -5,7 +5,7 @@ from typing import List, Optional, Union
 
 import pandas as pd
 
-from feature_engine.dataframe_checks import _is_dataframe
+from feature_engine.dataframe_checks import check_X
 from feature_engine.docstrings import (
     Substitution,
     _feature_names_in_docstring,
@@ -14,7 +14,7 @@ from feature_engine.docstrings import (
     _variables_attribute_docstring,
 )
 from feature_engine.imputation.base_imputer import BaseImputer
-from feature_engine.validation import _return_tags
+from feature_engine.tags import _return_tags
 from feature_engine.variable_manipulation import (
     _check_input_parameter_variables,
     _find_all_variables,
@@ -138,7 +138,7 @@ class CategoricalImputer(BaseImputer):
         """
 
         # check input dataframe
-        X = _is_dataframe(X)
+        X = check_X(X)
 
         # check or select the the right variables
         if not self.ignore_format:
diff --git a/feature_engine/imputation/drop_missing_data.py b/feature_engine/imputation/drop_missing_data.py
index 86f8996..8aea8fe 100644
--- a/feature_engine/imputation/drop_missing_data.py
+++ b/feature_engine/imputation/drop_missing_data.py
@@ -5,7 +5,7 @@ from typing import List, Optional, Union
 
 import pandas as pd
 
-from feature_engine.dataframe_checks import _is_dataframe
+from feature_engine.dataframe_checks import check_X
 from feature_engine.docstrings import (
     Substitution,
     _feature_names_in_docstring,
@@ -13,7 +13,7 @@ from feature_engine.docstrings import (
     _n_features_in_docstring,
 )
 from feature_engine.imputation.base_imputer import BaseImputer
-from feature_engine.validation import _return_tags
+from feature_engine.tags import _return_tags
 from feature_engine.variable_manipulation import (
     _check_input_parameter_variables,
     _find_all_variables,
@@ -125,7 +125,7 @@ class DropMissingData(BaseImputer):
         """
 
         # check input dataframe
-        X = _is_dataframe(X)
+        X = check_X(X)
 
         # find variables for which indicator should be added
         self.variables_ = _find_all_variables(X, self.variables)
diff --git a/feature_engine/imputation/end_tail.py b/feature_engine/imputation/end_tail.py
index e5bf2dc..0d9fe7b 100644
--- a/feature_engine/imputation/end_tail.py
+++ b/feature_engine/imputation/end_tail.py
@@ -5,7 +5,7 @@ from typing import List, Optional, Union
 
 import pandas as pd
 
-from feature_engine.dataframe_checks import _is_dataframe
+from feature_engine.dataframe_checks import check_X
 from feature_engine.docstrings import (
     Substitution,
     _feature_names_in_docstring,
@@ -147,7 +147,7 @@ class EndTailImputer(BaseImputer):
             y is not needed in this imputation. You can pass None or y.
         """
         # check input dataframe
-        X = _is_dataframe(X)
+        X = check_X(X)
 
         # find or check for numerical variables
         self.variables_ = _find_or_check_numerical_variables(X, self.variables)
diff --git a/feature_engine/imputation/mean_median.py b/feature_engine/imputation/mean_median.py
index e51c2fe..e61bbd9 100644
--- a/feature_engine/imputation/mean_median.py
+++ b/feature_engine/imputation/mean_median.py
@@ -5,7 +5,7 @@ from typing import List, Optional, Union
 
 import pandas as pd
 
-from feature_engine.dataframe_checks import _is_dataframe
+from feature_engine.dataframe_checks import check_X
 from feature_engine.docstrings import (
     Substitution,
     _feature_names_in_docstring,
@@ -94,7 +94,7 @@ class MeanMedianImputer(BaseImputer):
         """
 
         # check input dataframe
-        X = _is_dataframe(X)
+        X = check_X(X)
 
         # find or check for numerical variables
         self.variables_ = _find_or_check_numerical_variables(X, self.variables)
diff --git a/feature_engine/imputation/missing_indicator.py b/feature_engine/imputation/missing_indicator.py
index 20647e2..f278934 100644
--- a/feature_engine/imputation/missing_indicator.py
+++ b/feature_engine/imputation/missing_indicator.py
@@ -6,7 +6,7 @@ from typing import List, Optional, Union
 import pandas as pd
 from sklearn.utils.validation import check_is_fitted
 
-from feature_engine.dataframe_checks import _is_dataframe
+from feature_engine.dataframe_checks import check_X
 from feature_engine.docstrings import (
     Substitution,
     _feature_names_in_docstring,
@@ -14,7 +14,7 @@ from feature_engine.docstrings import (
     _n_features_in_docstring,
 )
 from feature_engine.imputation.base_imputer import BaseImputer
-from feature_engine.validation import _return_tags
+from feature_engine.tags import _return_tags
 from feature_engine.variable_manipulation import (
     _check_input_parameter_variables,
     _find_all_variables,
@@ -107,7 +107,7 @@ class AddMissingIndicator(BaseImputer):
         """
 
         # check input dataframe
-        X = _is_dataframe(X)
+        X = check_X(X)
 
         # find variables for which indicator should be added
         self.variables_ = _find_all_variables(X, self.variables)
diff --git a/feature_engine/imputation/random_sample.py b/feature_engine/imputation/random_sample.py
index fd15b7d..fbeb7f0 100644
--- a/feature_engine/imputation/random_sample.py
+++ b/feature_engine/imputation/random_sample.py
@@ -6,7 +6,7 @@ from typing import List, Optional, Union
 import numpy as np
 import pandas as pd
 
-from feature_engine.dataframe_checks import _is_dataframe
+from feature_engine.dataframe_checks import check_X
 from feature_engine.docstrings import (
     Substitution,
     _feature_names_in_docstring,
@@ -15,7 +15,7 @@ from feature_engine.docstrings import (
     _variables_attribute_docstring,
 )
 from feature_engine.imputation.base_imputer import BaseImputer
-from feature_engine.validation import _return_tags
+from feature_engine.tags import _return_tags
 from feature_engine.variable_manipulation import (
     _check_input_parameter_variables,
     _find_all_variables,
@@ -158,7 +158,7 @@ class RandomSampleImputer(BaseImputer):
         """
 
         # check input dataframe
-        X = _is_dataframe(X)
+        X = check_X(X)
 
         # find variables to impute
         self.variables_ = _find_all_variables(X, self.variables)
diff --git a/feature_engine/outliers/artbitrary.py b/feature_engine/outliers/artbitrary.py
index baf299b..2abf6dc 100644
--- a/feature_engine/outliers/artbitrary.py
+++ b/feature_engine/outliers/artbitrary.py
@@ -9,7 +9,7 @@ import pandas as pd
 from feature_engine.dataframe_checks import (
     _check_contains_inf,
     _check_contains_na,
-    _is_dataframe,
+    check_X,
 )
 from feature_engine.docstrings import (
     Substitution,
@@ -22,7 +22,7 @@ from feature_engine.docstrings import (
 )
 from feature_engine.outliers.base_outlier import BaseOutlier
 from feature_engine.parameter_checks import _define_numerical_dict
-from feature_engine.validation import _return_tags
+from feature_engine.tags import _return_tags
 from feature_engine.variable_manipulation import _find_or_check_numerical_variables
 
 
@@ -114,7 +114,7 @@ class ArbitraryOutlierCapper(BaseOutlier):
         y: pandas Series, default=None
             y is not needed in this transformer. You can pass y or None.
         """
-        X = _is_dataframe(X)
+        X = check_X(X)
 
         # find variables to be capped
         if self.min_capping_dict is None and self.max_capping_dict:
diff --git a/feature_engine/outliers/base_outlier.py b/feature_engine/outliers/base_outlier.py
index fe6b208..c014244 100644
--- a/feature_engine/outliers/base_outlier.py
+++ b/feature_engine/outliers/base_outlier.py
@@ -8,10 +8,10 @@ from sklearn.utils.validation import check_is_fitted
 from feature_engine.dataframe_checks import (
     _check_contains_inf,
     _check_contains_na,
-    _check_input_matches_training_df,
-    _is_dataframe,
+    _check_X_matches_training_df,
+    check_X,
 )
-from feature_engine.validation import _return_tags
+from feature_engine.tags import _return_tags
 from feature_engine.variable_manipulation import (
     _check_input_parameter_variables,
     _find_or_check_numerical_variables,
@@ -55,11 +55,11 @@ class BaseOutlier(BaseEstimator, TransformerMixin):
         check_is_fitted(self)
 
         # check that input is a dataframe
-        X = _is_dataframe(X)
+        X = check_X(X)
 
         # Check that the dataframe contains the same number of columns
         # than the dataframe used to fit the imputer.
-        _check_input_matches_training_df(X, self.n_features_in_)
+        _check_X_matches_training_df(X, self.n_features_in_)
 
         if self.missing_values == "raise":
             # check if dataset contains na
@@ -239,7 +239,7 @@ class WinsorizerBase(BaseOutlier):
         """
 
         # check input dataframe
-        X = _is_dataframe(X)
+        X = check_X(X)
 
         # find or check for numerical variables
         self.variables_ = _find_or_check_numerical_variables(X, self.variables)
diff --git a/feature_engine/outliers/winsorizer.py b/feature_engine/outliers/winsorizer.py
index d2304df..6e83480 100644
--- a/feature_engine/outliers/winsorizer.py
+++ b/feature_engine/outliers/winsorizer.py
@@ -6,7 +6,7 @@ from typing import List, Union
 import numpy as np
 import pandas as pd
 
-from feature_engine.dataframe_checks import _is_dataframe
+from feature_engine.dataframe_checks import check_X
 from feature_engine.docstrings import (
     Substitution,
     _feature_names_in_docstring,
@@ -129,7 +129,7 @@ class Winsorizer(WinsorizerBase):
             X_out = super().transform(X)
 
         else:
-            X_orig = _is_dataframe(X)
+            X_orig = check_X(X)
             X_out = super().transform(X_orig)
             X_orig = X_orig[self.variables_]
             X_out_filtered = X_out[self.variables_]
diff --git a/feature_engine/preprocessing/match_columns.py b/feature_engine/preprocessing/match_columns.py
index d179a51..735f9c4 100644
--- a/feature_engine/preprocessing/match_columns.py
+++ b/feature_engine/preprocessing/match_columns.py
@@ -5,8 +5,8 @@ import pandas as pd
 from sklearn.base import BaseEstimator, TransformerMixin
 from sklearn.utils.validation import check_is_fitted
 
-from feature_engine.dataframe_checks import _check_contains_na, _is_dataframe
-from feature_engine.validation import _return_tags
+from feature_engine.dataframe_checks import _check_contains_na, check_X
+from feature_engine.tags import _return_tags
 
 
 class MatchVariables(BaseEstimator, TransformerMixin):
@@ -142,7 +142,7 @@ class MatchVariables(BaseEstimator, TransformerMixin):
         y: None
             y is not needed for this transformer. You can pass y or None.
         """
-        X = _is_dataframe(X)
+        X = check_X(X)
 
         if self.missing_values == "raise":
             # check if dataset contains na
@@ -173,7 +173,7 @@ class MatchVariables(BaseEstimator, TransformerMixin):
         """
         check_is_fitted(self)
 
-        X = _is_dataframe(X)
+        X = check_X(X)
 
         if self.missing_values == "raise":
             # check if dataset contains na
diff --git a/feature_engine/selection/base_recursive_selector.py b/feature_engine/selection/base_recursive_selector.py
index 962cb47..745a710 100644
--- a/feature_engine/selection/base_recursive_selector.py
+++ b/feature_engine/selection/base_recursive_selector.py
@@ -3,9 +3,9 @@ from typing import List, Union
 import pandas as pd
 from sklearn.model_selection import cross_validate
 
-from feature_engine.dataframe_checks import _is_dataframe
+from feature_engine.dataframe_checks import check_X
 from feature_engine.selection.base_selector import BaseSelector, get_feature_importances
-from feature_engine.validation import _return_tags
+from feature_engine.tags import _return_tags
 from feature_engine.variable_manipulation import (
     _check_input_parameter_variables,
     _find_or_check_numerical_variables,
@@ -142,7 +142,7 @@ class BaseRecursiveSelector(BaseSelector):
         """
 
         # check input dataframe
-        X = _is_dataframe(X)
+        X = check_X(X)
 
         # If required exclude variables that are not in the input dataframe
         self._confirm_variables(X)
diff --git a/feature_engine/selection/base_selector.py b/feature_engine/selection/base_selector.py
index 5967b1c..ec97d22 100644
--- a/feature_engine/selection/base_selector.py
+++ b/feature_engine/selection/base_selector.py
@@ -5,11 +5,8 @@ import pandas as pd
 from sklearn.base import BaseEstimator, TransformerMixin
 from sklearn.utils.validation import check_is_fitted
 
-from feature_engine.dataframe_checks import (
-    _check_input_matches_training_df,
-    _is_dataframe,
-)
-from feature_engine.validation import _return_tags
+from feature_engine.dataframe_checks import _check_X_matches_training_df, check_X
+from feature_engine.tags import _return_tags
 from feature_engine.variable_manipulation import _filter_out_variables_not_in_dataframe
 
 
@@ -100,10 +97,10 @@ class BaseSelector(BaseEstimator, TransformerMixin):
         check_is_fitted(self)
 
         # check if input is a dataframe
-        X = _is_dataframe(X)
+        X = check_X(X)
 
         # check if number of columns in test dataset matches to train dataset
-        _check_input_matches_training_df(X, self.n_features_in_)
+        _check_X_matches_training_df(X, self.n_features_in_)
 
         # reorder df to match train set
         X = X[self.feature_names_in_]
diff --git a/feature_engine/selection/drop_constant_features.py b/feature_engine/selection/drop_constant_features.py
index 31395d9..09b82fa 100644
--- a/feature_engine/selection/drop_constant_features.py
+++ b/feature_engine/selection/drop_constant_features.py
@@ -2,7 +2,7 @@ from typing import List, Union
 
 import pandas as pd
 
-from feature_engine.dataframe_checks import _check_contains_na, _is_dataframe
+from feature_engine.dataframe_checks import _check_contains_na, check_X
 from feature_engine.docstrings import (
     Substitution,
     _feature_names_in_docstring,
@@ -14,7 +14,7 @@ from feature_engine.selection._docstring import (
     _variables_attribute_docstring,
 )
 from feature_engine.selection.base_selector import BaseSelector
-from feature_engine.validation import _return_tags
+from feature_engine.tags import _return_tags
 from feature_engine.variable_manipulation import (
     _check_input_parameter_variables,
     _find_all_variables,
@@ -136,7 +136,7 @@ class DropConstantFeatures(BaseSelector):
         """
 
         # check input dataframe
-        X = _is_dataframe(X)
+        X = check_X(X)
 
         # If required exclude variables that are not in the input dataframe
         self._confirm_variables(X)
diff --git a/feature_engine/selection/drop_correlated_features.py b/feature_engine/selection/drop_correlated_features.py
index 2754c03..86f0947 100644
--- a/feature_engine/selection/drop_correlated_features.py
+++ b/feature_engine/selection/drop_correlated_features.py
@@ -5,7 +5,7 @@ import pandas as pd
 from feature_engine.dataframe_checks import (
     _check_contains_inf,
     _check_contains_na,
-    _is_dataframe,
+    check_X,
 )
 from feature_engine.docstrings import (
     Substitution,
@@ -141,7 +141,7 @@ class DropCorrelatedFeatures(BaseSelector):
         """
 
         # check input dataframe
-        X = _is_dataframe(X)
+        X = check_X(X)
 
         # If required exclude variables that are not in the input dataframe
         self._confirm_variables(X)
diff --git a/feature_engine/selection/drop_duplicate_features.py b/feature_engine/selection/drop_duplicate_features.py
index bc3f07d..c034b0e 100644
--- a/feature_engine/selection/drop_duplicate_features.py
+++ b/feature_engine/selection/drop_duplicate_features.py
@@ -2,7 +2,7 @@ from typing import List, Union
 
 import pandas as pd
 
-from feature_engine.dataframe_checks import _check_contains_na, _is_dataframe
+from feature_engine.dataframe_checks import _check_contains_na, check_X
 from feature_engine.docstrings import (
     Substitution,
     _feature_names_in_docstring,
@@ -15,7 +15,7 @@ from feature_engine.selection._docstring import (
     _variables_attribute_docstring,
 )
 from feature_engine.selection.base_selector import BaseSelector
-from feature_engine.validation import _return_tags
+from feature_engine.tags import _return_tags
 from feature_engine.variable_manipulation import (
     _check_input_parameter_variables,
     _find_all_variables,
@@ -112,7 +112,7 @@ class DropDuplicateFeatures(BaseSelector):
         """
 
         # check input dataframe
-        X = _is_dataframe(X)
+        X = check_X(X)
 
         # If required exclude variables that are not in the input dataframe
         self._confirm_variables(X)
diff --git a/feature_engine/selection/drop_features.py b/feature_engine/selection/drop_features.py
index 7c7491f..02b2400 100644
--- a/feature_engine/selection/drop_features.py
+++ b/feature_engine/selection/drop_features.py
@@ -2,9 +2,9 @@ from typing import List, Union
 
 import pandas as pd
 
-from feature_engine.dataframe_checks import _is_dataframe
+from feature_engine.dataframe_checks import check_X
 from feature_engine.selection.base_selector import BaseSelector
-from feature_engine.validation import _return_tags
+from feature_engine.tags import _return_tags
 
 
 class DropFeatures(BaseSelector):
@@ -52,10 +52,10 @@ class DropFeatures(BaseSelector):
 
     def __init__(self, features_to_drop: List[Union[str, int]]):
 
-        if not isinstance(features_to_drop, list) or len(features_to_drop) == 0:
+        if not isinstance(features_to_drop, (str, list)) or len(features_to_drop) == 0:
             raise ValueError(
-                "features_to_drop should be a list with the name of the variables"
-                "you wish to drop from the dataframe."
+                f"features_to_drop should be a list with the name of the variables "
+                f"you wish to drop from the dataframe. Got {features_to_drop} instead."
             )
 
         self.features_to_drop = features_to_drop
@@ -72,7 +72,7 @@ class DropFeatures(BaseSelector):
             y is not needed for this transformer. You can pass y or None.
         """
         # check input dataframe
-        X = _is_dataframe(X)
+        X = check_X(X)
 
         # X[self.features_to_drops] calls to pandas to check if columns are
         # present in the df.
diff --git a/feature_engine/selection/drop_psi_features.py b/feature_engine/selection/drop_psi_features.py
index 114e50f..1a05ddf 100644
--- a/feature_engine/selection/drop_psi_features.py
+++ b/feature_engine/selection/drop_psi_features.py
@@ -8,7 +8,7 @@ from pandas.api.types import is_numeric_dtype
 from feature_engine.dataframe_checks import (
     _check_contains_inf,
     _check_contains_na,
-    _is_dataframe,
+    check_X,
 )
 from feature_engine.discretisation import (
     EqualFrequencyDiscretiser,
@@ -309,7 +309,7 @@ class DropHighPSIFeatures(BaseSelector):
             y is not needed in this transformer. You can pass y or None.
         """
         # check input dataframe
-        X = _is_dataframe(X)
+        X = check_X(X)
 
         # If required exclude variables that are not in the input dataframe
         self._confirm_variables(X)
diff --git a/feature_engine/selection/shuffle_features.py b/feature_engine/selection/shuffle_features.py
index 6c44ed2..b078b91 100644
--- a/feature_engine/selection/shuffle_features.py
+++ b/feature_engine/selection/shuffle_features.py
@@ -7,7 +7,7 @@ from sklearn.metrics import get_scorer
 from sklearn.model_selection import check_cv, cross_validate
 from sklearn.utils.validation import check_random_state
 
-from feature_engine.dataframe_checks import _is_dataframe
+from feature_engine.dataframe_checks import check_X
 from feature_engine.docstrings import (
     Substitution,
     _feature_names_in_docstring,
@@ -27,7 +27,7 @@ from feature_engine.selection._docstring import (
     _variables_numerical_docstring,
 )
 from feature_engine.selection.base_selector import BaseSelector
-from feature_engine.validation import _return_tags
+from feature_engine.tags import _return_tags
 from feature_engine.variable_manipulation import (
     _check_input_parameter_variables,
     _find_or_check_numerical_variables,
@@ -163,7 +163,7 @@ class SelectByShuffling(BaseSelector):
         """
 
         # check input dataframe
-        X = _is_dataframe(X)
+        X = check_X(X)
 
         # reset the index
         X = X.reset_index(drop=True)
diff --git a/feature_engine/selection/single_feature_performance.py b/feature_engine/selection/single_feature_performance.py
index 01b204f..5c44dd5 100644
--- a/feature_engine/selection/single_feature_performance.py
+++ b/feature_engine/selection/single_feature_performance.py
@@ -4,7 +4,7 @@ from typing import List, Union
 import pandas as pd
 from sklearn.model_selection import cross_validate
 
-from feature_engine.dataframe_checks import _is_dataframe
+from feature_engine.dataframe_checks import check_X
 from feature_engine.docstrings import (
     Substitution,
     _feature_names_in_docstring,
@@ -24,7 +24,7 @@ from feature_engine.selection._docstring import (
     _variables_numerical_docstring,
 )
 from feature_engine.selection.base_selector import BaseSelector
-from feature_engine.validation import _return_tags
+from feature_engine.tags import _return_tags
 from feature_engine.variable_manipulation import (
     _check_input_parameter_variables,
     _find_or_check_numerical_variables,
@@ -156,7 +156,7 @@ class SelectBySingleFeaturePerformance(BaseSelector):
         """
 
         # check input dataframe
-        X = _is_dataframe(X)
+        X = check_X(X)
 
         # If required exclude variables that are not in the input dataframe
         self._confirm_variables(X)
diff --git a/feature_engine/selection/smart_correlation_selection.py b/feature_engine/selection/smart_correlation_selection.py
index b6234b4..1840d7b 100644
--- a/feature_engine/selection/smart_correlation_selection.py
+++ b/feature_engine/selection/smart_correlation_selection.py
@@ -6,7 +6,7 @@ from sklearn.model_selection import cross_validate
 from feature_engine.dataframe_checks import (
     _check_contains_inf,
     _check_contains_na,
-    _is_dataframe,
+    check_X,
 )
 from feature_engine.docstrings import (
     Substitution,
@@ -214,7 +214,7 @@ class SmartCorrelatedSelection(BaseSelector):
         """
 
         # check input dataframe
-        X = _is_dataframe(X)
+        X = check_X(X)
 
         # If required exclude variables that are not in the input dataframe
         self._confirm_variables(X)
diff --git a/feature_engine/selection/target_mean_selection.py b/feature_engine/selection/target_mean_selection.py
index a1267f4..26d2693 100644
--- a/feature_engine/selection/target_mean_selection.py
+++ b/feature_engine/selection/target_mean_selection.py
@@ -6,7 +6,7 @@ from sklearn.model_selection import cross_validate
 
 from feature_engine._prediction.target_mean_classifier import TargetMeanClassifier
 from feature_engine._prediction.target_mean_regressor import TargetMeanRegressor
-from feature_engine.dataframe_checks import _is_dataframe
+from feature_engine.dataframe_checks import check_X
 from feature_engine.docstrings import (
     Substitution,
     _feature_names_in_docstring,
@@ -27,7 +27,7 @@ from feature_engine.selection._selection_constants import (
     _REGRESSION_METRICS,
 )
 from feature_engine.selection.base_selector import BaseSelector
-from feature_engine.validation import _return_tags
+from feature_engine.tags import _return_tags
 from feature_engine.variable_manipulation import (
     _check_input_parameter_variables,
     _find_all_variables,
@@ -224,7 +224,7 @@ class SelectByTargetMeanPerformance(BaseSelector):
            Target variable. Required to train the estimator.
         """
         # check input dataframe
-        X = _is_dataframe(X)
+        X = check_X(X)
 
         # If required exclude variables that are not in the input dataframe
         self._confirm_variables(X)
diff --git a/feature_engine/validation.py b/feature_engine/tags.py
similarity index 84%
rename from feature_engine/validation.py
rename to feature_engine/tags.py
index ec8c7f8..4b8f518 100644
--- a/feature_engine/validation.py
+++ b/feature_engine/tags.py
@@ -3,9 +3,8 @@ def _return_tags():
         "_xfail_checks": {
             # Complex data in math terms, are values like 4i (imaginary numbers
             # so to speak). I've never seen such a thing in the dfs I've
-            # worked with, so I dont need this test.
-            "check_complex_data": "I dont think we need this check, if users "
-            "disagree we can think how to introduce it at a later stage.",
+            # worked with, so I don't think we need this test.
+            "check_complex_data": "Test not needed.",
             # check that estimators treat dtype object as numeric if possible
             "check_dtype_object": "Feature-engine transformers use dtypes to select "
             "between numerical and categorical variables. Feature-engine trusts the "
@@ -25,8 +24,6 @@ def _return_tags():
             # the test below tests that a second fit overrides a first fit.
             # the problem is that the test does not work with pandas df.
             "check_fit_idempotent": "Test does not work on dataframes",
-            "check_fit1d": "Test not relevant, Feature-engine transformers only"
-            "work with dataframes",
             "check_fit2d_predict1d": "Test not relevant, Feature-engine transformers "
             "only work with dataframes",
         }
diff --git a/feature_engine/timeseries/forecasting/__init__.py b/feature_engine/timeseries/forecasting/__init__.py
index 5e85e81..a98ebec 100644
--- a/feature_engine/timeseries/forecasting/__init__.py
+++ b/feature_engine/timeseries/forecasting/__init__.py
@@ -1,9 +1,11 @@
 """ Transformers that create features for time-series forecasting."""
 
+from .expanding_window_features import ExpandingWindowFeatures
 from .lag_features import LagFeatures
 from .window_features import WindowFeatures
 
 __all__ = [
     "LagFeatures",
     "WindowFeatures",
+    "ExpandingWindowFeatures"
 ]
diff --git a/feature_engine/timeseries/forecasting/base_forecast_transformers.py b/feature_engine/timeseries/forecasting/base_forecast_transformers.py
index 40715a2..771538a 100644
--- a/feature_engine/timeseries/forecasting/base_forecast_transformers.py
+++ b/feature_engine/timeseries/forecasting/base_forecast_transformers.py
@@ -7,8 +7,8 @@ from sklearn.utils.validation import check_is_fitted
 from feature_engine.dataframe_checks import (
     _check_contains_inf,
     _check_contains_na,
-    _check_input_matches_training_df,
-    _is_dataframe,
+    _check_X_matches_training_df,
+    check_X,
 )
 from feature_engine.docstrings import (
     Substitution,
@@ -18,7 +18,7 @@ from feature_engine.docstrings import (
     _missing_values_docstring,
     _n_features_in_docstring,
 )
-from feature_engine.validation import _return_tags
+from feature_engine.tags import _return_tags
 from feature_engine.variable_manipulation import (
     _check_input_parameter_variables,
     _find_or_check_numerical_variables,
@@ -143,7 +143,7 @@ class BaseForecastTransformer(BaseEstimator, TransformerMixin):
             y is not needed in this transformer. You can pass None or y.
         """
         # check input dataframe
-        X = _is_dataframe(X)
+        X = check_X(X)
 
         # We need the dataframes to have unique values in the index and no missing data.
         # Otherwise, when we merge the new features we will duplicate rows.
@@ -178,11 +178,11 @@ class BaseForecastTransformer(BaseEstimator, TransformerMixin):
         check_is_fitted(self)
 
         # check if 'X' is a dataframe
-        X = _is_dataframe(X)
+        X = check_X(X)
 
         # check if input data contains the same number of columns as the fitted
         # dataframe.
-        _check_input_matches_training_df(X, self.n_features_in_)
+        _check_X_matches_training_df(X, self.n_features_in_)
 
         # Dataframes must have unique values in the index and no missing data.
         # Otherwise, when we merge the created features we will duplicate rows.
diff --git a/feature_engine/timeseries/forecasting/expanding_window_features.py b/feature_engine/timeseries/forecasting/expanding_window_features.py
new file mode 100644
index 0000000..dad3e41
--- /dev/null
+++ b/feature_engine/timeseries/forecasting/expanding_window_features.py
@@ -0,0 +1,242 @@
+# Author: Kishan Manani
+# License: BSD 3 clause
+
+from __future__ import annotations
+
+import pandas as pd
+from sklearn.utils.validation import check_is_fitted
+
+from feature_engine.docstrings import (
+    Substitution,
+    _drop_original_docstring,
+    _feature_names_in_docstring,
+    _fit_not_learn_docstring,
+    _fit_transform_docstring,
+    _missing_values_docstring,
+    _n_features_in_docstring,
+    _variables_numerical_docstring,
+)
+from feature_engine.timeseries.forecasting.base_forecast_transformers import (
+    BaseForecastTransformer,
+)
+
+
+@Substitution(
+    variables=_variables_numerical_docstring,
+    missing_values=_missing_values_docstring,
+    drop_original=_drop_original_docstring,
+    feature_names_in_=_feature_names_in_docstring,
+    n_features_in_=_n_features_in_docstring,
+    fit=_fit_not_learn_docstring,
+    fit_transform=_fit_transform_docstring,
+)
+class ExpandingWindowFeatures(BaseForecastTransformer):
+    """
+    ExpandingWindowFeatures adds new features to a dataframe based on expanding window
+    operations. Expanding window operations are operations that perform an
+    aggregation over an expanding window of all past values relative to the
+    value of interest. An expanding window feature is, in other words, a feature
+    created after computing statistics (e.g., mean, min, max, etc.) using a
+    window over all the past data. For example, the mean value of all months
+    prior to the month of interest is an expanding window feature.
+
+    ExpandingWindowFeatures uses the pandas' functions `expanding()`, `agg()` and
+    `shift()`. With `expanding()`, it creates expanding windows. With `agg()` it
+    applies multiple functions within those windows. With 'shift()' it allocates
+    the values to the correct rows.
+
+    For supported aggregation functions, see Expanding Window
+    `Functions
+    <https://pandas.pydata.org/docs/reference/window.html#expanding-window-functions>`_.
+
+    To be compatible with ExpandingWindowFeatures, the dataframe's index must
+    have unique values and no NaN.
+
+    ExpandingWindowFeatures works only with numerical variables. You can pass a
+    list of variables to use as input for the expanding window. Alternatively,
+    ExpandingWindowFeatures will automatically select all numerical variables
+    in the training set.
+
+    More details in the :ref:`User Guide <expanding_window_features>`.
+
+    Parameters
+    ----------
+    {variables}
+
+    min_periods: int, default None.
+        Minimum number of observations in window required to have a value;
+        otherwise, result is np.nan. See parameter `min_periods` in the pandas
+        `expanding()` documentation for more details.
+
+    functions: str, list of str, default = 'mean'
+        The functions to apply within the window. Valid functions can be found
+        `here <https://pandas.pydata.org/docs/reference/window.html>`_.
+
+    periods: int, list of ints, default=1
+        Number of periods to shift. Can be a positive integer. See param `periods` in
+        pandas `shift`.
+
+    freq: str, list of str, default=None
+        Offset to use from the tseries module or time rule. See parameter `freq` in
+        pandas `shift()`.
+
+    sort_index: bool, default=True
+        Whether to order the index of the dataframe before creating the
+        expanding window feature.
+
+    {missing_values}
+
+    {drop_original}
+
+    Attributes
+    ----------
+    variables_:
+        The group of variables that will be used to create the expanding window
+        features.
+
+    {feature_names_in_}
+
+    {n_features_in_}
+
+    Methods
+    -------
+    {fit}
+
+    transform:
+        Add expanding window features.
+
+    {fit_transform}
+
+    See Also
+    --------
+    pandas.expanding
+    pandas.aggregate
+    pandas.shift
+    """
+
+    def __init__(
+        self,
+        variables: None | int | str | list[str | int] = None,
+        min_periods: int | None = None,
+        functions: str | list[str] = "mean",
+        periods: int = 1,
+        freq: str = None,
+        sort_index: bool = True,
+        missing_values: str = "raise",
+        drop_original: bool = False,
+    ) -> None:
+
+        if not isinstance(functions, (str, list)) or not all(
+            isinstance(val, str) for val in functions
+        ):
+            raise ValueError(
+                f"functions must be a list of strings or a string."
+                f"Got {functions} instead."
+            )
+        if isinstance(functions, list) and len(functions) != len(set(functions)):
+            raise ValueError(f"There are duplicated functions in the list: {functions}")
+
+        if not isinstance(periods, int) or periods < 0:
+            raise ValueError(
+                f"periods must be a non-negative integer. Got {periods} instead."
+            )
+
+        super().__init__(variables, missing_values, drop_original)
+
+        self.min_periods = min_periods
+        self.functions = functions
+        self.periods = periods
+        self.freq = freq
+        self.sort_index = sort_index
+
+    def transform(self, X: pd.DataFrame) -> pd.DataFrame:
+        """
+        Adds expanding window features.
+
+        Parameters
+        ----------
+        X: pandas dataframe of shape = [n_samples, n_features]
+            The data to transform.
+
+        Returns
+        -------
+        X_new: Pandas dataframe, shape = [n_samples, n_features + window_features]
+            The dataframe with the original plus the new variables.
+        """
+        # Common dataframe checks and setting up.
+        X = super().transform(X)
+
+        tmp = (
+            X[self.variables_]
+            .expanding(min_periods=self.min_periods)
+            .agg(self.functions)
+            .shift(periods=self.periods, freq=self.freq)
+        )
+
+        tmp.columns = self.get_feature_names_out(self.variables_)
+
+        X = X.merge(tmp, left_index=True, right_index=True, how="left")
+
+        if self.drop_original:
+            X = X.drop(self.variables_, axis=1)
+
+        return X
+
+    def get_feature_names_out(self, input_features: list | None = None) -> list:
+        """
+        Get output feature names for transformation.
+
+        Parameters
+        ----------
+        input_features: list, default=None
+            Input features. If `input_features` is `None`, then the names of all the
+            variables in the transformed dataset (original + new variables) is returned.
+            Alternatively, only the names for the window features derived from
+            input_features will be returned.
+
+        Returns
+        -------
+        feature_names_out: list
+            The feature names.
+        """
+        check_is_fitted(self)
+
+        # create names for all expanding window features or just the indicated ones.
+        if input_features is None:
+            input_features_ = self.variables_
+        else:
+            if not isinstance(input_features, list):
+                raise ValueError(
+                    f"input_features must be a list. Got {input_features} instead."
+                )
+            if any([f for f in input_features if f not in self.variables_]):
+                raise ValueError(
+                    "Some of the indicated features were not used to create window "
+                    "features."
+                )
+            # create just indicated window features
+            input_features_ = input_features
+
+        if not isinstance(self.functions, list):
+            functions_ = [self.functions]
+        else:
+            functions_ = self.functions
+
+        feature_names = [
+            f"{feature}_expanding_{agg}"
+            for feature in input_features_
+            for agg in functions_
+        ]
+
+        # return names of all variables if input_features is None
+        if input_features is None:
+            if self.drop_original is True:
+                # removes names of variables to drop
+                original = [
+                    f for f in self.feature_names_in_ if f not in self.variables_
+                ]
+                feature_names = original + feature_names
+            else:
+                feature_names = self.feature_names_in_ + feature_names
+
+        return feature_names
diff --git a/feature_engine/timeseries/forecasting/window_features.py b/feature_engine/timeseries/forecasting/window_features.py
index 0186848..b21bebc 100644
--- a/feature_engine/timeseries/forecasting/window_features.py
+++ b/feature_engine/timeseries/forecasting/window_features.py
@@ -82,7 +82,7 @@ class WindowFeatures(BaseForecastTransformer):
 
     periods: int, list of ints, default=1
         Number of periods to shift. Can be a positive integer. See param `periods` in
-        pandas `shift`.
+        pandas `shift()`.
 
     freq: str, list of str, default=None
         Offset to use from the tseries module or time rule. See parameter `freq` in
diff --git a/feature_engine/transformation/boxcox.py b/feature_engine/transformation/boxcox.py
index 5560145..0700d2a 100644
--- a/feature_engine/transformation/boxcox.py
+++ b/feature_engine/transformation/boxcox.py
@@ -15,7 +15,7 @@ from feature_engine.docstrings import (
     _variables_attribute_docstring,
     _variables_numerical_docstring,
 )
-from feature_engine.validation import _return_tags
+from feature_engine.tags import _return_tags
 from feature_engine.variable_manipulation import _check_input_parameter_variables
 
 
diff --git a/feature_engine/transformation/log.py b/feature_engine/transformation/log.py
index e8078eb..72f9937 100644
--- a/feature_engine/transformation/log.py
+++ b/feature_engine/transformation/log.py
@@ -17,7 +17,7 @@ from feature_engine.docstrings import (
     _variables_attribute_docstring,
     _variables_numerical_docstring,
 )
-from feature_engine.validation import _return_tags
+from feature_engine.tags import _return_tags
 from feature_engine.variable_manipulation import _check_input_parameter_variables
 
 
diff --git a/feature_engine/transformation/reciprocal.py b/feature_engine/transformation/reciprocal.py
index 59b1396..eac8356 100644
--- a/feature_engine/transformation/reciprocal.py
+++ b/feature_engine/transformation/reciprocal.py
@@ -17,7 +17,7 @@ from feature_engine.docstrings import (
     _variables_attribute_docstring,
     _variables_numerical_docstring,
 )
-from feature_engine.validation import _return_tags
+from feature_engine.tags import _return_tags
 from feature_engine.variable_manipulation import _check_input_parameter_variables
 
 
diff --git a/feature_engine/transformation/yeojohnson.py b/feature_engine/transformation/yeojohnson.py
index 93ca41f..43e3731 100644
--- a/feature_engine/transformation/yeojohnson.py
+++ b/feature_engine/transformation/yeojohnson.py
@@ -98,9 +98,6 @@ class YeoJohnsonTransformer(BaseNumericalTransformer):
 
         self.lambda_dict_ = {}
 
-        # to avoid NumPy error
-        X[self.variables_] = X[self.variables_].astype("float")
-
         for var in self.variables_:
             _, self.lambda_dict_[var] = stats.yeojohnson(X[var])
 
diff --git a/feature_engine/wrappers/wrappers.py b/feature_engine/wrappers/wrappers.py
index a2c5993..fcbc718 100644
--- a/feature_engine/wrappers/wrappers.py
+++ b/feature_engine/wrappers/wrappers.py
@@ -4,11 +4,8 @@ import pandas as pd
 from sklearn.base import BaseEstimator, TransformerMixin, clone
 from sklearn.utils.validation import check_is_fitted
 
-from feature_engine.dataframe_checks import (
-    _check_input_matches_training_df,
-    _is_dataframe,
-)
-from feature_engine.validation import _return_tags
+from feature_engine.dataframe_checks import _check_X_matches_training_df, check_X
+from feature_engine.tags import _return_tags
 from feature_engine.variable_manipulation import (
     _check_input_parameter_variables,
     _find_all_variables,
@@ -211,7 +208,7 @@ class SklearnTransformerWrapper(BaseEstimator, TransformerMixin):
         """
 
         # check input dataframe
-        X = _is_dataframe(X)
+        X = check_X(X)
 
         self.transformer_ = clone(self.transformer)
 
@@ -266,12 +263,12 @@ class SklearnTransformerWrapper(BaseEstimator, TransformerMixin):
         check_is_fitted(self)
 
         # check that input is a dataframe
-        X = _is_dataframe(X)
+        X = check_X(X)
 
         # Check that input data contains same number of columns than
         # the dataframe used to fit the imputer.
 
-        _check_input_matches_training_df(X, self.n_features_in_)
+        _check_X_matches_training_df(X, self.n_features_in_)
 
         # reorder df to match train set
         X = X[self.feature_names_in_]
@@ -325,7 +322,7 @@ class SklearnTransformerWrapper(BaseEstimator, TransformerMixin):
         check_is_fitted(self)
 
         # check that input is a dataframe
-        X = _is_dataframe(X)
+        X = check_X(X)
 
         if self.transformer_.__class__.__name__ not in _INVERSE_TRANSFORM:
             raise NotImplementedError(


Test patch:
diff --git a/tests/test_dataframe_checks.py b/tests/test_dataframe_checks.py
index 396cefc..6ed8472 100644
--- a/tests/test_dataframe_checks.py
+++ b/tests/test_dataframe_checks.py
@@ -1,24 +1,58 @@
+import numpy as np
+import pandas as pd
 import pytest
 from pandas.testing import assert_frame_equal
+from scipy.sparse import csr_matrix
 
 from feature_engine.dataframe_checks import (
+    _check_contains_inf,
     _check_contains_na,
-    _check_input_matches_training_df,
-    _is_dataframe,
+    _check_X_matches_training_df,
+    check_X,
 )
 
 
-def test_is_dataframe(df_vartypes):
-    assert_frame_equal(_is_dataframe(df_vartypes), df_vartypes)
+def test_check_X_returns_df(df_vartypes):
+    assert_frame_equal(check_X(df_vartypes), df_vartypes)
+
+
+def test_check_X_converts_numpy_to_pandas():
+    a1D = np.array([1, 2, 3, 4])
+    a2D = np.array([[1, 2], [3, 4]])
+    a3D = np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])
+
+    df_2D = pd.DataFrame(a2D, columns=["0", "1"])
+    assert_frame_equal(df_2D, check_X(a2D))
+
+    with pytest.raises(ValueError):
+        check_X(a3D)
+    with pytest.raises(ValueError):
+        check_X(a1D)
+
+
+def test_check_X_raises_error_sparse_matrix():
+    sparse_mx = csr_matrix([[5]])
     with pytest.raises(TypeError):
-        assert _is_dataframe([1, 2, 4])
+        assert check_X(sparse_mx)
+
+
+def test_raises_error_if_empty_df():
+    df = pd.DataFrame([])
+    with pytest.raises(ValueError):
+        check_X(df)
 
 
-def test_check_input_matches_training_df(df_vartypes):
+def test_check_X_matches_training_df(df_vartypes):
     with pytest.raises(ValueError):
-        assert _check_input_matches_training_df(df_vartypes, 4)
+        assert _check_X_matches_training_df(df_vartypes, 4)
 
 
 def test_contains_na(df_na):
     with pytest.raises(ValueError):
         assert _check_contains_na(df_na, ["Name", "City"])
+
+
+def test_contains_inf(df_na):
+    df_na.fillna(np.inf, inplace=True)
+    with pytest.raises(ValueError):
+        assert _check_contains_inf(df_na, ["Age", "Marks"])
diff --git a/tests/test_datetime/test_check_estimator_datetime.py b/tests/test_datetime/test_check_estimator_datetime.py
index f424ac6..0d71fdd 100644
--- a/tests/test_datetime/test_check_estimator_datetime.py
+++ b/tests/test_datetime/test_check_estimator_datetime.py
@@ -1,4 +1,5 @@
 import pytest
+
 from feature_engine.datetime import DatetimeFeatures
 from feature_engine.estimator_checks import check_feature_engine_estimator
 
diff --git a/tests/test_datetime/test_datetime_features.py b/tests/test_datetime/test_datetime_features.py
index e040889..a7a4f4d 100644
--- a/tests/test_datetime/test_datetime_features.py
+++ b/tests/test_datetime/test_datetime_features.py
@@ -14,6 +14,13 @@ vars_dt = ["datetime_range", "date_obj1", "date_obj2", "time_obj"]
 vars_non_dt = ["Name", "Age"]
 feat_names_default = [FEATURES_SUFFIXES[feat] for feat in FEATURES_DEFAULT]
 dates_nan = pd.DataFrame({"dates_na": ["Feb-2010", np.nan, "Jun-1922", np.nan]})
+dates_idx_nan = pd.DataFrame(
+    [1, 2, 3, 4], index=["Feb-2010", np.nan, "Jun-1922", np.nan]
+)
+dates_idx_dt = pd.DataFrame(
+    [4, 3, 2, 1],
+    index=pd.date_range("2003-02-27", periods=4, freq="D"),
+)
 
 
 _false_input_params = [
@@ -91,6 +98,8 @@ def test_raises_error_when_variables_not_datetime(df_datetime):
         DatetimeFeatures(variables=["Age"]).fit(df_datetime)
     with pytest.raises(TypeError):
         DatetimeFeatures(variables=["Name", "Age", "date_obj1"]).fit(df_datetime)
+    with pytest.raises(TypeError):
+        DatetimeFeatures(variables="index").fit(df_datetime)
     # passing a df that contains no datetime variables
     with pytest.raises(ValueError):
         DatetimeFeatures().fit(df_datetime[["Name", "Age"]])
@@ -100,6 +109,8 @@ def test_raises_error_when_df_has_nan():
     # dataset containing nans
     with pytest.raises(ValueError):
         DatetimeFeatures().fit(dates_nan)
+    with pytest.raises(ValueError):
+        DatetimeFeatures(variables="index").fit(dates_idx_nan)
 
 
 def test_attributes_upon_fitting(df_datetime):
@@ -148,7 +159,11 @@ def test_raises_error_when_nan_in_transform_df(df_datetime):
     transformer.fit(df_datetime)
     # dataset containing nans
     with pytest.raises(ValueError):
-        DatetimeFeatures().transform(dates_nan)
+        transformer.transform(dates_nan)
+    transformer = DatetimeFeatures(variables="index")
+    transformer.fit(dates_idx_dt)
+    with pytest.raises(ValueError):
+        transformer.transform(dates_idx_nan)
 
 
 def test_raises_non_fitted_error(df_datetime):
@@ -218,6 +233,25 @@ def test_extract_datetime_features_from_specified_variables(
         ],
     )
 
+    # datetime variable is index
+    X = DatetimeFeatures(
+        variables="index", features_to_extract=["month", "day_of_month"]
+    ).fit_transform(dates_idx_dt)
+    pd.testing.assert_frame_equal(
+        X,
+        pd.concat(
+            [
+                dates_idx_dt,
+                pd.DataFrame(
+                    [[2, 27], [2, 28], [3, 1], [3, 2]],
+                    index=dates_idx_dt.index,
+                    columns=["month", "day_of_month"],
+                ),
+            ],
+            axis=1,
+        ),
+    )
+
 
 def test_extract_all_datetime_features(df_datetime, df_datetime_transformed):
     X = DatetimeFeatures(features_to_extract="all").fit_transform(df_datetime)
@@ -375,6 +409,22 @@ def test_extract_features_from_variables_containing_nans():
         X,
         pd.DataFrame({"dates_na_year": [2010, np.nan, 1922, np.nan]}),
     )
+    # dt variable is index
+    X = DatetimeFeatures(
+        variables="index", features_to_extract=["month"], missing_values="ignore"
+    ).fit_transform(dates_idx_nan)
+    pd.testing.assert_frame_equal(
+        X,
+        pd.concat(
+            [
+                dates_idx_nan,
+                pd.DataFrame(
+                    {"month": [2, np.nan, 6, np.nan]}, index=dates_idx_nan.index
+                ),
+            ],
+            axis=1,
+        ),
+    )
 
 
 def test_extract_features_with_different_datetime_parsing_options(df_datetime):
@@ -438,6 +488,23 @@ def test_get_feature_names_out(df_datetime, df_datetime_transformed):
         "date_obj1_" + feat for feat in ["semester", "week"]
     ]
 
+    # features were extracted from index
+    transformer = DatetimeFeatures(
+        variables="index", features_to_extract=["semester", "week"]
+    )
+    X = transformer.fit_transform(dates_idx_dt)
+    # user passes something else than index as input_features
+    with pytest.raises(ValueError):
+        transformer.get_feature_names_out(input_features="not_index")
+    with pytest.raises(ValueError):
+        transformer.get_feature_names_out(input_features=["still", "not", "index"])
+    # input_features is None
+    assert list(X.columns) == transformer.get_feature_names_out()
+    # input_features is index
+    assert ["semester", "week"] == transformer.get_feature_names_out(
+        input_features="index"
+    )
+
     # when drop original is False
     transformer = DatetimeFeatures(drop_original=False)
     X = transformer.fit_transform(df_datetime)
diff --git a/tests/test_selection/test_drop_features.py b/tests/test_selection/test_drop_features.py
index d463df0..49cb8db 100644
--- a/tests/test_selection/test_drop_features.py
+++ b/tests/test_selection/test_drop_features.py
@@ -4,6 +4,29 @@ import pytest
 from feature_engine.selection import DropFeatures
 
 
+def test_drop_1_variable(df_vartypes):
+    transformer = DropFeatures(features_to_drop="City")
+    X = transformer.fit_transform(df_vartypes)
+
+    # expected result
+    df = pd.DataFrame(
+        {
+            "Name": ["tom", "nick", "krish", "jack"],
+            "Age": [20, 21, 19, 18],
+            "Marks": [0.9, 0.8, 0.7, 0.6],
+            "dob": pd.date_range("2020-02-24", periods=4, freq="T"),
+        }
+    )
+
+    # init params
+    assert transformer.features_to_drop == "City"
+
+    # transform params
+    assert X.shape == (4, 4)
+    assert type(X) == pd.DataFrame
+    pd.testing.assert_frame_equal(X, df)
+
+
 def test_drop_2_variables(df_vartypes):
     transformer = DropFeatures(features_to_drop=["City", "dob"])
     X = transformer.fit_transform(df_vartypes)
diff --git a/tests/test_time_series/test_forecasting/test_check_estimator_forecasting.py b/tests/test_time_series/test_forecasting/test_check_estimator_forecasting.py
index 67af7ae..f94aa61 100644
--- a/tests/test_time_series/test_forecasting/test_check_estimator_forecasting.py
+++ b/tests/test_time_series/test_forecasting/test_check_estimator_forecasting.py
@@ -5,11 +5,16 @@ from sklearn.base import clone
 from sklearn.utils.estimator_checks import check_estimator
 
 from feature_engine.estimator_checks import check_feature_engine_estimator
-from feature_engine.timeseries.forecasting import LagFeatures, WindowFeatures
+from feature_engine.timeseries.forecasting import (
+    ExpandingWindowFeatures,
+    LagFeatures,
+    WindowFeatures,
+)
 
 _estimators = [
     LagFeatures(missing_values="ignore"),
     WindowFeatures(missing_values="ignore"),
+    ExpandingWindowFeatures(missing_values="ignore")
 ]
 
 
diff --git a/tests/test_time_series/test_forecasting/test_expanding_window_features.py b/tests/test_time_series/test_forecasting/test_expanding_window_features.py
new file mode 100644
index 0000000..ab4a1de
--- /dev/null
+++ b/tests/test_time_series/test_forecasting/test_expanding_window_features.py
@@ -0,0 +1,432 @@
+import numpy as np
+import pytest
+from pandas.testing import assert_frame_equal
+
+from feature_engine.timeseries.forecasting import ExpandingWindowFeatures
+
+
+def test_get_feature_names_out_raises_when_input_features_is_string(df_time):
+
+    tr = ExpandingWindowFeatures(functions=["mean", "sum"])
+    tr.fit(df_time)
+
+    with pytest.raises(ValueError):
+        # get error when user does not pass a list
+        tr.get_feature_names_out(input_features="ambient_temp")
+
+
+def test_get_feature_names_out_raises_when_input_features_not_transformed(df_time):
+
+    tr = ExpandingWindowFeatures(functions=["mean", "sum"])
+    tr.fit(df_time)
+
+    with pytest.raises(ValueError):
+        # assert error when uses passes features that were not transformed
+        tr.get_feature_names_out(input_features=["color"])
+
+
+@pytest.mark.parametrize("_periods", [1, 2, 3])
+def test_permitted_param_periods(_periods):
+    transformer = ExpandingWindowFeatures(periods=_periods)
+    assert transformer.periods == _periods
+
+
+def test_get_feature_names_out_multiple_variables_and_functions(df_time):
+    # input features
+    input_features = ["ambient_temp", "module_temp", "irradiation"]
+    original_features = ["ambient_temp", "module_temp", "irradiation", "color"]
+
+    tr = ExpandingWindowFeatures(functions=["mean", "sum"])
+    tr.fit(df_time)
+
+    # expected
+    output = [
+        "ambient_temp_expanding_mean",
+        "ambient_temp_expanding_sum",
+        "module_temp_expanding_mean",
+        "module_temp_expanding_sum",
+        "irradiation_expanding_mean",
+        "irradiation_expanding_sum",
+    ]
+    assert tr.get_feature_names_out(input_features=None) == original_features + output
+    assert tr.get_feature_names_out(input_features=input_features) == output
+    assert tr.get_feature_names_out(input_features=input_features[0:2]) == output[0:4]
+    assert tr.get_feature_names_out(input_features=[input_features[0]]) == output[0:2]
+
+
+def test_get_feature_names_out_single_variable_and_multiple_functions(df_time):
+    # input features
+    original_features = ["ambient_temp", "module_temp", "irradiation", "color"]
+
+    tr = ExpandingWindowFeatures(
+        variables="ambient_temp", functions=["sum", "mean", "count"]
+    )
+    tr.fit(df_time)
+
+    # expected
+    output = [
+        "ambient_temp_expanding_sum",
+        "ambient_temp_expanding_mean",
+        "ambient_temp_expanding_count",
+    ]
+    assert tr.get_feature_names_out(input_features=None) == original_features + output
+    assert tr.get_feature_names_out(input_features=["ambient_temp"]) == output
+
+
+def test_get_feature_names_out_single_variable_and_single_function(df_time):
+    # input features
+    original_features = ["ambient_temp", "module_temp", "irradiation", "color"]
+
+    tr = ExpandingWindowFeatures(variables="ambient_temp", functions="sum")
+    tr.fit(df_time)
+
+    # expected
+    output = [
+        "ambient_temp_expanding_sum",
+    ]
+    assert tr.get_feature_names_out(input_features=None) == original_features + output
+    assert tr.get_feature_names_out(input_features=["ambient_temp"]) == output
+
+
+def test_expanding_sum_single_var(df_time):
+    expected_results = {
+        "ambient_temp_expanding_sum": [
+            np.nan,
+            31.31,
+            62.82,
+            94.97,
+            127.36,
+            159.98,
+            192.48,
+            225.00,
+            257.68,
+            291.44,
+            325.57,
+            359.65,
+            393.35,
+            427.24,
+            461.28,
+        ],
+    }
+    expected_df = df_time.copy()
+    expected_df["ambient_temp_expanding_sum"] = expected_results[
+        "ambient_temp_expanding_sum"
+    ]
+
+    transformer = ExpandingWindowFeatures(variables=["ambient_temp"], functions="sum")
+    df_tr = transformer.fit_transform(df_time)
+    assert_frame_equal(df_tr, expected_df)
+
+
+def test_expanding_sum_multiple_vars(df_time):
+    expected_results = {
+        "ambient_temp_expanding_sum": [
+            np.nan,
+            31.31,
+            62.82,
+            94.97,
+            127.36,
+            159.98,
+            192.48,
+            225.00,
+            257.68,
+            291.44,
+            325.57,
+            359.65,
+            393.35,
+            427.24,
+            461.28,
+        ],
+        "irradiation_expanding_sum": [
+            np.nan,
+            0.51,
+            1.3,
+            1.95,
+            2.71,
+            3.13,
+            3.62,
+            4.19,
+            4.75,
+            5.49,
+            6.38,
+            6.85,
+            7.39,
+            7.79,
+            8.24,
+        ],
+    }
+    expected_df = df_time.copy()
+    expected_df["ambient_temp_expanding_sum"] = expected_results[
+        "ambient_temp_expanding_sum"
+    ]
+    expected_df["irradiation_expanding_sum"] = expected_results[
+        "irradiation_expanding_sum"
+    ]
+
+    transformer = ExpandingWindowFeatures(
+        variables=["ambient_temp", "irradiation"], functions="sum"
+    )
+    df_tr = transformer.fit_transform(df_time)
+    assert_frame_equal(df_tr, expected_df)
+
+
+def test_expanding_sum_and_mean_single_var(df_time):
+    expected_results = {
+        "ambient_temp_expanding_sum": [
+            np.nan,
+            31.31,
+            62.82,
+            94.97,
+            127.36,
+            159.98,
+            192.48,
+            225.00,
+            257.68,
+            291.44,
+            325.57,
+            359.65,
+            393.35,
+            427.24,
+            461.28,
+        ],
+        "ambient_temp_expanding_mean": [
+            np.nan,
+            31.3100,
+            31.4100,
+            31.6567,
+            31.8400,
+            31.9960,
+            32.0800,
+            32.1429,
+            32.2100,
+            32.3822,
+            32.5570,
+            32.6955,
+            32.7792,
+            32.8646,
+            32.9486,
+        ],
+    }
+    expected_df = df_time.copy()
+    expected_df["ambient_temp_expanding_sum"] = expected_results[
+        "ambient_temp_expanding_sum"
+    ]
+    expected_df["ambient_temp_expanding_mean"] = expected_results[
+        "ambient_temp_expanding_mean"
+    ]
+
+    transformer = ExpandingWindowFeatures(
+        variables=["ambient_temp"], functions=["sum", "mean"]
+    )
+    df_tr = transformer.fit_transform(df_time)
+    assert_frame_equal(df_tr, expected_df)
+
+
+def test_expanding_sum_and_mean_multiple_vars(df_time):
+    expected_results = {
+        "ambient_temp_expanding_sum": [
+            np.nan,
+            31.31,
+            62.82,
+            94.97,
+            127.36,
+            159.98,
+            192.48,
+            225.00,
+            257.68,
+            291.44,
+            325.57,
+            359.65,
+            393.35,
+            427.24,
+            461.28,
+        ],
+        "ambient_temp_expanding_mean": [
+            np.nan,
+            31.3100,
+            31.4100,
+            31.6567,
+            31.8400,
+            31.9960,
+            32.0800,
+            32.1429,
+            32.2100,
+            32.3822,
+            32.5570,
+            32.6955,
+            32.7792,
+            32.8646,
+            32.9486,
+        ],
+        "irradiation_expanding_sum": [
+            np.nan,
+            0.51,
+            1.3,
+            1.95,
+            2.71,
+            3.13,
+            3.62,
+            4.19,
+            4.75,
+            5.49,
+            6.38,
+            6.85,
+            7.39,
+            7.79,
+            8.24,
+        ],
+        "irradiation_expanding_mean": [
+            np.nan,
+            0.51000,
+            0.65000,
+            0.65000,
+            0.67750,
+            0.62600,
+            0.60333,
+            0.59857,
+            0.59375,
+            0.61000,
+            0.63800,
+            0.62273,
+            0.61583,
+            0.59923,
+            0.58857,
+        ],
+    }
+    expected_df = df_time.copy()
+    expected_df["ambient_temp_expanding_sum"] = expected_results[
+        "ambient_temp_expanding_sum"
+    ]
+    expected_df["ambient_temp_expanding_mean"] = expected_results[
+        "ambient_temp_expanding_mean"
+    ]
+    expected_df["irradiation_expanding_sum"] = expected_results[
+        "irradiation_expanding_sum"
+    ]
+    expected_df["irradiation_expanding_mean"] = expected_results[
+        "irradiation_expanding_mean"
+    ]
+
+    transformer = ExpandingWindowFeatures(
+        variables=["ambient_temp", "irradiation"], functions=["sum", "mean"]
+    )
+    df_tr = transformer.fit_transform(df_time)
+    assert_frame_equal(df_tr, expected_df)
+
+
+def test_expanding_sum_single_var_periods(df_time):
+    expected_results = {
+        "ambient_temp_expanding_sum": [
+            np.nan,
+            np.nan,
+            31.31,
+            62.82,
+            94.97,
+            127.36,
+            159.98,
+            192.48,
+            225.00,
+            257.68,
+            291.44,
+            325.57,
+            359.65,
+            393.35,
+            427.24,
+        ],
+    }
+    expected_df = df_time.copy()
+    expected_df["ambient_temp_expanding_sum"] = expected_results[
+        "ambient_temp_expanding_sum"
+    ]
+
+    transformer = ExpandingWindowFeatures(
+        variables=["ambient_temp"], functions="sum", periods=2
+    )
+    df_tr = transformer.fit_transform(df_time)
+    assert_frame_equal(df_tr, expected_df)
+
+
+def test_expanding_sum_single_var_freqs(df_time):
+    expected_results = {
+        "ambient_temp_expanding_sum": [
+            np.nan,
+            np.nan,
+            31.31,
+            62.82,
+            94.97,
+            127.36,
+            159.98,
+            192.48,
+            225.00,
+            257.68,
+            291.44,
+            325.57,
+            359.65,
+            393.35,
+            427.24,
+        ],
+    }
+    expected_df = df_time.copy()
+    expected_df["ambient_temp_expanding_sum"] = expected_results[
+        "ambient_temp_expanding_sum"
+    ]
+
+    transformer = ExpandingWindowFeatures(
+        variables=["ambient_temp"], functions="sum", freq="30T"
+    )
+    df_tr = transformer.fit_transform(df_time)
+    assert_frame_equal(df_tr, expected_df)
+
+
+def test_expanding_sum_single_var_periods_and_freqs(df_time):
+    expected_results = {
+        "ambient_temp_expanding_sum": [
+            np.nan,
+            np.nan,
+            31.31,
+            62.82,
+            94.97,
+            127.36,
+            159.98,
+            192.48,
+            225.00,
+            257.68,
+            291.44,
+            325.57,
+            359.65,
+            393.35,
+            427.24,
+        ],
+    }
+    expected_df = df_time.copy()
+    expected_df["ambient_temp_expanding_sum"] = expected_results[
+        "ambient_temp_expanding_sum"
+    ]
+
+    transformer = ExpandingWindowFeatures(
+        variables=["ambient_temp"], functions="sum", periods=2, freq="15T"
+    )
+    df_tr = transformer.fit_transform(df_time)
+    assert_frame_equal(df_tr, expected_df)
+
+
+def test_sort_index(df_time):
+    # Shuffle dataframe
+    Xs = df_time.sample(frac=1)
+
+    transformer = ExpandingWindowFeatures(sort_index=False)
+    df_tr = transformer.fit_transform(Xs)
+    assert_frame_equal(df_tr[transformer.variables_], Xs[transformer.variables_])
+
+    transformer = ExpandingWindowFeatures(sort_index=True)
+    df_tr = transformer.fit_transform(Xs)
+    assert_frame_equal(
+        df_tr[transformer.variables_], Xs[transformer.variables_].sort_index()
+    )
+
+
+def test_expanding_window_raises_when_periods_negative():
+    with pytest.raises(
+        ValueError, match="periods must be a non-negative integer. Got -1 instead."
+    ):
+        ExpandingWindowFeatures(periods=-1)
diff --git a/tests/test_transformation/test_yeojohnson_transformer.py b/tests/test_transformation/test_yeojohnson_transformer.py
index 3e9179c..ae884bf 100644
--- a/tests/test_transformation/test_yeojohnson_transformer.py
+++ b/tests/test_transformation/test_yeojohnson_transformer.py
@@ -24,6 +24,47 @@ def test_automatically_select_variables(df_vartypes):
     pd.testing.assert_frame_equal(X, transf_df)
 
 
+def test_transformer_on_integer_variables():
+    df = pd.DataFrame(
+        {
+            "var1": [0, 1, 0, 2, 3, 4, 5, 6, 8, 10],
+            "var2": [12, 11, 10, 15, 13, 12, 11, 10, 10, 20],
+        }
+    )
+
+    dft = pd.DataFrame(
+        {
+            "var1": {
+                0: 0.0,
+                1: 0.7871467037957388,
+                2: 0.0,
+                3: 1.34716625120788,
+                4: 1.797027857352365,
+                5: 2.1794549065159363,
+                6: 2.5155129679774246,
+                7: 2.817344570368886,
+                8: 3.346739213848269,
+                9: 3.8051709334268566,
+            },
+            "var2": {
+                0: 0.2891005444159968,
+                1: 0.2890875957028113,
+                2: 0.2890687942494933,
+                3: 0.2891213447054929,
+                4: 0.2891097235906253,
+                5: 0.2891005444159968,
+                6: 0.2890875957028113,
+                7: 0.2890687942494933,
+                8: 0.2890687942494933,
+                9: 0.28913341330818815,
+            },
+        }
+    )
+
+    X_tr = YeoJohnsonTransformer().fit_transform(df)
+    pd.testing.assert_frame_equal(X_tr, dft)
+
+
 def test_fit_raises_error_if_na_in_df(df_na):
     # test case 2: when dataset contains na, fit method
     with pytest.raises(ValueError):