Django / Python - Группировка объектов по общему набору из отношений «многие ко многим» - PullRequest
7 голосов
/ 02 октября 2008

Это частичный алгоритм-логический вопрос (как это сделать), частичный вопрос реализации (как это сделать лучше всего!). Я работаю с Django, поэтому я решил поделиться этим.

В Python стоит упомянуть, что проблема в некоторой степени связана с how-do-i-use-pythons-itertoolsgroupby .

Предположим, вы получили два класса, производных от Django Model:

from django.db import models

class Car(models.Model):
    mods = models.ManyToManyField(Representative)

и

from django.db import models

class Mods(models.Model):
   ...

Как получить список автомобилей, сгруппированных по автомобилям с общим набором модов?

т.е. Я хочу получить класс Likeo:

Cars_by_common_mods = [ 
  { mods: { 'a' }, cars: { 'W1', 'W2' } },
  { mods: { 'a', 'b' }, cars: { 'X1', 'X2', 'X3' }, },
  { mods: { 'b' }, cars: { 'Y1', 'Y2' } },
  { mods: { 'a', 'b', 'c' }, cars: { 'Z1' } },
]

Я думал о чем-то вроде:

def cars_by_common_mods():
  cars = Cars.objects.all()

  mod_list = []      

  for car in cars:
    mod_list.append( { 'car': car, 'mods': list(car.mods.all()) } 

  ret = []

  for key, mods_group in groupby(list(mods), lambda x: set(x.mods)):
    ret.append(mods_group)

  return ret

Однако, это не работает, потому что (возможно, среди других причин), кажется, что groupby не группирует по наборам модов. Я думаю, что mod_list должен быть отсортирован для работы с groupby. В общем, я уверен, что есть что-то простое и элегантное, которое будет и просветляющим, и просветляющим.

Приветствия и спасибо!

Ответы [ 5 ]

4 голосов
/ 02 октября 2008

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

import itertools

cars = [
    {'car': 'X2', 'mods': [1,2]},
    {'car': 'Y2', 'mods': [2]},
    {'car': 'W2', 'mods': [1]},
    {'car': 'X1', 'mods': [1,2]},
    {'car': 'W1', 'mods': [1]},
    {'car': 'Y1', 'mods': [2]},
    {'car': 'Z1', 'mods': [1,2,3]},
    {'car': 'X3', 'mods': [1,2]},
]

cars.sort(key=lambda car: car['mods'])

cars_by_common_mods = {}
for k, g in itertools.groupby(cars, lambda car: car['mods']):
    cars_by_common_mods[frozenset(k)] = [car['car'] for car in g]

print cars_by_common_mods

Теперь по поводу этих запросов:

import collections
import itertools
from operator import itemgetter

from django.db import connection

cursor = connection.cursor()
cursor.execute('SELECT car_id, mod_id FROM someapp_car_mod ORDER BY 1, 2')
cars = collections.defaultdict(list)
for row in cursor.fetchall():
    cars[row[0]].append(row[1])

# Here's one I prepared earlier, which emulates the sample data we've been working
# with so far, but using the car id instead of the previous string.
cars = {
    1: [1,2],
    2: [2],
    3: [1],
    4: [1,2],
    5: [1],
    6: [2],
    7: [1,2,3],
    8: [1,2],
}

sorted_cars = sorted(cars.iteritems(), key=itemgetter(1))
cars_by_common_mods = []
for k, g in itertools.groupby(sorted_cars, key=itemgetter(1)):
    cars_by_common_mods.append({'mods': k, 'cars': map(itemgetter(0), g)})

print cars_by_common_mods

# Which, for the sample data gives me (reformatted by hand for clarity)
[{'cars': [3, 5],    'mods': [1]},
 {'cars': [1, 4, 8], 'mods': [1, 2]},
 {'cars': [7],       'mods': [1, 2, 3]},
 {'cars': [2, 6],    'mods': [2]}]

Теперь, когда у вас есть списки идентификаторов автомобилей и модов, если вам нужны полные объекты для работы, вы можете выполнить один запрос для каждого из них, чтобы получить полный список для каждой модели и создать поиск dict для тех, кто пользуется их идентификаторами - тогда, я полагаю, Боб - брат твоего пресловутого отца.

2 голосов
/ 02 октября 2008

чек перегруппировать . это только для шаблонов, но я думаю, что такая классификация в любом случае относится к уровню представления.

1 голос
/ 06 октября 2008

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

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

1 голос
/ 02 октября 2008

У вас есть несколько проблем здесь.

Вы не сортировали свой список до вызова groupby, и это необходимо. Из документации itertools :

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

Тогда вы не дублируете список, возвращаемый groupby. Опять документация гласит:

Возвращенная группа сама является итератором, который разделяет базовый итерируемый с группа по(). Поскольку источник является общим, когда объект groupby продвинут, предыдущая группа больше не видна. Итак, если эти данные понадобятся позже, храниться в виде списка:

groups = []
uniquekeys = []
for k, g in groupby(data, keyfunc):
    groups.append(list(g))      # Store group iterator as a list
    uniquekeys.append(k)

И последняя ошибка - использование наборов в качестве ключей. Они не работают здесь. Быстрое решение состоит в том, чтобы привести их к отсортированным кортежам (могло бы быть лучшее решение, но я не могу думать об этом сейчас).

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

sortMethod = lambda x: tuple(sorted(set(x.mods)))
sortedMods = sorted(list(mods), key=sortMethod)
for key, mods_group in groupby(sortedMods, sortMethod):
    ret.append(list(mods_group))
0 голосов
/ 07 октября 2008

Спасибо всем за полезные ответы. Я решил эту проблему. «Лучшее» решение все еще ускользает от меня, но у меня есть некоторые мысли.

Я должен отметить, что статистика набора данных, с которым я работаю. В 75% случаев будет один мод. В 24% случаев два. В 1% случаев будет ноль или три или более. Для каждого мода есть хотя бы один уникальный автомобиль, хотя мод может применяться ко многим автомобилям.

Сказав это, я подумал (но не реализовал) что-то вроде этого:

class ModSet(models.Model):
  mods = models.ManyToManyField(Mod)

и поменяйте авто на

class Car(models.Model):
  modset = models.ForeignKey(ModSet)

Группировать по Car.modset тривиально: я могу использовать regroup, как, например, предложил Хавьер. Это кажется более простым и достаточно элегантным решением; мысли будут высоко ценится.

...