Skip to content

Fix check for byteorder swap in Table.to_pandas() - #11288

Merged
mhvk merged 2 commits into
astropy:masterfrom
dhomeier:pandas-endianess
Feb 2, 2021
Merged

mhvk merged 2 commits into
astropy:masterfrom
dhomeier:pandas-endianess

Conversation

@dhomeier

@dhomeier dhomeier commented Jan 31, 2021 •

Copy link
Copy Markdown
Contributor

Description

Fix #11286 – Table.to_pandas() is supposed to export columns always in native byteorder, i.e. swap if, and only if, dtype.byteorder is non-native. This failed in some cases with data identifying e.g. as byteorder='<' on a little-endian system, which was not recognised as '=' and thus incorrectly swap.

This PR attempts to improve identification of non-native byteorder and extends the tests to probe '<', '>', '=' independently.

EDIT: Corrected auto-close syntax

@github-actions github-actions Bot added the table label Jan 31, 2021
@dhomeier dhomeier added Bug copyedited zzz 💤 merge-when-ci-passes Do not use: We have auto-merge option now. and removed copyedited zzz 💤 merge-when-ci-passes Do not use: We have auto-merge option now. labels Jan 31, 2021
@dhomeier
dhomeier requested review from mhvk and taldcroft January 31, 2021 18:34
@mhvk mhvk added this to the v4.0.5 milestone Feb 1, 2021

@mhvk mhvk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ha, didn't know about isnative! That seems exactly what is needed.

The change and test look great, but could you add a changelog entry? This sounds like something we should backport, so I set the milestone to 4.0.5.

Comment thread astropy/table/table.py
if (hasattr(out[name].dtype, 'byteorder')
and out[name].dtype.byteorder not in ('=', '|')):
out[name] = out[name].byteswap().newbyteorder()
if not getattr(out[name].dtype, 'isnative', True):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at this again, why are we using getattr rather than just getting not out[name].dtype.isnative? Are there cases where the dtype does not have that attribute? (I'm actually confused that the dtype could possibly not have 'byteorder' as well....)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, at least some of the specific integer dtypes apparently have neither byteorder nor isnative, probably the reason for the original if (hasattr(out[name].dtype, 'byteorder'). Otherwise causing failures e.g. in

FAILED lib/python3.8/site-packages/astropy/table/tests/test_table.py::TestPandas::test_nullable_int[32-] - AttributeError: 'Int32Dtype' object has no attribute 'isnative'
FAILED lib/python3.8/site-packages/astropy/table/tests/test_table.py::TestPandas::test_nullable_int[64-u] - AttributeError: 'UInt64Dtype' object has no attribute 'isnative'
FAILED lib/python3.8/site-packages/astropy/table/tests/test_table.py::TestPandas::test_nullable_int[64-] - AttributeError: 'Int64Dtype' object has no attribute 'isnative'
FAILED lib/python3.8/site-packages/astropy/table/tests/test_table.py::TestPandas::test_masking[True] - AttributeError: 'Int64Dtype' object has no attribute 'isnative'

So I assumed that endianness did not apply here just as for '|' byteorders, but on a second look, being integer types, I am not so sure any more they will really end up in the right format.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe those are pandas dtypes? They don't look like numpy... Checking, indeed, they are specific to pandas.

Anyway, since you are not changing the way the code works here, I think it makes sense to just merge this.

But let me ping @taldcroft so he can think of whether, more generally, this is working as it should here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The above is the case for MaskedColumn conversions, which for integer types first convert to a Pandas Series, and those do not have the dtype attributes. They could instead be accessed under dtype.numpy_dtype, but converting a non-native column would already fail before that:

>>> out = Series(column, dtype='>I2')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/opt/lib/python3.8/site-packages/pandas/core/series.py", line 249, in __init__
    dtype = self._validate_dtype(dtype)
  File "/opt/lib/python3.8/site-packages/pandas/core/generic.py", line 253, in _validate_dtype
    dtype = pandas_dtype(dtype)
  File "/opt/lib/python3.8/site-packages/pandas/core/dtypes/common.py", line 1763, in pandas_dtype
    npdtype = np.dtype(dtype)
TypeError: data type '>I2' not understood

So that case is obviously not covered by the tests, but would fail for non-native, masked integer columns.
Not sure why the integer types require this special treatment, but at least it will raise a clear exception early on.
Perhaps the whole byteorder check could instead be done first thing on the original column before checking for MaskedColumn etc....

@mhvk mhvk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved. Thanks again!

@mhvk
mhvk merged commit 63f90ef into astropy:master Feb 2, 2021
@dhomeier

dhomeier commented Feb 2, 2021

Copy link
Copy Markdown
Contributor Author

Thanks; the MaskedColumn case quite certainly would not have worked before, could be fixed in a follow-up PR.

@mhvk

mhvk commented Feb 2, 2021

Copy link
Copy Markdown
Contributor

I think this is because in pandas for real data, one uses nan for a mask, but for integer data that obviously is not possible. In any case, your analysis makes it even clearer that we're best off looking at this in more detail in another PR; at least the original problem is now solved!

@taldcroft

Copy link
Copy Markdown
Member

Thanks @dhomeier and @mhvk for fixing this! I'm just going to defer to you two on getting this all right. 😄

@mhvk

mhvk commented Feb 2, 2021

Copy link
Copy Markdown
Contributor

@taldcroft - I think right now we may not be dealing quite properly with pandas masked integer columns, though my guess is that we're just happily keeping them in whatever byte order they have, which is probably OK. Anyway, I think it is fine to let those who use pandas try to fix any inconsistencies.

@dhomeier

dhomeier commented Feb 2, 2021 •

Copy link
Copy Markdown
Contributor Author

@mvhk yes, I think floating point columns with masked elements were left in original byteorder, which is fixed by #11294; this probably would lead to problems somewhere downstream in actually working with the dataframes, but is obviously rare enough to have never become an issue. For the integer types the Series(column) call would actually fix the byteorder, but failed already on dtypes such as >I4.
But I admit I did not look in detail if the nullable integer types are an adequate substitute for masked arrays.

@dhomeier
dhomeier deleted the pandas-endianess branch February 2, 2021 15:40
astrofrog pushed a commit that referenced this pull request Mar 19, 2021
Fix check for byteorder swap in Table.to_pandas()
eteq pushed a commit that referenced this pull request Mar 25, 2021
Fix check for byteorder swap in Table.to_pandas()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Astropy tables / pandas / hdf5 / Endian issue

3 participants