Анализируем требования рынка для data scientist

    В интернете много информации, что 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. Надо придумать как анализировать результат автоматически, понимать, что одно или два слова несут смысл или больше.
    Поделиться публикацией

    Похожие публикации

    Комментарии 7

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

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


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


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

          0
          Спасибо, это учту при следующем анализе.
          0
          Можно прикрутить для синонимов и понимания отношения слов word2vec, также там можно будет посмотреть на опечатки.
          0
          90% рекрутёров ничего не понимает в области, в которой они ищут специалистов, поэтому очень полезно знать, какие ключевые слова они используют в поиске. Можно втыкать в резюме такие ключи по сеошному принципу — видел книжку по машинному обучению, стоял рядом с биг дата, имеется ручной python.
          ;)

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

          Самое читаемое