extends functionality of DatetimeFeatures to extract features from the dataframe index - #405
Conversation
…ests to fail This reverts commit 28b750d.
solegalli
left a comment
There was a problem hiding this comment.
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!
| # check if datetime variables contains na | ||
| if self.missing_values == "raise": | ||
| _check_contains_na(X, self.variables_) | ||
| if self.variables_ == ["index"]: |
There was a problem hiding this comment.
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.
| # convert datetime variables | ||
| datetime_df = pd.concat( | ||
| [ | ||
| if self.variables_ == ["index"]: |
There was a problem hiding this comment.
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?
|
|
||
| # create new features | ||
| for var in self.variables_: | ||
| if self.variables_ == ["index"]: |
There was a problem hiding this comment.
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?
| raise ValueError("No datetime variables found in this dataframe.") | ||
|
|
||
| elif isinstance(variables, (str, int)): | ||
| if variables == "index": |
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
|
I think I've tackled pretty much all issues you raised @solegalli ?
|
There was a problem hiding this comment.
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.
| not is_numeric(X.index) and _is_categorical_and_is_datetime(X.index) | ||
| ) | ||
| ): | ||
| raise TypeError("Index is not datetime.") |
There was a problem hiding this comment.
Could we re-phrase the error as
"The dataframe index is not datetime."
| if X.index.isnull().any(): | ||
| raise ValueError( | ||
| "Index contain NaN. Check and " | ||
| "remove those before using this transformer." |
There was a problem hiding this comment.
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."
| "remove those before using this transformer." | ||
| ) | ||
|
|
||
| self.variables_: List[Union[str, int]] = ["index"] |
There was a problem hiding this comment.
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".
| _check_input_matches_training_df(X, self.n_features_in_) | ||
|
|
||
| # special case index | ||
| if self.variables_ == ["index"]: |
There was a problem hiding this comment.
We could use either self.variables_ is None or self.variables =="index" without the underscore.
| if X.index.isnull().any(): | ||
| raise ValueError( | ||
| "Index contain NaN. Check and " | ||
| "remove those before using this transformer." |
There was a problem hiding this comment.
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()
…eature_names out works as intended in that case
|
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 |
|
Thank you so much @dodoarg !!!! This will do! Merging on Monday. Have a nice weekend. |
|
|
||
| for feat in self.features_to_extract_: | ||
| X[FEATURES_SUFFIXES[feat][1:]] = FEATURES_FUNCTIONS[feat]( | ||
| pd.Series( |
There was a problem hiding this comment.
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?
| ) | ||
| ) | ||
|
|
||
| return X |
There was a problem hiding this comment.
could we have just 1 return at the end of the method and not break the logic in 2?
There was a problem hiding this comment.
If you'd rather have what follows under an else statement yea we can do that
| _check_contains_na(X, self.variables_) | ||
|
|
||
| # reorder variables to match train set | ||
| X = X[self.feature_names_in_] |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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"
| 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`. |
There was a problem hiding this comment.
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."
|
Should have made quick work of that in my break |
restructured get_feature_names_out for readability updated user guide
|
is this ready to merge @solegalli ? |
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