Breve artículo sobre minería de procesos de negocio en el contexto de creciente interés en el concepto de "gemelo digital". Debido a la aparición periódica de este tema, considero apropiado compartir enfoques para la solución.
Declaración del problema.
La situación es extremadamente simple.
- Hay una empresa X (Y, Z, ...).
- La empresa cuenta con procesos comerciales automatizados por diversos sistemas de TI.
- Hay analistas de negocios que han dibujado diagramas bpmn para estos procesos. Más específicamente, su propia "idea bpmn" de cómo deberían haberse visto estos procesos.
- Los usuarios comerciales desean tener algún tipo de representación (KPI) de estos procesos.
¿Cómo llegar a la verdad y contar estas métricas?
Es una continuación de publicaciones anteriores .
Postulados básicos:
- Hay un registro de eventos temporal (varios registros de sistemas de TI, cdr \ xdr, solo registros de eventos en la base de datos) de diversos grados de pureza, integridad y consistencia.
- Los sistemas de TI actúan como una máquina de estados y "caminan" entre diferentes estados de acuerdo con las acciones de los usuarios y la lógica de negocios establecida por los programadores en ellos.
- La interacción del usuario se lleva a cabo en forma transaccional.
Correcciones del mundo físico:
- El número de cambios realizados en el sistema de TI es tal que los diagramas bpmn de los analistas de negocios no tienen casi nada que ver con la realidad.
- Los datos pueden ser muy desestructurados (por ejemplo, registros de aplicaciones).
- "Transaccional" es un concepto lógico. Los registros de eventos en sí contienen solo atributos inherentes a este estado y no hay un identificador de transacción de extremo a extremo.
- El número de registros por día es de decenas, cientos, miles de millones de piezas .
Solución Set-Count
Para resolver tales problemas es necesario:
- Reconstruir transacciones
- Reconstruir procesos comerciales reales
- hacer cálculos
- generar resultados en formato legible para humanos.
Puede comenzar a buscar soluciones de proveedores y pagar millones. Pero tenemos R. en nuestras manos, nos permite resolver perfectamente este problema. Breves consideraciones a continuación.
Todo parece simple y R tiene un buen conjunto consistente de paquetes bupaR . Pero una mosca en la pomada está presente y envenena todo. Este conjunto en un tiempo aceptable solo puede hacer frente a una pequeña cantidad de eventos (cientos de miles - varios millones).
Para grandes volúmenes, se deben utilizar otros enfoques.
Añadir velocidad!
Emular un conjunto de datos de entrada
Para demostrar ideas, es necesario formar algún tipo de conjunto de datos de prueba. Tomemos un ejemplo de una cadena de tiendas federales como fuente física para un modelo matemático. Afortunadamente, esto es comprensible para todos. Aunque con el mismo éxito, pueden ser cajeros automáticos, centros de llamadas, transporte público, suministro de agua, etc.
- Hay tiendas de varios tamaños (pequeño, mediano y grande).
- En las tiendas hay mostradores de efectivo (terminales pos).
- Los números de tienda pueden ser alfanuméricos; los números de terminal pueden ser digitales.
- Los compradores van a las tiendas y compran algo mientras pagan con una tarjeta.
- La interacción de la terminal pos con la tarjeta y el banco se describe mediante un cierto conjunto de estados y las reglas para la transición entre ellos.
- Las transacciones son exitosas, fallidas, diferidas e incompletas (el banco no está disponible, por ejemplo).
- Las transacciones tienen tiempos de espera.
Tome el siguiente conjunto de patrones de transacciones comerciales:
"INIT-REQUEST-RESPONSE-SUCCESS" "INIT-REQUEST-RESPONSE-ERROR" "INIT-REQUEST-RESPONSE-DEFFERED" "INIT-REQUEST" "INIT"
Para demostrar el enfoque, crearemos una pequeña muestra, pero todo funciona bien en miles de millones de registros (para tal volumen sin una optimización profunda, el tiempo característico se mide en solo cientos de segundos en un solo servidor de rendimiento muy mediocre).
Spoilers directos para grandes volúmenes:
- en muchos lugares,
tidyverse
significa tidyverse
no puede obtener una respuesta; - optimizar incluso microsteps es útil y puede hacer una contribución significativa.
Código de simulación de muestra library(tidyverse) library(datapasta) library(tictoc) library(data.table) library(stringi) library(anytime) library(rTRNG) data.table::setDTthreads(0) # data.table data.table::getDTthreads() # set.seed(46572) RcppParallel::setThreadOptions(numThreads = parallel::detectCores() - 1) # -- -, # 5 -, 2 -- bo_pattern <- tibble::tribble( # , , ~pattern, ~prob, ~mean_duration, "INIT-REQUEST-RESPONSE-SUCCESS", 0.7, 5, "INIT-REQUEST-RESPONSE-ERROR", 0.15, 5, "INIT-REQUEST-RESPONSE-DEFFERED", 0.07, 8, "INIT-REQUEST", 0.05, 2, "INIT", 0.03, 0.5 ) # + checkmate::assertTRUE(sum(bo_pattern$prob) == 1) df <- bo_pattern %>% separate_rows(pattern) %>% # mutate(coeff = sum(prob)) %>% group_by(pattern) %>% # summarise(event_prob = sum(prob/coeff)*100) %>% ungroup() checkmate::assertTRUE(sum(df$event_prob) == 100) # 3 : (4 ), (12 ), (30 ) df1 <- tribble( ~type, ~n_pos, ~n_store, "small", 4, 10, "medium", 12, 5, "large", 30, 2 ) %>% # mutate(store = map2(row_number(), n_store, ~sample(x = .x * 1000 + 1:.y, size = .y, replace = FALSE))) %>% unnest(store) %>% # mutate(pos = map(n_pos, ~sample(x = .x, size = .x, replace = FALSE))) %>% unnest(pos) %>% mutate(pattern = sample(bo_pattern$pattern, n(), replace = TRUE, prob = bo_pattern$prob)) tic("Generate transactions") # , # , df2 <- df1 %>% # select(-matches("duration")) %>% left_join(bo_pattern, by = "pattern") %>% # sample_frac(size = 200, replace = TRUE) %>% mutate(duration = rnorm(n(), mean = mean_duration, sd = mean_duration * .25)) %>% select(-prob, -mean_duration) %>% # , > # 30 filter(duration > 0.5 & duration < 30) %>% # POS mutate(session_id = row_number()) %>% # , separate_rows(pattern) %>% rename(event = pattern) toc() tic("Generate time markers, data.table way") samples_tbl <- data.table::as.data.table(df2) %>% # setkey(session_id, duration, physical = FALSE) %>% # # 1- , , 5 # .[, ticks := base::sort(runif(.N, 5, 5 + duration)), by = .(session_id, duration)] %>% # match.arg base::order!! # # 0 1 # # .[, tshift := runif(.N, 0, 1)] %>% # trng ( ) # , .[, trand := runif_trng(.N, 0, 1, parallelGrain = 100L) * duration] %>% # , # .[, ticks := sort(tshift), by = .(session_id)] %>% # , session_id, , .[, t_idx := session_id + trand / max(trand)/10] %>% # # session_id . .[, tshift := (sort(t_idx) - session_id) * 10 * max(trand)] %>% # , POS (60 ) .[event == "INIT", tshift := tshift + runif_trng(.N, 0, 60, parallelGrain = 100L)] %>% # .[, `:=`(duration = NULL, trand = NULL, t_idx = NULL, n_store = NULL, n_pos = NULL, timestamp = as.numeric(anytime("2019-03-11 08:00:00 MSK")))] %>% # , 01.03.2019 .[, timestamp := timestamp + cumsum(tshift), by = .(store, pos)] %>% # .[timestamp <= as.numeric(anytime("2019-04-11 23:00:00 MSK")), ] %>% # .[, timestamp := anytime(timestamp, tz = "Europe/Moscow")] %>% as_tibble() %>% select(store, pos, event, timestamp, session_id) toc()
Para la pureza del experimento, dejamos solo los parámetros significativos y mezclamos todo. En la vida real, todavía es necesario tirar al azar parte de los fragmentos (posiblemente en bloques de tiempo separados), emulando así las pérdidas en la recepción de datos.
# log_tbl <- samples_tbl %>% select(store, pos, state = event, timestamp_msk = timestamp) %>% sample_n(n()) # log_tbl %>% mutate(timegroup = lubridate::ceiling_date(timestamp_msk, unit = "10 mins")) %>% ggplot(aes(timegroup)) + # geom_bar(width = 0.7*600) + geom_bar(colour = "white", size = 1.3) + theme_bw()

Ilustramos el diagrama del proceso con una imagen.

y distribución estatal

Las ligeras fluctuaciones se deben al hecho de que la tabla se considera al principio (está incluida en el código), y bupaR::process_map
funcionó al final cuando algunos de los datos generados aleatoriamente que no se ajustaban a las restricciones integrales se cortaron mediante elementos filtrantes.
Reconstrucción de transacciones
Lo primero que generalmente se ofrece cuando tiene que recopilar / desensamblar / comparar series temporales es agrupaciones y ciclos de comparación. En demostraciones con 100 entradas, esta caminata funcionará, pero millones de listas no. Para hacer frente a esta tarea, debe localizar los puntos de pérdida de tiempo (bucles internos, asignaciones de memoria intermedias y copia) e intentar eliminarlos al mínimo.
Como resultado, este problema puede reducirse a diez líneas.
código de reconstrucción de transacciones clean_dt <- as.data.table(log_tbl) %>% # INIT .[, start := (state == "INIT")] %>% # session_id , # .[, event_date := lubridate::as_date(timestamp_msk)] %>% .[, date_str := format(.BY[[1]], "%y%m%d"), by = event_date] %>% # # timestamp_msk setorder(store, pos, timestamp_msk) %>% # -- .[, session_id := paste(date_str, store, pos, cumsum(start), sep = "_")] %>% # ( 30 ) # .[, time_shift := timestamp_msk - shift(timestamp_msk), by = .(store, pos)] %>% # , INIT .[, time_locf := cummax(as.numeric(timestamp_msk) * as.numeric(start)), by = .(store, pos)] %>% .[, time_shift := as.numeric(timestamp_msk) - time_locf] %>% # , 30 .[, lost_chain := time_shift > 30] %>% # .[, time_shift := as.numeric(!start) * as.numeric(timestamp_msk - shift(timestamp_msk, fill = 0))] %>% # INIT # .[, time_accu := cumsum(time_shift)] %>% .[, date_str := NULL] # # tidyverse , dt <- as.data.table(clean_dt) %>% # !!! .[lost_chain != TRUE] %>% # 1- .[order(timestamp_msk, store, pos)] %>% .[, bp_pattern := stri_join(state, collapse = "-"), by = session_id] # as_tibble(dt) %>% distinct(session_id, bp_pattern) %>% count(session_id, sort = TRUE)
En unos segundos, tenemos una imagen reconstruida de los procesos comerciales.
Y (¡quién lo hubiera pensado!) De hecho, resulta que los procesos comerciales automatizados en los sistemas de TI funcionan de manera algo diferente (o nada) ya que los analistas comerciales convencieron a todos. Las maravillas y los argumentos de los "propietarios del proceso" acompañarán el estudio de la imagen final.
Aplicar trucos activamente
Cuando la velocidad informática se convierte en una cantidad importante, escribir un código de trabajo no es suficiente. Es necesario prestar atención a todos los niveles. También hay una serie de trucos algorítmicos que pueden reducir significativamente el tiempo de ejecución.
En particular, en esta tarea podemos mencionar lo siguiente:
- Para el procesamiento principal, solo
data.table
(velocidad, trabajo en enlaces), + contabilidad para la optimización de consultas internas. POSIXct
puede contener milisegundos (aunque no se muestra normalmente, pero puede corregirse usando options(digits.secs=X)
), los options(digits.secs=X)
allí, será más fácil compararlos y ordenarlos.- ¡Evita la clasificación física dentro de los grupos! Una sola ordenación física de todo el vector asegura la ordenación de los datos en grupos.
- Evite la computación dentro de los grupos. Intentamos hacer todo lo posible con los datos de origen (aplicamos vectorización, reducimos las facturas para las llamadas a funciones).
- Utilizamos un tiempo de espera de transacción para lidiar con brechas de tiempo.
- Los métodos locf (última observación llevada adelante) son lentos. Para transferir propiedades en una línea de tiempo, use
cumsum
, cummax
. - Operaciones que requieren mucho tiempo, como POSIX -> conversión de cadenas, búsqueda regular, etc. No lo hacemos elemento por elemento, sino por convoluciones. Los gastos generales en la indexación interna y la agrupación del campo convertido son incomparablemente más pequeños.
- Utilizamos activamente subprocesos múltiples (incluido intrapaquete).
- No descuides la microoptimización. Por ejemplo,
stri_c
es varias veces más rápido que paste0
.
# 1 log <- getLog(fileName) bench::mark( paste0 = paste0(log$value, collapse = "\n"), stringi = stri_c(log$value, collapse = "\n") ) # # A tibble: 2 x 13 # expression min median `itr/sec` mem_alloc `gc/sec` n_itr n_gc total_time # <bch:expr> <bch:> <bch:> <dbl> <bch:byt> <dbl> <int> <dbl> <bch:tm> # 1 paste0 58ms 59.1ms 16.9 496KB 0 9 0 533ms # 2 stringi 16.9ms 17.5ms 57.1 0B 0 29 0 508ms
Publicación anterior - Navaja suiza json .