机器学习实战--FP-growth算法挖掘频繁项集

FP-growth是频繁项集挖掘的一种优化算法。它先将事务先构造成一棵FP树,这样一来就不用像Apriori一项反复地扫描原来的事务了,大大提高了效率。这篇笔记主要是记录FP-growth的Python3代码实现的,会就算法来讲解FP-growth挖掘频繁项集的步骤,算法的详细内容在《数据挖掘-概念与技术》一书中有非常详细的讲解,这里不再赘述

完整代码在这里

  1. 数据格式

    类似下方的二维数组,每一个list代表一个transaction,里面的每个数都是item的编号

    1
    [[1,2,5], [2,4], [2,3], [1,2,4],[1,3], [2,3], [1,3], [1,2,3,5], [1,2,3]]
  2. 读取数据

    1
    2
    3
    4
    5
    6
    7
    8
    def loadDataSet():
    thing_arr = []

    with open('data/Groceries.txt', 'r') as f:
    X = f.read()
    thing_arr = json.loads(X)
    f.close()
    return thing_arr

  3. FP-Growth实现起来相对复杂,需要定义树结点结构

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    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)

  4. 初始化transaction计数

    1
    2
    3
    4
    5
    def createInitSet(dataSet):
    retDict = {}
    for trans in dataSet:
    retDict[frozenset(trans)] = 1
    return retDict

  5. 建立FP树

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    def createTree(dataSet, minSup = 1):
    '''
    创建根结点以及搜索链表表头
    :param dataSet:
    :param minSup:
    :return:
    '''

    # 搜索链表头
    headerTable = {}
    # 在搜索用的链表头除记录每个item的频数
    for trans in dataSet:
    for item in trans:
    headerTable[item] = headerTable.get(item, 0) + dataSet[trans]

    # 小于最小支持度的item不用考虑
    for k in list(headerTable.keys()):
    if headerTable[k] < minSup:
    del(headerTable[k])
    freqItemSet = set(headerTable.keys())
    # 如果不存在频繁项集则直接返回空
    if len(freqItemSet) == 0:
    return None, None
    # 为每个结点增加一个指向下一个同值结点的指针
    for k in headerTable.keys():
    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:
    # 每个transaction中的item按出现的次数从高到低排
    orderedItems = [v[0] for v in sorted(localD.items(), key=lambda p: p[1], reverse=True)]
    # 建树
    updateTree(orderedItems, retTree, headerTable, count)
    return retTree, headerTable

    def updateTree(items, inTree, headerTable, count):
    '''
    每个transaction递归更新到树上,并更新搜索链表
    :param items:
    :param inTree:
    :param headerTable:
    :param count: 每个transaction的出现次数
    :return:
    '''
    # 每个transaction的最高出现词数item直接接在root上
    if items[0] in inTree.children:
    # 有该元素项时计数值+1
    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函数
    updateTree(items[1:], inTree.children[items[0]], headerTable, count)


    def updateHeader(nodeToTest, targetNode):
    '''
    找到链表尾加上一个
    :param nodeToTest:
    :param targetNode:
    :return:
    '''
    while (nodeToTest.nodeLink != None):
    nodeToTest = nodeToTest.nodeLink
    nodeToTest.nodeLink = targetNode

  6. 在每次查询条件FP树时,需要递归寻找其条件前缀路径

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    def ascendTree(leafNode, prefixPath):
    '''
    递归寻找父节点
    :param leafNode:
    :param prefixPath:
    :return:
    '''
    if leafNode.parent != None:
    prefixPath.append(leafNode.name)
    ascendTree(leafNode.parent, prefixPath)

    def findPrefixPath(basePat, treeNode):
    condPats = {}
    while treeNode != None:
    prefixPath = []
    # 获得某个叶子节点的前缀路径
    ascendTree(treeNode, prefixPath)
    if len(prefixPath) >= 2:
    # 去掉自己获得前缀路径,且权重为当前结点的权重,用于建立条件前缀树
    condPats[frozenset(prefixPath[1:])] = treeNode.count
    treeNode = treeNode.nodeLink
    return condPats
  7. 进行频繁项挖掘

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    def mineTree(inTree, headerTable, minSup, preFix, freqItemList):
    '''
    递归查找频繁项集
    :param inTree: FP树
    :param headerTable:
    :param minSup:
    :param preFix: 当前前缀
    :param freqItemList: 存储频繁项集
    :return:
    '''
    # 从出现次数少的开始找
    bigL = [v[0] for v in sorted(headerTable.items(), key=lambda p: p[1][0])]

    for basePat in bigL:
    newFreqSet = preFix.copy()
    newFreqSet.add(basePat)
    freqItemList.append(newFreqSet)
    condPattBases = findPrefixPath(basePat, headerTable[basePat][1])
    CondTree, Header = createTree(condPattBases, minSup)

    if Header != None:
    mineTree(CondTree, Header, minSup, newFreqSet, freqItemList)