Source code for alf_core.dataclasses.predictions
# Copyright 2026 InstaDeep Ltd. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
from dataclasses import dataclass
import numpy as np
import pandas as pd
from alf_core.dataclasses.candidate import Candidate, DataFrameCompatible
from alf_core.utils.enums import ProblemType
logger = logging.getLogger("alf-core")
[docs]
@dataclass
class Predictions:
"""A data class for storing and managing predictions from a model.
Attributes:
means: A numpy array of mean predictions across all candidates.
variances: An optional numpy array of prediction variances, representing
the model's uncertainty for each prediction.
empirical_dist: An optional 2D numpy array containing predictions
from individual models in an ensemble. Typically has the shape
(num_candidates, num_ensemble_models).
"""
means: np.ndarray
variances: np.ndarray | None = None
empirical_dist: np.ndarray | None = None
def __post_init__(self) -> None:
"""Validate prediction arrays have consistent lengths.
Raises:
AssertionError: If means is empty, or if variances or empirical_dist
don't match the length of means.
"""
assert len(self.means) > 0, (
"Means must have at least one prediction — expected shape (num_candidates,)"
)
if self.variances is not None:
assert len(self.variances) == len(self.means), (
"Variances must have the same length as means (num_candidates,), "
f"got {len(self.variances)} variances and {len(self.means)} means"
)
if self.empirical_dist is not None:
assert len(self.empirical_dist) == len(self.means), (
"Empirical_dist must have the same length as means - "
"shape (num_candidates, num_ensemble_models), "
f"but its first dimension ({len(self.empirical_dist)}) does not match "
f"len(means) ({len(self.means)})"
)
def __len__(self) -> int:
"""Return the number of predictions.
Returns:
The number of predictions (length of the means array).
"""
return len(self.means)
[docs]
def to_dataframe(
self,
candidates: list[Candidate],
targets: np.ndarray,
problem_type: ProblemType,
) -> pd.DataFrame:
"""Convert predictions to a DataFrame.
Creates a DataFrame with predictions, targets, and optionally variances
and ensemble predictions.
Args:
candidates: List of Candidate objects corresponding to the predictions.
targets: Ground truth target values corresponding to each candidate.
problem_type: ProblemType to determine the predictions type.
Returns:
A DataFrame with predictions, targets, and optionally variances
and ensemble predictions.
"""
predictions_list = []
is_classification = problem_type in [ProblemType.BINARY, ProblemType.MULTICLASS]
for i in range(len(self.means)):
record_i: dict[str, DataFrameCompatible | None] = {
"data": candidates[i].to_serializable(),
"targets": targets[i],
}
if is_classification:
for cls_idx in range(self.means.shape[1]):
record_i[f"prob_class_{cls_idx}"] = self.means[i, cls_idx]
else:
record_i["mean"] = self.means[i]
record_i["variance"] = self.variances[i] if self.variances is not None else 0
if self.empirical_dist is not None:
for j in range(self.empirical_dist.shape[1]):
record_i[f"ensemble_pred_{j}"] = self.empirical_dist[i, j]
predictions_list.append(record_i)
df = pd.DataFrame.from_records(predictions_list)
return df