Upload 2 files
Browse files- app.js +72 -56
- index.html +91 -82
app.js
CHANGED
|
@@ -187,68 +187,84 @@ function buildSpecialtyPct() {
|
|
| 187 |
}
|
| 188 |
|
| 189 |
/* ============================================================
|
| 190 |
-
|
| 191 |
-
Model:
|
| 192 |
============================================================ */
|
| 193 |
|
| 194 |
-
|
| 195 |
-
// Note: If your model is private, you need a token. If it's public, you might not need it,
|
| 196 |
-
// but it is highly recommended to avoid rate limits.
|
| 197 |
-
const HF_TOKEN = "";
|
| 198 |
|
| 199 |
async function runModelPrediction() {
|
| 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 |
-
if (!response.ok) {
|
| 236 |
-
const errorText = await response.text();
|
| 237 |
-
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
| 238 |
-
}
|
| 239 |
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
}
|
| 252 |
|
| 253 |
/* Init */
|
| 254 |
-
document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
}
|
| 188 |
|
| 189 |
/* ============================================================
|
| 190 |
+
CLIENT-SIDE INFERENCE ENGINE (PYODIDE)
|
| 191 |
+
Model: scikit-learn model loaded directly in browser
|
| 192 |
============================================================ */
|
| 193 |
|
| 194 |
+
let pyodideInstance = null;
|
|
|
|
|
|
|
|
|
|
| 195 |
|
| 196 |
async function runModelPrediction() {
|
| 197 |
+
const resultDiv = document.getElementById('prediction-result');
|
| 198 |
+
const predictBtn = document.getElementById('btn-predict');
|
| 199 |
+
|
| 200 |
+
// Handle UI state for loading (disable button and show spinner)
|
| 201 |
+
predictBtn.disabled = true;
|
| 202 |
+
resultDiv.style.display = 'block';
|
| 203 |
+
resultDiv.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Initializing Pyodide & loading model (may take a moment)...';
|
| 204 |
+
|
| 205 |
+
try {
|
| 206 |
+
// Initialize Pyodide
|
| 207 |
+
if (!pyodideInstance) {
|
| 208 |
+
pyodideInstance = await loadPyodide();
|
| 209 |
+
// Load the scikit-learn and numpy packages into the browser memory
|
| 210 |
+
await pyodideInstance.loadPackage(['scikit-learn', 'numpy']);
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
resultDiv.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Downloading model and running inference...';
|
| 214 |
+
|
| 215 |
+
// Use Python code (executed via JavaScript) to download the model directly from this URL
|
| 216 |
+
const pythonCode = `
|
| 217 |
+
import pyodide.http
|
| 218 |
+
import numpy as np
|
| 219 |
+
import joblib
|
| 220 |
+
|
| 221 |
+
url = "https://huggingface.co/pfizer-project-team/binary-segA-vs-segBC/resolve/main/sklearn_model.joblib"
|
| 222 |
+
|
| 223 |
+
# In Pyodide, we must use pyfetch to make HTTPS requests
|
| 224 |
+
response = await pyodide.http.pyfetch(url)
|
| 225 |
+
with open("sklearn_model.joblib", "wb") as f:
|
| 226 |
+
f.write(await response.bytes())
|
| 227 |
+
|
| 228 |
+
# Load the scikit-learn model
|
| 229 |
+
model = joblib.load("sklearn_model.joblib")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
|
| 231 |
+
# The model expects a flattened tensor of exactly 5590 features (86 weeks * 65 features)
|
| 232 |
+
# Create a dummy numpy array of shape (1, 5590) of zeros for the prediction
|
| 233 |
+
dummy_features = np.zeros((1, 5590))
|
| 234 |
+
|
| 235 |
+
# Run the prediction using model.predict()
|
| 236 |
+
prediction = model.predict(dummy_features)
|
| 237 |
+
int(prediction[0])
|
| 238 |
+
`;
|
| 239 |
+
|
| 240 |
+
// Execute python code and wait for result
|
| 241 |
+
const result = await pyodideInstance.runPythonAsync(pythonCode);
|
| 242 |
+
|
| 243 |
+
// Return the result to JS and update UI
|
| 244 |
+
if (result === 1) {
|
| 245 |
+
resultDiv.innerHTML = '<i class="fas fa-check-circle" style="color: var(--accent-green);"></i> SEG_B/C (High Potential)';
|
| 246 |
+
} else {
|
| 247 |
+
resultDiv.innerHTML = '<i class="fas fa-circle" style="color: var(--text-muted);"></i> SEG_A (Traditionalist)';
|
| 248 |
}
|
| 249 |
+
|
| 250 |
+
} catch (error) {
|
| 251 |
+
console.error("Pyodide Client-Side ML Error:", error);
|
| 252 |
+
resultDiv.innerHTML = `<i class="fas fa-exclamation-triangle" style="color: var(--accent-coral);"></i> Inference Error: ${error.message}`;
|
| 253 |
+
} finally {
|
| 254 |
+
// Re-enable the button
|
| 255 |
+
predictBtn.disabled = false;
|
| 256 |
+
}
|
| 257 |
}
|
| 258 |
|
| 259 |
/* Init */
|
| 260 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 261 |
+
initTabs();
|
| 262 |
+
loadTab('tab-overview');
|
| 263 |
+
animateCounters();
|
| 264 |
+
|
| 265 |
+
// Bind live prediction button
|
| 266 |
+
const predictBtn = document.getElementById('btn-predict');
|
| 267 |
+
if (predictBtn) {
|
| 268 |
+
predictBtn.addEventListener('click', runModelPrediction);
|
| 269 |
+
}
|
| 270 |
+
});
|
index.html
CHANGED
|
@@ -11,6 +11,7 @@
|
|
| 11 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
| 12 |
|
| 13 |
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
|
|
|
| 14 |
</head>
|
| 15 |
|
| 16 |
<body>
|
|
@@ -424,112 +425,120 @@
|
|
| 424 |
|
| 425 |
<!-- ==================== TAB 6: UNLABELED OPPORTUNITY ==================== -->
|
| 426 |
<div id="tab-opportunity" class="tab-content">
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
|
|
|
| 433 |
</div>
|
| 434 |
-
</div>
|
| 435 |
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
|
|
|
| 454 |
</div>
|
| 455 |
-
</div>
|
| 456 |
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
|
|
|
|
|
|
|
|
|
| 460 |
</div>
|
| 461 |
-
</div>
|
| 462 |
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 473 |
</div>
|
| 474 |
</div>
|
|
|
|
| 475 |
</div>
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
<
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
<
|
| 485 |
</div>
|
| 486 |
-
</div>
|
| 487 |
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
<div class="section-
|
| 491 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 492 |
</div>
|
| 493 |
-
<div>
|
| 494 |
-
|
| 495 |
-
<
|
|
|
|
|
|
|
| 496 |
</div>
|
| 497 |
</div>
|
| 498 |
-
<div class="grid-5" id="hcp-detail-grid"></div>
|
| 499 |
-
<div class="alert-box alert-warning" style="margin-top:16px">
|
| 500 |
-
<i class="fas fa-bullhorn"></i>
|
| 501 |
-
<span><strong>Action Required:</strong> This HCP has never been visited by a sales representative yet shows significant UC prescribing activity. Recommend scheduling an initial detail call.</span>
|
| 502 |
-
</div>
|
| 503 |
-
</div>
|
| 504 |
|
| 505 |
-
</div> ```
|
| 506 |
|
| 507 |
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
</div>
|
| 516 |
</div>
|
|
|
|
| 517 |
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
</div>
|
| 527 |
</div>
|
| 528 |
</div>
|
|
|
|
| 529 |
|
| 530 |
|
| 531 |
|
| 532 |
-
|
| 533 |
</body>
|
| 534 |
|
| 535 |
</html>
|
|
|
|
| 11 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
| 12 |
|
| 13 |
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
| 14 |
+
<script src="https://cdn.jsdelivr.net/pyodide/v0.25.0/full/pyodide.js"></script>
|
| 15 |
</head>
|
| 16 |
|
| 17 |
<body>
|
|
|
|
| 425 |
|
| 426 |
<!-- ==================== TAB 6: UNLABELED OPPORTUNITY ==================== -->
|
| 427 |
<div id="tab-opportunity" class="tab-content">
|
| 428 |
+
|
| 429 |
+
<div class="section-header">
|
| 430 |
+
<div class="section-icon"><i class="fas fa-crosshairs"></i></div>
|
| 431 |
+
<div>
|
| 432 |
+
<div class="section-title">Unlabeled HCP Opportunity</div>
|
| 433 |
+
<div class="section-subtitle">Prioritizing 633 unclassified HCPs for commercial outreach</div>
|
| 434 |
+
</div>
|
| 435 |
</div>
|
|
|
|
| 436 |
|
| 437 |
+
<div class="grid-2" style="align-items: start; margin-bottom: 24px;">
|
| 438 |
+
|
| 439 |
+
<div>
|
| 440 |
+
<div class="grid-3" style="margin-bottom: 24px;">
|
| 441 |
+
<div class="card tier-card tier-1">
|
| 442 |
+
<div class="tier-value" style="color:var(--accent-green)">39</div>
|
| 443 |
+
<div class="tier-label">Tier 1 — Immediate</div>
|
| 444 |
+
<div class="tier-desc">Score ≥ 0.60. Highest prescribing + growth signals.</div>
|
| 445 |
+
</div>
|
| 446 |
+
<div class="card tier-card tier-2">
|
| 447 |
+
<div class="tier-value" style="color:var(--accent-amber)">14</div>
|
| 448 |
+
<div class="tier-label">Tier 2 — Validate</div>
|
| 449 |
+
<div class="tier-desc">Score 0.35–0.60. Moderate opportunity, needs validation.</div>
|
| 450 |
+
</div>
|
| 451 |
+
<div class="card tier-card tier-3">
|
| 452 |
+
<div class="tier-value" style="color:var(--text-muted)">580</div>
|
| 453 |
+
<div class="tier-label">Tier 3 — Monitor</div>
|
| 454 |
+
<div class="tier-desc">Score < 0.35. Low activity, monitor for emergence.</div>
|
| 455 |
+
</div>
|
| 456 |
</div>
|
|
|
|
| 457 |
|
| 458 |
+
<div class="alert-box alert-warning">
|
| 459 |
+
<i class="fas fa-exclamation-triangle"></i>
|
| 460 |
+
<span><strong>Coverage Gap:</strong> 347 of 633 unlabeled HCPs (54.8%) have zero rep visits. Among
|
| 461 |
+
Tier 1 (high-opportunity) HCPs, many prescribe actively but have never been contacted by a sales
|
| 462 |
+
representative.</span>
|
| 463 |
+
</div>
|
| 464 |
</div>
|
|
|
|
| 465 |
|
| 466 |
+
<div class="card" id="inference-card" style="height: 100%;">
|
| 467 |
+
<div class="chart-title">Live Model Prediction: Segment Classification</div>
|
| 468 |
+
<div class="chart-container" style="height: auto; padding: 15px 0;">
|
| 469 |
+
<p style="font-size: 14px; color: var(--text-secondary); margin-bottom: 25px;">
|
| 470 |
+
Test the live Hugging Face model (SEG_A vs SEG_BC) with sample HCP data.
|
| 471 |
+
</p>
|
| 472 |
+
<button id="btn-predict"
|
| 473 |
+
style="width: 100%; justify-content: center; border-radius: 8px; padding: 12px; background: var(--pfizer-blue); color: white; border: none; cursor: pointer; font-weight: 600; font-size: 14px; display: flex; align-items: center; gap: 8px; transition: opacity 0.2s;"
|
| 474 |
+
onmouseover="this.style.opacity=0.9" onmouseout="this.style.opacity=1">
|
| 475 |
+
<i class="fas fa-brain"></i> Run Live Prediction
|
| 476 |
+
</button>
|
| 477 |
+
<div id="prediction-result"
|
| 478 |
+
style="margin-top: 20px; font-weight: 600; color: var(--pfizer-deep); font-size: 16px; background: var(--bg-active); padding: 12px; border-radius: 8px; display: none;">
|
| 479 |
+
</div>
|
| 480 |
</div>
|
| 481 |
</div>
|
| 482 |
+
|
| 483 |
</div>
|
| 484 |
+
<div class="grid-2" style="margin-top:24px">
|
| 485 |
+
<div class="card">
|
| 486 |
+
<div class="chart-title">Opportunity Score Distribution (633 Unlabeled HCPs)</div>
|
| 487 |
+
<div class="chart-container" style="height:300px"><canvas id="chart-opp-hist"></canvas></div>
|
| 488 |
+
</div>
|
| 489 |
+
<div class="card">
|
| 490 |
+
<div class="chart-title">Click a red point to identify the HCP below ↓</div>
|
| 491 |
+
<div class="chart-container" style="height:300px"><canvas id="chart-opp-scatter"></canvas></div>
|
| 492 |
+
</div>
|
| 493 |
</div>
|
|
|
|
| 494 |
|
| 495 |
+
<div id="hcp-detail-panel" class="card"
|
| 496 |
+
style="margin-top:24px;display:none;border-left:4px solid var(--accent-coral)">
|
| 497 |
+
<div class="section-header" style="margin-bottom:16px">
|
| 498 |
+
<div class="section-icon" style="background:#fef2f2;color:var(--accent-coral)">
|
| 499 |
+
<i class="fas fa-user-md"></i>
|
| 500 |
+
</div>
|
| 501 |
+
<div>
|
| 502 |
+
<div class="section-title" id="hcp-detail-title">HCP Selected</div>
|
| 503 |
+
<div class="section-subtitle">Zero rep visits — high opportunity for outreach</div>
|
| 504 |
+
</div>
|
| 505 |
</div>
|
| 506 |
+
<div class="grid-5" id="hcp-detail-grid"></div>
|
| 507 |
+
<div class="alert-box alert-warning" style="margin-top:16px">
|
| 508 |
+
<i class="fas fa-bullhorn"></i>
|
| 509 |
+
<span><strong>Action Required:</strong> This HCP has never been visited by a sales representative yet
|
| 510 |
+
shows significant UC prescribing activity. Recommend scheduling an initial detail call.</span>
|
| 511 |
</div>
|
| 512 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 513 |
|
| 514 |
+
</div> ```
|
| 515 |
|
| 516 |
|
| 517 |
+
<!-- ==================== TAB 7: SPECIALTY MIX ==================== -->
|
| 518 |
+
<div id="tab-specialty" class="tab-content">
|
| 519 |
+
<div class="section-header">
|
| 520 |
+
<div class="section-icon"><i class="fas fa-stethoscope"></i></div>
|
| 521 |
+
<div>
|
| 522 |
+
<div class="section-title">Specialty & Demographics</div>
|
| 523 |
+
<div class="section-subtitle">HCP specialty distribution across segments</div>
|
|
|
|
| 524 |
</div>
|
| 525 |
+
</div>
|
| 526 |
|
| 527 |
+
<div class="grid-2">
|
| 528 |
+
<div class="card">
|
| 529 |
+
<div class="chart-title">HCPs by Specialty and Segment (Stacked)</div>
|
| 530 |
+
<div class="chart-container" style="height:300px"><canvas id="chart-spec-stack"></canvas></div>
|
| 531 |
+
</div>
|
| 532 |
+
<div class="card">
|
| 533 |
+
<div class="chart-title">Specialty Composition (% within each specialty)</div>
|
| 534 |
+
<div class="chart-container" style="height:300px"><canvas id="chart-spec-pct"></canvas></div>
|
|
|
|
| 535 |
</div>
|
| 536 |
</div>
|
| 537 |
+
</div>
|
| 538 |
|
| 539 |
|
| 540 |
|
| 541 |
+
<script src="app.js"></script>
|
| 542 |
</body>
|
| 543 |
|
| 544 |
</html>
|