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/__init__.py +38 -0
- hiplot/__main__.py +10 -0
- hiplot/compress.py +70 -0
- hiplot/csvvalues.py +83 -0
- hiplot/experiment.py +637 -0
- hiplot/fetchers.py +322 -0
- hiplot/fetchers_demo.py +459 -0
- hiplot/ipython.py +217 -0
- hiplot/pkginfo.py +15 -0
- hiplot/py.typed +0 -0
- hiplot/render.py +141 -0
- hiplot/server.py +76 -0
- hiplot/static/built/hiplot.bundle.js +1224 -0
- hiplot/static/built/streamlit_component/hiplot_streamlit.bundle.js +1220 -0
- hiplot/static/built/streamlit_component/index.html +16 -0
- hiplot/static/icon.png +0 -0
- hiplot/streamlit_helpers.py +127 -0
- hiplot/templates/index.html +68 -0
- hiplot_plus-0.1.0.dist-info/METADATA +197 -0
- hiplot_plus-0.1.0.dist-info/RECORD +24 -0
- hiplot_plus-0.1.0.dist-info/WHEEL +4 -0
- hiplot_plus-0.1.0.dist-info/entry_points.txt +4 -0
- hiplot_plus-0.1.0.dist-info/licenses/LICENSE +21 -0
- hiplot_plus-0.1.0.dist-info/licenses/NOTICE +207 -0
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
|
+
"""
|