Aryan0777/logistic_regression
0
1"""2Logistic Regression: Interactive Tutorial3A clean, professional learning experience.4"""5 6import gradio as gr7import numpy as np8import matplotlib9matplotlib.use('Agg')10import matplotlib.pyplot as plt11 12# ============================================================================13# DATA & CONFIGURATION14# ============================================================================15 16PATIENTS = [17 {"id": "A", "phq9": 5, "sleep": 8, "depressed": 0},18 {"id": "B", "phq9": 8, "sleep": 6, "depressed": 0},19 {"id": "C", "phq9": 12, "sleep": 5, "depressed": 1},20 {"id": "D", "phq9": 15, "sleep": 4, "depressed": 1},21 {"id": "E", "phq9": 18, "sleep": 3, "depressed": 1},22 {"id": "F", "phq9": 6, "sleep": 7, "depressed": 0},23]24 25# Trained model weights26W1, W2, B = 0.5, -0.3, -4.027 28# ============================================================================29# CORE FUNCTIONS30# ============================================================================31 32def sigmoid(z):33 z = np.clip(z, -500, 500)34 return 1 / (1 + np.exp(-z))35 36def compute_z(phq9, sleep, w1, w2, b):37 return w1 * phq9 + w2 * sleep + b38 39def predict(phq9, sleep, w1=W1, w2=W2, b=B):40 z = compute_z(phq9, sleep, w1, w2, b)41 return z, sigmoid(z)42 43# ============================================================================44# CLEAN CSS - Apple-inspired light theme45# ============================================================================46 47CSS = """48/* Base */49.gradio-container {50 font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', Roboto, sans-serif !important;51 background: #fafafa !important;52 max-width: 1200px !important;53 margin: 0 auto !important;54}55 56/* Typography */57h1, h2, h3, h4 {58 font-weight: 600 !important;59 color: #1d1d1f !important;60 letter-spacing: -0.01em !important;61}62 63h1 { font-size: 2.5rem !important; }64h2 { font-size: 1.75rem !important; }65h3 { font-size: 1.25rem !important; }66 67p, label, span {68 color: #424245 !important;69 line-height: 1.6 !important;70}71 72/* Cards */73.card {74 background: #ffffff;75 border-radius: 12px;76 padding: 24px;77 box-shadow: 0 1px 3px rgba(0,0,0,0.08);78 border: 1px solid #e5e5e7;79}80 81/* Data table */82.data-table {83 width: 100%;84 border-collapse: collapse;85 font-size: 14px;86}87 88.data-table th {89 text-align: left;90 padding: 12px 16px;91 background: #f5f5f7;92 color: #1d1d1f;93 font-weight: 600;94 border-bottom: 1px solid #e5e5e7;95}96 97.data-table td {98 padding: 12px 16px;99 border-bottom: 1px solid #f0f0f2;100 color: #424245;101}102 103.data-table tr:hover {104 background: #fafafa;105}106 107/* Status indicators */108.status {109 display: inline-block;110 padding: 4px 12px;111 border-radius: 100px;112 font-size: 12px;113 font-weight: 500;114}115 116.status-positive {117 background: #fef2f2;118 color: #dc2626;119}120 121.status-negative {122 background: #f0fdf4;123 color: #16a34a;124}125 126/* Result display */127.result-box {128 background: #ffffff;129 border-radius: 16px;130 padding: 32px;131 text-align: center;132 border: 1px solid #e5e5e7;133}134 135.result-probability {136 font-size: 64px;137 font-weight: 700;138 letter-spacing: -0.02em;139 margin: 16px 0;140}141 142.result-probability.high { color: #dc2626; }143.result-probability.low { color: #16a34a; }144 145.result-label {146 font-size: 14px;147 color: #86868b;148 text-transform: uppercase;149 letter-spacing: 0.05em;150}151 152/* Calculation breakdown */153.calc-row {154 display: flex;155 justify-content: space-between;156 align-items: center;157 padding: 12px 0;158 border-bottom: 1px solid #f0f0f2;159}160 161.calc-row:last-child {162 border-bottom: none;163 padding-top: 16px;164 margin-top: 8px;165 border-top: 2px solid #e5e5e7;166}167 168.calc-label {169 color: #86868b;170 font-size: 14px;171}172 173.calc-value {174 font-weight: 600;175 font-size: 18px;176 font-family: 'SF Mono', 'Menlo', monospace;177}178 179.calc-value.positive { color: #16a34a; }180.calc-value.negative { color: #dc2626; }181 182/* Info box */183.info-box {184 background: #f5f5f7;185 border-radius: 8px;186 padding: 16px 20px;187 margin: 16px 0;188 border-left: 3px solid #0066cc;189}190 191.info-box p {192 margin: 0;193 font-size: 14px;194 color: #424245;195}196"""197 198# ============================================================================199# VISUALIZATION (Matplotlib)200# ============================================================================201 202def create_sigmoid_plot(z_point=None, prob_point=None):203 """Clean sigmoid visualization using matplotlib."""204 fig, ax = plt.subplots(figsize=(10, 5))205 206 z_vals = np.linspace(-6, 6, 200)207 probs = sigmoid(z_vals)208 209 # Sigmoid curve210 ax.plot(z_vals, probs, color='#0066cc', linewidth=2.5, label='σ(z)')211 212 # Decision threshold213 ax.axhline(y=0.5, linestyle=':', color='#86868b', linewidth=1, label='Threshold (50%)')214 ax.axvline(x=0, linestyle=':', color='#e5e5e7', linewidth=1)215 216 # Shade regions217 ax.fill_between(z_vals, probs, 0, where=(z_vals < 0), alpha=0.1, color='#16a34a')218 ax.fill_between(z_vals, probs, 0, where=(z_vals >= 0), alpha=0.1, color='#dc2626')219 220 # Current point221 if z_point is not None:222 color = '#dc2626' if prob_point >= 0.5 else '#16a34a'223 ax.scatter([z_point], [prob_point], s=120, c=color, zorder=5, edgecolors='white', linewidths=2)224 ax.annotate(f'{prob_point:.0%}', (z_point, prob_point), 225 textcoords="offset points", xytext=(10, 10), fontsize=12, fontweight='600', color=color)226 227 ax.set_xlabel('z (weighted sum)', fontsize=12, color='#1d1d1f')228 ax.set_ylabel('Probability', fontsize=12, color='#1d1d1f')229 ax.set_xlim(-6.5, 6.5)230 ax.set_ylim(-0.05, 1.05)231 ax.set_yticks([0, 0.25, 0.5, 0.75, 1.0])232 ax.set_yticklabels(['0%', '25%', '50%', '75%', '100%'])233 234 ax.spines['top'].set_visible(False)235 ax.spines['right'].set_visible(False)236 ax.spines['left'].set_color('#e5e5e7')237 ax.spines['bottom'].set_color('#e5e5e7')238 ax.tick_params(colors='#424245')239 ax.grid(True, alpha=0.3, color='#e5e5e7')240 241 fig.patch.set_facecolor('white')242 ax.set_facecolor('white')243 244 plt.tight_layout()245 return fig246 247 248def create_boundary_plot(w1, w2, b):249 """Clean decision boundary visualization using matplotlib."""250 fig, ax = plt.subplots(figsize=(10, 6))251 252 # Create probability surface253 phq_range = np.linspace(0, 22, 100)254 sleep_range = np.linspace(0, 10, 100)255 PHQ, SLEEP = np.meshgrid(phq_range, sleep_range)256 Z = sigmoid(w1 * PHQ + w2 * SLEEP + b)257 258 # Probability heatmap259 from matplotlib.colors import LinearSegmentedColormap260 colors = ['#dcfce7', '#ffffff', '#fee2e2']261 cmap = LinearSegmentedColormap.from_list('custom', colors)262 263 contour = ax.contourf(PHQ, SLEEP, Z, levels=20, cmap=cmap, alpha=0.8)264 cbar = fig.colorbar(contour, ax=ax, format='%.0f%%', ticks=[0, 0.25, 0.5, 0.75, 1.0])265 cbar.ax.set_yticklabels(['0%', '25%', '50%', '75%', '100%'])266 cbar.set_label('P(Depressed)', fontsize=11)267 268 # Decision boundary269 if w2 != 0:270 boundary_phq = np.linspace(0, 22, 100)271 boundary_sleep = (-w1 * boundary_phq - b) / w2272 mask = (boundary_sleep >= 0) & (boundary_sleep <= 10)273 ax.plot(boundary_phq[mask], boundary_sleep[mask], 'k--', linewidth=2, label='Boundary (50%)')274 275 # Patient points276 for p in PATIENTS:277 color = '#dc2626' if p['depressed'] else '#16a34a'278 marker = 's' if p['depressed'] else 'o'279 ax.scatter(p['phq9'], p['sleep'], s=100, c=color, marker=marker, 280 edgecolors='white', linewidths=1.5, zorder=5)281 ax.annotate(p['id'], (p['phq9'], p['sleep']), 282 textcoords="offset points", xytext=(8, 0), fontsize=10, fontweight='500')283 284 ax.set_xlabel('PHQ-9 Score', fontsize=12, color='#1d1d1f')285 ax.set_ylabel('Sleep (hours)', fontsize=12, color='#1d1d1f')286 ax.set_xlim(0, 22)287 ax.set_ylim(0, 10)288 289 ax.spines['top'].set_visible(False)290 ax.spines['right'].set_visible(False)291 ax.spines['left'].set_color('#e5e5e7')292 ax.spines['bottom'].set_color('#e5e5e7')293 ax.tick_params(colors='#424245')294 295 # Legend296 from matplotlib.lines import Line2D297 legend_elements = [298 Line2D([0], [0], marker='o', color='w', markerfacecolor='#16a34a', markersize=10, label='Healthy'),299 Line2D([0], [0], marker='s', color='w', markerfacecolor='#dc2626', markersize=10, label='Depressed'),300 Line2D([0], [0], linestyle='--', color='black', label='Decision boundary')301 ]302 ax.legend(handles=legend_elements, loc='upper right', framealpha=0.9)303 304 fig.patch.set_facecolor('white')305 ax.set_facecolor('white')306 307 plt.tight_layout()308 return fig309 310 311# ============================================================================312# HTML COMPONENTS313# ============================================================================314 315def dataset_table_html():316 """Render the dataset as a clean table."""317 rows = ""318 for p in PATIENTS:319 status_class = "status-positive" if p['depressed'] else "status-negative"320 status_text = "Depressed" if p['depressed'] else "Healthy"321 rows += f"""322 <tr>323 <td style="font-weight: 500;">Patient {p['id']}</td>324 <td>{p['phq9']}</td>325 <td>{p['sleep']} hours</td>326 <td><span class="status {status_class}">{status_text}</span></td>327 </tr>328 """329 330 return f"""331 <table class="data-table">332 <thead>333 <tr>334 <th>Patient</th>335 <th>PHQ-9 Score</th>336 <th>Sleep</th>337 <th>Diagnosis</th>338 </tr>339 </thead>340 <tbody>{rows}</tbody>341 </table>342 """343 344 345def calculation_html(phq9, sleep, w1, w2, b):346 """Render the calculation breakdown."""347 phq_contrib = w1 * phq9348 sleep_contrib = w2 * sleep349 z = phq_contrib + sleep_contrib + b350 351 def value_class(v):352 return "positive" if v >= 0 else "negative"353 354 def fmt(v):355 return f"+{v:.2f}" if v >= 0 else f"{v:.2f}"356 357 return f"""358 <div class="card" style="margin-top: 16px;">359 <h3 style="margin-top: 0; margin-bottom: 16px;">Calculation</h3>360 361 <div class="calc-row">362 <span class="calc-label">PHQ-9 ({phq9}) × w₁ ({w1})</span>363 <span class="calc-value {value_class(phq_contrib)}">{fmt(phq_contrib)}</span>364 </div>365 366 <div class="calc-row">367 <span class="calc-label">Sleep ({sleep}h) × w₂ ({w2})</span>368 <span class="calc-value {value_class(sleep_contrib)}">{fmt(sleep_contrib)}</span>369 </div>370 371 <div class="calc-row">372 <span class="calc-label">Bias (b)</span>373 <span class="calc-value {value_class(b)}">{fmt(b)}</span>374 </div>375 376 <div class="calc-row">377 <span class="calc-label" style="font-weight: 600; color: #1d1d1f;">z (total)</span>378 <span class="calc-value" style="font-size: 22px;">{fmt(z)}</span>379 </div>380 </div>381 """382 383 384def result_html(phq9, sleep, w1, w2, b):385 """Render the prediction result."""386 z, prob = predict(phq9, sleep, w1, w2, b)387 prob_class = "high" if prob >= 0.5 else "low"388 prediction = "Likely Depressed" if prob >= 0.5 else "Likely Healthy"389 390 return f"""391 <div class="result-box">392 <div class="result-label">Probability of Depression</div>393 <div class="result-probability {prob_class}">{prob:.0%}</div>394 <div style="font-size: 18px; font-weight: 500; color: #1d1d1f;">{prediction}</div>395 </div>396 """397 398 399def comparison_table_html(trained=True):400 """Show predictions for all patients."""401 w1, w2, b = (W1, W2, B) if trained else (0.1, -0.1, 0.0)402 title = "Trained Model" if trained else "Untrained Model"403 weights_text = f"w₁={w1}, w₂={w2}, b={b}"404 405 rows = ""406 correct = 0407 408 for p in PATIENTS:409 z = compute_z(p['phq9'], p['sleep'], w1, w2, b)410 prob = sigmoid(z)411 predicted = 1 if prob >= 0.5 else 0412 is_correct = predicted == p['depressed']413 if is_correct:414 correct += 1415 416 prob_color = "#dc2626" if prob >= 0.5 else "#16a34a"417 check = "✓" if is_correct else "✗"418 check_color = "#16a34a" if is_correct else "#dc2626"419 420 rows += f"""421 <tr>422 <td>Patient {p['id']}</td>423 <td>{p['phq9']}</td>424 <td>{p['sleep']}h</td>425 <td style="font-family: monospace;">{z:.2f}</td>426 <td style="color: {prob_color}; font-weight: 500;">{prob:.0%}</td>427 <td style="color: {check_color}; font-weight: 600;">{check}</td>428 </tr>429 """430 431 accuracy = correct / len(PATIENTS) * 100432 acc_color = "#16a34a" if accuracy > 80 else "#f59e0b"433 434 return f"""435 <div class="card">436 <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">437 <div>438 <h3 style="margin: 0;">{title}</h3>439 <p style="margin: 4px 0 0 0; font-size: 13px; color: #86868b;">{weights_text}</p>440 </div>441 <div style="text-align: right;">442 <div style="font-size: 28px; font-weight: 700; color: {acc_color};">{accuracy:.0f}%</div>443 <div style="font-size: 12px; color: #86868b;">Accuracy</div>444 </div>445 </div>446 <table class="data-table">447 <thead>448 <tr>449 <th>Patient</th>450 <th>PHQ-9</th>451 <th>Sleep</th>452 <th>z</th>453 <th>P(Dep)</th>454 <th>Correct</th>455 </tr>456 </thead>457 <tbody>{rows}</tbody>458 </table>459 </div>460 """461 462 463# ============================================================================464# BUILD APP465# ============================================================================466 467def create_app():468 with gr.Blocks(title="Logistic Regression Tutorial") as app:469 470 # Header471 gr.HTML("""472 <div style="text-align: center; padding: 48px 24px 32px;">473 <h1 style="margin: 0; font-size: 2.5rem; font-weight: 700; color: #1d1d1f;">474 Logistic Regression475 </h1>476 <p style="margin: 12px 0 0; font-size: 17px; color: #86868b;">477 An interactive tutorial using depression screening as an example478 </p>479 </div>480 """)481 482 with gr.Tabs():483 484 # TAB 1: OVERVIEW485 with gr.Tab("Overview"):486 gr.HTML("""487 <div style="margin-bottom: 32px;">488 <h2 style="margin-bottom: 8px;">The Problem</h2>489 <p style="color: #86868b;">Given patient data, predict the probability of depression.</p>490 </div>491 """)492 493 gr.HTML(dataset_table_html())494 495 gr.HTML("""496 <div class="info-box" style="margin-top: 24px;">497 <p><strong>Observable patterns:</strong> Higher PHQ-9 scores and fewer hours of sleep 498 correlate with depression. Logistic regression learns the mathematical relationship 499 between these features and the outcome.</p>500 </div>501 """)502 503 gr.HTML("""504 <div style="margin-top: 48px;">505 <h2 style="margin-bottom: 8px;">The Model</h2>506 <p style="color: #86868b; margin-bottom: 24px;">Logistic regression combines features linearly, then applies the sigmoid function.</p>507 508 <div class="card">509 <div style="text-align: center; padding: 24px;">510 <div style="font-family: 'SF Mono', Menlo, monospace; font-size: 18px; color: #1d1d1f;">511 <span style="color: #0066cc;">z</span> = w₁ · PHQ-9 + w₂ · Sleep + b512 </div>513 <div style="margin: 24px 0; color: #86868b;">↓</div>514 <div style="font-family: 'SF Mono', Menlo, monospace; font-size: 18px; color: #1d1d1f;">515 <span style="color: #0066cc;">P(Depressed)</span> = σ(z) = 1 / (1 + e<sup>-z</sup>)516 </div>517 </div>518 </div>519 520 <div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-top: 24px;">521 <div class="card" style="text-align: center;">522 <div style="font-size: 13px; color: #86868b; margin-bottom: 8px;">w₁ (PHQ-9 weight)</div>523 <div style="font-size: 24px; font-weight: 600; color: #16a34a;">+0.5</div>524 <div style="font-size: 12px; color: #86868b; margin-top: 4px;">Higher score → higher risk</div>525 </div>526 <div class="card" style="text-align: center;">527 <div style="font-size: 13px; color: #86868b; margin-bottom: 8px;">w₂ (Sleep weight)</div>528 <div style="font-size: 24px; font-weight: 600; color: #dc2626;">−0.3</div>529 <div style="font-size: 12px; color: #86868b; margin-top: 4px;">More sleep → lower risk</div>530 </div>531 <div class="card" style="text-align: center;">532 <div style="font-size: 13px; color: #86868b; margin-bottom: 8px;">b (Bias)</div>533 <div style="font-size: 24px; font-weight: 600; color: #1d1d1f;">−4.0</div>534 <div style="font-size: 12px; color: #86868b; margin-top: 4px;">Default: assume healthy</div>535 </div>536 </div>537 </div>538 """)539 540 # TAB 2: PREDICT541 with gr.Tab("Predict"):542 gr.HTML("""543 <div style="margin-bottom: 24px;">544 <h2 style="margin-bottom: 8px;">Make a Prediction</h2>545 <p style="color: #86868b;">Adjust the patient values and model weights to see how the prediction changes.</p>546 </div>547 """)548 549 with gr.Row():550 with gr.Column(scale=1):551 gr.HTML('<p style="font-weight: 500; margin-bottom: 8px;">Patient Data</p>')552 phq9_input = gr.Slider(0, 27, value=14, step=1, label="PHQ-9 Score")553 sleep_input = gr.Slider(0, 12, value=5, step=0.5, label="Sleep (hours)")554 555 gr.HTML('<p style="font-weight: 500; margin: 24px 0 8px;">Model Weights</p>')556 w1_input = gr.Slider(-1, 2, value=0.5, step=0.05, label="w₁ (PHQ-9)")557 w2_input = gr.Slider(-1, 0.5, value=-0.3, step=0.05, label="w₂ (Sleep)")558 b_input = gr.Slider(-10, 5, value=-4.0, step=0.5, label="b (Bias)")559 560 with gr.Column(scale=1):561 result_display = gr.HTML()562 calc_display = gr.HTML()563 564 def update_prediction(phq9, sleep, w1, w2, b):565 return result_html(phq9, sleep, w1, w2, b), calculation_html(phq9, sleep, w1, w2, b)566 567 for inp in [phq9_input, sleep_input, w1_input, w2_input, b_input]:568 inp.change(update_prediction, 569 [phq9_input, sleep_input, w1_input, w2_input, b_input],570 [result_display, calc_display])571 572 app.load(update_prediction,573 [phq9_input, sleep_input, w1_input, w2_input, b_input],574 [result_display, calc_display])575 576 # TAB 3: SIGMOID577 with gr.Tab("Sigmoid Function"):578 gr.HTML("""579 <div style="margin-bottom: 24px;">580 <h2 style="margin-bottom: 8px;">The Sigmoid Function</h2>581 <p style="color: #86868b;">Converts any value z into a probability between 0 and 1.</p>582 </div>583 """)584 585 with gr.Row():586 with gr.Column(scale=1):587 gr.HTML('<p style="font-weight: 500; margin-bottom: 8px;">Adjust inputs</p>')588 sig_phq9 = gr.Slider(0, 27, value=14, step=1, label="PHQ-9 Score")589 sig_sleep = gr.Slider(0, 12, value=5, step=0.5, label="Sleep (hours)")590 591 sig_result = gr.HTML()592 593 with gr.Column(scale=2):594 sigmoid_plot = gr.Plot()595 596 def update_sigmoid_view(phq9, sleep):597 z, prob = predict(phq9, sleep)598 plot = create_sigmoid_plot(z, prob)599 600 result = f"""601 <div class="card" style="margin-top: 16px;">602 <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px; text-align: center;">603 <div>604 <div style="font-size: 12px; color: #86868b;">z value</div>605 <div style="font-size: 28px; font-weight: 600; font-family: monospace;">{z:.2f}</div>606 </div>607 <div>608 <div style="font-size: 12px; color: #86868b;">Probability</div>609 <div style="font-size: 28px; font-weight: 600; color: {'#dc2626' if prob >= 0.5 else '#16a34a'};">{prob:.0%}</div>610 </div>611 </div>612 </div>613 """614 return plot, result615 616 for inp in [sig_phq9, sig_sleep]:617 inp.change(update_sigmoid_view, [sig_phq9, sig_sleep], [sigmoid_plot, sig_result])618 619 app.load(update_sigmoid_view, [sig_phq9, sig_sleep], [sigmoid_plot, sig_result])620 621 gr.HTML("""622 <div class="info-box" style="margin-top: 24px;">623 <p><strong>Key insight:</strong> The sigmoid function creates an S-curve. 624 When z is very negative, probability approaches 0%. When z is very positive, 625 probability approaches 100%. At z=0, probability is exactly 50%.</p>626 </div>627 """)628 629 # TAB 4: TRAINING630 with gr.Tab("Training"):631 gr.HTML("""632 <div style="margin-bottom: 24px;">633 <h2 style="margin-bottom: 8px;">Why Training Matters</h2>634 <p style="color: #86868b;">Compare model performance before and after training.</p>635 </div>636 """)637 638 with gr.Row():639 with gr.Column():640 gr.HTML(comparison_table_html(trained=False))641 with gr.Column():642 gr.HTML(comparison_table_html(trained=True))643 644 gr.HTML("""645 <div class="info-box" style="margin-top: 24px;">646 <p><strong>Training process:</strong> The model starts with random weights and 647 iteratively adjusts them to minimize prediction errors. After training, the 648 weights capture the true relationship between features and the outcome.</p>649 </div>650 """)651 652 # TAB 5: DECISION BOUNDARY653 with gr.Tab("Decision Boundary"):654 gr.HTML("""655 <div style="margin-bottom: 24px;">656 <h2 style="margin-bottom: 8px;">Decision Boundary</h2>657 <p style="color: #86868b;">Visualize how the model separates the two classes.</p>658 </div>659 """)660 661 with gr.Row():662 with gr.Column(scale=1):663 gr.HTML('<p style="font-weight: 500; margin-bottom: 8px;">Adjust weights</p>')664 db_w1 = gr.Slider(-1, 2, value=0.5, step=0.05, label="w₁ (PHQ-9)")665 db_w2 = gr.Slider(-1, 0.5, value=-0.3, step=0.05, label="w₂ (Sleep)")666 db_b = gr.Slider(-10, 5, value=-4.0, step=0.5, label="b (Bias)")667 668 reset_btn = gr.Button("Reset to trained weights", variant="secondary")669 670 with gr.Column(scale=2):671 boundary_plot = gr.Plot()672 673 def update_boundary(w1, w2, b):674 return create_boundary_plot(w1, w2, b)675 676 for inp in [db_w1, db_w2, db_b]:677 inp.change(update_boundary, [db_w1, db_w2, db_b], boundary_plot)678 679 reset_btn.click(lambda: (0.5, -0.3, -4.0), outputs=[db_w1, db_w2, db_b])680 681 app.load(update_boundary, [db_w1, db_w2, db_b], boundary_plot)682 683 gr.HTML("""684 <div class="info-box" style="margin-top: 24px;">685 <p><strong>The boundary:</strong> The dashed line shows where P(Depressed) = 50%. 686 Points in green regions are predicted healthy, points in red regions are predicted 687 depressed. Adjust the weights to see how the boundary shifts.</p>688 </div>689 """)690 691 # Footer692 gr.HTML("""693 <div style="text-align: center; padding: 40px 24px; margin-top: 48px; border-top: 1px solid #e5e5e7;">694 <p style="color: #86868b; font-size: 13px; margin: 0;">695 Interactive tutorial demonstrating logistic regression concepts696 </p>697 </div>698 """)699 700 return app701 702 703# ============================================================================704# MAIN705# ============================================================================706 707if __name__ == "__main__":708 app = create_app()709 app.launch()710 