MySQL/PHP: Поиск похожих/связанных элементов по тегу/таксономии


У меня есть таблица городов, которая выглядит так.

|id| Name    |
|1 | Paris   |
|2 | London  |
|3 | New York|

У меня есть таблица тегов, которая выглядит так.

|id| tag            |
|1 | Europe         |
|2 | North America  |   
|3 | River          |

И таблица cities_tags:

|id| city_id | tag_id |
|1 | 1       | 1      | 
|2 | 1       | 3      | 
|3 | 2       | 1      |
|4 | 2       | 3      | 
|5 | 3       | 2      |     
|6 | 3       | 3      |

Как мне рассчитать, какие города наиболее тесно связаны между собой? Например. Если бы я смотрел на город 1 (Париж), результаты должны быть следующими: Лондон (2), Нью-Йорк (3)

Я нашел индекс Джаккарда, но я не уверен, как лучше всего это реализовать.

Author: Tom, 2013-08-02

5 answers

Вы спрашиваете о Как мне рассчитать, какие города наиболее тесно связаны между собой? Например. Если бы я смотрел на город 1 (Париж), результаты должны быть следующими: Лондон (2), Нью-Йорк (3) и на основе предоставленного вами набора данных есть только одна вещь, которую нужно связать, это общие теги между городами, поэтому города, которые имеют общие теги, будут самыми близкими ниже, это подзапрос, который находит города (кроме которых предоставляется для поиска ближайших городов), которые разделяет общие теги

SELECT * FROM `cities`  WHERE id IN (
SELECT city_id FROM `cities_tags` WHERE tag_id IN (
SELECT tag_id FROM `cities_tags` WHERE city_id=1) AND city_id !=1 )

Рабочий

Я предполагаю, что вы введете один из идентификаторов или названий городов, чтобы найти их ближайший, в моем случае у "Парижа" есть идентификатор

 SELECT tag_id FROM `cities_tags` WHERE city_id=1

Он найдет все идентификаторы тегов, которые есть у Парижа, затем

SELECT city_id FROM `cities_tags` WHERE tag_id IN (
    SELECT tag_id FROM `cities_tags` WHERE city_id=1) AND city_id !=1 )

Он выберет все города, кроме Парижа, у которых есть те же теги, что и у Парижа

Вот ваш Скрипка

Читая о сходстве/индексе Джаккарда нашел некоторые материал, чтобы понять, что на самом деле представляют собой термины, давайте возьмем этот пример, у нас есть два набора A & B

Установите A={A, B, C, D, E}

Набор B={I, H, G, F, E, D}

Формула для вычисления сходства жаккарда JS=(A пересекает B)/( Объединение B)

A пересекает B = {D,E}= 2

Объединение B={A, B, C, D, E, I, H, G, F} =9

JS=2/9 =0.2222222222222222

Теперь двигайтесь к своему сценарию

У Парижа есть tag_ids 1,3, поэтому мы создаем набор из этого и называем наш набор P={Европа, река}

В Лондоне есть tag_ids 1,3, поэтому мы создаем набор из этого и называем наш Набор L={Европа, река}

В Нью-Йорке есть tag_ids 2,3, поэтому мы создаем набор из этого и называем наш Установить NW={Северная Америка, Река}

Вычисление JS Парижа с Лондоном JSPL = P пересекает L/P союз L, JSPL = 2/2 = 1

Вычисление JS Парижа с Нью-Йорком JSPNW = P пересечение NW /P объединение NW, JSPNW = 1/3 = 0.3333333333

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

SELECT a.*, 
( (CASE WHEN a.`intersect` =0 THEN a.`union` ELSE a.`intersect` END ) /a.`union`) AS jaccard_index 
 FROM (
SELECT q.* ,(q.sets + q.parisset) AS `union` , 
(q.sets - q.parisset) AS `intersect`
FROM (
SELECT cities.`id`, cities.`name` , GROUP_CONCAT(tag_id SEPARATOR ',') sets ,
(SELECT  GROUP_CONCAT(tag_id SEPARATOR ',')  FROM `cities_tags` WHERE city_id= 1)AS parisset

FROM `cities_tags` 
LEFT JOIN `cities` ON (cities_tags.`city_id` = cities.`id`)
GROUP BY city_id ) q
) a ORDER BY jaccard_index DESC 

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

enter image description here

Вы можете добавить фильтр в выше запрос, чтобы не вычислять сходство с самим собой

SELECT a.*, 
( (CASE WHEN a.`intersect` =0 THEN a.`union` ELSE a.`intersect` END ) /a.`union`) AS jaccard_index 
 FROM (
SELECT q.* ,(q.sets + q.parisset) AS `union` , 
(q.sets - q.parisset) AS `intersect`
FROM (
SELECT cities.`id`, cities.`name` , GROUP_CONCAT(tag_id SEPARATOR ',') sets ,
(SELECT  GROUP_CONCAT(tag_id SEPARATOR ',')  FROM `cities_tags` WHERE city_id= 1)AS parisset

FROM `cities_tags` 
LEFT JOIN `cities` ON (cities_tags.`city_id` = cities.`id`) WHERE  cities.`id` !=1
GROUP BY city_id ) q
) a ORDER BY jaccard_index DESC

Таким образом, результат показывает, что Париж тесно связан с Лондоном, а затем связан с Нью-Йорком

Скрипка Подобия Джаккарда

 16
Author: M Khalid Junaid, 2013-08-11 09:57:42
select c.name, cnt.val/(select count(*) from cities) as jaccard_index
from cities c 
inner join 
  (
  select city_id, count(*) as val 
  from cities_tags 
  where tag_id in (select tag_id from cities_tags where city_id=1) 
  and not city_id in (1)
  group by city_id
  ) as cnt 
on c.id=cnt.city_id
order by jaccard_index desc

Этот запрос статически ссылается на city_id=1, поэтому вам придется сделать это переменной как в предложении where tag_id in, так и в предложении not city_id in.

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

|name      |jaccard_index  |
|London    |0.6667         |
|New York  |0.3333         |

Редактировать

С лучшим пониманием того, как реализовать индекс Джаккарда:

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

select c.name, 
  case -- when this city's tags are a subset of the chosen city's tags
    when not_in.cnt is null 
  then -- then the union count is the chosen city's tag count
    intersection.cnt/(select count(tag_id) from cities_tags where city_id=1) 
  else -- otherwise the union count is the chosen city's tag count plus everything not in the chosen city's tag list
    intersection.cnt/(not_in.cnt+(select count(tag_id) from cities_tags where city_id=1)) 
  end as jaccard_index
  -- Jaccard index is defined as the size of the intersection of a dataset, divided by the size of the union of a dataset
from cities c 
inner join 
  (
    --  select the count of tags for each city that match our chosen city
    select city_id, count(*) as cnt 
    from cities_tags 
    where tag_id in (select tag_id from cities_tags where city_id=1) 
    and city_id!=1
    group by city_id
  ) as intersection
on c.id=intersection.city_id
left join
  (
    -- select the count of tags for each city that are not in our chosen city's tag list
    select city_id, count(tag_id) as cnt
    from cities_tags
    where city_id!=1
    and not tag_id in (select tag_id from cities_tags where city_id=1)
    group by city_id
  ) as not_in
on c.id=not_in.city_id
order by jaccard_index desc

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

+----------+---------------+
| name     | jaccard_index |
+----------+---------------+
| London   |        1.0000 |
| New York |        0.3333 |
+----------+---------------+

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

 7
Author: Travis Hegner, 2013-08-08 13:13:51

Этот запрос без каких-либо причудливых функций или даже подзапросов. Это быстро. Просто убедитесь, что cities.id, cities_tags.id , cities_tags.city_id и cities_tags.tag_id имеют индекс.

Запросы возвращают результат, содержащий: город1, city2 и подсчитывают, сколько общих тегов у city1 и city2.

select
    c1.name as city1
    ,c2.name as city2
    ,count(ct2.tag_id) as match_count
from
    cities as c1
    inner join cities as c2 on
        c1.id != c2.id              -- change != into > if you dont want duplicates
    left join cities_tags as ct1 on -- use inner join to filter cities with no match
        ct1.city_id = c1.id
    left join cities_tags as ct2 on -- use inner join to filter cities with no match
        ct2.city_id = c2.id
        and ct1.tag_id = ct2.tag_id
group by
    c1.id
    ,c2.id
order by
    c1.id
    ,match_count desc
    ,c2.id

Измените != на >, чтобы избежать повторного возврата в каждый город. Это означает, что город больше не будет отображаться один раз в первом столбце так же, как и один раз во второй колонке.

Измените два left join на inner join, если вы не хотите видеть комбинации городов, у которых нет совпадений с тегами.

 2
Author: nl-x, 2013-08-07 23:33:02

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

  • В Индекс Джаккарда объяснение @m-халида-джунаида очень интересно и правильно, но реализация (q.sets + q.parisset) AS union и (q.sets - q.parisset) AS intersect это очень неправильно.
  • Версия @n-lx - это путь, но нужен индекс Jaccard, это очень важно, если в городе есть 2 тега и сопоставляет два тега другого города с 3 тегами, результат будет таким же, как и в другом городе, только с теми же двумя тегами. Я думаю, что полные матчи наиболее взаимосвязаны.

Мой ответ:

cities такой стол, как этот.

| id | Name      |
| 1  | Paris     |
| 2  | Florence  |
| 3  | New York  |
| 4  | São Paulo |
| 5  | London    |

cities_tag такой стол, как этот.

| city_id | tag_id |
| 1       | 1      | 
| 1       | 3      | 
| 2       | 1      |
| 2       | 3      | 
| 3       | 1      |     
| 3       | 2      |
| 4       | 2      |     
| 5       | 1      |
| 5       | 2      |
| 5       | 3      |

С помощью этого примера данных Флоренция имеет полные совпадения с Парижем, Нью-Йорк совпадает с одним тегом, Сан-Паулу не имеют никаких тегов совпадений и Лондон соответствует двум тегам и имеет еще один. Я думаю, что индекс Джаккарда этого образца равен:

Флоренция: 1.000 (2/2)

Лондон: 0.666 (2/3)

Нью-Йорк: 0.333 (1/3)

Сан-Паулу: 0.000 (0/3)

Мой запрос выглядит так:

select jaccard.city, 
       jaccard.intersect, 
       jaccard.union, 
       jaccard.intersect/jaccard.union as 'jaccard index'
from 
(select
    c2.name as city
    ,count(ct2.tag_id) as 'intersect' 
    ,(select count(distinct ct3.tag_id) 
      from cities_tags ct3 
      where ct3.city_id in(c1.id, c2.id)) as 'union'
from
    cities as c1
    inner join cities as c2 on c1.id != c2.id
    left join cities_tags as ct1 on ct1.city_id = c1.id
    left join cities_tags as ct2 on ct2.city_id = c2.id and ct1.tag_id = ct2.tag_id
where c1.id = 1
group by c1.id, c2.id) as jaccard
order by jaccard.intersect/jaccard.union desc

SQL-Фидд

 2
Author: Charles Cavalcante, 2016-04-24 13:22:58

Может ли это быть толчком в правильном направлении?

SELECT cities.name, ( 
                    SELECT cities.id FROM cities
                    JOIN cities_tags ON cities.id=cities_tags.city_id
                    WHERE tags.id IN(
                                     SELECT cities_tags.tag_id
                                     FROM cites_tags
                                     WHERE cities_tags.city_id=cites.id
                                     )
                    GROUP BY cities.id
                    HAVING count(*) > 0
                    ) as matchCount 
FROM cities
HAVING matchCount >0

Я попробовал вот что:

//Найдите названия городов:
Получить city.names (ПОДЗАПРОС) как количество совпадений ИЗ городов, ГДЕ количество совпадений >0

// подзапрос:
выберите количество тегов, которые есть у городов, в которых (ВЛОЖЕННЫЙ запрос) также есть

// вложенный запрос
выберите идентификатор тегов, которые имеет исходное имя

 1
Author: Martijn, 2013-08-07 20:25:43