CoolFace
Apppublic

Aryan0777/logistic_regression

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
app.py710 linesDownload Raw Back to root
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