Skip to content

Commit b293dcc

Browse files
authored
[jvm] Support Regressor and Ranker in Pipeline with columnar input (#12058)
1 parent 07cad5e commit b293dcc

5 files changed

Lines changed: 63 additions & 16 deletions

File tree

jvm-packages/xgboost4j-example/src/main/java/ml/dmlc/xgboost4j/java/example/PredictLeafIndices.java

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright (c) 2014 by Contributors
2+
Copyright (c) 2014-2026 by Contributors
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -15,7 +15,6 @@
1515
*/
1616
package ml.dmlc.xgboost4j.java.example;
1717

18-
import java.util.Arrays;
1918
import java.util.HashMap;
2019

2120
import ml.dmlc.xgboost4j.java.Booster;
@@ -53,14 +52,14 @@ public static void main(String[] args) throws XGBoostError {
5352

5453
//predict using first 2 tree
5554
float[][] leafindex = booster.predictLeaf(testMat, 2);
56-
for (float[] leafs : leafindex) {
57-
System.out.println(Arrays.toString(leafs));
55+
if (leafindex.length > 0 && leafindex[0].length > 1) {
56+
System.out.println(leafindex[0][0] + ", " + leafindex[0][1]);
5857
}
5958

6059
//predict all trees
6160
leafindex = booster.predictLeaf(testMat, 0);
62-
for (float[] leafs : leafindex) {
63-
System.out.println(Arrays.toString(leafs));
61+
if (leafindex.length > 0 && leafindex[0].length > 1) {
62+
System.out.println(leafindex[0][0] + ", " + leafindex[0][1]);
6463
}
6564
}
6665
}

jvm-packages/xgboost4j-example/src/main/scala/ml/dmlc/xgboost4j/scala/example/PredictLeafIndices.scala

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright (c) 2014-2024 by Contributors
2+
Copyright (c) 2014-2026 by Contributors
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -41,14 +41,14 @@ object PredictLeafIndices {
4141

4242
// predict using first 2 tree
4343
val leafIndex = booster.predictLeaf(testMat, 2)
44-
for (leafs <- leafIndex) {
45-
println(java.util.Arrays.toString(leafs))
44+
if (leafIndex.length > 0 && leafIndex(0).length > 1) {
45+
println(s"${leafIndex(0)(0)}, ${leafIndex(0)(1)}")
4646
}
4747

4848
// predict all trees
4949
val leafIndex2 = booster.predictLeaf(testMat, 0)
50-
for (leafs <- leafIndex2) {
51-
println(java.util.Arrays.toString(leafs))
50+
if (leafIndex2.length > 0 && leafIndex2(0).length > 1) {
51+
println(s"${leafIndex2(0)(0)}, ${leafIndex2(0)(1)}")
5252
}
5353
}
5454
}

jvm-packages/xgboost4j-spark/src/main/scala/ml/dmlc/xgboost4j/scala/spark/XGBoostEstimator.scala

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright (c) 2024-2025 by Contributors
2+
Copyright (c) 2024-2026 by Contributors
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -29,7 +29,7 @@ import org.apache.spark.ml.functions.array_to_vector
2929
import org.apache.spark.ml.linalg.{SparseVector, Vector}
3030
import org.apache.spark.ml.param.{Param, ParamMap}
3131
import org.apache.spark.ml.util.{DefaultParamsWritable, MLReader, MLWritable, MLWriter}
32-
import org.apache.spark.ml.xgboost.{SparkUtils, XGBProbabilisticClassifierParams}
32+
import org.apache.spark.ml.xgboost.{SparkUtils, XGBPredictorParams, XGBProbabilisticClassifierParams}
3333
import org.apache.spark.rdd.RDD
3434
import org.apache.spark.sql._
3535
import org.apache.spark.sql.functions.{array, col, udf}
@@ -725,7 +725,8 @@ private[spark] abstract class XGBoostModelReader[M <: XGBoostModel[M]] extends M
725725
}
726726

727727
// Trait for Ranker and Regressor Model
728-
private[spark] trait RankerRegressorBaseModel[M <: XGBoostModel[M]] extends XGBoostModel[M] {
728+
private[spark] trait RankerRegressorBaseModel[M <: XGBoostModel[M]] extends XGBoostModel[M]
729+
with XGBPredictorParams[M] {
729730

730731
override protected[spark] def postTransform(dataset: Dataset[_],
731732
pred: PredictedColumns): Dataset[_] = {

jvm-packages/xgboost4j-spark/src/main/scala/org/apache/spark/ml/xgboost/SparkUtils.scala

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright (c) 2024 by Contributors
2+
Copyright (c) 2024-2026 by Contributors
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
1717
package org.apache.spark.ml.xgboost
1818

1919
import org.apache.spark.{SparkContext, SparkException}
20+
import org.apache.spark.ml.PredictorParams
2021
import org.apache.spark.ml.classification.ProbabilisticClassifierParams
2122
import org.apache.spark.ml.linalg.VectorUDT
2223
import org.apache.spark.ml.param.Params
@@ -55,6 +56,20 @@ trait XGBProbabilisticClassifierParams[T <: Params]
5556
addNonXGBoostParam(rawPredictionCol, probabilityCol, thresholds)
5657
}
5758

59+
trait XGBPredictorParams[T <: Params] extends PredictorParams {
60+
61+
/**
62+
* XGBoost doesn't use validateAndTransformSchema since spark validateAndTransformSchema
63+
* needs to ensure the feature is vector type
64+
*/
65+
override protected def validateAndTransformSchema(schema: StructType,
66+
fitting: Boolean,
67+
featuresDataType: DataType): StructType = {
68+
SparkUtils.appendColumn(schema, $(predictionCol), DoubleType)
69+
}
70+
71+
}
72+
5873
/** Utils to access the spark internal functions */
5974
object SparkUtils {
6075

jvm-packages/xgboost4j-spark/src/test/scala/ml/dmlc/xgboost4j/scala/spark/XGBoostEstimatorSuite.scala

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright (c) 2024 by Contributors
2+
Copyright (c) 2024-2026 by Contributors
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -22,6 +22,7 @@ import java.util.Arrays
2222
import scala.collection.mutable.ArrayBuffer
2323

2424
import org.apache.spark.SparkException
25+
import org.apache.spark.ml.Pipeline
2526
import org.apache.spark.ml.linalg.{DenseVector, SparseVector, Vectors}
2627
import org.apache.spark.ml.param.ParamMap
2728
import org.apache.spark.ml.xgboost.SparkUtils
@@ -741,4 +742,35 @@ class XGBoostEstimatorSuite extends AnyFunSuite with PerTest with TmpFolderPerSu
741742
// No exception
742743
transformedDf.collect()
743744
}
745+
746+
test("Pipeline with columnar input for Regressor and Ranker") {
747+
val df = ss.createDataFrame(sc.parallelize(Seq(
748+
(0.0, 1, 2, 1.0, 2.0, 3.0),
749+
(1.0, 0, 5, 1.0, 2.0, 3.0),
750+
(2.0, 2, 7, 1.0, 2.0, 3.0)
751+
))).toDF("label", "group", "weight", "c1", "c2", "c3")
752+
753+
// XGBoostRegressor with columnar features
754+
val regressor = new XGBoostRegressor()
755+
.setNumRound(1)
756+
.setNumWorkers(1)
757+
.setFeaturesCol(Array("c1", "c2", "c3"))
758+
759+
val regressorPipeline = new Pipeline().setStages(Array(regressor))
760+
val regressorFit = regressorPipeline.fit(df)
761+
val regressorPredictions = regressorFit.transform(df)
762+
assert(regressorPredictions.count() === 3)
763+
764+
// XGBoostRanker with columnar features (requires group column)
765+
val ranker = new XGBoostRanker()
766+
.setNumRound(1)
767+
.setNumWorkers(1)
768+
.setGroupCol("group")
769+
.setFeaturesCol(Array("c1", "c2", "c3"))
770+
771+
val rankerPipeline = new Pipeline().setStages(Array(ranker))
772+
val rankerFit = rankerPipeline.fit(df)
773+
val rankerPredictions = rankerFit.transform(df)
774+
assert(rankerPredictions.count() === 3)
775+
}
744776
}

0 commit comments

Comments
 (0)