-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.py
More file actions
258 lines (214 loc) · 10.3 KB
/
Copy pathapp.py
File metadata and controls
258 lines (214 loc) · 10.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
# app.py
import streamlit as st
import pandas as pd
import numpy as np
import joblib
import shap
import matplotlib.pyplot as plt
from sklearn.metrics import roc_auc_score
import os
import requests
import re
# Load model and scaler
bundle = joblib.load("marketing_classifier_bundle.pkl")
model = bundle["model"]
scaler = bundle["scaler"]
# --- Hugging Face Inference API Setup ---
HF_API_TOKEN = st.secrets.get("HF_TOKEN", os.getenv("HF_TOKEN"))
headers = {
"Authorization": f"Bearer {HF_API_TOKEN}",
"Content-Type": "application/json"
}
HF_MODEL_URL = "https://api-inference.huggingface.co/models/mistralai/Mistral-7B-Instruct-v0.1"
def query_hf_mistral(prompt, max_tokens=512):
payload = {
"inputs": prompt,
"parameters": {"max_new_tokens": max_tokens}
}
response = requests.post(HF_MODEL_URL, headers=headers, json=payload)
try:
result = response.json()
if isinstance(result, list) and 'generated_text' in result[0]:
full_text = result[0]['generated_text']
return full_text.strip()
elif isinstance(result, dict) and 'error' in result:
return f"LLM error: unexpected response: {result}"
return f"LLM error: unrecognized output format"
except Exception as e:
return f"LLM error: {str(e)}"
# --- Custom HTML Table Renderer with Wider Feature Column ---
def render_wide_feature_table(df):
styles = """
<style>
.custom-table thead th:first-child {
width: 200px !important;
}
.custom-table {
width: 100%;
border-collapse: collapse;
}
.custom-table th, .custom-table td {
border: 1px solid #ccc;
padding: 8px;
text-align: left;
vertical-align: top;
}
</style>
"""
html = styles + '<table class="custom-table"><thead><tr><th>Feature</th><th>How does it impact?</th></tr></thead><tbody>'
for _, row in df.iterrows():
html += f"<tr><td>{row['Feature']}</td><td>{row['How does it impact?']}</td></tr>"
html += "</tbody></table>"
st.markdown(html, unsafe_allow_html=True)
# --- Feature Name Mapping ---
feature_name_map = {
"avg_friend_age": "Avg Age of Friends",
"songsListened": "Songs Listened",
"lovedTracks": "Loved Tracks",
"age": "User Age",
"subscriber_friend_cnt": "Friends Who Subscribed",
"delta_songsListened": "Recent Listening Spike",
"posts": "User Posts",
"shouts": "Shouts Made",
"male": "Is Male User"
}
# --- Custom Style ---
pink = "#f08ebc"
st.set_page_config(page_title="Customer Adopter Prediction", layout="wide")
st.markdown(f"""
<style>
.main {{
background-color: #fff0f5;
}}
.stButton>button, .stDownloadButton>button {{
background-color: {pink};
color: white;
font-weight: bold;
}}
.block-container{{
padding-top: 1rem;
padding-bottom: 1rem;
}}
.metric-box {{
border: 1px solid #ddd;
border-radius: 8px;
padding: 1rem;
background-color: #f8f8f8;
margin-bottom: 1rem;
}}
</style>
""", unsafe_allow_html=True)
st.title("📈 Premium Adoption Prediction Dashboard")
st.markdown("Upload new customer leads and simulate campaign ROI.")
# Sidebar Inputs
st.sidebar.header("🎯 Campaign ROI Simulator")
cost_per_customer = st.sidebar.number_input("Marketing Cost per Customer ($)", min_value=0.0, value=1.0, step=0.1)
revenue_per_conversion = st.sidebar.number_input("Revenue per Adoption ($)", min_value=0.0, value=10.0, step=0.5)
top_k_percent = st.sidebar.slider("Top % Customers to Target", min_value=5, max_value=100, value=20, step=10)
# File Upload
uploaded_file = st.file_uploader("Upload CSV file with new customer leads", type=["csv"])
if uploaded_file:
try:
df = pd.read_csv(uploaded_file)
X_scaled = pd.DataFrame(scaler.transform(df), columns=df.columns)
prob = model.predict_proba(X_scaled)[:, 1]
pred = (prob > 0.3).astype(int)
df_result = df.copy()
df_result["Predicted_Probability"] = prob
df_result["Predicted_Adopter"] = pred
tabs = st.tabs(["📊 Results", "💡 Insights & Campaign Suggestions"])
with tabs[0]:
st.subheader("Prediction Results (Top 20 Customers)")
st.dataframe(df_result.sort_values("Predicted_Probability", ascending=False).head(20), use_container_width=True, height=250)
csv_download = df_result.to_csv(index=False).encode('utf-8')
st.download_button("📥 Download Predictions", data=csv_download, file_name="predicted_customers.csv", mime='text/csv')
with tabs[1]:
st.subheader("Campaign Summary & Insights")
col1, col2 = st.columns([1, 2], gap="large")
with col1:
st.markdown("### 📊 Campaign ROI Summary")
cutoff = int(len(df_result) * top_k_percent / 100)
top_customers = df_result.sort_values("Predicted_Probability", ascending=False).head(cutoff)
n_targeted = len(top_customers)
n_predicted_adopters = top_customers["Predicted_Adopter"].sum()
total_cost = n_targeted * cost_per_customer
total_revenue = n_predicted_adopters * revenue_per_conversion
roi = (total_revenue - total_cost) / total_cost if total_cost > 0 else 0
st.metric("🎯 Targeted Customers", n_targeted)
st.metric("📈 Expected Adopters", int(n_predicted_adopters))
st.metric("💰 Estimated ROI", f"{roi:.2f}")
with col2:
st.markdown("### 🔍 Top Features Influencing Adoption")
explainer = shap.Explainer(model, X_scaled)
shap_values = explainer(X_scaled)
shap_values.feature_names = [feature_name_map.get(name, name) for name in X_scaled.columns]
shap_importance = shap_values.abs.mean(0).values
feature_names_original = X_scaled.columns
top_feature_df = pd.DataFrame({
"Feature": feature_names_original,
"Impact": shap_importance
}).sort_values(by="Impact", ascending=False).head(5)
top_feature_df["Readable_Feature"] = top_feature_df["Feature"].apply(lambda x: feature_name_map.get(x, x))
top5_llm_df = top_feature_df[["Feature", "Readable_Feature", "Impact"]]
fig = plt.figure(figsize=(6, 3.5))
shap.plots.beeswarm(shap_values, max_display=10, show=False)
st.pyplot(fig)
st.divider()
st.markdown("### ❓ What Influences Adoption?")
def parse_explanation_to_df(raw_text):
rows = []
for line in raw_text.splitlines():
match = re.match(r"^[-\*]?\s*\**(.*?)\**\s*:\s*(.*)", line.strip())
if match:
rows.append((match.group(1).strip(), match.group(2).strip()))
return pd.DataFrame(rows, columns=["Feature", "How does it impact?"])
explanation_prompt = "\n".join([
"Explain what user behavior each of the following features captures and how it might relate to adoption of a premium subscription.",
*[f"- {row['Readable_Feature']}" for _, row in top5_llm_df.iterrows()]
])
explanation_response = query_hf_mistral(explanation_prompt)
parsed_table = parse_explanation_to_df(explanation_response)
if parsed_table is not None and not parsed_table.empty:
ordered = pd.merge(top5_llm_df[["Readable_Feature"]], parsed_table, left_on="Readable_Feature", right_on="Feature", how="left")
render_wide_feature_table(ordered[["Feature", "How does it impact?"]])
else:
st.warning("LLM explanation could not be parsed. Please try again later.")
st.markdown("### 🎯 Campaign Recommendations")
readable_features = ", ".join(top5_llm_df["Readable_Feature"].tolist())
rec_prompt = f"""
Suggest 3 concise and relevant marketing campaign ideas based on these features: {readable_features}.
Each idea should be unique. Return each idea as a paragraph for each campaign idea. Wrap the campaign title in double quotes.
"""
campaign_response = query_hf_mistral(rec_prompt)
if campaign_response and "LLM error" not in campaign_response:
lines = [line.strip() for line in campaign_response.split("\n") if line.strip()]
formatted = []
current_title = None
paragraph_accumulator = []
for line in lines:
# Skip irrelevant LLM prompt metadata
if line.lower().startswith(("suggest", "return", "avg age", "songs listened", "loved tracks", "user age", "friends who subscribed")):
continue
# Detect new campaign title
if line.lower().startswith("campaign title:") or re.match(r'^"[^"]+"', line):
if current_title and paragraph_accumulator:
formatted.append(
f"<li><b>📣 {current_title}</b><ul><li>{' '.join(paragraph_accumulator)}</li></ul></li>"
)
title_match = re.search(r'"([^"]+)"', line)
current_title = title_match.group(1) if title_match else line.split(":")[-1].strip()
paragraph_accumulator = []
else:
paragraph_accumulator.append(line)
# Final one
if current_title and paragraph_accumulator:
formatted.append(
f"<li><b>📣 {current_title}</b><ul><li>{' '.join(paragraph_accumulator)}</li></ul></li>"
)
st.markdown("<ul>" + "\n".join(formatted) + "</ul>", unsafe_allow_html=True)
else:
st.warning("LLM recommendation could not be generated. Please try again later.")
except Exception as e:
st.error(f"There was a problem processing your file: {e}")
else:
st.info("📤 Please upload a CSV file to begin analysis.")