-
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 30 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 | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -25,7 +25,7 @@ | |||||||||||||||||||
| basestring = unicode = str | ||||||||||||||||||||
| xrange = range | ||||||||||||||||||||
| else: | ||||||||||||||||||||
| from itertools import imap as map | ||||||||||||||||||||
| from itertools import izip as zip, imap as map | ||||||||||||||||||||
|
|
||||||||||||||||||||
| from pyspark import since | ||||||||||||||||||||
| from pyspark.rdd import RDD, ignore_unicode_prefix | ||||||||||||||||||||
|
|
@@ -417,12 +417,12 @@ def _createFromLocal(self, data, schema): | |||||||||||||||||||
| data = [schema.toInternal(row) for row in data] | ||||||||||||||||||||
| return self._sc.parallelize(data), schema | ||||||||||||||||||||
|
|
||||||||||||||||||||
| def _get_numpy_record_dtypes(self, rec): | ||||||||||||||||||||
| def _get_numpy_record_dtype(self, rec): | ||||||||||||||||||||
| """ | ||||||||||||||||||||
| Used when converting a pandas.DataFrame to Spark using to_records(), this will correct | ||||||||||||||||||||
| the dtypes of records so they can be properly loaded into Spark. | ||||||||||||||||||||
| :param rec: a numpy record to check dtypes | ||||||||||||||||||||
| :return corrected dtypes for a numpy.record or None if no correction needed | ||||||||||||||||||||
| the dtypes of fields in a record so they can be properly loaded into Spark. | ||||||||||||||||||||
| :param rec: a numpy record to check field dtypes | ||||||||||||||||||||
| :return corrected dtype for a numpy.record or None if no correction needed | ||||||||||||||||||||
| """ | ||||||||||||||||||||
| import numpy as np | ||||||||||||||||||||
| cur_dtypes = rec.dtype | ||||||||||||||||||||
|
|
@@ -438,7 +438,7 @@ def _get_numpy_record_dtypes(self, rec): | |||||||||||||||||||
| curr_type = 'datetime64[us]' | ||||||||||||||||||||
| has_rec_fix = True | ||||||||||||||||||||
| record_type_list.append((str(col_names[i]), curr_type)) | ||||||||||||||||||||
| return record_type_list if has_rec_fix else None | ||||||||||||||||||||
| return np.dtype(record_type_list) if has_rec_fix else None | ||||||||||||||||||||
|
|
||||||||||||||||||||
| def _convert_from_pandas(self, pdf, schema): | ||||||||||||||||||||
| """ | ||||||||||||||||||||
|
|
@@ -454,13 +454,60 @@ def _convert_from_pandas(self, pdf, schema): | |||||||||||||||||||
|
|
||||||||||||||||||||
| # Check if any columns need to be fixed for Spark to infer properly | ||||||||||||||||||||
| if len(np_records) > 0: | ||||||||||||||||||||
| record_type_list = self._get_numpy_record_dtypes(np_records[0]) | ||||||||||||||||||||
| if record_type_list is not None: | ||||||||||||||||||||
| return [r.astype(record_type_list).tolist() for r in np_records], schema | ||||||||||||||||||||
| record_dtype = self._get_numpy_record_dtype(np_records[0]) | ||||||||||||||||||||
| if record_dtype is not None: | ||||||||||||||||||||
| return [r.astype(record_dtype).tolist() for r in np_records], schema | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Convert list of numpy records to python lists | ||||||||||||||||||||
| return [r.tolist() for r in np_records], schema | ||||||||||||||||||||
|
|
||||||||||||||||||||
| def _create_from_pandas_with_arrow(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, _create_batch | ||||||||||||||||||||
| from pyspark.sql.types import from_arrow_schema, to_arrow_type, TimestampType | ||||||||||||||||||||
| from pandas.api.types import is_datetime64_dtype, is_datetime64tz_dtype | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Determine arrow types to coerce data when creating batches | ||||||||||||||||||||
| if isinstance(schema, StructType): | ||||||||||||||||||||
| arrow_types = [to_arrow_type(f.dataType) for f in schema.fields] | ||||||||||||||||||||
| elif isinstance(schema, DataType): | ||||||||||||||||||||
| raise ValueError("Single data type %s is not supported with Arrow" % str(schema)) | ||||||||||||||||||||
| else: | ||||||||||||||||||||
| # Any timestamps must be coerced to be compatible with Spark | ||||||||||||||||||||
| arrow_types = [to_arrow_type(TimestampType()) | ||||||||||||||||||||
| if is_datetime64_dtype(t) or is_datetime64tz_dtype(t) else None | ||||||||||||||||||||
| for t in pdf.dtypes] | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Slice the DataFrame to be batched | ||||||||||||||||||||
| step = -(-len(pdf) // self.sparkContext.defaultParallelism) # round int up | ||||||||||||||||||||
| pdf_slices = (pdf[start:start + step] for start in xrange(0, len(pdf), step)) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Create Arrow record batches | ||||||||||||||||||||
| batches = [_create_batch([(c, t) for (_, c), t in zip(pdf_slice.iteritems(), arrow_types)]) | ||||||||||||||||||||
| for pdf_slice in pdf_slices] | ||||||||||||||||||||
|
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. However, if we go 1. way, I think we should avoid whole batches first. I think falling back might make sense if its cost is as cheap as possible. |
||||||||||||||||||||
|
|
||||||||||||||||||||
| # Create the Spark schema from the first Arrow batch (always at least 1 batch after slicing) | ||||||||||||||||||||
| if schema is None or isinstance(schema, list): | ||||||||||||||||||||
|
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
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, a test 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. Looks like this check should include tuples as well for converting from unicode? |
||||||||||||||||||||
| schema_from_arrow = from_arrow_schema(batches[0].schema) | ||||||||||||||||||||
| names = pdf.columns if schema is None else 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. Could we maybe just resemble spark/python/pyspark/sql/session.py Lines 403 to 411 in 1d34104
just to be more readable in a way? if schema is None or isinstance(schema, (list, tuple)):
struct = from_arrow_schema(batches[0].schema)
if isinstance(schema, (list, tuple)):
for i, name in enumerate(schema):
struct.fields[i].name = name
struct.names[i] = name
schema = struct
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. We still need the line for the case when schema is None, because the pdf column names are lost when creating the Arrow RecordBatch from using pandas.Series with
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. |
||||||||||||||||||||
| fields = [] | ||||||||||||||||||||
| for i, field in enumerate(schema_from_arrow): | ||||||||||||||||||||
| field.name = names[i] | ||||||||||||||||||||
| fields.append(field) | ||||||||||||||||||||
| schema = StructType(fields) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # 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): | ||||||||||||||||||||
|
|
@@ -557,6 +604,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 self.conf.get("spark.sql.execution.arrow.enabled", "false").lower() == "true" \ | ||||||||||||||||||||
| and len(data) > 0: | ||||||||||||||||||||
| try: | ||||||||||||||||||||
| return self._create_from_pandas_with_arrow(data, schema) | ||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||
| warnings.warn("Arrow will not be used in createDataFrame: %s" % str(e)) | ||||||||||||||||||||
| # Fallback to create DataFrame without arrow if raise some exception | ||||||||||||||||||||
| data, schema = self._convert_from_pandas(data, schema) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if isinstance(schema, StructType): | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3120,9 +3120,9 @@ def setUpClass(cls): | |
| StructField("5_double_t", DoubleType(), True), | ||
| StructField("6_date_t", DateType(), True), | ||
| StructField("7_timestamp_t", TimestampType(), True)]) | ||
| cls.data = [("a", 1, 10, 0.2, 2.0, datetime(1969, 1, 1), datetime(1969, 1, 1, 1, 1, 1)), | ||
| ("b", 2, 20, 0.4, 4.0, datetime(2012, 2, 2), datetime(2012, 2, 2, 2, 2, 2)), | ||
| ("c", 3, 30, 0.8, 6.0, datetime(2100, 3, 3), datetime(2100, 3, 3, 3, 3, 3))] | ||
| cls.data = [(u"a", 1, 10, 0.2, 2.0, datetime(1969, 1, 1), datetime(1969, 1, 1, 1, 1, 1)), | ||
| (u"b", 2, 20, 0.4, 4.0, datetime(2012, 2, 2), datetime(2012, 2, 2, 2, 2, 2)), | ||
| (u"c", 3, 30, 0.8, 6.0, datetime(2100, 3, 3), datetime(2100, 3, 3, 3, 3, 3))] | ||
|
|
||
| @classmethod | ||
| def tearDownClass(cls): | ||
|
|
@@ -3138,6 +3138,17 @@ def assertFramesEqual(self, df_with_arrow, df_without): | |
| ("\n\nWithout:\n%s\n%s" % (df_without, df_without.dtypes))) | ||
| self.assertTrue(df_without.equals(df_with_arrow), msg=msg) | ||
|
|
||
| def create_pandas_data_frame(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("decimal", DecimalType(), True)]) | ||
| df = self.spark.createDataFrame([(None,)], schema=schema) | ||
|
|
@@ -3154,21 +3165,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.create_pandas_data_frame() | ||
| df = self.spark.createDataFrame(self.data, schema=self.schema) | ||
| pdf_arrow = df.toPandas() | ||
| self.assertFramesEqual(pdf_arrow, pdf) | ||
|
|
@@ -3180,6 +3185,58 @@ def test_filtered_frame(self): | |
| self.assertEqual(pdf.columns[0], "i") | ||
| self.assertTrue(pdf.empty) | ||
|
|
||
| def test_createDataFrame_toggle(self): | ||
| pdf = self.create_pandas_data_frame() | ||
| 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.create_pandas_data_frame() | ||
| 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.create_pandas_data_frame() | ||
| 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.create_pandas_data_frame() | ||
| df = self.spark.createDataFrame(pdf, schema=list('abcdefg')) | ||
| self.assertEquals(df.schema.fieldNames(), list('abcdefg')) | ||
|
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 said above, let's add a test with a tuple too. |
||
|
|
||
| def test_createDataFrame_with_single_data_type(self): | ||
| import pandas as pd | ||
| with QuietTest(self.sc): | ||
| with self.assertRaisesRegexp(TypeError, ".*IntegerType.*tuple"): | ||
| self.spark.createDataFrame(pd.DataFrame({"a": [1]}), schema="int") | ||
|
|
||
| def test_createDataFrame_does_not_modify_input(self): | ||
| # Some series get converted for Spark to consume, this makes sure input is unchanged | ||
| pdf = self.create_pandas_data_frame() | ||
| # Use a nanosecond value to make sure it is not truncated | ||
| pdf.ix[0, '7_timestamp_t'] = 1 | ||
| # Integers with nulls will get NaNs filled with 0 and will be casted | ||
| pdf.ix[1, '2_int_t'] = None | ||
| pdf_copy = pdf.copy(deep=True) | ||
| self.spark.createDataFrame(pdf, schema=self.schema) | ||
| self.assertTrue(pdf.equals(pdf_copy)) | ||
|
|
||
| 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(ReusedSQLTestCase): | ||
|
|
||
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.
Hm, mind asking why
t is not Noneis added? I thoughtNoneisNoneTypeand won't bepa.TimestampTypeanyway.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.
This doesn't seem to be needed anymore. It came from an error when comparing pyarrow type instances to None.
So this check is still needed right below when we check for date32(). I can't remember if this was fixed in current versions of pyarrow, but I'll add a note here.
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.
verified this is not an issue with pyarrow >= 0.7.1