Automatización de la supervisión salarial con R

Cada oficina que se respeta a sí misma monitorea regularmente los salarios para navegar en el segmento del mercado laboral que le interesa. Sin embargo, a pesar de que la tarea es necesaria e importante, no todos están dispuestos a pagar servicios de terceros por esto.


En este caso, para evitar que RR.HH. necesite clasificar manualmente cientos de vacantes y currículums, es más eficiente escribir una pequeña aplicación una vez que lo haga usted mismo, y en la salida proporciona el resultado en forma de un hermoso tablero con tablas, gráficos, la capacidad de filtrar y cargar datos. Por ejemplo, esto:



Puedes ver en vivo (e incluso presionar los botones) aquí .


En este artículo, hablaré sobre cómo escribí una solicitud de este tipo y qué dificultades encontré en el camino.


Declaración del problema.


Es necesario escribir una aplicación que recopile datos de trabajo de hh.ru y se reanude para puestos específicos (Desarrollador back-end / Front-end / Full-stack, DevOps, QA, Project Manager, Systems Analyst, etc.) en San Petersburgo y dar el valor mínimo, promedio y máximo de las expectativas salariales y ofertas para especialistas de nivel junior, medio y superior para cada una de estas profesiones.


Se suponía que debía actualizar los datos aproximadamente cada seis meses, pero no más de una vez al mes.


Primer prototipo


Escrito en puro brillo, con un hermoso diseño de arranque, a primera vista no salió nada: simple y, lo más importante, comprensible. La página principal de la aplicación contiene lo más necesario: para cada especialidad, el valor promedio de los sueldos y las expectativas salariales (nivel medio) está disponible, también está la fecha de la última actualización de datos y el botón Actualizar. Las pestañas en el encabezado, por la cantidad de especialidades consideradas, contienen tablas con datos y gráficos recopilados completos.



Si el usuario ve que los datos no se han actualizado durante demasiado tiempo, presiona el botón "Actualizar" para la especialidad correspondiente. Hojas de aplicación en el inconsciente Piense durante 5 minutos, el empleado se va a tomar café. A su regreso, esperando datos actualizados en la página principal y en la pestaña correspondiente.


Pregunta para autoevaluación: ¿qué hay de malo en este prototipo?

Como mínimo, para actualizar los datos de las nueve especialidades, el usuario debe hacer clic en el botón Actualizar en cada mosaico , y así nueve veces.


¿Por qué no hacer un botón "Actualizar" para todo? El hecho es, y este es el segundo problema, que para cada solicitud ("actualizar y procesar datos en gerentes", "actualizar y procesar datos en QA", etc.) tomó de 5 a 10 minutos , lo que en sí mismo no está permitido por mucho tiempo Una sola solicitud para actualizar todos los datos convertiría 5 minutos en 45, o incluso los 60. El usuario no puede esperar tanto.


Incluso varias funciones withProgress() que envolvieron los procesos de recopilación y procesamiento de datos e hicieron que la expectativa del usuario fuera más significativa de esta manera no salvaron demasiado la situación.


El tercer problema con este prototipo es que si agregamos una docena de profesiones más (bueno, ¿y si?) Nos enfrentaríamos al hecho de que el lugar en el encabezado termina .


Estas tres razones fueron suficientes para repensar por completo el enfoque para crear una aplicación y UX. Si encuentra más, no dude en comentar.


Este prototipo también tenía puntos fuertes, a saber:


  • Un enfoque generalizado de la interfaz y la lógica empresarial: en lugar de copiar y pegar, eliminamos las mismas piezas en una función separada con parámetros.

Por ejemplo, así es como se ve el "mosaico" de una especialidad en la 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") ) ) } 

  • Formación dinámica de UI hasta identificadores (inputId) en el código, a través de inputId = paste0(, "Btn") , vea el ejemplo anterior. Este enfoque resultó extremadamente conveniente, porque era necesario inicializar con una docena de controles, multiplicado por el número de profesiones.
  • Funcionó :)

Los datos recopilados se almacenaron en archivos .csv para diversas profesiones ( append = TRUE ), y luego se leyeron desde allí cuando se lanzó la aplicación. Cuando aparecieron nuevos datos, se agregaron al archivo correspondiente y se volvieron a calcular los valores promedio.


Algunas palabras sobre separadores


Un matiz importante: los separadores estándar para archivos csv, una coma o punto y coma, no son muy adecuados para nuestro caso, porque a menudo puede encontrar vacantes y currículums con encabezados como "Shvets, segador, igrets (duda; html / css)". Por lo tanto, inmediatamente decidí elegir algo más exótico, y mi elección recayó en |.


Todo salió bien hasta la próxima vez que comencé, no encontré la fecha en la columna con la moneda, y luego las columnas se movieron hacia abajo y, como resultado, los gráficos de bloqueo. Comencé a entender. Al final resultó que una chica hermosa rompió mi sistema: "Analista de datos | Analista de negocios". Desde entonces, he estado usando \x1B como delimitador, el carácter ESC. Todavía no decepcionado.


Asignar o no asignar?


Mientras trabajaba en este proyecto, la función de asignación se convirtió en un verdadero descubrimiento para mí: puede generar dinámicamente los nombres de variables y otros marcos de fecha, ¡genial!


Por supuesto, quiero mantener los datos de origen en marcos de datos separados para diferentes vacantes. Y no quiero escribir "designer.vac = data.frame (...), analyst.vac = data.frame (...)". Por lo tanto, el código para inicializar estos objetos cuando comencé la aplicación se veía así:


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

Pero mi alegría no duró mucho. Ya no era posible acceder a dichos objetos en el futuro a través de un determinado parámetro, y esto, forzosamente, condujo a la duplicación de código. Al mismo tiempo, el número de objetos creció exponencialmente y, como resultado, se hizo fácil confundirse con ellos y asignar llamadas.


Así que tuve que usar un enfoque diferente, que terminó siendo mucho más simple: usar listas.


Inicializar un paquete de tramas de datos? 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 ) } 

Tenga en cuenta que en lugar del vector habitual con los nombres de profesiones, como antes, uso una lista, que al mismo tiempo incluye consultas de búsqueda, que buscan datos sobre vacantes y currículums para una profesión en particular. Así que me las arreglé para deshacerme del desagradable interruptor al llamar a la función de búsqueda de empleo.


Render N tablas y gráficos N de estos marcos de datos de una sola vez? Hm ...

Además, en general, no es difícil. Aquí hay un ejemplo esférico en vacío 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(...)) ) }) 

De ahí la conclusión: las listas son una cosa extremadamente conveniente que le permite reducir la cantidad de código y el tiempo que lleva procesarlo. (Por lo tanto, no asignar).


Y en ese momento cuando estaba distraído de refactorizar la charla de Joe Cheng sobre los tableros , llegó ...


Repensando


Resulta que en R hay un paquete especial, afilado para la creación de paneles: el panel brillante . También usa bootstrap y hace que sea un poco más fácil organizar una interfaz de usuario con una barra lateral concisa que se puede ocultar por completo sin ningún conditionalPanel() , lo que permite al usuario concentrarse en estudiar los datos.


Resulta que si Recursos Humanos revisa los datos una vez cada seis meses, no necesitan el botón Actualizar. Ninguno en absoluto. Esto no es exactamente un "tablero de instrumentos estático", pero está cerca de eso. El script de actualización de datos se puede implementar completamente por separado de la aplicación brillante y ejecutarlo de acuerdo con la programación con el Programador estándar Ventanas su sistema operativo


Esto resuelve dos problemas a la vez: una larga espera (si ejecuta regularmente el script en segundo plano, el usuario ni siquiera notará su trabajo, sino que siempre verá datos nuevos) y acciones redundantes requeridas por el usuario para actualizar los datos. Solía ​​tomar nueve clics (uno para cada especialidad), ahora toma cero. ¡Parece que hemos alcanzado una ganancia en eficiencia, luchando por el infinito!


Resulta que el código en diferentes partes de la aplicación se ejecuta un número desigual de veces. No me detendré en esto en detalle; si lo desea, es mejor familiarizarse con la explicación visual en el informe . Solo describiré la idea principal: manipular datos dentro de ggplot (), mal sobre la marcha, y cuanto más código pueda llevar a los niveles superiores de la aplicación, mejor. La productividad al mismo tiempo crece a veces.


De hecho, cuanto más miraba el informe, más claro me daba cuenta de cuánto el código en mi primer prototipo no estaba organizado por Feng Shui, y en algún momento se hizo evidente que el proyecto era más fácil de reescribir que de refactorizar. ¿Pero cómo dejar su creación cuando se ha invertido tanto esfuerzo en ella?


Lo que está muerto no puede morir.


- Pensé y reescribí el proyecto desde cero, y esta vez


  • entregó el código completo para recopilar datos sobre vacantes y currículums (de hecho, todo el proceso ETL) en un script separado que se puede ejecutar independientemente de una aplicación brillante, lo que evita que el usuario espere tediosamente;
  • utilicé reactiveFileReader () para leer datos recogidos previamente de archivos csv, asegurando la relevancia de los datos de origen en mi aplicación sin la necesidad de reiniciar y acciones innecesarias del usuario;
  • se deshizo de asignar () a favor de trabajar con listas y utilizó activamente lapply () donde antes había bucles;
  • aplicaciones de interfaz de usuario rediseñadas que utilizan una pantalla brillante, como beneficio adicional: no hay que preocuparse por la falta de espacio en la pantalla;
  • redujo varias veces el volumen total de la aplicación (de ~ 1800 a 360 líneas de código).

Ahora la solución funciona de la siguiente manera.


  1. El script ETL se ejecuta una vez al mes (aquí están las instrucciones sobre cómo hacer esto) y pasa concienzudamente por todas las profesiones, recolectando datos en bruto sobre vacantes y currículums vitae de hh.
    Además, los datos sobre vacantes se toman a través de la API del sitio (pude reutilizar parcialmente el código del proyecto anterior ), pero para cada currículum tuve que analizar páginas web usando el paquete rvest, porque el acceso al método API correspondiente ahora se ha pagado. Puedes adivinar cómo esto afectó la velocidad del guión.
  2. Los datos recopilados se combinan: el proceso se describe en detalle y con ejemplos de código aquí . Los datos procesados ​​se guardan en el disco en archivos separados con el formato hist / profession-hist-vac.csv e hist / profession-hist-res.csv. Por cierto, los valores atípicos en datos como este pueden conducir a cosas divertidas, tenga cuidado :)
    Para cada profesión, el script toma un archivo aumentado con datos históricos, selecciona los más relevantes (aquellos que no tienen más de un mes desde la fecha de la última actualización) y genera nuevos archivos csv del formulario data.res / profession-res-Recent.csv y data.vac / profession -vac-Recent.csv. La aplicación final también funciona con estos datos ...
  3. ... que, después de comenzar, lee el contenido del currículum y las carpetas de trabajo (data.res y data.vac, respectivamente), y luego comprueba cada hora los cambios en los archivos. Hacer esto con reactiveFileReader () es mucho más eficiente en términos de recursos y velocidad de ejecución que usar invalidateLater (). Si hubo cambios en los archivos, las tablas con los datos de origen se actualizan automáticamente y los valores promedio y los gráficos se vuelven a calcular, ya que dependen de valores reactivos (), es decir, no se requiere ningún código adicional para manejar esta situación.
  4. En la página principal, ahora hay una tabla que muestra los valores mínimos, medianos y máximos de las expectativas salariales y las ofertas para cada especialidad para cada uno de los niveles encontrados (todo para los conocimientos tradicionales). Además, puede ver los gráficos en las pestañas con información detallada y cargar los datos en el formato .xlsx (nunca se sabe cuáles serán estos números necesarios para RR. HH.).

Eso es todo. Resulta que el único botón ahora disponible para el usuario en nuestro tablero es el botón Descargar. Y esto es para mejor: cuanto menos botones tenga el usuario, menos posibilidades tendrá lanzar una excepción no controlada confundirse en ellos.


En lugar de un epílogo


Hoy, la aplicación recopila y analiza datos solo para San Petersburgo. Teniendo en cuenta que la principal parte interesada estaba satisfecha, y la reacción más frecuente fue "genial, pero ¿se puede hacer esto a Moscú?", Considero que el experimento fue un éxito.


Puede ver la aplicación en este enlace , y todo el código fuente (junto con ejemplos de archivos terminados) está disponible aquí .


Por cierto, la aplicación se llama Monitor de salario, Salmón abreviado - "salmón".


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


All Articles