目录
一.引言
前面 Flink 数据倾斜实战中我们学习了 Flink keyBy 的 Hash 实现算法并提出了更加广义的倾斜数据校验方法,其中一种是利用原有 Hash 算法进行遍历并得到分区分布,鉴于最近 ChatGPT4 非常火爆,于是我们也尝试学习下 Spark 面对倾斜数据如何统计处理,所以便有了下述结果:
不得不说给出的方法非常全面且靠谱,最后一个方法中给出了 Skewness 算子检测数据倾斜,Skewness 偏度算子和 kurtosis 峰度算子都是 SparkSql 的 Agg 算子,既然 ChatGPT4 建议了,下面我们就学习一下。
二.峰度 Skewness 简介
偏度是统计数据分布倾斜方向和程度的度量,又称偏态、偏态系数。其表征概率分布密度曲线相对于平均值不对称程度的特征数,直观看就是密度函数曲线尾部的相对长度。偏度定义为:
其中 k2、k3 分别代表二阶和三阶中心距。
正态分布的偏度为0,两侧尾部长度对称。以 S 代表偏度,S < 0 代表负偏离,也称左偏态,反之如果 S > 0 则代表正偏态,也称右偏态,如果 S ≈ 0 则可以认为分布是均匀的。由于左右偏态的图形特点和我们的视觉直观不符合,所以经常将左右偏态判断反,这里有一个好的办法区分,即观察分布的尾巴,哪边尾巴长,就是哪边的偏态分布。例如第一个正偏态的偏度 S > 0,我们可以观察到其右边的尾巴长。
Tips:
右偏分布 - 平均数 > 中位数 > 众数
左偏分布 - 众数 > 中位数 > 平均数
三.峰度 kurtosis 简介
本来 GPT 只告诉了 Skewness 偏度查看数据倾斜,但我看 SparkSQL 还有个 kurtosis 算子计算峰度,二者计算方法差别不大,一个三阶矩一个四阶矩所以本文也一并介绍了方便后续使用。风度,泛指一个人的言谈举止和仪态,一般有风度翩翩,哈哈被输入法搞跑题了,峰度又称峰态系数,表征概率密度分布曲线在平均值处峰值高低的特征数。峰度定义为:
其中 m4 是四阶样本中心距,m2 是二阶中心距即样本方差,这里减3是为了让正态分布峰度为0。
峰度值 K 如果大于 0,则称为尖峰态而如果 K 小于 0 则称为低峰态,从图中可以看到 K 越大其均值处的尖度越尖。
四.Skewness 偏度与 kurtosis 峰度实现
1.Spark 实现
- 初始化 SparkSession
val conf = (new SparkConf).setMaster("local[*]").setAppName("SparkSQLAgg")
val spark = SparkSession
.builder
.config(conf)
.getOrCreate()
val sc = spark.sparkContext
- 构建随机数据
使用 random 构建随机数据,这里 0-50 的数据会比 50-100 的数据多,所以 50-100 即后面的尾巴长,所以是右偏分布即正偏态,从而可知 S > 0。
val dataBuffer = new ArrayBuffer[Double]()
val random = scala.util.Random
(0 to 100000).foreach(num => {
if (num < 50000) {
dataBuffer.append(100 * random.nextDouble())
} else {
dataBuffer.append(50 * random.nextDouble())
}
})
- 计算偏度与峰度
将上面的模拟数据生成 DataFrame,直接 agg 调用聚合函数获取统计值,可以看到偏度 > 0 符合我们上面的预期,大家可以修改上面的自定义数据,查看偏度的变化。
val sqlContext = new SQLContext(sc)
import sqlContext.implicits._
val skewRDD = sc.parallelize(dataBuffer)
val skewDF = skewRDD.toDF("key1")
// key1、key2 为计算 Skewness 的列名,Skewness 会返回一个 DataFrame 算子,其中包含各列的 Skewness 的值
val skewnessValues = skewDF.agg(avg("key1"), stddev_pop("key1"), var_pop("key1"), skewness("key1"), kurtosis("key1"))
skewnessValues.show()
2.自定义实现
基于 spark.sql.functions 和定义公式,我们也可以自己实现偏度和峰度的代码:
- 偏度
/*
skewness_pop = E [((X - mu_pop) / stddev_pop) ^ 3]
X: the random variable
mu_pop: population mean
stddev_pop: population standard deviation
sqrt(n) * m3 / sqrt(m2 * m2 * m2)
where if m refers to (X - mu), then m2 refers to (X - mu)^2 and m3 refers to (X - mu)^3.
skewness_samp = SUM(i=1 to n) [(xi - mu_samp) ^ 3] / n
--------------------------------------
stddev_samp ^ 3
*/
def calculateSkewness(df: DataFrame, column: String): Double = {
val mean = calculateMean(df, column)
val stdDev = calculateStdDev(df, column)
val totalNum = df.count()
val thirdCentralSampleMoment = df.agg(
functions.sum(functions.pow(functions.column(column) - mean, 3) / totalNum)
).head.getDouble(0)
val thirdPowerOfSampleStdDev = scala.math.pow(stdDev, 3)
thirdCentralSampleMoment / thirdPowerOfSampleStdDev
}
- 峰度
/*
kurtosis_pop = E [((X - mu_pop) / stddev_pop) ^ 4]
excess_kurtosis_pop = kurtosis_pop - 3.0
X: the random variable
mu_pop: population_mean
stddev_pop: population standard deviation
n * m4 / (m2 * m2) - 3.0
where if m refers to (X - mu), then m2 refers to (X - mu)^2 and m4 refers to (X - mu)^4.
kurtosis_samp = SUM(i=1 to n) [(xi - mu_samp) ^ 4] / n
--------------------------------------
stddev_samp ^ 4
excess_kurtosis_samp = kurtosis_samp - 3.0
*/
def calculateExcessKurtosis(df: DataFrame, column: String): Double = {
val mean = calculateMean(df, column)
val stdDev = calculateStdDev(df, column)
val totalNum = df.count()
val fourthCentralSampleMoment = df.agg(
functions.sum(functions.pow(functions.column(column) - mean, 4) / totalNum)
).head.getDouble(0)
val fourthPowerOfSampleStdDev = scala.math.pow(stdDev, 4)
(fourthCentralSampleMoment / fourthPowerOfSampleStdDev) - 3
}
- 辅助函数
细心的同学可能会发现有 xxx_pop 和 xxx_samp 这样后缀的 agg 聚合统计函数,这里 pop 的计算基于完整数据,samp 的计算基于采样数据,如果大家数据量且只追求相对准确的结果可以选择 _samp 在损失一些精度的情况下提高速度。
def calculateMean(df: DataFrame, column: String): Double = {
df.agg(avg(column)).head.getDouble(0) // 均值
}
def calculateVar(df: DataFrame, column: String): Double = {
df.agg(var_pop(column)).head.getDouble(0) // 方差
}
def calculateStdDev(df: DataFrame, column: String): Double = {
df.agg(stddev_pop(column)).head.getDouble(0) // 标准差
}
- 计算测试
val selfSkewness = calculateSkewness(skewDF, "key1")
val selfKurtosis = calculateExcessKurtosis(skewDF, "key1")
println(s"Skewness: ${selfSkewness} Kurtosis: ${selfKurtosis}")
根据公式自定义的计算结果与官方 API 结果一致:
五.偏度、峰度绘图
上面计算出模拟的数据偏度 S > 0 为右偏态即右边尾巴长,K < 0 为低峰态即尖峰程度小于正态分布,这里懒得把数据带到 python 画图了,所以使用 scala 的统计与画图工具 breeze.plot._,使用前需先引入依赖,这里对应 scala 2.11 版本:
<!-- https://mvnrepository.com/artifact/org.scalanlp/breeze-viz -->
<dependency>
<groupId>org.scalanlp</groupId>
<artifactId>breeze-viz_2.11</artifactId>
<version>1.0-RC2</version>
</dependency>
1.偏度 Hist Plot
import breeze.plot._
def plotHistogram(data: Array[Double]): Unit = {
val f = Figure()
val p = f.subplot(0)
// 加入当前数据分布
p += hist(data, 100)
p.xlabel = "X-Axis"
p.ylabel = "Y-Axis"
f.saveas("./histogram.png")
}
将原始数据 rdd 采样获得数据传入 plotHistogram 即可得到对应数据分布,可以看到右边尾巴长,满足 S > 0 的正偏态即右偏分布。
2.峰度 Hist Plot
def plotHistogram(data: Array[Double]): Unit = {
val f = Figure()
val p = f.subplot(0)
// 加入当前数据分布
p += hist(normalization(data), 100)
// 加入正态分布
val g = breeze.stats.distributions.Gaussian(0, 1)
p += hist(g.sample(data.length),100)
p.xlabel = "X-Axis"
p.ylabel = "Y-Axis"
f.saveas("./histogram.png")
}
将数据归一化,再加入一部分 N(0, 1) 的正态分布数据,其中蓝色为原始数据归一化后的分布,K < 0,此时状态为低峰态。
六.总结
之前虽然有过 Spark 数据倾斜的实战经验且学习数理统计期间也学到过偏度与峰度的概念,但是没有想过把这些统计概念应用到数据倾斜分析的实战中,也正是在 ChatGPT4 的提示下才回忆起过往的知识,上面一些实现的代码也是 ChatGPT4 给出,不得不感叹现在 AI 技术发展的迅速,作为程序员的大伙还是要努力提升自己呀,成为那个不可替代的角色。