From: Benjamin Mako Hill Date: Mon, 15 Jan 2018 03:04:58 +0000 (-0800) Subject: initial import of opensym2017 scraper/report X-Git-Url: https://code.communitydata.science/opensym2017_postmortem.git/commitdiff_plain/81a31bb2835c4270d9303334c83c5c3e530c3a70 initial import of opensym2017 scraper/report --- 81a31bb2835c4270d9303334c83c5c3e530c3a70 diff --git a/easychair-review-scraper.py b/easychair-review-scraper.py new file mode 100755 index 0000000..356cb90 --- /dev/null +++ b/easychair-review-scraper.py @@ -0,0 +1,53 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +""" Bot to scrape a list of EasyChair submissions and upload them to a wiki """ +# +# (C) Benjamin Mako Hill, 2018 +# (C) Federico Leva, 2016 +# +# Distributed under the terms of the MIT license. +# +__version__ = '0.2.0' + +# NOTE: change all copies of FIXME + +import requests +from lxml import html +import re +from kitchen.text.converters import to_bytes +import pandas as pd + +cj = requests.utils.cookiejar_from_dict( { "cool2": "FIXME", "cool1": "FIXME" } ) +headers = {"User-Agent": "Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:42.0) Gecko/20100101 Firefox/42.0" } +index = requests.get("https://easychair.org/conferences/status.cgi?a=FIXME", cookies=cj, headers=headers) +indexdata = html.fromstring(index.text) +urls = indexdata.xpath('//a[contains(@href,"review_for_paper.cgi")]/@href') + +reviews = pd.DataFrame() + +def empty_to_none(s): + if s == "": + s = None + return(s) + +for url in urls: + sub_html = html.fromstring(requests.get("https://easychair.org/conferences/" + url, + cookies=cj, headers=headers).text) + + # capture features of submissions + sub_id = sub_html.xpath('//title')[0].text + sub_id = re.sub(r'^Reviews and Comments on Submission (\d+)$', r'\1', sub_id) + + score_labels = ['label', 'date', 'reviewer', 'subreviewer', 'score', 'confidence' 'overall'] + for tr in sub_html.xpath('//th[text()="PC member"]/../../../tbody/tr'): + score = [td.text_content() for td in tr.xpath('td')] + score = [empty_to_none(x) for x in score] + score_dict = dict(zip(score_labels, score)) + score_dict["sub_id"] = sub_id + reviews = reviews.append(pd.DataFrame(score_dict, index=[0])) + +reviews["date"] = reviews["date"] + ", 2017" +reviews["date"] = pd.to_datetime(reviews["date"]) + +reviews.to_csv("opensym-reviews-20180113.csv", index=False, index_label=False) + diff --git a/easychair-submissions-scraper.py b/easychair-submissions-scraper.py new file mode 100755 index 0000000..e839258 --- /dev/null +++ b/easychair-submissions-scraper.py @@ -0,0 +1,130 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +""" Bot to scrape a list of EasyChair submissions and upload them to a wiki """ +# +# (C) Benjamin Mako Hill, 2018 +# (C) Federico Leva, 2016 +# +# Distributed under the terms of the MIT license. +# +__version__ = '0.2.0' + +# NOTE: change all copies of FIXME + +import requests +from lxml import html +import re +from kitchen.text.converters import to_bytes +import pandas as pd + +cj = requests.utils.cookiejar_from_dict( { "cool2": "FIXME", "cool1": "FIXME" } ) +headers = {"User-Agent": "Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:42.0) Gecko/20100101 Firefox/42.0" } +index = requests.get("https://easychair.org/conferences/submission_show_all.cgi?a=FIXME", cookies=cj, headers=headers) +indexdata = html.fromstring(index.text) +urls = indexdata.xpath('//a[contains(@href,"submission_info_show.cgi")]/@href') + +submissions = pd.DataFrame() +authors = pd.DataFrame() +reviewers = pd.DataFrame() +author_keywords = pd.DataFrame() +easychair_keywords = pd.DataFrame() +bids = pd.DataFrame() + +for url in urls: + sub_html = html.fromstring(requests.get("https://easychair.org/conferences/" + url, + cookies=cj, headers=headers).text) + + # capture features of submissions + sub_id = sub_html.xpath('//title')[0].text + sub_id = re.sub(r'^Submission (\d+)$', r'\1', sub_id) + + final_type = sub_html.xpath('//td[text()="Category"]/../td[2]')[0].text + title = sub_html.xpath('//td[text()="Title:"]/../td[2]/text()')[0].strip() + + # it's possible to submit papers w/o topics + try: + topic = sub_html.xpath('//span[text()="Topics:"]/../../td[2]/text()')[0].strip() + except IndexError: + topic = None + + abstract = sub_html.xpath('//td[text()="Abstract:"]/../td[2]')[0].text.strip() + result = sub_html.xpath('//td[text()="Decision:"]/../td[2]')[0].text_content().strip() + + submissions = submissions.append(pd.DataFrame({ 'sub_id' : sub_id, + 'type' : final_type, + 'title' : title, + 'topic' : topic, + 'abstract' : abstract, + 'result' : result}, + index=[0])) + + # create a list of authors + names = sub_html.xpath('//b[text()="Authors"]/../../..//tr[@id!="row37"]/td[1]/text()') + surnames = sub_html.xpath('//b[text()="Authors"]/../../..//tr[@id!="row37"]/td[2]/text()') + countries = sub_html.xpath('//b[text()="Authors"]/../../..//tr[@id!="row37"]/td[4]/text()') + + for i in range(1, len(names)): + authors = authors.append(pd.DataFrame({ 'sub_id' : sub_id, + 'author' : " ".join([names[i], surnames[i]]), + 'country' : countries[i] }, + index=[0])) + + # add the list of reviewers + assigned_to = sub_html.xpath('//span[text()="Assigned to:"]/../../td[2]')[0].text.strip().split(", ") + + reviewers = reviewers.append(pd.DataFrame({ 'sub_id' : sub_id, + 'reviewer' : assigned_to, + 'type' : 'normal' })) + + senior_pc = sub_html.xpath('//span[text()="Senior PC member:"]/../../td[2]')[0].text + senior_pc = re.sub(r'^(.+?) \<.*$', r'\1', senior_pc) + + reviewers = reviewers.append(pd.DataFrame({ 'sub_id' : sub_id, + 'reviewer' : senior_pc, + 'type' : 'senior' }, + index=[0])) + + # add author keywords + sub_author_keywords = sub_html.xpath('//div[parent::td[@class="value"]]/text()') + sub_author_keywords = [x.lower() for x in sub_author_keywords] + + author_keywords = author_keywords.append(pd.DataFrame({ 'sub_id' : sub_id, + 'keyword' : sub_author_keywords})) + + + # easychair keywords + sub_easychair_keywords = sub_html.xpath('//span[text()="EasyChair keyphrases:"]/../../td[2]')[0].text.strip() + sub_easychair_keywords = sub_easychair_keywords.split(", ") + + for kw in sub_easychair_keywords: + g = re.match(r'^\s*([A-Za-z1-9 ]+) \((\d+)\)\s*$', kw).groups() + easychair_keywords = easychair_keywords.append(pd.DataFrame({ 'sub_id' : sub_id, + 'keyword' : g[0].lower(), + 'number' : g[1]}, + index=[0])) + + #coi = sub_html.xpath('//span[text()="Conflict of interest:"]/../../td[2]')[0].text.strip() + #if coi == "nobody": + # coi = [] + #else: # TODO this is not tested on /any/ data + # coi = coi.split(", ") + + def parse_bid_tbl(tbl): + key = re.sub(r'^\s*([a-z]+):\s*$', r'\1', tbl[0][0].text) + return((key, tbl[0][1].text.split(", "))) + + sub_bids = dict([ parse_bid_tbl(x) for x in sub_html.xpath('//td[text()="Bid:"]/../td[2]/table[*]') ]) + + for bid_type in sub_bids: + bids = bids.append(pd.DataFrame({ 'sub_id' : sub_id, + 'bid' : bid_type, + 'bidder' : sub_bids[bid_type] })) + + +submissions.to_csv("opensym-submissions-20180113.csv", index=False, index_label=False) +authors.to_csv("opensym-authors-20180113.csv", index=False, index_label=False) +reviewers.to_csv("opensym-reviewers-20180113.csv", index=False, index_label=False) +author_keywords.to_csv("opensym-author_keywords-20180113.csv", index=False, index_label=False) +easychair_keywords.to_csv("opensym-easychair_keywords-20180113.csv", index=False, index_label=False) +bids.to_csv("opensym-bids-20180113.csv", index=False, index_label=False) + diff --git a/extract_pdf_page_lengths.sh b/extract_pdf_page_lengths.sh new file mode 100755 index 0000000..cd08e16 --- /dev/null +++ b/extract_pdf_page_lengths.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +find $@ -print0 |xargs -0 -n1 pdfinfo|grep Pages|awk '{print $2}' > opensym2017-pdf_page_lengths.list diff --git a/opensym2017_postmortem.Rmd b/opensym2017_postmortem.Rmd new file mode 100644 index 0000000..62db158 --- /dev/null +++ b/opensym2017_postmortem.Rmd @@ -0,0 +1,238 @@ +--- +title: "OpenSym 2017 Program Postmortem" +output: html_document +--- + +```{r setup, include=FALSE} +knitr::opts_chunk$set(echo = TRUE) +``` + +```{r, echo=FALSE} +library(data.table) +library(ggplot2) +library(xtable) +library(knitr) + +reviews <- read.csv("opensym-reviews-20180113.csv", stringsAsFactors = FALSE) +colnames(reviews) <- gsub('_', '.', colnames(reviews)) +reviews$date <- as.Date(reviews$date) +reviews$subreviewer[reviews$subreviewer == ""] <- NA +reviews$reviewer[reviews$reviewer == ""] <- NA + +submissions <- read.csv("opensym-submissions-20180113.csv", stringsAsFactors=FALSE) +colnames(submissions) <- gsub('_', '.', colnames(submissions)) + +authors <- read.csv("opensym-authors-20180113.csv", stringsAsFactors=FALSE) +colnames(authors) <- gsub('_', '.', colnames(authors)) +``` + +```{r echo=FALSE} +crop.scores <- function (sub.id, before=FALSE) { + d <- reviews[reviews$sub.id == sub.id,] + cut.off <- d[d$label == "Author response", "date"] + + # reduce it to everything before if we're before + if (before) { + d <- d[d$date < cut.off,] + } + + d <- d[grep('^Review ', d$label),] + + # remove duplicates keeping the final score in the zone + d <- d[sort.list(d$date, decreasing=TRUE),] + d <- d[!duplicated(d$label),] + d <- d[sort.list(d$date),] + + return(d) +} + +scores.before <- do.call("rbind", lapply(unique(reviews$sub.id), crop.scores, before=TRUE)) +scores.after <- do.call("rbind", lapply(unique(reviews$sub.id), crop.scores)) + +avg.score.df <- function (d) { + tbl <- tapply(d$score, d$sub.id, mean) + data.frame(sub.id=as.integer(names(tbl)), + avg.score=as.numeric(tbl)) + +} + +scores <- merge(avg.score.df(scores.before), avg.score.df(scores.after), by="sub.id", suffixes=c(".before", ".after")) +scores$delta <- scores$avg.score.before - scores$avg.score.after + +grid.tmp <- melt(scores, id.vars=c("sub.id")) + +# build table of changes +scores$change <- NA +scores$change[scores$delta == 0] <- "none" +scores$change[scores$delta > 0] <- "decrease" +scores$change[scores$delta < 0] <- "increase" + +# merge in data about submissions +scores <- merge(scores, submissions[,c("sub.id", "type", "result")], + by="sub.id") + +scores$decision <- "Rejected" +scores$decision[scores$type == "full paper" & scores$result == "ACCEPT"] <- "Accepted" +scores$decision[scores$type == "poster" & scores$result == "ACCEPT"] <- "Poster" + +scores$sub.id <- ordered(scores$sub.id, levels=scores$sub.id[sort.list(scores$avg.score.after)]) +scores.after$sub.id <- ordered(scores.after$sub.id, levels=levels(scores$sub.id)) +``` + +The [International Symposium on Open Collaboration](http://www.opensym.org/) (*OpenSym*, formerly *WikiSym*) is the premier academic venue exclusively focused on scholarly research into open collaboration. OpenSym is an [ACM](http://www.acm.org/) conference which means that, like conferences in computer science, it's really more like a journal that gets published once a year than it is like most social science conferences. The "journal", in this case, is called the *Proceedings of OpenSym* and it consists of all the papers presented there. Papers that are published in the proceedings are not typically published elsewhere. + +Along with [Claudia Müller-Birn](https://www.clmb.de/) from the [Freie Universtät Berlin](http://www.fu-berlin.de/), I served as the *Program Chair* for OpenSym 2017. For the social scientists reading this, the role of program chair is similar to being an editor for a journal. My job was *not* to organize keynotes or logistics at the conference—that is the job of the General Chair. Indeed, in the end I didn't even attend the conference! Along with Claudia, my role as Program Chair was to recruit submissions, recruit reviewers, coordinate and manage the review process, make final decisions on papers, and ensure that everything makes it into the published proceedings in good shape. + +In OpenSym 2017, we made several changes to the way the conference has been run: + +* In previous years, OpenSym had tracks on topics like free/open source software, wikis, open innovation, open education, and so on. In 2017, **we used a single track model**. +* Because we eliminated tracks, we also eliminated track-level chairs. Instead, **we appointed a series of Associate Chairs or ACs**. +* **We eliminated page limits and the distinction between full papers and notes**. +* **We allowed authors to write rebuttals before reviews were finalized.** Reviewers and ACs were allowed to modify their reviews and decisions based on rebuttals. +* To assist in assigning papers to ACs and to reviewers, **we made extensive use of bidding**. This means we had to recruit the pool of reviewers before papers were submitted. + +Although each of these things have been tried in other conferences, all were new to + +# Overview + +```{r, echo=FALSE, results="asis"} +# two papers were originally submitted as posters +num.papers.accepted <- table(submissions$result == "ACCEPT" & submissions$type == "full paper")["TRUE"] + +tbl.tmp <- as.data.frame(rbind( + list("Papers submitted", + nrow(submissions) -2), # HARD CODED!! + list("Papers accepted", + num.papers.accepted), + list("Acceptance rate", + paste(round(num.papers.accepted / (nrow(submissions) - 2)*100), "%", sep="")), + list("Posters submitted", + 2), # HARD CODED!! + list("Posters presented", + table(submissions$result == "ACCEPT" & submissions$type == "poster")["TRUE"]), + list("Associate Chairs", + 8), + list("PC Members", + length(na.omit(unique(c(reviews$reviewer, reviews$subreviewer))))), + list("Authors", + length(unique(authors$author))), + list("Author countries", + length(unique(authors$country))) + )) + +colnames(tbl.tmp) <- c("Statistics", "") + +kable(tbl.tmp) + +``` + +The program was similar in size to the last 2-3 years in terms of the number of submissions. OpenSym is a small but mature and stable venue for research on open collaboration. This year was also similar, although slightly more competitive, in terms of the conference acceptance rate (`r round(num.papers.accepted / (nrow(submissions) - 2)*100)`%—it had been slightly above 50% in previous years). + +As in recent years, there were more posters accepted than submitted because the PC found that some rejected work, although not ready to be published in the proceedings, was promising and advanced enough to be presented as a poster at the conference. Authors of posters submitted 4-page extended abstracts for their projects which were published in a "*Companion to the Proceedings*."" + +# Topics + +Over the years, OpenSym has established a clear set of niches. Although we eliminated tracks, we asked authors to choose from a set of categories when submitting their work. These categories are similar to the tracks at OpenSym 2016. Interestingly, a number of authors selected more than one category. This would have led to difficult decisions in the old track-based system. + +```{r topics, echo=FALSE} +submissions$topic <- sub(", esp. Wikis and Social Media", '', submissions$topic) +submissions$topic.nums <- sapply(strsplit(submissions$topic, ", "), length) + +# create a list of topics +topics <- data.frame(topic=unlist(strsplit(submissions$topic, ", ")), + sub.id=unlist(lapply(1:nrow(submissions), + function (i) { rep(submissions$sub.id[i], submissions$topic.nums[i]) }))) + +# add decisions +topics <- merge(topics, scores[,c("sub.id", "decision")], by="sub.id") +topics$topic <- factor(topics$topic, levels=names(sort(table(topics$topic)))) + +ggplot(data=topics) + aes(x=topic, fill=decision) + + geom_bar() + + scale_y_continuous("Count") + + scale_x_discrete("") + + coord_flip() + + scale_fill_discrete("Decision") + + theme_minimal() + + theme(legend.position="bottom", legend.direction = "horizontal") +``` + +The figure above shows a breakdown of papers in terms of these categories as well as indicators of how many papers in each group were accepted. Research on FLOSS and Wikimedia/Wikipedia continue to make up a sizable chunk of OpenSym's submissions and publications. That said, these now make up a minority of total submissions. Although Wikipedia and Wikimedia research made up a smaller proportion of submission pool, it was accepted at a higher rate. Also notable is the fact that 2017 saw an uptick in the number of papers on open innovation. I suspect this was due, at least in part, to work by the General Chair [Lorraine Morgan's](https://www.nuigalway.ie/our-research/people/lorrainemorgan/) involvement (she specializes in that area). Somewhat surprisingly to me, we had a number of submission about Bitcoin and blockchains. These are natural areas of growth for OpenSym but have never been a big part of work in our community in the past. + +# Scores and Reviews + +As in previous years, review was single blind in that reviewers' identities are hidden but authors identities are not. Each papers received between 3 and 4 reviews plus a metareview by the Associate Chair assigned to the paper. Almost all papers received 3 reviews but ACs were encouraged to call in a 4th reviewer at any point in the process. In addition to the text of the reviews, we used a -3 to +3 scoring system where papers that are seen as borderline will be scored as 0. Reviewers scored papers using half-point increments. + +```{r, echo=FALSE} +## generate the score graphs +ggplot(data=scores) + aes(x=sub.id) + + geom_hline(yintercept = 0, color="orange") + + geom_line(data=scores.after, aes(y=score), color="grey") + + geom_point(aes(y=avg.score.after, color=decision)) + + scale_x_discrete("", limits=levels(scores$sub.id), labels=c()) + + scale_y_continuous("Reviewer Score") + + scale_color_discrete("Final Decision") + + theme_minimal() + + theme(legend.position="bottom", legend.direction="horizontal") +``` + + +The figure above shows scores for each paper submitted. The vertical grey lines reflect the distribution of scores where the minimum and maximum scores for each paper are the ends of the lines. The colored dots show the arithmetic mean for each score (unweighted by reviewer confidence). Colors show whether the papers were accepted, rejected, or presented as a poster. It's important to keep in mind that two papers were *submitted* as posters. Although Associate Chairs made the final decisions on a case-by-case basis, most papers that had an average score of less than 0 (the horizontal orange line) were rejected and most papers with positive average scores were accepted. We ultimately accepted `r num.papers.accepted` papers (`r paste(round(num.papers.accepted / (nrow(submissions) - 2)*100), "%", sep="")`) of those submitted. + +# Rebuttals + +This was the first time that OpenSym used a rebuttal or author response and were thrilled with how it went. Although they were entire optional, almost everybody used it! Authors of `r length(reviews[reviews$label == "Author response","sub.id"])` of our `r nrow(submissions)` submissions (`r round(length(reviews[reviews$label == "Author response","sub.id"]) / nrow(submissions)*100)`%!) submitted rebuttals. + +```{r, echo=FALSE} +# histogram of changes +# qplot(grid.tmp[grid.tmp$variable == "delta", "value"]) + +rebut.tbl <- table(scores[scores$sub.id %in% reviews[reviews$label == "Author response","sub.id"], "change"]) + +kable(data.frame("Lower"=rebut.tbl["decrease"], + "Unchanged"=rebut.tbl["none"], + "Higher"=rebut.tbl["increase"]), + row.names = FALSE) + +``` + +The table above shows how average scores changed after authors submitted rebuttals. The table shows that rebuttals' effect was typically neutral or positive. Most average scores stayed the same but nearly two times as many increased as decreased in the post-rebuttal period. We hope that this made the process feel more fair for authors and I feel, having read them all, that it led to improvements in the quality of final papers. + +# Page Lengths + +In previous years, OpenSym followed most other venues in computer science by allowing submission of two kinds of papers: full papers which could be up to 10 pages long and short papers which could be up to 4. Following some other conferences, we eliminated page limits altogether. This is the text we used in [the OpenSym 2017 CFP](https://perma.cc/87QY-27FS): + +> There is no minimum or maximum length for submitted papers. Rather, reviewers will be instructed to weigh the contribution of a paper relative to its length. Papers should report research thoroughly but succinctly: brevity is a virtue. A typical length of a “long research paper” is 10 pages (formerly the maximum length limit and the limit on OpenSym tracks), but may be shorter if the contribution can be described and supported in fewer pages— shorter, more focused papers (called “short research papers” previously) are encouraged and will be reviewed like any other paper. While we will review papers longer than 10 pages, the contribution must warrant the extra length. Reviewers will be instructed to reject papers whose length is incommensurate with the size of their contribution. + +The following graph shows the distribution of page lengths in our final program. + +```{r, echo=FALSE} +pdf.pages <- read.table("opensym2017-pdf_page_lengths.list") +colnames(pdf.pages) <- "pages" + +ggplot(data=pdf.pages) + aes(x=pages) + geom_bar() + scale_x_continuous("Pages", breaks=seq(1, 14), labels=seq(1, 14)) + scale_y_continuous("Number of papers") + coord_flip() + theme_minimal() +``` + +In the end `r table(pdf.pages$pages > 10)["TRUE"]` of `r num.papers.accepted` published papers (`r round(table(pdf.pages$pages > 10)["TRUE"] / num.papers.accepted * 100, 2)`%) were over 10 pages. More surprisingly, `r table(pdf.pages$pages < 10)["TRUE"]` of the accepted papers (`r round(table(pdf.pages$pages < 10)["TRUE"] / num.papers.accepted * 100, 2)`%) were below the old 10-page limit. Fears that some have expressed that page limits are the only thing keeping OpenSym authors from submitting enormous rambling manuscripts seems to be unwarranted—at least so far. + +# Bidding + +Although, I won't post any analysis or graphs, bidding worked well. With only two exceptions, every single assigned review was to someone who had bid "yes" or "maybe" for the paper in question and the vast majority went to people that had bid "yes". However, this comes with one major proviso: people that did not bid at all were marked as "maybe" for every single paper. + +Given a reviewer pool whose diversity of expertise matches that in your pool of authors, bidding works fantastically. *But everybody needs to bid*. The only problems with reviewers we had were with people that had failed to bid. +It might be reviewers who don't bid are less committed to the conference, more overextended, more likely to drop things in general, etc. It might also be that reviewers who fail to bid get good matches become less interested, willing, or able to do their reviews well and on time. + +Having used bidding twice as chair or track-chair, my sense is that bidding is a fantastic thing to incorporate into any conference review process. The major limitations are that you need to build a PC before the conference (rather than finding the perfect reviewers for specific papers) and you have to find ways to incentive or communicate the importance of getting your PC members to bid. + +# Conclusions + +The final results were [a fantastic collection of published papers](https://blog.communitydata.cc/opensym-2017-program/). Of course, it couldn't have been possible without the huge collection of [conference chairs, associate chairs, program committee members, external reviewers, and staff supporters](http://opensym.lero.ie/organisation/organization/). + +Although we tried quite a lot of new things, my sense is that nothing we tried made things worse and many things made things smoother or better. Although I'm not directly involved in organizing OpenSym 2018, I am on the OpenSym steering committee. My sense is that most of the changes we made are going to be carried over this year. + +Finally, it's also been announced that [OpenSym 2018 will be in Paris on August 22-24](http://www.opensym.org/os2018/). The call for papers should be out soon with, I suspect, a spring paper submission deadline. You should consider submitting! I hope to see you in Paris! + +# This Analysis + +OpenSym used the gratis version of [EasyChair](https://www.easychair.org/) to manage the conference which doesn't allow chairs to export data. As a result, data used in this this postmortem was scraped from EasyChair using two Python scripts. Numbers and graphs were created using a [knitr](https://yihui.name/knitr/) file that combines R visualization and analysis code and markdown. I've made all the code I used to produce this analysis available in [this git repository](FIXME). I hope someone else finds it useful. Because the data contains sensitive information on the review process, I'm not going to publish the data. +