diff --git a/Makefile b/Makefile index 3b72a15..961755c 100644 --- a/Makefile +++ b/Makefile @@ -21,14 +21,14 @@ dev: all pip install -e . clean: distclean - rm -rf q2_composition/_ancombc2_visualizer/node_modules + rm -rf q2_composition/_da_barplot/node_modules distclean: - rm -rf q2_composition/_ancombc2_visualizer/dist/ + rm -rf q2_composition/_da_barplot/dist/ -q2_composition/_ancombc2_visualizer/dist: - cd q2_composition/_ancombc2_visualizer/ && \ +q2_composition/_da_barplot/dist: + cd q2_composition/_da_barplot/ && \ npm install && \ npm run build -ancombc2-visualizer: q2_composition/_ancombc2_visualizer/dist +ancombc2-visualizer: q2_composition/_da_barplot/dist diff --git a/q2_composition/__init__.py b/q2_composition/__init__.py index d7653aa..c22a93a 100644 --- a/q2_composition/__init__.py +++ b/q2_composition/__init__.py @@ -15,9 +15,8 @@ from ._impute import add_pseudocount from ._ancom import ancom from ._ancombc import ancombc -from ._ancombc2 import ancombc2, ancombc2_visualizer +from ._ancombc2 import ancombc2, da_barplot from ._dataloaf_tabulate import tabulate -from ._diff_abundance_plots import da_barplot try: from ._version import __version__ @@ -27,6 +26,5 @@ __all__ = [ 'FrictionlessCSVFileFormat', 'DataPackageSchemaFileFormat', 'DataLoafPackageDirFmt', 'DifferentialAbundance', 'add_pseudocount', - 'ancom', 'ancombc', 'ancombc2', 'ancombc2_visualizer', 'tabulate', - 'da_barplot' + 'ancom', 'ancombc', 'ancombc2', 'tabulate', 'da_barplot' ] diff --git a/q2_composition/_ancombc2.py b/q2_composition/_ancombc2.py index f4f3fc8..7639297 100644 --- a/q2_composition/_ancombc2.py +++ b/q2_composition/_ancombc2.py @@ -51,7 +51,7 @@ def ancombc2( alpha: float = 0.05, diff_robust: bool = False, num_processes: int = 1, -) -> ANCOMBC2OutputDirFmt: +) -> ANCOMBC2SliceMapping: ''' Wraps the `ancombc2` R function from the ANCOMBC package. @@ -61,10 +61,8 @@ def ancombc2( Returns ------- - ANCOMBC2OutputDirFmt - A directory format containing the ANCOMBC2 model's per-feature - statistics and per-feature structural zero designations if - `structural_zeros` is set. + ANCOMBC2SliceMapping + A dictionary mapping the name of the slice to the slice's columns. ''' if structural_zeros and group is None: msg = ( @@ -111,6 +109,8 @@ def ancombc2( model_statistics_df = ro.conversion.get_conversion().rpy2py( model_statistics ) + model_statistics_df['taxon'] = \ + model_statistics_df['taxon'].astype('string') slices = _split_into_slices(model_statistics_df, diff_robust) @@ -130,7 +130,7 @@ def ancombc2( # split categorical variables from levels and annotate references slices = _process_categorical_variables(slices, metadata, table) - return transform(data=slices, to_type=ANCOMBC2OutputDirFmt) + return slices def _process_formula(formula: str, metadata: qiime2.Metadata) -> str: @@ -851,7 +851,7 @@ def _rename(column: str) -> str: return structural_zeros_df.rename(lambda c: _rename(c), axis='columns') -def ancombc2_visualizer( +def da_barplot( output_dir: str, data: ANCOMBC2OutputDirFmt, taxonomy: pd.DataFrame = None @@ -865,7 +865,7 @@ def ancombc2_visualizer( ---------- output_dir : str The path to the data/ directory in the to-be-created visualization. - data : ANCOMBC2SliceMapping + data : ANCOMBC2OutputDirFmt The ancombc2 slice data to visualize taxonomy : pd.DataFrame | None The taxonomy associated with the features present in `slices`. @@ -877,14 +877,17 @@ def ancombc2_visualizer( dist_dir = ( importlib.resources.files('q2_composition') / - '_ancombc2_visualizer' / 'dist' + '_da_barplot' / 'dist' ) shutil.copytree(Path(str(dist_dir)), output_dir, dirs_exist_ok=True) if taxonomy is not None: # subset taxonomy to only features present in the ancombc2 slices slice_map = transform(data=data, to_type=ANCOMBC2SliceMapping) - slice_feature_ids = slice_map['lfc']['taxon'].unique() + try: + slice_feature_ids = slice_map['lfc']['taxon'].unique() + except KeyError: + slice_feature_ids = slice_map['lfc']['id'].unique() taxonomy = taxonomy[taxonomy.index.isin(slice_feature_ids)] if len(taxonomy) == 0: diff --git a/q2_composition/_ancombc2_visualizer/.gitignore b/q2_composition/_da_barplot/.gitignore similarity index 100% rename from q2_composition/_ancombc2_visualizer/.gitignore rename to q2_composition/_da_barplot/.gitignore diff --git a/q2_composition/_ancombc2_visualizer/.vscode/extensions.json b/q2_composition/_da_barplot/.vscode/extensions.json similarity index 100% rename from q2_composition/_ancombc2_visualizer/.vscode/extensions.json rename to q2_composition/_da_barplot/.vscode/extensions.json diff --git a/q2_composition/_ancombc2_visualizer/index.html b/q2_composition/_da_barplot/index.html similarity index 100% rename from q2_composition/_ancombc2_visualizer/index.html rename to q2_composition/_da_barplot/index.html diff --git a/q2_composition/_ancombc2_visualizer/package-lock.json b/q2_composition/_da_barplot/package-lock.json similarity index 100% rename from q2_composition/_ancombc2_visualizer/package-lock.json rename to q2_composition/_da_barplot/package-lock.json diff --git a/q2_composition/_ancombc2_visualizer/package.json b/q2_composition/_da_barplot/package.json similarity index 100% rename from q2_composition/_ancombc2_visualizer/package.json rename to q2_composition/_da_barplot/package.json diff --git a/q2_composition/_ancombc2_visualizer/src/App.svelte b/q2_composition/_da_barplot/src/App.svelte similarity index 100% rename from q2_composition/_ancombc2_visualizer/src/App.svelte rename to q2_composition/_da_barplot/src/App.svelte diff --git a/q2_composition/_ancombc2_visualizer/src/app.css b/q2_composition/_da_barplot/src/app.css similarity index 100% rename from q2_composition/_ancombc2_visualizer/src/app.css rename to q2_composition/_da_barplot/src/app.css diff --git a/q2_composition/_ancombc2_visualizer/src/lib/Barplot.svelte b/q2_composition/_da_barplot/src/lib/Barplot.svelte similarity index 100% rename from q2_composition/_ancombc2_visualizer/src/lib/Barplot.svelte rename to q2_composition/_da_barplot/src/lib/Barplot.svelte diff --git a/q2_composition/_ancombc2_visualizer/src/lib/BarplotControls.svelte b/q2_composition/_da_barplot/src/lib/BarplotControls.svelte similarity index 100% rename from q2_composition/_ancombc2_visualizer/src/lib/BarplotControls.svelte rename to q2_composition/_da_barplot/src/lib/BarplotControls.svelte diff --git a/q2_composition/_ancombc2_visualizer/src/lib/BarplotFilters.svelte b/q2_composition/_da_barplot/src/lib/BarplotFilters.svelte similarity index 100% rename from q2_composition/_ancombc2_visualizer/src/lib/BarplotFilters.svelte rename to q2_composition/_da_barplot/src/lib/BarplotFilters.svelte diff --git a/q2_composition/_ancombc2_visualizer/src/lib/ControlContainer.svelte b/q2_composition/_da_barplot/src/lib/ControlContainer.svelte similarity index 100% rename from q2_composition/_ancombc2_visualizer/src/lib/ControlContainer.svelte rename to q2_composition/_da_barplot/src/lib/ControlContainer.svelte diff --git a/q2_composition/_ancombc2_visualizer/src/lib/SelectedTaxon.svelte b/q2_composition/_da_barplot/src/lib/SelectedTaxon.svelte similarity index 100% rename from q2_composition/_ancombc2_visualizer/src/lib/SelectedTaxon.svelte rename to q2_composition/_da_barplot/src/lib/SelectedTaxon.svelte diff --git a/q2_composition/_ancombc2_visualizer/src/lib/Taxonomy.svelte b/q2_composition/_da_barplot/src/lib/Taxonomy.svelte similarity index 100% rename from q2_composition/_ancombc2_visualizer/src/lib/Taxonomy.svelte rename to q2_composition/_da_barplot/src/lib/Taxonomy.svelte diff --git a/q2_composition/_ancombc2_visualizer/src/lib/TaxonomyFilter.svelte b/q2_composition/_da_barplot/src/lib/TaxonomyFilter.svelte similarity index 100% rename from q2_composition/_ancombc2_visualizer/src/lib/TaxonomyFilter.svelte rename to q2_composition/_da_barplot/src/lib/TaxonomyFilter.svelte diff --git a/q2_composition/_ancombc2_visualizer/src/lib/TaxonomyFilterList.svelte b/q2_composition/_da_barplot/src/lib/TaxonomyFilterList.svelte similarity index 100% rename from q2_composition/_ancombc2_visualizer/src/lib/TaxonomyFilterList.svelte rename to q2_composition/_da_barplot/src/lib/TaxonomyFilterList.svelte diff --git a/q2_composition/_ancombc2_visualizer/src/main.ts b/q2_composition/_da_barplot/src/main.ts similarity index 100% rename from q2_composition/_ancombc2_visualizer/src/main.ts rename to q2_composition/_da_barplot/src/main.ts diff --git a/q2_composition/_ancombc2_visualizer/src/util/features.svelte.ts b/q2_composition/_da_barplot/src/util/features.svelte.ts similarity index 100% rename from q2_composition/_ancombc2_visualizer/src/util/features.svelte.ts rename to q2_composition/_da_barplot/src/util/features.svelte.ts diff --git a/q2_composition/_ancombc2_visualizer/src/util/parse.ts b/q2_composition/_da_barplot/src/util/parse.ts similarity index 100% rename from q2_composition/_ancombc2_visualizer/src/util/parse.ts rename to q2_composition/_da_barplot/src/util/parse.ts diff --git a/q2_composition/_ancombc2_visualizer/src/util/plot.ts b/q2_composition/_da_barplot/src/util/plot.ts similarity index 100% rename from q2_composition/_ancombc2_visualizer/src/util/plot.ts rename to q2_composition/_da_barplot/src/util/plot.ts diff --git a/q2_composition/_ancombc2_visualizer/src/util/taxonomy.svelte.ts b/q2_composition/_da_barplot/src/util/taxonomy.svelte.ts similarity index 100% rename from q2_composition/_ancombc2_visualizer/src/util/taxonomy.svelte.ts rename to q2_composition/_da_barplot/src/util/taxonomy.svelte.ts diff --git a/q2_composition/_ancombc2_visualizer/src/vite-env.d.ts b/q2_composition/_da_barplot/src/vite-env.d.ts similarity index 100% rename from q2_composition/_ancombc2_visualizer/src/vite-env.d.ts rename to q2_composition/_da_barplot/src/vite-env.d.ts diff --git a/q2_composition/_ancombc2_visualizer/svelte.config.js b/q2_composition/_da_barplot/svelte.config.js similarity index 100% rename from q2_composition/_ancombc2_visualizer/svelte.config.js rename to q2_composition/_da_barplot/svelte.config.js diff --git a/q2_composition/_ancombc2_visualizer/tests/data/output-format/W.jsonl b/q2_composition/_da_barplot/tests/data/output-format/W.jsonl similarity index 100% rename from q2_composition/_ancombc2_visualizer/tests/data/output-format/W.jsonl rename to q2_composition/_da_barplot/tests/data/output-format/W.jsonl diff --git a/q2_composition/_ancombc2_visualizer/tests/data/output-format/diff.jsonl b/q2_composition/_da_barplot/tests/data/output-format/diff.jsonl similarity index 100% rename from q2_composition/_ancombc2_visualizer/tests/data/output-format/diff.jsonl rename to q2_composition/_da_barplot/tests/data/output-format/diff.jsonl diff --git a/q2_composition/_ancombc2_visualizer/tests/data/output-format/lfc.jsonl b/q2_composition/_da_barplot/tests/data/output-format/lfc.jsonl similarity index 100% rename from q2_composition/_ancombc2_visualizer/tests/data/output-format/lfc.jsonl rename to q2_composition/_da_barplot/tests/data/output-format/lfc.jsonl diff --git a/q2_composition/_ancombc2_visualizer/tests/data/output-format/p.jsonl b/q2_composition/_da_barplot/tests/data/output-format/p.jsonl similarity index 100% rename from q2_composition/_ancombc2_visualizer/tests/data/output-format/p.jsonl rename to q2_composition/_da_barplot/tests/data/output-format/p.jsonl diff --git a/q2_composition/_ancombc2_visualizer/tests/data/output-format/passed_ss.jsonl b/q2_composition/_da_barplot/tests/data/output-format/passed_ss.jsonl similarity index 100% rename from q2_composition/_ancombc2_visualizer/tests/data/output-format/passed_ss.jsonl rename to q2_composition/_da_barplot/tests/data/output-format/passed_ss.jsonl diff --git a/q2_composition/_ancombc2_visualizer/tests/data/output-format/q.jsonl b/q2_composition/_da_barplot/tests/data/output-format/q.jsonl similarity index 100% rename from q2_composition/_ancombc2_visualizer/tests/data/output-format/q.jsonl rename to q2_composition/_da_barplot/tests/data/output-format/q.jsonl diff --git a/q2_composition/_ancombc2_visualizer/tests/data/output-format/se.jsonl b/q2_composition/_da_barplot/tests/data/output-format/se.jsonl similarity index 100% rename from q2_composition/_ancombc2_visualizer/tests/data/output-format/se.jsonl rename to q2_composition/_da_barplot/tests/data/output-format/se.jsonl diff --git a/q2_composition/_ancombc2_visualizer/tests/data/output-format/structural-zeros.jsonl b/q2_composition/_da_barplot/tests/data/output-format/structural-zeros.jsonl similarity index 100% rename from q2_composition/_ancombc2_visualizer/tests/data/output-format/structural-zeros.jsonl rename to q2_composition/_da_barplot/tests/data/output-format/structural-zeros.jsonl diff --git a/q2_composition/_ancombc2_visualizer/tests/data/taxonomy-fake.tsv b/q2_composition/_da_barplot/tests/data/taxonomy-fake.tsv similarity index 100% rename from q2_composition/_ancombc2_visualizer/tests/data/taxonomy-fake.tsv rename to q2_composition/_da_barplot/tests/data/taxonomy-fake.tsv diff --git a/q2_composition/_ancombc2_visualizer/tests/data/taxonomy.tsv b/q2_composition/_da_barplot/tests/data/taxonomy.tsv similarity index 100% rename from q2_composition/_ancombc2_visualizer/tests/data/taxonomy.tsv rename to q2_composition/_da_barplot/tests/data/taxonomy.tsv diff --git a/q2_composition/_ancombc2_visualizer/tests/featureRecord.test.ts b/q2_composition/_da_barplot/tests/featureRecord.test.ts similarity index 100% rename from q2_composition/_ancombc2_visualizer/tests/featureRecord.test.ts rename to q2_composition/_da_barplot/tests/featureRecord.test.ts diff --git a/q2_composition/_ancombc2_visualizer/tests/parse.test.ts b/q2_composition/_da_barplot/tests/parse.test.ts similarity index 100% rename from q2_composition/_ancombc2_visualizer/tests/parse.test.ts rename to q2_composition/_da_barplot/tests/parse.test.ts diff --git a/q2_composition/_ancombc2_visualizer/tests/taxonomy.test.ts b/q2_composition/_da_barplot/tests/taxonomy.test.ts similarity index 100% rename from q2_composition/_ancombc2_visualizer/tests/taxonomy.test.ts rename to q2_composition/_da_barplot/tests/taxonomy.test.ts diff --git a/q2_composition/_ancombc2_visualizer/tsconfig.app.json b/q2_composition/_da_barplot/tsconfig.app.json similarity index 100% rename from q2_composition/_ancombc2_visualizer/tsconfig.app.json rename to q2_composition/_da_barplot/tsconfig.app.json diff --git a/q2_composition/_ancombc2_visualizer/tsconfig.json b/q2_composition/_da_barplot/tsconfig.json similarity index 100% rename from q2_composition/_ancombc2_visualizer/tsconfig.json rename to q2_composition/_da_barplot/tsconfig.json diff --git a/q2_composition/_ancombc2_visualizer/tsconfig.node.json b/q2_composition/_da_barplot/tsconfig.node.json similarity index 100% rename from q2_composition/_ancombc2_visualizer/tsconfig.node.json rename to q2_composition/_da_barplot/tsconfig.node.json diff --git a/q2_composition/_ancombc2_visualizer/vite.config.ts b/q2_composition/_da_barplot/vite.config.ts similarity index 100% rename from q2_composition/_ancombc2_visualizer/vite.config.ts rename to q2_composition/_da_barplot/vite.config.ts diff --git a/q2_composition/_diff_abundance_plots.py b/q2_composition/_diff_abundance_plots.py deleted file mode 100644 index 29baa99..0000000 --- a/q2_composition/_diff_abundance_plots.py +++ /dev/null @@ -1,242 +0,0 @@ -# ---------------------------------------------------------------------------- -# Copyright (c) 2016-2026, QIIME 2 development team. -# -# Distributed under the terms of the Modified BSD License. -# -# The full license is in the file LICENSE, distributed with this software. -# ---------------------------------------------------------------------------- -import importlib.resources -from pathlib import Path -import urllib.parse -from collections import Counter - -import altair as alt -import pandas as pd -import numpy as np - -import qiime2 -from q2_composition import DataLoafPackageDirFmt -import q2templates - - -def _plot_differentials( - output_dir, - df, - title, - feature_id_label, - effect_size_label, - significance_label, - error_label, - feature_ids, - effect_size_threshold, - significance_threshold, - level_delimiter, - label_limit): - - if len(df) == 0: - raise ValueError("No features present in input.") - - # ensure that all columns are of the types required internally - df = df.astype({feature_id_label: pd.StringDtype(), - error_label: pd.Float64Dtype(), - significance_label: pd.Float64Dtype(), - effect_size_label: pd.Float64Dtype()}) - - if feature_ids is not None: - df = df.query(f'`{feature_id_label}` in @feature_ids') - - df = df[df[significance_label] <= significance_threshold] - df = df[np.abs(df[effect_size_label]) >= effect_size_threshold] - - if len(df) == 0: - raise ValueError("No features remaining after applying filters.") - - output_dir = Path(output_dir) - output_dir.mkdir(parents=True, exist_ok=True) - safe_title = urllib.parse.quote(title) - fig_fn = Path(f'{safe_title}-ancombc-barplot.html') - fig_fp = output_dir / fig_fn - - if level_delimiter is not None: - # For readability, only the most specific named taxonomy will be - # included in the y-axis label. The full taxonomy text will be in - # the tool-tip. Because it's possible that the most specific - # taxonomic name are not unique, this code simply prepends a number - # to names. Providing separate labels for ticks, which - # would avoid this, doesn't seem straight-forward in altair (e.g., - # see https://github.com/altair-viz/altair/issues/938). - y_labels = [] - seen = Counter() - for i, e in enumerate(df[feature_id_label]): - if level_delimiter in e: - fields = [field for field in e.split(level_delimiter) - if not field.endswith('__')] - else: - # this is necessary to handle a case where the delimiter - # isn't found and the e ends with '__' - fields = [e] - most_specific = fields[-1] - if most_specific in seen: - y_labels.append(f"{seen[most_specific]}: {most_specific} *") - else: - y_labels.append(most_specific) - seen[most_specific] += 1 - df['y_label'] = y_labels - df['feature'] = [id_.replace(level_delimiter, ' ') - for id_ in df[feature_id_label]] - else: - df['y_label'] = df['feature'] = df[feature_id_label] - - df['enriched'] = ["enriched" if x else "depleted" - for x in df[effect_size_label] > 0] - - df['error-upper'] = df[effect_size_label] + df[error_label] - df['error-lower'] = df[effect_size_label] - df[error_label] - - # Normally we would call bars.mark_rule to add standard error marks - # (opposed to alt.Chart.mark_bar, but I want to color the the standard - # error marks differently so they stand out against the bars themselves. - # As far as I can tell, I can only do this by creating two separate charts, - # so this shared y-axis will be used for both of those. - shared_y = alt.Y("y_label", - title="Feature ID", - sort=alt.EncodingSortField(field=effect_size_label, - op="min", order="descending")) - - bars = alt.Chart(df).mark_bar().encode( - x=alt.X('lfc', title="Log Fold Change (LFC)"), - y=shared_y, - tooltip=alt.Tooltip(["feature", effect_size_label, - significance_label, error_label, - "error-lower", "error-upper"]), - color=alt.Color('enriched', title="Relative to reference", - scale=alt.Scale( - domain=["enriched", "depleted"], - range=["#4c78a8", "#f58518"]), - sort="descending") - ) - - error = alt.Chart(df).mark_rule(color='black').encode( - x='error-lower', - x2='error-upper', - y=shared_y, - ) - - chart = (bars + error).properties(title=title) - chart = chart.configure_legend( - strokeColor='gray', - padding=10, - cornerRadius=10, - ) - - # Moves the y-axis title to the top-left, making it more apparent when - # there are a lot of features. - chart = chart.configure_axisY(titleAlign='left', - titleY=-10, titleAngle=0) - if label_limit is not None: - chart = chart.configure_axis(labelLimit=label_limit) - - chart.save(fig_fp) - return fig_fp - - -def da_barplot(output_dir: str, - data: DataLoafPackageDirFmt, - effect_size_label: str = 'lfc', - feature_id_label: str = 'id', - error_label: str = 'se', - significance_label: str = 'q_val', - significance_threshold: float = 1.0, - effect_size_threshold: float = 0.0, - feature_ids: qiime2.Metadata = None, - level_delimiter: str = None, - label_limit: int = None): - - # setup for the index.html page - ASSETS = importlib.resources.files('q2_composition') / 'assets' - index = Path(ASSETS, 'diff_abundance_plots', 'index.html') - - # collect the user-provided labels for validation - provided_slice_labels = set([effect_size_label, - significance_label, - error_label]) - - slice_data = {} - for e in data.data_slices.iter_views(pd.DataFrame): - slice_data[str(e[0]).replace('_slice.csv', '')] = e[1] - - observed_slice_labels = set(slice_data.keys()) - missing_slice_labels = provided_slice_labels - observed_slice_labels - if len(missing_slice_labels) > 0: - raise KeyError( - f"Provide label(s) ({' '.join(missing_slice_labels)}) " - "are not present in input. Available options are: " - f"{' '.join(observed_slice_labels)}.") - - # exclude the Intercept column from plots - column_labels = [e for e in slice_data[effect_size_label].columns - if e not in '(Intercept)'] - - if feature_id_label not in column_labels: - raise KeyError(f"Feature id header \"{feature_id_label}\" is not " - "present in input. Available options are: " - f"{' '.join(column_labels)}.") - - # create figure_data, which contains the cross-slice data for each - # column of the input dataloaf. some of this logic likely makes sense - # to move into a transformer which handles the arrangement of data - # into a multi-index dataframe. - # - # if the input dataloaf looks like: - # slice1: lfc - # feature-id skin gut - # f1 0.2 5.4 - # f2 1.2 0.1 - # slice2: q_value - # feature-id skin gut - # f1 0.9 0.01 - # f2 0.05 0.8 - # - # figure_data for a specific category will look like: - # feature-id lfc q_value - # f1 0.2 0.9 - # f2 1.2 0.05 - # or: - # feature-id lfc q_value - # f1 5.4 0.01 - # f2 0.1 0.8 - figure_data = [] - for column_label in column_labels: - if column_label == feature_id_label: - continue - df = pd.concat( - [slice_data[effect_size_label][feature_id_label], - slice_data[effect_size_label][column_label], - slice_data[error_label][column_label], - slice_data[significance_label][column_label]], - keys=[feature_id_label, effect_size_label, - error_label, significance_label], - axis=1) - - try: - figure_fp = _plot_differentials( - output_dir, df, - title=column_label, - effect_size_label=effect_size_label, - feature_id_label=feature_id_label, - error_label=error_label, - significance_label=significance_label, - significance_threshold=significance_threshold, - effect_size_threshold=effect_size_threshold, - feature_ids=feature_ids, - level_delimiter=level_delimiter, - label_limit=label_limit) - figure_fn = figure_fp.parts[-1] - figure_data.append((True, figure_fn, column_label, None)) - except ValueError as e: - figure_data.append((False, None, column_label, str(e))) - - context = { - 'figures': figure_data - } - q2templates.render(str(index), output_dir, context=context) diff --git a/q2_composition/_transformer.py b/q2_composition/_transformer.py index 70cb2ac..2aae9f8 100644 --- a/q2_composition/_transformer.py +++ b/q2_composition/_transformer.py @@ -7,6 +7,7 @@ # ---------------------------------------------------------------------------- import json +import os import pandas as pd @@ -42,8 +43,16 @@ def _12(format: DataLoafPackageDirFmt) -> ANCOMBC2SliceMapping: slices = ANCOMBC2SliceMapping() slices.intercepts = intercepts + name_map = { + 'lfc': 'lfc', + 'se': 'se', + 'p_val': 'p', + 'q_val': 'q', + 'w': 'W', + } for (slice_path, slice_df) in format.data_slices.iter_views(pd.DataFrame): - slice_name = slice_path.name.split('_slice')[0] + slice_name = name_map[slice_path.name.split('_slice')[0]] + slice_df = slice_df.rename(columns={'id': 'taxon'}) slices[slice_name] = slice_df return slices @@ -60,6 +69,31 @@ def _2(slices: ANCOMBC2SliceMapping) -> ANCOMBC2OutputDirFmt: format_slice = format.__getattribute__(slice_name) format_slice.write_data(slice_df, pd.DataFrame) + extra_files = ['diff', 'passed_ss'] + if not any(file in slices.keys() for file in extra_files): + header = { + "doctype": { + "name": "table.jsonl", + "format": "application/x-json-lines", + "version": "1.0" + }, + "direction": "row", + "style": "key:value", + "fields": [ + {"name": "taxon", "type": "string", "missing": False}, + {"name": "(Intercept)", "type": "number", "missing": False} + ], + "index": [], + "title": "", + "description": "", + "extra": {} + } + + for file in extra_files: + output_path = os.path.join(format.path, file + '.jsonl') + with open(output_path, 'w') as f: + f.write(json.dumps(header, separators=(',', ':')) + "\n") + return format @@ -94,3 +128,10 @@ def _3(format: ANCOMBC2OutputDirFmt) -> ANCOMBC2SliceMapping: slices.intercepts = list(intercepts) return slices + + +@plugin.register_transformer +def _5(format: DataLoafPackageDirFmt) -> ANCOMBC2OutputDirFmt: + slice_format = _12(format) + ancombc2_format = _2(slice_format) + return ancombc2_format diff --git a/q2_composition/plugin_setup.py b/q2_composition/plugin_setup.py index 538d4fc..0422454 100644 --- a/q2_composition/plugin_setup.py +++ b/q2_composition/plugin_setup.py @@ -8,8 +8,6 @@ import importlib -import numpy as np - from qiime2.plugin import (Int, Float, Bool, Str, List, Choices, Citations, Plugin, Metadata, MetadataColumn, Categorical, Range, Threads) @@ -291,25 +289,24 @@ ) plugin.visualizers.register_function( - function=q2_composition.ancombc2_visualizer, + function=q2_composition.da_barplot, inputs={ - 'data': FeatureData[ANCOMBC2Output], + 'data': FeatureData[DifferentialAbundance | ANCOMBC2Output], 'taxonomy': FeatureData[Taxonomy], }, parameters={}, input_descriptions={ - 'data': 'The ANCOMBC2 output to visualize.', + 'data': 'The ANCOMBC or ANCOMBC2 output to visualize.', 'taxonomy': ( - 'The taxonomy associated with the features present in the ' - 'ANCOMBC2 data.' + 'The taxonomy associated with the features present in the data.' ) }, - name='Visualize ANCOMBC2 output.', + name='Visualize ANCOMBC/ANCOMBC2 output.', description=( - 'Displays ANCOMBC2 Log-Fold Change values in a barplot and allows ' - 'filtering based on p-value and standard error. If a taxonomy is ' - 'provided, features can be filtered by taxonomy using an interactive ' - 'tree.' + 'Displays ANCOMBC/ANCOMBC2 Log-Fold Change values in a barplot and ' + 'allows filtering based on p-value and standard error. If a taxonomy ' + 'is provided, features can be filtered by taxonomy using an ' + 'interactive tree.' ), ) @@ -327,48 +324,4 @@ ' (se), P values, Q values, and W scores.' ), ) - -plugin.visualizers.register_function( - function=q2_composition.da_barplot, - inputs={'data': FeatureData[DifferentialAbundance]}, - parameters={'effect_size_label': Str, - 'feature_id_label': Str, - 'error_label': Str, - 'significance_label': Str, - 'significance_threshold': Float % Range(0.0, 1.0, - inclusive_start=True, - inclusive_end=True), - 'effect_size_threshold': Float % Range(0.0, np.inf, - inclusive_start=True), - 'feature_ids': Metadata, - 'level_delimiter': Str, - 'label_limit': Int}, - input_descriptions={'data': 'The ANCOM-BC output to be plotted.'}, - parameter_descriptions={ - 'effect_size_label': "Label for effect sizes in `data`.", - 'feature_id_label': "Label for feature ids in `data`.", - 'error_label': "Label for effect size errors in `data`.", - 'significance_label': ("Label for statistical significance " - "level in `data`."), - 'significance_threshold': ("Exclude features with statistical " - "significance level greater (i.e., less " - "significant) than this threshold."), - 'effect_size_threshold': ("Exclude features with an absolute value " - "of effect size less than this threshold."), - 'feature_ids': ("Exclude features if their ids are not included in " - "this index."), - 'level_delimiter': ("If feature ids encode hierarchical information, " - "split the levels when generating feature labels " - "in the visualization using this delimiter."), - 'label_limit': ("Set the maximum length that will be viewable for " - "axis labels. You can set this parameter if your " - "axis labels are being cut off.")}, - name='Differential abundance bar plots', - description=('Generate bar plot views of ANCOM-BC output. One plot will ' - 'be present per column in the ANCOM-BC output. The ' - '`significance_threshold`, `effect_size_threshold` ' - 'and `feature_ids` filter results are intersected, ' - 'such that only features that remain after all three ' - 'filters have been applied will be present in the output.'), -) importlib.import_module('q2_composition._transformer') diff --git a/q2_composition/tests/test_ancombc2.py b/q2_composition/tests/test_ancombc2.py index 9ec8ebf..b957b24 100644 --- a/q2_composition/tests/test_ancombc2.py +++ b/q2_composition/tests/test_ancombc2.py @@ -10,7 +10,6 @@ import pandas as pd from pandas.testing import assert_frame_equal -import os from pathlib import Path import subprocess import tempfile @@ -24,9 +23,9 @@ r_base, ancombc2, _process_formula, _convert_metadata, _split_into_slices, _rename_variables_post, _is_categorical, _parse_variable_and_level, _deduce_reference_levels, _process_categorical_variables, - _process_structural_zeros, ancombc2_visualizer + _process_structural_zeros ) -from q2_composition._format import ANCOMBC2SliceMapping +from q2_composition._format import ANCOMBC2SliceMapping, ANCOMBC2OutputDirFmt class TestANCOMBC2Base(unittest.TestCase): @@ -120,7 +119,7 @@ def test_wrapped_ancombc2(self): 'q2_composition._ancombc2._process_structural_zeros', side_effect=lambda structural_zeros: structural_zeros ): - output_format = ancombc2( + slices = ancombc2( table=self.biom_table, metadata=self.metadata, fixed_effects_formula='body-site + year', @@ -129,7 +128,7 @@ def test_wrapped_ancombc2(self): diff_robust=True, ) - slices = transform(data=output_format, to_type=ANCOMBC2SliceMapping) + output_format = transform(data=slices, to_type=ANCOMBC2OutputDirFmt) model_stats = self._slices_to_single_df(slices) struc_zeros = output_format.structural_zeros.view(pd.DataFrame) @@ -146,8 +145,7 @@ def test_ancombc2_without_diff_robust(self): Ensure that the `diff_robust` slice is not present by default in the output format. ''' - slices = transform(data=self.abc2_output, to_type=ANCOMBC2SliceMapping) - self.assertNotIn('diff_robust', slices) + self.assertNotIn('diff_robust', self.abc2_output) def test_group_enforced_if_structural_zeros(self): ''' @@ -169,14 +167,12 @@ def test_spaces_in_metadata_column(self): ''' Tests that metadata columns that contain spaces are handled properly. ''' - output_format = ancombc2( + slices = ancombc2( table=self.biom_table, metadata=self.metadata, fixed_effects_formula='Variable with spaces', ) - slices = transform(data=output_format, to_type=ANCOMBC2SliceMapping) - lfc_slice_columns = list(slices['lfc'].columns) variable_with_spaces_columns = [ col for col in lfc_slice_columns if 'Variable with spaces::' in col @@ -185,34 +181,6 @@ def test_spaces_in_metadata_column(self): # three levels, so one reference and other two in the output self.assertEqual(len(variable_with_spaces_columns), 2) - def test_ancombc2_visualizer(self): - ''' - Tests that the visualizer runs successfully, which is essentially a - test of whether or not the visualizer got built into the - q2_composition/_ancombc2_visualizer/dist/ folder successfully. - ''' - with tempfile.TemporaryDirectory() as tempdir: - ancombc2_visualizer(tempdir, self.abc2_output) - assert os.path.exists(os.path.join(tempdir, 'index.html')) - - def test_ancombc2_visualizer_non_overlapping_taxonomy(self): - ''' - Tests that an error is raised when attempting to visualize ancombc2 - data with a taxonomy that contains none of the features present in the - ancombc2 data. - ''' - taxonomy_df = pd.DataFrame({ - 'Feature ID': ['feat1', 'feat2'], - 'Taxon': ['taxon1', 'taxon2'], - 'Confidence': [0.9, 0.95] - }) - - with tempfile.TemporaryDirectory() as tempdir: - with self.assertRaisesRegex( - ValueError, 'No features remained in your taxonomy' - ): - ancombc2_visualizer(tempdir, self.abc2_output, taxonomy_df) - class TestFormulaProcessing(TestANCOMBC2Base): def test_process_formula(self): diff --git a/q2_composition/tests/test_diff_abundance_plot.py b/q2_composition/tests/test_diff_abundance_plot.py index 6491033..d814e98 100644 --- a/q2_composition/tests/test_diff_abundance_plot.py +++ b/q2_composition/tests/test_diff_abundance_plot.py @@ -5,560 +5,139 @@ # # The full license is in the file LICENSE, distributed with this software. # ---------------------------------------------------------------------------- +import unittest +import qiime2 +import biom +import os import tempfile -from pathlib import Path -import urllib.parse +import pandas as pd -from qiime2.plugin.testing import TestPluginBase +from pathlib import Path +from q2_composition._ancombc2 import da_barplot, ancombc2 +from q2_composition._ancombc import ancombc +from q2_composition._format import ANCOMBC2OutputDirFmt +from qiime2.sdk import PluginManager from qiime2 import Artifact -from q2_composition import (da_barplot, DataLoafPackageDirFmt) - - -class TestBase(TestPluginBase): - package = 'q2_composition.tests' - - def setUp(self): - super().setUp() - self.dataloaf1 = Artifact.load(self.get_data_path('dataloaf.qza')) - self.dataloaf2 = \ - Artifact.load(self.get_data_path('dataloaf-hmp1-body-habitat.qza')) - - -class TestDABarplot(TestBase): - - def _get_output_filepath(self, dir, title): - safe_title = urllib.parse.quote(title) - return dir / f'{safe_title}-ancombc-barplot.html' - - def test_basic_dl1(self): - with tempfile.TemporaryDirectory() as output_dir: - output_dir = Path(output_dir) - _ = da_barplot(output_dir, - self.dataloaf1.view(DataLoafPackageDirFmt), - feature_id_label='id') - self.assertTrue((output_dir / 'index.html').exists()) - - tongue_path = self._get_output_filepath( - output_dir, 'bodysitetongue') - self.assertTrue(tongue_path.exists()) - - right_palm_path = self._get_output_filepath( - output_dir, 'bodysiteright palm') - self.assertTrue(right_palm_path.exists()) - - left_palm_path = self._get_output_filepath( - output_dir, 'bodysiteleft palm') - self.assertTrue(left_palm_path.exists()) - - # checking that spaces are converted to %20 - left_palm_path = self._get_output_filepath( - output_dir, 'bodysiteleft palm') - fp = str(left_palm_path) - expected_fp_segment = "bodysiteleft%20palm-ancombc-barplot.html" - self.assertIn(expected_fp_segment, fp) - - # file shouldn't exist for reference column - gut_path = self._get_output_filepath( - output_dir, 'bodysitegut') - self.assertFalse(gut_path.exists()) - - # spot check for a feature id - self.assertTrue( - 'd29fe3c70564fc0f69f2c03e0d1e5561' in - open(tongue_path).read()) - self.assertTrue( - 'd29fe3c70564fc0f69f2c03e0d1e5561' in - open(right_palm_path).read()) - self.assertTrue( - 'd29fe3c70564fc0f69f2c03e0d1e5561' in - open(left_palm_path).read()) - - def test_basic_dl2(self): - with tempfile.TemporaryDirectory() as output_dir: - output_dir = Path(output_dir) - _ = da_barplot(output_dir, - self.dataloaf2.view(DataLoafPackageDirFmt)) - self.assertTrue((output_dir / 'index.html').exists()) - - feces_path = self._get_output_filepath( - output_dir, 'body_habitatUBERON:feces') - self.assertTrue(feces_path.exists()) - - oral_cavity_path = self._get_output_filepath( - output_dir, 'body_habitatUBERON:oral cavity') - self.assertTrue(oral_cavity_path.exists()) - - saliva_path = self._get_output_filepath( - output_dir, 'body_habitatUBERON:saliva') - self.assertTrue(saliva_path.exists()) - - vagina_path = self._get_output_filepath( - output_dir, 'body_habitatUBERON:vagina') - self.assertTrue(vagina_path.exists()) - - # file shouldn't exist for reference column - skin_path = self._get_output_filepath( - output_dir, 'body_habitatUBERON:skin') - self.assertFalse(skin_path.exists()) - - # spot check for a feature id - self.assertTrue( - 'g__Staphylococcus' in - open(feces_path).read()) - self.assertTrue( - 'g__Staphylococcus' in - open(oral_cavity_path).read()) - self.assertTrue( - 'g__Staphylococcus' in - open(saliva_path).read()) - self.assertTrue( - 'g__Staphylococcus' in - open(vagina_path).read()) - - def test_label_limit(self): - with tempfile.TemporaryDirectory() as output_dir: - output_dir = Path(output_dir) - _ = da_barplot(output_dir, - self.dataloaf2.view(DataLoafPackageDirFmt), - label_limit=4242) - feces_path = self._get_output_filepath( - output_dir, 'body_habitatUBERON:feces') - self.assertTrue('{"labelLimit": 4242}' in open(feces_path).read()) - - with tempfile.TemporaryDirectory() as output_dir: - output_dir = Path(output_dir) - _ = da_barplot(output_dir, - self.dataloaf2.view(DataLoafPackageDirFmt), - label_limit=None) - feces_path = self._get_output_filepath( - output_dir, 'body_habitatUBERON:feces') - self.assertFalse('labelLimit' in open(feces_path).read()) - - def test_filter_on_significance(self): - # confirm feature presence when not filtering - with tempfile.TemporaryDirectory() as output_dir: - output_dir = Path(output_dir) - _ = da_barplot(output_dir, - self.dataloaf1.view(DataLoafPackageDirFmt), - feature_id_label='id', - significance_threshold=1.0) - tongue_path = self._get_output_filepath( - output_dir, 'bodysitetongue') - self.assertTrue(tongue_path.exists()) - - right_palm_path = self._get_output_filepath( - output_dir, 'bodysiteright palm') - self.assertTrue(right_palm_path.exists()) - - left_palm_path = self._get_output_filepath( - output_dir, 'bodysiteleft palm') - self.assertTrue(left_palm_path.exists()) - - self.assertTrue( - 'd29fe3c70564fc0f69f2c03e0d1e5561' in - open(tongue_path).read()) - self.assertTrue( - 'd29fe3c70564fc0f69f2c03e0d1e5561' in - open(right_palm_path).read()) - self.assertTrue( - 'd29fe3c70564fc0f69f2c03e0d1e5561' in - open(left_palm_path).read()) - - # confirm feature absence when filtering - with tempfile.TemporaryDirectory() as output_dir: - output_dir = Path(output_dir) - _ = da_barplot(output_dir, - self.dataloaf1.view(DataLoafPackageDirFmt), - feature_id_label='id', - significance_threshold=1e-3) - - tongue_path = self._get_output_filepath( - output_dir, 'bodysitetongue') - self.assertTrue(tongue_path.exists()) - - right_palm_path = self._get_output_filepath( - output_dir, 'bodysiteright palm') - self.assertTrue(right_palm_path.exists()) - - left_palm_path = self._get_output_filepath( - output_dir, 'bodysiteleft palm') - self.assertTrue(left_palm_path.exists()) - - # this feature is significant at this - # threshold only in tongue and left palm - self.assertTrue( - 'd29fe3c70564fc0f69f2c03e0d1e5561' in - open(tongue_path).read()) - self.assertFalse( - 'd29fe3c70564fc0f69f2c03e0d1e5561' in - open(right_palm_path).read()) - self.assertTrue( - 'd29fe3c70564fc0f69f2c03e0d1e5561' in - open(left_palm_path).read()) - - def test_filter_on_effect_size(self): - # confirm feature presence when not filtering - with tempfile.TemporaryDirectory() as output_dir: - output_dir = Path(output_dir) - _ = da_barplot(output_dir, - self.dataloaf1.view(DataLoafPackageDirFmt), - feature_id_label='id', - effect_size_threshold=0) - - tongue_path = self._get_output_filepath( - output_dir, 'bodysitetongue') - self.assertTrue(tongue_path.exists()) - - right_palm_path = self._get_output_filepath( - output_dir, 'bodysiteright palm') - self.assertTrue(right_palm_path.exists()) - - left_palm_path = self._get_output_filepath( - output_dir, 'bodysiteleft palm') - self.assertTrue(left_palm_path.exists()) - - self.assertTrue( - 'd29fe3c70564fc0f69f2c03e0d1e5561' in - open(tongue_path).read()) - self.assertTrue( - 'd29fe3c70564fc0f69f2c03e0d1e5561' in - open(right_palm_path).read()) - self.assertTrue( - 'd29fe3c70564fc0f69f2c03e0d1e5561' in - open(left_palm_path).read()) - - # confirm feature absence when filtering - with tempfile.TemporaryDirectory() as output_dir: - output_dir = Path(output_dir) - _ = da_barplot(output_dir, - self.dataloaf1.view(DataLoafPackageDirFmt), - feature_id_label='id', - effect_size_threshold=3.5) - - tongue_path = self._get_output_filepath( - output_dir, 'bodysitetongue') - self.assertTrue(tongue_path.exists()) - - right_palm_path = self._get_output_filepath( - output_dir, 'bodysiteright palm') - self.assertTrue(right_palm_path.exists()) - - left_palm_path = self._get_output_filepath( - output_dir, 'bodysiteleft palm') - self.assertTrue(left_palm_path.exists()) - - # this feature's effect size is greater - # than the threshold only in tongue and left palm - self.assertTrue( - 'd29fe3c70564fc0f69f2c03e0d1e5561' in - open(tongue_path).read()) - self.assertFalse( - 'd29fe3c70564fc0f69f2c03e0d1e5561' in - open(right_palm_path).read()) - self.assertTrue( - 'd29fe3c70564fc0f69f2c03e0d1e5561' in - open(left_palm_path).read()) - - def test_filter_on_feature_ids(self): - ids_to_keep = ['bfbed36e63b69fec4627424163d20118', - '4b5eeb300368260019c1fbc7a3c718fc'] - - # confirm feature presence when no filtering - with tempfile.TemporaryDirectory() as output_dir: - output_dir = Path(output_dir) - _ = da_barplot(output_dir, - self.dataloaf1.view(DataLoafPackageDirFmt), - feature_id_label='id', - feature_ids=None) - - tongue_path = self._get_output_filepath( - output_dir, 'bodysitetongue') - self.assertTrue(tongue_path.exists()) - - right_palm_path = self._get_output_filepath( - output_dir, 'bodysiteright palm') - self.assertTrue(right_palm_path.exists()) - - left_palm_path = self._get_output_filepath( - output_dir, 'bodysiteleft palm') - self.assertTrue(left_palm_path.exists()) - - self.assertTrue( - 'd29fe3c70564fc0f69f2c03e0d1e5561' in - open(tongue_path).read()) - self.assertTrue( - 'd29fe3c70564fc0f69f2c03e0d1e5561' in - open(right_palm_path).read()) - self.assertTrue( - 'd29fe3c70564fc0f69f2c03e0d1e5561' in - open(left_palm_path).read()) - - # confirm feature absence when filtering - with tempfile.TemporaryDirectory() as output_dir: - output_dir = Path(output_dir) - _ = da_barplot(output_dir, - self.dataloaf1.view(DataLoafPackageDirFmt), - feature_id_label='id', - feature_ids=ids_to_keep) - - tongue_path = self._get_output_filepath( - output_dir, 'bodysitetongue') - self.assertTrue(tongue_path.exists()) - - right_palm_path = self._get_output_filepath( - output_dir, 'bodysiteright palm') - self.assertTrue(right_palm_path.exists()) - - left_palm_path = self._get_output_filepath( - output_dir, 'bodysiteleft palm') - self.assertTrue(left_palm_path.exists()) - - # feature ids expected to be absent are absent - self.assertFalse( - 'd29fe3c70564fc0f69f2c03e0d1e5561' in - open(tongue_path).read()) - self.assertFalse( - 'd29fe3c70564fc0f69f2c03e0d1e5561' in - open(right_palm_path).read()) - self.assertFalse( - 'd29fe3c70564fc0f69f2c03e0d1e5561' in - open(left_palm_path).read()) - - # feature ids expected to be present are present - self.assertTrue( - 'bfbed36e63b69fec4627424163d20118' in - open(tongue_path).read()) - self.assertTrue( - 'bfbed36e63b69fec4627424163d20118' in - open(right_palm_path).read()) - self.assertTrue( - 'bfbed36e63b69fec4627424163d20118' in - open(left_palm_path).read()) - - self.assertTrue( - '4b5eeb300368260019c1fbc7a3c718fc' in - open(tongue_path).read()) - self.assertTrue( - '4b5eeb300368260019c1fbc7a3c718fc' in - open(right_palm_path).read()) - self.assertTrue( - '4b5eeb300368260019c1fbc7a3c718fc' in - open(left_palm_path).read()) - - def test_error_on_bad_column_names(self): - with tempfile.TemporaryDirectory() as output_dir: - with self.assertRaisesRegex(KeyError, "id.*\"taxon\".*not"): - da_barplot(output_dir, - data=self.dataloaf1.view(DataLoafPackageDirFmt), - feature_id_label='taxon') - - with tempfile.TemporaryDirectory() as output_dir: - with self.assertRaisesRegex(KeyError, "label.*stderr.*not"): - da_barplot(output_dir, - data=self.dataloaf1.view(DataLoafPackageDirFmt), - error_label='stderr') - - def test_toggle_level_delimiter(self): - # no delimiter - with tempfile.TemporaryDirectory() as output_dir: - output_dir = Path(output_dir) - _ = da_barplot(output_dir, - self.dataloaf2.view(DataLoafPackageDirFmt)) - - feces_path = self._get_output_filepath( - output_dir, 'body_habitatUBERON:feces') - self.assertTrue(feces_path.exists()) - - oral_cavity_path = self._get_output_filepath( - output_dir, 'body_habitatUBERON:oral cavity') - self.assertTrue(oral_cavity_path.exists()) - - saliva_path = self._get_output_filepath( - output_dir, 'body_habitatUBERON:saliva') - self.assertTrue(saliva_path.exists()) - - vagina_path = self._get_output_filepath( - output_dir, 'body_habitatUBERON:vagina') - self.assertTrue(vagina_path.exists()) - - # multi-level information should be present as 'y-label' - self.assertTrue( - '"y_label": "d__Bacteria;p__Firmicutes;c__Bacilli;o__Staphylococcales;f__Staphylococcaceae;g__Staphylococcus"' # noqa: E501 - in open(feces_path).read()) - self.assertTrue( - '"y_label": "d__Bacteria;p__Firmicutes;c__Bacilli;o__Staphylococcales;f__Staphylococcaceae;g__Staphylococcus"' # noqa: E501 - in open(oral_cavity_path).read()) - self.assertTrue( - '"y_label": "d__Bacteria;p__Firmicutes;c__Bacilli;o__Staphylococcales;f__Staphylococcaceae;g__Staphylococcus"' # noqa: E501 - in open(saliva_path).read()) - self.assertTrue( - '"y_label": "d__Bacteria;p__Firmicutes;c__Bacilli;o__Staphylococcales;f__Staphylococcaceae;g__Staphylococcus"' # noqa: E501 - in open(vagina_path).read()) - - # same labels should be present as 'feature' - self.assertTrue( - '"feature": "d__Bacteria;p__Firmicutes;c__Bacilli;o__Staphylococcales;f__Staphylococcaceae;g__Staphylococcus"' # noqa: E501 - in open(feces_path).read()) - self.assertTrue( - '"feature": "d__Bacteria;p__Firmicutes;c__Bacilli;o__Staphylococcales;f__Staphylococcaceae;g__Staphylococcus"' # noqa: E501 - in open(oral_cavity_path).read()) - self.assertTrue( - '"feature": "d__Bacteria;p__Firmicutes;c__Bacilli;o__Staphylococcales;f__Staphylococcaceae;g__Staphylococcus"' # noqa: E501 - in open(saliva_path).read()) - self.assertTrue( - '"feature": "d__Bacteria;p__Firmicutes;c__Bacilli;o__Staphylococcales;f__Staphylococcaceae;g__Staphylococcus"' # noqa: E501 - in open(vagina_path).read()) - - # typical delimiter: ; - with tempfile.TemporaryDirectory() as output_dir: - output_dir = Path(output_dir) - _ = da_barplot(output_dir, - self.dataloaf2.view(DataLoafPackageDirFmt), - level_delimiter=';') - - feces_path = self._get_output_filepath( - output_dir, 'body_habitatUBERON:feces') - self.assertTrue(feces_path.exists()) - - oral_cavity_path = self._get_output_filepath( - output_dir, 'body_habitatUBERON:oral cavity') - self.assertTrue(oral_cavity_path.exists()) - - saliva_path = self._get_output_filepath( - output_dir, 'body_habitatUBERON:saliva') - self.assertTrue(saliva_path.exists()) - - vagina_path = self._get_output_filepath( - output_dir, 'body_habitatUBERON:vagina') - self.assertTrue(vagina_path.exists()) - - # single-level information should be present as 'y-label' - self.assertTrue( - '"y_label": "g__Staphylococcus' in - open(feces_path).read()) - self.assertTrue( - '"y_label": "g__Staphylococcus' in - open(oral_cavity_path).read()) - self.assertTrue( - '"y_label": "g__Staphylococcus' in - open(saliva_path).read()) - self.assertTrue( - '"y_label": "g__Staphylococcus' in - open(vagina_path).read()) - - # space delimited labels should be present as 'feature' - self.assertTrue( - '"feature": "d__Bacteria p__Firmicutes c__Bacilli o__Staphylococcales f__Staphylococcaceae g__Staphylococcus"' # noqa: E501 - in open(feces_path).read()) - self.assertTrue( - '"feature": "d__Bacteria p__Firmicutes c__Bacilli o__Staphylococcales f__Staphylococcaceae g__Staphylococcus"' # noqa: E501 - in open(oral_cavity_path).read()) - self.assertTrue( - '"feature": "d__Bacteria p__Firmicutes c__Bacilli o__Staphylococcales f__Staphylococcaceae g__Staphylococcus"' # noqa: E501 - in open(saliva_path).read()) - self.assertTrue( - '"feature": "d__Bacteria p__Firmicutes c__Bacilli o__Staphylococcales f__Staphylococcaceae g__Staphylococcus"' # noqa: E501 - in open(vagina_path).read()) - - # weird delimiter: __ - with tempfile.TemporaryDirectory() as output_dir: - output_dir = Path(output_dir) - _ = da_barplot(output_dir, - self.dataloaf2.view(DataLoafPackageDirFmt), - level_delimiter='__') - - feces_path = self._get_output_filepath( - output_dir, 'body_habitatUBERON:feces') - self.assertTrue(feces_path.exists()) - - oral_cavity_path = self._get_output_filepath( - output_dir, 'body_habitatUBERON:oral cavity') - self.assertTrue(oral_cavity_path.exists()) - - saliva_path = self._get_output_filepath( - output_dir, 'body_habitatUBERON:saliva') - self.assertTrue(saliva_path.exists()) - - vagina_path = self._get_output_filepath( - output_dir, 'body_habitatUBERON:vagina') - self.assertTrue(vagina_path.exists()) - - # single-level information should be present as 'y-label' - self.assertTrue( - '"y_label": "Staphylococcus' in - open(feces_path).read()) - self.assertTrue( - '"y_label": "Staphylococcus' in - open(oral_cavity_path).read()) - self.assertTrue( - '"y_label": "Staphylococcus' in - open(saliva_path).read()) - self.assertTrue( - '"y_label": "Staphylococcus' in - open(vagina_path).read()) - - # space delimited labels should be present as 'feature' - self.assertTrue( - '"feature": "d Bacteria;p Firmicutes;c Bacilli;o Staphylococcales;f Staphylococcaceae;g Staphylococcus"' # noqa: E501 - in open(feces_path).read()) - self.assertTrue( - '"feature": "d Bacteria;p Firmicutes;c Bacilli;o Staphylococcales;f Staphylococcaceae;g Staphylococcus"' # noqa: E501 - in open(oral_cavity_path).read()) - self.assertTrue( - '"feature": "d Bacteria;p Firmicutes;c Bacilli;o Staphylococcales;f Staphylococcaceae;g Staphylococcus"' # noqa: E501 - in open(saliva_path).read()) - self.assertTrue( - '"feature": "d Bacteria;p Firmicutes;c Bacilli;o Staphylococcales;f Staphylococcaceae;g Staphylococcus"' # noqa: E501 - in open(vagina_path).read()) - - # missing delimiter: ! - with tempfile.TemporaryDirectory() as output_dir: - output_dir = Path(output_dir) - _ = da_barplot(output_dir, - self.dataloaf2.view(DataLoafPackageDirFmt), - level_delimiter='!') - - feces_path = self._get_output_filepath( - output_dir, 'body_habitatUBERON:feces') - self.assertTrue(feces_path.exists()) - - oral_cavity_path = self._get_output_filepath( - output_dir, 'body_habitatUBERON:oral cavity') - self.assertTrue(oral_cavity_path.exists()) - - saliva_path = self._get_output_filepath( - output_dir, 'body_habitatUBERON:saliva') - self.assertTrue(saliva_path.exists()) - - vagina_path = self._get_output_filepath( - output_dir, 'body_habitatUBERON:vagina') - self.assertTrue(vagina_path.exists()) - - # multi-level information should be present as 'y-label' - self.assertTrue( - '"y_label": "d__Bacteria;p__Firmicutes;c__Bacilli;o__Staphylococcales;f__Staphylococcaceae;g__Staphylococcus"' # noqa: E501 - in open(feces_path).read()) - self.assertTrue( - '"y_label": "d__Bacteria;p__Firmicutes;c__Bacilli;o__Staphylococcales;f__Staphylococcaceae;g__Staphylococcus"' # noqa: E501 - in open(oral_cavity_path).read()) - self.assertTrue( - '"y_label": "d__Bacteria;p__Firmicutes;c__Bacilli;o__Staphylococcales;f__Staphylococcaceae;g__Staphylococcus"' # noqa: E501 - in open(saliva_path).read()) - self.assertTrue( - '"y_label": "d__Bacteria;p__Firmicutes;c__Bacilli;o__Staphylococcales;f__Staphylococcaceae;g__Staphylococcus"' # noqa: E501 - in open(vagina_path).read()) - # same labels should be present as 'feature' - self.assertTrue( - '"feature": "d__Bacteria;p__Firmicutes;c__Bacilli;o__Staphylococcales;f__Staphylococcaceae;g__Staphylococcus"' # noqa: E501 - in open(feces_path).read()) - self.assertTrue( - '"feature": "d__Bacteria;p__Firmicutes;c__Bacilli;o__Staphylococcales;f__Staphylococcaceae;g__Staphylococcus"' # noqa: E501 - in open(oral_cavity_path).read()) - self.assertTrue( - '"feature": "d__Bacteria;p__Firmicutes;c__Bacilli;o__Staphylococcales;f__Staphylococcaceae;g__Staphylococcus"' # noqa: E501 - in open(saliva_path).read()) - self.assertTrue( - '"feature": "d__Bacteria;p__Firmicutes;c__Bacilli;o__Staphylococcales;f__Staphylococcaceae;g__Staphylococcus"' # noqa: E501 - in open(vagina_path).read()) +class TestDiffAbundancePlot(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.test_data_fp = Path(__file__).parent / 'data' / 'ancombc2' + + table_fp = cls.test_data_fp / 'feature-table.biom' + cls.biom_table = biom.load_table(table_fp) + + metadata_fp = cls.test_data_fp / 'metadata.tsv' + cls.ancombc2_metadata = qiime2.Metadata.load(metadata_fp) + + ancombc_table_fp = Path(__file__).parent / 'data' / 'table-ancombc.qza' + ancombc_table = qiime2.Artifact.load(ancombc_table_fp) + cls.ancombc_table = ancombc_table.view(pd.DataFrame) + + metadata_fp = Path(__file__).parent / 'data' / 'sample-md-ancombc.tsv' + cls.ancombc_metadata = qiime2.Metadata.load(metadata_fp) + + cls.abc2_output = ancombc2( + table=cls.biom_table, + metadata=cls.ancombc2_metadata, + fixed_effects_formula='body-site + year', + group='body-site', + structural_zeros=True + ) + + # Use this for raw calls to da_barplot but use the original for when we + # load it to an Artifact and invoke the action + cls.abc2_output_DirFmt = qiime2.plugin.util.transform( + data=cls.abc2_output, to_type=ANCOMBC2OutputDirFmt + ) + + cls.abc_output = ancombc( + table=cls.ancombc_table, + metadata=cls.ancombc_metadata, + formula='bodysite' + ) + + def test_da_barplot(self): + ''' + Tests that the visualizer runs successfully, which is essentially a + test of whether or not the visualizer got built into the + q2_composition/_da_barplot/dist/ folder successfully. + ''' + with tempfile.TemporaryDirectory() as tempdir: + da_barplot(tempdir, self.abc2_output_DirFmt) + assert os.path.exists(os.path.join(tempdir, 'index.html')) + + with tempfile.TemporaryDirectory() as tempdir: + da_barplot(tempdir, self.abc_output) + assert os.path.exists(os.path.join(tempdir, 'index.html')) + + def test_da_barplot_non_overlapping_taxonomy(self): + ''' + Tests that an error is raised when attempting to visualize ancombc2 + data with a taxonomy that contains none of the features present in the + ancombc2 data. + ''' + taxonomy_df = pd.DataFrame({ + 'Feature ID': ['feat1', 'feat2'], + 'Taxon': ['taxon1', 'taxon2'], + 'Confidence': [0.9, 0.95] + }) + + with tempfile.TemporaryDirectory() as tempdir: + with self.assertRaisesRegex( + ValueError, 'No features remained in your taxonomy' + ): + da_barplot(tempdir, self.abc2_output_DirFmt, taxonomy_df) + + with tempfile.TemporaryDirectory() as tempdir: + with self.assertRaisesRegex( + ValueError, 'No features remained in your taxonomy' + ): + da_barplot(tempdir, self.abc_output, taxonomy_df) + + def test_da_barplot_accepts_ancombc2(self): + ''' + Tests that da_barplot accepts ancombc2 outputs. + ''' + with tempfile.TemporaryDirectory() as tempdir: + da_barplot(tempdir, self.abc2_output_DirFmt) + assert os.path.exists(os.path.join(tempdir, 'index.html')) + + def test_da_barplot_accepts_ancombc(self): + ''' + Tests that da_barplot accepts ancombc outputs. + ''' + with tempfile.TemporaryDirectory() as tempdir: + da_barplot(tempdir, self.abc_output) + assert os.path.exists(os.path.join(tempdir, 'index.html')) + + def test_da_barplot_action_ancombc2(self): + pm = PluginManager() + da_barplot = pm.plugins['composition'].actions['da_barplot'] + + ancombc2_artifact = Artifact.import_data( + type='FeatureData[ANCOMBC2Output]', view=self.abc2_output + ) + + viz = da_barplot(ancombc2_artifact) + viz = viz.visualization + + with tempfile.TemporaryDirectory() as tempdir: + viz.export_data(tempdir) + assert os.path.exists(os.path.join(tempdir, 'index.html')) + + def test_da_barplot_action_ancombc(self): + pm = PluginManager() + da_barplot = pm.plugins['composition'].actions['da_barplot'] + + ancombc_artifact = Artifact.import_data( + type='FeatureData[DifferentialAbundance]', view=self.abc_output + ) + + viz = da_barplot(ancombc_artifact) + viz = viz.visualization + + with tempfile.TemporaryDirectory() as tempdir: + viz.export_data(tempdir) + assert os.path.exists(os.path.join(tempdir, 'index.html'))