Оптимизировать алгоритм для создания списка предметов, оцененных вместе, в Python - PullRequest
3 голосов
/ 24 июня 2010

дан список событий покупки (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, я перебираю его, просматриваю каждого пользователя, который купил предмет, и просматриваю другие покупки пользователей.Я признаю, что это не самый питонический способ делать вещи - я пытаюсь убедиться, что получаю правильный результат (которым я являюсь), прежде чем увлечься 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

Есть либолее эффективный способ, которым я могу сделать это?Кроме того, если для этого типа операции есть подходящее академическое название, я бы хотел услышать его.

edit: уточнено, чтобы включить тот факт, что я не ограничиваю покупки предметами, купленными одновременновремя.Предметы можно купить в любое время.

Ответы [ 4 ]

3 голосов
/ 24 июня 2010

Вам действительно нужно предварительно вычислить все возможные пары? Что, если бы вы делали это лениво, то есть по требованию?

Это может быть представлено в виде 2D матрицы. Строки соответствуют клиентам, а столбцы соответствуют продуктам.

Каждая запись имеет значение 0 или 1, сообщая, был ли продукт, соответствующий столбцу, куплен клиентом, соответствующим строке.

Если вы посмотрите на каждый столбец как вектор (около 5000) нулей и единиц, то количество раз, когда два продукта были куплены вместе, является просто точечным произведением соответствующих векторов!

Таким образом, вы можете сначала просто вычислить эти векторы, а затем вычислить точечное произведение по требованию.

Чтобы вычислить скалярное произведение:

Теперь хорошим представлением вектора, имеющего только 0 и 1, является массив целых чисел, который в основном является растровым изображением.

Для 5000 записей вам потребуется массив из 79 64-битных целых чисел.

Итак, учитывая два таких массива, вам нужно посчитать количество общих единиц.

Чтобы подсчитать количество битов, общих для двух целых чисел, сначала вы можете выполнить побитовое И, а затем сосчитать числа 1, которые установлены в полученном числе.

Для этого вы можете использовать таблицы поиска или некоторые методы битового монтирования (не уверен, что Python их поддержит), как здесь: http://graphics.stanford.edu/~seander/bithacks.html

Итак, ваш алгоритм будет выглядеть примерно так:

  • Инициализировать массив из 79 64-битных целых чисел для каждого продукта.

  • Для каждого покупателя посмотрите на купленные товары и установите соответствующий бит для этого покупателя в соответствующих товарах.

  • Теперь, учитывая запрос двух продуктов, для которых вам нужно узнать количество клиентов, которые купили их вместе, просто возьмите точечный продукт, как описано выше.

Это должно быть достаточно быстро.

В качестве дополнительной оптимизации вы можете рассмотреть возможность группировки клиентов.

2 голосов
/ 24 июня 2010
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}

(Это касается вашего первоначального запроса группировки товаров по событию покупки. Чтобы сгруппировать по пользователю, просто измените первый ключ в списке событий с номера события на идентификатор пользователя.)

1 голос
/ 25 июня 2010

Довольно странно видеть, что каждый раз, когда вы хотите получить статистику, все вышеупомянутые решения перемещаются по всей базе данных для получения подсчетов.

Предлагает хранить данные в неизменном виде, индексировать и получать результаты только для определенного элементапо одному.Если у вас большое количество предметов, оно будет более эффективным.

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
1 голос
/ 24 июня 2010

Ответ Павла может быть лучшим, но вот что я придумал после обеденного перерыва (непроверенный, по общему признанию, но все еще забавное упражнение в размышлении). Не уверен в скорости / оптимизации моего алгоритма. Я лично предложил бы взглянуть на что-то вроде MongoDB, базы данных NoSQL, так как кажется, что она может пригодиться для решения такого рода проблем (что с map / проводить и все остальное)

# 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
...