Задать вопрос

ORDER BY `вероятность`?

Добрый день, Хабр!


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


Например: записи — это адреса ресторанов. Надо, чтобы рестораны, у которых стоит флаг «в ресторане всегда длинные очереди в туалет», попадали в выборку в среднем вдвое реже всех остальных.


Если бы все записи были «равноценными», вероятность для каждой была бы равна частному единицы и количества записей в таблице. Но если по уловию вероятность умножается на коэффициент, перестает выполняться правило «сумма вероятностей для всех записей равна 100%», а это плохо — так нельзя использовать оценочную вероятность для прогноза количества выборок каждой записи в сутки, основываясь на статистике количества запросов за это время.


Куда копать? Модель-то сверхпопулярная — и в рекламных сетях (чаще показывать наиболее CTR-истые баннеры), и в играх (с монстров чаще дропается более дешёвый лут).


Впервые всерьез ощущаю нехватку математического образования, занимаясь программированием для веб =(
  • Вопрос задан
  • 2803 просмотра
Подписаться 9 Оценить Комментировать
Пригласить эксперта
Ответы на вопрос 15
lashtal
@lashtal
Я б сделал доп. поле в таблице, куда заносится вычисленный коэффициент (включая рандом), и сортировал по нему. Чтобы сортировка была случайной, пересчитывайте поле каждые n минут.
Ответ написан
Я не знаю как сделать это правильно, но вот что придумал:

Использовать стандартный рандом.
Как выше писали сделать рейтинг для каждого поля по любым вашим формулам.

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

Не знаю получилось ли у меня донести мысль, если что пишите, постараюсь подробнее с примерами описать.

Вообще, я вопросы по БД, обычно, задаю на sql.ru, там больше профильных специалистов.
Ответ написан
Комментировать
@zibada
тупое решение, если записей немного и не пугает делать full scan на каждый селект:

в каждой записи хранить:
— собственный приоритет (любое положительное число)
— сумму приоритетов всех предыдущих элементов (в порядке добавления, например, по id)
и где-то хранить сумму всех приоритетов (на самом деле, можно получать из записи с максимальным id)
можно их нормализовать, но тогда не получится сделать быстрый insert.

select сводится к генерации нескольких рандомных чисел из диапазона [0, сумма_всех_приоритетов), и выборке элементов, где диапазон [сумма, сумма+свой_приоритет) включает хотя бы одно из выбранных чисел.
insert — простое дописывание в конец, обновление суммы приоритетов
delete — простое удаление
update (изменение приоритета) — удаление+вставка если приоритет увеличился, простой апдейт, если уменьшился.

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

итого — все операции за амортизированную O(1), кроме селекта, с которым все печально.

для его оптимизации можно использовать spatial index (не в курсе насчет поддержки в современных БД), т.к. у нас запрос на принадлежность точки отрезку.

честное и быстрое, но сложное решение:

строить дерево, в листьях которого — id из таблицы и приоритеты, в промежуточных узлах — суммы приоритетов в поддеревьях.

select одного элемента (для простоты далее рассматривается бинарное дерево):
генерируем число от 0 до суммы приоритетов (хранится в корне), идем от корня:
— если число меньше суммы приоритетов в левом поддереве, то налево
— иначе — направо, и вычитаем из числа сумму приоритетов в левом поддереве
— когда дойдем до листа — вернуть его id

update/insert/delete — обычные операции с деревом с обновлением суммы приоритетов во всех промежуточных вершинах.

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

производительность всех операций — O(logN), выборки — O(KlogN), где K — размер выборки.
тоже есть проблема, что один элемент может выбраться несколько раз, чтобы это побороть, можно выбранные элементы удалять сразу после выбора (ну или просто обнулять приоритет), а в самом конце вставлять обратно.

как все это хранить?
ну если все влезает в память — то отлично, вешаем отдельного демона, который при старте строит дерево по исходной табличке и поехали.
если нет — либо в базе (но эффективность работы с деревьями на реляционных базах это большой вопрос), либо в файлах, т.е., по сути, писать свой движок базы…
но это уже явный overkill =) наверняка существуют готовые решения, которые все это уже делают.
Ответ написан
Навскидку приходит на ум:
У вас 10 тысяч записей, нужно показать, например, рандомно десять.
Рестораны с длинной очередью должны показываться, например, в два раза реже.
Делаем выборку — вытаскиваем рандомно 20 ресторанов с длинной очередью и в два раза больше (40) без очереди.
Из них выбираем 10 рандомно.
Ответ написан
Комментировать
@gnusy
программист
каждой записи добавляем поле с её весом (например поле ves). в дальнейшем выбираем записи схематично так:

select top 10 *
from [table]
where rand()<ves/sum(ves)


где sum(ves) — сумма весов по полю в таблице.

гарантированно запись с весм 1 будет попадать в выборку в 3 раза реже, чем запись с весом 3.
Ответ написан
Комментировать
shai_xylyd
@shai_xylyd
10000 — очень мало, может брать все и уже считать на любимом языке.

Можно считать отчет заранее по крону и показывать только результат.
Ответ написан
Комментировать
DanielWolf
@DanielWolf
Можно попробовать использовать sphinx, и настроить систему ранжирования под Ваши нужды.

http://habrahabr.ru/blogs/sphinx/62287/
Ответ написан
AntonioK
@AntonioK Автор вопроса
На затравку: сейчас работает костыль вида
Ответ написан
Комментировать
AntonioK
@AntonioK Автор вопроса
На затравку: сейчас работает костыль вида

SELECT
*,
(`flag` + RAND() * 0.33) as probability /* Powered by gypsy magic (bigger RAND() coefficient gives less effect) */
FROM
`table`
ORDER BY
`probability` DESC


Но он, во-первых, дико неизящен, и во-вторых, не позволяет посчитать вероятности для всех записей так, чтобы в сумме было 100%, и на основе этих вероятностей строить прогнозы.
Ответ написан
@DmitryGushin
Еще один «быдло-вариант». Делаете выборку не в N записей, а в N*коэфф из таблицы. В Вашем примере — выборка из 2N. В выборке получите случайные записи с равновероятным попаданием для всех ресторанов. А далее из этой выборки (зная значение коэффициента) выдергиваете нужные Вам записи в нужной пропорции…
Ответ написан
Комментировать
AlexeyK
@AlexeyK
любое такое действие лучше не в базе делать, а уже механизмами пост-сортировки
либо как сказали выше, включить пресловутые туалеты в рейтинг, присвоив им определенный «вес», чтобы выборка получалась такой, как вам нужно
Ответ написан
Комментировать
burdakovd
@burdakovd
В общем случае неразрешимо=)
Рассмотрим вырожденный случай: в таблице всего N записей. Тогда вероятность для каждой записи быть выбранной будет равна 1.0, абсолютно независимо от заданных весов.
Ответ написан
m08pvv
@m08pvv
Первое что приходит в голову при таком построении задачи — выбрать подходящую функцию распределения и ввести нумерацию ресторанов так, чтобы тем, которые надо показывать чаще, соответствовала бы большая вероятность появления.

Только вот не знаю как будет прикручиваться самописный рандом…
Ответ написан
Комментировать
@shavluk
1. Определить максимальный диапазон Rand, Пусть будет [0..M] (скорее всего M=1)
2. Количество записей N
2. Для каждой записи вводим понятие рейтинга R
3. Рассчитываем сумму всех рейтингов S=Sum®
4. Вводим для каждой строки два поля, начало и конец диапазона D1, D2
5. Начиная с первой записи и до последней разбиваем
D1(1) = 0; D2(1) = M*(R(1) / S)
D1(2) = D2(1); D2(2) = D1(2) + M*(R(2) / S);
D1(i) = D2(i-1); D2(i) = D1(i) + M*(R(i) / S);

6. И далее
select top 10 * from [table]
order by case when rand() between D1 and D2 then 0 else 1 end
Ответ написан
Комментировать
@mihaild
Еще одно индусское решение.
Пусть веса у записей x_1, x_2, ..., x_n.
У каждой записи добавляем поля «начало» и «конец». У i-й записи началом будет x_1+...+x_{i-1}, концом — x_1+...+{x_i}-1. Теперь генерируем случайное число R от 0 до x_1+...+x_n-1 и выбираем запись, у которой начало <= R <= конец. Выбираем столько раз, сколько нужно.
Придется проверять на дупликаты, но при отсутствии сильно перевешивающих записей они будут довольно редки.
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы