Идея

Период: Январь-Февраль

Тип проекта: рекомендательный

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

Наше приложение для анализа карьерных перспектив поможет:

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

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

Логика и данные проекта: После того, как данные были собраны методом парсинга сайта для поиска работы HeadHunter’а с помощью его API и объединения в единый файл через Python (11007929 наблюдений) , были предобработаны и очищены уже в R(1897663 наблюдений), мы приступили к построению языковой и рекомендательной модели на основе тематического моделирования на базе Latent Dirichlet Allocation и эмбеддинговой модели на базе text2vec. В результате мы выявили и описали темы, подвязав их под сферы на основе нашей логики, а затем включили их в модель рекомендации на основе находящихся в них навыков.

Распределение ролей

Период: Январь-Февраль

Роли в нашей команде были распределены так:

  1. Ильященко Константин: менеджер проекта, парсинг данных, работа с API hh.ru
  2. Калюжная Олеся: предобработка данных, дизайн приложения
  3. Босоногов Семен: работа с языковыми моделями, написание приложения
  4. Капендюхина Алёна: предобработка данных, дизайн приложения
  5. Казаков Вадим: работа с ML алгоритмами, написание приложения

Предобработка

Парсинг данных в Python

Период: Март

Данные: данные взяты с сайта для поиска работы hh.ru с использованием API.

Мы воспользовались открытым API и прописали несколько функций на Python из-за удобства работы с веб-страницами и API. В результате мы получили 4 функции, каждая из которых отвечает за регион, работодателя, специализацию и вакансии.

# парсинг данных https://colab.research.google.com/drive/1DtGCF1rcu0k4xNO70ITgxz3IyZBvmmZ3?usp=sharing

import requests # Для запросов по API
import json  # Для обработки полученных результатов
import time # Для задержки между запросами
import os  # Для работы с файлами
import pandas as pd  # Для формирования датафрейма с результатами

# References: 
#    https://prog.world/working-with-the-headhunter-api-with-python/
#    https://towardsdatascience.com/how-to-convert-json-into-a-pandas-dataframe-100b2ae1e0d8
#    https://github.com/aseventura/parse_hh

# Регионы
def getAreas():
    req = requests.get('https://api.hh.ru/areas')   # url регионов
    data = req.content.decode()
    req.close()
    jsObj = json.loads(data)
    areas = []
    for k in jsObj:
        for i in range(len(k['areas'])):
            if len(k['areas'][i]['areas']) != 0: # Если у зоны есть внутренние зоны
                for j in range(len(k['areas'][i]['areas'])):
                    areas.append([k['id'], 
                                  k['name'], 
                                  k['areas'][i]['areas'][j]['id'],
                                  k['areas'][i]['areas'][j]['name']])
            else:  # Если у зоны нет внутренних зон
                areas.append([k['id'], 
                              k['name'], 
                              k['areas'][i]['id'], 
                              k['areas'][i]['name']])
    return areas

areas = getAreas()

pd.DataFrame(areas, columns=["country_id", "country_name", "id", "name"])

# Работодатели
employers_full = []
def get_emp(i=0):
    try:
        req = requests.get('https://api.hh.ru/employers')
        data = req.content.decode()
        req.close()
        count_of_employers = json.loads(data)['found']
        employers = []
        employers_full_all = employers_full
        j = count_of_employers
        while i < j:
                req = requests.get('https://api.hh.ru/employers/'+str(i+1))
                data = req.content.decode()
                req.close()
                jsObj = json.loads(data)
                try:
                    employers.append([jsObj['id'], jsObj['name']])
                    i += 1
                    print([jsObj['id'], jsObj['name']])
                except:
                    i += 1
                    j += 1
                if i%200 == 0:
                    time.sleep(0.2)
                    employers_full_all = employers_full_all + employers
                    employers = []
        return (employers_full)
    except (ConnectionError, TimeoutError, ConnectionResetError):
        employers_full_all = employers_full_all + employers
        get_emp(i = int(employers_full_all[-1][0]))

#pd.DataFrame(employers_full).to_csv("employers_full")
get_emp()

# Специализации
url = "https://api.hh.ru/specializations"
specs = []
response = requests.get(url)
if response.status_code == 200:
    specializations = response.json()
else:
    print("Error: unable to retrieve specializations from HH API.")
for name in specializations:
  for spec in name['specializations']:
    descr = [name["id"], name["name"], spec["id"], spec["name"]]
    specs.append(descr)

#specs = pd.read_json(url) #все можно было сделать в одну строчку............
headers = ["id_head", "name_head", "id", "name"]

pd.DataFrame(specs, columns=headers)

# Вакансии
# ref for vac https://api.hh.ru/vacancies/33334899 "published_at":"2019-08-30T17:21:24+0300"
# https://api.hh.ru/vacancies/41000000 "published_at":"2020-12-15T10:56:01+0300"
vacancies_full = []
def get_vac(i=33334899, date_fr="2020-01-01", date_to="2023-01-01"):
    ############### packages
    import requests # Для запросов по API
    import json  # Для обработки полученных результатов
    import time # Для задержки между запросами
    import os  # Для работы с файлами
    import pandas as pd  # Для формирования датафрейма с результатами
    from datetime import datetime # Для времени
    ################ converting strings to dates
    date_fr = datetime.strptime(date_fr, '%Y-%m-%d').date()
    date_to = datetime.strptime(date_to, '%Y-%m-%d').date()
    ################ dataframe for appending
    url = 'https://api.hh.ru/vacancies/'+str(i)
    response = requests.get(url)
    vacancies_head = response.json()
    #data_vacancies = pd.DataFrame(vacancies_head)
    #print(data_vacancies)
    ################ avoiding errors
    try:
        req = requests.get('https://api.hh.ru/vacancies')
        data = req.content.decode()
        req.close()
        count_of_vacancies = json.loads(data)['found']
        vacancies = []
        vacancies_full_all = vacancies_full
        j = count_of_vacancies + i
        ############# ids
        while i < 70000000:
                req = requests.get('https://api.hh.ru/vacancies/'+str(i))

                ######## avoiding 404 error within hh id
                if req.status_code==200:
                    data = req.content.decode()
                    req.close()
                    jsObj = json.loads(data)
                    string_time = jsObj["published_at"]
                    date_of_vac = datetime.strptime(string_time, '%Y-%m-%dT%H:%M:%S%z').date()

                  ########################## date comparing part
                    if date_of_vac > date_fr:
                        vacancies.append([jsObj])
                        #try:
                        #    data_vacancies = pd.concat([data_vacancies,pd.DataFrame(jsObj)]) #выдает ошибку присоединения датасета
                        #except:
                        #    pass
                        i += 1 
                        print([jsObj])
                    #elif date_of_vac == date_to:
                    #    break
                    else:
                        i += 2
                        j += 2
                  ########################### 

                else:
                    i += 2
                    j += 2

                ############## back up and sleep
                if i%200 == 0:
                    time.sleep(0.2)
                    vacancies_full_all = vacancies_full_all + vacancies_full
                    vacancies = []
        return (vacancies_full)

    except (ConnectionError, TimeoutError, ConnectionResetError):
        vacancies_full_all = vacancies_full_all + vacancies
        get_vac(i = int(vacancies_full[-1][0]['id']),  date_fr="2020-01-01", date_to="2023-01-01")

В результате мы получили 11007929 вакансий, около 1.8М работодателей, около 6000 различных регионов и 630 специализаций, которые впоследствии нам не понадобились, так как мы решили остановиться только на вакансиях и не перегружать данные дополнительной информацией из-за её объёма.

Так как API блокирует частые запросы, мы столкнулись с проблемой, что едиоразовый запуск кода не способен выгрузить все и сразу, из-за этого мы запускали код на разных устройствах, поэтому впоследствии мы объединяли файлы воедино. Также структура JSON ответа сервера очень глубокая, ее также было необходимо правильно распаковать и присоединить.

# соединение данных в один файл https://colab.research.google.com/drive/1ZLiRhc-RaB5jKMkOTQE13WFBR28GxVeY?usp=sharing 

import ast
import pandas as pd
# пакеты, по идее аст стандартный и не требует отдельной установки

with open("название файла", "r", encoding = "utf-8-sig") as f:
    a = f.readlines()
#открываем файл

ast.literal_eval(a[t])[0][0] 
#t - номер вакансии в доке, начинается с 1!
# если вдруг будет выдавать out jf range или другую ошибку по длине списка, то попробовать
# ast.literal_eval(a[t])[0]

ids, name, published_at, area_id, experience_id, schedule_id, employment_id, employer_id, salary_from, salary_to, currency =[],[],[],[],[],[],[],[],[],[],[]
# создаём списки


for n in range(1,len(a)-1):
    ids.append(ast.literal_eval(a[n])[0][0]["id"])
    name.append(ast.literal_eval(a[n])[0][0]["name"])
    published_at.append(ast.literal_eval(a[n])[0][0]["published_at"])
    area_id.append(ast.literal_eval(a[n])[0][0]["area"]["id"]) #area_id
    experience_id.append(ast.literal_eval(a[n])[0][0]["experience"]["id"]) #experience_id
    schedule_id.append(ast.literal_eval(a[n])[0][0]["schedule"]["id"]) #schedule_id
    employment_id.append(ast.literal_eval(a[n])[0][0]["employment"]["id"]) #employment_id
    employer_id.append(ast.literal_eval(a[n])[0][0]["employer"]["name"]) #employer_id
#    salary_from.append(ast.literal_eval(a[n])[0][0]["salary"]["from"]) #salary_from в зарплатах много скипов, их достаём ниже отдельно
#    salary_to.append(ast.literal_eval(a[n])[0][0]["salary"]["to"]) #salary_to
#    currency.append(ast.literal_eval(a[n])[0][0]["salary"]["currency"])
    print(ast.literal_eval(a[n])[0][0]["id"])  # иногда ошибки выскакивают просто на приколе, эта штука поможет найти проблемную вакансию, из-за которой ошибка вылазит

len(employer_id) #проверка длины того, что достали. Должно быть на один меньше предыдущего чека длины

key_skills = [] #скиллы обрабатываем отдельно, так как там более тяжелая обработка

for n in range(1,len(a)-1):
    skills = ''
    for i in range (1, len(ast.literal_eval(a[n])[0][0]['key_skills'])):
        skills = skills+ast.literal_eval(a[n])[0][0]['key_skills'][i]['name']+'|'
    skills = skills[0:len(skills)-1]
    key_skills.append(skills)

len(key_skills) #ещё один чек длины, должно совпасть с предыдущим чеком

 #обрабатываем зп
for n in range(1,len(a)-1):
    if ast.literal_eval(a[n])[0][0]["salary"] == None:
        salary_from.append(None)
        salary_to.append(None)
        currency.append(None)
    else:
        salary_from.append(ast.literal_eval(a[n])[0][0]["salary"]["from"]) #salary_from
        salary_to.append(ast.literal_eval(a[n])[0][0]["salary"]["to"]) #salary_to
        currency.append(ast.literal_eval(a[n])[0][0]["salary"]["currency"])

len(currency) #ещё один чек длины, должно совпасть с предыдущим чеком

#делаем доп столбец, лишним не будет
salary_from_to = []

for n in range(len(salary_from)):
    if (salary_from[n] == None) and (salary_to[n] == None):
        salary_from_to.append(None)
    elif (salary_from[n] != None) and (salary_to[n] == None):
        salary_from_to.append("от "+str(salary_from[n]))
    elif (salary_from[n] == None) and (salary_to[n] != None):
        salary_from_to.append("до "+str(salary_to[n]))
    else:
        salary_from_to.append(str(salary_from[n])+'-'+str(salary_to[n]))

len(salary_from_to) #ещё один чек длины, должно совпасть с предыдущим чеком

#собираем всё в датафрейм, filename = имя тхт файла, чтобы не потерялось

filename_data = pd.DataFrame({'id' : ids, 'name':name, 'published_at': published_at, 'area_id': area_id, 'experience_id': experience_id,'schedule_id': schedule_id, 'employment_id': employment_id, 'employer_id': employer_id, 'key_skills': key_skills, 'salary_from': salary_from, 'salary_to': salary_to, 'salary_from_to': salary_from_to, 'currency': currency})

filename_data
#чек дф

Впоследствии мы получили готовый набор данных того же объёма наблюдений внутри, в результате чего для местной предобработки мы переходим в R.

Предобработка данных в R

Период: Март-Апрель

В качестве предобработки мы провели несколько манипуляций.

  1. Убрали все пропущенные значения:
  • пропуски очень сильно мешают и захламляют датасет при его описании
  • также с нашим количеством наблюдений мы можем себе это позволить
  1. Убрали все иностранные валюты:
  • основной фокус был на отечественный рынок труда, который предполагает рубли
  • невозможность постоянно актуализировать данные ForEx’а для корректного перевода зарплат из валюты в рубли
  1. Перевели все заработные платы в единый формат gross:
  • для возможности сравнения и предсказания

Загрузка нужных библиотек.

## библиотеки для обработки данных
library(tidyverse)
library(readr)
library(readxl)
library(splitstackshape)

## для обработки и анализа текста
library(stopwords)

## additional libraries for working with data and html
library(data.table)

## тексты
library(tidytext) # обработка текста
library(LDAvis) # визуализация LDA
library(topicmodels) # тематическое моделирование
library(text2vec) # embeddings
library(SnowballC)
library(stringi)

## для поиска косинусного расстояния
library(stringdist) 

Предобработка данных. Все комментарии находятся внутри чанков.

# Грузим изначальные данные
vac <- read_csv("D:/Downloads/vacancies_19-21.csv")

# Создаём копию для работы
vac1 <- vac

# Очищаем данные от всех пропущенных значений (уменьшаем сэмпл для работы для оптимальной ресурсоемкости)
vac1 <- vac1 %>% drop_na()

# Смотрим на данные
summary(vac1$salary.from)
summary(vac1$salary.to)
summary(vac1$salary.gross)
table(factor(vac1$salary.currency))

# Оставляем только отечественный рынок труда За счёт зп в отечественной валюте (рубли)
vac1 <- vac1 %>% filter(salary.currency == "RUR")

# gross = TRUE - до вычета , gross = FALSE - после вычета. Приводим к одному формату
vac1$salary.gross <- as.factor(vac1$salary.gross)

# нижний порог
vac_gross <- vac1
vac_gross <- vac_gross %>% mutate(salary_gross_from = 
                          case_when(salary.gross == "FALSE" ~ salary.from-salary.from*0.13,
                                    salary.gross == "TRUE" ~ salary.from))
options(scipen = 999)
# верхний порог
vac_gross <- vac_gross %>% mutate(salary_gross_to = 
                          case_when(salary.gross == "FALSE" ~ salary.to-salary.to*0.13,
                                    salary.gross == "TRUE" ~ salary.to))
 
# Удаляем лишние столбцы
vac_gross <- vac_gross %>% select(-salary.gross, -salary.currency, -salary.from, -salary.to)
# Объединяем в одну строку
vac_gross <- vac_gross %>% mutate(salary_gross = paste(salary_gross_from, salary_gross_to, sep = "-"))

# Добавляем значение в рублях
vac_gross <- vac_gross %>% mutate(salary_gross_rur = paste(salary_gross, "руб.", sep = " "))

# Проверка совпадений
upd_vacancies_19_21 <- read_csv("D:/Сourse topic/upd_vacancies 19-21.csv") %>% select(-`...1`)
table(upd_vacancies_19_21 == vac_gross)

rm(vac, vac_gross, vac1)
# Сэмпл за 2020-2023
vse_vakansii <- read_csv("D:/Downloads/vse_vakansii.csv")

# Переименуем колонки для удобства работы
names(vse_vakansii) <- c("id", "name", "published_at", "area.id", "experience.id","schedule.id","employment.id", "employer.id", "key_skills", "salary.from", "salary.to", "salary_from_to", "salary.currency")

# Создаём копию для работы
vac1 <- vse_vakansii

# Очищаем данные от всех пропущенных значений (уменьшаем сэмпл для работы для оптимальной ресурсоемкости)
vac1 <- vac1 %>% drop_na()
vac1$salary_gross_from <- as.integer(vac1$salary.from)
vac1$salary_gross_to <- as.integer(vac1$salary.to)

# Смотрим на данные
summary(vac1$salary.from)
summary(vac1$salary.to)
table(factor(vac1$salary.currency))

# Оставляем только отечественный рынок труда За счёт зп в отечественной валюте (рубли)
vac1 <- vac1 %>% filter(salary.currency == "RUR" | salary.currency=="NULL")

vac_gross <- vac1
# Удаляем лишние столбцы
vac_gross <- vac_gross %>% select(-salary.gross, -salary.currency, -salary.from, -salary.to)

# Объединяем в одну строку
vac_gross <- vac_gross %>% mutate(salary_gross = paste(salary_gross_from, salary_gross_to, sep = "-"))

# Добавляем значение в рублях
vac_gross <- vac_gross %>% mutate(salary_gross_rur = paste(salary_gross, "руб.", sep = " "))

upd_vacancies_19_21 <- vac_gross

rm(vac, vac_gross, vac1)
big.data <- upd_vacancies_19_21 %>% 
  select(-published_at, -experience.id) %>%
  select(id, name, specializations.id, key_skills, salary_gross_rur)

head(big.data)
# Быстрое преобразование в длинный формат по заранее разделенным навыкам
big.data.new <- cSplit(big.data, "key_skills", sep = "[[:punct:]]", "long", fixed = F)

# Приводим к одному формату
big.data.new <- big.data.new %>%
  mutate(
    skills_clear = str_to_lower(key_skills)
         ) %>% 
  select(-key_skills)

head(big.data.new)
# Выбираем нужные колонки (специализации включают в себя несколько направлений, к которым может относиться та или иная вакансия. Убираем, так как хотим найти чистую сферу, а не специализации)

big.data.new <- big.data.new %>%
  select(id, name, salary_gross_rur, skills_clear)

#write.csv(big.data.new, file = "C:/Личные файлы/HSE/1 Data Analysis in R/Stepik R Minor 2021/3. HWs/project final/big_data_clear.csv", col.names = T, row.names = F)

Методы машинного обучения

Период: Апрель

Общая информация

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

Первая задача – это классификация скиллов на Hard и Soft, что мы пытались сделать при помощи различных моделей классификации, и где по итогу остановились на использовании ChatGPT.

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

Более подробное решение каждой можно посмотреть далее по разделам.

Hard Soft Classification

Байесовская классификация:

# Load required libraries
library(tm)
library(e1071)
library(readxl)
library(Matrix)

# Load your dataset
data <- read_excel("Course theme/Skills.xlsx")

# Create a corpus of the skill names
corpus <- Corpus(VectorSource(data$skills))

# Clean and preprocess the text
corpus <- tm_map(corpus, content_transformer(tolower))
corpus <- tm_map(corpus, removePunctuation)
corpus <- tm_map(corpus, removeNumbers)
corpus <- tm_map(corpus, stripWhitespace)

# Create a document term matrix
dtm <- DocumentTermMatrix(corpus)

# Convert to a sparse matrix
dtm_sparse <- sparseMatrix(i = dtm$i, j = dtm$j, x = dtm$v, 
                           dimnames = dimnames(dtm))

# Create a factor variable for the skill classifications
classifications <- factor(data$labels)

# Train a Naive Bayes model
model <- naiveBayes(dtm_sparse %>% as.matrix(), classifications)

# Load a new dataset of skill names to classify
skill <- c("Python", "Java", "Leadership", "Communication", "SQL", "Teamwork")
label <- c("hard", "hard", "soft", "soft", "hard", "soft")
new_data <- data.frame(skills = skill, labels = label)

# Create a corpus of the skill names
new_corpus <- Corpus(VectorSource(new_data$skills))

# Clean and preprocess the text
new_corpus <- tm_map(new_corpus, content_transformer(tolower))
new_corpus <- tm_map(new_corpus, removePunctuation)
new_corpus <- tm_map(new_corpus, removeNumbers)
new_corpus <- tm_map(new_corpus, stripWhitespace)

# Create a document term matrix for the new data
new_dtm <- DocumentTermMatrix(new_corpus#, control = list(dictionary = Terms(dtm))
                              )
inspect(new_dtm)
# Convert to a sparse matrix
new_dtm_sparse <- sparseMatrix(i = new_dtm$i, j = new_dtm$j, x = new_dtm$v ,dimnames = dimnames(new_dtm)
                               )
dim(new_dtm)
# Predict the classifications of the new skill names
predictions <- predict(model, new_dtm_sparse %>% as.matrix())

Метод Опорных Векторов:

# Load packages
library(tidymodels)
library(textrecipes)
library(tidytext)
# Create example data
skills <- c("Python", "Java", "Leadership", "Communication", "SQL", "Teamwork")
labels <- c("hard", "hard", "soft", "soft", "hard", "soft")
data <- data.frame(skills = skills, labels = labels)

# Tokenize skills column separately
recipe(labels ~ skills, data = data) %>%
  step_tokenize(skills,options = list(lowercase = FALSE)) %>% 
  step_tfidf(skills)

# Define recipe to preprocess data
rec <- recipe(labels ~ skills, data = data) %>%
  step_tokenize(skills, options = list(lowercase = FALSE)) %>%
  step_tfidf(skills) %>%
  step_normalize(skills)

# Define model
svm_spec <- svm_rbf(cost = tune(), rbf_sigma = tune()) %>%
  set_engine("kernlab") %>%
  set_mode("classification")

# Define cross-validation workflow
svm_wf <- workflow() %>%
  add_recipe(rec) %>%
  add_model(svm_spec)

# Train model using cross-validation
set.seed(123)
svm_res <- svm_wf %>%
  tune_grid(resamples = vfold_cv(data, v = 5))

# Fit model to full dataset using best hyperparameters
best_svm <- svm_res %>%
  select_best("accuracy") %>%
  extract_workflow() %>%
  fit(data)

# Predict labels for new skills
new_skills <- c("Python", "Java", "Leadership", "Communication", "SQL", "Teamwork", "Data Analysis", "Project Management", "Writing")
new_data <- data.frame(skills = new_skills)
new_data <- bake(rec, new_data)
new_data$predicted_labels <- predict(best_svm, new_data)

##################

library(scipy.sparse)
# Preprocess data by converting skills to a sparse matrix
sparse_skills <- create_sparse_matrix(data$skills, verbose = FALSE)
preprocessed_data <- cbind(sparse_skills, data$labels)

# Define model
svm_spec <- svm_rbf(cost = tune(), rbf_sigma = tune()) %>%
  set_engine("kernlab") %>%
  set_mode("classification")

# Define cross-validation workflow
svm_wf <- workflow() %>%
  add_model(svm_spec)

# Train model using cross-validation
set.seed(123)
svm_res <- svm_wf %>%
  tune_grid(resamples = vfold_cv(preprocessed_data, v = 5))

# Fit model to full dataset using best hyperparameters
best_svm <- svm_res %>%
  select_best("accuracy") %>%
  extract_workflow() %>%
  fit(preprocessed_data)

# Predict labels for new skills
new_skills <- c("Python", "Java", "Leadership", "Communication", "SQL", "Teamwork", "Data Analysis", "Project Management", "Writing")
sparse_new_skills <- create_sparse_matrix(new_skills, vocabulary = vocabulary(sparse_skills))
new_data <- as.data.frame(sparse_new_skills)
new_data$predicted_labels <- predict(best_svm, new_data)

W2V модель:

library(tidyverse)
library(tidytext)
library(word2vec)
library(readxl)
library(SnowballC)
# Load data
skills_data <- read_excel("~/Course theme/Skills.xlsx")
skills_data$labels = factor(case_when(skills_data$labels=="soft" ~ 0,
                                                          TRUE ~ 1))
data_clean <- skills_data %>%
  mutate(skills = tolower(skills)) 


# Split data into training and test sets
set.seed(123)
train_index <- sample(nrow(data_clean), 0.8 * nrow(data_clean))
train_data <- data_clean[train_index, ]  %>%
  unnest_tokens(word, skills, drop=FALSE) %>%
  anti_join(stop_words, by = "word") %>%
  mutate(word = wordStem(word)) %>%
  filter(!is.na(word))
test_data <- data_clean[-train_index, ] %>%
  unnest_tokens(word, skills, drop=FALSE) %>%
  anti_join(stop_words, by = "word") %>%
  mutate(word = wordStem(word)) %>%
  filter(!is.na(word))

# Train word2vec model
data_clean <- skills_data %>%
  mutate(skills = tolower(skills)) %>%
  unnest_tokens(word, skills, drop=FALSE) %>%
  anti_join(stop_words, by = "word") %>%
  #mutate(word = wordStem(word)) %>%
  filter(!is.na(word))
labels = data_clean
#w2v_model <- word2vec(data_clean$word, type="skip-gram", dim = 50, iter = 10, min_count = 1)

#summary(w2v_model, type = "vocabulary")
# Generate embeddings for skills in training and test sets
train_embeddings <- predict(w2v_model, train_data$word, type = "embedding")
test_embeddings <- predict(w2v_model, test_data$word, type = "embedding")

# Add embeddings to data frames
train_data <- train_data %>%
  mutate(embeddings = train_embeddings)
test_data <- test_data %>%
  mutate(embeddings = test_embeddings)

# Sum of vectors
tr_d = train_data$embeddings %>% as.data.frame() 
tr_d$skills = train_data$skills
tr_d = tr_d %>% group_by(skills) %>% summarise_if(is.numeric, sum)  %>% left_join(train_data %>% select(skills, labels))
tr_d = tr_d %>% filter(!duplicated(tr_d))

te_d = test_data$embeddings %>% as.data.frame() #%>% mutate(stems = row.names(train_data$embeddings))
te_d$skills = test_data$skills
te_d = te_d %>% group_by(skills) %>% summarise_if(is.numeric, sum)  %>% left_join(train_data %>% select(skills, labels))
te_d = te_d %>% filter(!duplicated(te_d))

# Fit a logistic regression model using embeddings
model <- glm(labels ~ . - skills, family = binomial, data=tr_d)
train_pred = predict(model, newdata = tr_d, type = "response")
tr_d$pred = train_pred
train_accuracy <- mean((train_pred > 0.5) == if_else(train_data$labels==1, T, F))
# Make predictions on test set
test_predictions <- predict(model, newdata = te_d, type = "response")

# Evaluate model performance
test_accuracy <- mean((test_predictions > 0.5) == test_data$labels)


############### k means or 
labels = labels %>% mutate(embedding = list(get_embeddings(word)))
embed=list()
for (w in labels$word){
  p(w)
  embed %>% rbind(c(w) %>% rbind(get_embeddings(w)))
}

Нейронная сеть:

library(readxl)
library(tidymodels)

# Load your dataset
df <- read_excel("Course theme/Skills.xlsx") %>% as.data.frame()

library(caret)

set.seed(123) 
trainIndex <- initial_split(df, prop = 3/4)
training_set <- training(trainIndex)
test_set <- testing(trainIndex)

library(text2vec)

corpus <- training_set$skills
tokens <- word_tokenizer(corpus)
it <- itoken(tokens, progressbar = FALSE)
vocab <- create_vocabulary(it)
vectorizer <- vocab_vectorizer(vocab)
dtm <- create_dtm(it, vectorizer)
dtm %>% as.matrix()
library(neuralnet)

model <- neuralnet(labels ~ skills, data = training_set, hidden = 5)

predictions <- predict(model, newdata = training_set)

Однако все построенные нами модели выдавали точность не больше 45%, из-за чего мы решили обрабтиться к помощи языковой модели ChatGPT

Попытка создать функцию работы с ChatGPTчерез R:

# Working model in google sheets with chat gpt 
# "https://docs.google.com/spreadsheets/d/1BpQAjeTIrtN27gO4LoDn3ac56En5i3a6XxDrRs-rtYk/edit?usp=sharing"
# openai is hided (but exists working function in python)


####################################################################
library(httr)
library(jsonlite)
library(readxl)
Skills <- read_excel("Course theme/Skills.xlsx")
openai_api_key <- "скрыл для отчета"
skills = as.character(list(skills_data$skills))
prompt <- paste("Can you classify the following skills to the hard and soft one and make it in a form of table ",skills)

response <- POST("https://api.openai.com/v1/engines/davinci-codex/completions",
                 add_headers("Content-Type" = "application/json",
                             "Authorization" = paste("Bearer", openai_api_key)),
                 body = list(prompt = prompt,
                             max_tokens = 50,
                             n = 1,
                             stop = "\n",
                             temperature = 0.5))

result <- content(response, as = "parsed")$choices[[1]]$text
print(result)

############
api_key <- "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"
url <- "https://api.openai.com/v1/engines/davinci-codex/completions"
headers <- c(
  "Content-Type" = "application/json",
  "Authorization" = paste("Bearer", api_key)
)

# Define the prompt and other parameters
prompt <- "This is an example prompt"
params <- list(
  prompt = prompt,
  max_tokens = 10
)

# Make the API request
response <- POST(url, headers = headers, body = params, encode = "json")

# Get the response content
content(response, "text")

По каким-то причинам ни одна из реализаций чата в аре через reticulate или напрямую R не смогла связаться с чатом, из-за чего мы воплатили его на Python, откуда и взяли итоговую классификацию:

import pandas as pd
data = pd.read_excel("Desktop/Skills.xlsx")
url = "https://drive.google.com/file/d/11RJZVl7TWJCNbRjv_a05oCDOMVoryMz4/view?usp=share_link"
path = 'https://drive.google.com/uc?export=download&id='+url.split('/')[-2]
df = pd.read_csv(path)

import openai

# Set up your API key
openai.api_key = "скрыл для отчета"

def ChatGPT(prompt, model = "text-davinci-002", max_tokens = 1000):
    # Generate text using the GPT model
    response = openai.Completion.create(
        engine=model,
        prompt=prompt,
        max_tokens=max_tokens
    )
    return response.choices[0].text
  
import re
rank=[]
for skill in list(df.skills_stemmed):
    word = ChatGPT("Can you classify the following skill as hard or soft one " + skill + ". I expect from you only one word: 'hard' or 'soft'. Strictly one word and be very precise and accurate when allocating type of skill, no errors are possible.").lower()
    #try:
    word = re.findall("hard|soft", word)
    #except Error:
        #word = str_extract(word, "soft")
    rank.append(word[0])

metric=0
for i in range(len(rank)):
    print(rank[i], list(data.Skills)[i] ,list(data.Category)[i])
    if rank[i]==list(data.Category)[i]:
        metric+=1
        
metric*100/len(rank)

В результате качество предсказания составло 67%, в следствии чего мы решили остановиться на чат боте и далее вручную перепроверить предсказания и исправить те моменты, где бот оказался не прав. Результат можно увидеть в Google Tables.

LDA and Cosine String Similarity

В качестве метода машинного обучения мы решили взять так же тематическое моделирование с алгоритмом LDA (TM LDA). Такое решение мы обосновываем тем, что нам нужно было на основе описаний вакансий сгруппировать вакансии и, соответственно, навыки. Оптимальным решением мы увидели использование LDA, так как оно позволяет на основе вероятностей соотнести разные описания и навыки вакансий по определенным темам в зависимости от того количества, которое указывается при обучении модели. За документ мы используем вакансию и её описание, само обучение производили по словам, которые являются навыками вакансии. Все названия и навыки были предварительно очищенны и застеменны.

Единственный минус, с которым мы вскоре столкнулись при использовании LDA, это оценка модели. Так как обучение языковый моделей, включая модели тематического моделирования, требуют большое количество ресурсов, то обучение нескольких таких моделей с разнымии параметрами занимает большое количество времени и ресурсов, в случае ТМ это количество топиков, и значения гиперпараметров alpha и beta. И, к сожалению, у нас не было достаточно ресурсов для обучения и сравнения нескольких моделей. Таким образом, мы остановились на количестве тем в 30 штук, с тем условием, что мы работаем несовсем с текстами в прямом смысле – описания вакансий составлены из обычных слов, и мы решили, что если какие-то темы будут повторяться, то их можно будет объединить в дальнейшем. И, как итог, оценка производилась нами субъективно, т.е. насколько темы и набор навыков в них попадали под привычные и уже устаявшиеся сферы деятельности на рынке труда, и затем уже на основе этого мы давали названия получившимся темам.

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

big.data.final <- big.data.new %>%
  mutate(
    name = str_to_lower(name),
    name = str_replace_all(name, "\\W+", " "),
    name = str_replace_all(name, "[0-9]+", " ")) 

head(big.data.final)

## стемминг скиллов
big.data.counts <- big.data.final %>%
  mutate(skills_stemmed = wordStem(skills_clear, language = c("en", "ru")),
         ) %>%
  count(skills_stemmed, sort = T) %>%
  filter(n >= 1000) ## 

big.data.filtered <- big.data.final %>%
  mutate(skills_stemmed = wordStem(skills_clear, language = c("en", "ru"))) %>%
  filter(skills_stemmed %in% big.data.counts$skills_stemmed & name != " ")

head(big.data.filtered)

## убираем стоп слова из названий вакансий
rm_words <- function(string, words) {
    stopifnot(is.character(string), is.character(words))
    spltted <- strsplit(string, " ", fixed = TRUE) # fixed = TRUE for speedup
    vapply(spltted, function(x) paste(x[!tolower(x) %in% words], collapse = " "), character(1))
}

big.data.filtered <- big.data.filtered %>%
  mutate(spec = rm_words(name, c(tm::stopwords("en"), stopwords("ru"))))

## стемим названия вакансий

big.data.filtered$spec_final <- sapply(stri_split_fixed(big.data.filtered$spec, ","), function(x) {
        x <- lapply(stri_split_fixed(stri_trim_both(x), " "), function(y) {
            paste(SnowballC::wordStem(y, language = c("en", "ru")), collapse = " ")
        })
        paste(x, collapse = ", ")
    })

big.data.filtered$spec_final <- word(big.data.filtered$spec_final, 1, 2)

head(big.data.filtered)

Затем очищенные данные мы обработали для уже непосредственного обучения языковой модели LDA.

## делаем укороченную версию данных
data.short <- big.data.filtered %>%
  group_by(spec_final) %>%
  summarize(skills = paste(skills_stemmed, collapse = ', ')) %>%
  mutate(doc_id = row_number())

head(data.short)

## датасет для lda
word.counts <- big.data.filtered %>%
  count(spec_final, skills_stemmed, sort = T) %>%
  ungroup()

Теперь мы обучаем нашу модель LDA.

m.dtm <- word.counts %>%
  cast_dtm(spec_final, skills_stemmed, n)

m.lda <- LDA(m.dtm, k = 30, control = list(seed = 100))
lda_topics <- tidy(m.lda, matrix = "beta")

После обучения мы можем посмотреть на самые важные с точки зрения метрик навыки в каждой теме или сфере деятельности. Для этого нам нужно сначала получить вероятности того, что слово относится к той или иной теме (per-topic-per-word probabilities), обозначаемые \(\beta\) (beta). После этого мы дадим, как говорили ранее, названия получившимся темам ака сферам деятельности.

lda_top_terms <- lda_topics %>%
  group_by(topic) %>%
  top_n(10, beta) %>%
  ungroup() %>%
  arrange(topic, -beta)

Нарисуем топики

## рисуем топики
read.csv("C:/Личные файлы/HSE/1 Data Analysis in R/Stepik R Minor 2021/3. HWs/project final/lda_top_terms.csv") %>%
  mutate(term = reorder(term, beta)) %>%
  ggplot(aes(term, beta, fill = factor(topic))) +
  geom_col(show.legend = FALSE) +
  facet_wrap(~ topic, scales = "free") +
  coord_flip()

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

В следующем чанке мы обрабатывали датасеты для уже использования их в приложении. Мы дали названия топикам в зависимости от того, к какой сфере деятельности они скорее всего подходят, а так сделали два разных датасета с количеством навыков в 10 и 15 штук, и также добавили пропорции хард и софт скиллов, которые мы сделали при помощи ChatGPT в Google Tables. Для этого мы использовали датасет, который использовался для LDA с посчитанным количеством встречания навыков.

lda_top10_terms <- lda_top_terms %>%
  mutate(sphere = case_when(
    topic == 1 ~ "Event management",
    topic == 2 ~ "Контроль качества",
    topic == 3 ~ "Складные работы",
    topic == 4 ~ "Project Management",
    topic == 5 ~ "IT Programming",
    topic == 6 ~ "Купле-продажи",
    topic == 7 ~ "Дизайн",
    topic == 8 ~ "Делопроизводство",
    topic == 9 ~ "Делопроизводство",
    topic == 10 ~ "Техобслуживание ПК",
    topic == 11 ~ "Работа за кассой",
    topic == 12 ~ "B2C Продажи",
    topic == 13 ~ "Медицина и Фармацевтика",
    topic == 14 ~ "Строительное моделлирование",
    topic == 15 ~ "IT Programming",
    topic == 16 ~ "HR Management",
    topic == 17 ~ "Sales",
    topic == 18 ~ "Маркетинг",
    topic == 19 ~ "SMM и Копирайтинг",
    topic == 20 ~ "Data Science",
    topic == 21 ~ "Call-центр",
    topic == 22 ~ "Office management",
    topic == 23 ~ "B2B Продажи",
    topic == 24 ~ "Производственный делооборот",
    topic == 25 ~ "Перевозки",
    topic == 26 ~ "Охрана труда",
    topic == 27 ~ "Бухгалтерский учёт",
    topic == 28 ~ "Sales",
    topic == 29 ~ "Производственная инженерия",
    topic == 30 ~ "Business management"
  )) %>%
  mutate(sphere = as.factor(sphere))

hard_soft <- read.csv("C:/Личные файлы/HSE/1 Data Analysis in R/Stepik R Minor 2021/3. HWs/project final/skills_counted_hard_soft.csv", sep = ";") %>% 
  select(skills_stemmed, Final.class) %>% 
  mutate(
    Final.class = str_to_lower(Final.class),
    Final.class = str_remove_all(Final.class, "\\."))

spheres_top10 <- lda_top10_terms %>%
  filter(term != "") %>%
  inner_join(hard_soft, by = c("term" = "skills_stemmed")) 

lda_top15_terms <- lda_topics %>%
  group_by(topic) %>%
  top_n(15, beta) %>%
  ungroup() %>%
  arrange(topic, -beta)

lda_top15_terms <- lda_top15_terms %>%
  filter(term != "") %>%
  mutate(sphere = case_when(
    topic == 1 ~ "Event management",
    topic == 2 ~ "Контроль качества",
    topic == 3 ~ "Складные работы",
    topic == 4 ~ "Project Management",
    topic == 5 ~ "IT Programming",
    topic == 6 ~ "Купле-продажи",
    topic == 7 ~ "Дизайн",
    topic == 8 ~ "Делопроизводство",
    topic == 9 ~ "Делопроизводство",
    topic == 10 ~ "Техобслуживание ПК",
    topic == 11 ~ "Работа за кассой",
    topic == 12 ~ "B2C Продажи",
    topic == 13 ~ "Медицина и Фармацевтика",
    topic == 14 ~ "Строительное моделлирование",
    topic == 15 ~ "IT Programming",
    topic == 16 ~ "HR Management",
    topic == 17 ~ "Sales",
    topic == 18 ~ "Маркетинг",
    topic == 19 ~ "SMM и Копирайтинг",
    topic == 20 ~ "Data Science",
    topic == 21 ~ "Call-центр",
    topic == 22 ~ "Office management",
    topic == 23 ~ "B2B Продажи",
    topic == 24 ~ "Производственный делооборот",
    topic == 25 ~ "Перевозки",
    topic == 26 ~ "Охрана труда",
    topic == 27 ~ "Бухгалтерский учёт",
    topic == 28 ~ "Sales",
    topic == 29 ~ "Производственная инженерия",
    topic == 30 ~ "Business management"
  )) %>%
  mutate(sphere = as.factor(sphere))

spheres <- lda_top15_terms %>%
  filter(term != "") %>%
  inner_join(hard_soft, by = c("term" = "skills_stemmed")) %>%
  select(-beta)

### добавляем пропорции хард и софт скиллов

spheres.hard_soft <- spheres %>%
  group_by(sphere) %>%
  count(Final.class) %>%
  pivot_wider(names_from = Final.class, values_from = n)

spheres.hard_soft[is.na(spheres.hard_soft)] <- 1

spheres.hard_soft <- spheres.hard_soft %>%
  mutate(Hard_Proportion = round((hard) / (hard+soft), 2),
         Soft_Proportion = round((soft) / (hard+soft), 2)) %>%
  select(Hard_Proportion, Soft_Proportion)

spheres_short <- spheres %>%
  group_by(sphere) %>%
  summarize(skills = paste(term, collapse = ', ')) %>%
  left_join(spheres.hard_soft, by = c("sphere"))

Посмотрим, как темы распределяются по описаниям вакансий. Получим вероятности того, что документ или же вакансия относится к той или иной теме или же сфере деятельности (per-document-per-topic probabilities), обозначаемые \(\gamma\) (gamma)

lda_documents <- tidy(m.lda, matrix = "gamma")

lda_vacancies <- lda_documents %>%
  group_by(document) %>%
  slice(which.max(gamma)) %>%
  arrange(topic, -gamma) %>%
  filter(gamma > 0.95)

lda_vacancies <- lda_vacancies %>%
  mutate(sphere = case_when(
    topic == 1 ~ "Event management",
    topic == 2 ~ "Контроль качества",
    topic == 3 ~ "Складные работы",
    topic == 4 ~ "Project Management",
    topic == 5 ~ "IT Programming",
    topic == 6 ~ "Купле-продажи",
    topic == 7 ~ "Дизайн",
    topic == 8 ~ "Делопроизводство",
    topic == 9 ~ "Делопроизводство",
    topic == 10 ~ "Техобслуживание ПК",
    topic == 11 ~ "Работа за кассой",
    topic == 12 ~ "B2C Продажи",
    topic == 13 ~ "Медицина и Фармацевтика",
    topic == 14 ~ "Строительное моделлирование",
    topic == 15 ~ "IT Programming",
    topic == 16 ~ "HR Management",
    topic == 17 ~ "Sales",
    topic == 18 ~ "Маркетинг",
    topic == 19 ~ "SMM и Копирайтинг",
    topic == 20 ~ "Data Science",
    topic == 21 ~ "Call-центр",
    topic == 22 ~ "Office management",
    topic == 23 ~ "B2B Продажи",
    topic == 24 ~ "Производственный делооборот",
    topic == 25 ~ "Перевозки",
    topic == 26 ~ "Охрана труда",
    topic == 27 ~ "Бухгалтерский учёт",
    topic == 28 ~ "Sales",
    topic == 29 ~ "Производственная инженерия",
    topic == 30 ~ "Business management"
  )) %>%
  mutate(sphere = as.factor(sphere))

lda_vacancies <- lda_vacancies %>%
  group_by(sphere) %>%
  summarize(vacancies = paste(document, collapse = ', '))

Так для уже непосредственно рекомендаций в самом приложении мы решили использовать косинусное расстояние между навыками, которые выбирает пользователь, и набором навыков, которые являются наиболее существенными для сферы деятельности. Проверка нахождения схожести входных данных и наборов навыков мы производили через функцию stringsim(). По итогу мы получили работу по методу doc2vec – наборы навыков представляются как вектора, между которыми находится косинусное расстояние.

test <- read.csv("C:/Личные файлы/HSE/1 Data Analysis in R/Stepik R Minor 2021/3. HWs/project final/spheres_short.csv")

test$sim <- stringsim(test$skills,
          c("грамотная речь", "грамотность", "умение планировать", "детская психология"),
          method = "cosine")

Архитектура приложения

Период: Апрель-Май

Приложение имеет довольно простую архитектуру. Все данные, которые лежат в её основе, уже предварительно обработаны и используются непосредственно в приложении. Конкретнее, это датасеты со сферами деятельности и навыками в них в количестве 10-15 штук, и датасет с зарплатами вакансий для понимания перспективы размера ЗП в зависимости от стажа.

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

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

Интерфейс

Период: Апрель-Май

Cам интерфейс и работа с ним довольно просты.

Примерный дизайн мы решили сделать такой: элементы, с которыми может взаимодействовать пользователь, поставить по середине сайта, и все результаты работы так же сделать по середине. Макеты и черновики можно посмотреть тут и тут в Figma.

Теперь давайте посмотрим, как работает непосредственно наше приложение визуально.

Входные данные: К примеру, человек пользователь выбрал такие навыки в выпадающем списке навыков, как MS Excel, Python и SQL. И затем выбрал на ползунке 3 года опыта работы.

Выходные данные: На выходе пользователь получает две таблицы. Верхняя таблица показывает названия топ-3 сфер деятельности, которые наиболее схожи с тем набором, который составил пользователь, а также наиболее важные для этой сферы навыки и зарплата с учетом 3 лет работы в данной сфере. Нижняя таблица показывает для полученных сфер деятельности соотношение софт и хард скиллов.

Скрин интерфейса:

Код интерфейса:

# JobFindeR App based on Shiny R
## Authors: Ilyschenko, Kazakov, Bosonogov, Kalyuzhnaya, Kapendyukhina

library(shiny)
library(shinyWidgets)
library(shinybusy)
library(shinythemes)

library(tidyverse)
library(stringdist)

#spheres.long <- read.csv("spheres.csv")
spheres.short <- read.csv("spheres_short.csv")
filtered_vacancies <- read.csv("vse_vakansii_filtered.csv")

spheres.top10 <- read.csv("spheres_top10.csv")

spheres.skills <- spheres.top10 %>% select(term) %>% filter(term != "") %>% unique() %>% arrange(term) 


## UI code 

ui <- navbarPage(
  
  "JobFindeR App",
  
  theme = shinythemes::shinytheme("sandstone"),
  #theme = bslib::bs_theme(version = 4, bootswatch = "minty"),
  
  tabPanel("О приложении",
           
           fluidPage(
             
             fluidRow(
               column(2),
               
               column(8,
                      p("Добро пожаловать в приложение по поиску направления деятельности JobFindeR!", align = "center"),
                      br("Наше приложение для анализа карьерных перспектив поможет вам:"),
                      p("- найти подходящую сферу на основе ваших навыков"),
                      p("- расширить поиск специальностей, связанных с одними и теми же навыками, но в разных областях"),
                      p("- получить информацию о требуемых навыках в различных профессиональных направлениях"),
                      p("- оценить потенциальную заработную плату в зависимости от уровня вашей квалификации"),
                      p("- оценить порог входа в сферу на основе процентного отношения хард скиллов к общему набору навыков, необходимого для сферы"),
                      br("Ваши,"),
                      p("Ильященко Константин (ОП Международный бизнес и менеджмент)"),
                      p("Капендюхина Алёна (ОП Международный бизнес и менеджмент)"),
                      p("Босоногов Семён (ОП Социология и социальная информатика)"),
                      p("Калюжная Олеся (ОП Социология и социальная информатика)"),
                      p("Казаков Вадим (ОП Экономика)")
                      ),
               
               column(2)
               
             )
             
           )),
  
  tabPanel("Функционал",
           
           fluidPage(
             fluidRow(
               
               column(2),
               
               column(8,
                      selectizeInput(
                        inputId = 'input_skills',
                        label = 'Выберите навыки',
                        choices = c("", spheres.skills$term),
                        selected = NULL,
                        multiple = TRUE, # allow for multiple inputs
                        options = list(create = FALSE) # if TRUE, allows newly created inputs
                      ),
                 sliderInput("radio", "Experience",
                             min = 0, max = 6,
                             value = 0),
                 align = "center"
               ),
               
               column(2)),
             
             fluidRow(
               column(2),
               column(8,
                      
                      tags$style(type="text/css",
                                 ".shiny-output-error { visibility: hidden; }",
                                 ".shiny-output-error:before { visibility: hidden; }"
                      ),        
                      
                      textOutput("distText"),
                      tableOutput("distTable"),
                      tableOutput("distTable2"), align = "center"),
               
               column(2)))),
  
  #theme = "bootstrap.css",
  
)

# Define server logic required to draw a histogram
server <- function(input, output) {
  
    server.long <- read.csv("spheres.csv")
    server.short <- read.csv("spheres_short.csv")
    vse_vakansii_filtered <- read.csv("vse_vakansii_filtered.csv")
    
    output$distText <- renderText({
      
      server.short$sim <- stringsim(
        server.short$skills,
        input$input_skills,
        method = "cosine")
  
      server.final <- server.short %>%
        arrange(-sim) %>%
        top_n(3, sim)
      
      if (!is_empty(server.final$sim)) {
        paste0("Сферы, в которых вы можете работать: ", paste(server.final$sphere, collapse=", "))
      }
      
    })
    
    output$distTable <- renderTable({
      
      server.short$sim <- stringsim(
        server.short$skills,
        input$input_skills,
        method = "cosine")
      
      server.finall <- server.short %>%
        arrange(-sim) %>%
        top_n(3, sim)
      
      expected_wage <- c(
        quantile(as.numeric(filter(vse_vakansii_filtered, sphere == server.finall$sphere[1])$salary_from), 0.3+0.1*as.numeric(input$radio)),
        quantile(as.numeric(filter(vse_vakansii_filtered, sphere == server.finall$sphere[2])$salary_from), 0.3+0.1*as.numeric(input$radio)),
        quantile(as.numeric(filter(vse_vakansii_filtered, sphere == server.finall$sphere[3])$salary_from), 0.3+0.1*as.numeric(input$radio))
      )
      
      data.frame(Sphere = server.finall$sphere, Skills = server.finall$skills, 
                 Salary = ifelse(!is.na(expected_wage), expected_wage, "Недостаточно данных"))
      
    })
    
    output$distTable2 <- renderTable({
      
      server.short$sim <- stringsim(
        server.short$skills,
        input$input_skills,
        method = "cosine")
      
      server.final <- server.short %>%
        rename(Sphere = sphere, Skills = skills) %>%
        arrange(-sim) %>%
        top_n(3, sim)
      
      server.final %>% select(-sim, -Skills) %>% rename(`Hard Skills` = Hard_Proportion, `Soft Skills` = Soft_Proportion)
      
    })
    
    
}

# Run the application 

shinyApp(ui = ui, server = server)

Оценивание / Выводы

Период: Май

После того, как мы построили финальную версию рабочего продукта, перед тем, как объединять её в единое приложение, мы остановились на тех моментах, которые нам нужны и являются лишними во всём коде нашего приложения:

  • Мы сохранили только нужные и предобработанные данные
  • Выбрали модели классификации навыков на основе их точности, сделали отдельный датасет с классификацией заранее и также выгрузили его
  • Выгрузили ресурсоемкую LDA модель в уже готовый набор данных также для экономии ресурсов.

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

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

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

Пользователь 1:

  • Антон Никифоров,
  • Менеджмент,
  • Имеет навыки в IT-сфере:базовый Python, SQL
  • Результат: Осознал, что нужно еще учиться и учиться, так как очень не хватает глубокого понимания языков,в которых работает, в основном Python с его многообразием пакетов. Задумался об изучении R.

Пользователь 2:

  • Софья Батурина,
  • Юриспруденция,
  • Работает юристом, хотела оценить зарплату через год и варианты перехода в другую сферу
  • Результат: Узнала, что со своими навыками можно пойти в аудит и получать там бо́льшую заработную плату, чем получает сейчас, а также поняла, что хочет заняться арбитражным правом и его процессами.

Пользователь 3:

  • Алика Федорова
  • Менеджмент 2 курс
  • Указала навыки SMM, Бизнес, Англ язык
  • Результат: Business management, Event management, B2C Продажи
  • Комментарий: “Результаты соответствуют 2/3 навыков, сферы опираются на навык”бизнес” и “англ язык” и их пересечение, не соответствуют SMM.”

Пользователь 4:

  • Денис Машков,
  • Логистика 3 курс,
  • Указал навыки Работа с людьми, творческое мышление, умение работать в команде
  • Результат: Перевозки, тех обслуживание ПК, SMM и копирайтинг
  • Комментарий: “Не совсем понял, как связаны творческое мышление и работа с людьми с копирайтингом и тех обслуживанием ПК. На мой взгляд, должно было выпасть что-то из сферы развлечений, условно там ведущий мероприятий. Там бы как раз и творческое мышление и работа с людьми, плюс команда присутствует, так как обычно кто-то на музыке, кто-то на видеосъемке и тд. А в выпавших запросах скорее востребованы такие навыки, как системность, усидчивость, внимание к мелочам и что-то в этом духе.”

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

Ссылка на приложение

Приложение и работу с ним можно найти по данной ссылке: https://sbosonogov.shinyapps.io/JobFindeR/

Ответы на вопросы peer review

Период: Июнь

Ответы на вопросы и комментарии по проекту

Вопрос: Какие ml-методы были использованы?

Ответ: Основные методы: ChatGPT для классификации скиллов на хард софт LDA для разделения навыков и вакансий по сферам, Cosine Similarity непосредственно для рекомендаций в приложении. Для скиллов также были применены логистическая регрессия, байесовский классификатор, реализация эмбеддингов и нейросети многослойные.

Вопрос: Было бы очень удобно при наличии такой возможности добавить поиск по навыкам, чтобы не искать вручную и ставить галочки.

Ответ: Это очень хорошая рекомендация, мы пока что находимся в раздумьях по поводу этой функции. В защиту нашего текущего поиска - все навыки расположены в алфавитном порядке, и пользователь сможет найти необходимый навык в списке, поскольку некоторые навыки могут называться по разному, а с помощью алфавитного списка по первой букве будет проще найти необходимый навык.

Вопрос: Может стоит не ограничивать рекомендацию количеством сфер (3). Может, ограничить именно по точности совпадения навыков и сферы, типа выпадают сферы, показатель точности которых выше заданного значения. Но не уверена, что такое реализуемо

Ответ: Мы ограничили рекомендацию сфер в количестве топ-3 по схожести, можно добавить и больше, но мы не хотели перегружать интерфейс большим выводом. В принципе, можно добавить и больше сфер.

Вопрос: Кажется, в самом приложении нет описания того, как с ним работать. Сейчас там написано “Описание приложения”.

Ответ: Добавили подробное описание приложения.

Вопрос: Возможно, пользователю предлагается слишком мало факторов, определяющих обязанности и зарплату (только стаж и навыки), но это, конечно, придирки, проект крутой.

Ответ: Начальный функционал приложения предполагает только такие аргументы для ввода, в дальнейшем можно добавить больше параметров.

Вопрос: Единственное замечание, которое у меня возникло - это метод оценивания работы приложения. Для меня не очень показателен тот факт, что только 2 знакомых ребятам человека попробовали попользоваться данной программой, ведь они могут быть немного предвзяты в этом плане. Эти примеры можно использовать в качестве оценивания, но я бы добавила еще какие-то более независимые способы.

Ответ: Добавили методы оценивания приложения: запустили приложение в люди и попросили еще нескольких людей оценить на адекватность, в секции оценивание указали небольшую выжимку комментариев.

Вопрос: Как дорабатывали классификацию навыков на hard и soft?

Ответ: Мы использовали модель ChatGPT, которая помогла нам разделить 841 навык на hard и soft с помощью ответов модели на вопрос: “Can you classify the following skill as hard or soft one. I expect from you only one word: ‘hard’ or ‘soft’. Strictly one word and be very precise and accurate when allocating type of skill, no errors are possible.” Мы повторили этот алгоритм три раза, и 4 колонка в таблице навыков определяет самое повторяющееся слово из предыдущих колонок, которое точно описывает класс навыка. Мы провели ручную проверку после модели, поправили несколько навыков, которые модель неверно определила.

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

Ответ: Мы думали об этом, но в рамках проекта пока что не успели подумать о реализации этой идеи. В базе данных, которую мы собирали с хх.ру была колонка с full-time и part-time графиком, но в рамках того, что мы рекомендуем сферы, а не определенные вакансии, пока что не увидели смысла добавлять это в приложение.

Вопрос: Интересно, можно ли указать город/ регион,в котором пользователь планирует работать, ведь уровень зп отличается по регионам.

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

Вопрос: Может можно попробовать заменить появляющиеся NA в salary на какие-то слова?

Ответ: Мы обнаружили NA только в одной сфере из 33, тк там скорее всего мало вакансий и модель не смогла вывести число. Как итог, мы заменили NA на надпись Not Available в связи с отсутствием информации по зарплате.

Вопрос: Возникают ли ошибки в приложении? если да, какое выдается сообщение? больше 10 навыков просто не выбирается? Здорово, если на вкладке о приложении (или добавить вкладку “о данных”) представлена информация с “расшифровкой” навыков – допустим, что такое навык android? умение пользоваться техникой на этой базе? А Atlassian jira?…

Ответ: Ошибок не возникает, больше 10 навыков просто не выбирает. Расшифровка навыков - хорошая рекомендация, но у нас нет готового описания навыков, а вручную более 800 навыков описывать будет довольно сложно, и для этого нужно будет придумать интерфейс, чтобы по поиску выдавалось описание определенного навыка, а не целым списком, чтобы не перегружать сайт, в рамках проекта не успеваем добавить эту функцию. Зато человек, который будет выбирать навыки, поймет, что он значит, так как на английском языке в основном в навыках указаны названия программ.

Вопрос: Во вкладку “о приложении” можно написать какое-нибудь введение, расписать зачем и для кого это приложение придумано, какие есть у него ограничения.

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

Вопрос: Так как было сделано разделение навыков на hard и soft skills, можно было это применить к выбору навыков из выпадающего списка, так как на самом деле не очень удобно скролить такое большое количество скиллов, а какая-то фильтрация может в этом помочь, чтобы облегчить взаимодействие пользователя с приложением. Если моя гипотеза про взаимосвязь LDA и doc2vec подходов верна, то я бы порекомендовала попробовать изменить LDA на STM (достаточно интересный подход, который позволяет делать много всего интересного и, как по мне, более качественного). Также, можно было бы привязать позицию в компании, так как она очень сильно влияет на размер заработной платы (джун, сеньер и тд).

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

Вопрос: Я протестировала приложение. В нем можно выбирать сразу набор скиллов, которыми обладает кандидат, однако при добавлении нового навыка рекомендация приложения не расширяется, а полностью меня. Таким образом, я думаю, что кандидат может упустить сферу деятельности, которая ему подходит, только из-за того что он слегка расширил обозначенный круг навыков (например, MS SQL: IT Programming, Data Science, Дизайн. MS SQL и Английский язык: Работа за кассой, Event management, SMM и Копирайтинг)

Ответ: Да, мы согласны с этой особенностью приложения, скорее всего, мы можем упомянуть в описании, что для большей точности стоит “играться” с навыками, поскольку алгоритм не идеален на 100%, но может советовать, если указывать навыки более точечно.

Вопрос: Не все характеристики подразумевают опыт, как именно работал параметр experience?

Ответ: Параметр experience отвечает за опыт работы и работает для предсказания зарплаты, на сферу он никак не реагирует.

Ответы с примерами работы приложения

Вопрос: Я таксист, и автомобильные перевозки - мой единственный навык. Но я устала работать таксистом и хочу сменить работу. Скорее всего, на основе одного навыка сферы будут максимально рандомные, и к тому же среди них что-то связанное с перевозкой тоже будет, что может быть не совсем актуально.

Ответ: Ответ приложения: сферы, в которых вы можете работать - Перевозки, Call-центр, Sales. На основании одного навыка приложение первым делом рекомендует сферу перевозок, так что уставшему таксисту возможно будет интересно попробовать себя в этой сфере, где все навыки сконцентрированы вокруг автомобиля - водительское удостоверение категории b, знание устройства автомобиля, водительское удостоверение категории bc, вождение автомобилей представительского класса, автомобильные перевозки, пользователь пк, автомобильные грузоперевозки, мобильность, умение работать в команде, водительское удостоверение категории bce, безаварийное вождение, ответственность, работа в команде, первичные документы, техническое обслуживание. На основании этих навыков пользователь скорее всего поймет, что он умеет не только водить машину, и сможет пойти работать водителем дальнего следования, грузоперевозками, либо же перевозить товары. Две другие сферы, которые рекомендует приложение, подобраны с учетом наименьшей доли hard skills, чтобы пользователю не нужно было обучаться твердым навыкам.

Вопрос: Допустим, выбираются следующие навыки: sql, ms excel, python, английский язык. Ожидаю получить в качестве одной из сфер Data Science.

Ответ: Ответ приложения: Сферы, в которых вы можете работать: Data Science, IT Programming, Контроль качества. Приложение рекомендует две из трех сферы, посвященные полностью IT индустрии, в которой доля hard навыков примерно 95%, а навыки - всевозможные программы, которые необходимы для работы.

Вопрос: Что если я укажу в навыках владение графическими редакторами, видеомонтаж, рисование и опыт работы 2 года. Я ожидаю, что мне выдаст рекомендуемыми вакансиями графического дизайнера, motion-дизайнера с ожидаемой з/п от 100к.

Ответ: Приложение рекомендует сферу Дизайн, в которой вы сможете получить 70 тысяч с опытом два года. Чтобы получить зарплату в размере от 100 тысяч, требуется 4 года опыта работы.

Вопрос: Английский язык, Docker, Git, Аналитическое мышление, Python. IT Programming, Data Science, Devops.

Ответ: При тестировании приложения, мы поняли, что модель, которая считает схожести навыков по косинусному расстоянию, “перетягивает” некоторые навыки, допустим, если ввести три навыка: Docker, Git, Python, то модель выдает сферы Data Science, IT Programming, Маркетинг, а при добавлении навыков Английский язык и Аналитическое мышление, сферы кардинально меняются: Медицина и Фармацевтика, Office management, Call-центр. То есть добавление этих навыков делает рекомендации менее точными, и скорее всего, добавление меньшего количества наиболее ключевых навыков позволяет нашей модели рекомендовать наиболее эффективно.

Вопрос: Стаж - 3 года, навыки - Excel, SQL, бух. учет, финансы. Зп в районе 60к рублей.

Ответ: Сферы, в которых вы можете работать: Data Science - зарплата 100 000 рублей, Дизайн - зарплата 90 000 рублей, IT Programming - зарплата 40 000 рублей.

Вопрос: Поскольку приложение доступно для использования и я могу посмотреть различные варианты при выборе разных параметров, мне было бы интересно узнать, чтобы было бы, если бы можно было вводить личную информацию о себе (как я указала ранее). Например, я указываю свои софт скиллы, год окончание вуза, график, в котором мне комфортно будет работать. Еще можно добавить желаемые города, что тоже важно при выборе места трудоустройства. Я хочу видеть также, как было сказано раннее, немного больше вариантов, чем три, в которых будет отображаться не только зарплата, но и место работы (город), а еще требуемые софт скиллс, которые также важны при выборе места работы.

Ответ: Пока что наше приложение не сможет посоветовать зарплату по городу, и наверное странно, если приложение будет рекомендовать город, в котором наибольшая зарплата, ведь люди в основном ищут варианты работы в определенном городе, а не варианты городов, в которых платят больше. Требуемые soft и hard-скиллы и их пропорцию относительно всех навыков приложение уже показывает.

Вопрос: Что будет, если ввести навыки “sql” “excel” “аналитика продаж”? Ожидаю, что выдаст профессию бизнес-аналитик.

Ответ: Приложение предлагает пользователю сферу Business management, в которой требуются следующие навыки: управление проектами, ms powerpoint, бюджетирование, бизнес, ms excel, аналитическое мышление, управленческая отчетность, финансовый анализ, английский язык, анализ, финансовая отчетность, финансовый контроль, разработка технических заданий, планирование, аналитические исследования. В том числе, профессия бизнес-аналитик входит эту сферу, так что ожидания совпадают с результатами.

Вопрос: Интересно, если респондент не имеет никаких особенных навыков, владение ПК максимум, то что мне могут предложить? Думаю либо базовые вакансии типа кассира либо совершенно разнообразные

Ответ: Да, предлагаются базовые сферы, в которых есть навык “пользователь пк” и другие. Сферы, в которых вы можете работать: Охрана труда, Купле-продажи, Работа за кассой.

Вопрос: Допустим пользователь знает 1с, excel и ещё какие-то базовые навыки, на сколько поднимется уровень возможной зарплаты, если к навыкам добавить python?

Ответ: Наше приложение учитывает навыки только для определения сфер, в которой пользователь может работать, заработная плата же зависит от опыта работы, а не количества навыков. Но для рассмотрения примера, если ввести навыки 1с и ms excel, пользователю предлагается сфера IT Programming, в которой с нулевым опытом работы можно получать 33 380 тысяч рублей, а при добавлении навыка python приложение советует еще дополнительно сферу Data Science, в которой з/п на начальном этапе составляет 60 000 рублей.

Вопрос: Интересно, что будет, если выбрать, допустим, 5 сильно несхожих, не из одной направленности (или вовсе диаметрально противоположных) навыков (но для этого я бы, конечно, хотела ознакомиться со списком). Допустим, навыки: выкладка товаров, деловая переписка, монтажные работы, банк, HTML

Ответ: Сферы, в которых вы можете работать: Работа за кассой, Бухгалтерский учёт, Охрана труда. Приложение выдает самые базовые сферы, по каким навыкам он нашел наибольшую схожесть, поэтому в данном случае оно предложило несколько разных сфер, которые опираются на какие -то навыки из указанных.

Вопрос: Навыки - Python, GIT, mySQL, Docker. Ожидаю профессию Аналитик данных.

Ответ: Сферы, в которых вы можете работать: Дизайн, Data Science, IT Programming. В сферу Data Science входит профессия Аналитик данных, поэтому результат совпадает.

Вопрос: Я обладаю следующими навыками: управление командами, Excel, R, SQL, заключение договоров, управление проектами. Опыт работы у меня год. Я ожидаю, что в списке рекомендованных профессий у меня будет project management. ЗП будет около 30 000-35 000 руб.

Ответ: Сферы, в которых вы можете работать: Office management, Call-центр. Сфера office management довольна похожа с теми навыками, которые вы указываете, а зарплата оценивается в 58000 рублей.

Вопрос: Если указать SQL, CSS, грамотная речь и креативность. Приложение должно выдать сектор, связанный с IT, но возможно это будет что-то более общее, так как креативность и грамотная речь немного отдаляют вакансию от IT секторы - поэтому, скорее всего, в качестве рекомендации мы получим что-то около project или product менеджмента.

Ответ: Сферы, в которых вы можете работать: Охрана труда, Контроль качества, Производственный делооборот. Приложение не выдало ни одной сферы, напрямую связанной с IT, разве что во всех сферах необходимо знание ПК.

Вопрос: Что будет если я укажу навыки из абсолютно не связанных сфер? Например, я студент эконома, хорошо разбираюсь в бух учете, но при этом тренируюсь писать SQL запросы с перспективой развития в сфере аналитики, я хочу посмотреть, какие перспективы ждут меня в обеих сферах. Как будет строиться рекомендация? Я ожидаю, что приложение выдаст мне как вакансии, где пригодятся мои навыки как в ведении бух. учета, так и в написании sql запросов. Исходя из дополнительных требований (необходимых скиллов), я буду принимать решение в какой сфере у меня есть больше шансов найти стажировку при уже имеющихся у меня скиллов

Ответ:Сферы, в которых вы можете работать: Бухгалтерский учёт, Производственный делооборот, Техобслуживание ПК. Видимо, одного знания SQL недостаточно для того, чтобы рекомендовать вам сферу IT, однако вы сможете посмотреть полный список навыков для сферы Бухгалтерского учета и Производственного делооборота, в котором тоже необходимо знание бух. учета. Помимо списка необходимых навыков, вы сможете оценить долю hard и soft скиллов, чтобы понять в зависимости от ваших умений, насколько вам подойдет эта сфера.

Вопрос: Если я выбираю такие навыки как Финансовый анализ, Работа в команде, Работа с текущей базой данных и MS Excel, какую профессию мне выдаст приложение? Я ожидаю, что программа выдаст мне профессию менеджера по финансам.

Ответ: Сферы, в которых вы можете работать: HR Management, Производственный делооборот, Делопроизводство. Эти сферы лишь косвенно связаны с финансами, поскольку приложение посчитало более значимыми soft навыки, однако, если указать только финансовый анализ, то приложение выдаст сферу Business management, в которой присутствуют навыки ms excel и финансовый анализ.

Вопрос: Я бы хотел получить данные по двум параметрам: 3D и компас. В современном мире стремительно развивается графика, которая позволяет переносить в трёхмерный формат оцифрованные копии ландшафта и других географических объектов. Ожидаю, что по этим параметрам я получу данные о графических дизайнерах в сфере картографии

Ответ: К сожалению, в базе данных, которая у нас получилась, эта профессия оказалась недостаточно распространенной, и алгоритм не выделил сферу с этими навыками как графический дизайн, он советует стандартные сферы: Работа за кассой, Делопроизводство, Охрана труда

Вопрос: Проверить результаты для парня 23г и для девушки 20л, навыки у обоих: python, sql, английский язык, аналитическое мышление, графический дизайн. Ожидаю, что результаты будут отличаться для двух людей, интересно узнать, будут ли и как именно.

Ответ: Наше приложение чувствительно только к навыкам, пока нет возможности вводить пол и возраст, так что оно выдаст одинаковый результат - Сферы, в которых вы можете работать: Project Management, Складные работы, Охрана труда.

Ответ на вопрос преподавателей

Вопрос: Рассмотрите сценарий, когда пользователь хочет поменять сферу деятельности — нужно ли вводить навыки из предыдущей сферы или только из новой?

Ответ: Мы предсказываем сферы, которые соответствуют навыкам пользователя, а не пытаемся подобрать похожую сферу на основе навыков внутри неё. Рекомендовать сферу по навыкам, которых у пользователя нет, наше приложение может, однако результаты не будут релевантны для пользователя, так как он не обладает хоть каким-то набором навыков, необходимым для того, чтобы менее трудозатратно войти в сферу. Мы предполагаем, что пользователь, решивший сменить сферу деятельности, захочет посмотреть те сферы, где нужен тот же набор навыков, который есть у него сейчас, чтобы не тратить силы и время на изучение кардинально новых навыков, при этом находясь в состоянии временно безработного человека/работающего в сфере, из которой хочет перейти. Он ,конечно, может указать навыки, которые могут быть в предполагаемой новой сфере, но это будет нерелевантно с той точки зрения, что этими навыками он может не обладать.

Чем вы гордитесь в своем приложении

На основании peer review наши коллеги выделили следующее в нашем проекте:

  • Приложение позиционируется коммерчески и продающе, еще чуть-чуть и будет как у Apple
  • Парсинг данных с hh.ru
  • Текстовая обработка
  • Модель на базе ChatGPT
  • Тематическое моделирование LDA

Мы считаем, что справились отлично с обработкой огромного объема данных, собранного самостоятельно с помощью API hh.ru, и сделали на их основе работоспособное приложение, которое может быть полезно его целевой аудитории, в нашем случае – студентам.