Automação do monitoramento salarial usando R

Cada escritório que se preze monitora regularmente os salários, a fim de navegar no segmento do mercado de trabalho que lhe interessa. No entanto, apesar do fato de a tarefa ser necessária e importante, nem todos estão prontos para pagar serviços de terceiros por isso.


Nesse caso, para salvar o RH da necessidade de classificar manualmente manualmente centenas de vagas e currículos, é mais eficiente escrever um pequeno aplicativo uma vez que faça você mesmo e, na saída, fornecer o resultado na forma de um belo painel com tabelas, gráficos, a capacidade de filtrar e enviar dados. Por exemplo, isto:



Você pode assistir ao vivo (e até apertar os botões) aqui .


Neste artigo, falarei sobre como escrevi esse aplicativo e quais as armadilhas que encontrei ao longo do caminho.


Declaração do problema


É necessário escrever um aplicativo que colete dados da tarefa hh.ru e continue para posições específicas (desenvolvedor de back-end / front-end / full stack, DevOps, QA, gerente de projetos, analista de sistemas etc.) em São Petersburgo e forneça o valor mínimo, médio e máximo das expectativas e ofertas salariais para especialistas do nível júnior, médio e sênior de cada uma dessas profissões.


Era para atualizar os dados aproximadamente a cada seis meses, mas não mais do que uma vez por mês.


Primeiro protótipo


Escrito em puro brilho, com um belo layout de bootstrap, à primeira vista não saiu muito nada: simples e, o mais importante, compreensível. A página principal do aplicativo contém o mais necessário: para cada especialidade, o valor médio dos salários e expectativas salariais (nível médio) está disponível, há também a data da última atualização de dados e o botão Atualizar. As guias no cabeçalho - pelo número de especialidades em consideração - contêm tabelas com dados e gráficos coletados completos.



Se o usuário perceber que os dados não foram atualizados por muito tempo, ele pressiona o botão "Atualizar" da especialidade correspondente. Folhas de aplicação no inconsciente pense por 5 minutos, o funcionário sai para tomar café. Ao retornar, aguardando dados atualizados na página principal e na guia correspondente.


Pergunta para autoteste: o que há de errado com este protótipo?

No mínimo, para atualizar os dados de todas as nove especialidades, o usuário precisa clicar no botão Atualizar em cada bloco - e assim nove vezes.


Por que não fazer um botão "Atualizar" para tudo? O fato é - e esse é o segundo problema - que, para cada solicitação ("atualizar e processar dados sobre gerentes", "atualizar e processar dados sobre controle de qualidade" etc.), foram necessários de 5 a 10 minutos , o que por si só não é permitido por um longo tempo Uma única solicitação para atualizar todos os dados transformaria 5 minutos em 45 ou até 60. O usuário não pode esperar tanto.


Mesmo várias funções withProgress() que withProgress() processos de coleta e processamento de dados e tornaram a expectativa do usuário mais significativa dessa maneira, não withProgress() muito a situação.


O terceiro problema com esse protótipo é que, se adicionarmos mais uma dúzia de profissões (bem, e se), enfrentaremos o fato de que o local no cabeçalho termina .


Esses três motivos foram suficientes para eu repensar completamente a abordagem de criação de um aplicativo e UX. Se você encontrar mais, sinta-se à vontade para comentar.


Este protótipo também teve pontos fortes, a saber:


  • Uma abordagem generalizada da interface e da lógica de negócios: em vez de copiar e colar, removemos as mesmas partes em uma função separada com parâmetros.

Por exemplo, é assim que o "bloco" de uma especialidade fica na página principal:


Código
 tile <- function(title, midsal = NA, midsalres = NA, total.res = NA, total.vac = NA, updated = NA) { return( column(width = 4, h2(title), strong("  (middle):"), midsal, br(), strong("  (middle):"), midsalres, br(), strong(" :"), total.res, br(), strong(" : "), total.vac, br(), strong(" : "), updated, br(), br(), actionButton(inputId = paste0(tolower(prof), "Btn"), label = "Update", class = "btn-primary") ) ) } 

  • Formação dinâmica da interface do usuário até identificadores (inputId) no código, através de inputId = paste0(, "Btn") , veja o exemplo acima. Essa abordagem se mostrou extremamente conveniente, pois era necessário inicializar com uma dúzia de controles, multiplicados pelo número de profissões.
  • Funcionou :)

Os dados coletados foram armazenados em arquivos .csv para várias profissões ( append = TRUE ) e, em seguida, lidos a partir daí quando o aplicativo foi iniciado. Quando novos dados apareceram, eles foram adicionados ao arquivo correspondente e os valores médios foram recalculados.


Algumas palavras sobre separadores


Uma nuance importante: os separadores padrão para arquivos csv - uma vírgula ou ponto-e-vírgula - não são muito adequados para o nosso caso, porque muitas vezes você pode encontrar vagas e currículos com títulos como "Shvets, ceifeira, igrets (duda; html / css)". Portanto, eu imediatamente decidi escolher algo mais exótico, e minha escolha caiu sobre |.


Tudo correu bem até a próxima vez que comecei, não encontrei a data na coluna com a moeda e, em seguida, as colunas foram movidas para baixo e, como resultado, os gráficos de bloqueio. Eu comecei a entender Como se viu, meu sistema foi quebrado por uma garota bonita - "Data Analyst | Business Analyst". Desde então, tenho usado \x1B como delimitador, o caractere ESC. Ainda não decepcionou.


Atribuir ou não atribuir?


Enquanto trabalhava nesse projeto, a função de atribuição se tornou uma verdadeira descoberta para mim: você pode gerar dinamicamente os nomes de variáveis ​​e outros quadros de datas, legal!


Obviamente, quero manter os dados de origem em quadros de dados separados para diferentes vagas. E não quero escrever "designer.vac = data.frame (...), analyst.vac = data.frame (...)". Portanto, o código para inicializar esses objetos quando iniciei o aplicativo era assim:


Atribuir
 profs <- c("analyst", "designer", "developer", "devops", "manager", "qa") for (name in profs) { if (!exists(paste0(name, ".vac"))) assign(x = paste0(name, ".vac"), value = data.frame( URL = character() #    , id = numeric() # id  , Name = character() #   , City = character() , Published = character() , Currency = character() , From = numeric() # .    , To = numeric() # .  , Level = character() # jun/mid/sen , Salary = numeric() , stringsAsFactors = FALSE )) } 

Mas minha alegria não durou muito. Não era mais possível acessar esses objetos no futuro por meio de um determinado parâmetro, e isso, forçosamente, levou à duplicação de código. Ao mesmo tempo, o número de objetos cresceu exponencialmente e, como resultado, ficou fácil confundi-los e atribuir chamadas.


Então eu tive que usar uma abordagem diferente, que acabou sendo muito mais simples: usando listas.


Inicializar um pacote de quadros de dados? Fácil!
 profs <- list( devops = "devops" , analyst = c("systems+analyst", "business+analyst") , dev.full = "full+stack+developer" , dev.back = "back+end+developer" , dev.front = "front+end+developer" , designer = "ux+ui+designer" , qa = "QA+tester" , manager = "project+manager" , content = c("mathematics+teacher", "physics+teacher") ) for (name in names(profs)) { proflist[[name]] <- data.frame( URL = character() #    , id = numeric() # id  , Name = character() #   , City = character() , Published = character() , Currency = character() , From = numeric() # .    , To = numeric() # .  , Level = character() # jun/mid/sen , Salary = numeric() , stringsAsFactors = FALSE ) } 

Observe que, em vez do vetor usual com os nomes das profissões, como antes, eu uso uma lista que, ao mesmo tempo, inclui consultas de pesquisa, que procuram dados sobre vagas e currículos para uma determinada profissão. Então, consegui me livrar da opção feia ao chamar a função de procura de emprego.


Renderizar N tabelas e N gráficos desses quadros de dados de uma só vez? Hum ...

Além disso, em geral, não é difícil. Aqui está um exemplo esférico no vácuo para server.R:


 lapply(seq_along(my.list.of.data.frames), function(x) { output[[paste0(names(my.list.of.data.frames)[x], ".dt")]] <- renderDataTable({ datatable(data = my.list.of.data.frames[[names(my.list.of.data.frames)[x]]]() , style = 'bootstrap', selection = 'none' , escape = FALSE) }) output[[paste0(names(my.list.of.data.frames)[x], ".plot")]] <- renderPlot( ggplot(na.omit(my.list.of.data.frames[[names(my.list.of.data.frames)[x]]]()), aes(...)) ) }) 

Daí a conclusão: listas são uma coisa extremamente conveniente que permite reduzir a quantidade de código e o tempo necessário para processá-lo. (Portanto, não atribua.)


E naquele momento em que me distraí da refatoração na palestra de Joe Cheng sobre painéis , veio ...


Repensando


Acontece que no R existe um pacote especial, afiado para a criação de painéis - painel brilhante . Ele também usa bootstrap e facilita um pouco a organização de uma interface do usuário com uma barra lateral concisa que pode ser completamente oculta sem nenhum painel conditionalPanel() , permitindo que o usuário se concentre no estudo dos dados.


Acontece que, se o RH verifica os dados a cada seis meses, eles não precisam do botão Atualizar. Nenhuma. Este não é exatamente um "painel estático", mas próximo disso. O script de atualização de dados pode ser implementado completamente separadamente do aplicativo brilhante e executá-lo de acordo com a programação com o Agendador padrão Windows seu sistema operacional.


Isso resolve dois problemas ao mesmo tempo: uma longa espera (se você executar regularmente o script em segundo plano, o usuário nem perceberá seu trabalho, mas sempre verá novos dados) e ações redundantes necessárias para atualizar os dados. Antes eram necessários nove cliques (um para cada especialidade), agora são necessários zero. Parece que alcançamos um ganho de eficiência, lutando pelo infinito!


Acontece que o código em diferentes partes do aplicativo é executado um número desigual de vezes. Não vou me debruçar sobre isso em detalhes; se desejar, é melhor se familiarizar com a explicação visual no relatório . Apenas descreverei a idéia principal: manipular dados dentro de ggplot (), mal em tempo real e quanto mais código você puder trazer para os níveis superiores do aplicativo, melhor. A produtividade ao mesmo tempo cresce às vezes.


De fato, quanto mais eu olhava para o relatório, mais claramente percebia o quanto o código no meu primeiro protótipo não era organizado pelo Feng Shui e, em algum momento, ficou óbvio que o projeto era mais fácil de reescrever do que refatorar. Mas como deixar sua ideia quando tanto esforço foi investido nela?


O que está morto não pode morrer


- Pensei e reescrevi o projeto do zero, e desta vez


  • entregou todo o código para coletar dados sobre vagas e currículos (de fato, todo o processo ETL) em um script separado que pode ser executado independentemente de um aplicativo brilhante, poupando o usuário de uma espera tediosa;
  • usei reactiveFileReader () para ler dados pré-coletados de arquivos csv, garantindo a relevância dos dados de origem no meu aplicativo sem a necessidade de reiniciar e ações desnecessárias do usuário;
  • se livrou de assign () a favor de trabalhar com listas e usou ativamente lapply () onde havia loops antes;
  • aplicativos de interface do usuário redesenhados usando o painel brilhante, como um bônus - não há necessidade de se preocupar com a falta de espaço na tela;
  • várias vezes reduziu o volume total do aplicativo (de ~ 1800 para 360 linhas de código).

Agora a solução funciona da seguinte maneira.


  1. O script ETL é executado uma vez por mês (aqui está a instrução sobre como fazer isso) e conscientemente passa por todas as profissões, coletando dados brutos sobre vagas e currículos a partir de hh.
    Além disso, os dados sobre vagas são obtidos através da API do site (eu pude reutilizar parcialmente o código do projeto anterior ), mas para cada currículo eu tive que analisar páginas da Web usando o pacote rvest, porque o acesso ao método API correspondente agora foi pago. Você pode adivinhar como isso afetou a velocidade do script.
  2. Os dados coletados são penteados - o processo é descrito em detalhes e com exemplos de código aqui . Os dados processados ​​são salvos no disco em arquivos separados nos formatos hist / profession-hist-vac.csv e hist / profession-hist-res.csv. A propósito, valores discrepantes em dados como esse podem levar a coisas engraçadas, tenha cuidado :)
    Para cada profissão, o script usa um arquivo aumentado com dados históricos, seleciona os mais relevantes - aqueles com menos de um mês a partir da data da última atualização - e gera novos arquivos csv no formato data.res / profession-res-recent.csv e data.vac / profession -vac-recent.csv. A aplicação final também trabalha com esses dados ...
  3. ... que, após iniciar, lê o conteúdo das pastas de resumo e de tarefas (data.res e data.vac, respectivamente) e verifica a cada hora as alterações nos arquivos. Fazer isso com reactiveFileReader () é muito mais eficiente em termos de recursos e velocidade de execução do que usar invalidateLater (). Se houver alterações nos arquivos, as tabelas com os dados de origem serão atualizadas automaticamente e os valores e gráficos médios serão recalculados, pois dependem de reactiveValues ​​(), ou seja, nenhum código adicional é necessário para lidar com essa situação.
  4. Na página principal, agora existe uma tabela que mostra os valores mínimo, mediano e máximo das expectativas de salário e ofertas para cada especialidade para cada um dos níveis encontrados (todos para TK). Além disso, é possível ver os gráficos nas guias com informações detalhadas e carregar os dados no formato .xlsx (você nunca sabe o que esses números são necessários para o RH).

Só isso. Acontece que o único botão agora disponível para o usuário em nosso painel é o botão Download. E isso é para melhor: quanto menos o usuário tiver botões, menor a chance lançar uma exceção não tratada fique confuso neles.


Em vez de um epílogo


Hoje, o aplicativo coleta e analisa dados apenas para São Petersburgo. Considerando que o principal interessado estava satisfeito e a reação mais frequente foi “ótima, mas isso pode ser feito a Moscou?”, Considero o experimento um sucesso.


Você pode visualizar o aplicativo neste link e todo o código-fonte (junto com exemplos de arquivos concluídos) está disponível aqui .


A propósito, o aplicativo é chamado de Salary Monitor, abreviado Salmon - "salmon".


Source: https://habr.com/ru/post/pt417991/


All Articles