Исследование рынка вакансий BA/SA
«Исследование рынка вакансий аналитиков» — так звучала вполне реальная задача одного вполне реального ведущего аналитика одной ни большой, ни маленькой фирмы. Рисерчер парсил десятки описаний вакансий с hh вручную, раскидывая их по запрашиваемым скиллам и увеличивая счетчик в соответствующей колонке спредшита.
Я увидела в этой задаче неплохое поле для автоматизации и решила попытаться справиться с ней меньшей кровью, легко и просто.
Меня интересовали следующие вопросы, затронутые в данном исследовании:
- средний уровень зарплат бизнес- и системных аналитиков,
- наиболее востребованные умения и личные качества на этой позиции,
- зависимости (если есть) между определенными навыками и уровнем зп.
Спойлер: легко и просто не получилось.
Подготовка данных
Если мы хотим собрать кучу данных о вакансиях, то логично hh не ограничиваться. Однако, для чистоты эксперимента простоты начнем с этого ресурса.
Для сбора данных воспользуемся поиском по вакансиям через hh API.
Искать буду с помощью простого текстового запроса «systems analyst», «business analyst» и «product owner», потому как активности и зоны ответственности у этих позиций, как правило, пересекаются.
Для этого нужно сформировать запрос вида https://api.hh.ru/vacancies?text=»systems+analyst» и распарсить полученный JSON.
Чтобы в выборку попали максимально релевантные вакансии, искать будем только в заголовках вакансий, добавив в запрос параметр search_field=name .
Здесь можно посмотреть, какие поля вакансий возвращаются по этому запросу. Я выбрала следующие:
- название вакансии
- город
- дата публикации
- зарплата — верхняя и нижняя границы
- валюта, в которой указана зарплата
- gross — T/F
- компания
- обязанности
- требования к кандидату
Кроме того, я хочу дополнительно проанализировать скиллы, которые указываются в разделе «Ключевые навыки», но этот раздел доступен только в полном описании вакансии. Поэтому я также сохраню ссылки на найденные вакансии, чтобы впоследствии достать список скиллов для каждой из них.
В функции hh.getjobs() на вход принимается вектор интересующих нас поисковых запросов и уточнение, интересуют нас только вакансии с указанной зарплатой или все подряд (по умолчанию берем второй вариант). Создается пустая dafa frame, а затем используется функция fromJSON() пакета jsonlite , которая принимает на вход URL и возвращает структурированный list. Далее из узлов этого списка мы достаем интересующие нас данные и заполняем соответствующие поля data frame.
По умолчанию данные отдаются постранично, по 20 элементов на каждой странице. Максимально по одному запросу можно получить 2000 вакансий. Все полученные данные мы записываем в df .
Лайфхак 1: совершенно не факт, что по нашему запросу найдется 2000 вакансий, и начиная с какого-то момента нам будут приходить пустые страницы. В этом случае R ругается и выпрыгивает из цикла. Поэтому содержимое внутреннего цикла заботливо обернем в try() .
Лайфхак 2: во внутренний цикл также имеет смысл добавить вывод в консоль текущего статуса сбора данных, потому как дело это небыстрое. Я сделала так:
После заполнения данными столбцы переименовываются так, чтобы с ними было удобно работать, и возвращается полученная data frame.
Эту и другие вспомогательные функции я буду хранить в отдельном файле functions.R , чтобы не захламлять основной скрипт, который пока выглядит так:
Теперь из полного описания вакансий вытащим experience и key_skills .
Функции hh.getxp передаем data frame, проходимся по сохраненным ссылкам на вакансии, и из полного описания достаем значение требуемого опыта работы. Полученное значение сохраняем в новом столбце.
Описание новой вспомогательной функции отправляется в functions.R , а основной скрипт теперь обращается к ней:
В фрагменте выше мы также формируем новую data frame all.skills вида «id вакансии — навык»:
Препроцессинг
Посмотрим, сколько всего данных удалось собрать:
Почти полторы тысячи вакансий! Выглядит неплохо. И по всей видимости, несколько вакансий попались в результатах поиска дважды — по разным запросам. Поэтому первым делом оставим только уникальные записи: jobdf .
Для того, чтобы сравнивать зарплаты аналитиков на рынке труда, мне нужно
1) убедиться, что все имеющиеся данные по зарплатам представлены в единой валюте,
2) выделить в отдельную data frame те вакансии, для которых зарплата указана.
Рассмотрим каждую из подзадач детальнее. Предварительно можно выяснить, какие в принципе валюты встречаются в наших данных с помощью table(jobdf$Currency) . В моем случае помимо рублей фигурировали доллары, евро, гривны, казахские тенге и даже узбекские сумы.
Чтобы перевести значения зарплат в рублевые, нужно узнать актуальный курс валют. Узнавать будем у Центробанка:
Чтобы курсы корректно обрабатывались в R, нужно убедиться, что десятичная часть отделена точкой. Кроме того, стоит обратить внимание на колонку Nominal: где-то он равен 1, где-то 10 или 100. Это значит, один фунт стерлингов стоит
85 рублей, а, скажем, за сотню армянских драмов можно купить
13 рублей. Для удобства дальнейшей обработки я привела значения к номиналу 1 относительно рубля.
Теперь можно и переводить. Наш скрипт делает это с помощью функции convert.currency() . Актуальный курс валют в ней берется из таблицы quotations , куда мы сохранили данные из XML, предоставляемой Центробанком. Также на вход функция принимает целевую валюту для конвертации (по умолчанию RUR) и таблицу с вакансиями, значения зарплатных вилок в которой необходимо привести к единой валюте. Функция возвращает таблицу с обновленными зарплатными цифрами (уже без столбца Currency, за ненадобностью).
С белорусскими рублями пришлось повозиться: после получения весьма странных данных в несколько подходов, я провела небольшой рисерч и узнала, что начиная с 2016 года в Беларуси используется новая валюта, которая отличается не только курсом, но и аббревиатурой (теперь не BYR, а BYN). В справочниках hh до сих пор используется аббревиатура BYR, про которую XML от Центробанка ничего не знает. Поэтому в функции convert.currency() я не самым изящным образом сначала заменяю аббревиатуру на актуальную, и только затем перехожу непосредственно к конвертации.
Выглядит все это следующим образом:
Также можно учесть, что некоторые данные по зарплатам представлены в значениях gross, то есть на руки сотрудник будет получать несколько меньше. Чтобы рассчитать зарплату net для резидентов РФ, нужно вычесть из указанных цифр 13% (для нерезидентов вычитается 30%).
Делать этого я, конечно, не буду, потому что в таком случае стоит учитывать налоги в разных странах, а не только в России, либо в исходный поисковой запрос добавлять фильтр по стране.
Последним шагом перед анализом разделю найденные вакансии на три категории: джунов, миддлов и сеньоров и запишу полученные позиции в новый столбец. К старшим позициям будем относить те, в названиях которых присутствует слово «старший» и его синонимы. Аналогичным образом найдем стартовые позиции по ключевым словам «junior» и синонимам, а к миддлам отнесем всех, кто между:
В основной скрипт добавляем блок подготовки данных.
Анализ
Как упоминалось выше, я собираюсь анализировать следующие аспекты полученных данных:
- средний уровень зарплат BA/SA,
- наиболее востребованные умения и личные качества на этой позиции,
- зависимости (если есть) между определенными навыками и уровнем зп.
Средний доход BA/SA
Как выяснилось, компании неохотно указывают верхнюю или нижнюю границу зарплаты.
В нашей data frame jobdf эти значения находятся в колонках To и From соответсвенно. Я хочу найти средние значения и записать их в новый столбец Salary.
Для кейсов, где зарплата указана полностью, это легко сделать с помощью функции mean() , отфильтровав все остальные записи, где данные по вилке отсутствуют полностью или частично. Но в этом случае от нашей исходной выборки, которая и так невелика, осталось бы менее 10%. Поэтому я вычисляю коэффициент Подгониана , который подсказывает, насколько в среднем отличаются значения To и From в вакансиях, где указана полная вилка, и с его помощью примерно заполняю недостающие данные в кейсах, где пропущенно только одно значение.
Это «мягкая» фильтрация данных, которая в функции select.paid() задается параметром suggest = TRUE . Альтернативно мы можем указать suggest = FALSE при вызове функции и просто выпилить все строки, где зарплатные данные хотя бы частично отсутствуют. Однако с использованием мягкой фильтрации и волшебнго коэффициента мне удалось сохранить в выборке почти четверть от исходного набора данных.
Переходим к визуальной части:
На этом графике можно визуально оценить плотность распределения зарплат BA/SA в двух столицах и в регионах. Но что если конкретизировать запрос и сравнить, сколько получают миддлы и сеньоры в столицах?
Из полученного графика видно, что разница в зарплатных ситуациях у миддлов и сеньоров в Москве и Питере не слишком различается. Так, в Санкт-Петербурге мидлы получают, как правило, в районе 70 т.р., в то время как в Москве пик плотности приходится на
120 т.р., а разница в доходах старших специалистов уровня в Москве и Санкт-Петербурге отличается в среднем на 60 тысяч.
Также мы можем взглянуть, например, на московские зарплаты аналитиков в разрезе должности:
Можно сделать вывод, что а) на сегодняший день в Москве гораздо больший спрос на специалистов-аналитиков начального уровня, и б) в то же время, верхний порог зарплат таких специалистов ограничен куда более четко, чем у миддлов и сеньоров.
Еще одно наблюдение: средняя зп московских специалистов среднего и высокого уровня имеет довольно большую площадь пересечения. Это может говорить о том, что на рынке довольно размытая граница между этими двумя ступенями.
Полный код для графиков под катом.
Анализ навыков (Key skills)
Переходим к ключевой цели исследования — определить наиболее востребованные навыки для BA/SA. Для этого проведем анализ тех данных, что в явном виде указаны в специальном поле вакансии — key skills.
Наиболее популярные навыки
Ранее мы получили отдельную data frame all.skills , куда записали пары «id вакансии — навык». Найти наиболее часто встречающиеся скиллы несложно с помощью функции table() :
Получится примерно следующее:
Здесь Freq — это количество вакансий, в поле «key_skills» которых указан соответствующий навык из столбца Skill.
«Но это еще не все!»(ц) Совершенно очевидно, что одни и те же скиллы запросто могут встречаться в разных вакансиях в синонимичных выражениях.
Я составила небольшой словарь синонимов названий скиллов и разделила их по категориям.
Словарь представляет собой csv-файл со столбцами category — одно из следующего: Activities, Tools, Knowledge, Standards и Personal; skill — основное название навыка, которое я буду использовать вместо всех найденных синонимов; syn1, syn2,… syn13 — собственно возможные вариации для каждого навыка. Некоторые строки могут содержать пустые столбцы синонимов.
Сначала импортируем словарь, а затем раскидаем скиллы заново на основе имеющихся эквивалентностей:
Под катом можно посмотреть начинку функции categorize.skills() .
Я добавляю к исходной data frame с навыками столбец category и skill. group — для категории и обобщающего названия навыка соответсвенно. Затем я прохожусь по импортированному словарю и из каждой строчки синонимов составляю паттерн для функции grep() . Добавляя каждое непустое значение колонки к строке, я разделяю их чертой, чтобы получить условие «или». Так, для всех скиллов из исходной таблицы, в которые входит паттерн uml|activity diagram|use case diagram|ucd|class diagram , я запишу в колонку skill.group значение «uml». И так будет с каждым. скиллом из исходной data frame.
Повторно запросив топ наиболее популярных навыков можно увидеть, что расстановка сил несколько поменялась:
В тройке лидеров теперь управление проектами, бизнес-анализ и документирование, а знание UML сместили из топ-7.
Довольно интересно пройтись по категориям и выяснить, какие навыки наиболее востребованы в каждой из них.
Например, для категории Knowledge дело обстоит следующим образом:
Из графика видно, что наибольшим спросом пользуются знания в области баз данных, методологий разработки ПО и 1С. Далее идут знания в области CRM, ERP-систем и основы программирования.
В том, что касается стандартов, действительно большим спросом пользуется знание SQL и UML, на пятки им наступает нотация ARIS, а вот ГОСТы занимают всего лишь шестое место.
Что касается используемых тулов, — мы лишний раз видим подтверждение тому, что основным инструментом аналитика является голова. Без линейки MS Office и таск-трекинговых систем не обойтись, а в остальном мало кого волнует, в каком именно редакторе аналитик создает свои схемы или набрасывает макеты интерфейсов.
Влияние навыков на доход
Наконец, проанализируем, в каком диапазоне зарплат фигурируют упоминания различных навыков. Поскольку ранее мы уже убедились, как сильно влияет город на цифру, указанную в вакансии, мы будем рассматривать влияние навыков в разрезе городов.
Для начала соединим интересующие нас столбцы из таблиц по вакансиям jobs.paid и скиллам all.skills , чтобы было удобнее строить графики на основе полученной data frame.
Получится таблица следующего вида:
Из городов я решила отфильтровать Москву и Питер, т.к. по ним больше всего данных. Сначала взглянем на активности:
Из графика можно сделать вывод, что в вакансиях BA/SA конкретизация предстоящих активностей и требуемых навыков уменьшается прямо пропорционально увеличению зарплаты.
Теперь проанализируем личные качества желаемых кандидатов:
Что касается используемых инструментов, начиная от пакета MS Office и заканчивая софтом для составления диаграмм и создания мокапов, — здесь данных оказалось слишком мало, чтобы на их основании делать какие-то выводы о связи между владением определенным инструментом и уровнем дохода. Более того, чем выше зарплата, обозначенная в вакансии, тем меньше внимания уделяется конкретным инструментам в арсенале аналитика.
В том, что касается стандартов, картина немного отличается: умение обращатсья с нотациями UML и ARIS, а также знания SQL стабильно востребованы (в своих пропорциях) при разных уровнях зарплат, а вот знание IDEF — уже не такой популярный запрос, который и вовсе отсутствует на «максималках».
Анализ текста вакансий
На самом деле, эту часть статьи и работы я хотела отложить на следующий раз, но в ходе исследования стало понятно, что без анализа текста решительно не обойтись. Дело в том, что из найденных по исходному запросу 1478 вакансий лишь четверть содержали в себе упоминания хоть каких-нибудь навыков в поле key_skills. Это означает, что при публикации вакансий самая интересная и полная информация все-таки лежит в ее полном описании.
Импорт и подготовка описаний
Посмотрим, как выглядит типичное описание вакансии в нашей исходной data frame:
Текст, очевидно, не полный. Поэтому пришлось снова пробежаться по исходным URL’ам найденных вакансий, чтобы вытащить необходимую информацию.
Полное описание вакансии может содержать юникод-символы списка, html-теги и пр., от чего можно избавиться с помощью уже знакомой нам стандартной функции gsub :
Это, впрочем, не является обязательнм шагом, поскольку сравнивать тексты вакансий я собираюсь все с тем же словарем, составленным вручную. Следующая функция принимает на вход data frame и словарь (также в виде df), пробегается по столбцу с полным описанием вакансии, ищет совпадения со словарем и формирует новую df вида «id, skill.group, category».
Снова о самых востребованных навыках
Проверим, что получается?
Да, вот теперь баланс сил точно сместился! Project management, который неожиданно лидировал при анализе полей key_skills, теперь не входит даже в десятку (и в двадцатку тоже).
Если говорить об общем кругозоре, наиболее востребованными теперь представляются знания в области автоматизации процессов, в то время как при анализе полей key_skills эти знания даже не вошли в топ-5.
На следующем графике представлены области знаний, отсортированные в порядке убывания по частоте упоминания вакансиях аналитиков. Поскольку в этот раз мы анализируем тексты всех найденных 1478 вакансий, а не ограничиваемся теми, в которых заполнены key_skills, полученную картину можно считать достаточно достоверной, чтобы представить результат в процентах.
Что касается выбора инструментов, то из графика создается впечатление, что средний сферический BA в вакууме большую часть времени проводит в экселе, таск-трекере и составляет по итогам красивые презентации.
И снова о деньгах
Проверим, как распределились различные навыки и знания на шкале доходности.
Для построения графиков сформируем новую data frame из перечня скиллов, полученных из описания вакансий, и таблицы вакансий с указанными зарплатами. При этом сразу оставим только те записи, которые относятся к Москве и Санкт-Петербургу.
Следующим графиком я хочу отразить, упоминания каких навыков чаще всего встречаются в описаниях вакансий на позиции джунов, миддлов и сеньоров соответственно, а также на какие деньги может рассчитывать счастливый обладатель тех или иных навыков.
Что мы видим из этого графика?
Во-первых, то, что наибольшее количество вакансий включают требования в области дизайна бизнес-процессов и документирования. (Это больше похоже на правду, хоть и отличается от результата, полученного нами в первой части исследования, где уверенно лидировал навык управления проектами.)
Во-вторых, несмотря на это, наиболее «денежными» занятиями является непосредственно бизнес-анализ, создание макетов интерфейсов и проведение исследований.
В части стандартов картина также прояснилась и стала более правдоподобной, чем при анализе голых key_skills.
Глядя на график, можно сделать вывод, что на самом деле в зарплатном сегменте свыше 150 т.р. есть необходимость в специалистах со знанием не только UML или ARIS, но и IDEF, и ГОСТов, однако спрос на знание ГОСТов заметно ниже — в этой части первоначальный вывод подтверждается.
Некоторые изменения наблюдаются и в области личностных качеств аналитика:
Из графика видно, что по-прежнему лидируют аналитические и коммуникативные навыки, но при этом креативность, которая в перечне key_skills едва упоминалась, в описаниях вакансий встречается гораздо чаще. Более того, в зарплатном сегменте свыше 150 т.р. это качество ценится больше, чем умение работать в команде и даже способность организовать работу других.
А как же бесценный опыт?
Например, вот так выглядит плотность распределения зарплат в Москве для специалистов-аналитиков с различным стажем:
Интересно, почему на графике так много вакансий, где требуется специалист с более чем шестилетним опытом, но при этом область подозрительно смещена влево? Казалось бы, связь между опытом работы и стоимостью сотрудника должна быть самая прямая. К тому же, обратите внимание, что график затрагивает только столичный рынок вакансий. Я могу предположить, что в бóльшей части таких вакансий (лежащих в красной области графика) требуемый опыт работы указан из оптимизма нестрого.
Для сравнения, в Санкт-Петербурге разделение куда более выраженное:
Кое-какие выводы
Проведенный анализ ключевых навыков и текстов вакансий BA/SA показал, что
- качество такого анализа во многом зависит от словаря категорий навыков. В процессе работы я несколько раз обновляла и дополняла таблицу навыков и инструментов, но и сейчас классификация неидеальна;
- потолок зарплаты аналитика в Москве (и в целом в России) составляет приблизительно 200 т.р. Все, что выше этой цифры, встречается весьма эпизодически и требует нетипичных скиллов вроде знания статистики или специфической предметной области;
- на рынке вакансий довольно размытая граница между миддлами и сеньорами;
- главный инструмент аналитика — по-прежнеу голова (выбор тулов, как правило, остается на усмотрение аналитика и не влияет на доход)
- поле key_skills в вакансиях на hh заполняется через раз, и на основе только него нельзя делать выводы о наиболее востребованных навыках аналитика;
- анализ текста вакансий, в свою очередь, оказался достовернее и полезнее, поскольку данных в результате парсинга описаний было собрано в пять(!) раз больше;
- чтобы прийти к успеху во всех отношениях, аналитику стоит наиболее активно прокачивать навык бизнес-анализа, создания добротного UX и английский язык;
- нельзя недооценивать коммуникативные навыки. Впрочем, их значимость снижается где-то после отметки в 150 т.р.;
- в том, что касается стандартов, заслуживает упоминания SQL, а также нотации UML & ARIS. Для меня видеть такую популярность языка запросов довольно неожиданно, т.к. за несколько лет мне не приходилось активно его использовать. И это, пожалуй, единственный вывод данного исследования, который противоречит здравому смыслу личному опыту.