Создать комбинацию факторов с оптимизацией - PullRequest
0 голосов
/ 06 июля 2018
library(dplyr)
library(tidyr)

df <- data.frame(
  First = c("MW3", "MW3", "MW4", "MW5", "MW6", "MW7", "MW7", "MW8"),
  Second = c("MW4; MW5; MW6", "MW5; MW3; MW7", "MW8; MW7; MW3",
             "MW5; MW6; MW4", "MW3; MW7; MW8", "MW6; MW8; MW4",
             "MW3; MW4; MW5", "MW6; MW3; MW7")
)

df <- df %>%
  mutate(
    ID = row_number(),
    lmt = n_distinct(ID)
  ) %>%
  separate_rows(Second, sep = "; ") %>%
  group_by(ID) %>%
  mutate(
    wgt = row_number()
  ) %>% ungroup()

Допустим, что для каждого идентификатора я хочу сохранить только 1 комбинацию First и Second (т.е. длина уникальных идентификаторов в df всегда должна быть равна lmt).

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

  • Комбинации с wgt 1 должны выбираться, когда это возможно, в качестве альтернативы также 2, но следует избегать 3 (т. Е. Сумма wgt должна быть минимальной);

  • Разница между частотой значения в Second и частотой в First должна быть близка к 0.

Есть идеи, как подойти к этому в R?

Ожидаемый результат для вышеуказанного случая:

     ID First Second   wgt   lmt
1     1   MW3    MW4     1     8
2     2   MW3    MW7     3     8
3     3   MW4    MW7     2     8
4     4   MW5    MW5     1     8
5     5   MW6    MW3     1     8
6     6   MW7    MW8     2     8
7     7   MW7    MW3     1     8
8     8   MW8    MW6     1     8

Почему? Просто потому, что с этой комбинацией больше нет элементов справа (Second), чем слева (First). Например, есть два элемента MW3 справа и слева.

Однако цена, которую здесь нужно заплатить, заключается в том, что wgt не всегда равен 1 (сумма wgt - это не 8, а 12).

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

Ответы [ 2 ]

0 голосов
/ 15 июля 2018

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

library(lpSolve)
MISMATCH.COST <- 1000

.create.row <- function(row.names, first) {
    row <- vector(mode="numeric", length=length(first))
    for (i in 1:length(row.names))
        row = row + (-MISMATCH.COST+i)*(row.names[i]==first)
    return(row)
}

find.pairing <- function(First, Second) {
    row.names = sapply(Second, strsplit, "; ")

    # Create cost matrix for assignment
    mat = sapply(row.names, .create.row, First)
    assignment <- lp.assign(mat)
    print("Total cost:")
    print(assignment$objval+length(First)*MISMATCH.COST)
    solution <- lp.assign(mat)$solution
    pairs <- which(solution>0, arr.ind=T)
    matches = First[pairs[,1]]

    # Find out where a mismatch has occured, and replace match
    for (i in 1:length(matches)) {
        if (!(matches[i] %in% row.names[[i]])) {
            matches[i] = row.names[[i]][1]
        }
    }
    result = data.frame(
        First[pairs[,2]],
        matches)
    return(result)
}

Запуск его на вашем примере дает оптимальное решение (как и всегда должно быть)

> First = c("MW3", "MW3", "MW4", "MW5", "MW6", "MW7", "MW7", "MW8")
> Second = c("MW4; MW5; MW6", "MW5; MW3; MW7", "MW8; MW7; MW3",
           "MW5; MW6; MW4", "MW3; MW7; MW8", "MW6; MW8; MW4",
           "MW3; MW4; MW5", "MW6; MW3; MW7")
Second = c("MW4; MW5; MW6", "MW5; MW3; MW7", "MW8; MW7; MW3",
+            "MW5; MW6; MW4", "MW3; MW7; MW8", "MW6; MW8; MW4",
+            "MW3; MW4; MW5", "MW6; MW3; MW7")
> find.pairing(First, Second)
[1] "Total cost:"
[1] 12
  First.pairs...2.. matches
1               MW3     MW4
2               MW3     MW3
3               MW4     MW7
4               MW5     MW5
5               MW6     MW7
6               MW7     MW8
7               MW7     MW3
8               MW8     MW6
0 голосов
/ 11 июля 2018

Я поиграл с этой проблемой, и я могу поделиться решением, используя вариацию алгоритма minconflicts . Ключевым моментом здесь является найти функцию оценки, которая сочетает в себе ваши требования. Реализация ниже соответствует вашей рекомендации «», скажем, цель должна состоять в том, чтобы установить приоритеты минимизации 2-го критерия (разности частот) ». Поэкспериментируйте с другими функциями оценки ваших реальных данных и посмотрим, как далеко вы продвинулись.

На ваших исходных данных (8 идентификаторов) я получил такое же хорошее решение, как и то, которое вы опубликовали:

> solution_summary(current_solution)
   Name FirstCount SecondCount diff
1:  MW3          2           2    0
2:  MW4          1           1    0
3:  MW5          1           1    0
4:  MW6          1           1    0
5:  MW7          2           2    0
6:  MW8          1           1    0
[1] "Total freq diff:  0"
[1] "Total wgt:  12"

При случайных данных с 10000 идентификаторами алгоритм может найти решение без разницы в частотах первой / второй (но сумма wgt больше минимальной):

> solution_summary(current_solution)
   Name FirstCount SecondCount diff
1:  MW3       1660        1660    0
2:  MW4       1762        1762    0
3:  MW5       1599        1599    0
4:  MW6       1664        1664    0
5:  MW7       1646        1646    0
6:  MW8       1669        1669    0
[1] "Total freq diff:  0"
[1] "Total wgt:  19521"

Код ниже:

library(data.table)
df <- as.data.table(df)
df <- df[, .(ID, First, Second, wgt)]

# PLAY AROUND WITH THIS PARAMETER
freq_weight <- 0.9

wgt_min <- df[, uniqueN(ID)]
wgt_max <- df[, uniqueN(ID) * 3]

freq_min <- 0
freq_max <- df[, uniqueN(ID) * 2] #verify if this is the worst case scenario

score <- function(solution){
  # compute raw scores
  current_wgt <- solution[, sum(wgt)]
  second_freq <- solution[, .(SecondCount = .N), by = Second]
  names(second_freq)[1] <- "Name"
  compare <- merge(First_freq, second_freq, by = "Name", all = TRUE)
  compare[is.na(compare)] <- 0
  compare[, diff := abs(FirstCount - SecondCount)]
  current_freq <- compare[, sum(diff)]

  # normalize
  wgt_score <- (current_wgt - wgt_min) / (wgt_max - wgt_min)
  freq_score <- (current_freq - freq_min) / (freq_max - freq_min)

  #combine
  score <- (freq_weight * freq_score) + ((1 - freq_weight) * wgt_score)
  return(score)
}

#initialize random solution
current_solution <- df[, .SD[sample(.N, 1)], by = ID]

#get freq of First (this does not change)
First_freq <- current_solution[, .(FirstCount = .N), by = First]
names(First_freq)[1] <- "Name"

#get mincoflict to be applied on each iteration
minconflict <- function(df, solution){
  #pick ID
  change <- solution[, sample(unique(ID), 1)]

  #get permissible values
  values <- df[ID == change, .(Second, wgt)]

  #assign scores
  values[, score := NA_real_]
  for (i in 1:nrow(values)) {
    solution[ID == change, c("Second", "wgt") := values[i, .(Second, wgt)]]
    set(values, i, "score", score(solution))
  }

  #return the best combination
  scores <<- c(scores, values[, min(score)])
  solution[ID == change, c("Second", "wgt") := values[which.min(score), .(Second, wgt)]]
}

#optimize
scores <- 1
iter <- 0
while(TRUE){
  minconflict(df, current_solution)
  iter <- iter + 1
  #SET MAX NUMBER OF ITERATIONS HERE
  if(scores[length(scores)] == 0 | iter >= 1000) break
}

# summarize obtained solution
solution_summary <- function(solution){
  second_freq <- solution[, .(SecondCount = .N), by = Second]
  names(second_freq)[1] <- "Name"
  compare <- merge(First_freq, second_freq, by = "Name", all = TRUE)
  compare[is.na(compare)] <- 0
  compare[, diff := abs(FirstCount - SecondCount)]
  print(compare)
  print(paste("Total freq diff: ", compare[, sum(diff)]))
  print(paste("Total wgt: ", solution[, sum(wgt)]))
}
solution_summary(current_solution)
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...