Spark DataFrame 在 OneHotEncoder 中处理空字符串

Spark DataFrame handing empty String in OneHotEncoder

我正在将 CSV 文件(使用 spark-csv)导入 DataFrame 中,其中 String 值为空。应用 OneHotEncoder 时,应用程序崩溃并出现错误 requirement failed: Cannot have an empty string for name.。有什么办法可以解决这个问题吗?

我可以重现 example provided on Spark ml 页面中的错误:

val df = sqlContext.createDataFrame(Seq(
  (0, "a"),
  (1, "b"),
  (2, "c"),
  (3, ""),         //<- original example has "a" here
  (4, "a"),
  (5, "c")
)).toDF("id", "category")

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

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

encoded.show()

这很烦人,因为 missing/empty 值是一个非常通用的案例。

提前致谢, 尼克尔

是的,这有点棘手,但也许您可以将空字符串替换为肯定不同于其他值的内容。请注意,我使用的是 pyspark DataFrameNaFunctions API 但 Scala's 应该类似。

df = sqlContext.createDataFrame([(0,"a"), (1,'b'), (2, 'c'), (3,''), (4,'a'), (5, 'c')], ['id', 'category'])
df = df.na.replace('', 'EMPTY', 'category')
df.show()

+---+--------+
| id|category|
+---+--------+
|  0|       a|
|  1|       b|
|  2|       c|
|  3|   EMPTY|
|  4|       a|
|  5|       c|
+---+--------+

由于 OneHotEncoder/OneHotEncoderEstimator 不接受空字符串作为名称,否则您将收到以下错误:

java.lang.IllegalArgumentException: requirement failed: Cannot have an empty string for name. at scala.Predef$.require(Predef.scala:233) at org.apache.spark.ml.attribute.Attribute$$anonfun.apply(attributes.scala:33) at org.apache.spark.ml.attribute.Attribute$$anonfun.apply(attributes.scala:32) [...]

这就是我要做的:(还有其他方法,rf。@Anthony 的回答)

我将创建一个 UDF 来处理空类别:

import org.apache.spark.sql.functions._

def processMissingCategory = udf[String, String] { s => if (s == "") "NA"  else s }

然后,我将在列上应用 UDF :

val df = sqlContext.createDataFrame(Seq(
   (0, "a"),
   (1, "b"),
   (2, "c"),
   (3, ""),         //<- original example has "a" here
   (4, "a"),
   (5, "c")
)).toDF("id", "category")
  .withColumn("category",processMissingCategory('category))

df.show
// +---+--------+
// | id|category|
// +---+--------+
// |  0|       a|
// |  1|       b|
// |  2|       c|
// |  3|      NA|
// |  4|       a|
// |  5|       c|
// +---+--------+

现在,您可以返回到您的转换

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|      NA|          3.0|
// |  4|       a|          0.0|
// |  5|       c|          1.0|
// +---+--------+-------------+

// Spark <2.3
// val encoder = new OneHotEncoder().setInputCol("categoryIndex").setOutputCol("categoryVec")
// Spark +2.3
val encoder = new OneHotEncoderEstimator().setInputCols(Array("categoryIndex")).setOutputCols(Array("category2Vec"))
val encoded = encoder.transform(indexed)

encoded.show
// +---+--------+-------------+-------------+
// | id|category|categoryIndex|  categoryVec|
// +---+--------+-------------+-------------+
// |  0|       a|          0.0|(3,[0],[1.0])|
// |  1|       b|          2.0|(3,[2],[1.0])|
// |  2|       c|          1.0|(3,[1],[1.0])|
// |  3|      NA|          3.0|    (3,[],[])|
// |  4|       a|          0.0|(3,[0],[1.0])|
// |  5|       c|          1.0|(3,[1],[1.0])|
// +---+--------+-------------+-------------+

编辑:

@Anthony 在 Scala 中的解决方案:

df.na.replace("category", Map( "" -> "NA")).show
// +---+--------+
// | id|category|
// +---+--------+
// |  0|       a|
// |  1|       b|
// |  2|       c|
// |  3|      NA|
// |  4|       a|
// |  5|       c|
// +---+--------+

希望对您有所帮助!

如果该列包含 null,则 OneHotEncoder 将失败并出现 NullPointerException。 因此我也扩展了 udf 以转换空值

object OneHotEncoderExample {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setAppName("OneHotEncoderExample Application").setMaster("local[2]")
    val sc = new SparkContext(conf)
    val sqlContext = new SQLContext(sc)

    // $example on$
    val df1 = sqlContext.createDataFrame(Seq(
      (0.0, "a"),
      (1.0, "b"),
      (2.0, "c"),
      (3.0, ""),
      (4.0, null),
      (5.0, "c")
    )).toDF("id", "category")


    import org.apache.spark.sql.functions.udf
    def emptyValueSubstitution = udf[String, String] {
      case "" => "NA"
      case null => "null"
      case value => value
    }
    val df = df1.withColumn("category", emptyValueSubstitution( df1("category")) )


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

    val encoder = new OneHotEncoder()
      .setInputCol("categoryIndex")
      .setOutputCol("categoryVec")
      .setDropLast(false)
    val encoded = encoder.transform(indexed)
    encoded.show()
    // $example off$
    sc.stop()
  }
}