如何使用spark-ml处理分类功能?

时间:2015-08-28 18:28:11

标签: apache-spark categorical-data apache-spark-ml apache-spark-mllib

如何使用 spark-ml 处理分类数据,而不是 spark-mllib

认为文档不是很清楚,似乎分类器例如RandomForestClassifierLogisticRegression,有一个featuresCol参数,用于指定DataFrame中要素列的名称,以及一个labelCol参数,用于指定DataFrame中标记类的列的名称。

显然我想在预测中使用多个功能,所以我尝试使用VectorAssembler将所有功能放在featuresCol下的单个向量中。

但是,VectorAssembler只接受数字类型,布尔类型和矢量类型(根据Spark网站),所以我不能在我的特征向量中添加字符串。

我该怎么办?

5 个答案:

答案 0 :(得分:38)

我只是想完成霍尔顿的回答。

Spark 2.3.0 以来,OneHotEncoder已被弃用,并将在3.0.0中删除。请改用OneHotEncoderEstimator

Scala

import org.apache.spark.ml.Pipeline
import org.apache.spark.ml.feature.{OneHotEncoderEstimator, StringIndexer}

val df = Seq((0, "a", 1), (1, "b", 2), (2, "c", 3), (3, "a", 4), (4, "a", 4), (5, "c", 3)).toDF("id", "category1", "category2")

val indexer = new StringIndexer().setInputCol("category1").setOutputCol("category1Index")
val encoder = new OneHotEncoderEstimator()
  .setInputCols(Array(indexer.getOutputCol, "category2"))
  .setOutputCols(Array("category1Vec", "category2Vec"))

val pipeline = new Pipeline().setStages(Array(indexer, encoder))

pipeline.fit(df).transform(df).show
// +---+---------+---------+--------------+-------------+-------------+
// | id|category1|category2|category1Index| category1Vec| category2Vec|
// +---+---------+---------+--------------+-------------+-------------+
// |  0|        a|        1|           0.0|(2,[0],[1.0])|(4,[1],[1.0])|
// |  1|        b|        2|           2.0|    (2,[],[])|(4,[2],[1.0])|
// |  2|        c|        3|           1.0|(2,[1],[1.0])|(4,[3],[1.0])|
// |  3|        a|        4|           0.0|(2,[0],[1.0])|    (4,[],[])|
// |  4|        a|        4|           0.0|(2,[0],[1.0])|    (4,[],[])|
// |  5|        c|        3|           1.0|(2,[1],[1.0])|(4,[3],[1.0])|
// +---+---------+---------+--------------+-------------+-------------+

Python

from pyspark.ml import Pipeline
from pyspark.ml.feature import StringIndexer, OneHotEncoderEstimator

df = spark.createDataFrame([(0, "a", 1), (1, "b", 2), (2, "c", 3), (3, "a", 4), (4, "a", 4), (5, "c", 3)], ["id", "category1", "category2"])

indexer = StringIndexer(inputCol="category1", outputCol="category1Index")
inputs = [indexer.getOutputCol(), "category2"]
encoder = OneHotEncoderEstimator(inputCols=inputs, outputCols=["categoryVec1", "categoryVec2"])
pipeline = Pipeline(stages=[indexer, encoder])
pipeline.fit(df).transform(df).show()
# +---+---------+---------+--------------+-------------+-------------+
# | id|category1|category2|category1Index| categoryVec1| categoryVec2|
# +---+---------+---------+--------------+-------------+-------------+
# |  0|        a|        1|           0.0|(2,[0],[1.0])|(4,[1],[1.0])|
# |  1|        b|        2|           2.0|    (2,[],[])|(4,[2],[1.0])|
# |  2|        c|        3|           1.0|(2,[1],[1.0])|(4,[3],[1.0])|
# |  3|        a|        4|           0.0|(2,[0],[1.0])|    (4,[],[])|
# |  4|        a|        4|           0.0|(2,[0],[1.0])|    (4,[],[])|
# |  5|        c|        3|           1.0|(2,[1],[1.0])|(4,[3],[1.0])|
# +---+---------+---------+--------------+-------------+-------------+

由于 Spark 1.4.0 ,MLLib还提供OneHotEncoder功能,该功能将一列标签索引映射到一列二进制向量,最多只有一个单值。

此编码允许期望连续特征(例如Logistic回归)的算法使用分类特征

让我们考虑以下DataFrame

val df = Seq((0, "a"),(1, "b"),(2, "c"),(3, "a"),(4, "a"),(5, "c"))
            .toDF("id", "category")

第一步是使用DataFrame创建已编入索引的StringIndexer

import org.apache.spark.ml.feature.StringIndexer

val indexer = new StringIndexer()
                   .setInputCol("category")
                   .setOutputCol("categoryIndex")
                   .fit(df)

val indexed = indexer.transform(df)

indexed.show
// +---+--------+-------------+                                                    
// | id|category|categoryIndex|
// +---+--------+-------------+
// |  0|       a|          0.0|
// |  1|       b|          2.0|
// |  2|       c|          1.0|
// |  3|       a|          0.0|
// |  4|       a|          0.0|
// |  5|       c|          1.0|
// +---+--------+-------------+

然后,您可以使用categoryIndex编码OneHotEncoder

import org.apache.spark.ml.feature.OneHotEncoder

val encoder = new OneHotEncoder()
                   .setInputCol("categoryIndex")
                   .setOutputCol("categoryVec")

val encoded = encoder.transform(indexed)

encoded.select("id", "categoryVec").show
// +---+-------------+
// | id|  categoryVec|
// +---+-------------+
// |  0|(2,[0],[1.0])|
// |  1|    (2,[],[])|
// |  2|(2,[1],[1.0])|
// |  3|(2,[0],[1.0])|
// |  4|(2,[0],[1.0])|
// |  5|(2,[1],[1.0])|
// +---+-------------+

答案 1 :(得分:23)

我将从另一个角度提供答案,因为我也想知道Spark ML(不是MLlib)中基于树的模型的分类特征,文档并不清楚一切是如何工作的。

使用pyspark.ml.feature.StringIndexer转换数据框中的列时,额外的元数据会存储在数据框中,该数据框专门将转换后的要素标记为分类要素。

当您打印数据帧时,您将看到一个数值(这是一个与您的一个分类值对应的索引),如果您查看该架构,您将看到新转换列的类型为{{1} }。但是,使用double创建的这个新列不仅仅是一个普通的双列,它还有与之关联的额外元数据非常重要。您可以通过查看数据框架构中相应字段的pyspark.ml.feature.StringIndexer.transform属性来检查此元数据(您可以通过查看yourdataframe.schema来访问数据框架构对象)

这些额外的元数据有两个重要含义:

  1. 在使用基于树的模型时调用metadata时,它会扫描数据框的元数据,并使用.fit()等变换器识别您编码为分类的字段(如如上所述,还有其他变形金刚也会产生这种影响,例如pyspark.ml.feature.StringIndexer)。因此,在使用spark ML中使用基于树的模型时,在使用StringIndxer对其进行转换后,您不必对其进行单热编码(但是,在使用其他模型时,您仍需执行单热编码自然地处理线性回归等分类。

  2. 由于此元数据存储在数据框中,因此您可以使用pyspark.ml.feature.VectorIndexer将数字索引反转回原始分类值(通常是字符串)。

答案 2 :(得分:6)

有一个名为var = Replace(Request.Form("form"),"'","\'")的ML管道组件,您可以使用它以合理的方式将字符串转换为Double。 http://spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.ml.feature.StringIndexer有更多文档,http://spark.apache.org/docs/latest/ml-guide.html显示了如何构建管道。

答案 3 :(得分:0)

我使用以下方法对Spark dataFrame中的单列进行热编码:

def ohcOneColumn(df, colName, debug=False):

  colsToFillNa = []

  if debug: print("Entering method ohcOneColumn")
  countUnique = df.groupBy(colName).count().count()
  if debug: print(countUnique)

  collectOnce = df.select(colName).distinct().collect()
  for uniqueValIndex in range(countUnique):
    uniqueVal = collectOnce[uniqueValIndex][0]
    if debug: print(uniqueVal)
    newColName = str(colName) + '_' + str(uniqueVal) + '_TF'
    df = df.withColumn(newColName, df[colName]==uniqueVal)
    colsToFillNa.append(newColName)
  df = df.drop(colName)
  df = df.na.fill(False, subset=colsToFillNa)
  return df

我对oneHotEncoding Spark dataFrames使用以下方法:

from pyspark.sql.functions import col, countDistinct, approxCountDistinct
from pyspark.ml.feature import StringIndexer
from pyspark.ml.feature import OneHotEncoderEstimator

def detectAndLabelCat(sparkDf, minValCount=5, debug=False, excludeCols=['Target']):
  if debug: print("Entering method detectAndLabelCat")
  newDf = sparkDf
  colList = sparkDf.columns

  for colName in sparkDf.columns:
    uniqueVals = sparkDf.groupBy(colName).count()
    if debug: print(uniqueVals)
    countUnique = uniqueVals.count()
    dtype = str(sparkDf.schema[colName].dataType)
    #dtype = str(df.schema[nc].dataType)
    if (colName in excludeCols):
      if debug: print(str(colName) + ' is in the excluded columns list.')

    elif countUnique == 1:
      newDf = newDf.drop(colName)
      if debug:
        print('dropping column ' + str(colName) + ' because it only contains one unique value.')
      #end if debug
    #elif (1==2):
    elif ((countUnique < minValCount) | (dtype=="String") | (dtype=="StringType")):
      if debug: 
        print(len(newDf.columns))
        oldColumns = newDf.columns
      newDf = ohcOneColumn(newDf, colName, debug=debug)
      if debug: 
        print(len(newDf.columns))
        newColumns = set(newDf.columns) - set(oldColumns)
        print('Adding:')
        print(newColumns)
        for newColumn in newColumns:
          if newColumn in newDf.columns:
            try:
              newUniqueValCount = newDf.groupBy(newColumn).count().count()
              print("There are " + str(newUniqueValCount) + " unique values in " + str(newColumn))
            except:
              print('Uncaught error discussing ' + str(newColumn))
          #else:
          #  newColumns.remove(newColumn)

        print('Dropping:')
        print(set(oldColumns) - set(newDf.columns))

    else:
      if debug: print('Nothing done for column ' + str(colName))

      #end if countUnique == 1, elif countUnique other condition
    #end outer for
  return newDf

答案 4 :(得分:-1)

您可以使用强制转换函数将火花数据框中的字符串列类型转换为数字数据类型。

from pyspark.sql import SQLContext
from pyspark.sql.types import DoubleType, IntegerType

sqlContext = SQLContext(sc)
dataset = sqlContext.read.format('com.databricks.spark.csv').options(header='true').load('./data/titanic.csv')   

dataset = dataset.withColumn("Age", dataset["Age"].cast(DoubleType()))
dataset = dataset.withColumn("Survived", dataset["Survived"].cast(IntegerType()))

在上面的示例中,我们将csv文件作为数据框读入,将默认字符串数据类型转换为整数和双精度,并覆盖原始数据框。然后我们可以使用VectorAssembler将单个向量中的要素合并,并应用您最喜欢的Spark ML算法。