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

в 19:45, , рубрики: data mining, drupal, php

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

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

Рассчёт конечно можно написать на 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

  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 заняло 10 минут.

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

Конечный результат в профиле одного из пользователей

image

Автор: mikhailian

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js