第十章 家用电器用户行为分析与事件识别
10.1 背景与挖掘目标
在热水器用户行为分析过程中,用水事件识别是最关键的环节。由于用户不仅仅使用热水器来洗浴,还可能包括洗手、洗脸、刷牙、洗菜、做饭等用水行为,所以热水器采集到的数据来自各种不同的用水事件。本案例基于热水器采集的时间序列数据,将顺序排列的离散的用水时间节点根据水流量和停顿时间间隔划分为不同大小的时间区间,每个区间是一个可理解的一次完整用水事件,并以热水器一次完整用水事件为一个基本事件,将时间序列数据划分为独立的用水事件并识别出其中属于洗浴的事件。
10.2 分析方法与过程
热水器用户用水事件划分与识别步骤:
1)对热水用户的历史用水数据进行选择性抽取,构建专家样本。
2)对步骤1)形成的数据集进行数据探索分析与预处理,包括探索用水事件时间间隔的分布、规约冗余属性、识别用水数据的缺失值,并对缺失值进行处理,根据建模的需要进行属性构造等。根据以上处理,对用水样本数据建立用水事件时间间隔识别模型和划分一次完整的用水事件模型,再在一次完整用水事件划分结果的基础上,剔除短暂用水事件,缩小识别范围。
3)在步骤2)得到的建模样本数据基础上,建立洗浴事件识别模型,对洗浴事件识别模型进行模型分析评价。
4)对步骤3)形成的模型结果应用并对洗浴事件划分进行优化。
5)调用洗浴事件识别模型,对实时监控的热水器流水数据进行洗浴事件自动识别。
10.2.1 数据抽取
本案例对原始数据采用无放回随机抽样法抽取200家热水器用户从2014年1月1日至2014年12月31日的用水记录作为原始建模数据。
热水器采集的用水数据包含以下12个属性:热水器编码、发生时间、开关机状态、加热中、保温中、有无水流、实际温度、热水量、水流量、节能模式、加热剩余时间、当前设置温度。
10.2.2 数据探索分析
用水停顿时间间隔为一条水流量不为0的流水记录同下一条水流量不为0的流水记录之间的时间间隔。对用水停顿时间间隔作频率分布直方图。
由表可知,停顿时间间隔为0~0.3分钟的频率很高,判断其为一次用水时间中的停顿;停顿时间间隔为6 ~ 13分钟的频率较低,分析其为两次用水事件之间的停顿间隔。
10.2.3 数据预处理
1.数据规约
属性规约:对反映行为无关和冗余的属性进行去除,由此去除热水器编号、有无水流、节能模式。
数值规约:当热水器开关机状态为关且水流量为0时,数据记录可以规约掉。
import pandas as pd
data = pd.read_excel('./demo/data/original_data.xls')
#删除热水器编号、有无水流、节能模式的属性
data.pop('热水器编号')
data.pop('有无水流')
data.pop('节能模式')
delete = data[(data['开关机状态'] == '关') & (data['水流量'] == 0)].index # 开关机状态为关且水流量为0的数据的行索引
# print(delete)
data = data.drop(delete) # 删除行索引
print(data)
data.to_excel('./demo/data/waterheater.xlsx', index=False)
2.数据变换
(1)一次完整用水事件的划分模型
在用水状态记录中,水流量不为0表明用户正在使用热水;而水流量为0时用户用热水发生停顿或者用热水结束。如果水流量为0的状态记录之间的时间间隔超过一个阈值T,则从该段水流量为0的状态记录向前找到最后一条水流量不为0的用水记录作为上一次用水事件的结束;向后找到水流量不为0的状态记录作为下一个用水事件的开始。
划分模型的符号:
名称 | 符号 |
---|---|
状态记录i | Ri i∈{1,2,…,n} |
时间间隔阈值 | T |
Ri+1 与Ri之间的时间间隔 | gapi i∈{1,2,…,n} |
一次完整用水事件的划分步骤如下:
1)读取数据记录,识别到第一条水流量不为0的数据记录记为R1,按顺序识别接下来的一条水流量不为0数据记录为R2。
2)若gapi>T,则Ri+1与Ri及之间的数据记录不能划分到同一次用水事件。同时将Ri+1记录作为新的读取数据记录的开始,返回步骤1);若gapi<T,则Ri+1与Ri及之间的数据记录划分到同一次用水事件,并记录接下来的水流量不为0的数据记录为Ri+2。
3)循环执行步骤2),直到数据记录读取完毕,结束事件划分。
import pandas as pd
threshold = pd.Timedelta(minutes=4) # 设置阈值为4分钟
inputfile = './demo/data/waterheater.xlsx'
outputfile = './demo/tmp/dividsequence.xlsx'
data = pd.read_excel(inputfile)
data['发生时间'] = pd.to_datetime(data['发生时间'], format='%Y%m%d%H%M%S')
data = data[data['水流量'] > 0] # 选取流量大于0的记录
d = data['发生时间'].diff() > threshold # 相邻时间做差分,比较是否大于阈值
data['事件编号'] = d.cumsum() + 1 #通过累计求和的方式为事件编号,即True为1,False为0,当大于阈值时可以看作两次完整用水事件,事件编号在原值上加1;当小于阈值时看作一次完整用水事件,事件编号不变。
data.to_excel(outputfile)
划分结果:
(2)用水事件阈值寻优模式
考虑到不同的时间段内要更新阈值,本案例建立了阈值寻优模型来更新寻找最优的阈值,解决因时间变化和地域不同导致阈值存在差异的问题。
如果下降趋势明显,说明在这段阈值范围内,用户的停顿习惯比较集中。如果趋势比较平缓,则说明用户的停顿习惯趋于稳定,所以取该段时间开始作为阈值,既不会将短的用水事件合并,又不会将长的用水事件拆开。由于其方框开始的点的斜率趋于一个较小的值,为了提取这一特征,从相邻两个点之间斜率值的大小说明程序是如何识别图中方框的起始时间。
设A点是要计算斜率指标的点,阈值寻优模型符号说明如下:
符号 | 说明 |
---|---|
ks | 相邻两点的斜率的绝对值s∈{AB,BC,CD,DE} |
K | 任意两点(x1,y1),(x2,y2)的斜率的绝对值 |
K | 5个点的斜率之和的平均值 |
(xi,yi) | i点的坐标i∈{A,B,C,D,E} |
k
=
∣
y
1
−
y
2
∣
∣
x
1
−
x
2
∣
k=\frac{|y_{1}-y_{2}|}{|x_{1}-x_{2}|}
k=∣x1−x2∣∣y1−y2∣
根据上式计算出kAB、kBC、kCD、kDE四个斜率,再计算其平均值:K=(kAB+kBC+kCD+kDE)/4。
将K作为A点的斜率指标,特别指出横坐标上最后4个点没有斜率指标,因为找不出在它以后的4个更长的阈值。
因此阈值优化结果如下:
当存在一个阈值的斜率指标K<1时,则取阈值最小的点A(可能存在多个阈值的斜率指标小于1)的横坐标xA作为用水事件划分的阈值,其中K<1中的1是经过实际数据验证的一个专家阈值。当不存在K<1时,则找所有阈值中斜率指标最小的阈值;如果该阈值的斜率指标小于5,则取该阈值作为用水事件划分的阈值;如果不小于5,则阈值取默认值的阈值为4分钟。其中斜率指标小于5也是经验值。
import numpy as np
import pandas as pd
inputfile = './demo/data/waterheater.xlsx'
n = 4 # 使用以后四个点的平均斜率
threshold = pd.Timedelta(minutes=5) # 专家阈值
data = pd.read_excel(inputfile)
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)] # 寻优区间在1~9分钟
h = pd.DataFrame(dt, columns=['阈值']) # 定义阈值列
h['事件数'] = h['阈值'].apply(event_num) # 计算每个阈值对应的事件数,apply函数对每个阈值计算一次事件数
h['斜率'] = h['事件数'].diff()/0.25 # 计算每两个相邻点对应的斜率,即相邻两个点的事件数差除上阈值间隔0.25
h['斜率指标'] = (h['斜率'].abs()).rolling(n).mean() # 采用后n个斜率的绝对值平均作为斜率指标
ts = h['阈值'][h['斜率指标'].idxmin()-n]
# 用idxmin()返回最小值的index,由于rolling.mean()自动计算的是前n个斜率的绝对值平均,所以结果要进行平移(-n)
if ts > threshold:
ts = pd.Timedelta(minutes=4)
print(ts)
得到最优阈值为4分钟。
(3)属性构造
本案例研究的是用水行为,可构造4类指标:时长指标、频率指标、用水的量化指标以及用水的波动指标。
属性指标 | 含义 |
---|---|
时长指标 | 用水开始时间、用水结束时间、总用水时长、停顿时长、总停顿时长、用水时长、平均停顿时长、用水时长/总用水时长 |
频率指标 | 停顿次数 |
用水量化指标 | 总用水量、平均水流量 |
用水波动指标 | 水流量波动、停顿时长波动 |
在一次用水事件中:
根据用水数据得到属性构造说明图:
指标构建说明:
(4)筛选得“候选洗浴事件”
从已经划分好的一次用水事件中识别出哪些一次用水事件是洗浴事件。首先用三个比较宽松的条件筛选短暂用水事件:
1)一次用水事件中总用水量(纯热水)小于y升。
2)用水时长小于100秒。
3)总用水时长小于120秒。
对y的合理取值进行探究。由实验分析,热水器设定温度为50℃时,一次普通的洗浴时长为15分钟,总用水时长10分钟左右,热水的使用量为10~15升。
为了避免其他影响,把条件放宽到一次洗浴的总热水使用量为5升,同时取洗浴温度的均值为39℃来计算热水器不同设定温度下的热水使用量阈值。
热水使用量模型变量符号说明:
说明 | 符号 |
---|---|
洗浴用水温度 | T(39℃) |
自来水水温 | C(℃) |
自来水注入量 | M(升) |
设定温度 | X(℃) |
设定温度为X时的用水量 | Y(升) |
50℃时的用水量 | V(5升) |
假设两次洗浴事件热水和冷水混合后的花洒出水水温度恒为T摄氏度,总用水量不变且为M+V升,根据热量守恒建立方程组。
(
50
−
T
)
∗
V
+
(
C
−
T
)
∗
M
=
0
(
X
−
T
)
∗
Y
+
(
C
−
T
)
∗
(
M
+
V
−
Y
)
=
0
(50-T)*V+(C-T)*M=0 \\ (X-T)*Y+(C-T)*(M+V-Y)=0
(50−T)∗V+(C−T)∗M=0(X−T)∗Y+(C−T)∗(M+V−Y)=0
其中1式是50℃的热水V升与M升C℃自来水混合得到M+V升T摄氏度的洗浴用水的热守恒公式。2式是X℃的热水Y升与M+V-Y升C℃自来水混合得到M+V升T℃的洗浴用水的热守恒公式。得到如下公式:
Y
=
(
50
−
C
)
∗
V
X
−
C
Y=\frac{(50-C)*V}{X-C}
Y=X−C(50−C)∗V
其中V是热水器的水恒为50℃时洗浴时的最低用水量,自来水每月平均温度取平均室温。由此可以计算用水事件在不同实际用水温度下的标准热水使用量。
3.数据清洗
在划分一次完整用水事件时,数据中存在没有结束用水的状态记录情况:第五条状态记录的水流量为20,但第六条状态记录与第五条记录间隔了1小时27分28秒,时间间隔应该为2秒。这可能是由于网络故障等原因导致状态记录时间间隔为几十分钟甚至几个小时。
因此做如下处理:在存在用水状态记录缺失的情况下,填充一条状态记录使水流量为0,发生时间加2秒,其余属性状态不变。
10.2.4 模型构建
本案例建立多层神经网络模型识别洗浴事件。选取了“候选洗浴事件”的11个属性作为网络的输入,分别为:洗浴时间点、总用水时长、总停顿时长、平均停顿时长、停顿次数、用水时长、用水时长/总用水时长、总用水量、平均水流量、水流量波动和停顿时长波动。输出信号为1与0,其中1代表该次事件为洗浴事件,0表示该次事件不是洗浴事件。
训练得出,两个隐含层的神经网络训练效果较好,其中隐节点分别为17,10。
import pandas as pd
inputfile1 = './demo/data/train_neural_network_data.xls' # 训练数据
inputfile2 = './demo/data/test_neural_network_data.xls' # 测试数据
testoutputfile = './demo/tmp/test_output_data.xls' # 测试数据模型输出文件
data_train = pd.read_excel(inputfile1)
data_test = pd.read_excel(inputfile2)
y_train = data_train.iloc[:, 4].values # 训练样本标签列
x_train = data_train.iloc[:, 5:17].values # 训练样本特征
y_test = data_test.iloc[:, 4].values # 测试样本标签列
x_test = data_test.iloc[:, 5:17].values # 测试样本特征
import tensorflow as tf
from tensorflow.keras import datasets, layers, optimizers, Sequential, metrics, callbacks
model = Sequential([layers.Dense(20, activation='relu', input_shape=(11,)),
layers.BatchNormalization(),
layers.Dropout(0.2),
layers.Dense(10, activation='relu'),
layers.BatchNormalization(),
layers.Dropout(0.2),
layers.Dense(1, activation='sigmoid')])
model.summary()
model.compile(optimizer=optimizers.Adam(lr=0.001),
loss=tf.losses.binary_crossentropy,
metrics=['acc'])
model.fit(x_train, y_train, epochs=1000, batch_size=10) # 训练模型,nb_epoch改为epochs, batch_size控制每次训练几个样本,值太小会导致精度低
model.save_weights('./demo/tmp/net.model') # 保存模型参数
test_scores = model.evaluate(x_test, y_test)
print('test loss:', test_scores[0])
print('test accuracy:', test_scores[1])
r = pd.DataFrame(model.predict_classes(x_test), columns=['预测结果'])
pd.concat([data_test.iloc[:, :5], r], axis=1).to_excel(testoutputfile)
from cm_plot import *
cm_plot(y_test, model.predict_classes(x_test)).show()
这里的代码参考了家用电器用户行为分析与事件识别
模型代价与精度:
test loss: 0.47013187408447266
test accuracy: 0.8571428656578064
混淆矩阵:
训练样本过少,可能造成了模型训练不准确。