Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
cd3d51e
createDataFrame working but with fixed schema in python
BryanCutler Sep 29, 2017
c73c7c6
added schema conversion
BryanCutler Oct 4, 2017
e9c6de7
add from_arrow_schema, test and cleanup
BryanCutler Oct 9, 2017
06b033f
fix style
BryanCutler Oct 9, 2017
9d667c6
fixed xrange for Python 3
BryanCutler Oct 10, 2017
31851f8
Merge remote-tracking branch 'upstream/master' into arrow-createDataF…
BryanCutler Oct 10, 2017
ca474db
moved python jvm call to PythonSQLUtils, added tearDownClass to tests
BryanCutler Oct 10, 2017
c7ddee6
forgot to rename conf
BryanCutler Oct 10, 2017
b00a924
fixed typo
BryanCutler Oct 13, 2017
e36a176
using schema if passed in to createDataFrame, added unit test to veri…
BryanCutler Oct 14, 2017
fc3a554
Merge remote-tracking branch 'upstream/master' into arrow-createDataF…
BryanCutler Oct 14, 2017
f42e351
updated function name to_arrow_type
BryanCutler Oct 14, 2017
76e87dc
revert DataFrame schema arg, added test for wrong schema, fixed typos
BryanCutler Oct 16, 2017
81ddfa9
moved common code between parallelize to _serialize_to_jvm
BryanCutler Oct 18, 2017
5e8e11f
when schema provided, attempt to cast series and fallback if not matc…
BryanCutler Oct 18, 2017
3052f30
added support for schema as list of names
BryanCutler Oct 18, 2017
9f7b1c0
Simplify `_createFromPandasWithArrow()`.
ueshin Oct 19, 2017
dc03657
changed to use izip
BryanCutler Oct 24, 2017
f421e2d
added check for case of specifying schema with like 'int'
BryanCutler Oct 24, 2017
0de3126
changed single type to fallback and error
BryanCutler Oct 24, 2017
c41cf33
Merge remote-tracking branch 'upstream/master' into arrow-createDataF…
BryanCutler Oct 27, 2017
b6df7bf
add support for date and timestamp for from_arrow_type
BryanCutler Oct 27, 2017
cfb1c3d
using _create_batch to make arrow batches also without explicit schem…
BryanCutler Oct 30, 2017
b362b9a
Merge remote-tracking branch 'upstream/master' into arrow-createDataF…
BryanCutler Nov 7, 2017
1c244d1
some minor cleanup of _convert_from_pandas
BryanCutler Nov 7, 2017
99ce1e4
minor cleanup of _create_from_pandas_with_arrow
BryanCutler Nov 7, 2017
7d9cc3e
avoid double copies of series with nulls
BryanCutler Nov 9, 2017
126f2e7
added test to make sure input is unchanged
BryanCutler Nov 9, 2017
421d0be
removed copy=True option, did not improve anything
BryanCutler Nov 9, 2017
0ad736b
fix pydoc
BryanCutler Nov 9, 2017
6c72e37
added schema tuple support, refactored creating schema
BryanCutler Nov 10, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 38 additions & 3 deletions python/pyspark/sql/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

if sys.version >= '3':
basestring = unicode = str
xrange = range
else:
from itertools import imap as map

Expand Down Expand Up @@ -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" \

Copy link
Copy Markdown
Member

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.enabled at d29d1e8 and af8a34c.

Copy link
Copy Markdown
Member Author

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.

and len(data) > 0:
from pyspark.serializers import ArrowSerializer

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should split this block to a method like _createFromPandasDataFrame as the same as the other create methods?

@BryanCutler BryanCutler Oct 13, 2017

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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.

@BryanCutler BryanCutler Oct 13, 2017

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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 _createFromPandasDataFrame doesn't fit because they return different things. How about just putting the new arrow code in a method like _createFromArrowPandas?

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))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about split -> size (or length) and i -> offset (or start)?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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 parallelize

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd put those imports above with import pyarrow as pa or top of this file ...

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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if a user specify the schema?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. We can pass the schema, if provided, into to_pandas for pyarrow to use when creating the RecordBatch.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd leave some comments here about why _schema should be manually set rather than using it's own schema. Actually, mind if I ask why should we set this manually?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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
Expand Down
43 changes: 29 additions & 14 deletions python/pyspark/sql/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: typo createPandasDataFrameFromeData -> createPandasDataFrameFromData

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)
Expand All @@ -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)
Expand All @@ -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):
Expand Down
36 changes: 36 additions & 0 deletions python/pyspark/sql/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -1624,6 +1624,42 @@ def toArrowType(dt):
return arrow_type


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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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._
Expand Down Expand Up @@ -203,4 +205,16 @@ private[sql] object ArrowConverters {
reader.close()
}
}

def toDataFrame(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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 o.a.s.sql.api.python.PythonSQLUtils?

@HyukjinKwon HyukjinKwon Oct 10, 2017

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, I think we should put it there, o.a.s.sql.api.python.PythonSQLUtils.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left the conversion logic in ArrowConverters because I think there is a good chance it will change, so just added a wrapper to PythonSQLUtils let me know if it's ok.

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)
}
}