| import gradio as gr |
| import numpy as np |
| import matplotlib |
| matplotlib.use('Agg') |
| import matplotlib.pyplot as plt |
| from fractions import Fraction |
|
|
| COLORS = ['#00d4ff','#7c3aed','#f59e0b','#10b981','#f43f5e', |
| '#a78bfa','#34d399','#fb923c','#60a5fa','#e879f9'] |
|
|
| def to_frac(val, max_denom=64): |
| f = Fraction(val).limit_denominator(max_denom) |
| return str(f) |
|
|
| def fmt(val, mode): |
| if mode == "Fraction": |
| return f"{to_frac(val):>14}" |
| elif mode == "Both": |
| return f"{val:>10.4f} ({to_frac(val):>10})" |
| else: |
| return f"{val:>10.4f}" |
|
|
| def col_width(mode): |
| return 24 if mode == "Both" else 14 if mode == "Fraction" else 10 |
|
|
| def parse_points(text): |
| points, errors = [], [] |
| for i, line in enumerate([l.strip() for l in text.strip().split('\n') if l.strip()]): |
| try: |
| vals = [float(x) for x in line.replace(',', ' ').split()] |
| if len(vals) != 3: |
| errors.append(f"Line {i+1}: need 3 values, got {len(vals)}") |
| else: |
| points.append(vals) |
| except: |
| errors.append(f"Line {i+1}: invalid numbers") |
| return (np.array(points) if points else None), errors |
|
|
| def build_output(points, mean, centered, cov, eigenvalues, eigenvectors, W, projected, var_exp, mode): |
| sep = "=" * 66 |
| thin = "-" * 66 |
| cw = col_width(mode) |
| L = [] |
|
|
| title_mode = f"({mode})" |
| L.append(sep) |
| L.append(f" PCA | 3D to 2D DIMENSIONALITY REDUCTION {title_mode}") |
| L.append(sep) |
|
|
| |
| L.append(f"\n > STEP 1 | INPUT POINTS") |
| L.append(" " + thin) |
| L.append(f" {'#':<6} {'X':>{cw}} {'Y':>{cw}} {'Z':>{cw}}") |
| L.append(" " + thin) |
| for i, p in enumerate(points): |
| L.append(f" P{i+1:<5} {fmt(p[0],mode)} {fmt(p[1],mode)} {fmt(p[2],mode)}") |
|
|
| |
| L.append(f"\n > STEP 2 | MEAN & CENTERED POINTS") |
| L.append(" " + thin) |
| L.append(f" Mean = ( {fmt(mean[0],mode).strip()}, {fmt(mean[1],mode).strip()}, {fmt(mean[2],mode).strip()} )\n") |
| L.append(f" {'#':<6} {'X-mx':>{cw}} {'Y-my':>{cw}} {'Z-mz':>{cw}}") |
| L.append(" " + thin) |
| for i, p in enumerate(centered): |
| L.append(f" P{i+1:<5} {fmt(p[0],mode)} {fmt(p[1],mode)} {fmt(p[2],mode)}") |
|
|
| |
| L.append(f"\n > STEP 3 | COVARIANCE MATRIX (3x3)") |
| L.append(" " + thin) |
| L.append(f" {'':>6} {'X':>{cw}} {'Y':>{cw}} {'Z':>{cw}}") |
| for i, lbl in enumerate(['X','Y','Z']): |
| L.append(f" {lbl:<6}" + "".join(f"{fmt(cov[i,j],mode)}" for j in range(3))) |
|
|
| |
| L.append(f"\n > STEP 4 | EIGENVALUES & EIGENVECTORS") |
| L.append(" " + thin) |
| for i in range(3): |
| ev = eigenvectors[:, i] |
| lv = fmt(eigenvalues[i], mode).strip() |
| v0 = fmt(ev[0], mode).strip() |
| v1 = fmt(ev[1], mode).strip() |
| v2 = fmt(ev[2], mode).strip() |
| L.append(f" L{i+1} = {lv:<20} | v{i+1} = [ {v0}, {v1}, {v2} ]") |
|
|
| |
| L.append(f"\n > STEP 5 | TOP 2 PRINCIPAL COMPONENTS") |
| L.append(" " + thin) |
| for ci, label in enumerate(['PC1','PC2']): |
| lv = fmt(eigenvalues[ci], mode).strip() |
| w0 = fmt(W[0,ci], mode).strip() |
| w1 = fmt(W[1,ci], mode).strip() |
| w2 = fmt(W[2,ci], mode).strip() |
| L.append(f" {label} L={lv:<20} -> [ {w0}, {w1}, {w2} ]") |
| v1s = fmt(var_exp[0], mode).strip() |
| v2s = fmt(var_exp[1], mode).strip() |
| tot = fmt(var_exp[0]+var_exp[1], mode).strip() |
| L.append(f"\n Variance: PC1={v1s}% PC2={v2s}% Total={tot}%") |
|
|
| |
| L.append(f"\n > STEP 6 | PROJECTION MATRIX W (3x2)") |
| L.append(" " + thin) |
| L.append(f" {'':>4} {'PC1':>{cw}} {'PC2':>{cw}}") |
| for i, lbl in enumerate(['x','y','z']): |
| L.append(f" {lbl:<4} {fmt(W[i,0],mode)} {fmt(W[i,1],mode)}") |
|
|
| |
| L.append(f"\n > STEP 7 | REDUCED 2D COORDINATES") |
| L.append(" " + thin) |
| L.append(f" {'#':<6} {'3D (X, Y, Z)':<36} 2D (PC1, PC2)") |
| L.append(" " + thin) |
| for i in range(len(points)): |
| if mode == "Fraction": |
| orig = f"({to_frac(points[i,0])}, {to_frac(points[i,1])}, {to_frac(points[i,2])})" |
| red = f"({to_frac(projected[i,0])}, {to_frac(projected[i,1])})" |
| elif mode == "Both": |
| orig = f"({points[i,0]:.2f}, {points[i,1]:.2f}, {points[i,2]:.2f})" |
| red = f"({projected[i,0]:.4f} ({to_frac(projected[i,0])}), {projected[i,1]:.4f} ({to_frac(projected[i,1])}))" |
| else: |
| orig = f"({points[i,0]:.2f}, {points[i,1]:.2f}, {points[i,2]:.2f})" |
| red = f"({projected[i,0]:.4f}, {projected[i,1]:.4f})" |
| L.append(f" P{i+1:<5} {orig:<36} {red}") |
|
|
| L.append("\n" + sep) |
| return "\n".join(L) |
|
|
|
|
| def run_pca(points_text, output_mode): |
| points, errors = parse_points(points_text) |
| if errors: |
| return "❌ Input Errors:\n" + "\n".join(errors), None, None, None |
| if points is None or len(points) < 6: |
| n = len(points) if points is not None else 0 |
| return f"❌ Need at least 6 points (you entered {n})", None, None, None |
|
|
| mean = np.mean(points, axis=0) |
| centered = points - mean |
| cov = np.cov(centered.T) |
| eigenvalues, eigenvectors = np.linalg.eigh(cov) |
| idx = np.argsort(eigenvalues)[::-1] |
| eigenvalues = eigenvalues[idx] |
| eigenvectors = eigenvectors[:, idx] |
| W = eigenvectors[:, :2] |
| projected = centered @ W |
| var_exp = eigenvalues / np.sum(eigenvalues) * 100 |
|
|
| if output_mode == "Both (Decimal + Fraction)": |
| |
| dec_block = build_output(points, mean, centered, cov, eigenvalues, eigenvectors, W, projected, var_exp, "Decimal") |
| frac_block = build_output(points, mean, centered, cov, eigenvalues, eigenvectors, W, projected, var_exp, "Fraction") |
| output_text = dec_block + "\n\n\n" + frac_block |
| elif output_mode == "Fraction only": |
| output_text = build_output(points, mean, centered, cov, eigenvalues, eigenvectors, W, projected, var_exp, "Fraction") |
| else: |
| output_text = build_output(points, mean, centered, cov, eigenvalues, eigenvectors, W, projected, var_exp, "Decimal") |
|
|
| fig1 = make_3d_plot(points, mean) |
| fig2 = make_2d_plot(projected, var_exp) |
| fig3 = make_var_plot(var_exp) |
|
|
| return output_text, fig1, fig2, fig3 |
|
|
|
|
| def _style_ax(ax): |
| ax.set_facecolor('#111827') |
| for sp in ax.spines.values(): |
| sp.set_color('#1e293b') |
| ax.tick_params(colors='#94a3b8', labelsize=8) |
|
|
|
|
| def make_3d_plot(points, mean): |
| fig = plt.figure(figsize=(6, 5), facecolor='#0a0e1a') |
| ax = fig.add_subplot(111, projection='3d') |
| ax.set_facecolor('#111827') |
| for pane in [ax.xaxis.pane, ax.yaxis.pane, ax.zaxis.pane]: |
| pane.fill = False |
| pane.set_edgecolor('#1e293b') |
| ax.tick_params(colors='#94a3b8', labelsize=7) |
| ax.xaxis.label.set_color('#94a3b8') |
| ax.yaxis.label.set_color('#94a3b8') |
| ax.zaxis.label.set_color('#94a3b8') |
| ax.set_xlabel('X'); ax.set_ylabel('Y'); ax.set_zlabel('Z') |
| for i, p in enumerate(points): |
| c = COLORS[i % len(COLORS)] |
| ax.scatter(*p, color=c, s=80, edgecolors='white', linewidths=0.5, zorder=5) |
| ax.text(p[0], p[1], p[2], f' P{i+1}', color=c, fontsize=7.5, fontweight='bold') |
| ax.scatter(*mean, color='#f59e0b', s=160, marker='*', edgecolors='white', linewidths=0.8, zorder=10) |
| ax.set_title('Original 3D Points', color='#e2e8f0', fontsize=11, fontweight='bold', pad=10) |
| fig.tight_layout() |
| return fig |
|
|
|
|
| def make_2d_plot(projected, var_exp): |
| fig, ax = plt.subplots(figsize=(6, 5), facecolor='#0a0e1a') |
| _style_ax(ax) |
| ax.axhline(0, color='#1e293b', lw=1) |
| ax.axvline(0, color='#1e293b', lw=1) |
| ax.grid(True, color='#1e293b', lw=0.5, alpha=0.7) |
| ax.set_xlabel(f'PC1 ({var_exp[0]:.1f}%)', color='#00d4ff', fontsize=10) |
| ax.set_ylabel(f'PC2 ({var_exp[1]:.1f}%)', color='#7c3aed', fontsize=10) |
| for i, p in enumerate(projected): |
| c = COLORS[i % len(COLORS)] |
| ax.scatter(*p, color=c, s=100, edgecolors='white', linewidths=0.6, zorder=5) |
| ax.annotate(f'P{i+1}', p, xytext=(7,4), textcoords='offset points', |
| color=c, fontsize=8.5, fontweight='bold') |
| ax.set_title('2D Projection (PCA)', color='#e2e8f0', fontsize=11, fontweight='bold', pad=10) |
| fig.tight_layout() |
| return fig |
|
|
|
|
| def make_var_plot(var_exp): |
| fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(8, 4), facecolor='#0a0e1a') |
| labels = ['PC1','PC2','PC3'] |
| bar_colors = ['#00d4ff','#7c3aed','#f59e0b'] |
| _style_ax(ax1) |
| bars = ax1.bar(labels, var_exp, color=bar_colors, edgecolor='#0a0e1a', linewidth=1.5, width=0.5) |
| for bar, val in zip(bars, var_exp): |
| ax1.text(bar.get_x()+bar.get_width()/2, bar.get_height()+0.8, |
| f'{val:.1f}%', ha='center', color='#e2e8f0', fontsize=9, fontweight='bold') |
| ax1.set_title('Variance per PC', color='#e2e8f0', fontsize=10, fontweight='bold', pad=8) |
| ax1.set_ylabel('Variance (%)', color='#64748b', fontsize=9) |
| ax1.set_ylim(0, max(var_exp)*1.2) |
| _style_ax(ax2) |
| cum = np.cumsum(var_exp) |
| ax2.plot(labels, cum, 'o-', color='#10b981', lw=2.5, markersize=8, |
| markerfacecolor='#0a0e1a', markeredgecolor='#10b981', markeredgewidth=2.5) |
| ax2.fill_between(labels, cum, alpha=0.12, color='#10b981') |
| ax2.axhline(100, color='#f43f5e', lw=1, ls='--', alpha=0.5) |
| for x, y in zip(labels, cum): |
| ax2.text(x, y+2, f'{y:.1f}%', ha='center', color='#10b981', fontsize=9, fontweight='bold') |
| ax2.set_title('Cumulative Variance', color='#e2e8f0', fontsize=10, fontweight='bold', pad=8) |
| ax2.set_ylabel('Cumulative (%)', color='#64748b', fontsize=9) |
| ax2.set_ylim(0, 115) |
| ax2.grid(True, color='#1e293b', lw=0.5) |
| fig.tight_layout(pad=2) |
| return fig |
|
|
|
|
| css = """ |
| * { box-sizing: border-box; } |
| body, .gradio-container { |
| background: #0a0e1a !important; |
| color: #e2e8f0 !important; |
| font-family: ui-monospace, 'Cascadia Code', 'Segoe UI Mono', monospace !important; |
| } |
| .gradio-container { max-width: 1200px !important; margin: 0 auto !important; } |
| .header { |
| background: #111827; |
| border: 1px solid #1e293b; |
| border-top: 3px solid #00d4ff; |
| border-radius: 12px; |
| padding: 28px 36px; |
| margin-bottom: 20px; |
| } |
| .header h1 { font-size: 1.9rem; font-weight: 800; color: #00d4ff; margin: 0 0 6px 0; } |
| .header p { color: #64748b; font-size: 0.82rem; margin: 0; } |
| .hint { |
| background: rgba(0,212,255,0.04); |
| border: 1px solid rgba(0,212,255,0.15); |
| border-radius: 8px; |
| padding: 12px 16px; |
| font-size: 0.76rem; |
| color: #64748b; |
| line-height: 1.8; |
| margin-top: 10px; |
| } |
| .hint strong { color: #00d4ff; } |
| textarea { |
| background: #060a12 !important; |
| border: 1px solid #1e293b !important; |
| color: #94a3b8 !important; |
| font-family: ui-monospace, monospace !important; |
| font-size: 0.8rem !important; |
| border-radius: 8px !important; |
| } |
| textarea:focus { border-color: #00d4ff !important; outline: none !important; } |
| button { font-weight: 700 !important; border-radius: 8px !important; } |
| """ |
|
|
| EXAMPLE = """1 2 3 |
| 4 5 6 |
| 7 8 9 |
| 2 4 1 |
| 5 1 8 |
| 3 6 2 |
| 9 3 7 |
| 1 8 4""" |
|
|
| with gr.Blocks(title="PCA 3D to 2D") as demo: |
| gr.HTML(""" |
| <div class="header"> |
| <h1>▦ PCA Visualizer</h1> |
| <p>Principal Component Analysis · 3D → 2D Dimensionality Reduction</p> |
| </div>""") |
|
|
| with gr.Row(): |
| with gr.Column(scale=1): |
| points_input = gr.Textbox( |
| label="3D Points (one per line: X Y Z)", |
| placeholder="1 2 3\n4 5 6\n...", |
| lines=10, |
| value=EXAMPLE |
| ) |
|
|
| output_mode = gr.Radio( |
| choices=["Decimal only", "Fraction only", "Both (Decimal + Fraction)"], |
| value="Both (Decimal + Fraction)", |
| label="Output Format" |
| ) |
|
|
| gr.HTML("""<div class="hint"> |
| <strong>Decimal:</strong> 0.3333<br> |
| <strong>Fraction:</strong> 1/3<br> |
| <strong>Both:</strong> shows decimal then fraction section |
| </div>""") |
|
|
| with gr.Row(): |
| run_btn = gr.Button("Run PCA", variant="primary") |
| clear_btn = gr.Button("Clear", variant="secondary") |
|
|
| with gr.Column(scale=2): |
| with gr.Tabs(): |
| with gr.Tab("Steps & Results"): |
| steps_out = gr.Textbox( |
| label="Full PCA Computation", |
| interactive=False, |
| lines=36 |
| ) |
| with gr.Tab("3D Input Plot"): |
| plot_3d = gr.Plot(label="Given 3D Points") |
| with gr.Tab("2D Projection"): |
| plot_2d = gr.Plot(label="PCA 2D Projection") |
| with gr.Tab("Variance Analysis"): |
| plot_var = gr.Plot(label="Explained Variance") |
|
|
| run_btn.click( |
| fn=run_pca, |
| inputs=[points_input, output_mode], |
| outputs=[steps_out, plot_3d, plot_2d, plot_var] |
| ) |
| clear_btn.click( |
| fn=lambda: ("", None, None, None), |
| outputs=[points_input, plot_3d, plot_2d, plot_var] |
| ) |
|
|
| demo.launch(css=css, ssr_mode=False) |