Skip to content
Draft
2 changes: 1 addition & 1 deletion examples/getting_started/plot_skore_getting_started.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@
# for example getting the report metrics for the first split only:

# %%
cv_report.estimator_reports_[0].metrics.summarize().frame()
cv_report.reports_[0].metrics.summarize().frame()

# %%
# .. seealso::
Expand Down
2 changes: 1 addition & 1 deletion examples/use_cases/plot_employee_salaries.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@
# :class:`skore.EstimatorReport` for each split.

# %%
hgbt_split_1 = hgbt_model_report.estimator_reports_[0]
hgbt_split_1 = hgbt_model_report.reports_[0]
hgbt_split_1.metrics.summarize(indicator_favorability=True).frame()

# %%
Expand Down
4 changes: 2 additions & 2 deletions skore/src/skore/_sklearn/_comparison/metrics_accessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -1272,7 +1272,7 @@ def _get_display(

else:
for report_name, report in self._parent.reports_.items():
for split, estimator_report in enumerate(report.estimator_reports_):
for split, estimator_report in enumerate(report.reports_):
report_X, report_y, _ = (
estimator_report.metrics._get_X_y_and_data_source_hash(
data_source=data_source,
Expand Down Expand Up @@ -1320,7 +1320,7 @@ def _get_display(
estimators=[
estimator_report.estimator_
for report in self._parent.reports_.values()
for estimator_report in report.estimator_reports_
for estimator_report in report.reports_
],
ml_task=self._parent._ml_task,
data_source=data_source,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def coefficients(self) -> FeatureImportanceDisplay:
for split, df in enumerate(
list(
report.feature_importance.coefficients().frame()
for report in self._parent.estimator_reports_
for report in self._parent.reports_
)
)
},
Expand Down
14 changes: 7 additions & 7 deletions skore/src/skore/_sklearn/_cross_validation/metrics_accessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ def _compute_metric_scores(
progress = self._parent._progress_info["current_progress"]
main_task = self._parent._progress_info["current_task"]

total_estimators = len(self._parent.estimator_reports_)
total_estimators = len(self._parent.reports_)
progress.update(main_task, total=total_estimators)

if cache_key in self._parent._cache:
Expand All @@ -233,7 +233,7 @@ def _compute_metric_scores(
delayed(getattr(report.metrics, report_metric_name))(
data_source=data_source, X=X, y=y, **metric_kwargs
)
for report in self._parent.estimator_reports_
for report in self._parent.reports_
)
results = []
for result in generator:
Expand Down Expand Up @@ -315,10 +315,10 @@ def timings(
timings: pd.DataFrame = pd.concat(
[
pd.Series(report.metrics.timings())
for report in self._parent.estimator_reports_
for report in self._parent.reports_
],
axis=1,
keys=[f"Split #{i}" for i in range(len(self._parent.estimator_reports_))],
keys=[f"Split #{i}" for i in range(len(self._parent.reports_))],
)
if aggregate:
if isinstance(aggregate, str):
Expand Down Expand Up @@ -1128,15 +1128,15 @@ def _get_display(
assert self._parent._progress_info is not None, "Progress info not set"
progress = self._parent._progress_info["current_progress"]
main_task = self._parent._progress_info["current_task"]
total_estimators = len(self._parent.estimator_reports_)
total_estimators = len(self._parent.reports_)
progress.update(main_task, total=total_estimators)

if cache_key in self._parent._cache:
display = self._parent._cache[cache_key]
else:
y_true: list[YPlotData] = []
y_pred: list[YPlotData] = []
for report_idx, report in enumerate(self._parent.estimator_reports_):
for report_idx, report in enumerate(self._parent.reports_):
if data_source != "X_y":
# only retrieve data stored in the reports when we don't want to
# use an external common X and y
Expand Down Expand Up @@ -1178,7 +1178,7 @@ def _get_display(
y_pred=y_pred,
report_type="cross-validation",
estimators=[
report.estimator_ for report in self._parent.estimator_reports_
report.estimator_ for report in self._parent.reports_
],
ml_task=self._parent._ml_task,
data_source=data_source,
Expand Down
27 changes: 18 additions & 9 deletions skore/src/skore/_sklearn/_cross_validation/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ class CrossValidationReport(_BaseReport, DirNamesMixin):
estimator_name_ : str
The name of the estimator.

estimator_reports_ : list of EstimatorReport
reports_ : list of EstimatorReport
The estimator reports for each split.

See Also
Expand Down Expand Up @@ -170,16 +170,14 @@ def __init__(
self._split_indices = tuple(self._splitter.split(self._X, self._y))
self.n_jobs = n_jobs

self.estimator_reports_: list[EstimatorReport] = self._fit_estimator_reports()
self.reports_: list[EstimatorReport] = self._fit_estimator_reports()

self._rng = np.random.default_rng(time.time_ns())
self._hash = self._rng.integers(
low=np.iinfo(np.int64).min, high=np.iinfo(np.int64).max
)
self._cache: dict[tuple[Any, ...], Any] = {}
self._ml_task = _find_ml_task(
y, estimator=self.estimator_reports_[0]._estimator
)
self._ml_task = _find_ml_task(y, estimator=self.reports_[0]._estimator)

@progress_decorator(
description=lambda self: (
Expand Down Expand Up @@ -293,7 +291,7 @@ def clear_cache(self) -> None:
>>> report._cache
{}
"""
for report in self.estimator_reports_:
for report in self.reports_:
report.clear_cache()
self._cache = {}

Expand Down Expand Up @@ -336,10 +334,10 @@ def cache_predictions(
progress = self._progress_info["current_progress"]
main_task = self._progress_info["current_task"]

total_estimators = len(self.estimator_reports_)
total_estimators = len(self.reports_)
progress.update(main_task, total=total_estimators)

for split_idx, estimator_report in enumerate(self.estimator_reports_, 1):
for split_idx, estimator_report in enumerate(self.reports_, 1):
# Share the parent's progress bar with child report
estimator_report._progress_info = {
"current_progress": progress,
Expand Down Expand Up @@ -439,7 +437,7 @@ def get_predictions(
X=X,
pos_label=pos_label,
)
for report in self.estimator_reports_
for report in self.reports_
]

@property
Expand Down Expand Up @@ -489,6 +487,17 @@ def pos_label(self, value: PositiveLabel | None) -> None:
f"Call the constructor of {self.__class__.__name__} to create a new report."
)

@property
def estimator_reports_(self) -> list[EstimatorReport]:
"""
The estimator reports for each split.

.. deprecated
The ``report.estimator_reports_`` property will be removed in favor of
``report.reports_`` in a near future.
"""
return self.reports_

####################################################################################
# Methods related to the help and repr
####################################################################################
Expand Down
6 changes: 3 additions & 3 deletions skore/src/skore/_utils/_accessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ def _check_estimator_report_has_method(
method_name: str,
) -> Callable:
def check(accessor: Any) -> bool:
estimator_report = accessor._parent.estimator_reports_[0]
estimator_report = accessor._parent.reports_[0]

if not hasattr(estimator_report, accessor_name):
raise AttributeError(
Expand All @@ -163,7 +163,7 @@ def check(accessor: Any) -> bool:
def _check_cross_validation_sub_estimator_has_coef() -> Callable:
def check(accessor: Any) -> bool:
"""Check if the underlying estimator has a `coef_` attribute."""
return _check_has_coef(accessor._parent.estimator_reports_[0].estimator)
return _check_has_coef(accessor._parent.reports_[0].estimator)

return check

Expand All @@ -183,7 +183,7 @@ def check(accessor: Any) -> bool:
for parent_report in parent.reports_.values():
if parent._reports_type == "CrossValidationReport":
parent_report = cast(CrossValidationReport, parent_report)
parent_estimators.append(parent_report.estimator_reports_[0].estimator_)
parent_estimators.append(parent_report.reports_[0].estimator_)
elif parent._reports_type == "EstimatorReport":
parent_estimators.append(parent_report.estimator_)
else:
Expand Down
31 changes: 21 additions & 10 deletions skore/src/skore/_utils/_progress_bar.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,23 @@ def progress_decorator(
def decorator(func: Callable[..., T]) -> Callable[..., T]:
@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> T:
# Avoid circular import
from skore import ComparisonReport

# The object to which a `rich.Progress` instance will be attached.
# Expected to be a Report
# (EstimatorReport | CrossValidationReport | ComparisonReport)
self_obj: Any = args[0]

# If the decorated method is in an Accessor (e.g. MetricsAccessor),
# then make sure `self_obj` is the Report, not the Accessor.
if hasattr(self_obj, "_parent"):
# self_obj is an accessor
self_obj = self_obj._parent

desc = description(self_obj) if callable(description) else description

created_progress = False

if getattr(self_obj, "_progress_info", None) is not None:
if self_obj._progress_info is not None:
progress = self_obj._progress_info["current_progress"]
else:
progress = Progress(
Expand All @@ -67,15 +73,20 @@ def wrapper(*args: Any, **kwargs: Any) -> T:
progress.start()
created_progress = True

# assigning progress to child reports
# Make child reports share their parent's Progress instance
# so that there is only one Progress instance at any given point
reports_to_cleanup: list[Any] = []
if hasattr(self_obj, "reports_"):
if isinstance(self_obj, ComparisonReport):
for report in self_obj.reports_.values():
if hasattr(report, "_progress_info"):
report._progress_info = {"current_progress": progress}
reports_to_cleanup.append(report)

task = progress.add_task(desc, total=None)
report._progress_info = {"current_progress": progress}
reports_to_cleanup.append(report)

task = progress.add_task(
description=(
description(self_obj) if callable(description) else description
),
total=None,
)
self_obj._progress_info = {
"current_progress": progress,
"current_task": task,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def test_data_source_external(
for split_idx in range(splitter):
# check that it is equivalent to call the individual estimator report
report_result = (
report.estimator_reports_[split_idx]
report.reports_[split_idx]
.metrics.summarize(data_source="X_y", X=X, y=y)
.frame()
)
Expand Down Expand Up @@ -389,7 +389,7 @@ def test_scorer(linear_regression_data):
est_rep.y_test, est_rep.estimator_.predict(est_rep.X_test)
),
]
for est_rep in report.estimator_reports_
for est_rep in report.reports_
]
np.testing.assert_allclose(
result.to_numpy(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def test_binary_classification(

pos_label = 1
n_reports = len(report.reports_)
n_splits = len(list(report.reports_.values())[0].estimator_reports_)
n_splits = len(list(report.reports_.values())[0].reports_)

display.plot()
assert isinstance(display.lines_, list)
Expand Down Expand Up @@ -76,7 +76,7 @@ def test_multiclass_classification(

labels = display.precision_recall["label"].cat.categories
n_reports = len(report.reports_)
n_splits = len(list(report.reports_.values())[0].estimator_reports_)
n_splits = len(list(report.reports_.values())[0].reports_)

display.plot()
assert isinstance(display.lines_, list)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def test_binary_classification(

display.plot()

pos_label = report.estimator_reports_[0].estimator_.classes_[1]
pos_label = report.reports_[0].estimator_.classes_[1]

assert hasattr(display, "ax_")
assert hasattr(display, "figure_")
Expand Down Expand Up @@ -88,7 +88,7 @@ def test_multiclass_classification(

display.plot()

class_labels = report.estimator_reports_[0].estimator_.classes_
class_labels = report.reports_[0].estimator_.classes_

assert isinstance(display.lines_, list)
assert len(display.lines_) == len(class_labels) * cv
Expand Down
4 changes: 2 additions & 2 deletions skore/tests/unit/displays/roc_curve/test_cross_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def test_binary_classification(
assert isinstance(display, RocCurveDisplay)

check_display_data(display)
pos_label = report.estimator_reports_[0].estimator_.classes_[1]
pos_label = report.reports_[0].estimator_.classes_[1]
assert (
list(display.roc_curve["label"].unique())
== list(display.roc_auc["label"].unique())
Expand Down Expand Up @@ -88,7 +88,7 @@ def test_multiclass_classification(

# check the structure of the attributes
check_display_data(display)
class_labels = report.estimator_reports_[0].estimator_.classes_
class_labels = report.reports_[0].estimator_.classes_
assert (
list(display.roc_curve["label"].unique())
== list(display.roc_auc["label"].unique())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,9 @@ def test_get_predictions(
assert len(predictions) == len(report.reports_)
for i, cv_report in enumerate(report.reports_.values()):
assert len(predictions[i]) == cv_report._splitter.n_splits


def test_estimator_reports_(cross_validation_reports_regression):
cv, _ = cross_validation_reports_regression

assert id(cv.estimator_reports_) == id(cv.reports_)
14 changes: 7 additions & 7 deletions skore/tests/unit/reports/cross_validation/test_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,13 @@ def test_attributes(fixture_name, request, cv, n_jobs):
estimator, X, y = request.getfixturevalue(fixture_name)
report = CrossValidationReport(estimator, X, y, splitter=cv, n_jobs=n_jobs)
assert isinstance(report, CrossValidationReport)
assert isinstance(report.estimator_reports_, list)
for estimator_report in report.estimator_reports_:
assert isinstance(report.reports_, list)
for estimator_report in report.reports_:
assert isinstance(estimator_report, EstimatorReport)
assert report.X is X
assert report.y is y
assert report.n_jobs == n_jobs
assert len(report.estimator_reports_) == cv
assert len(report.reports_) == cv
if isinstance(estimator, Pipeline):
assert report.estimator_name_ == estimator[-1].__class__.__name__
else:
Expand Down Expand Up @@ -137,12 +137,12 @@ def test_cache_predictions(request, fixture_name, expected_n_keys, n_jobs):
# underlying estimator reports
assert report._cache == {}

for estimator_report in report.estimator_reports_:
for estimator_report in report.reports_:
assert len(estimator_report._cache) == expected_n_keys

report.clear_cache()
assert report._cache == {}
for estimator_report in report.estimator_reports_:
for estimator_report in report.reports_:
assert estimator_report._cache == {}


Expand Down Expand Up @@ -174,9 +174,9 @@ def test_get_predictions(
assert len(predictions) == 2
for split_idx, split_predictions in enumerate(predictions):
if data_source == "train":
expected_shape = report.estimator_reports_[split_idx].y_train.shape
expected_shape = report.reports_[split_idx].y_train.shape
elif data_source == "test":
expected_shape = report.estimator_reports_[split_idx].y_test.shape
expected_shape = report.reports_[split_idx].y_test.shape
else: # data_source == "X_y"
expected_shape = (X.shape[0],)
assert split_predictions.shape == expected_shape
Expand Down
2 changes: 1 addition & 1 deletion skore/tests/unit/utils/test_accessors.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ def __init__(self, estimator):

class MockParent:
def __init__(self, estimator):
self.estimator_reports_ = [MockReport(estimator)]
self.reports_ = [MockReport(estimator)]

class MockAccessor:
def __init__(self, parent):
Expand Down
Loading
Loading