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

Я уже около двух лет программирую на Python, и сейчас наступил момент осознанно подойти к освоению новых навыков. Для этого я решил проанализировать вакансии и представить востребованные навыки в виде графа. Я ожидал увидеть, что навыки будут образовывать кластеры, соответствующие разным специальностям: backend разработке, data science и др. А как же обстоят дела на самом деле? Обо всём по порядку.

Сбор данных


Сначала нужно было определиться с источником данных. Я рассмотрел несколько вариантов: Хабр Карьеру, Яндекс Работу, HeadHunter и другие. Наиболее удобным показался HeadHunter, так как здась в вакансиях присутствует список ключевых навыков и есть удобный открытый API.

Изучив API HeadHunter-a, я решил сначала парсить список id вакансий по заданному ключевому слову (в данном случае это “python”), а затем у каждой вакансии парсить список соответствующих тэгов.

При поиске вакансий, вакансии возвращаются постранично, максимальное количество вакансий на одну страницу равно 100. Сначала я сохранял полную выдачу в виде списка постраничных ответов.

Для этого был использован модуль requests. В поле “user-agent”, в соответствии с API, было вписано имя виртуального браузера, чтобы HH понимал, что к нему обращается скрипт. Делал небольшую задержку между запросами, чтобы не перегружать сервер.

ses = requests.Session()
ses.headers = {'HH-User-Agent': "Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20100101 Firefox/10.0"}

phrase_to_search = 'python'
url = f'https://api.hh.ru/vacancies?text={phrase_to_search}&per_page=100'
res = ses.get(url)

# getting a list of all pesponses
res_all = []
for p in range(res.json()['pages']):
    print(f'scraping page {p}')
    url_p = url + f'&page={p}'
    res = ses.get(url_p)
    res_all.append(res.json())
    time.sleep(0.2)

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

Как оказалось, API hh.ru ограничивает максималь��ое количество отдаваемых вакансий двумя тысячами, то есть при 100 вакансиях на страницу, максимальное количество страниц может быть 20. По ключевому слову Python было возвращено 20 страниц вакансий, и это значит, что реальных вакансий по Python скорее всего больше.

Чтобы получить списко тэгов я делал следующее:
  • итерировался по каждой странице поисковой выдачи,
  • итерировался по каждой вакансии на странице и получал id вакансии,
  • запрашивал подробности вакансии через API,
  • если в вакансии был указан хотя бы один тэг, то список тэгов добавлялся в список.

# parcing vacancies ids, getting vacancy page and scraping tags from each vacancy
tags_list = []
for page_res_json in res_all:
    for item in page_res_json['items']:
        vac_id = item['id']
        vac_res = ses.get(f'https://api.hh.ru/vacancies/{vac_id}')
        if len(vac_res.json()["key_skills"]) > 0:  # at least one skill present
            print(vac_id)
            tags = [v for v_dict in vac_res.json()["key_skills"] for _, v in v_dict.items()]
            print(' '.join(tags))
            tags_list.append(tags)
            print()
        time.sleep(0.1)

Списки тэгов сохранялись в виде словаря

res = {'phrase': phrase_to_search, 'items_number': len(tags_list), 'items': tags_list}
with open(f'./data/raw-tags_{phrase_to_search}.json', 'w') as fp:  # Serializing
    json.dump(res, fp)

Интересно, что из 2000 просмотренных вакансий, тэги имелись только у 1579 вакансий.

Форматирование данных


Теперь нужно обработать теги и перевести их в удобный для отображения в виде графа формат, а именно:
  • привести все тэги к единому регистру, так «machine learning», «Machine learning» и «Machine Learning» означают одно и то же,
  • вычислить величину нода как частоту встречаемости каждого тэга,
  • вычислить величину связи как частоту совместного встречания тэгов друг с другом.

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

tags_list['items'] = [[i.lower() for i in line] for line in tags_list['items']]

# counting words occurrences
flattened_list = [i for line in tags_list for i in line]
nodes_dict_all = {i: flattened_list.count(i) for i in set(flattened_list)}
nodes_dict = {k:v for k, v in nodes_dict_all.items() if v > del_nodes_count}

Попарную встречаемость вычислял так. Сначала создавал словарь, в котором ключами были все возможные пары тэгов в виде tuple, а значения равнялись нулям. Затем проходился по спискам тэгов и увеличивал счётчики для каждой встречаемой пары. Затем я удалял все те элементы, значения которых равнялись нулю.

# tags connection dict initialization
formatted_tags = {(tag1, tag2): 0 for tag1, tag2 in itertools.permutations(set(nodes_dict.keys()), 2)}

# count tags connection
for line in tags_list:
    for tag1, tag2 in itertools.permutations(line, 2):
        if (tag1, tag2) in formatted_tags.keys():
            formatted_tags[(tag1, tag2)] += 1

# filtering pairs with zero count
for k, v in formatted_tags.copy().items():
    if v == 0:
        del formatted_tags[k]

На выходе формировал словарь вида

{
'phrase': phrase searched,
'items_number': number of vacancies parced, 
'items': {
 	"nodes": [
			{
			"id": tag name, 
		 	"group": group id, 
		 	"popularity": tag count
			},
		… 
		] 
	"links": [
			{
			"source": pair[0], 
			"target": pair[1], 
			"value": pair count
			},
		…
		]
	}
}

nodes = []
links = []
for pair, count in formatted_tags.items():
    links.append({"source": pair[0], "target": pair[1], "value": count})

max_count = max(list(nodes_dict.values()))
count_step = max_count // 7
for node, count in nodes_dict.items():
    nodes.append({"id": node, "group": count // count_step, "popularity": count})

data_to_dump = in_json.copy()
data_to_dump['items'] = {"nodes": nodes, "links": links}

Визуализация на Python


Для визуализации графа я использовал модуль networkx. Вот, что получилось с первого раза без фильтрации нодов.



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

Поэтому я отфильтровал самые маленькие ноды, размер которых менее 5, а также сделал связи серого цвета. На этой картинке я ещё не привёл слова к единому регистру, при этом попробовал удалить самый большой нод «Python», чтобы разрядить связи.



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

Визуализация на JavaScript


Я бы наверно продолжил ковырять этот код, если бы в этот момент у меня не появилась подмога в виде брата. Он активно включился в работу и сделал красивое динамическое отображение на основе JavaScript модуля D3.

Получилось вот так.


Динамическая визуализация доступна по ссылке. Обратите внимание, что ноды можно тянуть.

Анализ результатов


Как мы види��, граф получился сильно переплетённым и чётко обозначенных кластеров с первого взгляда обнаружить не удаётся. Сразу можно заметить несколько больших нодов, которые востребованы больше всего: linux, sql, git, postgresql и django. Также есть навыки средней популярности и редко встречаемые навыки.

Кроме этого, можно обратить внимание на то, что навыки всё-таки формируют кластеры по профессиям располагаясь по разные стороны от центра:

  • слева внизу – анализ данных,
  • внизу – базы данных,
  • справа внизу – front-end разработка,
  • справа – тестирование,
  • справа вверху – web разработка,
  • слева вверху – machine learning.

Это описание кластеров основано на моих знаниях и может содержать ошибки, но сама идея, надеюсь, ясна.

На основе полученных результатов можно сделать следующие выводы:
  • нужно осваивать навыки, соответствующие большим нодам, они пригодятся всегда,
  • нужно осваивать навыки соответствующего вашим интересам кластера.

Надеюсь, вам понравилось, и данный анализ будет для вас полезен.

Взглянуть на код или поучаствовать в его развитии можно по ссылкам: GitHub проект, Observable ноутбук с визуализацией

Успехов в освоении новых горизонтов!