Сложная проблема с поиском соответствий и распределением сумм в R - PullRequest
0 голосов
/ 20 марта 2020

Маленькая проблема, которая (надеюсь) должна быть решена в R. Это относится к некоторым правилам, чтобы решить, как кредиты без категории распределяются по категории расходов.

Предположим, у меня есть этот набор данных (в качестве простой иллюстрации ):

df <- data.frame(id      = c(1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5),
                 counter = c(1, 2, 3, 4, 1, 2, 3, 4, 5, 1, 2, 3, 1, 2, 3, 4, 1, 2, 3),
                 type    = c("A", "B", "C", "Z", "B", "A", "C", "Z", "B", "A", "B", "Z", "A", "Z", "Z", "B", "A", "B", "B"),
                 amount  = c(100, 200, 300, -100, 300, 200, 400, -300, 500, 100, 200, -250, 200, -200, -50, 100, 100, 200, 100),
                 type_2  = rep(NA, 19))

df$type   <- as.character(df$type)
df$type_2 <- as.character(df$type_2)

Транзакции типа "Z" являются кредитами без категории. Мне нужно распределить сумму между типами "A", "B" и "C" в соответствии с некоторыми конкретными правилами.

Сначала мне нужно проверить каждую транзакцию типа "Z", чтобы выяснить, могу ли я ее спарить с другой транзакцией (где сумма равна и противоположна) с тем же id.

Затем я бы сгруппировал данные по id и type и суммировал бы переменную amount - потому что я ' Я больше не заинтересован в отдельных записях для заданных id и type - я просто хочу посмотреть общую сумму. Это должно быть легко с dplyr.

Затем существует второй этап для любого id, где любая транзакция типа "Z" не была спарена (ie точное совпадение не было найдено в первый этап).

Суммы для каждой непарной "Z" транзакции необходимо разделить и распределить по типам "A", "B" и "C" в указанном порядке. Например, для id = 3 у меня есть:

  • непарный type = "Z", amount = -250

  • type = "A", amount = 100

  • type = "B", amount = 200

  • type = "C": отсутствует

и я хотел бы распределить сумму для транзакции Z на:

  • type = "ZA", amount = -100
  • type = "ZB", amount = -150

, а затем удалите исходную запись type = "Z", amount = -250 (чтобы избежать двойного счета amount)

Обновлено: я думаю, что я написал некоторый код, чтобы сделать то, что мне нужно, но это ужасно!

df <- data.frame(id      = c(1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5),
                 counter = c(1, 2, 3, 4, 1, 2, 3, 4, 5, 1, 2, 3, 1, 2, 3, 4, 1, 2, 3),
                 type    = c("A", "B", "C", "Z", "B", "A", "C", "Z", "B", "A", "B", "Z", "A", "Z", "Z", "B", "A", "B", "B"),
                 amount  = c(100, 200, 300, -100, 300, 200, 400, -300, 300, 100, 200, -250, 200, -200, -50, 100, 100, 200, 100),
                 type_2  = rep(NA, 19))

df$type     <- as.character(df$type) # factor by default

df$type_2   <- df$type # to avoid overwriting data
df$amount_2 <- df$amount # ditto

# list of ids

id_list <- df %>% group_by(id) %>% summarise() %>% ungroup() %>% pull(id)

# for each of these...

for (my_id in id_list)
{

  # get list of counters for this id

  counter_list <- df %>% filter(id == my_id) %>% pull(counter)

  # now loop through counters for that ID

  for (my_counter in counter_list)
  {

    # is it type Z?

    if(df$type[df$id == my_id & df$counter == my_counter] == "Z")
    {
      # yes - so search for an exact match

      matched_flag <- FALSE

      for (my_counter_2 in counter_list)
      {

        # if the amounts are equal and opposite *AND* we're matching with type A/B/C *AND* we haven't found a match previously

        if(df$amount[df$id == my_id & df$counter == my_counter_2] == -df$amount[df$id == my_id & df$counter == my_counter] &
           df$type  [df$id == my_id & df$counter == my_counter_2] %in% c("A", "B", "C") & # only offset records of type A, B and C (check shouldn't be necessary since all type Zs should be negative, but...)
           matched_flag == FALSE)

        {
          # found a match, so set type_2 for the Z record to be A/B/C as appropriate

          df$type_2[df$id == my_id & df$counter == my_counter] <- df$type[df$id == my_id & df$counter == my_counter_2]

          # then do the offsetting calculation (set both amounts to zero - not strictly necessary since the grouping at the next stage will cancel them out)

          df$amount_2[df$id == my_id & df$counter == my_counter]   <- 0
          df$amount_2[df$id == my_id & df$counter == my_counter_2] <- 0

          matched_flag <- TRUE # so that we don't keep looking for multiple matches - we don't want that

        }

      }

    }

  }

}

# reorder a bit

df <- df %>% select(id, counter, type, amount, type_2, amount_2)

df %>% summarise(amount = sum(amount), amount_2 = sum(amount_2))

# now we can group by type_2 - and add a fresh counter

df_grouped <- df %>% 
  group_by(id, type_2) %>% 
  summarise(amount = sum(amount_2)) %>% # note amount vs amount_2 - this is deliberate
  ungroup() %>%
  group_by(id) %>%
  mutate(counter = row_number()) %>%
  select(id, counter, everything()) %>%
  arrange(id, type_2) %>%
  rename(type = type_2) %>%
  ungroup()

df_grouped$type_2   <- df_grouped$type  # just to avoid overwriting original data
df_grouped$amount_2 <- df_grouped$amount # ditto

# loop through ids (same list as first stage)

for (my_id in id_list)
{

  A_amount <- df_grouped %>% filter(id == my_id & type == "A") %>% summarise(amount = sum(amount)) %>% pull(amount) # returns 0 if no match is found, which is fine
  B_amount <- df_grouped %>% filter(id == my_id & type == "B") %>% summarise(amount = sum(amount)) %>% pull(amount)
  C_amount <- df_grouped %>% filter(id == my_id & type == "C") %>% summarise(amount = sum(amount)) %>% pull(amount)
  Z_amount <- df_grouped %>% filter(id == my_id & type == "Z") %>% summarise(amount = sum(amount)) %>% pull(amount)

  # take the amount in ZZ and apportion as A, B, C in turn (until the money runs out)

  if(Z_amount != 0) # occasional zero recoveries appear and we can leave those alone
  {

    A_offset <- pmin(A_amount, -Z_amount)
    A_amount <- A_amount - A_offset
    Z_amount <- Z_amount + A_offset

    B_offset <- pmin(B_amount, -Z_amount)
    B_amount <- B_amount - B_offset
    Z_amount <- Z_amount + B_offset

    C_offset <- pmin(C_amount, -Z_amount)
    C_amount <- C_amount - C_offset
    Z_amount <- Z_amount + C_offset

    # write back the relevant info to the dataframe

    df_grouped$amount_2[df_grouped$id == my_id & df_grouped$type == "A"] <- A_amount
    df_grouped$amount_2[df_grouped$id == my_id & df_grouped$type == "B"] <- B_amount
    df_grouped$amount_2[df_grouped$id == my_id & df_grouped$type == "C"] <- C_amount
    df_grouped$amount_2[df_grouped$id == my_id & df_grouped$type == "Z"] <- Z_amount

  }

}

# sort again

df_grouped <- df_grouped %>% arrange(id, counter)

Может кто-нибудь предложить более краткий путь вперед, пожалуйста? Любые решения на dplyr приветствуются (я достаточно знаком с этим). Скорость выполнения не должна быть большой проблемой, потому что для моих «настоящих» данных сумма довольно скромная.

Спасибо.

...