###################################################
# Chapter 9: Customer Lifetime Value (CLV)        #
# Chestnut Ridge Multi-channel Retailer Example   #
###################################################

# This script reproduces the full Chestnut Ridge customer lifetime value (CLV) example in Python:
#   Step 1. Data setup (uses the output file from the Chapter 8 example)
#   Step 2. Compute CLV using P(alive) and using the logistic regression prediction
#   Step 3. CLV decile charts
#   Step 4. Manage customers using CLV (compare customer selection methods in Table 9.3)
# Run it from top to bottom. Each step prints its result or shows a plot.
# The "# %%" lines split the script into cells you can run one at a time
# in VS Code, Spyder, or PyCharm.

# %%
###################################################
# Step 1: Data Setup                              #
###################################################

## Install Packages (if needed - only installs packages that are missing)
## Each entry is {name used with import: name used with pip install}
import sys, subprocess, importlib.util
packages = {"pandas": "pandas", "numpy": "numpy", "matplotlib": "matplotlib"}
new_packages = [pip_name for import_name, pip_name in packages.items() if importlib.util.find_spec(import_name) is None]
if new_packages: subprocess.check_call([sys.executable, "-m", "pip", "install", *new_packages])

## Load Packages and Set Seed
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from tkinter import Tk, filedialog
np.random.seed(1)
pd.set_option("display.width", 200, "display.max_columns", 20) ## Show all columns when printing

## Function to choose a file with a dialog box
## (the Python version of file.choose() in R - no need to set the working directory)
def file_choose(title="Choose a file"):
    root = Tk()
    root.withdraw()                     # hide the empty Tk window
    root.attributes("-topmost", True)   # bring the dialog to the front
    path = filedialog.askopenfilename(title=title, filetypes=[("CSV files", "*.csv"), ("All files", "*.*")])
    root.destroy()
    if not path:
        raise SystemExit("No file was chosen.")
    return path

## Read in CLV data (the output file from the Chapter 8 Logistic Regression example)
clv = pd.read_csv(file_choose("Choose logit_pred.csv")) ## Choose logit_pred.csv file

## Look at the data (see Table 9.2 for variable definitions)
print(clv.head())
print(clv.describe())


# %%
###################################################
# Step 2: Computing CLV                           #
###################################################

## Assumptions
months = 36         ## Observation window is 3 years (36 months)
days = 1095         ## Observation window is 3 years (1,095 days)
margin_rate = 0.52  ## Average gross margin for the retailer
mkt_cost = 1        ## Marketing cost per customer per month
annual_d = 0.10     ## Annual discount rate

## Monthly discount rate: (1 + d)^(1/12) - 1
discount = (1 + annual_d) ** (1 / 12) - 1
print(discount)  ## 0.00797

## Margin, P(alive), and the two CLV calculations for each customer
## margin:       average monthly revenue times the 52% margin, less $1 in marketing costs
## p_alive:      t^n, where t = (1095 - recency_days) / 1095 and n = number_of_orders
## clv_p_alive:  CLV using P(alive) as the retention rate
## clv_predict:  CLV using the predicted probability from the Logistic regression as the retention rate
clv["margin"] = (clv["revenue"] / months) * margin_rate - mkt_cost
clv["p_alive"] = ((days - clv["recency_days"]) / days) ** clv["number_of_orders"]
clv["clv_p_alive"] = clv["margin"] * (1 + discount) / (1 + discount - clv["p_alive"])
clv["clv_predict"] = clv["margin"] * (1 + discount) / (1 + discount - clv["predict"])

## Function to format numbers as dollars (e.g., -$15.68)
def dollar(x):
    return f"-${-x:,.2f}" if x < 0 else f"${x:,.2f}"

## Figure 9.2: CLV P(alive) and CLV Predict for each Customer
clv_table = clv[["customer_id", "clv_p_alive", "clv_predict"]].copy()
clv_table[["clv_p_alive", "clv_predict"]] = clv_table[["clv_p_alive", "clv_predict"]].map(dollar)
print(clv_table.head(20).to_string(index=False))

## How similar are the two CLV calculations?
print(clv["clv_p_alive"].corr(clv["clv_predict"]))


# %%
###################################################
# Step 3: CLV Decile Charts                       #
###################################################

## Assign each customer to a decile (10% = lowest CLV, 100% = highest CLV).
## A customer is placed in the first decile whose cutoff (percentile) is at or above
## the customer's CLV, matching the approach in the 1st edition.
decile_labels = [f"{p}%" for p in range(10, 101, 10)]

def clv_decile(x):
    cutoffs = np.quantile(x, np.arange(0.1, 1.0, 0.1))
    bins = np.searchsorted(cutoffs, x, side="left")
    return pd.Categorical([decile_labels[b] for b in bins], categories=decile_labels, ordered=True)

clv["decile_p_alive"] = clv_decile(clv["clv_p_alive"])
clv["decile_predict"] = clv_decile(clv["clv_predict"])

## Function to compute the number of customers, average CLV, and total CLV in each decile
def summarize_deciles(decile, value):
    deciles = (clv.groupby(decile, observed=True)[value]
               .agg(customers="count", avg_clv="mean", total_clv="sum")
               .reset_index()
               .rename(columns={decile: "decile"}))
    deciles["share"] = (deciles["total_clv"] / deciles["total_clv"].sum()).map("{:.1%}".format)
    return deciles

deciles_p_alive = summarize_deciles("decile_p_alive", "clv_p_alive")
deciles_predict = summarize_deciles("decile_predict", "clv_predict")

## Average CLV in each decile and share of total CLV
print(deciles_p_alive)  ## Top decile averages $67.01; 80% and 90% deciles average $9.32 and $16.68
print(deciles_predict)  ## Top decile averages $70.29; 80% and 90% deciles average $6.71 and $13.14

## Function to plot the average CLV in each decile (Figures 9.3 and 9.4)
def plot_clv_deciles(deciles, title):
    fig, ax = plt.subplots(figsize=(10, 6))
    bars = ax.bar(deciles["decile"].astype(str), deciles["avg_clv"], color="#2C5F8A")
    for bar, value in zip(bars, deciles["avg_clv"]):
        ax.annotate(dollar(value), (bar.get_x() + bar.get_width() / 2, value),
                    xytext=(0, 4 if value >= 0 else -4), textcoords="offset points",
                    ha="center", va="bottom" if value >= 0 else "top", fontsize=9)
    ax.axhline(0, color="black", linewidth=0.8)
    ax.yaxis.set_major_formatter(plt.matplotlib.ticker.StrMethodFormatter("${x:,.0f}"))
    ax.margins(y=0.1)
    ax.set_title(title, loc="left")
    ax.set_xlabel("CLV Decile")
    ax.set_ylabel("Average CLV")
    for side in ["top", "right"]:
        ax.spines[side].set_visible(False)
    plt.tight_layout()
    plt.show()

## Figure 9.3: CLV Decile Chart Using P(alive)
plot_clv_deciles(deciles_p_alive, "Decile Chart Using P(alive)")

## Figure 9.4: CLV Decile Chart Using Predict
plot_clv_deciles(deciles_predict, "Decile Chart Using Predict")


# %%
###################################################
# Step 4: Managing Customers Using CLV            #
###################################################

## Select the top 15% (1,500) of customers by CLV Predict and by the Logistic
## regression predicted probability. Table 9.3 compares the following year's profit
## of the customers selected using RFM - sequential sort (Chapter 7), Logistic
## regression (Chapter 8), and CLV Predict (this chapter).
n_select = round(0.15 * len(clv))

top_clv_predict = clv.nlargest(n_select, "clv_predict", keep="first")
top_logistic = clv.nlargest(n_select, "predict", keep="first")

## How many customers do the two methods have in common?
print(len(set(top_clv_predict["customer_id"]) & set(top_logistic["customer_id"])))

## Table 9.3: Profitability Based on Different Customer Selection Criteria
table_9_3 = pd.DataFrame({
    "RFM - Sequential Sort": [204.13, 310272],
    "Logistic Regression": [219.35, 329023],
    "CLV Predict": [247.17, 370749]},
    index=["Avg. Profit", "Total Profit"])
print(table_9_3)

## Percentage improvement in average profit
avg_profit = table_9_3.loc["Avg. Profit"]
print(avg_profit["Logistic Regression"] / avg_profit["RFM - Sequential Sort"] - 1)  ## 7.46%
print(avg_profit["CLV Predict"] / avg_profit["Logistic Regression"] - 1)            ## 12.68%
print(avg_profit["CLV Predict"] / avg_profit["RFM - Sequential Sort"] - 1)          ## 21.08%
