Skip to content

geff_to_csv handles dtypes poorly #408

Description

@cmalinmayor

For me geff_to_csv fails with motile-tracker generated geffs with groups:

Error message:
geff_path_with_groups = "/Users/AStokkermans/Downloads/geff_with_groups.zarr/tracks"

geff_to_csv(geff_path_with_groups, outpath)
---------------------------------------------------------------------------
LossySetitemError                         Traceback (most recent call last)
File ~/Documents/Code/HiiragiImageAnalysisGroup/napari_plugins/github/janelia/motile_napari_plugin/.venv/lib/python3.12/site-packages/pandas/core/internals/blocks.py:1175, in Block.putmask(self, mask, new)
   1174 try:
-> 1175     casted = np_can_hold_element(values.dtype, new)
   1177     self = self._maybe_copy(inplace=True)

File ~/Documents/Code/HiiragiImageAnalysisGroup/napari_plugins/github/janelia/motile_napari_plugin/.venv/lib/python3.12/site-packages/pandas/core/dtypes/cast.py:1830, in np_can_hold_element(dtype, element)
   1829         return element
-> 1830     raise LossySetitemError
   1832 if dtype.kind == "S":
   1833     # TODO: test tests.frame.methods.test_replace tests get here,
   1834     #  need more targeted tests.  xref phofl has a PR about this

LossySetitemError: 

During handling of the above exception, another exception occurred:

TypeError                                 Traceback (most recent call last)
Cell In[13], line 1
----> 1 geff_to_csv(geff_path_with_groups, outpath)

File ~/Documents/Code/HiiragiImageAnalysisGroup/napari_plugins/github/janelia/geff/packages/geff/src/geff/convert/_dataframe.py:112, in geff_to_csv(store, outpath, overwrite)
    109 edge_path = f"{outpath}-edges.csv"
    111 # Convert and write to disk
--> 112 node_df, edge_df = geff_to_dataframes(store)
    113 mode = "w" if overwrite else "x"
    114 node_df.to_csv(node_path, mode=mode)

File ~/Documents/Code/HiiragiImageAnalysisGroup/napari_plugins/github/janelia/geff/packages/geff/src/geff/convert/_dataframe.py:84, in geff_to_dataframes(store)
     82         series = pd.Series(values)
     83         if missing is not None and any(missing):
---> 84             series.mask(missing, inplace=True)
     85         df_dict[name] = series
     87 dataframes.append(pd.DataFrame(df_dict))

File ~/Documents/Code/HiiragiImageAnalysisGroup/napari_plugins/github/janelia/motile_napari_plugin/.venv/lib/python3.12/site-packages/pandas/core/generic.py:10491, in NDFrame.mask(self, cond, other, inplace, axis, level)
  10488 if not hasattr(cond, "__invert__"):
  10489     cond = np.array(cond)
> 10491 return self._where(
  10492     ~cond,
  10493     other=other,
  10494     inplace=inplace,
  10495     axis=axis,
  10496     level=level,
  10497 )

File ~/Documents/Code/HiiragiImageAnalysisGroup/napari_plugins/github/janelia/motile_napari_plugin/.venv/lib/python3.12/site-packages/pandas/core/generic.py:10140, in NDFrame._where(self, cond, other, inplace, axis, level)
  10134     align = self._get_axis_number(axis) == 1
  10136 if inplace:
  10137     # we may have different type blocks come out of putmask, so
  10138     # reconstruct the block manager
> 10140     new_data = self._mgr.putmask(mask=cond, new=other, align=align)
  10141     result = self._constructor_from_mgr(new_data, axes=new_data.axes)
  10142     self._update_inplace(result)

File ~/Documents/Code/HiiragiImageAnalysisGroup/napari_plugins/github/janelia/motile_napari_plugin/.venv/lib/python3.12/site-packages/pandas/core/internals/managers.py:488, in BaseBlockManager.putmask(self, mask, new, align)
    485     align_keys = ["mask"]
    486     new = extract_array(new, extract_numpy=True)
--> 488 return self.apply(
    489     "putmask",
    490     align_keys=align_keys,
    491     mask=mask,
    492     new=new,
    493 )

File ~/Documents/Code/HiiragiImageAnalysisGroup/napari_plugins/github/janelia/motile_napari_plugin/.venv/lib/python3.12/site-packages/pandas/core/internals/managers.py:442, in BaseBlockManager.apply(self, f, align_keys, **kwargs)
    440         applied = b.apply(f, **kwargs)
    441     else:
--> 442         applied = getattr(b, f)(**kwargs)
    443     result_blocks = extend_blocks(applied, result_blocks)
    445 out = type(self).from_blocks(result_blocks, [ax.view() for ax in self.axes])

File ~/Documents/Code/HiiragiImageAnalysisGroup/napari_plugins/github/janelia/motile_napari_plugin/.venv/lib/python3.12/site-packages/pandas/core/internals/blocks.py:1188, in Block.putmask(self, mask, new)
   1183 if self.ndim == 1 or self.shape[0] == 1:
   1184     # no need to split columns
   1186     if not is_list_like(new):
   1187         # using just new[indexer] can't save us the need to cast
-> 1188         return self.coerce_to_target_dtype(
   1189             new, raise_on_upcast=True
   1190         ).putmask(mask, new)
   1191     else:
   1192         indexer = mask.nonzero()[0]

File ~/Documents/Code/HiiragiImageAnalysisGroup/napari_plugins/github/janelia/motile_napari_plugin/.venv/lib/python3.12/site-packages/pandas/core/internals/blocks.py:468, in Block.coerce_to_target_dtype(self, other, raise_on_upcast)
    465     raise_on_upcast = False
    467 if raise_on_upcast:
--> 468     raise TypeError(f"Invalid value '{other}' for dtype '{self.values.dtype}'")
    469 if self.values.dtype == new_dtype:
    470     raise AssertionError(
    471         f"Did not expect new dtype {new_dtype} to equal self.dtype "
    472         f"{self.values.dtype}. Please report a bug at "
    473         "https://github.com/pandas-dev/pandas/issues."
    474     )

TypeError: Invalid value 'nan' for dtype 'bool'

Originally posted by @AnniekStok in https://github.com//pull/407#discussion_r3028540416_

Claude recommended a larger refactor:

It's not just the one bool bug. The series.mask(missing, inplace=True) pattern is fragile across multiple dtypes:

  1. bool — the reported bug. NaN can't go into a bool Series.
  2. int — pandas silently upcasts to float64 to represent NaN, which loses the integer
    dtype. Not a crash, but a lossy conversion that could break a round-trip.
  3. str/bytes — NaN can't go into a plain numpy string/bytes array. Would crash similarly to bool.

And the duplicated masking logic (lines 69-70 and 83-84) means any fix needs to be applied twice, which as you noted is a code smell.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions