Update my_pages/ica.py
Browse files- my_pages/ica.py +138 -44
my_pages/ica.py
CHANGED
|
@@ -1,62 +1,156 @@
|
|
| 1 |
-
# pages/ica.py
|
| 2 |
-
import streamlit as st
|
| 3 |
-
import matplotlib.pyplot as plt
|
| 4 |
import numpy as np
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
def render():
|
| 7 |
-
st.title("Intentional
|
| 8 |
|
| 9 |
-
#
|
| 10 |
-
|
| 11 |
-
|
|
|
|
|
|
|
| 12 |
"Conventional": (1.0, 0.0),
|
| 13 |
-
"Arbitrary":
|
| 14 |
}
|
| 15 |
|
| 16 |
-
# Explanations
|
| 17 |
-
|
| 18 |
-
"
|
| 19 |
-
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
}
|
| 22 |
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
-
#
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
|
|
|
| 30 |
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
|
| 49 |
-
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
-
|
| 53 |
-
|
| 54 |
|
| 55 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import numpy as np
|
| 2 |
+
import plotly.graph_objects as go
|
| 3 |
+
from streamlit_plotly_events import plotly_events
|
| 4 |
+
import streamlit as st
|
| 5 |
|
| 6 |
def render():
|
| 7 |
+
st.title("ICA Triangle (Intentional · Conventional · Arbitrary)")
|
| 8 |
|
| 9 |
+
# --------------------------
|
| 10 |
+
# Geometry: equilateral triangle in 2D
|
| 11 |
+
# --------------------------
|
| 12 |
+
V = {
|
| 13 |
+
"Intentional": (0.0, 0.0),
|
| 14 |
"Conventional": (1.0, 0.0),
|
| 15 |
+
"Arbitrary": (0.5, np.sqrt(3) / 2),
|
| 16 |
}
|
| 17 |
|
| 18 |
+
# Explanations for each corner
|
| 19 |
+
corner_examples = {
|
| 20 |
+
"Arbitrary": (
|
| 21 |
+
"Arbitrary",
|
| 22 |
+
"Examples: random seeds, shuffling order, tie-breaking by index, default RNG states."
|
| 23 |
+
),
|
| 24 |
+
"Conventional": (
|
| 25 |
+
"Conventional",
|
| 26 |
+
"Examples: using default hyperparams because 'that’s what everyone does', standard scalers, popular benchmarks."
|
| 27 |
+
),
|
| 28 |
+
"Intentional": (
|
| 29 |
+
"Intentional",
|
| 30 |
+
"Examples: constraint-aware objectives, domain rules, fairness constraints, targeted regularization."
|
| 31 |
+
),
|
| 32 |
}
|
| 33 |
|
| 34 |
+
# Keep selected point in session
|
| 35 |
+
if "ica_click" not in st.session_state:
|
| 36 |
+
st.session_state.ica_click = None
|
| 37 |
+
if "ica_nearest" not in st.session_state:
|
| 38 |
+
st.session_state.ica_nearest = None
|
| 39 |
|
| 40 |
+
# --------------------------
|
| 41 |
+
# Build base figure
|
| 42 |
+
# --------------------------
|
| 43 |
+
def base_figure(selected=None):
|
| 44 |
+
fig = go.Figure()
|
| 45 |
|
| 46 |
+
# Triangle outline
|
| 47 |
+
tri_x = [V["Intentional"][0], V["Conventional"][0], V["Arbitrary"][0], V["Intentional"][0]]
|
| 48 |
+
tri_y = [V["Intentional"][1], V["Conventional"][1], V["Arbitrary"][1], V["Intentional"][1]]
|
| 49 |
+
fig.add_trace(go.Scatter(
|
| 50 |
+
x=tri_x, y=tri_y, mode="lines",
|
| 51 |
+
line=dict(color="black", width=2), hoverinfo="skip", showlegend=False
|
| 52 |
+
))
|
| 53 |
|
| 54 |
+
# Vertex markers + labels
|
| 55 |
+
for name, (x, y) in V.items():
|
| 56 |
+
fig.add_trace(go.Scatter(
|
| 57 |
+
x=[x], y=[y], mode="markers+text",
|
| 58 |
+
marker=dict(size=10, color="black"),
|
| 59 |
+
text=[name], textposition="top center",
|
| 60 |
+
hoverinfo="skip", showlegend=False
|
| 61 |
+
))
|
| 62 |
|
| 63 |
+
# A dense transparent clickable grid within triangle
|
| 64 |
+
grid = np.linspace(0.0, 1.0, 41)
|
| 65 |
+
gx, gy = np.meshgrid(grid, grid)
|
| 66 |
+
pts = np.vstack([gx.ravel(), gy.ravel()]).T # (N,2)
|
| 67 |
|
| 68 |
+
def inside_triangle(p):
|
| 69 |
+
# Barycentric method for triangle ABC
|
| 70 |
+
A = np.array(V["Intentional"])
|
| 71 |
+
B = np.array(V["Conventional"])
|
| 72 |
+
C = np.array(V["Arbitrary"])
|
| 73 |
+
v0 = C - A
|
| 74 |
+
v1 = B - A
|
| 75 |
+
v2 = p - A
|
| 76 |
+
den = v0[0]*v1[1] - v1[0]*v0[1]
|
| 77 |
+
if abs(den) < 1e-9:
|
| 78 |
+
return False
|
| 79 |
+
a = (v2[0]*v1[1] - v1[0]*v2[1]) / den
|
| 80 |
+
b = (v0[0]*v2[1] - v2[0]*v0[1]) / den
|
| 81 |
+
c = 1 - a - b
|
| 82 |
+
return (a >= 0) and (b >= 0) and (c >= 0)
|
| 83 |
|
| 84 |
+
in_tri = np.array([inside_triangle(p) for p in pts])
|
| 85 |
+
tri_pts = pts[in_tri]
|
| 86 |
|
| 87 |
+
# Invisible markers to capture click events anywhere inside the triangle
|
| 88 |
+
fig.add_trace(go.Scatter(
|
| 89 |
+
x=tri_pts[:, 0], y=tri_pts[:, 1], mode="markers",
|
| 90 |
+
marker=dict(size=24, opacity=0.0), # invisible, but clickable
|
| 91 |
+
hoverinfo="skip", showlegend=False, name="click-capture"
|
| 92 |
+
))
|
| 93 |
|
| 94 |
+
# Highlight the selected point (if any)
|
| 95 |
+
if selected is not None:
|
| 96 |
+
fig.add_trace(go.Scatter(
|
| 97 |
+
x=[selected[0]], y=[selected[1]], mode="markers",
|
| 98 |
+
marker=dict(size=16, color="#1f77b4", line=dict(width=2, color="black")),
|
| 99 |
+
hoverinfo="skip", showlegend=False, name="selected"
|
| 100 |
+
))
|
| 101 |
+
|
| 102 |
+
fig.update_layout(
|
| 103 |
+
margin=dict(l=10, r=10, t=20, b=10),
|
| 104 |
+
xaxis=dict(visible=False, range=[-0.05, 1.05], scaleanchor="y", scaleratio=1),
|
| 105 |
+
yaxis=dict(visible=False, range=[-0.05, 0.95]),
|
| 106 |
+
clickmode="event+select",
|
| 107 |
+
dragmode=False,
|
| 108 |
+
height=420,
|
| 109 |
)
|
| 110 |
+
return fig
|
| 111 |
+
|
| 112 |
+
fig = base_figure(selected=st.session_state.ica_click)
|
| 113 |
+
|
| 114 |
+
# Render and capture click
|
| 115 |
+
events = plotly_events(
|
| 116 |
+
fig,
|
| 117 |
+
click_event=True,
|
| 118 |
+
hover_event=False,
|
| 119 |
+
select_event=False,
|
| 120 |
+
override_height=420,
|
| 121 |
+
override_width="100%",
|
| 122 |
+
key="ica_triangle"
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
# Process click
|
| 126 |
+
if events:
|
| 127 |
+
# The click snaps to the nearest invisible grid point:
|
| 128 |
+
x = float(events[0]["x"])
|
| 129 |
+
y = float(events[0]["y"])
|
| 130 |
+
st.session_state.ica_click = (x, y)
|
| 131 |
+
|
| 132 |
+
# Determine nearest vertex
|
| 133 |
+
def dist(p, q):
|
| 134 |
+
return np.linalg.norm(np.array(p) - np.array(q))
|
| 135 |
+
nearest = min(V.keys(), key=lambda k: dist((x, y), V[k]))
|
| 136 |
+
st.session_state.ica_nearest = nearest
|
| 137 |
+
|
| 138 |
+
# --------------------------
|
| 139 |
+
# Output text based on nearest vertex
|
| 140 |
+
# --------------------------
|
| 141 |
+
st.markdown("---")
|
| 142 |
+
if st.session_state.ica_nearest:
|
| 143 |
+
title, details = corner_examples[st.session_state.ica_nearest]
|
| 144 |
+
st.subheader(f"Closest to: {title}")
|
| 145 |
+
# If it's very close, emphasize "pure" examples
|
| 146 |
+
# (distance threshold ~ 0.12 in this normalized triangle space)
|
| 147 |
+
d = np.linalg.norm(np.array(st.session_state.ica_click) - np.array(V[st.session_state.ica_nearest]))
|
| 148 |
+
if st.session_state.ica_nearest == "Arbitrary" and d < 0.12:
|
| 149 |
+
st.write("**Example spotlight:** Random seeds (purely arbitrary choice).")
|
| 150 |
+
elif st.session_state.ica_nearest == "Conventional" and d < 0.12:
|
| 151 |
+
st.write("**Example spotlight:** Using library defaults just because it's common.")
|
| 152 |
+
elif st.session_state.ica_nearest == "Intentional" and d < 0.12:
|
| 153 |
+
st.write("**Example spotlight:** Encoding a fairness/robustness constraint explicitly.")
|
| 154 |
+
st.write(details)
|
| 155 |
+
else:
|
| 156 |
+
st.info("Click anywhere inside the triangle to indicate where your decision sits on the ICA spectrum.")
|