泰迪智能科技(数据挖掘平台:TipDM数据挖掘平台)最新推出的数据挖掘实战专栏
专栏将数据挖掘理论与项目案例实践相结合,可以让大家获得真实的数据挖掘学习与实践环境,更快、更好的学习数据挖掘知识与积累职业经验
专栏中每四篇文章为一个完整的数据挖掘案例。案例介绍顺序为:先由数据案例背景提出挖掘目标,再阐述分析方法与过程,最后完成模型构建,在介绍建模过程中同时穿插操作训练,把相关的知识点嵌入相应的操作过程中。
为方便读者轻松地获取一个真实的实验环境,本专栏使用大家熟知的Python语言对样本数据进行处理以进行挖掘建模。
————————————————
数据预处理
1. 属性规约
由于热水器采集的用水数据属性较多,本案例做以下处理。因分析的主要对象为用户,分析的主要目标为用户的洗浴行为的一般规律,所以“热水器编号”属性可以去除;因热水器采集的数据中,“有无水流”属性可以通过“水流量”属性反应,“节能模式”属性取值相同均为“关”,对分析无作用,可以去除。
删除冗余属性“热水器编号”“有无水流”“节能模式”,如代码清单1所示。
代码清单1 删除冗余属性
import pandas as pd
import numpy as np
data = pd.read_excel('../data/original_data.xls')
print('初始状态的数据形状为:', data.shape)
# 删除热水器编号、有无水流、节能模式属性
data.drop(labels=["热水器编号","有无水流","节能模式"],axis=1,inplace=True)
print('删除冗余属性后的数据形状为:', data.shape)
data.to_csv('../tmp/water_heart.csv',index=False)
删除冗余属性后得到用来建模的属性如表1所示。
表1 删除冗余属性后部分数据列表
2. 划分用水事件
用户的用水数据存储在数据库中,记录了各种各样的用水事件,包括洗浴,洗手,刷牙,洗脸,洗衣,洗菜等,而且一次用水事件由数条甚至数千条的状态记录组成。所以本案例首先需要在大量的状态记录中划分出哪些连续的数据是一次完整的用水事件。
用水状态记录中,水流量不为0表明用户正在使用热水;而水流量为0时用户用热水发生停顿或者用热水结束。对于任一个用水记录,如果它的向前时差超过阈值,则将它记为事件的开始编号;如果向后时差超过阈值,则将其记为事件的结束编号。划分模型的符号说明如表2。
表2 一次完整用水事件模型构建符号说明表
一次完整用水事件的划分步骤如下。
(1) 读取数据记录,识别到所有水流量不为0的状态记录,将它们的发生时间记为序列t1。
(2) 对序列t1构建其向前时差列和向后时差列,并分别与阈值进行比较。向前时差超过阈值T,则将它记为新的用水事件的开始编号;如果向后时差超过阈值T,则将其记为用水事件的结束编号。
循环执行步骤
(2)直到向前时差列和向后时差列与均值比较完毕,结束事件划分。
用水事件划分主要分为两个步骤,即确定单次用水时长间隔和计算两条相邻记录的时间,实现代码如代码清单2所示。
代码清单2 划分用水事件
import pandas as pd
import numpy as np
# 读取数据
data = pd.read_csv('../tmp/water_heart.csv')
# 划分用水事件
threshold = pd.Timedelta('4 min') # 阈值为4分钟
data['发生时间'] = pd.to_datetime(data['发生时间'], format = '%Y%m%d%H%M%S') # 转换时间格式
data = data[data['水流量'] > 0] # 只要流量大于0的记录
sjKs = data['发生时间'].diff() > threshold # 相邻时间向前差分,比较是否大于阈值
sjKs.iloc[0] = True # 令第一个时间为第一个用水事件的开始事件
sjJs = sjKs.iloc[1:] # 向后差分的结果
sjJs = pd.concat([sjJs,pd.Series(True)]) # 令最后一个时间作为最后一个用水事件的结束时间
# 创建数据框,并定义用水事件序列
sj = pd.DataFrame(np.arange(1,sum(sjKs)+1),columns = ["事件序号"])
sj["事件起始编号"] = data.index[sjKs == 1]+1 # 定义用水事件的起始编号
sj["事件终止编号"] = data.index[sjJs == 1]+1 # 定义用水事件的终止编号
print('当阈值为4分钟的时候事件数目为:',sj.shape[0])
sj.to_csv('../tmp/sj.csv',index = False)
对用户的用水数据进行划分结果如表3所示。
表3 用水数据划分结果
3. 确定单次用水事件时长阈值
对某热水器用户的数据根据不同的阈值划分用水事件,得到了相应的事件个数,阈值变化与划分得到事件个数如表4所示,阈值与划分事件个数关系如图1所示。
表4 某热水器用户家庭某时间段不同用水时间间隔
阈值事件划分个数
图1 阈值与划分事件个数关系
图1为阈值与划分事件个数的散点图,图中某段阈值范围内,下降趋势明显,说明在该段阈值范围内,用户的停顿习惯比较集中。如果趋势比较平缓,则说明用户的停顿热水的习惯趋于稳定,所以取该段时间开始的时间点作为阈值,既不会将短的用水事件合并,又不会将长的用水事件拆开。在图1中,用户停顿热水的习惯在方框的位置趋于稳定,说明热水器用户的用水的停顿习惯用方框开始的时间点作为划分阈值会有一个好的效果。
曲线在图1中方框趋于稳定时,其方框开始的点的斜率趋于一个较小的值。为了用程序来识别这一特征,将这一特征提取为规则。根据图2说明如何识别上图中的方框中起始的时间。
图2 斜率计算图
每个阈值对应一个点,给每个阈值计算得到一个斜率指标,如图2中所示。其中,点是要计算的斜率指标点。为了直观的展示,用表5所示的符号来进行说明。
表5 阈值寻优模型符号说明
根据第一个公式,计算出
四个斜率。于是可以根据第二个公式计算出四个斜率之和的平均值K。
将K作为A点的斜率指标,特别指出横坐标上的最后四个点没有斜率指标,因为找不出在它以后的4个更长的阈值。但这不影响对最优阈值的寻找,因为可以提高阈值的上限,以使最后的4个阈值不是考虑范围内的阈值。
先统计出各个阈值下的用水事件的个数,再通过阈值寻优的方式找出最优的阈值,具体实现方式如代码清单3所示。
代码清单3 确定单次用水事件时长阈值
# 确定单次用水事件时长阈值
n = 4 # 使用以后四个点的平均斜率
threshold = pd.Timedelta(minutes=5) # 专家阈值
data['发生时间'] = pd.to_datetime(data['发生时间'], format='%Y%m%d%H%M%S')
data = data[data['水流量'] > 0] # 只要流量大于0的记录
# 自定义函数:输入划分时间的时间阈值,得到划分的事件数
def event_num(ts):
d = data['发生时间'].diff() > ts # 相邻时间作差分,比较是否大于阈值
return d.sum() + 1 # 这样直接返回事件数
dt = [pd.Timedelta(minutes=i) for i in np.arange(1, 9, 0.25)]
h = pd.DataFrame(dt, columns=['阈值']) # 转换数据框,定义阈值列
h['事件数'] = h['阈值'].apply(event_num) # 计算每个阈值对应的事件数
h['斜率'] = h['事件数'].diff()/0.25 # 计算每两个相邻点对应的斜率
h['斜率指标']= h['斜率'].abs().rolling(4).mean() # 往前取n个斜率绝对值平均作为斜率指标
ts = h['阈值'][h['斜率指标'].idxmin() - n]
# 用idxmin返回最小值的Index,由于rolling_mean()计算的是前n个斜率的绝对值平均
# 所以结果要进行平移(-n)
if ts > threshold:
ts = pd.Timedelta(minutes=4)
print('计算出的单次用水时长的阈值为:',ts)
得到阈值优化的结果如下。
(1) 当存一个阈值的斜率指标时,则取阈值最小的点A(可能存在多个阈值的斜率指标小于1)的横坐标作为用水事件划分的阈值,其中中的“1”是经过实际数据验证的一个专家阈值。
(2) 当不存在时,则找所有阈值中斜率指标最小的阈值;如果该阈值的斜率指标小于5,则取该阈值作为用水事件划分的阈值;如果该阈值的斜率指标不小于5,则阈值取默认值的阈值4分钟。其中斜率指标小于5中的5是经过实际数据验证的一个专家阈值。
4. 属性构造
(1) 构建用水时长与频率属性
不同用水事件的用水时长是基础属性之一。例如单次洗漱事件一般总时长在5分钟左右,而一次手洗衣物事件的时长则根据衣物多少而不同。根据用水时长这一属性可以构建如表 109所示的事件开始时间、事件结束时间、洗浴时间点、用水时长、总用水时长和用水时长、总用水时长这6个属性。
表6 主要用水时长类属性构建说明
表6构建用水开始时间或结束的时间两个特征时分别减去或加上了发送阈值(发送阈值是指热水器传输数据的频率的大小)。其原因以图1为例。在20:00:10时热水器记录到的数据还没有用水,而在20:00:12时热水器记录的有用水行为。所以用水开始时间在20:00:10~20:00:12之间,考虑到网络不稳定导致网络数据传输延时数分钟或数小时之久等因素,取平均值会导致很大的偏差,综合分析构建“用水开始时间”为起始数据的时间减去“发送阈值”的一半。
用水时长相关的属性只能够区分出一部分用水事件,不同用水事件的用水停顿和频率也不同。例如一次完整洗漱事件的停顿次数不多,停顿的时间长短不一,平均停顿时长较短,一次手洗衣物事件的停顿次数较多,停顿时间相差不大,平均停顿时长一般。根据这一属性,可以构建如表 1010所示的停顿时长,总停顿时长,平均停顿时长,停顿次数四个属性。
表7 主要用水频率类属性构建说明
构建用水时长与用水频率属性,如代码清单4所示。
代码清单4 构建用水时长与用水频率属性
data = pd.read_excel ('../data/water_hearter.xlsx',encoding='gbk') # 读取热水器使用数据记录
sj = pd.read_csv('../tmp/sj.csv') # 读取用水事件记录
# 转换时间格式
data["发生时间"] = pd.to_datetime(data["发生时间"],format="%Y%m%d%H%M%S")
# 构造属性:总用水时长
timeDel = pd.Timedelta("1 sec")
sj["事件开始时间"] = data.iloc[sj["事件起始编号"]-1,0].values- timeDel
sj["事件结束时间"] = data.iloc[sj["事件终止编号"]-1,0].values + timeDel
sj['洗浴时间点'] = [i.hour for i in sj["事件开始时间"]]
sj["总用水时长"] = np.int64(sj["事件结束时间"] - sj["事件开始时间"] +)/1000000000 + 1
# 构造用水停顿事件
# 构造属性“停顿开始时间”、“停顿结束时间”
# 停顿开始时间指从有水流到无水流,停顿结束时间指从无水流到有水流
for i in range(len(data)-1):
if (data.loc[i,"水流量"] != 0) & (data.loc[i + 1,"水流量"] == 0) :
data.loc[i + 1,"停顿开始时间"] = data.loc[i +1, "发生时间"] - timeDel
if (data.loc[i,"水流量"] == 0) & (data.loc[i + 1,"水流量"] != 0) :
data.loc[i,"停顿结束时间"] = data.loc[i , "发生时间"] + timeDel
# 提取停顿开始时间与结束时间所对应行号,放在数据框Stop中
indStopStart = data.index[data["停顿开始时间"].notnull()]+1
indStopEnd = data.index[data["停顿结束时间"].notnull()]+1
Stop = pd.DataFrame(data={"停顿开始编号":indStopStart[:-1],
"停顿结束编号":indStopEnd[1:]})
# 计算停顿时长,并放在数据框stop中,停顿时长=停顿结束时间-停顿结束时间
Stop["停顿时长"] = np.int64(data.loc[indStopEnd[1:]-1,"停顿结束时间"].values-
data.loc[indStopStart[:-1]-1,"停顿开始时间"].values)/1000000000
# 将每次停顿与事件匹配,停顿的开始时间要大于事件的开始时间,
# 且停顿的结束时间要小于事件的结束时间
for i in range(len(sj)):
Stop.loc[(Stop["停顿开始编号"] > sj.loc[i,"事件起始编号"]) &
(Stop["停顿结束编号"] < sj.loc[i,"事件终止编号"]),"停顿归属事件"]=i+1
# 删除停顿次数为0的事件
Stop = Stop[Stop["停顿归属事件"].notnull()]
# 构造属性 用水事件停顿总时长、停顿次数、停顿平均时长、
# 用水时长,用水/总时长
stopAgg = Stop.groupby("停顿归属事件").agg({"停顿时长":sum,"停顿开始编号":len})
sj.loc[stopAgg.index - 1,"总停顿时长"] = stopAgg.loc[:,"停顿时长"].values
sj.loc[stopAgg.index-1,"停顿次数"] = stopAgg.loc[:,"停顿开始编号"].values
sj.fillna(0,inplace=True) # 对缺失值用0插补
stopNo0 = sj["停顿次数"] != 0 # 判断用水事件是否存在停顿
sj.loc[stopNo0,"平均停顿时长"] = sj.loc[stopNo0,"总停顿时长"]/sj.loc[stopNo0,"停顿次数"]
sj.fillna(0,inplace=True) # 对缺失值用0插补
sj["用水时长"] = sj["总用水时长"] - sj["总停顿时长"] # 定义属性用水时长
sj["用水/总时长"] = sj["用水时长"] / sj["总用水时长"] # 定义属性 用水/总时长
print('用水事件用水时长与频率属性构造完成后数据的属性为:\n',sj.columns)
print('用水事件用水时长与频率属性构造完成后数据的前5行5列属性为:\n',
sj.iloc[:5,:5])
(2) 构建用水量与波动属性
除了用水时长,停顿和频率外,用水量也是识别该事件是否为洗浴事件的重要属性。例如用水时间中的洗漱事件相比洗浴事件有停顿次数多,用水总量少,平均用水少的特点。手洗衣物事件相比于洗浴事件则有停顿次数多,用水总量多,平均用水量多的特点。根据这一原因可以构建出表8所示的两个用水量属性。
表8 用水量化属性构建说明
同时用水波动也是区分不同用水事件的关键。一般在一次洗漱事件中刷牙和洗脸的用水量完全不同,在一次手洗衣物事件中每次用水的量和停顿时间相差却都不大。根据不同用水事件的这一特征可以构建表9所示的水流量波动和停顿时长波动两个属性。
表9 用水波动属性构建说明
在用水时长和频率属性的基础之上构建用水量和用水波动属性,需要充分利用用水时长和频率属性,如代码清单5所示。
代码清单5 构建用水量和用水波动属性data["水流量"] = data["水流量"] / 60 # 原单位L/min,现转换为L/sec
sj["总用水量"] = 0 # 给总用水量赋一个初始值0
for i in range(len(sj)):
Start = sj.loc[i,"事件起始编号"]-1
End = sj.loc[i,"事件终止编号"]-1
if Start != End:
for j in range(Start,End):
if data.loc[j,"水流量"] != 0:
sj.loc[i,"总用水量"] = (data.loc[j + 1,"发生时间"] -
data.loc[j,"发生时间"]).seconds* \
data.loc[j,"水流量"] + sj.loc[i,"总用水量"]
sj.loc[i,"总用水量"] = sj.loc[i,"总用水量"] + data.loc[End,"水流量"] * 2
else:
sj.loc[i,"总用水量"] = data.loc[Start,"水流量"] * 2
sj["平均水流量"] = sj["总用水量"] / sj["用水时长"] # 定义属性 平均水流量
# 构造属性:水流量波动
# 水流量波动=∑(((单次水流的值-平均水流量)^2)*持续时间)/用水时长
sj["水流量波动"] = 0 # 给水流量波动赋一个初始值0
for i in range(len(sj)):
Start = sj.loc[i,"事件起始编号"] - 1
End = sj.loc[i,"事件终止编号"] - 1
for j in range(Start,End + 1):
if data.loc[j,"水流量"] != 0:
slbd = (data.loc[j,"水流量"] - sj.loc[i,"平均水流量"])**2
slsj = (data.loc[j + 1,"发生时间"] - data.loc[j,"发生时间"]).seconds
sj.loc[i,"水流量波动"] = slbd * slsj + sj.loc[i,"水流量波动"]
sj.loc[i,"水流量波动"] = sj.loc[i,"水流量波动"] / sj.loc[i,"用水时长"]
# 构造属性:停顿时长波动
# 停顿时长波动=∑(((单次停顿时长-平均停顿时长)^2)*持续时间)/总停顿时长
sj["停顿时长波动"] = 0 # 给停顿时长波动赋一个初始值0
for i in range(len(sj)):
if sj.loc[i,"停顿次数"] > 1: # 当停顿次数为0或1时,停顿时长波动值为0,故排除
for j in Stop.loc[Stop["停顿归属事件"] == (i+1),"停顿时长"].values:
sj.loc[i,"停顿时长波动"] = ((j - sj.loc[i,"平均停顿时长"])**2) * j + \
sj.loc[i,"停顿时长波动"]
sj.loc[i,"停顿时长波动"] = sj.loc[i,"停顿时长波动"] / sj.loc[i,"总停顿时长"]
print('用水量和波动属性构造完成后数据的属性为:\n',sj.columns)
print('用水量和波动属性构造完成后数据的前5行5列属性为:\n',sj.iloc[:5,:5])
5. 筛选候选洗浴事件
洗浴事件的识别是建立在一次用水事件识别的基础上,也就是从已经划分好的一次用水事件中识别出哪些一次用水事件是洗浴事件。
可以使用3个比较宽松的条件筛选掉那些非常短暂的用水事件,确定不可能为洗浴事件的数据去除掉,剩余的事件称为“候选洗浴事件”。这三个条件是“或”的关系,也就是说,只要一次完整的用水事件满足任意一个条件,就被判定为短暂用水事件,即会被筛选掉。3个筛选条件如下。
(1) 一次用水事件中总用水量小于5升。
(2) 用水时长小于100秒。
(3) 总用水时长小于120秒。
基于构建的用水时长、用水量属性,筛选候选洗浴事件,如代码清单6所示。
代码清单6 筛选候选洗浴事件
sj_bool = (sj['用水时长'] >100) & (sj['总用水时长'] > 120) & (sj['总用水量'] > 5)
sj_final = sj.loc[sj_bool,:]
sj_final.to_excel('../tmp/sj_final.xlsx',index=False)
print('筛选出候选洗浴事件前的数据形状为:',sj.shape)
print('筛选出候选洗浴事件后的数据形状为:',sj_final.shape)
筛选前用水事件数目总共172个,经过筛选后,余下75个用水事件。结合日志,最终用于建模的属性的总数为11个,其基本状况如表10所示。
表10 属性基本状况