Находим единомышленников и противников друзей и врагов среди пользователей сайта на Drupal, используя данные votingapi.
Делаем выборку данных
SELECT v1.uid uid1, v2.uid uid2, u.name name2,
v2.entity_id entity_id, v1.value value1, v2.value value2
FROM votingapi_vote v1
JOIN (votingapi_vote v2, users u)
ON (v1.uid != v2.uid AND v1.entity_id=v2.entity_id
AND v1.entity_type=v2.entity_type AND v2.uid=u.uid)
WHERE v1.uid < v2.uid AND v1.uid != 0 AND v2.uid != 0
ORDER BY v1.uid,v2.uid;
JOIN таблицы votingapi_vote на себя саму выбирает все пермутации пар пользователей, а условие v1.uid < v2.uid превращает пермутации в комбинации.
Условие v1.entity_type=v2.entity_type AND v2.uid=u.uid позволяет выбрать голоса, которые пользователи отдали за одну и ту же тему или комментарий. Скажем, первая строчка в нашей выборке означает, что администратор и Bob дали 100 очков одной и той же теме или одному и тому же комментарию.
Условие v1.uid != 0 AND v2.uid != 0 исключает анонимные комментарии.
В результате получаем таблицу из пяти колонок:
uid1 uid2 name2 value1 value2
1 2 Bob 100 100
1 2 Bob 20 20
1 2 Bob 40 40
1 2 Bob 100 100
1 2 Bob 20 100
1 2 Bob 100 100
1 2 Bob 100 100
1 2 Bob 100 100
1 2 Bob 100 100
1 2 Bob 80 80
1 2 Bob 100 20
1 2 Bob 20 20
1 2 Bob 60 60
1 2 Bob 100 100
1 2 Bob 100 100
- В первой колонке — id первого пользователя, в данном случае это администратор (uid=1)
- во второй колонке — id второго пользователя
- в третьей колонке — имя второго пользователя
- в четвёртой колонке — голос первого пользователя
- в пятой колонке — голос второго пользователя
Рассчитываем корреляцию голосов
Рассчёт конечно можно написать на PHP, но зачем тогда придумали R?
Берём табличку, сгенерированную на предыдущем этапе из записываем её в файл in.tsv. Затем:
#!/usr/bin/env Rscript
d <- read.delim("in.tsv")
for (uid1 in unique(d$uid1)) {
temp1 <- d[d$uid1==uid1, ]
for (uid2 in unique(temp1$uid2)) {
temp2 <- temp1[temp1$uid2==uid2, ]
x <- temp2$value1
y <- temp2$value2
n <- length(x)
if (n > 7) {
correlation <- cor(x,y)
pvalue <- cor.test(x,y)$p.value
name2 <- as.character(temp2$name2[[1]])
if (is.finite(pvalue) && pvalue < 0.05) {
cat(uid2, name2, n, correlation, pvalue, "n",
sep = "t", file = paste(uid1), append = T)
}
}
}
}
Вся работа по расчёту корреляции делается функцией cor(x,y). Функция cor.test(x,y) рассчитывает метрики корреляции, в том числе её значимость (p-value). По умолчанию считается, что всё, что имеет p-value ≥ 0.05 недостаточно значимо, поэтому отбираем только результаты с p-value < 0.05 и записываем в файл с именем, равным uid первого пользователя.
Из таблицы сверху должен получиться файл с названием «1» и следующим содержимым:
2 Bob 15 0.6039604 0.01710946
- В первой колонке id второго пользователя
- во второй колонке имя второго пользователя (для того, чтобы можно было его сразу же показать на экране)
- в третьей колонке количество тем и комментариев, за которые проголосовали оба пользователя
- в четвёртой колонке — корреляция
- в пяток колонке — p-value
С обработкой данных мы закончили.
Показываем результаты
Я решил показать результаты в профиле пользователя, вот соответствующий хук:
/**
* Hook into the user menu
*/
function mymodule_menu() {
$items['user/%user/likeminded'] = array(
'access callback' => TRUE,
'access arguments' => array(1),
'page callback' => 'mymodule_likeminded', // function defined below
'page arguments' => array(1),
'title' => 'Likeminded',
'weight' => 5,
'type' => MENU_LOCAL_TASK,
);
return $items;
}
Ну и самая длинная часть — вывод результатов.
/**
* Display likeminded users
*/
function mymodule_likeminded($arg){
if (is_object($arg) && !$arg->uid) {
return;
}
# this is my path to the results, your path may be different
$path = drupal_get_path('module', 'mymodule') . '/pearsons/' . $arg->uid;
$lines = array();
$min = 0; $max = 0;
if ($handle = @fopen($path, 'r')) {
while($line = fgets($handle)) {
$line = explode("t", $line);
if ($line[2] >= $max) { $max = $line[2]; }
if ($line[2] < $min) { $min = $line[2]; }
$lines[] = $line;
}
}
$output = '';
// Likeminded
$output .= '<h1>' .t('Likeminded') .'</h1>' ;
$output .= '<div class="likeminded">';
foreach($lines as &$line) {
if ($line[3] > 0 ) {
$size =mymodule_font_size($min, $max, $line[2]);
$opacity = $line[3];
$output .= "<span style=""font-size:"" .="" $size="" "pt;opacity:"="" $opacity="" ""="">";
$output .= l($line[1], 'user/' . $line[0]);
$output .= "</span>";
}
}
$output .= '</div>';
// Adversaries
$output .= '<h1>' .t('Adversaries') .'</h1>' ;
$output .= '<div class="adversaries">';
foreach($lines as &$line) {
if ($line[3] < 0 ) {
$size =mymodule_font_size($min, $max, $line[2]);
$opacity = abs($line[3]);
$output .= "<span style=""font-size:"" .="" $size="" "pt;opacity:"="" $opacity="" ""="">";
$output .= l($line[1], 'user/' . $line[0]);
$output .= "</span>";
}
}
$output .= '</div>';
return $output;
}
/**
* calculate the font size in proportion to the maximum and minimum of common votes
*/
function mymodule_font_size($min_count, $max_count, $cur_count,
$min_font_size=11, $max_font_size=36) {
if ($min_count == $max_count) # avoid DivideByZero exception
{
return $min_font_size;
}
return (
($max_font_size - $min_font_size)
/
($max_count - $min_count)
*
($cur_count - $min_count) + $min_font_size);
}
Тут всё просто. Чем больше шрифт — тем больше пользователи голосовали в одних и тех же темах. Чем ярче текст — тем больше корреляция. Если корреляция позитивная — то показываем пользователя в единомышленниках, иначе — в противниках.
На реальных данных в сто тысяч пользователей, миллион постов и комментариев и несколько миллионов голосов SQL запрос отработал за минуту, исполнение кода на R заняло 10 минут.
Спрашиваете, почему не сделан модуль для Drupal'а? Да кому нужен модуль, вызывающий R. А на PHP переписывать некрасиво.
Автор: mikhailian