Skip to content

extends functionality of DatetimeFeatures to extract features from the dataframe index - #405

Merged
solegalli merged 20 commits into
feature-engine:mainfrom
dodoarg:issue397
Apr 6, 2022
Merged

extends functionality of DatetimeFeatures to extract features from the dataframe index#405
solegalli merged 20 commits into
feature-engine:mainfrom
dodoarg:issue397

Conversation

@dodoarg

@dodoarg dodoarg commented Mar 31, 2022

Copy link
Copy Markdown
Contributor

Here's a draft to implement the functionality. I might have forgotten about some test cases and there might be better ways to refactor the logic overall

I tried to keep the logic as consistent as possible with the rest of the cases (e.g. find_or_check_dt_variables dumps "index" into ["index"] as it would do with any other string etc) although the fact that we need to access .index rather than a (set of) column(s) introduces some "boilerplate" code

I did think of having _check_contains_na check for nans in the index but I found out some check estimator tests would fail then, so I just added that logic under an if statement in the datetime transformer (more boilerplate, ugh)

Also, this is probably a trivial note, but as things stand there's no coverage for the case where an actual variable (not the index) is named "index". I suppose we might just leave that as it is as it sounds like a very unrealistic name for a column anyway

Let me know how we can improve it

@solegalli solegalli left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @dodoarg

Thank you so much for the quick turnaround! Very helpful and very kind of you :)

I was thinking in making fewer changes for this addition. I would only modify the transformer, and not the variables manipulation function. I was wondering if that would be possible?

Would you mind having a look at my comments and see if they make sense?

Thank you!

Comment thread feature_engine/datetime/datetime.py Outdated
# check if datetime variables contains na
if self.missing_values == "raise":
_check_contains_na(X, self.variables_)
if self.variables_ == ["index"]:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my idea was that the argument variables in the init, can take 3 values:

  • None: defaults to finding all dt variables (already implemented)
  • Variable list: checks if variables entered by user are or can be datetime (already implemented)
  • "index", taken as a string, which will extract datetime features from the index (new in this PR)

Then, here I would tailor the message to say, there were nan in the index, for example.

Comment thread feature_engine/datetime/datetime.py Outdated
# convert datetime variables
datetime_df = pd.concat(
[
if self.variables_ == ["index"]:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to extract features from the index, do we absolutely need to make it a column?

Can we not do df.index.month? or something like this? I saw it the other day on a stackoverflow thread.

But maybe it does not fit with how we structured the class?

Comment thread feature_engine/datetime/datetime.py Outdated

# create new features
for var in self.variables_:
if self.variables_ == ["index"]:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we extract features from the index, I am wondering whether we should just name the features as the datetime they represent? we are not creating features from multiple variables, so in principle, there would be no need to add the variable name at the front to distinguish them from anything else?

Comment thread feature_engine/variable_manipulation.py Outdated
raise ValueError("No datetime variables found in this dataframe.")

elif isinstance(variables, (str, int)):
if variables == "index":

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would not modify this function. I would handle the logic from the datetime class, because this is probably one of the only classes that will work with the index in this way.

with pytest.raises(ValueError):
DatetimeFeatures().transform(dates_nan)
transformer.transform(dates_nan)
transformer = DatetimeFeatures(variables="index")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, here is when the problem you raised would come up. If the user has a variable called "index", but we are now using this string to indicate the df index, then they would not be able to extract features from this variable.

I think this is very unlikely, but the user still could pass ["index"] and then, they could extract features from the column called index. And if they pass "index" as a string, then, the class would use the index of the df. And we could make this clear in the docstrings.

I think, as a first release, this could be enough, and then we hear user feedback. Unless you think otherwise?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea no I agree. If anything if someone comes up with a dataset where a column not only is named "index" but contains datetime info (??) it's probably their fault in terms of bad naming practices and even lack of common sense haha

@dodoarg

dodoarg commented Apr 1, 2022

Copy link
Copy Markdown
Contributor Author

I think I've tackled pretty much all issues you raised @solegalli ?

  • I removed the "index_" prefix for feature columns extracted from the index

  • The part where the DatetimeIndex is dumped into a pd.Series was necessary because the way you access to datetime functions on an index object looks like df.index.year i.e. it "bypasses" the call to the dt module. Since our logic calls a set of lambda functions (FEATURES_FUNCTIONS) that all look like x.dt.year I thought it would add less boilerplate code to put the index data into a Series so as to access those functions the same way rather than rewrite all them without the dt part just for this specific case

@solegalli solegalli left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @dodoarg

Thank you so much for the quick fixes on the code. This is really helpful.

I've got a couple of minor cosmetic requests, and then a question about pd.to_datetime() that might be worth exploring.

Would you be able to have a look?

Thank you!

FYI: I answered my own question below.

Comment thread feature_engine/datetime/datetime.py Outdated
not is_numeric(X.index) and _is_categorical_and_is_datetime(X.index)
)
):
raise TypeError("Index is not datetime.")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we re-phrase the error as

"The dataframe index is not datetime."

Comment thread feature_engine/datetime/datetime.py Outdated
if X.index.isnull().any():
raise ValueError(
"Index contain NaN. Check and "
"remove those before using this transformer."

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we re-phrase this error message?

"The dataframe index contains missing data. Check and remove those before using this transformer or set missing_values to False."

Comment thread feature_engine/datetime/datetime.py Outdated
"remove those before using this transformer."
)

self.variables_: List[Union[str, int]] = ["index"]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are using the index to extract features, I would leave variables_ to None.

I think Feature-engine users would expect to find the variables that are considered for the transformation in this attribute. And in this case, no variable is. Because we are using the index. Also, if we set this to ["index"], it would clash with the unlikely situation in which the user has a variable called "index".

Comment thread feature_engine/datetime/datetime.py Outdated
_check_input_matches_training_df(X, self.n_features_in_)

# special case index
if self.variables_ == ["index"]:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could use either self.variables_ is None or self.variables =="index" without the underscore.

Comment thread feature_engine/datetime/datetime.py Outdated
if X.index.isnull().any():
raise ValueError(
"Index contain NaN. Check and "
"remove those before using this transformer."

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we rephrase as in fit()?

Also wondering, we are repeating code in fit and transform, should we capture it in a private method in this class?

       if self.missing_values == "raise":
            self._check_contains_na()

Comment thread feature_engine/datetime/datetime.py Outdated
@dodoarg

dodoarg commented Apr 2, 2022

Copy link
Copy Markdown
Contributor Author

Should be better now. Notice that when setting self.variables_ as None in the index case it occurred to me that I had to adapt get_feature_names_out to the index case

@solegalli

Copy link
Copy Markdown
Collaborator

Thank you so much @dodoarg !!!! This will do!

Merging on Monday.

Have a nice weekend.

Comment thread feature_engine/datetime/datetime.py Outdated

for feat in self.features_to_extract_:
X[FEATURES_SUFFIXES[feat][1:]] = FEATURES_FUNCTIONS[feat](
pd.Series(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just noticed that with this logic, we would be converting the index to datetime for every feature.

Could we not convert it one, capture the series outside the loop, and then enter the loop using the already prepared series?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. My bad.

Comment thread feature_engine/datetime/datetime.py Outdated
)
)

return X

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we have just 1 return at the end of the method and not break the logic in 2?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you'd rather have what follows under an else statement yea we can do that

Comment thread feature_engine/datetime/datetime.py Outdated
_check_contains_na(X, self.variables_)

# reorder variables to match train set
X = X[self.feature_names_in_]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line should also apply for the index features. Is the final of the transform checks, before starting any transform logic. It ensures the df passed to transform has the columns in the same order than the one passed in fit.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. It probably didn't matter as far as the test go since we're not ever changing the existing columns but the index only when variables="index"

Comment thread feature_engine/datetime/datetime.py Outdated
Alternatively, only the names for the datetime features derived from
input_features will be returned.
If the transformer was fitted on the dataframe index, you may only pass
`index` as input_features, or leave it as `None`.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we reprhase these 2 sentences as "If the features were derived from the dataframe index, pass 'index' to obtain the datetime features or None for all output features."

@solegalli solegalli left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @dodoarg

I took a second look at the PR just now, and I am wondering if we could make the loop more efficient by taking the pandas series out of the loop?

What do you think?

@dodoarg

dodoarg commented Apr 4, 2022

Copy link
Copy Markdown
Contributor Author

Should have made quick work of that in my break

@solegalli solegalli changed the title Issue397 extends functionality of DatetimeFeatures to extract features from the dataframe index Apr 4, 2022
@solegalli solegalli linked an issue Apr 4, 2022 that may be closed by this pull request
restructured get_feature_names_out for readability
updated user guide
@dodoarg

dodoarg commented Apr 6, 2022

Copy link
Copy Markdown
Contributor Author

is this ready to merge @solegalli ?

@solegalli
solegalli merged commit fe50aab into feature-engine:main Apr 6, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

extend DatetimeFeatures to extract features from index as well

2 participants