Skip to content

Commit dfa8c0b

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 3b21a2d commit dfa8c0b

File tree

4 files changed

+177
-3
lines changed

4 files changed

+177
-3
lines changed

ax/analysis/summary.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,5 +73,6 @@ def compute(
7373
trial_indices=self.trial_indices,
7474
omit_empty_columns=self.omit_empty_columns,
7575
trial_statuses=self.trial_statuses,
76+
relativize=True,
7677
),
7778
)

ax/analysis/tests/test_summary.py

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

ax/core/experiment.py

Lines changed: 15 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,22 @@ 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+
data_df = data.df
1994+
1995+
# Relativize metrics if requested and experiment has status quo
1996+
if relativize and self.status_quo is not None:
1997+
relativized_data = data.relativize(
1998+
status_quo_name=none_throws(self.status_quo).name,
1999+
as_percent=True,
2000+
include_sq=True,
2001+
)
2002+
data_df = relativized_data.df
2003+
19912004
trials = (
19922005
self.get_trials_by_indices(trial_indices=trial_indices)
19932006
if trial_indices
@@ -2047,6 +2060,7 @@ def to_df(
20472060
records.append(record)
20482061

20492062
df = pd.DataFrame(records)
2063+
20502064
if omit_empty_columns:
20512065
df = df.loc[:, df.notnull().any()]
20522066
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)