-
Notifications
You must be signed in to change notification settings - Fork 28.9k
[WIP][SPARK-20396][SQL][PySpark][FOLLOW-UP] groupby().apply() with pandas udf #19505
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 all commits
4d2bd95
f096870
639af2c
10512a6
789e642
122a7bc
fdafb35
94d05f4
7332969
85f250d
1ef25c3
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 | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -28,7 +28,7 @@ | |||||||||||||||||||||
| from pyspark import since, SparkContext | ||||||||||||||||||||||
| from pyspark.rdd import _prepare_for_python_RDD, ignore_unicode_prefix | ||||||||||||||||||||||
| from pyspark.serializers import PickleSerializer, AutoBatchedSerializer | ||||||||||||||||||||||
| from pyspark.sql.types import StringType, DataType, _parse_datatype_string | ||||||||||||||||||||||
| from pyspark.sql.types import StringType, StructType, DataType, _parse_datatype_string | ||||||||||||||||||||||
| from pyspark.sql.column import Column, _to_java_column, _to_seq | ||||||||||||||||||||||
| from pyspark.sql.dataframe import DataFrame | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
@@ -2038,13 +2038,22 @@ def _wrap_function(sc, func, returnType): | |||||||||||||||||||||
| sc.pythonVer, broadcast_vars, sc._javaAccumulator) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| class PythonUdfType(object): | ||||||||||||||||||||||
| # row-based UDFs | ||||||||||||||||||||||
| NORMAL_UDF = 0 | ||||||||||||||||||||||
| # scalar vectorized UDFs | ||||||||||||||||||||||
| PANDAS_UDF = 1 | ||||||||||||||||||||||
| # grouped vectorized UDFs | ||||||||||||||||||||||
| PANDAS_GROUPED_UDF = 2 | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| class UserDefinedFunction(object): | ||||||||||||||||||||||
| """ | ||||||||||||||||||||||
| User defined function in Python | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| .. versionadded:: 1.3 | ||||||||||||||||||||||
| """ | ||||||||||||||||||||||
| def __init__(self, func, returnType, name=None, vectorized=False): | ||||||||||||||||||||||
| def __init__(self, func, returnType, name=None, pythonUdfType=PythonUdfType.NORMAL_UDF): | ||||||||||||||||||||||
| if not callable(func): | ||||||||||||||||||||||
| raise TypeError( | ||||||||||||||||||||||
| "Not a function or callable (__call__ is not defined): " | ||||||||||||||||||||||
|
|
@@ -2058,7 +2067,7 @@ def __init__(self, func, returnType, name=None, vectorized=False): | |||||||||||||||||||||
| self._name = name or ( | ||||||||||||||||||||||
| func.__name__ if hasattr(func, '__name__') | ||||||||||||||||||||||
| else func.__class__.__name__) | ||||||||||||||||||||||
| self.vectorized = vectorized | ||||||||||||||||||||||
| self.pythonUdfType = pythonUdfType | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| @property | ||||||||||||||||||||||
| def returnType(self): | ||||||||||||||||||||||
|
|
@@ -2090,7 +2099,7 @@ def _create_judf(self): | |||||||||||||||||||||
| wrapped_func = _wrap_function(sc, self.func, self.returnType) | ||||||||||||||||||||||
| jdt = spark._jsparkSession.parseDataType(self.returnType.json()) | ||||||||||||||||||||||
| judf = sc._jvm.org.apache.spark.sql.execution.python.UserDefinedPythonFunction( | ||||||||||||||||||||||
| self._name, wrapped_func, jdt, self.vectorized) | ||||||||||||||||||||||
| self._name, wrapped_func, jdt, self.pythonUdfType) | ||||||||||||||||||||||
| return judf | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def __call__(self, *cols): | ||||||||||||||||||||||
|
|
@@ -2121,33 +2130,40 @@ def wrapper(*args): | |||||||||||||||||||||
|
|
||||||||||||||||||||||
| wrapper.func = self.func | ||||||||||||||||||||||
| wrapper.returnType = self.returnType | ||||||||||||||||||||||
| wrapper.vectorized = self.vectorized | ||||||||||||||||||||||
| wrapper.pythonUdfType = self.pythonUdfType | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return wrapper | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def _create_udf(f, returnType, vectorized): | ||||||||||||||||||||||
| def _create_udf(f, returnType, pythonUdfType): | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def _udf(f, returnType=StringType(), vectorized=vectorized): | ||||||||||||||||||||||
| if vectorized: | ||||||||||||||||||||||
| def _udf(f, returnType=StringType(), pythonUdfType=pythonUdfType): | ||||||||||||||||||||||
| if pythonUdfType == PythonUdfType.PANDAS_UDF: | ||||||||||||||||||||||
| import inspect | ||||||||||||||||||||||
| argspec = inspect.getargspec(f) | ||||||||||||||||||||||
| if len(argspec.args) == 0 and argspec.varargs is None: | ||||||||||||||||||||||
| raise ValueError( | ||||||||||||||||||||||
| "0-arg pandas_udfs are not supported. " | ||||||||||||||||||||||
| "Instead, create a 1-arg pandas_udf and ignore the arg in your function." | ||||||||||||||||||||||
|
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 also update this error message, 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. Thanks! I'll update the message.
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. Hmm, when pandas_grouped_udfs, the number of args should be only 1?
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 think so. If it didn't become too complicated, maybe we can also check it for pandas_grouped_udf.
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, let me try. |
||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| udf_obj = UserDefinedFunction(f, returnType, vectorized=vectorized) | ||||||||||||||||||||||
| elif pythonUdfType == PythonUdfType.PANDAS_GROUPED_UDF: | ||||||||||||||||||||||
| import inspect | ||||||||||||||||||||||
| argspec = inspect.getargspec(f) | ||||||||||||||||||||||
| if len(argspec.args) != 1 and argspec.varargs is None: | ||||||||||||||||||||||
| raise ValueError("Only 1-arg pandas_grouped_udfs are supported.") | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| udf_obj = UserDefinedFunction(f, returnType, pythonUdfType=pythonUdfType) | ||||||||||||||||||||||
| return udf_obj._wrapped() | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # decorator @udf, @udf(), @udf(dataType()), or similar with @pandas_udf | ||||||||||||||||||||||
| # decorator @udf, @udf(), @udf(dataType()), or similar with @pandas_udf and @pandas_grouped_udf | ||||||||||||||||||||||
| if f is None or isinstance(f, (str, DataType)): | ||||||||||||||||||||||
| # If DataType has been passed as a positional argument | ||||||||||||||||||||||
| # for decorator use it as a returnType | ||||||||||||||||||||||
| return_type = f or returnType | ||||||||||||||||||||||
| return functools.partial(_udf, returnType=return_type, vectorized=vectorized) | ||||||||||||||||||||||
| return functools.partial( | ||||||||||||||||||||||
| _udf, returnType=return_type, pythonUdfType=pythonUdfType) | ||||||||||||||||||||||
| else: | ||||||||||||||||||||||
| return _udf(f=f, returnType=returnType, vectorized=vectorized) | ||||||||||||||||||||||
| return _udf(f=f, returnType=returnType, pythonUdfType=pythonUdfType) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| @since(1.3) | ||||||||||||||||||||||
|
|
@@ -2181,7 +2197,7 @@ def udf(f=None, returnType=StringType()): | |||||||||||||||||||||
| | 8| JOHN DOE| 22| | ||||||||||||||||||||||
| +----------+--------------+------------+ | ||||||||||||||||||||||
| """ | ||||||||||||||||||||||
| return _create_udf(f, returnType=returnType, vectorized=False) | ||||||||||||||||||||||
| return _create_udf(f, returnType=returnType, pythonUdfType=PythonUdfType.NORMAL_UDF) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| @since(2.3) | ||||||||||||||||||||||
|
|
@@ -2192,67 +2208,82 @@ def pandas_udf(f=None, returnType=StringType()): | |||||||||||||||||||||
| :param f: user-defined function. A python function if used as a standalone function | ||||||||||||||||||||||
| :param returnType: a :class:`pyspark.sql.types.DataType` object | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| The user-defined function can define one of the following transformations: | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| 1. One or more `pandas.Series` -> A `pandas.Series` | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| This udf is used with :meth:`pyspark.sql.DataFrame.withColumn` and | ||||||||||||||||||||||
| :meth:`pyspark.sql.DataFrame.select`. | ||||||||||||||||||||||
| The returnType should be a primitive data type, e.g., `DoubleType()`. | ||||||||||||||||||||||
| The length of the returned `pandas.Series` must be of the same as the input `pandas.Series`. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| >>> from pyspark.sql.types import IntegerType, StringType | ||||||||||||||||||||||
| >>> slen = pandas_udf(lambda s: s.str.len(), IntegerType()) | ||||||||||||||||||||||
| >>> @pandas_udf(returnType=StringType()) | ||||||||||||||||||||||
| ... def to_upper(s): | ||||||||||||||||||||||
| ... return s.str.upper() | ||||||||||||||||||||||
| ... | ||||||||||||||||||||||
| >>> @pandas_udf(returnType="integer") | ||||||||||||||||||||||
| ... def add_one(x): | ||||||||||||||||||||||
| ... return x + 1 | ||||||||||||||||||||||
| ... | ||||||||||||||||||||||
| >>> df = spark.createDataFrame([(1, "John Doe", 21)], ("id", "name", "age")) | ||||||||||||||||||||||
| >>> df.select(slen("name").alias("slen(name)"), to_upper("name"), add_one("age")) \\ | ||||||||||||||||||||||
| ... .show() # doctest: +SKIP | ||||||||||||||||||||||
| +----------+--------------+------------+ | ||||||||||||||||||||||
| |slen(name)|to_upper(name)|add_one(age)| | ||||||||||||||||||||||
| +----------+--------------+------------+ | ||||||||||||||||||||||
| | 8| JOHN DOE| 22| | ||||||||||||||||||||||
| +----------+--------------+------------+ | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| 2. A `pandas.DataFrame` -> A `pandas.DataFrame` | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| This udf is only used with :meth:`pyspark.sql.GroupedData.apply`. | ||||||||||||||||||||||
| The returnType should be a :class:`StructType` describing the schema of the returned | ||||||||||||||||||||||
| `pandas.DataFrame`. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| >>> df = spark.createDataFrame( | ||||||||||||||||||||||
| ... [(1, 1.0), (1, 2.0), (2, 3.0), (2, 5.0), (2, 10.0)], | ||||||||||||||||||||||
| ... ("id", "v")) | ||||||||||||||||||||||
| >>> @pandas_udf(returnType=df.schema) | ||||||||||||||||||||||
| ... def normalize(pdf): | ||||||||||||||||||||||
| ... v = pdf.v | ||||||||||||||||||||||
| ... return pdf.assign(v=(v - v.mean()) / v.std()) | ||||||||||||||||||||||
| >>> df.groupby('id').apply(normalize).show() # doctest: +SKIP | ||||||||||||||||||||||
| +---+-------------------+ | ||||||||||||||||||||||
| | id| v| | ||||||||||||||||||||||
| +---+-------------------+ | ||||||||||||||||||||||
| | 1|-0.7071067811865475| | ||||||||||||||||||||||
| | 1| 0.7071067811865475| | ||||||||||||||||||||||
| | 2|-0.8320502943378437| | ||||||||||||||||||||||
| | 2|-0.2773500981126146| | ||||||||||||||||||||||
| | 2| 1.1094003924504583| | ||||||||||||||||||||||
| +---+-------------------+ | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| .. note:: This type of udf cannot be used with functions such as `withColumn` or `select` | ||||||||||||||||||||||
| because it defines a `DataFrame` transformation rather than a `Column` | ||||||||||||||||||||||
| transformation. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| .. seealso:: :meth:`pyspark.sql.GroupedData.apply` | ||||||||||||||||||||||
| The user-defined function can define the following transformation: | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| One or more `pandas.Series` -> A `pandas.Series` | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| This udf is used with :meth:`pyspark.sql.DataFrame.withColumn` and | ||||||||||||||||||||||
| :meth:`pyspark.sql.DataFrame.select`. | ||||||||||||||||||||||
| The returnType should be a primitive data type, e.g., `DoubleType()`. | ||||||||||||||||||||||
|
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 happened if we do not pass a primitive data type? Do we have a test case for this?
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. It will fail in runtime. I'll add tests. |
||||||||||||||||||||||
| The length of the returned `pandas.Series` must be of the same as the input `pandas.Series`. | ||||||||||||||||||||||
|
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. Is this just a fact? or an input requirement?
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. It's an output requirement.
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. Can users break this requirement? If so, what happened?
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. Yes, they can and it will fail. spark/python/pyspark/sql/tests.py Lines 3316 to 3325 in 122a7bc
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 see. Thanks! |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| >>> from pyspark.sql.types import IntegerType, StringType | ||||||||||||||||||||||
| >>> slen = pandas_udf(lambda s: s.str.len(), IntegerType()) | ||||||||||||||||||||||
| >>> @pandas_udf(returnType=StringType()) | ||||||||||||||||||||||
| ... def to_upper(s): | ||||||||||||||||||||||
| ... return s.str.upper() | ||||||||||||||||||||||
| ... | ||||||||||||||||||||||
| >>> @pandas_udf(returnType="integer") | ||||||||||||||||||||||
| ... def add_one(x): | ||||||||||||||||||||||
| ... return x + 1 | ||||||||||||||||||||||
| ... | ||||||||||||||||||||||
| >>> df = spark.createDataFrame([(1, "John Doe", 21)], ("id", "name", "age")) | ||||||||||||||||||||||
| >>> df.select(slen("name").alias("slen(name)"), to_upper("name"), add_one("age")) \\ | ||||||||||||||||||||||
| ... .show() # doctest: +SKIP | ||||||||||||||||||||||
| +----------+--------------+------------+ | ||||||||||||||||||||||
| |slen(name)|to_upper(name)|add_one(age)| | ||||||||||||||||||||||
| +----------+--------------+------------+ | ||||||||||||||||||||||
| | 8| JOHN DOE| 22| | ||||||||||||||||||||||
| +----------+--------------+------------+ | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| .. note:: The user-defined function must be deterministic. | ||||||||||||||||||||||
| """ | ||||||||||||||||||||||
| return _create_udf(f, returnType=returnType, pythonUdfType=PythonUdfType.PANDAS_UDF) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| @since(2.3) | ||||||||||||||||||||||
| def pandas_grouped_udf(f=None, returnType=StructType()): | ||||||||||||||||||||||
|
Contributor
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. Per discussion here: Should we consider convert
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 submitted another pr #19517 based on this as a comparison.
Contributor
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 @ueshin , yes that's what I am thinking.
Contributor
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. Here is a summary of the current proposal during some offline disuccsion: I. Use only
|
||||||||||||||||||||||
| """ | ||||||||||||||||||||||
| Creates a grouped vectorized user defined function (UDF). | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| :param f: user-defined function. A python function if used as a standalone function | ||||||||||||||||||||||
| :param returnType: a :class:`pyspark.sql.types.StructType` object | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| The grouped user-defined function can define the following transformation: | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| A `pandas.DataFrame` -> A `pandas.DataFrame` | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| This udf is only used with :meth:`pyspark.sql.GroupedData.apply`. | ||||||||||||||||||||||
| The returnType should be a :class:`StructType` describing the schema of the returned | ||||||||||||||||||||||
| `pandas.DataFrame`. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| >>> df = spark.createDataFrame( | ||||||||||||||||||||||
| ... [(1, 1.0), (1, 2.0), (2, 3.0), (2, 5.0), (2, 10.0)], | ||||||||||||||||||||||
| ... ("id", "v")) | ||||||||||||||||||||||
| >>> @pandas_grouped_udf(returnType=df.schema) | ||||||||||||||||||||||
| ... def normalize(pdf): | ||||||||||||||||||||||
| ... v = pdf.v | ||||||||||||||||||||||
| ... return pdf.assign(v=(v - v.mean()) / v.std()) | ||||||||||||||||||||||
| >>> df.groupby('id').apply(normalize).show() # doctest: +SKIP | ||||||||||||||||||||||
| +---+-------------------+ | ||||||||||||||||||||||
| | id| v| | ||||||||||||||||||||||
| +---+-------------------+ | ||||||||||||||||||||||
| | 1|-0.7071067811865475| | ||||||||||||||||||||||
| | 1| 0.7071067811865475| | ||||||||||||||||||||||
| | 2|-0.8320502943378437| | ||||||||||||||||||||||
| | 2|-0.2773500981126146| | ||||||||||||||||||||||
| | 2| 1.1094003924504583| | ||||||||||||||||||||||
| +---+-------------------+ | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| .. note:: This type of udf cannot be used with functions such as `withColumn` or `select` | ||||||||||||||||||||||
| because it defines a `DataFrame` transformation rather than a `Column` | ||||||||||||||||||||||
| transformation. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| .. seealso:: :meth:`pyspark.sql.GroupedData.apply` | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| .. note:: The user-defined function must be deterministic. | ||||||||||||||||||||||
| """ | ||||||||||||||||||||||
| return _create_udf(f, returnType=returnType, vectorized=True) | ||||||||||||||||||||||
| return _create_udf(f, returnType=returnType, pythonUdfType=PythonUdfType.PANDAS_GROUPED_UDF) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| blacklist = ['map', 'since', 'ignore_unicode_prefix'] | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.
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.
Could you also add the descriptions about these three UDF types?
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'll add the descriptions.