Data-mining в 40 строк или С кем и против кого вы заодно

Находим единомышленников и противников друзей и врагов среди пользователей сайта на Drupal, используя данные votingapi.

Делаем выборку данных


SELECT v1.uid uid1, v2.uid uid2, u1.name name1, u2.name name2,
  v2.entity_id entity_id, v1.value value1, v2.value value2
FROM votingapi_vote v1
JOIN (votingapi_vote v2, users u1, users u2)
 ON (v1.uid != v2.uid AND v1.entity_id=v2.entity_id
   AND v1.entity_type=v2.entity_type AND v1.uid=u1.uid AND v2.uid=u2.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_id=v2.entity_id AND v1.entity_type=v2.entity_type позволяет выбрать голоса, которые пользователи отдали за одну и ту же тему или комментарий. Скажем, первая строчка в нашей выборке означает, что Administrator и Bob дали 100 очков одной и той же теме или одному и тому же комментарию.

Условие v1.uid != 0 AND v2.uid != 0 исключает анонимные комментарии.

В результате получаем таблицу из шести колонок:

uid1    uid2    name1    name2   value1  value2
1       2       Administrator   Bob     100     100
1       2       Administrator   Bob     20      20
1       2       Administrator   Bob     40      40
1       2       Administrator   Bob     100     100
1       2       Administrator   Bob     20      100
1       2       Administrator   Bob     100     100
1       2       Administrator   Bob     100     100
1       2       Administrator   Bob     100     100
1       2       Administrator   Bob     100     100
1       2       Administrator   Bob     80      80
1       2       Administrator   Bob     100     20
1       2       Administrator   Bob     20      20
1       2       Administrator   Bob     60      60
1       2       Administrator   Bob     100     100
1       2       Administrator   Bob     100     100


  1. В первой колонке — id первого пользователя, в данном случае это администратор (uid=1)
  2. во второй колонке — id второго пользователя
  3. в третьей колонке — имя первого пользователя
  4. в четвёртой колонке — имя второго пользователя
  5. в пятой колонке — голос первого пользователя
  6. в шестой колонке — голос второго пользователя


Рассчитываем корреляцию голосов


Рассчёт конечно можно написать на PHP, но зачем тогда придумали R?

Берём табличку, сгенерированную на предыдущем этапе из записываем её в файл in.tsv. Затем:

#!/usr/bin/env Rscript
d <- read.delim("../in.tsv")

unique1 <- unique(c(d$uid1, d$uid2))

for (id1 in unique1) {
  if (file.exists(as.character(id1))) {
    file.remove(as.character(id1))
  }
  temp1 <- d[d$uid1==id1 | d$uid2==id1, ]
  unique2 <- unique(c(temp1$uid1, temp1$uid2))
  unique2 <- unique2[!unique2 == id1] # remove id1
  for (id2 in unique2) {
    if (id1 < id2) {
      result <- temp1[temp1$uid1==id1 & temp1$uid2==id2, ]
      name <- as.character(result$name2[1])
    } else {
      result <- temp1[temp1$uid1==id2 & temp1$uid2==id1, ]
      name <- as.character(result$name1[1])
    }
    n = nrow(result)

    if (n > 7) {
      x <- result$value1
      y <- result$value2
      pvalue <- cor.test(x,y)$p.value
      if (is.finite(pvalue) && pvalue < 0.05) {
        correlation <- cor(x,y)
        cat(id2, name, n, correlation, pvalue, "\n", sep = "\t", file = paste(id1, sep = ""), append = T)
      }
    }
  }
}


Вся работа по расчёту корреляции делается функцией cor(x,y). Функция cor.test(x,y) рассчитывает метрики корреляции, в том числе её значимость (p-value). По умолчанию считается, что всё, что имеет p-value ≥ 0.05 недостаточно значимо, поэтому отбираем только результаты с p-value < 0.05 и записываем в файл с именем, равным uid первого пользователя.

Игра с id1, id2 и if-else нужна для того, чтобы выбрать все комбинации пар пользователей, вне зависимости от порядка.

Из таблицы сверху должен получиться файл с названием «1» и следующим содержимым:

2       Bob     15      0.6039604       0.01710946

  1. В первой колонке id второго пользователя
  2. во второй колонке имя второго пользователя (для того, чтобы можно было его сразу же показать на экране)
  3. в третьей колонке количество тем и комментариев, за которые проголосовали оба пользователя
  4. в четвёртой колонке — корреляция
  5. в пятой колонке — 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 заняло 30 минут.

Спрашиваете, почему не сделан модуль для Drupal'а? Да кому нужен модуль, вызывающий R. А на PHP переписывать некрасиво.

Конечный результат в профиле одного из пользователей
image
Поделиться публикацией
Ой, у вас баннер убежал!

Ну. И что?
Реклама
Комментарии 1
    0
    Красивенько вышло.
    Если всё-таки перепишете модуль на php, то он найдет своих пользователей.

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое