hiplot-plus 0.1.0__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.
hiplot/experiment.py ADDED
@@ -0,0 +1,637 @@
1
+ # Copyright (c) Facebook, Inc. and its affiliates.
2
+ # This source code is licensed under the MIT license found in the
3
+ # LICENSE file in the root directory of this source tree.
4
+
5
+ import codecs
6
+ import csv
7
+ import typing as tp
8
+ import uuid
9
+ from abc import ABCMeta, abstractmethod
10
+ from collections import defaultdict
11
+ from enum import Enum
12
+ from pathlib import Path
13
+
14
+ if tp.TYPE_CHECKING:
15
+ import optuna
16
+ import pandas as pd
17
+
18
+ from .streamlit_helpers import ExperimentStreamlitComponent
19
+
20
+ type TextWriterIO = tp.IO[str] | codecs.StreamWriter
21
+
22
+ type DisplayableType = bool | int | float | str | None
23
+
24
+
25
+ class ExperimentValidationError(Exception):
26
+ pass
27
+
28
+
29
+ class ExperimentValidationCircularRef(ExperimentValidationError):
30
+ pass
31
+
32
+
33
+ class ExperimentValidationMissingParent(ExperimentValidationError):
34
+ pass
35
+
36
+
37
+ class _DictSerializable:
38
+ """
39
+ All classes that are transmitted to Javascript must subclass this
40
+ """
41
+
42
+ def _asdict(self) -> dict[str, tp.Any]:
43
+ return self.__dict__
44
+
45
+
46
+ class ValueType(Enum):
47
+ """
48
+ Defines how we render a column (scaling, and color scheme)
49
+ """
50
+
51
+ CATEGORICAL = "categorical" #: Categorical value
52
+ NUMERIC = "numeric" #: Numeric value on a linear scale. Supports integers, floats, NaNs and inf
53
+ NUMERIC_LOG = "numericlog" #: Same as :attr:`hiplot.ValueType.NUMERIC`, displayed on a logarithmic scale.
54
+ NUMERIC_PERCENTILE = (
55
+ "numericpercentile" #: Same as :attr:`hiplot.ValueType.NUMERIC`, displayed on a percentile scale.
56
+ )
57
+ TIMESTAMP = "timestamp" #: Timestamps in seconds (only integers)
58
+
59
+
60
+ class Displays:
61
+ """
62
+ See :meth:`Experiment.display_data` and :ref:`frontendRenderingSettings`
63
+ """
64
+
65
+ PARALLEL_PLOT = "PARALLEL_PLOT" #: Parallel plot data
66
+ XY = "XY" #: XY scatter/line plot data
67
+ TABLE = "TABLE" #: Rows table data
68
+ #: Distribution plot data. Not a display of its own: an ``axis`` set here
69
+ #: opens a one-axis scatter panel, whose bins ``nbins`` sets.
70
+ DISTRIBUTION = "DISTRIBUTION"
71
+
72
+
73
+ def validate_colormap(cm: str | None) -> None:
74
+ VALID_MODIFIERS = ["inverse"]
75
+ if cm is None:
76
+ return
77
+ cm_modifiers = cm.split("#", 1)
78
+ cm = cm_modifiers[0]
79
+ # We don't want `d3.interpolateTurbo` but just `interpolateTurbo`
80
+ if cm is not None and not cm.startswith("interpolate") and not cm.startswith("scheme"):
81
+ raise ExperimentValidationError(f"""Invalid colormap `{cm}`.
82
+ Valid colormaps can be found in https://github.com/d3/d3-scale-chromatic.
83
+ Their name starts with `interpolate` or `scheme`.
84
+ Examples include `interpolateSpectral`, `interpolateViridis`, `interpolateSinebow`, `schemeYlOrRd`
85
+ """)
86
+ if len(cm_modifiers) > 1:
87
+ for modifier in cm_modifiers[1].split(","):
88
+ if modifier not in VALID_MODIFIERS:
89
+ raise ExperimentValidationError(f"""Invalid colormap modifier `{modifier}`.
90
+ Valid colormaps modifiers: {",".join(VALID_MODIFIERS)}
91
+ """)
92
+
93
+
94
+ class ValueDef(_DictSerializable):
95
+ """
96
+ Provides a custom type, color, etc.. for a column.
97
+
98
+ :ivar type: See :attr:`hiplot.ValueType` for possible values
99
+ :ivar colors: Categorical scales only: mapping from value to HTML color
100
+ (either :code:`rgb(R, G, B)` or :code:`#RRGGBB`)
101
+ :ivar colormap: Numerical scales only: `D3 scale <https://github.com/d3/d3-scale-chromatic>`_ to use
102
+ (default scale is `interpolateViridis <https://d3js.org/d3-scale-chromatic/sequential#interpolateViridis>`_).
103
+ For example :code:`"interpolateSinebow"`.
104
+ To inverse the colormap, append `#inverse` to the name (eg :code:`"interpolateSinebow#inverse"`)
105
+ :ivar label_css: Space-separated bootstrap CSS classes to apply on the label when supported
106
+ :ivar label_html: HTML code used to render the column name
107
+
108
+ See :attr:`hiplot.Experiment.parameters_definition`
109
+ """
110
+
111
+ def __init__(
112
+ self,
113
+ value_type: ValueType | None = None,
114
+ colors: dict[tp.Any, str] | None = None,
115
+ colormap: str | None = None,
116
+ label_css: str | None = None,
117
+ ) -> None:
118
+ self.type = value_type
119
+ self.colors = colors
120
+ self.colormap = colormap
121
+ self.label_css = label_css
122
+ self.force_value_min: float | None = None
123
+ self.force_value_max: float | None = None
124
+ self.label_html: str | None = None
125
+
126
+ def force_range(self, minimum: float, maximum: float) -> "ValueDef":
127
+ """
128
+ Enforces the range of the column.
129
+ """
130
+ self.force_value_min = minimum
131
+ self.force_value_max = maximum
132
+ return self
133
+
134
+ def validate(self) -> None:
135
+ if self.colors is not None:
136
+ for k, v in self.colors.items():
137
+ if not v.startswith("rgb(") and not v.startswith("hsl(") and not v.startswith("#"):
138
+ raise ExperimentValidationError(
139
+ f'Invalid color {v} for value {k}. Expected color to start with "rgb(", "hsl(", or "#"'
140
+ )
141
+ validate_colormap(self.colormap)
142
+
143
+ def _asdict(self) -> dict[str, tp.Any]:
144
+ return {
145
+ "type": self.type.value if self.type is not None else None,
146
+ "colors": self.colors,
147
+ "colormap": self.colormap,
148
+ "force_value_min": self.force_value_min,
149
+ "force_value_max": self.force_value_max,
150
+ "label_css": self.label_css,
151
+ "label_html": self.label_html,
152
+ }
153
+
154
+
155
+ class Datapoint(_DictSerializable):
156
+ """
157
+ A datapoint represents a single measurement of metrics - for instance a model checkpoint that is evaluated.
158
+ It can have a parent if it originates from another one (offspring).
159
+
160
+
161
+ :ivar uid: A unique identifier for this datapoint
162
+ :ivar values: A dictionary with arbitrary metrics/values
163
+ :ivar from_uid: The uid of the parent :class:`Datapoint` (tp.Optional)
164
+
165
+ :Example:
166
+
167
+ .. code-block:: python
168
+
169
+ import hiplot as hip
170
+ dp1 = hip.Datapoint(uid="parent", values={"loss": 0.0})
171
+ dp2 = hip.Datapoint(uid="child", from_uid="parent", values={
172
+ "loss": 1.0,
173
+ "another_metric": 0.0 # Different datapoints can have different metrics
174
+ })
175
+ hip.Experiment(datapoints=[dp1, dp2]).display() # Render in an ipython notebook
176
+ """
177
+
178
+ def __init__(
179
+ self, values: dict[str, DisplayableType], *, uid: str | None = None, from_uid: str | None = None
180
+ ) -> None:
181
+ self.uid = uid if uid is not None else str(uuid.uuid4())
182
+ self.values = values
183
+ self.from_uid = from_uid
184
+
185
+ def validate(self) -> None:
186
+ """
187
+ Makes sure this object is valid - throws an :class:`hiplot.ExperimentValidationError` exception otherwise.
188
+ """
189
+ for reserved_kw in ["uid", "from_uid"]:
190
+ if reserved_kw in self.values:
191
+ raise ExperimentValidationError(f'Datapoint {self.uid} contains a value for "{reserved_kw}"')
192
+
193
+
194
+ def _is_running_ipython() -> bool:
195
+ try:
196
+ get_ipython() # type: ignore[name-defined]
197
+ return True
198
+ except NameError:
199
+ return False
200
+
201
+
202
+ # pylint: disable=too-many-instance-attributes
203
+ class Experiment(_DictSerializable):
204
+ """
205
+ Object that can be rendered by HiPlot. It essentially contains a list of metrics,
206
+ but also some options on how to render it.
207
+
208
+ See :meth:`Experiment.display` to display an :class:`Experiment` in an ipython notebook.
209
+
210
+ :ivar datapoints: All the measurements we have.
211
+ One datapoint corresponds to one line in the parallel plot and to one line in the table.
212
+ :ivar parameters_definition: Characteristics of the columns (ordering, type, etc...)
213
+ :ivar colormap: Colormap to use
214
+ :ivar colorby: Default column to color by
215
+ :ivar weightcolumn: If rows have different weights, use this column as the weight (default to 1 if not specified)
216
+ :ivar enabledDisplays: Ordered displays to enable (by default the parallel plot, the scatter panels and the table)
217
+
218
+ :Example:
219
+
220
+ .. code-block:: python
221
+
222
+ import hiplot as hip
223
+ data = [{'param': 1, 'loss': 10, 'hidden_field': 'value1', 'c': 'red'},
224
+ {'param': 2, 'loss': 5, 'hidden_field': 'value2', 'c': 'black'}]
225
+ exp = hip.Experiment.from_iterable(data)
226
+
227
+ """
228
+
229
+ def __init__(
230
+ self,
231
+ datapoints: list[Datapoint] | None = None,
232
+ parameters_definition: dict[str, ValueDef] | None = None,
233
+ colormap: str | None = None,
234
+ ) -> None:
235
+ self.datapoints = datapoints if datapoints is not None else []
236
+ self.parameters_definition = (
237
+ parameters_definition if parameters_definition is not None else defaultdict(ValueDef)
238
+ )
239
+ self.colormap = colormap
240
+ self.colorby: str | None = None
241
+ self.weightcolumn: str | None = None
242
+ self.enabledDisplays: list[str] = [
243
+ Displays.PARALLEL_PLOT,
244
+ Displays.XY,
245
+ Displays.TABLE,
246
+ ]
247
+ self._display_data: dict[str, dict[str, tp.Any]] = {}
248
+ self._compress: bool = False
249
+
250
+ def validate(self) -> "Experiment":
251
+ """
252
+ Makes sure that this object is valid. Raises a :class:`hiplot.ExperimentValidationError` otherwise.
253
+ Experiments with circular references, non-existent parents, or without datapoints are invalid.
254
+ """
255
+ seen: set[str] = set()
256
+ dp_lookup: dict[str, Datapoint] = {dp.uid: dp for dp in self.datapoints}
257
+ for p in self.datapoints:
258
+ if p.uid not in seen:
259
+ seen_now: set[str] = {p.uid}
260
+ dp = p
261
+ while dp.from_uid is not None and dp.from_uid not in seen:
262
+ if dp.from_uid in seen_now:
263
+ raise ExperimentValidationCircularRef(
264
+ f"Circular reference in {p} parents ({len(seen_now)}-th parent)"
265
+ )
266
+ seen_now.add(dp.from_uid)
267
+ if dp.from_uid not in dp_lookup:
268
+ raise ExperimentValidationMissingParent(
269
+ f"Datapoint ({dp.uid}) parent ({dp.from_uid}) not found"
270
+ )
271
+ dp = dp_lookup[dp.from_uid]
272
+ seen |= seen_now
273
+ p.validate()
274
+ if not self.datapoints:
275
+ raise ExperimentValidationError("Not a single datapoint")
276
+ validate_colormap(self.colormap)
277
+ return self
278
+
279
+ def display(
280
+ self, force_full_width: bool = False, store_state_key: str | None = None, **kwargs: tp.Any
281
+ ) -> "ExperimentDisplayed":
282
+ """
283
+ Displays an experiment in an ipython notebook.
284
+
285
+ :param force_full_width: allows to force to have 100% width on Jupyter Notebooks only.
286
+ :param store_state_key: a string identifier for the HiPlot instance.
287
+ If not ``None``, HiPlot will store dynamic modifications (removing/reordering columns...)
288
+ in the URL, and restore them when calling ``display`` with the same value for ``store_state_key``
289
+ - see :ref:`tutoNotebookState`
290
+ :returns: An :class:`ExperimentDisplayed` object that can be used to interact with the visualization
291
+ - only implemented for Jupyter notebook.
292
+ See :ref:`tutonotebookdisplayedexperiment`
293
+ """
294
+ from .streamlit_helpers import _StreamlitHelpers # pylint: disable=cyclic-import
295
+
296
+ if not _is_running_ipython():
297
+ if _StreamlitHelpers.is_running_within_streamlit():
298
+ raise RuntimeError(r"""`experiment.display` can only be called with ipython.
299
+ It appears that you are trying to create a HiPlot visualization in Streamlit: you should use `display_st`""")
300
+ raise RuntimeError(r"""`display` can only be called on an ipython context. Are you in a notebook?
301
+ - To render an experiment to HTML, use `experiment.to_html(file_name)` or `html_page = experiment.to_html()`
302
+ - To render an experiment in Streamlit, use `experiment.display_st`""")
303
+ from .ipython import display_exp # pylint: disable=cyclic-import
304
+
305
+ self.validate()
306
+ return display_exp(self, force_full_width=force_full_width, store_state_url=store_state_key, **kwargs)
307
+
308
+ # pylint: disable=function-redefined
309
+ @tp.overload
310
+ def display_st(self, *, ret: str, key: str | None = None) -> tp.Any:
311
+ pass
312
+
313
+ @tp.overload
314
+ def display_st(self, *, ret: list[str], key: str | None = None) -> list[tp.Any]:
315
+ pass
316
+
317
+ @tp.overload
318
+ def display_st(self, *, key: str | None = None) -> None:
319
+ pass
320
+
321
+ def display_st(self, *, ret: str | list[str] | None = None, key: str | None = None) -> tp.Any:
322
+ """
323
+ Displays an experiment in a Streamlit app - see :ref:`tutoStreamlit`
324
+
325
+ This function can be pretty slow, see :ref:`tutoStreamlitCache` to learn how to make it faster.
326
+
327
+ :param key: Unique key for the streamlit component. It is strongly recommended to give some unique string.
328
+ :param ret: Specify what HiPlot should return.
329
+ :returns: Return value depends on ``ret``
330
+
331
+ :Example:
332
+
333
+ .. code-block:: python
334
+
335
+ exp.display_st(key="hiplot1")
336
+ brush_extents = exp.display_st(key="hiplot2", ret="brush_extents")
337
+ brush_extents, selected_uids = exp.display_st(key="hiplot3", ret=["brush_extents", "selected_uids"])
338
+
339
+ """
340
+ return self.to_streamlit(ret=ret, key=key).display()
341
+
342
+ # pylint: enable=function-redefined
343
+
344
+ def to_streamlit(
345
+ self, key: str | None = None, ret: str | list[str] | None = None
346
+ ) -> "ExperimentStreamlitComponent":
347
+ """
348
+ Streamlit only:
349
+ creates a copy of the Experiment that you can cache,
350
+ which only exposes the `display_st` method - see :ref:`tutoStreamlitCache`
351
+
352
+ :param key: Unique key for the streamlit component.
353
+ :param ret: Specify what HiPlot should return.
354
+ :returns: A `component` object that be rendered with `component.display()`
355
+
356
+ :Example:
357
+
358
+ .. code-block:: python
359
+
360
+ import streamlit as st
361
+ import hiplot as hip
362
+
363
+ @st.cache
364
+ def get_experiment():
365
+ # Create your hiplot experiment as usual
366
+ big_exp = hip.Experiment.from_iterable(...)
367
+ # ... and cache the component
368
+ return big_exp.to_streamlit(key="hipl", ret=["brush_extents", "selected_uids"])
369
+
370
+ exp = get_experiment() # This will be cached the second time
371
+ brush_extents, selected_uids = exp.display()
372
+
373
+ """
374
+
375
+ from . import streamlit_helpers # pylint: disable=cyclic-import
376
+
377
+ if not streamlit_helpers._StreamlitHelpers.is_running_within_streamlit():
378
+ if _is_running_ipython():
379
+ raise RuntimeError(r"""`experiment.to_streamlit` can only be called in a streamlit script.
380
+ It appears that you are trying to create a HiPlot visualization in ipython:
381
+ you should use `display` instead of `to_streamlit`""")
382
+ raise RuntimeError(r"""`experiment.to_streamlit` can only be called in a streamlit script.
383
+ To render an experiment to HTML, use `experiment.to_html(file_name)` or `html_page = experiment.to_html()`""")
384
+ return streamlit_helpers.ExperimentStreamlitComponent(self, key=key, ret=ret)
385
+
386
+ def to_html(self, file: Path | str | TextWriterIO | None = None, **kwargs: tp.Any) -> str:
387
+ """
388
+ Returns the content of a standalone .html file that displays this experiment
389
+ without any dependency to HiPlot server or static files.
390
+
391
+ :param file: Path/handle to a file to write (optional)
392
+ :returns: A standalone HTML code to display this Experiment.
393
+ """
394
+ from .render import make_experiment_standalone_page
395
+
396
+ self.validate()
397
+ html = make_experiment_standalone_page(options={**kwargs, "experiment": self._payload()}, inline_static=True)
398
+ if file is not None:
399
+ if isinstance(file, (Path, str)):
400
+ Path(file).write_text(html, encoding="utf-8")
401
+ else:
402
+ file.write(html)
403
+ return html
404
+
405
+ def to_csv(self, file: Path | str | TextWriterIO) -> None:
406
+ """
407
+ Dumps this Experiment as a .csv file.
408
+ Information about display_data, parameters definition will be lost.
409
+
410
+ :param file: Path/handle to a file to write
411
+ """
412
+ if isinstance(file, (Path, str)):
413
+ with Path(file).open("w", encoding="utf-8") as csvfile:
414
+ return self._to_csv(csvfile)
415
+ else:
416
+ return self._to_csv(file)
417
+
418
+ def _to_csv(self, fh: TextWriterIO) -> None:
419
+ fieldnames = {f for dp in self.datapoints for f in dp.values}
420
+ writer = csv.DictWriter(fh, fieldnames=["uid", "from_uid", *sorted(fieldnames)])
421
+ writer.writeheader()
422
+ for dp in self.datapoints:
423
+ writer.writerow(
424
+ {
425
+ **dp.values,
426
+ "uid": dp.uid,
427
+ "from_uid": dp.from_uid,
428
+ }
429
+ )
430
+
431
+ def _settings_asdict(self) -> dict[str, tp.Any]:
432
+ return {
433
+ "parameters_definition": {k: v._asdict() for k, v in self.parameters_definition.items()},
434
+ "colormap": self.colormap,
435
+ "colorby": self.colorby,
436
+ "weightcolumn": self.weightcolumn,
437
+ "display_data": self._display_data,
438
+ "enabled_displays": self.enabledDisplays,
439
+ }
440
+
441
+ def _payload(self) -> dict[str, tp.Any]:
442
+ """What the frontend receives: the settings plus column-major datapoints in strict JSON."""
443
+ from .compress import compress_columnar
444
+
445
+ return {**self._settings_asdict(), "datapoints_columnar": compress_columnar(self.datapoints)}
446
+
447
+ def _asdict(self) -> dict[str, tp.Any]:
448
+ data = self._settings_asdict()
449
+ if self._compress:
450
+ from .compress import compress
451
+
452
+ data["datapoints_compressed"] = compress(self.datapoints)
453
+ else:
454
+ data["datapoints"] = [d._asdict() for d in self.datapoints]
455
+ return data
456
+
457
+ def remove_missing_parents(self) -> "Experiment":
458
+ """
459
+ Sets :attr:`hiplot.Datapoint.from_uid` to None when set to a non-existing Datapoint.
460
+ """
461
+ existing_dp = {dp.uid for dp in self.datapoints}
462
+ for dp in self.datapoints:
463
+ if dp.from_uid not in existing_dp:
464
+ dp.from_uid = None
465
+ return self
466
+
467
+ def display_data(self, plugin: str) -> dict[str, tp.Any]:
468
+ """
469
+ Retrieve data dictionary for a plugin, which can be modified.
470
+
471
+ :param plugin: Name of the plugin
472
+
473
+ :Example:
474
+
475
+ .. code-block:: python
476
+
477
+ exp.display_data(hip.Displays.XY).update({
478
+ "axis_x": "time",
479
+ "axis_y": "loss"
480
+ })
481
+
482
+ """
483
+ return self._display_data.setdefault(plugin, {})
484
+
485
+ @staticmethod
486
+ def from_iterable(it: tp.Iterable[dict[str, tp.Any]]) -> "Experiment":
487
+ """
488
+ Creates a HiPlot experiment from an iterable/list of dictionnaries.
489
+ This is the easiest way to generate an `hiplot.Experiment` object.
490
+
491
+ :param it: A list (or iterable) of dictionnaries
492
+
493
+ :Example:
494
+
495
+ >>> import hiplot as hip
496
+ >>> hip.Experiment.from_iterable([{"p": "a"}, {"p": "b"}])
497
+ <hiplot.experiment.Experiment object at 0x7f0f2e13c590>
498
+
499
+ """
500
+ return Experiment(
501
+ datapoints=[
502
+ Datapoint(
503
+ uid=str(row.get("uid", k)),
504
+ from_uid=row.get("from_uid") if row.get("from_uid") != "" else None,
505
+ values={mk: mv for mk, mv in row.items() if mk not in ["uid", "from_uid"]},
506
+ )
507
+ for k, row in enumerate(it)
508
+ ]
509
+ )
510
+
511
+ @staticmethod
512
+ def from_csv(file: Path | str | tp.IO[str]) -> "Experiment":
513
+ """
514
+ Creates a HiPlot experiment from a CSV file. Columns whose every cell spells
515
+ a number (``nan``, ``inf`` and blank cells included) hold numbers; the
516
+ others hold the text of their cells.
517
+
518
+ :param file: CSV file path or text handle
519
+ """
520
+ from .csvvalues import typed_rows
521
+
522
+ if isinstance(file, (Path, str)):
523
+ with Path(file).open(encoding="utf-8", newline="") as csvfile:
524
+ return Experiment.from_csv(csvfile)
525
+ return Experiment.from_iterable(typed_rows(csv.DictReader(file)))
526
+
527
+ @staticmethod
528
+ def from_dataframe(
529
+ dataframe: "pd.DataFrame",
530
+ ) -> "Experiment": # No type hint to avoid having pandas as an additional dependency
531
+ """
532
+ Creates a HiPlot experiment from a pandas DataFrame.
533
+
534
+ :param dataframe: Pandas DataFrame
535
+ """
536
+ # Check if from_uid and uid is both in columns
537
+ if {"from_uid", "uid"}.issubset(dataframe.columns) and dataframe["from_uid"].isnull().values.any():
538
+ # NaN forces from_uid to float, so an integer uid would never match its child's from_uid.
539
+ # Give uid the same dtype, then compare both as strings; missing values become "".
540
+ uid_missing = dataframe["uid"].isnull()
541
+ parent_missing = dataframe["from_uid"].isnull()
542
+ dataframe["uid"] = dataframe["uid"].astype(dataframe["from_uid"].dtypes).astype(str).mask(uid_missing, "")
543
+ dataframe["from_uid"] = dataframe["from_uid"].astype(str).mask(parent_missing, "")
544
+
545
+ experiment = Experiment.from_iterable(dataframe.to_dict(orient="records"))
546
+
547
+ # Restore columns order
548
+ experiment.display_data(Displays.PARALLEL_PLOT)["order"] = list(dataframe.columns)
549
+ experiment.display_data(Displays.TABLE)["order"] = list(dataframe.columns)
550
+
551
+ return experiment
552
+
553
+ @staticmethod
554
+ def from_optuna(
555
+ study: "optuna.study.Study",
556
+ ) -> "Experiment": # No type hint to avoid having optuna as an additional dependency
557
+ """
558
+ Creates a HiPlot experiment from a Optuna Study.
559
+
560
+ :param study: Optuna Study
561
+ """
562
+
563
+ # Create a list of dictionary objects using study trials
564
+ # All parameters are taken using params.copy()
565
+ # pylint: disable=redefined-outer-name
566
+ import optuna
567
+
568
+ hyper_opt_data = []
569
+ for each_trial in study.get_trials(states=(optuna.trial.TrialState.COMPLETE,)):
570
+ trial_params = {}
571
+ # This checks if the trial was fully completed
572
+ # the value will be None if the trial was interrupted halfway (e.g. via KeyboardInterrupt)
573
+ if not each_trial.values:
574
+ continue
575
+ num_objectives = len(each_trial.values)
576
+
577
+ if num_objectives == 1:
578
+ # name = value, as it could be RMSE / accuracy, or any value that the user selects for tuning
579
+ trial_params["value"] = each_trial.value
580
+ else:
581
+ for objective_id, value in enumerate(each_trial.values):
582
+ trial_params[f"value_{objective_id}"] = value
583
+
584
+ trial_params["uid"] = each_trial.number
585
+ trial_params.update(each_trial.params.copy())
586
+ hyper_opt_data.append(trial_params)
587
+ return Experiment.from_iterable(hyper_opt_data)
588
+
589
+ @staticmethod
590
+ def merge(xp_dict: dict[str, "Experiment"]) -> "Experiment":
591
+ """
592
+ Merge several experiments into a single one
593
+ """
594
+ xp = Experiment(datapoints=[])
595
+ assert xp.parameters_definition is not None # for mypy
596
+ for k, subxp in xp_dict.items():
597
+ assert subxp is not None, k
598
+ xp.datapoints += [
599
+ Datapoint(
600
+ uid=f"{k}_{d.uid}",
601
+ from_uid=f"{k}_{d.from_uid}" if d.from_uid is not None else None,
602
+ values={**d.values, "exp": k},
603
+ )
604
+ for d in subxp.datapoints
605
+ ]
606
+ if subxp.parameters_definition is not None:
607
+ xp.parameters_definition.update(subxp.parameters_definition)
608
+ for d, v in subxp._display_data.items():
609
+ xp.display_data(d).update(v)
610
+ return xp
611
+
612
+
613
+ class ExperimentFetcherDoesntApply(Exception):
614
+ pass
615
+
616
+
617
+ ExperimentFetcher = tp.Callable[[str], Experiment]
618
+
619
+
620
+ class ExperimentDisplayed(metaclass=ABCMeta):
621
+ """
622
+ Class that allows to communicate with a displayed HiPlot visualization in a Jupyter notebook.
623
+ Read more in :ref:`tutoNotebookDisplayedExperiment`
624
+ """
625
+
626
+ @abstractmethod
627
+ def get_selected(self) -> list[Datapoint]:
628
+ """
629
+ Returns a list of currently rendered datapoints in the parallel plot
630
+ """
631
+
632
+ @abstractmethod
633
+ def get_brush_extents(self) -> dict[str, dict[str, tp.Any]]:
634
+ """
635
+ Returns a dictionary, where keys corresponds to columns currently brushed in parallel plot,
636
+ and values contain information about the current brush.
637
+ """