python数据分析与挖掘实战---chapter10家用电器用户行为分析与事件识别
1. 项目背景与挖掘目标
1.1 背景
居民在使用家用电器过程中,会因地区气候、不同区域、用户年龄性别差异,形成不同的使用习惯。家电企业若能深入了解不同用户群的使用习惯,开发新功能,就能开拓新市场。
要了解用户使用家用电器的习惯,必须采集用户使用电器的相关数据下面以热水器为例,分析用户的使用行为。在热水器用户行为分析过程中,用水事件识别是最关键的环节。比如,国内某热水器生产厂商新研发的一种高端智能热水器,在状态发生改变或者有水流状态时,会采集各监控指标数据。该厂商根据其采集的用户的用水数据,分析用户的用水行为特征。由于用户不仅仅使用热水器来洗浴,还可能包括洗手、洗脸、刷牙、洗菜、做饭等用水行为,所以热水器采集到的数据来自各种不同的用水事件。
1.2 目标
- 根据热水器采集到的数据,划分一次完整用水事件。
- 在划分好的一次完整用水事件中,识别出洗浴事件。
2. 数据预处理
热水器用户用水事件划分与识别包括以下步骤。
- 对热水用户的历史用水数据进行选择性抽取,构建专家样本。
- 对步骤1 形成的数据集进行数据探索分析与预处理,包括探索用水事件时间间隔的分布、规约冗余属性、识别用水数据的缺失值,并对缺失值进行处理,根据建模的需要进行属性构造等。根据以上处理,对用水样本数据建立用水事件时间间隔识别模型和划分一次完整的用水事件模型,再在一次完整用水事件划分结果的基础上,剔除短暂用水事件,缩小识别范围。
2.1 数据清洗
此案例中存在用水数据状态记录缺失的情况。在热水器工作态改变或处于用水阶段时,热水器每2秒(发送阈值)传输一条状态记录,而划分一次完整用水事件时,需要一个开始用水的状态记录和结束用水的状态记录。但是,在划分一次完整用水事件时,发现数据中存在没有结束用水的状态记录情况。如下图所示,热水器状态发生改变,第5条状态记录和第7条状态记录的时间间隔应该为2秒,而表中两条记录间隔为1小时27分28秒。
这可能是由于网络故障等原因导致状态记录时间间隔为几十分钟甚至几小时的情况,该类问题若用均值去填充会造成用水时间为几十分钟甚至几小时的误差。对于上述特殊情况,书中进行如下处理:在存在用水状态记录缺失的情况下,填充一条状态记录使水流量为0,发生时间加2秒,其余属性状态不变。
这同样存在偏差,即上表中第5条状态记录和第7条状态记录的1小时27分28秒间隔中,也许是连续用水半个小时才停止,此时插入的第6条状态记录只加了2秒。
对于书中的填充的思路:根据一次用水事件的结束时间和结束时间下一条记录的插值判断是否需要填充,创建一个新数据框用于填充(注意长度),用concat方法在指定位置填充,把发生时间加2秒
2.2 数据规约
由于热水器采集的用水数据属性较多,本案例对建模数据做以下数据规约。
- 属性规约:因为要对热水器用户的洗浴行为的一般规律进行挖掘分析,所以“热水器编号”可以去除;因热水器采集的数据中,“有无水流”可以通过“水流量”反映出来,“节能模式”数据都只为“关”,对建模无作用,可以去除。
- 数值规约:当热水器水流量为0时,说明热水器不处于工作状态,数据记录可以规约掉。
import pandas as pd
import numpy as np
data = pd.read_excel('./chapter10/demo/data/original_data.xls')
data = data.drop(['热水器编号', '有无水流', '节能模式'], axis = 1)
df = data[data['水流量'] > 0] # 只要流量大于0的记录
2.3 数据变换
2.3.1 划分一次用水事件
用户的用水数据存储在数据库中,记录了各种各样的用水事件,包括洗浴、洗手、刷牙、洗脸、洗衣和洗菜等,而且一次用水事件由数条甚至数千条的状态记录组成。所以,本案例首先需要在大量的状态记录中划分出哪些连续的数据是一次完整的用水事件。
一次完整用水事件的划分步骤如下。
- 读取数据记录,识别到第一条水流量不为0的数据记录记为R₁,按顺序识别接下来的一条水流量不为0数据记录为R₂。
- 若gapi > T,则Ri+1,与Ri,及之间的数据记录不能划分到同一次用水事件。同时将R;,记录作为新的读取数据记录的开始,返回步骤1 ;若gapi < T,则将Ri+1与Ri之间数据记录的划分到同一次用水事件,并记录接下来的水流量不为0数据记录为Ri+2
- 循环执行步骤2,直到数据记录读取完毕,结束事件划分。
考虑到不同地区的人们用热水器的习惯不同,以及不同季节使用热水器时停顿的时长也可能不同,固定的停顿时长阈值对于某些特殊的情况的处理是不理想的,存在把一个事件划分为两个事件或者把两个事件合为一个事件的情况。所以,考虑到在不同的时间段内要更新阈值,本案例建立了阈值寻优模型来更新寻找最优的阈值,这样可以解决因时间变化和地域不同导致阈值存在差异的问题。
于是,阈值优化的结果如下:
- 当存在一个阈值的斜率指标K之l时,则取阈值最小的点A(可能存在多个阈值的斜率指标小于1)的横坐标Xa作为用水事件划分的阈值,其中K之1中的1是经过实际数据验证的一个专家阈值。
- 当不存在K之1时,则找所有阈值中斜率指标最小的阈值;如果该阈值的斜率指标小于5,则取该阈值作为用水事件划分的阈值;如果该阈值的斜率指标不小于5,则阈值取默认值的阈值为4分钟。其中,斜率指标小于5中的5是经过实际数据验证的一个专家阈值。
n = 4
threshold = pd.Timedelta(minutes = 5)
def event_num(ts):
d = df['发生时间'].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)
print('计算出的单次用水时长的阈值为:',ts)
data['发生时间'] = pd.to_datetime(data['发生时间'], format = '%Y%m%d%H%M%S')
threshold = pd.Timedelta(minutes = 4)
sjks = df['发生时间'].diff() > threshold # 相邻时间向前差分,比较是否大于阈值
sjks.iloc[0] = True # 因为diff是向前取的原因,令第一个时间为第一个用水事件的开始事件
sjjs = sjks.iloc[1:] # 向后差分的结果
sjjs = pd.concat([sjjs, pd.Series(True)]) # 令最后一个时间作为最后一个用水事件的结束时间
sj = pd.DataFrame(np.arange(1, sum(sjks) +1), columns= ['事件序号'])
sj["事件起始编号"] = df.index[sjks == 1] # 定义用水事件的起始编号
sj["事件终止编号"] = df.index[sjjs == 1]+1 # 定义用水事件的终止编号
2.3.2 属性构造
# 构造特征:总用水时长
timeDel = pd.Timedelta("0.5 sec") # 数据延迟时间
sj["事件开始时间"] = data.loc[sj["事件起始编号"], '发生时间'].values - timeDel # 热水事件开始发生的时间
sj["事件结束时间"] = data.loc[sj["事件终止编号"] -1, '发生时间'].values + timeDel # 热水事件结束发生的时间
sj['洗浴时间点'] = [i.hour for i in sj["事件开始时间"]]
sj["总用水时长"] = np.int64(sj["事件结束时间"] - sj["事件开始时间"]) / 1000000000 # 单位秒
sj = sj[sj["总用水时长"] > 120] # 因为是为了对洗浴事件建模,加上没做数据清洗的原因,在这直接把一些短暂用水的事件去除
sj.index = (range(0, 76))
# 构造用水停顿事件
# 构造特征“停顿开始时间”、“停顿结束时间”
# 停顿开始时间指从有水流到无水流,停顿结束时间指从无水流到有水流
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 + 1,"停顿结束时间"] = data.loc[i + 1 , "发生时间"] - timeDel
# 提取停顿开始时间与结束时间所对应行号,放在数据框Stop中
indStopStart = data.index[data["停顿开始时间"].notnull()]
indStopEnd = data.index[data["停顿结束时间"].notnull()]
Stop = pd.DataFrame(data={"停顿开始编号":indStopStart[:-1], "停顿结束编号":indStopEnd[1:]})
# 计算停顿时长,并放在数据框stop中,停顿时长=停顿结束时间-停顿结束时间
Stop["停顿时长"] = np.int64(data.loc[indStopEnd[1:], "停顿结束时间"].values - data.loc[indStopStart[:-1],"停顿开始时间"].values) / 1000000000
# 将每次停顿与事件匹配,停顿的开始时间要大于事件的开始时间,
# 且停顿的结束时间要小于事件的结束时间
for i in range(len(sj)):
Stop.loc[(Stop["停顿开始编号"] > sj.loc[i, "事件起始编号"]) & (Stop["停顿结束编号"] < sj.loc[i,"事件终止编号"]), "停顿归属事件"] = i+1 # 事件排序从1开始,range排序从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["总用水时长"] # 判断用水时长占总用水时长的比重
data["水流量"] = data["水流量"] / 60 # 原单位L/min,现转换为L/sec
sj["总用水量"] = 0 # 给总用水量赋一个初始值0
for i in range(len(sj)):
Start = sj.loc[i, "事件起始编号"]
End = sj.loc[i, "事件终止编号"]
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, "总用水量"]
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, "事件起始编号"]
End = sj.loc[i, "事件终止编号"]
for j in range(Start, End ):
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, "水流量波动"]
# 构造特征:停顿时长波动
# 停顿时长波动=∑(((单次停顿时长-平均停顿时长)^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:
squ = (j - sj.loc[i, "平均停顿时长"]) ** 2
sj.loc[i, "停顿时长波动"] = (squ * j) / sj.loc[i, "总停顿时长"] + sj.loc[i, "停顿时长波动"]
sj_bool = sj[(sj['用水时长'] > 100) & (sj['总用水量'] > 5)] # 把剩下可以判定为短暂用水的事件去除
筛选洗浴事件(书中只有一个公式,并没相关代码和思路)
3. 模型构建
from __future__ import print_function
from keras.models import Sequential
from keras.layers.core import Dense, Activation, Dropout
data_train = pd.read_excel('./chapter10/demo/data/train_neural_network_data.xls')
data_test = pd.read_excel('./chapter10/demo/data/test_neural_network_data.xls')
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
model = Sequential()
model.add(Dense(input_dim = 11, units = 17))
model.add(Activation('relu'))
model.add(Dense(input_dim = 17, units = 10))
model.add(Activation('relu'))
model.add(Dense(input_dim = 10, units = 1))
model.add(Activation('sigmoid'))
model.compile(loss = 'binary_crossentropy', optimizer = 'adam')
model.fit(x_train, y_train, epochs = 100, batch_size = 1)
r = pd.DataFrame(model.predict_classes(x_test), columns = ['预测结果'])
pd.concat([data_test.iloc[:, :5], r], axis = 1)
model.predict(x_test)
得到一串警告和一个结果,警告是说predict_classes()方法到2021-01-01之后就会被删除
结果predict()方法和predict_classes()方法的得到的预测相似
predict_classes():