You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@spark.apache.org by ru...@apache.org on 2026/08/12 06:34:58 UTC

(spark) branch branch-4.x updated: [SPARK-58706][ML] Use SQL aggregation in OneHotEncoder fitting

This is an automated email from the ASF dual-hosted git repository.

zhengruifeng pushed a commit to branch branch-4.x
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/branch-4.x by this push:
     new 6bd1f806186f [SPARK-58706][ML] Use SQL aggregation in OneHotEncoder fitting
6bd1f806186f is described below

commit 6bd1f806186fbbfe1bf3d606300c8ca4bafab0c5
Author: Ruifeng Zheng <ru...@apache.org>
AuthorDate: Wed Aug 12 14:34:32 2026 +0800

    [SPARK-58706][ML] Use SQL aggregation in OneHotEncoder fitting
    
    ### What changes were proposed in this pull request?
    
    This PR replaces the RDD `treeAggregate` in `OneHotEncoder` fitting with a SQL aggregation. The SQL expressions validate category indices and compute the maximum category index for each input column.
    
    The validation expressions use `raise_error`, so an executor fails when it evaluates an invalid index. This replaces the stale RDD-specific early-stop comment. As with the old implementation, Spark cancels the query after the failure, although independently running tasks may already be processing rows.
    
    ### Why are the changes needed?
    
    The previous implementation serializes an array sized to all input columns as the aggregation zero value. SQL performs partial aggregation on executors and returns a single result row to the driver, reducing driver serialization and aggregation overhead for wide encoders.
    
    ### Does this PR introduce _any_ user-facing change?
    
    No.
    
    ### How was this patch tested?
    
    `build/sbt -java-home /usr/lib/jvm/java-17-openjdk-amd64 "mllib / Compile / compile"`
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Codex (GPT-5)
    
    Closes #57919 from zhengruifeng/SPARK-58706-onehot-fit-aggregation.
    
    Authored-by: Ruifeng Zheng <ru...@apache.org>
    Signed-off-by: Ruifeng Zheng <ru...@foxmail.com>
    (cherry picked from commit af6fe07b8a05adc821f0b483906feeff8fa775be)
    Signed-off-by: Ruifeng Zheng <ru...@foxmail.com>
---
 .../apache/spark/ml/feature/OneHotEncoder.scala    | 46 +++++++++-------------
 1 file changed, 18 insertions(+), 28 deletions(-)

diff --git a/mllib/src/main/scala/org/apache/spark/ml/feature/OneHotEncoder.scala b/mllib/src/main/scala/org/apache/spark/ml/feature/OneHotEncoder.scala
index c5120d026d47..5599aa73a4e8 100644
--- a/mllib/src/main/scala/org/apache/spark/ml/feature/OneHotEncoder.scala
+++ b/mllib/src/main/scala/org/apache/spark/ml/feature/OneHotEncoder.scala
@@ -31,8 +31,8 @@ import org.apache.spark.ml.param.shared.{HasHandleInvalid, HasInputCol, HasInput
 import org.apache.spark.ml.util._
 import org.apache.spark.sql.{DataFrame, Dataset}
 import org.apache.spark.sql.expressions.UserDefinedFunction
-import org.apache.spark.sql.functions.{col, lit, udf}
-import org.apache.spark.sql.types.{DoubleType, StructField, StructType}
+import org.apache.spark.sql.functions.{printf => fprintf, _}
+import org.apache.spark.sql.types.{DoubleType, IntegerType, StructField, StructType}
 import org.apache.spark.util.ArrayImplicits._
 import org.apache.spark.util.SizeEstimator
 
@@ -529,34 +529,24 @@ private[feature] object OneHotEncoderCommon {
       inputColNames: Seq[String],
       outputColNames: Seq[String],
       dropLast: Boolean): Seq[AttributeGroup] = {
-    // The RDD approach has advantage of early-stop if any values are invalid. It seems that
-    // DataFrame ops don't have equivalent functions.
     val columns = inputColNames.map { inputColName =>
-      col(inputColName).cast(DoubleType)
+      val doubleCol = col(inputColName).cast(DoubleType)
+      val intCol = doubleCol.cast(IntegerType)
+      val invalidIndexError = raise_error(fprintf(
+        lit(s"Values from column $inputColName must be indices, but got %s."), doubleCol))
+      val maxIndexError = raise_error(fprintf(
+        lit(s"OneHotEncoder only supports up to ${Int.MaxValue} indices, but got %s."),
+        doubleCol))
+      when(
+        isnull(doubleCol) || isnan(doubleCol) || doubleCol < 0.0 || doubleCol =!= intCol,
+        invalidIndexError)
+        .when(doubleCol > Int.MaxValue, maxIndexError)
+        .otherwise(intCol)
     }
-    val numOfColumns = columns.length
-
-    val numAttrsArray = dataset.select(columns: _*).rdd.map { row =>
-      (0 until numOfColumns).map(idx => row.getDouble(idx)).toArray
-    }.treeAggregate(new Array[Double](numOfColumns))(
-      (maxValues, curValues) => {
-        (0 until numOfColumns).foreach { idx =>
-          val x = curValues(idx)
-          assert(x <= Int.MaxValue,
-            s"OneHotEncoder only supports up to ${Int.MaxValue} indices, but got $x.")
-          assert(x >= 0.0 && x == x.toInt,
-            s"Values from column ${inputColNames(idx)} must be indices, but got $x.")
-          maxValues(idx) = math.max(maxValues(idx), x)
-        }
-        maxValues
-      },
-      (m0, m1) => {
-        (0 until numOfColumns).foreach { idx =>
-          m0(idx) = math.max(m0(idx), m1(idx))
-        }
-        m0
-      }
-    ).map(_.toInt + 1)
+
+    val maxValues = columns.map(c => coalesce(max(c) + 1, lit(1)))
+    val maxValuesRow = dataset.select(maxValues: _*).head()
+    val numAttrsArray = Array.tabulate(maxValues.length)(maxValuesRow.getInt)
 
     outputColNames.zip(numAttrsArray).map { case (outputColName, numAttrs) =>
       createAttrGroupForAttrNames(outputColName, numAttrs, dropLast, keepInvalid = false)


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@spark.apache.org
For additional commands, e-mail: commits-help@spark.apache.org