Skip to content
Closed
4 changes: 4 additions & 0 deletions python/pyspark/sql/pandas/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ def _create_batch(self, series):
"""
import pandas as pd
import pyarrow as pa

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: remove newline

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done

from pyspark.sql.pandas.types import _check_series_convert_timestamps_internal
# Make input conform to [(series1, type1), (series2, type2), ...]
if not isinstance(series, (list, tuple)) or \
Expand All @@ -154,6 +155,9 @@ def create_array(s, t):
# Ensure timestamp series are in expected form for Spark internal representation
if t is not None and pa.types.is_timestamp(t):
s = _check_series_convert_timestamps_internal(s, self._timezone)
elif type(s.dtype) == pd.CategoricalDtype:
# FIXME: This can be removed once minimum pyarrow version is >= 0.16.1

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.

please change FIXME -> NOTE. It sounds like we are adding broken code, which isn't the case. It's just not needed after a certain version.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done

s = s.astype(s.dtypes.categories.dtype)
try:
array = pa.Array.from_pandas(s, mask=mask, type=t, safe=self._safecheck)
except pa.ArrowException as e:
Expand Down
2 changes: 2 additions & 0 deletions python/pyspark/sql/pandas/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ def from_arrow_type(at):
return StructType(
[StructField(field.name, from_arrow_type(field.type), nullable=field.nullable)
for field in at])
elif types.is_dictionary(at):
spark_type = from_arrow_type(at.value_type)
else:
raise TypeError("Unsupported type in conversion from Arrow: " + str(at))
return spark_type
Expand Down
14 changes: 14 additions & 0 deletions python/pyspark/sql/tests/test_arrow.py
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,20 @@ def run_test(num_records, num_parts, max_records, use_delay=False):
for case in cases:
run_test(*case)

def test_createDateFrame_with_category_type(self):
pdf = pd.DataFrame({"A": [u"a", u"b", u"c", u"a"]})
pdf["B"] = pdf["A"].astype('category')

with self.sql_conf({"spark.sql.execution.arrow.pyspark.enabled": True}):
arrow_df = self.spark.createDataFrame(pdf)
result_arrow = arrow_df.toPandas()

with self.sql_conf({"spark.sql.execution.arrow.pyspark.enabled": False}):
df = self.spark.createDataFrame(pdf)
result_spark = df.toPandas()

assert_frame_equal(result_spark, result_arrow)

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.

could you add an assert that the Spark DataFrame has column "B" as a string type?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done, move other test checks here too.


@unittest.skipIf(
not have_pandas or not have_pyarrow,
Expand Down
24 changes: 24 additions & 0 deletions python/pyspark/sql/tests/test_pandas_udf_scalar.py
Original file line number Diff line number Diff line change
Expand Up @@ -897,6 +897,30 @@ def test_timestamp_dst(self):
result = df.withColumn('time', foo_udf(df.time))
self.assertEquals(df.collect(), result.collect())

def test_createDateFrame_with_category_type(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.

This test module is for pandas_udfs, not for createDataFrame. We do need to add a pandas_udf that tests this. The user would specify a return type of string and then return a categorical pandas.Series that has string categories. For example:

@pandas_udf('string')
def f(x):
    return x.astype('category')

pdf = pd.DataFrame({"A": [u"a", u"b", u"c", u"a"]})
df = spark.createDataFrame(pdf).withColumn("B", f(col("A")))
result = df.toPandas()
# Check result "B" is equal to "A"

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

aha, got it. fixed

pdf = pd.DataFrame({"A": [u"a", u"b", u"c", u"a"]})
pdf["B"] = pdf["A"].astype('category')
category_first_element = dict(enumerate(pdf['B'].cat.categories))[0]

with self.sql_conf({"spark.sql.execution.arrow.pyspark.enabled": True}):
arrow_df = self.spark.createDataFrame(pdf)
arrow_type = arrow_df.dtypes[1][1]
result_arrow = arrow_df.collect()
arrow_first_category_element = result_arrow[0][1]

with self.sql_conf({"spark.sql.execution.arrow.pyspark.enabled": False}):
df = self.spark.createDataFrame(pdf)
spark_type = df.dtypes[1][1]
result_spark = df.collect()
spark_first_category_element = result_spark[0][1]

# ensure original category elements are string
assert isinstance(category_first_element, str)
# spark dataframe and arrow execution mode enabled dataframe type must match padnads
assert spark_type == arrow_type == 'string'
assert isinstance(arrow_first_category_element, str)
assert isinstance(spark_first_category_element, str)

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.

Oh yeah, move these to the other test please.


@unittest.skipIf(sys.version_info[:2] < (3, 5), "Type hints are supported from Python 3.5.")
def test_type_annotation(self):
# Regression test to check if type hints can be used. See SPARK-23569.
Expand Down