Skip to content

Commit d85fefa

Browse files
shrutipatel31facebook-github-bot
authored andcommitted
Improve Summary Analysis by Relativize the metric results if there is a status quo to relativize against (#4342)
Summary: Pull Request resolved: #4342 Differential Revision: D82658357
1 parent 02af977 commit d85fefa

File tree

5 files changed

+194
-3
lines changed

5 files changed

+194
-3
lines changed

ax/analysis/summary.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ def compute(
6363
if experiment is None:
6464
raise UserInputError("`Summary` analysis requires an `Experiment` input")
6565

66+
# Determine if we should relativize based on:
67+
# (1) experiment has metrics and (2) experiment has status quo
68+
should_relativize = (
69+
len(experiment.metrics) > 0 and experiment.status_quo is not None
70+
)
71+
6672
return self._create_analysis_card(
6773
title=(
6874
"Summary for "
@@ -73,5 +79,6 @@ def compute(
7379
trial_indices=self.trial_indices,
7480
omit_empty_columns=self.omit_empty_columns,
7581
trial_statuses=self.trial_statuses,
82+
relativize=should_relativize,
7683
),
7784
)

ax/analysis/tests/test_summary.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,3 +275,64 @@ def test_default_excludes_stale_trials(self) -> None:
275275
# Verify that no trials in the output have STALE status
276276
stale_statuses = card.df[card.df["trial_status"] == "STALE"]
277277
self.assertEqual(len(stale_statuses), 0)
278+
279+
def test_metrics_relativized_with_status_quo(self) -> None:
280+
"""Test that Summary relativizes metrics by default when status quo is
281+
present."""
282+
client = Client()
283+
client.configure_experiment(
284+
name="test_experiment_relativize",
285+
parameters=[
286+
RangeParameterConfig(
287+
name="x1",
288+
parameter_type="float",
289+
bounds=(0, 1),
290+
),
291+
],
292+
)
293+
client.configure_optimization(objective="metric1")
294+
295+
# Add status quo
296+
baseline_trial_index = client.attach_baseline({"x1": 0.5})
297+
client.complete_trial(
298+
trial_index=baseline_trial_index, raw_data={"metric1": 90.0}
299+
)
300+
301+
# Get trials and complete with metric data
302+
client.get_next_trials(max_trials=2)
303+
304+
# Complete trials with different metric values
305+
client.complete_trial(
306+
trial_index=baseline_trial_index + 1, raw_data={"metric1": 100.0}
307+
)
308+
client.complete_trial(
309+
trial_index=baseline_trial_index + 2, raw_data={"metric1": 80.0}
310+
)
311+
312+
experiment = client._experiment
313+
314+
# Test that Summary works and produces results
315+
# (relativization happens internally)
316+
analysis = Summary()
317+
318+
card = analysis.compute(experiment=experiment)
319+
320+
# Verify basic structure
321+
self.assertEqual(card.name, "Summary")
322+
self.assertEqual(card.title, "Summary for test_experiment_relativize")
323+
self.assertTrue("metric1" in card.df.columns)
324+
self.assertEqual(len(card.df), 3)
325+
326+
# Verify all trials are present (baseline at index 0,
327+
# regular trials at indices 1 and 2)
328+
trial_indices = set(card.df["trial_index"].values)
329+
330+
self.assertEqual(trial_indices, {0, 1, 2})
331+
332+
# Check that metric values are present (actual relativization values depend on
333+
# the underlying experiment.to_df implementation with relativize=True)
334+
# Some values might be NaN due to relativization, but not all should be NaN
335+
metric_values = card.df["metric1"].values
336+
non_na_count = sum(~pd.isna(metric_values))
337+
# At least some trials should have non-NaN metric values
338+
self.assertGreater(non_na_count, 0, "All metric values are NaN")

ax/core/data.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,8 +473,15 @@ def relativize(
473473
axis=1,
474474
)
475475
)
476+
if not dfs:
477+
raise ValueError(
478+
f"Relativization not possible: status quo arm '{status_quo_name}' "
479+
f"not found or dataset contains no data."
480+
)
476481
df_rel = pd.concat(dfs, axis=0)
477482
if include_sq:
483+
# Set status quo to exactly 0 mean and 0 SEM to avoid negative zero display
484+
df_rel.loc[df_rel["arm_name"] == status_quo_name, "mean"] = 0.0
478485
df_rel.loc[df_rel["arm_name"] == status_quo_name, "sem"] = 0.0
479486
return Data(df_rel)
480487

ax/core/experiment.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1963,6 +1963,7 @@ def to_df(
19631963
trial_indices: Iterable[int] | None = None,
19641964
trial_statuses: Sequence[TrialStatus] | None = None,
19651965
omit_empty_columns: bool = True,
1966+
relativize: bool = False,
19661967
) -> pd.DataFrame:
19671968
"""
19681969
High-level summary of the Experiment with one row per arm. Any values missing at
@@ -1984,10 +1985,23 @@ def to_df(
19841985
trial_indices: If specified, only include these trial indices.
19851986
omit_empty_columns: If True, omit columns where every value is None.
19861987
trial_status: If specified, only include trials with this status.
1988+
relativize: If True and experiment has a status quo, relativize metrics
19871989
"""
19881990

19891991
records = []
1990-
data_df = self.lookup_data(trial_indices=trial_indices).df
1992+
data = self.lookup_data(trial_indices=trial_indices)
1993+
1994+
# Relativize metrics if requested
1995+
data_df = (
1996+
data.relativize(
1997+
status_quo_name=none_throws(self.status_quo).name,
1998+
as_percent=True,
1999+
include_sq=True,
2000+
).df
2001+
if relativize
2002+
else data.df
2003+
)
2004+
19912005
trials = (
19922006
self.get_trials_by_indices(trial_indices=trial_indices)
19932007
if trial_indices
@@ -2047,6 +2061,7 @@ def to_df(
20472061
records.append(record)
20482062

20492063
df = pd.DataFrame(records)
2064+
20502065
if omit_empty_columns:
20512066
df = df.loc[:, df.notnull().any()]
20522067
return df

ax/core/tests/test_experiment.py

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@
7474
)
7575
from ax.utils.testing.mock import mock_botorch_optimize
7676
from pandas.testing import assert_frame_equal
77-
from pyre_extensions import assert_is_instance
77+
from pyre_extensions import assert_is_instance, none_throws
7878

7979
DUMMY_RUN_METADATA_KEY_1 = "test_run_metadata_key_1"
8080
DUMMY_RUN_METADATA_KEY_2 = "test_run_metadata_key_2"
@@ -365,7 +365,7 @@ def test_StatusQuoSetter(self) -> None:
365365
sq_parameters["w"] = 3.5
366366
self.experiment.status_quo = Arm(sq_parameters)
367367
self.assertEqual(self.experiment.status_quo.parameters["w"], 3.5)
368-
self.assertEqual(self.experiment.status_quo.name, "status_quo_e0")
368+
self.assertEqual(none_throws(self.experiment.status_quo).name, "status_quo_e0")
369369

370370
# Verify all None values
371371
self.experiment.status_quo = Arm({n: None for n in sq_parameters.keys()})
@@ -1534,6 +1534,107 @@ def test_to_df(self) -> None:
15341534
)
15351535
self.assertTrue(df_completed.equals(expected_completed_df))
15361536

1537+
def test_to_df_with_relativize(self) -> None:
1538+
"""Test the relativize flag in to_df method with status quo."""
1539+
# Create an experiment with status quo and completed trials
1540+
experiment = get_branin_experiment(with_status_quo=True)
1541+
1542+
# Create two completed trials
1543+
for _ in range(2):
1544+
sobol_run = get_sobol(search_space=experiment.search_space).gen(n=1)
1545+
trial = experiment.new_trial(generator_run=sobol_run)
1546+
trial.mark_running(no_runner_required=True)
1547+
trial.mark_completed()
1548+
1549+
# Fetch and add status quo data
1550+
experiment.fetch_data()
1551+
sq_data = Data(
1552+
df=pd.DataFrame(
1553+
[
1554+
{
1555+
"trial_index": i,
1556+
"arm_name": "status_quo",
1557+
"metric_name": "branin",
1558+
"metric_signature": "branin",
1559+
"mean": 10.0,
1560+
"sem": 0.1,
1561+
}
1562+
for i in range(2)
1563+
]
1564+
)
1565+
)
1566+
experiment.attach_data(sq_data)
1567+
1568+
# Test without relativization
1569+
df_no_rel = experiment.to_df(relativize=False)
1570+
1571+
# Test with relativization
1572+
df_with_rel = experiment.to_df(relativize=True)
1573+
1574+
# Basic structure should be the same
1575+
self.assertEqual(len(df_with_rel), len(df_no_rel))
1576+
self.assertEqual(set(df_with_rel.columns), set(df_no_rel.columns))
1577+
1578+
# Find metric columns and verify relativization occurred
1579+
metric_cols = [
1580+
col
1581+
for col in df_no_rel.columns
1582+
if col
1583+
not in ["trial_index", "arm_name", "trial_status", "name", "x1", "x2"]
1584+
]
1585+
1586+
if metric_cols:
1587+
metric_name = metric_cols[0]
1588+
orig_values = df_no_rel[metric_name].dropna()
1589+
rel_values = df_with_rel[metric_name].dropna()
1590+
1591+
# Values should change for non-status-quo trials
1592+
non_sq_changed = any(
1593+
abs(o - r) > 1e-10 for o, r in zip(orig_values, rel_values) if o != 10.0
1594+
)
1595+
self.assertTrue(non_sq_changed, "Relativization should change some values")
1596+
1597+
def test_to_df_relativize_without_status_quo(self) -> None:
1598+
"""Test that relativize=True has no effect when there's no status quo."""
1599+
# Create experiment without status quo
1600+
experiment = Experiment(
1601+
name="test_no_sq",
1602+
search_space=get_search_space(),
1603+
optimization_config=get_optimization_config(),
1604+
# No status_quo parameter
1605+
)
1606+
1607+
# Add some trials with data
1608+
trial1 = experiment.new_trial()
1609+
trial1.add_arm(Arm({"w": 1.0, "x": 2, "y": "foo", "z": True}))
1610+
trial1.mark_running(no_runner_required=True)
1611+
1612+
# Attach some data
1613+
data = Data(
1614+
df=pd.DataFrame(
1615+
[
1616+
{
1617+
"trial_index": 0,
1618+
"arm_name": trial1.arm.name if trial1.arm else "unknown",
1619+
"metric_name": "m1",
1620+
"metric_signature": "m1",
1621+
"mean": 10.0,
1622+
"sem": 1.0,
1623+
}
1624+
]
1625+
)
1626+
)
1627+
experiment.attach_data(data)
1628+
1629+
# Both relativize=True and relativize=False should return the same result
1630+
df_no_relativize = experiment.to_df(relativize=False)
1631+
# Since there's no status quo, relativize=True should work but have no effect
1632+
# We need to pass relativize_func even though it won't be used
1633+
df_with_relativize = experiment.to_df(relativize=True)
1634+
1635+
# DataFrames should be identical when no status quo exists
1636+
pd.testing.assert_frame_equal(df_no_relativize, df_with_relativize)
1637+
15371638

15381639
class ExperimentWithMapDataTest(TestCase):
15391640
def setUp(self) -> None:

0 commit comments

Comments
 (0)