From f52eb4340caf71db3192adf4c9464eb0ee8e0332 Mon Sep 17 00:00:00 2001 From: qubixes <44498096+qubixes@users.noreply.github.com> Date: Thu, 26 Mar 2020 21:54:32 +0100 Subject: [PATCH] Modularize (#8) --- .github/workflows/ci-workflow.yml | 46 ++++ README.md | 62 +++-- asreviewcontrib/visualization/entrypoint.py | 48 ++-- asreviewcontrib/visualization/plot.py | 227 ++---------------- asreviewcontrib/visualization/plot_base.py | 32 +++ .../visualization/plot_discovery.py | 27 +++ .../visualization/plot_inclusions.py | 125 ++++++++++ asreviewcontrib/visualization/plot_limit.py | 39 +++ .../visualization/plot_progression.py | 60 +++++ asreviewcontrib/visualization/quick.py | 46 ++++ docs/progression.png | Bin 0 -> 79267 bytes tests/data/embase_labelled.csv | 7 + tests/test_plot.py | 85 +++++++ 13 files changed, 561 insertions(+), 243 deletions(-) create mode 100644 .github/workflows/ci-workflow.yml create mode 100644 asreviewcontrib/visualization/plot_base.py create mode 100644 asreviewcontrib/visualization/plot_discovery.py create mode 100644 asreviewcontrib/visualization/plot_inclusions.py create mode 100644 asreviewcontrib/visualization/plot_limit.py create mode 100644 asreviewcontrib/visualization/plot_progression.py create mode 100644 asreviewcontrib/visualization/quick.py create mode 100644 docs/progression.png create mode 100644 tests/data/embase_labelled.csv create mode 100644 tests/test_plot.py diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml new file mode 100644 index 0000000..ead4555 --- /dev/null +++ b/.github/workflows/ci-workflow.yml @@ -0,0 +1,46 @@ +name: test-suite +on: [push, pull_request] +jobs: + test-master: + name: pytest + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + path: asr-plot + + - uses: actions/checkout@v2 + with: + repository: asreview/asreview + path: asr-core + - uses: actions/setup-python@v1 + with: + python-version: '3.6' + architecture: 'x64' + - name: Install packages and run tests + run: | + pip install pytest + pip install --upgrade setuptools>=41.0.0 + pip install ./asr-core[all] + pip install ./asr-plot + pytest asr-plot/tests + + #test-older: + #name: pytest + #runs-on: ubuntu-latest + #strategy: + #matrix: + #asr_versions: ['0.6', '0.7', '0.7.1', '0.7.2'] + #steps: + #- uses: actions/checkout@v2 + #- uses: actions/setup-python@v1 + #with: + #python-version: '3.6' + #architecture: 'x64' + #- name: Install packages and run tests + #run: | + #pip install pytest + #pip install --upgrade setuptools>=41.0.0 + #pip install asreview[all]==${{ matrix.asr_versions }} + #pip install . + #pytest tests diff --git a/README.md b/README.md index 37f733e..01acae2 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ -## ASReview-visualization +# ASReview-visualization -![Deploy and release](https://github.com/msdslab/asreview-visualization/workflows/Deploy%20and%20release/badge.svg) +![Deploy and release](https://github.com/asreview/asreview-visualization/workflows/Deploy%20and%20release/badge.svg)![Build status](https://github.com/asreview/asreview-visualization/workflows/test-suite/badge.svg) This is a plotting/visualization supplemental package for the [ASReview](https://github.com/asreview/asreview) software. It is a fast way to create a visual impression of the ASReview with different dataset, models and model parameters. -### Installation +## Installation The easiest way to install the visualization package is to use the command line: @@ -24,11 +24,11 @@ asreview --help It should list the 'plot' modus. -### Basic usage +## Basic usage -Log files that were created with the same ASReview settings can be put together/averaged by putting -them in the same directory. Log files with different settings/datasets should be put in different -directories to compare them. It is advised to put these log files in the same directory. +State files that were created with the same ASReview settings can be put together/averaged by putting +them in the same directory. State files with different settings/datasets should be put in different +directories to compare them. As an example consider the following directory structure, where we have two datasets, called `ace` and `ptsd`, each of which have 8 runs: @@ -68,17 +68,18 @@ asreview plot ace ptsd --absolute-values ``` -### Plot types +## Plot types -There are currently three plot types implemented: _inclusions_, _discovery_, _limits_. They can be -individually selected with the `-t` or `--type` switch. Multiple plots can be made by using `,` as -a separator: +There are currently four plot types implemented: +_inclusion_, _discovery_, _limit_, _progression_. +They can be individually selected with the `-t` or `--type` switch. Multiple plots +can be made by using `,` as a separator: ```bash asreview plot ace ptsd --type 'inclusions,discovery' ``` -#### Inclusions +### Inclusion This figure shows the number/percentage of included papers found as a function of the number/percentage of papers reviewed. Initial included/excluded papers are subtracted so that the line @@ -88,7 +89,7 @@ The quicker the line goes to a 100%, the better the performance. ![alt text](https://github.com/msdslab/asreview-visualization/blob/master/docs/inclusions.png?raw=true "Inclusions") -#### Discovery +### Discovery This figure shows the distribution of the number of papers that have to be read before discovering each inclusion. Not every paper is equally hard to find. @@ -98,7 +99,7 @@ The closer to the left, the better. ![alt text](https://github.com/msdslab/asreview-visualization/blob/master/docs/discovery.png?raw=true "Discovery") -#### Limits +### Limit This figure shows how many papers need to be read with a given criterion. A criterion is expressed as "after reading _y_ % of the papers, at most an average of _z_ included papers have been not been @@ -109,3 +110,36 @@ _z_ are 0.1, 0.5 and 2.0. The quicker the lines touch the black (`y=x`) line, the better. ![alt text](https://github.com/msdslab/asreview-visualization/blob/master/docs/limits.png?raw=true "Limits") + +### Progression + +This figure shows the average inclusion rate as a function of time, number of papers read. +The more concentrated on the left, the better. The thick line is the average of individual runs +(thin lines). The visualization package will automatically detect which are directories and which +are files. The curve is smoothed out by using a Gaussian smoothing algorithm. + +![alt text](https://github.com/msdslab/asreview-visualization/blob/master/docs/progression.png?raw=true "Progression") + + +## API + +To make use of the more advanced features, you can also use the visualization package +as a library. The advantage is that you can make more reproducible plots where text, etc. is +in the place *you* want it. Examples can be found in module `asreviewcontrib.visualization.quick`. +Those are the scripts that are used for the command line interface. + +```python +with Plot.from_paths(["PATH_1", "PATH_2"]) as plot: + inc_plot = plot.new("inclusion") + inc_plot.set_grid() + inc_plot.set_xlim(0, 30) + inc_plot.set_ylim(0, 101) + inc_plot.set_legend() + inc_plot.show() + inc_plot.save("SOME_FILE.png") +``` + +Of course fill in `PATH_1` and `PATH_2` as the files you would like to plot. + +If the customization is not sufficient, you can also directly manipulate the `self.ax` and +`self.fig` attributes of the plotting class. \ No newline at end of file diff --git a/asreviewcontrib/visualization/entrypoint.py b/asreviewcontrib/visualization/entrypoint.py index 835ff9e..676f512 100644 --- a/asreviewcontrib/visualization/entrypoint.py +++ b/asreviewcontrib/visualization/entrypoint.py @@ -13,14 +13,18 @@ # limitations under the License. import argparse +import logging from asreview.config import LOGGER_EXTENSIONS from asreview.entry_points import BaseEntryPoint from asreviewcontrib.visualization import Plot -import logging +from asreviewcontrib.visualization.quick import progression_plot +from asreviewcontrib.visualization.quick import limit_plot +from asreviewcontrib.visualization.quick import discovery_plot +from asreviewcontrib.visualization.quick import inclusion_plot -PLOT_TYPES = ['inclusions', 'discovery', 'limits'] +PLOT_TYPES = ['inclusion', 'discovery', 'limit', 'progression'] class PlotEntryPoint(BaseEntryPoint): @@ -54,8 +58,9 @@ def execute(self, argv): else: result_format = "percentage" + output = args_dict["output"] + prefix = args_dict["prefix"] - legend = not args_dict["no_legend"] with Plot.from_paths(args_dict["data_paths"], prefix=prefix) as plot: if len(plot.analyses) == 0: print(f"No log files found in {args_dict['data_paths']}.\n" @@ -63,14 +68,15 @@ def execute(self, argv): f" and end with one of the following: \n" f"{', '.join(LOGGER_EXTENSIONS)}.") return - if "inclusions" in types: - plot.plot_inc_found(result_format=result_format, legend=legend, - abstract_only=args_dict["abstract_only"], - wss_value=args_dict["wss_value"]) + + if "inclusion" in types: + inclusion_plot(plot, output=output, result_format=result_format) # noqa if "discovery" in types: - plot.plot_time_to_discovery(result_format=result_format) - if "limits" in types: - plot.plot_limits(result_format=result_format) + discovery_plot(plot, output=output, result_format=result_format) # noqa + if "limit" in types: + limit_plot(plot, output=output, result_format=result_format) # noqa + if "progression" in types: + progression_plot(plot, output=output, result_format=result_format) # noqa def _parse_arguments(): @@ -102,21 +108,11 @@ def _parse_arguments(): 'starting with a prefix.' ) parser.add_argument( - "--abstract_only", - default=False, - action="store_true", - help="Use after abstract screening as the inclusions/exclusions." - ) - parser.add_argument( - "--no_legend", - default=False, - action="store_true", - help="Don't show a legend with the plot." - ) - parser.add_argument( - "--wss_value", - default=False, - action="store_true", - help="Add WSS values to plot." + "-o", "--output", + default=None, + help='Save the plot to a file. If multiple plots are made, only one' + ' is saved (non-deterministically). File formats are detected ' + ' by the matplotlib library, check there to see available ' + 'formats.' ) return parser diff --git a/asreviewcontrib/visualization/plot.py b/asreviewcontrib/visualization/plot.py index 20f3dc9..5c7d241 100644 --- a/asreviewcontrib/visualization/plot.py +++ b/asreviewcontrib/visualization/plot.py @@ -15,49 +15,12 @@ from collections import OrderedDict import os -import matplotlib.pyplot as plt -import numpy as np - from asreview.analysis.analysis import Analysis - -def _add_WSS(WSS, analysis, ax, col, result_format, box_dist=0.5, - add_value=False, **kwargs): - if WSS is None: - return - - text = f"WSS@{WSS}%" - WSS_val, WSS_x, WSS_y = analysis.wss(WSS, x_format=result_format, **kwargs) - if WSS_x is None or WSS_y is None: - return - - if add_value: - text += r"$\approx" + f" {round(WSS_val, 2)}" + r"$" - text_pos_x = WSS_x[0] + box_dist - text_pos_y = (WSS_y[0] + WSS_y[1])/2 - plt.plot(WSS_x, WSS_y, color=col, ls="--") - plt.plot(WSS_x, (0, WSS_y[0]), color=col, ls=":") - bbox = dict(boxstyle='round', facecolor=col, alpha=0.5) - ax.text(text_pos_x, text_pos_y, text, color="black", bbox=bbox) - - -def _add_RRF(RRF, analysis, ax, col, result_format, box_dist=0.5, **kwargs): - if RRF is None: - return - - text = f"RRF@{RRF}%" - _, RRF_x, RRF_y = analysis.rrf(RRF, x_format=result_format, **kwargs) - if RRF_x is None or RRF_y is None: - return - - RRF_x = 0, RRF_x[0] - RRF_y = RRF_y[1], RRF_y[1] - text_pos_x = RRF_x[0] + box_dist - text_pos_y = RRF_y[0] + box_dist + 2 - plt.plot(RRF_x, RRF_y, color=col, ls="--") - bbox = dict(boxstyle='round', facecolor=col, alpha=0.5) - ax.text(text_pos_x, text_pos_y, text, color="black", bbox=bbox) - +from asreviewcontrib.visualization.plot_inclusions import PlotInclusions +from asreviewcontrib.visualization.plot_progression import PlotProgression +from asreviewcontrib.visualization.plot_discovery import PlotDiscovery +from asreviewcontrib.visualization.plot_limit import PlotLimit class Plot(): def __init__(self, paths, prefix="result"): @@ -74,6 +37,12 @@ def __init__(self, paths, prefix="result"): self.is_file[data_key] = True else: self.is_file[data_key] = False + all_files = all(self.is_file.values()) + if all_files: + self.thick = {key: True for key in list(self.analyses)} + else: + self.thick = {key: not f for key, f in self.is_file.items()} + def __enter__(self): return self @@ -84,168 +53,20 @@ def __exit__(self, *_, **__): @classmethod def from_paths(cls, paths, prefix="result"): - plot_inst = cls(paths, prefix=prefix) + plot_inst = Plot(paths, prefix=prefix) return plot_inst - def plot_time_to_inclusion(self, X_fp): - for data_key, analysis in self.analyses.items(): - results = analysis.time_to_inclusion(X_fp) - for key in results["ttd"]: - plt.plot(results["x_range"], results["ttd"][key], - label=data_key + " - " + key) - plt.legend() - plt.show() - - def plot_time_to_discovery(self, result_format="percentage"): - avg_times = [] - for analysis in self.analyses.values(): - results = analysis.avg_time_to_discovery( - result_format=result_format) - avg_times.append(list(results.values())) - - if result_format == "number": - plt.hist(avg_times, 30, histtype='bar', density=False, - label=self.analyses.keys()) - plt.xlabel("# Reviewed") - plt.ylabel("# of papers included") - else: - plt.hist(avg_times, 30, histtype='bar', density=True, - label=self.analyses.keys()) - plt.xlabel("% Reviewed") - plt.ylabel("Fraction of papers included") - plt.legend() - plt.show() - - def plot_inc_found(self, result_format="percentage", abstract_only=False, - legend=True, wss_value=False): - """ - Plot the number of queries that turned out to be included - in the final review. - """ - legend_name = [] - legend_plt = [] + def new(self, plot_type="inclusion", **kwargs): + thick = kwargs.pop("thick", None) + if thick is None: + thick = self.thick + if plot_type == "inclusion": + return PlotInclusions(self.analyses, thick=thick, **kwargs) + elif plot_type == "progression": + return PlotProgression(self.analyses, thick=thick, **kwargs) + elif plot_type == "discovery": + return PlotDiscovery(self.analyses, **kwargs) + elif plot_type == "limit": + return PlotLimit(self.analyses, **kwargs) + raise ValueError(f"Error: plot type '{plot_type}' not found.") - fig, ax = plt.subplots() - - max_len = 0 - for i, data_key in enumerate(reversed(self.analyses)): - analysis = self.analyses[data_key] - - inc_found = analysis.inclusions_found(result_format=result_format) - n_initial = analysis.inc_found[False]["n_initial"] - n_after_init = len(analysis.labels) - n_initial - max_len = max(max_len, n_after_init) - if result_format == "percentage": - box_dist = 0.5 - else: - box_dist = 100 - col = "C"+str((len(self.analyses)-1-i) % 10) - if (legend or i == len(self.analyses)-1) and not abstract_only: - _add_WSS(95, analysis, ax, col, result_format, box_dist, - add_value=wss_value) - _add_WSS(100, analysis, ax, col, result_format, box_dist, - add_value=wss_value) - _add_RRF(10, analysis, ax, col, result_format, box_dist) -# _add_RRF(5, analysis, ax, col, result_format, box_dist) - - if self.is_file[data_key]: - line_width = 0.7 - else: - line_width = 2 - - myplot = plt.errorbar(*inc_found, color=col, lw=line_width) - if abstract_only: - legend_name.append(f"{data_key} (abstract)") - else: - legend_name.append(f"{data_key}") - legend_plt.append(myplot) - - if result_format == "percentage": - plt.plot(inc_found[0], inc_found[0], color='black', ls="--") - - if abstract_only: - col = "red" - inc_found_final = analysis.inclusions_found( - result_format=result_format, final_labels=True) - _add_WSS(90, analysis, ax, col, result_format, box_dist, - add_value=wss_value, - final_labels=True) - _add_WSS(100, analysis, ax, col, result_format, box_dist, - add_value=wss_value, - final_labels=True) - _, WSS95_x, _ = analysis.wss(95, x_format=result_format, - final_labels=True) - _, WSS100_x, _ = analysis.wss(100, x_format=result_format, - final_labels=True) - bbox = dict(boxstyle='round', facecolor=col, alpha=0.5) - prev_value = 0 - x_vals = [] - y_vals = [] - WSS_added = False - for i in range(len(inc_found_final[0])): - if inc_found_final[1][i] != prev_value: - x_vals.append(inc_found_final[0][i]) - y_vals.append(inc_found[1][i]) - prev_value = inc_found_final[1][i] - if inc_found_final[0][i] >= WSS100_x[0] - 1e-4 and not WSS_added: - ax.text(WSS100_x[0]+300, inc_found[1][i], - "WSS@100%", color="white", bbox=bbox) - WSS_added = True - myplot = plt.scatter(x_vals, y_vals, color=col) - legend_name.append(f"{data_key} (final)") - legend_plt.append(myplot) - - if legend: - plt.legend(legend_plt, legend_name, loc="lower right") - - if result_format == "number": - ax2 = ax.twiny() - ax.set_xlim(0, max_len) - ax2.set_xlim(0, 100) - ax.set_xlabel("# Reviewed") - ax2.set_xlabel("% Reviewed") - ax.set_ylabel("# Inclusions found") - symb = "#" - elif result_format == "percentage": - symb = "%" - ax.set_xlabel("% Reviewed") - ax.set_ylabel("% Inclusions found") - else: - symb = "?" - - plt.grid() - fig.tight_layout() - plt.show() - - def plot_limits(self, prob_allow_miss=[0.1, 0.5, 2.0], - result_format="percentage"): - legend_plt = [] - legend_name = [] - linestyles = ['-', '--', '-.', ':'] - - for i, data_key in enumerate(self.analyses): - res = self.analyses[data_key].limits( - prob_allow_miss=prob_allow_miss, - result_format=result_format) - x_range = res["x_range"] - col = "C"+str(i % 10) - - for i_limit, limit in enumerate(res["limits"]): - ls = linestyles[i_limit % len(linestyles)] - my_plot, = plt.plot(x_range, np.array(limit)+np.array(x_range), - color=col, ls=ls) - if i_limit == 0: - legend_plt.append(my_plot) - legend_name.append(f"{data_key}") - - plt.plot(x_range, x_range, color="black", ls='--') - if result_format == "percentage": - plt.xlabel("% of papers read") - plt.ylabel("Estimate of % of papers that need to be read") - else: - plt.xlabel("# of papers read") - plt.ylabel("Estimate of # of papers that need to be read") - plt.legend(legend_plt, legend_name, loc="upper right") - plt.title("Articles left to read") - plt.grid() - plt.show() diff --git a/asreviewcontrib/visualization/plot_base.py b/asreviewcontrib/visualization/plot_base.py new file mode 100644 index 0000000..6853665 --- /dev/null +++ b/asreviewcontrib/visualization/plot_base.py @@ -0,0 +1,32 @@ +import matplotlib.pyplot as plt + + +class PlotBase(): + def __init__(self, analyses): + """ + Plot the number of queries that turned out to be included + in the final review. + """ + super(PlotBase, self).__init__() + self.legend_name = [] + self.legend_plt = [] + self.fig, self.ax = plt.subplots() + self.analyses = analyses + + def set_legend(self, loc="lower right"): + self.ax.legend(self.legend_plt, self.legend_name, loc=loc) + + def set_grid(self): + self.ax.grid() + + def set_xlim(self, x_start, x_end): + self.ax.set_xlim(x_start, x_end) + + def set_ylim(self, y_start, y_end): + self.ax.set_ylim(y_start, y_end) + + def show(self): + plt.show() + + def save(self, fp, *args, **kwargs): + self.fig.savefig(fp, *args, **kwargs) diff --git a/asreviewcontrib/visualization/plot_discovery.py b/asreviewcontrib/visualization/plot_discovery.py new file mode 100644 index 0000000..0dd3b83 --- /dev/null +++ b/asreviewcontrib/visualization/plot_discovery.py @@ -0,0 +1,27 @@ +from asreviewcontrib.visualization.plot_base import PlotBase + + +class PlotDiscovery(PlotBase): + def __init__(self, analyses, result_format="percentage"): + super(PlotDiscovery, self).__init__(analyses) + self.result_format = result_format + + avg_times = [] + for analysis in self.analyses.values(): + results = analysis.avg_time_to_discovery( + result_format=result_format) + avg_times.append(list(results.values())) + + if result_format == "number": + self.ax.hist(avg_times, 30, histtype='bar', density=False, + label=self.analyses.keys()) + self.ax.set_xlabel("# Reviewed") + self.ax.set_ylabel("# of papers included") + else: + self.ax.hist(avg_times, 30, histtype='bar', density=True, + label=self.analyses.keys()) + self.ax.set_xlabel("% Reviewed") + self.ax.set_ylabel("Fraction of papers included") + + def set_legend(self, loc="upper right"): + self.ax.legend(loc=loc) diff --git a/asreviewcontrib/visualization/plot_inclusions.py b/asreviewcontrib/visualization/plot_inclusions.py new file mode 100644 index 0000000..be50e47 --- /dev/null +++ b/asreviewcontrib/visualization/plot_inclusions.py @@ -0,0 +1,125 @@ +import numpy as np + +from asreviewcontrib.visualization.plot_base import PlotBase + + +class PlotInclusions(PlotBase): + def __init__(self, analyses, result_format="percentage", thick=None): + """ + Plot the number of queries that turned out to be included + in the final review. + """ + super(PlotInclusions, self).__init__(analyses) + self.result_format = result_format + self.col = {} + + if thick is None: + thick = {key: True for key in list(self.analyses)} + self.thick = thick + + if result_format == "percentage": + self.box_dist = 0.5 + else: + self.box_dist = 100 + + max_len = 0 + for i, data_key in enumerate(reversed(self.analyses)): + analysis = self.analyses[data_key] + + inc_found = analysis.inclusions_found(result_format=result_format) + n_initial = analysis.inc_found[False]["n_initial"] + n_after_init = len(analysis.labels) - n_initial + max_len = max(max_len, n_after_init) + + self.col[data_key] = "C"+str((len(self.analyses)-1-i) % 10) + col = self.col[data_key] + + if self.thick[data_key]: + lw = 2 + else: + lw = 0.7 + + myplot = self.ax.errorbar(*inc_found, color=col, lw=lw) + if self.thick[data_key]: + self.legend_name.append(f"{data_key}") + self.legend_plt.append(myplot) + + if result_format == "number": + self.ax2 = self.ax.twiny() + self.ax.set_xlim(0, max_len) + self.ax2.set_xlim(0, 100) + self.ax.set_xlabel("# Reviewed") + self.ax2.set_xlabel("% Reviewed") + self.ax.set_ylabel("# Inclusions found") + elif result_format == "percentage": + self.ax.set_xlabel("% Reviewed") + self.ax.set_ylabel("% Inclusions found") + self.fig.tight_layout() + + def add_WSS(self, data_key, value=95, text_at=None, add_value=False, + alpha=0.8, text_col="white", **kwargs): + analysis = self.analyses[data_key] + col = self.col[data_key] + + if value is None: + return + + text = f"WSS@{value}%" + WSS_val, WSS_x, WSS_y = analysis.wss( + value, x_format=self.result_format, **kwargs) + if WSS_x is None or WSS_y is None: + return + + if add_value: + text += r"$\approx" + f" {round(WSS_val, 2)}" + r"\%$" + + if text_at is None: + text_at = (WSS_x[0] + self.box_dist, (WSS_y[0] + WSS_y[1])/2) + + self.ax.plot(WSS_x, WSS_y, color=col, ls="--") + self.ax.plot(WSS_x, (0, WSS_y[0]), color=col, ls=":") + bbox = dict(boxstyle='round', facecolor=col, alpha=alpha) + self.ax.text(*text_at, text, color=text_col, bbox=bbox) + + def add_RRF(self, data_key, value=10, text_at=None, add_value=False, + alpha=0.8, text_col="white", **kwargs): + analysis = self.analyses[data_key] + col = self.col[data_key] + if value is None: + return + + RRF_val, RRF_x, RRF_y = analysis.rrf( + value, x_format=self.result_format, **kwargs) + if RRF_x is None or RRF_y is None: + return + + text = f"RRF@{value}%" + if add_value: + text += r"$\approx" + f" {round(RRF_val, 2)}" + r"\%$" + + RRF_x = 0, RRF_x[0] + RRF_y = RRF_y[1], RRF_y[1] + if text_at is None: + text_at = (RRF_x[0] + self.box_dist, RRF_y[0] + self.box_dist + 2) + + self.ax.plot(RRF_x, RRF_y, color=col, ls="--") + bbox = dict(boxstyle='round', facecolor=col, alpha=alpha) + self.ax.text(*text_at, text, color=text_col, bbox=bbox) + + def add_random(self, text_at=None, col='black'): + xlim = self.ax.get_xlim() + ylim = self.ax.get_ylim() + if text_at is None: + xlim = self.ax.get_xlim() + ylim = self.ax.get_ylim() + text_at = ( + np.average(xlim) - 0.07 * (xlim[1]-xlim[0]), + np.average(xlim) + 0.07 * (ylim[1]-ylim[0]), + ) + + bbox = dict(boxstyle='round', facecolor='0.65') + self.ax.text(*text_at, "random", color=col, bbox=bbox) + xlim = [max(x, 0) for x in xlim] + if self.result_format == "percentage": + xlim = [min(x, 100) for x in xlim] + self.ax.plot(xlim, xlim, color='black', ls="--") diff --git a/asreviewcontrib/visualization/plot_limit.py b/asreviewcontrib/visualization/plot_limit.py new file mode 100644 index 0000000..5a4f3d8 --- /dev/null +++ b/asreviewcontrib/visualization/plot_limit.py @@ -0,0 +1,39 @@ +import numpy as np + +from asreviewcontrib.visualization.plot_base import PlotBase + + +class PlotLimit(PlotBase): + def __init__(self, analyses, prob_allow_miss=[0.1, 0.5, 2.0], + result_format="percentage"): + super(PlotLimit, self).__init__(analyses) + + self.legend_plt = [] + self.legend_name = [] + linestyles = ['-', '--', '-.', ':'] + self.result_format = result_format + + for i, data_key in enumerate(self.analyses): + res = self.analyses[data_key].limits( + prob_allow_miss=prob_allow_miss, + result_format=result_format) + x_range = res["x_range"] + col = "C"+str(i % 10) + + for i_limit, limit in enumerate(res["limits"]): + ls = linestyles[i_limit % len(linestyles)] + my_plot, = self.ax.plot( + x_range, np.array(limit)+np.array(x_range), + color=col, ls=ls) + if i_limit == 0: + self.legend_plt.append(my_plot) + self.legend_name.append(f"{data_key}") + + self.ax.plot(x_range, x_range, color="black", ls='--') + if result_format == "percentage": + self.ax.set_xlabel("% of papers read") + self.ax.set_ylabel("Estimate of % of papers that need to be read") + else: + self.ax.set_xlabel("# of papers read") + self.ax.set_ylabel("Estimate of # of papers that need to be read") + self.ax.set_title("Articles left to read") diff --git a/asreviewcontrib/visualization/plot_progression.py b/asreviewcontrib/visualization/plot_progression.py new file mode 100644 index 0000000..905a8db --- /dev/null +++ b/asreviewcontrib/visualization/plot_progression.py @@ -0,0 +1,60 @@ +import numpy as np + +from asreviewcontrib.visualization.plot_base import PlotBase + + +def gaussian_window(rel_ids, sigma): + factors = np.exp(-rel_ids**2/sigma**2) + return factors/np.sum(factors) + + +class PlotProgression(PlotBase): + def __init__(self, analyses, result_format="percentage", thick=None, + sigma=25, window=40): + super(PlotProgression, self).__init__(analyses) + self.col = {} + self.result_format = result_format + + if thick is None: + thick = {key: True for key in list(self.analyses)} + self.thick = thick + + for i, data_key in enumerate(reversed(self.analyses)): + analysis = self.analyses[data_key] + inc_found = analysis.inclusions_found(result_format="number") + + dy_inc = (inc_found[1] - np.append([0], inc_found[1][:-1])) + + self.col[data_key] = "C"+str((len(self.analyses)-1-i) % 10) + col = self.col[data_key] + + if self.thick[data_key]: + lw = 2 + else: + lw = 0.7 + + smooth_inc_perc = [] + for i in range(len(dy_inc)): + idx = np.arange(max(0, i-window), min(len(dy_inc), i+window+1)) + factor = gaussian_window(idx - i, sigma) + smooth_inc_perc.append(np.sum(dy_inc[idx]*factor)) + + if self.result_format == "percentage": + x_values = 100*inc_found[0]/len(analysis.labels) + else: + x_values = inc_found[0] + myplot, = self.ax.plot(x_values, 100*np.array(smooth_inc_perc), + color=col, lw=lw) + if self.thick[data_key]: + self.legend_plt.append(myplot) + self.legend_name.append(data_key) + + if self.result_format == "percentage": + self.ax.set_xlabel("% papers reviewed") + else: + self.ax.set_xlabel("# papers reviewed") + + self.ax.set_ylabel("% of proposed papers accepted") + + def set_legend(self, loc="upper right"): + super(PlotProgression, self).set_legend(loc=loc) diff --git a/asreviewcontrib/visualization/quick.py b/asreviewcontrib/visualization/quick.py new file mode 100644 index 0000000..34810f7 --- /dev/null +++ b/asreviewcontrib/visualization/quick.py @@ -0,0 +1,46 @@ +def inclusion_plot(plot, output=None, **kwargs): + all_files = all(plot.is_file.values()) + + inc_plot = plot.new("inclusion", **kwargs) + inc_plot.set_grid() + + for key in list(plot.analyses): + if all_files or not plot.is_file[key]: + inc_plot.add_WSS(key, 95) + inc_plot.add_WSS(key, 100) + inc_plot.add_RRF(key, 5) + inc_plot.add_random() + inc_plot.set_legend() + if output is None: + inc_plot.show() + else: + inc_plot.save(output) + + +def progression_plot(plot, output=None, **kwargs): + prog_plot = plot.new("progression", **kwargs) + prog_plot.set_grid() + prog_plot.set_legend() + if output is None: + prog_plot.show() + else: + prog_plot.save(output) + + +def discovery_plot(plot, output=None, **kwargs): + disc_plot = plot.new("discovery", **kwargs) + disc_plot.set_legend() + if output is None: + disc_plot.show() + else: + disc_plot.save(output) + + +def limit_plot(plot, output=None, **kwargs): + limit_plot = plot.new("limit", **kwargs) + limit_plot.set_legend() + limit_plot.set_grid() + if output is None: + limit_plot.show() + else: + limit_plot.save(output) diff --git a/docs/progression.png b/docs/progression.png new file mode 100644 index 0000000000000000000000000000000000000000..d1740943b93c43a171c26b52f967093718fd1407 GIT binary patch literal 79267 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

HKh+UO^8f03Ov1^#au>*K(h=`# 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 literal 0 HcmV?d00001 diff --git a/tests/data/embase_labelled.csv b/tests/data/embase_labelled.csv new file mode 100644 index 0000000..0d7565d --- /dev/null +++ b/tests/data/embase_labelled.csv @@ -0,0 +1,7 @@ +Title,Original Title,Author Names,Correspondence Address,Editors,Source,Abstract,Original Abstract,Emtree Drug Index Terms (Major Focus),Emtree Drug Index Terms,Emtree Medical Index Terms (Major Focus),Emtree Medical Index Terms,keywords,Drug Tradenames,Drug Manufacturer,Device Tradenames,Device Manufacturer,Clinical Trial Numbers,Open URL Link,Copyright,final_included +"Duplication cysts: Diagnosis, management, and the role of endoscopic ultrasound",,"Liu R., Adler D.G.","D.G. Adler, Department of Gastroenterology and Hepatology, University of Utah School of Medicine, Huntsman Cancer Center, Salt Lake City, UT, United States. Email: douglas.adler@hsc.utah.edu",,Endoscopic Ultrasound (2014) 3:3 (152-160). Date of Publication: 2014,"Gastrointestinal tract duplication cysts are rare congenital gastrointestinal malformation in young patients and adults. They consist of foregut duplication cysts, small bowel duplication cysts, and large bowel duplication cysts. Endoscopic ultrasound (EUS) has been widely used as a modality for the evaluation and diagnosis of duplication cysts. EUS is the diagnostic tool of choice to investigate duplication cysts since it can distinguish between solid and cystic lesions. The question of whether or not to perform EUS-fine needle aspiration (EUS-FNA) on a lesion suspected of being a duplication cyst is controversial as these lesions can become infected with significant consequences, although EUS-FNA is often required to obtain a definitive diagnosis and to rule out more ominous lesions. This manuscript will review the literature on duplication cysts throughout the body and will also focus on the role of EUS and FNA with regards to these lesions.",,,,"endoscopic ultrasonography, gastrointestinal malformation (diagnosis), gastrointestinal tract duplication cyst (diagnosis)","article, bronchogenic duplication cyst (diagnosis, surgery), colonic duplication cyst (diagnosis, surgery), colonoscopy, computer assisted tomography, cytology, duodenal duplication cyst (diagnosis, surgery), endoscopic ultrasound guided fine needle biopsy, enucleation, esophageal duplication cyst (diagnosis, surgery), gastric duplication cyst (diagnosis), gastrointestinal mucosa, hemicolectomy, histology, human, ilial duplication cyst (diagnosis), intestine surgery, jejunal duplication cyst (diagnosis, surgery), symptomatology","article, bronchogenic duplication cyst (diagnosis, surgery), colonic duplication cyst (diagnosis, surgery), colonoscopy, computer assisted tomography, cytology, duodenal duplication cyst (diagnosis, surgery), endoscopic ultrasound guided fine needle biopsy, enucleation, esophageal duplication cyst (diagnosis, surgery), gastric duplication cyst (diagnosis), gastrointestinal mucosa, hemicolectomy, histology, human, ilial duplication cyst (diagnosis), intestine surgery, jejunal duplication cyst (diagnosis, surgery), symptomatology",,,,,,http://sfx.library.uu.nl/utrecht?sid=EMBASE&issn=22267190&id=doi:10.4103%2F2303-9027.138783&atitle=Duplication+cysts%3A+Diagnosis%2C+management%2C+and+the+role+of+endoscopic+ultrasound&stitle=Endoscopic+Ultrasound&title=Endoscopic+Ultrasound&volume=3&issue=3&spage=152&epage=160&aulast=Liu&aufirst=Roy&auinit=R.&aufull=Liu+R.&coden=&isbn=&pages=152-160&date=2014&auinit1=R&auinitm=,"Copyright 2014 Elsevier B.V., All rights reserved.",0 +Endoscopic ultrasound-guided fine-needle aspiration in the diagnosis of foregut duplication cysts: The value of demonstrating detached ciliary tufts in cyst fluid,,"Eloubeidi M.A., Cohn M., Cerfolio R.J., Chhieng D.C., Jhala N., Jhala D., Eltoum I.A.","M.A. Eloubeidi, Endoscopic Ultrasound Program, Div. of Gastroenterol. and Hepatol., University of Alabama-Birmingham, 1530 3rd Avenue South-ZRB 633, Birmingham, AL 35294-0007, United States. Email: meloubeidi@uabmc.edu",,Cancer (2004) 102:4 (253-258). Date of Publication: 25 Aug 2004,,,,,"cyst fluid, endoscopic ultrasonography, fine needle aspiration biopsy, foregut duplication cyst (diagnosis), mediastinum cyst (diagnosis)","adult, aged, article, cancer cell, cancer cytodiagnosis, ciliary tuft, computer assisted tomography, conservative treatment, diagnostic accuracy, diagnostic value, electron microscopy, evaluation study, female, histopathology, human, major clinical study, male, mediastinum mass, priority journal, risk assessment, solid malignant neoplasm, symptomatology","adult, aged, article, cancer cell, cancer cytodiagnosis, ciliary tuft, computer assisted tomography, conservative treatment, diagnostic accuracy, diagnostic value, electron microscopy, evaluation study, female, histopathology, human, major clinical study, male, mediastinum mass, priority journal, risk assessment, solid malignant neoplasm, symptomatology",,,,,,http://sfx.library.uu.nl/utrecht?sid=EMBASE&issn=0008543X&id=doi:10.1002%2Fcncr.20369&atitle=Endoscopic+ultrasound-guided+fine-needle+aspiration+in+the+diagnosis+of+foregut+duplication+cysts%3A+The+value+of+demonstrating+detached+ciliary+tufts+in+cyst+fluid&stitle=Cancer&title=Cancer&volume=102&issue=4&spage=253&epage=258&aulast=Eloubeidi&aufirst=Mohamad+A.&auinit=M.A.&aufull=Eloubeidi+M.A.&coden=CANCA&isbn=&pages=253-258&date=2004&auinit1=M&auinitm=A,"Copyright 2009 Elsevier B.V., All rights reserved.",1 +A case of completely isolated advanced enteric duplication cyst cancer performed partial pancreatectomy,,"Nakashima S., Yamada T., Sato G., Sakai T., Chinen Y., Itakura H., Kato R., Ueda M., Tsuda Y., Ohta K., Matsuyama J., Ikenaga M.","T. Yamada, Department of Gastroenterological Surgery, Higashiosaka City Medical Center, Nishiiwata 3-4-5, Higashiosaka, Japan. Email: yamada-t@higashiosaka-hosp.jp",,International Journal of Surgery Case Reports (2019) 54 (83-86). Date of Publication: 1 Jan 2019,"Introduction: Enteric duplication cysts are rare and, in addition, isolated enteric duplication cysts are lower morbidity prevalence rate. These cysts lack a connection to the gastrointestinal tract or the adjacent mesenteric vasculature and have only been reported in 10 case reports. In these reports, only two reports were cases with malignant transformation. Our case was a report for the advanced cancer of the isolated enteric duplication cyst. Case presentation: The patient was a 43 year-old woman with slightly abdominal pain and mass formation. The abdominal contrast-enhanced computed tomography showed 130 × 100 × 90 mm huge cystic mass existed in right upper peritoneal cavity. The cystic mass had thickened wall and many enhanced nodules. As these imaging findings suggested a tumor originated from pancreas and the preoperative diagnose was suspect of mucinous cystic neoplasm. In operative findings, the tumor originated from pancreatic head and did not attach to gastrointestinal tract. Final pathology indicated the cyst was an isolated advanced enteric duplication cyst cancer and not originated from pancreas. Conclusion: We experienced an extremely rare case of completely isolated advanced enteric duplication cyst cancer. Unique to this case, the preoperative diagnosis was suspect of mucinous cystic neoplasm arising from pancreas head and partial pancreatectomy was performed. However, in the pathological findings, this cyst diagnosed advanced enteric duplication cyst cancer.",,,"cytokeratin 20 (endogenous compound), cytokeratin 7 (endogenous compound), gadolinium, gimeracil plus oteracil potassium plus tegafur (drug therapy), transcription factor Cdx2 (endogenous compound)","advanced cancer (diagnosis, drug therapy, surgery), cyst (diagnosis, drug therapy, surgery), intestine duplication (diagnosis, surgery)","abdominal pain, abdominal tenderness, abdominal tumor, adjuvant chemotherapy, adult, article, cancer diagnosis, cancer surgery, case report, clinical article, clinical examination, computer assisted tomography, consensus, contrast enhancement, differential diagnosis, diffusion weighted imaging, enteric duplication cyst (drug therapy), female, human, human tissue, immunohistochemistry, lymph node dissection, lymphatic system, malignant transformation, mucinous cystic neoplasm (diagnosis), nervous system, pancreatectomy, pancreaticoduodenectomy, pathology, peritoneal cavity, physical examination, practice guideline, priority journal, tumor invasion, upper abdominal pain, venous circulation","abdominal pain, abdominal tenderness, abdominal tumor, adjuvant chemotherapy, adult, article, cancer diagnosis, cancer surgery, case report, clinical article, clinical examination, computer assisted tomography, consensus, contrast enhancement, differential diagnosis, diffusion weighted imaging, enteric duplication cyst (drug therapy), female, human, human tissue, immunohistochemistry, lymph node dissection, lymphatic system, malignant transformation, mucinous cystic neoplasm (diagnosis), nervous system, pancreatectomy, pancreaticoduodenectomy, pathology, peritoneal cavity, physical examination, practice guideline, priority journal, tumor invasion, upper abdominal pain, venous circulation",s 1,,,,,http://sfx.library.uu.nl/utrecht?sid=EMBASE&issn=22102612&id=doi:10.1016%2Fj.ijscr.2018.11.060&atitle=A+case+of+completely+isolated+advanced+enteric+duplication+cyst+cancer+performed+partial+pancreatectomy&stitle=Int.+J.+Surg.+Case+Rep.&title=International+Journal+of+Surgery+Case+Reports&volume=54&issue=&spage=83&epage=86&aulast=Nakashima&aufirst=Shinsuke&auinit=S.&aufull=Nakashima+S.&coden=&isbn=&pages=83-86&date=2019&auinit1=S&auinitm=,"Copyright 2018 Elsevier B.V., All rights reserved.",0 +,,"Quintanilla-Dieck L., Penn E.B.","L. Quintanilla-Dieck, Department of Otolaryngology Head and Neck Surgery, Oregon Health & Science University, 3181 Southwest Sam Jackson Park Road, PV-01, Portland, United States. Email: quintani@ohsu.edu",,Clinics in Perinatology (2018) 45:4 (769-785). Date of Publication: 1 Dec 2018,"Congenital neck masses can be a developmental anomaly of cystic, solid, or vascular origin. They can also constitute neoplasms, including malignancies, although this is rare in the pediatric population. The history and examination can help quickly narrow the differential diagnosis. Imaging also plays an essential role in defining the characteristics and likely cause of neck masses. The most common neck masses in young children are thyroglossal duct cysts, branchial cleft anomalies, and dermoid cysts. Also important to consider in the differential diagnosis are solid tumors, such as teratomas, or vascular lesions, such as hemangiomas.",,,"antineoplastic agent (drug therapy), bleomycin (drug therapy), doxycycline (drug therapy), propranolol (drug therapy), tetradecyl sulfate sodium (drug therapy), thyroid hormone (drug therapy)","congenital tumor (congenital disorder), neck tumor (congenital disorder)","ablation therapy, branchial defect (congenital disorder, surgery), cancer chemotherapy, cancer radiotherapy, cancer surgery, capillary hemangioma (congenital disorder, diagnosis, drug therapy, surgery), carbon dioxide laser, congenital blood vessel malformation (congenital disorder, surgery), cyst (congenital disorder, surgery), differential diagnosis, ectopic thyroid gland (congenital disorder, drug therapy, surgery, therapy), epidermoid cyst (congenital disorder, surgery), foregut duplication cyst (congenital disorder, surgery), head and neck tumor (congenital disorder, diagnosis), hormone substitution, human, lung cyst (congenital disorder), lymphatic malformation (congenital disorder, diagnosis, drug therapy, surgery, therapy), prenatal diagnosis, priority journal, respiration control, review, rhabdomyosarcoma (congenital disorder, drug therapy, radiotherapy, surgery), sclerotherapy, Sistrunk procedure, surgical approach, surgical technique, teratoma (congenital disorder, surgery), thymus cyst (congenital disorder, surgery), thyroglossal duct cyst (congenital disorder, surgery), thyroidectomy","ablation therapy, branchial defect (congenital disorder, surgery), cancer chemotherapy, cancer radiotherapy, cancer surgery, capillary hemangioma (congenital disorder, diagnosis, drug therapy, surgery), carbon dioxide laser, congenital blood vessel malformation (congenital disorder, surgery), cyst (congenital disorder, surgery), differential diagnosis, ectopic thyroid gland (congenital disorder, drug therapy, surgery, therapy), epidermoid cyst (congenital disorder, surgery), foregut duplication cyst (congenital disorder, surgery), head and neck tumor (congenital disorder, diagnosis), hormone substitution, human, lung cyst (congenital disorder), lymphatic malformation (congenital disorder, diagnosis, drug therapy, surgery, therapy), prenatal diagnosis, priority journal, respiration control, review, rhabdomyosarcoma (congenital disorder, drug therapy, radiotherapy, surgery), sclerotherapy, Sistrunk procedure, surgical approach, surgical technique, teratoma (congenital disorder, surgery), thymus cyst (congenital disorder, surgery), thyroglossal duct cyst (congenital disorder, surgery), thyroidectomy",,,,,,http://sfx.library.uu.nl/utrecht?sid=EMBASE&issn=15579840&id=doi:10.1016%2Fj.clp.2018.07.012&atitle=Congenital+Neck+Masses&stitle=Clin.+Perinatol.&title=Clinics+in+Perinatology&volume=45&issue=4&spage=769&epage=785&aulast=Quintanilla-Dieck&aufirst=Lourdes&auinit=L.&aufull=Quintanilla-Dieck+L.&coden=CLPED&isbn=&pages=769-785&date=2018&auinit1=L&auinitm=,"Copyright 2018 Elsevier B.V., All rights reserved.",1 +Foregut duplication cysts: A report of two cases with emphasis on embryogenesis,,"Khoury T., Rivera L.","T. Khoury, Department of Pathology, Roswell Park Cancer Institute, Elm and Carlton Streets, Buffalo, NY 14263, United States. Email: thaer.khoury@roswellpark.org",,"World Journal of Gastroenterology (2011) 17:1 (130-134). Date of Publication: January 7, 2011","Duplication cyst of the stomach with a pseudostratified columnar ciliated epithelium is extremely rare. We describe two cases of these cysts, with emphasis on their immunophenotype and embryogenesis. The first patient was a 29-year-old man who presented with cramping abdominal pain in his left lower quadrant. The second patient was a 26-year-old woman who had a history, over several years, of chronic epigastric abdominal pain radiating to her back. Both lesions were surgically removed. They showed the same histomorphology. The cysts were lined by a pseudostratified respiratory epithelium with ciliated cells. The first cyst was connected to the stomach, while the second cyst was not connected. Both cysts expressed thyroid transcription factor-1 (TTF-1) and surfactant. In this report, we explore the possible embryogenesis of these lesions in the light of TTF-1 and surfactant expression. © 2011 Baishideng. All rights reserved.",,,"homeobox protein Nkx 2.1 (endogenous compound), surfactant (endogenous compound)","cyst (diagnosis, surgery), gastric duplication cyst (diagnosis, surgery), gastrointestinal duplication cyst (diagnosis, surgery)","abdominal pain, adult, article, case report, computer assisted tomography, embryo development, endoscopic ultrasonography, epigastric pain, female, human, immunophenotyping, male, partial gastrectomy, protein expression","abdominal pain, adult, article, case report, computer assisted tomography, embryo development, endoscopic ultrasonography, epigastric pain, female, human, immunophenotyping, male, partial gastrectomy, protein expression",,,,,,http://sfx.library.uu.nl/utrecht?sid=EMBASE&issn=10079327&id=doi:10.3748%2Fwjg.v17.i1.130&atitle=Foregut+duplication+cysts%3A+A+report+of+two+cases+with+emphasis+on+embryogenesis&stitle=World+J.+Gastroenterol.&title=World+Journal+of+Gastroenterology&volume=17&issue=1&spage=130&epage=134&aulast=Khoury&aufirst=Thaer&auinit=T.&aufull=Khoury+T.&coden=WJGAF&isbn=&pages=130-134&date=2011&auinit1=T&auinitm=,"Copyright 2011 Elsevier B.V., All rights reserved.",0 +Completely Isolated Retroperitoneal Enteric Duplication Cyst with Adenocarcinoma Transformation Managed with Robotic Radical Nephrectomy,,"Faraj K., Edwards L., Gupta A., Seifman B.","B. Seifman, Michigan Institute of Urology, 1701 E South Boulevard, Rochester Hills, United States. Email: seifmanb@michiganurology.com",,Journal of Endourology Case Reports (2017) 3:1 (31-33). Date of Publication: 1 Mar 2017,"Background: Enteric duplication cysts are congenital malformations that typically affect children in infancy, but can also affect adults. Rarely, these cysts can be complicated by malignancy. We present the first case of retroperitoneal duplication cyst that was complicated by malignancy transformation and managed by robot-assisted excision. Case presentation: A 64-year-old female with a history of a left-sided renal cyst presented with a 4-month history of abdominal pain and fatigue. MRI revealed a bilobed cyst, with components measuring 6.9 × 6.6 and 6.1 × 6.9 cm, which had grown since previous imaging, and hemorrhage in some portions of the cysts, as well as cystic wall enhancement, suggesting a possible malignancy. The patient consented to a robot-assisted partial (possible radical) nephrectomy. During the procedure, the cystic structure appeared to have grown since imaging, was intimately associated with the hilum, and had a complex vasculature, which prompted us to perform a radical nephrectomy. Grossly, the specimen consisted of a 14.8 cm cystic structure at the superior portion of the kidney, but was not contained within the renal parenchyma. Histologically, the internal mucosa of the cyst showed columnar epithelium with high-grade dysplasia and carcinoma in situ with focal individual cell infiltration into the superficial portion of the inferior part of the cyst. The patient saw a medical oncologist and was instructed to follow up with quarterly imaging to assess for disease progression. Conclusion: Enteric duplication cysts are uncommon entities that can occur in various locations in the body, causing a wide spectrum of symptoms, and are rarely complicated by malignancy transformation. Robot-assisted surgical resection is an option that we have shown to be effective in managing these patients.",,,"2 methylacyl coenzyme A racemase (endogenous compound), cytokeratin 20 (endogenous compound), cytokeratin 7 (endogenous compound), kidney injury molecule 1 (endogenous compound), protein p53 (endogenous compound), transcription factor Cdx2 (endogenous compound), transcription factor PAX8 (endogenous compound)","colloid carcinoma (diagnosis), cyst (congenital disorder, diagnosis, surgery), enteric duplication cyst (congenital disorder, diagnosis, surgery), malignant transformation, radical nephrectomy, robotic surgical procedure","abdominal pain, adult, artery ligation, article, bleeding (diagnosis), cancer risk, case report, cell infiltration, clinical article, columnar epithelium, computer assisted tomography, cyst hemorrhage (diagnosis), disease exacerbation, fatigue, female, follow up, general anesthesia, histopathology, human, human tissue, middle aged, nuclear magnetic resonance imaging, priority journal, renal artery ligation, vascularization","abdominal pain, adult, artery ligation, article, bleeding (diagnosis), cancer risk, case report, cell infiltration, clinical article, columnar epithelium, computer assisted tomography, cyst hemorrhage (diagnosis), disease exacerbation, fatigue, female, follow up, general anesthesia, histopathology, human, human tissue, middle aged, nuclear magnetic resonance imaging, priority journal, renal artery ligation, vascularization",,,,,,http://sfx.library.uu.nl/utrecht?sid=EMBASE&issn=23799889&id=doi:10.1089%2Fcren.2017.0016&atitle=Completely+Isolated+Retroperitoneal+Enteric+Duplication+Cyst+with+Adenocarcinoma+Transformation+Managed+with+Robotic+Radical+Nephrectomy&stitle=J.+Endourol.+Case+Rep.&title=Journal+of+Endourology+Case+Reports&volume=3&issue=1&spage=31&epage=33&aulast=Faraj&aufirst=Kassem&auinit=K.&aufull=Faraj+K.&coden=&isbn=&pages=31-33&date=2017&auinit1=K&auinitm=,"Copyright 2018 Elsevier B.V., All rights reserved.",0 diff --git a/tests/test_plot.py b/tests/test_plot.py new file mode 100644 index 0000000..be1c26a --- /dev/null +++ b/tests/test_plot.py @@ -0,0 +1,85 @@ +import os +from pathlib import Path + +from pytest import mark + +from asreview import review_simulate + +from asreviewcontrib.visualization.entrypoint import PlotEntryPoint + + +def create_state_file(data_fp, state_fp): + try: + os.remove(state_fp) + except FileNotFoundError: + pass + + review_simulate(str(data_fp), state_file=state_fp, n_prior_included=1, + n_prior_excluded=1, model="nb", + feature_extraction="tfidf") + + +def plot_setup(data_fp, state_dirs, state_files): + for dir_ in state_dirs: + os.makedirs(dir_, exist_ok=True) + for file_ in state_files: + create_state_file(data_fp, file_) + + +def plot_clean(dirs, files): + for f in files: + try: + os.remove(f) + except FileNotFoundError: + pass + for d in dirs: + try: + os.rmdir(d) + except (FileNotFoundError, OSError): + pass + + +COMBINATIONS = [ + (pt, numbers) + for pt in ["inclusion", "progress", "discovery", "limit"] + for numbers in [True, False] +] + + +@mark.parametrize( + "plot_type,numbers", COMBINATIONS +) +def test_plots(request, plot_type, numbers): + test_dir = request.fspath.dirname + output_dir = Path(test_dir, "output") + state_dirs = [Path(output_dir, x) for x in ["h5", "json"]] + h5_dir = Path(output_dir, "h5") + json_dir = Path(output_dir, "json") + h5_files = [Path(output_dir, "h5", f"result_{x}.h5") for x in [1, 2]] + json_files = [Path(output_dir, "json", f"result_{x}.json") for x in [1, 2]] + data_fp = Path(test_dir, "data", "embase_labelled.csv") + picture_fp = Path(output_dir, "test.png") + + if (plot_type, numbers) == COMBINATIONS[0]: + plot_setup(data_fp, state_dirs+[output_dir], h5_files+json_files) + + data_combis = [ + [h5_dir, *h5_files], + [json_dir, *json_files], + [h5_dir, json_dir], + ] + for combi in data_combis: + try: + os.remove(picture_fp) + except FileNotFoundError: + pass + args = [*[str(x) for x in combi], "-o", str(picture_fp)] + if numbers: + args += ["--absolute-values"] + entry = PlotEntryPoint() + entry.execute(args) + assert os.path.isfile(picture_fp) + + if (plot_type, numbers) == COMBINATIONS[-1]: + plot_clean(state_dirs + [output_dir], + h5_files + json_files + [picture_fp])