diff --git a/.gitignore b/.gitignore index d3e751f..250c366 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ __pycache__ *.pyc .DS_Store docs/_build/ +docs/savefig/ .idea .vscode build/ diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..d180754 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,14 @@ +version: 2 +build: + os: ubuntu-22.04 + tools: + python: "3.9" +sphinx: + configuration: docs/conf.py + fail_on_warning: false +python: + install: + - requirements: requirements_docs.txt + - method: pip + path: . + system_packages: false diff --git a/CHANGES b/CHANGES index af4d7b0..2d40e8f 100644 --- a/CHANGES +++ b/CHANGES @@ -1,11 +1,15 @@ pint-pandas Changelog ===================== -0.5 (unreleased) +0.6 (unreleased) ---------------- -<<<<<<< HEAD - Support for uncertainties as magnitudes in PintArrays. #140 + +0.5 (2023-09-07) +---------------- + +- ReadTheDocs Documentation created. - Support for Pandas version 2.1.0. #196 - Support for dtype-preserving `PintArray.map` for both Pandas 2.0.2 and Pandas 2.1. #196 - Support for values in columns with integer magnitudes diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d4bb2cb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/_static/index_api.svg b/docs/_static/index_api.svg new file mode 100644 index 0000000..ceb5be6 --- /dev/null +++ b/docs/_static/index_api.svg @@ -0,0 +1,26 @@ + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/docs/_static/index_contribute.svg b/docs/_static/index_contribute.svg new file mode 100644 index 0000000..24a29f3 --- /dev/null +++ b/docs/_static/index_contribute.svg @@ -0,0 +1,21 @@ + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/docs/_static/index_getting_started.svg b/docs/_static/index_getting_started.svg new file mode 100644 index 0000000..2afbbe1 --- /dev/null +++ b/docs/_static/index_getting_started.svg @@ -0,0 +1,18 @@ + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/docs/_static/index_user_guide.svg b/docs/_static/index_user_guide.svg new file mode 100644 index 0000000..d70b3d7 --- /dev/null +++ b/docs/_static/index_user_guide.svg @@ -0,0 +1,18 @@ + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/docs/_static/logo-full.jpg b/docs/_static/logo-full.jpg new file mode 100644 index 0000000..e672456 Binary files /dev/null and b/docs/_static/logo-full.jpg differ diff --git a/docs/_static/style.css b/docs/_static/style.css new file mode 100644 index 0000000..b2bc297 --- /dev/null +++ b/docs/_static/style.css @@ -0,0 +1,45 @@ +@import url('https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,400;0,700;0,900;1,400;1,700;1,900&family=Open+Sans:ital,wght@0,400;0,600;1,400;1,600&display=swap'); + +body { + font-family: 'Open Sans', sans-serif; +} + +h1 { + font-family: "Lato", sans-serif; +} + +pre, code { + font-size: 100%; + line-height: 155%; +} + +/* Main page overview cards */ + +.sd-card { + border-radius: 0; + padding: 30px 10px 20px 10px; + margin: 10px 0px; +} + +.sd-card .sd-card-header { + text-align: center; +} + +.sd-card .sd-card-header .sd-card-text { + margin: 0px; +} + +.sd-card .sd-card-img-top { + height: 52px; + width: 52px; + margin-left: auto; + margin-right: auto; +} + +.sd-card .sd-card-header { + border: none; + color: #150458 !important; + font-size: var(--pst-font-size-h5); + font-weight: bold; + padding: 2.5rem 0rem 0.5rem 0rem; +} diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..b90fe14 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,90 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +import datetime +from importlib.metadata import version + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. + + +# -- Project information ----------------------------------------------------- + +project = "pint-pandas" +author = "Hernan E. Grecco" + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. + +try: # pragma: no cover + version = version(project) +except Exception: # pragma: no cover + # we seem to have a local copy not installed without setuptools + # so the reported version will be unknown + version = "unknown" + +release = version +this_year = datetime.date.today().year +copyright = f"2020-{this_year}, Pint-pandas Developers" + +exclude_patterns = ["_build"] + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.autosectionlabel", + "sphinx.ext.doctest", + "sphinx.ext.intersphinx", + "sphinx.ext.coverage", + "sphinx.ext.napoleon", + "sphinx.ext.viewcode", + "sphinx.ext.mathjax", + "matplotlib.sphinxext.plot_directive", + "nbsphinx", + "sphinx_copybutton", + "IPython.sphinxext.ipython_directive", + "IPython.sphinxext.ipython_console_highlighting", + "sphinx_design", +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = "sphinx_book_theme" + +html_theme_options = { + "repository_url": "https://github.com/hgrecco/pint-pandas", + "repository_branch": "master", + "use_repository_button": True, + "use_issues_button": True, + "extra_navbar": "", + "navbar_footer_text": "", +} +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ["_static"] +html_logo = "_static/logo-full.jpg" +html_css_files = ["style.css"] diff --git a/docs/ecosystem.rst b/docs/ecosystem.rst new file mode 100644 index 0000000..3442a7d --- /dev/null +++ b/docs/ecosystem.rst @@ -0,0 +1,12 @@ +Ecosystem +========= + +Here is a list of known projects, packages and integrations using pint. + + +Pint packages: +------------------ + +- `pint ` Base package +- `pint-pandas `_ Pandas integration +- `pint-xarray `_ Xarray integration diff --git a/docs/getting/faq.rst b/docs/getting/faq.rst new file mode 100644 index 0000000..75d7db7 --- /dev/null +++ b/docs/getting/faq.rst @@ -0,0 +1,42 @@ +.. _faq: + +Frequently asked questions +========================== + + +Why the name *Pint*? +-------------------- + +Pint is a unit and sounds like Python in the first syllable. Most important, it is a good unit for beer. + + +You mention other similar Python libraries. Can you point me to those? +---------------------------------------------------------------------- + +`natu `_ + +`Buckingham `_ + +`Magnitude `_ + +`SciMath `_ + +`Python-quantities `_ + +`Unum `_ + +`Units `_ + +`udunitspy `_ + +`SymPy `_ + +`cf units `_ + +`astropy units `_ + +`yt `_ + +`measurement `_ + +If you're aware of another one, please contribute a patch to the docs. diff --git a/docs/getting/index.rst b/docs/getting/index.rst new file mode 100644 index 0000000..da8a44a --- /dev/null +++ b/docs/getting/index.rst @@ -0,0 +1,56 @@ +Getting Started +=============== + +The getting started guide aims to get you using pint-pandas productively as quickly as possible. + + +What is Pint-pandas? +-------------------- + +The Pandas package provides powerful DataFrame and Series abstractions for dealing with numerical, temporal, categorical, string-based, and even user-defined data (using its ExtensionArray feature). The Pint package provides a rich and extensible vocabulary of units for constructing Quantities and an equally rich and extensible range of unit conversions to make it easy to perform unit-safe calculations using Quantities. Pint-pandas provides PintArray, aPandas ExtensionArray that efficiently implements Pandas DataFrame and Series functionality as unit-aware operations where appropriate. + +Those who have used Pint know well that good units discipline often catches not only simple mistakes, but sometimes more fundamental errors as well. Pint-pandas can reveal similar errors when it comes to slicing and dicing Pandas data. + + +Installation +------------ + +Pint-pandas requires pint and pandas. + +.. grid:: 2 + + .. grid-item-card:: Prefer pip? + + **pint-pandas** can be installed via pip from `PyPI `__. + + ++++++++++++++++++++++ + + .. code-block:: bash + + pip install pint-pandas + + .. grid-item-card:: Working with conda? + + **pint-pandas** is part of the `Conda-Forge `__ + channel and can be installed with Anaconda or Miniconda: + + ++++++++++++++++++++++ + + .. code-block:: bash + + conda install -c conda-forge pint-pandas + + +That's all! You can check that Pint is correctly installed by starting up python, and importing Pint: + +.. code-block:: python + + >>> import pint_pandas + >>> pint_pandas.__version__ # doctest: +SKIP + +.. toctree:: + :maxdepth: 2 + :hidden: + + tutorial + faq diff --git a/docs/getting/tutorial.rst b/docs/getting/tutorial.rst new file mode 100644 index 0000000..4307012 --- /dev/null +++ b/docs/getting/tutorial.rst @@ -0,0 +1,88 @@ +.. _tutorial: + +************************** +Tutorial +************************** + +This example will show the simplist way to use pandas with pint and the underlying objects. +It's slightly fiddly to set up units compared to reading data and units from a file. +A more typical use case is given in :doc:`Reading from csv <../user/reading>`. + + +Imports +----------------------- +First some imports + +.. ipython:: python + + import pandas as pd + import pint + import pint_pandas + + pint_pandas.show_versions() + + +Create a DataFrame +----------------------- +Next, we create a DataFrame with PintArrays as columns. + +.. ipython:: python + + df = pd.DataFrame( + { + "torque": pd.Series([1.0, 2.0, 2.0, 3.0], dtype="pint[lbf ft]"), + "angular_velocity": pd.Series([1.0, 2.0, 2.0, 3.0], dtype="pint[rpm]"), + } + ) + df + + +DataFrame Operations +----------------------- +Operations with columns are units aware so behave as we would intuitively expect. + +.. ipython:: python + + df["power"] = df["torque"] * df["angular_velocity"] + df + + +.. note:: + + Notice that the units are not displayed in the cells of the DataFrame. + If you ever see units in the cells of the DataFrame, something isn't right. + See :ref:`units_in_cells` for more information. + +We can see the columns' units in the dtypes attribute + +.. ipython:: python + + df.dtypes + +Each column can be accessed as a Pandas Series + +.. ipython:: python + + df.power + +Which contains a PintArray + +.. ipython:: python + + df.power.values + +The PintArray contains a Quantity + +.. ipython:: python + + df.power.values.quantity + +Pandas Series Accessors +----------------------- +Pandas Series accessors are provided for most Quantity properties and methods. +Methods that return arrays will be converted to Series. + +.. ipython:: python + + df.power.pint.units + df.power.pint.to("kW") diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..317dc2c --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,86 @@ +:orphan: + +Pint: makes units easy +====================== + +**Useful links**: +`Code Repository `__ | +`Issues `__ | + + + +.. grid:: 1 1 2 2 + :gutter: 2 + + .. grid-item-card:: + :img-top: _static/index_getting_started.svg + :link: getting/index + :link-type: doc + + Getting Started + ^^^^^^^^^^^^^^^ + + New to pint-pandas? Check out the getting started guides. They contain an + introduction to pint-pandas's main concepts and links to additional tutorials. + + .. grid-item-card:: + :img-top: _static/index_user_guide.svg + :link: user/index + :link-type: doc + + User Guide + ^^^^^^^^^^ + + The user guide provides in-depth information on the + key concepts of pint-pandas with useful background information and explanation. + + .. grid-item-card:: + :img-top: _static/index_api.svg + :link: api/base + :link-type: doc + + API reference + ^^^^^^^^^^^^^ + + The reference guide contains a detailed description of the pint-pandas API. + The reference describes how the methods work and which parameters can + be used. It assumes that you have an understanding of the key concepts. + + .. grid-item-card:: + :img-top: _static/index_contribute.svg + :link: dev/contributing + :link-type: doc + + Developer guide + ^^^^^^^^^^^^^^^ + + Saw a typo in the documentation? Want to improve existing functionalities? + The contributing guidelines will guide you through the process of improving + pint-pandas. + + +.. toctree:: + :maxdepth: 2 + :hidden: + :caption: For users + + Getting started + User Guide + Advanced topics + ecosystem + API Reference + +.. toctree:: + :maxdepth: 1 + :hidden: + :caption: For developers + + dev/contributing + dev/pint-convert + +.. toctree:: + :maxdepth: 1 + :hidden: + :caption: Community + + GitHub repository diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..954237b --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/user/common.rst b/docs/user/common.rst new file mode 100644 index 0000000..0256db0 --- /dev/null +++ b/docs/user/common.rst @@ -0,0 +1,66 @@ +.. _common: + +************************** +Common Issues +************************** + +Pandas support for ``ExtensionArray`` is still in development. As a result, there are some common issues that pint-pandas users may encounter. +This page provides some guidance on how to resolve these issues. + +Units in Cells (Object dtype columns) +------------------------------------- + +The most common issue pint-pandas users encouter is that they have a DataFrame with column that aren't PintArrays. +An obvious indicator is unit strings showing in cells when viewing the DataFrame. +Several pandas operations return numpy arrays of ``Quantity`` objects, which can cause this. + + +.. ipython:: python + :suppress: + :okwarning: + + import pandas as pd + import pint + import pint_pandas + + PA_ = pint_pandas.PintArray + ureg = pint_pandas.PintType.ureg + Q_ = ureg.Quantity + + df = pd.DataFrame( + { + "length": pd.Series(np.array([Q_(2.0, ureg.m), Q_(3.0, ureg.m)], dtype="object")), + } + ) + +.. ipython:: python + + df + + +To confirm the DataFrame does not contain PintArrays, check the dtypes. + +.. ipython:: python + + df.dtypes + + +Pint-pandas provides an accessor to fix this issue by converting the non PintArray columns to PintArrays. + +.. ipython:: python + + df.pint.convert_object_dtype() + + +Creating DataFrames from Series +--------------------------------- + +The default operation of Pandas `pd.concat` function is to perform row-wise concatenation. When given a list of Series, each of which is backed by a PintArray, this will inefficiently convert all the PintArrays to arrays of `object` type, concatenate the several series into a DataFrame with that many rows, and then leave it up to you to convert that DataFrame back into column-wise PintArrays. A much more efficient approach is to concatenate Series in a column-wise fashion: + +.. ipython:: python + :suppress: + :okwarning: + df = pd.concat(list_of_series, axis=1) + + +This will preserve all the PintArrays in each of the Series. diff --git a/docs/user/index.rst b/docs/user/index.rst new file mode 100644 index 0000000..ccdd4b0 --- /dev/null +++ b/docs/user/index.rst @@ -0,0 +1,14 @@ +User Guide +========== + +In this user guide, you will find detailed descriptions and +examples that describe many common tasks that you can accomplish with pint. + + +.. toctree:: + :maxdepth: 2 + :hidden: + + reading + initializing + common diff --git a/docs/user/initializing.rst b/docs/user/initializing.rst new file mode 100644 index 0000000..f50fb3a --- /dev/null +++ b/docs/user/initializing.rst @@ -0,0 +1,38 @@ +.. _initializing: + +************************** +Initializing data +************************** + +There are several ways to initialize PintArrays in a DataFrame. Here's the most common methods. We'll use `PA_` and `Q_` as shorthand for PintArray and Quantity. + + + +.. ipython:: python + + import pandas as pd + import pint + import pint_pandas + import io + + PA_ = pint_pandas.PintArray + ureg = pint_pandas.PintType.ureg + Q_ = ureg.Quantity + + df = pd.DataFrame( + { + "A": pd.Series([1.0, 2.0], dtype="pint[m]"), + "B": pd.Series([1.0, 2.0]).astype("pint[m]"), + "C": PA_([2.0, 3.0], dtype="pint[m]"), + "D": PA_([2.0, 3.0], dtype="m"), + "E": PA_([2.0, 3.0], dtype=ureg.m), + "F": PA_.from_1darray_quantity(Q_([2, 3], ureg.m)), + "G": PA_(Q_([2.0, 3.0], ureg.m)), + } + ) + df + + +.. note:: + + "pint[unit]" must be used for the Series or DataFrame constuctor. diff --git a/docs/user/reading.rst b/docs/user/reading.rst new file mode 100644 index 0000000..60758a2 --- /dev/null +++ b/docs/user/reading.rst @@ -0,0 +1,142 @@ +.. _reading: + +************************** +Reading data and plotting +************************** + +Reading from files is the far more standard way to use pandas. +To facilitate this, :py:class:`DataFrame` accessors are provided to make it easy to get to :py:class:`PintArray` objects. + + +Read data from csv +----------------------- +First some imports + +.. ipython:: python + + import pandas as pd + import pint + import pint_pandas + import io + + +Here's the contents of the csv file. + +.. ipython:: python + + test_data = """ShaftSpeedIndex,rpm,1200,1200,1200,1600,1600,1600,2300,2300,2300 + pump,,A,B,C,A,B,C,A,B,C + TestDate,No Unit,01/01,01/01,01/01,01/01,01/01,01/01,01/02,01/02,01/02 + ShaftSpeed,rpm,1200,1200,1200,1600,1600,1600,2300,2300,2300 + FlowRate,m^3 h^-1,8.72,9.28,9.31,11.61,12.78,13.51,18.32,17.90,19.23 + DifferentialPressure,kPa,162.03,144.16,136.47,286.86,241.41,204.21,533.17,526.74,440.76 + ShaftPower,kW,1.32,1.23,1.18,3.09,2.78,2.50,8.59,8.51,7.61 + Efficiency,dimensionless,30.60,31.16,30.70,30.72,31.83,31.81,32.52,31.67,32.05""" + +Let's read that into a DataFrame. Here io.StringIO is used in place of reading a file from disk, whereas a csv file path would typically be used and is shown commented. + +.. ipython:: python + + df = pd.read_csv(io.StringIO(test_data), header=[0, 1], index_col=[0, 1]).T + # df = pd.read_csv("/path/to/test_data.csv", header=[0, 1]) + for col in df.columns: + df[col] = pd.to_numeric(df[col], errors="ignore") + df.dtypes + + +Pandas DataFrame Accessors +--------------------------- +Then use the :py:class:`DataFrame`'s pint accessor's quantify method to convert the columns from :py:class:`ndarray` to :py:class:`PintArray`, with units from the bottom column level. + +Using 'No Unit' as the unit will prevent quantify converting a column to a :py:class:`PintArray`. This can be changed by changing :py:attr:`pint_pandas.pint_array.NO_UNIT`. + +.. ipython:: python + + df_ = df.pint.quantify(level=-1) + df_ + +Let's confirm the units have been parsed correctly by looking at the dtypes. + +.. ipython:: python + + df_.dtypes + +Here the Efficiency has been parsed as dimensionless. Let's change it to percent. + +.. ipython:: python + + df_["Efficiency"] = pint_pandas.PintArray( + df_["Efficiency"].values.quantity.m, dtype="pint[percent]" + ) + df_.dtypes + +As previously, operations between DataFrame columns are unit aware + +.. ipython:: python + + df_.ShaftPower / df_.ShaftSpeed + df_["ShaftTorque"] = df_.ShaftPower / df_.ShaftSpeed + df_["FluidPower"] = df_["FlowRate"] * df_["DifferentialPressure"] + df_ + + +The DataFrame's pint.dequantify method then allows us to retrieve the units information as a header row once again. + +.. ipython:: python + + df_.pint.dequantify() + +This allows for some rather powerful abilities. For example, to change a column's units + +.. ipython:: python + + df_["FluidPower"] = df_["FluidPower"].pint.to("kW") + df_["FlowRate"] = df_["FlowRate"].pint.to("L/s") + df_["ShaftTorque"] = df_["ShaftTorque"].pint.to("N m") + df_.pint.dequantify() + +The units are harder to read than they need be, so lets change pint's `default format for displaying units `_. + +.. ipython:: python + + pint_pandas.PintType.ureg.default_format = "P~" + df_.pint.dequantify() + +or the entire table's units + +.. ipython:: python + + df_.pint.to_base_units().pint.dequantify() + + +Plotting +----------------------- + +Pint's `matplotlib support `_ allows columns with the same dimensionality to be plotted. +First, set up matplotlib to use pint's units. + + +.. ipython:: python + + import matplotlib.pyplot as plt + pint_pandas.PintType.ureg.setup_matplotlib() + +Let's convert a column to a different unit and plot two columns with different units. Pint's matplotlib support will automatically convert the units to the first units and add the units to the axis labels. + +.. ipython:: python + + df_['FluidPower'] = df_['FluidPower'].pint.to('W') + df_[["ShaftPower", "FluidPower"]].dtypes + + fig, ax = plt.subplots() + + @savefig plot_simple.png + ax = df_[["ShaftPower", "FluidPower"]].unstack("pump").plot(ax=ax) + + +.. ipython:: python + + ax.yaxis.units + ax.yaxis.label + +.. TODO add index with units example diff --git a/requirements_docs.txt b/requirements_docs.txt new file mode 100644 index 0000000..8f44109 --- /dev/null +++ b/requirements_docs.txt @@ -0,0 +1,22 @@ +sphinx>4 +ipython<=8.12 +matplotlib +mip>=1.13 +nbsphinx +numpy +pytest +jupyter_client +ipykernel +ipython +graphviz +xarray +pooch +sparse +dask[complete] +setuptools>=41.2 +Serialize +pygments>=2.4 +sphinx-book-theme==0.3.3 +sphinx_copybutton +sphinx_design +typing_extensions