layout(ring): degenerate ring_col values break the [min_r,max_r] contract three different ways — continuous silently all-NaN, categorical ZeroDivisionError, time_ring puts NaT at 44x max_r

未关闭
#1,964 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

维护者通常 1 天内回复

还没有人认领这个 Issue。

评估

难度
4/5
预计耗时
3-5 天
新手友好度
55/100
Issue 类型
缺陷
描述清晰度
描述清楚
活跃度
冷清
技术栈
pandas, python

调研方向

Start with the cited implementations in graphistry/layout/ring/continuous.py, categorical.py, and time.py, then run the pandas reproductions in the issue to confirm the radius and failure cases. Check existing ring-layout tests for coverage. Done means degenerate continuous and categorical values and NaT timestamps no longer violate the [min_r, max_r] contract, and time_unit affects bin width as intended.

由索引模型根据 Issue 内容生成。

描述

Found by the 2026-08 layouts/viz amplification probe. All repros on master @ 42b4f7f7d, pandas 2.3.3 / py3.12, CPU.

The three ring layouts share a contract — every node lands on a radius in [min_r, max_r] (default [100, 1000]) — and all three break it on ordinary degenerate column values. They also break it in three mutually inconsistent ways on the same input shape, which is the part worth fixing together.

Defect 1 (silent-wrong): ring_continuous_layout returns all-NaN positions for a constant column

graphistry/layout/ring/continuous.py:198-202:

198:        scalar = (max_r - min_r) / (v_end - v_start)
199:        r = (g._nodes[ring_col] - v_start) * scalar + min_r

v_start/v_end come from .min()/.max(), so they are numpy scalars: a constant column gives 900.0 / np.float64(0.0) = inf (RuntimeWarning only), and then 0 * inf = NaN for every row.

import pandas as pd, graphistry
e = pd.DataFrame({'s':[0,1,2],'d':[1,2,0]})
n = pd.DataFrame({'id':[0,1,2],'v':[2.0, 2.0, 2.0]})
out = graphistry.edges(e,'s','d').nodes(n,'id').ring_continuous_layout('v')
print(out._nodes[['r','x','y']])

Observed: r, x, y all NaN for every node. No warning, no error.
Expected: all nodes on one ring (the natural reading), or a typed decline.

Same for a single-node graph, and for an all-NaN ring_col. Note circle_layout asserts not x.isna().any() at circle.py:362-363 — the ring layouts have no such guard.

Sub-note: if a caller passes python ints for v_start/v_end instead, the same line raises ZeroDivisionError. Whether you get NaN or an exception depends on the scalar type.

Defect 2 (bare-crash): ring_categorical_layout ZeroDivisionError on a single distinct category

graphistry/layout/ring/categorical.py:229-230:

229:    num_rings = len(set(val_to_ring.values())) - 1
230:    scalar = (max_r - min_r) / num_rings

One distinct value ⇒ num_rings == 0 ⇒ ZeroDivisionError: division by zero.

import pandas as pd, graphistry
e = pd.DataFrame({'s':[0,1,2],'d':[1,2,0]})
n = pd.DataFrame({'id':[0,1,2],'cat':['a','a','a']})
graphistry.edges(e,'s','d').nodes(n,'id').ring_categorical_layout('cat')   # ZeroDivisionError

Same for an all-NaN column (unique() → [nan], one category). Empty nodes give num_rings == -1, a negative scalar, and no error at all — a third behaviour for a fourth degenerate shape.

This is the identical input shape as Defect 1 ("the ring column has one distinct value"), and the two sibling layouts disagree: continuous silently NaNs, categorical hard-crashes.

It also fires straight off a GFQL filter:

g2 = g.gfql([n({'grp': 'a'})])          # survivors all share one 'grp' value
g2.ring_categorical_layout('grp')       # ZeroDivisionError

Defect 3 (silent-wrong): time_ring_layout throws NaT nodes ~44x outside max_r

graphistry/layout/ring/time.py:355 does sf = s.astype('int64') on the datetime column; pandas maps NaT to -9223372036854775808, and that sentinel is then scaled as if it were a real timestamp at :369-370.

import pandas as pd, graphistry
e = pd.DataFrame({'s':[0,1,2],'d':[1,2,0]})
n = pd.DataFrame({'id':[0,1,2], 't': pd.to_datetime(['2020-01-01', None, '2022-12-01'])})
out = graphistry.edges(e,'s','d').nodes(n,'id').time_ring_layout('t')
print(out._nodes[['id','t','r','x','y']])

Observed (defaults min_r=100, max_r=1000):

   id          t             r             x             y
0   0 2020-01-01    105.917808    105.917808      0.000000
1   1        NaT  43744.817562  23635.425799  36809.994714   <-- 44x max_r
2   2 2022-12-01    631.123288   -262.639960    573.878781

Expected: NaT excluded, NaN'd, or placed on a named "unknown" ring — anything inside the declared radius band. A single null timestamp blows the viewport out by ~44x and destroys zoom-to-fit for the whole graph. A null in a timestamp column is normal data.

Defect 4 (minor, same module): time_unit is dead code in find_round_bin_width

graphistry/layout/ring/time.py:61-70:

61:    if time_unit is not None:
62:        (unit, date_offset, td_unit) = [(x, y, z) for (x, y, z) in units if x == time_unit][0]
63:
64:    for unit, date_offset, td_unit in (units):
65:        if duration_seconds <= td_unit:
66:            return unit, date_offset, td_unit

The for at :64 immediately rebinds all three names and returns based on duration alone, so line :62 has no effect — the user's time_unit never influences bin width. Observable as 'W', 'M', 'Y', 'C' all producing an identical r_max on the same data. Line :62's [0] is also an unguarded index (bare IndexError for an unknown unit), though in practice unit_to_timedelta at :39 gets there first with a bare KeyError (e.g. time_unit='d' — a plausible typo for the valid 'D').

Category

Defect 1: silent-wrong. Defect 2: bare-crash on valid input. Defect 3: silent-wrong (contract violation: radius outside [min_r, max_r]). Defects 1+2 together are a surface divergence between two layouts documented as siblings. Defect 4: dead code / ignored parameter.

Not exercised

cuDF arm not run: cupy in this environment cannot load libnvrtc.so.12, so every ring layout fails at import time on GPU frames for an environmental reason and no GPU statement is made. By inspection, Defect 3 diverges on GPU — cuDF's astype('int64') preserves <NA> rather than emitting the -2**63 sentinel, so pandas and cuDF would give different wrong answers for the same input. INDICATIVE, static only.

主要语言
Python
星标
2.6k
派生
229
平均合并
1 天 3 小时
30 天内合并 PR
61

环境准备

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

graphistry/pygraphistry 的其他 Issue

查看 graphistry/pygraphistry 的全部 Issue

相似的 Issue

更多 Python Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。