Skip to content

Commit 0324d57

Browse files
authored
Merge pull request #261 from GatorEducator/test/group-graph
Test Case, Documentation, Refactoring for `group_graph.py`
2 parents 019ee7f + 1a24c04 commit 0324d57

File tree

9 files changed

+355
-28
lines changed

9 files changed

+355
-28
lines changed

README.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,75 @@ pipenv run python3 gatorgrouper_cli.py --debug
244244

245245
If neither of these flags are set, logging will only be shown if an error occurs.
246246

247+
### Kernighan-Lin Grouping Method
248+
249+
The Kernighan-Lin algorithm creates a k-way graph partition that determines the
250+
grouping of students based on their preferences for working with other students
251+
and compatibility with other classmates. The graph recognizes student compatibility
252+
through numerical weights (indicators of student positional relationship on the graph).
253+
This grouping method allows for a systematic approach and balanced number of student
254+
groups capable of tackling different types of work. Students should enter student
255+
name, number of groups, objective weights (optional), objective_measures(optional),
256+
students preferred to work with (optional), preference weight(optional),
257+
and preferences_weight_match(optional). Note that number of groups must be at
258+
least 2 and be a power of 2, i.e. 2, 4, 8...
259+
260+
NOTE: `--method graph` and `--num-group` are required to create groups.
261+
262+
It is required to use the graph argument to generate groups through the graph
263+
partitioning. To generate groups using the Kernighan-Lin grouping algorithm use
264+
the flag `--method graph`
265+
266+
```shell
267+
pipenv run python gatorgrouper_cli.py --file filepath --method graph
268+
--num-group NUMBER
269+
```
270+
271+
To load student preferences, a preference weight, use the flag `--preferences`
272+
273+
```shell
274+
pipenv run python gatorgrouper_cli.py --file filepath --method graph
275+
--num-group NUMBER --preferences filepath
276+
```
277+
278+
To indicate student preference weight use the flag `--preferences_weight`
279+
280+
```shell
281+
pipenv run python gatorgrouper_cli.py --file filepath --method graph
282+
--num-group NUMBER --preferences filepath --preferences_weight PREFERENCES_WEIGHT
283+
```
284+
285+
To indicate preference weight match use the flag `--preferences_weight_match`
286+
287+
```shell
288+
pipenv run python gatorgrouper_cli.py --file filepath --method graph
289+
--num-group NUMBER --preferences filepath --preferences_weight PREFERENCES_WEIGHT
290+
--preferences_weight_match PREFERENCES_WEIGHT_MATCH
291+
```
292+
293+
To add objective measures use the flag `--objective_measures`
294+
295+
```shell
296+
pipenv run python gatorgrouper_cli.py --file filepath --method graph
297+
--num-group NUMBER --objective_measures LIST --objective_weights LIST
298+
```
299+
300+
To add objective weights use the flag `--objective_weights`
301+
302+
```shell
303+
pipenv run python gatorgrouper_cli.py --file filepath --method graph
304+
--num-group NUMBER --objective_measures LIST --objective_weights LIST
305+
```
306+
307+
A command line of all agruments would be:
308+
309+
```shell
310+
pipenv run python gatorgrouper_cli.py --file filepath --method graph
311+
--num-group NUMBER --preferences filepath --preferences-weight PREFERENCES_WEIGHT
312+
--preferences-weight-match PREFERENCES_WEIGHT_MATCH --objective-measures LIST
313+
--objective-weights LIST
314+
```
315+
247316
### Full Example
248317

249318
```shell

gatorgrouper/utils/constants.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
DEFAULT_GRPSIZE = 3
99
DEFAULT_NUMGRP = 3
1010
DEFAULT_ABSENT = ""
11+
DEFAULT_PREFERENCES = None
12+
DEFAULT_PREFERENCES_WEIGHT = 1.1
13+
DEFAULT_PREFERENCES_WEIGHT_MATCH = 1.3
14+
DEFAULT_OBJECTIVE_WEIGHTS = None
15+
DEFAULT_OBJECTIVE_MEASURES = None
1116

1217
# assertion
1318
NONE = ""

gatorgrouper/utils/group_graph.py

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,18 @@
1010

1111
def recursive_kl(graph: Graph, numgrp=2) -> List[Set[int]]:
1212
"""
13-
Recursively use Kernighan-Lin algorithm to create a k-way graph partition
13+
Recursively use the Kernighan-Lin algorithm to create a k-way graph partition.
14+
This function will either return two groups or more than two depending on the
15+
value of numgrp. Each group generated is different from the previous.
1416
"""
1517
power = log(numgrp, 2)
1618
if power != int(power) or power < 1:
1719
raise ValueError("numgrp must be a power of 2 and at least 2.")
20+
# For a group of two bisect it and return two groups
1821
if numgrp == 2:
1922
# Base case for recursion: use Kernighan-Lin to create 2 groups
2023
return list(kernighan_lin_bisection(graph))
24+
# For the next group of two divide numgrp by 2
2125
next_numgrp = numgrp / 2
2226
groups = []
2327
for subset in kernighan_lin_bisection(graph):
@@ -31,10 +35,11 @@ def total_cut_size(graph: Graph, partition: List[int]) -> float:
3135
Computes the sum of weights of all edges between different subsets in the partition
3236
"""
3337
cut = 0.0
38+
# Edges are added from the nodes on the graph, creating subsets
3439
for i, subset1 in enumerate(partition):
3540
for subset2 in partition[i:]:
41+
# Sum of weights added from all subsets and set equal to cut
3642
cut += cut_size(graph, subset1, T=subset2)
37-
print(subset1, subset2, cut)
3843
return cut
3944

4045

@@ -58,10 +63,13 @@ def compatibility(
5863
If no measures are specified, "avg" is used as a default.
5964
"""
6065
if not len(a) == len(b):
61-
raise Exception("Tuples passed to compatibility() must have same size")
66+
# Raise an exception notice if student tuples don't match
67+
raise Exception("Tuples passed to compatibility() must have same size.")
6268
if objective_weights is None:
69+
# Return length
6370
objective_weights = [1] * len(a)
6471
if objective_measures is None:
72+
# Default to return average if set equal to None
6573
objective_measures = ["avg"] * len(a)
6674
scores = []
6775
for a_score, b_score, weight, measure in zip(
@@ -80,6 +88,8 @@ def compatibility(
8088
compat = int(a_score == b_score)
8189
elif measure == "diff":
8290
compat = abs(a_score - b_score)
91+
else:
92+
raise Exception("Invalid measure")
8393

8494
# Scale the compatibility of a[i] and b[i] using the i-th objective weight
8595
scores.append(compat * weight)
@@ -96,7 +106,8 @@ def group_graph_partition(
96106
preferences_weight_match=1.3,
97107
):
98108
"""
99-
Form groups using recursive Kernighan-Lin algorithm
109+
Form groups using recursive Kernighan-Lin algorithm by reading in students list
110+
and weight list and partitioning the vertices.
100111
"""
101112
# Read in students list and the weight list
102113
students = [item[0] for item in inputlist]
@@ -133,17 +144,3 @@ def group_graph_partition(
133144
for p in partition:
134145
groups.append([inputlist[i] for i in p])
135146
return groups
136-
137-
138-
if __name__ == "__main__":
139-
student_data = [
140-
["one", 0, 0],
141-
["two", 0, 0.5],
142-
["three", 0.5, 0],
143-
["four", 0.75, 0.75],
144-
["five", 0.8, 0.1],
145-
["six", 0, 1],
146-
["seven", 1, 0],
147-
["eight", 1, 1],
148-
]
149-
student_groups = group_graph_partition(student_data, 4)

gatorgrouper/utils/parse_arguments.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,46 @@ def parse_arguments(args):
6666
required=False,
6767
)
6868

69+
gg_parser.add_argument(
70+
"--preferences",
71+
help="Preferences of students for graph algorithm",
72+
type=str,
73+
default=constants.DEFAULT_PREFERENCES,
74+
required=False,
75+
)
76+
77+
gg_parser.add_argument(
78+
"--preferences-weight",
79+
help="Prefered weights",
80+
type=float,
81+
default=constants.DEFAULT_PREFERENCES_WEIGHT,
82+
required=False,
83+
)
84+
85+
gg_parser.add_argument(
86+
"--preferences-weight-match",
87+
help="Prefered matching weights",
88+
type=float,
89+
default=constants.DEFAULT_PREFERENCES_WEIGHT_MATCH,
90+
required=False,
91+
)
92+
93+
gg_parser.add_argument(
94+
"--objective-weights",
95+
help="Objective weights for compatibility input csv file",
96+
type=list,
97+
default=constants.DEFAULT_OBJECTIVE_WEIGHTS,
98+
required=False,
99+
)
100+
101+
gg_parser.add_argument(
102+
"--objective-measures",
103+
help="Objective measures for compatibility input csv file: sum, avg, max, min, match, diff",
104+
type=list,
105+
default=constants.DEFAULT_OBJECTIVE_MEASURES,
106+
required=False,
107+
)
108+
69109
gg_arguments_finished = gg_parser.parse_args(args)
70110

71111
logging.basicConfig(

gatorgrouper/utils/read_student_file.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
""" Reads CSV data file """
22

33
import csv
4+
import re
45
from pathlib import Path
56

67

@@ -28,8 +29,10 @@ def read_csv_data(filepath):
2829
temp.append(True)
2930
elif value.lower() == "false":
3031
temp.append(False)
31-
else:
32+
elif re.match(r"^\d+?\.\d+?$", value) or value.isdigit():
3233
temp.append(float(value))
34+
else:
35+
temp.append(value)
3336
responses.append(temp)
3437
else:
3538
for record in csvdata:
@@ -40,7 +43,9 @@ def read_csv_data(filepath):
4043
temp.append(True)
4144
elif value.lower() == "false":
4245
temp.append(False)
43-
else:
46+
elif re.match(r"^\d+?\.\d+?$", value) or value.isdigit():
4447
temp.append(float(value))
48+
else:
49+
temp.append(value)
4550
responses.append(temp)
4651
return responses

gatorgrouper_cli.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
# read in the student identifiers from the specified file
2424
input_list = read_student_file.read_csv_data(GG_ARGUMENTS.file)
25+
preference = dict(read_student_file.read_csv_data(GG_ARGUMENTS.preferences))
2526
check_if_arguments_valid = parse_arguments.check_valid(GG_ARGUMENTS, input_list)
2627
if check_if_arguments_valid is False:
2728
print("Incorrect command-line arguments.")
@@ -53,7 +54,13 @@
5354
)
5455
elif GG_ARGUMENTS.method == constants.ALGORITHM_GRAPH:
5556
GROUPED_STUDENT_IDENTIFIERS = group_graph.group_graph_partition(
56-
SHUFFLED_STUDENT_IDENTIFIERS, GG_ARGUMENTS.num_group
57+
SHUFFLED_STUDENT_IDENTIFIERS,
58+
GG_ARGUMENTS.num_group,
59+
preferences=preference,
60+
preferences_weight=GG_ARGUMENTS.preferences_weight,
61+
preferences_weight_match=GG_ARGUMENTS.preferences_weight_match,
62+
objective_weights=GG_ARGUMENTS.objective_weights,
63+
objective_measures=GG_ARGUMENTS.objective_measures,
5764
)
5865
else:
5966
GROUPED_STUDENT_IDENTIFIERS = group_creation.group_random_num_group(

tests/conftest.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,12 +97,19 @@ def generate_csv_no_header(tmpdir_factory):
9797
def generate_csv_float(tmpdir_factory):
9898
""" Generate a tempory sample csv """
9999
fn = tmpdir_factory.mktemp("data").join("csvNg.csv")
100-
headers = ["NAME", "Q1", "Q2", "Q3", "Q4"]
100+
headers = ["NAME", "Q1", "Q2", "Q3", "Q4", "Q5"]
101101
with open(str(fn), "w") as csvfile:
102102
writer = csv.DictWriter(csvfile, fieldnames=headers)
103103
writer.writeheader()
104104
writer.writerow(
105-
{"NAME": "delgrecoj", "Q1": "1.2", "Q2": "1.1", "Q3": "0.9", "Q4": "2.3"}
105+
{
106+
"NAME": "delgrecoj",
107+
"Q1": "1.2",
108+
"Q2": "1.1",
109+
"Q3": "0.9",
110+
"Q4": "2.3",
111+
"Q5": "Name",
112+
}
106113
)
107114
return str(fn)
108115

@@ -113,8 +120,8 @@ def generate_csv_float_no_header(tmpdir_factory):
113120
fn = tmpdir_factory.mktemp("data").join("csvNg1.csv")
114121
data = [
115122
# optionally include headers as the first entry
116-
["delgrecoj", "1.2", "0.7", "1.1", "0.2"],
117-
["delgrecoj2", "0.1", "0.5", "0.8", "0.6"],
123+
["delgrecoj", "1.2", "0.7", "1.1", "0.2", "Name"],
124+
["delgrecoj2", "0.1", "0.5", "0.8", "0.6", "Name"],
118125
]
119126
csv_string = ""
120127
for entry in data:

0 commit comments

Comments
 (0)