В первую очередь сформулируем задачу и разработаем план:
Задача:
Посмотреть все вакансии на рынке и узнать общие требования, указанные в них.
План:
1. Собрать все вакансии по запросу Datа Scientist в удобном для обработки формате,
2. Выяснить часто встречамые в описание слова и словосочетания.
Для реализации понадобится немного знаний в SQL и Python.
Сбор данных
Источник: hh.ru
Сначала я подумал, что можно спарсить сайт. К счастью, я обнаружил, что у hh.ru есть API.
Для начало напишем функцию, которая получает список id вакансий для анализа. В параметрах функция получает текст поиска (сюда мы будем отправлять 'Datа Scientist') и зону поиска (согласно документации api), а возвращает список id. Для получения данных мы используем функцию api поиска вакансий:
def get_list_id_vacancies(area, text):
url_list = 'https://api.hh.ru/vacancies'
list_id = []
params = {'text': text, 'area': area}
r = requests.get(url_list, params=params)
found = json.loads(r.text)['found']; #кол-во всего найденных вакансий
if found <= 500: # API не отдает больше 500 вакансий за раз (на странице). Если найденно меньше 500 то получим все сразу.
params['per_page'] = found
r = requests.get(url_list, params=params)
data = json.loads(r.text)['items']
for vac in data:
list_id.append(vac['id'])
else:
i = 0;
while i <= 3: # если больше 500 то "перелистываем" страницы с 0 по 3 и получаем все вакансии поочереди. API не отдаст вам больше 2000 вакансий, поэтому тут захардкожено 3.
params['per_page'] = 500
params['page'] = i
r = requests.get(url_list, params=params)
if 200 != r.status_code:
break
data = json.loads(r.text)['items']
for vac in data:
list_id.append(vac['id'])
i += 1
return list_id
Для отладки я отправлял запосы напрямую в API. Рекомендую для этого использовать приложение chrome Postman.
После этого надо получить подробную информацию о каждой вакансии:
def get_vacancy(id):
url_vac = 'https://api.hh.ru/vacancies/%s'
r = requests.get(url_vac % id)
return json.loads(r.text)
Теперь у нас есть список вакансий и функция, которая получает подробную информацию о каждой вакансии. Надо принять решение куда записать полученные данные. У меня было два варианта: сохранить все в файл csv или создать базу данных. Так как мне проще писать запросы SQL, чем анализировать в Excel я выбрал базу данных. Предварительно нужно создать базу данных и таблицы в которые мы будем делать записи. Для этого анализируем, что отвечает API и принимаем решения, какие поля нам нужны.
Вставляем в Postman ссылку api, например api.hh.ru/vacancies/22285538, делаем GET запрос и получаем ответ:
{
"alternate_url": "https://hh.ru/vacancy/22285538",
"code": null,
"premium": false,
"description": "<p>Мы занимаемся....",
"schedule": {
"id": "fullDay",
"name": "Полный день"
},
"suitable_resumes_url": null,
"site": {
"id": "hh",
"name": "hh.ru"
},
"billing_type": {
"id": "standard_plus",
"name": "Стандарт+"
},
"published_at": "2017-09-05T11:43:08+0300",
"test": null,
"accept_handicapped": true,
"experience": {
"id": "noExperience",
"name": "Нет опыта"
},
"address": {
"building": "36с7",
"city": "Москва",
"description": null,
"metro": {
"line_name": "Калининская",
"station_id": "8.470",
"line_id": "8",
"lat": 55.736478,
"station_name": "Парк Победы",
"lng": 37.514401
},
"metro_stations": [
{
"line_name": "Калининская",
"station_id": "8.470",
"line_id": "8",
"lat": 55.736478,
"station_name": "Парк Победы",
"lng": 37.514401
}
],
"raw": null,
"street": "Кутузовский проспект",
"lat": 55.739068,
"lng": 37.525432
},
"key_skills": [
{
"name": "Математическое моделирование"
},
{
"name": "Анализ рисков"
}
],
"allow_messages": true,
"employment": {
"id": "full",
"name": "Полная занятость"
},
"id": "22285538",
"response_url": null,
"salary": {
"to": 90000,
"gross": false,
"from": 50000,
"currency": "RUR"
},
"archived": false,
"name": "Математик/ Data scientist",
"contacts": null,
"employer": {
"logo_urls": {
"90": "https://hhcdn.ru/employer-logo/1680554.png",
"240": "https://hhcdn.ru/employer-logo/1680555.png",
"original": "https://hhcdn.ru/employer-logo-original/309546.png"
},
"vacancies_url": "https://api.hh.ru/vacancies?employer_id=1475513",
"name": "Аналитическое агентство Скориста",
"url": "https://api.hh.ru/employers/1475513",
"alternate_url": "https://hh.ru/employer/1475513",
"id": "1475513",
"trusted": true
},
"created_at": "2017-09-05T11:43:08+0300",
"area": {
"url": "https://api.hh.ru/areas/1",
"id": "1",
"name": "Москва"
},
"relations": [],
"accept_kids": false,
"response_letter_required": false,
"apply_alternate_url": "https://hh.ru/applicant/vacancy_response?vacancyId=22285538",
"quick_responses_allowed": false,
"negotiations_url": null,
"department": null,
"branded_description": null,
"hidden": false,
"type": {
"id": "open",
"name": "Открытая"
},
"specializations": [
{
"profarea_id": "14",
"profarea_name": "Наука, образование",
"id": "14.91",
"name": "Информатика, Информационные системы"
},
{
"profarea_id": "14",
"profarea_name": "Наука, образование",
"id": "14.141",
"name": "Математика"
}]
}
Все, что не планируем анализировать удаляем из JSON.
{
"description": "<p>Мы занимаемся....",
"schedule": {
"id": "fullDay",
"name": "Полный день"
},
"accept_handicapped": true,
"experience": {
"id": "noExperience",
"name": "Нет опыта"
},
"key_skills": [
{
"name": "Математическое моделирование"
},
{
"name": "Анализ рисков"
}
],
"employment": {
"id": "full",
"name": "Полная занятость"
},
"id": "22285538",
"salary": {
"to": 90000,
"gross": false,
"from": 50000,
"currency": "RUR"
},
"name": "Математик/ Data scientist",
"employer": {
"name": "Аналитическое агентство Скориста",
},
"area": {
"name": "Москва"
},
"specializations": [
{
"profarea_id": "14",
"profarea_name": "Наука, образование",
"id": "14.91",
"name": "Информатика, Информационные системы"
},
{
"profarea_id": "14",
"profarea_name": "Наука, образование",
"id": "14.141",
"name": "Математика"
}]
}
На основе этого JSON делаем БД. Это несложно, поэтому я это опущу :)
Реализуем модуль взаимодействия с базой данных. Я использовал MySQL:
def get_salary(vac): #зарплата не всегда заполена. Поэтому при обращение внутрь будет ошибка, для этого пишем отдельную функцию, которая вернет словарь с None, если данные пустые.
if vac['salary'] is None:
return {'currency':None , 'from':None,'to':None,'gross':None}
else:
return {'currency':vac['salary']['currency'],
'from':vac['salary']['from'],
'to':vac['salary']['to'],
'gross':vac['salary']['gross']}
def get_connection():
conn = pymysql.connect(host='localhost', port=3306, user='root', password='-', db='hh', charset="utf8")
return conn
def close_connection(conn):
conn.commit()
conn.close()
def insert_vac(conn, vac, text):
a = conn.cursor()
salary = get_salary(vac)
print(vac['id'])
a.execute("INSERT INTO vacancies (id, name_v, description, code_hh, accept_handicapped, area_v, employer, employment, experience, salary_currency, salary_from, salary_gross, salary_to, schedule_d, text_search) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)",
(vac['id'], vac['name'], vac['description'],
vac['code'], vac['accept_handicapped'], vac['area']['name'],
vac['employer']['name'],
vac['employment']['name'], vac['experience']['name'], salary['currency'],
salary['from'], salary['gross'],
salary['to'], vac['schedule']['name'], text))
for key_skill in vac['key_skills']:
a.execute("INSERT INTO key_skills(vacancy_id, name) VALUES(%s, %s)",(vac['id'], key_skill['name']))
for spec in vac['specializations']:
a.execute("INSERT INTO specializations(vacancy_id, name, profarea_name) VALUES(%s, %s, %s)",
(vac['id'], spec['name'], spec['profarea_name']))
a.close()
Теперь собираем все вместе, добавив в файл метод main()
text_search = 'data scientist'
list_id_vacs = get_list_id_vacancies(text_search)
vacs = []
for vac_id in list_id_vacs:
vacs.append(get_vacancy(vac_id))
conn = get_connection()
for vac in vacs:
insert_vac(conn, vac, text_search)
close_connection(conn)
Меняя переменные text_search и area мы получаем разные вакансии из разных регионов.
На этом data mining закончен и переходим к интересному.
Анализ текста
Основным вдохновителем стала статья о поиске популряных фраз в сериале How I met your mother
Для начала будем получать описание всех вакансий из базы:
def get_vac_descriptions(conn, text_search):
a = conn.cursor()
a.execute("SELECT description FROM vacancies WHERE text_search = %s", text_search)
descriptions = a.fetchall()
a.close
return descriptions
Для работы с текстом мы будем использовать пакет nltk. По аналогии с расположенной выше статьей пишем функцию получения популярных фраз из текста:
def get_popular_phrase(text, len, count_phrases):
phrase_counter = Counter()
words = nltk.word_tokenize(text.lower())
for phrase in nltk.ngrams(words, len):
if all(word not in string.punctuation for word in phrase):
phrase_counter[phrase] += 1
return phrase_counter.most_common(count_phrases)
descriptions = get_vac_descriptions(get_connection(), 'data scientist')
text = ''
for description in descriptions:
text = text + description[0]
result = get_popular_phrase(text, 1, 20)
for r in result:
print(" ".join(r[0]) + " - " + str(r[1]))
Объединяем все описанные выше методы в методе main и запускаем его:
def main():
descriprions = get_vac_descriptions(get_connection(), 'data scientist')
text = ''
for descriprion in descriprions:
text = text + descriprion[0]
result = get_popular_phrase(text, 4, 20, stopwords)
for r in result:
print(" ".join(r[0]) + " - " + str(r[1]))
main()
Выполняем и видим:
li — 2459
/li — 2459
и — 1297
p — 1225
/p — 1224
в — 874
strong — 639
/strong — 620
and — 486
ul — 457
/ul — 457
с — 415
на — 341
данных — 329
data — 313
the — 308
опыт — 275
of — 269
для — 254
работы — 233
Видим, что в результат попало очень много слов, которые характерны для всех вакансий и теги, которые используются в описание. Уберем эти слова из анализа. Для этого нам нужен список стоп слов. Сформируем его автоматически, проанализровав вакансии из другой сферы. Я выбрал «повар», «уборщица» и «слесарь».
Вернемся к началу и получим вакансии по этим запросам. После этого добавим функцию получения стоп слов.
def get_stopwords():
descriptions = get_vac_descriptions(get_connection(), 'повар') + get_vac_descriptions(get_connection(), 'уборщица') + get_vac_descriptions(get_connection(), 'слесарь')
text = ''
for description in descriptions:
text = text + descriprion[0]
stopwords = []
list = get_popular_phrase(text, 1, None, 200) #размер списка стоп слов
for i in list:
stopwords.append(i[0][0])
return stopwords
Так же видим английские the и of. Поступим проще и уберем вакансии на английском.
Внесем изменения в main():
for description in descriptions:
if detect(description[0]) != 'en':
text = text + description[0]
Теперь результа выглядит так:
данных — 329
data — 180
анализа — 157
обучения — 134
машинного — 129
моделей — 128
области — 101
алгоритмов — 87
python — 86
задач — 82
задачи — 82
разработка — 77
анализ — 73
построение — 68
методов — 66
будет — 65
статистики — 56
высшее — 55
знания — 53
learning — 52
Ну это одно слово, оно не всегда отражает истину. Посмотрим что покажут словосочетания из 2 слов:
машинного обучения — 119
анализа данных — 56
machine learning — 44
data science — 38
data scientist — 38
big data — 34
математических моделей — 34
data mining — 28
алгоритмов машинного — 27
математической статистики — 23
будет плюсом — 21
статистического анализа — 20
обработки данных — 18
английский язык — 17
анализ данных — 17
том числе — 17
а также — 17
методов машинного — 16
области анализа — 15
теории вероятности — 14
Результаты анализа.
Более явно выдает результат запрос по двум словам, нам надо знать:
- Машинное обучение
- Математические модели
- Data mining
- Python
- Математическую статистику
- Английский язык
- Теория вероятности
Ничего нового, но было весело :)
Выводы.
Это далеко от идеального решения.
Ошибки:
1. Не надо исключать вакансии на английском, надо их перевести.
2. Не все стоп слова исключены.
3. Нужно привести все слова к базовой форме (машинного -> машинное, анализа -> анализ и т.п).
4. Придумать метод по которому вычислить более оптимальный список стоп слов. Ответить на вопрос «почему 200?», «почему уборщица?».
5. Надо придумать как анализировать результат автоматически, понимать, что одно или два слова несут смысл или больше.
Комментарии (6)
cointegrated
15.09.2017 10:43+1Чтобы приводить слова к базовой форме, можно заюзать
pymorphy2
Вместо отсеивания стоп-слов можно просто отранжировать слова и фразы по tf-idf, считая "документом" пачку вакансий по одному и тому же запросу. Тогда мусор типа "будет плюсом" уйдёт в низ рейтинга.
Идея на будущее: оценить, какие навыки ценятся выше всего (коррелируют с высокой зарплатой).
frostoffmax
15.09.2017 12:52Можно прикрутить для синонимов и понимания отношения слов word2vec, также там можно будет посмотреть на опечатки.
alexmikh Автор
15.09.2017 12:54Я рассматривал word2vec. Стыдно признаться, ну для мне было сложновато и я отложил его на будущее:)
frostoffmax
15.09.2017 13:51+1Есть такой неплохой туториал по Word2vec, можно поиграться, надеюсь поможет
nbviewer.jupyter.org/github/Yorko/mlcourse_open/blob/master/jupyter_notebooks/tutorials/word2vec_demonzheg.ipynb
Plesser
Прикольно получилось!