企业公司的业务流程:投机与现实。 带R的棚灯

关于“数字孪生”概念日益引起人们关注的业务流程挖掘的简短文章。 由于该主题的周期性出现,我认为分享解决方案的方法是适当的。


问题陈述


情况非常简单。


  • 有公司X(Y,Z,...)。
  • 该公司拥有由各种IT系统自动化的业务流程。
  • 有一些业务分析师为这些流程绘制了bpmn图。 更具体地说,他们自己对这些过程的外观的“ bpmn想法”。
  • 业务用户希望对这些过程进行某种形式的表示(KPI)。

如何得出真相并计算这些指标?


它是以前出版物的延续。


我们根据计算机友好的要求制定任务


基本假设:


  • 有一个纯度,完整性和一致性各异的临时事件日志(IT系统的各种日志,cdr \ xdr,只是数据库中的事件记录)。
  • IT系统充当状态机,并根据用户的行为以及其中的程序员所制定的业务逻辑在不同状态之间“走动”。
  • 用户交互以事务形式进行。

物理世界更正:


  • 对IT系统进行的更改数量如此之大,以至于业务分析师的bpmn图几乎与现实无关。
  • 数据可能是非常非结构化的(例如,应用程序日志)。
  • “交易”是一个逻辑概念。 事件记录本身仅包含此状态中固有的属性,并且没有端到端事务标识符。
  • 每天的记录数为数十,几百,几亿条

集数解决方案


要解决此类问题,有必要:


  • 重建交易
  • 重建真实的业务流程
  • 进行计算;
  • 以人类可读的格式生成结果。

您可以开始寻找供应商解决方案并支付数百万美元。 但是我们手中有R.它可以让我们完美地解决这个问题。 简要考虑如下。


一切似乎都很简单,R具有一组良好的一致bupaR软件包。 但是,药膏中有苍蝇存在,它会毒化一切。 在可接受的时间内设置此时间只能应付少量事件(数十万至数百万)。
对于大批量,必须使用其他方法。


增加速度!


模拟输入数据集


为了演示想法,有必要形成某种测试数据集。 让我们举一个联邦商店的示例作为数学模型的物理来源的例子,幸运的是,这对每个人都是可以理解的。 尽管取得了同样的成功,但它可以是自动柜员机,呼叫中心,公共交通,供水等。


  • 有各种规模的商店(小,中和大)。
  • 在商店中,有收银台(POS终端)。
  • 商店编号可以是字母数字;终端编号可以是数字。
  • 购物者去商店购物时用卡付款。
  • pos终端与卡和银行的交互作用由一组状态以及它们之间的转换规则来描述。
  • 交易成功,失败,延期且不完整(例如,银行不可用)。
  • 事务有超时。

采用以下一组业务交易模式:


"INIT-REQUEST-RESPONSE-SUCCESS" "INIT-REQUEST-RESPONSE-ERROR" "INIT-REQUEST-RESPONSE-DEFFERED" "INIT-REQUEST" "INIT" 

为了演示该方法,我们将创建一个小样本,但是在数十亿条记录上都可以正常工作(对于没有超深度优化的这种卷,在性能非常中等的单台服务器上, 特征时间仅以数百秒为单位进行测量 )。


大批量直接扰流板:


  • 在许多地方, tidyverse意味着tidyverse找不到答案;
  • 甚至优化微步都是有用的,并且可以做出重大贡献。

样本仿真代码
 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() 

为了保证实验的纯洁性,我们只保留重要参数并混合所有内容。 在现实生活中,仍然有必要随机丢弃部分片段(可能在单独的时间块中),从而模拟接收数据时的损失。


 #   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() 


我们用图片说明流程图


对原始`data.frame`的计算


和状态分布
使用`bupaR`进行可视化


轻微的波动是由于这样的事实,即在表的开头考虑了该表(该表已包含在代码中),而在bupaR::process_map的末尾起作用时,一些不符合积分约束的随机生成的数据被过滤元素切断了。


交易重构


当您必须收集/分解/比较时间序列时,通常提供的第一件事是分组和比较周期。 在具有100个条目的演示中,此加法将起作用,但数百万个列表将不起作用。 为了完成此任务,您需要定位时间损失点(内部循环,中间存储器分配和复制),并尝试将其减少到最小。


结果,该问题可以减少到十行。


交易重构码
 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) 

几秒钟后,我们便有了业务流程的重构图。


而且(实际上,谁会想到的!!)事实证明,由于业务分析人员说服了所有人,IT系统中自动化的业务流程的工作方式有所不同(或根本没有)。 “过程所有者”的奇迹和论据将伴随着对最终图景的研究。


积极运用技巧


当计算速度变得很重要时,编写工作代码是不够的。 有必要注意各个层次。 还有许多算法技巧可以显着减少执行时间。


特别是,在此任务中,我们可以提及以下内容:


  1. 对于主要处理,仅data.table (速度,在链接上工作),+负责内部查询优化。
  2. POSIXct可以包含毫秒(尽管无法正常显示,但可以使用options(digits.secs=X)进行更正),我们将其隐藏在此处,这样可以更轻松地进行比较和排序。
  3. 避免在组内进行物理分类! 整个向量的单个物理排序可确保对数据进行分组排序。
  4. 避免在组内进行计算。 我们尝试对源数据进行所有可能的操作(我们应用矢量化,减少函数调用的发票)。
  5. 我们使用事务超时来处理时间间隔。
  6. locf(最后一次进行结转)方法很慢。 要在时间轴上传递属性,请使用cumsumcummax
  7. 费时的操作,例如POSIX->字符串转换,常规搜索等。 我们不是逐个元素地做,而是在卷积上做。 内部索引和转换字段分组的开销无比小。
  8. 我们积极使用多线程(包括内部数据包)。
  9. 不要忽视微优化。 例如, stri_cpaste0paste0

 #  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 

先前的文章- 瑞士json处理刀

Source: https://habr.com/ru/post/zh-CN461463/


All Articles