pandas-dev / pandas

Flexible and powerful data analysis / manipulation library for Python, providing labeled data structures similar to R data.frame objects, statistical functions, and much more
https://pandas.pydata.org
BSD 3-Clause "New" or "Revised" License
43.86k stars 18.01k forks source link

BUG: many dataframe operations broken when a column contains numpy structured array #60108

Open digitalsignalperson opened 4 weeks ago

digitalsignalperson commented 4 weeks ago

Pandas version checks

Reproducible Example

import numpy as np
import pandas as pd

N = 10
hash_dtype = np.dtype([(f'h{i}', np.uint64) for i in range(4)])
hashes = np.zeros(N, dtype=hash_dtype)

df = pd.DataFrame(data={'hashes':hashes, 'other_stuff':np.zeros(N)})
idx = [0, 1]

## This fails
df.loc[idx, 'hashes'] = np.ones(len(idx), dtype=hash_dtype)
# LossySetitemError: 
# During handling of the above exception, another exception occurred:
# AssertionError
# AssertionError: Something has gone wrong, please report a bug at https://github.com/pandas-dev/pandas/issues

## This also files
print(df)
# TypeError: ufunc 'isnan' not supported for the input types, and the inputs could not be safely coerced to any supported types according to the casting rule ''safe''

## This is ok
print(df['other_stuff'])

## This is a partial workaround
df['hashes'] = np.zeros(len(df), dtype=hash_dtype) # same errors with or without this line
df['hashes'].values[idx] = np.ones(len(idx), dtype=hash_dtype)

## But things still broken
print(df['hashes'])
# TypeError: ufunc 'isnan' not supported for the input types, and the inputs could not be safely coerced to any supported types according to the casting rule ''safe''

## This works
print(df.loc[0, 'hashes'])

## This doesn't
print(df.loc[:, 'hashes'])
# TypeError: ufunc 'isnan' not supported for the input types, and the inputs could not be safely coerced to any supported types according to the casting rule ''safe''

## OK
x = df['hashes']

## Not OK
x = df[['hashes', 'other_stuff']]
# TypeError: void() takes at least 1 positional argument (0 given)

Issue Description

Working with a numpy array with a structured type consisting of four 64 bit integers, there are various errors when getting/setting the associated data in a dataframe.

Expected Behavior

can set values with .loc without assertion error can print dataframe without exception

Installed Versions

INSTALLED VERSIONS ------------------ commit : 0691c5cf90477d3503834d983f69350f250a6ff7 python : 3.12.7 python-bits : 64 OS : Linux OS-release : 6.6.57-1-lts Version : #1 SMP PREEMPT_DYNAMIC Thu, 17 Oct 2024 13:57:25 +0000 machine : x86_64 processor : byteorder : little LC_ALL : None LANG : en_US.UTF-8 LOCALE : en_US.UTF-8 pandas : 2.2.3 numpy : 2.1.2 pytz : 2024.2 dateutil : 2.9.0.post0 pip : 24.2 Cython : 3.0.11 sphinx : None IPython : 8.28.0 adbc-driver-postgresql: None adbc-driver-sqlite : None bs4 : 4.12.3 blosc : None bottleneck : None dataframe-api-compat : None fastparquet : None fsspec : None html5lib : None hypothesis : None gcsfs : None jinja2 : None lxml.etree : 5.3.0 matplotlib : 3.9.2 numba : None numexpr : None odfpy : None openpyxl : 3.1.5 pandas_gbq : None psycopg2 : None pymysql : None pyarrow : None pyreadstat : None pytest : None python-calamine : None pyxlsb : None s3fs : None scipy : 1.14.1 sqlalchemy : None tables : None tabulate : None xarray : None xlrd : 2.0.1 xlsxwriter : 3.1.9 zstandard : 0.22.0 tzdata : 2024.2 qtpy : 2.4.1 pyqt5 : None
digitalsignalperson commented 4 weeks ago

also worth mentioning the problems go away if you allow the structured array to have an object dtype

e.g.

import numpy as np
import pandas as pd

N = 10
df = pd.DataFrame(data={'other_stuff':np.zeros(N)})

idx = [0, 1]
hash_dtype = np.dtype([(f'h{i}', np.uint64) for i in range(4)])
df.loc[idx, 'hashes'] = np.ones(len(idx), dtype=hash_dtype)
# here df['hashes'] coerced to object data type
rhshadrach commented 4 weeks ago

Thanks for the report. Indeed, pandas does not support NumPy structured arrays. I do not think it is feasible to support these.

jorisvandenbossche commented 3 weeks ago

Should we rather error when a user creates such a Series with an unsupported dtype, instead of allowing to create it but then fail later on in various confusing ways?

Or is there enough that works that people would want to use a Series/DataFrame container with such data?

digitalsignalperson commented 3 weeks ago

For sure it would be helpful to fail earlier, it took me a while to figure out what led to creating this issue.

My workaround was to view the numpy array as bytes. In my example the structured type is 32 bytes and .astype('S32') makes each element appear as a 32 bytes string. Pandas seems fine with this data type.

Is there any trick where pandas could do a similar thing, just treat structured arrays as opaque "element is N bytes" or even internally storing as a np.dtype(f'S{element width') type?

rhshadrach commented 3 weeks ago

I'm +1 on failing in the constructor.

Is there any trick where pandas could do a similar thing, just treat structured arrays as opaque "element is N bytes" or even internally storing as a np.dtype(f'S{element width') type?

I don't think this should happen silently.

digitalsignalperson commented 3 weeks ago

I don't think this should happen silently.

I was thinking more in terms of something pandas could do internally, but I don't know the internals for why it trips on structured types or if this is a practical idea.

For example maybe some bookkeeping happens so that internally pandas sees a ndarray.view of dtype('S{item size}') but to the user it appears like their custom data type.

N = 10
hash_dtype = np.dtype([(f'h{i}', np.uint64) for i in range(4)])
hashes = np.zeros(N, dtype=hash_dtype)

hashes_view = hashes.view(f'S{hash_dtype.itemsize}') 
hashes[0] = (1, 1, 1, 1)

In [35]: hashes
Out[35]: 
array([(1, 1, 1, 1), (0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0),
       (0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0),
       (0, 0, 0, 0), (0, 0, 0, 0)],
      dtype=[('h0', '<u8'), ('h1', '<u8'), ('h2', '<u8'), ('h3', '<u8')])

In [37]: print(hashes_view)
[b'\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01'
 b'' b'' b'' b'' b'' b'' b'' b'' b'']
rhshadrach commented 3 weeks ago

For example maybe some bookkeeping happens so that internally pandas sees a ndarray.view of dtype('S{item size}') but to the user it appears like their custom data type.

If this were possible, would essentially any operation other than getting, setting, and reshaping just raise? E.g. groupby.sum. I'm quite negative on partial support for a dtype like this.

rhshadrach commented 3 weeks ago

Similar to #55011 I think

jorisvandenbossche commented 1 week ago

Another similar bug report in https://github.com/pandas-dev/pandas/issues/42739, with a masked structured array, where for this case the DataFrame constructor already fails, although it could use a more informative error message.

import numpy as np
import pandas as pd
import numpy.ma as ma

# create a masked, structured array
a = np.ma.array([(1, 2.2), (42, 5.5)],
                dtype=[('a',int),('b',float)],
                mask=[(True,False),(False,True)])

b = pd.DataFrame(a)

Currently gives TypeError: Iterator operand 1 dtype could not be cast from dtype([('a', '?'), ('b', '?')]) to dtype('bool') according to the rule 'unsafe'