josephsoo commited on
Commit
fce839b
·
1 Parent(s): 3a1d4e4

Animate feature attribution sorting

Browse files
Files changed (1) hide show
  1. app.py +107 -51
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.7, 0.3, 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
- start_x[ordered] = np.linspace(0.0, 1.0, len(ordered))
1740
- lane_jitter = ((np.arange(len(ordered)) % 7) - 3) * 0.006
1741
- start_y[ordered] = center + lane_jitter
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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.055,
1865
- y1=center + 0.055,
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
- showticklabels=False,
 
 
 
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=annotations,
1963
- margin=dict(l=58, r=28, t=78, b=100),
 
1964
  uirevision=f"feature-sorter:{dataset}:{model}:{arrangement}",
1965
  legend=dict(
1966
  orientation="h",
1967
- yanchor="bottom",
1968
- y=1.04,
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("Arrange features", htmlFor="feature-arrangement"),
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="Each dot is one input feature. Switch views to follow it from the reference annotation into the signed Kernel SHAP score-versus-rank curve.",
3051
  class_name="axis-feature",
3052
  ),
3053
  panel(
@@ -3379,8 +3435,8 @@ def update_feature_selection_detail(
3379
  arrangement: str,
3380
  ):
3381
  instruction = (
3382
- "Switch views, then hover or click a dot to inspect its feature index, "
3383
- "signed Kernel SHAP value and reference annotation."
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