Как собрать и проанализировать данные с headhunter с помощью R

Статья о том как самостоятельно собрать данные с работного сайта с помощью R.
Author

Дмитрий Берг

Published

2018-02-04

Keywords

анализ, данные, резюме, headhunter, статистика

Ежедневно специалист, работающий в области найма и управления персонала имеет дело с информацией. Информация очень разная и по структуре и по природе, однако её столько много, что создаётся впечатление о том, что нас окружает информационный хаос. И может возникнуть ощущение, что результат работы зависит от многих субъективных факторов, которые трудно контролировать. Отчасти могу согласиться с такой точкой зрения, но не могу согласиться с тем, что в нашу работу трудно добавить немного статистики, анализа и следовательно объективности. На мой взгляд современному hr-менеджеру или рекрутёру необходимы инструменты не только для поиска кандидатов, но так инструменты для сбора данных и их анализа. Одним из таких инструментом является язык R. Это специальный язык программирования разработанными математиками и статистиками для обработки данных. На сегодняшний день язык R стал настолько гибким, универсальным и мощным, что подходит для широкого круга задач.

Задача данной статьи познакомить с языком R и предоставить в распоряжение ещё один инструмент для сбора, анализа и визуализации информации из внешних источников. Статья будет полезна для тех hr-менеджеров и специалистов, которым приходится собирать и обрабатывать данные, для тех кто хотел бы начать работать с ними, но не знает с чего начать. В статье рассматривается мой конкретный пример. Я взял поисковый запрос на сайте HH.ru, который сохранил в своём аккаунте качестве автопоиска. Меня интересует выборка резюме, среди которых я ищу нужного для заказчика кандидата. Забегу немного вперёд и скажу, что мы будем собирать преимущественно структурированные данные, т.е. числовые данные, а именно: возраст, уровень заработной платы, совокупный опыт работы и период работы. Кроме этого мы соберём данные по полу и станции метро в Москве. Здесь не будет инструкции о том как установить язык R и R-Studio (IDE для R), предполагается, что вы это уже сделали.

Начнём с того, что установим необходимы нам пакеты.

install.packages("rvest")
install.packages("purrr")
install.packages("readr")
install.packages("stringr")
install.packages("RCurl")
install.packages("XML")
install.packages("doMC")
# Для ОС Windows
install.packages("doMC", repos="http://R-Forge.R-project.org")
install.packages("dplyr")
install.packages("plotly")
install.packages("moments")
install.packages("Hmisc")
install.packages("pastecs")
install.packages("psych")

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

Приступим к первому этапу - сбору данных из резюме с headhunter. Сбор данных будем осуществлять с помощью двух парсеров, двумя пакетами RCurl и rvest. Ядро каждого пакета одинакова и скорость сбора данных примерно одинакова, отличается лишь немного синтаксис написания команд. У rvest синтаксис более компактный и проще, но пакет RCurl более функциональный и имеет более гибкие настройки. Для своих целей задач вы можете пользоваться любым из них.

library(rvest)
library(purrr)
library(readr)
library(stringr)

url_base <- "https://hh.ru/search/resume?exp_period=all_time&order_by=publication_time&citizenship=113&schedule=fullDay&text=&area=1&relocation=living&pos=full_text&work_ticket=113&logic=normal&profiles_order_by=rating&saved_search_id=2261699&employment=full&skill=170&skill=1518&skill=3018&skill=6379&specialization=1.225&specialization=1.359&specialization=5.4&specialization=5.224&specialization=5.219&specialization=17.625&specialization=17.196&specialization=17.324&specialization=17.333&specialization=17.303&specialization=17.623&salary_to=100000&page=%d"

map_df(0:11,function(i){
# cat(".") - выводит в консоль процесс сбора

  cat(".")
  page <- read_html(sprintf(url_base,i))
  data.frame(links = html_attr(html_nodes(page, ".HH-VisitedResume-Href"), "href"),
             jobtitle = html_text(html_nodes(page,".HH-VisitedResume-Href")),
  stringsAsFactors = FALSE
)
}) -> hh_resume

hhresume <- data.frame(cbind(host= c("https://hh.ru"), links=hh_resume$links, jobtitle=hh_resume$jobtitle))
hh <- data.frame(url = paste(hhresume$host, hhresume$links, sep=''), title = hhresume$jobtitle)

library(RCurl)
library(XML)
library(doMC)
registerDoMC(cores=2)

urls <- hh$url

gender <- foreach(url=urls) %dopar% {

  html=getURL(url, followlocation=TRUE)
  doc=htmlParse(html,asText=TRUE)
  return(xpathSApply(doc,'//span[@itemprop="gender"]',xmlValue))
}

age <- foreach(url=urls, .combine=rbind) %dopar% {

  html=getURL(url, followlocation=TRUE)
  doc=htmlParse(html,asText=TRUE)
  return(xpathSApply(doc,'//span[@data-qa="resume-personal-age"]',xmlValue))
}

metro <- foreach(url=urls, .combine=rbind) %dopar% {

  html=getURL(url, followlocation=TRUE)
  doc=htmlParse(html,asText=TRUE)
  return(xpathSApply(doc,'//span[@data-qa="resume-personal-metro"]',xmlValue))
}

salary <- foreach(url=urls) %dopar% {

  html=getURL(url, followlocation=TRUE)
  doc=htmlParse(html,asText=TRUE)
  return(xpathSApply(doc,'//span[@class="resume-block__salary"]',xmlValue))
}

sumexp <- foreach(url=urls, .combine=cbind) %dopar% {

  html=getURL(url, followlocation=TRUE)
  doc=htmlParse(html,asText=TRUE)
  return(xpathSApply(doc,'//span[@class="resume-block__title-text resume-block__title-text_sub"]',xmlValue))
}

intexp <- foreach(url=urls) %dopar% {

  html=getURL(url, followlocation=TRUE)
  doc=htmlParse(html,asText=TRUE)
  return(xpathSApply(doc,'//div[@class="resume-block__experience-timeinterval"]',xmlValue))
}

Обращаю ваше внимание на три важных момент. Во-первых, в конце исходной ссылки с headhunter в конце должно стоять &page=%d. Во-вторых, На сайте hh.ru ссылки на резюме относительные, т.е. без доменного имени. Для того что бы второй парсер смог корректно зайти на страницу резюме и собрать данные специально добавляем доменное имя к ссылкам. В третьих, правильно установите нужное количество страниц. В моём случае поисковый запрос показывает 12 страниц резюме кандидатов. Однако первая страница, с которой начинается сбор данных имеет значение 0, следовательно последняя будет на единицу меньше, т.е. 11-ой.

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

sumexp <- gsub("[\\w ^Опыт работы есяц ев а д]", "", sumexp[1,])
sumexp <- gsub("[\\w ^г л]", ".", sumexp)
sumexp <- gsub("[\\w ^м]", "", sumexp)
sumexp <- gsub("[\\d ^.]", ".0", sumexp)

intexp <- do.call(rbind, lapply(intexp, data.frame, stringsAsFactors=FALSE))

gender <- do.call(rbind, lapply(gender, data.frame, stringsAsFactors=FALSE))
age <- do.call(rbind, lapply(age, data.frame, stringsAsFactors=FALSE))
metro <- do.call(rbind, lapply(metro, data.frame, stringsAsFactors=FALSE))
salary <- do.call(rbind, lapply(salary, data.frame, stringsAsFactors=FALSE))
sumexp <- do.call(rbind, lapply(sumexp, data.frame, stringsAsFactors=FALSE))
intexp <- do.call(cbind, lapply(intexp, data.frame, stringsAsFactors=FALSE))

library(dplyr)
library(stringi)
resume <- data.matrix(c(gender, age, metro, salary, sumexp, intexp))
do.call(rbind, resume)
resume <- t(simplify2array(resume))
resume <- as.data.frame(stri_list2matrix(resume, byrow = FALSE, fill = ''))
resume = rename(resume, gender = V1, age = V2, metro = V3, salary = V4, sumexp = V5, intexp = V6)

resume$sumexp <- stri_pad_left(str=resume$sumexp, 2, pad=".")

resume$age <- gsub("\\D", "", resume$age)
resume$salary <- gsub("\\D", "", resume$salary)
resume$intexp <- gsub("[\\w  ^о есяц ев а д т]", "", resume$intexp)
resume$intexp <- gsub("[\\w г л]", ".", resume$intexp)
resume$intexp <- gsub("[\\w ^м]", "", resume$intexp)
resume$intexp <- gsub("[\\d ^.]", ".0", resume$intexp)
resume$intexp <- stri_pad_left(str=resume$intexp, 2, pad=".")

resume$age <- as.numeric(as.character(resume$age))
resume$salary <- as.numeric(as.character(resume$salary))
resume$sumexp <- as.numeric(as.character(resume$sumexp))
resume$intexp <- as.numeric(as.character(resume$intexp))

resume$sumexp[is.na(resume$sumexp)] <- 0
resume$age[is.na(resume$age)] <- 0
resume$salary[is.na(resume$salary)] <- 0
resume$intexp[is.na(resume$intexp)] <- 0

str(resume)

После того как собрали данные с сайта они автоматически сохранились в формате list, что для работы не совсем удобно. Следовательно одной из задач на втором этапе является, перевести всё в формат data.frame. Ещё одной задачей является очистка числовых данных от текста и привести их в единый числовой формат. Особенность в том, что необходимо правильно разделить и указать в каких случаях цифа означает 7 месяцев и привести к значению в виде 0.7, а в каком случае цифра означает 7 лет и привести к значению 7.0. В конце заменяем все пропущенные значения NA на 0, а так же посмотрим на структуру data.frame.

Приступим к статистическим расчётам и перейдём на третий этап. Для данного этапа я позаимствовал информацию с сайта Сергея Мастицкого. Рекомендую данный сайт для всех потому, что Сергей пишет интересные статьи и обзоры, которые могут здорово вам помочь в работе. На третьем этапе вычислим следующие значения: арифметическую среднюю, медиану, дисперсию, стандартное отклонение, минимальные и максимальные значения, стандартную ошибку средней, квантили, межквартильный размах, децили, отношения между некоторыми значениями, коэффициент эксцесса, коэффициент асимметрии.

mean(resume$age)

median(resume$salary[1:226])

var(resume$sumexp)

sd(resume$intexp)

min(resume$age[1:224])
rownames(resume)[which.min(resume$age[1:224])]

max(resume$age)
rownames(resume)[which.max(resume$age)]

sd(resume$age)/sqrt(length(resume$age))

quantile(resume$salary)

IQR(resume$intexp)

quantile(resume$intexp, p = seq(0, 1, 0.1))

summary(resume)

tapply(X = resume$age, INDEX = resume$gender, FUN = mean)
tapply(X = resume$age, INDEX = list(resume$gender, resume$salary), FUN = mean)

SE <- function(x) {sd(x)/sqrt(length(x))}
tapply(X = resume$age, INDEX = resume$salary, FUN = SE)

library(moments)
kurtosis(resume$sumexp)
skewness(resume$intexp)

library(Hmisc)
describe(resume)

library(pastecs)
stat.desc(resume)

library(psych)
describe(resume)
describe.by(resume, resume$intexp)

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

library(plotly)

p0 <- plot_ly(resume, x = ~gender, y = ~age, type = 'box', color = ~gender, colors = "Set1") %>%
  layout(title = 'Количестов резюме в зависимости от возраста и пола',
         yaxis = list(title = 'Возраст'),
         xaxis = list(title = ' '))
p0

Sys.setenv("plotly_username"="Ваш логин")
Sys.setenv("plotly_api_key"="Ваш API KEY")
plotly_POST(x = p0, filename = "gender-age", sharing = "public")

p1 <- plot_ly(resume, x = ~salary, y = ~age, z = ~sumexp, type = 'scatter3d', mode = "markers+lines",
              color = ~factor(age), colors = "Set2") %>%
  layout(title = 'Количестов резюме в зависимости от заработной платы, возраста и совокупного опыт работы',
         scene = list(xaxis = list(title = 'Заработная плата'),
         yaxis = list(title = 'Возраст'),
         zaxis = list(title = 'Опыт работы')))
p1

plotly_POST(x = p1, filename = "salary-age-sumexp", sharing = "public") # Отправляем в личный профиль на plot.ly

p2 <- plot_ly(resume, x = ~salary, y = ~gender, type = 'scatter', name = "Пол", mode = 'markers', color = ~salary, colors = "Paired", size = ~salary) %>%
  add_trace(y = ~age, mode = 'markers', name = "Возраст") %>%
  layout(title = 'Распределение резюме по полу, возрасту и заработной платы, окрашены в зависимсти от уровня з/пл',
         xaxis = list(title = 'Заработная плата'),
         yaxis = list(title = 'Пол/Возраст'))
p2

plotly_POST(x = p2, filename = "salary-age-gender", sharing = "public") # Отправляем в личный профиль на plot.ly

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

На второй инфографике визуализация в виде 3D рассматриваем отношения показателей сразу по трём векторам: уровень заработной платы, возраст и совокупный опыт работы.

На третьей инфографике мы вывели данные пол и возраст по отношению к заработной плате. По сути это два графика в одном, на котором ось y состоит из двух типов данных: пол и возраст, а ось x - уровень заработной платы один.

Для статьи использовал пакет plotly с одной стороны это удобно и демонстрирует, что R способен работать с внешними сервисами и хостингами. Однако если вы визуализируете локально, для себя и коллег, то рекомендую использовать более мощный и гибкий пакет ggplot22.