From 8f2e5c67b7adc6f5a0ae853837ebb516967bca6c Mon Sep 17 00:00:00 2001 From: lewuathe Date: Fri, 1 Apr 2016 23:29:53 +0900 Subject: [PATCH 1/3] [SPARK-11938] Expose numFeatures in all ML PredictionModel for PySpark Aggregate numFeatures property in HasNumFeaturesModel in base.py. --- python/pyspark/ml/base.py | 13 +++++++++++++ python/pyspark/ml/classification.py | 25 +++++++++++++++++++------ python/pyspark/ml/regression.py | 17 +++++++++++++---- 3 files changed, 45 insertions(+), 10 deletions(-) diff --git a/python/pyspark/ml/base.py b/python/pyspark/ml/base.py index 339e5d6af52a..923fd52bd522 100644 --- a/python/pyspark/ml/base.py +++ b/python/pyspark/ml/base.py @@ -116,3 +116,16 @@ class Model(Transformer): """ __metaclass__ = ABCMeta + + +class HasNumFeaturesModel: + """ + Provides getter of the number of features especially for model class + It should be mixin with JavaModel. + """ + @property + def numFeatures(self): + """ + The number of features used to train the model. + """ + return self._call_java("numFeatures") \ No newline at end of file diff --git a/python/pyspark/ml/classification.py b/python/pyspark/ml/classification.py index d6d713ca5303..b89c56d2ea98 100644 --- a/python/pyspark/ml/classification.py +++ b/python/pyspark/ml/classification.py @@ -20,6 +20,7 @@ from pyspark import since, keyword_only from pyspark.ml import Estimator, Model +from pyspark.ml.base import HasNumFeaturesModel from pyspark.ml.param.shared import * from pyspark.ml.regression import DecisionTreeModel, DecisionTreeRegressionModel, \ RandomForestParams, TreeEnsembleModels, TreeEnsembleParams @@ -214,7 +215,7 @@ def _checkThresholdConsistency(self): " threshold (%g) and thresholds (equivalent to %g)" % (t2, t)) -class LogisticRegressionModel(JavaModel, JavaMLWritable, JavaMLReadable): +class LogisticRegressionModel(HasNumFeaturesModel, JavaModel, JavaMLWritable, JavaMLReadable): """ .. note:: Experimental @@ -548,6 +549,8 @@ class DecisionTreeClassifier(JavaEstimator, HasFeaturesCol, HasLabelCol, HasPred >>> model2 = DecisionTreeClassificationModel.load(model_path) >>> model.featureImportances == model2.featureImportances True + >>> model.numFeatures + 1 .. versionadded:: 1.4.0 """ @@ -597,7 +600,7 @@ def _create_model(self, java_model): @inherit_doc -class DecisionTreeClassificationModel(DecisionTreeModel, JavaMLWritable, JavaMLReadable): +class DecisionTreeClassificationModel(HasNumFeaturesModel, DecisionTreeModel, JavaMLWritable, JavaMLReadable): """ .. note:: Experimental @@ -680,6 +683,8 @@ class RandomForestClassifier(JavaEstimator, HasFeaturesCol, HasLabelCol, HasPred >>> model2 = RandomForestClassificationModel.load(model_path) >>> model.featureImportances == model2.featureImportances True + >>> model.numFeatures == model2.numFeatures + True .. versionadded:: 1.4.0 """ @@ -728,7 +733,8 @@ def _create_model(self, java_model): return RandomForestClassificationModel(java_model) -class RandomForestClassificationModel(TreeEnsembleModels, JavaMLWritable, JavaMLReadable): +class RandomForestClassificationModel(TreeEnsembleModels, JavaMLWritable, JavaMLReadable, + HasNumFeaturesModel): """ .. note:: Experimental @@ -820,6 +826,8 @@ class GBTClassifier(JavaEstimator, HasFeaturesCol, HasLabelCol, HasPredictionCol True >>> model.trees [DecisionTreeRegressionModel (uid=...) of depth..., DecisionTreeRegressionModel...] + >>> model.numFeatures == model2.numFeatures + True .. versionadded:: 1.4.0 """ @@ -883,7 +891,7 @@ def getLossType(self): return self.getOrDefault(self.lossType) -class GBTClassificationModel(TreeEnsembleModels, JavaMLWritable, JavaMLReadable): +class GBTClassificationModel(TreeEnsembleModels, JavaMLWritable, JavaMLReadable, HasNumFeaturesModel): """ .. note:: Experimental @@ -969,6 +977,8 @@ class NaiveBayes(JavaEstimator, HasFeaturesCol, HasLabelCol, HasPredictionCol, H >>> result = model3.transform(test0).head() >>> result.prediction 0.0 + >>> model.numFeatures == model2.numFeatures + 2 .. versionadded:: 1.5.0 """ @@ -1041,7 +1051,7 @@ def getModelType(self): return self.getOrDefault(self.modelType) -class NaiveBayesModel(JavaModel, JavaMLWritable, JavaMLReadable): +class NaiveBayesModel(JavaModel, JavaMLWritable, JavaMLReadable, HasNumFeaturesModel): """ .. note:: Experimental @@ -1120,6 +1130,8 @@ class MultilayerPerceptronClassifier(JavaEstimator, HasFeaturesCol, HasLabelCol, True >>> model3.layers == model.layers True + >>> model.numFeatures == model2.numFeatures + True .. versionadded:: 1.6.0 """ @@ -1242,7 +1254,8 @@ def getInitialWeights(self): return self.getOrDefault(self.initialWeights) -class MultilayerPerceptronClassificationModel(JavaModel, JavaMLWritable, JavaMLReadable): +class MultilayerPerceptronClassificationModel(JavaModel, JavaMLWritable, JavaMLReadable, + HasNumFeaturesModel): """ .. note:: Experimental diff --git a/python/pyspark/ml/regression.py b/python/pyspark/ml/regression.py index 29efd6a852e8..5a0f1eef8840 100644 --- a/python/pyspark/ml/regression.py +++ b/python/pyspark/ml/regression.py @@ -18,6 +18,7 @@ import warnings from pyspark import since, keyword_only +from pyspark.ml.base import HasNumFeaturesModel from pyspark.ml.param.shared import * from pyspark.ml.util import * from pyspark.ml.wrapper import JavaEstimator, JavaModel, JavaWrapper @@ -90,6 +91,8 @@ class LinearRegression(JavaEstimator, HasFeaturesCol, HasLabelCol, HasPrediction True >>> model.intercept == model2.intercept True + >>> model.numFeatures == model2.numFeatures + True .. versionadded:: 1.4.0 """ @@ -128,7 +131,7 @@ def _create_model(self, java_model): return LinearRegressionModel(java_model) -class LinearRegressionModel(JavaModel, JavaMLWritable, JavaMLReadable): +class LinearRegressionModel(JavaModel, JavaMLWritable, JavaMLReadable, HasNumFeaturesModel): """ .. note:: Experimental @@ -678,6 +681,8 @@ class DecisionTreeRegressor(JavaEstimator, HasFeaturesCol, HasLabelCol, HasPredi True >>> model.transform(test1).head().variance 0.0 + >>> model.numFeatures + 1 .. versionadded:: 1.4.0 """ @@ -799,7 +804,8 @@ def __repr__(self): @inherit_doc -class DecisionTreeRegressionModel(DecisionTreeModel, JavaMLWritable, JavaMLReadable): +class DecisionTreeRegressionModel(DecisionTreeModel, JavaMLWritable, JavaMLReadable, + HasNumFeaturesModel): """ .. note:: Experimental @@ -872,6 +878,8 @@ class RandomForestRegressor(JavaEstimator, HasFeaturesCol, HasLabelCol, HasPredi >>> model2 = RandomForestRegressionModel.load(model_path) >>> model.featureImportances == model2.featureImportances True + >>> model.numFeatures == model2.numFeatures + True .. versionadded:: 1.4.0 """ @@ -921,7 +929,7 @@ def _create_model(self, java_model): return RandomForestRegressionModel(java_model) -class RandomForestRegressionModel(TreeEnsembleModels, JavaMLWritable, JavaMLReadable): +class RandomForestRegressionModel(TreeEnsembleModels, JavaMLWritable, JavaMLReadable, HasNumFeaturesModel): """ .. note:: Experimental @@ -996,6 +1004,7 @@ class GBTRegressor(JavaEstimator, HasFeaturesCol, HasLabelCol, HasPredictionCol, True >>> model.trees [DecisionTreeRegressionModel (uid=...) of depth..., DecisionTreeRegressionModel...] + >>> model.numFeatures == model2.numFeatures .. versionadded:: 1.4.0 """ @@ -1063,7 +1072,7 @@ def getLossType(self): return self.getOrDefault(self.lossType) -class GBTRegressionModel(TreeEnsembleModels, JavaMLWritable, JavaMLReadable): +class GBTRegressionModel(TreeEnsembleModels, JavaMLWritable, JavaMLReadable, HasNumFeaturesModel): """ .. note:: Experimental From 461b7c64a274a745f88f8eeb0d9c39470c7aa958 Mon Sep 17 00:00:00 2001 From: lewuathe Date: Sat, 2 Apr 2016 11:16:33 +0900 Subject: [PATCH 2/3] [SPARK-11938] Fix style --- python/pyspark/ml/base.py | 2 +- python/pyspark/ml/classification.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/python/pyspark/ml/base.py b/python/pyspark/ml/base.py index 923fd52bd522..8284a130a57d 100644 --- a/python/pyspark/ml/base.py +++ b/python/pyspark/ml/base.py @@ -128,4 +128,4 @@ def numFeatures(self): """ The number of features used to train the model. """ - return self._call_java("numFeatures") \ No newline at end of file + return self._call_java("numFeatures") diff --git a/python/pyspark/ml/classification.py b/python/pyspark/ml/classification.py index b89c56d2ea98..51fe9568026f 100644 --- a/python/pyspark/ml/classification.py +++ b/python/pyspark/ml/classification.py @@ -600,7 +600,8 @@ def _create_model(self, java_model): @inherit_doc -class DecisionTreeClassificationModel(HasNumFeaturesModel, DecisionTreeModel, JavaMLWritable, JavaMLReadable): +class DecisionTreeClassificationModel(HasNumFeaturesModel, DecisionTreeModel, JavaMLWritable, + JavaMLReadable): """ .. note:: Experimental From 872d384be9710eb5fd5c381ac4dd9eb7e40fa00a Mon Sep 17 00:00:00 2001 From: Kai Jiang Date: Mon, 27 Jun 2016 00:50:14 -0700 Subject: [PATCH 3/3] export numFeatures in ML PredictionModel --- python/pyspark/ml/base.py | 13 ----- python/pyspark/ml/classification.py | 84 ++++++++++++++++++++++++----- python/pyspark/ml/regression.py | 55 ++++++++++++++++--- 3 files changed, 118 insertions(+), 34 deletions(-) diff --git a/python/pyspark/ml/base.py b/python/pyspark/ml/base.py index 8284a130a57d..339e5d6af52a 100644 --- a/python/pyspark/ml/base.py +++ b/python/pyspark/ml/base.py @@ -116,16 +116,3 @@ class Model(Transformer): """ __metaclass__ = ABCMeta - - -class HasNumFeaturesModel: - """ - Provides getter of the number of features especially for model class - It should be mixin with JavaModel. - """ - @property - def numFeatures(self): - """ - The number of features used to train the model. - """ - return self._call_java("numFeatures") diff --git a/python/pyspark/ml/classification.py b/python/pyspark/ml/classification.py index 51fe9568026f..2ec22f23e3c3 100644 --- a/python/pyspark/ml/classification.py +++ b/python/pyspark/ml/classification.py @@ -20,7 +20,6 @@ from pyspark import since, keyword_only from pyspark.ml import Estimator, Model -from pyspark.ml.base import HasNumFeaturesModel from pyspark.ml.param.shared import * from pyspark.ml.regression import DecisionTreeModel, DecisionTreeRegressionModel, \ RandomForestParams, TreeEnsembleModels, TreeEnsembleParams @@ -66,6 +65,8 @@ class LogisticRegression(JavaEstimator, HasFeaturesCol, HasLabelCol, HasPredicti DenseVector([5.5...]) >>> model.intercept -2.68... + >>> model.numFeatures + 1 >>> test0 = sc.parallelize([Row(features=Vectors.dense(-1.0))]).toDF() >>> result = model.transform(test0).head() >>> result.prediction @@ -93,6 +94,8 @@ class LogisticRegression(JavaEstimator, HasFeaturesCol, HasLabelCol, HasPredicti True >>> model.intercept == model2.intercept True + >>> model.numFeatures == model2.numFeatures + True .. versionadded:: 1.3.0 """ @@ -215,7 +218,7 @@ def _checkThresholdConsistency(self): " threshold (%g) and thresholds (equivalent to %g)" % (t2, t)) -class LogisticRegressionModel(HasNumFeaturesModel, JavaModel, JavaMLWritable, JavaMLReadable): +class LogisticRegressionModel(JavaModel, JavaMLWritable, JavaMLReadable): """ .. note:: Experimental @@ -240,6 +243,14 @@ def intercept(self): """ return self._call_java("intercept") + @property + @since("2.0.0") + def numFeatures(self): + """ + Number of features the model was trained on. + """ + return self._call_java("numFeatures") + @property @since("2.0.0") def summary(self): @@ -525,6 +536,8 @@ class DecisionTreeClassifier(JavaEstimator, HasFeaturesCol, HasLabelCol, HasPred 1 >>> model.featureImportances SparseVector(1, {0: 1.0}) + >>> model.numFeatures + 1 >>> print(model.toDebugString) DecisionTreeClassificationModel (uid=...) of depth 1 with 3 nodes... >>> test0 = spark.createDataFrame([(Vectors.dense(-1.0),)], ["features"]) @@ -549,8 +562,8 @@ class DecisionTreeClassifier(JavaEstimator, HasFeaturesCol, HasLabelCol, HasPred >>> model2 = DecisionTreeClassificationModel.load(model_path) >>> model.featureImportances == model2.featureImportances True - >>> model.numFeatures - 1 + >>> model.numFeatures == model2.numFeatures + True .. versionadded:: 1.4.0 """ @@ -600,8 +613,7 @@ def _create_model(self, java_model): @inherit_doc -class DecisionTreeClassificationModel(HasNumFeaturesModel, DecisionTreeModel, JavaMLWritable, - JavaMLReadable): +class DecisionTreeClassificationModel(DecisionTreeModel, JavaMLWritable, JavaMLReadable): """ .. note:: Experimental @@ -631,6 +643,14 @@ def featureImportances(self): """ return self._call_java("featureImportances") + @property + @since("2.0.0") + def numFeatures(self): + """ + Number of features the model was trained on. + """ + return self._call_java("numFeatures") + @inherit_doc class RandomForestClassifier(JavaEstimator, HasFeaturesCol, HasLabelCol, HasPredictionCol, HasSeed, @@ -672,6 +692,8 @@ class RandomForestClassifier(JavaEstimator, HasFeaturesCol, HasLabelCol, HasPred >>> test1 = spark.createDataFrame([(Vectors.sparse(1, [0], [1.0]),)], ["features"]) >>> model.transform(test1).head().prediction 1.0 + >>> model.numFeatures + 1 >>> model.trees [DecisionTreeClassificationModel (uid=...) of depth..., DecisionTreeClassificationModel...] >>> rfc_path = temp_path + "/rfc" @@ -734,8 +756,7 @@ def _create_model(self, java_model): return RandomForestClassificationModel(java_model) -class RandomForestClassificationModel(TreeEnsembleModels, JavaMLWritable, JavaMLReadable, - HasNumFeaturesModel): +class RandomForestClassificationModel(TreeEnsembleModels, JavaMLWritable, JavaMLReadable): """ .. note:: Experimental @@ -759,6 +780,14 @@ def featureImportances(self): """ return self._call_java("featureImportances") + @property + @since("2.0.0") + def numFeatures(self): + """ + Number of features the model was trained on. + """ + return self._call_java("numFeatures") + @property @since("2.0.0") def trees(self): @@ -811,6 +840,8 @@ class GBTClassifier(JavaEstimator, HasFeaturesCol, HasLabelCol, HasPredictionCol 1.0 >>> model.totalNumNodes 15 + >>> model.numFeatures + 1 >>> print(model.toDebugString) GBTClassificationModel (uid=...)...with 5 trees... >>> gbtc_path = temp_path + "gbtc" @@ -892,7 +923,7 @@ def getLossType(self): return self.getOrDefault(self.lossType) -class GBTClassificationModel(TreeEnsembleModels, JavaMLWritable, JavaMLReadable, HasNumFeaturesModel): +class GBTClassificationModel(TreeEnsembleModels, JavaMLWritable, JavaMLReadable): """ .. note:: Experimental @@ -916,6 +947,14 @@ def featureImportances(self): """ return self._call_java("featureImportances") + @property + @since("2.0.0") + def numFeatures(self): + """ + Number of features the model was trained on. + """ + return self._call_java("numFeatures") + @property @since("2.0.0") def trees(self): @@ -961,6 +1000,8 @@ class NaiveBayes(JavaEstimator, HasFeaturesCol, HasLabelCol, HasPredictionCol, H >>> test1 = sc.parallelize([Row(features=Vectors.sparse(2, [0], [1.0]))]).toDF() >>> model.transform(test1).head().prediction 1.0 + >>> model.numFeatures + 2 >>> nb_path = temp_path + "/nb" >>> nb.save(nb_path) >>> nb2 = NaiveBayes.load(nb_path) @@ -979,7 +1020,7 @@ class NaiveBayes(JavaEstimator, HasFeaturesCol, HasLabelCol, HasPredictionCol, H >>> result.prediction 0.0 >>> model.numFeatures == model2.numFeatures - 2 + True .. versionadded:: 1.5.0 """ @@ -1052,7 +1093,7 @@ def getModelType(self): return self.getOrDefault(self.modelType) -class NaiveBayesModel(JavaModel, JavaMLWritable, JavaMLReadable, HasNumFeaturesModel): +class NaiveBayesModel(JavaModel, JavaMLWritable, JavaMLReadable): """ .. note:: Experimental @@ -1077,6 +1118,14 @@ def theta(self): """ return self._call_java("theta") + @property + @since("2.0.0") + def numFeatures(self): + """ + Number of features the model was trained on. + """ + return self._call_java("numFeatures") + @inherit_doc class MultilayerPerceptronClassifier(JavaEstimator, HasFeaturesCol, HasLabelCol, HasPredictionCol, @@ -1102,6 +1151,8 @@ class MultilayerPerceptronClassifier(JavaEstimator, HasFeaturesCol, HasLabelCol, [2, 2, 2] >>> model.weights.size 12 + >>> model.numFeatures + 2 >>> testDF = spark.createDataFrame([ ... (Vectors.dense([1.0, 0.0]),), ... (Vectors.dense([0.0, 0.0]),)], ["features"]) @@ -1255,8 +1306,7 @@ def getInitialWeights(self): return self.getOrDefault(self.initialWeights) -class MultilayerPerceptronClassificationModel(JavaModel, JavaMLWritable, JavaMLReadable, - HasNumFeaturesModel): +class MultilayerPerceptronClassificationModel(JavaModel, JavaMLWritable, JavaMLReadable): """ .. note:: Experimental @@ -1281,6 +1331,14 @@ def weights(self): """ return self._call_java("weights") + @property + @since("2.0.0") + def numFeatures(self): + """ + Number of features the model was trained on. + """ + return self._call_java("numFeatures") + class OneVsRestParams(HasFeaturesCol, HasLabelCol, HasPredictionCol): """ diff --git a/python/pyspark/ml/regression.py b/python/pyspark/ml/regression.py index 5a0f1eef8840..9cab65ad73bd 100644 --- a/python/pyspark/ml/regression.py +++ b/python/pyspark/ml/regression.py @@ -18,7 +18,6 @@ import warnings from pyspark import since, keyword_only -from pyspark.ml.base import HasNumFeaturesModel from pyspark.ml.param.shared import * from pyspark.ml.util import * from pyspark.ml.wrapper import JavaEstimator, JavaModel, JavaWrapper @@ -72,6 +71,8 @@ class LinearRegression(JavaEstimator, HasFeaturesCol, HasLabelCol, HasPrediction True >>> abs(model.intercept - 0.0) < 0.001 True + >>> model.numFeatures + 1 >>> test1 = spark.createDataFrame([(Vectors.sparse(1, [0], [1.0]),)], ["features"]) >>> abs(model.transform(test1).head().prediction - 1.0) < 0.001 True @@ -131,7 +132,7 @@ def _create_model(self, java_model): return LinearRegressionModel(java_model) -class LinearRegressionModel(JavaModel, JavaMLWritable, JavaMLReadable, HasNumFeaturesModel): +class LinearRegressionModel(JavaModel, JavaMLWritable, JavaMLReadable): """ .. note:: Experimental @@ -156,6 +157,14 @@ def intercept(self): """ return self._call_java("intercept") + @property + @since("2.0.0") + def numFeatures(self): + """ + Number of features the model was trained on. + """ + return self._call_java("numFeatures") + @property @since("2.0.0") def summary(self): @@ -661,6 +670,8 @@ class DecisionTreeRegressor(JavaEstimator, HasFeaturesCol, HasLabelCol, HasPredi 3 >>> model.featureImportances SparseVector(1, {0: 1.0}) + >>> model.numFeatures + 1 >>> test0 = spark.createDataFrame([(Vectors.dense(-1.0),)], ["features"]) >>> model.transform(test0).head().prediction 0.0 @@ -681,8 +692,8 @@ class DecisionTreeRegressor(JavaEstimator, HasFeaturesCol, HasLabelCol, HasPredi True >>> model.transform(test1).head().variance 0.0 - >>> model.numFeatures - 1 + >>> model.numFeatures == model2.numFeatures + True .. versionadded:: 1.4.0 """ @@ -804,8 +815,7 @@ def __repr__(self): @inherit_doc -class DecisionTreeRegressionModel(DecisionTreeModel, JavaMLWritable, JavaMLReadable, - HasNumFeaturesModel): +class DecisionTreeRegressionModel(DecisionTreeModel, JavaMLWritable, JavaMLReadable): """ .. note:: Experimental @@ -835,6 +845,14 @@ def featureImportances(self): """ return self._call_java("featureImportances") + @property + @since("2.0.0") + def numFeatures(self): + """ + Number of features the model was trained on. + """ + return self._call_java("numFeatures") + @inherit_doc class RandomForestRegressor(JavaEstimator, HasFeaturesCol, HasLabelCol, HasPredictionCol, HasSeed, @@ -856,6 +874,8 @@ class RandomForestRegressor(JavaEstimator, HasFeaturesCol, HasLabelCol, HasPredi >>> model = rf.fit(df) >>> model.featureImportances SparseVector(1, {0: 1.0}) + >>> model.numFeatures + 1 >>> allclose(model.treeWeights, [1.0, 1.0]) True >>> test0 = spark.createDataFrame([(Vectors.dense(-1.0),)], ["features"]) @@ -929,7 +949,7 @@ def _create_model(self, java_model): return RandomForestRegressionModel(java_model) -class RandomForestRegressionModel(TreeEnsembleModels, JavaMLWritable, JavaMLReadable, HasNumFeaturesModel): +class RandomForestRegressionModel(TreeEnsembleModels, JavaMLWritable, JavaMLReadable): """ .. note:: Experimental @@ -959,6 +979,14 @@ def featureImportances(self): """ return self._call_java("featureImportances") + @property + @since("2.0.0") + def numFeatures(self): + """ + Number of features the model was trained on. + """ + return self._call_java("numFeatures") + @inherit_doc class GBTRegressor(JavaEstimator, HasFeaturesCol, HasLabelCol, HasPredictionCol, HasMaxIter, @@ -982,6 +1010,8 @@ class GBTRegressor(JavaEstimator, HasFeaturesCol, HasLabelCol, HasPredictionCol, >>> model = gbt.fit(df) >>> model.featureImportances SparseVector(1, {0: 1.0}) + >>> model.numFeatures + 1 >>> allclose(model.treeWeights, [1.0, 0.1, 0.1, 0.1, 0.1]) True >>> test0 = spark.createDataFrame([(Vectors.dense(-1.0),)], ["features"]) @@ -1005,6 +1035,7 @@ class GBTRegressor(JavaEstimator, HasFeaturesCol, HasLabelCol, HasPredictionCol, >>> model.trees [DecisionTreeRegressionModel (uid=...) of depth..., DecisionTreeRegressionModel...] >>> model.numFeatures == model2.numFeatures + True .. versionadded:: 1.4.0 """ @@ -1072,7 +1103,7 @@ def getLossType(self): return self.getOrDefault(self.lossType) -class GBTRegressionModel(TreeEnsembleModels, JavaMLWritable, JavaMLReadable, HasNumFeaturesModel): +class GBTRegressionModel(TreeEnsembleModels, JavaMLWritable, JavaMLReadable): """ .. note:: Experimental @@ -1096,6 +1127,14 @@ def featureImportances(self): """ return self._call_java("featureImportances") + @property + @since("2.0.0") + def numFeatures(self): + """ + Number of features the model was trained on. + """ + return self._call_java("numFeatures") + @property @since("2.0.0") def trees(self):