gitlabds 2.4.0__tar.gz → 2.4.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {gitlabds-2.4.0 → gitlabds-2.4.1}/PKG-INFO +18 -5
- {gitlabds-2.4.0 → gitlabds-2.4.1}/README.md +17 -4
- {gitlabds-2.4.0 → gitlabds-2.4.1}/gitlabds/feature_reduction.py +185 -89
- {gitlabds-2.4.0 → gitlabds-2.4.1}/gitlabds.egg-info/PKG-INFO +18 -5
- {gitlabds-2.4.0 → gitlabds-2.4.1}/gitlabds.egg-info/SOURCES.txt +1 -0
- gitlabds-2.4.1/tests/test_feature_reduction.py +232 -0
- {gitlabds-2.4.0 → gitlabds-2.4.1}/LICENSE +0 -0
- {gitlabds-2.4.0 → gitlabds-2.4.1}/gitlabds/__init__.py +0 -0
- {gitlabds-2.4.0 → gitlabds-2.4.1}/gitlabds/baselines.py +0 -0
- {gitlabds-2.4.0 → gitlabds-2.4.1}/gitlabds/config_generator.py +0 -0
- {gitlabds-2.4.0 → gitlabds-2.4.1}/gitlabds/dummy.py +0 -0
- {gitlabds-2.4.0 → gitlabds-2.4.1}/gitlabds/feature_descriptions.py +0 -0
- {gitlabds-2.4.0 → gitlabds-2.4.1}/gitlabds/insights.py +0 -0
- {gitlabds-2.4.0 → gitlabds-2.4.1}/gitlabds/memory_optimization.py +0 -0
- {gitlabds-2.4.0 → gitlabds-2.4.1}/gitlabds/missing.py +0 -0
- {gitlabds-2.4.0 → gitlabds-2.4.1}/gitlabds/model_evaluator.py +0 -0
- {gitlabds-2.4.0 → gitlabds-2.4.1}/gitlabds/monitoring_metrics.py +0 -0
- {gitlabds-2.4.0 → gitlabds-2.4.1}/gitlabds/outliers.py +0 -0
- {gitlabds-2.4.0 → gitlabds-2.4.1}/gitlabds/record_benchmarking.py +0 -0
- {gitlabds-2.4.0 → gitlabds-2.4.1}/gitlabds/serving_features.py +0 -0
- {gitlabds-2.4.0 → gitlabds-2.4.1}/gitlabds/snowflake_connectors.py +0 -0
- {gitlabds-2.4.0 → gitlabds-2.4.1}/gitlabds/split_data.py +0 -0
- {gitlabds-2.4.0 → gitlabds-2.4.1}/gitlabds/trends.py +0 -0
- {gitlabds-2.4.0 → gitlabds-2.4.1}/gitlabds.egg-info/dependency_links.txt +0 -0
- {gitlabds-2.4.0 → gitlabds-2.4.1}/gitlabds.egg-info/requires.txt +0 -0
- {gitlabds-2.4.0 → gitlabds-2.4.1}/gitlabds.egg-info/top_level.txt +0 -0
- {gitlabds-2.4.0 → gitlabds-2.4.1}/pyproject.toml +0 -0
- {gitlabds-2.4.0 → gitlabds-2.4.1}/setup.cfg +0 -0
- {gitlabds-2.4.0 → gitlabds-2.4.1}/setup.py +0 -0
- {gitlabds-2.4.0 → gitlabds-2.4.1}/tests/test_snowflake_connectors.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gitlabds
|
|
3
|
-
Version: 2.4.
|
|
3
|
+
Version: 2.4.1
|
|
4
4
|
Summary: GitLab Data Science Tools
|
|
5
5
|
Author-email: Kevin Dietz <kdietz@gitlab.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -376,9 +376,9 @@ test_df_transformed = gitlabds.apply_dummy(
|
|
|
376
376
|
<details><summary> Remove Low Variation </summary>
|
|
377
377
|
|
|
378
378
|
#### Description
|
|
379
|
-
Remove columns from a dataset that do not meet the variation threshold. That is, columns will be dropped that contain a high percentage of one value.
|
|
379
|
+
Remove columns from a dataset that do not meet the variation threshold. That is, columns will be dropped that contain a high percentage of one value. Columns that are entirely null are always dropped.
|
|
380
380
|
|
|
381
|
-
`gitlabds.remove_low_variation(df=None, dv=None, columns='all', threshold=.98, verbose=True):`
|
|
381
|
+
`gitlabds.remove_low_variation(df=None, dv=None, columns='all', threshold=.98, verbose=True, dropna=False):`
|
|
382
382
|
|
|
383
383
|
#### Parameters:
|
|
384
384
|
- _**df**_ : your pandas dataframe
|
|
@@ -386,6 +386,7 @@ Remove columns from a dataset that do not meet the variation threshold. That is,
|
|
|
386
386
|
- **_columns_** : Will examine at all columns by default. To limit to just a subset of columns, pass a list of column names.
|
|
387
387
|
- **_threshold_**: The maximum percentage one value in a column can represent. columns that exceed this threshold will be dropped. For example, the default value of `0.98` will drop any column where one value is present in more than 98% of rows.
|
|
388
388
|
- **_verbose_** : Set to `True` to print outputs of columns being dropped. Set to `False` to suppress.
|
|
389
|
+
- **_dropna_** : Set to `False` (default) to count missing values as their own value. Set to `True` to measure variation over non-null values only, so a column that is one value plus NaN is dropped. Use `True` when missing values are left in place for a model that handles them natively (e.g. XGBoost, LightGBM).
|
|
389
390
|
|
|
390
391
|
#### Returns
|
|
391
392
|
- DataFrame with low variation columns dropped.
|
|
@@ -399,6 +400,10 @@ new_df = gitlabds.remove_low_variation(df=my_df, dv='my_outcome', columns='all',
|
|
|
399
400
|
# Drop any of the selected columns where one value is present in more than 99% of rows.
|
|
400
401
|
new_df = gitlabds.remove_low_variation(df=my_df, dv=None, columns=['colA', 'colB', 'colC'], threshold=.99)
|
|
401
402
|
```
|
|
403
|
+
```python
|
|
404
|
+
# Ignore missing values: drop columns where one value makes up more than 98% of the non-null rows.
|
|
405
|
+
new_df = gitlabds.remove_low_variation(df=my_df, dv='my_outcome', dropna=True)
|
|
406
|
+
```
|
|
402
407
|
</details>
|
|
403
408
|
|
|
404
409
|
<details><summary> Correlation Reduction </summary>
|
|
@@ -406,7 +411,7 @@ new_df = gitlabds.remove_low_variation(df=my_df, dv=None, columns=['colA', 'colB
|
|
|
406
411
|
#### Description
|
|
407
412
|
Reduce the number of columns on a dataframe by dropping columns that are highly correlated with other columns. Note: only one of the two highly correlated columns will be dropped.
|
|
408
413
|
|
|
409
|
-
`gitlabds.correlation_reduction(df=None, dv=None, threshold=0.9, method="pearson", verbose=True):`
|
|
414
|
+
`gitlabds.correlation_reduction(df=None, dv=None, threshold=0.9, method="pearson", verbose=True, dropna=True, min_periods=1):`
|
|
410
415
|
|
|
411
416
|
#### Parameters:
|
|
412
417
|
- _**df**_ : your pandas dataframe
|
|
@@ -414,6 +419,8 @@ Reduce the number of columns on a dataframe by dropping columns that are highly
|
|
|
414
419
|
- **_threshold_**: The threshold above which columns will be dropped. If two variables exceed this threshold, one will be dropped from the dataframe. For example, the default value of `0.90` will identify columns that have correlations greater than 90% to each other and drop one of those columns.
|
|
415
420
|
- **_method_**: The correlation method to use. Options are "pearson" (linear relationships), "spearman" (monotonic relationships), or "mutual_info" (any statistical dependency).
|
|
416
421
|
- **_verbose_** : Set to `True` to print outputs of columns being dropped. Set to `False` to suppress.
|
|
422
|
+
- **_dropna_** : Set to `True` (default) to compare each pair of columns (and each column with the outcome) using only rows where both are non-null. Pearson and Spearman always work this way; for "mutual_info", `False` raises an error if any missing values are present.
|
|
423
|
+
- **_min_periods_** : Minimum number of rows where both columns of a pair are non-null for the pair to be evaluated. Pairs below it are never treated as correlated. Guards against spurious high correlations between sparsely populated columns. "mutual_info" always requires at least 4 such rows.
|
|
417
424
|
|
|
418
425
|
#### Returns
|
|
419
426
|
- DataFrame with redundant correlated columns dropped.
|
|
@@ -434,7 +441,7 @@ new_df = gitlabds.correlation_reduction(df=my_df, dv=None, threshold=0.90, metho
|
|
|
434
441
|
#### Description
|
|
435
442
|
Remove columns that are highly correlated with the outcome (target) column.
|
|
436
443
|
|
|
437
|
-
`gitlabds.remove_outcome_proxies(df, dv, threshold=.8, method="pearson", verbose=True):`
|
|
444
|
+
`gitlabds.remove_outcome_proxies(df, dv, threshold=.8, method="pearson", verbose=True, dropna=True, min_periods=1):`
|
|
438
445
|
|
|
439
446
|
#### Parameters:
|
|
440
447
|
- _**df**_ : your pandas dataframe
|
|
@@ -442,6 +449,8 @@ Remove columns that are highly correlated with the outcome (target) column.
|
|
|
442
449
|
- _**threshold**_ : The correlation value to the outcome above which columns will be dropped. For example, the default value of `0.80` will identify and drop columns that have correlations greater than 80% to the outcome.
|
|
443
450
|
- **_method_**: The correlation method to use. Options are "pearson" (linear relationships), "spearman" (monotonic relationships), or "mutual_info" (any statistical dependency).
|
|
444
451
|
- **_verbose_** : Set to `True` to print outputs of columns being dropped. Set to `False` to suppress.
|
|
452
|
+
- **_dropna_** : Set to `True` (default) to compare each column to the outcome using only rows where both are non-null. Pearson and Spearman always work this way; for "mutual_info", `False` raises an error if any missing values are present.
|
|
453
|
+
- **_min_periods_** : Minimum number of rows where both the column and the outcome are non-null for the column to be evaluated. Columns below it are never dropped. Guards against spurious high correlations from sparsely populated columns. "mutual_info" always requires at least 4 such rows.
|
|
445
454
|
|
|
446
455
|
#### Returns
|
|
447
456
|
- DataFrame with outcome proxy columns dropped.
|
|
@@ -455,6 +464,10 @@ new_df = gitlabds.remove_outcome_proxies(df=my_df, dv='my_outcome', threshold=.7
|
|
|
455
464
|
# Drop columns with correlations to the outcome greater than 80% using Spearman correlation
|
|
456
465
|
new_df = gitlabds.remove_outcome_proxies(df=my_df, dv='my_outcome', threshold=.8, method="spearman")
|
|
457
466
|
```
|
|
467
|
+
```python
|
|
468
|
+
# Data with missing values: only evaluate columns with at least 100 non-null rows alongside the outcome
|
|
469
|
+
new_df = gitlabds.remove_outcome_proxies(df=my_df, dv='my_outcome', method="spearman", min_periods=100)
|
|
470
|
+
```
|
|
458
471
|
</details>
|
|
459
472
|
|
|
460
473
|
<details><summary> Drop Categorical </summary>
|
|
@@ -340,9 +340,9 @@ test_df_transformed = gitlabds.apply_dummy(
|
|
|
340
340
|
<details><summary> Remove Low Variation </summary>
|
|
341
341
|
|
|
342
342
|
#### Description
|
|
343
|
-
Remove columns from a dataset that do not meet the variation threshold. That is, columns will be dropped that contain a high percentage of one value.
|
|
343
|
+
Remove columns from a dataset that do not meet the variation threshold. That is, columns will be dropped that contain a high percentage of one value. Columns that are entirely null are always dropped.
|
|
344
344
|
|
|
345
|
-
`gitlabds.remove_low_variation(df=None, dv=None, columns='all', threshold=.98, verbose=True):`
|
|
345
|
+
`gitlabds.remove_low_variation(df=None, dv=None, columns='all', threshold=.98, verbose=True, dropna=False):`
|
|
346
346
|
|
|
347
347
|
#### Parameters:
|
|
348
348
|
- _**df**_ : your pandas dataframe
|
|
@@ -350,6 +350,7 @@ Remove columns from a dataset that do not meet the variation threshold. That is,
|
|
|
350
350
|
- **_columns_** : Will examine at all columns by default. To limit to just a subset of columns, pass a list of column names.
|
|
351
351
|
- **_threshold_**: The maximum percentage one value in a column can represent. columns that exceed this threshold will be dropped. For example, the default value of `0.98` will drop any column where one value is present in more than 98% of rows.
|
|
352
352
|
- **_verbose_** : Set to `True` to print outputs of columns being dropped. Set to `False` to suppress.
|
|
353
|
+
- **_dropna_** : Set to `False` (default) to count missing values as their own value. Set to `True` to measure variation over non-null values only, so a column that is one value plus NaN is dropped. Use `True` when missing values are left in place for a model that handles them natively (e.g. XGBoost, LightGBM).
|
|
353
354
|
|
|
354
355
|
#### Returns
|
|
355
356
|
- DataFrame with low variation columns dropped.
|
|
@@ -363,6 +364,10 @@ new_df = gitlabds.remove_low_variation(df=my_df, dv='my_outcome', columns='all',
|
|
|
363
364
|
# Drop any of the selected columns where one value is present in more than 99% of rows.
|
|
364
365
|
new_df = gitlabds.remove_low_variation(df=my_df, dv=None, columns=['colA', 'colB', 'colC'], threshold=.99)
|
|
365
366
|
```
|
|
367
|
+
```python
|
|
368
|
+
# Ignore missing values: drop columns where one value makes up more than 98% of the non-null rows.
|
|
369
|
+
new_df = gitlabds.remove_low_variation(df=my_df, dv='my_outcome', dropna=True)
|
|
370
|
+
```
|
|
366
371
|
</details>
|
|
367
372
|
|
|
368
373
|
<details><summary> Correlation Reduction </summary>
|
|
@@ -370,7 +375,7 @@ new_df = gitlabds.remove_low_variation(df=my_df, dv=None, columns=['colA', 'colB
|
|
|
370
375
|
#### Description
|
|
371
376
|
Reduce the number of columns on a dataframe by dropping columns that are highly correlated with other columns. Note: only one of the two highly correlated columns will be dropped.
|
|
372
377
|
|
|
373
|
-
`gitlabds.correlation_reduction(df=None, dv=None, threshold=0.9, method="pearson", verbose=True):`
|
|
378
|
+
`gitlabds.correlation_reduction(df=None, dv=None, threshold=0.9, method="pearson", verbose=True, dropna=True, min_periods=1):`
|
|
374
379
|
|
|
375
380
|
#### Parameters:
|
|
376
381
|
- _**df**_ : your pandas dataframe
|
|
@@ -378,6 +383,8 @@ Reduce the number of columns on a dataframe by dropping columns that are highly
|
|
|
378
383
|
- **_threshold_**: The threshold above which columns will be dropped. If two variables exceed this threshold, one will be dropped from the dataframe. For example, the default value of `0.90` will identify columns that have correlations greater than 90% to each other and drop one of those columns.
|
|
379
384
|
- **_method_**: The correlation method to use. Options are "pearson" (linear relationships), "spearman" (monotonic relationships), or "mutual_info" (any statistical dependency).
|
|
380
385
|
- **_verbose_** : Set to `True` to print outputs of columns being dropped. Set to `False` to suppress.
|
|
386
|
+
- **_dropna_** : Set to `True` (default) to compare each pair of columns (and each column with the outcome) using only rows where both are non-null. Pearson and Spearman always work this way; for "mutual_info", `False` raises an error if any missing values are present.
|
|
387
|
+
- **_min_periods_** : Minimum number of rows where both columns of a pair are non-null for the pair to be evaluated. Pairs below it are never treated as correlated. Guards against spurious high correlations between sparsely populated columns. "mutual_info" always requires at least 4 such rows.
|
|
381
388
|
|
|
382
389
|
#### Returns
|
|
383
390
|
- DataFrame with redundant correlated columns dropped.
|
|
@@ -398,7 +405,7 @@ new_df = gitlabds.correlation_reduction(df=my_df, dv=None, threshold=0.90, metho
|
|
|
398
405
|
#### Description
|
|
399
406
|
Remove columns that are highly correlated with the outcome (target) column.
|
|
400
407
|
|
|
401
|
-
`gitlabds.remove_outcome_proxies(df, dv, threshold=.8, method="pearson", verbose=True):`
|
|
408
|
+
`gitlabds.remove_outcome_proxies(df, dv, threshold=.8, method="pearson", verbose=True, dropna=True, min_periods=1):`
|
|
402
409
|
|
|
403
410
|
#### Parameters:
|
|
404
411
|
- _**df**_ : your pandas dataframe
|
|
@@ -406,6 +413,8 @@ Remove columns that are highly correlated with the outcome (target) column.
|
|
|
406
413
|
- _**threshold**_ : The correlation value to the outcome above which columns will be dropped. For example, the default value of `0.80` will identify and drop columns that have correlations greater than 80% to the outcome.
|
|
407
414
|
- **_method_**: The correlation method to use. Options are "pearson" (linear relationships), "spearman" (monotonic relationships), or "mutual_info" (any statistical dependency).
|
|
408
415
|
- **_verbose_** : Set to `True` to print outputs of columns being dropped. Set to `False` to suppress.
|
|
416
|
+
- **_dropna_** : Set to `True` (default) to compare each column to the outcome using only rows where both are non-null. Pearson and Spearman always work this way; for "mutual_info", `False` raises an error if any missing values are present.
|
|
417
|
+
- **_min_periods_** : Minimum number of rows where both the column and the outcome are non-null for the column to be evaluated. Columns below it are never dropped. Guards against spurious high correlations from sparsely populated columns. "mutual_info" always requires at least 4 such rows.
|
|
409
418
|
|
|
410
419
|
#### Returns
|
|
411
420
|
- DataFrame with outcome proxy columns dropped.
|
|
@@ -419,6 +428,10 @@ new_df = gitlabds.remove_outcome_proxies(df=my_df, dv='my_outcome', threshold=.7
|
|
|
419
428
|
# Drop columns with correlations to the outcome greater than 80% using Spearman correlation
|
|
420
429
|
new_df = gitlabds.remove_outcome_proxies(df=my_df, dv='my_outcome', threshold=.8, method="spearman")
|
|
421
430
|
```
|
|
431
|
+
```python
|
|
432
|
+
# Data with missing values: only evaluate columns with at least 100 non-null rows alongside the outcome
|
|
433
|
+
new_df = gitlabds.remove_outcome_proxies(df=my_df, dv='my_outcome', method="spearman", min_periods=100)
|
|
434
|
+
```
|
|
422
435
|
</details>
|
|
423
436
|
|
|
424
437
|
<details><summary> Drop Categorical </summary>
|
|
@@ -57,13 +57,15 @@ def remove_low_variation(
|
|
|
57
57
|
dv: Optional[str] = None,
|
|
58
58
|
columns: Union[str, List[str]] = "all",
|
|
59
59
|
threshold: float = 0.98,
|
|
60
|
-
verbose: bool = True
|
|
60
|
+
verbose: bool = True,
|
|
61
|
+
dropna: bool = False
|
|
61
62
|
) -> pd.DataFrame:
|
|
62
63
|
"""
|
|
63
64
|
Remove columns from a dataset that do not meet the variation threshold.
|
|
64
65
|
|
|
65
66
|
Columns will be dropped if they contain a high percentage of one value, as they
|
|
66
|
-
provide minimal information for modeling.
|
|
67
|
+
provide minimal information for modeling. Columns that are entirely null are
|
|
68
|
+
always dropped.
|
|
67
69
|
|
|
68
70
|
Parameters
|
|
69
71
|
----------
|
|
@@ -81,6 +83,13 @@ def remove_low_variation(
|
|
|
81
83
|
will drop any column where one value is present in more than 98% of rows.
|
|
82
84
|
verbose : bool, default=True
|
|
83
85
|
Set to True to print outputs of columns being dropped. Set to False to suppress.
|
|
86
|
+
dropna : bool, default=False
|
|
87
|
+
If False, missing values count as their own value, so a column that is one value
|
|
88
|
+
plus NaN is kept as long as neither exceeds the threshold. If True, the share of
|
|
89
|
+
the most common value is computed over non-null rows only, so a column with a
|
|
90
|
+
single distinct non-null value is dropped regardless of how many NaNs it has.
|
|
91
|
+
Use True when missing values will be left in place for a model that handles them
|
|
92
|
+
natively (e.g. XGBoost, LightGBM).
|
|
84
93
|
|
|
85
94
|
Returns
|
|
86
95
|
-------
|
|
@@ -90,7 +99,7 @@ def remove_low_variation(
|
|
|
90
99
|
Examples
|
|
91
100
|
--------
|
|
92
101
|
>>> new_df = remove_low_variation(df=my_df, dv='target', threshold=0.95)
|
|
93
|
-
>>>
|
|
102
|
+
>>> new_df = remove_low_variation(df=my_df, dv='target', dropna=True)
|
|
94
103
|
"""
|
|
95
104
|
# Enhanced parameter validation
|
|
96
105
|
if not isinstance(df, pd.DataFrame):
|
|
@@ -105,6 +114,9 @@ def remove_low_variation(
|
|
|
105
114
|
if dv is not None and dv not in df.columns:
|
|
106
115
|
warnings.warn(f"Specified DV '{dv}' not found in dataframe")
|
|
107
116
|
|
|
117
|
+
if not isinstance(dropna, bool):
|
|
118
|
+
raise TypeError(f"dropna must be a bool, got {type(dropna)}")
|
|
119
|
+
|
|
108
120
|
# Create a deep copy of the dataframe
|
|
109
121
|
work_df = df.copy(deep=True)
|
|
110
122
|
|
|
@@ -115,7 +127,7 @@ def remove_low_variation(
|
|
|
115
127
|
print("\nExamining all columns as candidates for removal")
|
|
116
128
|
else:
|
|
117
129
|
if isinstance(columns, list):
|
|
118
|
-
var_list = columns
|
|
130
|
+
var_list = list(columns)
|
|
119
131
|
if verbose:
|
|
120
132
|
print(f"\nExamining the following columns as candidates for removal: {var_list}")
|
|
121
133
|
else:
|
|
@@ -139,12 +151,15 @@ def remove_low_variation(
|
|
|
139
151
|
# Process all columns
|
|
140
152
|
for v in var_list:
|
|
141
153
|
try:
|
|
142
|
-
#
|
|
154
|
+
# Drop columns with no non-null values
|
|
143
155
|
if work_df[v].isna().all():
|
|
156
|
+
removal_list.append(v)
|
|
157
|
+
if verbose:
|
|
158
|
+
print(f"Column '{v}' contains only null values")
|
|
144
159
|
continue
|
|
145
160
|
|
|
146
161
|
# Get value counts for the column
|
|
147
|
-
counts = work_df[v].value_counts(normalize=True, dropna=
|
|
162
|
+
counts = work_df[v].value_counts(normalize=True, dropna=dropna)
|
|
148
163
|
max_freq = counts.iloc[0]
|
|
149
164
|
max_value = counts.index[0]
|
|
150
165
|
|
|
@@ -153,7 +168,8 @@ def remove_low_variation(
|
|
|
153
168
|
removal_list.append(v)
|
|
154
169
|
|
|
155
170
|
if verbose:
|
|
156
|
-
|
|
171
|
+
basis = "non-null values" if dropna else "values"
|
|
172
|
+
print(f"In column '{v}', the value {max_value} accounts for {max_freq:.2%} of the {basis} (threshold: {threshold:.2%})")
|
|
157
173
|
except Exception as e:
|
|
158
174
|
warnings.warn(f"Error processing column '{v}': {str(e)}")
|
|
159
175
|
|
|
@@ -166,12 +182,47 @@ def remove_low_variation(
|
|
|
166
182
|
return work_df
|
|
167
183
|
|
|
168
184
|
|
|
185
|
+
def _mutual_info_to_target(
|
|
186
|
+
X: pd.DataFrame,
|
|
187
|
+
y: pd.Series,
|
|
188
|
+
min_periods: int = 1
|
|
189
|
+
) -> pd.Series:
|
|
190
|
+
"""
|
|
191
|
+
Mutual information of each column of X with y, on rows where both are non-null.
|
|
192
|
+
|
|
193
|
+
Columns with no nulls on y's non-null rows are scored in one call; the rest are scored
|
|
194
|
+
one at a time on their own complete rows. Columns with fewer than max(min_periods, 4)
|
|
195
|
+
complete rows are returned as NaN.
|
|
196
|
+
"""
|
|
197
|
+
if isinstance(y.dtype, pd.CategoricalDtype) or y.nunique() < 10:
|
|
198
|
+
mi_func = mutual_info_classif
|
|
199
|
+
else:
|
|
200
|
+
mi_func = mutual_info_regression
|
|
201
|
+
|
|
202
|
+
y_rows = y.notna()
|
|
203
|
+
pair_counts = X.notna().mul(y_rows, axis=0).sum()
|
|
204
|
+
eligible = pair_counts[pair_counts >= max(min_periods, 4)]
|
|
205
|
+
complete_cols = eligible.index[eligible == y_rows.sum()].tolist()
|
|
206
|
+
partial_cols = eligible.index[eligible < y_rows.sum()].tolist()
|
|
207
|
+
|
|
208
|
+
mi_values = pd.Series(np.nan, index=X.columns, dtype=float)
|
|
209
|
+
if complete_cols:
|
|
210
|
+
mi_values[complete_cols] = mi_func(X.loc[y_rows, complete_cols], y[y_rows])
|
|
211
|
+
for col in partial_cols:
|
|
212
|
+
rows = y_rows & X[col].notna()
|
|
213
|
+
mi_values[col] = mi_func(X.loc[rows, [col]], y[rows])[0]
|
|
214
|
+
|
|
215
|
+
return mi_values
|
|
216
|
+
|
|
217
|
+
|
|
169
218
|
def remove_outcome_proxies(
|
|
170
219
|
df: pd.DataFrame,
|
|
171
220
|
dv: str,
|
|
172
221
|
threshold: float = 0.8,
|
|
173
222
|
method: str = "pearson",
|
|
174
|
-
verbose: bool = True
|
|
223
|
+
verbose: bool = True,
|
|
224
|
+
dropna: bool = True,
|
|
225
|
+
min_periods: int = 1
|
|
175
226
|
) -> pd.DataFrame:
|
|
176
227
|
"""
|
|
177
228
|
Remove columns that are highly correlated with the outcome (target) column.
|
|
@@ -196,6 +247,17 @@ def remove_outcome_proxies(
|
|
|
196
247
|
- "mutual_info": Mutual information (any statistical dependency)
|
|
197
248
|
verbose : bool, default=True
|
|
198
249
|
Set to True to print outputs of columns being dropped. Set to False to suppress.
|
|
250
|
+
dropna : bool, default=True
|
|
251
|
+
If True, each feature is compared to `dv` using only the rows where both are
|
|
252
|
+
non-null. "pearson" and "spearman" always work this way (pandas pairwise-complete
|
|
253
|
+
correlation), so this flag only changes "mutual_info": with False, any missing
|
|
254
|
+
value in the numeric features or `dv` raises a ValueError, because scikit-learn's
|
|
255
|
+
mutual information estimators do not accept NaN.
|
|
256
|
+
min_periods : int, default=1
|
|
257
|
+
Minimum number of rows where both the feature and `dv` are non-null for the
|
|
258
|
+
feature to be evaluated. Features below it are never flagged as proxies. Guards
|
|
259
|
+
against spurious high correlations from sparsely populated columns. "mutual_info"
|
|
260
|
+
always requires at least 4 such rows.
|
|
199
261
|
|
|
200
262
|
Returns
|
|
201
263
|
-------
|
|
@@ -204,8 +266,8 @@ def remove_outcome_proxies(
|
|
|
204
266
|
|
|
205
267
|
Examples
|
|
206
268
|
--------
|
|
207
|
-
>>> new_df =
|
|
208
|
-
>>>
|
|
269
|
+
>>> new_df = remove_outcome_proxies(df=my_df, dv='target', threshold=0.7, method="spearman")
|
|
270
|
+
>>> new_df = remove_outcome_proxies(df=my_df, dv='target', method="mutual_info", min_periods=100)
|
|
209
271
|
"""
|
|
210
272
|
# Parameter validation
|
|
211
273
|
if not isinstance(df, pd.DataFrame):
|
|
@@ -230,6 +292,15 @@ def remove_outcome_proxies(
|
|
|
230
292
|
if method not in valid_methods:
|
|
231
293
|
raise ValueError(f"method must be one of {valid_methods}, got {method}")
|
|
232
294
|
|
|
295
|
+
if not isinstance(dropna, bool):
|
|
296
|
+
raise TypeError(f"dropna must be a bool, got {type(dropna)}")
|
|
297
|
+
|
|
298
|
+
if isinstance(min_periods, bool) or not isinstance(min_periods, int):
|
|
299
|
+
raise TypeError(f"min_periods must be an int, got {type(min_periods)}")
|
|
300
|
+
|
|
301
|
+
if min_periods < 1:
|
|
302
|
+
raise ValueError(f"min_periods must be at least 1, got {min_periods}")
|
|
303
|
+
|
|
233
304
|
# Create a deep copy of the dataframe
|
|
234
305
|
work_df = df.copy(deep=True)
|
|
235
306
|
|
|
@@ -241,35 +312,35 @@ def remove_outcome_proxies(
|
|
|
241
312
|
# Remove the target variable from the list of features
|
|
242
313
|
feature_cols = [col for col in numeric_cols if col != dv]
|
|
243
314
|
|
|
315
|
+
y = work_df[dv]
|
|
316
|
+
|
|
244
317
|
# Calculate correlations based on the specified method
|
|
245
318
|
if method in ["pearson", "spearman"]:
|
|
246
|
-
#
|
|
247
|
-
|
|
319
|
+
# Per-feature correlation with the dv (what DataFrame.corrwith does, plus min_periods);
|
|
320
|
+
# avoids computing the full feature-by-feature matrix
|
|
321
|
+
correlations = pd.Series(
|
|
322
|
+
{col: work_df[col].corr(y, method=method, min_periods=min_periods) for col in feature_cols},
|
|
323
|
+
dtype=float
|
|
324
|
+
)
|
|
248
325
|
corr_values = {col: abs(corr) for col, corr in correlations.items() if abs(corr) > threshold}
|
|
249
326
|
|
|
250
327
|
elif method == "mutual_info":
|
|
251
|
-
# For mutual information, we need to handle the target type differently
|
|
252
|
-
# Create X and y for mutual information calculation
|
|
253
328
|
X = work_df[feature_cols]
|
|
254
|
-
y = work_df[dv]
|
|
255
329
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
330
|
+
if not dropna and (X.isna().any().any() or y.isna().any()):
|
|
331
|
+
raise ValueError(
|
|
332
|
+
"mutual_info cannot be computed on missing values; set dropna=True to use "
|
|
333
|
+
"the rows where each feature and the dv are both non-null"
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
mi_values = _mutual_info_to_target(X, y, min_periods)
|
|
263
337
|
|
|
264
338
|
# Normalize MI values to 0-1 range for comparison with threshold
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
corr_values = {col: mi for col, mi in zip(feature_cols, mi_normalized) if mi > threshold}
|
|
271
|
-
else:
|
|
272
|
-
corr_values = {}
|
|
339
|
+
mi_max = mi_values.max()
|
|
340
|
+
if not mi_max > 0:
|
|
341
|
+
mi_max = 1
|
|
342
|
+
mi_normalized = mi_values / mi_max
|
|
343
|
+
corr_values = {col: mi for col, mi in mi_normalized.items() if mi > threshold}
|
|
273
344
|
|
|
274
345
|
# Create the list of columns to drop
|
|
275
346
|
dropped_columns = list(corr_values.keys())
|
|
@@ -295,7 +366,9 @@ def correlation_reduction(
|
|
|
295
366
|
dv: Optional[str] = None,
|
|
296
367
|
threshold: float = 0.9,
|
|
297
368
|
method: str = "pearson",
|
|
298
|
-
verbose: bool = True
|
|
369
|
+
verbose: bool = True,
|
|
370
|
+
dropna: bool = True,
|
|
371
|
+
min_periods: int = 1
|
|
299
372
|
) -> pd.DataFrame:
|
|
300
373
|
"""
|
|
301
374
|
Reduce the number of columns by dropping features that are highly correlated with each other.
|
|
@@ -311,6 +384,7 @@ def correlation_reduction(
|
|
|
311
384
|
dv : str, optional
|
|
312
385
|
The column name of your outcome. If provided, when choosing between
|
|
313
386
|
correlated features, the one with higher correlation to the target will be kept.
|
|
387
|
+
A feature whose correlation to the target cannot be computed counts as 0.
|
|
314
388
|
threshold : float, default=0.9
|
|
315
389
|
The correlation threshold above which columns will be considered redundant.
|
|
316
390
|
For example, the default value of 0.9 will identify columns that have
|
|
@@ -319,9 +393,21 @@ def correlation_reduction(
|
|
|
319
393
|
The correlation method to use. Options are:
|
|
320
394
|
- "pearson": Standard correlation coefficient (linear relationships)
|
|
321
395
|
- "spearman": Spearman rank correlation (monotonic relationships)
|
|
322
|
-
- "mutual_info": Mutual information (any statistical dependency)
|
|
396
|
+
- "mutual_info": Mutual information (any statistical dependency). Scores every
|
|
397
|
+
feature pair, so it is slow on wide data.
|
|
323
398
|
verbose : bool, default=True
|
|
324
399
|
Set to True to print outputs of columns being dropped. Set to False to suppress.
|
|
400
|
+
dropna : bool, default=True
|
|
401
|
+
If True, each pair (feature-feature, and feature-dv) is compared using only the
|
|
402
|
+
rows where both are non-null. "pearson" and "spearman" always work this way
|
|
403
|
+
(pandas pairwise-complete correlation), so this flag only changes "mutual_info":
|
|
404
|
+
with False, any missing value in the numeric features or `dv` raises a ValueError,
|
|
405
|
+
because scikit-learn's mutual information estimators do not accept NaN.
|
|
406
|
+
min_periods : int, default=1
|
|
407
|
+
Minimum number of rows where both columns of a pair are non-null for the pair to
|
|
408
|
+
be evaluated. Pairs below it are never treated as correlated. Guards against
|
|
409
|
+
spurious high correlations between sparsely populated columns. "mutual_info"
|
|
410
|
+
always requires at least 4 such rows.
|
|
325
411
|
|
|
326
412
|
Returns
|
|
327
413
|
-------
|
|
@@ -330,8 +416,8 @@ def correlation_reduction(
|
|
|
330
416
|
|
|
331
417
|
Examples
|
|
332
418
|
--------
|
|
333
|
-
>>> new_df =
|
|
334
|
-
>>>
|
|
419
|
+
>>> new_df = correlation_reduction(df=my_df, dv='target', threshold=0.85, method="spearman")
|
|
420
|
+
>>> new_df = correlation_reduction(df=my_df, dv='target', min_periods=100)
|
|
335
421
|
"""
|
|
336
422
|
# Parameter validation
|
|
337
423
|
if not isinstance(df, pd.DataFrame):
|
|
@@ -354,6 +440,15 @@ def correlation_reduction(
|
|
|
354
440
|
if method not in valid_methods:
|
|
355
441
|
raise ValueError(f"method must be one of {valid_methods}, got {method}")
|
|
356
442
|
|
|
443
|
+
if not isinstance(dropna, bool):
|
|
444
|
+
raise TypeError(f"dropna must be a bool, got {type(dropna)}")
|
|
445
|
+
|
|
446
|
+
if isinstance(min_periods, bool) or not isinstance(min_periods, int):
|
|
447
|
+
raise TypeError(f"min_periods must be an int, got {type(min_periods)}")
|
|
448
|
+
|
|
449
|
+
if min_periods < 1:
|
|
450
|
+
raise ValueError(f"min_periods must be at least 1, got {min_periods}")
|
|
451
|
+
|
|
357
452
|
# Create a deep copy of the dataframe
|
|
358
453
|
work_df = df.copy(deep=True)
|
|
359
454
|
|
|
@@ -364,46 +459,47 @@ def correlation_reduction(
|
|
|
364
459
|
# Remove the target variable from the list of features to consider for removal
|
|
365
460
|
feature_cols.remove(dv)
|
|
366
461
|
|
|
367
|
-
# Ensure we have the target in numeric_cols for correlation calculations if it's numeric
|
|
368
|
-
numeric_cols = feature_cols.copy()
|
|
369
|
-
if dv is not None and dv in work_df.columns and pd.api.types.is_numeric_dtype(work_df[dv]):
|
|
370
|
-
numeric_cols.append(dv)
|
|
371
|
-
|
|
372
462
|
# Check if we have enough numeric columns to proceed
|
|
373
463
|
if len(feature_cols) < 2:
|
|
374
464
|
if verbose:
|
|
375
465
|
print("Not enough numeric columns for correlation analysis")
|
|
376
466
|
return work_df
|
|
377
467
|
|
|
468
|
+
if method == "mutual_info" and not dropna:
|
|
469
|
+
check_cols = feature_cols + ([dv] if dv is not None else [])
|
|
470
|
+
if work_df[check_cols].isna().any().any():
|
|
471
|
+
raise ValueError(
|
|
472
|
+
"mutual_info cannot be computed on missing values; set dropna=True to use "
|
|
473
|
+
"the rows where both columns of each pair are non-null"
|
|
474
|
+
)
|
|
475
|
+
|
|
378
476
|
# Calculate correlations based on the chosen method
|
|
379
477
|
if method in ["pearson", "spearman"]:
|
|
380
|
-
# Standard correlation methods
|
|
381
|
-
correlation_matrix = work_df[
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
col2 = feature_cols[j]
|
|
390
|
-
|
|
391
|
-
# Find their positions in the correlation matrix
|
|
392
|
-
idx1 = numeric_cols.index(col1)
|
|
393
|
-
idx2 = numeric_cols.index(col2)
|
|
394
|
-
|
|
395
|
-
# Check if correlation exceeds threshold
|
|
396
|
-
if correlation_matrix.iloc[idx1, idx2] > threshold:
|
|
397
|
-
corr_pairs.append({
|
|
398
|
-
"feature1": col1,
|
|
399
|
-
"feature2": col2,
|
|
400
|
-
"correlation": correlation_matrix.iloc[idx1, idx2]
|
|
401
|
-
})
|
|
478
|
+
# Standard correlation methods; pairs above the threshold from the upper triangle
|
|
479
|
+
correlation_matrix = work_df[feature_cols].corr(method=method, min_periods=min_periods).abs().to_numpy()
|
|
480
|
+
rows, cols = np.triu_indices(len(feature_cols), k=1)
|
|
481
|
+
pair_corr = correlation_matrix[rows, cols]
|
|
482
|
+
above = pair_corr > threshold
|
|
483
|
+
corr_pairs = [
|
|
484
|
+
{"feature1": feature_cols[i], "feature2": feature_cols[j], "correlation": corr}
|
|
485
|
+
for i, j, corr in zip(rows[above], cols[above], pair_corr[above])
|
|
486
|
+
]
|
|
402
487
|
|
|
403
488
|
elif method == "mutual_info":
|
|
404
489
|
# For mutual information between features
|
|
405
490
|
corr_pairs = []
|
|
406
491
|
X = work_df[feature_cols]
|
|
492
|
+
non_null = X.notna()
|
|
493
|
+
complete = non_null.all()
|
|
494
|
+
n_unique = X.nunique()
|
|
495
|
+
min_rows = max(min_periods, 4)
|
|
496
|
+
|
|
497
|
+
# MI of each feature with itself, used for normalization; computed once for
|
|
498
|
+
# features without nulls, per pair otherwise
|
|
499
|
+
full_entropy = {}
|
|
500
|
+
|
|
501
|
+
def self_mi(values: pd.Series) -> float:
|
|
502
|
+
return mutual_info_regression(values.to_frame(), values.to_numpy())[0]
|
|
407
503
|
|
|
408
504
|
# Calculate pairwise mutual information
|
|
409
505
|
for i in range(len(feature_cols)):
|
|
@@ -412,23 +508,29 @@ def correlation_reduction(
|
|
|
412
508
|
feature2 = feature_cols[j]
|
|
413
509
|
|
|
414
510
|
# Skip if either column has all identical values
|
|
415
|
-
if
|
|
511
|
+
if n_unique[feature1] <= 1 or n_unique[feature2] <= 1:
|
|
416
512
|
continue
|
|
417
513
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
514
|
+
if complete[feature1] and complete[feature2]:
|
|
515
|
+
if len(X) < min_rows:
|
|
516
|
+
continue
|
|
517
|
+
x1, x2 = X[feature1], X[feature2]
|
|
518
|
+
if feature1 not in full_entropy:
|
|
519
|
+
full_entropy[feature1] = self_mi(x1)
|
|
520
|
+
if feature2 not in full_entropy:
|
|
521
|
+
full_entropy[feature2] = self_mi(x2)
|
|
522
|
+
entropy1, entropy2 = full_entropy[feature1], full_entropy[feature2]
|
|
523
|
+
else:
|
|
524
|
+
pair_rows = non_null[feature1] & non_null[feature2]
|
|
525
|
+
if pair_rows.sum() < min_rows:
|
|
526
|
+
continue
|
|
527
|
+
x1, x2 = X.loc[pair_rows, feature1], X.loc[pair_rows, feature2]
|
|
528
|
+
entropy1, entropy2 = self_mi(x1), self_mi(x2)
|
|
422
529
|
|
|
423
|
-
#
|
|
424
|
-
|
|
425
|
-
X[[feature1]], X[feature1].values.reshape(-1, 1)
|
|
426
|
-
)[0]
|
|
427
|
-
entropy2 = mutual_info_regression(
|
|
428
|
-
X[[feature2]], X[feature2].values.reshape(-1, 1)
|
|
429
|
-
)[0]
|
|
530
|
+
# Calculate mutual information between the two features
|
|
531
|
+
mi = mutual_info_regression(x1.to_frame(), x2.to_numpy())[0]
|
|
430
532
|
|
|
431
|
-
#
|
|
533
|
+
# Normalize by entropy for a 0-1 scale; avoid division by zero
|
|
432
534
|
if entropy1 > 0 and entropy2 > 0:
|
|
433
535
|
normalized_mi = mi / min(entropy1, entropy2)
|
|
434
536
|
|
|
@@ -448,21 +550,17 @@ def correlation_reduction(
|
|
|
448
550
|
|
|
449
551
|
if dv is not None and dv in work_df.columns:
|
|
450
552
|
# If we have a target variable, calculate correlations to the target
|
|
553
|
+
y = work_df[dv]
|
|
451
554
|
if method in ["pearson", "spearman"]:
|
|
452
|
-
target_corr =
|
|
453
|
-
|
|
555
|
+
target_corr = pd.Series(
|
|
556
|
+
{col: work_df[col].corr(y, method=method, min_periods=min_periods) for col in feature_cols},
|
|
557
|
+
dtype=float
|
|
558
|
+
).abs()
|
|
454
559
|
else: # mutual_info
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
if pd.api.types.is_categorical_dtype(y) or y.nunique() < 10:
|
|
460
|
-
mi_values = mutual_info_classif(X, y)
|
|
461
|
-
else:
|
|
462
|
-
mi_values = mutual_info_regression(X, y)
|
|
463
|
-
|
|
464
|
-
# Create a Series for target correlations
|
|
465
|
-
target_corr = pd.Series(mi_values, index=feature_cols)
|
|
560
|
+
target_corr = _mutual_info_to_target(work_df[feature_cols], y, min_periods)
|
|
561
|
+
|
|
562
|
+
# A feature whose target correlation cannot be computed ranks below any real one
|
|
563
|
+
target_corr = target_corr.fillna(0)
|
|
466
564
|
|
|
467
565
|
# Process each correlated pair
|
|
468
566
|
for pair in corr_pairs:
|
|
@@ -524,5 +622,3 @@ def correlation_reduction(
|
|
|
524
622
|
work_df = work_df.drop(columns=dropped_columns)
|
|
525
623
|
|
|
526
624
|
return work_df
|
|
527
|
-
|
|
528
|
-
return work_df
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gitlabds
|
|
3
|
-
Version: 2.4.
|
|
3
|
+
Version: 2.4.1
|
|
4
4
|
Summary: GitLab Data Science Tools
|
|
5
5
|
Author-email: Kevin Dietz <kdietz@gitlab.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -376,9 +376,9 @@ test_df_transformed = gitlabds.apply_dummy(
|
|
|
376
376
|
<details><summary> Remove Low Variation </summary>
|
|
377
377
|
|
|
378
378
|
#### Description
|
|
379
|
-
Remove columns from a dataset that do not meet the variation threshold. That is, columns will be dropped that contain a high percentage of one value.
|
|
379
|
+
Remove columns from a dataset that do not meet the variation threshold. That is, columns will be dropped that contain a high percentage of one value. Columns that are entirely null are always dropped.
|
|
380
380
|
|
|
381
|
-
`gitlabds.remove_low_variation(df=None, dv=None, columns='all', threshold=.98, verbose=True):`
|
|
381
|
+
`gitlabds.remove_low_variation(df=None, dv=None, columns='all', threshold=.98, verbose=True, dropna=False):`
|
|
382
382
|
|
|
383
383
|
#### Parameters:
|
|
384
384
|
- _**df**_ : your pandas dataframe
|
|
@@ -386,6 +386,7 @@ Remove columns from a dataset that do not meet the variation threshold. That is,
|
|
|
386
386
|
- **_columns_** : Will examine at all columns by default. To limit to just a subset of columns, pass a list of column names.
|
|
387
387
|
- **_threshold_**: The maximum percentage one value in a column can represent. columns that exceed this threshold will be dropped. For example, the default value of `0.98` will drop any column where one value is present in more than 98% of rows.
|
|
388
388
|
- **_verbose_** : Set to `True` to print outputs of columns being dropped. Set to `False` to suppress.
|
|
389
|
+
- **_dropna_** : Set to `False` (default) to count missing values as their own value. Set to `True` to measure variation over non-null values only, so a column that is one value plus NaN is dropped. Use `True` when missing values are left in place for a model that handles them natively (e.g. XGBoost, LightGBM).
|
|
389
390
|
|
|
390
391
|
#### Returns
|
|
391
392
|
- DataFrame with low variation columns dropped.
|
|
@@ -399,6 +400,10 @@ new_df = gitlabds.remove_low_variation(df=my_df, dv='my_outcome', columns='all',
|
|
|
399
400
|
# Drop any of the selected columns where one value is present in more than 99% of rows.
|
|
400
401
|
new_df = gitlabds.remove_low_variation(df=my_df, dv=None, columns=['colA', 'colB', 'colC'], threshold=.99)
|
|
401
402
|
```
|
|
403
|
+
```python
|
|
404
|
+
# Ignore missing values: drop columns where one value makes up more than 98% of the non-null rows.
|
|
405
|
+
new_df = gitlabds.remove_low_variation(df=my_df, dv='my_outcome', dropna=True)
|
|
406
|
+
```
|
|
402
407
|
</details>
|
|
403
408
|
|
|
404
409
|
<details><summary> Correlation Reduction </summary>
|
|
@@ -406,7 +411,7 @@ new_df = gitlabds.remove_low_variation(df=my_df, dv=None, columns=['colA', 'colB
|
|
|
406
411
|
#### Description
|
|
407
412
|
Reduce the number of columns on a dataframe by dropping columns that are highly correlated with other columns. Note: only one of the two highly correlated columns will be dropped.
|
|
408
413
|
|
|
409
|
-
`gitlabds.correlation_reduction(df=None, dv=None, threshold=0.9, method="pearson", verbose=True):`
|
|
414
|
+
`gitlabds.correlation_reduction(df=None, dv=None, threshold=0.9, method="pearson", verbose=True, dropna=True, min_periods=1):`
|
|
410
415
|
|
|
411
416
|
#### Parameters:
|
|
412
417
|
- _**df**_ : your pandas dataframe
|
|
@@ -414,6 +419,8 @@ Reduce the number of columns on a dataframe by dropping columns that are highly
|
|
|
414
419
|
- **_threshold_**: The threshold above which columns will be dropped. If two variables exceed this threshold, one will be dropped from the dataframe. For example, the default value of `0.90` will identify columns that have correlations greater than 90% to each other and drop one of those columns.
|
|
415
420
|
- **_method_**: The correlation method to use. Options are "pearson" (linear relationships), "spearman" (monotonic relationships), or "mutual_info" (any statistical dependency).
|
|
416
421
|
- **_verbose_** : Set to `True` to print outputs of columns being dropped. Set to `False` to suppress.
|
|
422
|
+
- **_dropna_** : Set to `True` (default) to compare each pair of columns (and each column with the outcome) using only rows where both are non-null. Pearson and Spearman always work this way; for "mutual_info", `False` raises an error if any missing values are present.
|
|
423
|
+
- **_min_periods_** : Minimum number of rows where both columns of a pair are non-null for the pair to be evaluated. Pairs below it are never treated as correlated. Guards against spurious high correlations between sparsely populated columns. "mutual_info" always requires at least 4 such rows.
|
|
417
424
|
|
|
418
425
|
#### Returns
|
|
419
426
|
- DataFrame with redundant correlated columns dropped.
|
|
@@ -434,7 +441,7 @@ new_df = gitlabds.correlation_reduction(df=my_df, dv=None, threshold=0.90, metho
|
|
|
434
441
|
#### Description
|
|
435
442
|
Remove columns that are highly correlated with the outcome (target) column.
|
|
436
443
|
|
|
437
|
-
`gitlabds.remove_outcome_proxies(df, dv, threshold=.8, method="pearson", verbose=True):`
|
|
444
|
+
`gitlabds.remove_outcome_proxies(df, dv, threshold=.8, method="pearson", verbose=True, dropna=True, min_periods=1):`
|
|
438
445
|
|
|
439
446
|
#### Parameters:
|
|
440
447
|
- _**df**_ : your pandas dataframe
|
|
@@ -442,6 +449,8 @@ Remove columns that are highly correlated with the outcome (target) column.
|
|
|
442
449
|
- _**threshold**_ : The correlation value to the outcome above which columns will be dropped. For example, the default value of `0.80` will identify and drop columns that have correlations greater than 80% to the outcome.
|
|
443
450
|
- **_method_**: The correlation method to use. Options are "pearson" (linear relationships), "spearman" (monotonic relationships), or "mutual_info" (any statistical dependency).
|
|
444
451
|
- **_verbose_** : Set to `True` to print outputs of columns being dropped. Set to `False` to suppress.
|
|
452
|
+
- **_dropna_** : Set to `True` (default) to compare each column to the outcome using only rows where both are non-null. Pearson and Spearman always work this way; for "mutual_info", `False` raises an error if any missing values are present.
|
|
453
|
+
- **_min_periods_** : Minimum number of rows where both the column and the outcome are non-null for the column to be evaluated. Columns below it are never dropped. Guards against spurious high correlations from sparsely populated columns. "mutual_info" always requires at least 4 such rows.
|
|
445
454
|
|
|
446
455
|
#### Returns
|
|
447
456
|
- DataFrame with outcome proxy columns dropped.
|
|
@@ -455,6 +464,10 @@ new_df = gitlabds.remove_outcome_proxies(df=my_df, dv='my_outcome', threshold=.7
|
|
|
455
464
|
# Drop columns with correlations to the outcome greater than 80% using Spearman correlation
|
|
456
465
|
new_df = gitlabds.remove_outcome_proxies(df=my_df, dv='my_outcome', threshold=.8, method="spearman")
|
|
457
466
|
```
|
|
467
|
+
```python
|
|
468
|
+
# Data with missing values: only evaluate columns with at least 100 non-null rows alongside the outcome
|
|
469
|
+
new_df = gitlabds.remove_outcome_proxies(df=my_df, dv='my_outcome', method="spearman", min_periods=100)
|
|
470
|
+
```
|
|
458
471
|
</details>
|
|
459
472
|
|
|
460
473
|
<details><summary> Drop Categorical </summary>
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for gitlabds.feature_reduction: remove_low_variation, remove_outcome_proxies,
|
|
3
|
+
correlation_reduction.
|
|
4
|
+
|
|
5
|
+
Run with: pytest tests/test_feature_reduction.py
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pandas as pd
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
from gitlabds import correlation_reduction, remove_low_variation, remove_outcome_proxies
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
# remove_low_variation
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.fixture
|
|
21
|
+
def low_variation_df():
|
|
22
|
+
n = 100
|
|
23
|
+
rng = np.random.default_rng(0)
|
|
24
|
+
return pd.DataFrame({
|
|
25
|
+
"target": rng.integers(0, 2, n),
|
|
26
|
+
"varied": rng.normal(size=n),
|
|
27
|
+
"one_value_plus_nan": [1.0] * 83 + [np.nan] * 17,
|
|
28
|
+
"two_values_plus_nan": [1.0] * 50 + [2.0] * 30 + [np.nan] * 20,
|
|
29
|
+
"mostly_nan": [1.0] + [np.nan] * 99,
|
|
30
|
+
"all_null": [np.nan] * n,
|
|
31
|
+
"constant": [5] * n,
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TestRemoveLowVariation:
|
|
36
|
+
def test_default_counts_nan_as_value(self, low_variation_df):
|
|
37
|
+
out = remove_low_variation(low_variation_df, dv="target", verbose=False)
|
|
38
|
+
assert set(out.columns) == {"target", "varied", "one_value_plus_nan", "two_values_plus_nan"}
|
|
39
|
+
|
|
40
|
+
def test_dropna_uses_non_null_values_only(self, low_variation_df):
|
|
41
|
+
out = remove_low_variation(low_variation_df, dv="target", dropna=True, verbose=False)
|
|
42
|
+
assert set(out.columns) == {"target", "varied", "two_values_plus_nan"}
|
|
43
|
+
|
|
44
|
+
@pytest.mark.parametrize("dropna", [False, True])
|
|
45
|
+
def test_all_null_column_dropped(self, low_variation_df, dropna):
|
|
46
|
+
out = remove_low_variation(low_variation_df, dropna=dropna, verbose=False)
|
|
47
|
+
assert "all_null" not in out.columns
|
|
48
|
+
|
|
49
|
+
def test_dropna_threshold_on_non_null_share(self):
|
|
50
|
+
# 97 of 98 non-null rows are 0 (99.0%); counting NaN, 0 is only 97%
|
|
51
|
+
df = pd.DataFrame({"x": [0.0] * 97 + [1.0] + [np.nan] * 2})
|
|
52
|
+
assert "x" in remove_low_variation(df, threshold=0.98, verbose=False).columns
|
|
53
|
+
assert "x" not in remove_low_variation(df, threshold=0.98, dropna=True, verbose=False).columns
|
|
54
|
+
|
|
55
|
+
def test_does_not_mutate_columns_argument(self, low_variation_df):
|
|
56
|
+
cols = ["target", "constant"]
|
|
57
|
+
remove_low_variation(low_variation_df, dv="target", columns=cols, verbose=False)
|
|
58
|
+
assert cols == ["target", "constant"]
|
|
59
|
+
|
|
60
|
+
def test_invalid_dropna(self, low_variation_df):
|
|
61
|
+
with pytest.raises(TypeError):
|
|
62
|
+
remove_low_variation(low_variation_df, dropna="yes")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
# remove_outcome_proxies
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@pytest.fixture
|
|
71
|
+
def proxy_df():
|
|
72
|
+
n = 500
|
|
73
|
+
rng = np.random.default_rng(1)
|
|
74
|
+
y = rng.normal(size=n)
|
|
75
|
+
df = pd.DataFrame({
|
|
76
|
+
"y": y,
|
|
77
|
+
"proxy": y + rng.normal(scale=0.05, size=n),
|
|
78
|
+
"noise": rng.normal(size=n),
|
|
79
|
+
"weak": 0.3 * y + rng.normal(size=n),
|
|
80
|
+
})
|
|
81
|
+
df["proxy_with_nan"] = df["proxy"].where(rng.random(n) > 0.3)
|
|
82
|
+
df["noise_with_nan"] = df["noise"].where(rng.random(n) > 0.3)
|
|
83
|
+
df["sparse"] = np.nan
|
|
84
|
+
df.loc[:2, "sparse"] = [0.0, 1.0, 2.0]
|
|
85
|
+
df.loc[:2, "y"] = [0.0, 1.0, 2.1]
|
|
86
|
+
return df
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class TestRemoveOutcomeProxies:
|
|
90
|
+
@pytest.mark.parametrize("method", ["pearson", "spearman"])
|
|
91
|
+
def test_matches_full_correlation_matrix(self, proxy_df, method):
|
|
92
|
+
expected = proxy_df.corr(method=method)["y"].drop("y").abs()
|
|
93
|
+
expected = set(expected[expected > 0.3].index)
|
|
94
|
+
out = remove_outcome_proxies(proxy_df, dv="y", threshold=0.3, method=method, verbose=False)
|
|
95
|
+
assert set(proxy_df.columns) - set(out.columns) == expected
|
|
96
|
+
|
|
97
|
+
@pytest.mark.parametrize("method", ["pearson", "spearman"])
|
|
98
|
+
def test_min_periods_guards_sparse_columns(self, proxy_df, method):
|
|
99
|
+
# "sparse" has 3 non-null rows that line up perfectly with y
|
|
100
|
+
out = remove_outcome_proxies(proxy_df, dv="y", method=method, verbose=False)
|
|
101
|
+
assert "sparse" not in out.columns
|
|
102
|
+
out = remove_outcome_proxies(proxy_df, dv="y", method=method, min_periods=30, verbose=False)
|
|
103
|
+
assert "sparse" in out.columns
|
|
104
|
+
assert "proxy" not in out.columns
|
|
105
|
+
|
|
106
|
+
def test_mutual_info_handles_nan(self, proxy_df):
|
|
107
|
+
out = remove_outcome_proxies(proxy_df, dv="y", method="mutual_info", threshold=0.5, verbose=False)
|
|
108
|
+
assert "proxy" not in out.columns
|
|
109
|
+
assert "proxy_with_nan" not in out.columns
|
|
110
|
+
assert {"noise", "noise_with_nan", "weak", "sparse"} <= set(out.columns)
|
|
111
|
+
|
|
112
|
+
def test_mutual_info_handles_nan_in_dv(self, proxy_df):
|
|
113
|
+
proxy_df.loc[proxy_df.index[-50:], "y"] = np.nan
|
|
114
|
+
out = remove_outcome_proxies(proxy_df, dv="y", method="mutual_info", threshold=0.5, verbose=False)
|
|
115
|
+
assert "proxy" not in out.columns
|
|
116
|
+
assert "noise" in out.columns
|
|
117
|
+
|
|
118
|
+
def test_mutual_info_dropna_false_raises_on_nan(self, proxy_df):
|
|
119
|
+
with pytest.raises(ValueError, match="dropna=True"):
|
|
120
|
+
remove_outcome_proxies(proxy_df, dv="y", method="mutual_info", dropna=False)
|
|
121
|
+
|
|
122
|
+
def test_mutual_info_dropna_false_without_nan(self, proxy_df):
|
|
123
|
+
df = proxy_df[["y", "proxy", "noise"]]
|
|
124
|
+
out = remove_outcome_proxies(df, dv="y", method="mutual_info", threshold=0.5, dropna=False, verbose=False)
|
|
125
|
+
assert list(out.columns) == ["y", "noise"]
|
|
126
|
+
|
|
127
|
+
def test_mutual_info_classification_dv(self, proxy_df):
|
|
128
|
+
df = proxy_df.assign(y=(proxy_df["y"] > 0).astype(int), proxy=(proxy_df["y"] > 0).astype(float))
|
|
129
|
+
df.loc[df.index[:100], "proxy"] = np.nan
|
|
130
|
+
out = remove_outcome_proxies(df, dv="y", method="mutual_info", threshold=0.5, verbose=False)
|
|
131
|
+
assert "proxy" not in out.columns
|
|
132
|
+
assert "noise" in out.columns
|
|
133
|
+
|
|
134
|
+
@pytest.mark.parametrize("min_periods", [0, 1.5, True])
|
|
135
|
+
def test_invalid_min_periods(self, proxy_df, min_periods):
|
|
136
|
+
with pytest.raises((TypeError, ValueError)):
|
|
137
|
+
remove_outcome_proxies(proxy_df, dv="y", min_periods=min_periods)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ---------------------------------------------------------------------------
|
|
141
|
+
# correlation_reduction
|
|
142
|
+
# ---------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@pytest.fixture
|
|
146
|
+
def redundant_df():
|
|
147
|
+
n = 400
|
|
148
|
+
rng = np.random.default_rng(2)
|
|
149
|
+
a = rng.normal(size=n)
|
|
150
|
+
b = rng.normal(size=n)
|
|
151
|
+
df = pd.DataFrame({
|
|
152
|
+
"y": a + rng.normal(scale=0.5, size=n),
|
|
153
|
+
"a1": a + rng.normal(scale=0.1, size=n),
|
|
154
|
+
"a2": a + rng.normal(scale=0.2, size=n),
|
|
155
|
+
"b1": b,
|
|
156
|
+
"b2": b + rng.normal(scale=0.1, size=n),
|
|
157
|
+
"noise": rng.normal(size=n),
|
|
158
|
+
})
|
|
159
|
+
df["a2"] = df["a2"].where(rng.random(n) > 0.2)
|
|
160
|
+
df["b2"] = df["b2"].where(rng.random(n) > 0.2)
|
|
161
|
+
return df
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _reference_drops(df, dv, threshold, method):
|
|
165
|
+
"""The pre-vectorization pair search, for pearson/spearman."""
|
|
166
|
+
features = [c for c in df.select_dtypes("number").columns if c != dv]
|
|
167
|
+
corr = df[features].corr(method=method).abs()
|
|
168
|
+
pairs = [
|
|
169
|
+
(corr.iloc[i, j], features[i], features[j])
|
|
170
|
+
for i in range(len(features)) for j in range(i + 1, len(features))
|
|
171
|
+
if corr.iloc[i, j] > threshold
|
|
172
|
+
]
|
|
173
|
+
pairs.sort(key=lambda p: p[0], reverse=True)
|
|
174
|
+
target = df[features].corrwith(df[dv], method=method).abs() if dv else None
|
|
175
|
+
dropped = set()
|
|
176
|
+
for _, f1, f2 in pairs:
|
|
177
|
+
if f1 in dropped or f2 in dropped:
|
|
178
|
+
continue
|
|
179
|
+
if target is None or target[f1] >= target[f2]:
|
|
180
|
+
dropped.add(f2)
|
|
181
|
+
else:
|
|
182
|
+
dropped.add(f1)
|
|
183
|
+
return dropped
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class TestCorrelationReduction:
|
|
187
|
+
@pytest.mark.parametrize("method", ["pearson", "spearman"])
|
|
188
|
+
@pytest.mark.parametrize("dv", [None, "y"])
|
|
189
|
+
def test_matches_reference(self, redundant_df, method, dv):
|
|
190
|
+
out = correlation_reduction(redundant_df, dv=dv, threshold=0.8, method=method, verbose=False)
|
|
191
|
+
dropped = set(redundant_df.columns) - set(out.columns)
|
|
192
|
+
assert dropped == _reference_drops(redundant_df, dv, 0.8, method)
|
|
193
|
+
assert {"a2", "b2"} <= dropped
|
|
194
|
+
|
|
195
|
+
def test_keeps_feature_with_target_correlation_over_nan(self):
|
|
196
|
+
n = 50
|
|
197
|
+
x = np.arange(n, dtype=float)
|
|
198
|
+
df = pd.DataFrame({"y": x, "f1": x, "f2": x})
|
|
199
|
+
# f2's target correlation is NaN (dv constant where f2 overlaps it); f1's is real
|
|
200
|
+
df.loc[: n // 2, "y"] = 1.0
|
|
201
|
+
df.loc[n // 2 + 1:, "f2"] = np.nan
|
|
202
|
+
out = correlation_reduction(df, dv="y", threshold=0.8, verbose=False)
|
|
203
|
+
assert list(out.columns) == ["y", "f1"]
|
|
204
|
+
|
|
205
|
+
def test_min_periods_guards_sparse_pairs(self):
|
|
206
|
+
n = 200
|
|
207
|
+
rng = np.random.default_rng(3)
|
|
208
|
+
df = pd.DataFrame({"s1": np.nan, "s2": np.nan, "x": rng.normal(size=n)}, index=range(n))
|
|
209
|
+
df.loc[:2, "s1"] = [0.0, 1.0, 2.0]
|
|
210
|
+
df.loc[:2, "s2"] = [0.0, 1.0, 2.1]
|
|
211
|
+
assert "s2" not in correlation_reduction(df, verbose=False).columns
|
|
212
|
+
assert list(correlation_reduction(df, min_periods=30, verbose=False).columns) == ["s1", "s2", "x"]
|
|
213
|
+
|
|
214
|
+
@pytest.mark.parametrize("dv", [None, "y"])
|
|
215
|
+
def test_mutual_info_handles_nan(self, redundant_df, dv):
|
|
216
|
+
out = correlation_reduction(redundant_df, dv=dv, threshold=0.5, method="mutual_info", verbose=False)
|
|
217
|
+
assert {"noise", "y"} <= set(out.columns)
|
|
218
|
+
assert not {"b1", "b2"} <= set(out.columns)
|
|
219
|
+
|
|
220
|
+
def test_mutual_info_dropna_false_raises_on_nan(self, redundant_df):
|
|
221
|
+
with pytest.raises(ValueError, match="dropna=True"):
|
|
222
|
+
correlation_reduction(redundant_df, method="mutual_info", dropna=False)
|
|
223
|
+
|
|
224
|
+
def test_mutual_info_dropna_false_without_nan(self, redundant_df):
|
|
225
|
+
df = redundant_df.drop(columns=["a2", "b2"])
|
|
226
|
+
out = correlation_reduction(df, dv="y", threshold=0.5, method="mutual_info", dropna=False, verbose=False)
|
|
227
|
+
assert "noise" in out.columns
|
|
228
|
+
|
|
229
|
+
@pytest.mark.parametrize("min_periods", [0, 1.5, True])
|
|
230
|
+
def test_invalid_min_periods(self, redundant_df, min_periods):
|
|
231
|
+
with pytest.raises((TypeError, ValueError)):
|
|
232
|
+
correlation_reduction(redundant_df, min_periods=min_periods)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|