Bridge619

Bridge619

Bridge619

命定的局限尽可永在,不屈的挑战却不可须臾或缺!

101 文章数
11 评论数
来首音乐
光阴似箭
今日已经过去小时
这周已经过去
本月已经过去
今年已经过去个月

家用热水器用户行为分析与事件识别

Bridge619
2024-03-07 / 0 评论 / 474 阅读 / 0 点赞

1. 目的

1)使用Python对数据进行预处理,掌握使用Python进行数据预处理的方法

2)掌握数据转换及属性提取过程

2.内容

包含以下两个方面:

1)对采集到的热水器用户数据以4分钟为阈值进行用水事件划分。

2)对划分得到的用水事件提取事件时长、一次用书事件中开关机切换次数、

  一次用水事件的总用水量、平均水流量这$4$个属性。

3. 方法与步骤

具体方法与步骤如下:
(1)

 ①打开Python载入pandas,使用read_excel()函数将“test/data/water_heater.xls数据读入Python中,water_heater.xls文件中的数据为热水器用户一个月左右的用水数据,数据量为2万行左右。

 ②利用pandas方便的函数和方法,得到用水事件的序号、事件起始数据编号和事件终止数据编号,其中用水事件的序号为一个连续编号(1,2,3,…)。根据水流量的值是否为0,明确地确定用户是否在用热水。再根据各条数据的发生时间,如果停顿时间超过阈值4分钟,则认为是二次用水事件。算法具体步骤可参考10.2.2节的数据变换中一次完整用水事件的划分模型,也可以根据自己的理解编写。

 ③使用to_excel()函数将得到的用水事件序号、事件起始数据编号、事件终止数据编号等划分结果保存到Excel文件中。

(2)

 ①打开Python载入pandas,使用read_excel函数将“test/data/water_heater.xls”数据读人Python中,并将实验一中得到的划分结果读入Python中。

 ②数据转换、属性提取。用水事件时长由事件终止数据时间点减去事件起始数据时间点得到。然后再得到一次用水事件中开关机切换次数、一次用水事件的总用水量、平均水流量等属性。

 ③用to_excel()函数将每个用水事件的基本信息与提取得到的属性保存到Excel文件中。

具体步骤如下:

3.1 数据探索分析

3.1.1 数据质量分析

3.1.1.1缺失值分析
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

inputfile = 'C:/Users/Documents/Course/Data_Mining/experiment/exp-chapter10/test/data/original_data.xls'  # 输入的数据文件
data = pd.read_excel(inputfile)  # 读取数据


data.isnull().sum()

out:

结论: 没有发现缺失值。

3.1.1.2 异常值分析
for column in data.columns:
    print(data[column].value_counts())

out:

结论: 通过对每一列的值进行统计,发现没有异常值。并且发现热水器编号、节能模式、当前设置温度均为常量。

3.1.1.3 重复数据分析
data.duplicated().sum()

结论: 没有发现重复数据。

3.1.2 数据特征分析

热水器采集的用水数据包含12个属性:热水器编码、发生时间、开关机状态、加热中、保温中、有无水流、实际温度、热水量、水流量、节能模式、加热剩余时间和当前设置温度等。


热水器数据属性说明:

要探索热水器的水流量状况,其中“有无水流”“水流量”属性最能直观体现热水器的水流情况,所以首先对于这两个属性进行分布分析:

# 查看有无水流的分布
# 数据提取
lv_non = pd.value_counts(data['有无水流'])['无']
lv_move = pd.value_counts(data['有无水流'])['有']

# 查看水流量分布
water = data['水流量']

# 创建画布
fig = plt.figure(figsize=(10, 5))

# 子图1:水流状态记录条形图
plt.subplot(1, 2, 1)  # 将画布分割成1行2列,当前子图为第1个
plt.bar(x=range(2), height=[lv_non, lv_move], width=0.4, alpha=0.8, color='skyblue')
plt.xticks([index for index in range(2)], ['无', '有'])
plt.xlabel('水流状态')
plt.ylabel('记录数')
plt.title('不同水流状态的记录条形图')

# 子图2:水流量分布箱线图
plt.subplot(1, 2, 2)  # 将画布分割成1行2列,当前子图为第2个
plt.boxplot(water, patch_artist=True, labels=['水流量'], boxprops={'facecolor': 'lightblue'})
plt.title('水流量分布箱线图')
plt.grid(axis='y')

# 显示画布
plt.show()
plt.close()

out:

结果分析:由记录条形图知,无水流状态记录明显比有水流状态的记录多,同时由箱型图可知,箱体贴近于0,说明无水流量的记录较多,所以水流量的分布和水流量状态的分布是一致的。

3.2 数据预处理

3.2.1 属性归约

  • 要分析的主要目标为热水器用户洗浴行为的一般规律,所以 “热水器编号”属性可以去除;
  • 在热水器采集的数据中,“有无水流”属性可以通过“水流量”属性反映出来;
  • “节能模式”属性取值相同均为“关”,对分析无作用,可以去除;
  • “当前设置温度”属性取值也为常量,对分析无作用,可以去除。

所以,删除冗余属性“热水器编号”“有无水流”“节能模式”“当前设置温度”。

# 打印原始数据前五行数据,查看原始数据属性
# 使用 Styler 类对数据的前 5 行进行格式化并输出
display(pd.DataFrame(data.head()).style)
print('原始数据形状为:', data.shape)

# 删除热水器编号、有无水流、节能模式属性、当前设置温度
data.drop(labels=["热水器编号", "有无水流", "节能模式", "当前设置温度"], axis=1, inplace=True)

# 打印前五行数据,查看删除冗余特征后数据属性
# 使用 Styler 类对数据的前 5 行进行格式化并输出
display(pd.DataFrame(data.head()).style)
print('删除冗余特征后的数据形状为:', data.shape)

# 将新数据存入tmp文件夹下water_heart.csv中
data.to_csv('../tmp/water_heart.csv', index=False)

out:

3.2.2 确定停顿时间间隔

在用水状态记录中:

  • 水流量不为0,表示用户正在使用热水
  • 而水流量为0,则表示用户用热水时发生停顿用热水结束

用户停顿时间间隔:定义为一条水流量不为0的记录同下一条水流量不为0的流水记录之间的时间间隔。
由现场实验统计用水停顿的时间间隔可知,两次用水事件的停顿时间间隔分布在$3-7$分钟。

通过阈值寻优的方法确定停顿时间间隔阈值$T$。
阈值寻优原理不在此处详细解释,在实验思考总结部分在做详细解释。

# 确定单词用水事件时长阈值
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个斜率绝对值平均作为斜率指标
# 用idxmin返回最小值的Index,由于rolling_mean()计算的是前n个斜率的绝对值平均,所以结果要进行平移(-n)
ts = h['阈值'][h['斜率指标'].idxmin() - n]
if ts > threshold:
    ts = pd.Timedelta(minutes=4)
print('计算出最优的停顿时间间隔阈值为:',ts)

out:

计算出最优的停顿时间间隔阈值为: 0 days 00:04:00

3.2.3 划分用水事件

  • 对于任何一个用水记录,如果它的向前时差超过了停顿时间间隔$T$,则将它记为用水事件的开始编号;
  • 如果它的向后时差超过了停顿时间间隔$T$,则将其记为用水事件的结束编号。

通过以下代码将数据按照时间间隔进行分割,分为多个用水事件,并得到用水事件序号、用水事件起始数据编号、用水事件终止数据编号。

# 读取数据
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])
# 使用 Styler 类对数据的前 5 行进行格式化并输出
display(pd.DataFrame(sj.head()).style)

# 将结果保存在sj.csv中
sj.to_csv('../tmp/sj.csv', index=False)

out:

实验二具体步骤如下:

3.2.4 属性构造

3.2.4.1 构建用水时长与频率属性

构建用水时长属性:

不同用水事件的用水时长是基础属性之一。例如,单次洗漱事件一般总时长在5分钟左右,而一次手洗衣物事件的时长则根据衣物多少而不同。
根据用水时长这一属性可以构建如表所示的事件开始时间、事件结束时间、洗浴时间点、用水时长、总用水时长和用水时长/总用水时长这6个属性:

其中,用水开始时间或结束时间两个特征时分别减去或加上了发送阈值(发送阈值是指热水器传输数据的频率的大小)。在20:00:10时,热水器记录到的数据是数据还没有用水,而在20:00:12时,热水器记录的数据是有用水行为。所以用水开始时间在20:00:10~20:00:12之间,考虑到网络不稳定导致的网络数据传输延时数分钟或数小时之久等因素,取平均值会导致很大的偏差,综合分析构建“用水开始时间”为起始数据的时间减去“发送阈值”的一半。

data = pd.read_excel('../data/water_hearter.xlsx',)  # 读取热水器使用数据记录
sj = pd.read_csv('../tmp/sj.csv', encoding='utf-8')  # 读取用水事件记录

# 转换时间格式
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

# 使用 Styler 类对数据的前 5 行进行格式化并输出
display(pd.DataFrame(sj.head()).style)

out:

构建频率属性:

用水时长相关的属性只能区分出一部分用水事件,不同用水事件的用水停顿和频率也不同。例如,一次完整洗漱事件的停顿次数不多,停顿的时间长短不一,平均停顿时长较短;一次手洗衣物事件的停顿次数较多,停顿时间相差不大,平均停顿时长一般。根据这一属性,可以构建如表所示的停顿时长、总停顿时长、平均停顿时长、停顿次数4个属性:

# 构造用水停顿事件


# 构造属性“停顿开始时间”、“停顿结束时间”
# 停顿开始时间指从有水流到无水流,停顿结束时间指从无水流到有水流
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["总用水时长"]  # 定义特征 用水/总时长

# 将用水事件用水时长与频率特征构造完成后数据的前 5 行进行格式化并输出
display(pd.DataFrame(sj.head()).style)

out:

3.2.4.2 构建用水量与波动属性

构建用水量属性:

除了用水时长、停顿和频率外,用水量也是识别该事件是否为洗浴事件的重要属性。例如,用水事件中的洗漱事件相比洗浴事件有停顿次数多、用水总量少、平均用水少的特点;手洗衣物事件相比于洗浴事件则有停顿次数多、用水总量多、平均用水量多的特点。根据这一原因可以构建出两个用水量属性


# 构造属性:总用水量、平均水流量
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, "总停顿时长"]

# 将用水量与波动属性构造完成后数据的前 5 行进行格式化并输出
display(pd.DataFrame(sj.head()).style)

out:

通过构造属性,此时数据属性有:[‘事件序号’, ‘事件起始编号’, ‘事件结束编号’, ‘事件开始时间’, ‘事件结束时间’, ‘洗浴时间点’, ‘总用水时长’, ‘总停顿时长’, ‘停顿次数’, ‘平均停顿时长’, ‘用水时长’, ‘用水时长/总用水时长’, ‘总用水量’, ‘平均水流量’, ‘水流量波动’, ‘停顿时长波动’]

3.2.5 筛选候选洗浴事件

由于我们建立的是洗浴事件识别模型,所以在一次完整用水事件划分结果的基础上,剔除短暂用水事件。可以使用3个比较宽松的条件筛选掉那些非常短暂的用水事件,确定不可能为洗浴事件的数据就删除,剩余的事件称为“候选洗浴事件”。这3个条件是“或”的关系。也就是说哦,只要一次完整的用水事件满足下面3个条件中的任何一个就被判为短暂用水事件,被删除

  • 一次用水事件中总用水量小于5升
  • 用水时长小于100秒
  • 总用水时长小于120秒。
# 删选候选洗浴事件
sj_bool = (sj['用水时长'] > 100) & (sj['总用水时长'] > 120) & (sj['总用水量'] > 5)
sj_final = sj.loc[sj_bool, :]

# 将筛选出候选洗浴事件前的数据的基本信息和属性保存到si_final.xlsx文件中
sj_final.to_excel('../tmp/sj_final.xlsx', index=False)
print('筛选出候选洗浴事件前的数据形状为:', sj.shape)
print('筛选出候选洗浴事件后的数据形状为:', sj_final.shape)

out:

筛选出候选洗浴事件前的数据形状为: (172, 16)
筛选出候选洗浴事件后的数据形状为: (75, 16)

筛选前,用水事件数目总数共为172个,经过筛选,余下75个用水事件。数据最终的属性为11个。

3.3 模型构建

根据建模样本数据建立BP神经网络模型识别洗浴事件。将洗浴事件的数据状态作为训练样本训练BP神经网络;然后用新采集的数据来检验模型。

from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.neural_network import MLPClassifier
import joblib

# 读取数据
Xtrain = pd.read_excel('../tmp/sj_final.xlsx')
ytrain = pd.read_excel('../data/water_heater_log.xlsx')
test = pd.read_excel('../data/test_data.xlsx')
# 训练集测试集区分。
x_train, x_test, y_train, y_test = Xtrain.iloc[:,5:],test.iloc[:,4:-1],\
                                   ytrain.iloc[:,-1],test.iloc[:,-1]
# 标准化
stdScaler = StandardScaler().fit(x_train)
x_stdtrain = stdScaler.transform(x_train)
x_stdtest = stdScaler.transform(x_test)
# 建立模型
bpnn = MLPClassifier(hidden_layer_sizes = (17,10), max_iter = 200, solver = 'lbfgs',random_state=50)
bpnn.fit(x_stdtrain, y_train)
# 保存模型
joblib.dump(bpnn,'../tmp/water_heater_nnet.m')
print('构建的模型为:\n',bpnn)

out:

构建的模型为:
 MLPClassifier(hidden_layer_sizes=(17, 10), random_state=50, solver='lbfgs')

3.4 模型检验

得到训练好的神经网络模型后,将测试集样本的11个属性作为输入,输出层输出一个在[-1,1]范围内的值。
如果该值小于0,则该事件不是洗浴事件;如果该值大于0,则该事件是洗浴事件。

使用精准率(precision)、召回率(recall)和F1值来检验模型的效果;同时结合ROC曲线,更直观地评价模型地效果。

# 模型评价
from sklearn.metrics import classification_report
from sklearn.metrics import roc_curve
from sklearn.metrics import accuracy_score
import matplotlib.pyplot as plt
import joblib

bpnn = joblib.load('../tmp/water_heater_nnet.m')  # 加载模型
y_pred = bpnn.predict(x_stdtest)  # 返回预测结果
report = classification_report(y_test, y_pred)  # 返回预测结果的评价报告
print('神经网络预测结果评价报告:\n{}'.format(report))

# 绘制roc曲线图
plt.rcParams['font.sans-serif'] = 'SimHei'  # 显示中文
plt.rcParams['axes.unicode_minus'] = False  # 显示负号
fpr, tpr, thresholds = roc_curve(y_pred,y_test)  # 求出TPR和FPR
plt.figure(figsize=(6,4))  # 创建画布
plt.plot(fpr,tpr)  # 绘制曲线
plt.title('用户用水事件识别ROC曲线')  # 标题
plt.xlabel('FPR')  # x轴标签
plt.ylabel('TPR')  # y轴标签
plt.savefig('../tmp/用户用水事件识别ROC曲线.png')  # 保存图片
plt.show()  # 显示图形

out:

  • precision: 精确率,即在所有被预测为正例的样本中,真正为正例的样本所占的比例。其计算公式为:TP/(TP+FP)。
  • recall: 召回率,即在所有实际为正例的样本中,被正确预测为正例的样本所占的比例。其计算公式为:TP/(TP+FN)。
  • f1-score: F1得分,是精确率和召回率的调和平均数。其计算公式为:2 * precision * recall / (precision + recall)。
  • support: 每个类别在测试集中的样本数。
  • accuracy: 准确率,即在所有预测的样本中,正确预测的样本所占的比例。其计算公式为:(TP+TN)/(TP+FP+TN+FN)。
  • macro avg: 对所有类别的指标值取平均,每个类别被视为等重要。
  • weighted avg: 对所有类别的指标值取加权平均,根据每个类别在测试集中的样本数进行加权。

根据这个模型的评价结果,可以看出:

  • 模型的准确率(accuracy)为0.78,即模型正确预测的样本数占总样本数的比例为0.78,表明模型整体预测的效果还可以;
  • 在预测“0”这个类别时,模型的精确率(precision)为0.52,召回率(recall)为0.92,f1-score为0.67,说明模型对该类别的预测效果较差;
  • 在预测“1”这个类别时,也就是在识别洗浴事件时,模型的精确率为0.96,召回率为0.73,f1-score为0.83,说明模型对该类别的预测效果较好;

综合考虑各类别的预测效果,可以看出模型的加权平均f1-score为0.79,表明模型整体的预测效果还可以,但仍有改进的空间。

4. 思考与总结

通过以上实验,我们可以对以下问题进行思考与总结:

(1) 在划分用水事件中采用得阈值为4分钟,而案例中有阈值寻优模型,可用阈值寻优模型对每家热水器用户、每个时间段寻找最优的阈值。

(2) 试着自行用循环语句(for或while)实现相同的功能,对比案例提供的代码(即用内置的广播式的函数),运行效率会下降多少?

具体内容如下:

(1) 在本次案例实现中,已经通过阈值寻优模型寻找最优的阈值,具体代码可见《 3.2.2 确定停顿时间间隔》,下面对案例所给的寻优模型做具体的解释:

寻优原理:

上图为阈值与划分事件个数的散点图。

图中某段阈值范围内,下降趋势明显,说明在该段阈值范围内,热水器用户的停顿习惯比较集中。如果趋势比较平缓,则说明热水器用户停顿热水的习惯趋于稳定,所以取该段时间开始的时间点作为阈值,既不会将短的用水事件合并,又不会将长的用水事件拆开。在图中,热水器用户停顿热水的习惯在方框中的位置趋于稳定,说明该热水器用户的用水停顿习惯用方框开始的时间点作为划分阈值会有好的效果。

曲线在上图中,方框趋于稳定时,其方框开始的点的斜率趋于一个较小的值。为了用程序来识别这一特征,将这一特征提取为规则。

下图可以说明如何识别上图方框中起始的时间。

每个阈值对应一个点,给每个阈值计算得到一个斜率指标,如上图所示。其中A点是要计算的斜率指标点。
根据式(1),计算出$k_{AB}、k_{AC}、k_{AD}、k_{AE}$四个斜率。然后可以根据式(2)计算出4个斜率之和的平均值K。

$k = \dfrac{y_{1}-y_{2}}{x_{1}-x_{2}}\ \ \ \ (1)$

$K=\dfrac{k_{A B}+k_{A C}+k_{A D}+k_{A E}}{4}\ \ \ \ (2)$


然后斜率作为择优指标进行阈值选择。

选择标准如下:

  • 1)当存在一个阈值的斜率指标$K<1$时,则取阈值最小的点A(可能存在多个阈值的斜率指标小于1)的横坐标$x_A$作为用水事件划分的阈值,其中$K<1$中的“1”是经过实际数据验证的一个专家阈值。
  • 2)当不存在一个阈值的斜率指标$K<1$时,则找所有阈值中斜率指标最小的阈值;如果该阈值的斜率指标小于5,则取该阈值作为用水事件划分的阈值;如果该阈值的斜率指标不小于5,则阈值取默认值的阈值:4分钟。其中,“斜率指标小于5”中的“5是经过实际数据验证的一个专家阈值。

(2)用for循环语句实现阈值寻优及其与案例代码使用内置的广播式函数实现阈值寻优效率对比如下:

案例代码使用内置的广播式函数实现阈值寻优:

# 案例代码使用内置的广播式函数实现阈值寻优

import time

# 确定单词用水事件时长阈值
data = pd.read_csv('../tmp/water_heart.csv')
n = 4  # 使用以后四个点的平均斜率
threshold = pd.Timedelta(minutes=5)  # 专家阈值
data['发生时间'] = pd.to_datetime(data['发生时间'], format='%Y%m%d%H%M%S')
data = data[data['水流量'] > 0]  # 只要流量大于0的记录

# 插入计时器,记录开始时间
start_time = time.time()

# 自定义函数:输入划分时间的时间阈值,得到划分的事件数
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个斜率绝对值平均作为斜率指标
# 用idxmin返回最小值的Index,由于rolling_mean()计算的是前n个斜率的绝对值平均,所以结果要进行平移(-n)
ts = h['阈值'][h['斜率指标'].idxmin() - n]
if ts > threshold:
    ts = pd.Timedelta(minutes=4)
print('计算出最优的停顿时间间隔阈值为:',ts)

# 记录结束时间
end_time = time.time()

# 计算运行时间
elapsed_time = end_time - start_time
print("代码运行时间:", elapsed_time, "秒")

out:

计算出最优的停顿时间间隔阈值为: 0 days 00:04:00
代码运行时间: 0.010999917984008789 秒

for循环语句实现阈值寻优:

# for循环语句实现阈值寻优

import time

# 确定单次用水事件时长阈值
data = pd.read_csv('../tmp/water_heart.csv')
n = 4  # 使用以后四个点的平均斜率
threshold = pd.Timedelta(minutes=5)  # 专家阈值
data['发生时间'] = pd.to_datetime(data['发生时间'], format='%Y%m%d%H%M%S')
data = data[data['水流量'] > 0]  # 只要流量大于0的记录

# 插入计时器,记录开始时间
start_time = time.time()

# 自定义函数:输入划分时间的时间阈值,得到划分的事件数
def event_num(ts):
    d = [False] * len(data)  # 创建一个长度与 data 相同的布尔列表,初始值为 False
    for i in range(1, len(data)):
        d[i] = (data['发生时间'].iloc[i] - data['发生时间'].iloc[i-1]) > ts  # 比较相邻时间差值是否大于阈值
    return sum(d) + 1  # 返回事件数

dt = [pd.Timedelta(minutes=i) for i in np.arange(1, 9, 0.25)]
h = pd.DataFrame(dt, columns=['阈值'])  # 转换数据框,定义阈值列

# 使用循环计算每个阈值对应的事件数、斜率和斜率指标
for i in range(len(h)):
    h.at[i, '事件数'] = event_num(h.at[i, '阈值'])
    if i > 0:
        h.at[i, '斜率'] = (h.at[i, '事件数'] - h.at[i-1, '事件数']) / 0.25
    if i >= n-1:
        h.at[i, '斜率指标'] = h['斜率'].abs().iloc[i-n+1:i+1].mean()

ts = h['阈值'][h['斜率指标'].idxmin() - n]
# 用idxmin返回最小值的Index,由于rolling_mean()计算的是前n个斜率的绝对值平均
# 所以结果要进行平移(-n)

if ts > threshold:
    ts = pd.Timedelta(minutes=4)
print('计算出的单次用水时长的阈值为:', ts)

# 记录结束时间
end_time = time.time()

# 计算运行时间
elapsed_time = end_time - start_time
print("代码运行时间:", elapsed_time, "秒")

out:

计算出的单次用水时长的阈值为: 0 days 00:04:00
代码运行时间: 5.163284778594971 秒

结果对比分析:

阈值寻优方式 运行时间(s)
内置广播式函数 0.010999917984008789
for实现 5.163284778594971

使用循环语句代替内置函数和广播式函数,会导致运行效率下降。
  • 从上表中也可以看出,运行时间显著增加,因为循环语句需要逐个处理数据点,导致迭代次数增加,从而增加了代码的运行时间。

  • 内存占用也增加:循环语句可能需要在每次迭代中创建临时变量,导致内存占用增加。

  • 代码复杂度也增加:使用循环语句可能需要更多的代码行数和更复杂的逻辑,从而增加了代码的复杂性和维护成本。

相比之下,内置函数和广播式函数通常经过了优化和向量化处理,可以在底层使用高效的C或者其他编译语言实现,并允许同时处理多个数据点,从而提供更好的性能。

因此,在性能要求较高的情况下,使用内置函数和广播式函数通常是更好的选择,可以提供更高效的运行效果。然而,对于小规模数据或者简单的计算任务,使用循环语句可能不会对性能产生显著影响,而且可能更加简洁和易于理解。在实际应用中,可以根据具体情况进行权衡和选择。

5. 拓展思考

根据模型划分的结果,发现有时候会将两次(或多次)洗浴事件划分为一次洗浴事件,因为在实际情况中,存在着一个人洗完澡后,另一个人马上洗的情况,这中间过渡期间的停顿间隔小于阈值。针对两次(或多次)洗浴事件被合并为一次洗浴事件的情况,需要进行优化,对连续洗浴事件作识别,提高模型识别精确度。

给出的连续洗浴识别法如下:

对每次用水事件,建立一个连续洗浴判别指标。连续洗浴判别指标初始值为0,每当有一个属性超过设定的阈值,就给该指标加上相应的值,最后判别连续洗浴指标是否超过给定的阈值,如果超过给定的阈值,则认为该次用水事件为连续洗浴事件。

选取5个前面提取得到的属性作为判别连续洗浴事件的特征属性,5个属性分别为总用水时长、停顿次数、用水时长/总用水时长、总用水量、停顿时长波动。详细说明如下。

  • 1)总用水时长的阈值为900秒,如果超过900秒,就认为可能是连续洗浴,对于每超出的一秒,就在该事件的连续洗浴判别指标上加上0.005。
  • 2)停顿次数的阈值为10次,如果超过10次,就认为可能是连续洗浴,对于每超出的一次,就在该事件的连续洗浴判别指标上加上0.5。
  • 3)用水时长/总用水时长的阈值为0.5,如果小于0.5,就认为可能是连续洗浴,对于每少一个单位,就在该事件的连续洗浴判别指标上加上0.2。
  • 4)总用水量的阈值为30L/次,如果超过30L,就认为可能是连续洗浴,对于每超出的1L,就在该事件的连续洗浴判别指标上加上0.2。
  • 5)停顿时长波动的阈值为1000,如果超过1000,就认为可能是连续洗浴,对于每超出一个单位,就在该事件的连续洗浴判别指标上加上0.002。

连续洗浴事件划分模型符合说明:


根据以上信息,建立的优化模型如下,其中S为连续洗浴判别指标。

image-20240307190104851

所以,连续洗浴事件的划分模型如下:

  • 1)当用水事件的连续洗浴判别指标S大于5时,确定为连续洗浴事件或一次洗浴事件加一次短暂用水事件,取中间停顿时间最长的停顿,划分为两次事件。
  • 2)如果S不大于5,确定为一次洗浴事件。

实现上述优化模型的代码如下:

import pandas as pd

# 读取Excel数据
df = pd.read_excel('../tmp/sj_final.xlsx')

# 定义阈值
total_duration_threshold = 900
pause_count_threshold = 10
water_ratio_threshold = 0.5
total_water_threshold = 30
pause_duration_variance_threshold = 1000

# 自定义函数封装相关功能
def detect_continuous_bathing_events(dataframe):
    # 初始化连续洗浴判别指标列
    dataframe['连续洗浴判别指标S'] = 0

    # 遍历每一行数据
    for index, row in dataframe.iterrows():
        # 计算连续洗浴判别指标S
        s = 0
        if row['总用水时长'] > total_duration_threshold:
            s += (row['总用水时长'] - total_duration_threshold) * 0.005
        if row['停顿次数'] > pause_count_threshold:
            s += (row['停顿次数'] - pause_count_threshold) * 0.5
        if row['用水/总时长'] < water_ratio_threshold:
            s += (water_ratio_threshold - row['用水/总时长']) * 0.2
        if row['总用水量'] > total_water_threshold:
            s += (row['总用水量'] - total_water_threshold) * 0.2
        if row['停顿时长波动'] > pause_duration_variance_threshold:
            s += (row['停顿时长波动'] - pause_duration_variance_threshold) * 0.002

        # 更新连续洗浴判别指标S列
        dataframe.at[index, '连续洗浴判别指标S'] = s

    # 根据连续洗浴判别指标S进行事件划分
    dataframe['事件划分'] = ''
    for index, row in dataframe.iterrows():
        if row['连续洗浴判别指标S'] > 5:
            # 计算连续洗浴事件的中点时间
            mid_time = (row['事件开始时间'].timestamp() + row['事件结束时间'].timestamp()) / 2
            mid_time = pd.Timestamp(mid_time, unit='s')

            # 将连续洗浴事件划分为两个事件
            dataframe.at[index, '事件划分'] = '第一个洗浴事件'
            new_row = row.copy()
            new_row['事件开始时间'] = mid_time
            dataframe = dataframe.append(new_row, ignore_index=True)
            dataframe.at[dataframe.index[-1], '事件划分'] = '第二个洗浴事件'
        else:
            dataframe.at[index, '事件划分'] = '一次洗浴事件'

    return dataframe

# 调用自定义函数进行连续洗浴事件的识别
df = detect_continuous_bathing_events(df)

# 将划分完成的数据存储到Excel文件中
df.to_excel('C:/Users/Documents/Course/Data_Mining/experiment/'
            'exp-chapter10/拓展思考/sj_final重划分.xlsx', index=False)


# 将划分完成后数据的前 5 行进行格式化并输出
display(pd.DataFrame(df.head(5)).style)

out:

划分完成后的数据保存在'拓展思考/sj_final重划分.xlsx'中。

6. 总结

本项目以基于实时监控的智能热水器的用户使用数据为背景,介绍了如何构建BP神经网络洗浴事件识别模型。首先,介绍了根据用水停顿时间间隔的阈值划分一次用水事件的过程,即将连续的用水时间划分为多个用水事件。然后,介绍了如何构建用水行为属性,包括用水时间、用水量、用水温度等。最后,介绍了如何根据热水器用户用水日志判断模型结果的好坏。

文章不错,扫码支持一下吧~
上一篇 下一篇
评论