目录
作者:赵凌宇
- 背景
当下是资本的时代,慈善事业的热潮似乎再不断减退,本次案例研究中,将使用Spark的大数据技术以及AS(algorithmStar)算法技术支持,对一份慈善捐赠者的捐赠情况数据进行基本的分析。
数据集等材料来自于Bruce Hardie's Home Page提供
- 意义
使用scala与Java结合的方式,对数据集进行数据清洗与指标分析,通过各类算法计算指标与需求,获取到参与捐赠比较稳定(即参与经历与全年参与经历之间的相似度系数较高),参与的一批捐赠者数据(包含每年参与以及与每年参与的捐赠者捐赠经历相差阈值不大的捐赠者ID名单以及其参与阈值),找到这类长期为慈善事业作出贡献的捐赠者,并对这类捐赠者进行数据可视化分析,给出分析结论。
- 数据集分析
下面就是本次数据分析所使用到的数据集描述(来源:Bruce Hardie's Home Page )
The file 1995_cohort_binary.txt contains the data on annual donation incidence for the 1995 cohort of charity supporters as used in Fader, Hardie and Shang (2010).
Each record in this file, 11104 in total, comprises the following 13 fields:
Supporter ID
Ever donate in 1995 (1 = yes, 0 = no)
Ever donate in 1996
Ever donate in 1997
Ever donate in 1998
Ever donate in 1999
Ever donate in 2000
Ever donate in 2001
Ever donate in 2002
Ever donate in 2003
Ever donate in 2004
Ever donate in 2005
Ever donate in 2006
Note that the value of the second field is 1 for all records because the defining characteristic of this cohort is that they made their first-ever donation in 1995.
Reference:
Fader, Peter S., Bruce G.S. Hardie, and Jen Shang (2010), "Customer-Base Analysis in a Discrete-Time Noncontractual Setting," Marketing Science, 29 (November-December), 1086-1108. [http://brucehardie.com/papers/020/]
简单来说,数据集中有13个字段,其中第一个字段是捐赠者ID,后面的所有字段为不同年份捐赠情况,从1995年开始依次递增,字段数值均为0/1,0代表当前年份未捐赠,1代表当年份已捐赠。,那么最为稳定的捐赠者的捐赠经历中应为“1 1 1 1 1 1 1 1 1 1 1 1”,即每一年都在捐赠。
- 数据算法分析
通过数据集可以看到,这是一份非常适合用来进行数据向量构建的数据集,在本次数据相似指标开发中,向量与西安距离是一个非常适合的算法,因此本次的与稳定捐赠经历相似数据指标,就使用向量余弦距离进行获取。
余弦距离(也称为余弦相似度): 用向量空间中两个向量夹角的余弦值 作为衡量两个个体间差异的大小的度量。向量:多维空间中有方向的线段,如果两个向量的 方向一致,即夹角接近零,那么这两个向量就相近 。而要确定两个向量方向是否一致,这就要用到余弦定理计算向量的夹角。
本次将使用余弦距离算法进行常驻捐赠者的数据提取操作,将两个特征向量,进行图计算,获取其之间的夹角,并构造出其夹角的余弦值,一般来说,向量越相似,向量夹角越小,cos余弦值就越大,相似度也就越高,有关向量余弦距离的最基本的两个参数就是模长与内积,向量的模长计算与两个向量之间的内积计算方式有很多中,本次根据我们的需求,算法逻辑如下所示。
- 技术划分
数据集的分组,格式清洗等宏观操作由大数据生态中火热的Spark分布式计算框架来进行,针对细节上的计算,采用AS(algorithmStar)技术。本次案例实现中采用scala与Java语言进行全程数据指标分析与开发,使用maven技术进行依赖的注入。
- Spark-core与AS的MAVEN坐标注入
<dependencies>
<dependency>
<groupId>io.github.BeardedManZhao</groupId>
<artifactId>algorithmStar</artifactId>
<version>1.12</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-core_2.12</artifactId>
<version>3.1.3</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-sql_2.12</artifactId>
<version>3.1.3</version>
</dependency>
</dependencies>
- 格式清洗
在我们的数据集中,为了确保无空值,无错误数据的出现,首先进行简单的数据清洗,空值或字段缺少的数值都进行初步的过滤,然后将新结果注册到Spark Session视图,配置上字段名称。
package run
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.types.{StringType, StructField, StructType}
import org.apache.spark.sql.{DataFrame, Row, SparkSession}
/**
* @author zhao
*/
object MAIN {
def main(args: Array[String]): Unit = {
System.setProperty("hadoop.home.dir", "E:\\RunTime\\Hadoop")
val spark: SparkSession = SparkSession.builder()
.master("local[2]")
.appName("zhao-ResidentDonorDigging")
.config("spark.testing.memory", "10245120000")
.config("hadoop.home.dir", "E:\\RunTime\\Hadoop")
.getOrCreate()
// 导入隐式转换
// 加载数据集文件,并进行空值与错误值的过滤
val context = spark.sparkContext
val value = {
context.textFile(
"C:\\Users\\zhao\\Desktop\\paper\\1995_cohort_binary\\1995_cohort_binary.txt"
)
}
// 开始进行数据清洗 这里使用正则表达式先将每一行的数据获取到
val value1: RDD[Array[String]] = value.flatMap(_.split("\n+"))
// 然后将每一个字段的数据切割出来
.mapPartitions(d => {
// 定义正则,这个正则被每一个分区复用,避免多次实例化
val rex = "\\s+"
d.map(_.split(rex))
})
// 过滤出来正确数据行,这里要求是13个字段
.filter(line => line != null && line.length == 13)
println("* >>> 格式清洗完成!目前数据行数 = " + value1.count())
// 开始将RDD转换为一个 DataSet
val value2: DataFrame = spark.createDataFrame(
// 在这里构建视图字段结构
value1.map(x => Row.fromSeq(x)), StructType(Seq(
StructField("user_id", StringType),
StructField("c_1995", StringType), StructField("c_1996", StringType),
StructField("c_1997", StringType), StructField("c_1998", StringType),
StructField("c_1999", StringType), StructField("c_2000", StringType),
StructField("c_2001", StringType), StructField("c_2002", StringType),
StructField("c_2003", StringType), StructField("c_2004", StringType),
StructField("c_2005", StringType), StructField("c_2006", StringType)
))
)
// 缓存数据
value2.cache()
// 进行一次查询
value2.show()
}
}
- 向量夹角余弦值计算函数
/**
* 获取两个序列之间的向量夹角余弦距离
* <p>
* Obtain the cosine distance of the vector angle between two sequences
*
* @param doubles1 数组序列1
* @param doubles2 数组序列2
* @return 这里的两个数组会被作为两个向量拿去计算。
*/
@Override
public double getTrueDistance(double[] doubles1, double[] doubles2) {
DoubleVector vector1 = DoubleVector.parse(doubles1);
DoubleVector vector2 = DoubleVector.parse(doubles2);
Double aDouble = vector1.innerProduct(vector2.expand());
logger.info(aDouble + " / ( " + vector1 + " * " + vector2 + " )");
return aDouble / (vector1.moduleLength() * vector2.moduleLength());
}
如上所示就是余弦夹角的计算逻辑的Java实现,数学表示如下所示
- 将每一个用户的数据字段进行向量的构建
在构建向量的时候,由于我们统计的指标中,主要在意的是常驻类型的捐赠者,因此不需要考虑向量元素顺序,直接将数据字段加载成为一个向量即可,在这个过程中,直接使用数组作为一个向量,将该向量传递给封装好的函数“getTrueDistance”,需要被计算的两个向量,第一个是样本向量,最深程度的常驻捐赠者,其12个年份经历字段全是1,因此现在将向量[1,1,1,1,1,1,1,1,1,1,1,1]作为样本向量a,下面是这一部分的代码逻辑,构建了样本向量,同时获取到余弦计算组件,并将封装好的函数,注册到函数库中,使得稍后的SQL处理可以使用余弦计算函数。
/**
* 在这里将挖掘出常驻捐赠者用户
*
* @param sparkSession SparkSql会话
* @param dataFrame Spark 数据框
*/
def IndicatorProcessing(sparkSession: SparkSession, dataFrame: DataFrame): Unit = {
// 将相似度阈值函数实现,并注册到Spark函数库中
val cos1: UserDefinedFunction = sparkSession.udf.register("getCos",
(v1: String, v2: String, v3: String,
v4: String, v5: String, v6: String,
v7: String, v8: String, v9: String,
v10: String, v11: String, v12: String) => {
// 获取到余弦距离计算组件
val cos: CosineDistance[DoubleVector] = {
CosineDistance.getInstance("zhao-cos")
}
cos.getTrueDistance(
// 构建一个样本向量 这个样本就是参与最稳定的向量样本
// 稍后将以此为中心进行相似度系数计算
Array(1.0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
Array(
v1.toDouble, v2.toInt, v3.toInt,
v4.toInt, v5.toInt, v6.toInt,
v7.toInt, v8.toInt, v9.toInt,
v10.toInt, v11.toInt, v12.toInt
))
}
)
}
- 开始进行向量夹角余弦值的计算
构建好的样本向量a,同时也构建好了用户向量b,现在我们将每一个捐赠者的经历作为向量b,这两个向量之间进行余弦相似度计算,当相似度达到阈值 80% 即为常驻捐赠者,以此进行数值的提取。
/**
* 在这里将挖掘出常驻捐赠者用户
*
* @param sparkSession SparkSql会话
* @param dataFrame Spark 数据框
*/
def IndicatorProcessing(sparkSession: SparkSession, dataFrame: DataFrame): Unit = {
// 将相似度阈值函数实现,并注册到Spark函数库中
val cos1: UserDefinedFunction = sparkSession.udf.register("getCos",
(v1: String, v2: String, v3: String,
v4: String, v5: String, v6: String,
v7: String, v8: String, v9: String,
v10: String, v11: String, v12: String) => {
// 获取到余弦距离计算组件
val cos: CosineDistance[DoubleVector] = CosineDistance.getInstance("zhao-cos")
cos.getTrueDistance(
// 构建一个样本向量
Array(1.0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
Array(
v1.toDouble, v2.toInt, v3.toInt,
v4.toInt, v5.toInt, v6.toInt,
v7.toInt, v8.toInt, v9.toInt,
v10.toInt, v11.toInt, v12.toInt
))
})
// 导入隐式转换库
import sparkSession.implicits._
// 使用DSL编程进行相似度计算与分析挖掘,这里是将后12个字段数据提供给注册好的函数,返回出相似度系数
val frame = dataFrame
.select($"user_id", cos1(
$"c_1995", $"c_1996", $"c_1997",
$"c_1998", $"c_1999", $"c_2000",
$"c_2001", $"c_2002", $"c_2003",
$"c_2004", $"c_2005", $"c_2006"
).as("coefficient"))
// 设置相似阈值为 80%
.where($"coefficient" >= 0.8)
frame.show()
// 查看结果并将结果保存为csv 进行可视化统计
println(f"* >>> 常驻捐赠者有【${frame.count()}】名,id与捐赠评分如下所示")
// 保存为csv TODO 这里由于Windows中针对Hadoop有依赖缺陷,所以直接使用Java IO进行数据输出
frame.repartition(1).foreachPartition((p: Iterator[Row]) => {
// 打开IO流
val stream: BufferedOutputStream = {
new BufferedOutputStream(
new FileOutputStream("C:\\Users\\zhao\\Desktop\\paper\\res\\result.csv")
)
}
p.foreach((data: Row) => {
// 输出数据
stream.write(data.toSeq.mkString("", ",", "\n").getBytes())
})
// 关闭IO流
stream.flush()
stream.close()
})
// 关闭spark
sparkSession.stop()
}
将清理之后的数据传递给定义好的函数,进行常驻捐赠者的挖掘。
- 格式清洗运行日志部分截图,与格式化之后的数据集部分展示
下面的突出字体就是数据清洗之后的数据行数,也是在后期的指标分析中被使用的数据样本,在这里的数据清洗逻辑规则图解如下。
[INFO][org.apache.spark.scheduler.TaskSetManager][22-11-23:10]] : Finished task 0.0 in stage 0.0 (TID 0) in 169 ms on LimingDeskTop (executor driver) (1/2)
[INFO][org.apache.spark.scheduler.TaskSetManager][22-11-23:10]] : Finished task 1.0 in stage 0.0 (TID 1) in 169 ms on LimingDeskTop (executor driver) (2/2)
[INFO][org.apache.spark.scheduler.TaskSchedulerImpl][22-11-23:10]] : Removed TaskSet 0.0, whose tasks have all completed, from pool
[INFO][org.apache.spark.scheduler.DAGScheduler][22-11-23:10]] : ResultStage 0 (count at MAIN.scala:35) finished in 0.217 s
[INFO][org.apache.spark.scheduler.DAGScheduler][22-11-23:10]] : Job 0 is finished. Cancelling potential speculative or zombie tasks for this job
[INFO][org.apache.spark.scheduler.TaskSchedulerImpl][22-11-23:10]] : Killing all running tasks in stage 0: Stage finished
[INFO][org.apache.spark.scheduler.DAGScheduler][22-11-23:10]] : Job 0 finished: count at MAIN.scala:35, took 0.237210 s
* >>> 格式清洗完成!目前数据行数 = 11104
[INFO][org.apache.spark.sql.internal.SharedState][22-11-23:10]] : Setting hive.metastore.warehouse.dir ('null') to the value of spark.sql.warehouse.dir ('file:/C:/Users/zhao/Desktop/paper/DataProcessing/spark-warehouse').
[INFO][org.apache.spark.sql.internal.SharedState][22-11-23:10]] : Warehouse path is 'file:/C:/Users/zhao/Desktop/paper/DataProcessing/spark-warehouse'.
[INFO][org.sparkproject.jetty.server.handler.ContextHandler][22-11-23:10]] : Started o.s.j.s.ServletContextHandler@1b7d869{/SQL,null,AVAILABLE,@Spark}
[INFO][org.sparkproject.jetty.server.handler.ContextHandler][22-11-23:10]] : Started o.s.j.s.ServletContextHandler@47a1e8{/SQL/json,null,AVAILABLE,@Spark}
[INFO][org.sparkproject.jetty.server.handler.ContextHandler][22-11-23:10]] : Started o.s.j.s.ServletContextHandler@539cb1{/SQL/execution,null,AVAILABLE,@Spark}
[INFO][org.sparkproject.jetty.server.handler.ContextHandler][22-11-23:10]] : Started o.s.j.s.ServletContextHandler@12d31af{/SQL/execution/json,null,AVAILABLE,@Spark}
[INFO][org.sparkproject.jetty.server.handler.ContextHandler][22-11-23:10]] : Started o.s.j.s.ServletContextHandler@1f723b0{/static/sql,null,AVAILABLE,@Spark}
[INFO][org.apache.spark.storage.BlockManagerInfo][22-11-23:10]] : Removed broadcast_1_piece0 on LimingDeskTop:60407 in memory (size: 2.6 KiB, free: 5.5 GiB)
下面是展示的在RDD进行数据清洗之后的前20条数据,在代码模型中,为13个字段赋予了不同的名称,并将其注册成为了视图,视图可以被Spark进行SQL查询,图表1中中可以看到,没有列缺失,数据清洗的效果还不错。
图表 1 数据清洗结果部分展示
- 向量余弦值计算之后,常参加该类捐赠的捐赠者列表展示
在进行了数据清洗之后,获取到了,正确无误的捐赠者ID信息数据集(DataSet)然后将其传递给了捐赠者挖掘函数中,在函数内,首先是准备了一个标准向量,越是相似于标准向量,就代表捐赠者参加的情况越是稳定,在函数内,将这一部分稳定常驻捐赠者的信息获取,并进行了数据输出,下面就是计算过程中的部分向量计算日志,左向量为样本向量[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0],右向量是每一个捐赠者的捐赠经历所计算出来的特征向量。
[INFO][OperationAlgorithmManager][22-11-24:11]] : An operation algorithm was obtained:zhao-cos
[INFO][OperationAlgorithmManager][22-11-24:11]] : An operation algorithm was obtained:zhao-cos
[INFO][zhao-cos][22-11-24:11]] : 9.0 / ( DoubleVector{UsePrimitiveType=true, Vector=[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]} * DoubleVector{UsePrimitiveType=true, Vector=[1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0]} )
[INFO][zhao-cos][22-11-24:11]] : 3.0 / ( DoubleVector{UsePrimitiveType=true, Vector=[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]} * DoubleVector{UsePrimitiveType=true, Vector=[1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]} )
[INFO][OperationAlgorithmManager][22-11-24:11]] : An operation algorithm was obtained:zhao-cos
[INFO][OperationAlgorithmManager][22-11-24:11]] : An operation algorithm was obtained:zhao-cos
[INFO][zhao-cos][22-11-24:11]] : 9.0 / ( DoubleVector{UsePrimitiveType=true, Vector=[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]} * DoubleVector{UsePrimitiveType=true, Vector=[1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0]} )
[INFO][zhao-cos][22-11-24:11]] : 1.0 / ( DoubleVector{UsePrimitiveType=true, Vector=[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]} * DoubleVector{UsePrimitiveType=true, Vector=[1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]} )
[INFO][OperationAlgorithmManager][22-11-24:11]] : An operation algorithm was obtained:zhao-cos
[INFO][OperationAlgorithmManager][22-11-24:11]] : An operation algorithm was obtained:zhao-cos
[INFO][zhao-cos][22-11-24:11]] : 9.0 / ( DoubleVector{UsePrimitiveType=true, Vector=[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]} * DoubleVector{UsePrimitiveType=true, Vector=[1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0]} )
[INFO][zhao-cos][22-11-24:11]] : 10.0 / ( DoubleVector{UsePrimitiveType=true, Vector=[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]} * DoubleVector{UsePrimitiveType=true, Vector=[1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0]} )
[INFO][OperationAlgorithmManager][22-11-24:11]] : An operation algorithm was obtained:zhao-cos
[INFO][OperationAlgorithmManager][22-11-24:11]] : An operation algorithm was obtained:zhao-cos
[INFO][zhao-cos][22-11-24:11]] : 12.0 / ( DoubleVector{UsePrimitiveType=true, Vector=[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]} * DoubleVector{UsePrimitiveType=true, Vector=[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]} )
[INFO][zhao-cos][22-11-24:11]] : 10.0 / ( DoubleVector{UsePrimitiveType=true, Vector=[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]} * DoubleVector{UsePrimitiveType=true, Vector=[1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0]} )
[INFO][OperationAlgorithmManager][22-11-24:11]] : An operation algorithm was obtained:zhao-cos
[INFO][OperationAlgorithmManager][22-11-24:11]] : An operation algorithm was obtained:zhao-cos
[INFO][zhao-cos][22-11-24:11]] : 12.0 / ( DoubleVector{UsePrimitiveType=true, Vector=[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]} * DoubleVector{UsePrimitiveType=true, Vector=[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]} )
在计算出结果之后,对数据结果进行了一次show的打印,控制台中展示出来了前20行结果,展示在图表2中,可以看到第二列已经成为了一个阈值,其中与标准向量有100%的相似度的捐赠者也存在,在数据集中,这类捐赠者是全年都在参加的。
图表 2 常驻捐赠着与标准向量阈值计算结果部分展示
在图表3中就是针对计算结果所输出的csv文件中,阈值的统计结果,对阈值进行分组统计,获取到不同阈值的分布情况,能够明显看到,在所有常驻捐赠者中,更多捐赠者的捐赠系数,与标准向量有着80%的相似,100%的捐赠者分布较为稀疏。
图表 3 参与程度系数分布情况
图表 4 常驻捐赠者人数与占比
结论
在图表4中展示的是在本次统计的样本中,常驻捐赠者,与非常驻捐赠者之间的占比,通过捐赠经历做构造的特征向量计算出来的常驻捐赠者(与全年参与特征向量相似度系数达到阈值80%以上)的人数占比较少,但参与捐赠人数较多,而在常驻捐赠者这一部分里,由图表3可看出,更多人员是偏向于[0.8, 0.85]的阈值区间,说明在系数在0.85之上捐赠者占少数,应适当对这类捐赠者进行关注于宣传,呼吁更多有志有足够基金的人士加入到常驻捐赠者中。