Repository: feature-engine/feature_engine

Issue:
include transformer for datetime variables
The first version of this module should include the following:

- New module: datetime (folder)

Three transformers:
- ExtractDateFeatures
- ExtractTimeFeatures
- ExtractDateTimeFeatures

A base class:
- DateTimeBaseTransformer

The base class should:

- check that the variables entered by the user are datetime, or if string / object, transform them to datetime (I wonder if we should make this a function part of [variable_manipulation](https://github.com/solegalli/feature_engine/blob/master/feature_engine/variable_manipulation.py)? and call it in this transfomrer? In case we develop the timeseries module, we may need it later for other modules as well.

- have a method to return time features
- have a method to return date features
- methods to check the input dataframe, and options on what to do if outliers are present

The ExtractDateFeatures should derive the following features:

- [ ] month, quarter, semester and year (all numeric outputs)
- [ ] week of the year
- [ ] is week of the month supported by pandas? if yes, when we should return it
- [ ] day (numeric 1-31), day of the week (numeric 0-6), and is_weekend (binary)
- [ ] anything else supported by pandas?
- [ ] anything else that would be useful

The ExtractTimeFeatures should extract the following features

- [ ] hr, minute, second
- [ ] timezone: with parameter `return_timezone=False` by default, we allow the user to return a time zone categorical feature
- [ ] unify_timezone=False, to handle different timezones. If True, the transformer is timezone aware, unifies to greenwich and then derived features (should we give option to user to unify to something else? probably yes)
- [ ]  **to discuss**: is_morning, is_afternoon, is_evening
- [ ] **to discuss**: working hrs (I am thinking of passing a string parameter like '9-17' and use those to determine this feature
- [ ] anything else that would be useful

The ExtractDateTimeFeatures is a sum of the previous transformers, so it should return all possible date and time features.

The reason I suggest to break this into 3 classes is because some timestamps contain only dates, some only times and some both. And I think it would be easier if the user, who knows the timestamp selects the appropriate transformer, instead of adding code to understand which type of timestamp it is, and then derive the features

To consider: should we in version 1 of this transformer return all possible features? or should we give the user an option of which features to return? example, the user may want year and month but not quarter and semester.

Example code in recipes 2 to 5 of the [this link.](https://github.com/solegalli/packt_featureengineering_cookbook/tree/master/ch07-datetime).

Things to think about for the transformers design:

- [ ] Transformer returns all new variables by default, or only those indicated by the user. This behavior could regulated by a parameter in the init. As per previous question, should we leave this for version 2 of this transformer? or add it straightaway?
- [ ] Transformer should be able to derive features from more than 1 datetime variable at the time, like all feature engine transformers.
- [ ] Option in Transformer to drops original datetime variables (as those are not useful for machine learning), drop_datetime_variables=True or similar. 


Files needed in addition to code files:
- [ ] add transformer to readme list
- [ ] add transformer to docs/index.rst
- [ ] add docs folder exclusive for this transformer with rst files with examples
- [ ] add jupyter notebooks showing how to use this transformers


This issue can be done in 1 big PR, or multiple PRs, maybe 1 per transformer.

Discussion:
dodoarg: Think I've done pretty much all the cleaning. Let me know if something is still out of place. This has been a great learning endeavor for me so far!
dodoarg: As for your question, yes you can extract dates from time-only variables and times from date-only variables.

```
>>> df
       date      time
0  10/11/12  10:15:30
1  09/09/09  05:27:54
2  05/25/95  22:22:32
3  03/17/04  14:09:45
>>> df.apply(pd.to_datetime)
        date                time
0 2012-10-11 2021-12-03 10:15:30
1 2009-09-09 2021-12-03 05:27:54
2 1995-05-25 2021-12-03 22:22:32
3 2004-03-17 2021-12-03 14:09:45
>>> df.apply(pd.to_datetime).date.dt.hour
0    0
1    0
2    0
3    0
Name: date, dtype: int64
>>> df.apply(pd.to_datetime).time.dt.isocalendar()
   year  week  day
0  2021    48    5
1  2021    48    5
2  2021    48    5
3  2021    48    5
>>>
```

time attributes are defaulted to zeros, date attributes are defaulted to today's date
solegalli: > attributes are defaulted to zeros, date attribu

Thank you for checking! This is great :)

In this case, let's create 1 class only called DateTimeFeatures() instead of the 3 we had originally planned.

Would you agree?
solegalli: Hi @dodoarg 

I went through the code changes you've done since the last time. It is looking really good. 

I also tried to answer some questions. 

Let me know if you are waiting to hear from me on something that I missed, or when you would like me to have a look at the code again.

Thanks a lot!
dodoarg: Hi @solegalli, merging your previous comments onto a single topic

> In this case, let's create 1 class only called DateTimeFeatures() instead of the 3 we had originally planned.
> So the parameter would take 3 types of input:

> we can return by default year, month, day (1-31) and day of the week (0-6)?
> ...
> None: returns the above 4 features
> "all": returns all features
> list: with short names to the features the user wants returned.

A few comments/ideas, let me know what you think

- I think if we want to make a date __and__ time feature extractor we want the default option _None_ to return something related to time aswell, maybe hour, minute, second.
- Although I had initially coded the _all_ option myself, I really can't see a situation where every single one of those features are useful, there is simply too many of them and most of them, especially the binary ones that I recently added, are extremely situational I think. So to thin out the code I would suggest to get rid of the _all_ option in the first place. <br>
- Since time-only features are parsed as _today's date_ and date-only features are parsed as _00:00:00_: <br> let's say features_to_extract=None defaults to returning month, day, year, hour, minute, second. Or maybe you disagree with getting rid of the _all_ option so the _all_ option returns all the supported date __and__ time features. In this case, if one or more datetime features are time-only or date-only, there will be potentially several features that are effectively garbage and do not add the slightest information to the dataset. Therefore, I was thinking maybe we could call DropConstantFeatures before returning the transformed df.
solegalli: Ok to everything :)

Good ideas @dodoarg 
dodoarg: A couple comments on these last commits @solegalli :
for starters, sorry about the duplicate commits, I slightly messed up the branch history as I thought a merge with a temporary local branch wouldn't be kept track of remotely. Looks like I was wrong

Speaking of commits, up to commit 211f796 it should be alright as I just did what we had discussed. The last two might be controversial in the way I decided to rework the code, so I will briefly explain:

- as I was adding more and more features to the (now datetime) transformer, I felt like the class was starting to be quite overcrowded, mostly due to string parsing and repeated if logic blocks
- to implement the option to drop constant features, since I didn't want to drop features that were __not__ extracted by the transformer, I needed to access the names of said extracted features to pass them to DropConstantFeatures. That would have added even more strings/boilerplate code

Thus, I decided to thin out the class (as well as the test file) by importing the constant stuff related to the features from a separate file that I put in the module.  I think this also has the positive side effect that in order to change anything in what features are supported, what are the default ones, how to compute them etc. one just needs to edit the correspondent constant variable rather than touch the code inside the class.
Let me know what you think, if it's unwanted I can just revert to the previous state
solegalli: Hi @dodoarg 

Thank you so much for such an amazing contribution. I particularly enjoyed the code of the datetime feature extraction class. Very tidy, very readable. Loved it.

I made a [PR to your repo](https://github.com/dodoarg/feature_engine/pull/1) with some code changes. Would you mind having a look and merging? That will update automatically the code in this PR.

This is what I've done:

**Big changes**

- removed drop constant functionality: the reason is that is non-deterministic. In other words, it will not output dataframes with the same number of features. Extreme example: if I transform a dataframe with only 2 rows that have the same date, all features will be constant and dropped. We don't want that, we want the output of the transform method to be a dataframe with the same number of features no matter what X is passed to the transform.
- I reformatted the check datetime variable, it should now accept categorical (I know we said not to at the beginning, but I can't remember why). I thought we would just move forward with this.

**Minor changes**
- I added the functionality to return all features when passing "all" as param to features to extract. It is just a few extra lines of code, so i think it does not hurt.
- I renamed the class, the script, the tests and one of the parameters for consistancy on how we call the same functionality in other classes

Main things to do:

- [ ] break down the tests. At the moment, we have only 1 test that tests everything. We should break this down into smaller tests, with a name that allows us to identify what is being tested. See for example [here](https://github.com/feature-engine/feature_engine/blob/main/tests/test_imputation/test_categorical_imputer.py). This makes it much easier to troubleshoot, identify what is failing and helps us maintain looking forward
- [ ] add test that class works as expected when df has variables of type categorical
- [ ] add test that class works as expected when var in different timezones

solegalli: on the drop constant, the user can easily do that by implementing this:

```
extract_dt = Pipeline([
  ('dt_features', DateTimeFeatures)(),
  ('drop_constatn', DropConstantFeatures()),
  ])

extract_df.fit(X_train)

Xtrain_t = extract_dt.transform(X_train)
Xtest_t = extract_dt.transform(X_test)
```

I think we should add this in the documentation as an example to guide the users, instead of complicating our lives with trying to introduce this logic here.

Thoughts?
dodoarg: Thank you for your support.
I merged and (I believe) fixed your recent changes. Now I can crack on with the test tweaks you've suggested.
dodoarg: About dropping constant variables, I've thought it over and I agree with you to a certain extent.

let's say we have a column 
date_var : ['Dec-2010', 'Feb-2010', 'Mar-2010']
and the user calls DatetimeFeatures without specifying features_to_extract (which currently defaults to month, year, day, hour, minute, second)
There's two fundamentally different types of constant variables that get generated by the DatetimeFeatures transformer:

- constant features that are extracted from inherently constant data - date_var_year : [2010, 2010, 2010]
- constant features that are extracted from variables that may or may not be constant that are generated as a byproduct of the pd.to_datetime parsing - date_var_hour: [0, 0, 0]

While I agree with you that we might not want the transformer to get rid of the former kind of constant features, the latter kind of constant features are effectively filling the dataset with made up information, on top of being clearly useless, and I can't see a user ever intending to extract a column (or multiple columns) arbitrarily filled with today's date or midnight o'clock time.

I don't necessarily think that having a call to DropConstantFeatures - perhaps limited to the "fake" features - would be particularly problematic on the technical side, but then again the easiest way would be to ask the user to be more conscious about what they're doing and it's ultimately up to you


solegalli: > About dropping constant variables, I've thought it over and I agree with you to a certain extent.
> 
> let's say we have a column date_var : ['Dec-2010', 'Feb-2010', 'Mar-2010'] and the user calls DatetimeFeatures without specifying features_to_extract (which currently defaults to month, year, day, hour, minute, second) There's two fundamentally different types of constant variables that get generated by the DatetimeFeatures transformer:
> 
>     * constant features that are extracted from inherently constant data - date_var_year : [2010, 2010, 2010]
> 
>     * constant features that are extracted from variables that may or may not be constant that are generated as a byproduct of the pd.to_datetime parsing - date_var_hour: [0, 0, 0]
> 
> 
> While I agree with you that we might not want the transformer to get rid of the former kind of constant features, the latter are effectively filling the dataset with made up data and I can't see a user ever intending to have a column (or multiple columns) suddenly filled with today's date or midnight o'clock time.
> 
> I don't necessarily think that having a call to DropConstantFeatures - perhaps limited to the "fake" features - would be particularly problematic on the technical side, but then again the easiest way would be to ask the user to be more conscious about what they're doing and it's ultimately up to you

I see the problem. Don't get me wrong. But there are a couple of constrains:

- the output of transform should have always the same number of columns.
- everything that needs to be learned, needs to be learned during fit.

So the only way we could implement the drop constant is to:

1. extract the features during fit.
2. find the constant features in the train set (during fit)
3. in transform always remove those features that were found constant in the train set

That will ensure that the output of transform contains always the same number of columns.

It is doable. It involves adding a more logic to our class. And it is so easily accomplished by tagging the dropconstantvariables in a pipeline at the back of this transformer, that it feels a bit like we are making our lives difficult when the functionality is already there.

I would be inclined not to add this functionality to the class. Explain how it could be done in the documentation, and see if users are happy with this, or if we should add the functionality to the class, which we could do later.

What do you think?
dodoarg: Yea I see. For consistency, it would be best not to have anything learned outside of the _fit_ scope, while my point precisely addresses something that comes up at the _transform_ stage, with the extracted features. We might aswell leave it like that then and rely on the docs and the awareness of users
solegalli: Is this ready for review?
dodoarg: I revamped the datetime testing with several functions and a datetime scoped conftest but I still wanted to see if I couldn't throw in some pyest.parametrize decorators
dodoarg: Small update on last commit: I found out that pd.to_datetime won't convert a series to datetime if different observations are in different timezones (it will rather leave it as object, preventing us from accessing dt attributes), so I decided to pass __utc=True__ everytime pd.to_datetime is called. This standardizes all times to utc and makes sure everything works properly. Let me know if you agree

About the parametrized tests, I couldn't really see a way to use it extensively in this case :/ I could use it for the attribute checks or some verbose error tests but I don't think it would look much better
solegalli: > 

What about making utc a parameter in the init instead and allowing the user to decide? It would default to None, just like in pandas datetime, and if user knows that they have time aware zones, they can switch it to true. 
solegalli: After some research, it seems that the only way to have different tz in the same variable is by the string with the plus or minus amount of hrs from utc.

```
data = {
    "Name": ["tom", "nick", "krish", "jack"],
    "Age": [20, 21, 19, 18],
    "datetime_range": pd.date_range("2020-02-24", periods=4, freq="D"),
    "date_obj1": ["01-Jan-2010", "24-Feb-1945", "14-Jun-2100", "17-May-1999"],
    "date_obj2": ["10/11/12", "12/31/09", "06/30/95", "03/17/04"],
    "time_obj": ["21:45:23", "09:15:33", "12:34:59", "03:27:02"],
    "tz_obj": ['12:00:00+0100', '12:00:00+0200',
               '12:00:00-0100', '12:00:00+0000'],
}

df = pd.DataFrame(data)
```

```
a = pd.concat([
    pd.Series(
        pd.date_range(
            start='2014-08-01 09:00', freq='H', periods=3,
            tz='Europe/Berlin')),
    pd.Series(
        pd.date_range(
            start='2014-08-01 09:00', freq='H', periods=3, tz='US/Central'))
    ], axis=0).reset_index(drop=True)

df['tz'] = a

df
```

```
   Name  Age datetime_range    date_obj1 date_obj2  time_obj         tz_obj  \
0    tom   20     2020-02-24  01-Jan-2010  10/11/12  21:45:23  12:00:00+0100   
1   nick   21     2020-02-25  24-Feb-1945  12/31/09  09:15:33  12:00:00+0200   
2  krish   19     2020-02-26  14-Jun-2100  06/30/95  12:34:59  12:00:00-0100   
3   jack   18     2020-02-27  17-May-1999  03/17/04  03:27:02  12:00:00+0000   

                          tz  
0  2014-08-01 09:00:00+02:00  
1  2014-08-01 10:00:00+02:00  
2  2014-08-01 11:00:00+02:00  
3  2014-08-01 09:00:00-05:00  
```

We could add another parameter in the init, tz_aware=False. And when user sets to true, we could extract the tz in another feature like this:
```

df['tz'].astype(str).str[-6:]

0    +02:00
1    +02:00
2    +02:00
3    -05:00
```

Not sure how much value it adds. It might be an overkill.

Thoughts?
dodoarg: >After some research, it seems that the only way to have different tz in the same variable is by the string with the plus or minus amount of hrs from utc.

Indeed, if you look at my demo above and the tests I added with the last commit, that's how I've created tz-aware variables

> What about making utc a parameter in the init instead and allowing the user to decide? 

The issue with your approach is that those variables are not datetime and __cannot__ be converted into datetime:
```
>>> a = pd.concat([
    pd.Series(
        pd.date_range(
            start='2014-08-01 09:00', freq='H', periods=3,
            tz='Europe/Berlin')),
    pd.Series(
        pd.date_range(
            start='2014-08-01 09:00', freq='H', periods=3, tz='US/Central'))
    ], axis=0).reset_index(drop=True)

>>> a.dtype
dtype('O')

>>> pd.to_datetime(a)
Traceback (most recent call last):
  File "D:\Vari\Anaconda3\envs\ML\lib\site-packages\pandas\core\arrays\datetimes.py", line 2085, in objects_to_datetime64ns
    values, tz_parsed = conversion.datetime_to_datetime64(data)
  File "pandas\_libs\tslibs\conversion.pyx", line 335, in pandas._libs.tslibs.conversion.datetime_to_datetime64
ValueError: Array must be all same time zone

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "D:\Vari\Anaconda3\envs\ML\lib\site-packages\pandas\core\tools\datetimes.py", line 805, in to_datetime
    values = convert_listlike(arg._values, format)
  File "D:\Vari\Anaconda3\envs\ML\lib\site-packages\pandas\core\tools\datetimes.py", line 465, in _convert_listlike_datetimes
    result, tz_parsed = objects_to_datetime64ns(
  File "D:\Vari\Anaconda3\envs\ML\lib\site-packages\pandas\core\arrays\datetimes.py", line 2090, in objects_to_datetime64ns
    raise e
  File "D:\Vari\Anaconda3\envs\ML\lib\site-packages\pandas\core\arrays\datetimes.py", line 2075, in objects_to_datetime64ns
    result, tz_parsed = tslib.array_to_datetime(
  File "pandas\_libs\tslib.pyx", line 364, in pandas._libs.tslib.array_to_datetime
  File "pandas\_libs\tslib.pyx", line 447, in pandas._libs.tslib.array_to_datetime
ValueError: Tz-aware datetime.datetime cannot be converted to datetime64 unless utc=True
```
Note: the same error is thrown even when errors="ignore" in pd.to_datetime (because it's not a parsing error, it's a value error)
dodoarg: To add to that, I'm not sure I got how the datetime would be parsed in case the user says tz_aware=False
Like, what would be the non-tz-aware _hour_ here?
0  2014-08-01 09:00:00+02:00  
1  2014-08-01 10:00:00+02:00  
2  2014-08-01 11:00:00+02:00  
3  2014-08-01 09:00:00-05:00 

[09, 10, 11, 09] just ignoring the offset from utc time? 
I believe we'd need a workaround like a parser that gets rid of whatever comes after the + or - before calling pd.to_datetime
solegalli: If time_aware=true, then utc=True. We would first capture the +06:00 in a variable. That should do it. I think.

If time_aware=False and the variable has +06:00, then pandas will raise an error. We would go with that. The user should know if their variables are time aware or not.

But still, I am not convinced we need this. Maybe we leave it for now?

I would just make utc a param in the init.
dodoarg: > The user should know if their variables are time aware or not.

Oh well, if you put it that way...I guess I keep underestimating feature engine's users :p

However, I would keep utc fixed to true in the pd.to_datetime calls that are inside _find_or_check_datetime_variables so that even when time_aware=false and the variable has +6:00 it's still recognized as datetime and the error is thrown only when it reaches the transform method. Does that sound right? 
dodoarg: I've adjusted the code @solegalli the only thing I'm not too sure I like is how the errors are handled as far as the time zone goes. This is not a huge problem but I would still like to hear your opinion on this as I'm trying to work things out as thoroughly as possible

Say you have the object-like variable

```
>>> tz
0    21:24:00+3
1    17:18:00-5
dtype: object 
```
If the user (erroneously) calls DatetimeFeatures(__time_aware=False__) or DatetimeFeatures(__time_aware=None__) the variable is correctly identified as datetime but when it gets to the transform you get this
```
>>> pd.to_datetime(tz, utc=None)
0    2021-12-10 21:24:00+03:00
1    2021-12-10 17:18:00-05:00
dtype: object
```
which does not raise an error but instead returns a variable that __still has dtype=object__.
This means that an __AttributeError__ is raised when the transformer tries to access the _dt_ method afterwards
`AttributeError: Can only use .dt accessor with datetimelike values`

Why does pd.to_datetime not return an error in the first place? Only calling the function __again__ on the series results in a value error:
```
>>> pd.to_datetime(pd.to_datetime(tz, utc=None))
...long traceback...
ValueError: Tz-aware datetime.datetime cannot be converted to datetime64 unless utc=True
```


This is is because the first call is actually converting __the single items__ from strings to datetime objects while leaving the series dtype unchanged
```
>>> pd.to_datetime(tz, utc=None)[0]
datetime.datetime(2021, 12, 10, 21, 24, tzinfo=tzoffset(None, 10800))
``` 

while the second call then tries to convert the series dtype and it throws a ValueError because the items are in different timezones (it cannot assign a unique _tzoffset_ value to the whole series)


What is the big deal then? 
Well, as things stand there is no second call to pd.to_datetime before extracting the features via _dt_, so the user __gets an attribute error rather than a more informative value error__ and might not understand what is going on
dodoarg: Hi @solegalli, thank you for your kind words and well done on the tests refactoring. The only thing I might consider worth double-checking is the time_zone related tests as I think there might be some redundancy considering the test you added jointly with the one I had written already (the one with the different timezones). Maybe we want to merge them into a single comprehensive test?

I think I've fixed the three code issues you mentioned.

I've also added some explanations and a basic example to the user guide. Since I'm not very experienced with building documentations etc would you mind giving me some heads up on what needs more attention?
On that note, I've also had an example notebook ready for quite some time, should I PR on feature-engine-examples?
dodoarg: @solegalli Here's some examples, let me know if we're going in the right direction now 🤔 
solegalli: > @solegalli Here's some examples, let me know if we're going in the right direction now thinking

Masterpiece. I added a few more examples and made a PR to your repo.
dodoarg: > Finally, we need to add the tests from check_estimator from sklearn. 

We still need this right?
solegalli: > y ideas on what's going

The check_estimator from sklearn creates a couple of numpy arrays to test the basic functionality of the transformers. To fullfill sklearn conventions, the transformers need to do a bunch of things, like be able to fit anew everytime you call fit, it tests that you do not overwrite the init params, that all init params have default values, that the transformer is compatible with the pipeline and the grid searches, that it has get params and set params, and a few other things.

All numerical transformers of sklearn pass most of the tests. For the categorical transformers, I needed to include that flag to ignore format, which on one hand allows the transformers to work with numerical variables as if they were categorical, which was requested by some users, and on the other hand, the transformers can now be checked by sklearn checker.

I also tried the check_estimator in my last PR and realised quickly that we will need a massive workaround to be able to test it. Because even if we add a flag to ignore format, whenever it hits the transform method it will raise an error when it is not able to get date features from the variables.

So I am inclined to leave it for now. We can create an issue to think about it and come back to it in the future.

So from my perspective, the only thing remaining would be to remove that week feature from the functionality and also the docs demo (there is one demo where I set the features_to_extract to all, we need to delete a couple of columns from the output.
dodoarg: Here's the thing, pd.datetime actually does work on numerical variables

```
>>> pd.to_datetime(pd.Series([1,2,3,4]))
0   1970-01-01 00:00:00.000000001
1   1970-01-01 00:00:00.000000002
2   1970-01-01 00:00:00.000000003
3   1970-01-01 00:00:00.000000004
dtype: datetime64[ns]
```

which is exactly we we need to have datetime checkers in the actual functionality otherwise whenever variables=None one would end up with a bunch of meaningless features. But in order to make those tests pass, maybe...this would actually be helpful?

Anyway I removed that feature @solegalli 
On the failing type tests: I believe it's got something to do with [this](https://github.com/python/mypy/commit/22ab84acc3f6174905a05ddadab94b6c2d83f9a1) recent update on mypy. The affected file is not part of this PR and it also breaks the test on the main branch, is it worth a quick separate issue before merging this feature branch?
solegalli: Hi @dodoarg 

You are a source of knowledge!!!!

I asked a friend of mine who is an experienced python developer, if there is a way to add a hidden parameter in the init, say to ignore variable format, that is visible to us developers but invisible to users. This way, we could use this parameter to test the transformer. As soon as I hear from him, I'll ping an answer here. 

If you have any ideas on how we could implement the above, let me know as well. I wonder if we could create a base class with that init param, that could be inherited by this transformer.

On the second point, yes, you are right, it fails with the lastest version of mypy. There are 3 ways forward, as I see it:

-  The simplest, add `# type: ignore` to feature_engine\selection\base_selector.py:28: line 28 in that file, as per [this thread](https://stackoverflow.com/questions/27567846/how-can-i-check-out-a-github-pull-request-with-git).
- The second simplest, restrict the version of mypy in the test requirements file.
- The hardest, find out why we have that error and fix it (we should probably do this in a separate PR, unless it is a modest workaround)

Unless you have another suggestion?
solegalli: Hi @ChristopherGS 

We want to introduce the test you find in [this file](https://github.com/feature-engine/feature_engine/blob/main/tests/test_preprocessing/test_check_estimator_preprocessing.py), for the class we are developing in this PR, which you find [here](https://github.com/feature-engine/feature_engine/pull/333/files#diff-5b7c0e1c3591791959727acd2b5d8f597cd1dfde501ec0116b06448689b545abR27).

The thing is, our class only works with variables of type datetime or object. But for the tests, sklearn creates random numerical numpy arrays. So our class will fail all tests, unless we had a flag that allowed the transformer to work with numerical variables as well, but just for the tests. This should not be (obviously) available to users.

Any idea how this could be implemented?
solegalli: @ChristopherGS and @dodoarg 

I implemented a plausible solution in this [PR](https://github.com/dodoarg/feature_engine/pull/6)

Let me know what you think please.
solegalli: @dodoarg 

The only thing remaining in this PR was whether to add the check_estimator or not, correct?

So if we were to not add those tests for now, I think we are good to merge?

I'll capture that in a separate issue. We would like to have other transformers what work with datetime, so it will come back at some point again.
dodoarg: I think so yes but you have the last word on it. If you spot something missing or wrong I'll try and fix that. I can't wait to see this horrible mega commit history squashed into one 

Patch:
diff --git a/README.md b/README.md
index 9cf4a8d..0ea6c01 100644
--- a/README.md
+++ b/README.md
@@ -54,14 +54,15 @@ transforming parameters from the data and then transform it.
 ## Current Feature-engine's transformers include functionality for:
 
 * Missing Data Imputation
-* Categorical Variable Encoding
-* Outlier Capping or Removal
+* Categorical Encoding
 * Discretisation
-* Numerical Variable Transformation
+* Outlier Capping or Removal
+* Variable Transformation
 * Variable Creation
 * Variable Selection
-* Scikit-learn Wrappers
+* Datetime Feature Extraction
 * Preprocessing
+* Scikit-learn Wrappers
 
 ### Imputation Methods
 * MeanMedianImputer
@@ -82,17 +83,17 @@ transforming parameters from the data and then transform it.
 * RareLabelEncoder
 * DecisionTreeEncoder
 
-### Outlier Handling methods
-* Winsorizer
-* ArbitraryOutlierCapper
-* OutlierTrimmer
-
 ### Discretisation methods
 * EqualFrequencyDiscretiser
 * EqualWidthDiscretiser
 * DecisionTreeDiscretiser
 * ArbitraryDiscreriser
 
+### Outlier Handling methods
+* Winsorizer
+* ArbitraryOutlierCapper
+* OutlierTrimmer
+
 ### Variable Transformation methods
 * LogTransformer
 * LogCpTransformer
diff --git a/docs/api_doc/datetime/DatetimeFeatures.rst b/docs/api_doc/datetime/DatetimeFeatures.rst
new file mode 100644
index 0000000..ed56d8b
--- /dev/null
+++ b/docs/api_doc/datetime/DatetimeFeatures.rst
@@ -0,0 +1,6 @@
+DatetimeFeatures
+================
+
+.. autoclass:: feature_engine.datetime.DatetimeFeatures
+    :members:
+
diff --git a/docs/api_doc/datetime/index.rst b/docs/api_doc/datetime/index.rst
new file mode 100644
index 0000000..c47d463
--- /dev/null
+++ b/docs/api_doc/datetime/index.rst
@@ -0,0 +1,13 @@
+.. -*- mode: rst -*-
+
+Datetime Features
+=================
+
+Feature-engine's datetime transformers are able to extract a wide variety of datetime
+features from existing datetime or object-like data.
+
+.. toctree::
+   :maxdepth: 2
+
+   DatetimeFeatures
+
diff --git a/docs/api_doc/encoding/index.rst b/docs/api_doc/encoding/index.rst
index 9120b91..f187632 100644
--- a/docs/api_doc/encoding/index.rst
+++ b/docs/api_doc/encoding/index.rst
@@ -1,7 +1,7 @@
 .. -*- mode: rst -*-
 
-Categorical Variable Encoding
-=============================
+Categorical Encoding
+====================
 
 Feature-engine's categorical encoders replace variable strings by estimated or
 arbitrary numbers.
diff --git a/docs/api_doc/index.rst b/docs/api_doc/index.rst
index 3dc3533..897645d 100644
--- a/docs/api_doc/index.rst
+++ b/docs/api_doc/index.rst
@@ -10,10 +10,11 @@ Full API documentation for Feature-engine transformers.
 
    imputation/index
    encoding/index
-   transformation/index
    discretisation/index
    outliers/index
+   transformation/index
    creation/index
    selection/index
+   datetime/index
    preprocessing/index
    wrappers/index
\ No newline at end of file
diff --git a/docs/index.rst b/docs/index.rst
index d050a73..2e7fefb 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -22,12 +22,13 @@ transform the data.
 Feature-engine includes transformers for:
 
 - Missing data imputation
-- Categorical variable encoding
+- Categorical encoding
 - Discretisation
-- Variable transformation
 - Outlier capping or removal
+- Variable transformation
 - Variable combination
 - Variable selection
+- Datetime features
 - Preprocessing
 
 Feature-engine allows you to select the variables you want to transform **within** each
@@ -114,8 +115,8 @@ Missing Data Imputation: Imputers
 - :doc:`api_doc/imputation/AddMissingIndicator`: adds a binary missing indicator to flag observations with missing data
 - :doc:`api_doc/imputation/DropMissingData`: removes observations (rows) containing missing values from dataframe
 
-Categorical Variable Encoders: Encoders
----------------------------------------
+Categorical Encoders: Encoders
+------------------------------
 
 - :doc:`api_doc/encoding/OneHotEncoder`: performs one hot encoding, optional: of popular categories
 - :doc:`api_doc/encoding/CountFrequencyEncoder`: replaces categories by the observation count or percentage
@@ -126,16 +127,6 @@ Categorical Variable Encoders: Encoders
 - :doc:`api_doc/encoding/DecisionTreeEncoder`: replaces categories by predictions of a decision tree
 - :doc:`api_doc/encoding/RareLabelEncoder`: groups infrequent categories
 
-Numerical Variable Transformation: Transformers
------------------------------------------------
-
-- :doc:`api_doc/transformation/LogTransformer`: performs logarithmic transformation of numerical variables
-- :doc:`api_doc/transformation/LogCpTransformer`: performs logarithmic transformation after adding a constant value
-- :doc:`api_doc/transformation/ReciprocalTransformer`: performs reciprocal transformation of numerical variables
-- :doc:`api_doc/transformation/PowerTransformer`: performs power transformation of numerical variables
-- :doc:`api_doc/transformation/BoxCoxTransformer`: performs Box-Cox transformation of numerical variables
-- :doc:`api_doc/transformation/YeoJohnsonTransformer`: performs Yeo-Johnson transformation of numerical variables
-
 Variable Discretisation: Discretisers
 -------------------------------------
 
@@ -151,6 +142,16 @@ Outlier Capping or Removal
 -  :doc:`api_doc/outliers/Winsorizer`: caps maximum or minimum values using statistical parameters
 -  :doc:`api_doc/outliers/OutlierTrimmer`: removes outliers from the dataset
 
+Numerical Transformation: Transformers
+--------------------------------------
+
+- :doc:`api_doc/transformation/LogTransformer`: performs logarithmic transformation of numerical variables
+- :doc:`api_doc/transformation/LogCpTransformer`: performs logarithmic transformation after adding a constant value
+- :doc:`api_doc/transformation/ReciprocalTransformer`: performs reciprocal transformation of numerical variables
+- :doc:`api_doc/transformation/PowerTransformer`: performs power transformation of numerical variables
+- :doc:`api_doc/transformation/BoxCoxTransformer`: performs Box-Cox transformation of numerical variables
+- :doc:`api_doc/transformation/YeoJohnsonTransformer`: performs Yeo-Johnson transformation of numerical variables
+
 Mathematical Combination:
 -------------------------
 
@@ -173,6 +174,9 @@ Feature Selection:
 - :doc:`api_doc/selection/RecursiveFeatureElimination`: selects features recursively, by evaluating model performance
 - :doc:`api_doc/selection/RecursiveFeatureAddition`: selects features recursively, by evaluating model performance
 
+Datetime:
+---------
+- :doc:`api_doc/datetime/DatetimeFeatures`: extract features from datetime variables
 
 Preprocessing:
 --------------
diff --git a/docs/user_guide/datetime/DatetimeFeatures.rst b/docs/user_guide/datetime/DatetimeFeatures.rst
new file mode 100644
index 0000000..ec51e5b
--- /dev/null
+++ b/docs/user_guide/datetime/DatetimeFeatures.rst
@@ -0,0 +1,615 @@
+.. _datetime_features:
+
+.. currentmodule:: feature_engine.datetime
+
+DatetimeFeatures
+================
+
+:class:`DatetimeFeatures()` extracts several datetime 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.
+
+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.
+
+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.
+
+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.
+
+Examples
+--------
+Through the following examples we highlight the functionality and versatility of
+:class:`DatetimeFeatures()`.
+
+Extract date features
+~~~~~~~~~~~~~~~~~~~~~
+
+In this example, we are going to extract three date features from a
+specific variable in the dataframe. In particular, we are interested
+in the month, the day of the year, and whether that day was the last
+day of its correspondent month.
+
+First, we will create a toy dataframe with 2 date variables.
+
+.. code:: python
+
+    import pandas as pd
+    from feature_engine.datetime import DatetimeFeatures
+
+    toy_df = pd.DataFrame({
+        "var_date1": ['May-1989', 'Dec-2020', 'Jan-1999', 'Feb-2002'],
+        "var_date2": ['06/21/12', '02/10/98', '08/03/10', '10/31/20'],
+    })
+
+Now, we will extract the variables month, month-end and the day of the year from the
+second datetime variable in our dataset.
+
+.. code:: python
+
+    dtfs = DatetimeFeatures(
+        variables="var_date2",
+        features_to_extract=["month", "month_end", "day_of_the_year"]
+    )
+
+    df_transf = dtfs.fit_transform(toy_df)
+
+    print(df_transf)
+
+.. code:: python
+
+      var_date1  var_date2_month  var_date2_month_end  var_date2_doty
+    0  May-1989                6                    0             173
+    1  Dec-2020                2                    0              41
+    2  Jan-1999                8                    0             215
+    3  Feb-2002               10                    1             305
+
+
+With `transform()`, the features extracted from the datetime variable are added to the
+dataframe.
+
+By default, :class:`DatetimeFeatures()` drops the variable from which the date and time
+features were extracted, in this case, *var_date2*. To keep the variable, we just need
+to indicate `drop_original=False` when initializing the transformer.
+
+Extract time features
+~~~~~~~~~~~~~~~~~~~~~
+
+In this example, we are going to extract the feature *minute* from the two time
+variables in our dataset.
+
+First, let's create a toy dataset with 2 time variables and an object variable.
+
+.. code:: python 
+
+    import pandas as pd
+    from feature_engine.datetime import DatetimeFeatures
+
+    toy_df = pd.DataFrame({
+        "not_a_dt": ['not', 'a', 'date', 'time'],
+        "var_time1": ['12:34:45', '23:01:02', '11:59:21', '08:44:23'],
+        "var_time2": ['02:27:26', '10:10:55', '17:30:00', '18:11:18'],
+    })
+
+:class:`DatetimeFeatures()` automatically finds all variables that can be parsed to
+datetime. So if we want to extract time features from all our datetime variables, we
+don't need to specify them.
+
+.. code:: python
+
+    dfts = DatetimeFeatures(features_to_extract=["minute"])
+
+    df_transf = dfts.fit_transform(toy_df)
+
+    print(df_transf)
+
+.. code:: python
+
+      not_a_dt  var_time1_minute  var_time2_minute
+    0      not                34                27
+    1        a                 1                10
+    2     date                59                30
+    3     time                44                11
+
+
+The transformer found two variales in the dataframe that can be cast to datetime and
+proceeded to extract the requested feature from them.
+
+We can find the variables detected as datetime by the transformer in one of its
+attributes.
+
+.. code:: python
+
+    dfts.variables_
+
+.. code:: python
+
+    ['var_time1', 'var_time2']
+
+Again, the original datetime variables are dropped from the data by default. If we
+want to keep them, we just need to indicate `drop_original=False` when initializing
+the transformer.
+
+Extract date and time features
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+In this example, we will combine what we have seen in the previous two examples
+and extract a date feature - *year* - and time feature - *hour* -
+from two variables that contain both date and time information.
+
+Let's go ahead and create a toy dataset.
+
+.. code:: python
+
+    import pandas as pd
+    from feature_engine.datetime import DatetimeFeatures
+
+    toy_df = pd.DataFrame({
+        "var_dt1": pd.date_range("2018-01-01", periods=3, freq="H"),
+        "var_dt2": ['08/31/00 12:34:45', '12/01/90 23:01:02', '04/25/01 11:59:21'],
+        "var_dt3": ['03/02/15 02:27:26', '02/28/97 10:10:55', '11/11/03 17:30:00'],
+    })
+
+Now, we set up the :class:`DatetimeFeatures()` to extract features only from 2 of the
+variables. In this case, we do not want to drop the datetime variable after extracting
+the features.
+
+.. code:: python
+
+    dfts = DatetimeFeatures(
+        variables=["var_dt1", "var_dt3"],
+        features_to_extract=["year", "hour"],
+        drop_original=False,
+    )
+    df_transf = dfts.fit_transform(toy_df)
+
+    print(df_transf)
+
+.. code:: python
+
+                  var_dt1            var_dt2            var_dt3  var_dt1_year  \
+    0 2018-01-01 00:00:00  08/31/00 12:34:45  03/02/15 02:27:26          2018
+    1 2018-01-01 01:00:00  12/01/90 23:01:02  02/28/97 10:10:55          2018
+    2 2018-01-01 02:00:00  04/25/01 11:59:21  11/11/03 17:30:00          2018
+
+       var_dt1_hour  var_dt3_year  var_dt3_hour
+    0             0          2015             2
+    1             1          1997            10
+    2             2          2003            17
+
+And that is it. The additional features are now added in the dataframe.
+
+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
+possible. In this case, keep in mind that if you extract date features from variables
+that have only time, or time features from variables that have only dates, your features
+will be meaningless.
+
+Let's explore the outcome with an example. We create a dataset with only time variables.
+
+.. code:: python
+
+    import pandas as pd
+    from feature_engine.datetime import DatetimeFeatures
+
+    toy_df = pd.DataFrame({
+        "not_a_dt": ['not', 'a', 'date', 'time'],
+        "var_time1": ['12:34:45', '23:01:02', '11:59:21', '08:44:23'],
+        "var_time2": ['02:27:26', '10:10:55', '17:30:00', '18:11:18'],
+    })
+
+And now we mistakenly extract only date features.
+
+.. code:: python
+
+    dfts = DatetimeFeatures(
+        features_to_extract=["year", "month", "day_of_the_week"],
+    )
+    df_transf = dfts.fit_transform(toy_df)
+
+    print(df_transf)
+
+.. code:: python
+
+      not_a_dt  var_time1_year  var_time1_month  var_time1_dotw  var_time2_year  \
+    0      not            2021               12               2            2021
+    1        a            2021               12               2            2021
+    2     date            2021               12               2            2021
+    3     time            2021               12               2            2021
+
+       var_time2_month  var_time2_dotw
+    0               12               2
+    1               12               2
+    2               12               2
+    3               12               2
+
+The transformer will still create features derived from today's date (the date of
+creating the docs).
+
+If instead we have a dataframe with only date variables.
+
+.. code:: python
+
+    import pandas as pd
+    from feature_engine.datetime import DatetimeFeatures
+
+    toy_df = pd.DataFrame({
+        "var_date1": ['May-1989', 'Dec-2020', 'Jan-1999', 'Feb-2002'],
+        "var_date2": ['06/21/12', '02/10/98', '08/03/10', '10/31/20'],
+    })
+
+And mistakenly extract the hour and the minute.
+
+.. code:: python
+
+    dfts = DatetimeFeatures(
+        features_to_extract=["hour", "minute"],
+    )
+    df_transf = dfts.fit_transform(toy_df)
+
+    print(df_transf)
+
+.. code:: python
+
+       var_date1_hour  var_date1_minute  var_date2_hour  var_date2_minute
+    0               0                 0               0                 0
+    1               0                 0               0                 0
+    2               0                 0               0                 0
+    3               0                 0               0                 0
+
+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,
+:class:`DatetimeFeatures()` has default options to extract a group of commonly used
+features, or all supported features.
+
+Let's first create a toy dataframe.
+
+.. code:: python
+
+    import pandas as pd
+    from feature_engine.datetime import DatetimeFeatures
+
+    toy_df = pd.DataFrame({
+        "var_dt1": pd.date_range("2018-01-01", periods=3, freq="H"),
+        "var_dt2": ['08/31/00 12:34:45', '12/01/90 23:01:02', '04/25/01 11:59:21'],
+        "var_dt3": ['03/02/15 02:27:26', '02/28/97 10:10:55', '11/11/03 17:30:00'],
+    })
+
+Now, we will extract the most common date and time features from one of the variables.
+To do this, we leave the parameter `features_to_extract` to `None`.
+
+.. code:: python
+
+    dfts = DatetimeFeatures(
+        variables=["var_dt1"],
+        features_to_extract=None,
+        drop_original=False,
+    )
+
+    df_transf = dfts.fit_transform(toy_df)
+
+    print(df_transf)
+
+.. code:: python
+
+                  var_dt1            var_dt2            var_dt3  var_dt1_month  \
+    0 2018-01-01 00:00:00  08/31/00 12:34:45  03/02/15 02:27:26              1
+    1 2018-01-01 01:00:00  12/01/90 23:01:02  02/28/97 10:10:55              1
+    2 2018-01-01 02:00:00  04/25/01 11:59:21  11/11/03 17:30:00              1
+
+       var_dt1_year  var_dt1_dotw  var_dt1_dotm  var_dt1_hour  var_dt1_minute  \
+    0          2018             0             1             0               0
+    1          2018             0             1             1               0
+    2          2018             0             1             2               0
+
+       var_dt1_second
+    0               0
+    1               0
+    2               0
+
+Our new dataset contains the original features plus the new variables extracted
+from them.
+
+We can find the group of features extracted by the transformer in its attribute.
+
+.. code:: python
+
+    dfts.features_to_extract_
+
+.. code:: python
+
+    ['month',
+     'year',
+     'day_of_the_week',
+     'day_of_the_month',
+     'hour',
+     'minute',
+     'second']
+
+We can also extract all supported features automatically.
+
+.. code:: python
+
+    dfts = DatetimeFeatures(
+        variables=["var_dt1"],
+        features_to_extract='all',
+        drop_original=False,
+    )
+
+    df_transf = dfts.fit_transform(toy_df)
+
+    print(df_transf)
+
+.. code:: python
+
+                  var_dt1            var_dt2            var_dt3  var_dt1_month  \
+    0 2018-01-01 00:00:00  08/31/00 12:34:45  03/02/15 02:27:26              1
+    1 2018-01-01 01:00:00  12/01/90 23:01:02  02/28/97 10:10:55              1
+    2 2018-01-01 02:00:00  04/25/01 11:59:21  11/11/03 17:30:00              1
+
+       var_dt1_quarter  var_dt1_semester  var_dt1_year  \
+    0                1                 1          2018
+    1                1                 1          2018
+    2                1                 1          2018
+
+       var_dt1_woty  var_dt1_dotw  ...  var_dt1_month_end  var_dt1_quarter_start  \
+    0             1             0  ...                  0                      1
+    1             1             0  ...                  0                      1
+    2             1             0  ...                  0                      1
+
+       var_dt1_quarter_end  var_dt1_year_start  var_dt1_year_end  \
+    0                    0                   1                 0
+    1                    0                   1                 0
+    2                    0                   1                 0
+
+       var_dt1_leap_year  var_dt1_days_in_month  var_dt1_hour  var_dt1_minute  \
+    0                  0                     31             0               0
+    1                  0                     31             1               0
+    2                  0                     31             2               0
+
+       var_dt1_second
+    0               0
+    1               0
+    2               0
+
+We can find the group of features extracted by the transformer in its attribute.
+
+.. code:: python
+
+    dfts.features_to_extract_
+
+.. code:: python
+
+    ['month',
+     'quarter',
+     'semester',
+     'year',
+     'week_of_the_year',
+     'day_of_the_week',
+     'day_of_the_month',
+     'day_of_the_year',
+     'weekend',
+     'month_start',
+     'month_end',
+     'quarter_start',
+     'quarter_end',
+     'year_start',
+     'year_end',
+     'leap_year',
+     'days_in_month',
+     'hour',
+     'minute',
+     '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
+go ahead and remove the irrelevant features with the `DropConstantFeatures()` class.
+
+Let's create a dataframe with a mix of datetime variables.
+
+.. code:: python
+
+    import pandas as pd
+    from sklearn.pipeline import Pipeline
+    from feature_engine.datetime import DatetimeFeatures
+    from feature_engine.selection import DropConstantFeatures
+
+    toy_df = pd.DataFrame({
+        "var_date": ['06/21/12', '02/10/98', '08/03/10', '10/31/20'],
+        "var_time1": ['12:34:45', '23:01:02', '11:59:21', '08:44:23'],
+        "var_dt": ['08/31/00 12:34:45', '12/01/90 23:01:02', '04/25/01 11:59:21', '04/25/01 11:59:21'],
+    })
+
+Now, we align in a Scikit-learn pipeline the :class:`DatetimeFeatures` and the
+`DropConstantFeatures()`. The :class:`DatetimeFeatures` will create date features
+derived from today for the time variable, and time features with the value 0 for the
+date only variable. `DropConstantFeatures()` will identify and remove these features
+from the dataset.
+
+.. code:: python
+
+    pipe = Pipeline([
+        ('datetime', DatetimeFeatures()),
+        ('drop_constant', DropConstantFeatures()),
+    ])
+
+    pipe.fit(toy_df)
+
+.. code:: python
+
+    Pipeline(steps=[('datetime', DatetimeFeatures()),
+                    ('drop_constant', DropConstantFeatures())])
+
+.. code:: python
+
+    df_transf = pipe.transform(toy_df)
+
+    print(df_transf)
+
+.. code:: python
+
+       var_date_month  var_date_year  var_date_dotw  var_date_dotm  \
+    0               6           2012              3             21
+    1               2           1998              1             10
+    2               8           2010              1              3
+    3              10           2020              5             31
+
+       var_time1_hour  var_time1_minute  var_time1_second  var_dt_month  \
+    0              12                34                45             8
+    1              23                 1                 2            12
+    2              11                59                21             4
+    3               8                44                23             4
+
+       var_dt_year  var_dt_dotw  var_dt_dotm  var_dt_hour  var_dt_minute  \
+    0         2000            3           31           12             34
+    1         1990            5            1           23              1
+    2         2001            2           25           11             59
+    3         2001            2           25           11             59
+
+       var_dt_second
+    0             45
+    1              2
+    2             21
+    3             21
+
+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
+with such variables in three different scenarios.
+
+**Case 1**: our dataset contains a time-aware variable in object format,
+with potentially different timezones across different observations. 
+We pass `utc=True` when initializing the transformer to make sure it
+converts all data to UTC timezone.
+
+.. code:: python
+
+    import pandas as pd
+    from feature_engine.datetime import DatetimeFeatures
+
+    toy_df = pd.DataFrame({
+        "var_tz": ['12:34:45+3', '23:01:02-6', '11:59:21-8', '08:44:23Z']
+    })
+
+    dfts = DatetimeFeatures(
+        features_to_extract=["hour", "minute"],
+        drop_original=False,
+        utc=True
+    )
+
+    df_transf = dfts.fit_transform(toy_df)
+
+    print(df_transf)
+
+.. code:: python
+
+           var_tz  var_tz_hour  var_tz_minute
+    0  12:34:45+3            9             34
+    1  23:01:02-6            5              1
+    2  11:59:21-8           19             59
+    3   08:44:23Z            8             44
+
+
+**Case 2**: our dataset contains a variable that is cast as a localized
+datetime in a particular timezone. However, we decide that we want to get all
+the datetime information extracted as if it were in UTC timezone.
+
+.. code:: python
+
+    import pandas as pd
+    from feature_engine.datetime import DatetimeFeatures
+
+    var_tz = pd.Series(['08/31/00 12:34:45', '12/01/90 23:01:02', '04/25/01 11:59:21'])
+    var_tz = pd.to_datetime(var_tz)
+    var_tz = var_tz.dt.tz_localize("US/eastern")
+    var_tz
+
+.. code:: python
+
+    0   2000-08-31 12:34:45-04:00
+    1   1990-12-01 23:01:02-05:00
+    2   2001-04-25 11:59:21-04:00
+    dtype: datetime64[ns, US/Eastern]
+
+We need to pass `utc=True` when initializing the transformer to revert back to the UTC
+timezone.
+
+.. code:: python
+
+    toy_df = pd.DataFrame({"var_tz": var_tz})
+
+    dfts = DatetimeFeatures(
+        features_to_extract=["day_of_the_month", "hour"],
+        drop_original=False,
+        utc=True,
+    )
+
+    df_transf = dfts.fit_transform(toy_df)
+
+    print(df_transf)
+
+.. code:: python
+
+                         var_tz  var_tz_dotm  var_tz_hour
+    0 2000-08-31 12:34:45-04:00           31           16
+    1 1990-12-01 23:01:02-05:00            2            4
+    2 2001-04-25 11:59:21-04:00           25           15
+
+
+**Case 3**: given a variable like *var_tz* in the example above, we now want
+to extract the features keeping the original timezone localization,
+therefore we pass `utc=False` or `None`. In this case, we leave it to `None` which
+is the default option.
+
+.. code:: python
+
+    dfts = DatetimeFeatures(
+        features_to_extract=["day_of_the_month", "hour"],
+        drop_original=False,
+        utc=None,
+    )
+
+    df_transf = dfts.fit_transform(toy_df)
+
+    print(df_transf)
+
+.. code:: python
+
+                         var_tz  var_tz_dotm  var_tz_hour
+    0 2000-08-31 12:34:45-04:00           31           12
+    1 1990-12-01 23:01:02-05:00            1           23
+    2 2001-04-25 11:59:21-04:00           25           11
+
+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.
+
+- `Jupyter notebook <https://nbviewer.org/github/feature-engine/feature-engine-examples/blob/main/datetime/DatetimeFeatures.ipynb>`_
diff --git a/docs/user_guide/datetime/index.rst b/docs/user_guide/datetime/index.rst
new file mode 100644
index 0000000..f4976c6
--- /dev/null
+++ b/docs/user_guide/datetime/index.rst
@@ -0,0 +1,12 @@
+.. -*- mode: rst -*-
+
+Datetime Features
+=================
+
+Feature-engine’s datetime transformers are able to extract a wide variety of datetime
+features from existing datetime or object-like data.
+
+.. toctree::
+   :maxdepth: 1
+
+   DatetimeFeatures
\ No newline at end of file
diff --git a/docs/user_guide/encoding/index.rst b/docs/user_guide/encoding/index.rst
index 7c26a6f..561d017 100644
--- a/docs/user_guide/encoding/index.rst
+++ b/docs/user_guide/encoding/index.rst
@@ -3,8 +3,8 @@
 .. currentmodule:: feature_engine.encoding
 
 
-Categorical Variable Encoding
-=============================
+Categorical Encoding
+====================
 
 Feature-engine's categorical encoders replace variable strings by estimated or
 arbitrary numbers. The following image summarizes the main encoder’s functionality.
diff --git a/docs/user_guide/index.rst b/docs/user_guide/index.rst
index d6ab9f7..928d916 100644
--- a/docs/user_guide/index.rst
+++ b/docs/user_guide/index.rst
@@ -12,10 +12,11 @@ and feature engineering transformations in general, as well as additional exampl
 
    imputation/index
    encoding/index
-   transformation/index
    discretisation/index
    outliers/index
+   transformation/index
    creation/index
    selection/index
+   datetime/index
    preprocessing/index
-   wrappers/index
\ No newline at end of file
+   wrappers/index
diff --git a/feature_engine/datetime/__init__.py b/feature_engine/datetime/__init__.py
new file mode 100644
index 0000000..ac06ff1
--- /dev/null
+++ b/feature_engine/datetime/__init__.py
@@ -0,0 +1,5 @@
+"The module datetime computes features from dates and times."
+
+from .datetime import DatetimeFeatures
+
+__all__ = ["DatetimeFeatures"]
diff --git a/feature_engine/datetime/datetime.py b/feature_engine/datetime/datetime.py
new file mode 100644
index 0000000..de73f22
--- /dev/null
+++ b/feature_engine/datetime/datetime.py
@@ -0,0 +1,274 @@
+# Authors: dodoarg <eargiolas96@gmail.com>
+
+from typing import List, Optional, Union
+
+import pandas as pd
+from pandas.api.types import is_datetime64_any_dtype as is_datetime
+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,
+)
+from feature_engine.datetime.datetime_constants import (
+    FEATURES_DEFAULT,
+    FEATURES_FUNCTIONS,
+    FEATURES_SUFFIXES,
+    FEATURES_SUPPORTED,
+)
+from feature_engine.variable_manipulation import (
+    _check_input_parameter_variables,
+    _find_or_check_datetime_variables,
+)
+
+
+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.
+
+    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:
+
+    - "month"
+    - "quarter"
+    - "semester"
+    - "year"
+    - "week_of_the_month"
+    - "week_of_the_year"
+    - "day_of_the_week"
+    - "day_of_the_month"
+    - "day_of_the_year"
+    - "weekend"
+    - "month_start"
+    - "month_end"
+    - "quarter_start"
+    - "quarter_end"
+    - "year_start"
+    - "year_end"
+    - "leap_year"
+    - "days_in_month"
+    - "hour"
+    - "minute"
+    - "second"
+
+    More details in the :ref:`User Guide <datetime_features>`.
+
+    Parameters
+    ----------
+    variables: list, default=None
+        The list of variables to extract date and time features from.
+        If None, the transformer will find and select all datetime variables,
+        including variables of type object that can be converted to datetime.
+
+    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_the_week", "day_of_the_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.
+
+    drop_original: bool, default="True"
+        If True, the original datetime variables will be dropped from the dataframe.
+
+    missing_values: string, default='raise'
+        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.
+
+    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.
+
+    yearfirst: bool, default="False"
+        Specify a date parse order if arg is str or its list-likes.
+
+        - If True parses dates with the year first, eg 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).
+
+    Attributes
+    ----------
+    variables_:
+        List of variables from which date and time features will be extracted.
+
+    features_to_extract_:
+        The date and time features that will be extracted from each variable.
+
+    n_features_in_:
+        The number of features in the train set used in fit.
+
+    Methods
+    -------
+    fit:
+        This transformer does not learn parameters.
+    transform:
+        Add the date and time features.
+    fit_transform:
+        Fit to the data, then transform it.
+
+    See also
+    --------
+    pandas.to_datetime
+    pandas.dt
+    """
+
+    def __init__(
+        self,
+        variables: Union[None, int, str, List[Union[str, int]]] = None,
+        features_to_extract: Union[None, str, List[str]] = None,
+        drop_original: bool = True,
+        missing_values: str = "raise",
+        dayfirst: bool = False,
+        yearfirst: bool = False,
+        utc: Union[None, bool] = None,
+
+    ) -> None:
+
+        if features_to_extract:
+            if not (
+                isinstance(features_to_extract, list) or features_to_extract == "all"
+            ):
+                raise ValueError(
+                    "features_to_extract must be a list of strings or 'all'. "
+                    f"Got {features_to_extract} instead."
+                )
+            elif isinstance(features_to_extract, list) and any(
+                feat not in FEATURES_SUPPORTED for feat in features_to_extract
+            ):
+                raise ValueError(
+                    "Some of the requested features are not supported. "
+                    "Supported features are {}.".format(", ".join(FEATURES_SUPPORTED))
+                )
+
+        if not isinstance(drop_original, bool):
+            raise ValueError(
+                "drop_original takes only booleans True or False. "
+                f"Got {drop_original} instead."
+            )
+
+        if missing_values not in ["raise", "ignore"]:
+            raise ValueError(
+                "missing_values takes only values 'raise' or 'ignore'. "
+                f"Got {missing_values} instead."
+            )
+
+        if utc is not None and not isinstance(utc, bool):
+            raise ValueError(
+                "utc takes only booleans or None. "
+                f"Got {utc} instead."
+            )
+
+        self.variables = _check_input_parameter_variables(variables)
+        self.drop_original = drop_original
+        self.missing_values = missing_values
+        self.dayfirst = dayfirst
+        self.yearfirst = yearfirst
+        self.utc = utc
+        self.features_to_extract = features_to_extract
+
+    def fit(self, X: pd.DataFrame, y: Optional[pd.Series] = None):
+        """
+        This transformer does not learn any parameter.
+
+        Finds datetime variables or checks that the variables selected by the user
+        can be converted to datetime.
+
+        Parameters
+        ----------
+        X: pandas dataframe of shape = [n_samples, n_features]
+            The training input samples. Can be the entire dataframe, not just the
+            variables to transform.
+
+        y: pandas Series, default=None
+            It is not needed in this transformer. You can pass y or None.
+        """
+        # check input dataframe
+        X = _is_dataframe(X)
+
+        # 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
+        elif isinstance(self.features_to_extract, str):
+            self.features_to_extract_ = FEATURES_SUPPORTED
+        else:
+            self.features_to_extract_ = self.features_to_extract
+
+        self.n_features_in_ = X.shape[1]
+
+        return self
+
+    def transform(self, X: pd.DataFrame) -> pd.DataFrame:
+        """
+        Extract the date and time features and add them to the dataframe.
+
+        Parameters
+        ----------
+        X: pandas dataframe of shape = [n_samples, n_features]
+            The data to transform.
+
+        Returns
+        -------
+        X_new: Pandas dataframe, shape = [n_samples, n_features x n_df_features]
+            The dataframe with the original variables plus the new variables.
+        """
+
+        # Check method fit has been called
+        check_is_fitted(self)
+
+        # check that input is a dataframe
+        X = _is_dataframe(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_)
+
+        # 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"
+            )
+
+        # 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
diff --git a/feature_engine/datetime/datetime_constants.py b/feature_engine/datetime/datetime_constants.py
new file mode 100644
index 0000000..123f3d5
--- /dev/null
+++ b/feature_engine/datetime/datetime_constants.py
@@ -0,0 +1,80 @@
+import numpy as np
+
+FEATURES_SUPPORTED = [
+    "month",
+    "quarter",
+    "semester",
+    "year",
+    "week_of_the_year",
+    "day_of_the_week",
+    "day_of_the_month",
+    "day_of_the_year",
+    "weekend",
+    "month_start",
+    "month_end",
+    "quarter_start",
+    "quarter_end",
+    "year_start",
+    "year_end",
+    "leap_year",
+    "days_in_month",
+    "hour",
+    "minute",
+    "second",
+]
+
+FEATURES_SUFFIXES = {
+    "month": "_month",
+    "quarter": "_quarter",
+    "semester": "_semester",
+    "year": "_year",
+    "week_of_the_year": "_woty",
+    "day_of_the_week": "_dotw",
+    "day_of_the_month": "_dotm",
+    "day_of_the_year": "_doty",
+    "weekend": "_weekend",
+    "month_start": "_month_start",
+    "month_end": "_month_end",
+    "quarter_start": "_quarter_start",
+    "quarter_end": "_quarter_end",
+    "year_start": "_year_start",
+    "year_end": "_year_end",
+    "leap_year": "_leap_year",
+    "days_in_month": "_days_in_month",
+    "hour": "_hour",
+    "minute": "_minute",
+    "second": "_second",
+}
+
+FEATURES_FUNCTIONS = {
+    "month": lambda x: x.dt.month,
+    "quarter": lambda x: x.dt.quarter,
+    "semester": lambda x: np.where(x.dt.month <= 6, 1, 2).astype(np.int64),
+    "year": lambda x: x.dt.year,
+    "week_of_the_year": lambda x: x.dt.isocalendar().week.astype(np.int64),
+    "day_of_the_week": lambda x: x.dt.dayofweek,
+    "day_of_the_month": lambda x: x.dt.day,
+    "day_of_the_year": lambda x: x.dt.dayofyear,
+    "weekend": lambda x: np.where(x.dt.dayofweek <= 4, 0, 1).astype(np.int64),
+    "month_start": lambda x: x.dt.is_month_start.astype(np.int64),
+    "month_end": lambda x: x.dt.is_month_end.astype(np.int64),
+    "quarter_start": lambda x: x.dt.is_quarter_start.astype(np.int64),
+    "quarter_end": lambda x: x.dt.is_quarter_end.astype(np.int64),
+    "year_start": lambda x: x.dt.is_year_start.astype(np.int64),
+    "year_end": lambda x: x.dt.is_year_end.astype(np.int64),
+    "leap_year": lambda x: x.dt.is_leap_year.astype(np.int64),
+    "days_in_month": lambda x: x.dt.days_in_month.astype(np.int64),
+    "hour": lambda x: x.dt.hour,
+    "minute": lambda x: x.dt.minute,
+    "second": lambda x: x.dt.second,
+}
+
+FEATURES_DEFAULT = [
+    "month",
+    "year",
+    "day_of_the_week",
+    "day_of_the_month",
+    "hour",
+    "minute",
+    "second",
+]
diff --git a/feature_engine/selection/base_selector.py b/feature_engine/selection/base_selector.py
index 35e7586..3c9d535 100644
--- a/feature_engine/selection/base_selector.py
+++ b/feature_engine/selection/base_selector.py
@@ -25,7 +25,9 @@ def get_feature_importances(estimator):
         else:
             importances = np.linalg.norm(coef_, axis=0, ord=len(estimator.coef_))
 
-    return list(importances)
+        importances = list(importances)
+
+    return importances
 
 
 class BaseSelector(BaseEstimator, TransformerMixin):
diff --git a/feature_engine/variable_manipulation.py b/feature_engine/variable_manipulation.py
index a043dad..bf8135c 100644
--- a/feature_engine/variable_manipulation.py
+++ b/feature_engine/variable_manipulation.py
@@ -3,9 +3,9 @@
 from typing import Any, List, Union
 
 import pandas as pd
-
-from pandas.api.types import is_numeric_dtype as is_numeric
 from pandas.api.types import is_categorical_dtype as is_categorical
+from pandas.api.types import is_datetime64_any_dtype as is_datetime
+from pandas.api.types import is_numeric_dtype as is_numeric
 from pandas.api.types import is_object_dtype as is_object
 
 Variables = Union[None, int, str, List[Union[str, int]]]
@@ -147,6 +147,68 @@ def _find_or_check_categorical_variables(
     return variables
 
 
+def _find_or_check_datetime_variables(
+    X: pd.DataFrame, variables: Variables = None
+) -> List[Union[str, int]]:
+    """
+    Checks that variables provided by the user are of type datetime.
+    If None, finds all datetime variables in the DataFrame.
+
+    Parameters
+    ----------
+    X : pandas DataFrame
+    variables : variable or list of variables. Defaults to None.
+
+    Returns
+    -------
+    variables : List of datetime variables.
+    """
+
+    if variables is None:
+        variables = [
+            column
+            for column in X.select_dtypes(exclude="number").columns
+            if is_datetime(X[column])
+            or is_datetime(pd.to_datetime(X[column], errors="ignore", utc=True))
+        ]
+
+        if len(variables) == 0:
+            raise ValueError("No datetime variables found in this dataframe.")
+
+    elif isinstance(variables, (str, int)):
+
+        if is_datetime(X[variables]) or (
+            not is_numeric(X[variables])
+            and is_datetime(pd.to_datetime(X[variables], errors="ignore", utc=True))
+        ):
+            variables = [variables]
+        else:
+            raise TypeError("The variable entered is not datetime.")
+
+    else:
+        if len(variables) == 0:
+            raise ValueError("The indicated list of variables is empty.")
+
+        # check that the variables entered by the user are datetime
+        else:
+            vars_non_dt = [
+                column
+                for column in variables
+                if is_numeric(X[column])
+                or (
+                    not is_datetime(X[column])
+                    and not is_datetime(
+                        pd.to_datetime(X[column], errors="ignore", utc=True)
+                    )
+                )
+            ]
+
+            if len(vars_non_dt) > 0:
+                raise TypeError("Some of the variables are not datetime.")
+
+    return variables
+
+
 def _find_all_variables(
     X: pd.DataFrame, variables: Variables = None
 ) -> List[Union[str, int]]:


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 6b56dfe..e6e664c 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -18,6 +18,22 @@ def df_vartypes():
     return df
 
 
+@pytest.fixture(scope="module")
+def df_datetime():
+    data = {
+        "Name": ["tom", "nick", "krish", "jack"],
+        "Age": [20, 21, 19, 18],
+        "datetime_range": pd.date_range("2020-02-24", periods=4, freq="D"),
+        "date_obj1": ["01-Jan-2010", "24-Feb-1945", "14-Jun-2100", "17-May-1999"],
+        "date_obj2": ["10/11/12", "12/31/09", "06/30/95", "03/17/04"],
+        "time_obj": ["21:45:23", "09:15:33", "12:34:59", "03:27:02"],
+    }
+
+    df = pd.DataFrame(data)
+
+    return df
+
+
 @pytest.fixture(scope="module")
 def df_numeric_columns():
     data = {
diff --git a/tests/test_datetime/__init__.py b/tests/test_datetime/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_datetime/conftest.py b/tests/test_datetime/conftest.py
new file mode 100644
index 0000000..1498a9f
--- /dev/null
+++ b/tests/test_datetime/conftest.py
@@ -0,0 +1,91 @@
+import pandas as pd
+import pytest
+
+
+@pytest.fixture(scope="module")
+def df_datetime_transformed(df_datetime):
+    today = pd.Timestamp.today()
+    data = {
+        "datetime_range_month": [2, 2, 2, 2],
+        "datetime_range_quarter": [1, 1, 1, 1],
+        "datetime_range_semester": [1, 1, 1, 1],
+        "datetime_range_year": [2020, 2020, 2020, 2020],
+        "datetime_range_woty": [9, 9, 9, 9],
+        "datetime_range_dotw": [0, 1, 2, 3],
+        "datetime_range_dotm": [24, 25, 26, 27],
+        "datetime_range_doty": [55, 56, 57, 58],
+        "datetime_range_weekend": [0, 0, 0, 0],
+        "datetime_range_month_start": [0, 0, 0, 0],
+        "datetime_range_month_end": [0, 0, 0, 0],
+        "datetime_range_quarter_start": [0, 0, 0, 0],
+        "datetime_range_quarter_end": [0, 0, 0, 0],
+        "datetime_range_year_start": [0, 0, 0, 0],
+        "datetime_range_year_end": [0, 0, 0, 0],
+        "datetime_range_leap_year": [1, 1, 1, 1],
+        "datetime_range_days_in_month": [29, 29, 29, 29],
+        "datetime_range_hour": [0] * 4,
+        "datetime_range_minute": [0] * 4,
+        "datetime_range_second": [0] * 4,
+        "date_obj1_month": [1, 2, 6, 5],
+        "date_obj1_quarter": [1, 1, 2, 2],
+        "date_obj1_semester": [1, 1, 1, 1],
+        "date_obj1_year": [2010, 1945, 2100, 1999],
+        "date_obj1_woty": [53, 8, 24, 20],
+        "date_obj1_dotw": [4, 5, 0, 0],
+        "date_obj1_dotm": [1, 24, 14, 17],
+        "date_obj1_doty": [1, 55, 165, 137],
+        "date_obj1_weekend": [0, 1, 0, 0],
+        "date_obj1_month_start": [1, 0, 0, 0],
+        "date_obj1_month_end": [0, 0, 0, 0],
+        "date_obj1_quarter_start": [1, 0, 0, 0],
+        "date_obj1_quarter_end": [0, 0, 0, 0],
+        "date_obj1_year_start": [1, 0, 0, 0],
+        "date_obj1_year_end": [0, 0, 0, 0],
+        "date_obj1_leap_year": [0, 0, 0, 0],
+        "date_obj1_days_in_month": [31, 28, 30, 31],
+        "date_obj1_hour": [0] * 4,
+        "date_obj1_minute": [0] * 4,
+        "date_obj1_second": [0] * 4,
+        "date_obj2_month": [10, 12, 6, 3],
+        "date_obj2_quarter": [4, 4, 2, 1],
+        "date_obj2_semester": [2, 2, 1, 1],
+        "date_obj2_year": [2012, 2009, 1995, 2004],
+        "date_obj2_woty": [41, 53, 26, 12],
+        "date_obj2_dotw": [3, 3, 4, 2],
+        "date_obj2_dotm": [11, 31, 30, 17],
+        "date_obj2_doty": [285, 365, 181, 77],
+        "date_obj2_weekend": [0, 0, 0, 0],
+        "date_obj2_month_start": [0, 0, 0, 0],
+        "date_obj2_month_end": [0, 1, 1, 0],
+        "date_obj2_quarter_start": [0, 0, 0, 0],
+        "date_obj2_quarter_end": [0, 1, 1, 0],
+        "date_obj2_year_start": [0, 0, 0, 0],
+        "date_obj2_year_end": [0, 1, 0, 0],
+        "date_obj2_leap_year": [1, 0, 0, 1],
+        "date_obj2_days_in_month": [31, 31, 30, 31],
+        "date_obj2_hour": [0] * 4,
+        "date_obj2_minute": [0] * 4,
+        "date_obj2_second": [0] * 4,
+        "time_obj_month": [today.month] * 4,
+        "time_obj_quarter": [today.quarter] * 4,
+        "time_obj_semester": [1 if today.month <= 6 else 2] * 4,
+        "time_obj_year": [today.year] * 4,
+        "time_obj_woty": [today.week] * 4,
+        "time_obj_dotw": [today.dayofweek] * 4,
+        "time_obj_dotm": [today.day] * 4,
+        "time_obj_doty": [today.dayofyear] * 4,
+        "time_obj_weekend": [1 if today.dayofweek > 4 else 0] * 4,
+        "time_obj_month_start": [int(today.is_month_start)] * 4,
+        "time_obj_month_end": [int(today.is_month_end)] * 4,
+        "time_obj_quarter_start": [int(today.is_quarter_start)] * 4,
+        "time_obj_quarter_end": [int(today.is_quarter_end)] * 4,
+        "time_obj_year_start": [int(today.is_year_start)] * 4,
+        "time_obj_year_end": [int(today.is_year_end)] * 4,
+        "time_obj_leap_year": [int(today.is_leap_year)] * 4,
+        "time_obj_days_in_month": [today.days_in_month] * 4,
+        "time_obj_hour": [21, 9, 12, 3],
+        "time_obj_minute": [45, 15, 34, 27],
+        "time_obj_second": [23, 33, 59, 2],
+    }
+    df = df_datetime.join(pd.DataFrame(data))
+    return df
diff --git a/tests/test_datetime/test_datetime_features.py b/tests/test_datetime/test_datetime_features.py
new file mode 100644
index 0000000..d497a80
--- /dev/null
+++ b/tests/test_datetime/test_datetime_features.py
@@ -0,0 +1,393 @@
+import numpy as np
+import pandas as pd
+import pytest
+from sklearn.exceptions import NotFittedError
+
+from feature_engine.datetime import DatetimeFeatures
+from feature_engine.datetime.datetime_constants import (
+    FEATURES_DEFAULT,
+    FEATURES_SUFFIXES,
+    FEATURES_SUPPORTED,
+)
+
+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]})
+
+
+_false_input_params = [
+    (["not_supported"], 3.519, "wrong_option"),
+    (["year", 1874], [1, -1.09, "var3"], 1),
+    ("year", [3.5], [True, False]),
+    (14198, [0.1, False], {True}),
+]
+
+
+@pytest.mark.parametrize(
+    "_features_to_extract, _variables, _other_params", _false_input_params
+)
+def test_raises_error_when_wrong_input_params(
+    _features_to_extract, _variables, _other_params
+):
+    with pytest.raises(ValueError):
+        assert DatetimeFeatures(features_to_extract=_features_to_extract)
+    with pytest.raises(ValueError):
+        assert DatetimeFeatures(variables=_variables)
+    with pytest.raises(ValueError):
+        assert DatetimeFeatures(missing_values=_other_params)
+    with pytest.raises(ValueError):
+        assert DatetimeFeatures(drop_original=_other_params)
+    with pytest.raises(ValueError):
+        assert DatetimeFeatures(utc=_other_params)
+
+
+def test_default_params():
+    transformer = DatetimeFeatures()
+    assert isinstance(transformer, DatetimeFeatures)
+    assert transformer.variables is None
+    assert transformer.features_to_extract is None
+    assert transformer.drop_original
+    assert transformer.utc is None
+    assert transformer.dayfirst is False
+    assert transformer.yearfirst is False
+    assert transformer.missing_values == "raise"
+
+
+_variables = [0, [0, 1, 9, 23], "var_str", ["var_str1", "var_str2"], [0, 1, "var3", 3]]
+
+
+@pytest.mark.parametrize("_variables", _variables)
+def test_variables_params(_variables):
+    assert DatetimeFeatures(variables=_variables).variables == _variables
+
+
+def test_features_to_extract_param():
+    assert DatetimeFeatures(features_to_extract=None).features_to_extract is None
+    assert DatetimeFeatures(features_to_extract=["year"]).features_to_extract == [
+        "year"
+    ]
+    assert DatetimeFeatures(features_to_extract="all").features_to_extract == "all"
+
+
+_not_a_df = [
+    "not_a_df",
+    [1, 2, 3, "some_data"],
+    pd.Series([-2, 1.5, 8.94], name="not_a_df"),
+]
+
+
+@pytest.mark.parametrize("_not_a_df", _not_a_df)
+def test_raises_error_when_fitting_not_a_df(_not_a_df):
+    transformer = DatetimeFeatures()
+    # trying to fit not a df
+    with pytest.raises(TypeError):
+        transformer.fit(_not_a_df)
+
+
+def test_raises_error_when_variables_not_datetime(df_datetime):
+    # asking for not datetime variable(s)
+    with pytest.raises(TypeError):
+        DatetimeFeatures(variables=["Age"]).fit(df_datetime)
+    with pytest.raises(TypeError):
+        DatetimeFeatures(variables=["Name", "Age", "date_obj1"]).fit(df_datetime)
+    # passing a df that contains no datetime variables
+    with pytest.raises(ValueError):
+        DatetimeFeatures().fit(df_datetime[["Name", "Age"]])
+
+
+def test_raises_error_when_df_has_nan():
+    # dataset containing nans
+    with pytest.raises(ValueError):
+        DatetimeFeatures().fit(dates_nan)
+
+
+def test_attributes_upon_fitting(df_datetime):
+    transformer = DatetimeFeatures()
+    transformer.fit(df_datetime)
+
+    assert transformer.variables_ == vars_dt
+    assert transformer.features_to_extract_ == FEATURES_DEFAULT
+    assert transformer.n_features_in_ == df_datetime.shape[1]
+
+    transformer = DatetimeFeatures(variables="date_obj1", features_to_extract="all")
+    transformer.fit(df_datetime)
+
+    assert transformer.variables_ == ["date_obj1"]
+    assert transformer.features_to_extract_ == FEATURES_SUPPORTED
+
+    transformer = DatetimeFeatures(
+        variables=["date_obj1", "time_obj"],
+        features_to_extract=["year", "quarter_end", "second"],
+    )
+    transformer.fit(df_datetime)
+
+    assert transformer.variables_ == ["date_obj1", "time_obj"]
+    assert transformer.features_to_extract_ == ["year", "quarter_end", "second"]
+
+
+@pytest.mark.parametrize("_not_a_df", _not_a_df)
+def test_raises_error_when_transforming_not_a_df(_not_a_df, df_datetime):
+    transformer = DatetimeFeatures()
+    transformer.fit(df_datetime)
+    # trying to transform not a df
+    with pytest.raises(TypeError):
+        transformer.transform(_not_a_df)
+
+
+def test_raises_error_when_transform_df_with_different_n_variables(df_datetime):
+    transformer = DatetimeFeatures()
+    transformer.fit(df_datetime)
+    # different number of columns than the df used to fit
+    with pytest.raises(ValueError):
+        transformer.transform(df_datetime[vars_dt])
+
+
+def test_raises_error_when_nan_in_transform_df(df_datetime):
+    transformer = DatetimeFeatures()
+    transformer.fit(df_datetime)
+    # dataset containing nans
+    with pytest.raises(ValueError):
+        DatetimeFeatures().transform(dates_nan)
+
+
+def test_raises_non_fitted_error(df_datetime):
+    # trying to transform before fitting
+    with pytest.raises(NotFittedError):
+        DatetimeFeatures().transform(df_datetime)
+
+
+def test_extract_datetime_features_with_default_options(
+    df_datetime, df_datetime_transformed
+):
+    transformer = DatetimeFeatures()
+    X = transformer.fit_transform(df_datetime)
+    pd.testing.assert_frame_equal(
+        X,
+        df_datetime_transformed[
+            vars_non_dt + [var + feat for var in vars_dt for feat in feat_names_default]
+        ],
+    )
+
+
+def test_extract_datetime_features_from_specified_variables(
+    df_datetime, df_datetime_transformed
+):
+    # single datetime variable
+    X = DatetimeFeatures(variables="date_obj1").fit_transform(df_datetime)
+    pd.testing.assert_frame_equal(
+        X,
+        df_datetime_transformed[
+            vars_non_dt
+            + ["datetime_range", "date_obj2", "time_obj"]
+            + ["date_obj1" + feat for feat in feat_names_default]
+        ],
+    )
+
+    # multiple datetime variables
+    X = DatetimeFeatures(variables=["datetime_range", "date_obj2"]).fit_transform(
+        df_datetime
+    )
+    pd.testing.assert_frame_equal(
+        X,
+        df_datetime_transformed[
+            vars_non_dt
+            + ["date_obj1", "time_obj"]
+            + [
+                var + feat
+                for var in ["datetime_range", "date_obj2"]
+                for feat in feat_names_default
+            ]
+        ],
+    )
+
+    # multiple datetime variables in different order than they appear in the df
+    X = DatetimeFeatures(variables=["date_obj2", "date_obj1"]).fit_transform(
+        df_datetime
+    )
+    pd.testing.assert_frame_equal(
+        X,
+        df_datetime_transformed[
+            vars_non_dt
+            + ["datetime_range", "time_obj"]
+            + [
+                var + feat
+                for var in ["date_obj2", "date_obj1"]
+                for feat in feat_names_default
+            ]
+        ],
+    )
+
+
+def test_extract_all_datetime_features(df_datetime, df_datetime_transformed):
+    X = DatetimeFeatures(features_to_extract="all").fit_transform(df_datetime)
+    pd.testing.assert_frame_equal(X, df_datetime_transformed.drop(vars_dt, axis=1))
+
+
+def test_extract_specified_datetime_features(df_datetime, df_datetime_transformed):
+    X = DatetimeFeatures(
+        features_to_extract=["semester", "week_of_the_year"]
+    ).fit_transform(df_datetime)
+    pd.testing.assert_frame_equal(
+        X,
+        df_datetime_transformed[
+            vars_non_dt
+            + [var + "_" + feat for var in vars_dt for feat in ["semester", "woty"]]
+        ],
+    )
+
+    # different order than they appear in the glossary
+    X = DatetimeFeatures(features_to_extract=["hour", "day_of_the_week"]).fit_transform(
+        df_datetime
+    )
+    pd.testing.assert_frame_equal(
+        X,
+        df_datetime_transformed[
+            vars_non_dt
+            + [var + "_" + feat for var in vars_dt for feat in ["hour", "dotw"]]
+        ],
+    )
+
+
+def test_extract_features_from_categorical_variable(
+    df_datetime, df_datetime_transformed
+):
+    cat_date = pd.DataFrame({"date_obj1": df_datetime["date_obj1"].astype("category")})
+    X = DatetimeFeatures(variables="date_obj1").fit_transform(cat_date)
+    pd.testing.assert_frame_equal(
+        X, df_datetime_transformed[["date_obj1" + feat for feat in feat_names_default]]
+    )
+
+
+def test_extract_features_from_different_timezones(
+    df_datetime, df_datetime_transformed
+):
+    time_zones = [4, -1, 9, -7]
+    tz_df = pd.DataFrame(
+        {"time_obj": df_datetime["time_obj"].add(["+4", "-1", "+9", "-7"])}
+    )
+    transformer = DatetimeFeatures(
+        variables="time_obj", features_to_extract=["hour"], utc=True
+    )
+    X = transformer.fit_transform(tz_df)
+
+    pd.testing.assert_frame_equal(
+        X,
+        df_datetime_transformed[["time_obj_hour"]].apply(
+            lambda x: x.subtract(time_zones)
+        ),
+    )
+    exp_err_msg = "ValueError: variable(s) time_obj " \
+        "could not be converted to datetime. Try setting utc=True"
+    with pytest.raises(ValueError) as errinfo:
+        assert DatetimeFeatures(
+            variables="time_obj", features_to_extract=["hour"], utc=False
+        ).fit_transform(tz_df)
+    assert str(errinfo.value) == exp_err_msg
+
+
+def test_extract_features_from_localized_tz_variables():
+    tz_df = pd.DataFrame(
+        {
+            "date_var": [
+                "2018-10-28 01:30:00",
+                "2018-10-28 02:00:00",
+                "2018-10-28 02:30:00",
+                "2018-10-28 02:00:00",
+                "2018-10-28 02:30:00",
+                "2018-10-28 03:00:00",
+                "2018-10-28 03:30:00",
+            ]
+        }
+    )
+
+    tz_df["date_var"] = pd.to_datetime(tz_df["date_var"]).dt.tz_localize(
+        tz="US/Eastern"
+    )
+
+    # when utc is None
+    transformer = DatetimeFeatures(features_to_extract=["hour"]).fit(tz_df)
+
+    # init params
+    assert transformer.variables is None
+    assert transformer.utc is None
+    assert transformer.features_to_extract == ["hour"]
+    # fit attr
+    assert transformer.variables_ == ["date_var"]
+    assert transformer.features_to_extract_ == ["hour"]
+    assert transformer.n_features_in_ == 1
+    # transform
+    X = transformer.transform(tz_df)
+    df_expected = pd.DataFrame({"date_var_hour": [1, 2, 2, 2, 2, 3, 3]})
+    pd.testing.assert_frame_equal(X, df_expected)
+
+    # when utc is True
+    transformer = DatetimeFeatures(features_to_extract=["hour"], utc=True).fit(tz_df)
+
+    # init params
+    assert transformer.variables is None
+    assert transformer.utc is True
+    assert transformer.features_to_extract == ["hour"]
+    # fit attr
+    assert transformer.variables_ == ["date_var"]
+    assert transformer.features_to_extract_ == ["hour"]
+    assert transformer.n_features_in_ == 1
+    # transform
+    X = transformer.transform(tz_df)
+    df_expected = pd.DataFrame({"date_var_hour": [5, 6, 6, 6, 6, 7, 7]})
+    pd.testing.assert_frame_equal(X, df_expected)
+
+
+def test_extract_features_without_dropping_original_variables(
+    df_datetime, df_datetime_transformed
+):
+    X = DatetimeFeatures(
+        variables=["datetime_range", "date_obj2"],
+        features_to_extract=["week_of_the_year", "quarter"],
+        drop_original=False,
+    ).fit_transform(df_datetime)
+
+    pd.testing.assert_frame_equal(
+        X,
+        pd.concat(
+            [df_datetime_transformed[column] for column in vars_non_dt]
+            + [df_datetime[var] for var in vars_dt]
+            + [
+                df_datetime_transformed[feat]
+                for feat in [
+                    var + "_" + feat
+                    for var in ["datetime_range", "date_obj2"]
+                    for feat in ["woty", "quarter"]
+                ]
+            ],
+            axis=1,
+        ),
+    )
+
+
+def test_extract_features_from_variables_containing_nans():
+    X = DatetimeFeatures(
+        features_to_extract=["year"], missing_values="ignore"
+    ).fit_transform(dates_nan)
+    pd.testing.assert_frame_equal(
+        X,
+        pd.DataFrame({"dates_na_year": [2010, np.nan, 1922, np.nan]}),
+    )
+
+
+def test_extract_features_with_different_datetime_parsing_options(df_datetime):
+    X = DatetimeFeatures(
+        features_to_extract=["day_of_the_month"], dayfirst=True
+    ).fit_transform(df_datetime[["date_obj2"]])
+    pd.testing.assert_frame_equal(
+        X,
+        pd.DataFrame({"date_obj2_dotm": [10, 31, 30, 17]}),
+    )
+
+    X = DatetimeFeatures(features_to_extract=["year"], yearfirst=True).fit_transform(
+        df_datetime[["date_obj2"]]
+    )
+    pd.testing.assert_frame_equal(
+        X,
+        pd.DataFrame({"date_obj2_year": [2010, 2009, 1995, 2004]}),
+    )
diff --git a/tests/test_variable_manipulation.py b/tests/test_variable_manipulation.py
index eafb2e8..635a9bb 100644
--- a/tests/test_variable_manipulation.py
+++ b/tests/test_variable_manipulation.py
@@ -1,9 +1,11 @@
+import pandas as pd
 import pytest
 
 from feature_engine.variable_manipulation import (
     _check_input_parameter_variables,
     _find_all_variables,
     _find_or_check_categorical_variables,
+    _find_or_check_datetime_variables,
     _find_or_check_numerical_variables,
 )
 
@@ -103,6 +105,83 @@ def test_find_or_check_categorical_variables(df_vartypes, df_numeric_columns):
     ]
 
 
+def test_find_or_check_datetime_variables(df_datetime):
+    var_dt = ["datetime_range"]
+    var_dt_str = "datetime_range"
+    vars_nondt = ["Age", "Name"]
+    vars_convertible_to_dt = ["datetime_range", "date_obj1", "date_obj2", "time_obj"]
+    var_convertible_to_dt = "date_obj1"
+    vars_mix = ["datetime_range", "Age"]
+    cat_date = pd.DataFrame(
+        {"date_obj1_cat": df_datetime["date_obj1"].astype("category")}
+    )
+    tz_time = pd.DataFrame(
+        {"time_objTZ": df_datetime["time_obj"].add(['+5', '+11', '-3', '-8'])}
+    )
+
+    # error when df has no datetime variables
+    with pytest.raises(ValueError):
+        assert _find_or_check_datetime_variables(
+            df_datetime.loc[:, vars_nondt], variables=None
+        )
+
+    # errors when vars entered by user are not datetime
+    with pytest.raises(TypeError):
+        assert _find_or_check_datetime_variables(df_datetime, variables="Age")
+    with pytest.raises(TypeError):
+        assert _find_or_check_datetime_variables(df_datetime, variables=vars_nondt)
+    with pytest.raises(TypeError):
+        assert _find_or_check_datetime_variables(df_datetime, variables=vars_mix)
+
+    # error when user enters empty list
+    with pytest.raises(ValueError):
+        assert _find_or_check_datetime_variables(df_datetime, variables=[])
+
+    # when variables=None
+    assert (
+        _find_or_check_datetime_variables(df_datetime, variables=None)
+        == vars_convertible_to_dt
+    )
+    assert (
+        _find_or_check_datetime_variables(
+            df_datetime[vars_convertible_to_dt].reindex(
+                columns=["date_obj1", "datetime_range", "date_obj2"]
+            ),
+            variables=None,
+        )
+        == ["date_obj1", "datetime_range", "date_obj2"]
+    )
+
+    # when variables are specified
+    assert _find_or_check_datetime_variables(df_datetime, var_dt_str) == [var_dt_str]
+    assert _find_or_check_datetime_variables(df_datetime, var_dt) == var_dt
+    assert _find_or_check_datetime_variables(
+        df_datetime, variables=var_convertible_to_dt
+    ) == [var_convertible_to_dt]
+    assert (
+        _find_or_check_datetime_variables(df_datetime, variables=vars_convertible_to_dt)
+        == vars_convertible_to_dt
+    )
+    assert (
+        _find_or_check_datetime_variables(
+            df_datetime.join(tz_time),
+            variables=None,
+        )
+        == vars_convertible_to_dt + ["time_objTZ"]
+    )
+    # vars cast as categorical
+    assert _find_or_check_datetime_variables(cat_date, variables="date_obj1_cat") == [
+        "date_obj1_cat"
+    ]
+    assert (
+        _find_or_check_datetime_variables(
+            df_datetime[vars_convertible_to_dt].join(cat_date),
+            variables=vars_convertible_to_dt + ["date_obj1_cat"],
+        )
+        == vars_convertible_to_dt + ["date_obj1_cat"]
+    )
+
+
 def test_find_all_variables(df_vartypes):
     all_vars = ["Name", "City", "Age", "Marks", "dob"]
     user_vars = ["Name", "City"]