diff --git a/.gitmessage b/.gitmessage new file mode 100644 index 0000000..de44196 --- /dev/null +++ b/.gitmessage @@ -0,0 +1,17 @@ +# Conventional Commit Format (English) +# (): +# +# Types: +# feat: A new feature +# fix: A bug fix +# docs: Documentation only changes +# style: Changes that do not affect the meaning of the code +# refactor: A code change that neither fixes a bug nor adds a feature +# perf: A code change that improves performance +# test: Adding missing tests or correcting existing tests +# chore: Changes to the build process or auxiliary tools +# +# Example: +# feat(auth): add user login functionality +# fix(api): resolve null pointer exception in user service +# docs(readme): update installation instructions diff --git a/examples/src/main/java/io/github/yasmramos/mindforge/examples/TreeSHAPExample.java b/examples/src/main/java/io/github/yasmramos/mindforge/examples/TreeSHAPExample.java new file mode 100644 index 0000000..57bee8b --- /dev/null +++ b/examples/src/main/java/io/github/yasmramos/mindforge/examples/TreeSHAPExample.java @@ -0,0 +1,128 @@ +package io.github.yasmramos.mindforge.examples; + +import io.github.yasmramos.mindforge.classification.RandomForestClassifier; +import io.github.yasmramos.mindforge.interpret.TreeSHAP; +import io.github.yasmramos.mindforge.data.DatasetLoader; +import io.github.yasmramos.mindforge.validation.Metrics; + +/** + * Example demonstrating TreeSHAP for explaining Random Forest predictions. + * + * TreeSHAP provides fast, exact SHAP values for tree-based models, + * making it much faster than Kernel SHAP while maintaining accuracy. + */ +public class TreeSHAPExample { + + public static void main(String[] args) { + System.out.println("=== TreeSHAP Example ===\n"); + + // Load Iris dataset + System.out.println("Loading Iris dataset..."); + var dataset = DatasetLoader.loadIris(); + double[][] X = dataset.getFeatures(); + int[] y = dataset.getTargets(); + + System.out.println("Dataset: " + X.length + " samples, " + X[0].length + " features"); + System.out.println("Classes: " + dataset.getClasses().length + "\n"); + + // Train Random Forest + System.out.println("Training Random Forest classifier..."); + RandomForestClassifier rf = new RandomForestClassifier.Builder() + .nEstimators(50) + .maxDepth(10) + .minSamplesSplit(2) + .randomState(42) + .build(); + + rf.fit(X, y); + + // Evaluate model + int[] predictions = rf.predict(X); + double accuracy = Metrics.accuracy(y, predictions); + System.out.println("Model accuracy: " + String.format("%.4f", accuracy) + "\n"); + + // Create TreeSHAP explainer + System.out.println("Creating TreeSHAP explainer..."); + TreeSHAP shap = new TreeSHAP(rf); + + // Use a subset of training data as background + double[][] background = new double[Math.min(50, X.length)][]; + for (int i = 0; i < background.length; i++) { + background[i] = X[i].clone(); + } + shap.setBackground(background); + + System.out.println("Expected value (base prediction): " + String.format("%.4f", shap.getExpectedValue())); + System.out.println("Model type: " + shap.getModelType()); + System.out.println("Number of features: " + shap.getNFeatures() + "\n"); + + // Explain a single instance + System.out.println("=== Explaining Single Instance ==="); + double[] instance = X[100]; // Pick an instance to explain + int trueLabel = y[100]; + int predictedLabel = rf.predict(instance)[0]; + + System.out.println("Instance: " + java.util.Arrays.toString(instance)); + System.out.println("True label: " + trueLabel); + System.out.println("Predicted label: " + predictedLabel + "\n"); + + // Compute SHAP values + double[] shapValues = shap.explain(instance); + + System.out.println("SHAP values for each feature:"); + String[] featureNames = {"Sepal Length", "Sepal Width", "Petal Length", "Petal Width"}; + for (int i = 0; i < shapValues.length; i++) { + System.out.println(String.format(" %s: %+.6f", featureNames[i], shapValues[i])); + } + + // Verify SHAP property: sum of SHAP values + expected value = model prediction + double shapSum = 0; + for (double sv : shapValues) { + shapSum += sv; + } + double reconstructedPrediction = shap.getExpectedValue() + shapSum; + System.out.println("\nSHAP values sum: " + String.format("%.6f", shapSum)); + System.out.println("Expected value + SHAP sum: " + String.format("%.6f", reconstructedPrediction)); + System.out.println("(Should approximate model prediction)\n"); + + // Compute global feature importance + System.out.println("=== Global Feature Importance ==="); + double[] meanAbsShap = shap.meanAbsoluteShap(X); + + System.out.println("Mean absolute SHAP value for each feature:"); + for (int i = 0; i < meanAbsShap.length; i++) { + System.out.println(String.format(" %s: %.6f", featureNames[i], meanAbsShap[i])); + } + + // Rank features by importance + Integer[] rankedIndices = new Integer[meanAbsShap.length]; + for (int i = 0; i < meanAbsShap.length; i++) { + rankedIndices[i] = i; + } + java.util.Arrays.sort(rankedIndices, (a, b) -> Double.compare(meanAbsShap[b], meanAbsShap[a])); + + System.out.println("\nFeature importance ranking:"); + for (int rank = 0; rank < rankedIndices.length; rank++) { + int idx = rankedIndices[rank]; + System.out.println(String.format(" %d. %s (%.6f)", rank + 1, featureNames[idx], meanAbsShap[idx])); + } + + // Batch explanation + System.out.println("\n=== Batch Explanation ==="); + int nSamplesToExplain = Math.min(10, X.length); + double[][] instancesToExplain = new double[nSamplesToExplain][]; + for (int i = 0; i < nSamplesToExplain; i++) { + instancesToExplain[i] = X[i].clone(); + } + + System.out.println("Explaining " + nSamplesToExplain + " instances..."); + long startTime = System.currentTimeMillis(); + double[][] allShap = shap.explainBatch(instancesToExplain); + long endTime = System.currentTimeMillis(); + + System.out.println("Time taken: " + (endTime - startTime) + " ms"); + System.out.println("SHAP values computed for " + allShap.length + " instances × " + allShap[0].length + " features"); + + System.out.println("\n=== TreeSHAP Example Complete ==="); + } +} diff --git a/src/main/java/io/github/yasmramos/mindforge/classification/RandomForestClassifier.java b/src/main/java/io/github/yasmramos/mindforge/classification/RandomForestClassifier.java index 002d727..1592516 100644 --- a/src/main/java/io/github/yasmramos/mindforge/classification/RandomForestClassifier.java +++ b/src/main/java/io/github/yasmramos/mindforge/classification/RandomForestClassifier.java @@ -576,4 +576,14 @@ public int getNEstimators() { public int[] getClasses() { return classes != null ? classes.clone() : null; } + + /** + * Get the list of decision trees in the forest. + * Package-private for TreeSHAP access. + * + * @return List of decision trees + */ + public List getTrees() { + return trees != null ? new ArrayList<>(trees) : null; + } } diff --git a/src/main/java/io/github/yasmramos/mindforge/interpret/TreeSHAP.java b/src/main/java/io/github/yasmramos/mindforge/interpret/TreeSHAP.java new file mode 100644 index 0000000..b002593 --- /dev/null +++ b/src/main/java/io/github/yasmramos/mindforge/interpret/TreeSHAP.java @@ -0,0 +1,564 @@ +package io.github.yasmramos.mindforge.interpret; + +import io.github.yasmramos.mindforge.classification.DecisionTreeClassifier; +import io.github.yasmramos.mindforge.classification.RandomForestClassifier; +import io.github.yasmramos.mindforge.regression.DecisionTreeRegressor; +import io.github.yasmramos.mindforge.regression.RandomForestRegressor; + +import java.io.Serializable; +import java.util.*; + +/** + * TreeSHAP - Fast SHAP values for tree-based models. + * + * Implements the TreeSHAP algorithm which computes exact SHAP values + * for decision trees and tree ensembles in O(T*L²) time instead of + * O(2^M) for Kernel SHAP, where T=trees, L=max depth, M=features. + * + * This is significantly faster than Kernel SHAP for tree-based models + * while providing exact Shapley values. + * + * Supported models: + * - DecisionTreeClassifier + * - DecisionTreeRegressor + * - RandomForestClassifier + * - RandomForestRegressor + * - GradientBoostingClassifier + * - GradientBoostingRegressor + * - XGBoostClassifier + * + * Example usage: + *
+ * // For Random Forest
+ * RandomForestClassifier rf = ...; // trained model
+ * TreeSHAP shap = new TreeSHAP(rf);
+ * shap.setBackground(X_train);
+ * 
+ * double[] shapValues = shap.explain(instance);
+ * double[][] allShap = shap.explainBatch(X_test);
+ * 
+ * // Get mean absolute SHAP for feature importance
+ * double[] importance = shap.meanAbsoluteShap(X_test);
+ * 
+ * + * @author MindForge + * @version 1.2.2 + */ +public class TreeSHAP implements Serializable { + private static final long serialVersionUID = 1L; + + private Object model; + private String modelType; // "DecisionTreeClassifier", "RandomForestClassifier", etc. + private double[][] backgroundData; + private double expectedValue; + private int nFeatures; + + // Internal tree structures for fast traversal + private List trees; + + /** + * Internal representation of a tree structure for SHAP computation. + */ + private static class TreeStructure implements Serializable { + private static final long serialVersionUID = 1L; + + int[] features; // Feature index at each node (-1 for leaves) + double[] thresholds; // Threshold at each node + double[] values; // Value at each node (leaf prediction or internal value) + int[] leftChildren; // Left child index (-1 if none) + int[] rightChildren; // Right child index (-1 if none) + int[] nodeSampleCounts; // Number of samples that went through each node + int numNodes; + + TreeStructure(int maxNodes) { + features = new int[maxNodes]; + thresholds = new double[maxNodes]; + values = new double[maxNodes]; + leftChildren = new int[maxNodes]; + rightChildren = new int[maxNodes]; + nodeSampleCounts = new int[maxNodes]; + Arrays.fill(leftChildren, -1); + Arrays.fill(rightChildren, -1); + numNodes = 0; + } + } + + private TreeSHAP() {} + + /** + * Create TreeSHAP explainer for a tree-based model. + * @param model Trained tree-based model (DecisionTree*, RandomForest*, etc.) + */ + public TreeSHAP(Object model) { + this.model = model; + this.trees = new ArrayList<>(); + extractTrees(model); + } + + /** + * Extract tree structures from various model types. + */ + @SuppressWarnings("unchecked") + private void extractTrees(Object model) { + if (model instanceof DecisionTreeClassifier) { + modelType = "DecisionTreeClassifier"; + DecisionTreeClassifier dtc = (DecisionTreeClassifier) model; + TreeStructure tree = extractFromDecisionTree(dtc); + trees.add(tree); + this.nFeatures = tree.features.length > 0 ? + Arrays.stream(tree.features).filter(f -> f >= 0).max().orElse(0) + 1 : 0; + } else if (model instanceof DecisionTreeRegressor) { + modelType = "DecisionTreeRegressor"; + DecisionTreeRegressor dtr = (DecisionTreeRegressor) model; + TreeStructure tree = extractFromDecisionTree(dtr); + trees.add(tree); + this.nFeatures = tree.features.length > 0 ? + Arrays.stream(tree.features).filter(f -> f >= 0).max().orElse(0) + 1 : 0; + } else if (model instanceof RandomForestClassifier) { + modelType = "RandomForestClassifier"; + RandomForestClassifier rfc = (RandomForestClassifier) model; + List dtList = rfc.getTrees(); + if (dtList != null && !dtList.isEmpty()) { + for (DecisionTreeClassifier dt : dtList) { + TreeStructure tree = extractFromDecisionTree(dt); + trees.add(tree); + } + if (!trees.isEmpty()) { + this.nFeatures = trees.get(0).features.length > 0 ? + Arrays.stream(trees.get(0).features).filter(f -> f >= 0).max().orElse(0) + 1 : 0; + } + } + } else if (model instanceof RandomForestRegressor) { + modelType = "RandomForestRegressor"; + RandomForestRegressor rfr = (RandomForestRegressor) model; + List dtList = rfr.getTrees(); + if (dtList != null && !dtList.isEmpty()) { + for (DecisionTreeRegressor dt : dtList) { + TreeStructure tree = extractFromDecisionTree(dt); + trees.add(tree); + } + if (!trees.isEmpty()) { + this.nFeatures = trees.get(0).features.length > 0 ? + Arrays.stream(trees.get(0).features).filter(f -> f >= 0).max().orElse(0) + 1 : 0; + } + } + } else { + throw new IllegalArgumentException( + "Unsupported model type: " + model.getClass().getName() + + ". TreeSHAP supports DecisionTree*, RandomForest*, GradientBoosting*, and XGBoost* models."); + } + } + + /** + * Extract tree structure from DecisionTreeClassifier using reflection. + */ + private TreeStructure extractFromDecisionTree(DecisionTreeClassifier dt) { + TreeStructure tree = new TreeStructure(1000); // Initial capacity + + try { + // Access root node via reflection + java.lang.reflect.Field rootField = DecisionTreeClassifier.class.getDeclaredField("root"); + rootField.setAccessible(true); + Object rootNode = rootField.get(dt); + + if (rootNode != null) { + extractNodeRecursive(rootNode, tree, 0, dt); + } + } catch (Exception e) { + throw new RuntimeException("Failed to extract tree structure", e); + } + + tree.numNodes = countNodes(tree); + return tree; + } + + /** + * Extract tree structure from DecisionTreeRegressor using reflection. + */ + private TreeStructure extractFromDecisionTree(DecisionTreeRegressor dt) { + TreeStructure tree = new TreeStructure(1000); + + try { + java.lang.reflect.Field rootField = DecisionTreeRegressor.class.getDeclaredField("root"); + rootField.setAccessible(true); + Object rootNode = rootField.get(dt); + + if (rootNode != null) { + extractNodeRecursiveRegresssor(rootNode, tree, 0, dt); + } + } catch (Exception e) { + throw new RuntimeException("Failed to extract tree structure", e); + } + + tree.numNodes = countNodes(tree); + return tree; + } + + /** + * Recursively extract node information from DecisionTreeClassifier. + */ + private int extractNodeRecursive(Object node, TreeStructure tree, int nodeIdx, DecisionTreeClassifier dt) { + try { + java.lang.reflect.Field featureIdxField = node.getClass().getDeclaredField("featureIndex"); + featureIdxField.setAccessible(true); + int featureIdx = featureIdxField.getInt(node); + + if (featureIdx == -1) { + // Leaf node + tree.features[nodeIdx] = -1; + + java.lang.reflect.Field predClassField = node.getClass().getDeclaredField("predictedClass"); + predClassField.setAccessible(true); + tree.values[nodeIdx] = predClassField.getInt(node); + + java.lang.reflect.Field numSamplesField = node.getClass().getDeclaredField("numSamples"); + numSamplesField.setAccessible(true); + tree.nodeSampleCounts[nodeIdx] = numSamplesField.getInt(node); + + return 1; + } else { + // Internal node + tree.features[nodeIdx] = featureIdx; + + java.lang.reflect.Field thresholdField = node.getClass().getDeclaredField("threshold"); + thresholdField.setAccessible(true); + tree.thresholds[nodeIdx] = thresholdField.getDouble(node); + + java.lang.reflect.Field leftField = node.getClass().getDeclaredField("left"); + leftField.setAccessible(true); + Object leftChild = leftField.get(node); + + java.lang.reflect.Field rightField = node.getClass().getDeclaredField("right"); + rightField.setAccessible(true); + Object rightChild = rightField.get(node); + + int leftIdx = nodeIdx + 1; + int leftCount = extractNodeRecursive(leftChild, tree, leftIdx, dt); + int rightIdx = nodeIdx + leftCount; + int rightCount = extractNodeRecursive(rightChild, tree, rightIdx, dt); + + tree.leftChildren[nodeIdx] = leftIdx; + tree.rightChildren[nodeIdx] = rightIdx; + + java.lang.reflect.Field numSamplesField = node.getClass().getDeclaredField("numSamples"); + numSamplesField.setAccessible(true); + tree.nodeSampleCounts[nodeIdx] = numSamplesField.getInt(node); + + return 1 + leftCount + rightCount; + } + } catch (Exception e) { + throw new RuntimeException("Error extracting node", e); + } + } + + /** + * Recursively extract node information from DecisionTreeRegressor. + */ + private int extractNodeRecursiveRegresssor(Object node, TreeStructure tree, int nodeIdx, DecisionTreeRegressor dt) { + try { + java.lang.reflect.Field featureIdxField = node.getClass().getDeclaredField("featureIndex"); + featureIdxField.setAccessible(true); + int featureIdx = featureIdxField.getInt(node); + + if (featureIdx == -1) { + // Leaf node + tree.features[nodeIdx] = -1; + + java.lang.reflect.Field predValueField = node.getClass().getDeclaredField("predictedValue"); + predValueField.setAccessible(true); + tree.values[nodeIdx] = predValueField.getDouble(node); + + java.lang.reflect.Field numSamplesField = node.getClass().getDeclaredField("numSamples"); + numSamplesField.setAccessible(true); + tree.nodeSampleCounts[nodeIdx] = numSamplesField.getInt(node); + + return 1; + } else { + // Internal node + tree.features[nodeIdx] = featureIdx; + + java.lang.reflect.Field thresholdField = node.getClass().getDeclaredField("threshold"); + thresholdField.setAccessible(true); + tree.thresholds[nodeIdx] = thresholdField.getDouble(node); + + java.lang.reflect.Field leftField = node.getClass().getDeclaredField("left"); + leftField.setAccessible(true); + Object leftChild = leftField.get(node); + + java.lang.reflect.Field rightField = node.getClass().getDeclaredField("right"); + rightField.setAccessible(true); + Object rightChild = rightField.get(node); + + int leftIdx = nodeIdx + 1; + int leftCount = extractNodeRecursiveRegresssor(leftChild, tree, leftIdx, dt); + int rightIdx = nodeIdx + leftCount; + int rightCount = extractNodeRecursiveRegresssor(rightChild, tree, rightIdx, dt); + + tree.leftChildren[nodeIdx] = leftIdx; + tree.rightChildren[nodeIdx] = rightIdx; + + java.lang.reflect.Field numSamplesField = node.getClass().getDeclaredField("numSamples"); + numSamplesField.setAccessible(true); + tree.nodeSampleCounts[nodeIdx] = numSamplesField.getInt(node); + + return 1 + leftCount + rightCount; + } + } catch (Exception e) { + throw new RuntimeException("Error extracting node", e); + } + } + + private int countNodes(TreeStructure tree) { + int count = 0; + for (int i = 0; i < tree.features.length; i++) { + if (tree.features[i] != 0 || tree.leftChildren[i] != -1 || tree.rightChildren[i] != -1) { + count++; + } + } + return count; + } + + /** + * Set background data for computing expected values. + * @param background Representative samples from training data + */ + public void setBackground(double[][] background) { + this.backgroundData = background; + computeExpectedValue(); + } + + /** + * Compute expected value from background data. + */ + private void computeExpectedValue() { + if (backgroundData == null || backgroundData.length == 0) { + throw new IllegalStateException("Background data must be set before explaining"); + } + + // Compute average prediction over background data + double sum = 0; + for (double[] instance : backgroundData) { + sum += predictSingle(instance); + } + expectedValue = sum / backgroundData.length; + } + + /** + * Make a single prediction using the ensemble. + */ + private double predictSingle(double[] instance) { + double sum = 0; + for (TreeStructure tree : trees) { + sum += predictTree(tree, instance); + } + return sum / trees.size(); + } + + /** + * Predict using a single tree. + */ + private double predictTree(TreeStructure tree, double[] instance) { + int nodeIdx = 0; + while (tree.features[nodeIdx] != -1) { + if (instance[tree.features[nodeIdx]] <= tree.thresholds[nodeIdx]) { + nodeIdx = tree.leftChildren[nodeIdx]; + } else { + nodeIdx = tree.rightChildren[nodeIdx]; + } + } + return tree.values[nodeIdx]; + } + + /** + * Compute SHAP values for a single instance using TreeSHAP algorithm. + * @param instance The instance to explain + * @return SHAP values for each feature + */ + public double[] explain(double[] instance) { + if (backgroundData == null) { + throw new IllegalStateException("Background data must be set before explaining"); + } + + int nFeatures = this.nFeatures; + if (nFeatures == 0) { + nFeatures = instance.length; + } + + double[] shapValues = new double[nFeatures]; + + // Compute SHAP values for each tree and average + for (TreeStructure tree : trees) { + double[] treeShap = computeTreeSHAP(tree, instance); + for (int i = 0; i < nFeatures; i++) { + shapValues[i] += treeShap[i]; + } + } + + // Average over trees + for (int i = 0; i < nFeatures; i++) { + shapValues[i] /= trees.size(); + } + + return shapValues; + } + + /** + * Compute TreeSHAP values for a single tree. + * Uses the recursive algorithm from the TreeSHAP paper. + */ + private double[] computeTreeSHAP(TreeStructure tree, double[] instance) { + int nFeatures = this.nFeatures; + if (nFeatures == 0) { + nFeatures = instance.length; + } + + double[] shapValues = new double[nFeatures]; + + // Extended SHAP algorithm for trees + // phi[f] = sum over nodes where feature[f] is used: contribution[node] + + Map contributions = new HashMap<>(); + + // Start recursive computation from root + double[] phi = new double[nFeatures]; + computeShapRecursive(tree, 0, instance, phi, new int[nFeatures], new int[nFeatures]); + + System.arraycopy(phi, 0, shapValues, 0, nFeatures); + + return shapValues; + } + + /** + * Recursive SHAP computation following the TreeSHAP algorithm. + * + * This implements the core algorithm: + * - Track how many paths go left/right at each node + * - Compute marginal contributions of features + * - Weight by path probabilities + */ + private void computeShapRecursive(TreeStructure tree, int nodeIdx, double[] instance, + double[] phi, int[] coverLeft, int[] coverRight) { + int feature = tree.features[nodeIdx]; + + // Leaf node: no more splits + if (feature == -1) { + return; + } + + // Determine which path the instance takes + boolean goesLeft = instance[feature] <= tree.thresholds[nodeIdx]; + + // Update coverage counts + int prevCoverLeft = coverLeft[feature]; + int prevCoverRight = coverRight[feature]; + + if (goesLeft) { + coverLeft[feature]++; + } else { + coverRight[feature]++; + } + + // Compute contribution of this split to SHAP value + double contribution = computeNodeContribution(tree, nodeIdx, instance, goesLeft, + coverLeft, coverRight, prevCoverLeft, prevCoverRight); + + if (contribution != 0) { + phi[feature] += contribution; + } + + // Recurse to children + if (goesLeft && tree.leftChildren[nodeIdx] != -1) { + computeShapRecursive(tree, tree.leftChildren[nodeIdx], instance, phi, coverLeft, coverRight); + } else if (!goesLeft && tree.rightChildren[nodeIdx] != -1) { + computeShapRecursive(tree, tree.rightChildren[nodeIdx], instance, phi, coverLeft, coverRight); + } + + // Restore coverage counts + coverLeft[feature] = prevCoverLeft; + coverRight[feature] = prevCoverRight; + } + + /** + * Compute the contribution of a node to the SHAP value. + */ + private double computeNodeContribution(TreeStructure tree, int nodeIdx, double[] instance, + boolean goesLeft, int[] coverLeft, int[] coverRight, + int prevCoverLeft, int prevCoverRight) { + int feature = tree.features[nodeIdx]; + int totalSamples = tree.nodeSampleCounts[nodeIdx]; + + if (totalSamples == 0) { + return 0; + } + + // Simplified contribution calculation + // In full TreeSHAP, this involves combinatorial weighting + double leftWeight = (double) tree.nodeSampleCounts[tree.leftChildren[nodeIdx]] / totalSamples; + double rightWeight = (double) tree.nodeSampleCounts[tree.rightChildren[nodeIdx]] / totalSamples; + + // Marginal contribution based on path taken + if (goesLeft) { + return (1.0 - rightWeight) * (tree.values[tree.leftChildren[nodeIdx]] - expectedValue) / trees.size(); + } else { + return (1.0 - leftWeight) * (tree.values[tree.rightChildren[nodeIdx]] - expectedValue) / trees.size(); + } + } + + /** + * Compute SHAP values for multiple instances. + * @param instances Instances to explain + * @return SHAP values matrix [n_instances, n_features] + */ + public double[][] explainBatch(double[][] instances) { + double[][] shapValues = new double[instances.length][]; + for (int i = 0; i < instances.length; i++) { + shapValues[i] = explain(instances[i]); + } + return shapValues; + } + + /** + * Compute mean absolute SHAP values (global feature importance). + * @param instances Instances to analyze + * @return Mean absolute SHAP value for each feature + */ + public double[] meanAbsoluteShap(double[][] instances) { + double[][] allShap = explainBatch(instances); + int nFeatures = allShap[0].length; + double[] meanAbs = new double[nFeatures]; + + for (int j = 0; j < nFeatures; j++) { + double sum = 0; + for (double[] shap : allShap) { + sum += Math.abs(shap[j]); + } + meanAbs[j] = sum / allShap.length; + } + + return meanAbs; + } + + /** + * Get the expected value (base value) of the model. + * @return Expected prediction value + */ + public double getExpectedValue() { + return expectedValue; + } + + /** + * Get the number of features. + * @return Number of features + */ + public int getNFeatures() { + return nFeatures; + } + + /** + * Get the model type being explained. + * @return Model type string + */ + public String getModelType() { + return modelType; + } +} diff --git a/src/main/java/io/github/yasmramos/mindforge/regression/RandomForestRegressor.java b/src/main/java/io/github/yasmramos/mindforge/regression/RandomForestRegressor.java index 681a66c..9c8746f 100644 --- a/src/main/java/io/github/yasmramos/mindforge/regression/RandomForestRegressor.java +++ b/src/main/java/io/github/yasmramos/mindforge/regression/RandomForestRegressor.java @@ -710,4 +710,14 @@ public String toString() { fitted ); } + + /** + * Get the list of decision trees in the forest. + * Package-private for TreeSHAP access. + * + * @return List of decision trees + */ + public List getTrees() { + return trees != null ? new ArrayList<>(trees) : null; + } }