【问题标题】:Optimize algorithm for creating a list of items rated together, in Python优化算法以在 Python 中创建一起评分的项目列表
【发布时间】:2010-07-05 07:39:41
【问题描述】:

给定购买事件列表(customer_id,item)

1-hammer
1-screwdriver
1-nails
2-hammer
2-nails
3-screws
3-screwdriver
4-nails
4-screws

我正在尝试构建一个数据结构,该结构可以告诉您一件商品与另一件商品一起购买了多少次。不是同时买的,是我开始存数据的时候买的。结果看起来像

{
       hammer : {screwdriver : 1, nails : 2}, 
  screwdriver : {hammer : 1, screws : 1, nails : 1}, 
       screws : {screwdriver : 1, nails : 1}, 
        nails : {hammer : 1, screws : 1, screwdriver : 1}
}

表示用钉子买了两次锤子(人 1,3),用螺丝刀买了一次(人 1),用螺丝刀买了一次螺丝(人 3),依此类推......

我目前的做法是

users = dict 其中 userid 是键,购买的物品列表是值

usersForItem = dict 其中 itemid 是键,购买商品的用户列表是值

userlist = 对当前项目评分的用户的临时列表

pseudo:
for each event(customer,item)(sorted by item):
  add user to users dict if not exists, and add the items
  add item to items dict if not exists, and add the user
----------

for item,user in rows:

  # add the user to the users dict if they don't already exist.
  users[user]=users.get(user,[])

  # append the current item_id to the list of items rated by the current user
  users[user].append(item)

  if item != last_item:
    # we just started a new item which means we just finished processing an item
    # write the userlist for the last item to the usersForItem dictionary.
    if last_item != None:
      usersForItem[last_item]=userlist

    userlist=[user]

    last_item = item
    items.append(item)
  else:
    userlist.append(user)

usersForItem[last_item]=userlist   

所以,在这一点上,我有 2 个字典 - 谁买了什么,谁买了什么。这就是棘手的地方。现在 usersForItem 已填充,我循环遍历它,循环遍历每个购买该项目的用户,并查看用户的其他购买。我承认这不是最 Pythonic 的做事方式 - 我试图确保在使用 Python 之前得到正确的结果(我就是这样)。

relatedItems = {}
for key,listOfUsers in usersForItem.iteritems():
  relatedItems[key]={}
  related=[]

  for ux in listOfReaders:
    for itemRead in users[ux]:
      if itemRead != key:
        if itemRead not in related:
          related.append(itemRead)
        relatedItems[key][itemRead]= relatedItems[key].get(itemRead,0) + 1    

  calc jaccard/tanimoto similarity between relatedItems[key] and its values

有没有更有效的方法可以做到这一点?另外,如果这种手术有合适的学术名称,我很想听听。

编辑:澄清包括我不限制购买同时购买的物品这一事实。物品可以随时购买。

【问题讨论】:

  • 你的数据集有多大?亚马逊在 hadoop 上使用了一些算法来查看它曾经销售过的每件商品并生成建议的商品......另外,请参阅stackoverflow.com/questions/1849693/…
  • 假设有 200 万个事件,大约有 5000 个用户和 10000 个项目。 K-最近邻不会涉及将每个项目与其他项目进行比较,即使是没有重叠的项目?
  • 如果您只关心商品一起购买了多少次,为什么还要担心用户?您可以简单地按购买事件进行分组,这样您就可以将产品字典与与他们一起购买的其他产品相关联。事实上,您可以将每个产品与其他产品的最大堆关联,其值是它们一起出现的次数,这样您就可以有效地获得任何给定项目的最常购买的项目. (堆位可能会走得太远)。如果这对您来说是一个可行的想法,我可以写一个更完整的答案。
  • Anthony 这正是我正在寻找的反馈。请详细说明。我不太关心用户 ID,以至于我想计算同一用户购买商品 a 和 b 的次数。
  • 您是否在查找同一用户的购买次数?或者它们在同一个用户中存在的次数? IE。重要的是我同时买了锤子和钉子,还是我买了锤子,然后在其他时间回来买钉子。

标签: python algorithm optimization similarity


【解决方案1】:

您真的需要预先计算所有可能的配对吗?如果你懒惰地做,即按需做呢?

这可以表示为二维矩阵。行对应客户,列对应产品。

每个条目是0或1,表示该列对应的产品是否被该行对应的客户购买。

如果把每一列看成一个(大约5000个)0和1的向量,那么两个产品一起购买的次数就是对应向量的点积!

因此,您可以先计算这些向量,然后根据需要懒惰地计算点积。

计算点积:

现在,只有 0 和 1 的向量的一个很好的表示是整数数组,它基本上是一个位图。

对于 5000 个条目,您将需要一个由 79 个 64 位整数组成的数组。

所以给定两个这样的数组,你需要计算常见的 1 的数量。

要计算两个整数共有的位数,首先可以进行按位与,然后计算结果数中设置的 1 的数量。

为此,您可以使用查找表或一些位计数方法(不确定 python 是否支持它们),例如:http://graphics.stanford.edu/~seander/bithacks.html

所以你的算法会是这样的:

  • 为每个产品初始化一个由 79 个 64 位整数组成的数组。

  • 对于每个客户,查看购买的产品并在相应产品中为该客户设置适当的位。

  • 现在给定两个产品的查询,您需要知道一起购买它们的客户数量,只需如上所述的点积。

这应该相当快。

作为进一步的优化,您可以考虑对客户进行分组。

【讨论】:

  • 我不敢相信我自己没想过要使用点积,尤其是来自另一个涉及余弦相似性的项目!谢谢。享受您的支持。
  • 但是,如果我创建一个 2D 矩阵,Order-N-Squared 会不会很高,因为我将每个项目都与其他项目进行比较?我考虑使用 Jaccard/Tanimoto 的原因之一是它让我不必比较不相关的项目。
  • @Neil:您正在为每个产品创建一个向量。初始化一个向量是 O(M)(M 是客户的数量),但这对于内存位图来说真的很快,你可以在块中清零。完成初始化后,处理成本为 O(S),其中 S 是 1 的数量,然后是每个查询的 O(M)(给定两个产品)。您的问题基本上是确定集合交集的大小,因此根据稀疏性,使用 dicts 表示您的集合来表示购买产品的客户集合可能会更好。对于大约 5000 名客户来说,这可能并不重要。
  • ...继续... 字典有计算哈希键等的开销,而位图没有。所以选择真的取决于你的数据,比如稀疏性等。当然,我想,dicts 更容易编码。顺便说一句,位图/向量只是散列的另一种形式,就像字典一样。
  • 白痴,所以为了简单起见,我夸大了一点 - 成千上万的客户和数万件商品怎么样?
【解决方案2】:
events = """\
1-hammer 
1-screwdriver 
1-nails 
2-hammer 
2-nails 
3-screws 
3-screwdriver 
4-nails 
4-screws""".splitlines()
events = sorted(map(str.strip,e.split('-')) for e in events)

from collections import defaultdict
from itertools import groupby

# tally each occurrence of each pair of items
summary = defaultdict(int)
for val,items in groupby(events, key=lambda x:x[0]):
    items = sorted(it[1] for it in items)
    for i,item1 in enumerate(items):
        for item2 in items[i+1:]:
            summary[(item1,item2)] += 1
            summary[(item2,item1)] += 1

# now convert raw pair counts into friendlier lookup table
pairmap = defaultdict(dict)
for k,v in summary.items():
    item1, item2 = k
    pairmap[item1][item2] = v

# print the results    
for k,v in sorted(pairmap.items()):
    print k,':',v

给予:

hammer : {'nails': 2, 'screwdriver': 1}
nails : {'screws': 1, 'hammer': 2, 'screwdriver': 1}
screwdriver : {'screws': 1, 'nails': 1, 'hammer': 1}
screws : {'nails': 1, 'screwdriver': 1}

(这解决了您按购买事件对项目进行分组的初始请求。要按用户分组,只需将事件列表的第一个键从事件编号更改为用户 ID。)

【讨论】:

  • 你让我在“events = sorted(map(str.strip,e.split('-')) for e in events)”
  • @Neil - 谢谢!对于 Python 3,它将是 events = sorted(tuple(map(str.strip,e.split('-'))) for e in events)
【解决方案3】:

Paul 的答案可能是最好的,但这是我在午休时想出的(诚然,未经测试,但仍然是一个有趣的思考练习)。不确定我的算法的速度/优化。我个人建议看一下 MongoDB,一个 NoSQL 数据库,因为它似乎很适合解决这类问题(map/reduce 等等)

# assuming events is a dictionary of id keyed to item bought...
user = {}
for (cust_id, item) in events:
    if not cust_id in users:
        user[cust_id] = set()
    user[cust_id].add(item)
# now we have a dictionary of cust_ids keyed to a set of every item
# they've ever bought (given that repeats don't matter)
# now we construct a dict of items keyed to a dictionary of other items
# which are in turn keyed to num times present
items = {}
def insertOrIter(d, k, v):
    if k in d:
        d[k] += v
    else:
        d[k] = v
for key in user:
    # keep track of items bought with each other
    itemsbyuser = []
    for item in user[key]:
        # make sure the item with dict is set up
        if not item in items:
            items[item] = {}
        # as we see each item, add to it others and others to it
        for other in itemsbyuser:
            insertOrIter(items[other], item, 1)
            insertOrIter(items[item], other, 1)
        itemsbyuser.append(item)
# now, unless i've screwed up my logic, we have a dictionary of items keyed
# to a dictionary of other items keyed to how many times they've been
# bought with the first item. *whew* 
# If you want something more (potentially) useful, we just turn that around to be a
# dictionary of items keyed to a list of tuples of (times seen, other item) and
# you're good to go.
useful = {}
for i in items:
    temp = []
    for other in items[i]:
        temp[].append((items[i][other], other))
    useful[i] = sorted(temp, reverse=True)
# Now you should have a dictionary of items keyed to tuples of
# (number times bought with item, other item) sorted in descending order of
# number of times bought together

【讨论】:

  • 拥抱默认字典!在更新它的值之前不要再检查字典键的存在 - 只需访问该键并让 defaultdict 初始化它(如果它不存在)。最容易维护的代码是不存在的代码。
【解决方案4】:

很奇怪,每次您想要获取统计信息时,上述所有解决方案都会翻遍整个数据库以获取计数。

建议将数据保持在平坦的索引中,并且只获取特定项目的结果,一次一个。如果您的项目数量很大,我会更有效率。

from collections import defaultdict
from itertools import groupby

class myDB:
    '''Example of "indexed" "database" of orders <-> items on order'''
    def __init__(self):
        self.id_based_index = defaultdict(set) 
        self.item_based_index = defaultdict(set)

    def add(self, order_data):
        for id, item in order_data:
            self.id_based_index[id].add(item)
            self.item_based_index[item].add(id)

    def get_compliments(self, item):
        all_items = []
        for id in self.item_based_index[item]:
            all_items.extend(self.id_based_index[id])
        gi = groupby(sorted(all_items), lambda x: x)
        return dict([(k, len(list(g))) for k, g in gi])

使用示例:

events = """1-hammer 
    1-screwdriver 
    1-nails 
    2-hammer 
    2-nails 
    3-screws 
    3-screwdriver 
    4-nails 
    4-screws"""

db = myDB()
db.add(
    [ map(str.strip,e.split('-')) for e in events.splitlines() ]
    )
# index is incrementally increased 
db.add([['5','plunger'],['5','beer']])

# this scans and counts only needed items
assert db.get_compliments('NotToBeFound') == {}
assert db.get_compliments('hammer') == {'nails': 2, 'hammer': 2, 'screwdriver': 1}
# you get back the count for the requested product as well. Discard if not needed.

这很有趣,但是,说真的,只需要真正的数据库存储。因为索引已经内置到任何数据库引擎中,所以上面所有的 SQL 代码都只是:

select
    p_others.product_name,
    count(1) cnt
from products p
join order_product_map opm
    on p.product_id = opm.product_id
join products p_others
    on opm.product_id = p_others.product_id
where p.product_name in ('hammer')
group by p_others.product_name

【讨论】:

  • 我提出的方案可以用在缓存层,不同的查询不需要任何DB。事实上,如果发生更新,您可以更新结构(非常简单),然后延迟写入 DB 或其他什么。此外,OP 要求提供数据结构,他得到了(不仅在我的答案中,在其他答案中也是如此)。每次访问数据库都不会扩展(特别是如果您的查询中有多个连接!)。缓存成为必须。另外,我想如果 OP 想要一个 SQL 查询,他会要求的。
  • 同意 - 发布的解决方案并不是建议每次都完成这个复杂的过程,而是说执行上述过程会得到你想要的数据(然后可以存储、索引、放入一个数据库,不管是什么情况)。这是一个间歇性运行的服务器端操作,假设数据已经被计算,页面只会访问数据。至于 SQL,这就是我建议使用 MongoDB 的原因——我有一个类似的想法,即这段代码可以很好地在数据库上本地完成。不过,这是一个很好的 SQL(从没想过我会这么说)。
猜你喜欢
  • 1970-01-01
  • 2016-08-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-06-29
  • 2015-10-25
  • 1970-01-01
  • 2010-12-04
相关资源
最近更新 更多