gitlabds 2.1.10__tar.gz → 2.2.0__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.1.10 → gitlabds-2.2.0}/PKG-INFO +186 -2
- {gitlabds-2.1.10 → gitlabds-2.2.0}/README.md +185 -1
- {gitlabds-2.1.10 → gitlabds-2.2.0}/gitlabds/__init__.py +4 -1
- gitlabds-2.2.0/gitlabds/feature_descriptions.py +106 -0
- {gitlabds-2.1.10 → gitlabds-2.2.0}/gitlabds/model_evaluator.py +93 -10
- gitlabds-2.2.0/gitlabds/record_benchmarking.py +360 -0
- {gitlabds-2.1.10 → gitlabds-2.2.0}/gitlabds.egg-info/PKG-INFO +186 -2
- {gitlabds-2.1.10 → gitlabds-2.2.0}/gitlabds.egg-info/SOURCES.txt +2 -0
- {gitlabds-2.1.10 → gitlabds-2.2.0}/LICENSE +0 -0
- {gitlabds-2.1.10 → gitlabds-2.2.0}/gitlabds/baselines.py +0 -0
- {gitlabds-2.1.10 → gitlabds-2.2.0}/gitlabds/config_generator.py +0 -0
- {gitlabds-2.1.10 → gitlabds-2.2.0}/gitlabds/dummy.py +0 -0
- {gitlabds-2.1.10 → gitlabds-2.2.0}/gitlabds/feature_reduction.py +0 -0
- {gitlabds-2.1.10 → gitlabds-2.2.0}/gitlabds/insights.py +0 -0
- {gitlabds-2.1.10 → gitlabds-2.2.0}/gitlabds/memory_optimization.py +0 -0
- {gitlabds-2.1.10 → gitlabds-2.2.0}/gitlabds/missing.py +0 -0
- {gitlabds-2.1.10 → gitlabds-2.2.0}/gitlabds/monitoring_metrics.py +0 -0
- {gitlabds-2.1.10 → gitlabds-2.2.0}/gitlabds/outliers.py +0 -0
- {gitlabds-2.1.10 → gitlabds-2.2.0}/gitlabds/serving_features.py +0 -0
- {gitlabds-2.1.10 → gitlabds-2.2.0}/gitlabds/split_data.py +0 -0
- {gitlabds-2.1.10 → gitlabds-2.2.0}/gitlabds/trends.py +0 -0
- {gitlabds-2.1.10 → gitlabds-2.2.0}/gitlabds.egg-info/dependency_links.txt +0 -0
- {gitlabds-2.1.10 → gitlabds-2.2.0}/gitlabds.egg-info/requires.txt +0 -0
- {gitlabds-2.1.10 → gitlabds-2.2.0}/gitlabds.egg-info/top_level.txt +0 -0
- {gitlabds-2.1.10 → gitlabds-2.2.0}/pyproject.toml +0 -0
- {gitlabds-2.1.10 → gitlabds-2.2.0}/setup.cfg +0 -0
- {gitlabds-2.1.10 → gitlabds-2.2.0}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gitlabds
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: GitLab Data Science Tools
|
|
5
5
|
Author-email: Kevin Dietz <kdietz@gitlab.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -63,7 +63,7 @@ pip install gitlabds
|
|
|
63
63
|
|
|
64
64
|
#### Snowflake Feature Store
|
|
65
65
|
|
|
66
|
-
The `serve_features` and `
|
|
66
|
+
The `serve_features`, `clear_feature_serving_locks`, and `get_feature_descriptions` functions require additional Snowflake dependencies.
|
|
67
67
|
|
|
68
68
|
To use these functions, install with:
|
|
69
69
|
|
|
@@ -815,6 +815,151 @@ results = gitlabds.prescriptions(
|
|
|
815
815
|
```
|
|
816
816
|
</details>
|
|
817
817
|
|
|
818
|
+
### Record Benchmarking
|
|
819
|
+
|
|
820
|
+
<details><summary> Value vs. Peer Status </summary>
|
|
821
|
+
|
|
822
|
+
#### Description
|
|
823
|
+
Status label for a value vs. its peer median (+/- tolerance), not percentile rank.
|
|
824
|
+
|
|
825
|
+
`gitlabds.value_vs_peer_status(value, peer_median, good_direction=None, tolerance=0.15, no_activity_value=None, peer_percentile=None, ceiling_pct=0.95, ceiling_label="near ceiling", labels=("below average", "average", "above average", "No activity")):`
|
|
826
|
+
|
|
827
|
+
#### Parameters:
|
|
828
|
+
- _**value**_ : value(s) to evaluate, scalar or array-like
|
|
829
|
+
- _**peer_median**_ : peer group median(s) to compare against
|
|
830
|
+
- _**good_direction**_ : `"decreasing"` flips which side of the median counts as "above" (e.g., for a metric where lower is better). Anything else is treated as increasing/default.
|
|
831
|
+
- _**tolerance**_ : relative distance from the peer median (as a fraction of the median) required to be classified above/below average rather than "average". Default is 0.15 (a value must be >15% above/below its peer median to leave the "average" band)
|
|
832
|
+
- _**no_activity_value**_ : value that should always map to `labels[3]` ("No activity") regardless of tolerance
|
|
833
|
+
- _**peer_percentile**_ : optional 0-1 percentile rank within the peer group. When provided, an "above average" reading that also sits in the top `ceiling_pct` toward `good_direction` is upgraded to `ceiling_label`
|
|
834
|
+
- _**ceiling_pct**_ : percentile threshold used with `peer_percentile` to detect a "tapped out" strength. Default is 0.95
|
|
835
|
+
- _**ceiling_label**_ : label applied when the ceiling condition is met. Default is "near ceiling"
|
|
836
|
+
- _**labels**_ : tuple of (below average, average, above average, no activity) labels
|
|
837
|
+
|
|
838
|
+
#### Returns
|
|
839
|
+
- Series of status labels, one per input value
|
|
840
|
+
|
|
841
|
+
#### Examples:
|
|
842
|
+
```python
|
|
843
|
+
import gitlabds
|
|
844
|
+
|
|
845
|
+
status = gitlabds.value_vs_peer_status(
|
|
846
|
+
value=df["monthly_spend"],
|
|
847
|
+
peer_median=df["peer_median_spend"],
|
|
848
|
+
peer_percentile=df["peer_percentile_spend"],
|
|
849
|
+
)
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
</details>
|
|
853
|
+
|
|
854
|
+
<details><summary> Per-Record SHAP </summary>
|
|
855
|
+
|
|
856
|
+
#### Description
|
|
857
|
+
Per-record SHAP attribution (log-odds / margin space) for a tree model. Uses tree_path_dependent TreeSHAP, which natively handles the model's categorical splits. Returns the top_n features by |contribution| per record, guaranteeing at least min_actionable actionable ones.
|
|
858
|
+
|
|
859
|
+
`gitlabds.per_record_shap(model, df, feature_dict, unique_id="record_id", top_n=25, min_actionable=3, actionable_values=["Yes", "Partial"]):`
|
|
860
|
+
|
|
861
|
+
#### Parameters:
|
|
862
|
+
- _**model**_ : trained tree-based model (e.g., LightGBM, XGBoost)
|
|
863
|
+
- _**df**_ : model-ready feature matrix, indexed by `unique_id`
|
|
864
|
+
- _**feature_dict**_ : DataFrame or path to CSV with `field` and `actionable` columns
|
|
865
|
+
- _**unique_id**_ : name to use for the record identifier column in the output. Default is "record_id"
|
|
866
|
+
- _**top_n**_ : number of features to return per record, ranked by |contribution|. Default is 25
|
|
867
|
+
- _**min_actionable**_ : minimum number of actionable features guaranteed per record, even if outside the top_n. Default is 3
|
|
868
|
+
- _**actionable_values**_ : values in `feature_dict["actionable"]` treated as actionable
|
|
869
|
+
|
|
870
|
+
#### Returns
|
|
871
|
+
- Long DataFrame, one row per (record, feature): `unique_id`, `rank`, `feature`, `contribution`, `direction`, `feature_value`, `actionable`
|
|
872
|
+
|
|
873
|
+
#### Requirements
|
|
874
|
+
This function requires the `shap` package (included in core dependencies).
|
|
875
|
+
|
|
876
|
+
#### Examples:
|
|
877
|
+
```python
|
|
878
|
+
import gitlabds
|
|
879
|
+
|
|
880
|
+
shap_df = gitlabds.per_record_shap(
|
|
881
|
+
model=trained_model,
|
|
882
|
+
df=model_features,
|
|
883
|
+
feature_dict="feature_dict.csv",
|
|
884
|
+
)
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
</details>
|
|
888
|
+
|
|
889
|
+
<details><summary> Per-Record Counterfactuals </summary>
|
|
890
|
+
|
|
891
|
+
#### Description
|
|
892
|
+
"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 the gain in predicted probability. No monotonicity assumption is made.
|
|
893
|
+
|
|
894
|
+
`gitlabds.per_record_counterfactuals(model, df, shap_df, model_algo="lgb", unique_id="record_id", peer_group=None, candidate_quantiles=[0.1, 0.25, 0.5, 0.75, 0.9], direction=None, actionable_values=["Yes", "Partial"], min_delta_p=0.005, impact_medium=0.01, impact_high=0.03, bands={"poor fit": 0.03, "good fit": 0.10, "best fit": float("inf")}, n_bins=5):`
|
|
895
|
+
|
|
896
|
+
#### Parameters:
|
|
897
|
+
- _**model**_ : trained model (as in `per_record_shap`)
|
|
898
|
+
- _**df**_ : model-ready feature matrix, indexed by `unique_id`
|
|
899
|
+
- _**shap_df**_ : output of `per_record_shap`; scopes which (record, feature) levers to test
|
|
900
|
+
- _**model_algo**_ : `"lgb"` or `"xgb"`/`"xgboost"`; controls how probabilities are scored
|
|
901
|
+
- _**unique_id**_ : name of the record identifier column. Default is "record_id"
|
|
902
|
+
- _**peer_group**_ : Series/DataFrame indexed by `unique_id`; numeric columns are auto-binned into `n_bins` quantile bands. `None` uses one global group
|
|
903
|
+
- _**candidate_quantiles**_ : peer-group quantiles evaluated as candidate values
|
|
904
|
+
- _**direction**_ : optional per-feature achievability constraint mapping feature -> `'increasing'` | `'decreasing'` | `'both'` (dict or Series)
|
|
905
|
+
- _**actionable_values**_ : values in `shap_df["actionable"]` treated as actionable levers
|
|
906
|
+
- _**min_delta_p**_ : drop levers whose best achievable gain is below this
|
|
907
|
+
- _**impact_medium, impact_high**_ : delta_p thresholds bucketing each lever into Low/Medium/High `impact_bucket`
|
|
908
|
+
- _**bands**_ : dict mapping label -> upper bound on `p_before`/`p_after` for that band, ordered worst to best (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 (sorted by cutoff), but the top band's cutoff must be `float("inf")`. Used to detect whether a move crosses a band boundary (any crossing move is floored at "Medium" impact). Pass your own dict (e.g., `{"low": 0.03, "medium": 0.10, "high": float("inf")}`) to match your own pipeline
|
|
909
|
+
- _**n_bins**_ : quantile bins for numeric `peer_group` columns
|
|
910
|
+
|
|
911
|
+
#### Returns
|
|
912
|
+
- Long DataFrame, one row per (record, lever): `current_value`, `intervention_value`, `intervention_quantile`, `p_before`, `p_after`, `delta_p`, `impact_bucket`, `band_before`, `band_after`, `crosses_band`
|
|
913
|
+
|
|
914
|
+
#### Requirements
|
|
915
|
+
For XGBoost models without `predict_proba`, requires the `xgboost` package (included in core dependencies).
|
|
916
|
+
|
|
917
|
+
#### Examples:
|
|
918
|
+
```python
|
|
919
|
+
import gitlabds
|
|
920
|
+
|
|
921
|
+
counterfactuals_df = gitlabds.per_record_counterfactuals(
|
|
922
|
+
model=trained_model,
|
|
923
|
+
df=model_features,
|
|
924
|
+
shap_df=shap_df,
|
|
925
|
+
peer_group=peer_group_df,
|
|
926
|
+
)
|
|
927
|
+
```
|
|
928
|
+
|
|
929
|
+
</details>
|
|
930
|
+
|
|
931
|
+
<details><summary> Per-Record Peer Percentiles </summary>
|
|
932
|
+
|
|
933
|
+
#### Description
|
|
934
|
+
Peer benchmarking. Each record's percentile for each feature within its peer group, plus the group's p25/p50/p75 for context. Pure pandas -- no model required.
|
|
935
|
+
|
|
936
|
+
`gitlabds.per_record_peer_percentiles(df, feature_dict, peer_group=None, unique_id="record_id", actionable_values=["Yes", "Partial"], features=None, n_bins=5):`
|
|
937
|
+
|
|
938
|
+
#### Parameters:
|
|
939
|
+
- _**df**_ : model features DataFrame, indexed by `unique_id`
|
|
940
|
+
- _**feature_dict**_ : DataFrame or path to CSV with `field` and `actionable` columns
|
|
941
|
+
- _**peer_group**_ : Series/DataFrame indexed by `unique_id`; numeric columns are auto-binned into `n_bins` quantile bands. `None` uses one global group
|
|
942
|
+
- _**unique_id**_ : name of the record identifier column. Default is "record_id"
|
|
943
|
+
- _**actionable_values**_ : values in `feature_dict["actionable"]` treated as actionable
|
|
944
|
+
- _**features**_ : optional explicit list of features to benchmark (used as-is, no actionable filter). If `None` (default), computed for every actionable feature in `df`
|
|
945
|
+
- _**n_bins**_ : quantile bins for numeric `peer_group` columns
|
|
946
|
+
|
|
947
|
+
#### Returns
|
|
948
|
+
- Long DataFrame, one row per (record, feature): `unique_id`, `feature`, `record_value`, `peer_group`, `percentile`, `peer_p25`, `peer_p50`, `peer_p75`
|
|
949
|
+
|
|
950
|
+
#### Examples:
|
|
951
|
+
```python
|
|
952
|
+
import gitlabds
|
|
953
|
+
|
|
954
|
+
percentiles_df = gitlabds.per_record_peer_percentiles(
|
|
955
|
+
df=model_features,
|
|
956
|
+
feature_dict="feature_dict.csv",
|
|
957
|
+
peer_group=peer_group_df,
|
|
958
|
+
)
|
|
959
|
+
```
|
|
960
|
+
|
|
961
|
+
</details>
|
|
962
|
+
|
|
818
963
|
### Model Monitoring
|
|
819
964
|
|
|
820
965
|
<details><summary> Generate Baseline Features </summary>
|
|
@@ -1072,6 +1217,45 @@ print(f"Cleared {locks_cleared} locks")
|
|
|
1072
1217
|
|
|
1073
1218
|
</details>
|
|
1074
1219
|
|
|
1220
|
+
<details><summary> Get Feature Descriptions </summary>
|
|
1221
|
+
|
|
1222
|
+
#### Description
|
|
1223
|
+
Look up each feature's source feature view and description from the feature store's registered feature views (column comments set via `FeatureView.attach_feature_desc(...)`). Useful for documenting a model's fields or auditing everything registered in the store.
|
|
1224
|
+
|
|
1225
|
+
`gitlabds.get_feature_descriptions(session, feature_store, features=None):`
|
|
1226
|
+
|
|
1227
|
+
#### Parameters:
|
|
1228
|
+
- **_session_** : Active Snowpark session (e.g., `fs_engine._session`)
|
|
1229
|
+
- **_feature_store_** : Initialized FeatureStore instance (e.g., `fs_engine`); used to enumerate the registered feature views and locate their db/schema
|
|
1230
|
+
- **_features_** : Feature names to look up. If None, returns every feature in the store's registered feature views.
|
|
1231
|
+
|
|
1232
|
+
#### Returns
|
|
1233
|
+
- DataFrame with columns: `feature`, `feature_view`, `data_type`, `description`. Features found in more than one feature view have their views/descriptions joined (`" | "` / `" || "`) so nothing is dropped. When `features` is provided, unmatched features (e.g., transformed or dummy-coded features) come back with blank `feature_view` / `data_type` / `description` for the user to populate manually, and the caller's order/list is preserved.
|
|
1234
|
+
|
|
1235
|
+
#### Requirements
|
|
1236
|
+
This function requires Snowflake packages. Install with:
|
|
1237
|
+
```bash
|
|
1238
|
+
pip install snowflake-snowpark-python snowflake-ml-python
|
|
1239
|
+
```
|
|
1240
|
+
|
|
1241
|
+
#### Examples:
|
|
1242
|
+
|
|
1243
|
+
```python
|
|
1244
|
+
import gitlabds
|
|
1245
|
+
|
|
1246
|
+
# Look up a specific list (e.g. the model fields from a config)
|
|
1247
|
+
desc = gitlabds.get_feature_descriptions(
|
|
1248
|
+
session=fs_engine._session,
|
|
1249
|
+
feature_store=fs_engine,
|
|
1250
|
+
features=scoring_config["model"]["fields"],
|
|
1251
|
+
)
|
|
1252
|
+
|
|
1253
|
+
# Or pull every feature registered in the store
|
|
1254
|
+
all_desc = gitlabds.get_feature_descriptions(session=fs_engine._session, feature_store=fs_engine)
|
|
1255
|
+
```
|
|
1256
|
+
|
|
1257
|
+
</details>
|
|
1258
|
+
|
|
1075
1259
|
### SQL and Trend Analysis
|
|
1076
1260
|
|
|
1077
1261
|
<details><summary> SQL Trend Query Generator </summary>
|
|
@@ -31,7 +31,7 @@ pip install gitlabds
|
|
|
31
31
|
|
|
32
32
|
#### Snowflake Feature Store
|
|
33
33
|
|
|
34
|
-
The `serve_features` and `
|
|
34
|
+
The `serve_features`, `clear_feature_serving_locks`, and `get_feature_descriptions` functions require additional Snowflake dependencies.
|
|
35
35
|
|
|
36
36
|
To use these functions, install with:
|
|
37
37
|
|
|
@@ -783,6 +783,151 @@ results = gitlabds.prescriptions(
|
|
|
783
783
|
```
|
|
784
784
|
</details>
|
|
785
785
|
|
|
786
|
+
### Record Benchmarking
|
|
787
|
+
|
|
788
|
+
<details><summary> Value vs. Peer Status </summary>
|
|
789
|
+
|
|
790
|
+
#### Description
|
|
791
|
+
Status label for a value vs. its peer median (+/- tolerance), not percentile rank.
|
|
792
|
+
|
|
793
|
+
`gitlabds.value_vs_peer_status(value, peer_median, good_direction=None, tolerance=0.15, no_activity_value=None, peer_percentile=None, ceiling_pct=0.95, ceiling_label="near ceiling", labels=("below average", "average", "above average", "No activity")):`
|
|
794
|
+
|
|
795
|
+
#### Parameters:
|
|
796
|
+
- _**value**_ : value(s) to evaluate, scalar or array-like
|
|
797
|
+
- _**peer_median**_ : peer group median(s) to compare against
|
|
798
|
+
- _**good_direction**_ : `"decreasing"` flips which side of the median counts as "above" (e.g., for a metric where lower is better). Anything else is treated as increasing/default.
|
|
799
|
+
- _**tolerance**_ : relative distance from the peer median (as a fraction of the median) required to be classified above/below average rather than "average". Default is 0.15 (a value must be >15% above/below its peer median to leave the "average" band)
|
|
800
|
+
- _**no_activity_value**_ : value that should always map to `labels[3]` ("No activity") regardless of tolerance
|
|
801
|
+
- _**peer_percentile**_ : optional 0-1 percentile rank within the peer group. When provided, an "above average" reading that also sits in the top `ceiling_pct` toward `good_direction` is upgraded to `ceiling_label`
|
|
802
|
+
- _**ceiling_pct**_ : percentile threshold used with `peer_percentile` to detect a "tapped out" strength. Default is 0.95
|
|
803
|
+
- _**ceiling_label**_ : label applied when the ceiling condition is met. Default is "near ceiling"
|
|
804
|
+
- _**labels**_ : tuple of (below average, average, above average, no activity) labels
|
|
805
|
+
|
|
806
|
+
#### Returns
|
|
807
|
+
- Series of status labels, one per input value
|
|
808
|
+
|
|
809
|
+
#### Examples:
|
|
810
|
+
```python
|
|
811
|
+
import gitlabds
|
|
812
|
+
|
|
813
|
+
status = gitlabds.value_vs_peer_status(
|
|
814
|
+
value=df["monthly_spend"],
|
|
815
|
+
peer_median=df["peer_median_spend"],
|
|
816
|
+
peer_percentile=df["peer_percentile_spend"],
|
|
817
|
+
)
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
</details>
|
|
821
|
+
|
|
822
|
+
<details><summary> Per-Record SHAP </summary>
|
|
823
|
+
|
|
824
|
+
#### Description
|
|
825
|
+
Per-record SHAP attribution (log-odds / margin space) for a tree model. Uses tree_path_dependent TreeSHAP, which natively handles the model's categorical splits. Returns the top_n features by |contribution| per record, guaranteeing at least min_actionable actionable ones.
|
|
826
|
+
|
|
827
|
+
`gitlabds.per_record_shap(model, df, feature_dict, unique_id="record_id", top_n=25, min_actionable=3, actionable_values=["Yes", "Partial"]):`
|
|
828
|
+
|
|
829
|
+
#### Parameters:
|
|
830
|
+
- _**model**_ : trained tree-based model (e.g., LightGBM, XGBoost)
|
|
831
|
+
- _**df**_ : model-ready feature matrix, indexed by `unique_id`
|
|
832
|
+
- _**feature_dict**_ : DataFrame or path to CSV with `field` and `actionable` columns
|
|
833
|
+
- _**unique_id**_ : name to use for the record identifier column in the output. Default is "record_id"
|
|
834
|
+
- _**top_n**_ : number of features to return per record, ranked by |contribution|. Default is 25
|
|
835
|
+
- _**min_actionable**_ : minimum number of actionable features guaranteed per record, even if outside the top_n. Default is 3
|
|
836
|
+
- _**actionable_values**_ : values in `feature_dict["actionable"]` treated as actionable
|
|
837
|
+
|
|
838
|
+
#### Returns
|
|
839
|
+
- Long DataFrame, one row per (record, feature): `unique_id`, `rank`, `feature`, `contribution`, `direction`, `feature_value`, `actionable`
|
|
840
|
+
|
|
841
|
+
#### Requirements
|
|
842
|
+
This function requires the `shap` package (included in core dependencies).
|
|
843
|
+
|
|
844
|
+
#### Examples:
|
|
845
|
+
```python
|
|
846
|
+
import gitlabds
|
|
847
|
+
|
|
848
|
+
shap_df = gitlabds.per_record_shap(
|
|
849
|
+
model=trained_model,
|
|
850
|
+
df=model_features,
|
|
851
|
+
feature_dict="feature_dict.csv",
|
|
852
|
+
)
|
|
853
|
+
```
|
|
854
|
+
|
|
855
|
+
</details>
|
|
856
|
+
|
|
857
|
+
<details><summary> Per-Record Counterfactuals </summary>
|
|
858
|
+
|
|
859
|
+
#### Description
|
|
860
|
+
"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 the gain in predicted probability. No monotonicity assumption is made.
|
|
861
|
+
|
|
862
|
+
`gitlabds.per_record_counterfactuals(model, df, shap_df, model_algo="lgb", unique_id="record_id", peer_group=None, candidate_quantiles=[0.1, 0.25, 0.5, 0.75, 0.9], direction=None, actionable_values=["Yes", "Partial"], min_delta_p=0.005, impact_medium=0.01, impact_high=0.03, bands={"poor fit": 0.03, "good fit": 0.10, "best fit": float("inf")}, n_bins=5):`
|
|
863
|
+
|
|
864
|
+
#### Parameters:
|
|
865
|
+
- _**model**_ : trained model (as in `per_record_shap`)
|
|
866
|
+
- _**df**_ : model-ready feature matrix, indexed by `unique_id`
|
|
867
|
+
- _**shap_df**_ : output of `per_record_shap`; scopes which (record, feature) levers to test
|
|
868
|
+
- _**model_algo**_ : `"lgb"` or `"xgb"`/`"xgboost"`; controls how probabilities are scored
|
|
869
|
+
- _**unique_id**_ : name of the record identifier column. Default is "record_id"
|
|
870
|
+
- _**peer_group**_ : Series/DataFrame indexed by `unique_id`; numeric columns are auto-binned into `n_bins` quantile bands. `None` uses one global group
|
|
871
|
+
- _**candidate_quantiles**_ : peer-group quantiles evaluated as candidate values
|
|
872
|
+
- _**direction**_ : optional per-feature achievability constraint mapping feature -> `'increasing'` | `'decreasing'` | `'both'` (dict or Series)
|
|
873
|
+
- _**actionable_values**_ : values in `shap_df["actionable"]` treated as actionable levers
|
|
874
|
+
- _**min_delta_p**_ : drop levers whose best achievable gain is below this
|
|
875
|
+
- _**impact_medium, impact_high**_ : delta_p thresholds bucketing each lever into Low/Medium/High `impact_bucket`
|
|
876
|
+
- _**bands**_ : dict mapping label -> upper bound on `p_before`/`p_after` for that band, ordered worst to best (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 (sorted by cutoff), but the top band's cutoff must be `float("inf")`. Used to detect whether a move crosses a band boundary (any crossing move is floored at "Medium" impact). Pass your own dict (e.g., `{"low": 0.03, "medium": 0.10, "high": float("inf")}`) to match your own pipeline
|
|
877
|
+
- _**n_bins**_ : quantile bins for numeric `peer_group` columns
|
|
878
|
+
|
|
879
|
+
#### Returns
|
|
880
|
+
- Long DataFrame, one row per (record, lever): `current_value`, `intervention_value`, `intervention_quantile`, `p_before`, `p_after`, `delta_p`, `impact_bucket`, `band_before`, `band_after`, `crosses_band`
|
|
881
|
+
|
|
882
|
+
#### Requirements
|
|
883
|
+
For XGBoost models without `predict_proba`, requires the `xgboost` package (included in core dependencies).
|
|
884
|
+
|
|
885
|
+
#### Examples:
|
|
886
|
+
```python
|
|
887
|
+
import gitlabds
|
|
888
|
+
|
|
889
|
+
counterfactuals_df = gitlabds.per_record_counterfactuals(
|
|
890
|
+
model=trained_model,
|
|
891
|
+
df=model_features,
|
|
892
|
+
shap_df=shap_df,
|
|
893
|
+
peer_group=peer_group_df,
|
|
894
|
+
)
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
</details>
|
|
898
|
+
|
|
899
|
+
<details><summary> Per-Record Peer Percentiles </summary>
|
|
900
|
+
|
|
901
|
+
#### Description
|
|
902
|
+
Peer benchmarking. Each record's percentile for each feature within its peer group, plus the group's p25/p50/p75 for context. Pure pandas -- no model required.
|
|
903
|
+
|
|
904
|
+
`gitlabds.per_record_peer_percentiles(df, feature_dict, peer_group=None, unique_id="record_id", actionable_values=["Yes", "Partial"], features=None, n_bins=5):`
|
|
905
|
+
|
|
906
|
+
#### Parameters:
|
|
907
|
+
- _**df**_ : model features DataFrame, indexed by `unique_id`
|
|
908
|
+
- _**feature_dict**_ : DataFrame or path to CSV with `field` and `actionable` columns
|
|
909
|
+
- _**peer_group**_ : Series/DataFrame indexed by `unique_id`; numeric columns are auto-binned into `n_bins` quantile bands. `None` uses one global group
|
|
910
|
+
- _**unique_id**_ : name of the record identifier column. Default is "record_id"
|
|
911
|
+
- _**actionable_values**_ : values in `feature_dict["actionable"]` treated as actionable
|
|
912
|
+
- _**features**_ : optional explicit list of features to benchmark (used as-is, no actionable filter). If `None` (default), computed for every actionable feature in `df`
|
|
913
|
+
- _**n_bins**_ : quantile bins for numeric `peer_group` columns
|
|
914
|
+
|
|
915
|
+
#### Returns
|
|
916
|
+
- Long DataFrame, one row per (record, feature): `unique_id`, `feature`, `record_value`, `peer_group`, `percentile`, `peer_p25`, `peer_p50`, `peer_p75`
|
|
917
|
+
|
|
918
|
+
#### Examples:
|
|
919
|
+
```python
|
|
920
|
+
import gitlabds
|
|
921
|
+
|
|
922
|
+
percentiles_df = gitlabds.per_record_peer_percentiles(
|
|
923
|
+
df=model_features,
|
|
924
|
+
feature_dict="feature_dict.csv",
|
|
925
|
+
peer_group=peer_group_df,
|
|
926
|
+
)
|
|
927
|
+
```
|
|
928
|
+
|
|
929
|
+
</details>
|
|
930
|
+
|
|
786
931
|
### Model Monitoring
|
|
787
932
|
|
|
788
933
|
<details><summary> Generate Baseline Features </summary>
|
|
@@ -1040,6 +1185,45 @@ print(f"Cleared {locks_cleared} locks")
|
|
|
1040
1185
|
|
|
1041
1186
|
</details>
|
|
1042
1187
|
|
|
1188
|
+
<details><summary> Get Feature Descriptions </summary>
|
|
1189
|
+
|
|
1190
|
+
#### Description
|
|
1191
|
+
Look up each feature's source feature view and description from the feature store's registered feature views (column comments set via `FeatureView.attach_feature_desc(...)`). Useful for documenting a model's fields or auditing everything registered in the store.
|
|
1192
|
+
|
|
1193
|
+
`gitlabds.get_feature_descriptions(session, feature_store, features=None):`
|
|
1194
|
+
|
|
1195
|
+
#### Parameters:
|
|
1196
|
+
- **_session_** : Active Snowpark session (e.g., `fs_engine._session`)
|
|
1197
|
+
- **_feature_store_** : Initialized FeatureStore instance (e.g., `fs_engine`); used to enumerate the registered feature views and locate their db/schema
|
|
1198
|
+
- **_features_** : Feature names to look up. If None, returns every feature in the store's registered feature views.
|
|
1199
|
+
|
|
1200
|
+
#### Returns
|
|
1201
|
+
- DataFrame with columns: `feature`, `feature_view`, `data_type`, `description`. Features found in more than one feature view have their views/descriptions joined (`" | "` / `" || "`) so nothing is dropped. When `features` is provided, unmatched features (e.g., transformed or dummy-coded features) come back with blank `feature_view` / `data_type` / `description` for the user to populate manually, and the caller's order/list is preserved.
|
|
1202
|
+
|
|
1203
|
+
#### Requirements
|
|
1204
|
+
This function requires Snowflake packages. Install with:
|
|
1205
|
+
```bash
|
|
1206
|
+
pip install snowflake-snowpark-python snowflake-ml-python
|
|
1207
|
+
```
|
|
1208
|
+
|
|
1209
|
+
#### Examples:
|
|
1210
|
+
|
|
1211
|
+
```python
|
|
1212
|
+
import gitlabds
|
|
1213
|
+
|
|
1214
|
+
# Look up a specific list (e.g. the model fields from a config)
|
|
1215
|
+
desc = gitlabds.get_feature_descriptions(
|
|
1216
|
+
session=fs_engine._session,
|
|
1217
|
+
feature_store=fs_engine,
|
|
1218
|
+
features=scoring_config["model"]["fields"],
|
|
1219
|
+
)
|
|
1220
|
+
|
|
1221
|
+
# Or pull every feature registered in the store
|
|
1222
|
+
all_desc = gitlabds.get_feature_descriptions(session=fs_engine._session, feature_store=fs_engine)
|
|
1223
|
+
```
|
|
1224
|
+
|
|
1225
|
+
</details>
|
|
1226
|
+
|
|
1043
1227
|
### SQL and Trend Analysis
|
|
1044
1228
|
|
|
1045
1229
|
<details><summary> SQL Trend Query Generator </summary>
|
|
@@ -3,6 +3,7 @@ from .config_generator import ConfigGenerator
|
|
|
3
3
|
from .dummy import dummy_code, dummy_top, apply_dummy
|
|
4
4
|
from .feature_reduction import drop_categorical, remove_low_variation, remove_outcome_proxies, correlation_reduction
|
|
5
5
|
from .insights import marginal_effects, prescriptions
|
|
6
|
+
from .record_benchmarking import value_vs_peer_status, per_record_shap, per_record_counterfactuals, per_record_peer_percentiles
|
|
6
7
|
from .memory_optimization import reduce_memory_usage, sparse_encode_df, identify_categorical_candidates, compact_categorical, memory_profile_df, memory_optimization
|
|
7
8
|
from .missing import missing_values, apply_missing_values
|
|
8
9
|
from .model_evaluator import ModelEvaluator
|
|
@@ -14,6 +15,7 @@ from .trends import generate_sql_trend_query, trend_analysis
|
|
|
14
15
|
# Optional imports for Snowflake feature serving
|
|
15
16
|
try:
|
|
16
17
|
from .serving_features import clear_feature_serving_locks, serve_features
|
|
18
|
+
from .feature_descriptions import get_feature_descriptions
|
|
17
19
|
except ImportError as e:
|
|
18
20
|
# Snowflake packages not installed - create placeholder functions with helpful error messages
|
|
19
21
|
def _snowflake_not_installed(*args, **kwargs):
|
|
@@ -21,7 +23,8 @@ except ImportError as e:
|
|
|
21
23
|
"Snowflake feature serving functions require additional dependencies. "
|
|
22
24
|
"Install them with: pip install snowflake-snowpark-python snowflake-ml-python\n\n"
|
|
23
25
|
)
|
|
24
|
-
|
|
26
|
+
|
|
25
27
|
clear_feature_serving_locks = _snowflake_not_installed
|
|
26
28
|
serve_features = _snowflake_not_installed
|
|
29
|
+
get_feature_descriptions = _snowflake_not_installed
|
|
27
30
|
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Feature description lookup for Snowflake Feature Store integration.
|
|
3
|
+
|
|
4
|
+
Given a list of feature names, return each feature's source feature view and its
|
|
5
|
+
description (pulled from the registered feature views' column comments, which is
|
|
6
|
+
where FeatureView.attach_feature_desc(...) stores them). Follows the same call
|
|
7
|
+
pattern as gitlabds.serve_features: pass the feature-store session and object.
|
|
8
|
+
|
|
9
|
+
Note: This module requires snowflake-snowpark-python and snowflake-ml-python packages.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import Iterable, Optional
|
|
13
|
+
import pandas as pd
|
|
14
|
+
|
|
15
|
+
# Optional imports with graceful fallback
|
|
16
|
+
try:
|
|
17
|
+
from snowflake.snowpark import Session
|
|
18
|
+
from snowflake.ml.feature_store import FeatureStore
|
|
19
|
+
SNOWFLAKE_AVAILABLE = True
|
|
20
|
+
except ImportError:
|
|
21
|
+
SNOWFLAKE_AVAILABLE = False
|
|
22
|
+
Session = None
|
|
23
|
+
FeatureStore = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _check_snowflake_available():
|
|
27
|
+
"""Check if Snowflake packages are available."""
|
|
28
|
+
if not SNOWFLAKE_AVAILABLE:
|
|
29
|
+
raise ImportError(
|
|
30
|
+
"Snowflake packages are required for feature descriptions. "
|
|
31
|
+
"Install with: pip install snowflake-snowpark-python snowflake-ml-python"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_feature_descriptions(
|
|
36
|
+
session: 'Session',
|
|
37
|
+
feature_store: 'FeatureStore',
|
|
38
|
+
features: Optional[Iterable[str]] = None,
|
|
39
|
+
) -> pd.DataFrame:
|
|
40
|
+
"""
|
|
41
|
+
Look up each feature's source feature view and description from the feature
|
|
42
|
+
store's registered feature views (column comments set via
|
|
43
|
+
FeatureView.attach_feature_desc(...)).
|
|
44
|
+
|
|
45
|
+
Parameters
|
|
46
|
+
----------
|
|
47
|
+
session : snowflake.snowpark.Session
|
|
48
|
+
Active Snowpark session (e.g., fs_engine._session).
|
|
49
|
+
feature_store : snowflake.ml.feature_store.FeatureStore
|
|
50
|
+
Initialized FeatureStore instance (e.g., fs_engine); used to enumerate the
|
|
51
|
+
registered feature views and to locate their db/schema.
|
|
52
|
+
features : Iterable[str], optional
|
|
53
|
+
Feature names to look up. If None, returns every feature in the store's
|
|
54
|
+
registered feature views.
|
|
55
|
+
|
|
56
|
+
Returns
|
|
57
|
+
-------
|
|
58
|
+
pd.DataFrame
|
|
59
|
+
Columns: [feature, feature_view, data_type, description]. Features found
|
|
60
|
+
in more than one feature view have their views/descriptions joined
|
|
61
|
+
(" | " / " || ") so nothing is dropped. When `features` is provided,
|
|
62
|
+
unmatched features (e.g., transformed or dummy-coded features) come back
|
|
63
|
+
with blank feature_view / data_type / description for the user to
|
|
64
|
+
populate manually, and order/list is preserved.
|
|
65
|
+
"""
|
|
66
|
+
_check_snowflake_available()
|
|
67
|
+
|
|
68
|
+
# Registered feature views -> exact published table names (e.g. ACCOUNT_CI_USAGE$1.0).
|
|
69
|
+
fvs = feature_store.list_feature_views().select("NAME", "VERSION").to_pandas()
|
|
70
|
+
tables = [f"{n}${v}" for n, v in zip(fvs["NAME"], fvs["VERSION"])]
|
|
71
|
+
|
|
72
|
+
# Database/schema come from the live feature-store session, not hard-coded.
|
|
73
|
+
# Pulled directly rather than parsed out of get_fully_qualified_current_schema(),
|
|
74
|
+
# which would require quote-aware splitting to handle identifiers containing ".".
|
|
75
|
+
database = session.get_current_database().strip('"')
|
|
76
|
+
schema = session.get_current_schema().strip('"')
|
|
77
|
+
|
|
78
|
+
def sql_list(values):
|
|
79
|
+
return ",".join("'" + str(v).replace("'", "''") + "'" for v in values)
|
|
80
|
+
|
|
81
|
+
meta = session.sql(
|
|
82
|
+
"SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, COMMENT "
|
|
83
|
+
f"FROM {database}.INFORMATION_SCHEMA.COLUMNS "
|
|
84
|
+
f"WHERE TABLE_SCHEMA = '{schema}' AND TABLE_NAME IN ({sql_list(tables)})"
|
|
85
|
+
).to_pandas()
|
|
86
|
+
meta.columns = [c.upper() for c in meta.columns]
|
|
87
|
+
|
|
88
|
+
meta["feature_view"] = meta["TABLE_NAME"].str.split("$").str[0]
|
|
89
|
+
meta["feature"] = meta["COLUMN_NAME"].str.lower()
|
|
90
|
+
|
|
91
|
+
catalog = meta.groupby("feature", as_index=False).agg(
|
|
92
|
+
feature_view=("feature_view", lambda s: " | ".join(sorted(set(s)))),
|
|
93
|
+
data_type=("DATA_TYPE", lambda s: " | ".join(sorted(set(s.dropna().astype(str))))),
|
|
94
|
+
description=("COMMENT", lambda s: " || ".join(dict.fromkeys(x for x in s if x))),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if features is None:
|
|
98
|
+
return catalog.sort_values("feature").reset_index(drop=True)
|
|
99
|
+
|
|
100
|
+
# Preserve the caller's feature list/order; blanks where unmatched.
|
|
101
|
+
requested = pd.DataFrame({"feature": list(features)})
|
|
102
|
+
requested["_key"] = requested["feature"].str.lower()
|
|
103
|
+
out = requested.merge(
|
|
104
|
+
catalog.rename(columns={"feature": "_key"}), on="_key", how="left"
|
|
105
|
+
).drop(columns="_key")
|
|
106
|
+
return out[["feature", "feature_view", "data_type", "description"]].fillna("")
|