# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .py
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.14.4
#   kernelspec:
#     display_name: Python 3
#     language: python
#     name: python3
# ---

# # Customer Churn Prediction

# +
import numpy as np
import pandas as pd
import lightgbm as lgb
import seaborn as sns
import matplotlib.pyplot as plt

from bdrk.model_analyzer import ModelAnalyzer, ModelTypes

from utils import lgb_roc_auc_score, print_results, plot_pr_curve, plot_roc_curve

sns.set()
# %matplotlib inline
# %load_ext autoreload
# %autoreload 2
# -

# Parameter config
LR = 0.05
NUM_LEAVES = 10
N_ESTIMATORS = 100
OUTPUT_MODEL = "data/lgb_model.pkl"

# ## EDA - data deepdive
#
# The first task in any data science or ML project is to understand and clean the data.
#
# - Understand the data types for each feature
# - Look for anomalies and missing values
# - Understand the distributions for each feature

subscribers = pd.read_parquet('data/subscribers.gz.parquet', engine='fastparquet')
subscribers.head()

# Churn counts
subscribers["Churn"].value_counts()

# +
vcount = subscribers["State"].value_counts().reset_index()
vcount.columns = ["State", "Total"]

vcount2 = subscribers["State"].loc[subscribers["Churn"] == "yes"].value_counts().reset_index()
vcount2.columns = ["State", "Left"]

vcount = pd.merge(vcount, vcount2, on=["State"])
vcount = vcount.fillna(0)

plt.figure(figsize=(12, 12))
sns.set_color_codes("pastel")
sns.barplot(x="Total", y="State", data=vcount, color="b", label="total count")

sns.set_color_codes("muted")
sns.barplot(x="Left", y="State", data=vcount, color="b", label="churn count")
plt.xlabel("")
plt.legend()
plt.show()
# -

all_calls = pd.read_parquet('data/all_calls.gz.parquet', engine='fastparquet')
all_calls.head()

plt.figure()
sns.distplot(all_calls["Duration"].loc[all_calls["Call_type"] == "Day"],
             color="skyblue", label="Day")
sns.distplot(all_calls["Duration"].loc[all_calls["Call_type"] == "Eve"],
             color="red", label="Eve")
sns.distplot(all_calls["Duration"].loc[all_calls["Call_type"] == "Intl"],
             color="gold", label="Intl")
sns.distplot(all_calls["Duration"].loc[all_calls["Call_type"] == "Night"],
             color="teal", label="Night")
plt.legend()
plt.show()

# ## Feature engineering and Preprocessing
#
# One can increase the predictive quality of the data using feature engineering.
#
# - Feature crosses
# - Embeddings
# - PCA
# - Categorical encoding
#
# For example, we shall perform one-hot encoding on `Area_Code` and `State`.
#
# ```
# Unique area codes =  [415 408 510]
#
# Unique state codes = ['KS' 'OH' 'NJ' 'OK' 'AL' 'MA' 'MO' 'LA' 'WV' 'IN' 'RI' 'IA' 'MT' 'NY'
#  'ID' 'VT' 'VA' 'TX' 'FL' 'CO' 'AZ' 'SC' 'NE' 'WY' 'HI' 'IL' 'NH' 'GA'
#  'AK' 'MD' 'AR' 'WI' 'OR' 'MI' 'DE' 'UT' 'CA' 'MN' 'SD' 'NC' 'WA' 'NM'
#  'NV' 'DC' 'KY' 'ME' 'MS' 'TN' 'PA' 'CT' 'ND']
# ```

# +
# Subscribers data
subscribers = pd.read_parquet("data/subscribers.gz.parquet", engine="fastparquet")
for col in ["Churn", "Intl_Plan", "VMail_Plan"]:
    subscribers[col] = (subscribers[col] == "yes").astype(int)

# Calls data
all_calls = pd.read_parquet("data/all_calls.gz.parquet", engine="fastparquet")
all_calls = all_calls.groupby(["Call_type", "User_id"]).agg(["sum", "count"]).reset_index()
all_calls.columns = ["Call_type", "User_id", "Mins", "Calls"]
all_calls = all_calls.pivot(index="User_id", columns="Call_type", values=["Mins", "Calls"]).reset_index()
all_calls.columns = [
    "User_id", "Day_Mins", "Eve_Mins", "Intl_Mins", "Night_Mins",
    "Day_Calls", "Eve_Calls", "Intl_Calls", "Night_Calls"]

# Joining
model_data = pd.merge(subscribers, all_calls, on=["User_id"], how="left")
model_data = model_data.fillna(0)

# Categorical encoding
model_data = pd.get_dummies(model_data, columns=["Area_Code", "State"])
# -

# ## Split train, validation and test data

# +
FEATURE_COLS = [
    "Intl_Plan", "VMail_Plan", "VMail_Message", "CustServ_Calls",
    "Day_Mins", "Day_Calls", "Eve_Mins", "Eve_Calls", 
    "Night_Mins", "Night_Calls", "Intl_Mins", "Intl_Calls",
    "Area_Code_408", "Area_Code_415", "Area_Code_510",
    "State_AK", "State_AL", "State_AR", "State_AZ", "State_CA", "State_CO",
    "State_CT", "State_DC", "State_DE", "State_FL", "State_GA", "State_HI",
    "State_IA", "State_ID", "State_IL", "State_IN", "State_KS", "State_KY",
    "State_LA", "State_MA", "State_MD", "State_ME", "State_MI", "State_MN",
    "State_MO", "State_MS", "State_MT", "State_NC", "State_ND", "State_NE",
    "State_NH", "State_NJ", "State_NM", "State_NV", "State_NY", "State_OH",
    "State_OK", "State_OR", "State_PA", "State_RI", "State_SC", "State_SD",
    "State_TN", "State_TX", "State_UT", "State_VA", "State_VT", "State_WA",
    "State_WI", "State_WV", "State_WY",
]

TARGET_COL = "Churn"

# +
train_data, eval_data, test_data = np.split(
    model_data.sample(frac=1, random_state=1729),
    [int(0.7 * len(model_data)), int(0.9 * len(model_data))])

X_train = train_data[FEATURE_COLS]
y_train = train_data[TARGET_COL]

X_val = eval_data[FEATURE_COLS]
y_val = eval_data[TARGET_COL]

X_test = test_data[FEATURE_COLS]
y_test = test_data[TARGET_COL]
# -

test_data.to_csv("data/test.csv", index=False)

# ## Train model
#
# We use **LightGBM**: a gradient boosting framework that uses tree based learning algorithms.

clf = lgb.LGBMClassifier(num_leaves=NUM_LEAVES, learning_rate=LR, n_estimators=N_ESTIMATORS)
clf.fit(
    X_train,
    y_train,
    eval_set=[(X_val, y_val)],
    eval_metric=lgb_roc_auc_score,
    verbose=False,
)

fig, axs = plt.subplots(1, 2, figsize=(12, 4))
lgb.plot_metric(clf, "roc_auc", ax=axs[0])
lgb.plot_metric(clf, "binary_logloss", ax=axs[1])
plt.show()

# ## Evaluate using test set

# +
y_probs = clf.predict_proba(X_test)[:,1]

print_results(y_test, y_probs)
# -

fig, axs = plt.subplots(1, 2, figsize=(12, 4))
plot_pr_curve(y_test, y_probs, axs[0])
plot_roc_curve(y_test, y_probs, axs[1])
plt.show()

# ## Explainability using ModelAnalyzer
#
# We shall use `bdrk.model_analyzer.ModelAnalyzer` to generate model explainability analysis locally. You can use it to generate and log them in Bedrock.
#
# Refer to [documentation](https://docs.basis-ai.com/guides/explainability-and-fairness) and [API references](https://docs.basis-ai.com/guides/explainability-and-fairness/api-references) for more details.

# +
analyzer = ModelAnalyzer(clf, "churn_prediction", model_type=ModelTypes.TREE)

# To set test data used for calculating explainability
analyzer.test_features(X_test)
analyzer.stored_sample_limit(sample_limit=None)  # To output all SHAP values for the test data

(shap_values, base_values, global_xai_data, fairness_metrics) = analyzer.analyze()
# -

# ## Analyzer outputs
#
# ### SHAP values
# Array of SHAP values calculated on provided `test_features`

shap_values

# ### Base value
# Base value is the average model output calculated on background data

base_values

# ### Global XAI data
# Global XAI returns the mean absolute shap values of each feature, together with its correlation with said feature

global_xai_data.keys()

global_xai_data["class 1"]["Intl_Plan"]

# ### Fairness metrics
#
# Fairness metrics are empty since no fairness config was passed

fairness_metrics

# ## Optional: Plotting explainability
#
# Using the compute SHAP values and `shap.summary_plot`, we can obtain a preliminary view of the effects of the top features.

# +
import shap

# We use the SHAP values that correspond to `Churn=1`, ie shap_values[1]
shap.summary_plot(shap_values[1], X_test, plot_type="bar", max_display=10)
shap.summary_plot(shap_values[1], X_test, max_display=10)
# -

# ## Start a training run locally and log related metrics
#
# This feature is supported in version [bdrk0.9.1](https://pypi.org/project/bdrk/0.9.1) and above.
#
# The full documentation can be found [here](https://basisai.github.io)
#

# ### Initialize the setting

# +
import os
import bdrk
import logging
import sys
import pickle
from sklearn import metrics

# You can customize the logging logic here
_logger = logging.getLogger(bdrk.utils.vars.Constants.MAIN_LOG)
_logger.setLevel(logging.INFO)
if not _logger.handlers:
    _logger.addHandler(logging.StreamHandler(stream=sys.stdout))
    
# Visit https://bedrock.basis-ai.com/setting/token/ to get the personal access token.
os.environ["BEDROCK_API_TOKEN"] = ""

# -

# ### Start a local run and log

# +
# Initialize the project
bdrk.init(project_id="bdrk-churn-prediction")

with bdrk.start_run(pipeline_id="new-pipeline", environment_id="playground-production"):
    # Log related parameters
    bdrk.log_params({"LR": LR, "NUM_LEAVES": NUM_LEAVES, "N_ESTIMATORS": N_ESTIMATORS})
    
    # Log a single metric
    bdrk.log_metric(key="Feature Count", value=len(FEATURE_COLS))
    
    # Log metrics for each training step
    evals = clf.evals_result_["valid_0"]
    for iteration in range(N_ESTIMATORS):
        bdrk.log_metrics(metrics={
            "Training binary_logloss": evals["binary_logloss"][iteration],
            "Training roc_auc": evals["roc_auc"][iteration]
        }, x=iteration)
        
    
    # Log final metrics in batch
    y_pred = (y_probs > 0.5).astype(int)
    bdrk.log_metrics(metrics={
        "Accuracy": metrics.accuracy_score(y_test, y_pred),
        "Precision": metrics.precision_score(y_test, y_pred),
        "Recall": metrics.recall_score(y_test, y_pred),
        "F1 score": metrics.f1_score(y_test, y_pred),
        "Roc auc": metrics.roc_auc_score(y_test, y_pred),
        "Avg precision": metrics.average_precision_score(y_test, y_pred),
    })
    
    # Log the binary classifier from the predicted data
    bdrk.log_binary_classifier_metrics(actual=y_test, probability=y_probs)

    # Saving and log the model
    with open(OUTPUT_MODEL, "wb") as model_file:
        pickle.dump(clf, model_file)
    bdrk.log_model(OUTPUT_MODEL)

# -



StarcoderdataJupyterScriptsDedupFiltered