Dun3Co commited on
Commit
f3ca9d5
Β·
verified Β·
1 Parent(s): 2b789d9

Update pages/6_πŸ“ž_Callcenter_dashboard.py

Browse files
Files changed (1) hide show
  1. pages/6_πŸ“ž_Callcenter_dashboard.py +235 -235
pages/6_πŸ“ž_Callcenter_dashboard.py CHANGED
@@ -1,235 +1,235 @@
1
- import streamlit as st
2
- from streamlit_extras.let_it_rain import rain
3
- import requests
4
- import random
5
- import pandas as pd
6
- import datetime
7
-
8
-
9
- st.title("πŸ“ž Callcenter Dashboard")
10
-
11
- with st.expander("ℹ️ - About this dashboard", expanded=False):
12
- st.markdown(
13
- """
14
- This dashboard simulates a call center environment where agents can manage a queue of customers to upsell a long term deposit bank product.
15
- In the original paper that came with the dataset, they mention that there was inbound calls too, but it's not present in the dataset.
16
- The dashboard fetches customer data from an API(NocoDB with test and synthetic data), displays customer information, and uses a machine learning model to predict the likelihood of a successful upsell.
17
-
18
- **How to use the dashboard:**
19
- 1. Set the queue size and upsell bonus in the sidebar. The bonus is simply a multiplier for the potential earnings from successful upsells.
20
- 2. View the current queue of customers and their details.
21
- 3. For each customer, see the model's predicted probability of subscription.
22
- 4. After each call, indicate whether the upsell was successful and submit the result.
23
- 5. Track your total bonus based on successful upsells.
24
-
25
- **TIP** see what happens when the queue is empty πŸ˜‰
26
- """
27
- )
28
-
29
- # --- Sidebar: Set queue size and bonus, and show model probability ---
30
- with st.sidebar:
31
- st.header("Queue Settings")
32
- queue_size = st.number_input("Queue size", min_value=1, max_value=50, value=10, step=1)
33
- bonus = st.number_input("Upsell Bonus (currency/unit)", min_value=1.0, value=10.0, step=1.0)
34
- if st.button("Reset Queue"):
35
- st.session_state.queue = None # Force re-fetch
36
- st.session_state.total_bonus = 0.0
37
- # Placeholder for model probability
38
- model_prob_placeholder = st.empty()
39
-
40
-
41
-
42
- # --- Cached data fetch ---
43
- @st.cache_data(show_spinner=False)
44
- def fetch_customers(limit):
45
- API_DATA_URL = "https://dun3co-sdc-nocodb.hf.space/api/v2/tables/m39a8axnn3980w9/records"
46
- API_DATA_TOKEN = st.secrets["NOCODB_TOKEN"]
47
- HEADERS = {"xc-token": API_DATA_TOKEN}
48
- params = {"offset": 0, "limit": limit, "viewId": "vwjuv5jnaet9npuu"}
49
- res = requests.get(API_DATA_URL, headers=HEADERS, params=params)
50
- res.raise_for_status()
51
- return res.json()["list"]
52
-
53
- # --- Initialize or reset queue and bonus ---
54
- if "queue" not in st.session_state or st.session_state.queue is None:
55
- records = fetch_customers(queue_size)
56
- st.session_state.queue = random.sample(records, len(records))
57
- if "total_bonus" not in st.session_state:
58
- st.session_state.total_bonus = 0.0
59
-
60
- # --- Calculate maximum potential bonus for the remaining queue ---
61
- def get_max_potential_bonus(queue, bonus):
62
- if not queue:
63
- return 0.0, []
64
- API_MODEL_URL = "https://dun3co-marketing-lr-prediction.hf.space/predict"
65
- inputs = []
66
- for row in queue:
67
- inputs.append({
68
- "age": int(row["age"]),
69
- "balance": float(row["balance"]),
70
- "day": int(row["day"]),
71
- "campaign": int(row["campaign"]),
72
- "job": str(row["job"]),
73
- "education": str(row["education"]),
74
- "default": str(row["default"]),
75
- "housing": str(row["housing"]),
76
- "loan": str(row["loan"]),
77
- "months_since_previous_contact": str(row["months_since_previous_contact"]),
78
- "n_previous_contacts": str(row["n_previous_contacts"]),
79
- "poutcome": str(row["poutcome"]),
80
- "had_contact": bool(row["had_contact"]),
81
- "is_single": bool(row["is_single"]),
82
- "uknown_contact": bool(row["uknown_contact"]),
83
- })
84
- try:
85
- response = requests.post(API_MODEL_URL, json={"data": inputs})
86
- response.raise_for_status()
87
- probabilities = response.json()["probabilities"]
88
- max_bonus = sum((1 - p) * bonus for p in probabilities)
89
- return max_bonus, probabilities
90
- except Exception:
91
- return None, None
92
-
93
- # --- 3. Show queue visually and bonus info ---
94
- #st.subheader("Queue")
95
-
96
- # Layout: queue info (left), bonus info (center), (right column left empty for centering)
97
- queue_col, bonus_col, empty_col = st.columns([2, 1.2, 0.8])
98
-
99
- with queue_col:
100
- st.subheader("Queue")
101
- for i, row in enumerate(st.session_state.queue):
102
- st.write(f"Position {i+1}: {row['job']} ({row['age']} yrs, {row['education']})")
103
-
104
- # Calculate max potential bonus and get probabilities for queue
105
- max_potential_bonus, queue_probabilities = get_max_potential_bonus(st.session_state.queue, bonus)
106
-
107
- # --- 4. Simulate next call ---
108
- if st.session_state.queue:
109
- st.subheader("Active Call")
110
- active_row = st.session_state.queue[0]
111
-
112
- # Use current day of month if possible, fallback to API day
113
- today_day = datetime.datetime.now().day
114
- try:
115
- day_value = int(today_day)
116
- except Exception:
117
- day_value = int(active_row["day"])
118
-
119
- # Prepare model input for active call
120
- input_row = {
121
- "age": int(active_row["age"]),
122
- "balance": float(active_row["balance"]),
123
- "day": day_value,
124
- "campaign": int(active_row["campaign"]),
125
- "job": str(active_row["job"]),
126
- "education": str(active_row["education"]),
127
- "default": str(active_row["default"]),
128
- "housing": str(active_row["housing"]),
129
- "loan": str(active_row["loan"]),
130
- "months_since_previous_contact": str(active_row["months_since_previous_contact"]),
131
- "n_previous_contacts": str(active_row["n_previous_contacts"]),
132
- "poutcome": str(active_row["poutcome"]),
133
- "had_contact": bool(active_row["had_contact"]),
134
- "is_single": bool(active_row["is_single"]),
135
- "uknown_contact": bool(active_row["uknown_contact"]),
136
- }
137
- payload = {"data": [input_row]}
138
-
139
- # --- 5. Get model prediction for active call ---
140
- API_MODEL_URL = "https://dun3co-marketing-lr-prediction.hf.space/predict"
141
- try:
142
- response = requests.post(API_MODEL_URL, json=payload)
143
- response.raise_for_status()
144
- result = response.json()
145
- probability = result["probabilities"][0]
146
- # Show in sidebar
147
- model_prob_placeholder.metric("Model Probability (Subscribe)", f"{probability:.2%}")
148
- except Exception as e:
149
- st.error(f"Model API call failed: {e}")
150
- probability = None
151
- model_prob_placeholder.metric("Model Probability (Subscribe)", "N/A")
152
-
153
- # --- Customer info as tiles ---
154
- st.write("### Customer Information")
155
- keys = [k for k in active_row.keys() if k != "y"] #Dropping the target variable "y"
156
- values = [active_row[k] for k in keys] #Dropping the target variable "y"
157
- n_cols = 4
158
- cols = st.columns(n_cols)
159
- for i, key in enumerate(keys):
160
- col = cols[i % n_cols]
161
- with col:
162
- # Show the current day_value for the "day" field
163
- display_value = day_value if key == "day" else values[i]
164
- st.markdown(
165
- f"""
166
- <div style="
167
- border: 2px solid #e6e6e6;
168
- border-radius: 16px;
169
- padding: 18px 10px 14px 10px;
170
- margin-bottom: 1em;
171
- background: linear-gradient(135deg, #f9f9f9 80%, #eaf6ff 100%);
172
- box-shadow: 0 2px 8px 0 rgba(0,0,0,0.04);
173
- min-height: 80px;
174
- text-align: center;
175
- ">
176
- <div style="font-size: 1.05em; font-weight: 600; color: #2c3e50; margin-bottom: 0.3em;">
177
- {key.replace('_', ' ').capitalize()}
178
- </div>
179
- <div style="font-size: 1.15em; color: #0074d9;">
180
- {display_value}
181
- </div>
182
- </div>
183
- """,
184
- unsafe_allow_html=True,
185
- )
186
-
187
- # --- Bonus info and worker action column ---
188
- with bonus_col:
189
- st.markdown(
190
- """
191
- <div style="border:2px solid #e6e6e6; border-radius:14px; padding:18px 14px; background:#f8fbff; margin-bottom:1em;">
192
- <div style="font-size:1.2em; font-weight:700; margin-bottom:1em;">Bonus KPI's</div>
193
- <div style="font-size:1.1em; margin-bottom:0.7em;">
194
- <b>Current Bonus:</b> <span style="color:#0074d9;">{current_bonus}</span>
195
- </div>
196
- <div style="font-size:1.1em; margin-bottom:0.7em;">
197
- <b>Current Call Bonus:</b> <span style="color:#28a745;">{current_call_bonus}</span>
198
- </div>
199
- <div style="font-size:1.1em;">
200
- <b>Max Potential Bonus:</b> <span style="color:#ff851b;">{max_potential_bonus}</span>
201
- </div>
202
- </div>
203
- """.format(
204
- current_bonus=f"{st.session_state.total_bonus:.2f}",
205
- current_call_bonus=f"{(1 - probability) * bonus:.2f}" if probability is not None else "N/A",
206
- max_potential_bonus=f"{max_potential_bonus:.2f}" if max_potential_bonus is not None else "N/A"
207
- ),
208
- unsafe_allow_html=True,
209
- )
210
-
211
- # Plain Streamlit widgets for worker action (no custom styling)
212
- st.subheader("Callcenter Worker Action")
213
- upsell = st.radio("Did you upsell?", options=["Yes", "No"], key="upsell_radio", horizontal=True)
214
- submit = st.button("Submit", disabled=not st.session_state.queue, key="upsell_submit")
215
-
216
- if submit:
217
- if upsell == "Yes" and probability is not None:
218
- st.session_state.total_bonus += (1 - probability) * bonus
219
- st.session_state.queue.pop(0)
220
- st.rerun()
221
-
222
- else:
223
- rain(emoji="πŸ’Έ", font_size=54, falling_speed=5, animation_length="infinite")
224
- st.success("Queue is empty! All calls handled.")
225
- st.markdown(
226
- f"""
227
- <div style="border:2px solid #e6e6e6; border-radius:14px; padding:18px 14px; background:#f8fbff; margin-bottom:1em;">
228
- <div style="font-size:1.2em; font-weight:700; margin-bottom:1em;">Total Bonus Earned</div>
229
- <div style="font-size:2em; color:#0074d9; text-align:center;">
230
- {st.session_state.total_bonus:.2f}
231
- </div>
232
- </div>
233
- """,
234
- unsafe_allow_html=True,
235
- )
 
1
+ import streamlit as st
2
+ from streamlit_extras.let_it_rain import rain
3
+ import requests
4
+ import random
5
+ import pandas as pd
6
+ import datetime
7
+
8
+
9
+ st.title("πŸ“ž Callcenter Dashboard")
10
+
11
+ with st.expander("ℹ️ - About this dashboard", expanded=False):
12
+ st.markdown(
13
+ """
14
+ This dashboard simulates a call center environment where agents can manage a queue of customers to upsell a long term deposit bank product.
15
+ In the original paper that came with the dataset, they mention that there was inbound calls too, but it's not present in the dataset.
16
+ The dashboard fetches customer data from an API(NocoDB with test and synthetic data), displays customer information, and uses a machine learning model to predict the likelihood of a successful upsell.
17
+
18
+ **How to use the dashboard:**
19
+ 1. Set the queue size and upsell bonus in the sidebar. The bonus is simply a multiplier for the potential earnings from successful upsells.
20
+ 2. View the current queue of customers and their details.
21
+ 3. For each customer, see the model's predicted probability of subscription.
22
+ 4. After each call, indicate whether the upsell was successful and submit the result.
23
+ 5. Track your total bonus based on successful upsells.
24
+
25
+ **TIP** see what happens when the queue is empty πŸ˜‰
26
+ """
27
+ )
28
+
29
+ # --- Sidebar: Set queue size and bonus, and show model probability ---
30
+ with st.sidebar:
31
+ st.header("Queue Settings")
32
+ queue_size = st.number_input("Queue size", min_value=1, max_value=50, value=10, step=1)
33
+ bonus = st.number_input("Upsell Bonus (currency/unit)", min_value=1.0, value=10.0, step=1.0)
34
+ if st.button("Reset Queue"):
35
+ st.session_state.queue = None # Force re-fetch
36
+ st.session_state.total_bonus = 0.0
37
+ # Placeholder for model probability
38
+ model_prob_placeholder = st.empty()
39
+
40
+
41
+
42
+ # --- Cached data fetch ---
43
+ @st.cache_data(show_spinner=False)
44
+ def fetch_customers(limit):
45
+ API_DATA_URL = "https://dun3co-sdc-nocodb.hf.space/api/v2/tables/mum7zkkj2gzsdb8/records"
46
+ API_DATA_TOKEN = st.secrets["NOCODB_TOKEN"]
47
+ HEADERS = {"xc-token": API_DATA_TOKEN}
48
+ params = {"offset": 0, "limit": limit, "viewId": "vwm8chvup11gg6kj"}
49
+ res = requests.get(API_DATA_URL, headers=HEADERS, params=params)
50
+ res.raise_for_status()
51
+ return res.json()["list"]
52
+
53
+ # --- Initialize or reset queue and bonus ---
54
+ if "queue" not in st.session_state or st.session_state.queue is None:
55
+ records = fetch_customers(queue_size)
56
+ st.session_state.queue = random.sample(records, len(records))
57
+ if "total_bonus" not in st.session_state:
58
+ st.session_state.total_bonus = 0.0
59
+
60
+ # --- Calculate maximum potential bonus for the remaining queue ---
61
+ def get_max_potential_bonus(queue, bonus):
62
+ if not queue:
63
+ return 0.0, []
64
+ API_MODEL_URL = "https://dun3co-marketing-lr-prediction.hf.space/predict"
65
+ inputs = []
66
+ for row in queue:
67
+ inputs.append({
68
+ "age": int(row["age"]),
69
+ "balance": float(row["balance"]),
70
+ "day": int(row["day"]),
71
+ "campaign": int(row["campaign"]),
72
+ "job": str(row["job"]),
73
+ "education": str(row["education"]),
74
+ "default": str(row["default"]),
75
+ "housing": str(row["housing"]),
76
+ "loan": str(row["loan"]),
77
+ "months_since_previous_contact": str(row["months_since_previous_contact"]),
78
+ "n_previous_contacts": str(row["n_previous_contacts"]),
79
+ "poutcome": str(row["poutcome"]),
80
+ "had_contact": bool(row["had_contact"]),
81
+ "is_single": bool(row["is_single"]),
82
+ "uknown_contact": bool(row["uknown_contact"]),
83
+ })
84
+ try:
85
+ response = requests.post(API_MODEL_URL, json={"data": inputs})
86
+ response.raise_for_status()
87
+ probabilities = response.json()["probabilities"]
88
+ max_bonus = sum((1 - p) * bonus for p in probabilities)
89
+ return max_bonus, probabilities
90
+ except Exception:
91
+ return None, None
92
+
93
+ # --- 3. Show queue visually and bonus info ---
94
+ #st.subheader("Queue")
95
+
96
+ # Layout: queue info (left), bonus info (center), (right column left empty for centering)
97
+ queue_col, bonus_col, empty_col = st.columns([2, 1.2, 0.8])
98
+
99
+ with queue_col:
100
+ st.subheader("Queue")
101
+ for i, row in enumerate(st.session_state.queue):
102
+ st.write(f"Position {i+1}: {row['job']} ({row['age']} yrs, {row['education']})")
103
+
104
+ # Calculate max potential bonus and get probabilities for queue
105
+ max_potential_bonus, queue_probabilities = get_max_potential_bonus(st.session_state.queue, bonus)
106
+
107
+ # --- 4. Simulate next call ---
108
+ if st.session_state.queue:
109
+ st.subheader("Active Call")
110
+ active_row = st.session_state.queue[0]
111
+
112
+ # Use current day of month if possible, fallback to API day
113
+ today_day = datetime.datetime.now().day
114
+ try:
115
+ day_value = int(today_day)
116
+ except Exception:
117
+ day_value = int(active_row["day"])
118
+
119
+ # Prepare model input for active call
120
+ input_row = {
121
+ "age": int(active_row["age"]),
122
+ "balance": float(active_row["balance"]),
123
+ "day": day_value,
124
+ "campaign": int(active_row["campaign"]),
125
+ "job": str(active_row["job"]),
126
+ "education": str(active_row["education"]),
127
+ "default": str(active_row["default"]),
128
+ "housing": str(active_row["housing"]),
129
+ "loan": str(active_row["loan"]),
130
+ "months_since_previous_contact": str(active_row["months_since_previous_contact"]),
131
+ "n_previous_contacts": str(active_row["n_previous_contacts"]),
132
+ "poutcome": str(active_row["poutcome"]),
133
+ "had_contact": bool(active_row["had_contact"]),
134
+ "is_single": bool(active_row["is_single"]),
135
+ "uknown_contact": bool(active_row["uknown_contact"]),
136
+ }
137
+ payload = {"data": [input_row]}
138
+
139
+ # --- 5. Get model prediction for active call ---
140
+ API_MODEL_URL = "https://dun3co-marketing-lr-prediction.hf.space/predict"
141
+ try:
142
+ response = requests.post(API_MODEL_URL, json=payload)
143
+ response.raise_for_status()
144
+ result = response.json()
145
+ probability = result["probabilities"][0]
146
+ # Show in sidebar
147
+ model_prob_placeholder.metric("Model Probability (Subscribe)", f"{probability:.2%}")
148
+ except Exception as e:
149
+ st.error(f"Model API call failed: {e}")
150
+ probability = None
151
+ model_prob_placeholder.metric("Model Probability (Subscribe)", "N/A")
152
+
153
+ # --- Customer info as tiles ---
154
+ st.write("### Customer Information")
155
+ keys = [k for k in active_row.keys() if k != "y"] #Dropping the target variable "y"
156
+ values = [active_row[k] for k in keys] #Dropping the target variable "y"
157
+ n_cols = 4
158
+ cols = st.columns(n_cols)
159
+ for i, key in enumerate(keys):
160
+ col = cols[i % n_cols]
161
+ with col:
162
+ # Show the current day_value for the "day" field
163
+ display_value = day_value if key == "day" else values[i]
164
+ st.markdown(
165
+ f"""
166
+ <div style="
167
+ border: 2px solid #e6e6e6;
168
+ border-radius: 16px;
169
+ padding: 18px 10px 14px 10px;
170
+ margin-bottom: 1em;
171
+ background: linear-gradient(135deg, #f9f9f9 80%, #eaf6ff 100%);
172
+ box-shadow: 0 2px 8px 0 rgba(0,0,0,0.04);
173
+ min-height: 80px;
174
+ text-align: center;
175
+ ">
176
+ <div style="font-size: 1.05em; font-weight: 600; color: #2c3e50; margin-bottom: 0.3em;">
177
+ {key.replace('_', ' ').capitalize()}
178
+ </div>
179
+ <div style="font-size: 1.15em; color: #0074d9;">
180
+ {display_value}
181
+ </div>
182
+ </div>
183
+ """,
184
+ unsafe_allow_html=True,
185
+ )
186
+
187
+ # --- Bonus info and worker action column ---
188
+ with bonus_col:
189
+ st.markdown(
190
+ """
191
+ <div style="border:2px solid #e6e6e6; border-radius:14px; padding:18px 14px; background:#f8fbff; margin-bottom:1em;">
192
+ <div style="font-size:1.2em; font-weight:700; margin-bottom:1em;">Bonus KPI's</div>
193
+ <div style="font-size:1.1em; margin-bottom:0.7em;">
194
+ <b>Current Bonus:</b> <span style="color:#0074d9;">{current_bonus}</span>
195
+ </div>
196
+ <div style="font-size:1.1em; margin-bottom:0.7em;">
197
+ <b>Current Call Bonus:</b> <span style="color:#28a745;">{current_call_bonus}</span>
198
+ </div>
199
+ <div style="font-size:1.1em;">
200
+ <b>Max Potential Bonus:</b> <span style="color:#ff851b;">{max_potential_bonus}</span>
201
+ </div>
202
+ </div>
203
+ """.format(
204
+ current_bonus=f"{st.session_state.total_bonus:.2f}",
205
+ current_call_bonus=f"{(1 - probability) * bonus:.2f}" if probability is not None else "N/A",
206
+ max_potential_bonus=f"{max_potential_bonus:.2f}" if max_potential_bonus is not None else "N/A"
207
+ ),
208
+ unsafe_allow_html=True,
209
+ )
210
+
211
+ # Plain Streamlit widgets for worker action (no custom styling)
212
+ st.subheader("Callcenter Worker Action")
213
+ upsell = st.radio("Did you upsell?", options=["Yes", "No"], key="upsell_radio", horizontal=True)
214
+ submit = st.button("Submit", disabled=not st.session_state.queue, key="upsell_submit")
215
+
216
+ if submit:
217
+ if upsell == "Yes" and probability is not None:
218
+ st.session_state.total_bonus += (1 - probability) * bonus
219
+ st.session_state.queue.pop(0)
220
+ st.rerun()
221
+
222
+ else:
223
+ rain(emoji="πŸ’Έ", font_size=54, falling_speed=5, animation_length="infinite")
224
+ st.success("Queue is empty! All calls handled.")
225
+ st.markdown(
226
+ f"""
227
+ <div style="border:2px solid #e6e6e6; border-radius:14px; padding:18px 14px; background:#f8fbff; margin-bottom:1em;">
228
+ <div style="font-size:1.2em; font-weight:700; margin-bottom:1em;">Total Bonus Earned</div>
229
+ <div style="font-size:2em; color:#0074d9; text-align:center;">
230
+ {st.session_state.total_bonus:.2f}
231
+ </div>
232
+ </div>
233
+ """,
234
+ unsafe_allow_html=True,
235
+ )