Comments (6)
The issue exists even with polars v 0.20.3
from polars.
Hi @deep8324,
the data schema of the DataFrame in your example is not well-defined, so it's no surprise that you get an error. It seems like the actual schema that the example seems to represent is the following:
OrderedDict([('offer', Struct({'my_value': Int64, 'condition': List(Struct({'applicationId': Int64, 'conditionRequestReason': List(Struct({'a': Int64}))}))}))])
The data for the list of struct with the field a
contains a list with an empty struct [{}]
. It is not clear what this is supposed to represent. When you explicitly specify the schema as mentioned above, then this would represent a list with a single struct where the value for a
is null. This is also the behavior that you can observe when you run the following example. Note that in the example below, the well_defined_data
contains an empty list []
instead of [{}]
. The incomplete_schema_data
is exactly your example, but with additional schema information, such that you can read the data anyway.
# pylint: disable=missing-class-docstring, missing-function-docstring
""" Test for reading NDJSON files with nested fields. """
import io
import json
import unittest
from typing import Sequence, Any, cast
import polars as pl
import polars.testing
class TestNdJson(unittest.TestCase):
def test_json_format(self):
well_defined_data = io.StringIO(
"""{"offer": {"my_value": 0, "condition": [{"applicationId": 0, "conditionRequestReason": []}]}}
{"offer": {"my_value": 0, "condition": [{"applicationId": 0, "conditionRequestReason": [{"a":0}]}]}}""")
# Initialize the frame from a sequence of dictionaries.
dicts = cast(Sequence[dict[str, Any]],
(json.loads(line) for line in well_defined_data))
frame_from_dicts = pl.from_dicts(dicts)
print(frame_from_dicts.schema)
# Initialize the frame from the NDJSON file.
frame_from_ndjson = pl.read_ndjson(well_defined_data)
print(frame_from_ndjson.schema)
# Compare the content of the frames.
polars.testing.assert_frame_equal(frame_from_dicts, frame_from_ndjson)
# Initialize the frame from a malformed NDJSON
incomplete_schema_data = io.StringIO(
"""{"offer": {"my_value": 0, "condition": [{"applicationId": 0, "conditionRequestReason": [{}]}]}}""")
frame_with_schema = pl.read_ndjson(incomplete_schema_data, schema=frame_from_ndjson.schema)
frame_with_schema_export = frame_with_schema.write_ndjson()
self.assertEqual(
'{"offer":{"my_value":0,"condition":[{"applicationId":0,"conditionRequestReason":[{"a":null}]}]}}\n',
frame_with_schema_export)
if __name__ == '__main__':
unittest.main()
However, I can observe that without schema information, the behavior of from_dicts
and read_ndjson
differs when the data contains the awkward list with the empty struct [{}]
.
def test_read_ndjson_list_of_awkward_struct(self):
input_data = io.StringIO(
"""{"offer": {"my_value": 0, "condition": [{"applicationId": 0, "conditionRequestReason": [{}]}]}}
{"offer": {"my_value": 0, "condition": [{"applicationId": 0, "conditionRequestReason": [{"a":0}]}]}}""")
# Initialize the frame from a sequence of dictionaries.
dicts = cast(Sequence[dict[str, Any]],
(json.loads(line) for line in input_data))
frame_from_dicts = pl.from_dicts(dicts)
print(frame_from_dicts.schema)
# Initialize the frame from the NDJSON file.
frame_from_ndjson = pl.read_ndjson(input_data)
print(frame_from_ndjson.schema)
# Compare the content of the frames.
polars.testing.assert_frame_equal(frame_from_dicts, frame_from_ndjson)
The behavior of from_dicts
seems to be a bit unexpected:
>>> frame_from_dicts
shape: (2, 1)
┌─────────────────────────┐
│ offer │
│ --- │
│ struct[2] │
╞═════════════════════════╡
│ {0,[{0,[{null,null}]}]} │
│ {0,[{0,[{null,0}]}]} │
└─────────────────────────┘
>>> frame_from_dicts.schema
OrderedDict([('offer', Struct({'my_value': Int64, 'condition': List(Struct({'applicationId': Int64, 'conditionRequestReason': List(Struct({'': Null, 'a': Int64}))}))}))])
from polars.
Enclosed is a small simple example, where JSON decode doesn't determine the correct schema for a list of integer. It works for list of string, but not for list of integer.
# pylint: disable=missing-class-docstring, missing-function-docstring, too-few-public-methods
""" Unit tests for the polars JSON decode functionality. """
import io
import pytest
import polars as pl
import polars.testing
class TestPolarsJsonDecode:
@pytest.mark.parametrize('test_name, input_ndjson', [
("str", """{"list_field": ["a", "b"]}
{"list_field": []}
{"list_field": ["c", "d", "e"]}"""),
("int", """{"list_field": [1, 2]}
{"list_field": []}
{"list_field": [4, 5, 6]}""")
])
def test_json_decode_list_of_basic_type(self, test_name, input_ndjson):
print(f"Reading list of {test_name}...")
input_buf = io.StringIO(input_ndjson)
frame_from_ndjson = pl.read_ndjson(input_buf)
# pylint: disable-next=assignment-from-no-return
series_with_json = pl.Series(values=input_buf).str.strip_chars_start()
series_decoded_unnested = series_with_json.str.json_decode().struct.unnest()
assert frame_from_ndjson.schema == series_decoded_unnested.schema
polars.testing.assert_frame_equal(frame_from_ndjson, series_decoded_unnested)
if __name__ == '__main__':
pytest.main()
from polars.
I just did some tests with polars 0.20.6 (and pyarrow 13.0.0)
- The issue
test_json_decode_list_of_basic_type
seems to be resolved and doesn't reproduce anymore (since polars 0.20.4; it was still present in polars 0.20.3.) - The issue
test_read_ndjson_list_of_awkward_struct
still reproduces. - The panic in
test_json_format
still reproduces. - The panic in
test_json_decode_error
still reproduces.
Is polars using arrow for decoding JSON input?
from polars.
The following minimal test reproduces the PanicException that @deep8324 reported above.
import io
import json
import pytest
import polars as pl
@pytest.mark.parametrize('input_ndjson', [
'{"bar": [{}]}', # nested_null
'{"foo":[{"bar":[{}]}]}' # nested_nested_null
])
def test_ndjson_nested_nested_null(input_ndjson):
""" Test whether read_ndjson and from_dicts imports nested null structs in the same way. """
buffer = io.StringIO(input_ndjson)
json_obj = json.loads(next(buffer))
df_from_dicts = pl.from_dicts([json_obj])
df_from_ndjson = pl.read_ndjson(buffer)
assert df_from_dicts.schema == df_from_ndjson.schema
First we have the nested_null
subtest, where the assertion for the expected schema fails:
Expected :OrderedDict([('bar', List(Struct({})))])
Actual :OrderedDict([('bar', List(Struct({'': Null})))])
And then we have the nested_nested_null
subtest, that causes the PanicException, that reports exactly the same unexpected type mismatch:
ListArray's child's DataType must match. However, the expected DataType is
Struct([Field { name: \"bar\", data_type: LargeList(Field { name: \"item\", data_type:
>>>>>>>>Struct([]),
is_nullable: true, metadata: {} }), is_nullable: true, metadata: {} }])
while it got
Struct([Field { name: \"bar\", data_type: LargeList(Field { name: \"item\", data_type:
>>>>>>>>Struct([Field { name: \"\", data_type: Null, is_nullable: true, metadata: {} }]),
is_nullable: true, metadata: {} }), is_nullable: true, metadata: {} }])."))
The unexpected difference is in the line that I highlighted with the arrows. Instead of an empty struct, the code receives a struct with a field that has the name "" and a null value.
polars unit tested behavior of the import of empty structs from dictionaries and the import of empty structs from NDJSON is inconsistent. Importing {}
as Struct([])
, like it is done for NDJSON, seems to be the right approach. Importing {}
as Struct({'': Null})
seems to be a bug in from_dicts
, even if this behavior is unit tested. (I had a look at the polars unit tests.)
Feel free to add this test to the polars unit tests.
from polars.
It still reproduces in polars 0.20.10. I think it really comes down to the fact that polars is treating the empty object "{}" in a different way, depending on where it shows up in the schema.
def test_ndjson_empty_object() -> None:
"""
The actual type of `empty_object_column` (tested in `test_ndjson_null_buffer()`) is inconsistent with
the actual type of `nested_empty_object_column` (tested in `test_ndjson_nested_null()`).
"""
data = io.BytesIO(
b"""\
{"id": 1, "empty_object_column": {}, "nested_empty_object_column": [{}]}
{"id": 2, "empty_object_column": {}, "nested_empty_object_column": [{}]}
{"id": 3, "empty_object_column": {}, "nested_empty_object_column": [{}]}
"""
)
assert pl.read_ndjson(data).schema == {
"id": pl.Int64,
"empty_object_column": pl.Struct([]), # or pl.Struct([pl.Field("", pl.Null)]) ?
"nested_empty_object_column": pl.List(pl.Struct([]))
}
The test above fails with the output:
E - 'empty_object_column': Struct({}),
E + 'empty_object_column': Struct({'': Null}),
E ? ++++++++
@ritchie46 : It seems like you wrote the test_ndjson_null_buffer
test.
@alexander-beedie : It seems like you wrote the test_ndjson_nested_null
test.
I'm mentioning the two of you, since I would be interested to know which of the two behaviors is the expected one.
from polars.
Related Issues (20)
- pl.Enum equivalence is category order dependent HOT 3
- `Decimal[*, scale>0] * Int` has differing result type than `Decimal[*, scale>0] * Decimal[*,scale=0]` HOT 1
- The `pivot` feature does not compile in Rust polars v38-40. HOT 2
- Struct with decimals not read properly in parquet HOT 7
- Regression from 0.20.21 -> 0.20.22-rc.1 `pl.Expr.list.to_array(n)` is throwing `polars.exceptions.ComputeError: not all elements have the specified width n` HOT 1
- performance issue with tpch q7 after dropping columns and using sink_parquet HOT 4
- `map_elements` doesn't respect `return_dtype` within an `over` statement on an empty DataFrame HOT 1
- `pl.lit(None, dtype=pl.Struct({"a": pl.Int64()}))` gives `{'a': None}`, not `None` HOT 1
- Support equality operation on nested Array types
- Unordered enum data type HOT 4
- Support interval expressions in Python SQL Context
- minimal `dyn int` when reading from python HOT 1
- Panic when casting Array of Categoricals to Array of String HOT 2
- dt.epoch() is much slower than truediv() for the same operations HOT 1
- PanicException when using collect(streaming=True) on two LazyFrames from `scan_parquet()` calls.
- Allow Zero width no-break space in float parser HOT 7
- Alternative method 10x faster than dt.offset_by() HOT 2
- Sampling with groupby HOT 1
- Sample by Group HOT 4
- Add `make test-ci` to (mostly) replicate CI tests HOT 4
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from polars.