目标是统计4月和5月因为联系方式错误而产生的抱怨工单。本身只有少量标注的信息,本次探索是从一个非业务人员的角度去探索如何分类抱怨的类型。

import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import re

统计4月与5月联系方式错误的抱怨工单。

data = pd.read_excel("april-may.xlsx")
# 分成4月与5月的数据
april = data[data.受理时间.dt.month == 4]
may = data[data.受理时间.dt.month == 5 ]

大概看一下四月分的抱怨工单长什么样,先看看字段名。

april.columns
Index(['工作单编号', '用户地址', '来电内容', '来电号码', '诉求人', '业务类型', '业务子类', '服务座席', '受理时间',
       '处理意见', '市提级意见', '当前环节', '处理状态', '工单转办次数', '服务渠道', '信息来源', '首次评议',
       '归档评议', '当前部门', '当前处理人员', '供电所', '供电单位', '供电局', '回复信息', '归档人', '坐席意见',
       '用户编号', '用户名称', '用电类别', '线路名称', '台区名称', '归档时间', '后续工单号', '是否属实', '涉及部门',
       '处理单位', '处理时限', '是否销号', '销号时间', '预计销号时间'],
      dtype='object')

在看看四月的3条工单抱怨内容。

for index, item in enumerate(april.来电内容.values[:3]):
    print("序号:{}, 内容:{}\n{}\n".format(index, item, "-"*100))
序号:0, 内容:投诉

市政热线客户来电反映前单:08000010000027151200已到海珠局本部申请办理增容,表示现已更换新电表,但现在电容量还是不够用,称已向营业厅咨询,由于街线直径过细无法满足客户需求,要求相关部门提供解决方案问题,表示至今未有工作人员联系及处理,对此感到不满,认为处理时间过长,要求尽快处理。(已上报)

地址:新港西路88号
----------------------------------------------------------------------------------------------------

序号:1, 内容:(该号码近6个月来电总数:1,投诉数:0)客户反映该址自最近两个月开始出现停电频繁:客户表示一下雨就会出现停电,影响用户日常生活与作息,客户对此意见较大,要求贵局能够出具一定有效的整改处理措施去改善,请核查跟进,请尽快与客户联系。

地址:花都区狮岭镇康政西路47号
----------------------------------------------------------------------------------------------------

序号:2, 内容:(手动开单,话机签入)客户来电反映2018年4月8号到黄埔营业厅办理新装手续,表示已有工作人员到场勘察及告答复客户由于门牌地址:黄埔区登穗路44-1与产权地址:黄埔区登穗路44号相隔比较远,报装不成功。客户不接受,表示街道及居委都已经批了同意安装电表文件(客户未能提供是什么文件),为何我局还不给安装电表,对此感到不满,要求我局给予合理答复,请核查处理。(未能提供受理号)

地址:黄埔区登穗路44号-1
----------------------------------------------------------------------------------------------------

尝试query一下,看看会不会有惊喜。

april_contacts = april[(april.来电内容.str.contains("没收到")) | (april.来电内容.str.contains("联系方式")) | (april.来电内容.str.contains("乱发"))]
for item in april_contacts.来电内容.values[:3]:
    print("{}\n{}\n".format(item, "-"*100))
回复08000010000027163934,客户此前反映收不到欠费停电通知书导致被停电的问题,中心已按前单反馈解释,客户表示有工作人员联系,但只是简单告知客户有派单,目前提出新问题:
1.为何手机号码13825133330一致没有收到201803期电费短信通知,通知经查系统该号码在联系人“李高铭“的联系方式有登记,时间是2016年8月,但该号码手机的的联系方式未添加“电费通知”,导致客户不知道有欠费,2017年全年都有收到通知(经查属于客户通过其他渠道缴费后的通知,并非电费通知),中心已再致歉,已重新为客户开通短信通知(机主:李高铭 手机:13825133330),请客户留意下期短信发送情况。经查请贵局核查当时短信没有成功开通的原因。
2.表示当时工作人员在4月26日下午14:30才致电客户询问其复电情况,也没有询问客户是否在现场,因此无法确认复电情况(前单反馈客户201803期电费已于2018-04-26 00:24:24交清,4月26日上午10:30已为客户复电),客户也提及当时致电中心的时候表示家里冰箱里的东西已经坏掉,请求我局工作人员致电给上址管理处,让管理处人员帮客户恢复供电,但也不满为何工作人员没有联系管理处复电,中心也尝试解释欠费停复电工作属我局工作,管理处没有权限或义务实施复电,客户不接受,意见较大,表示以往肯定有我局人员进行过此操作,管理处才知情的,客户通话中提及要对其损失进行赔偿。随后客户表示有其他电话进,因此自行挂机。请贵局核查处理,再次给出上述问题的明确答复。
----------------------------------------------------------------------------------------------------

手动开单

客户来电反映该址计划停电,并客户表示该址停电时间为:2018/04/26 08:30,实际复电时间为:2018/04/26 20:00 ,客户不满此次停电时间比较久,现因天气寒冷家里有小孩子不愿耐心等待到复电时间,中心多次都无法安抚成功且也请客户耐心等待复电时间,但客户坚称会致电12315还有发起村民致电95598投诉计划停电的复电时间较久,现客户要求贵局人员尽快安排复电,请协办。(中心已记录客户诉求未承诺并已上报)

客户要求半小时内联系答复,联系人:涂先生   联系方式:16620093528
----------------------------------------------------------------------------------------------------

(该号码近6个月来电总数:1,投诉数:0)
客户来电反映上址没有收到停电通知短信,表示之前也有反映同样的问题但也没彻底解决问题,已跟客户解释可能性,经查客户有开通停电通知短信但客户表示从2017年开始就没收到过停电通知单只有收电费通知单,现客户意见较大要求我局工作人员核查处理给出答复,并以后能正常发送短信。
号码:13640698244
----------------------------------------------------------------------------------------------------

并没有什么惊喜,内容混乱,字符错误。甚至部分内容对不上子类别。

types = data.drop_duplicates('业务子类').业务子类
print("业务类别预览:\n{}\n\n总业务子类数:{}".format(types[:10], len(types.values)))
业务类别预览:
0                              NaN
1                   供电可靠/故障停电/频繁停电
9              用电缴费/单据派发/电费发票收取不到位
16         信息沟通/停电信息沟通/计划停电信息通知不及时
17            供电可靠/预安排停电/停电安排时间不合理
21            业务办理/服务效率/业务办理超时(其他)
23    业务办理/业务办理条件与流程/条件设置不合理(光伏发电)
27       服务态度/营销服务人员服务态度/营业厅人员服务态度
29            用电缴费/计费/客户感知电量电费变化异常
30                  供电安全/供电设备/设备噪音
Name: 业务子类, dtype: object

总业务子类数:114

我们挑一个看起来最符合因为联系方式不对而抱怨系统的子类别,查询整个系统的工单看看。可以看到都是5月份的,对此我表示怀疑。前面月份的也可能存在问题。

due_to_system = data[data["业务子类"] == "服务渠道/短信平台/发送内容有误(系统问题)"]
print(due_to_system[["来电内容","受理时间"]])
                                                   来电内容                受理时间
2298  (该号码近6个月来电总数:2,投诉数:0)\n客户来电投诉我局短信乱发送,表示2018年5月... 2018-05-17 11:45:24
2339  (该号码近6个月来电总数:2,投诉数:0),客户反映上址不是其户主,经常收到该户电费信息,客... 2018-05-15 16:50:55
2347  (该号码近6个月来电总数:1,投诉数:0)客户来电反映上址在今早接受到我局发布的欠费通知,但... 2018-05-15 13:16:28
2407  我是13288613280电话主人,我不叫陈子超,我新市合益街B3西梯605也没有物业,为什... 2018-05-13 19:06:32
2498  (该号码近6个月来电总数:1,投诉数:0)\n客户来电反映上址不属于其物业,并表示已经接收了... 2018-05-10 08:56:41
2543  该号码近6个月来电总数:1,投诉数:0  客户反映该户已开通手机短信服务(手机号码:1302... 2018-05-07 11:56:25

选3条看看效果:

for item in due_to_system.来电内容.values[:3]:
    print("{}\n".format(item))
(该号码近6个月来电总数:2,投诉数:0)
客户来电投诉我局短信乱发送,表示2018年5月17号本机号码收到9559801发送的上址201805期电费短信,表示半年前已致电我局取消短信的接受,但至今还是会收到我局的短信,中心经查询上址并未绑定手机号码:18922234445,短信明细也未查询到有发送记录,请核查处理。

(该号码近6个月来电总数:2,投诉数:0),客户反映上址不是其户主,经常收到该户电费信息,客户建议我局以后开通手机电费通知单,要验证手机号码,不要再有这种事情发生了。

(该号码近6个月来电总数:1,投诉数:0)客户来电反映上址在今早接受到我局发布的欠费通知,但客户强烈表示在昨天已经收到银行发布扣费成功的短信,经中心上址是正在代扣当中,中心已尝试向客户解释,但客户不接受对于此事件意见非常大表示我局工作不认真,乱发短信打扰客户,请核查清楚并答复客户。

观察了一下,感觉客户或者记录员并没有统一的格式,否定语气出现的地方也是不确定的,否定的词也是不定的,有关联系方式的描述也是多样的。因此我们假设几个条件:

  1. 否定语气出现位置随机分布,否定语气的出现的概率跟位置无关系
  2. 不同词汇可以修饰同一个情况
  3. 词与词之间没有必然关联性

思路

目的是做一个关于是否是系统原因而投诉的分类器,感觉上比较靠谱的方案是做一个贝叶斯分类器EM收敛。前提是需要部分正确的数据。

  1. 数据准备
    • 去除无用信息,分词,存储,生成bag-of-words模型
  2. 生成假设模型
    • 用正确标注的数据做假设的模型
  3. EM
    • 加上未标注的数据提高准确性
# 数据准备
import jieba
import jieba.analyse
import pickle
from sklearn.feature_extraction.text import CountVectorizer
import operator
from sklearn.naive_bayes import MultinomialNB
import scipy.sparse as sp

features = data["来电内容"]
labels = np.zeros(len(features))
jieba.analyse.set_stop_words("chineseStopwords.txt")
corpus = []
seg_lists = []
for item in features:
    item = item.replace(":","")
    item = item.replace("\r\n","")
    item = item.replace(" ","")
    item = item.replace("。","")
    item = item.replace(":", "")
    item = re.sub('\d', '', item)
    seg_list = jieba.lcut(item,cut_all=False)
    corpus.append(" ".join(seg_list))
    seg_lists.append(seg_list)
Building prefix dict from the default dictionary ...
Dumping model to file cache /tmp/jieba.cache
Loading model cost 0.788 seconds.
Prefix dict has been built succesfully.

查看分词结果

for index, item in enumerate(corpus[:4]):
    print(index,":",item)
    print("\n")
0 : 手机号 留言 内容 整个 菊树村 电压 都 不 稳定 空调 开着 开着 都 自动 关 的 请 尽快 解决


1 : 接 南方 能 监局 线下 投诉 案件 ( 工作 单 编号 NO ) 客户 反映 因 当地 频繁 停电 , 所以 在 年 月 开始 扩容 , 现在 变压器 和 线路 已经 安装 完成 , 但是 一直 没有 接电 , 现在 又 开始 停电 , 向 和 供电所 反馈 未 解决 , 不 认可 , 诉求 彻底解决 频繁 停电 问题


2 : 手机号 留言 内容 明明 没有 复电 , 但是 你们 公众 号 却说 复电 了 , 每晚 这样 停电 真的 很 难熬 的 啊 , 第二天 还要 那么 早 起床 睡觉 , 没有 复电 就别 在 公众 号 说 复电 给 我 希望 好 吗 ? ?


3 : 手机号 留言 内容 停电 没人管 , 电话 没人接 , 还 让 不让 人活 ?

将分词转成向量, 同时需要保持这个向量空间的维度,意味着我们需要将vocab词汇量给保留下来

vectorizer = CountVectorizer(max_df=0.5)
tqm = vectorizer.fit_transform(corpus)
vocab = vectorizer.vocabulary_

我们挑频率出现最多的几个词看看

sorted_vocab = sorted(vocab.items(), key=operator.itemgetter(1))
top100 = sorted_vocab[-100:]
print(top100[::-1])
[('龙碧巷', 7824), ('龙石金', 7823), ('龙濠', 7822), ('龙潭', 7821), ('龙湖', 7820), ('龙洞', 7819), ('龙归', 7818), ('龙帝荣', 7817), ('龙岗', 7816), ('龙壁巷', 7815), ('龙口', 7814), ('龙博路', 7813), ('鼓动', 7812), ('黑点', 7811), ('黑夜', 7810), ('黎锡桐', 7809), ('黎铭威', 7808), ('黎灼兴', 7807), ('黎杏潮', 7806), ('黎伟洪', 7805), ('黄陂', 7804), ('黄阁', 7803), ('黄锦英', 7802), ('黄进华', 7801), ('黄英照', 7800), ('黄芳', 7799), ('黄胜培', 7798), ('黄耀良', 7797), ('黄维', 7796), ('黄石路', 7795), ('黄登', 7794), ('黄猄', 7793), ('黄润', 7792), ('黄浦区', 7791), ('黄泥', 7790), ('黄河', 7789), ('黄沙', 7788), ('黄汉钊', 7787), ('黄村', 7786), ('黄朗仪', 7785), ('黄景', 7784), ('黄新艳', 7783), ('黄志胜', 7782), ('黄应', 7781), ('黄少尚', 7780), ('黄埔区', 7779), ('黄埔', 7778), ('黄国', 7777), ('黄先生', 7776), ('麻黑', 7775), ('麻烦', 7774), ('麻将馆', 7773), ('麦村', 7772), ('麒麟', 7771), ('鹿鸣', 7770), ('鹤边员村', 7769), ('鹤边', 7768), ('鹤南', 7767), ('鹤北', 7766), ('鹅潭湾', 7765), ('鸿福', 7764), ('鸭塘', 7763), ('鸦湖', 7762), ('鱼珠', 7761), ('鱼塘', 7760), ('魏先生', 7759), ('鬼鬼祟祟', 7758), ('鬼操', 7757), ('鬼天气', 7756), ('高额', 7755), ('高达度', 7754), ('高考', 7753), ('高烧', 7752), ('高点', 7751), ('高溫', 7750), ('高温', 7749), ('高楼', 7748), ('高桥', 7747), ('高昂', 7746), ('高德', 7745), ('高度重视', 7744), ('高峰期', 7743), ('高峰', 7742), ('高层', 7741), ('高处', 7740), ('高城街', 7739), ('高地', 7738), ('高园', 7737), ('高压线', 7736), ('高压电', 7735), ('高压柜', 7734), ('高压', 7733), ('高出', 7732), ('高伟洪', 7731), ('高价', 7730), ('骚扰', 7729), ('骗人', 7728), ('骏景', 7727), ('验证码', 7726), ('验证', 7725)]

出现频率最高的100个,刨除地名,其中高温,骗人,骚扰,验证验证码出现得也比较高。

当有新的数据进来的时候我们可以用这个更新我们的词汇空间。前提是保证词汇量不变。很遗憾这里用不上这个函数。

def vectorizer(vocab, features,df=0.5):
    """
    This is used to update vectorized space based on the same vocab
    """
    vectorizer = CountVectorizer(vocabulary=vocab, max_df=df)
    tqm = vectorizer.fit_transform(features)
    return tqm

用有限的标签训练初步的贝叶斯分类器, 然后再用这个分类器去识别部分未标注的数据。假设这些数据是正确的,再重新训练。这样反复5-6次。

labeld_index = list(due_to_system.index)
# craft some feature matrix
features_with_labels = tqm[labeld_index]
labels_pre_trained = np.ones(features_with_labels.shape[0])
# number of unlabeled data
num_steps = 10
epochs = 5
trained_labels = []

for i in range(epochs):
    if len(trained_labels) != 0:
        labels_pre_trained = trained_labels
        features_with_labels = trained_features
    
    features_without_labels = tqm[i*num_steps:(i+1)*num_steps]
    vertical_stack_features = sp.vstack((features_with_labels, features_without_labels), format='csr')

    # craft some labels
    labels_untrained = np.zeros(features_without_labels.shape[0])
    vertical_stack_labels = np.concatenate((labels_pre_trained, labels_untrained), axis=0)

    # create a classifier
    classifier = MultinomialNB(alpha=0.1).fit(vertical_stack_features, vertical_stack_labels)
    trained_labels = classifier.predict(vertical_stack_features)
    trained_features = vertical_stack_features
    
# predicts probability
predicts_prob = classifier.predict_proba(tqm)
predicts = classifier.predict(tqm)

# set a treshhold for the predicted label, thershhold=0.85
predicts_result = np.max(predicts_prob,1)
masked_labels = np.zeros(len(predicts_result))
# masking
masked_labels[(predicts_result > 0.9) & (predicts == 1)] = 1

# find the resulting index
result_index = np.argwhere(masked_labels == 1)
len(result_index)
125
result_index_list = result_index.reshape(len(result_index))

result_comments = data.iloc[result_index_list].来电内容

for item in result_comments.values[:10]:
    print(item)
    print("-"*90)
(该号码近6个月来电总数:1,投诉数:0)
客户来电投诉我局在未发送电费通知单以及短信提醒的情况下就计收上址电费的违约金,客户表示不是自己原因而导致产生的违约金,自己出差一段时间,不知道上址有欠费,中心经查询系统上址客户有绑定手机号码及邮箱,已跟客户解释,客户不接受,表示是我局的责任,要求我局返还违约金金额,请核查处理。
联系人:王先生
联系电话:13718877936
------------------------------------------------------------------------------------------
(该号码近6个月来电总数:1,投诉数:0)客户不满201805期电费通知单没有派发到位,导致该址没有准时缴费并且产生滞纳金,要求贵局减免该期滞纳金并要求日后务必准时派发纸质电费通知单,请协调处理。中心已向客户解释并开通手机短信通知,但客户仍表示需要投诉,要求贵局务必派发纸质通知单。请跟进。
------------------------------------------------------------------------------------------
(该号码近6个月来电总数:1,投诉数:0)客户来电反映上址欠201805期电费,现已缴清,但客户表示201805期电费没有收到我局任何纸质的电费通知及手机电费短信通知,导致不清楚201805期欠费并接到通知将于6月14日执行停电,对此客户意见十分大,表示是我局电费通知派发不到位,要求我局保证日后电费通知派发质量,并给予答复,请跟进。
------------------------------------------------------------------------------------------
(该号码近6个月来电总数:1,投诉数:0)
客户反映上址户名为姚锐明有窃电行为,接了街外的电线使用,据客户了解,已长达两年,表示这种行为非常不好,窃电客户在大队做事的,要求我局核查,最好穿便衣,另客户强烈强调保密客户信息,不能透露客户的手机号的该次举报,因客户担心个人安危,请跟进。
------------------------------------------------------------------------------------------
(该号码近6个月来电总数:2,投诉数:0)客户来电表示上址在5月收到我局欠费停止供电执行通知书,对欠费停止供电执行通知书张贴在自家门口十分不满,已按口径向客户解释,客户不接受,表示损害客户的个人名义权,现客户咨询我局是否有规定可以张贴欠费停止供电执行通知书,如有这项规定请出示书面文字,请相关人员联系处理。  
------------------------------------------------------------------------------------------
回访工单08000010000027535650关于领取发票问题。
客户表示已有工作人员联系跟进,现客户就领取发票问题提出建议:
客户对于我局前往营业厅领取发票的手续感到繁琐,客户表示现时大部分都是通过银行划账进行缴费,很多客户不愿意为了发票特地前往银行打印缴费凭证,现客户建议我局对于领取发票的手续可以优化,取消缴费凭证这一项,请记录。
------------------------------------------------------------------------------------------
(该号码近6个月来电总数:1,投诉数:0)南洲北路燕安一街239号904房,客户反映对201805期电费在2018-5-27 00:00已缴费,但在5-28日家门口张贴了一张欠费停电通知书的情况有意见,要求注意以后派送通知单的时间,请贵局核查。
------------------------------------------------------------------------------------------
(该号码近6个月来电总数:1,投诉数:0)客户反映我局于201805期电费,对其收取了1.34元违约金,对我局收取违约金表示不满(描述不满的原因),表示从未收到我局欠费停电通知书或通知信息,中心已向客户解释,客户不接受,要求将其退还。请协调处理。  客户要求今天内联系答复。中心只作记录。
------------------------------------------------------------------------------------------
(该号码近6个月来电总数:6,投诉数:0)
1、客户来电反映上址为新户表工程,表示当时已将该户的业主姓名,联系电话,银行划账等相关资料递交给我局,但该户的户名和结算户名显示的是地址,并无登记个人名字,客户于5月25号在我局微信公众号办理过户(工单08000010000027518501)业务,今天本机号码接收到过户不成功的短信,中心已向客户解释因该户没有上传相关的产权资料故办理过户不成功,客户不满,表示产权证正在办理,表示上址小区其他住户都是上传完税证明和购房发票都可以办理,为何客户办理就不通过,客户十分不满意,表示完税证明和购房发票足以证明该房子是王先生的,中心尝试解释,客户不接受,要求我局尽快为其办理该户的过户业务,并表示当时王先生已将该户业主的相关资料递交给我局,表示是我局没有登记好属我局责任,要求尽快更正户名
2、王先生反映该户已经办理了银行划账业务,但我局登记的结算户名是鹤园中一巷1号1号门1611房,导致客户不能开具出个人名字的发票,要求我局尽快更正结算户名,请协办。
(客户通话过程中多次提及投诉,和表示如果我局再处理不好客户将反映给市委,已上报值长催办)
------------------------------------------------------------------------------------------
(该号码近6个月来电总数:6,投诉数:0)客户反映我局于201805期电费,对其收取了1元违约金,对我局收取违约金表示不满,称在2018年4月初到我局营业厅办理更改银行划扣业务(客户不清楚具体到那间营业厅办理),称当时营业厅工作人员告知客户已更改成功,现客户要求划扣201805期电费,经查询系统,划扣失败,失败原因为:借记卡对帐折,该项业务不支持,中心已向客户解释,但客户不接受,表示当时到营业厅办理更改银行划扣时为何不告知客户对账折不能用于办理银行划扣业务,通话中要求投诉营业厅工作人员没有告知客户对账折不能用于划扣电费,现客户要求我局尽快核查清楚,并退还违约金,请处理。注:已建议客户通过其他方式缴交201805期电费,客户同意。
------------------------------------------------------------------------------------------

感觉有一些效果,基本上是一个Multinomial Bayes加上典型的Semi-supervised training做的效果。选择参数是epoch=5num_steps=10.

具体参照了一下这个截图里的过程。

Naive Bayes本质:

假设的是个特征本身是独立的:

我们假设的$p(x_i \mid y)$是multinomial分布的,这意味着每个文章在归内的时候只考虑它自己本身存在的词汇。具体Naive Bayes与multinomial结合算出来的最优解可以去查看Andrew Ng的课件。

现在让我们利用统计一下4月和5月的数量对比。从结果看5月份比4月份还上升了。算法本身或需要更多的业务人员的标注。数据本身也需要提升本身的质量。

resulting_data = data.iloc[result_index_list]
april_complains = resulting_data[resulting_data.受理时间.dt.month == 4]
may_complains = resulting_data[resulting_data.受理时间.dt.month == 5]
april_counts = len(april_complains)
may_counts = len(may_complains)
april_may_changes = ((may_counts - april_counts) / april_counts) * 100 
print("使用自定义的分类器统计的,四月份因系统问题产生抱怨数{}, 五月份因系统问题产生抱怨数{}, 变化率{:.2f}%".format(april_counts, may_counts, april_may_changes))
使用自定义的分类器统计的,四月份因系统问题产生抱怨数54, 五月份因系统问题产生抱怨数71, 变化率31.48%
april_complains.to_excel("april_complains.xlsx")
may_complains.to_excel("may_complains.xlsx")