rlshiftlab 0.2.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.
- rlshiftlab/__init__.py +23 -0
- rlshiftlab/comparisons.py +301 -0
- rlshiftlab/exports.py +98 -0
- rlshiftlab/groups.py +440 -0
- rlshiftlab/py.typed +0 -0
- rlshiftlab/records.py +50 -0
- rlshiftlab/report.py +273 -0
- rlshiftlab/results.py +200 -0
- rlshiftlab/runner.py +458 -0
- rlshiftlab/shifts.py +170 -0
- rlshiftlab/stats.py +208 -0
- rlshiftlab-0.2.0.dist-info/METADATA +341 -0
- rlshiftlab-0.2.0.dist-info/RECORD +15 -0
- rlshiftlab-0.2.0.dist-info/WHEEL +4 -0
- rlshiftlab-0.2.0.dist-info/licenses/LICENSE +21 -0
rlshiftlab/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Stress-test frozen reinforcement learning policies under controlled shifts."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.2.0"
|
|
4
|
+
|
|
5
|
+
from rlshiftlab.comparisons import ComparisonResults
|
|
6
|
+
from rlshiftlab.groups import GroupResults
|
|
7
|
+
from rlshiftlab.results import Results
|
|
8
|
+
from rlshiftlab.runner import EpisodeRecord, EpisodeResult, Suite, Transition
|
|
9
|
+
from rlshiftlab.shifts import ActionSlip, ObservationNoise, StickyActions
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"ActionSlip",
|
|
13
|
+
"ComparisonResults",
|
|
14
|
+
"EpisodeRecord",
|
|
15
|
+
"EpisodeResult",
|
|
16
|
+
"GroupResults",
|
|
17
|
+
"ObservationNoise",
|
|
18
|
+
"Results",
|
|
19
|
+
"StickyActions",
|
|
20
|
+
"Suite",
|
|
21
|
+
"Transition",
|
|
22
|
+
"__version__",
|
|
23
|
+
]
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"""Direct differences between frozen policies on matched evaluation episodes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import zlib
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from math import ceil
|
|
10
|
+
from numbers import Integral
|
|
11
|
+
from os import PathLike
|
|
12
|
+
from typing import TYPE_CHECKING, Any
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
from matplotlib.backends.backend_agg import FigureCanvasAgg
|
|
16
|
+
from matplotlib.figure import Figure
|
|
17
|
+
|
|
18
|
+
from rlshiftlab.records import EpisodeRecord
|
|
19
|
+
from rlshiftlab.stats import crossed_independent_group_diff_ci, paired_diff_ci
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from rlshiftlab.groups import GroupResults
|
|
23
|
+
from rlshiftlab.results import Results
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class ComparisonResults:
|
|
28
|
+
"""Pointwise differences; positive means the left metric is larger."""
|
|
29
|
+
|
|
30
|
+
rows: list[dict[str, Any]]
|
|
31
|
+
meta: dict[str, Any]
|
|
32
|
+
|
|
33
|
+
def to_csv(self, path: str | PathLike[str]) -> None:
|
|
34
|
+
"""Write condition contrasts with a stable comparison-only schema."""
|
|
35
|
+
from rlshiftlab.exports import COMPARISON_COLUMNS, write_rows
|
|
36
|
+
|
|
37
|
+
write_rows(path, self.rows, COMPARISON_COLUMNS)
|
|
38
|
+
|
|
39
|
+
def summary(self) -> str:
|
|
40
|
+
"""Summarize pointwise left-minus-right differences by condition."""
|
|
41
|
+
left = str(self.meta.get("left", "left"))
|
|
42
|
+
right = str(self.meta.get("right", "right"))
|
|
43
|
+
metric = str(self.meta.get("metric", "return"))
|
|
44
|
+
lines = [
|
|
45
|
+
f"{left} - {right}: {metric} (positive means a higher measured value for {left})",
|
|
46
|
+
"shift level mean difference pointwise 95% CI",
|
|
47
|
+
]
|
|
48
|
+
for row in self.rows:
|
|
49
|
+
lines.append(
|
|
50
|
+
f"{row['shift']} {float(row['level']):g} {float(row['mean_diff']):.3f} "
|
|
51
|
+
f"[{float(row['ci_low']):.3f}, {float(row['ci_high']):.3f}]"
|
|
52
|
+
)
|
|
53
|
+
return "\n".join(lines)
|
|
54
|
+
|
|
55
|
+
def plot(self, path: str | PathLike[str] | None = None) -> Figure:
|
|
56
|
+
"""Plot pointwise contrast intervals with a zero reference per shift."""
|
|
57
|
+
shifts = list(self.meta.get("shifts", {}))
|
|
58
|
+
if not shifts:
|
|
59
|
+
shifts = list(dict.fromkeys(row["shift"] for row in self.rows if row["shift"] != "nominal"))
|
|
60
|
+
if not shifts:
|
|
61
|
+
raise ValueError("comparison contains no shifted conditions to plot")
|
|
62
|
+
nominal = next((row for row in self.rows if row["shift"] == "nominal"), None)
|
|
63
|
+
if nominal is None:
|
|
64
|
+
raise ValueError("comparison is missing its nominal condition")
|
|
65
|
+
columns = min(3, len(shifts))
|
|
66
|
+
figure = Figure(figsize=(5.5 * columns, 4 * ceil(len(shifts) / columns)))
|
|
67
|
+
FigureCanvasAgg(figure)
|
|
68
|
+
left = str(self.meta.get("left", "left"))
|
|
69
|
+
right = str(self.meta.get("right", "right"))
|
|
70
|
+
metric = str(self.meta.get("metric", "return"))
|
|
71
|
+
for index, shift in enumerate(shifts, start=1):
|
|
72
|
+
axis = figure.add_subplot(ceil(len(shifts) / columns), columns, index)
|
|
73
|
+
shifted = sorted(
|
|
74
|
+
(row for row in self.rows if row["shift"] == shift),
|
|
75
|
+
key=lambda row: float(row["level"]),
|
|
76
|
+
)
|
|
77
|
+
points = [nominal, *shifted]
|
|
78
|
+
x = [float(point["level"]) for point in points]
|
|
79
|
+
mean = [float(point["mean_diff"]) for point in points]
|
|
80
|
+
low = [float(point["ci_low"]) for point in points]
|
|
81
|
+
high = [float(point["ci_high"]) for point in points]
|
|
82
|
+
axis.axhline(0.0, color="black", linewidth=1, linestyle="--", alpha=0.6)
|
|
83
|
+
line = axis.plot(x, mean, marker="o", label=f"{left} - {right}")[0]
|
|
84
|
+
axis.fill_between(x, low, high, color=line.get_color(), alpha=0.18)
|
|
85
|
+
axis.set_title(shift.replace("_", " "))
|
|
86
|
+
axis.set_xlabel("shift strength")
|
|
87
|
+
axis.set_ylabel(f"{metric}: {left} - {right} (pointwise 95% CI)")
|
|
88
|
+
axis.grid(alpha=0.2)
|
|
89
|
+
axis.legend()
|
|
90
|
+
figure.tight_layout()
|
|
91
|
+
if path is not None:
|
|
92
|
+
figure.savefig(path, dpi=150)
|
|
93
|
+
return figure
|
|
94
|
+
|
|
95
|
+
def to_html(self, path: str | PathLike[str]) -> None:
|
|
96
|
+
"""Write a standalone HTML comparison report."""
|
|
97
|
+
from rlshiftlab.report import write_html
|
|
98
|
+
|
|
99
|
+
write_html(self, path)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _setup(
|
|
103
|
+
results: Results | GroupResults,
|
|
104
|
+
left: str,
|
|
105
|
+
right: str,
|
|
106
|
+
metric: str,
|
|
107
|
+
grouped: bool,
|
|
108
|
+
) -> tuple[list[tuple[str, float]], list[int], dict[str, list[str | None]]]:
|
|
109
|
+
if not isinstance(left, str) or not left or not isinstance(right, str) or not right:
|
|
110
|
+
raise ValueError("left and right must be nonempty policy or group names")
|
|
111
|
+
if left == right:
|
|
112
|
+
raise ValueError("left and right must be distinct")
|
|
113
|
+
labels = results.meta.get("groups" if grouped else "policies")
|
|
114
|
+
if grouped:
|
|
115
|
+
if not isinstance(labels, Mapping):
|
|
116
|
+
raise ValueError("group metadata is required for a group comparison")
|
|
117
|
+
if left not in labels or right not in labels:
|
|
118
|
+
raise ValueError("left and right must be existing groups")
|
|
119
|
+
members = {name: list(labels[name]) for name in (left, right)}
|
|
120
|
+
if any(len(ids) < 2 or len(ids) != len(set(ids)) for ids in members.values()):
|
|
121
|
+
raise ValueError("groups must declare at least two distinct members")
|
|
122
|
+
else:
|
|
123
|
+
if not isinstance(labels, (list, tuple)) or left not in labels or right not in labels:
|
|
124
|
+
raise ValueError("left and right must be existing policies")
|
|
125
|
+
members = {left: [None], right: [None]}
|
|
126
|
+
|
|
127
|
+
configured_metrics = {
|
|
128
|
+
"return", "length", "success", *results.meta.get("step_metrics", []),
|
|
129
|
+
*results.meta.get("episode_metrics", []),
|
|
130
|
+
}
|
|
131
|
+
if not isinstance(metric, str) or metric not in configured_metrics:
|
|
132
|
+
raise ValueError(f"metric {metric!r} is not configured")
|
|
133
|
+
shifts = results.meta.get("shifts")
|
|
134
|
+
if not isinstance(shifts, Mapping):
|
|
135
|
+
raise TypeError("shift metadata must be a mapping for a comparison")
|
|
136
|
+
conditions = [("nominal", 0.0)] + [
|
|
137
|
+
(name, float(level)) for name, levels in shifts.items() for level in levels
|
|
138
|
+
]
|
|
139
|
+
if len(conditions) != len(set(conditions)):
|
|
140
|
+
raise ValueError("condition metadata contains duplicate shift levels")
|
|
141
|
+
count = results.meta.get("n_episodes")
|
|
142
|
+
seed = results.meta.get("seed")
|
|
143
|
+
if (
|
|
144
|
+
isinstance(count, bool) or not isinstance(count, Integral) or count < 2
|
|
145
|
+
or isinstance(seed, bool) or not isinstance(seed, Integral) or seed < 0
|
|
146
|
+
):
|
|
147
|
+
raise ValueError("comparison requires valid evaluation seed and episode count")
|
|
148
|
+
return conditions, [int(seed + index) for index in range(count)], members
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _numeric_value(record: EpisodeRecord, metric: str) -> float:
|
|
152
|
+
if metric == "return":
|
|
153
|
+
value = record.episode.episode_return
|
|
154
|
+
elif metric == "length":
|
|
155
|
+
value = record.episode.length
|
|
156
|
+
elif metric == "success":
|
|
157
|
+
value = record.success
|
|
158
|
+
if value is None:
|
|
159
|
+
raise ValueError("success metric requires recorded success values")
|
|
160
|
+
else:
|
|
161
|
+
if metric not in record.episode.metrics:
|
|
162
|
+
raise ValueError(f"record lacks configured metric {metric!r}")
|
|
163
|
+
value = record.episode.metrics[metric]
|
|
164
|
+
try:
|
|
165
|
+
array = np.asarray(value)
|
|
166
|
+
if array.ndim != 0 or array.dtype.kind not in "bifu":
|
|
167
|
+
raise ValueError("not a numeric scalar")
|
|
168
|
+
number = float(array)
|
|
169
|
+
except (TypeError, ValueError, OverflowError) as exc:
|
|
170
|
+
raise ValueError(f"record has invalid {metric!r} value") from exc
|
|
171
|
+
if not np.isfinite(number):
|
|
172
|
+
raise ValueError(f"record has nonfinite {metric!r} value")
|
|
173
|
+
return number
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _panels(
|
|
177
|
+
records: list[EpisodeRecord],
|
|
178
|
+
entities: dict[str, list[str | None]],
|
|
179
|
+
conditions: list[tuple[str, float]],
|
|
180
|
+
episode_seeds: list[int],
|
|
181
|
+
metric: str,
|
|
182
|
+
) -> dict[tuple[str, str, float], np.ndarray]:
|
|
183
|
+
"""Validate every expected key, then align cells by declared member and seed."""
|
|
184
|
+
expected_conditions = set(conditions)
|
|
185
|
+
keyed: dict[tuple[str, str | None, str, float, int], float] = {}
|
|
186
|
+
for record in records:
|
|
187
|
+
if record.policy not in entities:
|
|
188
|
+
continue
|
|
189
|
+
if not isinstance(record.shift, str):
|
|
190
|
+
raise TypeError(f"invalid condition shift for {record.policy!r}")
|
|
191
|
+
if (
|
|
192
|
+
isinstance(record.level, bool)
|
|
193
|
+
or not isinstance(record.level, (int, float))
|
|
194
|
+
or not np.isfinite(record.level)
|
|
195
|
+
):
|
|
196
|
+
raise ValueError(f"invalid condition level for {record.policy!r}")
|
|
197
|
+
condition = (record.shift, record.level)
|
|
198
|
+
if condition not in expected_conditions:
|
|
199
|
+
raise ValueError(f"unexpected condition for {record.policy!r}: {condition!r}")
|
|
200
|
+
if record.member not in entities[record.policy]:
|
|
201
|
+
raise ValueError(f"unexpected member {record.member!r} for {record.policy!r}")
|
|
202
|
+
index = record.episode_index
|
|
203
|
+
if isinstance(index, bool) or not isinstance(index, Integral) or not 0 <= index < len(episode_seeds):
|
|
204
|
+
raise ValueError(f"invalid episode index for {record.policy!r}")
|
|
205
|
+
if (
|
|
206
|
+
isinstance(record.episode_seed, bool)
|
|
207
|
+
or not isinstance(record.episode_seed, Integral)
|
|
208
|
+
or record.episode_seed != episode_seeds[index]
|
|
209
|
+
):
|
|
210
|
+
raise ValueError(f"mismatched episode seed for {record.policy!r}")
|
|
211
|
+
key = (record.policy, record.member, record.shift, record.level, int(index))
|
|
212
|
+
if key in keyed:
|
|
213
|
+
raise ValueError(f"duplicate condition episode key for {record.policy!r}")
|
|
214
|
+
keyed[key] = _numeric_value(record, metric)
|
|
215
|
+
|
|
216
|
+
expected_count = sum(len(ids) for ids in entities.values()) * len(conditions) * len(episode_seeds)
|
|
217
|
+
if len(keyed) != expected_count:
|
|
218
|
+
raise ValueError("incomplete condition and evaluation episode grid")
|
|
219
|
+
|
|
220
|
+
panels: dict[tuple[str, str, float], np.ndarray] = {}
|
|
221
|
+
for name, members in entities.items():
|
|
222
|
+
for shift, level in conditions:
|
|
223
|
+
try:
|
|
224
|
+
panel = [
|
|
225
|
+
[keyed[(name, member, shift, level, index)] for index in range(len(episode_seeds))]
|
|
226
|
+
for member in members
|
|
227
|
+
]
|
|
228
|
+
except KeyError as exc:
|
|
229
|
+
raise ValueError("incomplete condition and evaluation episode grid") from exc
|
|
230
|
+
panels[(name, shift, level)] = np.asarray(panel, dtype=float)
|
|
231
|
+
return panels
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _comparison_seed(
|
|
235
|
+
suite_seed: int, left: str, right: str, shift: str, level: float, metric: str
|
|
236
|
+
) -> int:
|
|
237
|
+
key = json.dumps([left, right, shift, level, metric], ensure_ascii=False, separators=(",", ":"))
|
|
238
|
+
rng = np.random.default_rng([suite_seed, zlib.crc32(key.encode("utf-8"))])
|
|
239
|
+
return int(rng.integers(0, 2**32))
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def compare_results(
|
|
243
|
+
results: Results | GroupResults,
|
|
244
|
+
left: str,
|
|
245
|
+
right: str,
|
|
246
|
+
metric: str = "return",
|
|
247
|
+
*,
|
|
248
|
+
grouped: bool = False,
|
|
249
|
+
) -> ComparisonResults:
|
|
250
|
+
"""Compare complete raw panels with paired evaluation episode keys."""
|
|
251
|
+
conditions, episode_seeds, members = _setup(results, left, right, metric, grouped)
|
|
252
|
+
panels = _panels(results.records, members, conditions, episode_seeds, metric)
|
|
253
|
+
canonical_left, canonical_right = sorted((left, right))
|
|
254
|
+
reverse = left != canonical_left
|
|
255
|
+
method = "crossed_member_episode_bootstrap" if grouped else "paired_episode_bootstrap"
|
|
256
|
+
rows: list[dict[str, Any]] = []
|
|
257
|
+
for shift, level in conditions:
|
|
258
|
+
first = panels[(canonical_left, shift, level)]
|
|
259
|
+
second = panels[(canonical_right, shift, level)]
|
|
260
|
+
seed = _comparison_seed(
|
|
261
|
+
int(results.meta["seed"]), canonical_left, canonical_right, shift, level, metric
|
|
262
|
+
)
|
|
263
|
+
if grouped:
|
|
264
|
+
mean, low, high = crossed_independent_group_diff_ci(first, second, seed=seed)
|
|
265
|
+
else:
|
|
266
|
+
mean, low, high = paired_diff_ci(first[0], second[0], seed=seed)
|
|
267
|
+
if reverse:
|
|
268
|
+
mean, low, high = -mean, -high, -low
|
|
269
|
+
rows.append(
|
|
270
|
+
{
|
|
271
|
+
"left": left,
|
|
272
|
+
"right": right,
|
|
273
|
+
"shift": shift,
|
|
274
|
+
"level": level,
|
|
275
|
+
"metric": metric,
|
|
276
|
+
"n_episodes": len(episode_seeds),
|
|
277
|
+
"n_members_left": len(members[left]) if grouped else None,
|
|
278
|
+
"n_members_right": len(members[right]) if grouped else None,
|
|
279
|
+
"method": method,
|
|
280
|
+
"mean_diff": mean,
|
|
281
|
+
"ci_low": low,
|
|
282
|
+
"ci_high": high,
|
|
283
|
+
}
|
|
284
|
+
)
|
|
285
|
+
return ComparisonResults(
|
|
286
|
+
rows=rows,
|
|
287
|
+
meta={
|
|
288
|
+
**results.meta,
|
|
289
|
+
"left": left,
|
|
290
|
+
"right": right,
|
|
291
|
+
"metric": metric,
|
|
292
|
+
"comparison_method": method,
|
|
293
|
+
"ci_confidence": 0.95,
|
|
294
|
+
"ci_replicates": 2000,
|
|
295
|
+
"comparison_direction": "left minus right; positive means left has a higher metric",
|
|
296
|
+
"ci_scope": "pointwise, not simultaneous or multiple-comparison adjusted",
|
|
297
|
+
},
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
__all__ = ["ComparisonResults", "compare_results"]
|
rlshiftlab/exports.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Stable CSV schemas for v0.2 evaluation and audit exports."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import csv
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
from os import PathLike
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from rlshiftlab.records import EpisodeRecord
|
|
12
|
+
from rlshiftlab.results import CSV_COLUMNS
|
|
13
|
+
|
|
14
|
+
METRIC_COLUMNS = (
|
|
15
|
+
"policy", "metric", "shift", "level", "n_episodes", "mean", "ci_low", "ci_high",
|
|
16
|
+
"drop", "drop_ci_low", "drop_ci_high",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
GROUP_COLUMNS = (
|
|
20
|
+
"group", "shift", "level", "n_members", "n_episodes_per_member", "total_episodes",
|
|
21
|
+
"mean_return", "return_ci_low", "return_ci_high", "member_std", "member_min",
|
|
22
|
+
"member_max", "drop", "drop_ci_low", "drop_ci_high", "mean_length",
|
|
23
|
+
"success_rate", "success_ci_low", "success_ci_high",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
GROUP_METRIC_COLUMNS = (
|
|
27
|
+
"group", "metric", "shift", "level", "n_members", "n_episodes_per_member",
|
|
28
|
+
"total_episodes", "mean", "ci_low", "ci_high", "member_std", "member_min",
|
|
29
|
+
"member_max", "drop", "drop_ci_low", "drop_ci_high",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
MEMBER_COLUMNS = ("group", "member", *CSV_COLUMNS[1:])
|
|
33
|
+
|
|
34
|
+
EPISODE_COLUMNS = (
|
|
35
|
+
"policy", "member", "shift", "level", "episode_index", "episode_seed",
|
|
36
|
+
"episode_return", "length", "terminated", "truncated", "success",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
COMPARISON_COLUMNS = (
|
|
40
|
+
"left", "right", "shift", "level", "metric", "n_episodes", "n_members_left",
|
|
41
|
+
"n_members_right", "method", "mean_diff", "ci_low", "ci_high",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def write_rows(
|
|
46
|
+
path: str | PathLike[str], rows: Sequence[dict[str, Any]], columns: Sequence[str]
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Write one deterministic UTF-8 CSV with a fixed field order."""
|
|
49
|
+
with Path(path).open("w", encoding="utf-8", newline="") as output:
|
|
50
|
+
writer = csv.DictWriter(output, fieldnames=columns, lineterminator="\n")
|
|
51
|
+
writer.writeheader()
|
|
52
|
+
for row in rows:
|
|
53
|
+
writer.writerow(
|
|
54
|
+
{
|
|
55
|
+
column: (
|
|
56
|
+
"" if row.get(column) is None else repr(row[column])
|
|
57
|
+
if isinstance(row[column], float)
|
|
58
|
+
else row[column]
|
|
59
|
+
)
|
|
60
|
+
for column in columns
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def episode_columns(meta: dict[str, Any]) -> tuple[str, ...]:
|
|
66
|
+
"""Append custom metric names in their configured order."""
|
|
67
|
+
names = [*meta.get("step_metrics", ()), *meta.get("episode_metrics", ())]
|
|
68
|
+
return (*EPISODE_COLUMNS, *(f"metric:{name}" for name in names))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def episode_rows(records: Sequence[EpisodeRecord], meta: dict[str, Any]) -> list[dict[str, Any]]:
|
|
72
|
+
"""Convert episode records to scalar audit rows without Gym info or observations."""
|
|
73
|
+
names = [*meta.get("step_metrics", ()), *meta.get("episode_metrics", ())]
|
|
74
|
+
rows = []
|
|
75
|
+
for record in records:
|
|
76
|
+
episode = record.episode
|
|
77
|
+
row = {
|
|
78
|
+
"policy": record.policy,
|
|
79
|
+
"member": record.member,
|
|
80
|
+
"shift": record.shift,
|
|
81
|
+
"level": record.level,
|
|
82
|
+
"episode_index": record.episode_index,
|
|
83
|
+
"episode_seed": record.episode_seed,
|
|
84
|
+
"episode_return": episode.episode_return,
|
|
85
|
+
"length": episode.length,
|
|
86
|
+
"terminated": episode.terminated,
|
|
87
|
+
"truncated": episode.truncated,
|
|
88
|
+
"success": record.success,
|
|
89
|
+
}
|
|
90
|
+
row.update({f"metric:{name}": episode.metrics[name] for name in names})
|
|
91
|
+
rows.append(row)
|
|
92
|
+
return rows
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
__all__ = [
|
|
96
|
+
"COMPARISON_COLUMNS", "EPISODE_COLUMNS", "GROUP_COLUMNS", "GROUP_METRIC_COLUMNS",
|
|
97
|
+
"MEMBER_COLUMNS", "METRIC_COLUMNS", "episode_columns", "episode_rows", "write_rows",
|
|
98
|
+
]
|