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


В первую очередь сформулируем задачу и разработаем план:

Задача:

Посмотреть все вакансии на рынке и узнать общие требования, указанные в них.

План:

1. Собрать все вакансии по запросу Datа Scientist в удобном для обработки формате,
2. Выяснить часто встречамые в описание слова и словосочетания.

Для реализации понадобится немного знаний в SQL и Python.

Если их нет, то вам сюда
Для изучения SQL рекомендую sqlbolt.com, а для Python мобильное приложение SoloLearn (GooglePlay и AppStore).

Сбор данных


Источник: 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 запрос и получаем ответ:

Полный JSON
{
    "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.

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)


  1. Plesser
    15.09.2017 09:38
    -1

    Прикольно получилось!


  1. cointegrated
    15.09.2017 10:43
    +1

    Чтобы приводить слова к базовой форме, можно заюзать pymorphy2


    Вместо отсеивания стоп-слов можно просто отранжировать слова и фразы по tf-idf, считая "документом" пачку вакансий по одному и тому же запросу. Тогда мусор типа "будет плюсом" уйдёт в низ рейтинга.


    Идея на будущее: оценить, какие навыки ценятся выше всего (коррелируют с высокой зарплатой).


    1. alexmikh Автор
      15.09.2017 12:53

      Спасибо, это учту при следующем анализе.


  1. frostoffmax
    15.09.2017 12:52

    Можно прикрутить для синонимов и понимания отношения слов word2vec, также там можно будет посмотреть на опечатки.


    1. alexmikh Автор
      15.09.2017 12:54

      Я рассматривал word2vec. Стыдно признаться, ну для мне было сложновато и я отложил его на будущее:)


      1. frostoffmax
        15.09.2017 13:51
        +1

        Есть такой неплохой туториал по Word2vec, можно поиграться, надеюсь поможет
        nbviewer.jupyter.org/github/Yorko/mlcourse_open/blob/master/jupyter_notebooks/tutorials/word2vec_demonzheg.ipynb