gitlabds 2.4.0__tar.gz → 2.4.2__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.
Files changed (31) hide show
  1. {gitlabds-2.4.0 → gitlabds-2.4.2}/PKG-INFO +31 -7
  2. {gitlabds-2.4.0 → gitlabds-2.4.2}/README.md +30 -6
  3. {gitlabds-2.4.0 → gitlabds-2.4.2}/gitlabds/feature_reduction.py +185 -89
  4. {gitlabds-2.4.0 → gitlabds-2.4.2}/gitlabds/record_benchmarking.py +34 -3
  5. {gitlabds-2.4.0 → gitlabds-2.4.2}/gitlabds.egg-info/PKG-INFO +31 -7
  6. {gitlabds-2.4.0 → gitlabds-2.4.2}/gitlabds.egg-info/SOURCES.txt +2 -0
  7. gitlabds-2.4.2/tests/test_feature_reduction.py +232 -0
  8. gitlabds-2.4.2/tests/test_record_benchmarking.py +182 -0
  9. {gitlabds-2.4.0 → gitlabds-2.4.2}/LICENSE +0 -0
  10. {gitlabds-2.4.0 → gitlabds-2.4.2}/gitlabds/__init__.py +0 -0
  11. {gitlabds-2.4.0 → gitlabds-2.4.2}/gitlabds/baselines.py +0 -0
  12. {gitlabds-2.4.0 → gitlabds-2.4.2}/gitlabds/config_generator.py +0 -0
  13. {gitlabds-2.4.0 → gitlabds-2.4.2}/gitlabds/dummy.py +0 -0
  14. {gitlabds-2.4.0 → gitlabds-2.4.2}/gitlabds/feature_descriptions.py +0 -0
  15. {gitlabds-2.4.0 → gitlabds-2.4.2}/gitlabds/insights.py +0 -0
  16. {gitlabds-2.4.0 → gitlabds-2.4.2}/gitlabds/memory_optimization.py +0 -0
  17. {gitlabds-2.4.0 → gitlabds-2.4.2}/gitlabds/missing.py +0 -0
  18. {gitlabds-2.4.0 → gitlabds-2.4.2}/gitlabds/model_evaluator.py +0 -0
  19. {gitlabds-2.4.0 → gitlabds-2.4.2}/gitlabds/monitoring_metrics.py +0 -0
  20. {gitlabds-2.4.0 → gitlabds-2.4.2}/gitlabds/outliers.py +0 -0
  21. {gitlabds-2.4.0 → gitlabds-2.4.2}/gitlabds/serving_features.py +0 -0
  22. {gitlabds-2.4.0 → gitlabds-2.4.2}/gitlabds/snowflake_connectors.py +0 -0
  23. {gitlabds-2.4.0 → gitlabds-2.4.2}/gitlabds/split_data.py +0 -0
  24. {gitlabds-2.4.0 → gitlabds-2.4.2}/gitlabds/trends.py +0 -0
  25. {gitlabds-2.4.0 → gitlabds-2.4.2}/gitlabds.egg-info/dependency_links.txt +0 -0
  26. {gitlabds-2.4.0 → gitlabds-2.4.2}/gitlabds.egg-info/requires.txt +0 -0
  27. {gitlabds-2.4.0 → gitlabds-2.4.2}/gitlabds.egg-info/top_level.txt +0 -0
  28. {gitlabds-2.4.0 → gitlabds-2.4.2}/pyproject.toml +0 -0
  29. {gitlabds-2.4.0 → gitlabds-2.4.2}/setup.cfg +0 -0
  30. {gitlabds-2.4.0 → gitlabds-2.4.2}/setup.py +0 -0
  31. {gitlabds-2.4.0 → gitlabds-2.4.2}/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.0
3
+ Version: 2.4.2
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>
@@ -901,7 +914,7 @@ shap_df = gitlabds.per_record_shap(
901
914
  #### Description
902
915
  "Change X -> +Y" counterfactual search, direction chosen empirically. For each actionable lever SHAP surfaced for a record, evaluates candidate values drawn from peer-group quantiles, re-scores the model at each, and keeps the single move that maximizes `gain` — the improvement in the model score, signed by `score_goal` so that bigger is always better. No monotonicity assumption is made.
903
916
 
904
- `gitlabds.per_record_counterfactuals(model, df, shap_df, unique_id="record_id", peer_group=None, candidate_quantiles=[0.1, 0.25, 0.5, 0.75, 0.9], direction=None, score_goal="maximize", actionable_values=["Yes", "Partial"], min_gain=0.005, impact_medium=0.01, impact_high=0.03, levels={"poor fit": 0.03, "good fit": 0.10, "best fit": float("inf")}, n_bins=5):`
917
+ `gitlabds.per_record_counterfactuals(model, df, shap_df, unique_id="record_id", peer_group=None, candidate_quantiles=[0.1, 0.25, 0.5, 0.75, 0.9], direction=None, score_goal="maximize", actionable_values=["Yes", "Partial"], min_gain=0.005, impact_medium=0.01, impact_high=0.03, levels={"poor fit": 0.03, "good fit": 0.10, "best fit": float("inf")}, n_bins=5, prefer_nearest_within=None):`
905
918
 
906
919
  #### Parameters:
907
920
  - _**model**_ : trained model (as in `per_record_shap`). How probabilities are scored is chosen from the model object itself — `.predict_proba()` where available, otherwise a raw `xgboost.Booster` or `lightgbm.Booster` is scored natively. An unsupported type raises a `TypeError` naming it
@@ -913,10 +926,11 @@ shap_df = gitlabds.per_record_shap(
913
926
  - _**direction**_ : optional per-feature constraint mapping feature -> `'increasing'` | `'decreasing'` | `'both'` | `'none'` (dict or Series). `'increasing'`/`'decreasing'` restrict candidates to moves in that direction (an achievability constraint); `'both'` allows either; `'none'` excludes the feature from lever evaluation entirely, for features with no meaningful good direction (categoricals, identifiers, context-only numerics). Features absent from the mapping, or `direction=None`, default to `'both'`. A blank/NaN raises — normalize it to `'none'` where the feature dictionary is read
914
927
  - _**score_goal**_ : `"maximize"` if a higher predicted probability of class 1 is the good outcome (e.g. class 1 = uptier), `"minimize"` if it is the bad one (e.g. class 1 = churn). Sets the sign of `gain`, which drives best-move selection, the `min_gain` filter, `impact_bucket`, and the output ordering. This is the outcome's polarity only — `direction` above is a separate, feature-level concept and is unaffected. Default is `"maximize"`
915
928
  - _**actionable_values**_ : values in `shap_df["actionable"]` treated as actionable levers
916
- - _**min_gain**_ : drop levers whose best achievable gain is below this. Note this is a threshold on `gain`, not on raw `delta_p`: under `score_goal="minimize"` a good move has a negative `delta_p` but a positive `gain`, so this stays positive for both polarities. Negative values (or `-inf`) are allowed, and keep moves that make the outcome worse — useful for inspecting the full candidate distribution while tuning thresholds
929
+ - _**min_gain**_ : drop levers whose best achievable gain is below this. Note this is a threshold on `gain`, not on raw `delta_p`: under `score_goal="minimize"` a good move has a negative `delta_p` but a positive `gain`, so this stays positive for both polarities. Negative values (or `-inf`) are allowed, and keep moves that make the outcome worse — useful for inspecting the full candidate distribution while tuning thresholds. With `prefer_nearest_within` set, selection runs first, so this threshold applies to the gain of the chosen (nearer, lower-gain) candidate rather than the lever's best — a lever whose best gain clears `min_gain` can still be dropped on the nearer move it settled for
917
930
  - _**impact_medium, impact_high**_ : `gain` thresholds bucketing each lever into Low/Medium/High `impact_bucket`. Only meaningful for `gain > 0` — under a negative `min_gain` the retained harmful moves stay `"Low"` (the level-crossing floor doesn't apply to them), so read `gain`'s sign rather than the bucket alone
918
931
  - _**levels**_ : dict mapping label -> upper bound on P(class 1) for that level (e.g., default `{"poor fit": 0.03, "good fit": 0.10, "best fit": float("inf")}` means `p <= 0.03` -> "poor fit", `0.03 < p <= 0.10` -> "good fit", `p > 0.10` -> "best fit"). Key order doesn't matter — levels are sorted by cutoff and labels follow that order — but the top level's cutoff must be `float("inf")`. Cutoffs are always in P(class 1) space regardless of `score_goal`, and `crosses_level` is a plain inequality, so name the labels for your own outcome: a churn model would pass something like `{"low risk": 0.03, "medium risk": 0.10, "high risk": float("inf")}`. Used to detect whether a move crosses a level boundary (any crossing move is floored at "Medium" impact)
919
932
  - _**n_bins**_ : quantile bins for numeric `peer_group` columns
933
+ - _**prefer_nearest_within**_ : `None` (default) keeps the highest-gain candidate per (record, feature). A float in `(0, 1]` instead keeps the *nearest* candidate that still captures that fraction of the best gain: among candidates with `gain >= prefer_nearest_within * best_gain`, the one with the smallest `abs(intervention_value - current_value)` wins, ties broken on higher gain.
920
934
 
921
935
  #### Returns
922
936
  - Long DataFrame, one row per (record, lever): `current_value`, `intervention_value`, `intervention_quantile`, `p_before`, `p_after`, `delta_p`, `gain`, `impact_bucket`, `level_before`, `level_after`, `crosses_level`
@@ -947,6 +961,16 @@ counterfactuals_df = gitlabds.per_record_counterfactuals(
947
961
  direction=dict(zip(feature_dict["field"], feature_dict["good_direction"])),
948
962
  levels={"low risk": 0.03, "medium risk": 0.10, "high risk": float("inf")},
949
963
  )
964
+
965
+ # Prefer a nearer, more sellable move: the closest candidate that still captures
966
+ # at least 70% of the best available gain.
967
+ counterfactuals_df = gitlabds.per_record_counterfactuals(
968
+ model=trained_model,
969
+ df=model_features,
970
+ shap_df=shap_df,
971
+ peer_group=peer_group_df,
972
+ prefer_nearest_within=0.7,
973
+ )
950
974
  ```
951
975
 
952
976
  </details>
@@ -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>
@@ -865,7 +878,7 @@ shap_df = gitlabds.per_record_shap(
865
878
  #### Description
866
879
  "Change X -> +Y" counterfactual search, direction chosen empirically. For each actionable lever SHAP surfaced for a record, evaluates candidate values drawn from peer-group quantiles, re-scores the model at each, and keeps the single move that maximizes `gain` — the improvement in the model score, signed by `score_goal` so that bigger is always better. No monotonicity assumption is made.
867
880
 
868
- `gitlabds.per_record_counterfactuals(model, df, shap_df, unique_id="record_id", peer_group=None, candidate_quantiles=[0.1, 0.25, 0.5, 0.75, 0.9], direction=None, score_goal="maximize", actionable_values=["Yes", "Partial"], min_gain=0.005, impact_medium=0.01, impact_high=0.03, levels={"poor fit": 0.03, "good fit": 0.10, "best fit": float("inf")}, n_bins=5):`
881
+ `gitlabds.per_record_counterfactuals(model, df, shap_df, unique_id="record_id", peer_group=None, candidate_quantiles=[0.1, 0.25, 0.5, 0.75, 0.9], direction=None, score_goal="maximize", actionable_values=["Yes", "Partial"], min_gain=0.005, impact_medium=0.01, impact_high=0.03, levels={"poor fit": 0.03, "good fit": 0.10, "best fit": float("inf")}, n_bins=5, prefer_nearest_within=None):`
869
882
 
870
883
  #### Parameters:
871
884
  - _**model**_ : trained model (as in `per_record_shap`). How probabilities are scored is chosen from the model object itself — `.predict_proba()` where available, otherwise a raw `xgboost.Booster` or `lightgbm.Booster` is scored natively. An unsupported type raises a `TypeError` naming it
@@ -877,10 +890,11 @@ shap_df = gitlabds.per_record_shap(
877
890
  - _**direction**_ : optional per-feature constraint mapping feature -> `'increasing'` | `'decreasing'` | `'both'` | `'none'` (dict or Series). `'increasing'`/`'decreasing'` restrict candidates to moves in that direction (an achievability constraint); `'both'` allows either; `'none'` excludes the feature from lever evaluation entirely, for features with no meaningful good direction (categoricals, identifiers, context-only numerics). Features absent from the mapping, or `direction=None`, default to `'both'`. A blank/NaN raises — normalize it to `'none'` where the feature dictionary is read
878
891
  - _**score_goal**_ : `"maximize"` if a higher predicted probability of class 1 is the good outcome (e.g. class 1 = uptier), `"minimize"` if it is the bad one (e.g. class 1 = churn). Sets the sign of `gain`, which drives best-move selection, the `min_gain` filter, `impact_bucket`, and the output ordering. This is the outcome's polarity only — `direction` above is a separate, feature-level concept and is unaffected. Default is `"maximize"`
879
892
  - _**actionable_values**_ : values in `shap_df["actionable"]` treated as actionable levers
880
- - _**min_gain**_ : drop levers whose best achievable gain is below this. Note this is a threshold on `gain`, not on raw `delta_p`: under `score_goal="minimize"` a good move has a negative `delta_p` but a positive `gain`, so this stays positive for both polarities. Negative values (or `-inf`) are allowed, and keep moves that make the outcome worse — useful for inspecting the full candidate distribution while tuning thresholds
893
+ - _**min_gain**_ : drop levers whose best achievable gain is below this. Note this is a threshold on `gain`, not on raw `delta_p`: under `score_goal="minimize"` a good move has a negative `delta_p` but a positive `gain`, so this stays positive for both polarities. Negative values (or `-inf`) are allowed, and keep moves that make the outcome worse — useful for inspecting the full candidate distribution while tuning thresholds. With `prefer_nearest_within` set, selection runs first, so this threshold applies to the gain of the chosen (nearer, lower-gain) candidate rather than the lever's best — a lever whose best gain clears `min_gain` can still be dropped on the nearer move it settled for
881
894
  - _**impact_medium, impact_high**_ : `gain` thresholds bucketing each lever into Low/Medium/High `impact_bucket`. Only meaningful for `gain > 0` — under a negative `min_gain` the retained harmful moves stay `"Low"` (the level-crossing floor doesn't apply to them), so read `gain`'s sign rather than the bucket alone
882
895
  - _**levels**_ : dict mapping label -> upper bound on P(class 1) for that level (e.g., default `{"poor fit": 0.03, "good fit": 0.10, "best fit": float("inf")}` means `p <= 0.03` -> "poor fit", `0.03 < p <= 0.10` -> "good fit", `p > 0.10` -> "best fit"). Key order doesn't matter — levels are sorted by cutoff and labels follow that order — but the top level's cutoff must be `float("inf")`. Cutoffs are always in P(class 1) space regardless of `score_goal`, and `crosses_level` is a plain inequality, so name the labels for your own outcome: a churn model would pass something like `{"low risk": 0.03, "medium risk": 0.10, "high risk": float("inf")}`. Used to detect whether a move crosses a level boundary (any crossing move is floored at "Medium" impact)
883
896
  - _**n_bins**_ : quantile bins for numeric `peer_group` columns
897
+ - _**prefer_nearest_within**_ : `None` (default) keeps the highest-gain candidate per (record, feature). A float in `(0, 1]` instead keeps the *nearest* candidate that still captures that fraction of the best gain: among candidates with `gain >= prefer_nearest_within * best_gain`, the one with the smallest `abs(intervention_value - current_value)` wins, ties broken on higher gain.
884
898
 
885
899
  #### Returns
886
900
  - Long DataFrame, one row per (record, lever): `current_value`, `intervention_value`, `intervention_quantile`, `p_before`, `p_after`, `delta_p`, `gain`, `impact_bucket`, `level_before`, `level_after`, `crosses_level`
@@ -911,6 +925,16 @@ counterfactuals_df = gitlabds.per_record_counterfactuals(
911
925
  direction=dict(zip(feature_dict["field"], feature_dict["good_direction"])),
912
926
  levels={"low risk": 0.03, "medium risk": 0.10, "high risk": float("inf")},
913
927
  )
928
+
929
+ # Prefer a nearer, more sellable move: the closest candidate that still captures
930
+ # at least 70% of the best available gain.
931
+ counterfactuals_df = gitlabds.per_record_counterfactuals(
932
+ model=trained_model,
933
+ df=model_features,
934
+ shap_df=shap_df,
935
+ peer_group=peer_group_df,
936
+ prefer_nearest_within=0.7,
937
+ )
914
938
  ```
915
939
 
916
940
  </details>