Update my_pages/ica.py
Browse files- my_pages/ica.py +63 -151
my_pages/ica.py
CHANGED
|
@@ -1,156 +1,68 @@
|
|
| 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
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
#
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 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 |
-
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 |
-
#
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
#
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 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.")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import streamlit as st
|
| 2 |
+
import matplotlib.pyplot as plt
|
| 3 |
+
import numpy as np
|
| 4 |
|
| 5 |
def render():
|
| 6 |
+
st.title("ICA Triangle")
|
| 7 |
+
|
| 8 |
+
st.write("Adjust the sliders to change the position of the decision in the triangle.")
|
| 9 |
+
|
| 10 |
+
# Initialize values in session_state so they persist
|
| 11 |
+
if "intentional" not in st.session_state:
|
| 12 |
+
st.session_state.intentional = 0.33
|
| 13 |
+
st.session_state.conventional = 0.33
|
| 14 |
+
st.session_state.arbitrary = 0.34
|
| 15 |
+
|
| 16 |
+
# Sliders (manually enforce sum to 1)
|
| 17 |
+
intentional = st.slider("Intentional", 0.0, 1.0, st.session_state.intentional, 0.01)
|
| 18 |
+
remaining_for_two = 1.0 - intentional
|
| 19 |
+
conventional = st.slider("Conventional", 0.0, remaining_for_two, st.session_state.conventional, 0.01)
|
| 20 |
+
arbitrary = 1.0 - intentional - conventional
|
| 21 |
+
|
| 22 |
+
# Save back to session_state
|
| 23 |
+
st.session_state.intentional = intentional
|
| 24 |
+
st.session_state.conventional = conventional
|
| 25 |
+
st.session_state.arbitrary = arbitrary
|
| 26 |
+
|
| 27 |
+
st.write(f"**Arbitrary:** {arbitrary:.2f}")
|
| 28 |
+
|
| 29 |
+
# Triangle vertices (equilateral)
|
| 30 |
+
vertices = np.array([
|
| 31 |
+
[0.5, np.sqrt(3)/2], # Intentional
|
| 32 |
+
[0, 0], # Conventional
|
| 33 |
+
[1, 0] # Arbitrary
|
| 34 |
+
])
|
| 35 |
+
|
| 36 |
+
# Compute point location from barycentric coordinates
|
| 37 |
+
point = (
|
| 38 |
+
intentional * vertices[0] +
|
| 39 |
+
conventional * vertices[1] +
|
| 40 |
+
arbitrary * vertices[2]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
)
|
| 42 |
|
| 43 |
+
# Plot
|
| 44 |
+
fig, ax = plt.subplots()
|
| 45 |
+
ax.plot(*np.append(vertices, [vertices[0]], axis=0).T, 'k-') # Triangle outline
|
| 46 |
+
ax.scatter(vertices[:,0], vertices[:,1], c=["blue", "green", "red"], s=100)
|
| 47 |
+
ax.text(*vertices[0], "Intentional", ha="center", va="bottom")
|
| 48 |
+
ax.text(*vertices[1], "Conventional", ha="right", va="top")
|
| 49 |
+
ax.text(*vertices[2], "Arbitrary", ha="left", va="top")
|
| 50 |
+
|
| 51 |
+
ax.scatter(point[0], point[1], c="orange", s=200, zorder=5)
|
| 52 |
+
ax.set_aspect("equal")
|
| 53 |
+
ax.axis("off")
|
| 54 |
+
|
| 55 |
+
st.pyplot(fig)
|
| 56 |
+
|
| 57 |
+
# Show explanation text based on closeness
|
| 58 |
+
closest = max(
|
| 59 |
+
[("Intentional", intentional), ("Conventional", conventional), ("Arbitrary", arbitrary)],
|
| 60 |
+
key=lambda x: x[1]
|
| 61 |
+
)[0]
|
| 62 |
+
|
| 63 |
+
if closest == "Arbitrary":
|
| 64 |
+
st.info("Example: Random seeds — a truly arbitrary decision.")
|
| 65 |
+
elif closest == "Intentional":
|
| 66 |
+
st.info("Example: Ethical constraints — a fully intentional choice.")
|
| 67 |
+
elif closest == "Conventional":
|
| 68 |
+
st.info("Example: Industry standard preprocessing — a conventional decision.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|