001package org.cpsolver.studentsct.report; 002 003import java.io.File; 004import java.text.DecimalFormat; 005import java.util.ArrayList; 006import java.util.HashMap; 007import java.util.HashSet; 008import java.util.List; 009import java.util.Map; 010import java.util.Set; 011 012import org.cpsolver.coursett.Constants; 013import org.cpsolver.coursett.model.Placement; 014import org.cpsolver.coursett.model.RoomLocation; 015import org.cpsolver.coursett.model.TimeLocation; 016import org.cpsolver.ifs.assignment.Assignment; 017import org.cpsolver.ifs.assignment.DefaultSingleAssignment; 018import org.cpsolver.ifs.util.CSVFile; 019import org.cpsolver.ifs.util.DataProperties; 020import org.cpsolver.ifs.util.DistanceMetric; 021import org.cpsolver.ifs.util.CSVFile.CSVField; 022import org.cpsolver.studentsct.StudentSectioningModel; 023import org.cpsolver.studentsct.StudentSectioningXMLLoader; 024import org.cpsolver.studentsct.extension.DistanceConflict; 025import org.cpsolver.studentsct.extension.StudentQuality; 026import org.cpsolver.studentsct.extension.TimeOverlapsCounter; 027import org.cpsolver.studentsct.extension.TimeOverlapsCounter.Conflict; 028import org.cpsolver.studentsct.model.AreaClassificationMajor; 029import org.cpsolver.studentsct.model.Choice; 030import org.cpsolver.studentsct.model.Config; 031import org.cpsolver.studentsct.model.Course; 032import org.cpsolver.studentsct.model.CourseRequest; 033import org.cpsolver.studentsct.model.Enrollment; 034import org.cpsolver.studentsct.model.FreeTimeRequest; 035import org.cpsolver.studentsct.model.Offering; 036import org.cpsolver.studentsct.model.Request; 037import org.cpsolver.studentsct.model.SctAssignment; 038import org.cpsolver.studentsct.model.Section; 039import org.cpsolver.studentsct.model.Student; 040import org.cpsolver.studentsct.model.Subpart; 041import org.cpsolver.studentsct.model.Unavailability; 042import org.cpsolver.studentsct.model.Request.RequestPriority; 043import org.cpsolver.studentsct.model.Student.StudentPriority; 044 045/** 046 * This class computes solution statistics report. 047 * <br> 048 * <br> 049 * 050 * Usage: new SolutionStatsReport(model).create(assignment, config).save(aFile); 051 * 052 * <br> 053 * <br> 054 * 055 * @author Tomáš Müller 056 * @version StudentSct 1.3 (Student Sectioning)<br> 057 * Copyright (C) 2013 - 2025 Tomáš Müller<br> 058 * <a href="mailto:muller@unitime.org">muller@unitime.org</a><br> 059 * <a href="http://muller.unitime.org">http://muller.unitime.org</a><br> 060 * <br> 061 * This library is free software; you can redistribute it and/or modify 062 * it under the terms of the GNU Lesser General Public License as 063 * published by the Free Software Foundation; either version 3 of the 064 * License, or (at your option) any later version. <br> 065 * <br> 066 * This library is distributed in the hope that it will be useful, but 067 * WITHOUT ANY WARRANTY; without even the implied warranty of 068 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 069 * Lesser General Public License for more details. <br> 070 * <br> 071 * You should have received a copy of the GNU Lesser General Public 072 * License along with this library; if not see 073 * <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>. 074 */ 075public class SolutionStatsReport extends AbstractStudentSectioningReport { 076 protected static DecimalFormat sIntFormat = new DecimalFormat("#,##0"); 077 protected static DecimalFormat sPercentFormat = new DecimalFormat("0.00"); 078 protected static DecimalFormat sDoubleFormat = new DecimalFormat("0.00"); 079 080 public SolutionStatsReport(StudentSectioningModel model) { 081 super(model); 082 } 083 084 public static interface StudentFilter { 085 public boolean matches(Student student); 086 } 087 088 public static class NotFilter implements StudentFilter { 089 StudentFilter iFilter; 090 public NotFilter(StudentFilter filter) { 091 iFilter = filter; 092 } 093 @Override 094 public boolean matches(Student student) { 095 return !iFilter.matches(student); 096 } 097 } 098 099 public static class OrFilter implements StudentFilter { 100 StudentFilter[] iFilters; 101 public OrFilter(StudentFilter... filters) { 102 iFilters = filters; 103 } 104 @Override 105 public boolean matches(Student student) { 106 for (StudentFilter filter: iFilters) 107 if (filter.matches(student)) return true; 108 return false; 109 } 110 } 111 112 public static class AndFilter implements StudentFilter { 113 StudentFilter[] iFilters; 114 public AndFilter(StudentFilter... filters) { 115 iFilters = filters; 116 } 117 @Override 118 public boolean matches(Student student) { 119 for (StudentFilter filter: iFilters) 120 if (!filter.matches(student)) return false; 121 return true; 122 } 123 } 124 125 public static class GroupFilter implements StudentFilter { 126 private String iGroup; 127 public GroupFilter(String group) { 128 iGroup = group; 129 } 130 @Override 131 public boolean matches(Student student) { 132 for (org.cpsolver.studentsct.model.StudentGroup g: student.getGroups()) 133 if (iGroup.equalsIgnoreCase(g.getReference())) return true; 134 return false; 135 } 136 } 137 138 public static class PriorityFilter implements StudentFilter { 139 private StudentPriority iPriority; 140 public PriorityFilter(StudentPriority p) { 141 iPriority = p; 142 } 143 @Override 144 public boolean matches(Student student) { 145 return student.getPriority() == iPriority; 146 } 147 } 148 149 public static class DummyFilter implements StudentFilter { 150 public DummyFilter() { 151 } 152 @Override 153 public boolean matches(Student student) { 154 return student.isDummy(); 155 } 156 } 157 158 public static class DummyOrNoRequestsFilter implements StudentFilter { 159 public DummyOrNoRequestsFilter() { 160 } 161 @Override 162 public boolean matches(Student student) { 163 return student.isDummy() || student.getRequests().isEmpty(); 164 } 165 } 166 167 public static class OnlineFilter implements StudentFilter { 168 public OnlineFilter() { 169 } 170 @Override 171 public boolean matches(Student student) { 172 for (org.cpsolver.studentsct.model.StudentGroup aac: student.getGroups()) { 173 if ("SCOVIDONL".equalsIgnoreCase(aac.getReference())) return true; 174 if ("SCONTONL".equalsIgnoreCase(aac.getReference())) return true; 175 if ("SCOVIDPMPE".equalsIgnoreCase(aac.getReference())) return true; 176 } 177 return false; 178 } 179 } 180 181 public static class AthletesFilter implements StudentFilter { 182 public AthletesFilter() { 183 } 184 @Override 185 public boolean matches(Student student) { 186 for (org.cpsolver.studentsct.model.StudentGroup aac: student.getGroups()) { 187 if ("SPORT".equalsIgnoreCase(aac.getType())) return true; 188 } 189 return false; 190 } 191 } 192 193 public static class OnlineLateFilter extends OnlineFilter { 194 public OnlineLateFilter() { 195 } 196 @Override 197 public boolean matches(Student student) { 198 if (!super.matches(student)) return false; 199 boolean hasOL = false; 200 boolean hasRS = false; 201 for (Request r: student.getRequests()) { 202 if (r instanceof CourseRequest) { 203 CourseRequest cr = (CourseRequest)r; 204 for (Course c: cr.getCourses()) { 205 if (c.getName().matches(".* [0-9]+I?OL(\\-[A-Za-z]+)?")) 206 hasOL = true; 207 else 208 hasRS = true; 209 } 210 } 211 } 212 return hasRS && !hasOL; 213 } 214 } 215 216 public static class StarFilter implements StudentFilter { 217 public StarFilter() { 218 } 219 @Override 220 public boolean matches(Student student) { 221 for (org.cpsolver.studentsct.model.StudentGroup aac: student.getGroups()) { 222 if (aac.getReference() != null && aac.getReference().startsWith("STAR")) return true; 223 if (aac.getReference() != null && aac.getReference().startsWith("VSTAR")) return true; 224 if (aac.getReference() != null && aac.getReference().startsWith("NewStCRF")) return true; 225 if (aac.getReference() != null && aac.getReference().startsWith("NewStOther")) return true; 226 } 227 return false; 228 } 229 } 230 231 private static StudentFilter FILTER_ALL = new AndFilter(new NotFilter(new DummyOrNoRequestsFilter()), new NotFilter(new OnlineLateFilter())); 232 private static StudentFilter FILTER_ALL_RES = new AndFilter(new NotFilter(new DummyOrNoRequestsFilter()), new NotFilter(new OnlineFilter())); 233 234 public enum StudentGroup implements StudentFilter { 235 ALL("All Students", FILTER_ALL), 236 237 DUMMY("Projected", new DummyFilter()), 238 // ONLINE_LATE("Online-Late", new OnlineLateFilter()), 239 240 PRIORITY("Priority", new AndFilter(new PriorityFilter(StudentPriority.Priority), FILTER_ALL)), 241 SENIOR("Seniors", new AndFilter(new PriorityFilter(StudentPriority.Senior), FILTER_ALL)), 242 JUNIOR("Juniors", new AndFilter(new PriorityFilter(StudentPriority.Junior), FILTER_ALL)), 243 SOPHOMORE("Sophomores", new AndFilter(new PriorityFilter(StudentPriority.Sophomore), FILTER_ALL)), 244 FRESHMEN("Freshmen", new AndFilter(new PriorityFilter(StudentPriority.Freshmen), FILTER_ALL)), 245 NORMAL("Non-priority", new AndFilter(new PriorityFilter(StudentPriority.Normal), FILTER_ALL)), 246 247 REBATCH("RE-BATCH", new AndFilter(new GroupFilter("RE-BATCH"), FILTER_ALL_RES)), 248 ONLINE("Online", new AndFilter(new OnlineFilter(), FILTER_ALL)), 249 GR_SCONTONL("SCONTONL", new AndFilter(new GroupFilter("SCONTONL"), FILTER_ALL)), 250 GR_SCOVIDONL("SCOVIDONL", new AndFilter(new GroupFilter("SCOVIDONL"), FILTER_ALL)), 251 GR_SCOVIDPMPE("SCOVIDPMPE", new AndFilter(new GroupFilter("SCOVIDPMPE"), FILTER_ALL)), 252 PREREG("PREREG", new AndFilter(new GroupFilter("PREREG"), FILTER_ALL_RES, new NotFilter(new StarFilter()))), 253 STAR("STAR", new AndFilter(new StarFilter(), FILTER_ALL_RES, new NotFilter(new GroupFilter("RE-BATCH")))), 254 GR_STAR("On-campus STAR", new AndFilter(new GroupFilter("STAR"), FILTER_ALL_RES, new NotFilter(new GroupFilter("RE-BATCH")))), 255 GR_VSTAR("Virtual STAR", new AndFilter(new GroupFilter("VSTAR"), FILTER_ALL_RES, new NotFilter(new GroupFilter("RE-BATCH")))), 256 OTHER("Other", new AndFilter(FILTER_ALL_RES, new NotFilter(new GroupFilter("RE-BATCH")), new NotFilter(new GroupFilter("PREREG")), new NotFilter(new StarFilter()))), 257 258 ATHLETES("Athletes", new AndFilter(new AthletesFilter(), FILTER_ALL)), 259 PRIORITY_ATHLETES("Priority\nAthletes", new AndFilter(new AthletesFilter(), new PriorityFilter(StudentPriority.Priority), FILTER_ALL)), 260 OTHER_ATHLETES("Other\nAthletes", new AndFilter(new AthletesFilter(), new NotFilter(new PriorityFilter(StudentPriority.Priority)), FILTER_ALL)), 261 ; 262 String iName; 263 StudentFilter iFilter; 264 StudentGroup(String name, StudentFilter filter) { 265 iName = name; 266 iFilter = filter; 267 } 268 public String getName() { return iName; } 269 270 @Override 271 public boolean matches(Student student) { return iFilter.matches(student); } 272 public boolean matches(Student student, StudentSectioningReport.Filter filter) { return iFilter.matches(student) && filter.matches(student); } 273 } 274 275 public static interface Statistic { 276 public String[] getValues(StudentGroup group, StudentSectioningModel model, Assignment<Request, Enrollment> assignment, StudentSectioningReport.Filter filter); 277 } 278 279 public enum Statistics { 280 NBR_STUDENTS( 281 "Number of Students", 282 "Number of students for which a schedule was computed", 283 new Statistic() { 284 @Override 285 public String[] getValues(StudentGroup group, StudentSectioningModel model, Assignment<Request, Enrollment> assignment, StudentSectioningReport.Filter filter) { 286 int count = 0; 287 for (Student student: model.getStudents()) { 288 if (!group.matches(student, filter)) continue; 289 count ++; 290 } 291 return new String[] {sIntFormat.format(count)}; 292 } 293 }), 294 COMPL_SCHEDULE( 295 new String[] {"Complete Schedule","- missing one course", "- missing two courses", "- missing three courses", "- missing four or more courses"}, 296 new String[] { 297 "Percentage of students with a complete schedule (all requested courses assigned or reaching max credit)", 298 "Students that did not get a requested course", 299 "Students that did not get two requested courses" 300 }, 301 new Statistic() { 302 @Override 303 public String[] getValues(StudentGroup group, StudentSectioningModel model, Assignment<Request, Enrollment> assignment, StudentSectioningReport.Filter filter) { 304 int total = 0; 305 int[] missing = new int[] {0, 0, 0, 0}; 306 int complete = 0; 307 for (Student student: model.getStudents()) { 308 if (!group.matches(student, filter)) continue; 309 total ++; 310 int nrRequests = 0; 311 int nrAssignedRequests = 0; 312 for (Request r : student.getRequests()) { 313 if (!(r instanceof CourseRequest)) continue; // ignore free times 314 if (!filter.matches(r)) continue; // check the filter 315 if (!r.isAlternative()) nrRequests++; 316 if (r.isAssigned(assignment)) nrAssignedRequests++; 317 } 318 if (nrAssignedRequests < nrRequests) { 319 missing[Math.min(nrRequests - nrAssignedRequests, missing.length) - 1] ++; 320 } 321 if (student.isComplete(assignment)) complete ++; 322 } 323 return new String[] { 324 sPercentFormat.format(100.0 * complete / total) + "%", 325 sPercentFormat.format(100.0 * missing[0] / total) + "%", 326 sPercentFormat.format(100.0 * missing[1] / total) + "%", 327 sPercentFormat.format(100.0 * missing[2] / total) + "%", 328 sPercentFormat.format(100.0 * missing[3] / total) + "%" 329 }; 330 } 331 }), 332 REQUESTED_COURSES( 333 new String[] { 334 "Requested Courses", "- pre-enrolled", "- impossible", 335 "Courses per Student", "Assigned Courses", "- 1st choice", "- 2nd choice", "- 3rd choice", "- 4th+ choice", "- substitute"}, 336 new String[] { 337 "Total number of requested courses by all students (not counting substitutes or alternatives)", 338 "Percentage of requested courses that were already enrolled (solver was not allowed to change)", 339 "Percentage of requested courses that have no possible enrollment (e.g., due to having all classes disabled)", 340 "The average number of course requested per student", 341 "Percentage of all course requests satisfied", 342 "Out of the above, the percentage of cases where the 1st choice course was given", 343 "2nd choice (1st alternative) course was given", "3rd choice course was given", "4th or later choice was given", 344 "a substitute course was given instead", 345 }, 346 new Statistic() { 347 @Override 348 public String[] getValues(StudentGroup group, StudentSectioningModel model, Assignment<Request, Enrollment> assignment, StudentSectioningReport.Filter filter) { 349 int requests = 0, students = 0, assigned = 0; 350 int fixed = 0, initial = 0; 351 int noenrl = 0; 352 int[] assignedChoice = new int[] {0, 0, 0, 0}; 353 int assignedSubst = 0; 354 int assignedChoiceTotal = 0; 355 for (Student student: model.getStudents()) { 356 if (!group.matches(student, filter)) continue; 357 students ++; 358 for (Request r : student.getRequests()) { 359 if (!(r instanceof CourseRequest)) continue; // ignore free times 360 if (!filter.matches(r)) continue; // check the filter 361 if (!r.isAlternative()) requests ++; 362 if (!r.isAlternative() && ((CourseRequest)r).isFixed()) fixed++; 363 if (!r.isAlternative() && ((CourseRequest)r).computeRandomEnrollments(assignment, 1).isEmpty()) noenrl ++; 364 Enrollment e = r.getAssignment(assignment); 365 if (r.getInitialAssignment() != null && r.getInitialAssignment().equals(e)) initial ++; 366 if (e != null) { 367 assigned ++; 368 if (r.isAlternative()) 369 assignedSubst ++; 370 else 371 assignedChoice[Math.min(e.getTruePriority(), assignedChoice.length - 1)] ++; 372 assignedChoiceTotal ++; 373 } 374 } 375 } 376 if (fixed == 0 && initial > 0) 377 fixed = initial; 378 if (requests == 0) 379 return new String[] { 380 sIntFormat.format(requests), 381 "", 382 "", 383 "", 384 "", 385 "", 386 "", 387 "", 388 "", 389 "", 390 }; 391 return new String[] { 392 sIntFormat.format(requests), 393 (fixed == 0 ? "" : sPercentFormat.format(100.0 * fixed / requests) + "%"), 394 (noenrl == 0 ? "" : sPercentFormat.format(100.0 * noenrl / requests) + "%"), 395 sDoubleFormat.format(((double)requests)/students), 396 sPercentFormat.format(100.0 * assigned / requests) + "%", 397 sPercentFormat.format(100.0 * assignedChoice[0] / assignedChoiceTotal) + "%", 398 sPercentFormat.format(100.0 * assignedChoice[1] / assignedChoiceTotal) + "%", 399 sPercentFormat.format(100.0 * assignedChoice[2] / assignedChoiceTotal) + "%", 400 sPercentFormat.format(100.0 * assignedChoice[3] / assignedChoiceTotal) + "%", 401 sPercentFormat.format(100.0 * assignedSubst / assignedChoiceTotal) + "%", 402 }; 403 } 404 }), 405 NOT_ASSIGNED_PRIORITY( 406 new String[] {"Not-assigned priority", "- 1st priority not assigned", 407 "- 2nd priority not assigned", "- 3rd priority not assigned", "- 4th priority not assigned", 408 "- 5th priority not assigned", "- 6th or later priority not assigned"}, 409 new String[] { 410 "The average priority of the course requests that were not satisfied", 411 "Number of cases where a student did not get a 1st priority course", 412 "Number of cases where a student did not get a 2nd priority course" 413 }, 414 new Statistic() { 415 @Override 416 public String[] getValues(StudentGroup group, StudentSectioningModel model, Assignment<Request, Enrollment> assignment, StudentSectioningReport.Filter filter) { 417 int[] notAssignedPriority = new int[] {0, 0, 0, 0, 0, 0}; 418 int notAssignedTotal = 0; 419 int avgPriority = 0; 420 for (Student student: model.getStudents()) { 421 if (!group.matches(student, filter)) continue; 422 for (Request r : student.getRequests()) { 423 if (!(r instanceof CourseRequest)) continue; // ignore free times 424 if (!filter.matches(r)) continue; // check the filter 425 Enrollment e = r.getAssignment(assignment); 426 if (e == null) { 427 if (!r.isAlternative()) { 428 notAssignedPriority[Math.min(r.getPriority(), notAssignedPriority.length - 1)] ++; 429 notAssignedTotal ++; 430 avgPriority += r.getPriority(); 431 } 432 } 433 } 434 } 435 if (notAssignedTotal == 0) 436 return new String[] { 437 "", 438 "", 439 "", 440 "", 441 "", 442 "", 443 "" 444 }; 445 return new String[] { 446 sDoubleFormat.format(1.0 + ((double)avgPriority) / notAssignedTotal), 447 sIntFormat.format(notAssignedPriority[0]), 448 sIntFormat.format(notAssignedPriority[1]), 449 sIntFormat.format(notAssignedPriority[2]), 450 sIntFormat.format(notAssignedPriority[3]), 451 sIntFormat.format(notAssignedPriority[4]), 452 sIntFormat.format(notAssignedPriority[5]) 453 }; 454 } 455 }, true), 456 ASSIGNED_COM(new String[] {"Assigned WC/OC", "Missing space in WC/OC"}, 457 new String[] { 458 "Number of students enrolled in a WC/OC course", 459 "Number of unassigned course requests in written/oral communication courses" 460 }, 461 new Statistic() { 462 String[] sComCourses = new String[] { 463 "AMST 10100", "CLCS 23100", "CLCS 23700", "CLCS 33900", 464 "COM 11400", "COM 20400", "COM 21700", "EDCI 20500", 465 "EDPS 31500", "ENGL 10600", "ENGL 10800", "HONR 19903", 466 "PHIL 26000", "SCLA 10100", "SCLA 10200", "SPAN 33000", 467 "EDCI 49600", "EDCI 49800", "EDPS 49800", "ENGL 30400", 468 "ENGL 38000", "HDFS 45000", 469 }; 470 private boolean isComCourse(Course course) { 471 for (String c: sComCourses) { 472 if (course.getName().startsWith(c)) return true; 473 } 474 return false; 475 } 476 @Override 477 public String[] getValues(StudentGroup group, StudentSectioningModel model, Assignment<Request, Enrollment> assignment, StudentSectioningReport.Filter filter) { 478 479 480 int assigned = 0, notAssigned = 0; 481 for (Student student: model.getStudents()) { 482 if (!group.matches(student, filter)) continue; 483 for (Request r : student.getRequests()) { 484 if (!(r instanceof CourseRequest)) continue; // ignore free times 485 if (!filter.matches(r)) continue; // check the filter 486 CourseRequest cr = (CourseRequest)r; 487 Enrollment e = cr.getAssignment(assignment); 488 if (e != null && isComCourse(e.getCourse())) { 489 assigned ++; 490 } else if (e == null && isComCourse(cr.getCourses().get(0)) && student.canAssign(assignment, r) && !r.isAlternative()) { 491 notAssigned ++; 492 } 493 } 494 } 495 return new String[] { sIntFormat.format(assigned), sIntFormat.format(notAssigned) }; 496 } 497 }, true), 498 LC(new String[] {"LC courses", "Assigned LC courses"}, 499 new String[] { 500 "Number of course requests with a matching LC reservation" 501 }, 502 new Statistic() { 503 @Override 504 public String[] getValues(StudentGroup group, StudentSectioningModel model, Assignment<Request, Enrollment> assignment, StudentSectioningReport.Filter filter) { 505 int assigned = 0, total = 0; 506 for (Student student: model.getStudents()) { 507 if (!group.matches(student, filter)) continue; 508 for (Request r : student.getRequests()) { 509 if (!(r instanceof CourseRequest)) continue; // ignore free times 510 if (!filter.matches(r)) continue; // check the filter 511 CourseRequest cr = (CourseRequest)r; 512 if (!cr.isAlternative() && cr.getRequestPriority() == RequestPriority.LC) { 513 total ++; 514 if (cr.isAssigned(assignment)) assigned ++; 515 } 516 } 517 } 518 if (total == 0) return new String[] { "N/A", ""}; 519 return new String[] { sIntFormat.format(total), sPercentFormat.format(100.0 * assigned / total) + "%" }; 520 } 521 }), 522 CRITICAL(new String[] {"Critical courses", "Assigned critical courses"}, 523 new String[] { 524 "Number of course requests marked as critical (~ course/group/placeholder critical in degree plan)" 525 }, 526 new Statistic() { 527 @Override 528 public String[] getValues(StudentGroup group, StudentSectioningModel model, Assignment<Request, Enrollment> assignment, StudentSectioningReport.Filter filter) { 529 int assigned = 0, total = 0; 530 for (Student student: model.getStudents()) { 531 if (!group.matches(student, filter)) continue; 532 for (Request r : student.getRequests()) { 533 if (!(r instanceof CourseRequest)) continue; // ignore free times 534 if (!filter.matches(r)) continue; // check the filter 535 CourseRequest cr = (CourseRequest)r; 536 if (!cr.isAlternative() && cr.getRequestPriority() == RequestPriority.Critical) { 537 total ++; 538 if (cr.isAssigned(assignment)) assigned ++; 539 } 540 } 541 } 542 if (total == 0) return new String[] { "N/A", ""}; 543 return new String[] { sIntFormat.format(total), sPercentFormat.format(100.0 * assigned / total) + "%" }; 544 } 545 }), 546 VITAL(new String[] {"Vital courses", "Assigned vital courses"}, 547 new String[] { 548 "Number of course requests marked as vital by advisors" 549 }, 550 new Statistic() { 551 @Override 552 public String[] getValues(StudentGroup group, StudentSectioningModel model, Assignment<Request, Enrollment> assignment, StudentSectioningReport.Filter filter) { 553 int assigned = 0, total = 0; 554 for (Student student: model.getStudents()) { 555 if (!group.matches(student, filter)) continue; 556 for (Request r : student.getRequests()) { 557 if (!(r instanceof CourseRequest)) continue; // ignore free times 558 if (!filter.matches(r)) continue; // check the filter 559 CourseRequest cr = (CourseRequest)r; 560 if (!cr.isAlternative() && cr.getRequestPriority() == RequestPriority.Vital) { 561 total ++; 562 if (cr.isAssigned(assignment)) assigned ++; 563 } 564 } 565 } 566 if (total == 0) return new String[] { "N/A", ""}; 567 return new String[] { sIntFormat.format(total), sPercentFormat.format(100.0 * assigned / total) + "%" }; 568 } 569 }), 570 IMPORTANT(new String[] {"Important courses", "Assigned important courses"}, 571 new String[] { 572 "Number of course requests marked as important (~ course/group/placeholder critical in the first choice major)" 573 }, 574 new Statistic() { 575 @Override 576 public String[] getValues(StudentGroup group, StudentSectioningModel model, Assignment<Request, Enrollment> assignment, StudentSectioningReport.Filter filter) { 577 int assigned = 0, total = 0; 578 for (Student student: model.getStudents()) { 579 if (!group.matches(student, filter)) continue; 580 for (Request r : student.getRequests()) { 581 if (!(r instanceof CourseRequest)) continue; // ignore free times 582 if (!filter.matches(r)) continue; // check the filter 583 CourseRequest cr = (CourseRequest)r; 584 if (!cr.isAlternative() && cr.getRequestPriority() == RequestPriority.Important) { 585 total ++; 586 if (cr.isAssigned(assignment)) assigned ++; 587 } 588 } 589 } 590 if (total == 0) return new String[] { "N/A", ""}; 591 return new String[] { sIntFormat.format(total), sPercentFormat.format(100.0 * assigned / total) + "%" }; 592 } 593 }, true), 594 PREFERENCES(new String[] {"Course requests with preferences", "Satisfied preferences", "- instructional method", "- classes"}, 595 new String[] { 596 "Course requests with IM or section preferences", 597 "Percentage of satisfied preferences (both class and IM)", 598 "Percentage of cases when the preferred instructional method was given to the student", 599 "Percentage of cases when the preferred class was given to the student" 600 }, 601 new Statistic() { 602 @Override 603 public String[] getValues(StudentGroup group, StudentSectioningModel model, Assignment<Request, Enrollment> assignment, StudentSectioningReport.Filter filter) { 604 int prefs = 0, configPrefs = 0, sectionPrefs = 0; 605 double sectionPref = 0.0, configPref = 0.0; 606 double satisfied = 0.0; 607 for (Student student: model.getStudents()) { 608 if (!group.matches(student, filter)) continue; 609 for (Request r : student.getRequests()) { 610 if (!(r instanceof CourseRequest)) continue; // ignore free times 611 if (!filter.matches(r)) continue; // check the filter 612 CourseRequest cr = (CourseRequest)r; 613 Enrollment e = r.getAssignment(assignment); 614 if (e != null) { 615 if (r.hasSelection()) { 616 prefs ++; 617 satisfied += //0.3 * e.percentSelectedSameConfig() + 0.7 * e.percentSelectedSameSection(); 618 e.percentSelected(); 619 for (Choice ch: cr.getSelectedChoices()) { 620 if (ch.getConfigId() != null) { 621 configPrefs ++; 622 configPref += e.percentSelectedSameConfig(); 623 break; 624 } 625 } 626 for (Choice ch: cr.getSelectedChoices()) { 627 if (ch.getSectionId() != null) { 628 sectionPrefs ++; 629 sectionPref += e.percentSelectedSameSection(); 630 break; 631 } 632 } 633 } 634 } 635 } 636 } 637 if (prefs == 0) return new String[] { "N/A", "", "", ""}; 638 return new String[] { sIntFormat.format(prefs), sPercentFormat.format(100.0 * satisfied / prefs) + "%", 639 sPercentFormat.format(100.0 * sectionPref / sectionPrefs) + "%", 640 sPercentFormat.format(100.0 * configPref / configPrefs) + "%" 641 }; 642 } 643 }, true), 644 BALANCING(new String[] {"Unbalanced sections", "- average disbalance"}, 645 new String[] {"Classes dis-balanced by 10% or more", "Average difference between target and actual enrollment in the section"}, 646 new Statistic() { 647 @Override 648 public String[] getValues(StudentGroup group, StudentSectioningModel model, Assignment<Request, Enrollment> assignment, StudentSectioningReport.Filter filter) { 649 double disbWeight = 0; 650 int disb10Sections = 0; 651 int totalSections = 0; 652 for (Offering offering: model.getOfferings()) { 653 if (offering.isDummy()) continue; 654 for (Config config: offering.getConfigs()) { 655 double enrl = 0; 656 for (Enrollment e: config.getEnrollments(assignment)) { 657 if (group.matches(e.getStudent()) && filter.matches(e.getRequest(), e)) enrl += e.getRequest().getWeight(); 658 } 659 for (Subpart subpart: config.getSubparts()) { 660 if (subpart.getSections().size() <= 1) continue; 661 if (subpart.getLimit() > 0) { 662 // sections have limits -> desired size is section limit x (total enrollment / total limit) 663 double ratio = enrl / subpart.getLimit(); 664 for (Section section: subpart.getSections()) { 665 double sectEnrl = 0; 666 for (Enrollment e: section.getEnrollments(assignment)) { 667 if (group.matches(e.getStudent()) && filter.matches(e.getRequest(), e)) sectEnrl += e.getRequest().getWeight(); 668 } 669 double desired = ratio * section.getLimit(); 670 disbWeight += Math.abs(sectEnrl - desired); 671 if (Math.abs(desired - sectEnrl) >= Math.max(1.0, 0.1 * section.getLimit())) { 672 disb10Sections++; 673 } 674 totalSections++; 675 } 676 } else { 677 // unlimited sections -> desired size is total enrollment / number of sections 678 for (Section section: subpart.getSections()) { 679 double sectEnrl = 0; 680 for (Enrollment e: section.getEnrollments(assignment)) { 681 if (group.matches(e.getStudent()) && filter.matches(e.getRequest(), e)) sectEnrl += e.getRequest().getWeight(); 682 } 683 double desired = enrl / subpart.getSections().size(); 684 disbWeight += Math.abs(sectEnrl - desired); 685 if (Math.abs(desired - sectEnrl) >= Math.max(1.0, 0.1 * desired)) { 686 disb10Sections++; 687 } 688 totalSections++; 689 } 690 } 691 } 692 } 693 } 694 return new String[] { 695 sPercentFormat.format(100.0 * disb10Sections / totalSections) + "%", 696 sDoubleFormat.format(disbWeight / totalSections) 697 }; 698 } 699 }, true), 700 DISTANCE(new String[] {"Distance conflicts", "- students with distance conflicts", "- average distance in minutes", 701 "Distance conflicts (SD)", "- students with distance conflicts", "- average distance in minutes" 702 }, new String[] {"Total number of distance conflicts", 703 "Total number of students with one or more distance conflicts", 704 "Average distance between two classes in minutes per conflict", 705 "Total number of distance conflicts (students needed short distances)", 706 "Total number of SD students with one or more distance conflicts", 707 "Average distance between two classes in minutes per conflict"}, 708 new Statistic() { 709 710 protected int getDistanceInMinutes(StudentSectioningModel model, RoomLocation r1, RoomLocation r2) { 711 if (r1.getId().compareTo(r2.getId()) > 0) return getDistanceInMinutes(model, r2, r1); 712 if (r1.getId().equals(r2.getId()) || r1.getIgnoreTooFar() || r2.getIgnoreTooFar()) 713 return 0; 714 if (r1.getPosX() == null || r1.getPosY() == null || r2.getPosX() == null || r2.getPosY() == null) 715 return model.getDistanceMetric().getMaxTravelDistanceInMinutes(); 716 return model.getDistanceMetric().getDistanceInMinutes(r1.getId(), r1.getPosX(), r1.getPosY(), r2.getId(), r2.getPosX(), r2.getPosY()); 717 } 718 719 protected int getDistanceInMinutes(StudentSectioningModel model, Placement p1, Placement p2) { 720 if (p1.isMultiRoom()) { 721 if (p2.isMultiRoom()) { 722 int dist = 0; 723 for (RoomLocation r1 : p1.getRoomLocations()) { 724 for (RoomLocation r2 : p2.getRoomLocations()) { 725 dist = Math.max(dist, getDistanceInMinutes(model, r1, r2)); 726 } 727 } 728 return dist; 729 } else { 730 if (p2.getRoomLocation() == null) 731 return 0; 732 int dist = 0; 733 for (RoomLocation r1 : p1.getRoomLocations()) { 734 dist = Math.max(dist, getDistanceInMinutes(model, r1, p2.getRoomLocation())); 735 } 736 return dist; 737 } 738 } else if (p2.isMultiRoom()) { 739 if (p1.getRoomLocation() == null) 740 return 0; 741 int dist = 0; 742 for (RoomLocation r2 : p2.getRoomLocations()) { 743 dist = Math.max(dist, getDistanceInMinutes(model, p1.getRoomLocation(), r2)); 744 } 745 return dist; 746 } else { 747 if (p1.getRoomLocation() == null || p2.getRoomLocation() == null) 748 return 0; 749 return getDistanceInMinutes(model, p1.getRoomLocation(), p2.getRoomLocation()); 750 } 751 } 752 753 public boolean inConflict(StudentSectioningModel model, Student student, Section s1, Section s2) { 754 if (s1.getPlacement() == null || s2.getPlacement() == null) 755 return false; 756 TimeLocation t1 = s1.getTime(); 757 TimeLocation t2 = s2.getTime(); 758 if (!t1.shareDays(t2) || !t1.shareWeeks(t2)) 759 return false; 760 int a1 = t1.getStartSlot(), a2 = t2.getStartSlot(); 761 if (student.isNeedShortDistances()) { 762 if (model.getDistanceMetric().doComputeDistanceConflictsBetweenNonBTBClasses()) { 763 if (a1 + t1.getNrSlotsPerMeeting() <= a2) { 764 int dist = getDistanceInMinutes(model, s1.getPlacement(), s2.getPlacement()); 765 if (dist > Constants.SLOT_LENGTH_MIN * (a2 - a1 - t1.getLength())) 766 return true; 767 } else if (a2 + t2.getNrSlotsPerMeeting() <= a1) { 768 int dist = getDistanceInMinutes(model, s1.getPlacement(), s2.getPlacement()); 769 if (dist > Constants.SLOT_LENGTH_MIN * (a1 - a2 - t2.getLength())) 770 return true; 771 } 772 } else { 773 if (a1 + t1.getNrSlotsPerMeeting() == a2) { 774 int dist = getDistanceInMinutes(model, s1.getPlacement(), s2.getPlacement()); 775 if (dist > 0) return true; 776 } else if (a2 + t2.getNrSlotsPerMeeting() == a1) { 777 int dist = getDistanceInMinutes(model, s1.getPlacement(), s2.getPlacement()); 778 if (dist > 0) return true; 779 } 780 } 781 return false; 782 } 783 if (model.getDistanceMetric().doComputeDistanceConflictsBetweenNonBTBClasses()) { 784 if (a1 + t1.getNrSlotsPerMeeting() <= a2) { 785 int dist = getDistanceInMinutes(model, s1.getPlacement(), s2.getPlacement()); 786 if (dist > t1.getBreakTime() + Constants.SLOT_LENGTH_MIN * (a2 - a1 - t1.getLength())) 787 return true; 788 } else if (a2 + t2.getNrSlotsPerMeeting() <= a1) { 789 int dist = getDistanceInMinutes(model, s1.getPlacement(), s2.getPlacement()); 790 if (dist > t2.getBreakTime() + Constants.SLOT_LENGTH_MIN * (a1 - a2 - t2.getLength())) 791 return true; 792 } 793 } else { 794 if (a1 + t1.getNrSlotsPerMeeting() == a2) { 795 int dist = getDistanceInMinutes(model, s1.getPlacement(), s2.getPlacement()); 796 if (dist > t1.getBreakTime()) 797 return true; 798 } else if (a2 + t2.getNrSlotsPerMeeting() == a1) { 799 int dist = getDistanceInMinutes(model, s1.getPlacement(), s2.getPlacement()); 800 if (dist > t2.getBreakTime()) 801 return true; 802 } 803 } 804 return false; 805 } 806 807 public Set<DistanceConflict.Conflict> conflicts(StudentSectioningModel model, Enrollment e1) { 808 Set<DistanceConflict.Conflict> ret = new HashSet<DistanceConflict.Conflict>(); 809 if (!e1.isCourseRequest()) 810 return ret; 811 for (Section s1 : e1.getSections()) { 812 for (Section s2 : e1.getSections()) { 813 if (s1.getId() < s2.getId() && inConflict(model, e1.getStudent(), s1, s2)) 814 ret.add(new DistanceConflict.Conflict(e1.getStudent(), e1, s1, e1, s2)); 815 } 816 } 817 return ret; 818 } 819 820 public Set<DistanceConflict.Conflict> conflicts(StudentSectioningModel model, Enrollment e1, Enrollment e2) { 821 Set<DistanceConflict.Conflict> ret = new HashSet<DistanceConflict.Conflict>(); 822 if (!e1.isCourseRequest() || !e2.isCourseRequest() || !e1.getStudent().equals(e2.getStudent())) 823 return ret; 824 for (Section s1 : e1.getSections()) { 825 for (Section s2 : e2.getSections()) { 826 if (inConflict(model, e1.getStudent(), s1, s2)) 827 ret.add(new DistanceConflict.Conflict(e1.getStudent(), e1, s1, e2, s2)); 828 } 829 } 830 return ret; 831 } 832 833 public Set<DistanceConflict.Conflict> computeAllConflicts(StudentSectioningModel model, Assignment<Request, Enrollment> assignment) { 834 Set<DistanceConflict.Conflict> ret = new HashSet<DistanceConflict.Conflict>(); 835 for (Request r1 : model.variables()) { 836 Enrollment e1 = assignment.getValue(r1); 837 if (e1 == null || !(r1 instanceof CourseRequest)) 838 continue; 839 ret.addAll(conflicts(model, e1)); 840 for (Request r2 : r1.getStudent().getRequests()) { 841 Enrollment e2 = assignment.getValue(r2); 842 if (e2 == null || r1.getId() >= r2.getId() || !(r2 instanceof CourseRequest)) 843 continue; 844 ret.addAll(conflicts(model, e1, e2)); 845 } 846 } 847 return ret; 848 } 849 850 @Override 851 public String[] getValues(StudentGroup group, StudentSectioningModel model, Assignment<Request, Enrollment> assignment, StudentSectioningReport.Filter filter) { 852 if (model.getDistanceMetric() == null) 853 return new String[] {"N/A", "", ""}; 854 Set<DistanceConflict.Conflict> conflicts = computeAllConflicts(model, assignment); 855 Set<Student> students = new HashSet<Student>(), studentsSD = new HashSet<Student>(); 856 double distance = 0, distanceSD = 0; 857 int total = 0, totalSD = 0; 858 for (DistanceConflict.Conflict conflict: conflicts) { 859 if (group.matches(conflict.getStudent()) && filter.matches(conflict.getR1(), conflict.getE1())) { 860 if (conflict.getStudent().isNeedShortDistances()) { 861 totalSD ++; 862 studentsSD.add(conflict.getStudent()); 863 distanceSD += Placement.getDistanceInMinutes(model.getDistanceMetric(), conflict.getS1().getPlacement(), conflict.getS2().getPlacement()); 864 } else { 865 total ++; 866 students.add(conflict.getStudent()); 867 distance += Placement.getDistanceInMinutes(model.getDistanceMetric(), conflict.getS1().getPlacement(), conflict.getS2().getPlacement()); 868 } 869 } 870 } 871 return new String[] { 872 sIntFormat.format(total), 873 sIntFormat.format(students.size()), 874 (total == 0 ? "" : sDoubleFormat.format(distance / total)), 875 sIntFormat.format(totalSD), 876 sIntFormat.format(studentsSD.size()), 877 (totalSD == 0 ? "" : sDoubleFormat.format(distanceSD / totalSD)) 878 }; 879 } 880 }, true), 881 OVERLAP(new String[] {"Free time conflict", "- students in conflict", "- average minutes", "Course time conflict", "- students in conflict", "- average minutes", "Teaching conflicts", "- students in conflict", "- average minutes"}, 882 new String[] { 883 "Total number of free time conflicts", 884 "Total number of students with a free time conflict", 885 "For students with a free time conflict, the average number of overlapping minutes per student", 886 "Total number of course time conflicts", 887 "Total number of students with a course time conflict", 888 "For students with a course time conflict, the average number of overlapping minutes per student", 889 "Total number of teaching time conflicts", 890 "Total number of students with a teaching conflict", 891 "For students with a teaching time conflict, the average number of overlapping minutes per student" 892 }, 893 new Statistic() { 894 895 public boolean inConflict(SctAssignment a1, SctAssignment a2) { 896 if (a1.getTime() == null || a2.getTime() == null) return false; 897 if (a1 instanceof Section && a2 instanceof Section && ((Section)a1).isToIgnoreStudentConflictsWith(a2.getId())) return false; 898 return a1.getTime().hasIntersection(a2.getTime()); 899 } 900 901 public int share(SctAssignment a1, SctAssignment a2) { 902 if (!inConflict(a1, a2)) return 0; 903 return a1.getTime().nrSharedDays(a2.getTime()) * a1.getTime().nrSharedHours(a2.getTime()); 904 } 905 906 public Set<Conflict> conflicts(Enrollment e1, Enrollment e2) { 907 Set<Conflict> ret = new HashSet<Conflict>(); 908 if (!e1.getStudent().equals(e2.getStudent())) return ret; 909 if (e1.getRequest() instanceof FreeTimeRequest && e2.getRequest() instanceof FreeTimeRequest) return ret; 910 for (SctAssignment s1 : e1.getAssignments()) { 911 for (SctAssignment s2 : e2.getAssignments()) { 912 if (inConflict(s1, s2)) 913 ret.add(new Conflict(e1.getStudent(), share(s1, s2), e1, s1, e2, s2)); 914 } 915 } 916 return ret; 917 } 918 919 public Set<Conflict> computeAllConflicts(StudentSectioningModel model, Assignment<Request, Enrollment> assignment) { 920 Set<Conflict> ret = new HashSet<Conflict>(); 921 for (Request r1 : model.variables()) { 922 Enrollment e1 = assignment.getValue(r1); 923 if (e1 == null || r1 instanceof FreeTimeRequest) continue; 924 for (Request r2 : r1.getStudent().getRequests()) { 925 Enrollment e2 = assignment.getValue(r2); 926 if (r2 instanceof FreeTimeRequest) { 927 FreeTimeRequest ft = (FreeTimeRequest)r2; 928 ret.addAll(conflicts(e1, ft.createEnrollment())); 929 } else if (e2 != null && r1.getId() < r2.getId()) { 930 ret.addAll(conflicts(e1, e2)); 931 } 932 } 933 for (Unavailability unavailability: e1.getStudent().getUnavailabilities()) 934 for (SctAssignment section: e1.getAssignments()) 935 if (inConflict(section, unavailability)) 936 ret.add(new Conflict(e1.getStudent(), share(section, unavailability), e1, section, unavailability.createEnrollment(), unavailability)); 937 } 938 return ret; 939 } 940 941 @Override 942 public String[] getValues(StudentGroup group, StudentSectioningModel model, Assignment<Request, Enrollment> assignment, StudentSectioningReport.Filter filter) { 943 Set<Student> timeFt = new HashSet<Student>(); 944 Set<Student> timeCourse = new HashSet<Student>(); 945 Set<Student> timeUnav = new HashSet<Student>(); 946 int ftMin = 0, courseMin = 0, unavMin = 0; 947 int totFt = 0, totCourse = 0, totUn = 0; 948 Set<TimeOverlapsCounter.Conflict> conf = computeAllConflicts(model, assignment); 949 for (TimeOverlapsCounter.Conflict c: conf) { 950 if (group.matches(c.getStudent()) && filter.matches(c.getR1(), c.getE1())) { 951 if (c.getR1() instanceof CourseRequest && c.getR2() instanceof CourseRequest) { 952 totCourse ++; 953 courseMin += 5 * c.getShare(); 954 timeCourse.add(c.getStudent()); 955 } else if (c.getS2() instanceof Unavailability) { 956 totUn ++; 957 unavMin += 5 * c.getShare(); 958 timeUnav.add(c.getStudent()); 959 } else { 960 totFt ++; 961 ftMin += 5 * c.getShare(); 962 timeFt.add(c.getStudent()); 963 } 964 } 965 } 966 return new String[] { 967 sIntFormat.format(totFt), 968 sIntFormat.format(timeFt.size()), 969 (timeFt.isEmpty() ? "" : sDoubleFormat.format(((double)ftMin) / timeFt.size())), 970 sIntFormat.format(totCourse), 971 sIntFormat.format(timeCourse.size()), 972 (timeCourse.isEmpty() ? "" : sDoubleFormat.format(((double)courseMin) / timeCourse.size())), 973 sIntFormat.format(totUn), 974 sIntFormat.format(timeUnav.size()), 975 (timeUnav.isEmpty() ? "" : sDoubleFormat.format(((double)unavMin) / timeUnav.size())) 976 }; 977 } 978 }, true), 979 CREDITS(new String[] { "Students requesting 12+ credits", "- 12+ credits assigned", "Students requesting 15+ credits", "- 15+ credits assigned" }, 980 new String[] { 981 "Total number of students requesting 12 or more credit hours", 982 "Out of these, the percentage of students having 12 or more credits assigned", 983 "Total number of students requesting 15 or more credit hours", 984 "Out of these, the percentage of students having 15 or more credits assigned", 985 }, 986 new Statistic() { 987 @Override 988 public String[] getValues(StudentGroup group, StudentSectioningModel model, Assignment<Request, Enrollment> assignment, StudentSectioningReport.Filter filter) { 989 int total12 = 0, assigned12 = 0; 990 int total15 = 0, assigned15 = 0; 991 for (Student student: model.getStudents()) { 992 if (!group.matches(student, filter)) continue; 993 float credit = 0; 994 float assignedCredit = 0; 995 for (Request r : student.getRequests()) { 996 if (!(r instanceof CourseRequest)) continue; // ignore free times 997 if (!filter.matches(r)) continue; // check the filter 998 CourseRequest cr = (CourseRequest)r; 999 if (!cr.isAlternative()) { 1000 Course c = cr.getCourses().get(0); 1001 if (c.hasCreditValue()) 1002 credit += c.getCreditValue(); 1003 else 1004 credit += cr.getMinCredit(); 1005 } 1006 Enrollment e = cr.getAssignment(assignment); 1007 if (e != null) { 1008 assignedCredit += e.getCredit(); 1009 } 1010 } 1011 if (credit >= 12f) { 1012 total12 ++; 1013 if (assignedCredit >= 12f) 1014 assigned12 ++; 1015 } 1016 if (credit >= 15f) { 1017 total15 ++; 1018 if (assignedCredit >= 15f) 1019 assigned15 ++; 1020 } 1021 } 1022 return new String[] { 1023 sIntFormat.format(total12), 1024 (total12 == 0 ? "" : sPercentFormat.format(100.0 * assigned12 / total12) + "%"), 1025 sIntFormat.format(total15), 1026 (total15 == 0 ? "" : sPercentFormat.format(100.0 * assigned15 / total15) + "%"), 1027 }; 1028 } 1029 }, true), 1030 F2F(new String[] { 1031 "Residential Students", 1032 "Arranged Hours Assignments", "- percentage of all assignments", 1033 "Online Assignments", "- percentage of all assignments", 1034 "Students with no face-to-face classes", "- percentage of all undergrad students", 1035 "Students with <50% classes face-to-face", "- percentage of all undergrad students"}, 1036 new String[] { 1037 "Number of students that are NOT online-only (only residential students are counted in the following numbers)", 1038 "Number of class assignments that are Arranged Hours", "Percentage of all class assignments", 1039 "Number of class assignments that are Online (no time, time with no room, or time with ONLINE room)", "Percentage of all class assignments", 1040 "Total number of undergraduate students with no face-to-face classes.", "Percentage of all undergraduate students", 1041 "Total number of undergraduate students with less than half of their schedule face-to-face.", "Percentage of all undergraduate students", 1042 }, 1043 new Statistic() { 1044 @Override 1045 public String[] getValues(StudentGroup group, StudentSectioningModel model, Assignment<Request, Enrollment> assignment, StudentSectioningReport.Filter filter) { 1046 int arrClass = 0, onlineClass = 0, allClass = 0; 1047 int residentialStudents = 0; 1048 for (Student student: model.getStudents()) { 1049 if (!group.matches(student, filter)) continue; 1050 if (!FILTER_ALL_RES.matches(student)) { continue; } 1051 residentialStudents ++; 1052 for (Request r: student.getRequests()) { 1053 Enrollment e = r.getAssignment(assignment); 1054 if (e != null && e.isCourseRequest()) { 1055 for (Section section: e.getSections()) { 1056 if (section.isOnline()) onlineClass ++; 1057 if (!section.hasTime()) arrClass ++; 1058 allClass ++; 1059 } 1060 } 1061 } 1062 } 1063 int online = 0; 1064 int half = 0; 1065 int total = 0; 1066 for (Student student: model.getStudents()) { 1067 if (!group.matches(student, filter)) continue; 1068 if (!FILTER_ALL_RES.matches(student)) continue; 1069 boolean gr = false; 1070 for (AreaClassificationMajor acm: student.getAreaClassificationMajors()) { 1071 if (acm.getClassification().startsWith("G") || acm.getClassification().startsWith("P")) gr = true; 1072 } 1073 if (gr) continue; 1074 int sections = 0, onlineSections = 0; 1075 for (Request r : student.getRequests()) { 1076 if (!(r instanceof CourseRequest)) continue; // ignore free times 1077 if (!filter.matches(r)) continue; // check the filter 1078 Enrollment e = r.getAssignment(assignment); 1079 if (e != null) 1080 for (Section s: e.getSections()) { 1081 sections ++; 1082 if (s.isOnline()) onlineSections ++; 1083 } 1084 } 1085 if (sections > 0) { 1086 total ++; 1087 if (onlineSections == sections) online ++; 1088 if (onlineSections > 0.5 * sections) half++; 1089 } 1090 } 1091 return new String[] { 1092 sIntFormat.format(residentialStudents), 1093 (residentialStudents == 0 ? "" : sIntFormat.format(arrClass)), 1094 (residentialStudents == 0 ? "" : sPercentFormat.format(100.0 * arrClass / allClass) + "%"), 1095 (onlineClass == 0 ? "" : sIntFormat.format(onlineClass)), 1096 (onlineClass == 0 ? "" : sPercentFormat.format(100.0 * onlineClass / allClass) + "%"), 1097 (online == 0 ? "" : sIntFormat.format(online)), 1098 (online == 0 ? "" : sPercentFormat.format(100.0 * online / total) + "%"), 1099 (half == 0 ? "" : sIntFormat.format(half)), 1100 (half == 0 ? "" : sPercentFormat.format(100.0 * half / total) + "%") 1101 }; 1102 } 1103 }, true), 1104 FULL_OFFERINGS( 1105 new String[] { 1106 "Full Offerings", "- percentage of all requested offerings", "- percentage of all assignments", 1107 "Offerings with ≤ 2% available", "- percentage of all requested offerings", "- percentage of all assignments", 1108 "Offerings with ≤ 5% available", "- percentage of all requested offerings", "- percentage of all assignments", 1109 "Offerings with ≤ 10% available", "- percentage of all requested offerings", "- percentage of all assignments", 1110 "Full Sections", "- percentage of all sections", "- percentage of all assignments", 1111 "Disabled Sections", "- percentage of all sections", "- percentage of all assignments", 1112 "Sections with ≤ 2% available", "- percentage of all sections", "- percentage of all assignments", 1113 "Sections with ≤ 5% available", "- percentage of all sections", "- percentage of all assignments", 1114 "Sections with ≤ 10% available", "- percentage of all sections", "- percentage of all assignments", 1115 }, 1116 new String[] { 1117 "Number of instructional offerings that are completely full (only counting courses that are requested by the students)", 1118 "Percentage full offerings vs all requested offerings", 1119 "Percentage of all course assignments that are for courses that are full", 1120 "Number of instructional offerings that have 2% or less space available", "", "", 1121 "Number of instructional offerings that have 5% or less space available", "", "", 1122 "Number of instructional offerings that have 10% or less space available", "", "", 1123 "Number of sections that have no space available (only counting sections from courses that are requested by the students)", 1124 "Percentage full sections vs all sections of the requested courses", 1125 "Percentage of all class assignments that are in sections that are full", 1126 "Number of sections that are disabled", 1127 "Percentage disabled sections vs all sections of the requested courses", 1128 "Percentage of all class assignments that are in sections that are disabled", 1129 "Number of sections that have 2% or less space available", "", "", 1130 "Number of sections that have 5% or less space available", "", "", 1131 "Number of sections that have 10% or less space available", "", "", 1132 }, 1133 new Statistic() { 1134 1135 protected int getEnrollments(StudentGroup group, Section section, Assignment<Request, Enrollment> assignment, StudentSectioningReport.Filter filter) { 1136 int enrl = 0; 1137 for (Enrollment e: section.getEnrollments(assignment)) { 1138 if (group.matches(e.getStudent()) && filter.matches(e.getRequest(), e)) enrl ++; 1139 } 1140 return enrl; 1141 } 1142 1143 protected int getEnrollments(StudentGroup group, Config config, Assignment<Request, Enrollment> assignment, StudentSectioningReport.Filter filter) { 1144 int enrl = 0; 1145 for (Enrollment e: config.getEnrollments(assignment)) { 1146 if (group.matches(e.getStudent()) && filter.matches(e.getRequest(), e)) enrl ++; 1147 } 1148 return enrl; 1149 } 1150 1151 @Override 1152 public String[] getValues(StudentGroup group, StudentSectioningModel model, Assignment<Request, Enrollment> assignment, StudentSectioningReport.Filter filter) { 1153 1154 int nbrSections = 0, nbrFullSections = 0, nbrSections98 = 0, nbrSections95 = 0, nbrSections90 = 0, nbrSectionsDis = 0; 1155 int enrlSections = 0, enrlFullSections = 0, enrlSections98 = 0, enrlSections95 = 0, enrlSections90 = 0, enrlSectionsDis = 0; 1156 int nbrOfferings = 0, nbrFullOfferings = 0, nbrOfferings98 = 0, nbrOfferings95 = 0, nbrOfferings90 = 0; 1157 int enrlOfferings = 0, enrlOfferingsFull = 0, enrlOfferings98 = 0, enrlOfferings95 = 0, enrlOfferings90 = 0; 1158 for (Offering offering: model.getOfferings()) { 1159 if (offering.isDummy()) continue; 1160 int crs = 0; 1161 for (Course course: offering.getCourses()) { 1162 for (CourseRequest cr: course.getRequests()) { 1163 if (group.matches(cr.getStudent()) && filter.matches(cr)) crs++; 1164 } 1165 } 1166 if (crs == 0) continue; 1167 int offeringLimit = 0, offeringEnrollment = 0, offeringMatchingEnrollment = 0; 1168 for (Config config: offering.getConfigs()) { 1169 int configLimit = config.getLimit(); 1170 for (Subpart subpart: config.getSubparts()) { 1171 int subpartLimit = 0; 1172 for (Section section: subpart.getSections()) { 1173 if (section.isCancelled()) continue; 1174 int enrl = section.getEnrollments(assignment).size(); 1175 int matchingEnrl = getEnrollments(group, section, assignment, filter); 1176 if (section.getLimit() < 0 || subpartLimit < 0) 1177 subpartLimit = -1; 1178 else 1179 subpartLimit += (section.isEnabled() ? section.getLimit() : enrl); 1180 nbrSections ++; 1181 enrlSections += matchingEnrl; 1182 if (section.getLimit() >= 0 && section.getLimit() <= enrl) { 1183 nbrFullSections ++; 1184 enrlFullSections += matchingEnrl; 1185 } 1186 if (!section.isEnabled()) { //&& (enrl > 0 || section.getLimit() >= 0)) { 1187 nbrSectionsDis ++; 1188 enrlSectionsDis += matchingEnrl; 1189 } 1190 if (section.getLimit() >= 0 && (section.getLimit() - enrl) <= Math.round(0.02 * section.getLimit())) { 1191 nbrSections98 ++; 1192 enrlSections98 += matchingEnrl; 1193 } 1194 if (section.getLimit() >= 0 && (section.getLimit() - enrl) <= Math.round(0.05 * section.getLimit())) { 1195 nbrSections95 ++; 1196 enrlSections95 += matchingEnrl; 1197 } 1198 if (section.getLimit() >= 0 && (section.getLimit() - enrl) <= Math.round(0.10 * section.getLimit())) { 1199 nbrSections90 ++; 1200 enrlSections90 += matchingEnrl; 1201 } 1202 } 1203 if (configLimit < 0 || subpartLimit < 0) 1204 configLimit = -1; 1205 else 1206 configLimit = Math.min(configLimit, subpartLimit); 1207 } 1208 if (offeringLimit < 0 || configLimit < 0) 1209 offeringLimit = -1; 1210 else 1211 offeringLimit += configLimit; 1212 offeringEnrollment += config.getEnrollments(assignment).size(); 1213 offeringMatchingEnrollment += getEnrollments(group, config, assignment, filter); 1214 } 1215 nbrOfferings ++; 1216 enrlOfferings += offeringMatchingEnrollment; 1217 1218 if (offeringLimit >=0 && offeringEnrollment >= offeringLimit) { 1219 nbrFullOfferings ++; 1220 enrlOfferingsFull += offeringMatchingEnrollment; 1221 } 1222 if (offeringLimit >= 0 && (offeringLimit - offeringEnrollment) <= Math.round(0.02 * offeringLimit)) { 1223 nbrOfferings98++; 1224 enrlOfferings98 += offeringMatchingEnrollment; 1225 } 1226 if (offeringLimit >= 0 && (offeringLimit - offeringEnrollment) <= Math.round(0.05 * offeringLimit)) { 1227 nbrOfferings95++; 1228 enrlOfferings95 += offeringMatchingEnrollment; 1229 } 1230 if (offeringLimit >= 0 && (offeringLimit - offeringEnrollment) <= Math.round(0.10 * offeringLimit)) { 1231 nbrOfferings90++; 1232 enrlOfferings90 += offeringMatchingEnrollment; 1233 } 1234 } 1235 return new String[] { 1236 sIntFormat.format(nbrFullOfferings), sPercentFormat.format(100.0 * nbrFullOfferings / nbrOfferings) + "%", sPercentFormat.format(100.0 * enrlOfferingsFull / enrlOfferings) + "%", 1237 sIntFormat.format(nbrOfferings98), sPercentFormat.format(100.0 * nbrOfferings98 / nbrOfferings) + "%", sPercentFormat.format(100.0 * enrlOfferings98 / enrlOfferings) + "%", 1238 sIntFormat.format(nbrOfferings95), sPercentFormat.format(100.0 * nbrOfferings95 / nbrOfferings) + "%", sPercentFormat.format(100.0 * enrlOfferings95 / enrlOfferings) + "%", 1239 sIntFormat.format(nbrOfferings90), sPercentFormat.format(100.0 * nbrOfferings90 / nbrOfferings) + "%", sPercentFormat.format(100.0 * enrlOfferings90 / enrlOfferings) + "%", 1240 sIntFormat.format(nbrFullSections), sPercentFormat.format(100.0 * nbrFullSections / nbrSections) + "%", sPercentFormat.format(100.0 * enrlFullSections / enrlSections) + "%", 1241 sIntFormat.format(nbrSectionsDis), sPercentFormat.format(100.0 * nbrSectionsDis / nbrSections) + "%", sPercentFormat.format(100.0 * enrlSectionsDis / enrlSections) + "%", 1242 sIntFormat.format(nbrSections98), sPercentFormat.format(100.0 * nbrSections98 / nbrSections) + "%", sPercentFormat.format(100.0 * enrlSections98 / enrlSections) + "%", 1243 sIntFormat.format(nbrSections95), sPercentFormat.format(100.0 * nbrSections95 / nbrSections) + "%", sPercentFormat.format(100.0 * enrlSections95 / enrlSections) + "%", 1244 sIntFormat.format(nbrSections90), sPercentFormat.format(100.0 * nbrSections90 / nbrSections) + "%", sPercentFormat.format(100.0 * enrlSections90 / enrlSections) + "%", 1245 }; 1246 } 1247 }), 1248 ; 1249 String[] iNames; 1250 String[] iNotes; 1251 Statistic iStatistic; 1252 boolean iNewLine = false; 1253 Statistics(String[] names, String notes[], Statistic stat, boolean nl) { 1254 iNames = names; iNotes = notes; iStatistic = stat; iNewLine = nl; 1255 } 1256 Statistics(String name, String note, Statistic stat, boolean nl) { 1257 this(new String[] {name}, new String[] {note}, stat, nl); 1258 } 1259 Statistics(String[] names, String notes[], Statistic stat) { 1260 this(names, notes, stat, false); 1261 } 1262 Statistics(String name, String note, Statistic stat) { 1263 this(name, note, stat, false); 1264 } 1265 public String[] getNames() { return iNames; } 1266 public String[] getNotes() { return iNotes; } 1267 public String[] getValues(StudentGroup group, StudentSectioningModel model, Assignment<Request, Enrollment> assignment, StudentSectioningReport.Filter filter) { 1268 return iStatistic.getValues(group, model, assignment, filter); 1269 } 1270 public boolean isNewLine() { return iNewLine; } 1271 } 1272 1273 @Override 1274 public CSVFile createTable(Assignment<Request, Enrollment> assignment, DataProperties properties) { 1275 CSVFile csv = new CSVFile(); 1276 List<CSVField> header = new ArrayList<CSVField>(); 1277 List<StudentGroup> groups = new ArrayList<StudentGroup>(); 1278 header.add(new CSVField("")); 1279 Map<Integer, StudentGroup> counts = new HashMap<Integer, StudentGroup>(); 1280 1281 for (StudentGroup g: StudentGroup.values()) { 1282 int nrStudents = 0; 1283 for (Student student: getModel().getStudents()) { 1284 if (g.matches(student, this)) nrStudents ++; 1285 } 1286 if (nrStudents > 0 && !counts.containsKey(nrStudents)) { 1287 groups.add(g); 1288 header.add(new CSVField(g.getName())); 1289 counts.put(nrStudents, g); 1290 } 1291 } 1292 header.add(new CSVField("Note")); 1293 csv.setHeader(header); 1294 for (Statistics stat: Statistics.values()) { 1295 String[] names = stat.getNames(); 1296 List<List<CSVField>> table = new ArrayList<List<CSVField>>(); 1297 for (String name: names) { 1298 List<CSVField> line = new ArrayList<CSVField>(); line.add(new CSVField(name)); 1299 table.add(line); 1300 } 1301 for (StudentGroup g: groups) { 1302 String[] values = stat.getValues(g, getModel(), assignment, this); 1303 for (int i = 0; i < values.length; i++) { 1304 table.get(i).add(new CSVField(values[i])); 1305 } 1306 } 1307 String[] notes = stat.getNotes(); 1308 for (int i = 0; i < notes.length; i++) { 1309 table.get(i).add(new CSVField(notes[i])); 1310 } 1311 for (List<CSVField> line: table) { 1312 csv.addLine(line); 1313 } 1314 if (stat.isNewLine()) 1315 csv.addLine(new CSVField[] {new CSVField(" ")}); 1316 } 1317 return csv; 1318 } 1319 1320 public static void main(String[] args) { 1321 try { 1322 DataProperties cfg = new DataProperties(); 1323 cfg.setProperty("General.Input", args[0]); 1324 cfg.setProperty("Distances.Ellipsoid", "WGS84"); 1325 cfg.setProperty("Distances.ShortDistanceAccommodationReference", "SD"); 1326 StudentSectioningModel model = new StudentSectioningModel(cfg); 1327 model.setStudentQuality(new StudentQuality((DistanceMetric)null, cfg)); 1328 Assignment<Request, Enrollment> assignment = new DefaultSingleAssignment<Request, Enrollment>(); 1329 new StudentSectioningXMLLoader(model, assignment).load(); 1330 new SolutionStatsReport(model).create(assignment, cfg).save(new File(new File(args[0]).getParentFile(), "stats.csv")); 1331 } catch (Exception e) { 1332 e.printStackTrace(); 1333 } 1334 1335 } 1336 1337}