Automatisation du suivi des salaires avec R

Chaque bureau qui se respecte surveille régulièrement les salaires afin de naviguer dans le segment du marché du travail qui l'intéresse. Cependant, malgré le fait que la tâche est nécessaire et importante, tout le monde n'est pas prêt à payer des services tiers pour cela.


Dans ce cas, afin d'économiser les ressources humaines de la nécessité de trier régulièrement manuellement des centaines de postes vacants et de curriculum vitae, il est plus efficace d'écrire une petite application une fois qui le fera vous-même et, à la sortie, fournir le résultat sous la forme d'un beau tableau de bord avec des tableaux, des graphiques, la possibilité de filtrer et de télécharger des données. Par exemple, ceci:



Vous pouvez regarder en direct (et même appuyer sur les boutons) ici .


Dans cet article, je parlerai de la façon dont j'ai écrit une telle application et des pièges que j'ai rencontrés en cours de route.


Énoncé du problème


Il est nécessaire d'écrire une application qui collectera les données de travail hh.ru et reprendra pour des postes spécifiques (développeur Back-end / Front-end / Full-stack, DevOps, QA, Project Manager, Systems Analyst, etc.) à Saint-Pétersbourg et donner la valeur minimale, moyenne et maximale des attentes salariales et des offres pour les spécialistes de niveau junior, moyen et senior pour chacune de ces professions.


Il était censé mettre à jour les données environ tous les six mois, mais pas plus d'une fois par mois.


Premier prototype


Écrit en pur brillant, avec une belle mise en page bootstrap, à première vue, il est sorti presque rien: simple, et surtout - compréhensible. La page principale de l'application contient les plus nécessaires: pour chaque spécialité, la valeur moyenne des salaires et des attentes salariales (niveau intermédiaire) est disponible, il y a aussi la date de la dernière mise à jour des données et le bouton Mettre à jour. Les onglets dans l'en-tête - par le nombre de spécialités considérées - contiennent des tableaux avec des données et des graphiques collectés complets.



Si l'utilisateur constate que les données n'ont pas été mises à jour depuis trop longtemps, il appuie sur le bouton "Mettre à jour" pour la spécialité correspondante. Feuilles d'application dans l'inconscient pensez pendant 5 minutes, l'employé part boire un café. A son retour, en attente de mise à jour des données sur la page principale et sur l'onglet correspondant.


Question pour l'auto-test: quel est le problème avec ce prototype?

À tout le moins, afin de mettre à jour les données sur les neuf spécialités, l'utilisateur doit cliquer sur le bouton Mettre à jour sur chaque tuile - et donc neuf fois.


Pourquoi ne pas créer un bouton «Mettre à jour» pour tout? Le fait est - et c'est le deuxième problème - que pour chaque demande ("mettre à jour et traiter les données sur les gestionnaires", "mettre à jour et traiter les données sur l'AQ", etc.), cela a pris 5 à 10 minutes , ce qui en soi n'est pas autorisé. depuis longtemps. Une seule demande de mise à jour de toutes les données transformerait 5 minutes en 45, voire les 60. L' utilisateur ne peut pas attendre autant.


Même plusieurs fonctions withProgress() qui enveloppaient les processus de collecte et de traitement des données et rendaient les attentes des utilisateurs plus significatives de cette manière n'ont pas trop sauvé la situation.


Le troisième problème avec ce prototype est que si nous ajoutons une douzaine de professions supplémentaires (enfin, et si), nous serions confrontés au fait que la place dans l'en-tête se termine .


Ces trois raisons m'ont suffi pour repenser complètement l'approche de construction d'une application et de l'UX. Si vous en trouvez plus, n'hésitez pas à commenter.


Ce prototype avait également des atouts, à savoir:


  • Une approche généralisée de l'interface et de la logique métier: au lieu de copier-coller, nous supprimons les mêmes éléments dans une fonction séparée avec des paramètres.

Par exemple, voici à quoi ressemble la «tuile» d'une spécialité sur la page principale:


Code
 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") ) ) } 

  • Formation dynamique de l' interface utilisateur jusqu'aux identifiants (inputId) dans le code, via inputId = paste0(, "Btn") , voir l'exemple ci-dessus. Cette approche s'est avérée extrêmement pratique, car il fallait initialiser avec une dizaine de contrôles, multipliés par le nombre de professions.
  • Ça a marché :)

Les données collectées ont été stockées dans des fichiers .csv pour diverses professions ( append = TRUE ), puis lues à partir de là lorsque l'application a été lancée. Lorsque de nouvelles données sont apparues, elles ont été ajoutées au fichier correspondant et les valeurs moyennes ont été recalculées.


Quelques mots sur les séparateurs


Une nuance importante: les séparateurs standard pour les fichiers csv - une virgule ou un point-virgule - ne conviennent pas très bien à notre cas, car vous pouvez souvent trouver des postes vacants et des CV avec des titres comme "Shvets, reaper, igrets (duda; html / css)". Par conséquent, j'ai immédiatement décidé de choisir quelque chose de plus exotique, et mon choix s'est porté sur |.


Tout s'est bien passé jusqu'à la prochaine fois que j'ai commencé, je n'ai pas trouvé la date dans la colonne avec la devise, puis les colonnes ont baissé et, par conséquent, les tableaux de verrouillage. J'ai commencé à comprendre. Il s'est avéré que mon système a été brisé par une belle fille - "Data Analyst | Business Analyst". Depuis lors, j'utilise \x1B comme délimiteur, le caractère ESC. Toujours pas déçu.


Attribuer ou ne pas attribuer?


En travaillant sur ce projet, la fonction assign est devenue une vraie découverte pour moi: vous pouvez générer dynamiquement les noms des variables et autres dates, cool!


Bien sûr, je souhaite conserver les données source dans des trames de données distinctes pour différents postes vacants. Et je ne veux pas écrire "designer.vac = data.frame (...), analyst.vac = data.frame (...)". Par conséquent, le code pour initialiser ces objets lorsque j'ai démarré l'application ressemblait à ceci:


Attribuer
 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 )) } 

Mais ma joie n'a pas duré longtemps. Il n'était plus possible d'accéder à de tels objets à l'avenir via un certain paramètre, ce qui a forcément conduit à une duplication de code. Dans le même temps, le nombre d'objets a augmenté de façon exponentielle, et en conséquence, il est devenu facile de se confondre en eux et d'attribuer des appels.


J'ai donc dû utiliser une approche différente, qui s'est avérée beaucoup plus simple: utiliser des listes.


Initialiser un paquet de trames de données? C'est facile!
 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 ) } 

Veuillez noter qu'au lieu du vecteur habituel avec les noms des professions, comme auparavant, j'utilise une liste, qui comprend en même temps des requêtes de recherche, qui recherchent des données sur les postes vacants et les curriculum vitae pour une profession particulière. J'ai donc réussi à me débarrasser du commutateur laid lors de l'appel de la fonction de recherche d'emploi.


Rendre N tableaux et N graphiques à partir de ces trames de données d'un seul coup? Hm ...

De plus, en général, ce n'est pas difficile. Voici un exemple sphérique dans le vide pour 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(...)) ) }) 

D'où la conclusion: les listes sont une chose extrêmement pratique qui vous permet de réduire la quantité de code et le temps nécessaire pour le traiter. (Par conséquent, ne pas attribuer.)


Et à ce moment où j'ai été distrait de refactoring sur le discours de Joe Cheng sur les tableaux de bord , c'est venu ...


Repenser


Il s'avère que dans R, il existe un package spécial, affûté pour la création de tableaux de bord - shinydashboard . Il utilise également le bootstrap et facilite un peu plus l'organisation d'une interface utilisateur avec une barre latérale concise qui peut être complètement masquée sans conditionalPanel() , permettant à l'utilisateur de se concentrer sur l'étude des données.


Il s'avère que si les RH vérifient les données une fois tous les six mois, elles n'ont pas besoin du bouton Mettre à jour. Pas du tout. Ce n'est pas exactement un "tableau de bord statique", mais proche de cela. Le script de mise à jour des données peut être implémenté complètement séparément de l'application brillante et l'exécuter selon le calendrier avec le planificateur standard Windows votre système d'exploitation.


Cela résout deux problèmes à la fois: une longue attente (si vous exécutez régulièrement le script en arrière-plan, l'utilisateur ne remarquera même pas son travail, mais ne verra toujours que des données fraîches) et des actions redondantes requises de l'utilisateur pour mettre à jour les données. Auparavant, il fallait neuf clics (un pour chaque spécialité), maintenant il n'en faut plus. Il semble que nous ayons atteint un gain d'efficacité, en recherchant l'infini!


Il s'avère que le code dans différentes parties de l'application est exécuté un nombre de fois inégal. Je ne m'attarderai pas là-dessus en détail; si vous le souhaitez, il est préférable de vous familiariser avec l'explication visuelle du rapport . Je ne présenterai que l'idée principale: manipuler les données à l'intérieur de ggplot (), le mal à la volée, et plus vous pouvez apporter de code aux niveaux supérieurs de l'application, mieux c'est. La productivité augmente en même temps parfois.


En fait, plus je regardais le rapport, plus je réalisais clairement à quel point le code de mon premier prototype n'était pas organisé par le Feng Shui, et à un moment donné, il est devenu évident que le projet était plus facile à réécrire qu'à refactoriser. Mais comment quitter votre idée quand tant d'efforts y ont été investis?


Ce qui est mort ne peut pas mourir


- J'ai pensé et réécrit le projet à partir de zéro, et cette fois


  • livré le code complet pour la collecte de données sur les postes vacants et les curriculum vitae (en fait, l'ensemble du processus ETL) dans un script distinct qui peut être exécuté indépendamment d'une application brillante, ce qui évite à l'utilisateur une attente fastidieuse;
  • utilisé reactiveFileReader () pour lire les données pré-collectées à partir des fichiers csv, garantissant la pertinence des données source dans mon application sans avoir besoin de redémarrer et des actions utilisateur inutiles;
  • s'est débarrassé de assign () en faveur de l'utilisation des listes et a utilisé activement lapply () là où il y avait des boucles auparavant;
  • des applications d'interface utilisateur repensées utilisant shinydashboard, en bonus - pas besoin de s'inquiéter du manque d'espace sur l'écran;
  • plusieurs fois réduit le volume total de l'application (de ~ 1800 à 360 lignes de code).

Maintenant, la solution fonctionne comme suit.


  1. Le script ETL est exécuté une fois par mois (voici les instructions sur la façon de le faire) et passe consciencieusement à travers toutes les professions, collectant des données brutes sur les postes vacants et les CV de hh.
    De plus, les données sur les postes vacants sont collectées via l'API du site (j'ai pu réutiliser partiellement le code du projet précédent ), mais pour chaque CV, j'ai dû analyser des pages Web à l'aide du package rvest, car l'accès à la méthode API correspondante est désormais payé. Vous pouvez deviner comment cela a affecté la vitesse du script.
  2. Les données collectées sont peignées - le processus est décrit en détail et avec des exemples de code ici . Les données traitées sont enregistrées sur le disque dans des fichiers séparés de la forme hist / profession-hist-vac.csv et hist / profession-hist-res.csv. Soit dit en passant, les valeurs aberrantes dans des données comme celles-ci peuvent conduire à des choses amusantes, soyez prudent :)
    Pour chaque profession, le script prend un fichier augmenté avec des données historiques, sélectionne les plus pertinentes - celles qui n'ont pas plus d'un mois à compter de la date de la dernière mise à jour - et génère de nouveaux fichiers csv de la forme data.res / profession-res-recent.csv et data.vac / profession -vac-recent.csv. L'application finale fonctionne également avec ces données ...
  3. ... qui, après le démarrage, lit le contenu des dossiers de curriculum vitae et de travail (data.res et data.vac, respectivement), puis vérifie toutes les heures les modifications apportées aux fichiers. Faire cela avec reactiveFileReader () est beaucoup plus efficace en termes de ressources et de vitesse d'exécution que d'utiliser invalidateLater (). S'il y a eu des changements dans les fichiers, les tables avec les données source sont automatiquement mises à jour et les valeurs moyennes et les graphiques sont recalculés, car ils dépendent de reactiveValues ​​(), c'est-à-dire qu'aucun code supplémentaire n'est requis pour gérer cette situation.
  4. Sur la page principale, il y a maintenant un tableau qui montre les valeurs minimale, médiane et maximale des attentes salariales et des offres pour chaque spécialité pour chacun des niveaux trouvés (tous pour les savoirs traditionnels). De plus, vous pouvez voir les graphiques sur les onglets avec des informations détaillées et télécharger les données au format .xlsx (vous ne savez jamais ce que ces chiffres seront nécessaires pour les RH).

C’est tout. Il s'avère que le seul bouton désormais disponible pour l'utilisateur sur notre tableau de bord est le bouton Télécharger. Et c'est pour le mieux: moins l'utilisateur a de boutons, moins il y a de chance lever une exception non gérée se confondre en eux.


Au lieu d'un épilogue


Aujourd'hui, l'application collecte et analyse des données uniquement pour Saint-Pétersbourg. Étant donné que le principal intervenant était satisfait et que la réaction la plus fréquente était «excellente, mais cela peut-il être fait à Moscou?», Je considère l'expérience comme un succès.


Vous pouvez voir l' application sur ce lien , et tout le code source (ainsi que des exemples de fichiers finis) est disponible ici .


Par ailleurs, l'application est appelée Salary Monitor, abrégé Salmon - "saumon".


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


All Articles