-
Notifications
You must be signed in to change notification settings - Fork 29.3k
[SPARK-20791][PYSPARK] Use Arrow to create Spark DataFrame from Pandas #19459
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 16 commits
cd3d51e
c73c7c6
e9c6de7
06b033f
9d667c6
31851f8
ca474db
c7ddee6
b00a924
e36a176
fc3a554
f42e351
76e87dc
81ddfa9
5e8e11f
3052f30
9f7b1c0
dc03657
f421e2d
0de3126
c41cf33
b6df7bf
cfb1c3d
b362b9a
1c244d1
99ce1e4
7d9cc3e
126f2e7
421d0be
0ad736b
6c72e37
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -23,6 +23,7 @@ | |||||
|
|
||||||
| if sys.version >= '3': | ||||||
| basestring = unicode = str | ||||||
| xrange = range | ||||||
| else: | ||||||
| from itertools import imap as map | ||||||
|
|
||||||
|
|
@@ -414,6 +415,73 @@ def _createFromLocal(self, data, schema): | |||||
| data = [schema.toInternal(row) for row in data] | ||||||
| return self._sc.parallelize(data), schema | ||||||
|
|
||||||
| def _createFromPandasWithArrow(self, pdf, schema): | ||||||
| """ | ||||||
| Create a DataFrame from a given pandas.DataFrame by slicing it into partitions, converting | ||||||
| to Arrow data, then sending to the JVM to parallelize. If a schema is passed in, the | ||||||
| data types will be used to coerce the data in Pandas to Arrow conversion. | ||||||
| """ | ||||||
| from pyspark.serializers import ArrowSerializer | ||||||
| from pyspark.sql.types import from_arrow_schema, to_arrow_type, _cast_pandas_series_type | ||||||
| import pyarrow as pa | ||||||
|
|
||||||
| # Slice the DataFrame into batches | ||||||
| step = -(-len(pdf) // self.sparkContext.defaultParallelism) # round int up | ||||||
| pdf_slices = (pdf[start:start + step] for start in xrange(0, len(pdf), step)) | ||||||
|
|
||||||
| if schema is None or isinstance(schema, list): | ||||||
| batches = [pa.RecordBatch.from_pandas(pdf_slice, preserve_index=False) | ||||||
| for pdf_slice in pdf_slices] | ||||||
|
|
||||||
| # There will be at least 1 batch after slicing the pandas.DataFrame | ||||||
| schema_from_arrow = from_arrow_schema(batches[0].schema) | ||||||
|
|
||||||
| # If passed schema as a list of names then rename fields | ||||||
| if isinstance(schema, list): | ||||||
| fields = [] | ||||||
| for i, field in enumerate(schema_from_arrow): | ||||||
| field.name = schema[i] | ||||||
| fields.append(field) | ||||||
| schema = StructType(fields) | ||||||
| else: | ||||||
| schema = schema_from_arrow | ||||||
| else: | ||||||
| batches = [] | ||||||
| for i, pdf_slice in enumerate(pdf_slices): | ||||||
|
|
||||||
| # convert to series to pyarrow.Arrays to use mask when creating Arrow batches | ||||||
| arrs = [] | ||||||
| names = [] | ||||||
| for c, (_, series) in enumerate(pdf_slice.iteritems()): | ||||||
| field = schema[c] | ||||||
| names.append(field.name) | ||||||
| t = to_arrow_type(field.dataType) | ||||||
| try: | ||||||
| # NOTE: casting is not necessary with Arrow >= 0.7 | ||||||
| arrs.append(pa.Array.from_pandas(_cast_pandas_series_type(series, t), | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any chance that the given data type for the field is not correct? By a wrong data type of a field, what the behavior of this casting? For example, for a Series of numpy.int16`, if the given data type is bytetype, this casting will turn it to numpy.int8, so we will get: >>> s = pd.Series([1, 2, 10001], dtype=np.int16)
>>> s
0 1
1 2
2 10001
dtype: int16
>>> s.astype(np.int8)
0 1
1 2
2 17
dtype: int8
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that is a problem with using |
||||||
| mask=series.isnull(), type=t)) | ||||||
| except ValueError as e: | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this guard only works to prevent casting like: >>> s = pd.Series(["abc", "2", "10001"])
>>> s
0 abc
1 2
2 10001
dtype: object
>>> s.astype(np.int8)
...
ValueError: invalid literal for long() with base 10: 'abc'For the casting that can cause overflow, this seems don't work.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah, there doesn't seem to be a way to guard against overflow with |
||||||
| warnings.warn("Arrow will not be used in createDataFrame: %s" % str(e)) | ||||||
| return None | ||||||
| batches.append(pa.RecordBatch.from_arrays(arrs, names)) | ||||||
|
|
||||||
| # Verify schema of first batch, return None if not equal and fallback without Arrow | ||||||
| if i == 0: | ||||||
| schema_from_arrow = from_arrow_schema(batches[i].schema) | ||||||
| if schema != schema_from_arrow: | ||||||
| warnings.warn("Arrow will not be used in createDataFrame.\n" + | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will we reach this block?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we won't reach the block, I think we can simplify
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks @ueshin! This does simplify things quite a bit, which I like. My only concerns are that we rely on Arrow/Pandas to raise an error somewhere during the casting in order to fallback, and the fields in the Arrow record batches get arbitrary names (doesn't use schema names). What are your thoughts @HyukjinKwon ?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK by me too but let's keep on our eyes on mailing list and JIRAs that complains about it in the future, and improve it next time if this sounds more important than we think here.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok, merging now |
||||||
| "Supplied schema: %s\n!=\nArrow schema: %s" | ||||||
| % (str(schema), str(schema_from_arrow))) | ||||||
| return None | ||||||
|
|
||||||
| # Create the Spark DataFrame directly from the Arrow data and schema | ||||||
| jrdd = self._sc._serialize_to_jvm(batches, len(batches), ArrowSerializer()) | ||||||
| jdf = self._jvm.PythonSQLUtils.arrowPayloadToDataFrame( | ||||||
| jrdd, schema.json(), self._wrapped._jsqlContext) | ||||||
| df = DataFrame(jdf, self._wrapped) | ||||||
| df._schema = schema | ||||||
| return df | ||||||
|
|
||||||
| @since(2.0) | ||||||
| @ignore_unicode_prefix | ||||||
| def createDataFrame(self, data, schema=None, samplingRatio=None, verifySchema=True): | ||||||
|
|
@@ -510,6 +578,12 @@ def createDataFrame(self, data, schema=None, samplingRatio=None, verifySchema=Tr | |||||
| except Exception: | ||||||
| has_pandas = False | ||||||
| if has_pandas and isinstance(data, pandas.DataFrame): | ||||||
| if self.conf.get("spark.sql.execution.arrow.enabled", "false").lower() == "true" \ | ||||||
| and len(data) > 0: | ||||||
| df = self._createFromPandasWithArrow(data, schema) | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As of #19459 (comment), spark/python/pyspark/sql/tests.py Line 1325 in bfc7e1f
although I don't think we have (intendedly and properly) supported this case with For spark/python/pyspark/sql/session.py Line 515 in d492cc5
So, I think we should only support list of strings maybe with a proper exception for Of course, this case should work: |
||||||
| # Fallback to create DataFrame without arrow if return None | ||||||
| if df is not None: | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shall we show some log messages to users in this case?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the PR from @ueshin better captures errors to show a warning, i'll merge that now |
||||||
| return df | ||||||
| if schema is None: | ||||||
| schema = [str(x) for x in data.columns] | ||||||
| data = [r.tolist() for r in data.to_records(index=False)] | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3095,16 +3095,32 @@ def setUpClass(cls): | |
| StructField("3_long_t", LongType(), True), | ||
| StructField("4_float_t", FloatType(), True), | ||
| StructField("5_double_t", DoubleType(), True)]) | ||
| cls.data = [("a", 1, 10, 0.2, 2.0), | ||
| ("b", 2, 20, 0.4, 4.0), | ||
| ("c", 3, 30, 0.8, 6.0)] | ||
| cls.data = [(u"a", 1, 10, 0.2, 2.0), | ||
| (u"b", 2, 20, 0.4, 4.0), | ||
| (u"c", 3, 30, 0.8, 6.0)] | ||
|
|
||
| @classmethod | ||
| def tearDownClass(cls): | ||
| ReusedPySparkTestCase.tearDownClass() | ||
| cls.spark.stop() | ||
|
|
||
| def assertFramesEqual(self, df_with_arrow, df_without): | ||
| msg = ("DataFrame from Arrow is not equal" + | ||
| ("\n\nWith Arrow:\n%s\n%s" % (df_with_arrow, df_with_arrow.dtypes)) + | ||
| ("\n\nWithout:\n%s\n%s" % (df_without, df_without.dtypes))) | ||
| self.assertTrue(df_without.equals(df_with_arrow), msg=msg) | ||
|
|
||
| def createPandasDataFrameFromData(self): | ||
| import pandas as pd | ||
| import numpy as np | ||
| data_dict = {} | ||
| for j, name in enumerate(self.schema.names): | ||
| data_dict[name] = [self.data[i][j] for i in range(len(self.data))] | ||
| # need to convert these to numpy types first | ||
| data_dict["2_int_t"] = np.int32(data_dict["2_int_t"]) | ||
| data_dict["4_float_t"] = np.float32(data_dict["4_float_t"]) | ||
| return pd.DataFrame(data=data_dict) | ||
|
|
||
| def test_unsupported_datatype(self): | ||
| schema = StructType([StructField("dt", DateType(), True)]) | ||
| df = self.spark.createDataFrame([(datetime.date(1970, 1, 1),)], schema=schema) | ||
|
|
@@ -3121,21 +3137,15 @@ def test_null_conversion(self): | |
| def test_toPandas_arrow_toggle(self): | ||
| df = self.spark.createDataFrame(self.data, schema=self.schema) | ||
| self.spark.conf.set("spark.sql.execution.arrow.enabled", "false") | ||
| pdf = df.toPandas() | ||
| self.spark.conf.set("spark.sql.execution.arrow.enabled", "true") | ||
| try: | ||
| pdf = df.toPandas() | ||
| finally: | ||
| self.spark.conf.set("spark.sql.execution.arrow.enabled", "true") | ||
| pdf_arrow = df.toPandas() | ||
| self.assertFramesEqual(pdf_arrow, pdf) | ||
|
|
||
| def test_pandas_round_trip(self): | ||
| import pandas as pd | ||
| import numpy as np | ||
| data_dict = {} | ||
| for j, name in enumerate(self.schema.names): | ||
| data_dict[name] = [self.data[i][j] for i in range(len(self.data))] | ||
| # need to convert these to numpy types first | ||
| data_dict["2_int_t"] = np.int32(data_dict["2_int_t"]) | ||
| data_dict["4_float_t"] = np.float32(data_dict["4_float_t"]) | ||
| pdf = pd.DataFrame(data=data_dict) | ||
| pdf = self.createPandasDataFrameFromData() | ||
| df = self.spark.createDataFrame(self.data, schema=self.schema) | ||
| pdf_arrow = df.toPandas() | ||
| self.assertFramesEqual(pdf_arrow, pdf) | ||
|
|
@@ -3147,6 +3157,41 @@ def test_filtered_frame(self): | |
| self.assertEqual(pdf.columns[0], "i") | ||
| self.assertTrue(pdf.empty) | ||
|
|
||
| def test_createDataFrame_toggle(self): | ||
| pdf = self.createPandasDataFrameFromData() | ||
| self.spark.conf.set("spark.sql.execution.arrow.enabled", "false") | ||
| try: | ||
| df_no_arrow = self.spark.createDataFrame(pdf) | ||
| finally: | ||
| self.spark.conf.set("spark.sql.execution.arrow.enabled", "true") | ||
| df_arrow = self.spark.createDataFrame(pdf) | ||
| self.assertEquals(df_no_arrow.collect(), df_arrow.collect()) | ||
|
|
||
| def test_createDataFrame_with_schema(self): | ||
| pdf = self.createPandasDataFrameFromData() | ||
| df = self.spark.createDataFrame(pdf, schema=self.schema) | ||
| self.assertEquals(self.schema, df.schema) | ||
| pdf_arrow = df.toPandas() | ||
| self.assertFramesEqual(pdf_arrow, pdf) | ||
|
|
||
| def test_createDataFrame_with_incorrect_schema(self): | ||
| pdf = self.createPandasDataFrameFromData() | ||
| wrong_schema = StructType([field for field in reversed(self.schema)]) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not a big deal at all: |
||
| with QuietTest(self.sc): | ||
| with self.assertRaisesRegexp(TypeError, ".*field.*can.not.accept.*type"): | ||
| self.spark.createDataFrame(pdf, schema=wrong_schema) | ||
|
|
||
| def test_createDataFrame_with_names(self): | ||
| pdf = self.createPandasDataFrameFromData() | ||
| df = self.spark.createDataFrame(pdf, schema=list('abcde')) | ||
| self.assertEquals(df.schema.fieldNames(), list('abcde')) | ||
|
|
||
| def test_schema_conversion_roundtrip(self): | ||
| from pyspark.sql.types import from_arrow_schema, to_arrow_schema | ||
| arrow_schema = to_arrow_schema(self.schema) | ||
| schema_rt = from_arrow_schema(arrow_schema) | ||
| self.assertEquals(self.schema, schema_rt) | ||
|
|
||
|
|
||
| @unittest.skipIf(not _have_pandas or not _have_arrow, "Pandas or Arrow not installed") | ||
| class VectorizedUDFTests(ReusedPySparkTestCase): | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isinstance(schema, list)->isinstance(schema, (list, tuple))maybe?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe, a test like
spark.createDataFrame([[1]], ("v",))would be great.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like this check should include tuples as well for converting from unicode?
https://github.com/apache/spark/pull/19459/files#diff-3b5463566251d5b09fd328738a9e9bc5L579
I'll change that since it's related..