spark Attribute由来原理示例用法源码详解


文章目录

  • spark Attribute由来原理示例用法源码详解
  • 由来
  • 示例
  • 中文源码分析
  • abstract class Attribute
  • NumericAttribute
  • NominalAttribute
  • BinaryAttribute
  • UnresolvedAttribute


由来

Spark的Attribute类是用来表示特征属性的基类。它提供了不同类型属性的共同方法和属性。

在Spark中,属性被用于描述数据集中的特征或列。每个属性都具有名称、索引和类型等信息。Spark的Attribute类提供了一种统一的方式来管理和操作这些属性。

Attribute类本身是一个抽象类,它定义了以下常用方法和属性:

  • attrType: AttributeType:返回属性的类型,包括数值、标称、二元和未解析等类型。
  • isNumeric: Boolean:判断属性是否为数值类型。
  • isNominal: Boolean:判断属性是否为标称类型。
  • name: Option[String]:返回属性的名称。
  • index: Option[Int]:返回属性的索引。
  • withName(name: String): Attribute:设置属性的名称,并返回一个新的属性对象。
  • withoutName: Attribute:移除属性的名称,并返回一个新的属性对象。
  • withIndex(index: Int): Attribute:设置属性的索引,并返回一个新的属性对象。
  • withoutIndex: Attribute:移除属性的索引,并返回一个新的属性对象。
  • toMetadata(withType: Boolean): Metadata:将属性转换为元数据。
  • equals(other: Any): Boolean:判断属性对象是否与另一个对象相等。
  • hashCode: Int:计算属性对象的哈希值。

Attribute类的子类包括NumericAttributeNominalAttributeBinaryAttributeUnresolvedAttribute,分别用于表示数值属性、标称属性、二元属性和未解析属性。

Spark的Attribute类提供了一种通用的方式来描述和操作特征属性,它在数据集的处理过程中发挥着重要的作用。通过使用属性对象,我们可以方便地获取和修改属性的信息,并进行属性类型的判断和转换。

示例

object AttributeTest extends App{
  import org.apache.spark.ml.attribute._

  // 创建数值属性
  val numericAttr = NumericAttribute.defaultAttr.withName("numeric").withIndex(0)

  // 创建标称属性
  val nominalAttr = NominalAttribute.defaultAttr.withName("nominal").withIndex(1).withValues("A", "B", "C")

  // 创建二元属性
  val binaryAttr = BinaryAttribute.defaultAttr.withName("binary").withIndex(2).withValues("negative", "positive")

  // 创建未解析属性
  val unresolvedAttr = UnresolvedAttribute

  // 打印属性信息
  println(numericAttr.name)      // Some(numeric)
  println(numericAttr.index)     // Some(0)
  println(numericAttr.attrType)  // Numeric
  println(numericAttr.isNumeric) // true
  println(numericAttr.isNominal) // false

  println(nominalAttr.name)      // Some(nominal)
  println(nominalAttr.index)     // Some(1)
  println(nominalAttr.attrType)  // Nominal
  println(nominalAttr.isNumeric) // false
  println(nominalAttr.isNominal) // true

  println(binaryAttr.name)      // Some(binary)
  println(binaryAttr.index)     // Some(2)
  println(binaryAttr.attrType)  // Binary
  println(binaryAttr.isNumeric) // true
  println(binaryAttr.isNominal) // true

  println(unresolvedAttr.name)      // None
  println(unresolvedAttr.index)     // None
  println(unresolvedAttr.attrType)  // Unresolved
  println(unresolvedAttr.isNumeric) // false
  println(unresolvedAttr.isNominal) // false

  // 将属性转换为元数据
  val metadata1 = numericAttr.toMetadata()
  println(metadata1) //

  val metadata2 = nominalAttr.toMetadata()
  println(metadata2) // 

  val metadata3 = binaryAttr.toMetadata()
  println(metadata3) //

  val metadata4 = unresolvedAttr.toMetadata()
  println(metadata4) // {}
}
Some(numeric)
Some(0)
Numeric
true
false
Some(nominal)
Some(1)
Nominal
false
true
Some(binary)
Some(2)
Binary
true
true
None
None
Unresolved
false
false
{"ml_attr":{"idx":0,"name":"numeric"}}
{"ml_attr":{"vals":["A","B","C"],"type":"nominal","idx":1,"name":"nominal"}}
{"ml_attr":{"vals":["negative","positive"],"type":"binary","idx":2,"name":"binary"}}
{"ml_attr":{}}

中文源码分析

abstract class Attribute

/**
 * :: DeveloperApi ::
 * ML属性的抽象类。
 */
@DeveloperApi
sealed abstract class Attribute extends Serializable {

  name.foreach { n =>
    require(n.nonEmpty, "名称不能为空字符串。")
  }
  index.foreach { i =>
    require(i >= 0, s"索引不能为负数,但是得到的值为 $i")
  }

  /** 属性类型。 */
  def attrType: AttributeType

  /** 属性的名称。如果未设置,则为None。 */
  def name: Option[String]

  /** 使用新名称创建副本。 */
  def withName(name: String): Attribute

  /** 创建没有名称的副本。 */
  def withoutName: Attribute

  /** 属性的索引。如果未设置,则为None。 */
  def index: Option[Int]

  /** 使用新索引创建副本。 */
  def withIndex(index: Int): Attribute

  /** 创建没有索引的副本。 */
  def withoutIndex: Attribute

  /**
   * 检查该属性是否是数值属性,对于NumericAttribute和BinaryAttribute返回true。
   */
  def isNumeric: Boolean

  /**
   * 检查该属性是否是标称属性,对于NominalAttribute和BinaryAttribute返回true。
   */
  def isNominal: Boolean

  /**
   * 将属性转换为Metadata。
   * @param withType 是否包含类型信息
   */
  private[attribute] def toMetadataImpl(withType: Boolean): Metadata

  /**
   * 将属性转换为Metadata。对于数值属性,排除类型信息以节省空间,因为数值类型是默认的属性类型。
   * 对于标称和二进制属性,包含类型信息。
   */
  private[attribute] def toMetadataImpl(): Metadata = {
    if (attrType == AttributeType.Numeric) {
      toMetadataImpl(withType = false)
    } else {
      toMetadataImpl(withType = true)
    }
  }

  /** 转换为带有现有Metadata的ML元数据。 */
  def toMetadata(existingMetadata: Metadata): Metadata = {
    new MetadataBuilder()
      .withMetadata(existingMetadata)
      .putMetadata(AttributeKeys.ML_ATTR, toMetadataImpl())
      .build()
  }

  /** 转换为ML元数据。 */
  def toMetadata(): Metadata = toMetadata(Metadata.empty)

  /**
   * 转换为带有现有Metadata的`StructField`。
   * @param existingMetadata 需要携带的现有元数据
   */
  def toStructField(existingMetadata: Metadata): StructField = {
    val newMetadata = new MetadataBuilder()
      .withMetadata(existingMetadata)
      .putMetadata(AttributeKeys.ML_ATTR, withoutName.withoutIndex.toMetadataImpl())
      .build()
    StructField(name.get, DoubleType, nullable = false, newMetadata)
  }

  /**
   * 转换为`StructField`。
   */
  def toStructField(): StructField = toStructField(Metadata.empty)

  override def toString: String = toMetadataImpl(withType = true).toString
}

/** ML属性工厂的特质。 */
private[attribute] trait AttributeFactory {

  /**
   * 从`Metadata`实例创建[[Attribute]]。
   */
  private[attribute] def fromMetadata(metadata: Metadata): Attribute

  /**
   * 从`StructField`实例创建[[Attribute]],可选择保留名称。
   */
  private[ml] def decodeStructField(field: StructField, preserveName: Boolean): Attribute = {
    require(field.dataType.isInstanceOf[NumericType])
    val metadata = field.metadata
    val mlAttr = AttributeKeys.ML_ATTR
    if (metadata.contains(mlAttr)) {
      val attr = fromMetadata(metadata.getMetadata(mlAttr))
      if (preserveName) {
        attr
      } else {
        attr.withName(field.name)
      }
    } else {
      UnresolvedAttribute
    }
  }

  /**
   * 从`StructField`实例创建[[Attribute]]。
   */
  def fromStructField(field: StructField): Attribute = decodeStructField(field, false)
}

/**
 * :: DeveloperApi ::
 * 属性的工厂类。
 */
@DeveloperApi
object Attribute extends AttributeFactory {

  private[attribute] override def fromMetadata(metadata: Metadata): Attribute = {
    import org.apache.spark.ml.attribute.AttributeKeys._
    val attrType = if (metadata.contains(TYPE)) {
      metadata.getString(TYPE)
    } else {
      AttributeType.Numeric.name
    }
    getFactory(attrType).fromMetadata(metadata)
  }

  /** 根据属性类型名称获取属性工厂。 */
  private def getFactory(attrType: String): AttributeFactory = {
    if (attrType == AttributeType.Numeric.name) {
      NumericAttribute
    } else if (attrType == AttributeType.Nominal.name) {
      NominalAttribute
    } else if (attrType == AttributeType.Binary.name) {
      BinaryAttribute
    } else {
      throw new IllegalArgumentException(s"无法识别的类型 $attrType.")
    }
  }
}
  1. nameindexattrType:这些方法提供了获取属性名称、索引和类型的功能,可以直接调用。
  • attrType:属性类型,可取值为Numeric、Nominal和Binary。
  • name:属性的名称,类型为Option[String],如果未设置则为None。
  • index:属性的索引,类型为Option[Int],如果未设置则为None。
  1. withName(name: String)withIndex(index: Int):这些方法用于创建属性的副本,并设置新的名称或索引。可以通过调用这些方法来修改属性对象。
  2. withoutNamewithoutIndex:这些方法创建没有名称或索引的属性副本。可以使用这些方法移除属性对象的名称或索引。
  3. isNumericisNominal:这些方法用于检查属性是否为数值型标称型属性。可以根据返回的布尔值判断属性的类型。
  • isNumeric:检查属性是否是数值属性,对于NumericAttributeBinaryAttribute返回true。
  • isNominal:检查属性是否是标称属性,对于NominalAttributeBinaryAttribute返回true。
  1. toMetadataImpl(withType: Boolean):这个方法将属性转换为Metadata对象。参数withType表示是否包含属性类型信息。可以根据需要调用此方法来生成相应的Metadata对象。
  2. toMetadata(existingMetadata: Metadata)toMetadata():这些方法将属性转换为ML元数据对象(Metadata)toMetadata(existingMetadata: Metadata)方法会在现有的Metadata上添加属性相关的元数据,而toMetadata()方法则以空的Metadata对象开始构建新的ML元数据。可以根据需求选择适当的方法来生成ML元数据。
  3. toStructField(existingMetadata: Metadata)toStructField():这些方法将属性转换为StructField对象,用于在Spark中定义数据集的模式。toStructField(existingMetadata: Metadata)方法会在现有的Metadata上添加属性相关的元数据,而toStructField()方法则以空的Metadata对象开始构建新的StructField。可以根据需求选择适当的方法来生成StructField
    可以在DataFrame中使用。
  4. toString:这个方法将属性对象转换为字符串形式,输出属性的元数据信息的字符串表示。可以直接调用此方法来获取属性的字符串表示形式。

Attribute有三个子类:NumericAttribute、NominalAttribute和BinaryAttribute,分别表示数值属性、标称属性和二进制属性。这些子类提供了特定属性类型的额外方法和属性。

NumericAttribute为例,它包含了数值属性的一些统计信息,比如最小值、最大值、标准差和稀疏度等。NumericAttribute提供了一系列方法来修改这些统计信息,并且继承了Attribute的方法和属性。

Attribute工厂类还提供了fromMetadata()fromStructField()方法,用于从Metadata和StructField创建属性实例。

NumericAttribute

/**
 * :: DeveloperApi ::
 * 具有可选汇总统计信息的数值属性。
 * @param name 可选的名称
 * @param index 可选的索引
 * @param min 可选的最小值
 * @param max 可选的最大值
 * @param std 可选的标准差
 * @param sparsity 可选的稀疏度(零的比率)
 */
@DeveloperApi
class NumericAttribute private[ml] (
    override val name: Option[String] = None,
    override val index: Option[Int] = None,
    val min: Option[Double] = None,
    val max: Option[Double] = None,
    val std: Option[Double] = None,
    val sparsity: Option[Double] = None) extends Attribute {

  std.foreach { s =>
    require(s >= 0.0, s"标准差不能为负数,但得到的值为 $s.")
  }
  sparsity.foreach { s =>
    require(s >= 0.0 && s <= 1.0, s"稀疏度必须在 [0, 1] 范围内,但得到的值为 $s.")
  }

  override def attrType: AttributeType = AttributeType.Numeric

  override def withName(name: String): NumericAttribute = copy(name = Some(name))
  override def withoutName: NumericAttribute = copy(name = None)

  override def withIndex(index: Int): NumericAttribute = copy(index = Some(index))
  override def withoutIndex: NumericAttribute = copy(index = None)

  /** 创建具有新最小值的副本。 */
  def withMin(min: Double): NumericAttribute = copy(min = Some(min))

  /** 创建没有最小值的副本。 */
  def withoutMin: NumericAttribute = copy(min = None)


  /** 创建具有新最大值的副本。 */
  def withMax(max: Double): NumericAttribute = copy(max = Some(max))

  /** 创建没有最大值的副本。 */
  def withoutMax: NumericAttribute = copy(max = None)

  /** 创建具有新标准差的副本。 */
  def withStd(std: Double): NumericAttribute = copy(std = Some(std))

  /** 创建没有标准差的副本。 */
  def withoutStd: NumericAttribute = copy(std = None)

  /** 创建具有新稀疏度的副本。 */
  def withSparsity(sparsity: Double): NumericAttribute = copy(sparsity = Some(sparsity))

  /** 创建没有稀疏度的副本。 */
  def withoutSparsity: NumericAttribute = copy(sparsity = None)

  /** 创建没有汇总统计信息的副本。 */
  def withoutSummary: NumericAttribute = copy(min = None, max = None, std = None, sparsity = None)

  override def isNumeric: Boolean = true

  override def isNominal: Boolean = false

  /** 将属性转换为元数据。 */
  override private[attribute] def toMetadataImpl(withType: Boolean): Metadata = {
    import org.apache.spark.ml.attribute.AttributeKeys._
    val bldr = new MetadataBuilder()
    if (withType) bldr.putString(TYPE, attrType.name)
    name.foreach(bldr.putString(NAME, _))
    index.foreach(bldr.putLong(INDEX, _))
    min.foreach(bldr.putDouble(MIN, _))
    max.foreach(bldr.putDouble(MAX, _))
    std.foreach(bldr.putDouble(STD, _))
    sparsity.foreach(bldr.putDouble(SPARSITY, _))
    bldr.build()
  }

  /** 创建具有可选更改的属性副本。 */
  private def copy(
      name: Option[String] = name,
      index: Option[Int] = index,
      min: Option[Double] = min,
      max: Option[Double] = max,
      std: Option[Double] = std,
      sparsity: Option[Double] = sparsity): NumericAttribute = {
    new NumericAttribute(name, index, min, max, std, sparsity)
  }

  override def equals(other: Any): Boolean = {
    other match {
      case o: NumericAttribute =>
        (name == o.name) &&
          (index == o.index) &&
          (min == o.min) &&
          (max == o.max) &&
          (std == o.std) &&
          (sparsity == o.sparsity)
      case _ =>
        false
    }
  }

  override def hashCode: Int = {
    var sum = 17
    sum = 37 * sum + name.hashCode
    sum = 37 * sum + index.hashCode
    sum = 37 * sum + min.hashCode
    sum = 37 * sum + max.hashCode
    sum = 37 * sum + std.hashCode
    sum = 37 * sum + sparsity.hashCode
    sum
  }
}

/**
 * :: DeveloperApi ::
 * 数值属性的工厂方法。
 */
@DeveloperApi
object NumericAttribute extends AttributeFactory {

  /** 默认的数值属性。 */
  val defaultAttr: NumericAttribute = new NumericAttribute

  /**
   * 从元数据创建数值属性对象。
   * @param metadata 元数据对象
   * @return 数值属性对象
   */
  private[attribute] override def fromMetadata(metadata: Metadata): NumericAttribute = {
    import org.apache.spark.ml.attribute.AttributeKeys._
    // 从元数据中提取名称、索引、最小值、最大值、标准差和稀疏度等属性信息,并使用这些信息创建一个新的NumericAttribute对象
    val name = if (metadata.contains(NAME)) Some(metadata.getString(NAME)) else None
    val index = if (metadata.contains(INDEX)) Some(metadata.getLong(INDEX).toInt) else None
    val min = if (metadata.contains(MIN)) Some(metadata.getDouble(MIN)) else None
    val max = if (metadata.contains(MAX)) Some(metadata.getDouble(MAX)) else None
    val std = if (metadata.contains(STD)) Some(metadata.getDouble(STD)) else None
    val sparsity = if (metadata.contains(SPARSITY)) Some(metadata.getDouble(SPARSITY)) else None
    new NumericAttribute(name, index, min, max, std, sparsity)
  }
}

/**
 * 用于存储属性的键。
 */
private[attribute] object AttributeKeys {
  val ML_ATTR: String = "ml_attr" // ML属性的键
  val TYPE: String = "type" // 属性类型的键
  val NAME: String = "name" // 属性名称的键
  val INDEX: String = "idx" // 属性索引的键
  val MIN: String = "min" // 最小值的键
  val MAX: String = "max" // 最大值的键
  val STD: String = "std" // 标准差的键
  val SPARSITY: String = "sparsity" // 稀疏度的键
  val ORDINAL: String = "ord" // 序数型属性的键
  val VALUES: String = "vals" // 属性值列表的键
  val NUM_VALUES: String = "num_vals" // 属性值数量的键
  val ATTRIBUTES: String = "attrs" // 属性列表的键
  val NUM_ATTRIBUTES: String = "num_attrs" // 属性数量的键
}

以下是对NumericAttribute源码的分析:

sealed class NumericAttribute(
    override val name: Option[String] = None,
    override val index: Option[Int] = None,
    val min: Option[Double] = None,
    val max: Option[Double] = None,
    val std: Option[Double] = None,
    val sparsity: Option[Double] = None)
  extends Attribute {

  override def attrType: AttributeType = AttributeType.Numeric

  override def withName(name: String): NumericAttribute =
    new NumericAttribute(Some(name), index, min, max, std, sparsity)

  override def withoutName: NumericAttribute =
    new NumericAttribute(None, index, min, max, std, sparsity)

  override def withIndex(index: Int): NumericAttribute =
    new NumericAttribute(name, Some(index), min, max, std, sparsity)

  override def withoutIndex: NumericAttribute =
    new NumericAttribute(name, None, min, max, std, sparsity)

  override def isNumeric: Boolean = true

  override def isNominal: Boolean = false

  private[spark] override def toMetadataImpl(withType: Boolean): Metadata = {
    val metadataBuilder = new MetadataBuilder()
    if (withType) {
      metadataBuilder.putString(AttributeKeys.TYPE, AttributeType.Numeric.toString)
    }
    name.foreach(metadataBuilder.putString(AttributeKeys.NAME, _))
    index.foreach(i => metadataBuilder.putLong(AttributeKeys.INDEX, i.toLong))
    min.foreach(metadataBuilder.putDouble(AttributeKeys.MIN, _))
    max.foreach(metadataBuilder.putDouble(AttributeKeys.MAX, _))
    std.foreach(metadataBuilder.putDouble(AttributeKeys.STD, _))
    sparsity.foreach(metadataBuilder.putDouble(AttributeKeys.SPARSITY, _))
    metadataBuilder.build()
  }
}

NumericAttribute是表示数值属性的类,它继承自Attribute抽象类。

该类具有以下主要特点和功能:

  • 构造函数:接受可选的名称、索引以及最小值、最大值、标准差和稀疏度等属性值作为参数。
  • 属性类型:重写了attrType方法,返回属性类型为AttributeType.Numeric
  • 设置和获取属性值:提供了一系列方法(如withNamewithIndexwithoutNamewithoutIndex)用于设置和获取属性的名称和索引。
  • 数值属性判断:重写了isNumeric方法,返回true,表示该属性是数值属性;重写了isNominal方法,返回false,表示该属性不是标称属性。
  • 元数据转换:通过重写toMetadataImpl方法,将属性转换为Metadata对象,其中包含了属性的各种信息(如类型、名称、索引、最小值、最大值、标准差和稀疏度等)。
  • 属性复制:通过创建新的NumericAttribute对象,并传递相应的属性值,可以进行属性的复制和修改操作。

通过对NumericAttribute源码的分析,我们可以看到它提供了一系列方法和功能,方便地表示和处理数值属性,并与其他Spark ML组件进行集成。

NominalAttribute

/**
 * :: DeveloperApi ::
 * 标称属性。
 * @param name 可选的名称
 * @param index 可选的索引
 * @param isOrdinal 是否为序数型属性(可选)
 * @param numValues 可选的值的数量。`numValues`和`values`只能定义其中一个。
 * @param values 可选的值。`numValues`和`values`只能定义其中一个。
 */
@DeveloperApi
class NominalAttribute private[ml] (
    override val name: Option[String] = None,
    override val index: Option[Int] = None,
    val isOrdinal: Option[Boolean] = None,
    val numValues: Option[Int] = None,
    val values: Option[Array[String]] = None) extends Attribute {

  numValues.foreach { n =>
    require(n >= 0, s"numValues不能为负数,但得到的是 $n.")
  }
  require(!(numValues.isDefined && values.isDefined),
    "不能同时定义numValues和values.")

  override def attrType: AttributeType = AttributeType.Nominal

  override def isNumeric: Boolean = false

  override def isNominal: Boolean = true

  private lazy val valueToIndex: Map[String, Int] = {
    values.map(_.zipWithIndex.toMap).getOrElse(Map.empty)
  }

  /** 获取特定值的索引。 */
  def indexOf(value: String): Int = {
    valueToIndex(value)
  }

  /** 检查属性是否包含特定值。 */
  def hasValue(value: String): Boolean = valueToIndex.contains(value)

  /** 根据索引获取值。 */
  def getValue(index: Int): String = values.get(index)

  override def withName(name: String): NominalAttribute = copy(name = Some(name))
  override def withoutName: NominalAttribute = copy(name = None)

  override def withIndex(index: Int): NominalAttribute = copy(index = Some(index))
  override def withoutIndex: NominalAttribute = copy(index = None)

  /**
   * 使用新的值和空的`numValues`进行复制。
   */
  def withValues(values: Array[String]): NominalAttribute = {
    copy(numValues = None, values = Some(values))
  }

  /**
   * 使用新的值和空的`numValues`进行复制。
   */
  @varargs
  def withValues(first: String, others: String*): NominalAttribute = {
    copy(numValues = None, values = Some((first +: others).toArray))
  }

  /** 不包含值的复制。 */
  def withoutValues: NominalAttribute = {
    copy(values = None)
  }

  /**
   * 使用新的`numValues`和空的`values`进行复制。
   */
  def withNumValues(numValues: Int): NominalAttribute = {
    copy(numValues = Some(numValues), values = None)
  }

  /**
   * 不包含`numValues`的复制。
   */
  def withoutNumValues: NominalAttribute = copy(numValues = None)

  /**
   * 获取值的数量,可以从`numValues`或`values`中获取。
   * 如果未知,则返回None。
   */
  def getNumValues: Option[Int] = {
    if (numValues.nonEmpty) {
      numValues
    } else if (values.nonEmpty) {
      Some(values.get.length)
    } else {
      None
    }
  }

  /** 创建带有可选更改的属性副本。 */
  private def copy(
      name: Option[String] = name,
      index: Option[Int] = index,
      isOrdinal: Option[Boolean] = isOrdinal,
      numValues: Option[Int] = numValues,
      values: Option[Array[String]] = values): NominalAttribute = {
    new NominalAttribute(name, index, isOrdinal, numValues, values)
  }

  override private[attribute] def toMetadataImpl(withType: Boolean): Metadata = {
    import org.apache.spark.ml.attribute.AttributeKeys._
    val bldr = new MetadataBuilder()
    if (withType) bldr.putString(TYPE, attrType.name)
    name.foreach(bldr.putString(NAME, _))
    index.foreach(bldr.putLong(INDEX, _))
    isOrdinal.foreach(bldr.putBoolean(ORDINAL, _))
    numValues.foreach(bldr.putLong(NUM_VALUES, _))
    values.foreach(v => bldr.putStringArray(VALUES, v))
    bldr.build()
  }

  override def equals(other: Any): Boolean = {
    other match {
      case o: NominalAttribute =>
        (name == o.name) &&
          (index == o.index) &&
          (isOrdinal == o.isOrdinal) &&
          (numValues == o.numValues) &&
          (values.map(_.toSeq) == o.values.map(_.toSeq))
      case _ =>
        false
    }
  }

  override def hashCode: Int = {
    var sum = 17
    sum = 37 * sum + name.hashCode
    sum = 37 * sum + index.hashCode
    sum = 37 * sum + isOrdinal.hashCode
    sum = 37 * sum + numValues.hashCode
    sum = 37 * sum + values.map(_.toSeq).hashCode
    sum
  }
    
/**
 * :: DeveloperApi ::
 * 标称属性的工厂方法。
 */
@DeveloperApi
object NominalAttribute extends AttributeFactory {

  /** 默认的标称属性。 */
  final val defaultAttr: NominalAttribute = new NominalAttribute

  private[attribute] override def fromMetadata(metadata: Metadata): NominalAttribute = {
    import org.apache.spark.ml.attribute.AttributeKeys._
    val name = if (metadata.contains(NAME)) Some(metadata.getString(NAME)) else None
    val index = if (metadata.contains(INDEX)) Some(metadata.getLong(INDEX).toInt) else None
    val isOrdinal = if (metadata.contains(ORDINAL)) Some(metadata.getBoolean(ORDINAL)) else None
    val numValues =
      if (metadata.contains(NUM_VALUES)) Some(metadata.getLong(NUM_VALUES).toInt) else None
    val values =
      if (metadata.contains(VALUES)) Some(metadata.getStringArray(VALUES)) else None
    new NominalAttribute(name, index, isOrdinal, numValues, values)
  }
}

NominalAttribute 是一个标称属性的类,它提供了一些工厂方法来创建和操作标称属性。

使用 NominalAttribute.defaultAttr 可以获取默认的标称属性对象。

可以使用 NominalAttribute.fromMetadata(metadata: Metadata) 方法从元数据中创建标称属性对象。该方法会解析元数据并返回相应的标称属性对象。

标称属性对象的常用方法包括:

  • withName(name: String): NominalAttribute:设置属性的名称,并返回一个新的标称属性对象。
  • withoutName: NominalAttribute:移除属性的名称,并返回一个新的标称属性对象。
  • withIndex(index: Int): NominalAttribute:设置属性的索引,并返回一个新的标称属性对象。
  • withoutIndex: NominalAttribute:移除属性的索引,并返回一个新的标称属性对象。
  • withValues(values: Array[String]): NominalAttribute:设置属性的值,并返回一个新的标称属性对象。该方法会清空 numValues 的值。
  • withoutValues: NominalAttribute:移除属性的值,并返回一个新的标称属性对象。
  • withNumValues(numValues: Int): NominalAttribute:设置属性的值的数量,并返回一个新的标称属性对象。该方法会清空属性的值。
  • withoutNumValues: NominalAttribute:移除属性的值的数量,并返回一个新的标称属性对象。
  • getNumValues: Option[Int]:获取属性的值的数量,如果已知则返回具体的数量,否则返回 None
  • equals(other: Any): Boolean:判断属性对象是否与另一个对象相等。
  • hashCode: Int:计算属性对象的哈希值。

通过上述方法,可以使用 NominalAttribute 类创建和操作标称属性对象。

BinaryAttribute

/**
 * :: DeveloperApi ::
 * 二元属性。
 * @param name 可选的名称
 * @param index 可选的索引
 * @param values 可选的值。如果设置,其大小必须为2。
 */
@DeveloperApi
class BinaryAttribute private[ml] (
    override val name: Option[String] = None,
    override val index: Option[Int] = None,
    val values: Option[Array[String]] = None)
  extends Attribute {

  values.foreach { v =>
    require(v.length == 2, s"二元属性的值数量必须为2,但得到的是 ${v.toSeq}。")
  }

  override def attrType: AttributeType = AttributeType.Binary

  override def isNumeric: Boolean = true

  override def isNominal: Boolean = true

  override def withName(name: String): BinaryAttribute = copy(name = Some(name))
  override def withoutName: BinaryAttribute = copy(name = None)

  override def withIndex(index: Int): BinaryAttribute = copy(index = Some(index))
  override def withoutIndex: BinaryAttribute = copy(index = None)

  /**
   * 使用新的值进行复制。
   * @param negative 负面名称
   * @param positive 正面名称
   */
  def withValues(negative: String, positive: String): BinaryAttribute =
    copy(values = Some(Array(negative, positive)))

  /** 不包含值的复制。 */
  def withoutValues: BinaryAttribute = copy(values = None)

  /** 创建带有可选更改的属性副本。 */
  private def copy(
      name: Option[String] = name,
      index: Option[Int] = index,
      values: Option[Array[String]] = values): BinaryAttribute = {
    new BinaryAttribute(name, index, values)
  }

  override private[attribute] def toMetadataImpl(withType: Boolean): Metadata = {
    import org.apache.spark.ml.attribute.AttributeKeys._
    val bldr = new MetadataBuilder
    if (withType) bldr.putString(TYPE, attrType.name)
    name.foreach(bldr.putString(NAME, _))
    index.foreach(bldr.putLong(INDEX, _))
    values.foreach(v => bldr.putStringArray(VALUES, v))
    bldr.build()
  }

  override def equals(other: Any): Boolean = {
    other match {
      case o: BinaryAttribute =>
        (name == o.name) &&
          (index == o.index) &&
          (values.map(_.toSeq) == o.values.map(_.toSeq))
      case _ =>
        false
    }
  }

  override def hashCode: Int = {
    var sum = 17
    sum = 37 * sum + name.hashCode
    sum = 37 * sum + index.hashCode
    sum = 37 * sum + values.map(_.toSeq).hashCode
    sum
  }
}

/**
 * :: DeveloperApi ::
 * 二元属性的工厂方法。
 */
@DeveloperApi
object BinaryAttribute extends AttributeFactory {

  /** 默认的二元属性。 */
  final val defaultAttr: BinaryAttribute = new BinaryAttribute

  private[attribute] override def fromMetadata(metadata: Metadata): BinaryAttribute = {
    import org.apache.spark.ml.attribute.AttributeKeys._
    val name = if (metadata.contains(NAME)) Some(metadata.getString(NAME)) else None
    val index = if (metadata.contains(INDEX)) Some(metadata.getLong(INDEX).toInt) else None
    val values =
      if (metadata.contains(VALUES)) Some(metadata.getStringArray(VALUES)) else None
    new BinaryAttribute(name, index, values)
  }
}

以上是BinaryAttribute类和BinaryAttribute对象的源代码。BinaryAttribute类代表一个二元属性,它具有名称、索引和值等属性。BinaryAttribute对象是一个工厂对象,提供了创建和解析二元属性的方法。

BinaryAttribute类的常用方法包括:

  • withName(name: String): BinaryAttribute:设置属性的名称,并返回一个新的二元属性对象。
  • withoutName: BinaryAttribute:移除属性的名称,并返回一个新的二元属性对象。
  • withIndex(index: Int): BinaryAttribute:设置属性的索引,并返回一个新的二元属性对象。
  • withoutIndex: BinaryAttribute:移除属性的索引,并返回一个新的二元属性对象。
  • withValues(negative: String, positive: String): BinaryAttribute:设置属性的值,并返回一个新的二元属性对象。
  • withoutValues: BinaryAttribute:移除属性

UnresolvedAttribute

/**
 * :: DeveloperApi ::
 * 一个未解析的属性。
 */
@DeveloperApi
object UnresolvedAttribute extends Attribute {

  override def attrType: AttributeType = AttributeType.Unresolved

  override def withIndex(index: Int): Attribute = this

  override def isNumeric: Boolean = false

  override def withoutIndex: Attribute = this

  override def isNominal: Boolean = false

  override def name: Option[String] = None

  override private[attribute] def toMetadataImpl(withType: Boolean): Metadata = {
    Metadata.empty
  }

  override def withoutName: Attribute = this

  override def index: Option[Int] = None

  override def withName(name: String): Attribute = this

}

以上是UnresolvedAttribute对象的源代码。UnresolvedAttribute代表一个未解析的属性,它的属性类型为未解析,并且没有名称和索引。它不是数值属性,也不是标称属性。

该对象提供以下方法:

  • attrType: AttributeType:返回属性类型,此处为未解析。
  • withIndex(index: Int): Attribute:设置属性的索引,并返回当前对象(因为未解析属性不具有索引)。
  • isNumeric: Boolean:判断属性是否为数值属性,此处为false。
  • withoutIndex: Attribute:移除属性的索引,并返回当前对象。
  • isNominal: Boolean:判断属性是否为标称属性,此处为false。
  • name: Option[String]:返回属性的名称,此处为None。
  • toMetadataImpl(withType: Boolean): Metadata:将属性转换为元数据,此处返回一个空的元数据。
  • withoutName: Attribute:移除属性的名称,并返回当前对象。
  • index: Option[Int]:返回属性的索引,此处为None。
  • withName(name: String): Attribute:设置属性的名称,并返回当前对象。