Automatisierung der Gehaltsüberwachung mit R.

Jedes Büro mit Selbstachtung überwacht regelmäßig die Löhne, um sich in dem Segment des Arbeitsmarktes zurechtzufinden, das es interessiert. Trotz der Tatsache, dass die Aufgabe notwendig und wichtig ist, ist nicht jeder bereit, dafür Dienste von Drittanbietern zu bezahlen.


In diesem Fall ist es effizienter, eine kleine Anwendung zu schreiben, sobald die Personalabteilung dies selbst erledigt, und am Ende das Ergebnis in Form eines schönen Dashboards mit Tabellen, Grafiken und der Möglichkeit zum Filtern und Hochladen von Daten bereitzustellen. Zum Beispiel:



Sie können hier live zuschauen (und sogar die Tasten drücken).


In diesem Artikel werde ich darüber sprechen, wie ich eine solche Anwendung geschrieben habe und auf welche Fallstricke ich unterwegs gestoßen bin.


Erklärung des Problems


Es ist erforderlich, eine Anwendung zu schreiben, die hh.ru-Auftragsdaten sammelt und für bestimmte Positionen (Back-End- / Front-End- / Full-Stack-Entwickler, DevOps, Qualitätssicherung, Projektmanager, Systemanalytiker usw.) in St. Petersburg wieder aufnimmt und geben Sie den minimalen, durchschnittlichen und maximalen Wert der Gehaltserwartungen und Angebote für Spezialisten der Junior-, Middle- und Senior-Ebene für jeden dieser Berufe an.


Die Daten sollten ungefähr alle sechs Monate aktualisiert werden, jedoch nicht öfter als einmal im Monat.


Erster Prototyp


In reinem Glanz geschrieben, mit einem schönen Bootstrap-Layout, kam auf den ersten Blick so gut wie nichts heraus: einfach und vor allem - verständlich. Die Hauptseite des Antrags enthält das Notwendigste: Für jede Spezialität ist der Durchschnittswert der Gehälter und Gehaltserwartungen (mittlere Ebene) verfügbar, es gibt auch das Datum der letzten Datenaktualisierung und die Schaltfläche Aktualisieren. Registerkarten in der Kopfzeile enthalten - nach Anzahl der betrachteten Spezialitäten - Tabellen mit vollständig gesammelten Daten und Grafiken.



Wenn der Benutzer feststellt, dass die Daten nicht zu lange aktualisiert wurden, drückt er die Schaltfläche "Aktualisieren" für die entsprechende Spezialität. Anwendung verlässt ins Unbewusste Denken Sie für 5 Minuten, der Mitarbeiter geht, um Kaffee zu trinken. Warten Sie nach seiner Rückkehr auf aktualisierte Daten auf der Hauptseite und auf der entsprechenden Registerkarte.


Frage zum Selbsttest: Was ist mit diesem Prototyp falsch?

Um die Daten aller neun Spezialitäten zu aktualisieren, muss der Benutzer mindestens neun Mal auf die Schaltfläche Aktualisieren klicken .


Warum nicht für alles eine Schaltfläche "Aktualisieren" erstellen? Tatsache ist - und dies ist das zweite Problem -, dass für jede Anforderung ("Aktualisieren und Verarbeiten von Daten zu Managern", "Aktualisieren und Verarbeiten von Daten zu QS" usw.) 5 bis 10 Minuten gedauert haben , was an sich nicht zulässig ist für eine lange Zeit. Eine einzelne Anforderung zum Aktualisieren aller Daten würde 5 Minuten in 45 oder sogar alle 60 verwandeln. Der Benutzer kann nicht so lange warten.


Selbst mehrere withProgress() -Funktionen, die Datenerfassungs- und -verarbeitungsprozesse withProgress() und die Benutzererwartung auf diese Weise aussagekräftiger machten, retteten die Situation nicht zu sehr.


Das dritte Problem bei diesem Prototyp ist, dass wir, wenn wir ein Dutzend weitere Berufe hinzufügen (na ja, was wäre wenn), mit der Tatsache konfrontiert werden, dass der Platz in der Kopfzeile endet .


Diese drei Gründe reichten mir, um den Ansatz zum Erstellen einer Anwendung und von UX völlig zu überdenken. Wenn Sie mehr finden, können Sie dies gerne kommentieren.


Dieser Prototyp hatte auch Stärken, nämlich:


  • Ein verallgemeinerter Ansatz für die Benutzeroberfläche und die Geschäftslogik: Anstelle von Kopieren und Einfügen entfernen wir dieselben Teile in eine separate Funktion mit Parametern.

So sieht beispielsweise die „Kachel“ einer Spezialität auf der Hauptseite aus:


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

  • Dynamische Bildung der Benutzeroberfläche bis zu Bezeichnern (inputId) im Code durch inputId = paste0(, "Btn") , siehe Beispiel oben. Dieser Ansatz erwies sich als äußerst praktisch, da ein Dutzend Kontrollen, multipliziert mit der Anzahl der Berufe, initialisiert werden mussten.
  • Es hat funktioniert :)

Die gesammelten Daten wurden in CSV-Dateien für verschiedene Berufe gespeichert ( append = TRUE ) und dann beim Starten der Anwendung von dort gelesen. Wenn neue Daten angezeigt wurden, wurden sie der entsprechenden Datei hinzugefügt und die Durchschnittswerte neu berechnet.


Ein paar Worte zu Trennzeichen


Eine wichtige Nuance: Die Standardtrennzeichen für CSV-Dateien - ein Komma oder ein Semikolon - sind für unseren Fall nicht sehr geeignet, da Sie häufig freie Stellen und Lebensläufe mit Überschriften wie "Shvets, Reaper, igrets (duda; html / css)" finden. Deshalb habe ich mich sofort für etwas Exotischeres entschieden und meine Wahl fiel auf |.


Alles lief gut, bis ich das nächste Mal anfing, fand ich das Datum in der Spalte mit der Währung nicht, und dann bewegten sich die Spalten nach unten und infolgedessen die Sperrdiagramme. Ich begann zu verstehen. Wie sich herausstellte, wurde mein System von einem schönen Mädchen kaputt gemacht - "Data Analyst | Business Analyst". Seitdem verwende ich \x1B als Trennzeichen, das ESC-Zeichen. Immer noch nicht enttäuscht.


Zuweisen oder nicht zuweisen?


Während der Arbeit an diesem Projekt wurde die Zuweisungsfunktion für mich zu einer echten Entdeckung: Sie können dynamisch die Namen von Variablen und anderen Datumsrahmen generieren , cool!


Natürlich möchte ich die Quelldaten für verschiedene Stellen in getrennten Datenrahmen aufbewahren. Und ich möchte nicht "designer.vac = data.frame (...), Analyst.vac = data.frame (...)" schreiben. Daher sah der Code zum Initialisieren dieser Objekte beim Starten der Anwendung folgendermaßen aus:


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

Aber meine Freude hielt nicht lange an. Es war zukünftig nicht mehr möglich, über einen bestimmten Parameter auf solche Objekte zuzugreifen, und dies führte zwangsläufig zu einer Duplizierung des Codes. Gleichzeitig nahm die Anzahl der Objekte exponentiell zu, und infolgedessen wurde es leicht, sich in ihnen zu verwirren und Anrufe zuzuweisen.


Also musste ich einen anderen Ansatz verwenden, der viel einfacher war: Listen verwenden.


Ein Paket von Datenrahmen initialisieren? Einfach!
 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 ) } 

Bitte beachten Sie, dass ich anstelle des üblichen Vektors mit den Namen der Berufe wie zuvor eine Liste verwende, die gleichzeitig Suchanfragen enthält, die nach Daten zu offenen Stellen suchen und für einen bestimmten Beruf wieder aufgenommen werden. Also habe ich es geschafft, den hässlichen Schalter beim Aufrufen der Jobsuchfunktion loszuwerden.


Rendern Sie N Tabellen und N Diagramme aus diesen Datenrahmen auf einen Schlag? Hm ...

Auch im Allgemeinen ist nicht schwierig. Hier ist ein Beispiel für eine Kugel im Vakuum für 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(...)) ) }) 

Daher die Schlussfolgerung: Listen sind eine äußerst praktische Sache, mit der Sie die Menge an Code und die Zeit reduzieren können, die für die Verarbeitung erforderlich ist. (Daher nicht zuweisen.)


Und in dem Moment, als ich von Joe Chengs Vortrag über Dashboards abgelenkt war, kam es ...


Umdenken


Es stellt sich heraus, dass es in R ein spezielles Paket gibt, das für die Erstellung von Dashboards geschärft wurde - Shinydashboard . Es verwendet auch Bootstrap und erleichtert das Organisieren einer Benutzeroberfläche mit einer übersichtlichen Seitenleiste, die ohne ein conditionalPanel() Panel conditionalPanel() vollständig ausgeblendet werden kann, sodass sich der Benutzer auf das Studium der Daten konzentrieren kann.


Es stellt sich heraus, dass die Personalabteilung, wenn sie die Daten alle sechs Monate überprüft, die Schaltfläche "Aktualisieren" nicht benötigt. Gar keine. Dies ist nicht gerade ein "statisches Dashboard", aber nahe daran. Das Datenaktualisierungsskript kann vollständig getrennt von der glänzenden Anwendung implementiert und gemäß dem Zeitplan mit dem Standard-Scheduler ausgeführt werden Windows Ihr Betriebssystem.


Dies löst zwei Probleme gleichzeitig: eine lange Wartezeit (wenn Sie das Skript regelmäßig im Hintergrund ausführen, bemerkt der Benutzer nicht einmal seine Arbeit, sondern sieht immer nur neue Daten) und redundante Aktionen, die der Benutzer zum Aktualisieren der Daten benötigt. Früher dauerte es neun Klicks (einer für jede Spezialität), jetzt dauert es null. Es scheint, dass wir einen Effizienzgewinn erreicht haben und nach Unendlichkeit streben!


Es stellt sich heraus, dass der Code in verschiedenen Teilen der Anwendung ungleich oft ausgeführt wird. Ich werde nicht im Detail darauf eingehen. Wenn Sie möchten, ist es besser, sich mit der visuellen Erklärung im Bericht vertraut zu machen. Ich werde nur die Hauptidee skizzieren: Manipulieren von Daten in ggplot (), Böses im laufenden Betrieb und je mehr Code Sie in die oberen Ebenen der Anwendung bringen können, desto besser. Gleichzeitig wächst die Produktivität.


Je weiter ich mir den Bericht ansah, desto deutlicher wurde mir klar, wie sehr der Code in meinem ersten Prototyp nicht von Feng Shui organisiert wurde, und irgendwann wurde klar, dass das Projekt leichter umzuschreiben als umzugestalten war. Aber wie können Sie Ihre Idee verlassen, wenn so viel Aufwand in sie investiert wurde?


Was tot ist, kann nicht sterben


- Ich dachte und schrieb das Projekt von Grund auf neu, und diesmal


  • lieferte den gesamten Code zum Sammeln von Daten zu offenen Stellen und Lebensläufen (tatsächlich den gesamten ETL-Prozess) in ein separates Skript, das unabhängig von einer glänzenden Anwendung ausgeführt werden kann, wodurch der Benutzer vor langwierigem Warten bewahrt wird;
  • hat reactiveFileReader () verwendet , um vorab gesammelte Daten aus CSV-Dateien zu lesen, um die Relevanz der Quelldaten in meiner Anwendung sicherzustellen, ohne dass ein Neustart und unnötige Benutzeraktionen erforderlich sind.
  • hat assign () zugunsten der Arbeit mit Listen losgeworden und lapply () aktiv verwendet, wo es zuvor Schleifen gab;
  • Überarbeitete UI-Anwendungen mit Shinydashboard als Bonus - Sie müssen sich keine Sorgen über Platzmangel auf dem Bildschirm machen.
  • Das Gesamtvolumen der Anwendung wurde mehrmals reduziert (von ~ 1800 auf 360 Codezeilen).

Jetzt funktioniert die Lösung wie folgt.


  1. Das ETL-Skript wird einmal im Monat ausgeführt (hier finden Sie eine Anleitung dazu) und durchläuft gewissenhaft alle Berufe. Dabei werden Rohdaten zu offenen Stellen gesammelt und von hh wieder aufgenommen.
    Darüber hinaus werden die Daten zu offenen Stellen über die API der Site abgerufen (ich konnte den Code aus dem vorherigen Projekt teilweise wiederverwenden), aber für jeden Lebenslauf musste ich Webseiten mit dem rvest-Paket analysieren, da der Zugriff auf die entsprechende API-Methode jetzt bezahlt wurde. Sie können sich vorstellen, wie sich dies auf die Geschwindigkeit des Skripts auswirkt.
  2. Die gesammelten Daten werden gekämmt - der Vorgang wird hier ausführlich und mit Codebeispielen beschrieben. Die verarbeiteten Daten werden in separaten Dateien der Form hist / profession-hist-vac.csv und hist / profession-hist-res.csv auf der Festplatte gespeichert. Übrigens können Ausreißer in solchen Daten zu lustigen Dingen führen, seien Sie vorsichtig :)
    Für jeden Beruf nimmt das Skript eine erweiterte Datei mit historischen Daten, wählt die relevantesten aus - diejenigen, die nicht älter als einen Monat ab dem Datum der letzten Aktualisierung sind - und generiert neue CSV-Dateien in den Formaten data.res / profession-res-Recent.csv und data.vac / profession -vac-Recent.csv. Die endgültige Anwendung funktioniert auch mit diesen Daten ...
  3. ... liest nach dem Start den Inhalt der Ordner "Lebenslauf" und "Job" ("data.res" bzw. "data.vac") und überprüft dann stündlich, ob Änderungen an den Dateien vorgenommen wurden. Dies mit reactiveFileReader () zu tun ist in Bezug auf Ressourcen und Ausführungsgeschwindigkeit viel effizienter als die Verwendung von invalidateLater (). Wenn sich die Dateien geändert haben, werden die Tabellen mit den Quelldaten automatisch aktualisiert und die Durchschnittswerte und Diagramme neu berechnet, da sie von reactiveValues ​​() abhängen. Das heißt, es ist kein zusätzlicher Code erforderlich, um diese Situation zu bewältigen.
  4. Auf der Hauptseite gibt es jetzt eine Tabelle, die die Min-, Median- und Maximalwerte der Gehaltserwartungen und Angebote für jede Spezialität für jede der gefundenen Stufen (alle für TK) zeigt. Außerdem können Sie die Diagramme auf den Registerkarten mit detaillierten Informationen anzeigen und die Daten im XLSX-Format hochladen (Sie wissen nie, welche Zahlen für die Personalabteilung benötigt werden).

Das ist alles. Es stellt sich heraus, dass die einzige Schaltfläche, die dem Benutzer jetzt in unserem Dashboard zur Verfügung steht, die Schaltfläche Herunterladen ist. Und das ist zum Besseren: Je weniger Tasten der Benutzer hat, desto geringer ist die Chance eine unbehandelte Ausnahme auslösen in ihnen verwirrt werden.


Anstelle eines Nachworts


Heute sammelt und analysiert die Anwendung Daten nur für St. Petersburg. In Anbetracht der Tatsache, dass der Hauptakteur zufrieden war und die häufigste Reaktion „großartig, aber kann dies Moskau angetan werden?“ War, halte ich das Experiment für einen Erfolg.


Sie können die Anwendung unter diesem Link anzeigen. Der gesamte Quellcode (zusammen mit Beispielen für fertige Dateien) ist hier verfügbar.


Übrigens heißt die Anwendung Salary Monitor, abgekürzt Lachs - "Lachs".


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


All Articles