alf_core 0.1.0a0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
alf_core/__init__.py ADDED
@@ -0,0 +1,58 @@
1
+ # Copyright 2026 InstaDeep Ltd. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # This file makes alf_core a Python package
16
+ import importlib.metadata
17
+
18
+ from alf_core.dataclasses import (
19
+ Candidate,
20
+ LabelledCandidates,
21
+ Modality,
22
+ Predictions,
23
+ Results,
24
+ State,
25
+ SurrogateEpochMetrics,
26
+ )
27
+ from alf_core.dataset.base_dataset import BaseDataset, BaseDatasetConfig
28
+ from alf_core.model.base_model import BaseModel, BaseTrainConfig
29
+ from alf_core.model.normaliser import (
30
+ InputNormaliser,
31
+ InputStandardiser,
32
+ OutputStandardiser,
33
+ make_input_transform,
34
+ )
35
+ from alf_core.optimizer.acquisition_function import AcquisitionFunction
36
+ from alf_core.optimizer.optimizer import Optimizer
37
+ from alf_core.optimizer.search import (
38
+ BaseSearch,
39
+ DatasetSearch,
40
+ GeneratorSearch,
41
+ ModelProtocolSearch,
42
+ ProtocolSearch,
43
+ SearchProtocol,
44
+ )
45
+ from alf_core.oracle.oracle import Oracle
46
+ from alf_core.surrogate.surrogate import Surrogate
47
+ from alf_core.tasks.base_task import BaseTask
48
+ from alf_core.tasks.design_task import DesignTask
49
+ from alf_core.tasks.supervised_task import SupervisedTask
50
+ from alf_core.tasks.zeroshot_task import ZeroShotTask
51
+ from alf_core.utils.enums import ProblemType
52
+ from alf_core.utils.state_logger import (
53
+ FileStateLogger,
54
+ StateLogger,
55
+ TerminalStateLogger,
56
+ )
57
+
58
+ __version__ = importlib.metadata.version("alf-core")
@@ -0,0 +1,26 @@
1
+ # Copyright 2026 InstaDeep Ltd. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # ruff: noqa: E402
16
+ import beartype.claw
17
+
18
+ beartype.claw.beartype_this_package()
19
+
20
+ from alf_core.dataclasses.candidate import Candidate, Modality
21
+ from alf_core.dataclasses.labelled_candidates import LabelledCandidates
22
+ from alf_core.dataclasses.predictions import Predictions
23
+ from alf_core.dataclasses.results import Results
24
+ from alf_core.dataclasses.round_metrics import RoundMetrics
25
+ from alf_core.dataclasses.state import State
26
+ from alf_core.dataclasses.surrogate_epoch_metrics import SurrogateEpochMetrics
@@ -0,0 +1,225 @@
1
+ # Copyright 2026 InstaDeep Ltd. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ from dataclasses import dataclass
17
+ from enum import Enum
18
+ from typing import TYPE_CHECKING, Any, TypeAlias, Union
19
+
20
+ import numpy as np
21
+
22
+ if TYPE_CHECKING:
23
+ import torch
24
+
25
+ DataFrameCompatible: TypeAlias = Union[
26
+ str, int, float, bool, dict, list, tuple, np.ndarray, torch.Tensor
27
+ ]
28
+ else:
29
+ DataFrameCompatible: TypeAlias = Union[
30
+ str, int, float, bool, dict, list, tuple, np.ndarray, Any
31
+ ]
32
+
33
+ try:
34
+ import torch
35
+
36
+ HAS_TORCH = True
37
+ except ImportError:
38
+ HAS_TORCH = False
39
+
40
+
41
+ class Modality(Enum):
42
+ """The *kind* of candidate — used to match datasets with compatible models and metrics.
43
+
44
+ It is the data's domain where one exists, not how the data is stored. This is why a
45
+ protein sequence and a SMILES string are distinct modalities (``SEQUENCE`` vs
46
+ ``MOLECULE``) even though both are stored as ``str``: they pair with different models
47
+ and metrics. Storage type is never encoded here — it is inferred from ``type(data)``
48
+ (see :meth:`Candidate.to_serializable`).
49
+
50
+ Members:
51
+ SEQUENCE: Biological sequences (protein / nucleotide), as strings.
52
+ MOLECULE: Small molecules, as SMILES strings.
53
+ TABULAR: Domain-agnostic numeric feature vectors (arrays, tensors, scalars, dicts).
54
+ """
55
+
56
+ SEQUENCE = "sequence"
57
+ MOLECULE = "molecule"
58
+ TABULAR = "tabular"
59
+
60
+
61
+ @dataclass(eq=False, unsafe_hash=False)
62
+ class Candidate:
63
+ """A candidate is a data point with a modality and features.
64
+
65
+ Attributes:
66
+ data: The raw data of the candidate (e.g., a sequence string, a SMILES string,
67
+ a feature vector).
68
+ modality: The data's domain (see :class:`Modality`) — what it represents, e.g.
69
+ ``"sequence"`` or ``"molecule"``. Not its storage type.
70
+ features: Optional dictionary of precomputed features for the candidate.
71
+ """
72
+
73
+ data: Any
74
+ modality: Modality | str
75
+ features: dict | None = None
76
+
77
+ def __post_init__(self) -> None:
78
+ """Check and convert modality to Modality enum if necessary and
79
+ initialize features to empty dict if None.
80
+
81
+ Raises:
82
+ ValueError: If the modality is not a valid Modality enum.
83
+ """
84
+ if not isinstance(self.modality, Modality):
85
+ try:
86
+ self.modality = Modality(self.modality)
87
+ except ValueError:
88
+ raise ValueError(f"Invalid modality: {self.modality}")
89
+
90
+ if self.features is None:
91
+ self.features = {}
92
+
93
+ def __repr__(self) -> str:
94
+ """Return a string representation of the candidate.
95
+
96
+ Returns:
97
+ A string representation showing the candidate's data, modality, and features.
98
+ """
99
+ return f"Candidate(data={self.data}, modality={self.modality}, features={self.features})"
100
+
101
+ def _safe_equal(self, a: Any, b: Any) -> bool:
102
+ """Compare two values, handling numpy arrays and nested structures.
103
+
104
+ Args:
105
+ a: First value to compare.
106
+ b: Second value to compare.
107
+
108
+ Returns:
109
+ True if the values are equal, False otherwise.
110
+ """
111
+ # Handle None cases
112
+ if a is None and b is None:
113
+ return True
114
+ if a is None or b is None:
115
+ return False
116
+
117
+ # Handle numpy arrays
118
+ if isinstance(a, np.ndarray) and isinstance(b, np.ndarray):
119
+ return np.array_equal(a, b, equal_nan=True)
120
+
121
+ # Handle torch tensors
122
+ if HAS_TORCH and isinstance(a, torch.Tensor) and isinstance(b, torch.Tensor):
123
+ return torch.equal(a, b)
124
+
125
+ # Handle dict (for features)
126
+ if isinstance(a, dict) and isinstance(b, dict):
127
+ if a.keys() != b.keys():
128
+ return False
129
+ return all(self._safe_equal(a[k], b[k]) for k in a.keys())
130
+
131
+ # Handle lists/tuples (for nested data)
132
+ if isinstance(a, (list, tuple)) and isinstance(b, (list, tuple)):
133
+ if len(a) != len(b):
134
+ return False
135
+ return all(self._safe_equal(x, y) for x, y in zip(a, b))
136
+
137
+ # Default comparison
138
+ # Note, if comparison fails, an error will be thrown
139
+ result = a == b
140
+ # Handle case where comparison returns array-like object
141
+ # Convert to boolean if possible
142
+ if hasattr(result, "__len__") and len(result) == 1:
143
+ return bool(result[0])
144
+ elif hasattr(result, "item"): # For single-element tensors/arrays
145
+ return bool(result.item())
146
+ return bool(result)
147
+
148
+ def __eq__(self, other: object) -> bool:
149
+ """Compare two Candidate objects for equality.
150
+
151
+ Handles numpy arrays in data and features fields correctly.
152
+
153
+ Args:
154
+ other: The object to compare with.
155
+
156
+ Returns:
157
+ True if the candidates are equal, False otherwise.
158
+ """
159
+ if not isinstance(other, Candidate):
160
+ return False
161
+
162
+ return (
163
+ self._safe_equal(self.data, other.data)
164
+ and self.modality == other.modality
165
+ and self._safe_equal(self.features, other.features)
166
+ )
167
+
168
+ __hash__ = None # type: ignore[assignment]
169
+
170
+ def to_serializable(self) -> DataFrameCompatible | None:
171
+ """Convert candidate data to a format suitable for pandas DataFrame storage.
172
+
173
+ Dispatch is based on the **type** of ``data``, not on :attr:`modality`. Modality
174
+ describes the data's domain (see :class:`Modality`); how it is stored — and
175
+ therefore how it is serialised — is determined by its Python type:
176
+
177
+ - ``str`` (sequences, SMILES, JSON-encoded payloads): returned unchanged.
178
+ - ``torch.Tensor``: converted to a numpy array for compact storage.
179
+ - numpy array, scalar (Python ``int``/``float``/``bool`` or a numpy scalar such
180
+ as ``np.int64``), ``dict``, ``list``, ``tuple``, pandas ``Series``: returned
181
+ unchanged.
182
+ - ``None``: returned as ``None``.
183
+
184
+ Returns:
185
+ DataFrameCompatible: The candidate data in a DataFrame-compatible format.
186
+ Common types include str, dict, np.ndarray, pd.Series, or torch.Tensor.
187
+
188
+ Raises:
189
+ TypeError: If the data type is not DataFrame-compatible.
190
+
191
+ Examples:
192
+ >>> # A sequence or SMILES string is stored as-is
193
+ >>> Candidate(data="ACDEFG", modality=Modality.SEQUENCE).to_serializable()
194
+ 'ACDEFG'
195
+
196
+ >>> Candidate(data="CC(=O)O", modality=Modality.MOLECULE).to_serializable()
197
+ 'CC(=O)O'
198
+
199
+ >>> # A feature dict is stored as-is
200
+ >>> c = Candidate(data={"age": 32, "height": 178}, modality=Modality.TABULAR)
201
+ >>> c.to_serializable()
202
+ {'age': 32, 'height': 178}
203
+ """
204
+ data = self.data
205
+ if data is None:
206
+ return None
207
+ # Strings (sequences, SMILES, JSON-encoded payloads) are stored as-is.
208
+ if isinstance(data, str):
209
+ return data
210
+ # Torch tensors are converted to numpy arrays for compact storage.
211
+ if HAS_TORCH and isinstance(data, torch.Tensor):
212
+ return data.cpu().numpy()
213
+ # numpy arrays, scalars (incl. numpy scalars like np.int64), and standard
214
+ # containers are stored as-is.
215
+ if isinstance(data, (int, float, bool, np.generic, dict, np.ndarray, list, tuple)):
216
+ return data
217
+ # pandas Series (checked without importing pandas).
218
+ if data.__class__.__name__ == "Series":
219
+ return data
220
+
221
+ raise TypeError(
222
+ f"Cannot serialise candidate data of type {type(data).__name__}. Supported "
223
+ f"types: str, int, float, bool, dict, list, tuple, numpy.ndarray, "
224
+ f"pandas.Series, or torch.Tensor."
225
+ )
@@ -0,0 +1,234 @@
1
+ # Copyright 2026 InstaDeep Ltd. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ from dataclasses import dataclass
17
+ from typing import Any, Union
18
+
19
+ import numpy as np
20
+ import pandas as pd
21
+
22
+ from alf_core.dataclasses.candidate import Candidate
23
+
24
+
25
+ @dataclass(eq=False, unsafe_hash=False)
26
+ class LabelledCandidates:
27
+ """A collection of candidates paired with their labels.
28
+
29
+ Attributes:
30
+ candidates: A list of Candidate objects.
31
+ labels: A numpy array of labels corresponding to each candidate.
32
+ """
33
+
34
+ candidates: list[Candidate]
35
+ labels: np.ndarray
36
+
37
+ def __post_init__(self) -> None:
38
+ """Validate that candidates and labels have the same length.
39
+
40
+ Raises:
41
+ AssertionError: If the length of candidates and labels don't match.
42
+ """
43
+ assert len(self.candidates) == len(self.labels), (
44
+ f"Candidates and labels must have the same length, "
45
+ f"got {len(self.candidates)} candidates and {len(self.labels)} labels"
46
+ )
47
+
48
+ def __len__(self) -> int:
49
+ """Return the number of candidates in the collection.
50
+
51
+ Returns:
52
+ The number of candidates (and labels) in the collection.
53
+ """
54
+ return len(self.candidates)
55
+
56
+ def __getitem__(
57
+ self, index: Union[int, slice, np.ndarray]
58
+ ) -> tuple[list[Candidate], np.ndarray]:
59
+ """Make LabelledCandidates subscriptable.
60
+
61
+ Args:
62
+ index: Integer index or slice to select candidates and labels.
63
+
64
+ Returns:
65
+ The candidates and labels at the specified index or slice.
66
+ """
67
+ if isinstance(index, int):
68
+ return ([self.candidates[index]], np.array([self.labels[index]]))
69
+ elif isinstance(index, np.ndarray):
70
+ return ([self.candidates[i] for i in index], self.labels[index])
71
+ else:
72
+ return (self.candidates[index], self.labels[index])
73
+
74
+ @property
75
+ def data(self) -> list[Any]:
76
+ """Return the raw data of each candidate.
77
+
78
+ Returns:
79
+ A list containing the raw data (e.g. a sequence string, a SMILES string,
80
+ a feature vector) of each candidate in the collection.
81
+ """
82
+ return [cand.data for cand in self.candidates]
83
+
84
+ def append(
85
+ self,
86
+ candidates: Union[list[Candidate], "LabelledCandidates"],
87
+ labels: np.ndarray | None = None,
88
+ ) -> None:
89
+ """Append candidates and labels to this collection.
90
+
91
+ Args:
92
+ candidates: Either a list of Candidate objects or another LabelledCandidates
93
+ object. If a list is provided, labels must also be provided.
94
+ labels: Optional numpy array of labels. Required if candidates is a list,
95
+ ignored if candidates is a LabelledCandidates object.
96
+
97
+ Raises:
98
+ AssertionError: If candidates is a list and labels is None, or if the
99
+ length of candidates and labels don't match.
100
+ """
101
+ if isinstance(candidates, LabelledCandidates):
102
+ self.candidates.extend(candidates.candidates)
103
+ self.labels = np.concatenate((self.labels, candidates.labels), axis=0)
104
+ else:
105
+ assert labels is not None, (
106
+ "Labels must be provided when appending a list of Candidates "
107
+ "(pass a LabelledCandidates instead to append "
108
+ "without providing labels separately)"
109
+ )
110
+ assert len(candidates) == len(labels), (
111
+ f"Candidates and labels must have the same length, "
112
+ f"got {len(candidates)} candidates and {len(labels)} labels"
113
+ )
114
+ self.candidates.extend(candidates)
115
+ self.labels = np.concatenate((self.labels, labels), axis=0)
116
+
117
+ def shuffle(self, seed: int) -> "LabelledCandidates":
118
+ """Create a new LabelledCandidates object with shuffled candidates and labels.
119
+
120
+ Args:
121
+ seed: Random seed for reproducibility of the shuffle.
122
+
123
+ Returns:
124
+ A new LabelledCandidates object with the same candidates and labels,
125
+ but in a randomly shuffled order.
126
+ """
127
+ shuffled_indices = np.random.RandomState(seed).permutation(len(self.candidates))
128
+ return LabelledCandidates(
129
+ candidates=[self.candidates[i] for i in shuffled_indices],
130
+ labels=self.labels[shuffled_indices],
131
+ )
132
+
133
+ def sort(self, ascending: bool = True) -> "LabelledCandidates":
134
+ """Sort the candidates and labels by label values.
135
+
136
+ Args:
137
+ ascending: If True, sort in ascending order (lowest to highest).
138
+ If False, sort in descending order (highest to lowest).
139
+ Defaults to True.
140
+
141
+ Returns:
142
+ A new LabelledCandidates object with candidates and labels sorted
143
+ by label values.
144
+ """
145
+ sorted_indices = np.argsort(self.labels)
146
+ if not ascending:
147
+ sorted_indices = sorted_indices[::-1]
148
+ return LabelledCandidates(
149
+ candidates=[self.candidates[i] for i in sorted_indices],
150
+ labels=self.labels[sorted_indices],
151
+ )
152
+
153
+ def remove(self, candidates: Union[list[Candidate], "LabelledCandidates"]) -> None:
154
+ """Remove specified candidates and their corresponding labels from this collection.
155
+
156
+ Uses identity-based comparison (same object instance) for removal.
157
+ Candidates not present in the collection are silently ignored.
158
+
159
+ Args:
160
+ candidates: Either a list of Candidate objects or a LabelledCandidates
161
+ object containing the candidates to remove.
162
+ """
163
+ if isinstance(candidates, LabelledCandidates):
164
+ candidates = candidates.candidates
165
+
166
+ # Build set of object IDs to remove for O(1) lookup
167
+ ids_to_remove = {id(c) for c in candidates}
168
+
169
+ # Find indices to keep (single pass)
170
+ indices_to_keep = [i for i, c in enumerate(self.candidates) if id(c) not in ids_to_remove]
171
+
172
+ # Update candidates and labels
173
+ self.candidates = [self.candidates[i] for i in indices_to_keep]
174
+ self.labels = self.labels[indices_to_keep]
175
+
176
+ def to_dataframe(self) -> pd.DataFrame:
177
+ """Convert the labelled candidates to a pandas DataFrame.
178
+
179
+ Returns:
180
+ A DataFrame with columns:
181
+ - "data": The formatted data of each candidate
182
+ - "label": The label value for each candidate
183
+ - Additional columns for any features present in the candidates
184
+ """
185
+ rows = [
186
+ {"data": cand.to_serializable(), "label": label}
187
+ | (cand.features if isinstance(cand.features, dict) else {})
188
+ for cand, label in zip(self.candidates, self.labels)
189
+ ]
190
+ return pd.DataFrame.from_records(rows)
191
+
192
+ def get_top_k(self, k: int) -> "LabelledCandidates":
193
+ """Return the top k candidates based on their label values.
194
+
195
+ Args:
196
+ k: Number of top candidates to select.
197
+
198
+ Returns:
199
+ New LabelledCandidates object containing the top k candidates sorted
200
+ by label values (highest first).
201
+ """
202
+ top_k_indices = self.labels.argsort()[::-1][:k]
203
+ top_k_candidates = [self.candidates[i] for i in top_k_indices]
204
+ top_k_labels = self.labels[top_k_indices]
205
+ return LabelledCandidates(candidates=top_k_candidates, labels=top_k_labels)
206
+
207
+ def __iter__(self):
208
+ """Iterate over candidates and labels, yielding (candidate, label) tuples.
209
+
210
+ Yields:
211
+ Tuple of (Candidate, float): A candidate and its corresponding label
212
+ """
213
+ for candidate, label in zip(self.candidates, self.labels):
214
+ yield candidate, label
215
+
216
+ def __eq__(self, other: object) -> bool:
217
+ """Compare two LabelledCandidates objects for equality.
218
+
219
+ Handles numpy array labels correctly.
220
+
221
+ Args:
222
+ other: The object to compare with.
223
+
224
+ Returns:
225
+ True if the labeled candidates are equal, False otherwise.
226
+ """
227
+ if not isinstance(other, LabelledCandidates):
228
+ return False
229
+
230
+ return self.candidates == other.candidates and np.array_equal(
231
+ self.labels, other.labels, equal_nan=True
232
+ )
233
+
234
+ __hash__ = None # type: ignore[assignment]
@@ -0,0 +1,119 @@
1
+ # Copyright 2026 InstaDeep Ltd. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ import logging
17
+ from dataclasses import dataclass
18
+
19
+ import numpy as np
20
+ import pandas as pd
21
+
22
+ from alf_core.dataclasses.candidate import Candidate
23
+ from alf_core.utils.enums import ProblemType
24
+
25
+ logger = logging.getLogger("alf-core")
26
+
27
+
28
+ @dataclass
29
+ class Predictions:
30
+ """A data class for storing and managing predictions from a model.
31
+
32
+ Attributes:
33
+ means: A numpy array of mean predictions across all candidates.
34
+ variances: An optional numpy array of prediction variances, representing
35
+ the model's uncertainty for each prediction.
36
+ empirical_dist: An optional 2D numpy array containing predictions
37
+ from individual models in an ensemble. Typically has the shape
38
+ (num_candidates, num_ensemble_models).
39
+ """
40
+
41
+ means: np.ndarray
42
+ variances: np.ndarray | None = None
43
+ empirical_dist: np.ndarray | None = None
44
+
45
+ def __post_init__(self) -> None:
46
+ """Validate prediction arrays have consistent lengths.
47
+
48
+ Raises:
49
+ AssertionError: If means is empty, or if variances or empirical_dist
50
+ don't match the length of means.
51
+ """
52
+ assert len(self.means) > 0, (
53
+ "Means must have at least one prediction — expected shape (num_candidates,)"
54
+ )
55
+ if self.variances is not None:
56
+ assert len(self.variances) == len(self.means), (
57
+ "Variances must have the same length as means (num_candidates,), "
58
+ f"got {len(self.variances)} variances and {len(self.means)} means"
59
+ )
60
+ if self.empirical_dist is not None:
61
+ assert len(self.empirical_dist) == len(self.means), (
62
+ "Empirical_dist must have the same length as means - "
63
+ "shape (num_candidates, num_ensemble_models), "
64
+ f"but its first dimension ({len(self.empirical_dist)}) does not match "
65
+ f"len(means) ({len(self.means)})"
66
+ )
67
+
68
+ def __len__(self) -> int:
69
+ """Return the number of predictions.
70
+
71
+ Returns:
72
+ The number of predictions (length of the means array).
73
+ """
74
+ return len(self.means)
75
+
76
+ def to_dataframe(
77
+ self,
78
+ candidates: list[Candidate],
79
+ targets: np.ndarray,
80
+ problem_type: ProblemType,
81
+ ) -> pd.DataFrame:
82
+ """Convert predictions to a DataFrame.
83
+
84
+ Creates a DataFrame with predictions, targets, and optionally variances
85
+ and ensemble predictions.
86
+
87
+ Args:
88
+ candidates: List of Candidate objects corresponding to the predictions.
89
+ targets: Ground truth target values corresponding to each candidate.
90
+ problem_type: ProblemType to determine the predictions type.
91
+
92
+ Returns:
93
+ A DataFrame with predictions, targets, and optionally variances
94
+ and ensemble predictions.
95
+ """
96
+ predictions_list = []
97
+ is_classification = problem_type in [ProblemType.BINARY, ProblemType.MULTICLASS]
98
+
99
+ for i in range(len(self.means)):
100
+ record_i: dict[str, str | float | np.floating | list[float] | np.ndarray] = {
101
+ "data": candidates[i].data,
102
+ "targets": targets[i],
103
+ }
104
+
105
+ if is_classification:
106
+ for cls_idx in range(self.means.shape[1]):
107
+ record_i[f"prob_class_{cls_idx}"] = self.means[i, cls_idx]
108
+ else:
109
+ record_i["mean"] = self.means[i]
110
+ record_i["variance"] = self.variances[i] if self.variances is not None else 0
111
+
112
+ if self.empirical_dist is not None:
113
+ for j in range(self.empirical_dist.shape[1]):
114
+ record_i[f"ensemble_pred_{j}"] = self.empirical_dist[i, j]
115
+
116
+ predictions_list.append(record_i)
117
+
118
+ df = pd.DataFrame.from_records(predictions_list)
119
+ return df