-
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 12 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,43 @@ def _createFromLocal(self, data, schema): | |
| data = [schema.toInternal(row) for row in data] | ||
| return self._sc.parallelize(data), schema | ||
|
|
||
| def _createFromPandasWithArrow(self, df, 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. nit: df -> pdf. |
||
| """ | ||
| Create a DataFrame from a given pandas.DataFrame by slicing the into partitions, converting | ||
|
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. typo: slicing the ...
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! fixed |
||
| to Arrow data, then reading into the JVM to parallelsize. If a schema is passed in, the | ||
|
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. typo:
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, fixed! |
||
| data types will be used to coerce the data in Pandas to Arrow conversion. | ||
| """ | ||
| import os | ||
| from tempfile import NamedTemporaryFile | ||
| from pyspark.serializers import ArrowSerializer | ||
| from pyspark.sql.types import from_arrow_schema, to_arrow_schema | ||
| import pyarrow as pa | ||
|
|
||
| # Slice the DataFrame into batches | ||
| step = -(-len(df) // self.sparkContext.defaultParallelism) # round int up | ||
| df_slices = (df[start:start + step] for start in xrange(0, len(df), step)) | ||
| arrow_schema = to_arrow_schema(schema) if schema is not None else None | ||
| batches = [pa.RecordBatch.from_pandas(df_slice, schema=arrow_schema, preserve_index=False) | ||
| for df_slice in df_slices] | ||
|
|
||
| # write batches to temp file, read by JVM (borrowed from context.parallelize) | ||
| tempFile = NamedTemporaryFile(delete=False, dir=self._sc._temp_dir) | ||
|
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. This looks kind of duplicate with the main logic of
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, it is - I didn't want to mess around with the def _dump_to_tempfile(data, serializer, parallelism):
tempFile = NamedTemporaryFile(delete=False, dir=self._temp_dir)
try:
serializer.dump_stream(c, tempFile)
tempFile.close()
readRDDFromFile = self._jvm.PythonRDD.readRDDFromFile
return readRDDFromFile(self._jsc, tempFile.name, parallelism)
finally:
# readRDDFromFile eagerily reads the file so we can delete right after.
os.unlink(tempFile.name)and some changes to # Make sure we distribute data evenly if it's smaller than self.batchSize
if "__len__" not in dir(c):
c = list(c) # Make it a list so we can compute its length
batchSize = max(1, min(len(c) // numSlices, self._batchSize or 1024))
serializer = BatchedSerializer(self._unbatched_serializer, batchSize)
jrdd = _dump_to_tempfile(c, serializer, numSlices)Let me know if you all think we should change this?
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 prefer less duplicate. Let's see if others support it.
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. Sounds good to me. |
||
| 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.
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. This brings up a good point, the schema should usually be the same, but what should the behavior be if the user specifies the wrong schema?
I'm thinking that (2) is the best since it's the safest and would produce a clear error message.
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. Could we check ahead and fall back to import pandas as pd
import numpy as np
spark.conf.set("spark.sql.execution.arrow.enabled", "false")
spark.createDataFrame(pd.DataFrame(data={"dcol": [0.8]}), schema="dcol STRING").show()
spark.conf.set("spark.sql.execution.arrow.enabled", "true")
spark.createDataFrame(pd.DataFrame(data={"dcol": [0.8]}), schema="dcol STRING").show()WDYT @BryanCutler, @ueshin and @viirya ?
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. There might be some differences because without Arrow, Spark just gets the column names from Pandas and infers the data type. This is by design, but I've seen a lot of users get tripped up by it and create JIRAs. With Arrow, it makes it easy to use the schema from Pandas so I would consider this an improvement.
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. But I thought we should reduce the diff between
but if they are different, fall back 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. Btw, do we also need to support schema 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. Sorry @HyukjinKwon , I misread your example. Yes, we can fall back to not use arrow if the user supplied schema doesn't match from arrow - I actually get equal results from your example tho
@ueshin we could do this too, I'm not sure what would be better in this case.. I'll think about it for a bit
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. Hm.. FWIW, I get something 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. Oh pyarrow 0.4.1 is what is installed on Jenkins, so that is what I've been testing against. Maybe try that version?
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. Hmmmm.. still get the same exception with pyarrow 0.4.1 .. |
||
| jdf = self._jvm.PythonSQLUtils.arrowPayloadToDataFrame( | ||
| jrdd, schema.json(), self._wrapped._jsqlContext) | ||
| return DataFrame(jdf, self._wrapped, schema) | ||
|
|
||
| @since(2.0) | ||
| @ignore_unicode_prefix | ||
| def createDataFrame(self, data, schema=None, samplingRatio=None, verifySchema=True): | ||
|
|
@@ -510,9 +548,13 @@ 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.enabled", "false").lower() == "true" \ | ||
| and len(data) > 0: | ||
| return self._createFromPandasWithArrow(data, schema) | ||
| 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 | ||
|
|
@@ -541,9 +583,7 @@ def prepare(obj): | |
| rdd, schema = self._createFromLocal(map(prepare, data), schema) | ||
| jrdd = self._jvm.SerDeUtil.toJavaArray(rdd._to_java_object_rdd()) | ||
| jdf = self._jsparkSession.applySchemaToPythonRDD(jrdd.rdd(), schema.json()) | ||
| df = DataFrame(jdf, self._wrapped) | ||
| df._schema = schema | ||
| return df | ||
| return DataFrame(jdf, self._wrapped, schema) | ||
|
|
||
| @ignore_unicode_prefix | ||
| @since(2.0) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1624,6 +1624,50 @@ def to_arrow_type(dt): | |
| return arrow_type | ||
|
|
||
|
|
||
| def to_arrow_schema(schema): | ||
| """ Convert a schema from Spark to Arrow | ||
| """ | ||
| import pyarrow as pa | ||
| fields = [pa.field(field.name, to_arrow_type(field.dataType)) for field in 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. We should add
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, good idea |
||
| return pa.schema(fields) | ||
|
|
||
|
|
||
| def from_arrow_type(at): | ||
| """ Convert pyarrow type to Spark data type. | ||
| """ | ||
| # TODO: newer pyarrow has is_boolean(at) functions that would be better to check type | ||
| import pyarrow as pa | ||
| if at == pa.bool_(): | ||
| spark_type = BooleanType() | ||
| elif at == pa.int8(): | ||
| spark_type = ByteType() | ||
| elif at == pa.int16(): | ||
| spark_type = ShortType() | ||
| elif at == pa.int32(): | ||
| spark_type = IntegerType() | ||
| elif at == pa.int64(): | ||
| spark_type = LongType() | ||
| elif at == pa.float32(): | ||
| spark_type = FloatType() | ||
| elif at == pa.float64(): | ||
| spark_type = DoubleType() | ||
| elif type(at) == pa.DecimalType: | ||
| spark_type = DecimalType(precision=at.precision, scale=at.scale) | ||
| elif at == pa.string(): | ||
| spark_type = StringType() | ||
| else: | ||
| raise TypeError("Unsupported type in conversion from Arrow: " + str(at)) | ||
| return spark_type | ||
|
|
||
|
|
||
| def from_arrow_schema(arrow_schema): | ||
| """ Convert schema from Arrow to Spark. | ||
| """ | ||
| return StructType( | ||
| [StructField(field.name, from_arrow_type(field.type), nullable=field.nullable) | ||
| for field in arrow_schema]) | ||
|
|
||
|
|
||
| def _test(): | ||
| import doctest | ||
| from pyspark.context import SparkContext | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,9 +17,12 @@ | |
|
|
||
| package org.apache.spark.sql.api.python | ||
|
|
||
| import org.apache.spark.api.java.JavaRDD | ||
| import org.apache.spark.sql.{DataFrame, SQLContext} | ||
| import org.apache.spark.sql.catalyst.analysis.FunctionRegistry | ||
| import org.apache.spark.sql.catalyst.expressions.ExpressionInfo | ||
| import org.apache.spark.sql.catalyst.parser.CatalystSqlParser | ||
| import org.apache.spark.sql.execution.arrow.ArrowConverters | ||
| import org.apache.spark.sql.types.DataType | ||
|
|
||
| private[sql] object PythonSQLUtils { | ||
|
|
@@ -29,4 +32,19 @@ private[sql] object PythonSQLUtils { | |
| def listBuiltinFunctionInfos(): Array[ExpressionInfo] = { | ||
| FunctionRegistry.functionSet.flatMap(f => FunctionRegistry.builtin.lookupFunction(f)).toArray | ||
| } | ||
|
|
||
| /** | ||
| * Python Callable function to convert ArrowPayloads into a [[DataFrame]]. | ||
| * | ||
| * @param payloadRDD A JavaRDD of ArrowPayloads. | ||
| * @param schemaString JSON Formatted Schema for ArrowPayloads. | ||
| * @param sqlContext The active [[SQLContext]]. | ||
| * @return The converted [[DataFrame]]. | ||
| */ | ||
| def arrowPayloadToDataFrame( | ||
| payloadRDD: JavaRDD[Array[Byte]], | ||
| schemaString: String, | ||
| sqlContext: SQLContext): DataFrame = { | ||
|
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 can't believe I found this looks 5 spaces instead of 4.
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 man, good catch! I don't know how that happened :\ |
||
| ArrowConverters.toDataFrame(payloadRDD, schemaString, sqlContext) | ||
| } | ||
| } | ||
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.
@BryanCutler, what do you think about taking this out back? Maybe, I am too much worried but I think we maybe should avoid it to be assigned actually except for few special cases ...
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.
Sure, I can undo it. I don't really like manually assigning the schema after the constructor but it's just done in these 2 special cases..