-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsimulate_traffic.py
More file actions
224 lines (193 loc) · 8.51 KB
/
Copy pathsimulate_traffic.py
File metadata and controls
224 lines (193 loc) · 8.51 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
"""
simulate_traffic.py — Send realistic diverse prediction requests to the API.
This script closes the gap between testing with a single repeated payload
and real production traffic. It generates customers that match the actual
Telco dataset distribution, so the drift monitor sees genuine variance.
Two modes:
normal — data matches training distribution → drift monitor: no_drift
shifted — data is artificially shifted → drift monitor: drift_detected
Usage:
python simulate_traffic.py --mode normal --n 100
python simulate_traffic.py --mode shifted --n 100
python simulate_traffic.py --mode normal --n 100 --url http://localhost:8000
"""
import argparse
import json
import random
import time
import urllib.request
import urllib.error
# ---------------------------------------------------------------------------
# Realistic Telco data distributions (from training data analysis)
# ---------------------------------------------------------------------------
NORMAL_DISTRIBUTIONS = {
# tenure: skewed — many new customers, many long-term
"tenure": lambda: int(random.choices(
range(0, 73),
weights=[max(1, 10 - abs(i - 0) * 0.1) if i < 12
else max(1, 5 - abs(i - 36) * 0.05) if i < 60
else 3
for i in range(73)]
)[0]),
"MonthlyCharges": lambda: round(random.gauss(64.8, 30.1), 2),
"TotalCharges": lambda: round(max(18.8, random.gauss(2283.3, 2266.8)), 2),
"gender": lambda: random.choice(["Male", "Female"]),
"SeniorCitizen": lambda: random.choices([0, 1], weights=[84, 16])[0],
"Partner": lambda: random.choices(["Yes", "No"], weights=[48, 52])[0],
"Dependents": lambda: random.choices(["Yes", "No"], weights=[30, 70])[0],
"PhoneService": lambda: random.choices(["Yes", "No"], weights=[90, 10])[0],
"MultipleLines": lambda: random.choices(
["Yes", "No", "No phone service"], weights=[42, 48, 10])[0],
"InternetService": lambda: random.choices(
["DSL", "Fiber optic", "No"], weights=[34, 44, 22])[0],
"OnlineSecurity": lambda: random.choices(
["Yes", "No", "No internet service"], weights=[29, 50, 21])[0],
"OnlineBackup": lambda: random.choices(
["Yes", "No", "No internet service"], weights=[34, 44, 22])[0],
"DeviceProtection": lambda: random.choices(
["Yes", "No", "No internet service"], weights=[34, 44, 22])[0],
"TechSupport": lambda: random.choices(
["Yes", "No", "No internet service"], weights=[29, 49, 22])[0],
"StreamingTV": lambda: random.choices(
["Yes", "No", "No internet service"], weights=[38, 40, 22])[0],
"StreamingMovies": lambda: random.choices(
["Yes", "No", "No internet service"], weights=[39, 39, 22])[0],
"Contract": lambda: random.choices(
["Month-to-month", "One year", "Two year"], weights=[55, 21, 24])[0],
"PaperlessBilling": lambda: random.choices(["Yes", "No"], weights=[59, 41])[0],
"PaymentMethod": lambda: random.choices(
["Electronic check", "Mailed check",
"Bank transfer (automatic)", "Credit card (automatic)"],
weights=[34, 23, 22, 21])[0],
}
# Shifted distribution — simulates a change in customer profile
# e.g. a new marketing campaign attracted younger, high-spending customers
SHIFTED_DISTRIBUTIONS = {
**NORMAL_DISTRIBUTIONS,
# Much shorter tenure (new customer acquisition surge)
"tenure": lambda: int(random.gauss(5, 3)),
# Higher monthly charges (premium tier customers)
"MonthlyCharges": lambda: round(random.gauss(95.0, 15.0), 2),
# Almost all fiber optic (shift in internet service mix)
"InternetService": lambda: random.choices(
["DSL", "Fiber optic", "No"], weights=[5, 90, 5])[0],
# Mostly month-to-month (more churn risk)
"Contract": lambda: random.choices(
["Month-to-month", "One year", "Two year"], weights=[85, 10, 5])[0],
# Electronic check dominates
"PaymentMethod": lambda: random.choices(
["Electronic check", "Mailed check",
"Bank transfer (automatic)", "Credit card (automatic)"],
weights=[70, 10, 10, 10])[0],
}
def generate_customer(dist: dict) -> dict:
"""Generate one realistic customer record from a distribution."""
customer = {field: fn() for field, fn in dist.items()}
# Clamp numeric values to valid ranges
customer["SeniorCitizen"] = max(0, min(1, customer["SeniorCitizen"]))
customer["tenure"] = max(0, min(72, customer["tenure"]))
customer["MonthlyCharges"] = max(18.25, min(118.75, customer["MonthlyCharges"]))
customer["TotalCharges"] = max(18.8, customer["TotalCharges"])
return customer
def send_request(url: str, payload: dict) -> dict:
"""Send one POST request and return the response."""
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(
url,
data=data,
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req, timeout=30) as resp:
return json.loads(resp.read().decode())
def main():
parser = argparse.ArgumentParser(
description="Send realistic traffic to the Churn API to test drift monitoring."
)
parser.add_argument(
"--mode",
choices=["normal", "shifted"],
default="normal",
help="normal = matches training distribution, shifted = drifted data",
)
parser.add_argument(
"--n",
type=int,
default=60,
help="Number of requests to send (default: 60)",
)
parser.add_argument(
"--url",
default="https://churn-api-0cwo.onrender.com",
help="Base URL of the API",
)
parser.add_argument(
"--delay",
type=float,
default=0.3,
help="Delay in seconds between requests (default: 0.3)",
)
args = parser.parse_args()
dist = NORMAL_DISTRIBUTIONS if args.mode == "normal" else SHIFTED_DISTRIBUTIONS
predict_url = f"{args.url}/predict"
drift_url = f"{args.url}/drift"
print(f"\n{'='*55}")
print(f" Mode: {args.mode.upper()}")
print(f" Requests: {args.n}")
print(f" Target: {predict_url}")
print(f"{'='*55}\n")
success = 0
failed = 0
for i in range(1, args.n + 1):
customer = generate_customer(dist)
try:
result = send_request(predict_url, customer)
pred = result.get("prediction", "?")
prob = round(result.get("probability", 0), 3)
print(f" [{i:>3}/{args.n}] prediction={pred} probability={prob}")
success += 1
except Exception as exc:
print(f" [{i:>3}/{args.n}] ERROR: {exc}")
failed += 1
if args.delay > 0:
time.sleep(args.delay)
# ── Check drift after sending all requests ──────────────────────────────
print(f"\n{'='*55}")
print(f" Sent: {success} successful, {failed} failed")
print(f" Checking drift...")
print(f"{'='*55}\n")
try:
req = urllib.request.Request(drift_url, method="GET")
with urllib.request.urlopen(req, timeout=30) as resp:
report = json.loads(resp.read().decode())
print(f" Status: {report.get('status', 'unknown').upper()}")
print(f" Samples: {report.get('samples', 0)}")
print()
features = report.get("features", {})
if features:
print(" Feature drift scores:")
for feat, info in features.items():
if info["type"] == "numeric":
score = f"PSI={info['psi']}"
flag = "🔴" if info["psi"] > 0.2 else "🟡" if info["psi"] > 0.1 else "🟢"
else:
score = f"chi2={info['chi2']}"
flag = "🔴" if info["chi2"] > 0.5 else "🟡" if info["chi2"] > 0.2 else "🟢"
print(f" {flag} {feat:<20} {score}")
alerts = report.get("alerts", [])
warnings = report.get("warnings", [])
if alerts:
print(f"\n 🔴 Alerts ({len(alerts)}):")
for a in alerts:
print(f" {a}")
if warnings:
print(f"\n 🟡 Warnings ({len(warnings)}):")
for w in warnings:
print(f" {w}")
if not alerts and not warnings:
print(" 🟢 No drift detected — data matches training distribution")
except Exception as exc:
print(f" Drift check failed: {exc}")
print(f"\n{'='*55}\n")
if __name__ == "__main__":
main()