improve handling of non-finite floating point values (#490)

* improve HTTP error reporting

* generate standards-compatible JSON

* add --nan-to-num work-around for non-finite floating point values

* lint

* update tests

* correctly set Infinities to min/max

* REAMDE update for --nan-to-num

* define constant for repetitive warning message

* clarify where NaN errors will occure
This commit is contained in:
Bruce Martin
2018-12-04 14:56:16 -08:00
committed by GitHub
parent 296ed752fa
commit 3bfeadc2b9
8 changed files with 138 additions and 47 deletions
+12 -5
View File
@@ -27,6 +27,7 @@ If you want an example dataset download [this file](https://github.com/chanzucke
``` ```
cellxgene launch pbmc3k.h5ad --open cellxgene launch pbmc3k.h5ad --open
``` ```
You should see your web browser open with the following You should see your web browser open with the following
<img width="450" src="https://raw.githubusercontent.com/chanzuckerberg/cellxgene/master/docs/cellxgene-opening-screenshot.png" pad="50px"> <img width="450" src="https://raw.githubusercontent.com/chanzuckerberg/cellxgene/master/docs/cellxgene-opening-screenshot.png" pad="50px">
@@ -35,7 +36,7 @@ You should see your web browser open with the following
There are several options available, such as: There are several options available, such as:
- `--layout` to specify the layout as `tsne` or `umap` - `--layout` to specify the layout as `tsne` or `umap`
- `--title` to show a title on the explorer - `--title` to show a title on the explorer
- `--open` to automatically open the web browser after launching (OS X only) - `--open` to automatically open the web browser after launching (OS X only)
@@ -56,7 +57,7 @@ The `launch` command assumes that the data is stored in the `.h5ad` format from
- an `obs` field has a unique identifier for every cell (you can specify which field to use with the `--obs-names` option, by default it will use the value of `data.obs_names`) - an `obs` field has a unique identifier for every cell (you can specify which field to use with the `--obs-names` option, by default it will use the value of `data.obs_names`)
- a `var` field has a unique identifier for every gene (you can specify which field to use with the `--var-names` option, by default it will use the value of `data.var_names`) - a `var` field has a unique identifier for every gene (you can specify which field to use with the `--var-names` option, by default it will use the value of `data.var_names`)
- an `obsm` field contains the two-dimensional coordinates for the layout that you want to render (e.g. `X_tsne` for the `tsne` layout or `X_umap` for the `umap` layout) - an `obsm` field contains the two-dimensional coordinates for the layout that you want to render (e.g. `X_tsne` for the `tsne` layout or `X_umap` for the `umap` layout)
- any additional `obs` fields will be rendered as per-cell continuous or categorical metadata by the app (e.g. `louvain` cluster assignments) - any additional `obs` fields will be rendered as per-cell continuous or categorical metadata by the app (e.g. `louvain` cluster assignments)
### prepare ### prepare
@@ -153,7 +154,7 @@ This may happen, especially as we work out bugs in our installation process! Ple
> How are you computing and sorting differential expression results? > How are you computing and sorting differential expression results?
Currently we use a [Welch's *t*-test](https://en.wikipedia.org/wiki/Welch%27s_t-test) implementation including the same variance overestimation correction as used in `scanpy`. We sort the `tscore` to identify the top N genes, and then filter to remove any that fall below a cutoff log fold change value, which can help remove spurious test results. The default threshold is `0.01` and can be changed using the option `--diffexp-lfc-cutoff`. We can explore adding support for other test types in the future. Currently we use a [Welch's _t_-test](https://en.wikipedia.org/wiki/Welch%27s_t-test) implementation including the same variance overestimation correction as used in `scanpy`. We sort the `tscore` to identify the top N genes, and then filter to remove any that fall below a cutoff log fold change value, which can help remove spurious test results. The default threshold is `0.01` and can be changed using the option `--diffexp-lfc-cutoff`. We can explore adding support for other test types in the future.
> I'm following the developer instructions and get an error about "missing files and directories” when trying to build the client > I'm following the developer instructions and get an error about "missing files and directories” when trying to build the client
@@ -165,6 +166,12 @@ This is likely because you do not have node and npm installed, we recommend usin
- `.X` is used to display expression (histograms, scatterplot & colorscale) and to compute differential expression - `.X` is used to display expression (histograms, scatterplot & colorscale) and to compute differential expression
- `.obsm` is used for layout - `.obsm` is used for layout
> When I start cellxgene, I get an error `Unexpected HTTP response 500, INTERNAL SERVER ERROR -- Out of range float values are not JSON compliant` in the web UI, or `Warning: JSON encoding failure - suggest trying --nan-to-num command line option` in the CLI. What can I do?
At the moment, cellxgene is unable to transmit floating point NaN or Inifinty values to the web UI (due to a limitation on data serialization method in use). We expect to resolve this in a future release, but in the meantime, you can work around this issue by starting cellxgene with the `--nan-to-num` command line option, ie, `cellxgene launch data.h5ad --nan-to-num`.
This option will convert all NaNs to zero, and all positive/negative infinities to the min/max of the data element within which the value was found (eg, +Infinity within an `obs` annotation will be converted to the maximum finite value in that annotation). This option will increase startup time, so we recommend only using it when the dataset contains NaN/Infinities.
## developer guide ## developer guide
This project has made a few key design choices This project has made a few key design choices
@@ -207,8 +214,8 @@ If you have any questions about developing or contributing, come hang out with u
`cellxgene` is still very much in development, and we've love to include the community as we plan new features to work on. We are thinking about working on the following features over the next 3-12 months. If you are interested in updates, want to give feedback, want to contribute, or have ideas about other features we should work on, please [contact us](#help-and-contact) `cellxgene` is still very much in development, and we've love to include the community as we plan new features to work on. We are thinking about working on the following features over the next 3-12 months. If you are interested in updates, want to give feedback, want to contribute, or have ideas about other features we should work on, please [contact us](#help-and-contact)
- **Visualizaling spatial metadata** Image-based transcriptomics methods also generate large cell by gene matrices, alongside rich metadata about spatial location; we would like to render this information in `cellxgene` - **Visualizaling spatial metadata** Image-based transcriptomics methods also generate large cell by gene matrices, alongside rich metadata about spatial location; we would like to render this information in `cellxgene`
- **Visualizing trajectories** Trajectory analyses infer progression along some ordering or pseudotime; we would like `cellxgene ` to render the results of these analyses when they have been performed - **Visualizing trajectories** Trajectory analyses infer progression along some ordering or pseudotime; we would like `cellxgene` to render the results of these analyses when they have been performed
- **Deploy to web** Many projects release public data browser websites alongside their publicatons; we would like to make it easy for anyone to deploy `cellxgene` to a custom URL with their own dataset that they own and operate - **Deploy to web** Many projects release public data browser websites alongside their publicatons; we would like to make it easy for anyone to deploy `cellxgene` to a custom URL with their own dataset that they own and operate
- **HCA Integration** The [Human Cell Atlas](https://humancellatlas.org) is generating a large corpus of single-cell expression data and will make it available through the Data Coordination Platform; we would like `cellxgene` to be one of several different portals for browsing these data - **HCA Integration** The [Human Cell Atlas](https://humancellatlas.org) is generating a large corpus of single-cell expression data and will make it available through the Data Coordination Platform; we would like `cellxgene` to be one of several different portals for browsing these data
## contributing ## contributing
+5 -1
View File
@@ -38,7 +38,11 @@ export const doJsonRequest = async url => {
return res.json(); return res.json();
} }
// else an error // else an error
const msg = `Unexpected HTTP response ${res.status}, ${res.statusText}`; let msg = `Unexpected HTTP response ${res.status}, ${res.statusText}`;
const body = await res.text();
if (body && body.length > 0) {
msg = `${msg} -- ${body}`;
}
dispatchNetworkErrorMessageToUser(msg); dispatchNetworkErrorMessageToUser(msg);
throw new Error(msg); throw new Error(msg);
}; };
+54 -7
View File
@@ -1,5 +1,6 @@
from http import HTTPStatus from http import HTTPStatus
import pkg_resources import pkg_resources
import warnings
from flask import ( from flask import (
Blueprint, current_app, jsonify, make_response, request Blueprint, current_app, jsonify, make_response, request
@@ -7,7 +8,7 @@ from flask import (
from flask_restful_swagger_2 import Api, swagger, Resource from flask_restful_swagger_2 import Api, swagger, Resource
from werkzeug.datastructures import ImmutableMultiDict from werkzeug.datastructures import ImmutableMultiDict
from server.app.util.constants import Axis, DiffExpMode from server.app.util.constants import Axis, DiffExpMode, JSON_NaN_to_num_warning_msg
from server.app.util.filter import parse_filter, QueryStringError from server.app.util.filter import parse_filter, QueryStringError
from server.app.util.models import FilterModel from server.app.util.models import FilterModel
from server.app.util.utils import get_mime_type from server.app.util.utils import get_mime_type
@@ -160,7 +161,12 @@ class AnnotationsObsAPI(Resource):
annotation_response = current_app.data.annotation({}, "obs", fields) annotation_response = current_app.data.annotation({}, "obs", fields)
except KeyError: except KeyError:
return make_response(f"Error bad key in {fields}", HTTPStatus.BAD_REQUEST) return make_response(f"Error bad key in {fields}", HTTPStatus.BAD_REQUEST)
return make_response(jsonify(annotation_response), HTTPStatus.OK) try:
return make_response(jsonify(annotation_response), HTTPStatus.OK)
except ValueError as e:
# JSON encoding failure, usually due to bad data
warnings.warn(JSON_NaN_to_num_warning_msg)
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
@swagger.doc({ @swagger.doc({
"summary": "Fetch annotations (metadata) for filtered subset of observations.", "summary": "Fetch annotations (metadata) for filtered subset of observations.",
@@ -211,7 +217,12 @@ class AnnotationsObsAPI(Resource):
return make_response(f"Error bad key in {fields}", HTTPStatus.BAD_REQUEST) return make_response(f"Error bad key in {fields}", HTTPStatus.BAD_REQUEST)
except FilterError as e: except FilterError as e:
return make_response(e.message, HTTPStatus.BAD_REQUEST) return make_response(e.message, HTTPStatus.BAD_REQUEST)
return make_response(jsonify(annotation_response), HTTPStatus.OK) try:
return make_response(jsonify(annotation_response), HTTPStatus.OK)
except ValueError as e:
# JSON encoding failure, usually due to bad data
warnings.warn(JSON_NaN_to_num_warning_msg)
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
class AnnotationsVarAPI(Resource): class AnnotationsVarAPI(Resource):
@@ -253,7 +264,12 @@ class AnnotationsVarAPI(Resource):
annotation_response = current_app.data.annotation({}, "var", fields) annotation_response = current_app.data.annotation({}, "var", fields)
except KeyError: except KeyError:
return make_response(f"Error bad key in {fields}", HTTPStatus.BAD_REQUEST) return make_response(f"Error bad key in {fields}", HTTPStatus.BAD_REQUEST)
return make_response(jsonify(annotation_response), HTTPStatus.OK) try:
return make_response(jsonify(annotation_response), HTTPStatus.OK)
except ValueError as e:
# JSON encoding failure, usually due to bad data
warnings.warn(JSON_NaN_to_num_warning_msg)
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
@swagger.doc({ @swagger.doc({
"summary": "Fetch annotations (metadata) for filtered subset of variables.", "summary": "Fetch annotations (metadata) for filtered subset of variables.",
@@ -302,7 +318,12 @@ class AnnotationsVarAPI(Resource):
return make_response(f"Error bad key in {fields}", HTTPStatus.BAD_REQUEST) return make_response(f"Error bad key in {fields}", HTTPStatus.BAD_REQUEST)
except FilterError: except FilterError:
return make_response("Malformed filter", HTTPStatus.BAD_REQUEST) return make_response("Malformed filter", HTTPStatus.BAD_REQUEST)
return make_response(jsonify(annotation_response), HTTPStatus.OK) try:
return make_response(jsonify(annotation_response), HTTPStatus.OK)
except ValueError as e:
# JSON encoding failure, usually due to bad data
warnings.warn(JSON_NaN_to_num_warning_msg)
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
class DataObsAPI(Resource): class DataObsAPI(Resource):
@@ -363,6 +384,10 @@ class DataObsAPI(Resource):
return make_response((jsonify(current_app.data.data_frame(filter_, axis=Axis.OBS))), HTTPStatus.OK) return make_response((jsonify(current_app.data.data_frame(filter_, axis=Axis.OBS))), HTTPStatus.OK)
except FilterError as e: except FilterError as e:
return make_response(e.message, HTTPStatus.BAD_REQUEST) return make_response(e.message, HTTPStatus.BAD_REQUEST)
except ValueError as e:
# JSON encoding failure, usually due to bad data
warnings.warn(JSON_NaN_to_num_warning_msg)
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
@swagger.doc({ @swagger.doc({
"summary": "Get data (expression values) from the dataframe.", "summary": "Get data (expression values) from the dataframe.",
@@ -407,6 +432,10 @@ class DataObsAPI(Resource):
HTTPStatus.OK) HTTPStatus.OK)
except FilterError as e: except FilterError as e:
return make_response(e.message, HTTPStatus.BAD_REQUEST) return make_response(e.message, HTTPStatus.BAD_REQUEST)
except ValueError as e:
# JSON encoding failure, usually due to bad data
warnings.warn(JSON_NaN_to_num_warning_msg)
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
class DataVarAPI(Resource): class DataVarAPI(Resource):
@@ -465,6 +494,10 @@ class DataVarAPI(Resource):
return make_response((jsonify(current_app.data.data_frame(filter_, axis=Axis.VAR))), HTTPStatus.OK) return make_response((jsonify(current_app.data.data_frame(filter_, axis=Axis.VAR))), HTTPStatus.OK)
except FilterError as e: except FilterError as e:
return make_response(e.message, HTTPStatus.BAD_REQUEST) return make_response(e.message, HTTPStatus.BAD_REQUEST)
except ValueError as e:
# JSON encoding failure, usually due to bad data
warnings.warn(JSON_NaN_to_num_warning_msg)
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
@swagger.doc({ @swagger.doc({
"summary": "Get data (expression values) from the dataframe.", "summary": "Get data (expression values) from the dataframe.",
@@ -510,6 +543,10 @@ class DataVarAPI(Resource):
HTTPStatus.OK) HTTPStatus.OK)
except FilterError as e: except FilterError as e:
return make_response(e.message, HTTPStatus.BAD_REQUEST) return make_response(e.message, HTTPStatus.BAD_REQUEST)
except ValueError as e:
# JSON encoding failure, usually due to bad data
warnings.warn(JSON_NaN_to_num_warning_msg)
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
class DiffExpObsAPI(Resource): class DiffExpObsAPI(Resource):
@@ -614,7 +651,12 @@ class DiffExpObsAPI(Resource):
return make_response(e.message, HTTPStatus.BAD_REQUEST) return make_response(e.message, HTTPStatus.BAD_REQUEST)
except InteractiveError: except InteractiveError:
return make_response("Non-interactive request", HTTPStatus.FORBIDDEN) return make_response("Non-interactive request", HTTPStatus.FORBIDDEN)
return make_response(jsonify(diffexp), HTTPStatus.OK) try:
return make_response(jsonify(diffexp), HTTPStatus.OK)
except ValueError as e:
# JSON encoding failure, usually due to bad data
warnings.warn(JSON_NaN_to_num_warning_msg)
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
class LayoutObsAPI(Resource): class LayoutObsAPI(Resource):
@@ -647,7 +689,12 @@ class LayoutObsAPI(Resource):
layout = current_app.data.layout({}) layout = current_app.data.layout({})
except PrepareError as e: except PrepareError as e:
return make_response(e.message, HTTPStatus.INTERNAL_SERVER_ERROR) return make_response(e.message, HTTPStatus.INTERNAL_SERVER_ERROR)
return make_response((jsonify({"layout": layout})), HTTPStatus.OK) try:
return make_response((jsonify({"layout": layout})), HTTPStatus.OK)
except ValueError as e:
# JSON encoding failure, usually due to bad data
warnings.warn(JSON_NaN_to_num_warning_msg)
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
# @swagger.doc({ # @swagger.doc({
# "summary": "Observation layout for filtered subset.", # "summary": "Observation layout for filtered subset.",
+41 -27
View File
@@ -36,8 +36,8 @@ class ScanpyEngine(CXGDriver):
self._create_schema() self._create_schema()
# TODO: temporary work-arounds # TODO: temporary work-arounds
self._IEEE754_X_warning_issued = False if args['nan_to_num']:
self._IEEE754_special_values_workaround_annotations() self._IEEE754_special_values_workaround()
def _alias_annotation_names(self, axis, name): def _alias_annotation_names(self, axis, name):
""" """
@@ -176,7 +176,7 @@ class ScanpyEngine(CXGDriver):
f"`cellxgene prepare --layout {self.layout_method} <datafile>` " f"`cellxgene prepare --layout {self.layout_method} <datafile>` "
f"to solve this problem. ") f"to solve this problem. ")
def _IEEE754_special_values_workaround_annotations(self): def _IEEE754_special_values_workaround(self):
""" """
TODO: temporary workaround TODO: temporary workaround
@@ -187,40 +187,55 @@ class ScanpyEngine(CXGDriver):
This will likely be removed in the future, contingent upon improved marshalling. This will likely be removed in the future, contingent upon improved marshalling.
Where non-finite floating point is present in obs, var or X: Where non-finite floating point is present in obs, var or X:
* issue a warning to the user that these values will be treated as zeros. * issue a warning to the user that these values will be convert to finite numbers.
* set the value to zero within the in-memory data (self.data) * set NaN to zero, and Infinities to min/max of the element.
""" """
# annotations
for ax in Axis: for ax in Axis:
curr_axis = getattr(self.data, str(ax)) curr_axis = getattr(self.data, str(ax))
for ann in curr_axis: for ann in curr_axis:
dtype = curr_axis[ann].dtype dtype = curr_axis[ann].dtype
if dtype.kind == 'f': if dtype.kind == 'f':
not_finite = np.isfinite(curr_axis[ann]) == False # noqa: E712 finite_idx = np.isfinite(curr_axis[ann])
if np.count_nonzero(not_finite) > 0: if not finite_idx.all():
curr_axis.loc[np.isnan(curr_axis[ann]), ann] = 0
curr_axis.loc[np.isneginf(curr_axis[ann]), ann] = curr_axis[ann][finite_idx].min()
curr_axis.loc[np.isposinf(curr_axis[ann]), ann] = curr_axis[ann][finite_idx].max()
warnings.warn( warnings.warn(
f"{str(ax).title()} annotation '{ann}' contains floating point NaN or Infinities. " f"{str(ax).title()} annotation '{ann}' contains floating point NaN or Infinities. "
f"These values will be treated as zero." f"These will be converted to finite values."
) )
curr_axis[ann][not_finite] = 0
def _IEEE754_special_values_workaround_X(self, _X): # X
""" non_finite_X_found = False
TODO: temporary workaround if sparse.issparse(self.data._X):
coo = self.data._X.tocoo()
finite_idx = np.isfinite(coo.data)
if not finite_idx.all():
non_finite_X_found = True
coo.data[np.isnan(coo.data)] = 0
coo.data[np.isneginf(coo.data)] = np.min(coo.data[finite_idx])
coo.data[np.isposinf(coo.data)] = np.max(coo.data[finite_idx])
coo.eliminate_zeros()
_X = coo.asformat(self.data._X.getformat())
self.data._X = _X
else:
_X = self.data._X
finite_idx = np.isfinite(_X.flat)
if not finite_idx.all():
non_finite_X_found = True
min_X = _X.flat[finite_idx].min()
max_X = _X.flat[finite_idx].max()
_X[np.isnan(_X)] = 0
_X[np.isneginf(_X)] = min_X
_X[np.isposinf(_X)] = max_X
See comments in _IEEE754_special_values_workaround_annotations if non_finite_X_found:
""" warnings.warn(
not_finite = np.isfinite(_X) == False # noqa: E712 "Dataframe X contains floating point NaN or Infinities. "
if np.count_nonzero(not_finite) > 0: "These will be converted to finite values."
_X[not_finite] = 0 )
if not self._IEEE754_X_warning_issued:
# only want to issue this warning once.
warnings.warn(
"Dataframe X contains floating point NaN or Infinities. "
"These values will be treated as zero."
)
self._IEEE754_X_warning_issued = True
return _X
def filter_dataframe(self, filter): def filter_dataframe(self, filter):
""" """
@@ -373,7 +388,6 @@ class ScanpyEngine(CXGDriver):
_X = _X.toarray() _X = _X.toarray()
var_index_sliced = self.data.var.index[var_selector] var_index_sliced = self.data.var.index[var_selector]
obs_index_sliced = self.data.obs.index[obs_selector] obs_index_sliced = self.data.obs.index[obs_selector]
_X = self._IEEE754_special_values_workaround_X(_X)
if axis == Axis.OBS: if axis == Axis.OBS:
result = { result = {
"var": var_index_sliced.tolist(), "var": var_index_sliced.tolist(),
+3
View File
@@ -25,3 +25,6 @@ class Axis(AugmentedEnum):
class DiffExpMode(AugmentedEnum): class DiffExpMode(AugmentedEnum):
TOP_N = "topN" TOP_N = "topN"
VAR_FILTER = "varFilter" VAR_FILTER = "varFilter"
JSON_NaN_to_num_warning_msg = "JSON encoding failure - suggest trying --nan-to-num command line option"
+11
View File
@@ -7,6 +7,17 @@ from server.app.util.errors import MimeTypeError
class Float32JSONEncoder(json.JSONEncoder): class Float32JSONEncoder(json.JSONEncoder):
def __init__(self, *args, **kwargs):
"""
NaN/Infinities are illegal in standard JSON. Python extends JSON with
non-standard symbols that most JavaScript JSON parsers do not understand.
The `allow_nan` parameter will force Python simplejson to throw an ValueError
if it runs into non-finite floating point values which are unsupported by
standard JSON.
"""
kwargs['allow_nan'] = False
super().__init__(*args, **kwargs)
def default(self, obj): def default(self, obj):
if isinstance(obj, float32): if isinstance(obj, float32):
return float(obj) return float(obj)
+6 -2
View File
@@ -29,8 +29,11 @@ from server.app.util.errors import ScanpyFileError
help="Limits the number of categorical annotation items displayed.") help="Limits the number of categorical annotation items displayed.")
@click.option("--diffexp-lfc-cutoff", default=0.01, show_default=True, @click.option("--diffexp-lfc-cutoff", default=0.01, show_default=True,
help="Relative expression cutoff used when selecting top N differentially expressed genes") help="Relative expression cutoff used when selecting top N differentially expressed genes")
@click.option("--nan-to-num", is_flag=True, default=False, show_default=True,
help="Replace all floating point NaN with zero, and infinities with finite numbers")
def launch(data, layout, diffexp, title, verbose, debug, obs_names, var_names, def launch(data, layout, diffexp, title, verbose, debug, obs_names, var_names,
open_browser, port, host, max_category_items, diffexp_lfc_cutoff): open_browser, port, host, max_category_items, diffexp_lfc_cutoff,
nan_to_num):
"""Launch the cellxgene data viewer. """Launch the cellxgene data viewer.
This web app lets you explore single-cell expression data. This web app lets you explore single-cell expression data.
Data must be in a format that cellxgene expects, read the Data must be in a format that cellxgene expects, read the
@@ -91,7 +94,8 @@ def launch(data, layout, diffexp, title, verbose, debug, obs_names, var_names,
"max_category_items": max_category_items, "max_category_items": max_category_items,
"diffexp_lfc_cutoff": diffexp_lfc_cutoff, "diffexp_lfc_cutoff": diffexp_lfc_cutoff,
"obs_names": obs_names, "obs_names": obs_names,
"var_names": var_names "var_names": var_names,
"nan_to_num": nan_to_num
} }
try: try:
+2 -1
View File
@@ -14,7 +14,8 @@ from server.app.scanpy_engine.scanpy_engine import ScanpyEngine
class UtilTest(unittest.TestCase): class UtilTest(unittest.TestCase):
def setUp(self): def setUp(self):
args = {'layout': 'umap', 'diffexp': 'ttest', 'max_category_items': 100, args = {'layout': 'umap', 'diffexp': 'ttest', 'max_category_items': 100,
'obs_names': None, 'var_names': None, 'diffexp_lfc_cutoff': 0.01} 'obs_names': None, 'var_names': None, 'diffexp_lfc_cutoff': 0.01,
'nan_to_num': True}
self.data = ScanpyEngine("example-dataset/pbmc3k.h5ad", args) self.data = ScanpyEngine("example-dataset/pbmc3k.h5ad", args)
self.data._create_schema() self.data._create_schema()