R, dplyr: функция, которая быстро строит список дополнительных строк на основе условий - PullRequest
6 голосов
/ 08 апреля 2019

У меня есть набор данных ~ 80 000 строк по 26 столбцам. Строки соответствуют "SKU" или уникальным идентификаторам для наборов для сборки роботов. Столбцы соответствуют 26 различным частям робота. Ячейка содержит вклад части в создание целого робота. Сумма пропорции строки может не составлять 1,0, поскольку в строительном наборе не всегда будет 100% частей, необходимых для сборки всего робота.

Основная цель - создать функцию, которая принимает SKU в качестве входных данных и выводит список дополнительных SKU. Дополнительная строка определяется как:

  1. если данная строка имеет ненулевое значение для столбца, то дополнение должно иметь нулевое значение для этого столбца.

Цель состоит в том, чтобы найти все возможные наборы SKU, которые дополняют данный SKU, так что можно построить целый робот. Кроме того, важно увидеть взвешенный доход на одного робота ("weightedPrice") для этого набора SKU "Франкенштейн". Также приятно показать, как меняется weightedPrice с добавлением каждого дополнительного SKU.

Минимальный рабочий, игрушечный пример (MWE):

set.seed(1)
a = runif(n=60, min=0, max=0.2)
a[a<0.12] = 0
n = 10
A = as.data.frame(matrix(a,              
                         nrow=n,         
                         ncol=6,         
                         byrow = TRUE))
A$rowTally <- rowSums(A != 0)
A$sku <- seq(from = 1, to = n)
A$totalDollarSales <- runif(n=n, min=1*10^2, max=1*10^6)
A$totalUnitSales <- runif(n=n, min=1*10^2, max=1*10^6)
names(A) <- c("p1_prop", "p2_prop", "p3_prop", "p4_prop", "p5_prop", "p6_prop", "rowTally", "sku", "totalDollarSales", "totalUnitSales")
A <- A[c("sku", "p1_prop", "p2_prop", "p3_prop", "p4_prop", "p5_prop", "p6_prop", "rowTally", "totalDollarSales", "totalUnitSales")]
A$dollarsPerRobot <- A$totalDollarSales/A$totalUnitSales
A

   sku   p1_prop   p2_prop   p3_prop   p4_prop   p5_prop   p6_prop rowTally
1    1 0         0         0         0.1816416 0         0.1796779        2
2    2 0.1889351 0.1321596 0.1258228 0         0         0                3
3    3 0.1374046 0         0.1539683 0         0.1435237 0.1983812        4
4    4 0         0.1554890 0.1869410 0         0.1303348 0                3
5    5 0         0         0         0         0.1739382 0                1
6    6 0         0         0         0         0.1654747 0.1336933        2
7    7 0.1588480 0         0.1447422 0         0.1641893 0.1294120        4
8    8 0.1565866 0         0         0.1578712 0         0                2
9    9 0.1464627 0.1385463 0         0.1722419 0         0                3
10  10 0         0         0         0         0.1324010 0                1
   totalDollarSales totalUnitSales dollarsPerRobot
1         912884.64       339139.0       2.6917711
2         293674.01       839456.4       0.3498383
3         459119.82       346748.8       1.3240703
4         332461.43       333841.6       0.9958659
5         650905.38       476403.6       1.3662898
6         258090.98       892209.1       0.2892718
7         478597.39       864353.0       0.5537059
8         766334.04       390050.5       1.9647044
9          84338.49       777343.0       0.1084959
10        875333.80       960621.9       0.9112157

Я пытаюсь написать функцию:

def frankensteinRobot(df, sku, skuRowTally):
    1. find another SKU in dataframe, df.
       - must have non-overlapping parts with existing SKU set
       - rowTally <= skuRowTally (want to find small SKUs to add)
       - must be relatively same number of totalUnitSales
    2. append new SKU to list, and take mininum of totalUnitSales. 
    3. Calculate the weighted, per robot price
       dollarsPerRobotSKU_1*(1/length(SKU_list))+...+dollarsPerRobotSKU_n*(1/length(SKU_list)) 
       and append to the end of a list so we can track profitability with each additional SKU.
    4. repeat steps 1, 2 & 3.

Я только смог выяснить, как найти следующий дополнительный SKU, но не полный набор SKU:

A_candidates <- sapply(data.frame(outer(1:nrow(A), 1:nrow(A), Vectorize(check_compliment))), which)

Пример ввода:

frankensteinRobot(df = A, sku = 5, skuRowTally = 3)

Пример вывода (обратите внимание, что, поскольку в моем MWE всего 10 строк, в списках вывода примера есть только 2 элемента, но на самом деле они будут длиннее. Кроме того, я не уверен в том, какая структура данных подходит. Может быть, кадр данных где 1 столбец - это список?):

[list of SKUs]; [propSum]; [maxLb]; [list of weightedPrice]

[5, 2]; [propSum=0.6208557]; [maxLb=476403.6]; [0.8580641)
[5, 8]; [propSum=0.488396]; [maxLb=390050.5]; [1.665497]
[5, 9]; [propSum=0.6311891]; [maxLb=476403.6]; [0.7373929]

Пример ввода:

frankensteinRobot(df = A, sku = 6, skuRowTally = 2)

Пример вывода:

[6, 8]; [propSum=0.6136258]; [maxLb=390050.5]; [1.126988]

Ответы [ 6 ]

3 голосов
/ 13 апреля 2019

Окончательное редактирование : Это решение основано на data.table и может использовать цикл в конце.Тем не менее, вы можете продолжать копировать и вставлять, чтобы это работало.Решение основано главным образом на этом невероятно быстром решении:

  search_dt <- dt[sku1 == searchSKU]
  current_parts <- names(search_dt[, .SD, .SDcols = part_names])[which(search_dt[, .SD, .SDcols = part_names]>0)]

  steal_dt <- dt[rowTally <= searchRowTally]

#returns SKUs which are 0 for the parts we already have
  steal_dt <- steal_dt[steal_dt[, j = rowSums(.SD) == 0, .SDcols = current_parts]]

  franken_rob <- cbind(search_dt, steal_dt)

Я все еще не уверен в некоторых критериях.Я предполагаю, что, поскольку FrankenBot собран, каждый последующий SKU не может быть в предыдущих частях.Другими словами, SKU3 не имеет ничего общего с SKU1 и SKU2.

Вывод решения [Я отредактировал его еще раз ...] {и еще один ...}:

# A tibble: 15 x 8
    sku1  sku2  sku3 propSums Parts Robots dollarsPerRobot totalUnitSales
   <int> <int> <int>    <dbl> <dbl>  <dbl>           <dbl>          <dbl>
 1     1     2     5    0.982     6      3           1.47         339139.
 2     1     2    10    0.941     6      3           1.32         339139.
 3     1     4    NA    0.834     5      2           1.84         333842.
 4     1     5    NA    0.535     3      2           2.03         339139.
 5     1    10    NA    0.494     3      2           1.80         339139.
 6     2     5    NA    0.621     4      2           0.858        476404.
 7     2     6    NA    0.746     5      2           0.320        839456.
 8     2    10    NA    0.579     4      2           0.631        839456.
 9     4     8    NA    0.787     5      2           1.48         333842.
10     5     8    NA    0.488     3      2           1.67         390051.
11     5     9    NA    0.631     4      2           0.737        476404.
12     6     8    NA    0.614     4      2           1.13         390051.
13     6     9    NA    0.756     5      2           0.199        777343.
14     8    10    NA    0.447     3      2           1.44         390051.
15     9    10    NA    0.590     4      2           0.510        777343.

код решения:

library(data.table)
# generate data -----------------------------------------------------------

set.seed(1)
n = 10
cols = 6 #added
part_names =  paste0('p', c(1:cols), '_prop')

a = runif(n* cols, min=0, max=0.2)
a[a<0.12] = 0

A <- data.table(matrix(a, nrow=n, ncol=cols,byrow = TRUE))
A[, `:=`(rowTally1 = rowSums(.SD != 0),
         sku1 = .I
         ,totalDollarSales1 = runif(n=n, min=1*10^2, max=1*10^6)
         ,totalUnitSales1 =  runif(n=n, min=1*10^2, max=1*10^6))]

A[, dollarsPerRobot1:=totalDollarSales1/totalUnitSales1]

setnames(A, c(paste0('V',1:cols)), part_names)
setcolorder(A, 'sku1')

non_part_names<- setdiff(names(A), c('sku1',part_names))
non_part_names<- stringr::str_sub(non_part_names, 1, -2)

search_fun <- function (search_dt, steal_dt, searchSKU, b_loop = FALSE, sale_range = NULL) {

  sku_count<- length(grep('sku', names(search_dt)))
  skus <- paste0('sku', 1:(sku_count+1))

  non_parts<- paste0(non_part_names, rep(1:(sku_count+1), each = length(non_part_names)))

  blank_table <- setnames(data.table(matrix(nrow = 0, ncol = length(search_dt) + 1 + length(non_part_names))),c(skus,part_names, non_parts))

  if (length(searchSKU) != sku_count) {
    stop('not enough SKUs to go around')
  } 

  for (i in 1:length(searchSKU)) {
    search_dt <- search_dt[get(paste0('sku', i)) == searchSKU[i]]
  }
  current_parts <- names(search_dt[, .SD, .SDcols = part_names])[which(search_dt[, .SD, .SDcols = part_names]>0)]
  search_dt[, (setdiff(part_names, current_parts)) := NULL, ]

  # Could be made faster if sku1s were filtered out to whichever ones were is sku.N 
  # Right now it still looks through skus that may have already been filtered out.

  if (!is.null(sale_range)) {
    if (length(sale_range) != 2) {
      warning('Sale range needs to be length two with sale_range[1] = lower range and sale_range[2] = upper range')
    } else {
    steal_dt <- steal_dt[between(totalUnitSales1, sale_range[1] * search_dt$totalUnitSales1, search_dt$totalUnitSales1 * sale_range[2])]
    }
  }


  if (b_loop) {
    steal_dt <- steal_dt[sku1 > searchSKU[sku_count]]
  }

  steal_dt <- steal_dt[steal_dt[, j = rowSums(.SD) == 0, .SDcols = current_parts]]
  if (nrow(steal_dt) == 0) {
    return(blank_table)
  }

  steal_dt[, (current_parts) := NULL]
  setnames(steal_dt,
           c('sku1', paste0(non_part_names, '1')) ,
           c(paste0('sku',sku_count+1),
             paste0(non_part_names, sku_count+1))
  )

  franken_rob <- cbind(search_dt, steal_dt)
  setcolorder(franken_rob, c(skus, part_names))
  return(franken_rob)

}

searchRowTally <- 3
dt_search <- A

#this is done outside the function because there can be a lot of looping otherwise
dt_steal <- dt_search[rowTally1 <= searchRowTally]

#Near-instant with 80,000 rows and 26 columns
search_fun(dt_search, dt_steal, dt_search$sku1[5])
search_fun(dt_search, dt_steal, dt_search$sku1[5], b_loop = TRUE)
search_fun(dt_search, dt_steal, dt_search$sku1[5], sale_range = c(0.8, 1.2))
search_fun(dt_search, dt_steal, dt_search$sku1[5], b_loop = TRUE, sale_range = c(0.8, 1.2))

#Not doable with 80,000 rows, but still nice
rbindlist(lapply(1:(n-1), function (i) search_fun(dt_search, dt_steal, dt_search$sku1[i], b_loop = TRUE)))
rbindlist(lapply(1:(n-1), function (i) search_fun(dt_search, dt_steal, dt_search$sku1[i], b_loop = TRUE, sale_range = c(0.8, 1.2))))

#much more likely that the first regression would be a single value
# frank_1 <- search_fun(dt_search, dt_steal, dt_search$sku1[5], FALSE)
frank_1 <-  rbindlist(lapply(1:(n-1), function (i) search_fun(dt_search, dt_steal, dt_search$sku1[i],  TRUE)))

#This takes every n-1 of each sku1 group. 
frank_2 <- frank_1[frank_1[, head(.I, -1), by = sku1]$V1]
# frank_2 <- frank_1[, j = if(.N!=1) .SD, by = sku1]
dt_steal2 <- dt_steal[sku1 %in% base::unique(frank_1$sku2)]

frank_2 = rbindlist(lapply(1:nrow(frank_2), function (i) search_fun(frank_2, dt_steal2, melt(frank_2[i, .SD, .SDcols = grep('sku', names(frank_2))])[[2]],  TRUE)))

frank_3 <- frank_2[frank_2[, head(.I, -1), by = sku2]$V1]
dt_steal3 <- dt_steal2[sku1 %in% base::unique(frank_2$sku3)]

frank_3 = rbindlist(lapply(1:nrow(frank_3), function (i) search_fun(frank_3, dt_steal3, melt(frank_3[i, .SD, .SDcols = grep('sku', names(frank_3))])[[2]],  TRUE)))


# start combindine our lists

franken_rob <- frank_1[!frank_2, on = c('sku1', 'sku2')]
franken_rob[, j= sku3:= integer()]
setcolorder(franken_rob, c('sku1','sku2','sku3'))

franken_rob <- rbind(frank_2, franken_rob, fill = TRUE)
#do above for frank_n times)

franken_rob[, `:=`(propSums=rowSums(.SD),
                   Parts = rowSums(.SD > 0))
            , .SDcols = part_names]

franken_rob[, Robots:= rowSums(.SD > 0, na.rm = TRUE), .SDcols = grep('sku', names(franken_rob))]
franken_rob[, dollarsPerRobot := rowSums(.SD, na.rm = TRUE) / Robots, .SDcols = grep ('dollarsPerRobot', names(franken_rob))]
franken_rob[, totalUnitSales := do.call(pmin,  c(.SD, list(na.rm = TRUE))), .SDcols = grep('totalUnitSales', names(franken_rob))]

franken_rob[, (part_names) := NULL]
franken_rob

tibble::as_tibble(franken_rob[, c(1:3, 16, 17, 18, 19,20)])

Редактировать : мне не хватает представителя, чтобы комментировать - при попытке решения data.table с 80 000 строк и 26 столбцов он пытается выделить вектор размером 2,3 ГБ, когда rowTally <= 13.Однако, когда я изменяю это значение на 3, получается 1,1 миллиона строк и фильтруется до 0,3 миллиона строк.Это супер декартово

Оригинал : вот решение dplyr, которое, кажется, работает с 80000 строк и 26 столбцов.Хитрость заключалась в том, чтобы выяснить, какие столбцы имели ненулевой результат для подмножества sku.С этими столбцами я вернулся к исходному df и отфильтровал.

Есть также строка, закомментированная для UnitSales, находящейся в некотором диапазоне.


set.seed(1)
n = 10
cols = 6 #added

part_names =  paste0('p', c(1:cols), '_prop') #added
a = runif(n * cols, min=0, max=0.2) #changed from n to n * cols
a[a<0.12] = 0

A = as.data.frame(matrix(a,              
                         nrow=n,         
                         ncol=cols,  #changed to cols      
                         byrow = TRUE))
A$rowTally <- rowSums(A != 0)
A$sku <- seq(from = 1, to = n)
A$totalDollarSales <- runif(n=n, min=1*10^2, max=1*10^6)
A$totalUnitSales <- runif(n=n, min=1*10^2, max=1*10^6)
names(A) <- c(part_names, "rowTally", "sku", "totalDollarSales", "totalUnitSales")
A <- A[c("sku", part_names, "rowTally", "totalDollarSales", "totalUnitSales")]
A$dollarsPerRobot <- A$totalDollarSales/A$totalUnitSales


library(dplyr)

df <- as_tibble(A)%>%
  mutate(propSum = rowSums(.[, part_names]))

search_sku <- 5
skuRowTally <- 3

search_df <- df%>%
  filter(sku == search_sku)

current_parts <- search_df%>%
  select(part_names)%>%
  select_if(~sum(.)> 0)%>%
  names()

non_current_parts <- search_df%>%
  select(part_names)%>%
  select_if(~sum(.) == 0)%>%
  names()

df%>%
  filter(rowTally <= skuRowTally,
         sku != search_sku
         # , between(totalUnitSales, 0.7 * search_df$totalUnitSales, 1.3 * search_df$totalUnitSales)
         )%>%
  filter_at(vars(current_parts), all_vars(. == 0))%>%
  filter_at(vars(non_current_parts), any_vars(. != 0))%>%
  rowwise()%>%
  transmute(sku_search = search_sku,
            sku = sku,
            propSum = propSum + search_df$propSum,
            minLB = min(totalUnitSales, search_df$totalUnitSales),
            weightedPrice = (dollarsPerRobot + search_df$dollarsPerRobot) / 2,
            total_parts = rowTally + search_df$rowTally,
            complete_robot = if_else(total_parts == cols, 'COMPLETE', 'incomplete')
  )%>%
  ungroup()


frankensteinRobot <- function (df, sku1, skuTally) {
  # df <- as_tibble(df)%>%
  #   mutate(propSum = rowSums(.[, part_names]))


#part_name and cols would also need to be passed to make this
#completely stand alone.  

  search_sku <- sku1
  skuRowTally <- skuTally

  search_df <- df%>%
    filter(sku == search_sku)

  current_parts <- search_df%>%
    select(part_names)%>%
    select_if(~sum(.)> 0)%>%
    names()

  non_current_parts <- search_df%>%
    select(part_names)%>%
    select_if(~sum(.) == 0)%>%
    names()

  df%>%
    filter(rowTally <= skuRowTally,
           sku > search_sku
           # , between(totalUnitSales, 0.7 * search_df$totalUnitSales, 1.3 * search_df$totalUnitSales)
    )%>%
    filter_at(vars(current_parts), all_vars(. == 0))%>%
    filter_at(vars(non_current_parts), any_vars(. != 0))%>%
    rowwise()%>%
    transmute(sku_search = search_sku,
              sku = sku,
              propSum = propSum + search_df$propSum,
              minLB = min(totalUnitSales, search_df$totalUnitSales),
              weightedPrice = (dollarsPerRobot + search_df$dollarsPerRobot) / 2,
              total_parts = rowTally + search_df$rowTally,
              complete_robot = if_else(total_parts == cols, 'COMPLETE', 'incomplete')
    )%>%
    ungroup()
}

A<- as_tibble(A)%>%
  mutate(propSum = rowSums(.[, part_names]))

#I tried running 1:n with 80,000 rows. It wasn't pretty

bind_rows(lapply(1:n, function(x) frankensteinRobot(A, x, 3)))

edit: вот попытка решения для data.table.У этого есть некоторые сходства, но вместо того, чтобы делать это как петлю, это один способ обойти.Если бы я мог выяснить, как получить ваше основное состояние не соответствующих частей, это, вероятно, не будет слишком потертым.Прямо сейчас узким местом является память, и это потому, что я не могу пересекаться, чтобы работать с моим списком списков.

results[
apply(results[, .(current_parts, rbt_missing_curr_parts)], 1, function(x) length(intersect(x[[1]], x[[2]]))==0)
]

Основной код:

library(data.table)

dt <- as.data.table(A)

dt[
  ,j = `:=`(propSum = rowSums(.SD),
           current_parts = list(which(.SD > 0)),
           missing_parts = list(which(.SD == 0)))
  ,.SDcols = part_names,
  by = sku]

#could subset here as dt[1:100, ...] which would allow bigger datasets
dt_missing_parts <- dt[, .( sku, propSum, current_parts, rowTally, missing_parts, dollarsPerRobot, up_range = 1.3 *totalUnitSales, low_range = 0.7 * totalUnitSales)]

results<- dt_missing_parts[dt[rowTally <= round(cols / 2)],
                 j = .(i.sku, sku,
                       propSum = propSum + i.propSum, 
                       dollarsPerRobot = (dollarsPerRobot + i.dollarsPerRobot) / 2,
                       totalUnitSales = pmin(totalUnitSales, i.totalUnitSales),
                       rbt_missing_curr_parts = i.current_parts, 
                       current_parts,
                       rpt_missing_missing_parts= i.missing_parts,
                       missing_parts,
                       total_parts = rowTally + i.rowTally),
                 on = .(sku > sku
                        #more conditions would be great
                        # ,low_range < totalUnitSales
                        # ,up_range > totalUnitSales
                        ),
                 allow.cartesian = TRUE,
                 nomatch = 0L,
                 by = .I
                 ]
results
results[
apply(results[, .(current_parts, rbt_missing_curr_parts)], 1, function(x) length(intersect(x[[1]], x[[2]]))==0)
]
2 голосов
/ 12 апреля 2019

Так что мой словарь по кодированию не очень широк, но я подумал, что смогу освоить то, что знаю, и мне удалось сделать это с небольшим набором данных (немного больше, чем в вашем примере OP).Кажется, работает и производит что-то очень близкое к желаемому результату.Я попытался проверить это с помощью большего набора данных, даже не близкого к 80 000 x 26, и он очень быстро остановился.Любой, кто более опытен в кодировании, чем я, вероятно, может увидеть, что это не очень хороший подход, учитывая размер набора данных.Я не рекомендую использовать это для большого набора данных, но, учитывая, что я потратил на него время, что он временно работает и что, возможно, он может быть полезен в качестве источника вдохновения для замены более быстрых функций и достижения лучшего результата - я думал, что смогуотправьте это так или иначе.Это действительно выдает сообщение об ошибке за один шаг, я не знаю почему, но на самом деле все еще работало нормально.Я не мог получить его в функцию либо из-за ошибки, но скрипт сделал работу.

# (df = A, SKU = 5, skuRowTally =  26)    
a = runif(n=120, min=0, max=0.2)
a[a<0.12] = 0
n = 20
A = as.data.frame(matrix(a,              
                         nrow=n,         
                         ncol=6,         
                         byrow = TRUE))
A$rowTally <- rowSums(A != 0)
A$sku <- seq(from = 1, to = n)
A$totalDollarSales <- runif(n=n, min=1*10^2, max=1*10^6)
A$totalUnitSales <- runif(n=n, min=1*10^2, max=1*10^6)
names(A) <- c("p1_prop", "p2_prop", "p3_prop", "p4_prop", "p5_prop", "p6_prop", "rowTally", "sku", "totalDollarSales", "totalUnitSales")
A <- A[c("sku", "p1_prop", "p2_prop", "p3_prop", "p4_prop", "p5_prop", "p6_prop", "rowTally", "totalDollarSales", "totalUnitSales")]
A$dollarsPerRobot <- A$totalDollarSales/A$totalUnitSales


Output <- unique(rbind(A[which(A$sku == 5),],A[which(A$rowTally <= 26),])) # change df, SKU and skuRowTally here

for(i in 2:7) { #change 2:7 to your columns with parts props
  if(Output[which(Output$sku == 5),][i] !=  0) { # change SKU here
    Output <- Output[which(Output[,i] == 0),]
    Output <- rbind(A[which(A$sku == 5),],Output) # change SKU here
  }
}

Sets <- vector('list', nrow(Output)) 
head_list <- paste(rep("V",nrow(Output)),seq(1:nrow(Output)),sep="")
for(i in 2:nrow(Output)){
  Sets[[i]] <- as.data.frame(t(combn(Output$sku,i)))
  Sets[[i]] <- Sets[[i]][which(Sets[[i]][,1]==5),] # change SKU here
}

for(i in 2:length(Sets)){
  for(j in min(which(seq(1,length(head_list))>i),na.rm = TRUE):max(which(seq(1,length(head_list))>i),na.rm=TRUE)){
    Sets[[i]][,head_list[j]]<-NA
  }
}

Sets <- do.call(rbind,Sets)

Binary.Output <- Output

for(i in 2:7){ #change 2:7 to your columns with parts props
  Binary.Output[,i] <- ifelse(Binary.Output[,i] == 0,0,1)
}

for(i in 1:nrow(Sets)){
  Sets$Good.Combo[i] <-
    ifelse(any(apply(Binary.Output[which(Binary.Output$sku %in% Sets[i,1:nrow(Output)]),], MARGIN = 2, sum)[2:7] > 1),"BAD","GOOD") #change 2:7 to your columns with parts props
}

Sets <- Sets[which(Sets$Good.Combo == "GOOD"),]

for(i in 1:nrow(Sets)){
  Sets$Total.Parts[i] <-
    sum(Binary.Output[which(Binary.Output$sku %in% Sets[i,1:nrow(Binary.Output)]),][2:7]) #change 2:7 to your columns with parts props
  Sets$Complete[i] <- 
    ifelse(Sets$Total.Parts[i]< ncol(Output[,2:7]), "INCOMPLETE", "COMPLETE")
  Sets$MaxLb[i] <-
    min(Output[which(Output$sku %in% Sets[i,1:nrow(Output)]),"totalDollarSales"],na.rm = TRUE)
  Sets$Prop.Sum[i] <-
    sum(Output[which(Output$sku %in% Sets[i,1:nrow(Output)]),2:7])

}

for(i in 1:nrow(Sets)) {
  DPR <- c(1:length(c(t(Sets[i,1:nrow(Output)]))[which(!is.na(c(t(Sets[i,1:nrow(Output)]))))]))
  for (j in 1:length(DPR))  { 
    DPR[j] <- Output[which(Output$sku %in% Sets[i,1:nrow(Output)]),"dollarsPerRobot"][j]*1/length(DPR)
  }
  Sets$weightedPrice[i] <- sum(DPR)
}

print(Sets)
  V1 V2 V3 V4 V5 Good.Combo Total.Parts   Complete    MaxLb  Prop.Sum weightedPrice
1  5  4 NA NA NA       GOOD           4 INCOMPLETE 82485.02 0.6324902     2.6031580
2  5  7 NA NA NA       GOOD           5 INCOMPLETE 82485.02 0.8323490    13.2757958
3  5  9 NA NA NA       GOOD           4 INCOMPLETE 82485.02 0.6152630     1.4411304
4  5 10 NA NA NA       GOOD           4 INCOMPLETE 82485.02 0.6117570     0.5724854
5  5  4  7 NA NA       GOOD           6   COMPLETE 82485.02 0.9854523    10.5475486
6  5  4  9 NA NA       GOOD           5 INCOMPLETE 82485.02 0.7683664     2.6577717
7  5  4 10 NA NA       GOOD           5 INCOMPLETE 82485.02 0.7648603     2.0786750
1 голос
/ 12 апреля 2019

Помогает ли эта функция хотя бы уменьшить количество комбинаций, чтобы избежать проблем с памятью?Я хотел посмотреть, смогу ли я получить:

  1. , если заданная строка имеет ненулевое значение для столбца, тогда дополнение должно иметь нулевое значение для этого столбца.

правильно.Если моя функция поможет сначала сократить данные, я добавлю к ответу больше, иначе я, вероятно, удалю их.Дайте мне знать, если я полностью неправильно понимаю проблему.

library(dplyr)
robot <- function(df=A,x=1,skuRowTally=NA){
#get list of prop variables
A2 <- A[,2:7]
#get each name
nms <- colnames(A2)
#turn data into indicators
Ax <- data.frame(sapply(nms, function(x){ifelse(A2[x] ==0,0,1)}))
#reattach id
Ax2 <- cbind(A['sku'],Ax)
#get SKU of interest
b <- Ax[x,]
#comparison data
A3 <- Ax[-x,]
#names where it is a non-0 value
nms2<-names(b)[which(b==1)]

#get each indicator where you have to remove rows
x1 <- sapply(nms2, function(x){which(Ax[,x] != b[,x])})
#find final comparaitors 
Ax3 <- Ax2[Reduce(intersect,x1),]
#join back to original data 
inner_join(A,select(Ax3,sku), by = 'sku') %>% 
          bind_rows(A[x,]) 

}

robot()
0 голосов
/ 15 апреля 2019
library(Matrix)

# Matrix of non zeros
M <- as.matrix(A[, grepl("^p\\d+_prop$", names(A))]) != 0
M <- Matrix(M)

Функция, которая перебирает комбинации:

combine_parts <- function(M1 = list(mat = M, sku = as.matrix(A$sku)), 
                          M2 = list(mat = M, sku = as.matrix(A$sku))) {
  # Combining the parts is a matrix product. Non overlapping means 0
  combinations <- M1$mat %*% t(M2$mat) == 0
  # Make it a sparse matrix to use the convenient summary() function
  combinations <- Matrix(combinations, sparse = TRUE)
  x <- summary(combinations)
  # This is to stop the computation when valid combinations are exhausted
  if (!nrow(x)) return(NULL)
  # Combine the SKUs separately
  sku <- apply(x, 1, function(.) c(M1$sku[.["i"], ], M2$sku[.["j"], ]))
  # Deduplicate SKUs
  sku <- t(apply(sku, 2, sort))
  # Whether a part is used by a new combination is a logical OR
  list(
    mat = t(apply(
      x[!duplicated(sku), ],
      MARGIN = 1, 
      function(.) M1$mat[.["i"], ] | M2$mat[.["j"], ]
    )),
    sku = sku[!duplicated(sku), ]
  )
}

Результаты:

# Function combine_parts() is wrapped in a while loop to exhaust all combinations
x <- list(mat = M, sku = as.matrix(A$sku))
res <- list(x)
while(!is.null(x)) {
  x <- combine_parts(x, list(mat = M, sku = as.matrix(A$sku)))
  res <- c(res, list(x))
}

#> [[1]]
#> [[1]]$mat
#>       p1_prop p2_prop p3_prop p4_prop p5_prop p6_prop
#>  [1,]   FALSE   FALSE   FALSE    TRUE   FALSE    TRUE
#>  [2,]    TRUE    TRUE    TRUE   FALSE   FALSE   FALSE
#>  [3,]    TRUE   FALSE    TRUE   FALSE    TRUE    TRUE
#>  [4,]   FALSE    TRUE    TRUE   FALSE    TRUE   FALSE
#>  [5,]   FALSE   FALSE   FALSE   FALSE    TRUE   FALSE
#>  [6,]   FALSE   FALSE   FALSE   FALSE    TRUE    TRUE
#>  [7,]    TRUE   FALSE    TRUE   FALSE    TRUE    TRUE
#>  [8,]    TRUE   FALSE   FALSE    TRUE   FALSE   FALSE
#>  [9,]    TRUE    TRUE   FALSE    TRUE   FALSE   FALSE
#> [10,]   FALSE   FALSE   FALSE   FALSE    TRUE   FALSE
#> 
#> [[1]]$sku
#>       [,1]
#>  [1,]    1
#>  [2,]    2
#>  [3,]    3
#>  [4,]    4
#>  [5,]    5
#>  [6,]    6
#>  [7,]    7
#>  [8,]    8
#>  [9,]    9
#> [10,]   10
#> 
#> 
#> [[2]]
#> [[2]]$mat
#>    p1_prop p2_prop p3_prop p4_prop p5_prop p6_prop
#> 1     TRUE    TRUE    TRUE    TRUE   FALSE    TRUE
#> 2    FALSE    TRUE    TRUE    TRUE    TRUE    TRUE
#> 3    FALSE   FALSE   FALSE    TRUE    TRUE    TRUE
#> 4     TRUE    TRUE    TRUE   FALSE    TRUE   FALSE
#> 5     TRUE    TRUE    TRUE   FALSE    TRUE    TRUE
#> 6     TRUE    TRUE    TRUE    TRUE    TRUE   FALSE
#> 7     TRUE   FALSE   FALSE    TRUE    TRUE   FALSE
#> 8     TRUE   FALSE   FALSE    TRUE    TRUE    TRUE
#> 9     TRUE    TRUE   FALSE    TRUE    TRUE   FALSE
#> 10    TRUE    TRUE   FALSE    TRUE    TRUE    TRUE
#> 11   FALSE   FALSE   FALSE    TRUE    TRUE    TRUE
#> 12    TRUE    TRUE    TRUE   FALSE    TRUE   FALSE
#> 13    TRUE   FALSE   FALSE    TRUE    TRUE   FALSE
#> 14    TRUE    TRUE   FALSE    TRUE    TRUE   FALSE
#> 
#> [[2]]$sku
#>       [,1] [,2]
#>  [1,]    1    2
#>  [2,]    1    4
#>  [3,]    1    5
#>  [4,]    2    5
#>  [5,]    2    6
#>  [6,]    4    8
#>  [7,]    5    8
#>  [8,]    6    8
#>  [9,]    5    9
#> [10,]    6    9
#> [11,]    1   10
#> [12,]    2   10
#> [13,]    8   10
#> [14,]    9   10
#> 
#> 
#> [[3]]
#> [[3]]$mat
#>   p1_prop p2_prop p3_prop p4_prop p5_prop p6_prop
#> 1    TRUE    TRUE    TRUE    TRUE    TRUE    TRUE
#> 2    TRUE    TRUE    TRUE    TRUE    TRUE    TRUE
#> 
#> [[3]]$sku
#>      [,1] [,2] [,3]
#> [1,]    1    2    5
#> [2,]    1    2   10
#> 
#> 
#> [[4]]
#> NULL

Создано в 2019-04-15 пакетом представ. (v0.2.1)


Примечание: другие условия и вычисления не реализованы, поскольку они будут мешать решению, но их будет легко добавить.

0 голосов
/ 13 апреля 2019

Это поможет?Я не уверен, что получил все, но это дает вам все совместимые комбинации без декартового произведения или дублирования пар A / BB / A, а также с некоторыми агрегатами, которые вы можете легко настроить.

library(tidyverse)

df <- A
skuRowTally = 3

# we convert to matrix and transpose to be able to use vectorized
# operations later
m <- df %>% select(ends_with("prop")) %>% t()
colnames(m) <- 1:ncol(m)
m
#>                 1         2         3         4         5         6
#> p1_prop 0.0000000 0.1889351 0.1374046 0.0000000 0.0000000 0.0000000
#> p2_prop 0.0000000 0.1321596 0.0000000 0.1554890 0.0000000 0.0000000
#> p3_prop 0.0000000 0.1258228 0.1539683 0.1869410 0.0000000 0.0000000
#> p4_prop 0.1816416 0.0000000 0.0000000 0.0000000 0.0000000 0.0000000
#> p5_prop 0.0000000 0.0000000 0.1435237 0.1303348 0.1739382 0.1654747
#> p6_prop 0.1796779 0.0000000 0.1983812 0.0000000 0.0000000 0.1336933
#>                 7         8         9       10
#> p1_prop 0.1588480 0.1565866 0.1464627 0.000000
#> p2_prop 0.0000000 0.0000000 0.1385463 0.000000
#> p3_prop 0.1447422 0.0000000 0.0000000 0.000000
#> p4_prop 0.0000000 0.1578712 0.1722419 0.000000
#> p5_prop 0.1641893 0.0000000 0.0000000 0.132401
#> p6_prop 0.1294120 0.0000000 0.0000000 0.000000
# subset potential complements
m_low <- m[, colSums(m) <= skuRowTally]
m_low
#>                 1         2         3         4         5         6
#> p1_prop 0.0000000 0.1889351 0.1374046 0.0000000 0.0000000 0.0000000
#> p2_prop 0.0000000 0.1321596 0.0000000 0.1554890 0.0000000 0.0000000
#> p3_prop 0.0000000 0.1258228 0.1539683 0.1869410 0.0000000 0.0000000
#> p4_prop 0.1816416 0.0000000 0.0000000 0.0000000 0.0000000 0.0000000
#> p5_prop 0.0000000 0.0000000 0.1435237 0.1303348 0.1739382 0.1654747
#> p6_prop 0.1796779 0.0000000 0.1983812 0.0000000 0.0000000 0.1336933
#>                 7         8         9       10
#> p1_prop 0.1588480 0.1565866 0.1464627 0.000000
#> p2_prop 0.0000000 0.0000000 0.1385463 0.000000
#> p3_prop 0.1447422 0.0000000 0.0000000 0.000000
#> p4_prop 0.0000000 0.1578712 0.1722419 0.000000
#> p5_prop 0.1641893 0.0000000 0.0000000 0.132401
#> p6_prop 0.1294120 0.0000000 0.0000000 0.000000

# Then we extract all possible combinations, by looping, so we avoid a cartesian product
nc <- ncol(m)
candidates <- vector("list", nc-1) #%>% setNames(1:(nc-1))
for(i in seq_along(candidates)){
  if(any(rng <- colnames(m_low) > i)){
  candidates[[i]] <- names(which(!colSums(m[,i] & m_low[,rng, drop = FALSE])))
  }
}
candidates
#> [[1]]
#> [1] "2"  "4"  "5"  "10"
#> 
#> [[2]]
#> [1] "5" "6"
#> 
#> [[3]]
#> character(0)
#> 
#> [[4]]
#> [1] "8"
#> 
#> [[5]]
#> [1] "8" "9"
#> 
#> [[6]]
#> [1] "8" "9"
#> 
#> [[7]]
#> character(0)
#> 
#> [[8]]
#> character(0)
#> 
#> [[9]]
#> NULL

# reformat and aggregate into output
candidates <- candidates[!!lengths(candidates)]
candidates_df <- 
  enframe(candidates[!!lengths(candidates)],"sku1","sku2") %>% 
  unnest() %>%
  mutate(sum = map2(
    sku1, sku2, ~summarize_all(df[c(.x, .y),-1], sum))) %>%
  mutate(delta = map2(
    sku1, sku2, ~summarize_all(df[c(.x, .y),9:11], ~abs(diff(.))))) %>%
  unnest(.sep = "_")
candidates_df
#> # A tibble: 11 x 15
#>     sku1 sku2  sum_p1_prop sum_p2_prop sum_p3_prop sum_p4_prop sum_p5_prop
#>    <int> <chr>       <dbl>       <dbl>       <dbl>       <dbl>       <dbl>
#>  1     1 2           0.189       0.132       0.126       0.182       0    
#>  2     1 4           0           0.155       0.187       0.182       0.130
#>  3     1 5           0           0           0           0.182       0.174
#>  4     1 10          0           0           0           0.182       0.132
#>  5     2 5           0.189       0.132       0.126       0           0.174
#>  6     2 6           0.189       0.132       0.126       0           0.165
#>  7     3 8           0.294       0           0.154       0.158       0.144
#>  8     4 8           0.157       0.155       0.187       0.158       0.130
#>  9     4 9           0.146       0.294       0.187       0.172       0.130
#> 10     5 8           0.157       0           0           0.158       0.174
#> 11     5 9           0.146       0.139       0           0.172       0.174
#> # ... with 8 more variables: sum_p6_prop <dbl>, sum_rowTally <dbl>,
#> #   sum_totalDollarSales <dbl>, sum_totalUnitSales <dbl>,
#> #   sum_dollarsPerRobot <dbl>, delta_totalDollarSales <dbl>,
#> #   delta_totalUnitSales <dbl>, delta_dollarsPerRobot <dbl>
0 голосов
/ 13 апреля 2019

Редактировать: добавлены вспомогательные функции, позволяющие итерации завершить решение

Вот решение dplyr / tidyr, которое разбивает проблему на два основных шага.

Сначала даносписок уже используемых строк, какие оставшиеся строки дополняют без перекрывающегося skus?Функция find_complements, представленная ниже, выводит таблицу, показывающую все неперекрывающиеся скусы и сколько новых скусов они внесут.Это может быть присоединено к исходным данным, если вы хотите использовать определенное правило для выбора.

Другая основная часть - это «следующий ход».Чтобы помочь с этим, функция select_top принимает имя столбца и находит оставшийся номер с наибольшим номером, используя в этом примере столбец по умолчанию totalUnitSales.

. Для выполнения шага используется вспомогательная функция.по имени sku_string_to_chr_string получает выходные данные select_top и превращает новую цепочку sku в вектор, который может быть возвращен на шаг 1.

Эта строка работает автоматически, разборчивым и легко изменяемым образом, для построениядо полного робота.Он использует некоторую вырезку и вставку, чтобы повторить процесс, пока он не будет завершен, но, вероятно, есть и простой способ автоматизировать это.


library(tidyverse)
find_complements <- function(test_skus) {
  A_test <- A %>%
      add_row(sku = 0) %>%    # Creates a blank row corresponding to the 
                              #   case that no additional skus are added
      select(sku, p1_prop:p6_prop) %>%
      gather(part, val, -sku) %>%
      mutate(val = if_else(val > 0, TRUE, FALSE))

  A_test %>% 
    filter(sku %in% test_skus) %>%
    group_by(part) %>%
    summarize(val = any(val)) %>%
    mutate(sku = paste(test_skus, collapse = "_")) %>%
    right_join(A_test, by = "part") %>%
    mutate(eval = case_when(val.y & !val.x   ~ "Additive", # Adds part not already present
                            val.y & val.x    ~ "Overlap", # Uh-oh! Adds repeated part
                            val.x            ~ "Already", # Part already present, not doubled
                            TRUE             ~ "Both_missing")) %>% 
    mutate(sku_string = paste(sku.x, sku.y, sep = "_")) %>%
    count(sku_string, sku = sku.y, eval) %>%
    spread(eval, n, fill = 0) %>%
    filter(Overlap == 0)
}

Вот вывод, показывающий, как строка 1 на егоown (представленный строкой 1_0 вверху) состоит из двух частей, но отсутствует 4 части.Добавление sku 2 или 4 добавит еще три части без наложения, в результате чего одна часть будет отсутствовать.Добавление sku 5 или 10 добавит еще одну часть, при этом 3 части будут отсутствовать.

> find_complements(1)
# A tibble: 5 x 6
  sku_string   sku Additive Already Both_missing Overlap
  <chr>      <dbl>    <dbl>   <dbl>        <dbl>   <dbl>
1 1_0            0        0       2            4       0
2 1_10          10        1       2            3       0
3 1_2            2        3       2            1       0
4 1_4            4        3       2            1       0
5 1_5            5        1       2            3       0

Мы могли бы продолжить эти строки, например, поиск дополнительных дополнительных строк, учитывая использование skus 1 и 2. Этопоказывает, что строки 5 или 10 завершат работу робота, добавив еще одну деталь к пяти уже существующим, не вызывая репликации деталей.

> find_complements(c(1,2))
# A tibble: 3 x 6
  sku_string   sku Additive Already Both_missing Overlap
  <chr>      <dbl>    <dbl>   <dbl>        <dbl>   <dbl>
1 1_2_0          0        0       5            1       0
2 1_2_10        10        1       5            0       0
3 1_2_5          5        1       5            0       0

Если другие аспекты нового sku, такие как rowTally, несутпо своему выбору вы можете снова присоединиться к исходной таблице и отфильтровать по ней свой выбор:

> find_complements(c(1,2)) %>%
+   left_join(A) %>%
+   filter(rowTally <= 2)
Joining, by = "sku"
# A tibble: 2 x 16
  sku_string   sku Additive Already Both_missing Overlap p1_prop p2_prop p3_prop
  <chr>      <dbl>    <dbl>   <dbl>        <dbl>   <dbl>   <dbl>   <dbl>   <dbl>
1 1_2_10        10        1       5            0       0       0       0       0
2 1_2_5          5        1       5            0       0       0       0       0
# … with 7 more variables: p4_prop <dbl>, p5_prop <dbl>, p6_prop <dbl>,
#   rowTally <dbl>, totalDollarSales <dbl>, totalUnitSales <dbl>,
#   dollarsPerRobot <dbl>

Редактировать: вот две вспомогательные функции, которые выводят один find_complements,выберите дополнение с наибольшим значением totalUnitSales (или другой переменной выбора) и создайте ввод для повторного запуска find_complements.

# This function takes the output of `find_complements` and adds a column of choosing from the original table, defaulting to `totalUnitSales`, and picking the top one.
select_top <- function(df, top_col = "totalUnitSales") {
  df %>%
    left_join(A %>% select(sku, !!top_col)) %>%
    arrange(desc(!!rlang::sym(top_col))) %>%
    slice(1)
}

# This function takes the first cell and converts to a vector, so "10_2" becomes
#   vector   c(10, 2)
sku_string_to_chr_string <- function(df) {
  df[1,1] %>%
    str_split(pattern = "_") %>%
    flatten_chr()
}

Примените их итеративно, и мы получим полный ответ, используя строки10, 2 и 1.

A %>%
  select_top() %>%
  sku_string_to_chr_string() %>%
  find_complements() %>%

  select_top() %>%
  sku_string_to_chr_string() %>%
  find_complements() %>%

  select_top() 

# A tibble: 1 x 7
  sku_string   sku Additive Already Both_missing Overlap totalUnitSales
  <chr>      <dbl>    <dbl>   <dbl>        <dbl>   <dbl>          <dbl>
1 10_2_1         1        2       4            0       0        339139.
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...