diff --git a/docs/source/_static/focal.html b/docs/source/_static/focal.html new file mode 100644 index 00000000..a27f3a75 --- /dev/null +++ b/docs/source/_static/focal.html @@ -0,0 +1,221 @@ + + + + + + Growth rates + + + + + + + + + +
+
+

+ Focal interactions for Akkermansia muciniphila +

+

+ Summarized interactions +

+ +

+ Each point denotes the total flux between the focal taxon and one interaction + partner in one sample. The interaction classes (provided, received, co-consumed) are always + in reference to the focal taxon.
+ Click a point to select points from only that interaction partner. Click on an + empty region of the plot to reset the selection.
+ Hover over each point to see its annotations. Use the "..." menu in the + upper right hand side to export the plot into various formats. +

+
+ +
+
+ Download as CSV... +


+
+

+ Metabolic interactions +

+ +

+ Each point denotes the flux of a single metabolite between the reference (focal) + taxon and another interaction partner in one sample. The interaction classes + (provided, received, co-consumed) are always in reference to the focal taxon.
+ Click a point to select points from only that interaction. Click on an + empty region of the plot to reset the selection.
+ Hover over each point to see its annotations. Use the "..." menu in the + upper right hand side to export the plot into various formats. +

+
+ +
+
+ Download as CSV... +
+ +
+ + \ No newline at end of file diff --git a/docs/source/_static/focal.png b/docs/source/_static/focal.png new file mode 100644 index 00000000..1f7cf344 Binary files /dev/null and b/docs/source/_static/focal.png differ diff --git a/docs/source/_static/interactions.png b/docs/source/_static/interactions.png new file mode 100644 index 00000000..709ac188 Binary files /dev/null and b/docs/source/_static/interactions.png differ diff --git a/docs/source/_static/mes.html b/docs/source/_static/mes.html new file mode 100644 index 00000000..49492878 --- /dev/null +++ b/docs/source/_static/mes.html @@ -0,0 +1,182 @@ + + + + + + Metabolic Exchange Score (MES) + + + + + + + + + +
+
+

+ Metabolic Exchange Score (MES) +

+

+

+ Summarized Metabolic Exchange Score (MES) across all samples in a group.
+ Hover over each point to see its annotations. Use the "..." menu in the + upper right hand side to export the plot into various formats. +

+
+ +
+
+ Download as CSV... +
+

+
+

+ Each point denotes the Metabolic Exchange Score (MES) for a single metabolite in one sample. + Click a point to select points from only that sample. Click on an + empty region of the plot to reset the selection.
+ Hover over each point to see its annotations. Use the "..." menu in the + upper right hand side to export the plot into various formats. +

+
+ +
+
+ Download as CSV... +
+ +
+ + \ No newline at end of file diff --git a/docs/source/_static/mes.png b/docs/source/_static/mes.png new file mode 100644 index 00000000..72bde267 Binary files /dev/null and b/docs/source/_static/mes.png differ diff --git a/docs/source/index.rst b/docs/source/index.rst index 3fd7a070..ee23fadc 100755 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -47,6 +47,7 @@ Contents Installing MICOM MICOM workflows + Interactions Visualizations Writing your own workflows diff --git a/docs/source/interactions.ipynb b/docs/source/interactions.ipynb new file mode 100644 index 00000000..3883e0e7 --- /dev/null +++ b/docs/source/interactions.ipynb @@ -0,0 +1,761 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Analyzing metabolic interactions\n", + "\n", + "Whereas the default results will show you which taxon consumes and produces which metabolite it is not immediately apparent which metabolic interactions that implies. Thus, we provide some helpers to quantify and summarize metabolic interactions between taxa.\n", + "\n", + "It should be noted that MICOM provides mechanistic interactions, thus they differ quite abit from correlations. First, they are calculated on a per sample basis and can thus differ between samples as well. They are also non-symmetric and directed and thus qualify as ecological interactions. The strategy used in `grow` workflow might also affect the predicted interactions. The most conservative (least interactions) will be predicted with parsimonious FBA because it will also minimize inter-taxon fluxes. The other strategies are somewhat more permissive for inter-taxon fluxes but may also include futile cycles (which will not appear in parsimonious FBA).\n", + "\n", + "Finally all-versus-all interactions can be somewhat slow for larger data sets due to the inherent combinatorial explosion. The complexity will scale with $n_{samples} \\cdot n^2_{taxa} \\cdot n_{metabolites}$.\n", + "\n", + "## Calculating focal interactions\n", + "\n", + "Interactions are obatined from a `GrowthResults` object as obtained by the `grow` workflow. By default they are based on a single taxon of interest (called a focal taxon) for which we will calculate all metabolic interactions with all other taxa in all samples. MICOM stratifies interactions into 3 ecological types shown below.\n", + "\n", + "![MICOM interaction types](_static/interactions.png)\n", + "\n", + "Let's see what that looks like by using a larger example result." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
metabolitefocalpartnerclassfluxsample_idnamemolecular_weightC_numberN_number...kegg.compoundlipidmapsmetanetx.chemicalpubchem.compoundreactomesboseed.compoundchebismilesreaction
2115ala_L[e]s__Akkermansia_muciniphilas__Bacteroides_fragilisprovided11.238551S_SRR5935812L-alanine89.0931831...C00041NaNMNXM11057325950.0NaNSBO:0000247cpd00035CHEBI:16977NaNEX_ala_L(e)
2141lac_D[e]s__Akkermansia_muciniphilas__Bacteroides_fragilisprovided11.141526S_SRR5935812D-lactate89.0700030...C00256NaNMNXM73183561503.0NaNSBO:0000247cpd00221CHEBI:42111NaNEX_lac_D(e)
1080acald[e]s__Akkermansia_muciniphilas__Escherichia_colireceived9.277781S_SRR5935769Acetaldehyde44.0525620...C00084NaNMNXM75177.0NaNSBO:0000247cpd00071CHEBI:15343NaNEX_acald(e)
1120pro_L[e]s__Akkermansia_muciniphilas__Escherichia_colireceived9.208372S_SRR5935769L-proline115.1304651...C00148;C000763NaNMNXM114145742.0NaNSBO:0000247cpd00129CHEBI:17203NaNEX_pro_L(e)
1094etoh[e]s__Akkermansia_muciniphilas__Escherichia_colireceived8.980545S_SRR5935769Ethanol46.0684420...C00469NaNMNXM734299702.0NaNSBO:0000247cpd00363CHEBI:16236NaNEX_etoh(e)
\n", + "

5 rows × 24 columns

\n", + "
" + ], + "text/plain": [ + " metabolite focal partner \\\n", + "2115 ala_L[e] s__Akkermansia_muciniphila s__Bacteroides_fragilis \n", + "2141 lac_D[e] s__Akkermansia_muciniphila s__Bacteroides_fragilis \n", + "1080 acald[e] s__Akkermansia_muciniphila s__Escherichia_coli \n", + "1120 pro_L[e] s__Akkermansia_muciniphila s__Escherichia_coli \n", + "1094 etoh[e] s__Akkermansia_muciniphila s__Escherichia_coli \n", + "\n", + " class flux sample_id name molecular_weight \\\n", + "2115 provided 11.238551 S_SRR5935812 L-alanine 89.09318 \n", + "2141 provided 11.141526 S_SRR5935812 D-lactate 89.07000 \n", + "1080 received 9.277781 S_SRR5935769 Acetaldehyde 44.05256 \n", + "1120 received 9.208372 S_SRR5935769 L-proline 115.13046 \n", + "1094 received 8.980545 S_SRR5935769 Ethanol 46.06844 \n", + "\n", + " C_number N_number ... kegg.compound lipidmaps metanetx.chemical \\\n", + "2115 3 1 ... C00041 NaN MNXM1105732 \n", + "2141 3 0 ... C00256 NaN MNXM731835 \n", + "1080 2 0 ... C00084 NaN MNXM75 \n", + "1120 5 1 ... C00148;C000763 NaN MNXM114 \n", + "1094 2 0 ... C00469 NaN MNXM734299 \n", + "\n", + " pubchem.compound reactome sbo seed.compound chebi \\\n", + "2115 5950.0 NaN SBO:0000247 cpd00035 CHEBI:16977 \n", + "2141 61503.0 NaN SBO:0000247 cpd00221 CHEBI:42111 \n", + "1080 177.0 NaN SBO:0000247 cpd00071 CHEBI:15343 \n", + "1120 145742.0 NaN SBO:0000247 cpd00129 CHEBI:17203 \n", + "1094 702.0 NaN SBO:0000247 cpd00363 CHEBI:16236 \n", + "\n", + " smiles reaction \n", + "2115 NaN EX_ala_L(e) \n", + "2141 NaN EX_lac_D(e) \n", + "1080 NaN EX_acald(e) \n", + "1120 NaN EX_pro_L(e) \n", + "1094 NaN EX_etoh(e) \n", + "\n", + "[5 rows x 24 columns]" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from micom.data import test_results\n", + "from micom.interaction import interactions\n", + "\n", + "results = test_results()\n", + "ints = interactions(results, taxa=\"s__Akkermansia_muciniphila\")\n", + "ints.sort_values(by=\"flux\", ascending=False).head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "So you see the individual metabolite flux between the pair of taxa and the interaction class in each single sample. This is quite detailed and can tell you a lot, but we can also summarize it to the overall fluxes between two taxa in a sample." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
sample_idfocalpartnerclassfluxmass_fluxC_fluxN_fluxn_ints
0S_SRR5935769s__Akkermansia_muciniphilas__Alistipes_finegoldiico-consumed7.3492600.53565916.3892352.6996648
1S_SRR5935769s__Akkermansia_muciniphilas__Alistipes_finegoldiiprovided3.1807560.34265313.2097272.5601376
2S_SRR5935769s__Akkermansia_muciniphilas__Alistipes_finegoldiireceived1.5136320.32105213.0306122.9671286
3S_SRR5935769s__Akkermansia_muciniphilas__Alistipes_onderdonkiico-consumed2.3294650.2595268.7345201.7381148
4S_SRR5935769s__Akkermansia_muciniphilas__Alistipes_onderdonkiiprovided0.9061430.1127545.4901180.7647954
\n", + "
" + ], + "text/plain": [ + " sample_id focal partner \\\n", + "0 S_SRR5935769 s__Akkermansia_muciniphila s__Alistipes_finegoldii \n", + "1 S_SRR5935769 s__Akkermansia_muciniphila s__Alistipes_finegoldii \n", + "2 S_SRR5935769 s__Akkermansia_muciniphila s__Alistipes_finegoldii \n", + "3 S_SRR5935769 s__Akkermansia_muciniphila s__Alistipes_onderdonkii \n", + "4 S_SRR5935769 s__Akkermansia_muciniphila s__Alistipes_onderdonkii \n", + "\n", + " class flux mass_flux C_flux N_flux n_ints \n", + "0 co-consumed 7.349260 0.535659 16.389235 2.699664 8 \n", + "1 provided 3.180756 0.342653 13.209727 2.560137 6 \n", + "2 received 1.513632 0.321052 13.030612 2.967128 6 \n", + "3 co-consumed 2.329465 0.259526 8.734520 1.738114 8 \n", + "4 provided 0.906143 0.112754 5.490118 0.764795 4 " + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from micom.interaction import summarize_interactions\n", + "\n", + "summary = summarize_interactions(ints)\n", + "summary.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This calculates the overall flux in each class for the pair of taxa. It also provides some more meaningful eestimates of flux such as the exchanged mass, carbon or nitrogen. So in this example you see that Akkermansia competes for most of the mass with Alistipes but actually receives more nitrogen from Alistipes than it competes for in that sample." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Calculating all interactions\n", + "\n", + "It's also possible to calculate all interactions, just be aware that this (1) generates a lot of data and (2) will take a while for larger data sets. You can parallelize the analysis over each taxon by providing the the `threads` argument. Simply provide `None` for the taxa to obtain all interactions." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "350423eff34b4836a8570d1365f2fd9b", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n",
+       "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "(1115688, 24)" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "full = interactions(results, taxa=None, threads=8)\n", + "full.shape" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This generates quite a lot of results, but we can again use a summary." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(117108, 9)" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "full_summary = summarize_interactions(full)\n", + "full_summary.shape" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Metabolic Exchange Score\n", + "\n", + "For a slightly more global view on exchanges we also provide calculation of the Metabolic Exchange Score (MES) by Marcelino et al., which is decribed [in detail here](https://doi.org/10.1038/s41467-023-42112-w). The MES is the geometric mean of the number of producers P and consumers C of a metabolite m in a single sample i, given by:\n", + "\n", + "$$\n", + "MES^i_m = 2\\cdot\\frac{P^i_m\\cdot C^i_m}{P^i_m + C^i_m}\n", + "$$\n", + "\n", + "This can be interpreted as a normalized number of all observed metabolic interactions (becuse $P\\cdot C) is the number of all possible directed combinations of producers and consumers. So it is a measure of cross-feeding. \n", + "\n", + "It can also be calculated very fast for large data sets, so let's go." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
metabolitesample_idMESnamemolecular_weightC_numberN_numberbigg.metabolitebiocychmdb...kegg.compoundlipidmapsmetanetx.chemicalpubchem.compoundreactomesboseed.compoundchebismilesreaction
012dhchol[e]S_SRR59357694.80000012-Dehydrocholate405.5475824012dhcholNaNNaN...NaNNaNNaNNaNNaNSBO:0000247NaNNaNNaNEX_12dhchol(e)
112dhchol[e]S_SRR59358124.80000012-Dehydrocholate405.5475824012dhcholNaNNaN...NaNNaNNaNNaNNaNSBO:0000247NaNNaNNaNEX_12dhchol(e)
212dhchol[e]S_SRR59358163.42857112-Dehydrocholate405.5475824012dhcholNaNNaN...NaNNaNNaNNaNNaNSBO:0000247NaNNaNNaNEX_12dhchol(e)
312dhchol[e]S_SRR59358434.44444412-Dehydrocholate405.5475824012dhcholNaNNaN...NaNNaNNaNNaNNaNSBO:0000247NaNNaNNaNEX_12dhchol(e)
412dhchol[e]S_SRR59359244.44444412-Dehydrocholate405.5475824012dhcholNaNNaN...NaNNaNNaNNaNNaNSBO:0000247NaNNaNNaNEX_12dhchol(e)
\n", + "

5 rows × 21 columns

\n", + "
" + ], + "text/plain": [ + " metabolite sample_id MES name molecular_weight \\\n", + "0 12dhchol[e] S_SRR5935769 4.800000 12-Dehydrocholate 405.54758 \n", + "1 12dhchol[e] S_SRR5935812 4.800000 12-Dehydrocholate 405.54758 \n", + "2 12dhchol[e] S_SRR5935816 3.428571 12-Dehydrocholate 405.54758 \n", + "3 12dhchol[e] S_SRR5935843 4.444444 12-Dehydrocholate 405.54758 \n", + "4 12dhchol[e] S_SRR5935924 4.444444 12-Dehydrocholate 405.54758 \n", + "\n", + " C_number N_number bigg.metabolite biocyc hmdb ... kegg.compound \\\n", + "0 24 0 12dhchol NaN NaN ... NaN \n", + "1 24 0 12dhchol NaN NaN ... NaN \n", + "2 24 0 12dhchol NaN NaN ... NaN \n", + "3 24 0 12dhchol NaN NaN ... NaN \n", + "4 24 0 12dhchol NaN NaN ... NaN \n", + "\n", + " lipidmaps metanetx.chemical pubchem.compound reactome sbo \\\n", + "0 NaN NaN NaN NaN SBO:0000247 \n", + "1 NaN NaN NaN NaN SBO:0000247 \n", + "2 NaN NaN NaN NaN SBO:0000247 \n", + "3 NaN NaN NaN NaN SBO:0000247 \n", + "4 NaN NaN NaN NaN SBO:0000247 \n", + "\n", + " seed.compound chebi smiles reaction \n", + "0 NaN NaN NaN EX_12dhchol(e) \n", + "1 NaN NaN NaN EX_12dhchol(e) \n", + "2 NaN NaN NaN EX_12dhchol(e) \n", + "3 NaN NaN NaN EX_12dhchol(e) \n", + "4 NaN NaN NaN EX_12dhchol(e) \n", + "\n", + "[5 rows x 21 columns]" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from micom.interaction import MES\n", + "\n", + "scores = MES(results)\n", + "scores.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "All of the computed interaction measures also have [matching visualizations](viz.html)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "micom", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.8" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/source/viz.ipynb b/docs/source/viz.ipynb index 63e18910..d7eff280 100644 --- a/docs/source/viz.ipynb +++ b/docs/source/viz.ipynb @@ -58,7 +58,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 3, @@ -174,11 +174,11 @@ { "data": { "text/html": [ - "
[13:02:03] WARNING  Not enough samples. Adjusting T-SNE perplexity to 5.                           exchanges.py:127\n",
+       "
[12:54:10] WARNING  Not enough samples. Adjusting T-SNE perplexity to 5.                           exchanges.py:127\n",
        "
\n" ], "text/plain": [ - "\u001b[2;36m[13:02:03]\u001b[0m\u001b[2;36m \u001b[0m\u001b[31mWARNING \u001b[0m Not enough samples. Adjusting T-SNE perplexity to \u001b[1;36m5\u001b[0m. \u001b]8;id=5441;file:///home/cdiener/code/micom/micom/viz/exchanges.py\u001b\\\u001b[2mexchanges.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=120284;file:///home/cdiener/code/micom/micom/viz/exchanges.py#127\u001b\\\u001b[2m127\u001b[0m\u001b]8;;\u001b\\\n" + "\u001b[2;36m[12:54:10]\u001b[0m\u001b[2;36m \u001b[0m\u001b[31mWARNING \u001b[0m Not enough samples. Adjusting T-SNE perplexity to \u001b[1;36m5\u001b[0m. \u001b]8;id=930319;file:///home/cdiener/code/micom/micom/viz/exchanges.py\u001b\\\u001b[2mexchanges.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=152218;file:///home/cdiener/code/micom/micom/viz/exchanges.py#127\u001b\\\u001b[2m127\u001b[0m\u001b]8;;\u001b\\\n" ] }, "metadata": {}, @@ -222,7 +222,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 8, "metadata": {}, "outputs": [], "source": [ @@ -251,7 +251,7 @@ "source": [ "The output will look something like the following.\n", "\n", - "[![fit](_static/association.png)](_static/association.html)" + "[![image of the associations](_static/association.png)](_static/association.html)" ] }, { @@ -261,11 +261,78 @@ "So we see we recovered propionate in the analysis but we would need larger sample sizes here." ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Plotting interactions\n", + "\n", + "We provide support to plot focal interactions for a taxon of interest or the Metabolic Exchange Score (MES). For instance, let's start by plotting the interactions for Akkermansia." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "from micom.viz import plot_focal_interactions\n", + "\n", + "pl = plot_focal_interactions(results, taxon=\"s__Akkermansia_muciniphila\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This will give you a larger overview that looks like this:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "[![focal examples](_static/focal.png)](_static/focal.html)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Alternatively you can also visualize the Metabolic Exchange Scores. This can be done across different groups as well. To illustrate this let's do this with some random groups." + ] + }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "metadata": {}, "outputs": [], + "source": [ + "from micom.viz import plot_mes\n", + "\n", + "groups = pd.Series(\n", + " 5 * [\"a\"] + 5 * [\"b\"],\n", + " index=results.growth_rates.sample_id.unique(),\n", + " name=\"random\"\n", + ")\n", + "\n", + "pl = plot_mes(results, groups=groups, filename=\"mes.html\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here you see the MES for each metabolite and also a global overview across the groups.\n", + "\n", + "\n", + "[![MES example](_static/mes.png)](_static/mes.html)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, "source": [] } ], diff --git a/micom/__init__.py b/micom/__init__.py index 7b47b54a..97e00000 100755 --- a/micom/__init__.py +++ b/micom/__init__.py @@ -14,6 +14,7 @@ qiime_formats, solution, workflows, + interaction, ) diff --git a/micom/community.py b/micom/community.py index 7a17f161..be6f9749 100755 --- a/micom/community.py +++ b/micom/community.py @@ -223,7 +223,7 @@ def __init__( ) logger.info( "Matched %g%% of total abundance in model DB." - % (100.0 * self.__db_metrics.iloc[3]) + % (100.0 * self.__db_metrics["found_abundance_fraction"]) ) if self.__db_metrics["found_abundance_fraction"] < 0.5: logger.warning( diff --git a/micom/data/artifacts/species_models.qza b/micom/data/artifacts/species_models.qza index a8a766ae..9f83ba5c 100644 Binary files a/micom/data/artifacts/species_models.qza and b/micom/data/artifacts/species_models.qza differ diff --git a/micom/data/templates/focal_interactions.html b/micom/data/templates/focal_interactions.html new file mode 100644 index 00000000..9e85096e --- /dev/null +++ b/micom/data/templates/focal_interactions.html @@ -0,0 +1,200 @@ + + + + + + Growth rates + + + + + + + + + +
+
+

+ Focal interactions for {{ taxon }} +

+

+ Summarized interactions +

+ +

+ Each point denotes the total flux between the focal taxon and one interaction + partner in one sample. The interaction classes (provided, received, co-consumed) are always + in reference to the focal taxon.
+ Click a point to select points from only that interaction partner. Click on an + empty region of the plot to reset the selection.
+ Hover over each point to see its annotations. Use the "..." menu in the + upper right hand side to export the plot into various formats. +

+
+ +

+
+

+ Metabolic interactions +

+ +

+ Each point denotes the flux of a single metabolite between the reference (focal) + taxon and another interaction partner in one sample. The interaction classes + (provided, received, co-consumed) are always in reference to the focal taxon.
+ Click a point to select points from only that interaction. Click on an + empty region of the plot to reset the selection.
+ Hover over each point to see its annotations. Use the "..." menu in the + upper right hand side to export the plot into various formats. +

+
+ + + +
+ + diff --git a/micom/data/templates/scores.html b/micom/data/templates/scores.html new file mode 100644 index 00000000..2d91adc0 --- /dev/null +++ b/micom/data/templates/scores.html @@ -0,0 +1,161 @@ + + + + + + {{name}} + + + + + + + + + +
+
+

+ {{ name }} +

+

+

+ Summarized {{name}} across all samples in a group.
+ Hover over each point to see its annotations. Use the "..." menu in the + upper right hand side to export the plot into various formats. +

+
+ + +

+
+

+ Each point denotes the {{ name }} for a single metabolite in one sample. + Click a point to select points from only that sample. Click on an + empty region of the plot to reset the selection.
+ Hover over each point to see its annotations. Use the "..." menu in the + upper right hand side to export the plot into various formats. +

+
+ + + +
+ + diff --git a/micom/interaction/__init__.py b/micom/interaction/__init__.py new file mode 100644 index 00000000..823ae12f --- /dev/null +++ b/micom/interaction/__init__.py @@ -0,0 +1,9 @@ +from .focal import interactions +from .scores import MES +from .summary import summarize_interactions + +__all__ = ( + "interactions", + "summarize_interactions", + "MES", +) \ No newline at end of file diff --git a/micom/interaction/focal.py b/micom/interaction/focal.py new file mode 100644 index 00000000..816f13bb --- /dev/null +++ b/micom/interaction/focal.py @@ -0,0 +1,117 @@ +"""Quantify metabolic interactions between taxa.""" + +from collections import Counter +from micom.workflows import GrowthResults, workflow +import pandas as pd +from typing import List, Union + + +def _metabolite_interaction( + fluxes: pd.DataFrame, taxon: str, partner: str +) -> pd.DataFrame: + """Checks if and how taxa interact.""" + tol = fluxes.tolerance.max() + f = fluxes[(fluxes.flux.abs() * fluxes.abundance) > tol] + if (f.shape[0] < 2) or (f.direction == "export").all(): + return None + if (f.direction == "import").sum() == 2: + int_type = "co-consumed" + elif (f.loc[f.taxon == taxon, "direction"] == "export").all(): + int_type = "provided" + else: + int_type = "received" + + return pd.DataFrame( + { + "focal": taxon, + "partner": partner, + "class": int_type, + "flux": (f.flux.abs() * f.abundance).min(), + }, + index=[0], + ) + + +def sample_interactions( + fluxes: pd.DataFrame, sample_id: str, taxon: str +) -> pd.DataFrame: + """Quantify interactions in a single sammple. + + Arguments + --------- + fluxes : pandas.DataFrame + A table of exchange fluxes. + sample_id : str + The sample id to use. + taxon : str + The focal taxon to use. + + Returns + ------- + pandas.DataFrame + The mapped interactions between the focal taxon and all other taxa. + """ + ex = fluxes[fluxes.sample_id == sample_id] + partners = pd.Series(ex.taxon.unique()) + partners = partners[(partners != taxon) & (partners != "medium")] + ints = [] + for p in partners: + fluxes = ex[ex.taxon.isin((taxon, p))] + ints.append( + fluxes.groupby("metabolite") + .apply(lambda df: _metabolite_interaction(df, taxon, p)) + .reset_index() + ) + ints = pd.concat([i for i in ints if i is not None]) + ints["sample_id"] = sample_id + return ints + + +def _interact(args: List) -> pd.DataFrame: + """Quantify interactions of a focal taxon with other taxa.""" + results, taxon = args + ex = results.exchanges[results.exchanges.taxon != "medium"] + + ints = ( + ex.groupby("sample_id") + .apply(lambda df: sample_interactions(df, df.name, taxon)) + .reset_index(drop=True) + .drop(["level_1", "index"], axis=1, errors="ignore") + .merge(results.annotations, on="metabolite") + ) + + return ints + + +def interactions( + results: GrowthResults, + taxa: Union[None, str, List[str]], + threads: int = 1, + progress: bool = True, +) -> pd.DataFrame: + """Quantify interactions of a focal/reference taxon with other taxa. + + Arguments + --------- + results : GrowthResults + The growth results to use. + taxa : str, list of str, or None + The focal taxa to use. Can be a single taxon, a list of taxa or None in which + case all taxa are considered. + + Returns + ------- + pandas.DataFrame + The mapped interactions between the focal taxon and all other taxa. + """ + if isinstance(taxa, str): + return _interact([results, taxa]) + elif taxa is None: + taxa = results.growth_rates.taxon.unique() + + ints = pd.concat( + workflow( + _interact, [[results, t] for t in taxa], threads=threads, progress=progress + ) + ) + return ints diff --git a/micom/interaction/scores.py b/micom/interaction/scores.py new file mode 100644 index 00000000..6379fca8 --- /dev/null +++ b/micom/interaction/scores.py @@ -0,0 +1,52 @@ +"""Various interaction scores.""" + +from collections import Counter +import pandas as pd +from micom.workflows import GrowthResults + +def _mes(df : pd.DataFrame) -> float: + """Helper to calculate the MES score.""" + cn = Counter(df.direction) + p, c = cn["export"], cn["import"] + return pd.Series(2.0 * p * c / (p + c), index=["MES"]) + +def MES(results: GrowthResults, cutoff : float = None) -> pd.DataFrame: + """Calculate the Metabolic Exchange Score (MES) for each metabolite. + + MES is the harmonic mean of producers and consumers for a chosen metabolite + in one sample. High values indicate a large prevalence of cross-feeding for the + particular metabolite. A value of zero indicates an absence of cross-feeding for + the particular metabolite. + + Arguments + --------- + results : GrowthResults + The growth results to use. + cutoff : float + The smallest flux to consider in the analysis. Will default to the + solver tolerance if set to None. + + Returns + ------- + pandas.DataFrame + The scores for each metabolite and each sample including metabolite annotations. + + References + ---------- + .. [1] Marcelino, V.R., et al. + Disease-specific loss of microbial cross-feeding interactions in the human gut + Nat Commun 14, 6546 (2023). https://doi.org/10.1038/s41467-023-42112-w + """ + if cutoff is None: + cutoff = results.exchanges.tolerance[0] + fluxes = results.exchanges[ + (results.exchanges.flux.abs() > cutoff) & + (results.exchanges.taxon != "medium") + ] + mes = fluxes.groupby(["metabolite", "sample_id"]).apply(_mes).reset_index() + mes = mes.merge( + results.annotations.drop_duplicates(subset=["metabolite"]), + on="metabolite", + how="inner" + ) + return mes \ No newline at end of file diff --git a/micom/interaction/summary.py b/micom/interaction/summary.py new file mode 100644 index 00000000..6b38e803 --- /dev/null +++ b/micom/interaction/summary.py @@ -0,0 +1,41 @@ +"""Functions to summarize interactions over all metabolites.""" + +import pandas as pd + +def _summarize(ints: pd.DataFrame) -> pd.DataFrame: + """Summarize the overall interactions.""" + return ints.groupby("class").apply( + lambda df: pd.DataFrame( + { + "flux": df.flux.sum(), + "mass_flux": (df.flux * df.molecular_weight).sum() * 1e-3, + "C_flux": (df.flux * df.C_number).sum(), + "N_flux": (df.flux * df.N_number).sum(), + "n_ints": df.metabolite.count(), + }, + index=[0], + ) + ) + + +def summarize_interactions(ints: pd.DataFrame) -> pd.DataFrame: + """Summarize interactions to key quantities. + + Arguments + --------- + ints : pandas.DataFrame + The interactions for individual metabolites calculated before. + + Returns + ------- + pandas.DataFrame + The summarized interactions contaning the total flux, mass flux, carbon flux, + nitrogen flux and number of interactions between any pair of taxa in that + sample. + """ + return ( + ints.groupby(["sample_id", "focal", "partner"]) + .apply(_summarize) + .reset_index() + .drop("level_4", axis=1) + ) \ No newline at end of file diff --git a/micom/viz/__init__.py b/micom/viz/__init__.py index c1e2bb65..5ace5966 100644 --- a/micom/viz/__init__.py +++ b/micom/viz/__init__.py @@ -3,6 +3,7 @@ from .core import Visualization from .exchanges import plot_exchanges_per_sample, plot_exchanges_per_taxon from .growth import plot_growth +from .interactions import plot_focal_interactions, plot_mes from .association import plot_association from .tradeoff import plot_tradeoff @@ -10,6 +11,8 @@ "plot_exchanges_per_sample", "plot_exchanges_per_taxon", "plot_growth", + "plot_focal_interactions", + "plot_mes", "plot_association", "plot_tradeoff", "Visualization", diff --git a/micom/viz/association.py b/micom/viz/association.py index e0664d74..bb7de930 100644 --- a/micom/viz/association.py +++ b/micom/viz/association.py @@ -75,8 +75,6 @@ def plot_association( """ exchanges = results.exchanges - anns = results.annotations.drop_duplicates(subset=["metabolite"]) - anns.index = anns.metabolite if flux_type == "import": exchanges = consumption_rates(results) else: diff --git a/micom/viz/exchanges.py b/micom/viz/exchanges.py index fd7df884..c4097612 100644 --- a/micom/viz/exchanges.py +++ b/micom/viz/exchanges.py @@ -34,7 +34,7 @@ def plot_exchanges_per_sample( A MICOM visualization. Can be served with `viz.serve`. """ exchanges = results.exchanges - anns = results.annotations + anns = results.annotations.copy().drop_duplicates(subset="metabolite") anns.index = anns.metabolite tol = exchanges.tolerance.iloc[0] if direction not in ["import", "export"]: diff --git a/micom/viz/interactions.py b/micom/viz/interactions.py new file mode 100644 index 00000000..43a826c2 --- /dev/null +++ b/micom/viz/interactions.py @@ -0,0 +1,141 @@ +"""Visualizations for interactions.""" + +from datetime import datetime +import json +from micom.interaction import interactions, summarize_interactions, MES +from micom.logger import logger +from micom.workflows import GrowthResults +from micom.viz.core import Visualization +import pandas as pd +import re + +UNITS = { + "flux": "mmol/[gDW·h]", + "mass": "g/[gDW·h]", + "C": "C/[gDW·h]", + "N": "N/[gDW·h]" +} + + +def plot_focal_interactions( + results : GrowthResults, + taxon : str, + filename : str = "focal_interactions_%s.html" % datetime.now().strftime("%Y%m%d"), + kind : str = "mass" +) -> None: + """Plot metabolic interactions between a focal taxa and all other taxa. + + This will visualize metabolic interaction between a taxon of interest (focal taxon) + and all other taxa across all samples. + + Parameters + ---------- + results : micom.workflows.GrowthResults + The results returned by the `grow` workflow. + taxon : str + The focal taxon to use as a reference. Must be one of the taxa appearing + in `results.growth_rates.taxon`. + filename : str + The HTML file where the visualization will be saved. + kind : str + Which kind of flux to use. Either + - "flux": molar flux of a metabolite + - "mass" (default): the mass flux (flux normalized by molecular weight) + - "C": carbon flux + - "N": nitrogen flux + + Returns + ------- + Visualization + A MICOM visualization. Can be served with `viz.serve`. + """ + if not kind in UNITS: + raise ValueError( + f"Not a supported flux type. Please choose one of {','.join(UNITS)}.") + ints = interactions(results, taxon, progress=False) + n_taxa = ints.partner.nunique() + n_mets = ints.metabolite.nunique() + data = {"interactions": ints} + viz = Visualization(filename, data, "focal_interactions.html") + summ = summarize_interactions(ints) + data["summary"] = summ + summ["flux"] = summ["flux" if kind == "flux" else kind + "_flux"] + viz.save( + summary=summ.to_json(orient="records"), + interactions=data["interactions"].to_json(orient="records"), + n_taxa=n_taxa, + n_mets=n_mets, + taxon=re.sub(r"\w__", "", taxon).replace("_", " "), + unit=UNITS[kind] + ) + return viz + + +def plot_mes( + results : GrowthResults, + filename : str = "mes_%s.html" % datetime.now().strftime("%Y%m%d"), + groups : pd.Series = None, + prevalence : float = 0.5 +) -> None: + """Plot metabolic interactions between a focal taxa and all other taxa. + + This will plot the metabolic exchange score across samples and metabolites. + The metabolic exchange score (MES) is defined as the geometric mean of the + number of producers and consumers for a given metabolite in a sample. + + $$ + MES = 2\cdot\frac{|p||c|}{|p| + |c|} + $$ + + Parameters + ---------- + results : micom.workflows.GrowthResults + The results returned by the `grow` workflow. + filename : str + The HTML file where the visualization will be saved. + groups : pandas.Series + Additional metadata to stratify MES score. The index must correspond to the + `sample_id` in the results and values must be categorical. The `.name` attribute + will be used to name the groups. + prevalence : float in [0, 1] + In what proportion of samples the metabolite has to have a non-zero MES to + be shown on the plots. Can be used to show only very commonly exchanged + metabolites. + + Notes + ----- + The CSV files will always include all MES scores independent of the prevalence + filter which is only used for visualization. + + Returns + ------- + Visualization + A MICOM visualization. Can be served with `viz.serve`. + """ + tol = results.exchanges.tolerance.max() + raw = MES(results, tol) + scores = raw[raw.MES > 0] + prev = scores.metabolite.value_counts() / scores.sample_id.nunique() + prev = prev[prev > prevalence].index + scores = scores[scores.metabolite.isin(prev)] + + if groups is not None: + if groups.dtype not in ["object", "category", "bool"]: + raise ValueError("Groups need to be categorical.") + name = "group" if groups.name is None else groups.name + scores[name] = groups[scores.sample_id].values + else: + scores["group"] = "all" + name = "group" + n_mets = scores.metabolite.nunique() + data = {"scores": raw} + viz = Visualization(filename, data, "scores.html") + viz.save( + scores=scores.to_json(orient="records"), + n_mets=n_mets, + name="Metabolic Exchange Score (MES)", + col_name="MES", + cat=name + ) + return viz + diff --git a/tests/fixtures.py b/tests/fixtures.py index 0e65bf21..40eb6499 100755 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -15,6 +15,12 @@ def community(): """A simple community containing 4 species.""" return micom.Community(micom.data.test_taxonomy(), progress=False) +@pytest.fixture +def results(): + """A more complex results example.""" + res = md.test_results() + return res + @pytest.fixture def linear_community(): @@ -23,7 +29,6 @@ def linear_community(): solver="glpk") - def check_viz(v): """Check a visualization.""" for d in v.data: diff --git a/tests/test_db.py b/tests/test_db.py index a7f460b7..0a2fd4e2 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -16,12 +16,12 @@ def test_qiime_community(): tax["abundance"] = [1, 2, 3, 4] del tax["file"] com = mm.Community(tax, db, progress=False) - assert len(com.abundances) == 3 + assert len(com.abundances) == 4 m = com.build_metrics - assert m[0] == 3 + assert m[0] == 4 assert m[1] == 4 - assert m[2] == approx(0.75) - assert m[3] == approx(0.6) + assert m[2] == approx(1.0) + assert m[3] == approx(1.0) @mark.parametrize("rank", ["genus", "species"]) diff --git a/tests/data/test_db_media.py b/tests/test_db_media.py similarity index 100% rename from tests/data/test_db_media.py rename to tests/test_db_media.py diff --git a/tests/test_high_level.py b/tests/test_high_level.py index b825c1ed..a332c833 100644 --- a/tests/test_high_level.py +++ b/tests/test_high_level.py @@ -24,7 +24,7 @@ def test_db(tmp_path): tax = md.test_taxonomy() manifest.file = tax.file[0] built = build_database(manifest, str(tmp_path), rank="species") - assert built.shape[0] == 3 + assert built.shape[0] == 4 for fi in built.file: assert (tmp_path / fi).exists() built = build_database(manifest, str(tmp_path / "db.zip")) diff --git a/tests/test_interaction.py b/tests/test_interaction.py new file mode 100644 index 00000000..9a126e3a --- /dev/null +++ b/tests/test_interaction.py @@ -0,0 +1,41 @@ +"""Test the interaction module.""" + +from .fixtures import results, growth_data +import micom.interaction as mi +import pytest + +def test_focal(results): + """Test single focal taxon.""" + ints = mi.interactions(results, taxa="s__Akkermansia_muciniphila", progress=False) + assert all(ints.focal == "s__Akkermansia_muciniphila") + assert ints.partner.nunique() > 10 + assert all(ints.flux > 0) + +def test_summary(results): + """Test the the results summary.""" + ints = mi.interactions(results, taxa="s__Akkermansia_muciniphila", progress=False) + summ = mi.summarize_interactions(ints) + for col in ["mass_flux", "flux", "C_flux", "N_flux", "n_ints"]: + assert col in summ.columns + for cl in ["provided", "received", "co-consumed"]: + assert cl in summ["class"].unique() + assert all(summ.groupby(["sample_id", "focal", "partner"]).flux.count() <= 3) + +def test_all_interactions(growth_data): + """Test all vs all.""" + ints = mi.interactions(growth_data, taxa=None, progress=False) + summ = mi.summarize_interactions(ints) + assert ints.focal.nunique() == 3 + assert ints.partner.nunique() == 3 + assert all(ints.flux > 0) + assert summ.focal.nunique() == 3 + assert summ.partner.nunique() == 3 + for col in ["mass_flux", "flux", "C_flux", "N_flux", "n_ints"]: + assert col in summ.columns + +def test_mes(results): + """Test MES score.""" + mes = mi.MES(results) + assert "MES" in mes.columns + assert "metabolite" in mes.columns + assert all(mes.MES >= 0) diff --git a/tests/test_qiime.py b/tests/test_qiime.py index 8204bd51..21a74bc5 100644 --- a/tests/test_qiime.py +++ b/tests/test_qiime.py @@ -14,7 +14,7 @@ def test_qiime_db(tmp_path): assert "uuid" in meta assert meta["type"] == "MetabolicModels[JSON]" manifest = qf.load_qiime_model_db(db, str(tmp_path)) - assert manifest.shape[0] == 3 + assert manifest.shape[0] == 4 assert all(path.exists(f) for f in manifest.file) @mark.parametrize("arti", [db, models]) diff --git a/tests/test_viz.py b/tests/test_viz.py index 4b007968..e7273e18 100644 --- a/tests/test_viz.py +++ b/tests/test_viz.py @@ -5,7 +5,7 @@ from os import path import pandas as pd import pytest -import sys +import random def test_plot_growth(growth_data, tmp_path): @@ -55,4 +55,26 @@ def test_association(growth_data, tmp_path): with pytest.raises(ValueError): v = viz.plot_association(growth_data, meta, variable_type="dog", - filename=str(tmp_path / "viz.html")) + filename=str(tmp_path / "viz.html") + ) + + +def test_plot_focal_interactions(growth_data, tmp_path): + v = viz.plot_focal_interactions( + growth_data, + taxon="Escherichia_coli_2", + filename=str(tmp_path / "viz.html") + ) + check_viz(v) + + +def test_plot_mes(growth_data, tmp_path): + v = viz.plot_mes(growth_data, filename=str(tmp_path / "viz.html")) + check_viz(v) + groups = pd.Series( + random.choices(["a", "b"], k=4), + index=growth_data.growth_rates.sample_id.unique(), + name="random" + ) + v = viz.plot_mes(growth_data, groups=groups, filename=str(tmp_path / "viz.html")) + check_viz(v) \ No newline at end of file