Animate feature attribution sorting
Browse files
app.py
CHANGED
|
@@ -1701,6 +1701,7 @@ def feature_sorter_figure(
|
|
| 1701 |
n_features = len(frame)
|
| 1702 |
values = frame["signed_attribution"].to_numpy(dtype=float)
|
| 1703 |
ranks = frame["attribution_rank"].to_numpy(dtype=float)
|
|
|
|
| 1704 |
y_low = min(float(np.nanmin(values)), 0.0)
|
| 1705 |
y_high = max(float(np.nanmax(values)), 0.0)
|
| 1706 |
y_span = max(y_high - y_low, 1e-6)
|
|
@@ -1715,7 +1716,6 @@ def feature_sorter_figure(
|
|
| 1715 |
vmax = float(np.nanmax(validation))
|
| 1716 |
scale = max(vmax - vmin, 1e-9)
|
| 1717 |
start_x = (validation - vmin) / scale
|
| 1718 |
-
feature_ids = frame["feature_index"].to_numpy(dtype=int)
|
| 1719 |
start_y = 0.5 + (
|
| 1720 |
((feature_ids * 37) % 101) / 100.0 - 0.5
|
| 1721 |
) * 0.42
|
|
@@ -1729,16 +1729,31 @@ def feature_sorter_figure(
|
|
| 1729 |
np.flatnonzero(frame["feature_group"].astype(str).eq(group))
|
| 1730 |
for group in trace_groups
|
| 1731 |
]
|
| 1732 |
-
centers = np.linspace(0.
|
| 1733 |
start_x = np.zeros(n_features, dtype=float)
|
| 1734 |
start_y = np.zeros(n_features, dtype=float)
|
| 1735 |
for center, indices in zip(centers, trace_indices):
|
| 1736 |
ordered = indices[
|
| 1737 |
np.argsort(frame.loc[indices, "feature_index"].to_numpy(), kind="stable")
|
| 1738 |
]
|
| 1739 |
-
|
| 1740 |
-
|
| 1741 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1742 |
|
| 1743 |
marker_size = 6 if n_features > 300 else (8 if n_features > 120 else 10)
|
| 1744 |
|
|
@@ -1788,7 +1803,7 @@ def feature_sorter_figure(
|
|
| 1788 |
ids=[f"{dataset}:{int(value)}" for value in subset["feature_index"]],
|
| 1789 |
mode="markers",
|
| 1790 |
name=group,
|
| 1791 |
-
showlegend=dataset != "allen_neuropixels",
|
| 1792 |
marker=marker,
|
| 1793 |
customdata=customdata,
|
| 1794 |
hovertemplate=(
|
|
@@ -1810,7 +1825,6 @@ def feature_sorter_figure(
|
|
| 1810 |
]
|
| 1811 |
)
|
| 1812 |
shapes: list[dict] = []
|
| 1813 |
-
annotations: list[dict] = []
|
| 1814 |
if ranked:
|
| 1815 |
for rank_bin in ATTRIBUTION_BIN_ORDER:
|
| 1816 |
bin_ranks = frame.loc[
|
|
@@ -1832,17 +1846,6 @@ def feature_sorter_figure(
|
|
| 1832 |
layer="below",
|
| 1833 |
)
|
| 1834 |
)
|
| 1835 |
-
annotations.append(
|
| 1836 |
-
dict(
|
| 1837 |
-
x=float(bin_x.mean()),
|
| 1838 |
-
xref="x",
|
| 1839 |
-
y=1.035,
|
| 1840 |
-
yref="paper",
|
| 1841 |
-
text=f"{rank_bin} third",
|
| 1842 |
-
showarrow=False,
|
| 1843 |
-
font=dict(size=10, color="#334957"),
|
| 1844 |
-
)
|
| 1845 |
-
)
|
| 1846 |
shapes.append(
|
| 1847 |
dict(
|
| 1848 |
type="line",
|
|
@@ -1861,35 +1864,16 @@ def feature_sorter_figure(
|
|
| 1861 |
type="rect",
|
| 1862 |
x0=display_range[0],
|
| 1863 |
x1=display_range[1],
|
| 1864 |
-
y0=center - 0.
|
| 1865 |
-
y1=center + 0.
|
| 1866 |
fillcolor=group_colors[group],
|
| 1867 |
opacity=0.08,
|
| 1868 |
line_width=0,
|
| 1869 |
layer="below",
|
| 1870 |
)
|
| 1871 |
)
|
| 1872 |
-
annotations.append(
|
| 1873 |
-
dict(
|
| 1874 |
-
x=0.012,
|
| 1875 |
-
xref="paper",
|
| 1876 |
-
y=center,
|
| 1877 |
-
yref="y",
|
| 1878 |
-
text=group,
|
| 1879 |
-
showarrow=False,
|
| 1880 |
-
xanchor="left",
|
| 1881 |
-
bgcolor="rgba(255,255,255,0.85)",
|
| 1882 |
-
font=dict(size=11, color="#334957"),
|
| 1883 |
-
)
|
| 1884 |
-
)
|
| 1885 |
|
| 1886 |
-
title =
|
| 1887 |
-
"Signed Kernel SHAP vs. rank"
|
| 1888 |
-
if ranked
|
| 1889 |
-
else "Continuous gOSI"
|
| 1890 |
-
if dataset == "allen_neuropixels"
|
| 1891 |
-
else "Features by reference group"
|
| 1892 |
-
)
|
| 1893 |
figure_layout(figure, height=560)
|
| 1894 |
if ranked:
|
| 1895 |
rank_ticks = np.unique(
|
|
@@ -1941,31 +1925,103 @@ def feature_sorter_figure(
|
|
| 1941 |
range=display_range,
|
| 1942 |
showticklabels=False,
|
| 1943 |
ticks="",
|
| 1944 |
-
title="
|
| 1945 |
showgrid=False,
|
| 1946 |
zeroline=False,
|
| 1947 |
)
|
| 1948 |
figure.update_yaxes(
|
| 1949 |
visible=True,
|
| 1950 |
range=display_range,
|
| 1951 |
-
|
|
|
|
|
|
|
|
|
|
| 1952 |
ticks="",
|
| 1953 |
-
title="
|
| 1954 |
showgrid=False,
|
| 1955 |
zeroline=False,
|
| 1956 |
)
|
| 1957 |
figure.update_xaxes(automargin=False)
|
| 1958 |
figure.update_yaxes(automargin=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1959 |
figure.update_layout(
|
| 1960 |
title=title,
|
| 1961 |
shapes=shapes,
|
| 1962 |
-
annotations=
|
| 1963 |
-
|
|
|
|
| 1964 |
uirevision=f"feature-sorter:{dataset}:{model}:{arrangement}",
|
| 1965 |
legend=dict(
|
| 1966 |
orientation="h",
|
| 1967 |
-
yanchor="
|
| 1968 |
-
y=
|
| 1969 |
xanchor="left",
|
| 1970 |
x=0,
|
| 1971 |
font=dict(size=10),
|
|
@@ -3003,7 +3059,7 @@ app.layout = html.Div(
|
|
| 3003 |
),
|
| 3004 |
html.Div(
|
| 3005 |
[
|
| 3006 |
-
html.Label("
|
| 3007 |
dcc.RadioItems(
|
| 3008 |
id="feature-arrangement",
|
| 3009 |
options=[
|
|
@@ -3047,7 +3103,7 @@ app.layout = html.Div(
|
|
| 3047 |
],
|
| 3048 |
className="download-grid panel-downloads",
|
| 3049 |
),
|
| 3050 |
-
subtitle="
|
| 3051 |
class_name="axis-feature",
|
| 3052 |
),
|
| 3053 |
panel(
|
|
@@ -3379,8 +3435,8 @@ def update_feature_selection_detail(
|
|
| 3379 |
arrangement: str,
|
| 3380 |
):
|
| 3381 |
instruction = (
|
| 3382 |
-
"
|
| 3383 |
-
"
|
| 3384 |
)
|
| 3385 |
if not click_data or not click_data.get("points"):
|
| 3386 |
return instruction
|
|
|
|
| 1701 |
n_features = len(frame)
|
| 1702 |
values = frame["signed_attribution"].to_numpy(dtype=float)
|
| 1703 |
ranks = frame["attribution_rank"].to_numpy(dtype=float)
|
| 1704 |
+
feature_ids = frame["feature_index"].to_numpy(dtype=int)
|
| 1705 |
y_low = min(float(np.nanmin(values)), 0.0)
|
| 1706 |
y_high = max(float(np.nanmax(values)), 0.0)
|
| 1707 |
y_span = max(y_high - y_low, 1e-6)
|
|
|
|
| 1716 |
vmax = float(np.nanmax(validation))
|
| 1717 |
scale = max(vmax - vmin, 1e-9)
|
| 1718 |
start_x = (validation - vmin) / scale
|
|
|
|
| 1719 |
start_y = 0.5 + (
|
| 1720 |
((feature_ids * 37) % 101) / 100.0 - 0.5
|
| 1721 |
) * 0.42
|
|
|
|
| 1729 |
np.flatnonzero(frame["feature_group"].astype(str).eq(group))
|
| 1730 |
for group in trace_groups
|
| 1731 |
]
|
| 1732 |
+
centers = np.linspace(0.72, 0.28, len(trace_groups))
|
| 1733 |
start_x = np.zeros(n_features, dtype=float)
|
| 1734 |
start_y = np.zeros(n_features, dtype=float)
|
| 1735 |
for center, indices in zip(centers, trace_indices):
|
| 1736 |
ordered = indices[
|
| 1737 |
np.argsort(frame.loc[indices, "feature_index"].to_numpy(), kind="stable")
|
| 1738 |
]
|
| 1739 |
+
n_columns = max(1, int(np.ceil(np.sqrt(len(ordered) * 4.0))))
|
| 1740 |
+
n_rows = max(1, int(np.ceil(len(ordered) / n_columns)))
|
| 1741 |
+
row_y = (
|
| 1742 |
+
np.asarray([center])
|
| 1743 |
+
if n_rows == 1
|
| 1744 |
+
else center + np.linspace(-0.075, 0.075, n_rows)
|
| 1745 |
+
)
|
| 1746 |
+
for row_index in range(n_rows):
|
| 1747 |
+
row = ordered[row_index * n_columns : (row_index + 1) * n_columns]
|
| 1748 |
+
if not len(row):
|
| 1749 |
+
continue
|
| 1750 |
+
if n_columns == 1:
|
| 1751 |
+
row_x = np.asarray([0.5])
|
| 1752 |
+
else:
|
| 1753 |
+
offset = (n_columns - len(row)) / 2.0
|
| 1754 |
+
row_x = (np.arange(len(row)) + offset) / (n_columns - 1)
|
| 1755 |
+
start_x[row] = 0.04 + 0.92 * row_x
|
| 1756 |
+
start_y[row] = row_y[row_index]
|
| 1757 |
|
| 1758 |
marker_size = 6 if n_features > 300 else (8 if n_features > 120 else 10)
|
| 1759 |
|
|
|
|
| 1803 |
ids=[f"{dataset}:{int(value)}" for value in subset["feature_index"]],
|
| 1804 |
mode="markers",
|
| 1805 |
name=group,
|
| 1806 |
+
showlegend=ranked and dataset != "allen_neuropixels",
|
| 1807 |
marker=marker,
|
| 1808 |
customdata=customdata,
|
| 1809 |
hovertemplate=(
|
|
|
|
| 1825 |
]
|
| 1826 |
)
|
| 1827 |
shapes: list[dict] = []
|
|
|
|
| 1828 |
if ranked:
|
| 1829 |
for rank_bin in ATTRIBUTION_BIN_ORDER:
|
| 1830 |
bin_ranks = frame.loc[
|
|
|
|
| 1846 |
layer="below",
|
| 1847 |
)
|
| 1848 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1849 |
shapes.append(
|
| 1850 |
dict(
|
| 1851 |
type="line",
|
|
|
|
| 1864 |
type="rect",
|
| 1865 |
x0=display_range[0],
|
| 1866 |
x1=display_range[1],
|
| 1867 |
+
y0=center - 0.095,
|
| 1868 |
+
y1=center + 0.095,
|
| 1869 |
fillcolor=group_colors[group],
|
| 1870 |
opacity=0.08,
|
| 1871 |
line_width=0,
|
| 1872 |
layer="below",
|
| 1873 |
)
|
| 1874 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1875 |
|
| 1876 |
+
title = "Signed Kernel SHAP vs. rank" if ranked else None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1877 |
figure_layout(figure, height=560)
|
| 1878 |
if ranked:
|
| 1879 |
rank_ticks = np.unique(
|
|
|
|
| 1925 |
range=display_range,
|
| 1926 |
showticklabels=False,
|
| 1927 |
ticks="",
|
| 1928 |
+
title="",
|
| 1929 |
showgrid=False,
|
| 1930 |
zeroline=False,
|
| 1931 |
)
|
| 1932 |
figure.update_yaxes(
|
| 1933 |
visible=True,
|
| 1934 |
range=display_range,
|
| 1935 |
+
tickmode="array",
|
| 1936 |
+
tickvals=centers,
|
| 1937 |
+
ticktext=trace_groups,
|
| 1938 |
+
tickfont=dict(size=10),
|
| 1939 |
ticks="",
|
| 1940 |
+
title="",
|
| 1941 |
showgrid=False,
|
| 1942 |
zeroline=False,
|
| 1943 |
)
|
| 1944 |
figure.update_xaxes(automargin=False)
|
| 1945 |
figure.update_yaxes(automargin=False)
|
| 1946 |
+
animation_controls: list[dict] = []
|
| 1947 |
+
if ranked:
|
| 1948 |
+
animated_traces = list(range(len(trace_indices)))
|
| 1949 |
+
|
| 1950 |
+
def coordinate_frame(
|
| 1951 |
+
name: str,
|
| 1952 |
+
x_values: np.ndarray,
|
| 1953 |
+
y_values: np.ndarray,
|
| 1954 |
+
) -> go.Frame:
|
| 1955 |
+
return go.Frame(
|
| 1956 |
+
name=name,
|
| 1957 |
+
traces=animated_traces,
|
| 1958 |
+
data=[
|
| 1959 |
+
go.Scatter(x=x_values[indices], y=y_values[indices])
|
| 1960 |
+
for indices in trace_indices
|
| 1961 |
+
],
|
| 1962 |
+
)
|
| 1963 |
+
|
| 1964 |
+
figure.frames = [
|
| 1965 |
+
coordinate_frame("feature-reference", start_x, start_y),
|
| 1966 |
+
coordinate_frame("feature-ranked", final_x, final_y),
|
| 1967 |
+
]
|
| 1968 |
+
animation_controls = [
|
| 1969 |
+
dict(
|
| 1970 |
+
type="buttons",
|
| 1971 |
+
direction="left",
|
| 1972 |
+
showactive=False,
|
| 1973 |
+
x=1.0,
|
| 1974 |
+
xanchor="right",
|
| 1975 |
+
y=-0.38,
|
| 1976 |
+
yanchor="top",
|
| 1977 |
+
pad=dict(t=4, r=0),
|
| 1978 |
+
bgcolor="#F3EFF8",
|
| 1979 |
+
bordercolor="#C8BBDD",
|
| 1980 |
+
borderwidth=1,
|
| 1981 |
+
font=dict(size=11, color="#4D3880"),
|
| 1982 |
+
buttons=[
|
| 1983 |
+
dict(
|
| 1984 |
+
label="↻ Replay sorting",
|
| 1985 |
+
method="animate",
|
| 1986 |
+
args=[
|
| 1987 |
+
["feature-reference", "feature-ranked"],
|
| 1988 |
+
{
|
| 1989 |
+
"mode": "immediate",
|
| 1990 |
+
"fromcurrent": False,
|
| 1991 |
+
"frame": [
|
| 1992 |
+
{"duration": 120, "redraw": False},
|
| 1993 |
+
{"duration": 900, "redraw": False},
|
| 1994 |
+
],
|
| 1995 |
+
"transition": [
|
| 1996 |
+
{"duration": 0},
|
| 1997 |
+
{
|
| 1998 |
+
"duration": 900,
|
| 1999 |
+
"easing": "cubic-in-out",
|
| 2000 |
+
},
|
| 2001 |
+
],
|
| 2002 |
+
},
|
| 2003 |
+
],
|
| 2004 |
+
)
|
| 2005 |
+
],
|
| 2006 |
+
)
|
| 2007 |
+
]
|
| 2008 |
+
if ranked:
|
| 2009 |
+
margin = dict(l=58, r=28, t=66, b=165)
|
| 2010 |
+
elif dataset == "allen_neuropixels":
|
| 2011 |
+
margin = dict(l=46, r=54, t=26, b=76)
|
| 2012 |
+
else:
|
| 2013 |
+
margin = dict(l=112, r=24, t=26, b=76)
|
| 2014 |
figure.update_layout(
|
| 2015 |
title=title,
|
| 2016 |
shapes=shapes,
|
| 2017 |
+
annotations=[],
|
| 2018 |
+
updatemenus=animation_controls,
|
| 2019 |
+
margin=margin,
|
| 2020 |
uirevision=f"feature-sorter:{dataset}:{model}:{arrangement}",
|
| 2021 |
legend=dict(
|
| 2022 |
orientation="h",
|
| 2023 |
+
yanchor="top",
|
| 2024 |
+
y=-0.17,
|
| 2025 |
xanchor="left",
|
| 2026 |
x=0,
|
| 2027 |
font=dict(size=10),
|
|
|
|
| 3059 |
),
|
| 3060 |
html.Div(
|
| 3061 |
[
|
| 3062 |
+
html.Label("Feature view", htmlFor="feature-arrangement"),
|
| 3063 |
dcc.RadioItems(
|
| 3064 |
id="feature-arrangement",
|
| 3065 |
options=[
|
|
|
|
| 3103 |
],
|
| 3104 |
className="download-grid panel-downloads",
|
| 3105 |
),
|
| 3106 |
+
subtitle="Explore each input feature in its reference context, then replay how the features sort into the signed Kernel SHAP ranking.",
|
| 3107 |
class_name="axis-feature",
|
| 3108 |
),
|
| 3109 |
panel(
|
|
|
|
| 3435 |
arrangement: str,
|
| 3436 |
):
|
| 3437 |
instruction = (
|
| 3438 |
+
"Each dot is one input feature. In the ranked view, use ↻ Replay sorting "
|
| 3439 |
+
"to follow every feature into its signed Kernel SHAP rank."
|
| 3440 |
)
|
| 3441 |
if not click_data or not click_data.get("points"):
|
| 3442 |
return instruction
|