From 52e21a199c29be88f7c9189e4cecca8e92e9086c Mon Sep 17 00:00:00 2001 From: lenin Date: Wed, 14 Oct 2026 10:27:26 -0700 Subject: [PATCH 1/5] fix: generate TabularSHAP samples lazily --- .../synapse/ml/explainers/TabularSHAP.scala | 60 +++++++++++-------- .../split1/TabularSHAPExplainerSuite.scala | 47 ++++++++++++++- 2 files changed, 80 insertions(+), 27 deletions(-) diff --git a/core/src/main/scala/com/microsoft/azure/synapse/ml/explainers/TabularSHAP.scala b/core/src/main/scala/com/microsoft/azure/synapse/ml/explainers/TabularSHAP.scala index 74d2656eee7..2c752002083 100644 --- a/core/src/main/scala/com/microsoft/azure/synapse/ml/explainers/TabularSHAP.scala +++ b/core/src/main/scala/com/microsoft/azure/synapse/ml/explainers/TabularSHAP.scala @@ -3,14 +3,13 @@ package com.microsoft.azure.synapse.ml.explainers -import breeze.stats.distributions.RandBasis import com.microsoft.azure.synapse.ml.core.schema.DatasetExtensions import com.microsoft.azure.synapse.ml.logging.FeatureNames -import org.apache.spark.injections.UDFUtils import org.apache.spark.ml.ComplexParamsReadable +import org.apache.spark.ml.linalg.SQLDataTypes.VectorType import org.apache.spark.ml.param.shared.HasInputCols import org.apache.spark.ml.util.Identifiable -import org.apache.spark.sql.functions._ +import org.apache.spark.sql.functions.{col, struct} import org.apache.spark.sql.types._ import org.apache.spark.sql.{DataFrame, Row} @@ -53,30 +52,35 @@ class TabularSHAP(override val uid: String) }) val infWeightVal = this.getInfWeight - val samplesUdf = UDFUtils.oldUdf( - { - (instance: Row, background: Row) => - val sampler = new KernelSHAPTabularSampler(instance, background, effectiveNumSamples, infWeightVal) - (1 to effectiveNumSamples) map { - _ => - implicit val randBasis: RandBasis = RandBasis.mt0 - sampler.sample - } - }, - getSampleSchema(sampleType) + val pairs = instances.crossJoin(background) + val outputSchema = StructType( + Seq(df.schema(idCol)) ++ + sampleType.fields.map(_.copy(nullable = true)) ++ + Seq( + StructField(coalitionCol, VectorType), + StructField(weightCol, DoubleType), + df.schema(targetClassesCol) + ) ) - val samplesCol = DatasetExtensions.findUnusedColumnName("samples", df) - - instances.crossJoin(background) - .withColumn(samplesCol, explode(samplesUdf(col(instanceCol), col(backgroundCol)))) - .select( - col(idCol), - expr(s"$samplesCol.$sampleField.*"), - col(samplesCol).getField(coalitionField).alias(coalitionCol), - col(samplesCol).getField(weightField).alias(weightCol), - col(targetClassesCol) - ) + val sampleRows = pairs.rdd.mapPartitions { + rows => + rows.flatMap { + row => + val id = row.get(0) + val targetClasses = row.get(1) + val instance = row.getAs[Row](2) + val background = row.getAs[Row](3) + val sampler = new KernelSHAPTabularSampler(instance, background, effectiveNumSamples, infWeightVal) + + TabularSHAP.sampleIterator(effectiveNumSamples)(sampler.sample).map { + case (sample, coalition, weight) => + Row.fromSeq(Seq(id) ++ sample.toSeq ++ Seq(coalition, weight, targetClasses)) + } + } + } + + df.sparkSession.createDataFrame(sampleRows, outputSchema) } override def validateSchema(schema: StructType): Unit = { @@ -98,4 +102,8 @@ class TabularSHAP(override val uid: String) } } -object TabularSHAP extends ComplexParamsReadable[TabularSHAP] +object TabularSHAP extends ComplexParamsReadable[TabularSHAP] { + private[explainers] def sampleIterator[T](numSamples: Int)(sample: => T): Iterator[T] = { + Iterator.fill(numSamples)(()).map(_ => sample) + } +} diff --git a/core/src/test/scala/com/microsoft/azure/synapse/ml/explainers/split1/TabularSHAPExplainerSuite.scala b/core/src/test/scala/com/microsoft/azure/synapse/ml/explainers/split1/TabularSHAPExplainerSuite.scala index 6b186c31404..5066f05218e 100644 --- a/core/src/test/scala/com/microsoft/azure/synapse/ml/explainers/split1/TabularSHAPExplainerSuite.scala +++ b/core/src/test/scala/com/microsoft/azure/synapse/ml/explainers/split1/TabularSHAPExplainerSuite.scala @@ -9,7 +9,7 @@ import com.microsoft.azure.synapse.ml.core.utils.BreezeUtils._ import com.microsoft.azure.synapse.ml.explainers.LocalExplainer.KernelSHAP import com.microsoft.azure.synapse.ml.explainers.TabularSHAP import org.apache.spark.ml.classification.{LogisticRegression, LogisticRegressionModel} -import org.apache.spark.ml.feature.{OneHotEncoder, StringIndexer, VectorAssembler} +import org.apache.spark.ml.feature.{OneHotEncoder, SQLTransformer, StringIndexer, VectorAssembler} import org.apache.spark.ml.linalg.Vector import org.apache.spark.ml.util.MLReadable import org.apache.spark.ml.{Pipeline, PipelineModel} @@ -88,6 +88,51 @@ class TabularSHAPExplainerSuite extends TestBase assert(r2(0) === 1d) } + test("Kernel SHAP samples are generated lazily") { + var generated = 0 + val samples = TabularSHAP.sampleIterator(100000) { + generated += 1 + generated + } + + assert(generated === 0) + assert(samples.next() === 1) + assert(generated === 1) + } + + test("TabularKernelSHAP accepts nullable background values") { + val instances = spark.range(3).selectExpr( + "cast(id as double) as x", + "cast(id * 2 as double) as y" + ) + val background = Seq( + (Option.empty[Double], Option(1d)), + (Option(2d), Option.empty[Double]) + ).toDF("x", "y") + val additiveModel = new SQLTransformer().setStatement( + "SELECT *, cast(coalesce(x, 10.0) + coalesce(y, 100.0) AS DOUBLE) AS score FROM __THIS__" + ) + + val results = KernelSHAP.tabular + .setInputCols(Array("x", "y")) + .setOutputCol("shapValues") + .setBackgroundData(background) + .setModel(additiveModel) + .setTargetCol("score") + .transform(instances) + .orderBy("x") + .select("shapValues") + .as[Seq[Vector]] + .collect() + + results.zipWithIndex.foreach { + case (Seq(values), x) => + assert(values(0) === 56.5) + assert(values(1) === x.toDouble - 6d) + assert(values(2) === 2d * x - 50.5) + } + } + override def testObjects(): Seq[TestObject[TabularSHAP]] = Seq(new TestObject(kernelShap, infer)) override def reader: MLReadable[_] = TabularSHAP From aaf1f9af746204f2e64d6891a7f830f917dd5ada Mon Sep 17 00:00:00 2001 From: lenin Date: Wed, 14 Oct 2026 11:03:20 -0700 Subject: [PATCH 2/5] fix: keep TabularSHAP sampling in Dataset API --- .../microsoft/azure/synapse/ml/explainers/TabularSHAP.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/main/scala/com/microsoft/azure/synapse/ml/explainers/TabularSHAP.scala b/core/src/main/scala/com/microsoft/azure/synapse/ml/explainers/TabularSHAP.scala index 2c752002083..af1c6b4f922 100644 --- a/core/src/main/scala/com/microsoft/azure/synapse/ml/explainers/TabularSHAP.scala +++ b/core/src/main/scala/com/microsoft/azure/synapse/ml/explainers/TabularSHAP.scala @@ -9,6 +9,7 @@ import org.apache.spark.ml.ComplexParamsReadable import org.apache.spark.ml.linalg.SQLDataTypes.VectorType import org.apache.spark.ml.param.shared.HasInputCols import org.apache.spark.ml.util.Identifiable +import org.apache.spark.sql.catalyst.encoders.ExpressionEncoder import org.apache.spark.sql.functions.{col, struct} import org.apache.spark.sql.types._ import org.apache.spark.sql.{DataFrame, Row} @@ -62,8 +63,9 @@ class TabularSHAP(override val uid: String) df.schema(targetClassesCol) ) ) + implicit val outputEncoder: ExpressionEncoder[Row] = ExpressionEncoder(outputSchema) - val sampleRows = pairs.rdd.mapPartitions { + pairs.mapPartitions { rows => rows.flatMap { row => @@ -79,8 +81,6 @@ class TabularSHAP(override val uid: String) } } } - - df.sparkSession.createDataFrame(sampleRows, outputSchema) } override def validateSchema(schema: StructType): Unit = { From 696d2d3630c8d577aeacb389dfbe779083a85598 Mon Sep 17 00:00:00 2001 From: lenin Date: Wed, 14 Oct 2026 13:13:48 -0700 Subject: [PATCH 3/5] test: guard TabularSHAP production laziness --- .../synapse/ml/explainers/TabularSHAP.scala | 31 ++++++++------ .../split1/TabularSHAPExplainerSuite.scala | 42 ++++++++++++++----- 2 files changed, 51 insertions(+), 22 deletions(-) diff --git a/core/src/main/scala/com/microsoft/azure/synapse/ml/explainers/TabularSHAP.scala b/core/src/main/scala/com/microsoft/azure/synapse/ml/explainers/TabularSHAP.scala index af1c6b4f922..0dc635c367b 100644 --- a/core/src/main/scala/com/microsoft/azure/synapse/ml/explainers/TabularSHAP.scala +++ b/core/src/main/scala/com/microsoft/azure/synapse/ml/explainers/TabularSHAP.scala @@ -67,18 +67,9 @@ class TabularSHAP(override val uid: String) pairs.mapPartitions { rows => - rows.flatMap { - row => - val id = row.get(0) - val targetClasses = row.get(1) - val instance = row.getAs[Row](2) - val background = row.getAs[Row](3) - val sampler = new KernelSHAPTabularSampler(instance, background, effectiveNumSamples, infWeightVal) - - TabularSHAP.sampleIterator(effectiveNumSamples)(sampler.sample).map { - case (sample, coalition, weight) => - Row.fromSeq(Seq(id) ++ sample.toSeq ++ Seq(coalition, weight, targetClasses)) - } + TabularSHAP.expandPairs(rows, effectiveNumSamples) { + (instance, background) => + new KernelSHAPTabularSampler(instance, background, effectiveNumSamples, infWeightVal) } } } @@ -106,4 +97,20 @@ object TabularSHAP extends ComplexParamsReadable[TabularSHAP] { private[explainers] def sampleIterator[T](numSamples: Int)(sample: => T): Iterator[T] = { Iterator.fill(numSamples)(()).map(_ => sample) } + + /** Expands (id, targetClasses, instance, background) pair rows into sample rows one at a time. */ + private[explainers] def expandPairs(pairs: Iterator[Row], numSamples: Int) + (newSampler: (Row, Row) => KernelSHAPSampler[Row]): Iterator[Row] = { + pairs.flatMap { + row => + val id = row.get(0) + val targetClasses = row.get(1) + val sampler = newSampler(row.getAs[Row](2), row.getAs[Row](3)) + + sampleIterator(numSamples)(sampler.sample).map { + case (sample, coalition, weight) => + Row.fromSeq(Seq(id) ++ sample.toSeq ++ Seq(coalition, weight, targetClasses)) + } + } + } } diff --git a/core/src/test/scala/com/microsoft/azure/synapse/ml/explainers/split1/TabularSHAPExplainerSuite.scala b/core/src/test/scala/com/microsoft/azure/synapse/ml/explainers/split1/TabularSHAPExplainerSuite.scala index 5066f05218e..3c2b12b1ef1 100644 --- a/core/src/test/scala/com/microsoft/azure/synapse/ml/explainers/split1/TabularSHAPExplainerSuite.scala +++ b/core/src/test/scala/com/microsoft/azure/synapse/ml/explainers/split1/TabularSHAPExplainerSuite.scala @@ -7,13 +7,13 @@ import com.microsoft.azure.synapse.ml.core.test.base.TestBase import com.microsoft.azure.synapse.ml.core.test.fuzzing.{TestObject, TransformerFuzzing} import com.microsoft.azure.synapse.ml.core.utils.BreezeUtils._ import com.microsoft.azure.synapse.ml.explainers.LocalExplainer.KernelSHAP -import com.microsoft.azure.synapse.ml.explainers.TabularSHAP +import com.microsoft.azure.synapse.ml.explainers.{KernelSHAPTabularSampler, TabularSHAP} import org.apache.spark.ml.classification.{LogisticRegression, LogisticRegressionModel} import org.apache.spark.ml.feature.{OneHotEncoder, SQLTransformer, StringIndexer, VectorAssembler} import org.apache.spark.ml.linalg.Vector import org.apache.spark.ml.util.MLReadable import org.apache.spark.ml.{Pipeline, PipelineModel} -import org.apache.spark.sql.DataFrame +import org.apache.spark.sql.{DataFrame, Row} import org.apache.spark.sql.functions.avg import org.scalactic.{Equality, TolerantNumerics} @@ -88,16 +88,38 @@ class TabularSHAPExplainerSuite extends TestBase assert(r2(0) === 1d) } - test("Kernel SHAP samples are generated lazily") { - var generated = 0 - val samples = TabularSHAP.sampleIterator(100000) { - generated += 1 - generated + test("TabularSHAP sample expansion emits rows without buffering a pair's sample budget") { + val numFeatures = 14 + val numSamples = 10000 + var pairsPulled = 0 + var samplesDrawn = 0 + val instance = Row.fromSeq(Seq.fill(numFeatures)(1d)) + val background = Row.fromSeq(Seq.fill(numFeatures)(0d)) + val pairs = Iterator(0L, 1L).map { + id => + pairsPulled += 1 + Row(id, Seq(1), instance, background) } - assert(generated === 0) - assert(samples.next() === 1) - assert(generated === 1) + val rows = TabularSHAP.expandPairs(pairs, numSamples) { + (i, b) => + new KernelSHAPTabularSampler(i, b, numSamples, 1E8) { + override def sample: (Row, Vector, Double) = { + samplesDrawn += 1 + super.sample + } + } + } + + assert(samplesDrawn === 0) + val first = rows.next() + assert(samplesDrawn === 1) + assert(pairsPulled === 1) + assert(first.length === numFeatures + 4) + assert(first.getLong(0) === 0L) + assert(rows.take(9).size === 9) + assert(samplesDrawn === 10) + assert(pairsPulled === 1) } test("TabularKernelSHAP accepts nullable background values") { From 1ac2df5342ee5a423c134eb3adfc1984961733f8 Mon Sep 17 00:00:00 2001 From: lenin Date: Wed, 14 Oct 2026 14:29:15 -0700 Subject: [PATCH 4/5] test: require TabularSHAP result rows --- .../synapse/ml/explainers/split1/TabularSHAPExplainerSuite.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/test/scala/com/microsoft/azure/synapse/ml/explainers/split1/TabularSHAPExplainerSuite.scala b/core/src/test/scala/com/microsoft/azure/synapse/ml/explainers/split1/TabularSHAPExplainerSuite.scala index 3c2b12b1ef1..0cca9582de2 100644 --- a/core/src/test/scala/com/microsoft/azure/synapse/ml/explainers/split1/TabularSHAPExplainerSuite.scala +++ b/core/src/test/scala/com/microsoft/azure/synapse/ml/explainers/split1/TabularSHAPExplainerSuite.scala @@ -147,6 +147,7 @@ class TabularSHAPExplainerSuite extends TestBase .as[Seq[Vector]] .collect() + assert(results.length === 3) results.zipWithIndex.foreach { case (Seq(values), x) => assert(values(0) === 56.5) From 9e66546f8147755c4d2142f940090c2607a62139 Mon Sep 17 00:00:00 2001 From: Rana Singh Date: Thu, 15 Oct 2026 04:30:39 -0400 Subject: [PATCH 5/5] fix: preserve nested SHAP background nulls and verify streaming ## Summary Keep the contributor's lazy Dataset sampling and recursively relax nested sample nullability. Add public nested-null regressions and an execution-path test that observes bounded encoding before the first sample. Explain sampling cost and null handling in the tabular explainer notebook. ## Prompting Intent Improve microsoft/SynapseML#2768 with meaningful, focused changes without bloating the contribution. Verify correctness and measure performance trade-offs rather than assuming a plausible memory fix is sufficient. ## Linked Sources - Contribution: https://github.com/microsoft/SynapseML/pull/2768 - Reported problem: https://github.com/microsoft/SynapseML/issues/2206 - Production-path regression finding: https://github.com/microsoft/SynapseML/pull/2768#discussion_r4274467915 - Local review of 1ac2df5342: public array/map/struct background-null encoding regression ## Rationale Only sample schema construction changes in production; sampler behavior, public signatures, sample budgets, weights and column order stay intact. The execution-path test fails with 20000 encoded values on the original eager UDF without requiring an OOM or timing threshold. The nested regression fails on the submitted PR and passes with recursive nullability. Preserve the helper test as a separate iterator-consumption check. Core compilation, main/test Scalastyle, all 13 targeted SHAP tests, core code generation, pinned Black and generated Python imports pass. A 1 GiB notebook-shaped synthetic workload OOMs with the old sampling implementation and completes in 21.7 seconds with lazy sampling. With sufficient heap, representative warm time is about 5 percent slower, an explicit trade-off for bounded expansion memory rather than a claim of universal speedup. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2765695e-c815-4211-ad89-e0be56141b31 --- .../synapse/ml/explainers/TabularSHAP.scala | 12 ++- .../split1/TabularSHAPExplainerSuite.scala | 88 ++++++++++++++++++- .../Responsible AI/Tabular Explainers.ipynb | 4 +- 3 files changed, 101 insertions(+), 3 deletions(-) diff --git a/core/src/main/scala/com/microsoft/azure/synapse/ml/explainers/TabularSHAP.scala b/core/src/main/scala/com/microsoft/azure/synapse/ml/explainers/TabularSHAP.scala index 0dc635c367b..b5b44b42624 100644 --- a/core/src/main/scala/com/microsoft/azure/synapse/ml/explainers/TabularSHAP.scala +++ b/core/src/main/scala/com/microsoft/azure/synapse/ml/explainers/TabularSHAP.scala @@ -56,7 +56,8 @@ class TabularSHAP(override val uid: String) val pairs = instances.crossJoin(background) val outputSchema = StructType( Seq(df.schema(idCol)) ++ - sampleType.fields.map(_.copy(nullable = true)) ++ + sampleType.fields.map(field => field.copy( + dataType = TabularSHAP.nullableType(field.dataType), nullable = true)) ++ Seq( StructField(coalitionCol, VectorType), StructField(weightCol, DoubleType), @@ -94,6 +95,15 @@ class TabularSHAP(override val uid: String) } object TabularSHAP extends ComplexParamsReadable[TabularSHAP] { + private def nullableType(dataType: DataType): DataType = dataType match { + case struct: StructType => + StructType(struct.fields.map(field => field.copy(dataType = nullableType(field.dataType), nullable = true))) + case ArrayType(elementType, _) => ArrayType(nullableType(elementType), containsNull = true) + case MapType(keyType, valueType, _) => + MapType(nullableType(keyType), nullableType(valueType), valueContainsNull = true) + case other => other + } + private[explainers] def sampleIterator[T](numSamples: Int)(sample: => T): Iterator[T] = { Iterator.fill(numSamples)(()).map(_ => sample) } diff --git a/core/src/test/scala/com/microsoft/azure/synapse/ml/explainers/split1/TabularSHAPExplainerSuite.scala b/core/src/test/scala/com/microsoft/azure/synapse/ml/explainers/split1/TabularSHAPExplainerSuite.scala index 0cca9582de2..798c44e5a06 100644 --- a/core/src/test/scala/com/microsoft/azure/synapse/ml/explainers/split1/TabularSHAPExplainerSuite.scala +++ b/core/src/test/scala/com/microsoft/azure/synapse/ml/explainers/split1/TabularSHAPExplainerSuite.scala @@ -14,9 +14,12 @@ import org.apache.spark.ml.linalg.Vector import org.apache.spark.ml.util.MLReadable import org.apache.spark.ml.{Pipeline, PipelineModel} import org.apache.spark.sql.{DataFrame, Row} -import org.apache.spark.sql.functions.avg +import org.apache.spark.sql.functions.{array, avg, broadcast, lit} +import org.apache.spark.sql.types._ import org.scalactic.{Equality, TolerantNumerics} +import java.util.concurrent.atomic.AtomicLong + class TabularSHAPExplainerSuite extends TestBase with TransformerFuzzing[TabularSHAP] { @@ -156,7 +159,90 @@ class TabularSHAPExplainerSuite extends TestBase } } + test("TabularSHAP createSamples streams encoded rows before expanding the sample budget") { + import TabularSHAPExplainerSuite._ + val numSamples = 10000 + val features = (0 until 14).map(i => s"f$i").toArray + val schema = StructType(Seq(StructField(features.head, new CountedValueUDT)) ++ + features.tail.map(StructField(_, DoubleType))) + val instance = Row.fromSeq(Seq(CountedValue(1)) ++ Seq.fill(13)(1d)) + val backgroundRow = Row.fromSeq(Seq(CountedValue(0)) ++ Seq.fill(13)(0d)) + val instances = spark.createDataFrame(java.util.Collections.singletonList(instance), schema) + .withColumn("id", lit(0L)).withColumn("targetClasses", array(lit(1))) + val background = spark.createDataFrame(java.util.Arrays.asList(backgroundRow, backgroundRow), schema) + val samples = new SampleAccess() + .setInputCols(features).setNumSamples(numSamples).setBackgroundData(broadcast(background)) + .samples(instances) + + Serialized.set(0) + val rows = samples.limit(1).collect() + assert(rows.length === 1) + assert(rows.head.getAs[CountedValue]("f0").value === 0) + assert(rows.head.getAs[Vector]("coalition").toArray.forall(_ == 0d)) + assert(rows.head.getAs[Double]("weight") === 1E8) + assert(Serialized.get() > 0 && Serialized.get() < numSamples, + s"Encoded ${Serialized.get()} feature values before returning the first sample") + } + + test("TabularKernelSHAP preserves nulls inside background features") { + val featureShapes = Seq( + ("named_struct('value', %s)", "x.value"), + ("array(%s)", "x[0]"), + ("map('value', %s)", "x['value']"), + ("map('outer', array(named_struct('value', %s)))", "x['outer'][0].value") + ) + featureShapes.foreach { + case (shape, access) => + val instances = spark.range(1, 2).selectExpr( + s"${shape.format("cast(id as double)")} as x", "cast(id * 2 as double) as y") + val background = spark.range(1).selectExpr( + s"${shape.format("cast(null as double)")} as x", "cast(id as double) as y") + val additiveModel = new SQLTransformer().setStatement( + s"SELECT *, cast(coalesce($access, 10.0) + y AS DOUBLE) AS score FROM __THIS__") + + val results = KernelSHAP.tabular + .setInputCols(Array("x", "y")) + .setOutputCol("shapValues") + .setBackgroundData(background) + .setModel(additiveModel) + .setTargetCol("score") + .transform(instances) + try { + val values = results.select("shapValues").as[Seq[Vector]].collect() + assert(values.length === 1) + assert(values.head.length === 1) + assert(values.head.head(0) === 10d) + assert(values.head.head(1) === -9d) + assert(values.head.head(2) === 2d) + } finally { + spark.catalog.clearCache() + } + } + } + override def testObjects(): Seq[TestObject[TabularSHAP]] = Seq(new TestObject(kernelShap, infer)) override def reader: MLReadable[_] = TabularSHAP } + +object TabularSHAPExplainerSuite { + // The local Spark test counts encoding at the production sampling boundary, not just iterator calls. + private val Serialized = new AtomicLong() + + @SQLUserDefinedType(udt = classOf[CountedValueUDT]) + case class CountedValue(value: Int) + + class CountedValueUDT extends UserDefinedType[CountedValue] { + override def sqlType: DataType = IntegerType + override def userClass: Class[CountedValue] = classOf[CountedValue] + override def serialize(value: CountedValue): Any = { + Serialized.incrementAndGet() + value.value + } + override def deserialize(value: Any): CountedValue = CountedValue(value.asInstanceOf[Int]) + } + + private class SampleAccess extends TabularSHAP { + def samples(df: DataFrame): DataFrame = createSamples(df, "id", "coalition", "weight", "targetClasses") + } +} diff --git a/docs/Explore Algorithms/Responsible AI/Tabular Explainers.ipynb b/docs/Explore Algorithms/Responsible AI/Tabular Explainers.ipynb index 394a93b2d87..3ae8280df62 100644 --- a/docs/Explore Algorithms/Responsible AI/Tabular Explainers.ipynb +++ b/docs/Explore Algorithms/Responsible AI/Tabular Explainers.ipynb @@ -164,7 +164,9 @@ } }, "source": [ - "We create a TabularSHAP explainer, set the input columns to all the features the model takes, specify the model and the target output column we're trying to explain. In this case, we're trying to explain the \"probability\" output, which is a vector of length 2, and we're only looking at class 1 probability. Specify targetClasses to `[0, 1]` if you want to explain class 0 and 1 probability at the same time. Finally we sample 100 rows from the training data for background data, which is used for integrating out features in Kernel SHAP." + "We create a TabularSHAP explainer, set the input columns to all the features the model takes, specify the model and the target output column we're trying to explain. In this case, we're trying to explain the \"probability\" output, which is a vector of length 2, and we're only looking at class 1 probability. Specify targetClasses to `[0, 1]` if you want to explain class 0 and 1 probability at the same time. Finally we sample 100 rows from the training data for background data, which is used for integrating out features in Kernel SHAP.\n", + "\n", + "Sample rows are generated incrementally, but model-scoring work still scales with the number of explained rows, background rows, and effective samples per pair. Start with a small representative background and sample budget before scaling up. Background features can contain nulls, including nested nulls in arrays, maps, and structs, provided the model can score those values." ] }, {