Scala 如何在Spark ML中创建正确的分类数据框架

Scala 如何在Spark ML中创建正确的分类数据框架,scala,apache-spark,apache-spark-sql,apache-spark-mllib,Scala,Apache Spark,Apache Spark Sql,Apache Spark Mllib,我试图使用运行随机森林分类,但在创建正确的数据帧输入到管道中时遇到了问题 以下是示例数据: age,hours_per_week,education,sex,salaryRange 38,40,"hs-grad","male","A" 28,40,"bachelors","female","A" 52,45,"hs-grad","male","B" 31,50,"masters","female","B" 42,40,"bachelors","male","B" 年龄和每周小时数是整数,而包括

我试图使用运行随机森林分类,但在创建正确的数据帧输入到管道中时遇到了问题

以下是示例数据:

age,hours_per_week,education,sex,salaryRange
38,40,"hs-grad","male","A"
28,40,"bachelors","female","A"
52,45,"hs-grad","male","B"
31,50,"masters","female","B"
42,40,"bachelors","male","B"
年龄每周小时数是整数,而包括标签工资范围在内的其他特征是分类的(字符串)

加载此csv文件(我们称之为sample.csv)可以通过以下方式完成:

val data = sqlContext.csvFile("/home/dusan/sample.csv")
默认情况下,所有列都作为字符串导入,因此我们需要将“年龄”和“每周小时数”更改为Int:

现在只需检查模式的外观:

scala> dataFixed.printSchema
root
 |-- age: integer (nullable = true)
 |-- hours_per_week: integer (nullable = true)
 |-- education: string (nullable = true)
 |-- sex: string (nullable = true)
 |-- salaryRange: string (nullable = true)
然后让我们设置交叉验证程序和管道:

val rf = new RandomForestClassifier()
val pipeline = new Pipeline().setStages(Array(rf)) 
val cv = new CrossValidator().setNumFolds(10).setEstimator(pipeline).setEvaluator(new BinaryClassificationEvaluator)
运行此行时显示错误:

val cmModel = cv.fit(dataFixed)
java.lang.IllegalArgumentException:字段“功能”不存在。

可以在RandomForestClassifier中设置标签列和特征列,但是我有4列作为预测器(特征),而不仅仅是一列

我应该如何组织我的数据框,使其具有正确组织的标签和功能列?

为方便起见,以下是完整代码:

import org.apache.spark.SparkConf
import org.apache.spark.SparkContext
import org.apache.spark.ml.classification.RandomForestClassifier
import org.apache.spark.ml.evaluation.BinaryClassificationEvaluator
import org.apache.spark.ml.tuning.CrossValidator
import org.apache.spark.ml.Pipeline
import org.apache.spark.sql.DataFrame

import org.apache.spark.sql.functions._
import org.apache.spark.mllib.linalg.{Vector, Vectors}


object SampleClassification {

  def main(args: Array[String]): Unit = {

    //set spark context
    val conf = new SparkConf().setAppName("Simple Application").setMaster("local");
    val sc = new SparkContext(conf)
    val sqlContext = new org.apache.spark.sql.SQLContext(sc)

    import sqlContext.implicits._
    import com.databricks.spark.csv._

    //load data by using databricks "Spark CSV Library" 
    val data = sqlContext.csvFile("/home/dusan/sample.csv")

    //by default all columns are imported as string so we need to change "age" and  "hours_per_week" to Int
    val toInt    = udf[Int, String]( _.toInt)
    val dataFixed = data.withColumn("age", toInt(data("age"))).withColumn("hours_per_week",toInt(data("hours_per_week")))


    val rf = new RandomForestClassifier()

    val pipeline = new Pipeline().setStages(Array(rf))

    val cv = new CrossValidator().setNumFolds(10).setEstimator(pipeline).setEvaluator(new BinaryClassificationEvaluator)

    // this fails with error
    //java.lang.IllegalArgumentException: Field "features" does not exist.
    val cmModel = cv.fit(dataFixed) 
  }

}

谢谢你的帮助

根据mllib-random trees上的spark文档,在我看来,您应该定义您正在使用的特征映射,并且点应该是一个标签点

这将告诉算法哪些列应该用作预测,哪些是特征

val assembler = new VectorAssembler()
  .setInputCols(Array("col1", "col2", "col3"))
  .setOutputCol("features")

您只需确保您的数据帧中有一个
“features”
列,其类型为
VectorUDF
,如下所示:

scala> val df2 = dataFixed.withColumnRenamed("age", "features")
df2: org.apache.spark.sql.DataFrame = [features: int, hours_per_week: int, education: string, sex: string, salaryRange: string]

scala> val cmModel = cv.fit(df2) 
java.lang.IllegalArgumentException: requirement failed: Column features must be of type org.apache.spark.mllib.linalg.VectorUDT@1eef but was actually IntegerType.
    at scala.Predef$.require(Predef.scala:233)
    at org.apache.spark.ml.util.SchemaUtils$.checkColumnType(SchemaUtils.scala:37)
    at org.apache.spark.ml.PredictorParams$class.validateAndTransformSchema(Predictor.scala:50)
    at org.apache.spark.ml.Predictor.validateAndTransformSchema(Predictor.scala:71)
    at org.apache.spark.ml.Predictor.transformSchema(Predictor.scala:118)
    at org.apache.spark.ml.Pipeline$$anonfun$transformSchema$4.apply(Pipeline.scala:164)
    at org.apache.spark.ml.Pipeline$$anonfun$transformSchema$4.apply(Pipeline.scala:164)
    at scala.collection.IndexedSeqOptimized$class.foldl(IndexedSeqOptimized.scala:51)
    at scala.collection.IndexedSeqOptimized$class.foldLeft(IndexedSeqOptimized.scala:60)
    at scala.collection.mutable.ArrayOps$ofRef.foldLeft(ArrayOps.scala:108)
    at org.apache.spark.ml.Pipeline.transformSchema(Pipeline.scala:164)
    at org.apache.spark.ml.tuning.CrossValidator.transformSchema(CrossValidator.scala:142)
    at org.apache.spark.ml.PipelineStage.transformSchema(Pipeline.scala:59)
    at org.apache.spark.ml.tuning.CrossValidator.fit(CrossValidator.scala:107)
    at $iwC$$iwC$$iwC$$iwC$$iwC$$iwC$$iwC$$iwC$$iwC$$iwC$$iwC$$iwC$$iwC$$iwC$$iwC$$iwC.<init>(<console>:67)
    at $iwC$$iwC$$iwC$$iwC$$iwC$$iwC$$iwC$$iwC$$iwC$$iwC$$iwC$$iwC$$iwC$$iwC$$iwC.<init>(<console>:72)
    at $iwC$$iwC$$iwC$$iwC$$iwC$$iwC$$iwC$$iwC$$iwC$$iwC$$iwC$$iwC$$iwC$$iwC.<init>(<console>:74)
    at $iwC$$iwC$$iwC$$iwC$$iwC$$iwC$$iwC$$iwC$$iwC$$iwC$$iwC$$iwC$$iwC.<init>(<console>:76)
val toVec4    = udf[Vector, Int, Int, String, String] { (a,b,c,d) => 
  val e3 = c match {
    case "hs-grad" => 0
    case "bachelors" => 1
    case "masters" => 2
  }
  val e4 = d match {case "male" => 0 case "female" => 1}
  Vectors.dense(a, b, e3, e4) 
}
现在,要对“标签”字段进行编码,请创建另一个
udf
,如下所示:

val encodeLabel    = udf[Double, String]( _ match { case "A" => 0.0 case "B" => 1.0} )
现在,我们使用这两个
udf
转换原始数据帧:

val df = dataFixed.withColumn(
  "features",
  toVec4(
    dataFixed("age"),
    dataFixed("hours_per_week"),
    dataFixed("education"),
    dataFixed("sex")
  )
).withColumn("label", encodeLabel(dataFixed("salaryRange"))).select("features", "label")
请注意,数据框中可能存在额外的列/字段,但在本例中,我仅选择了
功能
标签

scala> df.show()
+-------------------+-----+
|           features|label|
+-------------------+-----+
|[38.0,40.0,0.0,0.0]|  0.0|
|[28.0,40.0,1.0,1.0]|  0.0|
|[52.0,45.0,0.0,0.0]|  1.0|
|[31.0,50.0,2.0,1.0]|  1.0|
|[42.0,40.0,1.0,0.0]|  1.0|
+-------------------+-----+

现在,您需要为学习算法设置正确的参数以使其工作。

从Spark 1.4开始,您可以使用Transformer。 只需提供您希望成为功能的列名

val assembler = new VectorAssembler()
  .setInputCols(Array("col1", "col2", "col3"))
  .setOutputCol("features")

并将其添加到您的管道中

不知道scala语言,但您在哪里设置数据集中的标签和功能,如LabeledPoint(标签,列表(功能)),请检查@ABC中的示例,请检查我在下面问题中的注释。检查此示例,其中val model=pipeline.fit(training.toDF())利用管道中的dataframe在包mllib中有一个旧的api,这些点实际上应该标记为点。然而,我尝试使用ml包中的新api,因为它支持管道、交叉验证等。。这个新api使用DataFrame作为输入。e、 g.比较这两个:从使用数据帧的ml和从MLLIB的RandomForestModel(),您是否有机会展示我如何从我的数据创建VectorUDF类型的名为“features”的列?@DusanGrubjesic:我添加了代码示例。请查看编辑1这真是太棒了!我只是不确定我们如何将信息从ML传递到分类器,现在这些e3和e4是分类特征而不是数字特征?原因在“低级”mllib api中,可以通过索引和分类功能的类别数传递分类功能信息。在“高级”ml api中,这应该直接从模式中提取。在这种情况下,对
值(所有数字)的重新解析
向量
,构成您的特征向量。你可能想做标准化,ohe热编码,标准化。。。无论您看起来是否适合您的算法,但您的特征向量中的值必须全部为
Double
。你指的是哪种低级API?@DusanGrubjesic:我很高兴它很有用。感谢mlllib和ml之间的区别:-)解释了问题的细节,以及解决方案的外观。这个答案展示了一个很好的方法来完成它。这不起作用,因为一些特性是String类型的。对于严格的数字数据,这是一个很好的解决方案。@gstvolvr您需要首先使用
StringIndexer
将字符串转换为数字。为清楚起见,可能值得在答案中添加此步骤。