-
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 5 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 | ||
|
|
||
|
|
@@ -510,9 +511,43 @@ def createDataFrame(self, data, schema=None, samplingRatio=None, verifySchema=Tr | |
| except Exception: | ||
| has_pandas = False | ||
| if has_pandas and isinstance(data, pandas.DataFrame): | ||
| if schema is None: | ||
| schema = [str(x) for x in data.columns] | ||
| data = [r.tolist() for r in data.to_records(index=False)] | ||
| if self.conf.get("spark.sql.execution.arrow.enable", "false").lower() == "true" \ | ||
| and len(data) > 0: | ||
| from pyspark.serializers import ArrowSerializer | ||
|
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. Maybe we should split this block to a method like
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. That's probably a good idea since it's a big block of code. The other create methods return a (rdd, schema) pair, then do further processing to create a DataFrame. Here we would have to just return a DataFrame since we don't want to do the further processing.
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. Oh wait, without Arrow it creates a (rdd, schema) pair like the others, so having with Arrow and without in |
||
| from pyspark.sql.types import from_arrow_schema | ||
| import pyarrow as pa | ||
|
|
||
| # Slice the DataFrame into batches | ||
| split = -(-len(data) // self.sparkContext.defaultParallelism) # round int up | ||
| slices = (data[i:i + split] for i in xrange(0, len(data), split)) | ||
|
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. How about
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. sure, how about size, start and step? That looks like the terminology used in
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. Yea, sounds fine. |
||
| batches = [pa.RecordBatch.from_pandas(sliced_df, preserve_index=False) | ||
| for sliced_df in slices] | ||
|
|
||
| # write batches to temp file, read by JVM (borrowed from context.parallelize) | ||
| import os | ||
| from tempfile import NamedTemporaryFile | ||
|
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'd put those imports above with |
||
| tempFile = NamedTemporaryFile(delete=False, dir=self._sc._temp_dir) | ||
| try: | ||
| serializer = ArrowSerializer() | ||
| serializer.dump_stream(batches, tempFile) | ||
| tempFile.close() | ||
| readRDDFromFile = self._jvm.PythonRDD.readRDDFromFile | ||
| jrdd = readRDDFromFile(self._jsc, tempFile.name, len(batches)) | ||
| finally: | ||
| # readRDDFromFile eagerily reads the file so we can delete right after. | ||
| os.unlink(tempFile.name) | ||
|
|
||
| # Create the Spark DataFrame, there will be at least 1 batch | ||
| schema = from_arrow_schema(batches[0].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. What if a user specify the schema?
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. Good point. We can pass the schema, if provided, into |
||
| jdf = self._jvm.org.apache.spark.sql.execution.arrow.ArrowConverters.toDataFrame( | ||
| jrdd, schema.json(), self._wrapped._jsqlContext) | ||
| df = DataFrame(jdf, self._wrapped) | ||
| df._schema = 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. I'd leave some comments here about why
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. If the schema is not set here, then it will lazily create it through a py4j exchange with the java DataFrame. Since we already have it here, we can just set it and save some time. I don't like manually setting it like this though, it should be an optional arg in the DataFrame constructor. I'll make that change, but if you prefer not to do that I can revert.
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. Ahh, okay, that's fine to me. |
||
| return df | ||
| else: | ||
| if schema is None: | ||
| schema = [str(x) for x in data.columns] | ||
| data = [r.tolist() for r in data.to_records(index=False)] | ||
|
|
||
| if isinstance(schema, StructType): | ||
| verify_func = _make_type_verifier(schema) if verifySchema else lambda _: True | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3095,16 +3095,27 @@ 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)] | ||
|
|
||
| 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 createPandasDataFrameFromeData(self): | ||
|
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. nit: typo |
||
| 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 +3132,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.enable", "false") | ||
| pdf = df.toPandas() | ||
| self.spark.conf.set("spark.sql.execution.arrow.enable", "true") | ||
| try: | ||
| pdf = df.toPandas() | ||
| finally: | ||
| self.spark.conf.set("spark.sql.execution.arrow.enable", "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.createPandasDataFrameFromeData() | ||
| df = self.spark.createDataFrame(self.data, schema=self.schema) | ||
| pdf_arrow = df.toPandas() | ||
| self.assertFramesEqual(pdf_arrow, pdf) | ||
|
|
@@ -3147,6 +3152,16 @@ def test_filtered_frame(self): | |
| self.assertEqual(pdf.columns[0], "i") | ||
| self.assertTrue(pdf.empty) | ||
|
|
||
| def test_createDataFrame_toggle(self): | ||
| pdf = self.createPandasDataFrameFromeData() | ||
| self.spark.conf.set("spark.sql.execution.arrow.enable", "false") | ||
| try: | ||
| df_no_arrow = self.spark.createDataFrame(pdf) | ||
| finally: | ||
| self.spark.conf.set("spark.sql.execution.arrow.enable", "true") | ||
| df_arrow = self.spark.createDataFrame(pdf) | ||
| self.assertEquals(df_no_arrow.collect(), df_arrow.collect()) | ||
|
|
||
|
|
||
| @unittest.skipIf(not _have_pandas or not _have_arrow, "Pandas or Arrow not installed") | ||
| class VectorizedUDFTests(ReusedPySparkTestCase): | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -29,6 +29,8 @@ import org.apache.arrow.vector.schema.ArrowRecordBatch | |
| import org.apache.arrow.vector.util.ByteArrayReadableSeekableByteChannel | ||
|
|
||
| import org.apache.spark.TaskContext | ||
| import org.apache.spark.api.java.JavaRDD | ||
| import org.apache.spark.sql.{DataFrame, SQLContext} | ||
| import org.apache.spark.sql.catalyst.InternalRow | ||
| import org.apache.spark.sql.execution.vectorized.{ArrowColumnVector, ColumnarBatch, ColumnVector} | ||
| import org.apache.spark.sql.types._ | ||
|
|
@@ -203,4 +205,16 @@ private[sql] object ArrowConverters { | |
| reader.close() | ||
| } | ||
| } | ||
|
|
||
| def toDataFrame( | ||
|
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 had to make this public to be callable with py4j. Alternatively, something could be added to
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. Yup, I think we should put it there,
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 left the conversion logic in |
||
| arrowRDD: JavaRDD[Array[Byte]], | ||
| schemaString: String, | ||
| sqlContext: SQLContext): DataFrame = { | ||
| val rdd = arrowRDD.rdd.mapPartitions { iter => | ||
| val context = TaskContext.get() | ||
| ArrowConverters.fromPayloadIterator(iter.map(new ArrowPayload(_)), context) | ||
| } | ||
| val schema = DataType.fromJson(schemaString).asInstanceOf[StructType] | ||
| sqlContext.internalCreateDataFrame(rdd, schema) | ||
| } | ||
| } | ||
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.
The config name was modified to
spark.sql.execution.arrow.enabledat d29d1e8 and af8a34c.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.
Oh thanks, I didn't see that go in. I'll update.