README.md

SeminarMatching

Author: Sebastian Kranz, Ulm University

Overview

This is an shiny based R package to perform centralized matching of students to seminars based on a student optimal Gale-Shapley algorithm.

The software is developed and used for a centralized seminar assignment at the economics department of Ulm University. Yet, it is designed such that it can be customized also for other universities or departments. The documentation for customization is fairly poor, however.

Installation of a local test version

If you are interested in customizing the software for your own department, it is probably best to download a local version on your own computer. Try to get it run and make the customization work. If that works you can test and deploy it on a webserver using a docker container as explained in a subsequent section.

I recommend to use RStudio (https://www.rstudio.com/) for your local development.

Install R packages

If you use docker on your local computer, you can install the docker image skranz/seminarmatching as explained in the server installation. You can even use RStudio via a web browser to test and modify the software inside the docker container.

If you don't use docker on your local computer, you first need to install all R packages. That is a little bit tedious, since the SeminarMatching package relies on several R packages written by me that are only hosted on Github and not on CRAN. Automatic installation of dependencies hosted on Github is not as well developed, as for CRAN packages, however. Below is a script that (hopefully) installs all needed packages (and probably some more, because I was to lazy to find the minimal required set). Just try to run it in R:

library(methods)

# path to which you want to install
path = .libPaths()[1]
# shall existing packages be overwritten
glob.overwrite = FALSE

success = failed = NULL
from.cran = function(pkg, lib = path, overwrite = glob.overwrite,...) {
  if (!overwrite) {
    if (require(pkg,character.only = TRUE)) {
      cat("\npackage ",pkg," already exists.")
      return()
    }
  }
  res = try(install.packages(pkg, lib=lib))
  if (require(pkg,character.only = TRUE)) {
    success <<- c(success,pkg)
  } else {
    failed <<- c(failed,pkg)
  }
}

from.github = function(pkg, lib = path, ref="master", overwrite = glob.overwrite,upgrade_dependencies = FALSE,...) {
  repo = pkg
  pkg = strsplit(pkg,"/",fixed=TRUE)[[1]]
  pkg = pkg[length(pkg)]

  if (!overwrite) {
    if (require(pkg,character.only = TRUE)) {
      cat("\npackage ",pkg," already exists.")
      return()
    }
  }

  library(devtools)
  res = try(
  with_libpaths(new = path,
    install_github(repo,ref = ref,upgrade_dependencies = upgrade_dependencies,...)
  ))
  if (require(pkg,character.only = TRUE)) {
    success <<- c(success,pkg)
  } else {
    failed <<- c(failed,pkg)
  }

}

from.cran("shiny", lib=path)
from.cran("RCPP", lib=path)
from.cran("devtools", lib=path)

from.cran("curl",lib = path)
from.cran("openssl",lib = path)
from.cran("roxygen2", lib=path)
from.cran("dplyr",lib = path)

from.cran("knitr",lib = path)
from.cran("shinyjs",lib = path)
from.cran("V8",lib = path)
from.cran("rmarkdown", lib=path)

from.cran("rJava",lib = path)
from.cran("mailR",dep=TRUE, lib=path)

from.cran("shinyBS",lib = path)
from.cran("shinyAce",lib = path)

from.cran("RColorBrewer",lib = path)
from.cran("memoise", lib=path)

from.cran("mime", lib=path)

from.cran("xtable", lib=path)


from.cran("tidyr",lib=path)


# Install github packages
from.github(lib=path,"rstats-db/DBI",ref = "master")
from.github(lib=path,"rstats-db/RSQLite",ref = "master")

from.github(lib=path,"skranz/restorepoint",ref = "master")
from.github(lib=path,"skranz/stringtools",ref = "master")
from.github(lib=path,"skranz/codeUtils",ref = "master")
from.github(lib=path,"skranz/rmdtools",ref = "master")
from.github(lib=path,"skranz/dplyrExtras",ref = "master")
from.github(lib=path,"skranz/dbmisc",ref = "master")
#from.github(lib=path,"skranz/rowmins",ref = "master")

from.github(lib=path,"skranz/shinyEvents",ref = "master")
from.github(lib=path,"skranz/shinyEventsUI",ref = "master")
from.github(lib=path,"skranz/shinyEventsLogin",ref = "master")

from.github(lib=path,"skranz/TableTree",ref = "master")
from.github(lib=path,"skranz/YamlObjects",ref = "master")
from.github(lib=path,"skranz/shinyPart",ref = "master")
from.github(lib=path,"skranz/loginPart",ref = "master")
from.github(lib=path,"skranz/SeminarMatching",ref = "master")

In addition you probably want to download the source code of the SeminarMatching package from this Github page as a zip folder:

https://github.com/skranz/SeminarMatching/archive/master.zip

You can then also locally build this package on your computer and see more details on how it works from the source code.

Creating an example app

For the sake of brevity, I will refer to all files beyond the R packages that are needed to customize and deploy a working seminar matching software as the "app".

Copy example files

The folder /examples in this github repository contains skeletons of an example app that can be customized.

The subfolder sem-shiny-server contains brief files for shiny apps for the admin, teacher and students shiny apps.

The subfolder sem-shared contains folders for the databases, database schemas, yaml specification files and different RMarkdown files that can be adapted in order to customize reports and the user interfaces.

Best copy these two folders to some directory on your computer. In this guide we assume you have a Windows PC and that folder is C:/sema.

Create sqlite databases

The folder sem-shared/db will contain two SQLite databases: - loginDB.sqlite containing login credentials for students and teachers - semDB.sqlite containing all other relevant data for the seminarmatching, like seminars, student informations, preference lists, or matching results.

We first have to create empty versions of these database. For this purpose adapt and run the following R code:

# adapt working directory
library(SeminarMatching)
setwd("C:/sema/sem-shared")
db.dir = paste0(getwd(),"/db")


# Create loginDB.sqlite
logindb.arg = list(dbname=file.path(db.dir,"loginDB.sqlite"),drv=SQLite())
create.login.db(db.arg = logindb.arg)

# Insert a test user (make sure to delete him before production run)
create.user.in.db(userid = "test", email = "test",password = "test",db.arg = logindb.arg)

# Create semDB
schema.file = "./schema/semdb.yaml"
semdb = dbConnect(dbname=paste0(db.dir,"/semDB.sqlite"), drv = SQLite())
dbCreateSchemaTables(semdb, schema.file=schema.file,overwrite = FALSE)

# add test user to adminstaff
dbInsert(semdb, table="adminstaff",vals = list(userid="test",email="test"))

You can have now a look in the folder sem-shared/db and you see that the two files loginDB.sqlite and semDB.sqlite have been generated.

Now install some software like SQLiteStudio (https://sqlitestudio.pl/) to examine these databases, and possible to conduct some manual entries.

Note that we have generated a simple test user with id, email and password test to our loginDB and to the adminstaff table in semDB. The adminstaff table contains all users that are allowed to login as admin.

You should definitely remove that test user before going to production.

Starting the admin interface

Now try to open the admin web interface by running the following lines of code:

main.dir = "C:/sema/sem-shared"
app = AdminSeminarsApp(init.userid = "test", init.password="test", lang="en", main.dir = main.dir)
viewApp(app)

In the panel settings you can pick a semester in which the seminars take place and set a timeline for the matching process. Pick a semester and pick some dates.

For testing, set for the moment the "Date from which on the semester will be shown as default for lecturers and students" and the "Date at which students start to see the seminars and can submit their preferences" be some dates in the past and the matching dates be some date in the future.

You have to enter dates in international date format, e.g.

2017-04-30

for the 30th of April 2017. Then save your settings by pressing submit.

Now stop the webapp (red stop button in the RStudio viewer or just close the tab in a browser) and start it again. You should know see a nice timeline for the seminar matching in the initial panel.

The set of semester and many other sets like courses of study etc, can be adapted by changing the file sets.yaml in the folder sem-shared/yaml. Also the other yaml files allow customization, but leave them in the moment as they are.

Starting the teacher interface

To start the teacher interface that allows to add and modify seminars run the following code

main.dir = "C:/sema/sem-shared"
app = EditSeminarsApp(init.userid = "test", init.password="test", lang="en", main.dir = main.dir)
viewApp(app)

After you login, you should get the following error message:

The user test has not been given any rights to edit seminars in any group.

Teachers will be assigned to groups (e.g. institutes). There can be several members in a a group that can edit seminars (professors, secretaries, teaching assisstants...), but each person can only be in one group. The first members of groups have to be added manually in the table groupstaff in semDB. You can do this manually with SQLLiteStudio or run the following code:

db.dir = paste0(main.dir,"/db")
semdb = dbConnect(dbname=paste0(db.dir,"/semDB.sqlite"), drv = SQLite())
dbInsert(semdb,"groupstaff",list(userid="test",groupid="myinstitute",email="test",edit_Sem=TRUE, notify=TRUE, admin=TRUE, boss=TRUE))

Now try starting the app again and it should work.

Under the panel "Group Staff" you can now add additional users with rights to edit seminars or create new users in your group. The different permission fields are explained. A user with permission "boss" can not be deleted via the app but only with direct database access. In Ulm, I first manually added into the database the professors as bossess of their institute and let them later add additional users in a decentralized fashion.

In the panel "Seminars", you can create a new seminar for the current semester. Try it out.

The different form fields can be customized by modifying the corresponding yaml files in the folder sem-shared/yaml. When you change the fields, you also have to change the semDB database. For this purpose, you have to adapt the schema file sem-shared/schema/semdb.yaml and then recreate the database in a similar fashion as it was originally created. Note that some field names are hardcoded in the program and cannot be changed, e.g. "active" or "semname". These are fields with should be relevant for any university. So just don't change field names just for the fun of it.

I am aware that my documentation on customization is fairly sparse here, but I hope a bit try and error makes this work.

Starting the student interface

The following code starts a german student interface (change lang="en" for an english interface)

main.dir = "C:/sema/sem-shared"
app = StudSeminarsApp(init.userid = "test", init.password="test", lang="de", main.dir = main.dir)
viewApp(app)

Students must first enter background data. Similar to the seminar form for teachers, you can customize these forms by adapting the corresponding yaml files and database schema. Some fields like email, cannot be changed. Again, I note that this documentation is fairly sparse. So I guess it is trial-and-error.

After a student has entered her background data, she sees a list of all offfered seminars and can submit her preferences.

The panel "Hilfe" or in the englisch version "Help", gives some details on the matching algorithm and how the seminar matching proceeds.

Running an example matching

The following code generates some random students and then runs a matching round for test purposes.

# set working directory to your shared folder
setwd("C:/sema/sem-shared")


n = 100 # number of random students

# take the currently active semester
# based on the dates in the admin interface
# (some seminars should exist for that semester)
semester = "WS1617" 

round = 1 # round 1 or 2

# uncomment to delete previous matching
# delete.seminar.matching(semester=semester)
# delete.random.students(semester=semester)

draw.random.students(n=n,semester=semester,insert.into.db = TRUE, round=round)
df = perform.matching(semester=semester,insert.into.db = TRUE, round=round)

Now start the student app again. You will see on the first page, whether and where the student got a seminar slot. Of course she only can get a slot, if she listed some seminars in here preference list.

More interesting is the teacher app. Start it and click on some seminar.

In the tab "Particpants" you see a list of students that were matched to your seminar. Below you also see a list of students that did not get any seminar slot but put your seminar in their preference list. Teachers can manually add or remove students here. Students may be removed if they don't show up when topics are assigned and students may be added to fill the opened slots.

In the tab "Reports" you see some info on students' preferences and the resulting matching concerning your seminar.

Also take a look at the report admin interface. There you see some aggregate statistics about the first round of matching.

If you like, you can also run an example matching for round 2. In round 2 only seminar slots can be assigned that are still open after round 1. The main reason for there to be a 2nd matching round is for students who want to participate in 2 seminars to

Hints for customization

TO BE DONE

Installation on a webserver via Docker

Here is some basic information:

https://github.com/skranz/SeminarMatchingDocker

For a guide how to set-up an mail transfer agent, which is needed to send emails when new users register, see this document:

https://github.com/skranz/SeminarMatchingDocker/blob/master/Emails%20from%20Dockerized%20Shiny%20Apps.md

Timeline of seminar matching for a semester

Further aspects

Negative Autocorrelation in Random Priority Points

Often students have to participate in more than one seminar. It seems a bit unfair, if one student draws in every semester a low random priority. To counterveil this effect, we can include some negative autocorrelation between random priorities. If you had a high priority last time, you are more likely to get low priority this time, and vice versa. The relant lines of codes are the following:

   # give a bonus if last time random points were below 5
    if (stud$random_points < 5) {
      stud$random_points = runif(1,5-stud$random_points,10)
    # give a malus if last time random points were above 5
    } else {
      stud$random_points = runif(1,0,15-stud$random_points)
    }


skranz/SeminarMatching documentation built on June 9, 2020, 6:57 p.m.