В наше динамичное время программисту необходимо держать руку на пульсе и постоянно осваивать новые навыки, чтобы оставаться востребованным специалистом.
Я уже около двух лет программирую на Python, и сейчас наступил момент осознанно подойти к освоению новых навыков. Для этого я решил проанализировать вакансии и представить востребованные навыки в виде графа. Я ожидал увидеть, что навыки будут образовывать кластеры, соответствующие разным специальностям: backend разработке, data science и др. А как же обстоят дела на самом деле? Обо всём по порядку.
Сначала нужно было определиться с источником данных. Я рассмотрел несколько вариантов: Хабр Карьеру, Яндекс Работу, HeadHunter и другие. Наиболее удобным показался HeadHunter, так как здась в вакансиях присутствует список ключевых навыков и есть удобный открытый API.
Изучив API HeadHunter-a, я решил сначала парсить список id вакансий по заданному ключевому слову (в данном случае это “python”), а затем у каждой вакансии парсить список соответствующих тэгов.
При поиске вакансий, вакансии возвращаются постранично, максимальное количество вакансий на одну страницу равно 100. Сначала я сохранял полную выдачу в виде списка постраничных ответов.
Для этого был использован модуль requests. В поле “user-agent”, в соответствии с API, было вписано имя виртуального браузера, чтобы HH понимал, что к нему обращается скрипт. Делал небольшую задержку между запросами, чтобы не перегружать сервер.
В результате я получил список словарей ответов, где каждый словарь соответствовал одной странице поисковой выдачи.
Как оказалось, API hh.ru ограничивает максималь��ое количество отдаваемых вакансий двумя тысячами, то есть при 100 вакансиях на страницу, максимальное количество страниц может быть 20. По ключевому слову Python было возвращено 20 страниц вакансий, и это значит, что реальных вакансий по Python скорее всего больше.
Чтобы получить списко тэгов я делал следующее:
Списки тэгов сохранялись в виде словаря
Интересно, что из 2000 просмотренных вакансий, тэги имелись только у 1579 вакансий.
Теперь нужно обработать теги и перевести их в удобный для отображения в виде графа формат, а именно:
Приведение к единому регистру, вычисление частоты встречаемости каждого тэга, фильтрация по размеру нода осуществлялись следующим образом.
Попарную встречаемость вычислял так. Сначала создавал словарь, в котором ключами были все возможные пары тэгов в виде tuple, а значения равнялись нулям. Затем проходился по спискам тэгов и увеличивал счётчики для каждой встречаемой пары. Затем я удалял все те элементы, значения которых равнялись нулю.
На выходе формировал словарь вида
Для визуализации графа я использовал модуль networkx. Вот, что получилось с первого раза без фильтрации нодов.

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

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

Динамическая визуализация доступна по ссылке. Обратите внимание, что ноды можно тянуть.
Как мы види��, граф получился сильно переплетённым и чётко обозначенных кластеров с первого взгляда обнаружить не удаётся. Сразу можно заметить несколько больших нодов, которые востребованы больше всего: linux, sql, git, postgresql и django. Также есть навыки средней популярности и редко встречаемые навыки.
Кроме этого, можно обратить внимание на то, что навыки всё-таки формируют кластеры по профессиям располагаясь по разные стороны от центра:
Это описание кластеров основано на моих знаниях и может содержать ошибки, но сама идея, надеюсь, ясна.
На основе полученных результатов можно сделать следующие выводы:
Надеюсь, вам понравилось, и данный анализ будет для вас полезен.
Взглянуть на код или поучаствовать в его развитии можно по ссылкам: GitHub проект, Observable ноутбук с визуализацией
Успехов в освоении новых горизонтов!
Я уже около двух лет программирую на 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 ноутбук с визуализацией
Успехов в освоении новых горизонтов!