7

Background: I'm doing a simple binary classification, using RandomForestClassifier from pyspark.ml. Before feeding the data to training, I managed to use VectorIndexer to decide whether features would be numerical or categorical by providing the argument maxCategories.

Problem: Even if I have used the VectorIndexer with maxCategories setting to 30, I was still getting an error during training pipeline:

An error occurred while calling o15371.fit.
: java.lang.IllegalArgumentException: requirement failed: DecisionTree requires maxBins (= 32) to be at least as large as the number of values in each categorical feature, but categorical feature 0 has 10765 values. Considering remove this and other categorical features with a large number of values, or add more training examples.

My code is simple, col_idx is a column string list I generated which will be passed to stringindexer, col_all is a column string list which will be passed to stringindexer and onehotencoder, col_num are numeric column names.

from pyspark.ml.feature import OneHotEncoderEstimator, StringIndexer, VectorAssembler, IndexToString, VectorIndexer
from pyspark.ml import Pipeline
from pyspark.ml.classification import RandomForestClassifier

my_data.cache()

# stringindexers and encoders
stIndexers = [StringIndexer(inputCol = Col, outputCol = Col + 'Index').setHandleInvalid('keep') for Col in col_idx]
encoder = OneHotEncoderEstimator(inputCols = [Col + 'Index' for Col in col_all], outputCols = [Col + 'ClassVec' for Col in col_all]).setHandleInvalid('keep')

# vector assemblor
col_into_assembler = [cols + 'Index' for cols in col_idx] + [cols + 'ClassVec' for cols in col_all] + col_num
assembler = VectorAssembler(inputCols = col_into_assembler, outputCol = "features")

# featureIndexer, labelIndexer, rf classifier and labelConverter
featureIndexer = VectorIndexer(inputCol = "features", outputCol = "indexedFeatures", maxCategories = 30)
# columns smaller than maxCategories => categorical features, columns larger than maxCategories => numerical / continuous features, smaller value => less categorical features, larger value => more categorical features.
labelIndexer = StringIndexer(inputCol = "label", outputCol = "indexedLabel").fit(my_data)
rf = RandomForestClassifier(featuresCol = "indexedFeatures", labelCol = "indexedLabel")
labelConverter = IndexToString(inputCol = "prediction", outputCol = "predictedLabel", labels=labelIndexer.labels)

# chain all the estimators and transformers stages into a Pipeline estimator
rfPipeline = Pipeline(stages = stIndexers + [encoder, assembler, featureIndexer, labelIndexer, rf, labelConverter])

# split data, cache them
training, test = my_data.randomSplit([0.7, 0.3], seed = 100)
training.cache()
test.cache()

# fit the estimator with training dataset to get a compiled pipeline with transformers and fitted models.
ModelRF = rfPipeline.fit(training)

# make predictions
predictions = ModelRF.transform(test)
predictions.printSchema()
predictions.show(5)

So my question is: how come there's still a high levels categorical feature in my data even if I have set maxCategories to 30 in VectorIndexer. I can set maxBins in rf classifier to higher value but I'm just curious: why the VectorIndexer is not working as expected (well, as I expected): casting feature smaller than maxCategories to categorical feature, larger to numerical features.

zero323
  • 322,348
  • 103
  • 959
  • 935
Yiming Wu
  • 611
  • 1
  • 5
  • 11

1 Answers1

4

It looks like, that contrary to the documentation, which lists:

Preserve metadata in transform; if a feature's metadata is already present, do not recompute.

among TODO, metadata is already preserved.

from pyspark.sql.functions import col
from pyspark.ml import Pipeline
from pyspark.ml.feature import  *

df = spark.range(10)

stages = [StringIndexer(inputCol="id", outputCol="idx"), VectorAssembler(inputCols=["idx"], outputCol="features"), VectorIndexer(inputCol="features", outputCol="features_indexed", maxCategories=5)]
Pipeline(stages=stages).fit(df).transform(df).schema["features"].metadata
# {'ml_attr': {'attrs': {'nominal': [{'vals': ['8',
#       '4',
#       '9',
#       '5',
#       '6',
#       '1',
#       '0',
#       '2',
#       '7',
#       '3'],
#      'idx': 0,
#      'name': 'idx'}]},
#   'num_attrs': 1}}

Pipeline(stages=stages).fit(df).transform(df).schema["features_indexed"].metadata

# {'ml_attr': {'attrs': {'nominal': [{'ord': False,
#      'vals': ['0.0',
#       '1.0',
#       '2.0',
#       '3.0',
#       '4.0',
#       '5.0',
#       '6.0',
#       '7.0',
#       '8.0',
#       '9.0'],
#      'idx': 0,
#      'name': 'idx'}]},
#   'num_attrs': 1}}

Under normal circumstances it is a desired behavior. You shouldn't use indexed categorical features as continuous variables

But if still want to circumvent this behavior, you'll have to reset metadata, for example:

pipeline1 = Pipeline(stages=stages[:1])
pipeline2 = Pipeline(stages=stages[1:])

dft1 = pipeline1.fit(df).transform(df).withColumn("idx", col("idx").alias("idx", metadata={}))
dft2 = pipeline2.fit(dft1).transform(dft1)


dft2.schema["features_indexed"].metadata

# {'ml_attr': {'attrs': {'numeric': [{'idx': 0, 'name': 'idx'}]},
#   'num_attrs': 1}}
Alper t. Turker
  • 34,230
  • 9
  • 83
  • 115
  • I don't understand this sentence : "It looks like, that contrary to the documentation, which lists" – eliasah May 22 '18 at 13:04
  • Yeah. It seems like the doc haven't been updated in feature.py – eliasah May 22 '18 at 13:12
  • It should have been removed here https://github.com/apache/spark/commit/b1bc5ebdd52ed12aea3fdc7b8f2fa2d00ea09c6b – eliasah May 22 '18 at 13:13
  • alternatively I'm thinking about the solution that you have provided. I would have indexed that column instead of changing the metadata. – eliasah May 22 '18 at 13:22
  • @user8371915 So basically the transformer VectorIndexer will not change column metadata if it's already present. It's wierd because from my point I don't see WHY they want to preserve the metadata if a user have explicitly indicated a specific maxCategories. I mean, if any way a presented metadata will be preserved, then there's no need to set maxCategories. The maxCategories will only apply to columns without metadata. – Yiming Wu May 22 '18 at 15:11
  • @user8371915 I see, what I wanted to do in my case is to use StringIndexer to cast categorical features to numerical ones, but it turns out it's not the case I thought. The printSchema showed the indexed columns were double typed, but in fact the indexed columns were still categorical features. I need to find a way to cast them to numerical ones in pipelines for training and test data, because the reason why I'm using StringIndexer is because I want to preserve the frequency information in the categorical columns. – Yiming Wu May 22 '18 at 17:00
  • 1
    @YimingWu If you want to do it in a `Pipeline` you could try `SQLTransformer`: https://stackoverflow.com/q/49734374/8371915 – Alper t. Turker May 22 '18 at 17:12