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) # STEP 1 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)}") # STEP 2 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)}") # STEP 3 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))) # STEP 4 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} ]") # STEP 5 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}%") # STEP 6 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)}") # STEP 7 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)": # Show both sections separated 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("""
Principal Component Analysis · 3D → 2D Dimensionality Reduction