【背景】
首先我们先看一下,本期度词条对信息增益的广义定义:
其实,我们主要用到信息增益,还是在特征选择上。个人理解信息增益就是目标列(y)整体信息熵和特征列每一个类别分组下对应目标列信息熵期望的差值,所有在命名时会用到增益。整体来看是y值的整体分布和y值在不同特征类型别下分布累加的差距。依此来衡量变量对目标(y)列的区分度,或者叫纯度。但是信息增益评价指标会倾向于类别个数比较多的列。所以特征选择过程中会配合增益率一块使用,或者对特征列进行分分箱离散化时特征列类别个数相近,从而去除特征类别个数对评价指标的影响。
【原理】
【公式】
其中 Ent(D)是目标y列的信息熵,第二项是条件信息熵期望,即D列每个类别(D^v)对应y的信息熵的期望
【spark实现】
如下为计算每隔待选列的信息熵
object EntropyCalculator {
/**
* @desc 计算信息熵
* @param calCols 需要计算的列
* @param dfRowNum 当前数据集总行数
* @return
*/
def calculator(
df: DataFrame,
calCols: Array[String],
ycol: String,
dfRowNum: Double
): Map[String, Double] = {
if (df.storageLevel == StorageLevel.NONE) {
df.persist(StorageLevel.DISK_ONLY)
}
val result = if (dfRowNum == 0) {
calCols.map(col => (col, 0.0D)).toMap
} else {
/** 将列分成多组,一次计算多列的信息熵 */
val calculaAggColNum = PropertiesUtils.calculaAggColNum
val colsGroup: Array[Array[String]] = calCols.sliding(calculaAggColNum, calculaAggColNum).toArray
val result = colsGroup.map(cols => {
/** 拼接数据行转列逻辑计划 */
val expodeEtlPlan = explode(array(cols.map(colT => array(lit(colT), col(colT))): _*)).alias("arr")
/** 拼接每组百分比的计算的逻辑计划 */
val percent = col("count") / dfRowNum.toDouble
/** 信息计算逻辑 */
val percentMultLogPercent = -percent * log(percent)
df.select(expodeEtlPlan)
.select(col("arr").getItem(0).alias("key"), col("arr").getItem(1).alias("value"))
.groupBy(col("key"), col("value"))
.agg(count("key").alias("count"))
.select(percentMultLogPercent.alias("percent"), col("key"))
.groupBy("key")
.agg(sum(col("percent")).alias("percent_sum"))
.collect()
.map(row => (row.getAs[String]("key"), row.getAs[Double]("percent_sum")))
.toMap
})
if (result.isEmpty) Map[String, Double]() else result.reduce(_ ++ _)
}
val remainCols = (calCols.toSet -- result.keySet).map((_, 0.0D)).toMap
result ++ remainCols
}
}
object GroupEntropyCalculator {
/**
* @desc 条件信息熵期望计算
* @param df
* @param yCol
* @param calCol
* @return
*/
def calculator(
df: DataFrame,
calCols: Array[String],
yCol: String, dfRowNum: Double
): Map[String, Double] = {
if (df.storageLevel == StorageLevel.NONE) {
df.persist(StorageLevel.DISK_ONLY)
}
/** 注册临时函数,计算map[count,num]计算信息熵 */
df.sparkSession.udf.register("cal_entropy", (map: Map[Long, Long]) => {
val size = map.map(tup => tup._1 * tup._2).sum.toDouble
if (size == 0) 0 else map.map(tup => {
val count = tup._1
val percent: Double = count.toDouble / size
val message: Double = -percent * Math.log(percent)
(message, tup._2)
}).map(tp => tp._1 * tp._2).sum * (size / dfRowNum)
})
val calculaAggColNum = PropertiesUtils.calculaAggColNum
val resultT = calCols.filter(!_.equals(yCol)).sliding(calculaAggColNum, calculaAggColNum).map(cols => {
val colNameAndColValue = cols.filter(!_.equals(yCol)).map(colT => array(lit(colT), col(colT), col(yCol)))
val expodeEtlPlan = explode(array(colNameAndColValue: _*)).alias("arr")
df.select(expodeEtlPlan, col(yCol))
.select(
col("arr").getItem(0).alias("key"),
col("arr").getItem(1).alias("value"),
col("arr").getItem(2).alias(yCol)
)
.groupBy("key", "value", yCol)
.agg(count(yCol).alias("ycount"))
.groupBy("key", "value")
.agg(callUDF(UDFManager.collect2MapLong, col("ycount")).alias("count_map"))
.select(callUDF("cal_entropy", col("count_map")).alias("entropy"), col("key"))
.groupBy(col("key"))
.agg(sum("entropy"))
.collect()
.map(row => (row.getString(0), row.getDouble(1)))
.toMap
}).toArray
val result = if (resultT.isEmpty) Map[String, Double]() else resultT.reduce(_ ++ _)
val remain = (calCols.toSet -- result.keySet - yCol).map((_, 0.0D))
result ++ remain
}
}
object GainCalculator {
def calculator(
df: DataFrame,
calCols: Array[String],
ycol: String,
dfRowNum: Double
): Map[String, Double] = {
if (df.storageLevel == StorageLevel.NONE) {
df.persist(StorageLevel.DISK_ONLY)
}
/** 计算y列信息熵 */
val allEntropy: Map[String, Double] = EntropyCalculator.calculator(df, Array(ycol), null, dfRowNum)
/** y列的信息熵 */
val yEntropy: Double = allEntropy(ycol)
/** 计算分组信息熵 */
val groupEntropys: Map[String, Double] = GroupEntropyCalculator.calculator(df, calCols, ycol, dfRowNum)
/** 计算信息增益率 */
((allEntropy.keySet - ycol) ++ groupEntropys.keySet).map(col => {
val groupEntropy = groupEntropys.getOrElse(col, 0.0D)
val gain = yEntropy - groupEntropy
(col, gain)
}).toMap
}
}
【批量计算优化】