Feature selection - #1126
Feature selection#1126
Conversation
Codecov Report
@@ Coverage Diff @@
## main #1126 +/- ##
==========================================
+ Coverage 98.35% 98.37% +0.01%
==========================================
Files 126 126
Lines 13308 13466 +158
==========================================
+ Hits 13089 13247 +158
Misses 219 219
Continue to review full report at Codecov.
|
|
A couple things still up for discussion:
|
|
I'm not sure featuretools should contain a feature selection module, as feature selection is an extremely complex matter and it also depends on which model you will use afterwards (e.g. maybe a Gradient Boosting Machine could take advantage of information and interactions that your simple correlation-based method couldn't capture). However, if you really want to add a selection module, maybe you could take a look at tsfresh's selection module: https://tsfresh.readthedocs.io/en/latest/text/feature_filtering.html
Obviously this is possible only if you have a target. Afaik it doesn't compute correlation between features since, as you already said, it can blow up quickly. |
5b272f7 to
b4888e8
Compare
rwedge
left a comment
There was a problem hiding this comment.
These functions should be added to the API reference. The Returns: block of the docstrings will probably need to be reformatted to display well in the docs. The docs expect a different form
Returns:
return type: return values description
see #1125 for discussion on the formatting
rwedge
left a comment
There was a problem hiding this comment.
the functions should be usable like this:
import featuretools as ft
...
ft.selection.remove_highly_null_features(feature_matrix, features)
accessible after the regular featuretools import, without needing to import the selection module separately.
63177e2 to
1b90b43
Compare
| continue | ||
|
|
||
| if abs(col1.corr(col2)) >= pct_corr_threshold: | ||
| dropped.add(f_name1) |
There was a problem hiding this comment.
We don't want to drop both features -- we want to keep one and drop the other.
There was a problem hiding this comment.
Changed to keep the less complex of the two features, which we'll treat as the feature that comes earlier in the list
| # Get all pairs of columns and calculate their correlation, dropping any columns that are highly correlated | ||
| dropped = set() | ||
| for f_name1, col1 in fm_to_check.iteritems(): | ||
| for f_name2, col2 in fm_to_check.iteritems(): |
There was a problem hiding this comment.
Checking if f_name1 is in dropped here would let us skip iterating over the other columns
There was a problem hiding this comment.
The new implementation iterates from most complex to least complex, so when we run into a pair of columns that are highly correlated, we can drop the more complex one and don't need to keep checking its correlation to the rest of features. This also keeps us from dropping any columns that we'll run into later on in the iteration.
85d27e9 to
d7c869a
Compare
| feature_matrix (:class:`pd.DataFrame`): DataFrame whose columns are feature names and rows are instances. | ||
| features (list[:class:`featuretools.FeatureBase`] or list[str], optional): List of features to select. | ||
| count_nan_as_value (bool): If True, missing values will be counted as their own unique value. | ||
| If set to True, a feature that has one unique value and all other |
There was a problem hiding this comment.
Shouldn't this be:
If set to False, a feature that has one unique value and all other data missing will be removed from the feature matrix.
Unless I'm misunderstanding, one unique value and the rest of the data missing should count as two unique values when NaNs are being counted as a unique value so the feature shouldn't get dropped.
There was a problem hiding this comment.
No yeah, you're understanding correctly. It should be False here--thanks for pointing it out!
| new_feature_names = set(new_matrix.columns) | ||
|
|
||
| if features is not None: | ||
| features = [f for f in features if f.get_name() in new_feature_names] |
There was a problem hiding this comment.
have you tested this with a multi-output feature? this might not handle the case where a feature has multiple columns associated with it
There was a problem hiding this comment.
I added a test_multi_output_selection and it's performing like I'd expect it to--not sure if the extra test is necessary though. Could maybe just view it as a multiple entity test as well
There was a problem hiding this comment.
test_multioutput_selection would need to pass in the feature list as well
There was a problem hiding this comment.
Oh right. Also, I had the wrong target entity, so I wasn't seeing the issue.
So if we have a feature <Feature: N_MOST_COMMON(second.quarter)> that outputs 3 features, with names of the format N_MOST_COMMON(second.all_nulls)[i] and we only keep one of them in the resulting feature matrix, how do we present that feature?
I feel like just keeping the feature in the feature list as is is misleading, because it would still show all the possible output features.
There was a problem hiding this comment.
We can divide up the feature into FeatureOutputSlice sub-features if some but not all feature columns should be kept
There was a problem hiding this comment.
Using FeatureOutputSlice to do this
|
|
||
|
|
||
| def remove_highly_correlated_features(feature_matrix, features=None, pct_corr_threshold=0.95, features_to_check=None, features_to_keep=None): | ||
| """Removes columns in feature matrix that are highly correlated with another column. |
There was a problem hiding this comment.
The assumption that the right-most features are more complex is based around the user not re-ordering the feature list returned by DFS. We might want to make note of that here.
There was a problem hiding this comment.
Ok, adding this section
Note: We make the assumption that, for a pair of features, the feature that is further right in the feature matrix produced by ``dfs`` is the more complex one. The assumption does not hold if the order of columns in the feature matrix has changed from what ``dfs`` produces.
| sliced_features.append(f) | ||
| else: | ||
| sliced_features.extend([f[i] for i in range(f.number_output_features)]) | ||
| new_features = [f for f in sliced_features if f.get_name() in new_feature_names] |
There was a problem hiding this comment.
if new_feature_names is a set of column names, wouldn't f.get_name() fail for a multi-output feature?
da8b9aa to
6bbf449
Compare
Feature Selection
In order to provide some insight into the quality of features produced by
dfs, we're adding in functions that can notify users which features might not be of much importance to a machine learning model. The feature selection tools we are adding at this stage are as follows:find_highly_null_features- which features have many null valuesfind_single_value_features- which features don't have much variance in their valuesfind_highly_correlated_features- which features are highly correlated to one anotherWe're leaving the potential for these to be integrated into
dfsor to be built up to have a larger and more strict API like that of EvalML's data checks. For now, though, these will just be standalone functions that can be called on a feature matrix produced bydfs.