10

In R I want to allocate projects to people based on their rank preferences but also their performance. Say I have 5 projects and 3 people. In this case, all three people want project A because it's their top preference but Anna should get it because she has the best performance score. Now she's out of the equation and James and Billy are both vying for project B which Billy should get because he's got a better performance measure. How could I do this in R? I will have more projects and people in reality.

Project Rank Person Performance 
A        1   Billy   95
B        2   Billy   95
C        3   Billy   95
D        4   Billy   95
E        5   Billy   95
A        1   Anna    97
B        2   Anna    97
C        3   Anna    97
D        5   Anna    97
E        4   Anna    97
A        1   James   92
B        2   James   92
C        4   James   92
D        3   James   92
E        5   James   92

EDIT

New data in light of issue - see comment to accepted answer.

structure(list(Project = c("1", "2", "3", "4", "5", "6", "7", 
"8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", 
"19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", 
"30", "31", "32", "33", "1", "2", "3", "4", "5", "6", "7", "8", 
"9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", 
"20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", 
"31", "32", "33", "22", "17", "2", "5", "6", "14", "26", "27", 
"24", "32", "31", "13", "15", "28", "25", "8", "7", "4", "3", 
"2", "1", "9", "11", "12", "23", "30", "33", "29", "20", "10", 
"19", "16", "18", "1", "2", "3", "4", "5", "6", "7", "8", "9", 
"10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", 
"21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31", 
"32", "33", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", 
"11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", 
"22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32", 
"33", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", 
"12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", 
"23", "24", "25", "26", "27", "28", "29", "30", "31", "32", "33", 
"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", 
"13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", 
"24", "25", "26", "27", "28", "29", "30", "31", "32", "33", "1", 
"2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", 
"14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", 
"25", "26", "27", "28", "29", "30", "31", "32", "33", "17", "11", 
"12", "13", "21", "20", "19", "22", "26", "27", "9", "28", "18", 
"1", "2", "3", "4", "5", "6", "7", "8", "10", "14", "15", "16", 
"23", "24", "25", "29", "30", "31", "32", "33", "1", "2", "3", 
"4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", 
"16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", 
"27", "28", "29", "30", "31", "32", "33", "8", "14", "4", "5", 
"6", "9", "17", "11", "12", "13", "7", "21", "22", "2", "3", 
"32", "24", "33", "31", "26", "27", "25", "10", "30", "29", "28", 
"23", "19", "20", "18", "16", "15", "1", "1", "2", "3", "4", 
"5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", 
"16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", 
"27", "28", "29", "30", "31", "32", "33", "1", "2", "3", "4", 
"5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", 
"16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", 
"27", "28", "29", "30", "31", "32", "33", "1", "2", "3", "4", 
"5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", 
"16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", 
"27", "28", "29", "30", "31", "32", "33", "33", "1", "11", "12", 
"13", "31", "30", "3", "4", "10", "2", "5", "6", "7", "8", "9", 
"14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", 
"25", "26", "27", "28", "29", "32", "1", "2", "3", "4", "5", 
"6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", 
"17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", 
"28", "29", "30", "31", "32", "33", "1", "2", "3", "4", "5", 
"6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", 
"17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", 
"28", "29", "30", "31", "32", "33", "1", "2", "3", "4", "5", 
"6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", 
"17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", 
"28", "29", "30", "31", "32", "33", "20", "18", "28", "2", "19", 
"12", "11", "13", "15", "10", "1", "6", "5", "25", "16", "30", 
"9", "21", "33", "23", "31", "8", "17", "22", "27", "26", "32", 
"29", "4", "3", "7", "14", "24", "1", "2", "3", "4", "5", "6", 
"7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", 
"18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", 
"29", "30", "31", "32", "33", "1", "2", "3", "4", "5", "6", "7", 
"8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", 
"19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", 
"30", "31", "32", "33", "1", "2", "3", "4", "5", "6", "7", "8", 
"9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", 
"20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", 
"31", "32", "33", "1", "2", "3", "4", "5", "6", "7", "8", "9", 
"10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", 
"21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31", 
"32", "33", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", 
"11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", 
"22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32", 
"33", "17", "21", "29", "19", "31", "18", "8", "22", "16", "13", 
"24", "11", "12", "32", "30", "6", "5", "20", "4", "3", "15", 
"2", "1", "28", "10", "23", "26", "27", "14", "33", "9", "25", 
"7"), Rank = c(10, 26, 24, 25, 20, 21, 33, 4, 1, 16, 29, 30, 
31, 5, 9, 19, 2, 6, 3, 7, 32, 18, 8, 28, 11, 13, 14, 12, 27, 
22, 17, 23, 15, 11, 4, 12, 13, 9, 9, 20, 18, 7, 22, 26, 25, 24, 
27, 23, 33, 6, 10, 28, 29, 32, 5, 30, 8, 3, 2, 1, 14, 31, 19, 
17, 16, 21, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 
16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 
32, 33, 10, 4, 2, 2, 26, 26, 24, 21, 5, 9, 14, 14, 14, 22, 1, 
15, 16, 23, 27, 3, 6, 8, 17, 18, 25, 11, 11, 7, 19, 13, 12, 20, 
23, 33, 23, 5, 6, 9, 10, 22, 18, 15, 13, 19, 20, 21, 28, 24, 
29, 2, 14, 7, 27, 1, 3, 12, 4, 8, 16, 17, 11, 30, 25, 31, 32, 
26, 1, 23, 25, 26, 28, 29, 33, 31, 11, 5, 19, 20, 21, 32, 3, 
4, 16, 27, 17, 8, 12, 7, 24, 22, 18, 14, 15, 9, 2, 18, 6, 10, 
30, 13, 19, 27, 28, 12, 11, 4, 7, 29, 26, 16, 17, 18, 10, 20, 
32, 1, 25, 21, 33, 24, 5, 8, 9, 6, 2, 3, 14, 22, 23, 31, 30, 
15, 33, 18, 31, 30, 32, 29, 28, 27, 12, 6, 26, 25, 24, 23, 2, 
3, 10, 9, 4, 5, 14, 21, 13, 19, 11, 8, 7, 1, 17, 22, 15, 16, 
18, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 
18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 
27, 17, 6, 7, 1, 2, 26, 12, 25, 13, 3, 4, 5, 24, 18, 19, 30, 
23, 8, 14, 28, 31, 15, 9, 29, 10, 11, 16, 20, 33, 21, 32, 22, 
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 
19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 22, 
23, 6, 7, 8, 9, 24, 1, 26, 5, 10, 11, 12, 13, 27, 14, 28, 15, 
29, 30, 21, 16, 31, 17, 18, 32, 33, 25, 19, 4, 2, 20, 3, 28, 
20, 19, 18, 15, 11, 12, 13, 23, 5, 6, 9, 25, 21, 8, 1, 17, 2, 
7, 22, 10, 16, 24, 14, 33, 3, 4, 26, 27, 31, 29, 32, 30, 3, 6, 
15, 14, 17, 16, 8, 28, 1, 13, 9, 10, 11, 20, 7, 29, 30, 21, 12, 
2, 4, 19, 18, 22, 31, 32, 33, 5, 26, 27, 24, 25, 23, 1, 2, 3, 
4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 
21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 7, 8, 9, 
10, 15, 14, 16, 8, 17, 29, 18, 19, 20, 21, 6, 30, 3, 18, 22, 
20, 13, 4, 23, 11, 25, 26, 27, 28, 2, 1, 31, 32, 33, 17, 26, 
2, 3, 5, 6, 25, 24, 27, 18, 8, 9, 10, 29, 1, 23, 11, 19, 28, 
13, 20, 12, 14, 30, 31, 32, 33, 15, 29, 16, 4, 7, 21, 31, 25, 
12, 11, 8, 7, 26, 27, 13, 20, 16, 15, 14, 30, 18, 6, 21, 3, 5, 
4, 19, 28, 10, 17, 24, 1, 2, 9, 32, 33, 22, 23, 29, 1, 2, 3, 
4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 
21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 3, 10, 21, 
17, 15, 9, 23, 24, 7, 8, 13, 6, 11, 25, 1, 26, 4, 33, 31, 32, 
30, 2, 29, 19, 18, 14, 16, 5, 28, 27, 12, 22, 20, 11, 17, 13, 
12, 18, 19, 26, 16, 3, 2, 5, 6, 7, 27, 1, 20, 4, 15, 14, 8, 21, 
28, 22, 10, 33, 30, 29, 9, 32, 31, 25, 24, 23, 23, 22, 1, 2, 
3, 4, 26, 27, 24, 30, 8, 9, 11, 14, 25, 29, 5, 32, 7, 19, 6, 
9, 16, 23, 13, 21, 20, 18, 15, 28, 27, 31, 10, 7, 23, 1, 2, 8, 
9, 3, 22, 21, 20, 4, 5, 6, 12, 17, 18, 19, 16, 15, 33, 25, 24, 
27, 14, 26, 10, 11, 28, 13, 29, 30, 31, 32, 3, 27, 14, 15, 21, 
20, 32, 23, 8, 2, 19, 18, 13, 29, 1, 31, 12, 11, 24, 5, 30, 7, 
16, 10, 33, 26, 25, 28, 9, 22, 4, 6, 17, 1, 2, 3, 4, 5, 6, 7, 
8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 
24, 25, 26, 27, 28, 29, 30, 31, 32, 33), Person = structure(c(3L, 
3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 
3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 
4L, 4L, 4L, 4L, 4L, 4L, 4L, 4L, 4L, 4L, 4L, 4L, 4L, 4L, 4L, 4L, 
4L, 4L, 4L, 4L, 4L, 4L, 4L, 4L, 4L, 4L, 4L, 4L, 4L, 4L, 4L, 4L, 
4L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 
5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 5L, 
5L, 5L, 6L, 6L, 6L, 6L, 6L, 6L, 6L, 6L, 6L, 6L, 6L, 6L, 6L, 6L, 
6L, 6L, 6L, 6L, 6L, 6L, 6L, 6L, 6L, 6L, 6L, 6L, 6L, 6L, 6L, 6L, 
6L, 6L, 6L, 7L, 7L, 7L, 7L, 7L, 7L, 7L, 7L, 7L, 7L, 7L, 7L, 7L, 
7L, 7L, 7L, 7L, 7L, 7L, 7L, 7L, 7L, 7L, 7L, 7L, 7L, 7L, 7L, 7L, 
7L, 7L, 7L, 7L, 8L, 8L, 8L, 8L, 8L, 8L, 8L, 8L, 8L, 8L, 8L, 8L, 
8L, 8L, 8L, 8L, 8L, 8L, 8L, 8L, 8L, 8L, 8L, 8L, 8L, 8L, 8L, 8L, 
8L, 8L, 8L, 8L, 8L, 9L, 9L, 9L, 9L, 9L, 9L, 9L, 9L, 9L, 9L, 9L, 
9L, 9L, 9L, 9L, 9L, 9L, 9L, 9L, 9L, 9L, 9L, 9L, 9L, 9L, 9L, 9L, 
9L, 9L, 9L, 9L, 9L, 9L, 10L, 10L, 10L, 10L, 10L, 10L, 10L, 10L, 
10L, 10L, 10L, 10L, 10L, 10L, 10L, 10L, 10L, 10L, 10L, 10L, 10L, 
10L, 10L, 10L, 10L, 10L, 10L, 10L, 10L, 10L, 10L, 10L, 10L, 11L, 
11L, 11L, 11L, 11L, 11L, 11L, 11L, 11L, 11L, 11L, 11L, 11L, 11L, 
11L, 11L, 11L, 11L, 11L, 11L, 11L, 11L, 11L, 11L, 11L, 11L, 11L, 
11L, 11L, 11L, 11L, 11L, 11L, 12L, 12L, 12L, 12L, 12L, 12L, 12L, 
12L, 12L, 12L, 12L, 12L, 12L, 12L, 12L, 12L, 12L, 12L, 12L, 12L, 
12L, 12L, 12L, 12L, 12L, 12L, 12L, 12L, 12L, 12L, 12L, 12L, 12L, 
13L, 13L, 13L, 13L, 13L, 13L, 13L, 13L, 13L, 13L, 13L, 13L, 13L, 
13L, 13L, 13L, 13L, 13L, 13L, 13L, 13L, 13L, 13L, 13L, 13L, 13L, 
13L, 13L, 13L, 13L, 13L, 13L, 13L, 14L, 14L, 14L, 14L, 14L, 14L, 
14L, 14L, 14L, 14L, 14L, 14L, 14L, 14L, 14L, 14L, 14L, 14L, 14L, 
14L, 14L, 14L, 14L, 14L, 14L, 14L, 14L, 14L, 14L, 14L, 14L, 14L, 
14L, 15L, 15L, 15L, 15L, 15L, 15L, 15L, 15L, 15L, 15L, 15L, 15L, 
15L, 15L, 15L, 15L, 15L, 15L, 15L, 15L, 15L, 15L, 15L, 15L, 15L, 
15L, 15L, 15L, 15L, 15L, 15L, 15L, 15L, 16L, 16L, 16L, 16L, 16L, 
16L, 16L, 16L, 16L, 16L, 16L, 16L, 16L, 16L, 16L, 16L, 16L, 16L, 
16L, 16L, 16L, 16L, 16L, 16L, 16L, 16L, 16L, 16L, 16L, 16L, 16L, 
16L, 16L, 17L, 17L, 17L, 17L, 17L, 17L, 17L, 17L, 17L, 17L, 17L, 
17L, 17L, 17L, 17L, 17L, 17L, 17L, 17L, 17L, 17L, 17L, 17L, 17L, 
17L, 17L, 17L, 17L, 17L, 17L, 17L, 17L, 17L, 1L, 1L, 1L, 1L, 
1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 
1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 18L, 18L, 
18L, 18L, 18L, 18L, 18L, 18L, 18L, 18L, 18L, 18L, 18L, 18L, 18L, 
18L, 18L, 18L, 18L, 18L, 18L, 18L, 18L, 18L, 18L, 18L, 18L, 18L, 
18L, 18L, 18L, 18L, 18L, 19L, 19L, 19L, 19L, 19L, 19L, 19L, 19L, 
19L, 19L, 19L, 19L, 19L, 19L, 19L, 19L, 19L, 19L, 19L, 19L, 19L, 
19L, 19L, 19L, 19L, 19L, 19L, 19L, 19L, 19L, 19L, 19L, 19L, 20L, 
20L, 20L, 20L, 20L, 20L, 20L, 20L, 20L, 20L, 20L, 20L, 20L, 20L, 
20L, 20L, 20L, 20L, 20L, 20L, 20L, 20L, 20L, 20L, 20L, 20L, 20L, 
20L, 20L, 20L, 20L, 20L, 20L, 21L, 21L, 21L, 21L, 21L, 21L, 21L, 
21L, 21L, 21L, 21L, 21L, 21L, 21L, 21L, 21L, 21L, 21L, 21L, 21L, 
21L, 21L, 21L, 21L, 21L, 21L, 21L, 21L, 21L, 21L, 21L, 21L, 21L, 
22L, 22L, 22L, 22L, 22L, 22L, 22L, 22L, 22L, 22L, 22L, 22L, 22L, 
22L, 22L, 22L, 22L, 22L, 22L, 22L, 22L, 22L, 22L, 22L, 22L, 22L, 
22L, 22L, 22L, 22L, 22L, 22L, 22L, 23L, 23L, 23L, 23L, 23L, 23L, 
23L, 23L, 23L, 23L, 23L, 23L, 23L, 23L, 23L, 23L, 23L, 23L, 23L, 
23L, 23L, 23L, 23L, 23L, 23L, 23L, 23L, 23L, 23L, 23L, 23L, 23L, 
23L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 
2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 
2L, 2L, 2L, 24L, 24L, 24L, 24L, 24L, 24L, 24L, 24L, 24L, 24L, 
24L, 24L, 24L, 24L, 24L, 24L, 24L, 24L, 24L, 24L, 24L, 24L, 24L, 
24L, 24L, 24L, 24L, 24L, 24L, 24L, 24L, 24L, 24L, 25L, 25L, 25L, 
25L, 25L, 25L, 25L, 25L, 25L, 25L, 25L, 25L, 25L, 25L, 25L, 25L, 
25L, 25L, 25L, 25L, 25L, 25L, 25L, 25L, 25L, 25L, 25L, 25L, 25L, 
25L, 25L, 25L, 25L), levels = c("student M13", "student F6", 
"student L12", "student S19", "student D4", "student V22", "student G7", 
"student H8", "student W23", "student R18", "student N14", "student O15", 
"student Q17", "student I9", "student B2", "student P16", "student C3", 
"student A1", "student K11", "student X24", "student E5", "student U21", 
"student Y25", "student T20", "student J10"), class = "factor"), 
    Performance = c(3.43, 3.43, 3.43, 3.43, 3.43, 3.43, 3.43, 
    3.43, 3.43, 3.43, 3.43, 3.43, 3.43, 3.43, 3.43, 3.43, 3.43, 
    3.43, 3.43, 3.43, 3.43, 3.43, 3.43, 3.43, 3.43, 3.43, 3.43, 
    3.43, 3.43, 3.43, 3.43, 3.43, 3.43, 3.47, 3.47, 3.47, 3.47, 
    3.47, 3.47, 3.47, 3.47, 3.47, 3.47, 3.47, 3.47, 3.47, 3.47, 
    3.47, 3.47, 3.47, 3.47, 3.47, 3.47, 3.47, 3.47, 3.47, 3.47, 
    3.47, 3.47, 3.47, 3.47, 3.47, 3.47, 3.47, 3.47, 3.47, 3.23, 
    3.23, 3.23, 3.23, 3.23, 3.23, 3.23, 3.23, 3.23, 3.23, 3.23, 
    3.23, 3.23, 3.23, 3.23, 3.23, 3.23, 3.23, 3.23, 3.23, 3.23, 
    3.23, 3.23, 3.23, 3.23, 3.23, 3.23, 3.23, 3.23, 3.23, 3.23, 
    3.23, 3.23, 3.35, 3.35, 3.35, 3.35, 3.35, 3.35, 3.35, 3.35, 
    3.35, 3.35, 3.35, 3.35, 3.35, 3.35, 3.35, 3.35, 3.35, 3.35, 
    3.35, 3.35, 3.35, 3.35, 3.35, 3.35, 3.35, 3.35, 3.35, 3.35, 
    3.35, 3.35, 3.35, 3.35, 3.35, 3.23, 3.23, 3.23, 3.23, 3.23, 
    3.23, 3.23, 3.23, 3.23, 3.23, 3.23, 3.23, 3.23, 3.23, 3.23, 
    3.23, 3.23, 3.23, 3.23, 3.23, 3.23, 3.23, 3.23, 3.23, 3.23, 
    3.23, 3.23, 3.23, 3.23, 3.23, 3.23, 3.23, 3.23, 3.25, 3.25, 
    3.25, 3.25, 3.25, 3.25, 3.25, 3.25, 3.25, 3.25, 3.25, 3.25, 
    3.25, 3.25, 3.25, 3.25, 3.25, 3.25, 3.25, 3.25, 3.25, 3.25, 
    3.25, 3.25, 3.25, 3.25, 3.25, 3.25, 3.25, 3.25, 3.25, 3.25, 
    3.25, 2.91, 2.91, 2.91, 2.91, 2.91, 2.91, 2.91, 2.91, 2.91, 
    2.91, 2.91, 2.91, 2.91, 2.91, 2.91, 2.91, 2.91, 2.91, 2.91, 
    2.91, 2.91, 2.91, 2.91, 2.91, 2.91, 2.91, 2.91, 2.91, 2.91, 
    2.91, 2.91, 2.91, 2.91, 2.83, 2.83, 2.83, 2.83, 2.83, 2.83, 
    2.83, 2.83, 2.83, 2.83, 2.83, 2.83, 2.83, 2.83, 2.83, 2.83, 
    2.83, 2.83, 2.83, 2.83, 2.83, 2.83, 2.83, 2.83, 2.83, 2.83, 
    2.83, 2.83, 2.83, 2.83, 2.83, 2.83, 2.83, 3.87, 3.87, 3.87, 
    3.87, 3.87, 3.87, 3.87, 3.87, 3.87, 3.87, 3.87, 3.87, 3.87, 
    3.87, 3.87, 3.87, 3.87, 3.87, 3.87, 3.87, 3.87, 3.87, 3.87, 
    3.87, 3.87, 3.87, 3.87, 3.87, 3.87, 3.87, 3.87, 3.87, 3.87, 
    3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 
    3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3.07, 3.07, 3.07, 
    3.07, 3.07, 3.07, 3.07, 3.07, 3.07, 3.07, 3.07, 3.07, 3.07, 
    3.07, 3.07, 3.07, 3.07, 3.07, 3.07, 3.07, 3.07, 3.07, 3.07, 
    3.07, 3.07, 3.07, 3.07, 3.07, 3.07, 3.07, 3.07, 3.07, 3.07, 
    3.67, 3.67, 3.67, 3.67, 3.67, 3.67, 3.67, 3.67, 3.67, 3.67, 
    3.67, 3.67, 3.67, 3.67, 3.67, 3.67, 3.67, 3.67, 3.67, 3.67, 
    3.67, 3.67, 3.67, 3.67, 3.67, 3.67, 3.67, 3.67, 3.67, 3.67, 
    3.67, 3.67, 3.67, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 
    3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 
    3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 
    3.1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 
    3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3.5, 3.5, 3.5, 
    3.5, 3.5, 3.5, 3.5, 3.5, 3.5, 3.5, 3.5, 3.5, 3.5, 3.5, 3.5, 
    3.5, 3.5, 3.5, 3.5, 3.5, 3.5, 3.5, 3.5, 3.5, 3.5, 3.5, 3.5, 
    3.5, 3.5, 3.5, 3.5, 3.5, 3.5, 3.27, 3.27, 3.27, 3.27, 3.27, 
    3.27, 3.27, 3.27, 3.27, 3.27, 3.27, 3.27, 3.27, 3.27, 3.27, 
    3.27, 3.27, 3.27, 3.27, 3.27, 3.27, 3.27, 3.27, 3.27, 3.27, 
    3.27, 3.27, 3.27, 3.27, 3.27, 3.27, 3.27, 3.27, 3.33, 3.33, 
    3.33, 3.33, 3.33, 3.33, 3.33, 3.33, 3.33, 3.33, 3.33, 3.33, 
    3.33, 3.33, 3.33, 3.33, 3.33, 3.33, 3.33, 3.33, 3.33, 3.33, 
    3.33, 3.33, 3.33, 3.33, 3.33, 3.33, 3.33, 3.33, 3.33, 3.33, 
    3.33, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 
    3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 
    3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.1, 3.15, 3.15, 
    3.15, 3.15, 3.15, 3.15, 3.15, 3.15, 3.15, 3.15, 3.15, 3.15, 
    3.15, 3.15, 3.15, 3.15, 3.15, 3.15, 3.15, 3.15, 3.15, 3.15, 
    3.15, 3.15, 3.15, 3.15, 3.15, 3.15, 3.15, 3.15, 3.15, 3.15, 
    3.15, 3.5, 3.5, 3.5, 3.5, 3.5, 3.5, 3.5, 3.5, 3.5, 3.5, 3.5, 
    3.5, 3.5, 3.5, 3.5, 3.5, 3.5, 3.5, 3.5, 3.5, 3.5, 3.5, 3.5, 
    3.5, 3.5, 3.5, 3.5, 3.5, 3.5, 3.5, 3.5, 3.5, 3.5, 3.57, 3.57, 
    3.57, 3.57, 3.57, 3.57, 3.57, 3.57, 3.57, 3.57, 3.57, 3.57, 
    3.57, 3.57, 3.57, 3.57, 3.57, 3.57, 3.57, 3.57, 3.57, 3.57, 
    3.57, 3.57, 3.57, 3.57, 3.57, 3.57, 3.57, 3.57, 3.57, 3.57, 
    3.57, 3.56, 3.56, 3.56, 3.56, 3.56, 3.56, 3.56, 3.56, 3.56, 
    3.56, 3.56, 3.56, 3.56, 3.56, 3.56, 3.56, 3.56, 3.56, 3.56, 
    3.56, 3.56, 3.56, 3.56, 3.56, 3.56, 3.56, 3.56, 3.56, 3.56, 
    3.56, 3.56, 3.56, 3.56, 3.73, 3.73, 3.73, 3.73, 3.73, 3.73, 
    3.73, 3.73, 3.73, 3.73, 3.73, 3.73, 3.73, 3.73, 3.73, 3.73, 
    3.73, 3.73, 3.73, 3.73, 3.73, 3.73, 3.73, 3.73, 3.73, 3.73, 
    3.73, 3.73, 3.73, 3.73, 3.73, 3.73, 3.73, 3.53, 3.53, 3.53, 
    3.53, 3.53, 3.53, 3.53, 3.53, 3.53, 3.53, 3.53, 3.53, 3.53, 
    3.53, 3.53, 3.53, 3.53, 3.53, 3.53, 3.53, 3.53, 3.53, 3.53, 
    3.53, 3.53, 3.53, 3.53, 3.53, 3.53, 3.53, 3.53, 3.53, 3.53, 
    3.2, 3.2, 3.2, 3.2, 3.2, 3.2, 3.2, 3.2, 3.2, 3.2, 3.2, 3.2, 
    3.2, 3.2, 3.2, 3.2, 3.2, 3.2, 3.2, 3.2, 3.2, 3.2, 3.2, 3.2, 
    3.2, 3.2, 3.2, 3.2, 3.2, 3.2, 3.2, 3.2, 3.2)), row.names = c(NA, 
-825L), class = c("tbl_df", "tbl", "data.frame"))
adkane
  • 1,429
  • 14
  • 29
  • How should ties be handled? eg if Anna and Billy had the same score and wanted the same project? – zephryl Mar 13 '23 at 12:34
  • @zephryl good point but in my case ties will be extremely unlikely. – adkane Mar 13 '23 at 12:39
  • (A) Is it possible that one person has different performance in different projects? (B) Can every person work in every project? – GKi Mar 13 '23 at 17:16
  • Should there be made some optimization? E.g. minimize the sum of taken Ranks. Assume best performer Anna's ranking will be A, then C then B, Billy A, B, C and James B, A, C. If first Anna with A, than Billy with B and finally James with C the sum of their Ranks = 6, But when Anna gets C, Billy A and James B the sum would be 4. – GKi Mar 14 '23 at 05:03
  • Every person will select all projects but their performance is a constant. I'm not actually sure if that optimization is needed though. – adkane Mar 14 '23 at 08:33

6 Answers6

10

This's an iterative solution with purrr::reduce. This approach can also show the rank of a project chosen by each person, e.g. James chooses the project D which is his 3rd ranked option.

library(dplyr)
library(purrr)

df %>%
  arrange(desc(Performance), Rank) %>%
  split(.$Person) %>% # group_split(Person)
  reduce(~ bind_rows(.x, head(anti_join(.y, .x, by = 'Project'), n = 1)),
         .init = tibble(Project = character(0)))

# # A tibble: 3 × 4
#   Project  Rank Person Performance
#   <chr>   <int> <chr>        <int>
# 1 A           1 Anna            97
# 2 B           2 Billy           95
# 3 D           3 James           92
  1. Arrange the data by Performance and Rank, so that the person with higher Performance is prior to choose a project.

  2. Split the data frame into a list by Person.

  3. Iterate from high Performance to low:

    • In Anna's turn, she has the highest priority to choose her first ranked project, A.
    • In Billy's turn, his first ranked project, A, has been chosen by Anna, so remove that option from his project list. Billy turns out to choose his second ranked project, B.
    • In James's turn, his first two ranked project, A & B, has been chosen by others, so remove those options from his project list. James can't but choose his third ranked project, D.
Darren Tsai
  • 32,117
  • 5
  • 21
  • 51
  • Darren, I really liked your tidyverse focussed code but when I tried it on my actual dataset it didn't give me the result I expected. If you have the chance you might take a look. The best ranked student (student N14) does not get their top choice when I run your code. I've added the new data to my question. Cheers. – adkane Jun 26 '23 at 16:13
4

A way in base might look like:

  1. Create unique vector of Persons sorted them by their performance.
  2. Split the Projects by Person and sort them by their ranking.
  3. Assign (remaining) highest ranked Project to each Person, starting by the Person with the highest performance using Reduce.
#Get Persons ordered by Performance
P <- which(!duplicated(DF$Person))
P <- DF$Person[P[order(DF$Performance[P], decreasing = TRUE)]]

#Split by person and order by Rank
. <- lapply(split(DF[c("Project", "Rank")], DF$Person), \(x) x[[1]][x[[2]]])

#Get highest ranked (remaining) project per Person
setNames(Reduce(\(x, y) {c(x, y[!y %in% x][1])}, .[P[-1]], .[[P[1]]][1]), P)
# Anna Billy James 
#  "A"   "B"   "D" 

Data:

DF <- read.table(header=TRUE, text="Project Rank Person Performance 
A        1   Billy   95
B        2   Billy   95
C        3   Billy   95
D        4   Billy   95
E        5   Billy   95
A        1   Anna    97
B        2   Anna    97
C        3   Anna    97
D        5   Anna    97
E        4   Anna    97
A        1   James   92
B        2   James   92
C        4   James   92
D        3   James   92
E        5   James   92")

Benchmark

library(dplyr)
library(purrr)
library(data.table)

bench::mark(check=FALSE,
Maël = {l <- DF %>% 
  mutate(perf_rank = dense_rank(-Performance)) %>% 
  group_split(perf_rank, Person)

choice = setNames(character(length(l)), unique(DF$Person[order(-DF$Performance)]))
for(i in seq_along(l)){
  tmp <- l[[i]]
  choice[i] <- tmp$Project[which.min(tmp$Rank)]
  l <- lapply(l, \(x) subset(x, x$Project != choice[i]))
}  
choice},
"Darren Tsai" = {DF %>%
  arrange(desc(Performance), Rank) %>%
  split(.$Person) %>% # group_split(Person)
  reduce(~ bind_rows(.x, head(anti_join(.y, .x, by = 'Project'), n = 1)),
         .init = tibble(Project = character(0)))},
IceCreamToucan = {df <- as.data.table(DF)
setkey(df, Person, Rank)
person <- df[order(-Performance), unique(Person)]
project <- Reduce(
  \(project, person) c(project, df[person, setdiff(Project, project)[1]]), 
  person, init = character())
df[data.table(Person = person, Project = project), on = .(Person, Project)]},
"Nobody/alexis" = {mat = xtabs(Performance / Rank ~ Person + Project, DF); mrg = matchingR::galeShapley.marriageMarket(mat, t(mat))$engagements; data.frame(pers = rownames(mat), proj = colnames(mat)[mrg])},
"alexis_laz" = {mat = xtabs(Performance * Rank ~ Person + Project, DF)
x = clue::solve_LSAP(mat, maximum = FALSE)
data.frame(pers = rownames(mat), proj = colnames(mat)[x])},
GKi = {P <- which(!duplicated(DF$Person))
P <- DF$Person[P[order(DF$Performance[P], decreasing = TRUE)]]
. <- lapply(split(DF[c("Project", "Rank")], DF$Person), \(x) x[[1]][x[[2]]])
setNames(Reduce(\(A, B) {c(A, B[!B %in% A][1])}, .[P[-1]], .[[P[1]]][1]), P)}
)

Result

  expression          min   median itr/se…¹ mem_al…² gc/se…³ n_itr  n_gc total…⁴
  <bch:expr>     <bch:tm> <bch:tm>    <dbl> <bch:by>   <dbl> <int> <dbl> <bch:t>
1 Maël             7.45ms   7.62ms     129.  55.91KB    22.4    52     9   402ms
2 Darren Tsai       5.5ms   5.61ms     176.  18.98KB    18.6    76     8   431ms
3 IceCreamToucan   1.98ms   2.05ms     478. 385.87KB    15.4   218     7   456ms
4 Nobody/alexis  460.87µs 478.88µs    2026.   4.98KB    19.0   960     9   474ms
5 alexis_laz     425.96µs 457.64µs    2075.     496B    19.1   980     9   472ms
6 GKi            146.92µs 156.23µs    6075.  23.35KB    24.4  2491    10   410ms

In this case GKi1 is the fastest, about 3 times faster than alexis_laz the second. alexis_laz allocates the lowest amount of memory.

GKi
  • 37,245
  • 2
  • 26
  • 48
3

Here a way with a for loop. First, split by groups according to performance (and Person, if several have same performance). First should be the one with the best performance.

Then, in a for loop, choose iteratively the Project with the lowest rank, and remove that one for the other individuals.

l <- split(df, list(-df$Performance, df$Person), drop = TRUE)
choice = setNames(character(length(l)), unique(df$Person[order(-df$Performance)]))
for(i in seq_along(l)){
  tmp <- l[[i]]
  choice[i] <- tmp$Project[which.min(tmp$Rank)]
  l <- lapply(l, \(x) subset(x, x$Project != choice[i]))
}  
choice
# Anna Billy James 
#  "A"   "B"   "D" 
Maël
  • 45,206
  • 3
  • 29
  • 67
  • I tried your answer with me new, real dataset and it's not working as expected. student N14 has the best grade but comes out with their last choice. I've added the new dataset if you have time to investigate. – adkane Jun 26 '23 at 16:42
2

Using data.table

library(data.table)
setDT(df)

setkey(df, Person, Rank)
person <- df[order(-Performance), unique(Person)]
project <- Reduce(
  \(project, person) c(project, df[person, setdiff(Project, project)[1]]), 
  person, init = character())
df[data.table(Person = person, Project = project), on = .(Person, Project)]

#> Key: <Person, Rank>
#>    Project  Rank Person Performance
#>     <char> <int> <char>       <int>
#> 1:       A     1   Anna          97
#> 2:       B     2  Billy          95
#> 3:       D     3  James          92
IceCreamToucan
  • 28,083
  • 2
  • 22
  • 38
  • Your answer looks like the only one that worked as expect when I tried it on real data (see my edited answer). Thank you. – adkane Jul 07 '23 at 16:19
2

After a bit of searching, it seems that clue::solve_LSAP could be of use here.

First, we make a matrix of a "score"/"cost" metric between persons and projects; here we could use a score set as 'performance * rank' (DF copied from GKi's answer):

mat = xtabs(Performance * Rank ~ Person + Project, DF)
mat
#       Project
#Person    A   B   C   D   E
#  Anna   97 194 291 485 388
#  Billy  95 190 285 380 475
#  James  92 184 368 276 460

Then, we assign persons to projects so that the summed "cost" is minimized:

library(clue)
x = solve_LSAP(mat, maximum = FALSE)
x
#Optimal assignment:
#1 => 1, 2 => 2, 3 => 4
data.frame(pers = rownames(mat), proj = colnames(mat)[x])
#   pers proj
#1  Anna    A
#2 Billy    B
#3 James    D
alexis_laz
  • 12,884
  • 4
  • 27
  • 37
1

The problem as described is a special case of the stable matching problem. There are (at least) two R packages for solving this problem. At a glance the documentation to matchingR looks a bit more approachable, and the authors claim to have used it for problems with approximately 30,000 matching candidates, so performance should be fine for this application.

Note also that the algorithm used in these packages will also handle the general case where the workers have different project preferences and/or the performance is project-dependent.

Nobody
  • 153
  • 7
  • 2
    That looks really interesting. I believe a complementary R code would be: `mat = xtabs(Performance / Rank ~ Person + Project, DF); mrg = matchingR::galeShapley.marriageMarket(mat, t(mat))$engagements; data.frame(pers = rownames(mat), proj = colnames(mat)[mrg])` – alexis_laz Mar 14 '23 at 13:32