需求
目前有了一个excel的简单的决策树矩阵,如下图,有14个症状,目标是要完成一个通过回答是否满足症状来判断最有可能患的疾病。
主要分了两部分完成,第一部分首先是在本地完成逻辑功能,之后再基于flask做出接口完成一个服务。
逻辑功能
这个任务本质上就是根据用户回答的问题来对excel表格进行筛选,根据筛选出来的题目根据某种规则来确定下一个提问的问题,即下一个筛选条件是什么,直到整个excel表格只剩下一行,便可以输出结果。
操作excel我用了pandas,这也是第一次使用pandas,边用边学,发现pandas操作excel还是比较方便的。
程序主体即为下面的一个while循环,循环的条件是剩下的行数大于一
while arr1.shape[0] > 1:
print(arr1.shape[0])
print(question2lable[questionid])
j +=1
answer = input()
if answer == '是':
arr1 = arr1[(arr1[question2id[questionid]] == "\\") | (arr1[question2id[questionid]] == "是")]
list1 = []
for i in range(7, 21):
list1.append(getlistnum(arr1.iloc[:, i]))
print(list1)
questionid = get_nextquestion(list1)
elif answer == '否':
arr1 = arr1[(arr1[question2id[questionid]] == "\\") | (arr1[question2id[questionid]] == "否")]
list1 = []
for i in range(7, 21):
list1.append(getlistnum(arr1.iloc[:, i]))
questionid = get_nextquestion(list1)
初始的问题固定好之后,接下来的问题就是要如何确定下一个问题问哪个,我们的原则是能最大化保证问题的区分度,即是与否的数量越接近越好,并且无效选项“\”越少越好,所以我定义了一个分数为是减去否的绝对值加上\的数量,得分最低的选项即是下一个问题,并且假如剩下的该问题中没有了否或 没有是,即进行选择不会筛掉任何一个候选答案,则把分数设为100。具体实现如下:
#对一组数据进行计数,返回一个字典
def getlistnum(li):
li = list(li)
set1 = set(li)
dict = {}
for item in set1:
dict.update({item: li.count(item)})
return dict
# 根据剩下的条目的各项数值计算分数,返回下一个题目的序号
def get_nextquestion(list):
dict1 = {}
i = 1
#print(list)
for item in list:
#print(item)
if "否" not in item or "是" not in item: # 再次选择对之后没有意义的条目
dict1.update({i: 100})
i += 1
continue
score = abs(item["是"]-item["否"])
if "\\" in item:
score = score + item["\\"]
dict1.update({i: score})
i += 1
min_ = min(dict1, key=lambda x: dict1[x])
#print(dict1)
#print(min_)
return min_
def getlistnum(li)函数返回的dict形如:{'\\': 2, '否': 20, '是': 11}
把所有的问题列返回的字典放到一个list里,进行分数的计算,即def get_nextquestion(list),返回的值就是下一个问题的id。
循环结束后用一个for循环输出可能得到的结果即可。
print("您最可能患的疾病是:\n")
for i in range(1, 7):
#print(np.isnan(arr1.iloc[0, i]))
if str(arr1.iloc[0, i]) == 'nan':
continue
print(arr1.iloc[0, i])
print("\n共询问了%d轮" % j)
效果如下:
服务搭建
要做一个基于flask的服务,其实就是建一个接口,第一次请求时返回给第一个题目,之后每次请求要带着答案参数,进入之前写的逻辑里面判断出下一个问题是什么然后返回下一个题目。
首先要把之前写的脚本进行封装,封装成函数,可以在请求的函数下调用,设置好需要的参数。还要考虑到因为我的每次请求,都要重新调用逻辑,所以我所有的中间结果都需要保存,这里用了redis数据库,关于python调用redis主要参考了这篇文章。
在使用redis的过程中遇到了非常多的坑,我们定义的数据库的Key是用户的id,value是一个字典return_dict = {'tag': '', 'arr': '', 'questionid': '', 'returndata': ''}
其中解决花费时间最久的其中的中间结果arr,redis存储的value必须是byte型或者string型,所以一开始我们使用Json.dumps进行str化,但是由于Json的数据格式是完全无序的,导致我们Json.loads之后解码出来的arr行列的顺序全部打乱了,最终的解决方法是通过pickle.dumps来把字典转换成bytes格式存储到redis中,成功解决。
flask服务代码:
@app.route('/choice', methods=["GET", "POST"])
def get_choice():
userid = request.form.get('userid')
answer = request.form.get('answer')
print(r.hexists('QA', userid))
if not r.hexists('QA', userid):
print("第一次使用")
arr1, questionid = QA.get_data()
data = QA.QA(arr1, questionid, None)
r.hset('QA', userid, pickle.dumps({"arr1": data["arr"], "questionid": data["questionid"], "tag": data["tag"]}))
return data['returndata']
else:
print("不是第一次使用")
data_redis = r.hget('QA', userid)
dict = pickle.loads(data_redis)
print(dict)
if dict['tag'] == 1:
return '您的会话已结束,请重新打开测评'
arr1 = dict["arr1"]
questionid = dict['questionid']
data = QA.QA(arr1, questionid, answer)
r.hset('QA', userid, pickle.dumps({"arr1": data["arr"], "questionid": data["questionid"], "tag": data["tag"]}))
return data['returndata']
逻辑实现的代码只是进行了封装,主体没有变动,把while循环去掉,因为我每一次提出请求都会触发逻辑,就已经相当于循环完成的工作了。
逻辑里的初始化代码,当该用户id第一次出现时,进行初始化arr和questionid:
def get_data():
data = pd.read_excel('决策树矩阵.xlsx')
questionid = 3
arr1 = data
return arr1, questionid
主体逻辑函数:
def QA(arr1, questionid, answer):
return_dict = {'tag': '', 'arr': '', 'questionid': '', 'returndata': ''}
return_dict['tag'] = 0
if answer == '是':
arr1 = arr1[(arr1[question2id[questionid]] == "\\") | (arr1[question2id[questionid]] == "是")]
list1 = []
for i in range(7, 21):
list1.append(getlistnum(arr1.iloc[:, i]))
questionid = get_nextquestion(list1)
print("下一个问题:", questionid)
elif answer == '否':
arr1 = arr1[(arr1[question2id[questionid]] == "\\") | (arr1[question2id[questionid]] == "否")]
list1 = []
for i in range(7, 21):
list1.append(getlistnum(arr1.iloc[:, i]))
questionid = get_nextquestion(list1)
print("还剩%d行" % arr1.shape[0])
if arr1.shape[0] == 1:
return_dict['tag'] = 1
disease = []
for i in range(1, 7):
if str(arr1.iloc[0, i]) == 'nan':
continue
print(arr1.iloc[0, i])
disease.append(arr1.iloc[0, i])
return_dict['returndata'] = "您可能患的疾病是:\n"+'\n'.join(disease)
return return_dict
return_dict['arr'] = arr1
return_dict['questionid'] = questionid
return_dict['returndata'] = question2lable[questionid]
return return_dict
效果如下: