initial upload of modeling pipeline and summary
Browse files
README.md
ADDED
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
license: mit
|
3 |
+
tags:
|
4 |
+
- scikit-learn
|
5 |
+
- tabular
|
6 |
+
- nonprofit
|
7 |
+
- planned-giving
|
8 |
+
- classification
|
9 |
+
- snowflake
|
10 |
+
---
|
11 |
+
|
12 |
+
# Planned Giving Propensity Model
|
13 |
+
|
14 |
+
A machine learning solution to optimize planned giving donor targeting for the National Parks Conservation Association (NPCA).
|
15 |
+
|
16 |
+
> **Note**: This model is not currently deployed or downloadable due to data privacy constraints. This repository shares the modeling approach, evaluation strategy, and relevant pipeline components for reproducibility and educational use.
|
17 |
+
|
18 |
+
## Project Overview
|
19 |
+
|
20 |
+
This project implements a Random Forest classifier to identify potential planned giving donors, with the goal of improving mailing efficiency and response rates. The model processes donor data through Snowflake’s computing infrastructure and uses SMOTE to handle class imbalance.
|
21 |
+
|
22 |
+
## Key Results
|
23 |
+
|
24 |
+
- **PR-AUC**: 0.88 — strong performance on imbalanced data
|
25 |
+
- **F1 Score**: 0.8125
|
26 |
+
- **Precision**: 0.7558
|
27 |
+
- **Recall**: 0.8784 — high capture rate of known planned givers
|
28 |
+
- **1,019 new high-potential donor predictions** for targeted outreach
|
29 |
+
|
30 |
+
## Technical Implementation
|
31 |
+
|
32 |
+
### Data Pipeline
|
33 |
+
- Donor data extracted from CRM into Snowflake
|
34 |
+
- Modular Python scripts for feature engineering and cleaning
|
35 |
+
- SMOTE oversampling to address class imbalance
|
36 |
+
|
37 |
+
### Machine Learning
|
38 |
+
- Random Forest classifier with `scikit-learn`
|
39 |
+
- Stratified cross-validation and grid search
|
40 |
+
- Multiple imputation strategies (MICE, mean, median)
|
41 |
+
- Key temporal features (e.g., time since last gift)
|
42 |
+
|
43 |
+
📂 [Training Script](./model/split_SMOTE_crossval.py)
|
44 |
+
📓 [Evaluation Notebook](./model/snowflake_model_evaluation.py)
|
45 |
+
|
46 |
+
## Model Performance Insights
|
47 |
+
|
48 |
+
Post-modeling analysis validated predictions against known donor engagement indicators:
|
49 |
+
|
50 |
+
- **66.3%** of predicted donors were already flagged as prospects by fundraisers
|
51 |
+
- **37.6%** are major donor households
|
52 |
+
- **18%** are members of the Mather Legacy Society
|
53 |
+
|
54 |
+
### Top 5 Most Important Features
|
55 |
+
1. Highest Previous Contribution (22.8%)
|
56 |
+
2. Most Recent Contribution (20.1%)
|
57 |
+
3. Years Since HPC Gift (14.6%)
|
58 |
+
4. Total Amount (14.3%)
|
59 |
+
5. Years Since MRC Gift (11.2%)
|
60 |
+
|
61 |
+
### Demographics of Predicted Donors
|
62 |
+
- Average age: 69
|
63 |
+
- Giving history: 16 years (on average)
|
64 |
+
- Median total giving: $10,932
|
65 |
+
- Average number of transactions: 18
|
66 |
+
|
67 |
+
## Tools and Technologies
|
68 |
+
|
69 |
+
- `scikit-learn`, `pandas`, `numpy`
|
70 |
+
- Snowflake
|
71 |
+
- `imbalanced-learn`, `matplotlib`, `seaborn`
|
72 |
+
|
73 |
+
## Repository Structure
|
74 |
+
|
75 |
+
```plaintext
|
76 |
+
├── model/
|
77 |
+
│ ├── split_SMOTE_crossval.py # ML model executed on Snowflake
|
78 |
+
│ └── snowflake_model_evaluation.py # Model evaluation and visualization
|
79 |
+
├── predictions_analyzed/ # Post-modeling analysis
|
80 |
+
│ ├── predictions_analyzed.ipynb # Model concurrence evaluation
|
81 |
+
├── requirements.txt
|
82 |
+
└── README.md
|
83 |
+
```
|
84 |
+
|
85 |
+
## Potential Future Improvements
|
86 |
+
- Schedule automated data refresh and model retraining
|
87 |
+
- Incorporate additional feature engineering
|
88 |
+
- Develop dashboard for tracking model performance
|
89 |
+
|
90 |
+
*Note: Full project repository: [GitHub – dbouquin/bequest_modeling](https://github.com/dbouquin/bequest_modeling)*
|
model/snowflake_model_evaluation.ipynb
ADDED
The diff for this file is too large to render.
See raw diff
|
|
model/split_SMOTE_crossval.py
ADDED
@@ -0,0 +1,166 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import snowflake.snowpark as snowpark
|
2 |
+
from snowflake.snowpark.functions import col
|
3 |
+
from sklearn.experimental import enable_iterative_imputer
|
4 |
+
from sklearn.impute import SimpleImputer, IterativeImputer
|
5 |
+
from imblearn.over_sampling import SMOTE
|
6 |
+
from sklearn.ensemble import RandomForestClassifier
|
7 |
+
from sklearn.metrics import classification_report, accuracy_score, precision_recall_curve, auc
|
8 |
+
from sklearn.model_selection import StratifiedKFold
|
9 |
+
import pandas as pd
|
10 |
+
import numpy as np
|
11 |
+
import json
|
12 |
+
|
13 |
+
def main(session: snowpark.Session):
|
14 |
+
# Load data from table
|
15 |
+
df = session.table("PUBLIC.BEQUESTS_CLEAN").to_pandas()
|
16 |
+
|
17 |
+
# Define imputers (only mean and mice)
|
18 |
+
imputers = {
|
19 |
+
'mean': SimpleImputer(strategy='mean'),
|
20 |
+
'median': SimpleImputer(strategy='median'),
|
21 |
+
'mice': IterativeImputer(random_state=42)
|
22 |
+
}
|
23 |
+
|
24 |
+
# Store results
|
25 |
+
results_dead = []
|
26 |
+
results_alive = []
|
27 |
+
results_modeling = []
|
28 |
+
|
29 |
+
# Function to evaluate imputation method
|
30 |
+
def evaluate_imputation(df, imputer_name, imputer):
|
31 |
+
# Impute BIRTH_YEAR
|
32 |
+
df['BIRTH_YEAR'] = imputer.fit_transform(df[['BIRTH_YEAR']])
|
33 |
+
|
34 |
+
# Encode categorical variables
|
35 |
+
df = pd.get_dummies(df, columns=['REGION_CODE'], drop_first=True)
|
36 |
+
|
37 |
+
# Define features after one-hot encoding
|
38 |
+
feature_columns = [
|
39 |
+
'TOTAL_TRANSACTIONS',
|
40 |
+
'TOTAL_AMOUNT',
|
41 |
+
'FIRST_GIFT_AMOUNT',
|
42 |
+
'MRC_AMOUNT',
|
43 |
+
'HPC_AMOUNT',
|
44 |
+
'YEARS_SINCE_FIRST_GIFT',
|
45 |
+
'YEARS_SINCE_MRC_GIFT',
|
46 |
+
'YEARS_SINCE_HPC_GIFT',
|
47 |
+
'BIRTH_YEAR'
|
48 |
+
] + [col for col in df.columns if col.startswith('REGION_CODE_')]
|
49 |
+
|
50 |
+
# Separate dead and alive individuals
|
51 |
+
df_dead = df[df['DEATH_FLAG'] == 1]
|
52 |
+
df_alive = df[df['DEATH_FLAG'] == 0]
|
53 |
+
|
54 |
+
# Train model on dead individuals
|
55 |
+
if len(df_dead) > 0:
|
56 |
+
X_dead = df_dead[feature_columns]
|
57 |
+
y_dead = df_dead['BEQUEST_RECEIVED']
|
58 |
+
ROI_FAMILY_ID_dead = df_dead['ROI_FAMILY_ID']
|
59 |
+
|
60 |
+
# Cross-validation setup
|
61 |
+
skf = StratifiedKFold(n_splits=3, shuffle=True, random_state=42)
|
62 |
+
smote = SMOTE(random_state=42)
|
63 |
+
model = RandomForestClassifier(random_state=42, n_jobs=-1) # Use all available cores
|
64 |
+
|
65 |
+
# Cross-validated predictions
|
66 |
+
y_pred_dead = np.zeros(len(y_dead))
|
67 |
+
y_pred_proba_dead = np.zeros(len(y_dead))
|
68 |
+
for train_index, test_index in skf.split(X_dead, y_dead):
|
69 |
+
X_train, X_test = X_dead.iloc[train_index], X_dead.iloc[test_index]
|
70 |
+
y_train, y_test = y_dead.iloc[train_index], y_dead.iloc[test_index]
|
71 |
+
|
72 |
+
X_train_res, y_train_res = smote.fit_resample(X_train, y_train)
|
73 |
+
model.fit(X_train_res, y_train_res)
|
74 |
+
y_pred_dead[test_index] = model.predict(X_test)
|
75 |
+
y_pred_proba_dead[test_index] = model.predict_proba(X_test)[:, 1] # Probability for class 1
|
76 |
+
|
77 |
+
# Evaluation for dead individuals
|
78 |
+
accuracy_dead = accuracy_score(y_dead, y_pred_dead)
|
79 |
+
precision_dead, recall_dead, _ = precision_recall_curve(y_dead, y_pred_proba_dead)
|
80 |
+
auc_pr_dead = auc(recall_dead, precision_dead)
|
81 |
+
report_dead = classification_report(y_dead, y_pred_dead, output_dict=True)
|
82 |
+
model.fit(X_dead, y_dead)
|
83 |
+
feature_importance_dead = pd.DataFrame({
|
84 |
+
'Feature': X_dead.columns,
|
85 |
+
'Importance': model.feature_importances_
|
86 |
+
}).sort_values(by='Importance', ascending=False)
|
87 |
+
|
88 |
+
results_dead.append({
|
89 |
+
'imputer': imputer_name,
|
90 |
+
'accuracy': accuracy_dead,
|
91 |
+
'auc_pr': auc_pr_dead,
|
92 |
+
'report': pd.DataFrame(report_dead).transpose(),
|
93 |
+
'feature_importance': feature_importance_dead,
|
94 |
+
'ROI_FAMILY_ID': ROI_FAMILY_ID_dead,
|
95 |
+
'y_true': y_dead,
|
96 |
+
'y_pred': y_pred_dead
|
97 |
+
})
|
98 |
+
|
99 |
+
results_modeling.append({
|
100 |
+
'imputer': imputer_name,
|
101 |
+
'accuracy': accuracy_dead,
|
102 |
+
'auc_pr': auc_pr_dead,
|
103 |
+
'classification_report': json.dumps(report_dead),
|
104 |
+
'feature_importance': feature_importance_dead.to_dict(orient='list')
|
105 |
+
})
|
106 |
+
|
107 |
+
# Predict on alive individuals
|
108 |
+
if len(df_alive) > 0:
|
109 |
+
X_alive = df_alive[feature_columns]
|
110 |
+
y_pred_alive = model.predict(X_alive)
|
111 |
+
ROI_FAMILY_ID_alive = df_alive['ROI_FAMILY_ID']
|
112 |
+
|
113 |
+
results_alive.append({
|
114 |
+
'imputer': imputer_name,
|
115 |
+
'ROI_FAMILY_ID': ROI_FAMILY_ID_alive,
|
116 |
+
'y_pred': y_pred_alive
|
117 |
+
})
|
118 |
+
|
119 |
+
# Evaluate each imputation method
|
120 |
+
for imputer_name, imputer in imputers.items():
|
121 |
+
evaluate_imputation(df.copy(), imputer_name, imputer)
|
122 |
+
|
123 |
+
# Print the modeling results for dead individuals
|
124 |
+
for result in results_dead:
|
125 |
+
print(f"Imputer: {result['imputer']} (Dead)")
|
126 |
+
print("Accuracy:", result['accuracy'])
|
127 |
+
print("AUC-PR:", result['auc_pr'])
|
128 |
+
print("Classification Report:")
|
129 |
+
print(result['report'])
|
130 |
+
print("Feature Importance:")
|
131 |
+
print(result['feature_importance'])
|
132 |
+
print("\n" + "-"*50 + "\n")
|
133 |
+
|
134 |
+
# Combine all dead predictions into a single DataFrame
|
135 |
+
predictions_dead_df = pd.concat([
|
136 |
+
pd.DataFrame({
|
137 |
+
'ROI_FAMILY_ID': result['ROI_FAMILY_ID'],
|
138 |
+
'imputer': result['imputer'],
|
139 |
+
'y_true': result['y_true'],
|
140 |
+
'y_pred': result['y_pred'],
|
141 |
+
'status': 'dead'
|
142 |
+
}) for result in results_dead
|
143 |
+
], ignore_index=True)
|
144 |
+
|
145 |
+
# Combine all alive predictions into a single DataFrame
|
146 |
+
predictions_alive_df = pd.concat([
|
147 |
+
pd.DataFrame({
|
148 |
+
'ROI_FAMILY_ID': result['ROI_FAMILY_ID'],
|
149 |
+
'imputer': result['imputer'],
|
150 |
+
'y_pred': result['y_pred'],
|
151 |
+
'status': 'alive'
|
152 |
+
}) for result in results_alive
|
153 |
+
], ignore_index=True)
|
154 |
+
|
155 |
+
# Write the dead predictions DataFrame to a new table
|
156 |
+
session.write_pandas(predictions_dead_df, 'BEQUEST_PREDICTIONS_DEAD', auto_create_table=True)
|
157 |
+
|
158 |
+
# Write the alive predictions DataFrame to a new table
|
159 |
+
session.write_pandas(predictions_alive_df, 'BEQUEST_PREDICTIONS_ALIVE', auto_create_table=True)
|
160 |
+
|
161 |
+
# Write the modeling results to a new table
|
162 |
+
modeling_results_df = pd.DataFrame(results_modeling)
|
163 |
+
session.write_pandas(modeling_results_df, 'BEQUEST_MODELING_RESULTS', auto_create_table=True)
|
164 |
+
|
165 |
+
# Return string
|
166 |
+
return "Data processing, prediction, and table creation completed successfully."
|
predictions_analyzed/predictions_analyzed.ipynb
ADDED
The diff for this file is too large to render.
See raw diff
|
|
predictions_analyzed/predictions_existing_flags.sql
ADDED
@@ -0,0 +1,222 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
WITH
|
2 |
+
pred_beq AS (
|
3 |
+
SELECT ROIFAMILYID
|
4 |
+
FROM UP_16425_1193735
|
5 |
+
),
|
6 |
+
|
7 |
+
account_profile_family AS (
|
8 |
+
SELECT roi_id,
|
9 |
+
roi_family_id
|
10 |
+
FROM v_account_profile_family apf
|
11 |
+
WHERE EXISTS (
|
12 |
+
SELECT *
|
13 |
+
FROM pred_beq pb
|
14 |
+
WHERE pb.ROIFAMILYID = apf.roi_family_id
|
15 |
+
)
|
16 |
+
),
|
17 |
+
|
18 |
+
account_profile AS (
|
19 |
+
SELECT roi_id, -- Added roi_id here to reference it later
|
20 |
+
account_classification
|
21 |
+
FROM v_account_profile ap
|
22 |
+
WHERE EXISTS (
|
23 |
+
SELECT *
|
24 |
+
FROM account_profile_family apf
|
25 |
+
WHERE apf.roi_id = ap.roi_id
|
26 |
+
)
|
27 |
+
),
|
28 |
+
|
29 |
+
primaryAddresses AS (
|
30 |
+
SELECT roi_id,
|
31 |
+
city,
|
32 |
+
state_code as state,
|
33 |
+
zipcode
|
34 |
+
FROM v_account_primary_address
|
35 |
+
WHERE EXISTS (
|
36 |
+
SELECT *
|
37 |
+
FROM account_profile_family apf
|
38 |
+
WHERE apf.roi_id = v_account_primary_address.roi_id
|
39 |
+
)
|
40 |
+
),
|
41 |
+
|
42 |
+
flag_universe AS (
|
43 |
+
SELECT roi_id,
|
44 |
+
MAX(
|
45 |
+
CASE
|
46 |
+
WHEN flagstd_code LIKE 'MD_GROUP%' THEN 'Y'
|
47 |
+
ELSE 'N'
|
48 |
+
END
|
49 |
+
) AS MD_GROUP,
|
50 |
+
MAX(
|
51 |
+
CASE
|
52 |
+
WHEN flagstd_code LIKE 'MD_TFP_HIGH' THEN 'Y'
|
53 |
+
ELSE 'N'
|
54 |
+
END
|
55 |
+
) AS DEV_TFP,
|
56 |
+
MAX(
|
57 |
+
CASE
|
58 |
+
WHEN flagstd_code like 'MLS%' THEN 'Y'
|
59 |
+
ELSE 'N'
|
60 |
+
END
|
61 |
+
) AS MLS,
|
62 |
+
MAX(
|
63 |
+
CASE
|
64 |
+
WHEN flagstd_code like 'REGCOUNCIL%' THEN 'Y'
|
65 |
+
ELSE 'N'
|
66 |
+
END
|
67 |
+
) AS REG_COUNCIL,
|
68 |
+
MAX(
|
69 |
+
CASE
|
70 |
+
WHEN flagstd_code like 'NPROLE_COUNCIL' THEN 'Y'
|
71 |
+
ELSE 'N'
|
72 |
+
END
|
73 |
+
) AS Nat_Council,
|
74 |
+
MAX(
|
75 |
+
CASE
|
76 |
+
WHEN flagstd_code like 'NPROLE_BOARD%' THEN 'Y'
|
77 |
+
ELSE 'N'
|
78 |
+
END
|
79 |
+
) AS Board_or_Emeritus,
|
80 |
+
MAX(
|
81 |
+
CASE WHEN flagstd_code like 'SUSTAINER%' THEN 'Y'
|
82 |
+
ELSE 'N'
|
83 |
+
END
|
84 |
+
) AS SUSTAINER,
|
85 |
+
MAX(
|
86 |
+
CASE
|
87 |
+
WHEN flagstd_code like 'SF_%' THEN v_account_flag_active.flagstd_name
|
88 |
+
ELSE NULL
|
89 |
+
END
|
90 |
+
) AS SUPERFUND,
|
91 |
+
MAX(
|
92 |
+
CASE
|
93 |
+
WHEN flagstd_code like 'SF_GROUP5_PLG_PROSP_FY24' THEN v_account_flag_active.flagstd_name
|
94 |
+
ELSE NULL
|
95 |
+
END
|
96 |
+
) AS SUPERFUND_PlannedGift,
|
97 |
+
MAX(
|
98 |
+
CASE
|
99 |
+
WHEN flagstd_code = 'NPROLE_VETCOUNCIL' THEN 'Y'
|
100 |
+
ELSE 'N'
|
101 |
+
END
|
102 |
+
) AS Vet_Council,
|
103 |
+
MAX(
|
104 |
+
CASE
|
105 |
+
WHEN flagstd_code like 'CF_GROUP_%' THEN 'Y'
|
106 |
+
ELSE 'N'
|
107 |
+
END
|
108 |
+
) AS CF_GROUP
|
109 |
+
FROM v_account_flag_active
|
110 |
+
WHERE
|
111 |
+
(
|
112 |
+
(flagstd_code LIKE 'MD_GROUP%' AND end_date IS NULL)
|
113 |
+
OR (flagstd_code LIKE 'MD_TFP_HIGH' AND end_date IS NULL)
|
114 |
+
OR (flagstd_code like 'MLS%' AND end_date IS NULL)
|
115 |
+
OR (flagstd_code LIKE 'REGCOUNCIL%' AND end_date IS NULL)
|
116 |
+
OR (flagstd_code LIKE 'NPROLE_COUNCIL' AND end_date IS NULL)
|
117 |
+
OR (flagstd_code LIKE 'NPROLE_BOARD%' AND end_date IS NULL)
|
118 |
+
OR (flagstd_code like 'SF_%' AND end_date IS NULL)
|
119 |
+
OR (flagstd_code = 'NPROLE_VETCOUNCIL' AND end_date IS NULL)
|
120 |
+
OR flagstd_code LIKE 'CF_GROUP_%' /*Active or inactive CF_GROUP flags*/
|
121 |
+
)
|
122 |
+
AND EXISTS (
|
123 |
+
SELECT *
|
124 |
+
FROM account_profile_family apf
|
125 |
+
WHERE apf.roi_id = v_account_flag_active.roi_id
|
126 |
+
)
|
127 |
+
GROUP BY roi_id
|
128 |
+
),
|
129 |
+
|
130 |
+
universe AS (
|
131 |
+
SELECT
|
132 |
+
flag_universe.roi_id,
|
133 |
+
flag_universe.MD_GROUP,
|
134 |
+
flag_universe.DEV_TFP,
|
135 |
+
flag_universe.MLS,
|
136 |
+
flag_universe.REG_COUNCIL,
|
137 |
+
flag_universe.Nat_Council,
|
138 |
+
flag_universe.Board_or_Emeritus,
|
139 |
+
flag_universe.Vet_Council,
|
140 |
+
flag_universe.CF_GROUP,
|
141 |
+
flag_universe.SUPERFUND,
|
142 |
+
flag_universe.SUPERFUND_PlannedGift
|
143 |
+
FROM flag_universe
|
144 |
+
),
|
145 |
+
|
146 |
+
criticalFlags AS (
|
147 |
+
SELECT roi_id,
|
148 |
+
MAX(
|
149 |
+
CASE
|
150 |
+
WHEN flagstd_code like 'SF_%'
|
151 |
+
and flagstd_code <> 'SF_GROUP5_PLG_PROSP_FY24' THEN v_account_flag_active.flagstd_name
|
152 |
+
ELSE NULL
|
153 |
+
END
|
154 |
+
) AS SUPERFUND,
|
155 |
+
MAX(
|
156 |
+
CASE
|
157 |
+
WHEN flagstd_code like 'SF_GROUP5_PLG_PROSP_FY24'
|
158 |
+
and flagstd_code NOT LIKE '%5' THEN v_account_flag_active.flagstd_name
|
159 |
+
ELSE NULL
|
160 |
+
END
|
161 |
+
) AS SUPERFUND_PlannedGift,
|
162 |
+
MAX(
|
163 |
+
CASE
|
164 |
+
WHEN flagstd_code like 'SOLICIT_NO_MAIL' THEN 'Y'
|
165 |
+
ELSE 'N'
|
166 |
+
END
|
167 |
+
) AS SOLICIT_NO_MAIL,
|
168 |
+
MAX(
|
169 |
+
CASE
|
170 |
+
WHEN flagstd_code like 'NO_EMAIL%' THEN 'Y'
|
171 |
+
ELSE 'N'
|
172 |
+
END
|
173 |
+
) AS NO_EMAIL,
|
174 |
+
MAX(
|
175 |
+
CASE
|
176 |
+
WHEN flagstd_code like 'NPROLE_STAFF' THEN 'Y'
|
177 |
+
ELSE 'N'
|
178 |
+
END
|
179 |
+
) AS np_role_staff
|
180 |
+
FROM v_account_flag_active
|
181 |
+
WHERE (
|
182 |
+
flagstd_code LIKE 'SF_%'
|
183 |
+
OR flagstd_code like 'NO_EMAIL'
|
184 |
+
OR flagstd_code like 'SOLICIT_NO_MAIL'
|
185 |
+
OR flagstd_code like 'NPROLE_STAFF'
|
186 |
+
OR flagstd_code like 'SOLICIT_NO_PHONE'
|
187 |
+
)
|
188 |
+
AND EXISTS (
|
189 |
+
SELECT *
|
190 |
+
FROM universe
|
191 |
+
WHERE universe.roi_id = v_account_flag_active.roi_id
|
192 |
+
)
|
193 |
+
GROUP BY roi_id
|
194 |
+
)
|
195 |
+
|
196 |
+
SELECT
|
197 |
+
apf.roi_family_id, -- Include roi_family_id
|
198 |
+
apf.roi_id, -- Include roi_id
|
199 |
+
primaryAddresses.city,
|
200 |
+
primaryAddresses.state,
|
201 |
+
primaryAddresses.zipcode,
|
202 |
+
COALESCE(criticalFlags.SOLICIT_NO_MAIL, 'N') AS SOLICIT_NO_MAIL,
|
203 |
+
COALESCE(criticalFlags.NO_EMAIL, 'N') AS NO_EMAIL,
|
204 |
+
COALESCE(universe.MD_GROUP, 'N') AS MD_GROUP,
|
205 |
+
COALESCE(universe.DEV_TFP, 'N') AS DEV_TFP,
|
206 |
+
COALESCE(universe.MLS, 'N') AS MLS,
|
207 |
+
COALESCE(universe.REG_COUNCIL, 'N') AS REG_COUNCIL,
|
208 |
+
COALESCE(universe.Nat_Council, 'N') AS Nat_Council,
|
209 |
+
COALESCE(universe.Vet_Council, 'N') AS Vet_Council,
|
210 |
+
COALESCE(universe.CF_GROUP, 'N') AS CF_GROUP,
|
211 |
+
COALESCE(universe.Board_or_Emeritus, 'N') AS Board_or_Emeritus,
|
212 |
+
COALESCE(universe.SUPERFUND, criticalFlags.SUPERFUND, NULL) AS SUPERFUND,
|
213 |
+
COALESCE(universe.SUPERFUND_PlannedGift, criticalFlags.SUPERFUND_PlannedGift, NULL) AS SUPERFUND_PlannedGift,
|
214 |
+
COALESCE(criticalFlags.np_role_staff, 'N') AS np_role_staff
|
215 |
+
FROM
|
216 |
+
account_profile_family apf
|
217 |
+
JOIN
|
218 |
+
universe ON apf.roi_id = universe.roi_id
|
219 |
+
JOIN
|
220 |
+
primaryAddresses ON apf.roi_id = primaryAddresses.roi_id
|
221 |
+
LEFT JOIN
|
222 |
+
criticalFlags ON apf.roi_id = criticalFlags.roi_id
|
requirements.txt
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Core ML libraries
|
2 |
+
scikit-learn>=1.0.0
|
3 |
+
pandas>=1.3.0
|
4 |
+
numpy>=1.20.0
|
5 |
+
imbalanced-learn>=0.8.0
|
6 |
+
|
7 |
+
# Snowflake integration
|
8 |
+
snowflake-snowpark-python>=1.0.0
|
9 |
+
|
10 |
+
# Visualization (for evaluation notebook)
|
11 |
+
seaborn>=0.11.0
|
12 |
+
matplotlib>=3.4.0
|
13 |
+
|
14 |
+
# Jupyter for running evaluation notebook
|
15 |
+
jupyter>=1.0.0
|
16 |
+
ipykernel>=6.0.0
|