Рассчитать взвешенную (байесовскую) среднюю оценку / индекс в хранимой процедуре? - PullRequest
4 голосов
/ 02 апреля 2012

У меня есть база данных MS SQL Server 2008, где я храню места, где подают еду (кафе, рестораны, закусочные и т. Д.). На веб-сайте, связанном с этой базой данных, люди могут оценивать места по шкале от 1 до 3.

На веб-сайте есть страница, на которой люди могут просматривать топ-лист с топ-25 (по рейтингу) мест в определенном городе. Структура базы данных выглядит примерно так (в таблицах хранится больше информации, но есть соответствующая информация): imagePlaces->Votes">

Место находится в городе, и голоса размещаются на месте.

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

vote_count = total number of votes for the place
vote_sum = total sum of all the votes for the place

vote_score = vote_sum/vote_count

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

ALTER PROCEDURE [dbo].[GetTopListByCity]
    (
    @city_id Int
    )
AS
    SELECT TOP 25 dbo.Places.place_id, 
           dbo.Places.city_id,
           dbo.Places.place_name,
           dbo.Places.place_alias,
           dbo.Places.place_street_address,
           dbo.Places.place_street_number,
           dbo.Places.place_zip_code,
           dbo.Cities.city_name,
           dbo.Cities.city_alias,
           dbo.Places.place_phone,
           dbo.Places.place_lat,
           dbo.Places.place_lng,
           ISNULL(SUM(dbo.Votes.vote_score),0) AS vote_sum,
           (SELECT COUNT(*) FROM dbo.Votes WHERE dbo.Votes.place_id = dbo.Places.place_id) AS vote_count,
           COALESCE((CONVERT(FLOAT,SUM(dbo.Votes.vote_score))/(CONVERT(FLOAT,(SELECT COUNT(*) FROM dbo.Votes WHERE dbo.Votes.place_id = dbo.Places.place_id)))),0) AS vote_score

    FROM dbo.Places INNER JOIN dbo.Cities ON dbo.Places.city_id = dbo.Cities.city_id
    LEFT OUTER JOIN dbo.Votes ON dbo.Places.place_id = dbo.Votes.place_id
    WHERE dbo.Places.city_id = @city_id
    AND dbo.Places.hidden = 0
    GROUP BY dbo.Places.place_id,
             dbo.Places.city_id,
             dbo.Places.place_name,
             dbo.Places.place_alias,
             dbo.Places.place_street_address,
             dbo.Places.place_street_number,
             dbo.Places.place_zip_code,
             dbo.Cities.city_name,
             dbo.Cities.city_alias,
             dbo.Places.place_phone,
             dbo.Places.place_lat,
             dbo.Places.place_lng
    ORDER BY vote_score DESC, vote_count DESC, place_name ASC

    RETURN

Как вы можете видеть, это приносит больше, чем просто голосование - мне нужны данные о месте, городе, в котором он находится, и так далее. Это хорошо работает, но есть одна большая проблема: подсчет голосов слишком прост, потому что он не учитывает количество голосов. При использовании простого метода подсчета место, имеющее один голос с результатом 3, окажется в списке выше, чем место с четырнадцатью голосами с результатом 3 и одним голосом с результатом 2:

3/1 = 3
(14*3 + 1*2) = 44/15 = 2.933333333333

Чтобы исправить это, я пытался использовать некую форму средневзвешенного / взвешенного индекса. Я нашел пример истинной байесовской оценки, которая выглядит многообещающей. Это выглядит так:

weighted rating (WR) = (v ÷ (v+m)) × R + (m ÷ (v+m)) × C

where:

R = average for the place (mean) = (Rating)
v = number of votes for the place = (votes)
m = minimum number of votes required to be listed in the Top 25 (unsure how many, but somewhere between 2-5 seems realistic)
C = the mean vote across the whole database

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

Теперь мне нужна помощь с двумя вопросами:

Подходит ли этот метод для расчета взвешенного индекса для моего сайта?

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

Ответы [ 3 ]

1 голос
/ 02 апреля 2012

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

;WITH CTE
(
    SELECT
        SUM(dbo.Votes.vote_score) AS SumOfVoteScore,
        COUNT(*) AS CountOfVotes,
        Votes.place_id
    FROM
        Votes
    GROUP BY
        Votes.place_id
)
 SELECT TOP 25 
    dbo.Places.place_id, 
    dbo.Places.city_id,
    dbo.Places.place_name,
    dbo.Places.place_alias,
    dbo.Places.place_street_address,
    dbo.Places.place_street_number,
    dbo.Places.place_zip_code,
    dbo.Cities.city_name,
    dbo.Cities.city_alias,
    dbo.Places.place_phone,
    dbo.Places.place_lat,
    dbo.Places.place_lng,
    ISNULL(CTE.SumOfVoteScore,0) AS vote_sum,
    CTE.CountOfVotes AS vote_count,
    COALESCE((CONVERT(FLOAT,CTE.SumOfVoteScore)/
    (CONVERT(FLOAT,CTE.CountOfVotes))),0) AS vote_score

FROM dbo.Places INNER JOIN dbo.Cities ON dbo.Places.city_id = dbo.Cities.city_id
LEFT JOIN CTE ON dbo.Places.place_id=CTE.place_id
WHERE dbo.Places.city_id = @city_id
AND dbo.Places.hidden = 0
GROUP BY dbo.Places.place_id,
         dbo.Places.city_id,
         dbo.Places.place_name,
         dbo.Places.place_alias,
         dbo.Places.place_street_address,
         dbo.Places.place_street_number,
         dbo.Places.place_zip_code,
         dbo.Cities.city_name,
         dbo.Cities.city_alias,
         dbo.Places.place_phone,
         dbo.Places.place_lat,
         dbo.Places.place_lng
ORDER BY vote_score DESC, vote_count DESC, place_name ASC

Функция CTE помогает нам повторно использовать вычисления. Так что нам не нужно использовать SUM(vote_score) и SELECT COUNT(*) FROM Votes WHERE... кратные разы. Так что тогда, когда вы выбираете вычисления довольно легко следовать.

Надеюсь, это поможет

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

Вам не нужно определять столбцы таблицы в CTE. Это CTE (SumOfVoteScore, CountOfVotes, place_id) AS работает так же хорошо, как это CTE AS. Вам нужно определить столбцы, если вы используете рекурсивный cte. Поскольку вы union с другой частью.

Для справки здесь и здесь вы найдете некоторую информацию о функциях CTE

0 голосов
/ 02 апреля 2012

Хорошо. Итак, вот хранимая процедура, которую я придумал:

ALTER PROCEDURE dbo.GetTopListByCityCTE
    (
    @city_id Int
    )
AS

DECLARE @MinimumNumber float;
DECLARE @TotalNumberOfVotes int;
DECLARE @AverageRating float;
DECLARE @AverageNumberOfVotes float;

/* MINIMUM NUMBER */
SET @MinimumNumber = 1;

/* TOTAL NUMBER OF VOTES -- ALL PLACES */
SET @TotalNumberOfVotes = (
    SELECT COUNT(*) FROM Votes
);

/* AVERAGE RATING -- ALL PLACES */
SET @AverageRating = (
    SELECT
        CONVERT(FLOAT,(SUM(dbo.Votes.vote_score))) / CONVERT(FLOAT,COUNT(*)) AS AverageRating
    FROM 
        Votes);

/* AVERAGE NUMBER OF VOTES -- ALL PLACES */
/* CURRENTLY NOT USED IN INDEX - KEPT FOR REFERENCE */
SET @AverageNumberOfVotes = (
    SELECT AVG(CONVERT(FLOAT,NumberOfVotes)) FROM (SELECT COUNT(*) AS NumberOfVotes FROM Votes GROUP BY place_id) AS AverageNumberOfVotes

);
/* SUM OF ALL VOTE SCORES AND COUNT OF ALL VOTES -- INDIVIDUAL PLACES */
WITH CTE AS (
    SELECT
        CONVERT(FLOAT, SUM(dbo.Votes.vote_score)) AS SumVotesForPlace,
        CONVERT(FLOAT, COUNT(*)) AS CountVotesForPlace,
        Votes.place_id
    FROM
        Votes
    GROUP BY
        Votes.place_id
)

 SELECT 
    dbo.Places.place_id, 
    dbo.Places.city_id,
    dbo.Places.place_name,
    dbo.Places.place_alias,
    dbo.Places.place_street_address,
    dbo.Places.place_street_number,
    dbo.Places.place_zip_code,
    dbo.Cities.city_name,
    dbo.Cities.city_alias,
    dbo.Places.place_phone,
    dbo.Places.place_lat,
    dbo.Places.place_lng,
    ISNULL(CTE.SumVotesForPlace,0) AS vote_sum,
    ISNULL(CTE.CountVotesForPlace,0) AS vote_count,
    COALESCE((CTE.SumVotesForPlace/
    CTE.CountVotesForPlace),0) AS vote_score,
    ISNULL((CTE.CountVotesForPlace / (CTE.CountVotesForPlace + @MinimumNumber)) * (COALESCE((CTE.SumVotesForPlace / CTE.CountVotesForPlace),0)) + (@MinimumNumber / (CTE.CountVotesForPlace + @MinimumNumber)) * @AverageRating,0) AS WeightedIndex

FROM dbo.Places INNER JOIN dbo.Cities ON dbo.Places.city_id = dbo.Cities.city_id
LEFT JOIN CTE ON dbo.Places.place_id = CTE.place_id
WHERE dbo.Places.city_id = @city_id
AND dbo.Places.hidden = 0
GROUP BY dbo.Places.place_id,
         dbo.Places.city_id,
         dbo.Places.place_name,
         dbo.Places.place_alias,
         dbo.Places.place_street_address,
         dbo.Places.place_street_number,
         dbo.Places.place_zip_code,
         dbo.Cities.city_name,
         dbo.Cities.city_alias,
         dbo.Places.place_phone,
         dbo.Places.place_lat,
         dbo.Places.place_lng,
         CTE.SumVotesForPlace,
         CTE.CountVotesForPlace
ORDER BY WeightedIndex DESC, vote_count DESC, place_name ASC

Есть переменная с именем @AverageNumberOfVotes, которая не используется в вычислениях, но я оставил ее там для справки на случай, если она может бытьНужно.

Работая с данными, которые у меня есть, я получаю результаты, которые немного отличаются от полученных ранее, но это не революция и не совсем то, что мне нужно.Вот первые 10 строк, которые возвращаются, когда я выполняю вышеуказанный SP:

vote_sum        vote_count  vote_score          WeightedIndex
1110            409         2,71393643031785    2,7140960047496
807             310         2,60322580645161    2,60449697749787
38              15          2,53333333333333    2,56708633093525
25              10          2,5                 2,55442722744881
2               1           2                   2,55188848920863
2               1           2                   2,55188848920863
2               1           2                   2,55188848920863
2               1           2                   2,55188848920863
2               1           2                   2,55188848920863
2               1           2                   2,55188848920863

Проблема здесь заключается в том, что при наличии только одного голоса и 2 баллов, взвешенный индекс становится равным 2,55188848920863.?

Формула для расчета этого индекса взята из IMDB (http://www.imdb.com/chart/top), и я думаю, что либо я сделал что-то не так, либо данные, которые у меня есть в моей базе данных, не сопоставимы сданные (количество голосов или шкала голосования), которыми располагает IMDB?

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

Можно ли как-то настроить эту функцию, чтобы она лучше работала для меня?другая функция / подход, который будет работать лучше? Мне все еще нужно делать вычисления в хранимой процедуре.

0 голосов
/ 02 апреля 2012

Спасибо, Арион!

Я искал что-то по образцу CTE, но я просто не знал, что это то, что я искал! Всегда приятно узнавать что-то новое, и я знаю, что буду использовать CTE в других проектах. Когда я реализую ваш CTE в своей хранимой процедуре, я получаю этот код:

ALTER PROCEDURE dbo.GetTopListByCityCTE
    (
    @city_id Int
    )
AS

;WITH CTE (SumOfVoteScore, CountOfVotes, place_id) AS
(
    SELECT
        SUM(dbo.Votes.vote_score) AS SumOfVoteScore,
        COUNT(*) AS CountOfVotes,
        Votes.place_id
    FROM
        Votes
    GROUP BY
        Votes.place_id

)

 SELECT TOP 25 
    dbo.Places.place_id, 
    dbo.Places.city_id,
    dbo.Places.place_name,
    dbo.Places.place_alias,
    dbo.Places.place_street_address,
    dbo.Places.place_street_number,
    dbo.Places.place_zip_code,
    dbo.Cities.city_name,
    dbo.Cities.city_alias,
    dbo.Places.place_phone,
    dbo.Places.place_lat,
    dbo.Places.place_lng,
    ISNULL(CTE.SumOfVoteScore,0) AS vote_sum,
    CTE.CountOfVotes AS vote_count,
    COALESCE((CONVERT(FLOAT,CTE.SumOfVoteScore)/
    (CONVERT(FLOAT,CTE.CountOfVotes))),0) AS vote_score

FROM dbo.Places INNER JOIN dbo.Cities ON dbo.Places.city_id = dbo.Cities.city_id
LEFT JOIN CTE ON dbo.Places.place_id = CTE.place_id
WHERE dbo.Places.city_id = @city_id
AND dbo.Places.hidden = 0
GROUP BY dbo.Places.place_id,
         dbo.Places.city_id,
         dbo.Places.place_name,
         dbo.Places.place_alias,
         dbo.Places.place_street_address,
         dbo.Places.place_street_number,
         dbo.Places.place_zip_code,
         dbo.Cities.city_name,
         dbo.Cities.city_alias,
         dbo.Places.place_phone,
         dbo.Places.place_lat,
         dbo.Places.place_lng,
         CTE.SumOfVoteScore,
         CTE.CountOfVotes
ORDER BY vote_score DESC, vote_count DESC, place_name ASC

Быстрая проверка показывает, что он возвращает тот же результат, что и предыдущий код, но его гораздо легче читать и отслеживать, и, надеюсь, гораздо эффективнее.

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

...