运行环境
python3.7、PyCharm 2018.2.4 (Community Edition)
数据来源
思路
从所给数据及其说明文档可以看出此数据集是从购物数据中收集而来的,每行数据都是一条购物记录,每行数据中的元素包括了此次购物所买的东西,不考虑数量,只考虑种类,即每行数据都记录了此次购物购买了哪几种商品,每种商品用序号标识,共有88162条记录。现在要求找出哪些商品是顾客经常一起买的,一起买的频率是多大,即找出这些商品中的频繁项集,考虑采用FP-Growth算法进行分析,先构建此数据集的FP树,然后从这棵FP树中搜寻其频繁项集,最后返回打印频繁项集,另外还将FP树进行可视化。
代码包括两个文件,一个文件FP-Growth.py用来读取数据文件,用数据集构建FP树并保存,然后找出其频繁项集打印输出,另一个文件plotTree.py用来读取保存的FP树,然后对其进行可视化(采用graphviz包进行可视化)。
源代码
FP-Growth.py
#-*- coding: utf-8 -*-
#Author: Yinli
import pickle
def loadData(filename):
fr = open(filename, 'r', encoding='utf-8')
arrayOfLines = fr.readlines()
dataSet = []
for i in range(len(arrayOfLines)):
arrayOfLines[i] = arrayOfLines[i].rstrip('\n')
currentList = arrayOfLines[i].split(',')
dataSet.append(currentList)
return dataSet
#定义树节点数据结构
class treeNode:
#构造函数
def __init__(self, nameValue, numOccur, parentNode):
self.name = nameValue#节点名字
self.count = numOccur#节点的计数值
self.nodeLink = None#用于指向下一个名字相同的节点
self.parent = parentNode#指向父节点
self.children = {}#子节点用字典存储
#增加节点计数值
def inc(self, numOccur):
self.count += numOccur
#打印输出此节点为根的树
def disp(self, ind=1):
print(' ' *ind, self.name, ' ', self.count)
for child in self.children.values():
child.disp(ind +1)
#用传入的数据集构造一个字典,键为每行数据的不可变集合,值为1,即数量
def createInitSet(dataSet):
retDict = {}
for trans in dataSet:
retDict[frozenset(trans)] = 1
return retDict
def createTree(dataSet, minSup=1):
#初始化头指针表
headerTable = {}
#遍历数据集中每个单个数据,统计频度
for trans in dataSet:
for item in trans:
headerTable[item] = headerTable.get(item, 0) + dataSet[trans]
#遍历头指针表,删除频度小于指定值的元素
for k in list(headerTable.keys()):
if headerTable[k] < minSup:
del(headerTable[k])
#统计频繁单元素,构造集合
freqItemSet = set(headerTable.keys())
#如果没有频繁单元素返回None
if len(freqItemSet) == 0:
return None,None
#字典值中添加每个元素的指针,初始化为None
for k in headerTable:
headerTable[k] = [headerTable[k], None]
#根节点设为空
retTree = treeNode('Null Set', 1, None)
#遍历每组数据
for tranSet, count in dataSet.items():
#初始化记录每组数据的字典
localD = {}
#遍历数据中的每个元素,若频繁则记录其频度
for item in tranSet:
if item in freqItemSet:
localD[item] = headerTable[item][0]
#若这组数据中有频繁元素
if len(localD)>0:
#将数据中的元素按频度从大到小排序
orderedItems = [v[0] for v in sorted(localD.items(),key=lambda p:p[1], reverse=True)]
#更新节点
updateTree(orderedItems, retTree, headerTable, count)
return retTree, headerTable
#更新节点
#items是处理过后的只含频繁元素的按频度排序的一组数据
#inTree是要更新的节点,count是此组数据的频度
def updateTree(items, inTree, headerTable, count):
#若第一个元素在子节点中,增加频度,否则生成子节点
if items[0] in inTree.children:
inTree.children[items[0]].inc(count)
#否则生成子节点并更新头指针表
else:
inTree.children[items[0]] = treeNode(items[0], count, inTree)
#若当前元素在头指针表中的指针指向空,则令其指向当前子节点
if headerTable[items[0]][1] == None:
headerTable[items[0]][1] = inTree.children[items[0]]
#否则遍历当前元素头指针,将当前节点接在最末尾
else:
updateHeader(headerTable[items[0]][1], inTree.children[items[0]])
#若还有频繁元素,递归更新子节点
if len(items) > 1:
updateTree(items[1::], inTree.children[items[0]], headerTable, count)
#更新头指针表
def updateHeader(nodeToTest, targetNode):
#指针一路向下,直到最末尾
while(nodeToTest.nodeLink != None):
nodeToTest = nodeToTest.nodeLink
#将目标节点接在最末尾
nodeToTest.nodeLink = targetNode
#从leafNode节点一路向上开始找它的前缀路径
def ascendTree(leafNode, prefixPath):
if leafNode.parent != None:
prefixPath.append(leafNode.name)
ascendTree(leafNode.parent, prefixPath)
#找treeNode及其相似节点的前缀路径集合即条件模式基并存入返回
def findPrefixPath(basePat, treeNode):
#初始化前缀路径集合字典
condPats = {}
while treeNode != None:
#初始化前缀路径
prefixPath = []
#向上回溯记录前缀路径
ascendTree(treeNode, prefixPath)
#若前缀路径存在,存入字典,路径为键,频数为值
if len(prefixPath) > 1:
condPats[frozenset(prefixPath[1:])] = treeNode.count
#继续遍历下一个名字相同的相似节点
treeNode = treeNode.nodeLink
return condPats
#挖掘FP树中的频繁项集,minSup为阈值
#prefix为当前的前缀,freqItemList用来记录频繁项集
def mineTree(inTree, headerTable, minSup, prefix, freqItemList):
#首先将头指针表中的频繁元素从小到大排序
bigL = [v[0] for v in sorted(headerTable.items(), key=lambda p:p[0])]
#遍历每个频繁元素
for basePat in bigL:
#newFreqSet记录加了当前节点的前缀
newFreqSet = prefix.copy()
newFreqSet.add(basePat)
#将当前前缀加入到频繁项集列表中
freqItemList.append(newFreqSet)
#求当前元素的前缀路径集及条件模式基
condPattBases = findPrefixPath(basePat, headerTable[basePat][1])
#对此条件模式基构造FP树
myCondTree, myHead = createTree(condPattBases, minSup)
#若树不为空,即此条件模式基中含有频繁元素
#则递归地挖掘此FP树中的频繁项集
if(myHead != None):
mineTree(myCondTree, myHead, minSup, newFreqSet, freqItemList)
#用pickle方法将结果存在文件中
def storeResult(input, storeFilename):
with open(storeFilename, 'wb') as fw:
pickle.dump(input, fw)
#主函数
if __name__ == '__main__':
#从文件中导入数据
filename = r"D:\python_things\code\第5次作业更新\FP树数据集\retail.csv"
dataSet = loadData(filename)
#将数据集初始化为字典
dataSet = createInitSet(dataSet)
#设定阈值
minSup = 10000
#对数据集构造FP树并打印查看
myTree, myHeaderTable = createTree(dataSet, minSup)
myTree.disp()
treeFilename = r"D:\python_things\code\第5次作业更新\storedTree.pkl"
storeResult(myTree, treeFilename)
#初始化频繁项集列表,挖掘FP树的频繁项集并打印查看
freqItems = []
mineTree(myTree, myHeaderTable, 5000 ,set([]), freqItems)
print("频繁项集:")
for i in range(len(freqItems)):
if(len(freqItems[i]) > 1):
print(freqItems[i])
plotTree.py
#-*- coding: utf-8 -*-
#Author: Yinli
from graphviz import Digraph
import pickle
#定义树节点数据结构
class treeNode:
#构造函数
def __init__(self, nameValue, numOccur, parentNode):
self.name = nameValue#节点名字
self.count = numOccur#节点的计数值
self.nodeLink = None#用于指向下一个名字相同的节点
self.parent = parentNode#指向父节点
self.children = {}#子节点用字典存储
#增加节点计数值
def inc(self, numOccur):
self.count += numOccur
#打印输出此节点为根的树
def disp(self, ind=1):
print(' ' *ind, self.name, ' ', self.count)
for child in self.children.values():
child.disp(ind +1)
#从文件中读取分类结果
def loadResult(storeFilename):
fr = open(storeFilename,'rb')
return pickle.load(fr)
#传入结点,将以此结点为根的FP树画出来
def plotTree(inTree, dot):
#结点名字
currentNodeName = 'No.' + inTree.name + ' : ' + str(inTree.count)
#结点标签,用于标识每个唯一的结点
currentNodeLabel = inTree.name + '_' + str(inTree.count)
#将当前结点画出来
dot.node(currentNodeLabel, currentNodeName)
#如果此结点有孩子
if (inTree.children != {}):
#则对于每个孩子递归调用plotTree
for key in inTree.children.keys():
#返回此孩子结点的唯一标签
childrenNodeLabel = plotTree(inTree.children[key], dot)
#将当前结点和此孩子结点连接起来
dot.edge(currentNodeLabel,childrenNodeLabel)
#返回当前结点的唯一标签
return currentNodeLabel
#主函数
if __name__ == '__main__':
#读取FP树的根节点
treeFilename = r"D:\python_things\code\第5次作业更新\storedTree.pkl"
myTree = loadResult(treeFilename)
#将FP树画出来并保存
dot = Digraph(comment='tree')
plotTree(myTree, dot)
dot.render('tree.gv', view=True)
运行结果
将构建FP树的最小阈值设定为10000,即在这8w+条数据中频度超过10000才算频繁项,寻找频繁项集的最小阈值也设定为5000,得到的FP树和频繁项集如下所示:
FP树
频繁项集
结果分析
从结果来看,一起购买次数超过5000次的频繁项集有以上10组,这为卖场的营销策略提供了支持,可以认为分析工作是有效的。