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 件 担当者 0 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

まだ誰も着手していません。

評価

難易度
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日 5時間
マージ済み PR(30日)
99

環境構築

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

graphistry/pygraphistry のほかの issue

graphistry/pygraphistry の issue をすべて見る

似ている issue

Python の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。