-
Notifications
You must be signed in to change notification settings - Fork 41
Read membership as the absence of a value, however pandas spells it #962
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -56,11 +56,20 @@ def _as_numeric(values): | |
| return array.astype(float) | ||
|
|
||
|
|
||
| def _is_membership(value) -> bool: | ||
| """Whether a row states membership of its lane rather than a value. | ||
|
|
||
| A frame carries that as no value at all, which pandas spells None, | ||
| NaN or NA depending on what else the column holds. | ||
| """ | ||
| return bool(pd.isna(value)) | ||
|
|
||
|
|
||
| def _default_label(value) -> str: | ||
| """Text for a value which was not given a label of its own.""" | ||
| if isinstance(value, str): | ||
| return value | ||
| if value is None: | ||
| if _is_membership(value): | ||
| return "" | ||
| # A number states itself; a membership lane is named by its lane instead. | ||
| return f"{value:g}" if isinstance(value, float) else str(value) | ||
|
|
@@ -124,15 +133,13 @@ def _read_frame(intervals, start, end, lane, value, label): | |
|
|
||
| def _lane_kind(values) -> str: | ||
| """Return the one value kind a lane states, refusing a mixture.""" | ||
| kinds = {value_kind(normalize_value(x)) for x in values if x is not None} | ||
| kinds = {value_kind(normalize_value(x)) for x in values if not _is_membership(x)} | ||
| kinds.discard(None) | ||
| kinds.discard("membership") | ||
| if not kinds: | ||
| return "none" | ||
| if len(kinds) > 1: | ||
| return "mixed" | ||
| if any(x is None for x in values): | ||
| # a lane states membership (no values) or a value in every row | ||
| return "mixed" | ||
| return kinds.pop() | ||
|
|
||
|
|
||
|
|
@@ -208,8 +215,11 @@ def _resolve_colors(rows, kind, lane_index, string_map, color): | |
| # default treatment rather than being matched against lane names. | ||
| color = color.get(rows["lane"].iloc[0]) | ||
| if isinstance(color, Mapping): | ||
| colors = [color.get(x, UNCOVERED_COLOR) for x in rows["value"]] | ||
| used = {x: color[x] for x in rows["value"] if x in color} | ||
| # A mapping is keyed by the value, and membership has one key | ||
| # however the column's dtype spelled it. | ||
| keys = [None if _is_membership(x) else x for x in rows["value"]] | ||
| colors = [color.get(x, UNCOVERED_COLOR) for x in keys] | ||
| used = {x: color[x] for x in keys if x in color} | ||
| return colors, ("legend", used) | ||
| if isinstance(color, str) and kind != "numeric": | ||
| return [color] * len(rows), None | ||
|
|
@@ -221,7 +231,11 @@ def _resolve_colors(rows, kind, lane_index, string_map, color): | |
| ) | ||
| if kind == "numeric": | ||
| values = np.asarray( | ||
| [float(normalize_value(x)) for x in rows["value"]], dtype=float | ||
| [ | ||
| np.nan if _is_membership(x) else float(normalize_value(x)) | ||
| for x in rows["value"] | ||
|
Comment on lines
+234
to
+236
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a numeric lane contains one distinct finite value plus Useful? React with 👍 / 👎.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed. Reproduced first: a lane of [5.0, NaN, 5.0] gave all three rows cmap(0.5), so the hole read as data. The single-value shortcut now keeps the membership mask and returns UNCOVERED_COLOR for the rows which state nothing. Covered by |
||
| ], | ||
| dtype=float, | ||
| ) | ||
| if isinstance(color, str): | ||
| try: | ||
|
|
@@ -235,7 +249,8 @@ def _resolve_colors(rows, kind, lane_index, string_map, color): | |
| if len(np.unique(values[np.isfinite(values)])) < 2: | ||
| # One value is not a scale, so it gets a color and its number | ||
| # rather than a colorbar reading from it to a value nothing has. | ||
| return [cmap(0.5)] * len(rows), None | ||
| # A row which states none is still not that value. | ||
| return [UNCOVERED_COLOR if np.isnan(x) else cmap(0.5) for x in values], None | ||
| cmap, norm, ticks = numeric_scale(values, getattr(cmap, "name", NUMERIC_CMAP)) | ||
| # A value nothing states maps to a transparent color unless the | ||
| # colormap is told otherwise, and the box would simply vanish. | ||
|
|
@@ -350,8 +365,8 @@ def plot_lanes( | |
| the lane has no row). | ||
| label | ||
| Column holding the text drawn in each box. Values supply it by | ||
| default: text as itself, a number as its digits, a membership | ||
| row as nothing, since the lane it sits in already names it. | ||
| default: text as itself, a number as its digits, and a row which | ||
| states no value nothing, since its lane already names it. | ||
| lanes | ||
| The lanes to draw, in order. Names with no rows are kept as empty | ||
| lanes, so two figures of different subjects still line up. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -806,7 +806,21 @@ def _segment_colors(one, color, mid, crs, handles, palette): | |
| handles.setdefault("n/a", PatchArtist(facecolor=UNPLACED, label="n/a")) | ||
| return None, [UNPLACED] * len(mid) | ||
| masks = interval_masks(mid, [x.interval for x in items]) | ||
| kinds = {value_kind(normalize_value(k)) for k in keys} | ||
| kinds = { | ||
| value_kind(normalize_value(k)) for k in keys if not _lanes._is_membership(k) | ||
| } | ||
|
Comment on lines
+809
to
+811
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed. It really did render |
||
| if not kinds: | ||
| # Every row states membership, so the group itself is the value | ||
| # and belonging to it is the only thing there is to color. | ||
| colors = [UNPLACED] * len(mid) | ||
| base = plt.get_cmap(_lanes.STRING_CMAP)(_lanes.WHEEL_ORDER[0]) | ||
| for mask in masks: | ||
| for position in np.flatnonzero(mask): | ||
| colors[position] = base | ||
| handles.setdefault(color, PatchArtist(facecolor=base, label=color)) | ||
| if any(c is UNPLACED for c in colors): | ||
| handles.setdefault("n/a", PatchArtist(facecolor=UNPLACED, label="n/a")) | ||
| return None, colors | ||
|
Comment on lines
+809
to
+823
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Skip membership labels during numeric conversion. Lines 809-811 classify a group with numeric values and a membership label as numeric. The numeric branch then converts every label at Line 827. Skip membership labels in the numeric assignment loop. Leave their initialized Proposed fix values = np.full(len(mid), np.nan)
for item, mask in zip(items, masks, strict=True):
+ if _lanes._is_membership(item.value):
+ continue
values[mask] = float(normalize_value(item.value))🤖 Prompt for AI Agents |
||
| if kinds == {"numeric"}: | ||
| values = np.full(len(mid), np.nan) | ||
| for item, mask in zip(items, masks, strict=True): | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When pandas represents an unset value as
NaNorNA, a caller's membership override such ascolor={None: "red"}is ignored because_resolve_colorslooks up the raw scalar even though this helper declares all three representations equivalent. The same logical frame therefore changes colors with its pandas dtype; canonicalize membership values to one key before applying value-to-color mappings.Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed. Mapping lookups now canonicalize membership to a single
Nonekey before consulting the mapping, socolor={None: 'red'}applies whether the column spelled it None, NaN or NA. Covered bytest_membership_has_one_key_in_a_mapping.