Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .test-checker/ledger.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -2079,3 +2079,16 @@
{"ts":"2026-10-02T20:30:43.618Z","run":"r-20261002-162723-b8b5","type":"classified","exec":1,"counts":{"sgtlearn-ensemble-random-sgforest-regressor-py--randomsgforestregressor-check-x-y":{"accepted":47}},"queued":0}
{"ts":"2026-10-02T20:30:43.673Z","run":"r-20261002-162723-b8b5","type":"run_reported","likely_bugs":0,"needs_decision":0}
{"ts":"2026-10-02T20:30:43.740Z","run":"r-20261002-162723-b8b5","type":"promoted","files":["tests/regression/test_sgtlearn_ensemble_random_sgforest_regressor_py__randomsgforestregressor_check_x_y_1.py"],"verified":["sgtlearn/ensemble/random_sgforest_regressor.py"]}
{"ts":"2026-10-03T01:57:33.139Z","run":"r-20261002-215733-b7b8","type":"run_started","mode":"bugfix","revisions":{"buggy":"53a5c148d71076030fca81ed23bf8df64780e3d2","fixed":"676c746e8e2c52431adb7bb362fb3c2428833a4f"},"given":{"buggy":"HEAD","fixed":"WORKTREE"}}
{"ts":"2026-10-03T01:57:36.583Z","run":"r-20261002-215733-b7b8","type":"target_added","target":"sgtlearn-export-py--draw-leaf-text","file":"sgtlearn/_export.py","symbol":"_draw_leaf_text","lines":[685,765],"rev":"fixed","body_sha":"faf7044cbc68687fcfa0ea5b13ea538a606b5779d860a71be10b8c0c9ccb09a1"}
{"ts":"2026-10-03T01:58:04.295Z","run":"r-20261002-215733-b7b8","type":"context_set","target":"sgtlearn-export-py--draw-leaf-text","shingle_ratio":0}
{"ts":"2026-10-03T01:58:04.349Z","run":"r-20261002-215733-b7b8","type":"payload_emitted","payload":"p-e471ca8a","role":"spec","sha256":"6d27fc85bb8d000d2c86b86805d3ec975f42a1fda3dba494d8f92f355edf3aa9","target":"sgtlearn-export-py--draw-leaf-text"}
{"ts":"2026-10-03T01:59:28.987Z","run":"r-20261002-215733-b7b8","type":"spec_saved","target":"sgtlearn-export-py--draw-leaf-text","shingle_ratio":0}
{"ts":"2026-10-03T01:59:29.046Z","run":"r-20261002-215733-b7b8","type":"bundle_frozen","target":"sgtlearn-export-py--draw-leaf-text","bundle":"b-b7b8-sgtlearn-export-py--draw-leaf-text"}
{"ts":"2026-10-03T01:59:34.750Z","run":"r-20261002-215733-b7b8","type":"payload_emitted","payload":"p-c938749f","role":"writer","sha256":"2bb722170802873f243950a6b3c462a56963a47bfb1eb760125e1fc085d7860b","target":"sgtlearn-export-py--draw-leaf-text"}
{"ts":"2026-10-03T01:59:42.323Z","run":"r-20261002-215733-b7b8","type":"payload_emitted","payload":"p-4bd33228","role":"writer","sha256":"325fe67e3a0cfc9e93e785cb01f65540cddf10cd4c2a63ba227634458617b724","target":"sgtlearn-export-py--draw-leaf-text"}
{"ts":"2026-10-03T02:00:21.771Z","run":"r-20261002-215733-b7b8","type":"tests_submitted","target":"sgtlearn-export-py--draw-leaf-text","round":0,"role":"writer","payload":"p-4bd33228","files":["test_sgtlearn_export_py__draw_leaf_text_1.py"]}
{"ts":"2026-10-03T02:02:09.212Z","run":"r-20261002-215733-b7b8","type":"exec_completed","exec":1,"labels":{"buggy":{"failure":9,"pass":30,"load_errors":0},"fixed":{"pass":39,"load_errors":0}}}
{"ts":"2026-10-03T02:02:11.890Z","run":"r-20261002-215733-b7b8","type":"classified","exec":1,"counts":{"sgtlearn-export-py--draw-leaf-text":{"effective":9,"neutral":30}},"queued":0}
{"ts":"2026-10-03T02:02:15.743Z","run":"r-20261002-215733-b7b8","type":"run_reported","likely_bugs":0,"needs_decision":0}
{"ts":"2026-10-03T02:02:15.813Z","run":"r-20261002-215733-b7b8","type":"promoted","files":["tests/regression/test_sgtlearn_export_py__draw_leaf_text_1.py"],"verified":["sgtlearn/_export.py"]}
41 changes: 41 additions & 0 deletions .test-checker/reports/r-20261002-215733-b7b8.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"run": "r-20261002-215733-b7b8",
"mode": "bugfix",
"revisions": {
"buggy": {
"given": "HEAD",
"commit": "53a5c148d71076030fca81ed23bf8df64780e3d2"
},
"fixed": {
"given": "WORKTREE",
"commit": "676c746e8e2c52431adb7bb362fb3c2428833a4f"
}
},
"date": "2026-10-03T02:02:15.741Z",
"harness": "claude-code",
"blind_isolation": "strong",
"summary": [
{
"target": "sgtlearn-export-py--draw-leaf-text",
"effective": 9,
"misguided": 0,
"neutral": 30,
"broken": 0,
"flaky": 0,
"dropped": 0
}
],
"likely_bugs": [],
"suspicions": [],
"needs_decision": [],
"misguided": [],
"dropped": [],
"promotable": [
{
"file": "tests/tcheck/test_sgtlearn_export_py__draw_leaf_text_1.py",
"dest": "tests/regression/test_sgtlearn_export_py__draw_leaf_text_1.py",
"tests": 39
}
],
"not_promotable": []
}
38 changes: 38 additions & 0 deletions .test-checker/reports/r-20261002-215733-b7b8.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# test-checker report r-20261002-215733-b7b8

- Mode: bugfix
- buggy: HEAD → `53a5c148d7`
- fixed: WORKTREE → `676c746e8e`
- Date: 2026-10-03T02:02:15.741Z
- Harness: claude-code
- Blind isolation: strong

## Summary

| target | effective | misguided | neutral | broken | flaky | dropped |
|---|---|---|---|---|---|---|
| sgtlearn-export-py--draw-leaf-text | 9 | 0 | 30 | 0 | 0 | 0 |

## Likely bugs

_none_

## Suspicions

_none_

## Needs your decision

_none_

## Misguided tests and what happened to them

_none_

## Dropped

_none_

## Promotable tests

- tests/tcheck/test_sgtlearn_export_py__draw_leaf_text_1.py → `tests/regression/test_sgtlearn_export_py__draw_leaf_text_1.py` (39 test(s))
49 changes: 49 additions & 0 deletions .test-checker/state.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@
"sha": "b2ad723f36c9520fda0013c7951e257e4907ba73",
"run": "r-20261002-162723-b8b5",
"at": "2026-10-02T20:30:43.728Z"
},
"sgtlearn/_export.py": {
"sha": "9af0b3c1a90f76795010d09eac3d2257bab5af37",
"run": "r-20261002-215733-b7b8",
"at": "2026-10-03T02:02:15.801Z"
}
},
"protected": {
Expand Down Expand Up @@ -715,6 +720,50 @@
"sgtlearn-ensemble-random-sgforest-regressor-py--randomsgforestregressor-check-x-y::tests.tcheck.test_sgtlearn_ensemble_random_sgforest_regressor_py__randomsgforestregressor_check_x_y_1::test_float32_overflow_in_sample_weight_is_rejected"
],
"run": "r-20261002-162723-b8b5"
},
"tests/regression/test_sgtlearn_export_py__draw_leaf_text_1.py": {
"test_ids": [
"sgtlearn-export-py--draw-leaf-text::tests.tcheck.test_sgtlearn_export_py__draw_leaf_text_1::test_classifier_single_output_shows_majority_class_name",
"sgtlearn-export-py--draw-leaf-text::tests.tcheck.test_sgtlearn_export_py__draw_leaf_text_1::test_classifier_tie_picks_lowest_class_index",
"sgtlearn-export-py--draw-leaf-text::tests.tcheck.test_sgtlearn_export_py__draw_leaf_text_1::test_classifier_all_equal_counts_picks_first_class",
"sgtlearn-export-py--draw-leaf-text::tests.tcheck.test_sgtlearn_export_py__draw_leaf_text_1::test_classifier_multi_output_shows_bracketed_comma_separated_names",
"sgtlearn-export-py--draw-leaf-text::tests.tcheck.test_sgtlearn_export_py__draw_leaf_text_1::test_classifier_multi_output_uses_per_output_class_names",
"sgtlearn-export-py--draw-leaf-text::tests.tcheck.test_sgtlearn_export_py__draw_leaf_text_1::test_classifier_empty_class_counts_shows_empty_string",
"sgtlearn-export-py--draw-leaf-text::tests.tcheck.test_sgtlearn_export_py__draw_leaf_text_1::test_classifier_missing_class_counts_shows_empty_string",
"sgtlearn-export-py--draw-leaf-text::tests.tcheck.test_sgtlearn_export_py__draw_leaf_text_1::test_regressor_none_value_shows_ellipsis",
"sgtlearn-export-py--draw-leaf-text::tests.tcheck.test_sgtlearn_export_py__draw_leaf_text_1::test_regressor_scalar_value_formatted_to_precision",
"sgtlearn-export-py--draw-leaf-text::tests.tcheck.test_sgtlearn_export_py__draw_leaf_text_1::test_regressor_scalar_value_respects_different_precision",
"sgtlearn-export-py--draw-leaf-text::tests.tcheck.test_sgtlearn_export_py__draw_leaf_text_1::test_regressor_scalar_value_zero_precision",
"sgtlearn-export-py--draw-leaf-text::tests.tcheck.test_sgtlearn_export_py__draw_leaf_text_1::test_regressor_multi_output_shows_bracketed_formatted_values",
"sgtlearn-export-py--draw-leaf-text::tests.tcheck.test_sgtlearn_export_py__draw_leaf_text_1::test_regressor_multi_output_tuple_sequence",
"sgtlearn-export-py--draw-leaf-text::tests.tcheck.test_sgtlearn_export_py__draw_leaf_text_1::test_regressor_ignores_class_names",
"sgtlearn-export-py--draw-leaf-text::tests.tcheck.test_sgtlearn_export_py__draw_leaf_text_1::test_primary_line_is_bold",
"sgtlearn-export-py--draw-leaf-text::tests.tcheck.test_sgtlearn_export_py__draw_leaf_text_1::test_primary_line_positioned_at_xy",
"sgtlearn-export-py--draw-leaf-text::tests.tcheck.test_sgtlearn_export_py__draw_leaf_text_1::test_primary_line_uses_axes_transform",
"sgtlearn-export-py--draw-leaf-text::tests.tcheck.test_sgtlearn_export_py__draw_leaf_text_1::test_primary_line_centred_horizontally_and_vertically",
"sgtlearn-export-py--draw-leaf-text::tests.tcheck.test_sgtlearn_export_py__draw_leaf_text_1::test_primary_line_uses_given_fontsize",
"sgtlearn-export-py--draw-leaf-text::tests.tcheck.test_sgtlearn_export_py__draw_leaf_text_1::test_primary_line_default_fontsize_when_none",
"sgtlearn-export-py--draw-leaf-text::tests.tcheck.test_sgtlearn_export_py__draw_leaf_text_1::test_primary_line_uses_given_color",
"sgtlearn-export-py--draw-leaf-text::tests.tcheck.test_sgtlearn_export_py__draw_leaf_text_1::test_primary_line_is_added_to_host_axes",
"sgtlearn-export-py--draw-leaf-text::tests.tcheck.test_sgtlearn_export_py__draw_leaf_text_1::test_label_none_returns_single_text_artist",
"sgtlearn-export-py--draw-leaf-text::tests.tcheck.test_sgtlearn_export_py__draw_leaf_text_1::test_other_label_value_returns_single_text_artist",
"sgtlearn-export-py--draw-leaf-text::tests.tcheck.test_sgtlearn_export_py__draw_leaf_text_1::test_label_all_returns_two_text_artists",
"sgtlearn-export-py--draw-leaf-text::tests.tcheck.test_sgtlearn_export_py__draw_leaf_text_1::test_label_all_primary_artist_first",
"sgtlearn-export-py--draw-leaf-text::tests.tcheck.test_sgtlearn_export_py__draw_leaf_text_1::test_label_none_does_not_require_n_samples",
"sgtlearn-export-py--draw-leaf-text::tests.tcheck.test_sgtlearn_export_py__draw_leaf_text_1::test_subtitle_shows_n_samples_without_impurity",
"sgtlearn-export-py--draw-leaf-text::tests.tcheck.test_sgtlearn_export_py__draw_leaf_text_1::test_subtitle_includes_impurity_line_when_requested",
"sgtlearn-export-py--draw-leaf-text::tests.tcheck.test_sgtlearn_export_py__draw_leaf_text_1::test_subtitle_for_classifier_node",
"sgtlearn-export-py--draw-leaf-text::tests.tcheck.test_sgtlearn_export_py__draw_leaf_text_1::test_subtitle_is_not_bold",
"sgtlearn-export-py--draw-leaf-text::tests.tcheck.test_sgtlearn_export_py__draw_leaf_text_1::test_subtitle_placed_below_primary",
"sgtlearn-export-py--draw-leaf-text::tests.tcheck.test_sgtlearn_export_py__draw_leaf_text_1::test_subtitle_anchored_top_and_centred",
"sgtlearn-export-py--draw-leaf-text::tests.tcheck.test_sgtlearn_export_py__draw_leaf_text_1::test_subtitle_uses_axes_transform",
"sgtlearn-export-py--draw-leaf-text::tests.tcheck.test_sgtlearn_export_py__draw_leaf_text_1::test_subtitle_fontsize_one_smaller_than_integer_fontsize",
"sgtlearn-export-py--draw-leaf-text::tests.tcheck.test_sgtlearn_export_py__draw_leaf_text_1::test_subtitle_default_fontsize_when_fontsize_none",
"sgtlearn-export-py--draw-leaf-text::tests.tcheck.test_sgtlearn_export_py__draw_leaf_text_1::test_label_all_missing_n_samples_raises_key_error",
"sgtlearn-export-py--draw-leaf-text::tests.tcheck.test_sgtlearn_export_py__draw_leaf_text_1::test_label_all_with_impurity_missing_impurity_raises_key_error",
"sgtlearn-export-py--draw-leaf-text::tests.tcheck.test_sgtlearn_export_py__draw_leaf_text_1::test_label_all_without_impurity_does_not_need_impurity_key"
],
"run": "r-20261002-215733-b7b8"
}
},
"quarantined": [],
Expand Down
79 changes: 51 additions & 28 deletions sgtlearn/_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,6 @@ def group_label(cols: Sequence[int]) -> str:
prefer_logical_name=feature_names is None,
)

def per_output(items: Sequence[str]) -> str:
return items[0] if len(items) == 1 else "[" + ", ".join(items) + "]"

def fmt_list(values: Sequence[float]) -> str:
return "[" + ", ".join(f"{v:.{decimals}f}" for v in values) + "]"

names: Any = class_names
if names is None:
names = getattr(estimator, "classes_", None)
Expand All @@ -98,14 +92,15 @@ def leaf_text(node: dict) -> str:
if not isinstance(estimator, SGTClassifier):
value = node["value"]
if isinstance(value, (list, tuple)):
return f"value: {fmt_list(value)}"
return f"value: {_fmt_list(value, decimals)}"
return f"value: {value:.{decimals}f}"
counts = node["class_counts"]
text = "class: " + per_output(
text = "class: " + _per_output(
[str(names_per_output[o][int(np.argmax(c))]) for o, c in enumerate(counts)]
)
if show_weights:
text = f"weights: {per_output([fmt_list(c) for c in counts])} {text}"
fmt = [_fmt_list(c, decimals) for c in counts]
text = f"weights: {_per_output(fmt)} {text}"
return text

lines: list[str] = []
Expand All @@ -126,6 +121,15 @@ def walk(nid: int, depth: int) -> None:
return "\n".join(lines) + "\n"


def _per_output(items: Sequence[str]) -> str:
"""One item as-is; several (one per output) as ``[a, b]``."""
return items[0] if len(items) == 1 else "[" + ", ".join(items) + "]"


def _fmt_list(values: Sequence[float], decimals: int) -> str:
return "[" + ", ".join(f"{v:.{decimals}f}" for v in values) + "]"


def _check_fitted_sgt(estimator: Any, caller: str) -> None:
if not isinstance(estimator, (SGTClassifier, SGTRegressor)):
raise TypeError(
Expand Down Expand Up @@ -687,7 +691,7 @@ def _draw_leaf_text(
node: dict,
*,
is_classifier: bool,
class_names: list[str] | None,
class_names: list[list[str]] | None,
criterion: str,
precision: int,
fontsize: int | None,
Expand All @@ -697,19 +701,33 @@ def _draw_leaf_text(
) -> list:
"""Render a leaf as bold colored text (no box).

``node`` may also be an internal node cut off by ``max_depth``. Regressor
internals carry no ``value``, so they render as ``"…"``; classifier
internals still carry ``class_counts`` and show their majority class.
Multi-output nodes show one entry per output, as ``[a, b]``.

Returns a list of Text artists (one bold class/value line, optional
subtitle lines for ``label='all'``).
"""
artists: list = []

if is_classifier:
counts_by_output = list(node.get("class_counts", []))
counts = list(counts_by_output[0]) if counts_by_output else []
if counts:
arg = max(range(len(counts)), key=lambda i: counts[i])
primary = class_names[arg] if class_names is not None else str(arg)
else:
primary = ""
assert class_names is not None
counts_by_output = node.get("class_counts") or []
primary = (
_per_output(
[
class_names[o][int(np.argmax(counts))]
for o, counts in enumerate(counts_by_output)
]
)
if counts_by_output
else ""
)
elif node["value"] is None:
primary = "…"
elif isinstance(node["value"], (list, tuple)):
primary = _fmt_list(node["value"], precision)
else:
primary = f"{node['value']:.{precision}f}"

Expand Down Expand Up @@ -1395,11 +1413,15 @@ def plot_tree(
class_names : list of str, bool, or None, default=None
Class labels shown on classifier leaves, in ``classes_`` order.
``True`` uses ``classes_``; ``None`` or ``False`` shows the encoded
class index (``0``, ``1``, ...).
class index (``0``, ``1``, ...). For multi-output classifiers, pass
one list per output.
label : str, default="feature"
Sample-count annotations: ``"feature"`` shows ``n=`` on internal
nodes, ``"all"`` also adds each leaf's ``n`` (and impurity when
``impurity=True``), and ``"none"`` hides them.
``impurity=True``), and ``"none"`` hides them. The ``n=`` counts are
the rounded weighted sample counts for classifiers (so they reflect
``sample_weight`` and ``class_weight``) and unweighted training
sample counts for regressors.
impurity : bool, default=False
Whether to display leaf impurity; only used with ``label="all"``.
proportion : bool, default=False
Expand Down Expand Up @@ -1462,19 +1484,20 @@ class index (``0``, ``1``, ...).
palette = _build_palette(cmap, tree["num_partitions"])

is_classifier = isinstance(estimator, SGTClassifier)
resolved_class_names: list[str] | None
# One list of class labels per output.
resolved_class_names: list[list[str]] | None
if not is_classifier:
resolved_class_names = None
elif class_names is True:
clf_estimator: SGTClassifier = estimator # type: ignore[assignment]
classes: Any = (
clf_estimator.classes_ if clf_estimator.classes_ is not None else []
)
resolved_class_names = [str(c) for c in classes]
elif class_names in (None, False):
resolved_class_names = [str(c) for c in range(tree["num_classes"][0])]
resolved_class_names = [[str(c) for c in range(k)] for k in tree["num_classes"]]
else:
resolved_class_names = list(class_names) # type: ignore[arg-type]
names: Any = (
estimator.classes_ # type: ignore[union-attr]
if class_names is True
else class_names
)
names_per_output = [names] if tree["num_outputs"] == 1 else names
resolved_class_names = [[str(c) for c in n] for n in names_per_output]

feat_names = _resolve_feature_names(estimator, feature_names)

Expand Down
Loading
Loading