Update app.py
Browse files
app.py
CHANGED
|
@@ -18,6 +18,8 @@ from google.analytics.data_v1beta.types import (
|
|
| 18 |
Filter
|
| 19 |
)
|
| 20 |
|
|
|
|
|
|
|
| 21 |
warnings.filterwarnings("ignore", category=FutureWarning)
|
| 22 |
|
| 23 |
# =====================================================
|
|
@@ -25,7 +27,10 @@ warnings.filterwarnings("ignore", category=FutureWarning)
|
|
| 25 |
# =====================================================
|
| 26 |
GA4_PROPERTY_ID = os.environ["GA4_PROPERTY_ID"]
|
| 27 |
SERVICE_ACCOUNT_JSON = json.loads(os.environ["SERVICE_ACCOUNT_JSON"])
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
# =====================================================
|
| 31 |
# GA CLIENT
|
|
@@ -55,7 +60,7 @@ def classify_source(src):
|
|
| 55 |
return "External"
|
| 56 |
|
| 57 |
# =====================================================
|
| 58 |
-
#
|
| 59 |
# =====================================================
|
| 60 |
@lru_cache(maxsize=3)
|
| 61 |
def fetch_top_pages(start, end):
|
|
@@ -99,7 +104,7 @@ def fetch_journey(start, end, landing_page):
|
|
| 99 |
return pd.DataFrame(rows)
|
| 100 |
|
| 101 |
# =====================================================
|
| 102 |
-
# FORM SUBMISSIONS (GA4-SAFE
|
| 103 |
# =====================================================
|
| 104 |
def get_form_submissions(start, end, landing_page):
|
| 105 |
req = RunReportRequest(
|
|
@@ -134,21 +139,74 @@ def get_form_submissions(start, end, landing_page):
|
|
| 134 |
res = ga.run_report(req)
|
| 135 |
return sum(int(r.metric_values[0].value) for r in res.rows)
|
| 136 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
# =====================================================
|
| 138 |
# MAIN LOGIC
|
| 139 |
# =====================================================
|
| 140 |
def run_journey(start, end, landing_page):
|
| 141 |
df = fetch_journey(start, end, landing_page)
|
| 142 |
if df.empty:
|
| 143 |
-
return None, pd.DataFrame(), "No data found."
|
| 144 |
|
| 145 |
total_users = df["Views"].sum()
|
| 146 |
form_yes = get_form_submissions(start, end, landing_page)
|
|
|
|
| 147 |
journey_converted = "Yes" if form_yes > 0 else "No"
|
| 148 |
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
exit_df = (
|
| 153 |
df.groupby("From")["Views"]
|
| 154 |
.sum()
|
|
@@ -161,72 +219,32 @@ def run_journey(start, end, landing_page):
|
|
| 161 |
exit_df["Source Type"] = exit_df["Exit Source / Page"].apply(classify_source)
|
| 162 |
exit_df["Exit Source / Page"] = exit_df["Exit Source / Page"].apply(clean_path)
|
| 163 |
|
| 164 |
-
# 🔑 Option C logic
|
| 165 |
exit_df["Journey Converted?"] = journey_converted
|
| 166 |
-
exit_df["
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
"High exit – landing intent mismatch"
|
| 174 |
-
if r["Source Type"] == "External"
|
| 175 |
-
else "Conversion leak – improve CTA"
|
| 176 |
-
),
|
| 177 |
-
axis=1
|
| 178 |
-
)
|
| 179 |
|
| 180 |
-
exit_df = exit_df
|
| 181 |
-
[
|
| 182 |
-
"Exit Source / Page",
|
| 183 |
-
"Source Type",
|
| 184 |
-
"Users Entered",
|
| 185 |
-
"Users Exited",
|
| 186 |
-
"Exit Rate %",
|
| 187 |
-
"Journey Converted?",
|
| 188 |
-
"Form Submitted Before Exit?",
|
| 189 |
-
"Insight"
|
| 190 |
-
]
|
| 191 |
-
].sort_values("Users Exited", ascending=False)
|
| 192 |
-
|
| 193 |
-
# ===========================
|
| 194 |
-
# SANKEY
|
| 195 |
-
# ===========================
|
| 196 |
-
labels, idx = [], {}
|
| 197 |
-
|
| 198 |
-
def add(n):
|
| 199 |
-
if n not in idx:
|
| 200 |
-
idx[n] = len(labels)
|
| 201 |
-
labels.append(n)
|
| 202 |
-
return idx[n]
|
| 203 |
-
|
| 204 |
-
s, t, v = [], [], []
|
| 205 |
-
|
| 206 |
-
for _, r in df.iterrows():
|
| 207 |
-
s.append(add(clean_path(r["From"])))
|
| 208 |
-
t.append(add(clean_path(r["To"])))
|
| 209 |
-
v.append(r["Views"])
|
| 210 |
-
|
| 211 |
-
p = add(clean_path(landing_page))
|
| 212 |
-
e = add("Exit")
|
| 213 |
-
s.append(p)
|
| 214 |
-
t.append(e)
|
| 215 |
-
v.append(total_users - form_yes)
|
| 216 |
-
|
| 217 |
-
if form_yes > 0:
|
| 218 |
-
f = add("Form Submitted")
|
| 219 |
-
s.append(p)
|
| 220 |
-
t.append(f)
|
| 221 |
-
v.append(form_yes)
|
| 222 |
|
| 223 |
fig = go.Figure(go.Sankey(
|
| 224 |
-
node=dict(
|
| 225 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
))
|
| 227 |
|
| 228 |
fig.update_layout(
|
| 229 |
-
title="Customer Journey Flow (
|
| 230 |
height=650
|
| 231 |
)
|
| 232 |
|
|
@@ -237,7 +255,9 @@ def run_journey(start, end, landing_page):
|
|
| 237 |
|
| 238 |
- **Journey Converted:** {journey_converted}
|
| 239 |
- **Form Submissions:** {form_yes}
|
| 240 |
-
- **
|
|
|
|
|
|
|
| 241 |
"""
|
| 242 |
|
| 243 |
return fig, exit_df, summary
|
|
@@ -246,7 +266,7 @@ def run_journey(start, end, landing_page):
|
|
| 246 |
# UI
|
| 247 |
# =====================================================
|
| 248 |
with gr.Blocks() as app:
|
| 249 |
-
gr.Markdown("## 🚀 Customer Journey
|
| 250 |
|
| 251 |
with gr.Row():
|
| 252 |
sd = gr.Textbox(value=str(date.today() - timedelta(days=30)), label="Start Date")
|
|
|
|
| 18 |
Filter
|
| 19 |
)
|
| 20 |
|
| 21 |
+
from openai import OpenAI
|
| 22 |
+
|
| 23 |
warnings.filterwarnings("ignore", category=FutureWarning)
|
| 24 |
|
| 25 |
# =====================================================
|
|
|
|
| 27 |
# =====================================================
|
| 28 |
GA4_PROPERTY_ID = os.environ["GA4_PROPERTY_ID"]
|
| 29 |
SERVICE_ACCOUNT_JSON = json.loads(os.environ["SERVICE_ACCOUNT_JSON"])
|
| 30 |
+
OPENAI_API_KEY = os.environ["OPENAI_API_KEY"]
|
| 31 |
+
SITE_DOMAIN = "rishabhsoft.com"
|
| 32 |
+
|
| 33 |
+
client = OpenAI(api_key=OPENAI_API_KEY)
|
| 34 |
|
| 35 |
# =====================================================
|
| 36 |
# GA CLIENT
|
|
|
|
| 60 |
return "External"
|
| 61 |
|
| 62 |
# =====================================================
|
| 63 |
+
# TOP PAGES (FAST)
|
| 64 |
# =====================================================
|
| 65 |
@lru_cache(maxsize=3)
|
| 66 |
def fetch_top_pages(start, end):
|
|
|
|
| 104 |
return pd.DataFrame(rows)
|
| 105 |
|
| 106 |
# =====================================================
|
| 107 |
+
# FORM SUBMISSIONS (GA4-SAFE)
|
| 108 |
# =====================================================
|
| 109 |
def get_form_submissions(start, end, landing_page):
|
| 110 |
req = RunReportRequest(
|
|
|
|
| 139 |
res = ga.run_report(req)
|
| 140 |
return sum(int(r.metric_values[0].value) for r in res.rows)
|
| 141 |
|
| 142 |
+
# =====================================================
|
| 143 |
+
# LLM INTENT CLASSIFIER (CACHED)
|
| 144 |
+
# =====================================================
|
| 145 |
+
@lru_cache(maxsize=100)
|
| 146 |
+
def classify_intent_llm(start_page, pages, converted):
|
| 147 |
+
prompt = f"""
|
| 148 |
+
You are a digital analytics expert.
|
| 149 |
+
|
| 150 |
+
Classify the user intent into ONE of:
|
| 151 |
+
- Conversion
|
| 152 |
+
- Evaluation
|
| 153 |
+
- Research
|
| 154 |
+
- Navigation
|
| 155 |
+
- Low Intent
|
| 156 |
+
|
| 157 |
+
Rules:
|
| 158 |
+
- Use only the data given
|
| 159 |
+
- Do NOT invent behavior
|
| 160 |
+
- Be concise
|
| 161 |
+
|
| 162 |
+
Journey Data:
|
| 163 |
+
Start Page: {start_page}
|
| 164 |
+
Pages Visited: {", ".join(pages[:5])}
|
| 165 |
+
Form Submitted: {"Yes" if converted else "No"}
|
| 166 |
+
|
| 167 |
+
Return format:
|
| 168 |
+
Intent: <Intent>
|
| 169 |
+
Reason: <One sentence explanation>
|
| 170 |
+
"""
|
| 171 |
+
|
| 172 |
+
resp = client.chat.completions.create(
|
| 173 |
+
model="gpt-4o-mini",
|
| 174 |
+
messages=[{"role": "user", "content": prompt}],
|
| 175 |
+
temperature=0
|
| 176 |
+
)
|
| 177 |
+
|
| 178 |
+
txt = resp.choices[0].message.content.strip()
|
| 179 |
+
intent = "Unknown"
|
| 180 |
+
reason = ""
|
| 181 |
+
|
| 182 |
+
for line in txt.splitlines():
|
| 183 |
+
if line.lower().startswith("intent"):
|
| 184 |
+
intent = line.split(":", 1)[1].strip()
|
| 185 |
+
if line.lower().startswith("reason"):
|
| 186 |
+
reason = line.split(":", 1)[1].strip()
|
| 187 |
+
|
| 188 |
+
return intent, reason
|
| 189 |
+
|
| 190 |
# =====================================================
|
| 191 |
# MAIN LOGIC
|
| 192 |
# =====================================================
|
| 193 |
def run_journey(start, end, landing_page):
|
| 194 |
df = fetch_journey(start, end, landing_page)
|
| 195 |
if df.empty:
|
| 196 |
+
return None, pd.DataFrame(), "No journey data found."
|
| 197 |
|
| 198 |
total_users = df["Views"].sum()
|
| 199 |
form_yes = get_form_submissions(start, end, landing_page)
|
| 200 |
+
conv_rate = round((form_yes / total_users) * 100, 2) if total_users else 0.0
|
| 201 |
journey_converted = "Yes" if form_yes > 0 else "No"
|
| 202 |
|
| 203 |
+
top_pages = df.sort_values("Views", ascending=False)["To"].unique().tolist()[:5]
|
| 204 |
+
intent, intent_reason = classify_intent_llm(
|
| 205 |
+
clean_path(landing_page),
|
| 206 |
+
[clean_path(p) for p in top_pages],
|
| 207 |
+
form_yes > 0
|
| 208 |
+
)
|
| 209 |
+
|
| 210 |
exit_df = (
|
| 211 |
df.groupby("From")["Views"]
|
| 212 |
.sum()
|
|
|
|
| 219 |
exit_df["Source Type"] = exit_df["Exit Source / Page"].apply(classify_source)
|
| 220 |
exit_df["Exit Source / Page"] = exit_df["Exit Source / Page"].apply(clean_path)
|
| 221 |
|
|
|
|
| 222 |
exit_df["Journey Converted?"] = journey_converted
|
| 223 |
+
exit_df["Journey Conversion Rate %"] = f"{conv_rate:.2f}%"
|
| 224 |
+
exit_df["Exit Severity Score"] = (
|
| 225 |
+
exit_df["Users Exited"] * (1 - conv_rate / 100)
|
| 226 |
+
).round(1)
|
| 227 |
+
|
| 228 |
+
exit_df["User Intent"] = intent
|
| 229 |
+
exit_df["Intent Reason"] = intent_reason
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
|
| 231 |
+
exit_df = exit_df.sort_values("Exit Severity Score", ascending=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
|
| 233 |
fig = go.Figure(go.Sankey(
|
| 234 |
+
node=dict(
|
| 235 |
+
label=["Entry", clean_path(landing_page), "Exit", "Form Submitted"],
|
| 236 |
+
pad=20,
|
| 237 |
+
thickness=18
|
| 238 |
+
),
|
| 239 |
+
link=dict(
|
| 240 |
+
source=[0, 1, 1],
|
| 241 |
+
target=[1, 2, 3],
|
| 242 |
+
value=[total_users, total_users - form_yes, form_yes]
|
| 243 |
+
)
|
| 244 |
))
|
| 245 |
|
| 246 |
fig.update_layout(
|
| 247 |
+
title="Customer Journey Flow (Business View)",
|
| 248 |
height=650
|
| 249 |
)
|
| 250 |
|
|
|
|
| 255 |
|
| 256 |
- **Journey Converted:** {journey_converted}
|
| 257 |
- **Form Submissions:** {form_yes}
|
| 258 |
+
- **Conversion Rate:** {conv_rate:.2f}%
|
| 259 |
+
- **Primary User Intent:** **{intent}**
|
| 260 |
+
- **Why:** {intent_reason}
|
| 261 |
"""
|
| 262 |
|
| 263 |
return fig, exit_df, summary
|
|
|
|
| 266 |
# UI
|
| 267 |
# =====================================================
|
| 268 |
with gr.Blocks() as app:
|
| 269 |
+
gr.Markdown("## 🚀 Customer Journey Intelligence (GA4 + AI)")
|
| 270 |
|
| 271 |
with gr.Row():
|
| 272 |
sd = gr.Textbox(value=str(date.today() - timedelta(days=30)), label="Start Date")
|