diff --git a/.test_steps/test_ERMA.ipynb b/.test_steps/test_ERMA.ipynb index ba31599..1140388 100644 --- a/.test_steps/test_ERMA.ipynb +++ b/.test_steps/test_ERMA.ipynb @@ -13,60 +13,7 @@ "cell_type": "code", "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "diamond v2.1.10.164 (C) Max Planck Society for the Advancement of Science, Benjamin Buchfink, University of Tuebingen\n", - "Documentation, support and updates available at http://www.diamondsearch.org\n", - "Please cite: http://dx.doi.org/10.1038/s41592-021-01101-x Nature Methods (2021)\n", - "\n", - "#CPU threads: 64\n", - "Scoring parameters: (Matrix=BLOSUM62 Lambda=0.267 K=0.041 Penalties=11/1)\n", - "Database input file: /local/work/adrian/ERMA/.github/data/card_db/protein_fasta_protein_homolog_model.fasta\n", - "Opening the database file... [0.002s]\n", - "Loading sequences... [0.006s]\n", - "Masking sequences... [0.022s]\n", - "Writing sequences... [0.001s]\n", - "Hashing sequences... [0s]\n", - "Loading sequences... [0s]\n", - "Writing trailer... [0s]\n", - "Closing the input file... [0s]\n", - "Closing the database file... [0.001s]\n", - "\n", - "Database sequences 4840\n", - " Database letters 1551614\n", - " Database hash c85c2d4da9e198c424f39f410dc52749\n", - " Total time 0.034000s\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "sample,state,total_count\n", - "fastq input reads,test4046\n", - "diamond output hits,test72633\n", - "usearch output hits,test1565\n" - ] - }, - { - "data": { - "text/markdown": [ - "### Processing Complete\n", - "- CARD hits: `72633`\n", - "- SILVA hits: `1565`" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "# 1. Step similarity search\n", "# Input: fasta files\n", @@ -133,9 +80,9 @@ "\n", "# === Summary ===\n", "print(f\"\\nsample,state,total_count\")\n", - "print(f\"fastq input reads,test,{count_lines(fasta, '^>')}\")\n", - "print(f\"diamond output hits,test,{count_lines(card_results)}\")\n", - "print(f\"usearch output hits,test,{count_lines(silva_results)}\")\n", + "print(f\"Number of FastQ input reads,{count_lines(fasta, '^>')}\")\n", + "print(f\"Diamond output hits,test,{count_lines(card_results)}\")\n", + "print(f\"Usearch output hits,test,{count_lines(silva_results)}\")\n", "\n", "# === Cleanup ===\n", "clean(card_dir, {card_tar.name})\n", @@ -148,7 +95,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, "outputs": [ { @@ -156,7 +103,7 @@ "output_type": "stream", "text": [ "./aro_index.tsv\n", - "merge output,74198\n", + "Merged similarity hits,74198\n", "\n" ] } @@ -179,7 +126,6 @@ "\n", "silva_dir = github / \"data/silva_db\"\n", "card_dir = github / \"data/card_db\"\n", - "fastq_dir = github / \"data/fastq\"\n", "result_dir = base / \".test_steps/results\"\n", "\n", "silva_res = result_dir / \"SILVA_results.txt\"\n", @@ -241,7 +187,7 @@ " # Count number of rows in the combined DataFrame\n", " count = len(combined_df)\n", "\n", - " print(f\"merge output,{count}\\n\")\n", + " print(f\"Merged similarity hits,{count}\\n\")\n", "\n", "blast_columns = [\n", " \"query_id\",\n", @@ -277,19 +223,19 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "test_epic_data,filtered min similarity ABR,-26023\n", - "test_epic_data,filtered max identity ABR,-2105\n", - "test_epic_data,filtered min similarity 16S,-2\n", - "test_epic_data,filtered max identity 16S,-0\n", - "test_epic_data,filtered query id mismatch,-27561\n", - "test_epic_data,filtration output,18507\n" + "test_epic_data,Diamond hits < similarity threshold,-26023\n", + "test_epic_data,Diamond hits NOT highest percentage identity per query,-44719\n", + "test_epic_data,Usearch hits < similarity threshold,-2\n", + "test_epic_data,Usearch hits NOT highest percentage identity per query,-0\n", + "test_epic_data,Query hit in only one of two databases,-2729\n", + "test_epic_data,Filtered fusion reads,725\n" ] } ], @@ -347,6 +293,14 @@ " merged = df.merge(max_identities, on=[\"query_id\", \"perc_identity\"])\n", " return merged\n", "\n", + "def keep_best_per_query(df):\n", + " \"\"\"For each query_id, keep the row with the highest perc_identity and lowest evalue\"\"\"\n", + " return (\n", + " df.sort_values(\n", + " by=[\"query_id\"] + [\"perc_identity\", \"evalue\"], \n", + " ascending=[True,False, True]\n", + " ).drop_duplicates(subset=\"query_id\", keep=\"first\")\n", + " )\n", "\n", "def clean_16s_query_ids(df):\n", " \"\"\"Remove anything after the first whitespace in 16S query IDs\"\"\"\n", @@ -373,30 +327,46 @@ " file.write(f\"{sample},{stat_name},{value}\\n\")\n", " print(f\"{sample},{stat_name},{value}\") \n", "\n", + "def rename_for_merge(df,part):\n", + " df_renamed = df.rename(columns={\n", + " \"perc_identity\": \"perc_identity_\"+part,\n", + " \"align_length\": \"align_length_\"+part,\n", + " \"evalue\": \"evalue_\"+part,\n", + " })\n", + " return df_renamed\n", "\n", "def filter_blast_results(input_file, output_file, min_similarity):\n", " \"\"\"Main filtering logic for BLAST results across ABR and 16S data parts\"\"\"\n", " df = read_input_data(input_file)\n", "\n", " # ABR filtering\n", - " abr_filtered, abr_removed_identity = filter_by_identity(df, \"ABR\", min_similarity)\n", - " abr_final = keep_max_identity_per_query(abr_filtered)\n", - " abr_removed_max = len(abr_filtered) - len(abr_final)\n", + " abr_threshold_filtered, abr_removed_identity = filter_by_identity(df, \"ABR\", min_similarity)\n", + " abr_best_identity = keep_max_identity_per_query(abr_threshold_filtered)\n", + " abr_best_query = keep_best_per_query(abr_best_identity)\n", + " abr_final = rename_for_merge(abr_best_query ,\"ABR\")\n", + " abr_removed_max = len(abr_threshold_filtered) - len(abr_final)\n", "\n", " # 16S filtering\n", - " s16_filtered, s16_removed_identity = filter_by_identity(df, \"16S\", min_similarity)\n", - " s16_filtered = clean_16s_query_ids(s16_filtered)\n", - " s16_final = keep_max_identity_per_query(s16_filtered)\n", - " s16_removed_max = len(s16_filtered) - len(s16_final)\n", + " s16_threshold_filtered, s16_removed_identity = filter_by_identity(df, \"16S\", min_similarity)\n", + " s16_cleaned = clean_16s_query_ids(s16_threshold_filtered)\n", + " s16_best_identity = keep_max_identity_per_query(s16_cleaned)\n", + " s16_best_query = keep_best_per_query(s16_best_identity)\n", + " s16_final = rename_for_merge(s16_best_query,\"16S\")\n", + " s16_removed_max = len(s16_threshold_filtered) - len(s16_final)\n", "\n", " # Match ABR and 16S by query_id\n", " abr_common, s16_common = merge_parts_on_query_id(abr_final, s16_final)\n", " removed_query_id_mismatch = (len(abr_final) + len(s16_final)) - (\n", - " len(abr_common) + len(s16_common)\n", + " len(abr_common)\n", " )\n", "\n", - " # Merge and write final output\n", - " merged = pd.concat([abr_common, s16_common])\n", + " # Merge side-by-side on query_id\n", + " merged = pd.merge(\n", + " abr_final[[\"query_id\", \"AMR Gene Family\", \"perc_identity_ABR\", \"align_length_ABR\", \"evalue_ABR\"]],\n", + " s16_final[[\"query_id\", \"genus\", \"perc_identity_16S\", \"align_length_16S\", \"evalue_16S\"]],\n", + " on=\"query_id\",\n", + " how=\"inner\",\n", + " )\n", " merged.to_csv(output_file, index=False)\n", "\n", " # Extract sample and part from file path\n", @@ -404,12 +374,12 @@ "\n", " # Write summary\n", " stats = {\n", - " \"filtered min similarity ABR\": \"-\" + str(abr_removed_identity),\n", - " \"filtered max identity ABR\": \"-\" + str(abr_removed_max),\n", - " \"filtered min similarity 16S\": \"-\" + str(s16_removed_identity),\n", - " \"filtered max identity 16S\": \"-\" + str(s16_removed_max),\n", - " \"filtered query id mismatch\": \"-\" + str(removed_query_id_mismatch),\n", - " \"filtration output\": len(merged),\n", + " \"Diamond hits < similarity threshold\": \"-\" + str(abr_removed_identity),\n", + " \"Diamond hits NOT highest percentage identity per query\": \"-\" + str(abr_removed_max),\n", + " \"Usearch hits < similarity threshold\": \"-\" + str(s16_removed_identity),\n", + " \"Usearch hits NOT highest percentage identity per query\": \"-\" + str(s16_removed_max),\n", + " \"Query hit in only one of two databases\": \"-\" + str(removed_query_id_mismatch),\n", + " \"Filtered fusion reads\": len(merged),\n", " }\n", " write_summary(sample, stats)\n", "\n", @@ -418,7 +388,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, "outputs": [ { @@ -533,7 +503,7 @@ ], "source": [ "# 4. Create abundance table\n", - "# Selfwritten python script \"generate_genus_distribution_plot.py\"\n", + "# Selfwritten python script \"generate_genus_distribution_table.py\"\n", "# Input: all filtered_result.csv parts of one sample\n", "# Output: abundance plot over all ABRs\n", "\n", @@ -548,29 +518,13 @@ "abundance_result = result_dir / \"genera_abundance.csv\"\n", "\n", "# === Abundance Table Script ===\n", - "necessary_columns = [\n", - " \"query_id\",\n", - " \"part\",\n", - " \"genus\",\n", - " \"AMR Gene Family\",\n", - " \"perc_identity\",\n", - "]\n", "\n", "def process_combined_data(combined_data, sample_name):\n", - " \"\"\"Separate ABR and 16S data for merging by query_id\"\"\"\n", - " abr_data = combined_data[combined_data[\"part\"] == \"ABR\"]\n", - " sixteen_s_data = combined_data[combined_data[\"part\"] == \"16S\"]\n", + " combined_data[\"sample\"] = sample_name\n", "\n", - " # Prepare to merge only unique hits\n", - " abr_unique = abr_data[[\"query_id\", \"AMR Gene Family\"]].drop_duplicates()\n", - " sixteen_unique = sixteen_s_data[[\"query_id\", \"genus\"]].drop_duplicates()\n", - "\n", - " merged = pd.merge(abr_unique, sixteen_unique, on=\"query_id\", how=\"inner\")\n", - " merged[\"sample\"] = sample_name\n", - "\n", - " # Calculate genus counts per AMR Gene Family and genus for the sample\n", + " # Count genus occurrences per AMR Gene Family\n", " genus_counts = (\n", - " merged.groupby([\"sample\", \"AMR Gene Family\", \"genus\"])\n", + " combined_data.groupby([\"sample\", \"AMR Gene Family\", \"genus\"])\n", " .size()\n", " .reset_index(name=\"genus_count\")\n", " )\n", @@ -589,20 +543,19 @@ " )\n", " return result\n", "\n", - "\n", "def load_and_merge_parts(file_list):\n", - " \"\"\"Load and merges dataframes over all samples\"\"\"\n", + " \"\"\"Load and merges dataframes from compressed CSV files\"\"\"\n", " data_frames = []\n", " for file in file_list:\n", " try:\n", - " df = pd.read_csv(file, usecols=necessary_columns)\n", + " df = pd.read_csv(file)\n", " data_frames.append(df)\n", " except Exception as e:\n", " print(f\"Skipping file due to read error [{file}]: {repr(e)}\")\n", " if data_frames:\n", " merged_df = pd.concat(data_frames, ignore_index=True)\n", " else:\n", - " merged_df = pd.DataFrame(columns=necessary_columns)\n", + " merged_df = pd.DataFrame()\n", " return merged_df\n", "\n", "\n", @@ -622,7 +575,7 @@ " all_data.append(sample_data)\n", "\n", " final_df = pd.concat(all_data, ignore_index=True)\n", - " final_df = final_df.sort_values(by=[\"genus_count\"], ascending=False)\n", + " final_df = final_df.sort_values(by=[\"sample\",\"AMR Gene Family\",\"genus_count\"], ascending=False)\n", "\n", " # Export the final aggregated data to a CSV file\n", " final_df.to_csv(output_path, index=False)\n", @@ -635,1030 +588,178 @@ "cell_type": "code", "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - " \n", - " " - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.plotly.v1+json": { - "config": { - "plotlyServerURL": "https://plot.ly" - }, - "data": [ - { - "customdata": [ - [ - 597, - 0.8234, - 725 - ], - [ - 60, - 0.0828, - 725 - ], - [ - 58, - 0.08, - 725 - ], - [ - 6, - 0.0083, - 725 - ], - [ - 3, - 0.0041, - 725 - ], - [ - 1, - 0.0014, - 725 - ] - ], - "hovertemplate": "%{hovertext}

genus=%{y}
relative_genus_count=%{customdata[1]}
genus_count=%{customdata[0]}
total_count=%{marker.color}", - "hovertext": [ - "Enterobacter", - "Salmonella", - "Klebsiella", - "Lelliottia", - "Citrobacter", - "Escherichia-Shigella" - ], - "legendgroup": "", - "marker": { - "color": [ - 725, - 725, - 725, - 725, - 725, - 725 - ], - "coloraxis": "coloraxis", - "size": [ - 0.8234, - 0.0828, - 0.08, - 0.0083, - 0.0041, - 0.0014 - ], - "sizemode": "area", - "sizeref": 0.0020585, - "symbol": "circle" - }, - "mode": "markers", - "name": "", - "orientation": "v", - "showlegend": false, - "type": "scatter", - "x": [ - "test_epic_data", - "test_epic_data", - "test_epic_data", - "test_epic_data", - "test_epic_data", - "test_epic_data" - ], - "xaxis": "x", - "y": [ - "Enterobacter", - "Salmonella", - "Klebsiella", - "Lelliottia", - "Citrobacter", - "Escherichia-Shigella" - ], - "yaxis": "y" - } - ], - "layout": { - "annotations": [ - { - "font": { - "size": 16 - }, - "showarrow": false, - "text": "OXA beta-lactamase;OXA-48-like beta-lactamase", - "x": 0.5, - "xanchor": "center", - "xref": "paper", - "y": 1, - "yanchor": "bottom", - "yref": "paper" - } - ], - "coloraxis": { - "colorbar": { - "title": { - "text": "Filtered Hit Count" - } - } - }, - "height": 900, - "plot_bgcolor": "lightgrey", - "template": { - "data": { - "bar": [ - { - "error_x": { - "color": "#2a3f5f" - }, - "error_y": { - "color": "#2a3f5f" - }, - "marker": { - "line": { - "color": "#E5ECF6", - "width": 0.5 - }, - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "bar" - } - ], - "barpolar": [ - { - "marker": { - "line": { - "color": "#E5ECF6", - "width": 0.5 - }, - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "barpolar" - } - ], - "carpet": [ - { - "aaxis": { - "endlinecolor": "#2a3f5f", - "gridcolor": "white", - "linecolor": "white", - "minorgridcolor": "white", - "startlinecolor": "#2a3f5f" - }, - "baxis": { - "endlinecolor": "#2a3f5f", - "gridcolor": "white", - "linecolor": "white", - "minorgridcolor": "white", - "startlinecolor": "#2a3f5f" - }, - "type": "carpet" - } - ], - "choropleth": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "choropleth" - } - ], - "contour": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "contour" - } - ], - "contourcarpet": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "contourcarpet" - } - ], - "heatmap": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "heatmap" - } - ], - "heatmapgl": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "heatmapgl" - } - ], - "histogram": [ - { - "marker": { - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "histogram" - } - ], - "histogram2d": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "histogram2d" - } - ], - "histogram2dcontour": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "histogram2dcontour" - } - ], - "mesh3d": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "mesh3d" - } - ], - "parcoords": [ - { - "line": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "parcoords" - } - ], - "pie": [ - { - "automargin": true, - "type": "pie" - } - ], - "scatter": [ - { - "fillpattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - }, - "type": "scatter" - } - ], - "scatter3d": [ - { - "line": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatter3d" - } - ], - "scattercarpet": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattercarpet" - } - ], - "scattergeo": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattergeo" - } - ], - "scattergl": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattergl" - } - ], - "scattermapbox": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattermapbox" - } - ], - "scatterpolar": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterpolar" - } - ], - "scatterpolargl": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterpolargl" - } - ], - "scatterternary": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterternary" - } - ], - "surface": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "surface" - } - ], - "table": [ - { - "cells": { - "fill": { - "color": "#EBF0F8" - }, - "line": { - "color": "white" - } - }, - "header": { - "fill": { - "color": "#C8D4E3" - }, - "line": { - "color": "white" - } - }, - "type": "table" - } - ] - }, - "layout": { - "annotationdefaults": { - "arrowcolor": "#2a3f5f", - "arrowhead": 0, - "arrowwidth": 1 - }, - "autotypenumbers": "strict", - "coloraxis": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "colorscale": { - "diverging": [ - [ - 0, - "#8e0152" - ], - [ - 0.1, - "#c51b7d" - ], - [ - 0.2, - "#de77ae" - ], - [ - 0.3, - "#f1b6da" - ], - [ - 0.4, - "#fde0ef" - ], - [ - 0.5, - "#f7f7f7" - ], - [ - 0.6, - "#e6f5d0" - ], - [ - 0.7, - "#b8e186" - ], - [ - 0.8, - "#7fbc41" - ], - [ - 0.9, - "#4d9221" - ], - [ - 1, - "#276419" - ] - ], - "sequential": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "sequentialminus": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ] - }, - "colorway": [ - "#636efa", - "#EF553B", - "#00cc96", - "#ab63fa", - "#FFA15A", - "#19d3f3", - "#FF6692", - "#B6E880", - "#FF97FF", - "#FECB52" - ], - "font": { - "color": "#2a3f5f" - }, - "geo": { - "bgcolor": "white", - "lakecolor": "white", - "landcolor": "#E5ECF6", - "showlakes": true, - "showland": true, - "subunitcolor": "white" - }, - "hoverlabel": { - "align": "left" - }, - "hovermode": "closest", - "mapbox": { - "style": "light" - }, - "paper_bgcolor": "white", - "plot_bgcolor": "#E5ECF6", - "polar": { - "angularaxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - }, - "bgcolor": "#E5ECF6", - "radialaxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - } - }, - "scene": { - "xaxis": { - "backgroundcolor": "#E5ECF6", - "gridcolor": "white", - "gridwidth": 2, - "linecolor": "white", - "showbackground": true, - "ticks": "", - "zerolinecolor": "white" - }, - "yaxis": { - "backgroundcolor": "#E5ECF6", - "gridcolor": "white", - "gridwidth": 2, - "linecolor": "white", - "showbackground": true, - "ticks": "", - "zerolinecolor": "white" - }, - "zaxis": { - "backgroundcolor": "#E5ECF6", - "gridcolor": "white", - "gridwidth": 2, - "linecolor": "white", - "showbackground": true, - "ticks": "", - "zerolinecolor": "white" - } - }, - "shapedefaults": { - "line": { - "color": "#2a3f5f" - } - }, - "ternary": { - "aaxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - }, - "baxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - }, - "bgcolor": "#E5ECF6", - "caxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - } - }, - "title": { - "x": 0.05 - }, - "xaxis": { - "automargin": true, - "gridcolor": "white", - "linecolor": "white", - "ticks": "", - "title": { - "standoff": 15 - }, - "zerolinecolor": "white", - "zerolinewidth": 2 - }, - "yaxis": { - "automargin": true, - "gridcolor": "white", - "linecolor": "white", - "ticks": "", - "title": { - "standoff": 15 - }, - "zerolinecolor": "white", - "zerolinewidth": 2 - } - } - }, - "title": { - "text": "Bubble Plots of Top Genera for Each AMR Gene Family" - }, - "width": 500, - "xaxis": { - "anchor": "y", - "categoryorder": "category ascending", - "domain": [ - 0, - 1 - ] - }, - "yaxis": { - "anchor": "x", - "categoryorder": "category descending", - "domain": [ - 0, - 1 - ] - } - } - }, - "text/html": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ - "# 5. Create bubble plot\n", + "# 5. Create stacked bar abundance plot\n", + "# Selfwritten python script \"generate_genus_distribution_plot.py\"\n", + "# Input: abundance file\n", + "# Output: bubble plot per sample\n", + "\n", + "import os, pathlib\n", + "import numpy as np\n", + "import pandas as pd\n", + "import plotly.express as px\n", + "import plotly.graph_objects as go\n", + "from plotly.subplots import make_subplots\n", + "\n", + "# === Paths ===\n", + "base = pathlib.Path(os.path.dirname(pathlib.Path().resolve()))\n", + "result_dir = base / \".test_steps/results\"\n", + "\n", + "abundance_result = result_dir / \"genera_abundance.csv\"\n", + "abundance_bar_plot = result_dir / \"combined_genus_abundance_barplot.html\"\n", + "\n", + "# ─── Constants ─────────────────────────────────────────────────────────\n", + "RESERVED_COLOR = 'rgb(217,217,217)'\n", + "AMR_MIN_FRACTION = 0.01\n", + "\n", + "def get_genus_colors(all_genera):\n", + " \"\"\"Assign consistent, distinguishable colors to each genus.\"\"\"\n", + " top_colors = [\n", + " '#D62728', # dark red\n", + " '#FF7F0E', # orange\n", + " '#8B4513', # brown\n", + " '#1F77B4', # dark blue\n", + " '#800080', # purple\n", + " '#7F7F7F', # gray\n", + " '#2CA02C', # dark green\n", + " '#1E90FF', # blue\n", + " '#BA55D3', # medium orchid\n", + " '#BCBD22', # yellow-green\n", + " ]\n", + "\n", + " fallback_palette = (\n", + " px.colors.qualitative.Pastel +\n", + " px.colors.qualitative.Set3 +\n", + " px.colors.qualitative.Alphabet +\n", + " px.colors.qualitative.Light24 +\n", + " px.colors.qualitative.Bold\n", + " )\n", + "\n", + " # Remove duplicates and reserved color from palette\n", + " color_pool = list(dict.fromkeys(top_colors + fallback_palette))\n", + " if RESERVED_COLOR in color_pool:\n", + " color_pool.remove(RESERVED_COLOR)\n", + "\n", + " # Assign genera with a unique color each\n", + " genus_list = [g for g in all_genera if g != \"Others\"]\n", + " if len(genus_list) > len(color_pool):\n", + " raise ValueError(f\"Too many genera ({len(genus_list)}) for available color pool.\")\n", + " genus_colors = {g: color_pool[i] for i, g in enumerate(genus_list)}\n", + " genus_colors[\"Others\"] = RESERVED_COLOR\n", + " return genus_colors\n", + "\n", + "def preprocess_abundance(df, amr, min_genus_abundance, force_include, force_exclude):\n", + " \"\"\"Filter and aggregate genus abundance data for a given AMR family.\"\"\"\n", + " df_amr = df[df[\"AMR Gene Family\"] == amr].copy()\n", + "\n", + " # Determine low-abundance or excluded genera\n", + " low_abundance = df_amr[\n", + " ((df_amr[\"relative_genus_count\"] <= min_genus_abundance) & (~df_amr[\"genus\"].isin(force_include))) |\n", + " (df_amr[\"genus\"].isin(force_exclude))\n", + " ]\n", + " others = (\n", + " low_abundance.groupby(['sample', 'total_count'], as_index=False)\n", + " .agg({\"relative_genus_count\": \"sum\"})\n", + " .assign(genus=\"Others\")\n", + " )\n", + " others[\"sample_label\"] = others[\"sample\"] + \" (\" + others[\"total_count\"].astype(str) + \")\"\n", + "\n", + " # Remove excluded genera\n", + " df_amr = df_amr[~df_amr[\"genus\"].isin(force_exclude)]\n", + " df_amr = df_amr.sort_values(by=['sample','AMR Gene Family','genus_count'],ascending=[True,False,False])\n", + " # plot high abundance or forced-includes\n", + " df_amr_filtered = df_amr[\n", + " (df_amr[\"relative_genus_count\"] > min_genus_abundance) | (df_amr[\"genus\"].isin(force_include))\n", + " ]\n", + "\n", + " # Add \"Others\"\n", + " df_final = pd.concat([df_amr_filtered, others], ignore_index=True)\n", + " df_final[\"sample_label\"] = df_final[\"sample\"] + \" (\" + df_final[\"total_count\"].astype(str) + \")\"\n", + " return df_final\n", + "\n", + "\n", + "def plot_stacked_abundance(observed_csv, output_html, min_genus_abundance, force_include=None, force_exclude=None):\n", + " \"\"\"Main function to generate a stacked bar plot of genus abundance by AMR family.\"\"\"\n", + " force_include = force_include or []\n", + " force_exclude = force_exclude or []\n", + "\n", + " df = pd.read_csv(observed_csv)\n", + " df = df.sort_values([\"sample\",\"genus_count\"],ascending=[True,False])\n", + "\n", + " # ─── Filter AMR families by total count ─────────────────────────────\n", + " amr_totals = df.groupby(\"AMR Gene Family\")[\"total_count\"].sum()\n", + " total_all = amr_totals.sum()\n", + " amrs_to_plot = amr_totals[amr_totals >= total_all * AMR_MIN_FRACTION].index.tolist()\n", + " \n", + " if not amrs_to_plot:\n", + " print(\"No AMR Gene Families meet the abundance threshold.\")\n", + " return\n", + "\n", + " df = df[df[\"AMR Gene Family\"].isin(amrs_to_plot)]\n", + " amrs = sorted(df[\"AMR Gene Family\"].unique())\n", + " samples = df[\"sample\"].nunique()\n", + "\n", + " # ─── Set up subplots ────────────────────────────────────────────────\n", + " fig = make_subplots(\n", + " rows=len(amrs), cols=1,\n", + " subplot_titles=amrs,\n", + " shared_xaxes=True,\n", + " vertical_spacing=0.2\n", + " )\n", + "\n", + " for i, amr in enumerate(amrs, start=1):\n", + " df_amr = preprocess_abundance(df, amr, min_genus_abundance, force_include, force_exclude)\n", + " genus_colors = get_genus_colors(df_amr[\"genus\"].unique())\n", + " \n", + " genera = df_amr[\"genus\"].unique()\n", + " for genus in genera:\n", + " genus_data = df_amr[df_amr[\"genus\"] == genus]\n", + " fig.add_trace(\n", + " go.Bar(\n", + " x=genus_data[\"sample_label\"],\n", + " y=genus_data[\"relative_genus_count\"],\n", + " name=genus,\n", + " marker_color=genus_colors[genus],\n", + " showlegend=True\n", + " ),\n", + " row=i, col=1\n", + " )\n", + "\n", + " # ─── Layout ────────────────────────────────────────────────────────\n", + " fig.update_layout(\n", + " barmode=\"stack\",\n", + " title=\"Relative Genus Abundance per AMR Gene Family\",\n", + " height=800 * len(amrs),\n", + " width=1000 * np.log10(samples) if samples >2 else 500,\n", + " plot_bgcolor=\"white\",\n", + " yaxis=dict(tickformat=\".0%\"),\n", + " legend_title=\"Genus\",\n", + " )\n", + "\n", + " fig.update_xaxes(tickangle=45)\n", + " fig.update_yaxes(title_text=\"Relative Abundance\")\n", + "\n", + " # Save and show\n", + " fig.show()\n", + " fig.write_html(output_html)\n", + "\n", + "\n", + "if __name__ == \"__main__\":\n", + " input_csv = abundance_result\n", + " output_html = abundance_bar_plot\n", + " min_abundance = 0.01\n", + " #sys.stderr = open(snakemake.log[0], \"w\")\n", + " plot_stacked_abundance(input_csv, output_html, float(min_abundance))\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# 6. Create bubble plot\n", "# Selfwritten python script \"generate_genus_distribution_plot.py\"\n", "# Input: abundance file\n", "# Output: bubble plot per sample\n", @@ -1785,7 +886,7 @@ " plot_bgcolor=\"lightgrey\",\n", " height=900,\n", " width=500 * num_cols,\n", - " coloraxis_colorbar=dict(title=\"Filtered Hit Count\"),\n", + " coloraxis_colorbar=dict(title=\"Fusion Read Count\"),\n", " )\n", " fig.update_yaxes(categoryorder=\"category descending\")\n", " fig.update_xaxes(categoryorder=\"category ascending\")\n", @@ -1807,32 +908,11 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/local/tmp/ipykernel_2378280/1591328539.py:90: UserWarning:\n", - "\n", - "FixedFormatter should only be used together with FixedLocator\n", - "\n" - ] - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAABdEAAAMWCAYAAAAeaM88AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAADJgElEQVR4nOzdeXhTVf7H8U+arekKbaEbUBbZF9lXB1AWRVRcsCgqKKgooqLgUlcQpIOOgILgOIOiKKKOouMoAiqgCCouMAKK4ABCoayl+5Km9/cHv0ZCG2hCS1p4v54nj+bk3HO/9ya51U9PzzUZhmEIAAAAAAAAAACUERToAgAAAAAAAAAAqK4I0QEAAAAAAAAA8IIQHQAAAAAAAAAALwjRAQAAAAAAAADwghAdAAAAAAAAAAAvCNEBAAAAAAAAAPCCEB0AAAAAAAAAAC8I0QEAAAAAAAAA8IIQHQAAAAAAAAAALwjRAQDnnAULFshkMnk86tSpo759++o///lPoMtza9iwoW6++Waft8vLy9OkSZO0atWqSq9p586dGjx4sKKiomQymTR+/HivfRs2bOhxjsPCwtStWze9/vrrlV5XIMydO1cLFiwIdBkV0rBhQ1122WWn7Ldz506ZTKYqPy5v5668/a9du1aTJk3S0aNHq7SmqmQymTRp0qST9ik99tJHUFCQateurX79+mn58uVnptBy+HIdysrK0tNPP63OnTsrIiJCdrtdDRs21KhRo/Tjjz9WbaEVtGXLFk2aNEk7d+6s1HEfe+wxNWjQQBaLRbVq1arUsU80adKkMj/Djn9U9rGV6tu3r9q0aeP39if+TAgODtZ5552n+++/X4cOHarESj3t3btXkyZN0oYNG6pk/NL/pvDnvPu67X//+1+NHj1aTZo0kcPhkMPhUNOmTTVmzBh9//33Pu8fAABUnCXQBQAAECivvvqqWrRoIcMwlJ6erjlz5ujyyy/Xv//9b11++eWBLs9veXl5mjx5sqRjoUdluu+++/Ttt9/qlVdeUVxcnOLj40/av1evXvrb3/4mSdqzZ4/+9re/aeTIkcrNzdWdd95ZqbWdaXPnzlVMTIxfv+g413k7d/Hx8Vq3bp2aNGniblu7dq0mT56sm2++ucrDyerg7rvv1vDhw+VyufTrr79q8uTJuvTSS/XFF1+od+/egS7Pq99//10DBw7UgQMHdMcdd2jy5MkKCwvTzp079c4776hTp046evSoIiMjA1rnli1bNHnyZPXt21cNGzaslDE//PBDPf3003r00Uc1aNAg2e32Shn3VD799NNyz+eprsuBdPzPhPz8fH3//feaNGmSvvzyyyoLgffu3avJkyerYcOGat++faWPP3jwYK1bt67Kz/vf//53jRs3Ts2bN9e9996r1q1by2Qy6ZdfftFbb72lLl26aPv27R7XTwAAUHkI0QEA56w2bdqoc+fO7ueXXHKJateurbfeeqtGh+hVadOmTeratauuvPLKCvWvVauWunfv7n7ev39/JSUlacaMGacdojudTplMJlks/OfM2cJut3t8Xs5FDRo0cJ+DXr16qWnTpurTp4/mz59fbUN0l8ulq666SocOHdK6des8Ziv36dNHI0eO1NKlS2W1WgNYZdXZtGmTJOmee+5R3bp1K2XMvLw8hYSEnLRPp06dFBMTUyn7O1NO/Jlw4YUXKjs7W1OmTNFvv/2mZs2aBbA63+Tn5ys4OFh16tRRnTp1qnRfX3/9tcaOHavBgwfrX//6l2w2m/u1iy66SHfddZfeffddORyOKq0DAIBzGcu5AADw/4KDg2Wz2coEPUeOHNHYsWOVmJgom82mxo0b69FHH1VhYaEkqaCgQB06dNB5552nzMxM93bp6emKi4tT37595XK5JEk333yzwsLCtHnzZvXr10+hoaGqU6eOxo0bp7y8vFPW+Mcff+jGG29U3bp1Zbfb1bJlSz333HMqKSmRdGxJiNL/mZ88ebL7z+ZPNVv6VOOuWrVKJpNJ27dv19KlS/1eNqBWrVpq3ry5du3a5W7btm2bhg8f7rHvF1980WO70v0vXLhQEyZMUGJioux2u7Zv3y7p2IzMfv36KTIyUiEhIWrZsqVSU1M9xvj+++91xRVXKCoqSsHBwerQoYPeeecdjz6lf1q/cuVK3XnnnYqJiVF0dLSuvvpq7d27192vYcOG2rx5s1avXu0+F6WzWgsKCjRhwgS1b99ekZGRioqKUo8ePfThhx+WOR9Hjx7V6NGjFRUVpbCwMA0ePFj/+9//yl3+oyLnyRd79+5VcnKywsPDFRkZqWHDhik9Pb3cvmfq3J24nMukSZP0wAMPSJIaNWrk7r9q1Sr3eSvve3PRRRepdevWJz3+FStWaMiQIapXr557WYkxY8aUWVaidOmMzZs36/rrr1dkZKRiY2M1atQoj++7dGw5k9tuu03R0dEKCwvTJZdcot9+++2kdZxK6S/69u/f79Genp6uMWPGqF69erLZbGrUqJEmT56s4uJij36TJ09Wt27dFBUVpYiICHXs2FHz58+XYRge/ZxOpx588EHFxcUpJCREF1xwgb777rsK1fjBBx/o559/VkpKitflPgYNGuQRCq9Zs0b9+vVTeHi4QkJC1LNnT3388cce25Se+xOVtwRG6ZJFn376qTp27CiHw6EWLVrolVde8dju2muvlXQsvC39PJV+3n766Sdddtll7u9YQkKCBg8erD179ng99oYNG+qxxx6TJMXGxnp8d0tKSvTMM8+oRYsWstvtqlu3rkaMGFFmvNJlUr788kv17NlTISEhGjVqlNd9+qKi778kLVq0SD169FBYWJjCwsLUvn17zZ8/v0y/9evX6y9/+YtCQkLUuHFj/fWvf3X/rPBH6Wz643/2fv/997ruuuvUsGFDORwONWzYUNdff73Hz45SaWlpuv3221W/fn3ZbDYlJCRo6NCh2r9/v1atWqUuXbpIkm655Rb3e3789dWX69vy5cs1atQo1alTRyEhISosLCz381jR60tFTZs2TWazWX//+989AvTjXXvttUpISPBoq8xrt+R9aaoTl33Ky8vTxIkT1ahRIwUHBysqKkqdO3fWW2+95dfxAwBQHTB1CwBwznK5XCouLpZhGNq/f7+effZZ5ebmavjw4e4+BQUFuvDCC/X7779r8uTJateunb766iulpqZqw4YN+vjjjxUcHOxermDUqFF67733VFJSohtuuEGGYeitt96S2Wx2j+l0OnXppZdqzJgxevjhh7V27VpNnTpVu3bt0kcffeS13oMHD6pnz54qKirSlClT1LBhQ/3nP//RxIkT9fvvv2vu3LmKj4/Xp59+qksuuUSjR4/WrbfeKkknnSVXkXE7duyodevW6aqrrlKTJk3cf47v65+vO51O7dq1y13Pli1b1LNnTzVo0EDPPfec4uLitGzZMt1zzz06dOiQnnzySY/tU1JS1KNHD7300ksKCgpS3bp1NX/+fN12223q06ePXnrpJdWtW1e//fabe3aoJK1cuVKXXHKJunXrppdeekmRkZFavHixhg0bpry8vDK/ZLj11ls1ePBgLVq0SLt379YDDzygG2+8UV98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fPONx3jXXHONx/O1a9cqKytLY8eOLXfZB0navn27fv31V/c5P3E/+/bt09atWz22ueKKKzyet2vXTpLKXUqgPO+++6569eqlsLAwWSwWWa1WzZ8/X7/88ou7z+rVqyUd+9wc7/rrr/d47s95OpWVK1cqPDy8zHEe/1cYUmDOnS/uvfdebdiwQV9//bWkY8upLFy4UCNHjlRYWNhJty29AWb9+vXd71FSUpIkebxPpco7roKCAh04cEDSn99db9/tinrooYdktVoVHBys9u3ba9OmTfroo488boL5n//8RxdeeKESEhI83pPSWe+lny3p2Pe7f//+ioyMlNlsltVq1RNPPKHDhw+fsvbk5ORKv+dAbm6uvv32Ww0dOtTjPTKbzbrpppu0Z8+eMp+pimrfvr17xrV0bImuZs2aVeizd95556l27dp66KGH9NJLL2nLli1+1VCq9JyeONu3a9euatmypcfPDkmqXbu2LrroIp/28dlnn7l/hpU+PvjgA48+FXn/V6xYIZfLpbvuuuuU+4yLiyvzs6Jdu3YV/n5fcMEF7lq//vprzZ8/XwcPHtRFF13ksdRJTk6OHnroIZ133nmyWCyyWCwKCwtTbm6ux/dz6dKluvDCC9WyZcsK7f94/lzfTvwZ5I2v1xd/derUyf0z1mq16rnnnvP72Crz2t21a1ctXbpUDz/8sFatWqX8/Hx/Dg8AgGqF5VwAAOesli1blrmx6K5du/Tggw/qxhtvVK1atXT48GHFxcWVCWjr1q0ri8VS5k+TR40apXnz5slms+mee+4pd78Wi0XR0dEebXFxcZJ00j91Pnz4sEeQVqp0DVR//0y6qsaVjgUmM2fOlMlkUkhIiJo0aeJezzUtLU3FxcWaPXu2Zs+eXe72J64fe+Kfsh88eFDSn3+aX57StaQnTpyoiRMnVmg/J74/pcuNVCQIeP/995WcnKxrr71WDzzwgOLi4mSxWDRv3jyP9ZkPHz4si8WiqKgoj+1jY2M9nh8+fNjn83Qqhw8fLrMf6c/PYakzfe58NWTIEDVs2FAvvviievXqpQULFig3N/eUYWBJSYkGDhyovXv36vHHH1fbtm0VGhqqkpISde/evdxaT3Vcpe+nt+92Rd1777268cYbVVhYqG+++UaPPfaYhgwZoo0bN7rH3r9/vz766COvS0KUviffffedBg4cqL59++of//iHe/30Dz74QE8//bRH7eXVWt7xlKc0uN6xY4datGhx0r4ZGRkyDKPcZSlO95pTXq12u71Cn73IyEitXr1aTz/9tB555BFlZGQoPj5et912mx577DGfb4paegzejvPEYNLXpbEk6fzzzz/pjUUr+v5X5Dpa6nTOsXTsPB//c7dnz55q1aqVevTooeeee859L4vhw4fr888/1+OPP64uXbooIiJCJpNJl156qce+Dh48WKG6y+PP9a0i75M/15eTiYmJkcPhKDfMXrRokfLy8rRv3z6PEDzQ1+4XXnhB9erV09tvv63p06crODhYF198sZ599lk1bdrU5/EAAKgOCNEBADhOu3bttGzZMv3222/q2rWroqOj9e2338owDI8g/cCBAyouLvYIMHJzc3XTTTepWbNm2r9/v2699dZybyZZXFysw4cPe/wPa+kNHU8WWEVHR2vfvn1l2ktv/HWyMOVkqmpcqWxgcrzatWu7Z596Cz2PXyNXUplfZpSurX6yG/+V1p+SkqKrr7663D7Nmzf3ur2v3njjDTVq1Ehvv/22R72lN6ItFR0dreLiYh05csQjSD/x5p7+nKdTiY6OLvemkSfu+0yfO18FBQXprrvu0iOPPKLnnntOc+fOVb9+/U5Z06ZNm7Rx40YtWLBAI0eOdLeX3qjWH6Xvp7fvdkXVq1fP/Z3p1auX4uLidOONN+rJJ590/5VMTEyM2rVrp6effrrcMUrD6MWLF8tqteo///mPgoOD3a+fOFu5tN709HQlJia620uP51Quvvhivfzyy/rggw/08MMPn7Rv7dq1FRQUVKFrTmnNhYWFHuvm+3tzxlNp27atFi9eLMMw9N///lcLFizQU089JYfDccrjOlHpOd23b1+ZkHfv3r1lrqve/pLmdFT0/T/+Olq/fv1Kr+NUSmc8b9y4UZKUmZmp//znP3ryySc9znvp/SaOV6dOnZNe/0/Gn+tbRd6nyr6+mM1mXXTRRVq+fLn27dvnEeS3atVKksrc5Luqrt12u73MzzKp7C++QkNDNXnyZE2ePFn79+93z0q//PLL9euvv/q8XwAAqgOWcwEA4DgbNmyQ9Geo0K9fP+Xk5JQJHV5//XX366XuuOMO/fHHH3r//fc1f/58/fvf/9bMmTPL3c+bb77p8XzRokWSpL59+3qtrV+/ftqyZYt+/PHHMrWYTCZdeOGFknyfPVbRcStbSEiILrzwQv30009q166dOnfuXOZxqlmwPXv2VGRkpF566SUZhlFun+bNm6tp06bauHFjufvo3LmzwsPDfa7f2+xLk8kkm83mEbakp6eX+YVKnz59JElvv/22R/vixYs9nlfGeTrRhRdeqOzsbP373//2aC/9HJY60+fOW1/J++f51ltvlc1m0w033KCtW7dq3Lhxpxyz9L058Yamf//73ytUU3lKvyfevtv+uuGGG9wziUtnol522WXatGmTmjRpUu57Uhqim0wmWSwWjxtN5ufna+HChR77KL3unFj7O++8U+bGleUZMmSI2rZtq9TUVI8b+h5v2bJlysvLU2hoqLp166b333/f4z0tKSnRG2+8oXr16rlvolr6FzL//e9/PcY62Q2YT6Ui10eTyaTzzz9fM2fOVK1atcpcGyuidGmWN954w6N9/fr1+uWXXzx+dlSVir7/AwcOlNls1rx586q8pvKU/tytW7eupGN1G4ZR5vv5z3/+Uy6Xy6Nt0KBBWrly5UmXAPL2nlfV9a0qri8pKSlyuVy644475HQ6T9m/qo6tYcOGZb6PX3zxhXJycrxuExsbq5tvvlnXX3+9tm7dqry8PJ/3CwBAdcBMdADAOWvTpk3ugOjw4cN6//33tWLFCl111VXumb0jRozQiy++qJEjR2rnzp1q27at1qxZo2nTpunSSy9V//79JR37n/s33nhDr776qlq3bq3WrVtr3Lhxeuihh9SrVy+PNWRtNpuee+455eTkqEuXLlq7dq2mTp2qQYMG6YILLvBa73333afXX39dgwcP1lNPPaWkpCR9/PHHmjt3ru6880538BQeHq6kpCR9+OGH6tevn6KiohQTE1Puki2+jFsVnn/+eV1wwQX6y1/+ojvvvFMNGzZUdna2tm/fro8++uiU67GHhYXpueee06233qr+/fvrtttuU2xsrLZv366NGze6Z+7+/e9/16BBg3TxxRfr5ptvVmJioo4cOaJffvlFP/74o959912fay+dtfr222+rcePGCg4OVtu2bXXZZZfp/fff19ixYzV06FDt3r1bU6ZMUXx8vLZt2+be/pJLLlGvXr00YcIEZWVlqVOnTlq3bp37FzRBQX/OdTjd83SiESNGaObMmRoxYoSefvppNW3aVJ988omWLVtWpu+ZPHfe+paeg5EjR8pqtap58+buAKhWrVoaMWKE5s2bp6SkJF1++eWn3H+LFi3UpEkTPfzwwzIMQ1FRUfroo4+0YsUKn4+l1MCBA9W7d289+OCDys3NVefOnfX111+XCSz9MX36dHXr1k1TpkzRP//5Tz311FNasWKFevbsqXvuuUfNmzdXQUGBdu7cqU8++UQvvfSS6tWrp8GDB2vGjBkaPny4br/9dh0+fFh/+9vfyoR7LVu21I033qhZs2bJarWqf//+2rRpk/72t78pIiLilPWZzWYtWbJEAwcOVI8ePXTnnXfqwgsvVGhoqHbt2qV//etf+uijj5SRkSFJSk1N1YABA3ThhRdq4sSJstlsmjt3rjZt2qS33nrLHUJeeumlioqK0ujRo/XUU0/JYrFowYIF2r17t9/nsk2bNpKkl19+WeHh4QoODlajRo20bt06zZ07V1deeaUaN24swzD0/vvv6+jRo17vbXEyzZs31+23367Zs2crKChIgwYN0s6dO/X444+rfv36uu+++/w+hlI//PCDIiMjy7S3atVKERERFX7/GzZsqEceeURTpkxRfn6+rr/+ekVGRmrLli06dOiQJk+efNq1ljp69Kj7Hg5Op1O//PKLpk2bJrvd7v5Lm4iICPXu3VvPPvus+2fX6tWrNX/+fNWqVctjvKeeekpLly5V79699cgjj6ht27Y6evSoPv30U91///3u77rD4dCbb76pli1bKiwsTAkJCUpISKiS61tVXF969eqlF198UXfffbc6duyo22+/Xa1bt3b/Vcd7773nPnelquLYbrrpJj3++ON64okn1KdPH23ZskVz5swp8zns1q2bLrvsMrVr1061a9fWL7/8ooULF6pHjx4KCQnx+zwAABBQgbqjKQAAgfLqq68akjwekZGRRvv27Y0ZM2YYBQUFHv0PHz5s3HHHHUZ8fLxhsViMpKQkIyUlxd3vv//9r+FwOIyRI0d6bFdQUGB06tTJaNiwoZGRkWEYhmGMHDnSCA0NNf773/8affv2NRwOhxEVFWXceeedRk5Ojsf2SUlJZcbctWuXMXz4cCM6OtqwWq1G8+bNjWeffdZwuVwe/T777DOjQ4cOht1uNySVGedEFR03KSnJGDx48EnH8rXvjh07jFGjRhmJiYmG1Wo16tSpY/Ts2dOYOnWqu8/KlSsNSca7775b7hiffPKJ0adPHyM0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AAAAAAAAVcbe1e7ZUCK50l1y/u5UcI9gv8b0OQmfcV/sGQvQi4uL9dhjj6lRo0ZyOBxq3LixnnrqKZWUlLj7mEymch/PPvus13EXLFhQ7jYFBQVn4rAAAAAAAAAAAFUguFuw56NHsEKvClVwn2C5Drv8GtPnRcxdJYaWrcvVj1sLdDTbpZIT7jQ6Y3ysX4WUZ/r06XrppZf02muvqXXr1vr+++91yy23KDIyUvfee68kad++fR7bLF26VKNHj9Y111xz0rEjIiK0detWj7bgYP9+EwEAAAAAAAAAqL6s51mV/0W+Xzce9TlEn/NuhpZ9k6vurR1qmGCTyeTzPits3bp1GjJkiAYPHixJatiwod566y19//337j5xcXEe23z44Ye68MIL1bhx45OObTKZymwLAAAAAAAAADj7OH9xKsjh3worPofoK7/P0xOjY9S9jcOvHfriggsu0EsvvaTffvtNzZo108aNG7VmzRrNmjWr3P779+/Xxx9/rNdee+2UY+fk5CgpKUkul0vt27fXlClT1KFDB6/9CwsLVVhY6H6enZ3t8/EAAAAAAAAAAKpO1stZ0vETvw2pJLdERq6hkEtD/BrT5xDdajEpsY7Pm/nloYceUmZmplq0aCGz2SyXy6Wnn35a119/fbn9X3vtNYWHh+vqq68+6bgtWrTQggUL1LZtW2VlZen5559Xr169tHHjRjVt2rTcbVJTUzV58uTTPiYAAAAAAAAAQNWwNrd6NpikoNAgWZIsMseY/RrT5zT82n7hen9ltu4ZVlumqlzLRdLbb7+tN954Q4sWLVLr1q21YcMGjR8/XgkJCRo5cmSZ/q+88opuuOGGU65t3r17d3Xv3t39vFevXurYsaNmz56tF154odxtUlJSdP/997ufp6WlqVWrVn4eGQAAAAAAAACgsjn6VP4KKj6H6D//XqgNvxXou835Soq3ymL2DNKfGlOn0op74IEH9PDDD+u6666TJLVt21a7du1SampqmRD9q6++0tatW/X222/7vJ+goCB16dJF27Zt89rHbrfLbre7n2dlZfm8HwAAAAAAAABAzeJziB7mCNIF5/u3doyv8vLyFBTkudi72WxWSUlJmb7z589Xp06ddP755/u8H8MwtGHDBrVt29bvWgEAAAAAAAAAgZExJcNzLXQvaj9W2+exfQ7RHxoR7fNO/HX55Zfr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NUuTJ09W9erV5e/vX2A/PAoEoPAlJibe8tyWLVsWYRIAAAA4g/nz51u/v3z5subNm6euXbsqOjpaJUqUsJk7fPjw4o4H3BVKdDg8SgTAeP7+/nrppZc0cOBAo6MAAGCIH374QT/++KNatGghT09PWSwWm18qAyg6p0+f1uHDh2UymVStWjVVrFjR6EiAQwoPD7+leSaTSceOHSviNEDhokQHABS5wMBA/e9//1PVqlWNjgI4ra+//lpvvvmmjh07pjVr1ig4OFjLly9XeHi4mjdvbnQ8wGGdOXNGffr00datW2UymXT06FFFRERoyJAhKlOmjP7zn/8YHRFwWJcvX9Zzzz2nZcuWWbcTdHV11YABA7RgwQKVKlXK4IQAgPuFi9EBgOJ2/vx5/ec//9GTTz6pp556Si+//LKys7ONjgU4tFGjRum1114zOgbgtD744AN17NhRnp6e2r17t3JyciRJFy9e1KxZswxOBzi2UaNGqUSJEkpPT7cp7Pr27avPP//cwGSA4xs9erQSExP18ccf6/z58zp//rw++ugjJSYmasyYMUbHA5xKXl6eLl26ZHQM4I6xEh1OZefOndYSoWHDhrJYLNq5c6euXr2qjRs3ql69ekZHBBxSfn6+unbtqiNHjigqKqrAfnhr1641KBngHOrWratRo0ZpwIAB8vb21t69exUREaGUlBR16tRJmZmZRkcEHFZAQIC++OIL1a5d2+b1l5aWpujoaAoFoAhVqFBB77//vlq1amUzvnXrVvXp00enT582JhjgwDZs2KAzZ87o8ccft47NnDlTL7zwgvLy8tSmTRslJCSobNmyBqYEbh8r0eFURo0apR49euj48eNau3at1q1bp7S0NHXr1k0jR440Oh7gsJ577jlt3bpV1apVU/ny5eXr62vzBaBoHT58WC1atCgw7uPjo/Pnzxd/IMCJXL58+aZbRvz666/y8PAwIBHgPK5cuSJ/f/8C435+frpy5YoBiQDHN3fuXF24cMH6eNu2bZoyZYpiY2O1evVqZWRk6IUXXjAwIXBn3IwOABSnnTt3atGiRXJz+/9/9N3c3DRu3DjVr1/fwGSAY1u2bJk++OADde3a1egogFMKDAzUDz/8oLCwMJvxpKQkRUREGBMKcBItWrTQsmXLrIWByWRSfn6+/v3vfxdYHQugcDVp0kRTp07VsmXLVLJk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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ - "# 6. Create boxplots\n", + "# 7. Create boxplots\n", "# Selfwritten python script \"percidt_per_genus.py\"\n", "# Input: all filtered_result.csv parts of one sample\n", "# Output: boxplot over all samples per percentage identity, number of unique hits and genera\n", @@ -1849,23 +929,6 @@ "filter_result = result_dir / \"filtered_result.csv\"\n", "boxplot = result_dir / \"genus_idt_per_genus_plot.png\"\n", "\n", - "# === PercIDT Boxplot Script ===\n", - "necessary_columns = [\n", - " \"query_id\",\n", - " \"part\",\n", - " \"genus\",\n", - " \"AMR Gene Family\",\n", - " \"perc_identity\",\n", - "]\n", - "\n", - "dtype_dict = {\n", - " \"query_id\": \"string\",\n", - " \"part\": \"string\",\n", - " \"genus\": \"string\",\n", - " \"AMR Gene Family\": \"string\",\n", - " \"perc_identity\": \"float\",\n", - "}\n", - "\n", "\n", "def generate_percentage_idt_per_genus(input_files, output_file):\n", " all_data = [] # List to hold DataFrames from all input files\n", @@ -1874,9 +937,7 @@ " df = pd.read_csv(\n", " input_file,\n", " sep=\",\",\n", - " usecols=necessary_columns,\n", " header=0,\n", - " dtype=dtype_dict,\n", " )\n", " all_data.append(df)\n", "\n", @@ -1909,7 +970,7 @@ " fig, ax1 = plt.subplots(figsize=(15, 8))\n", " sns.boxplot(\n", " x=\"genus\",\n", - " y=\"perc_identity\",\n", + " y=\"perc_identity_16S\",\n", " data=combined_data,\n", " ax=ax1,\n", " order=genus_order,\n", @@ -1948,181 +1009,205 @@ "cell_type": "code", "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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obxDhw4CAGHv3r0K5eX9TNjY2AhdunQp1m9Jn+GSlHZvCYIgfPvttwIAIS4uThAEQXj27JkwadIkwcnJSdDQ0BBkMpng4uIijBw5UkhPTxfPs7GxEYKCghTaKuu5crlcWLRokdCgQQOxnru7u7Bv3z6xzu3bt4WOHTsKenp6AgDx+01pY3f8+HGhXbt2go6OjiCVSoWWLVsqtCcIpd//JT2xqyQlPUXNyMhIaNGihbB27VqFJ5iW9r24UFxcnNClSxfByMhIUFdXF2rWrCl06dJFoX5GRoYwaNAgwdTUVNDW1hZat24tHD9+vNj7XlJfBQUFQkhIiKCioqLwxM69e/cKrq6ugoaGhmBtbS3Mnj27xCe9vXjxQhg3bpxgY2MjqKurCxYWFsLQoUOFjIyMYtdS3vu/tLEp7b0tiTLf70ePHpV4vKxPzSvtXn7TPVLWn72CIAgxMTFCo0aNBE1NTQGA+POi6FPzCv3000/iPSCTyYSuXbsKf/75p0Kd0n6elXR9JfH09BTq169f4rGSnvRX0ljm5OQIgwcPFkxMTASJRCJeS1RUlNC5c2ehZs2agoaGhmBqair4+voKx48ff2tcREREHwuJIJSy/oSIKtXo0aOxfPly3L17t8SNmokqy5MnT+Do6Ihu3bph1apVVR3OB8nIyAgDBw7E/PnzqzoUIiIiIqJqhUvziJTszJkzuH79OpYtW4avvvqKSSiqVOnp6ZgxYwbatm0LY2Nj3LlzB4sWLcLTp08/iOWxH5rLly/jwIEDyMjI4KbARERERESVgDOiiJRMIpFAW1sbvr6+WLduHXR1das6JKrGMjIy0L9/f8THx+Px48fQ1tZGy5YtER4eXup+SP9lbdu2xbVr1/D5559j7ty5H9x+PEREREREHzsmooiIiIiIiIiISCne/sgkIiIiIiIiIiKiCsBEFBERERERERERKQUTUUREREREREREpBR8al4FKigowP3796Gnp8cNbomIiIiIKpggCHj69CksLS2hosL/Uyci+hgxEVWB7t+/Dysrq6oOg4iIiIioWrt79y5q1apV1WEQEdE7YCKqAunp6QF49YNRX1+/iqMhIiIiIqpesrKyYGVlJf7eTUREHx8moipQ4XI8fX19JqKIiIiIiCoJt8EgIvp4cWE1EREREREREREpBRNRRERERERERESkFExEERERERER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ZcuW4fbt2xXR3EcpKysLMpkMmZmZVTqTSr5wUJX1TUT0sVEdtaaqQ/h4XHer6giISuaYUNURkJJ8KL9vExHRuyv30rxGjRopbFYuCALS09Px6NEjLFu2rEKDIyIiIiIiIiKi6qPciajCp9kVUlFRgYmJCby8vFC3bt2KiouIiIiIiIiIiKqZcieipkyZUhlxEBERERERERFRNVfuRBQAyOVy7N69G0lJSZBIJHB2dkZAQIDCBuFERERERERERESvK3ci6ubNm/D19cXff/8NJycnCIKA69evw8rKCvv370edOnUqI04iIiIiIiIiIvrIqZT3hJCQENSpUwd3797FhQsXcPHiRaSmpsLOzg4hISGVESMREREREREREVUD5Z4RFRcXhzNnzsDIyEgsMzY2xuzZs9GqVasKDY6IiIiIiIiIiKqPcs+I0tTUxNOnT4uVP3v2DBoaGhUSFBERERERERERVT/lTkT5+fnhyy+/xNmzZyEIAgRBwJkzZ/D1118jICCgMmIkIiIiIiIiIqJqoNyJqCVLlqBOnTpwd3eHlpYWtLS00KpVK9jb22Px4sWVESMREREREREREVUD5d4jysDAAHv27MHNmzeRlJQEQRDg7OwMe3v7yoiPiIiIiIiIiIiqiXInogrZ29sz+URERERERERERGVW7qV5n376KWbPnl2sfN68eejZs2eFBEVERERERERERNVPuRNRcXFx6NKlS7HyTp064ffff6+QoIiIiIiIiIiIqPopdyLq2bNn0NDQKFaurq6OrKysCgmKiIiIiIiIiIiqn3Inoho0aICtW7cWK9+yZQucnZ0rJCgiIiIiIiIiIqp+yr1Z+eTJk9GjRw/cunUL7dq1AwAcOXIEmzdvxq+//lrhARIRERERERERUfVQ7kRUQEAAdu/ejZkzZ2L79u2QSqVwdXVFTEwMPD09KyNGIiIi+gCEO3Wv6hCISjRFqOoIiIiIqKzKnYgCgC5dupS4YTkREREREREREVFpyr1HFBERERERERER0btgIoqIiIiIiIiIiJSCiSgiIiIiIiIiIlIKJqKIiIiIiIiIiEgpyp2Imjp1Kp4/f16s/MWLF5g6dWqFBEVERERERERERNVPuRNR4eHhePbsWbHy58+fIzw8vEKCIiIiIiIiIiKi6qfciShBECCRSIqVX7p0CUZGRhUSFBERERERERERVT9qZa1oaGgIiUQCiUQCR0dHhWSUXC7Hs2fP8PXXX1dKkERERERERERE9PErcyIqIiICgiBg4MCBCA8Ph0wmE49paGjA1tYW7u7ulRIkERERERERERF9/MqciBowYAAAwM7ODh4eHlBXV6+0oIiIiIiIiIiIqPopUyIqKysL+vr6AIBGjRrhxYsXePHiRYl1C+sRERERERERERG9rkyJKENDQ6SlpcHU1BQGBgYlblZeuIm5XC6v8CCJiIiIiIiIiOjjV6ZE1NGjR8Un4h07dqxSAyIiIqIPU9zQL6o6BCIiIiL6yJUpEeXp6Vniv4mIiIiIiIiIiMqqzJuVv+7Jkyf4448/8PDhQxQUFCgc69+/f4UERkRERERERERE1Uu5E1H79u1Dv379kJ2dDT09PYX9oiQSCRNRRERERERERERUIpXynjB69GgMHDgQT58+xZMnT5CRkSG+Hj9+XBkxEhERERERERFRNVDuRNTff/+NkJAQaGtrV0Y8RERERERERERUTZU7EeXj44Nz585VRixERERERERERFSNlWmPqL1794r/7tKlC7799lskJibCxcUF6urqCnUDAgIqNkIiIiIiIiIiIqoWypSI6tatW7GyqVOnFiuTSCSQy+XvHRQREREREREREVU/ZUpEFRQUVHYcRERERERERERUzZV7jygiIiIiIiIiIqJ3wUQUEREREREREREpBRNRRERERERERESkFExEERERERERERGRUjARRURERERERERESlGmp+YVVVBQgJs3b+Lhw4fFnqjXpk2bCgmMiIiIiIiIiIiql3Inos6cOYO+ffvizp07EARB4ZhEIoFcLq+w4IiIiIiIiIiIqPoodyLq66+/RtOmTbF//35YWFhAIpFURlxERERERERERFTNlDsRdePGDWzfvh329vaVEQ8REREREVGVkcvlyMvLq+owiIg+Gurq6lBVVS1z/XInolq0aIGbN28yEUVERERERNWGIAhIT0/HkydPqjoUIqKPjoGBAczNzcu0aq5MiajLly+L//7mm28wevRopKenw8XFBerq6gp1XV1dyxkuERERERFR1SpMQpmamkJbW5tbkBARlYEgCHj+/DkePnwIALCwsHjrOWVKRLm5uUEikShsTj5w4EDx34XHuFk5ERERERF9bORyuZiEMjY2rupwiIg+KlKpFADw8OFDmJqa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0aNH+OKLL3Dw4MESjxd94gARERERERERFWdlZYWTJ09WdRhESlXuzcpDQ0ORkZGBM2fOQCqVIjo6GuvXr4eDgwP27t1bGTESEREREREREVE1UO4ZUUePHsWePXvQrFkzqKiowMbGBh06dIC+vj5mzZqFLl26VEacRERERERERET0kSv3jKjs7GyYmpoCAIyMjPDo0SMAgIuLCy5cuFCx0RERERERERERUbVR7kSUk5MTkpOTAQBubm5YuXIl/v77b6xYsQIWFhYVHiAREREREREREVUP5V6aFxoairS0NADAlClT4OPjg02bNkFDQwORkZEVHR8REREREREREVUT5U5E9evXT/x3o0aNcPv2bVy7dg3W1taoUaNGhQZHRERERERERETVR7mX5hXKzc1FcnIyNDQ00LhxYyahiIiIiIiIPmASiQS7d++u6jDemZeXF0JDQ0s9HhYWBjc3t/dq40NTEfFGRkbCwMBA/Los41QWsbGxkEgkePLkyXu3VRZV9d7Z2toiIiLivdoICgpCt27d3ljnY7s330e5E1HPnz/HoEGDoK2tjfr16yM1NRUAEBISgtmzZ1d4gERERERERFSyoKAgSCQSSCQSqKurw8zMDB06dMDatWtRUFCgUDctLQ2dO3euokg/DDt37sS0adPErysiyVCZisb7Lnr37o3r169XUET/x8PDA2lpaZDJZACKJ7zelbITXKR85V6aN2HCBFy6dAmxsbHo1KmTWO7t7Y0pU6Zg/PjxFRogERERERFRVWkXnKq0vo4us36n8zp16oR169ZBLpfjwYMHiI6OxogRI7B9+3bs3bsXamqv/uwzNzevyHA/SkZGRkrvMy0tDSYmJuL7UB4VEa9UKoVUKn3vdl6Xl5cHDQ2ND/6eysvLg7q6elWHQUWUe0bU7t27sXTpUrRu3RoSiUQsd3Z2xq1btyo0OCIiIiIiInozTU1NmJubo2bNmmjcuDG+++477NmzBwcPHlR4oFTRpXnjxo2Do6MjtLW1Ubt2bUyePBl5eXni8cIlXGvXroW1tTV0dXUxdOhQyOVyzJ07F+bm5jA1NcWMGTMU4klNTUXXrl2hq6sLfX199OrVCw8ePCjW7oYNG2BrawuZTIY+ffrg6dOnYp3s7Gz0798furq6sLCwwIIFC8o8Hm9q9/XlT15eXrhz5w5GjhwpzioDgDt37sDf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+vTpiIyMhFwuR4sWLaCtrY0NGzZAKpXCxsamTO3369cPU6ZMwYABAxAWFoZHjx7hm2++QWBgYLFleYBy7+8PVbmv9KuvvhIzdX/99Rd69+4NbW1t/Prrrwqb2hEREREREVHli46OhoWFBWxtbdGpUyccO3YMS5YswZ49e6CqqlriOV27dsXIkSMxfPhwuLm54dSpU5g8efJ7xyKRSLB7924YGhqiTZs28Pb2Ru3atbF169ZytTNv3jy0adMGAQEB8Pb2RuvWrStlK5ipU6fi9u3bqFOnDkxMTAC8mokzbNgw1KtXD506dYKTkxOWLVtWrnb19fUxePBgHD9+HNevXy81sWNgYICdO3eiXbt2qFevHlasWIHNmzejfv36731tZbV27VpkZGSgUaNGCAwMREhICExNTcvdjoeHB77++mv07t0bJiYmmDt3rnjMwcEBHh4ecHJyQosWLd7YTs2aNREeHo7x48fDzMzsjU+fKwsfHx9ERUXh8OHDaNasGVq2bImFCxeKiSYDAwOsXr0arVq1gqurK44cOYJ9+/bB2Ni4TO1ra2vjt99+w+PHj9GsWTN8+umnaN++PZYuXVrqOcq6vz9UEqGcCxRlMhkuXLiAOnXqYM6cOTh69Ch+++03nDx5En369FGYgvlfk5WVBZlMhszMzBKz3coiXzioyvomIvrYqI5aU9UhfDTaBae+vRJRFTi6zLqqQyAlqazft1++fImUlBTY2dlBS0urwtololcEQUDdunXx1VdfYdSoUVUdDlWC8nwfLffSPEEQxE25YmJixMc5WllZvdM6UiIiIiIiIiKqnh4+fIgNGzbg77//xhdffFHV4dAHoNyJqKZNm2L69Onw9vZGXFwcli9fDgBISUkpcf0jEREREREREf03mZmZoUaNGli1ahUMDQ2rOhz6AJQ7ERUREYF+/fph9+7dmDhxIuzt7QEA27dvh4eHR4UHSEREREREREQfp3LuBkT/AeVORLm6upb4GMN58+aVuhEeERERERERERFRuRNRpeGmfkRERERERERE9CYqVR0AERERERERERH9NzARRURERERERERESsFEFBERERERERERKUW5E1GxsbGVEAYREREREREREVV35U5EderUCXXq1MH06dNx9+7dyoiJiIiIiIiIKphEIsHu3burOox35uXlhdDQ0FKPh4WFwc3N7b3a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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ - "# 7. Create Overview Plot\n", - "# Selfwritten python script \"plot_overview.py\"\n", - "# Input: Overview table created iteritavely within the snakemake run\n", - "# Output: barplots for all samples showing generated and filtered similarity search hits\n", - "# Note: Overview Table is created here after the process while in the original snakemake run\n", - "# it's created iteratively within the workflow.\n", + "# 7. Create boxplots\n", + "# Selfwritten python scripts \"boxplot_[align_lengths,evalue,percidt].py\"\n", + "# Input: all filtered_result.csv parts of one sample\n", + "# Output: boxplot over all samples per parameter alignment lengths, E-value or percentage identity\n", "\n", - "import numpy as np\n", "import pandas as pd\n", - "import os, pathlib, subprocess\n", - "\n", - "# === Paths ===\n", - "base = pathlib.Path(os.path.dirname(pathlib.Path().resolve()))\n", + "import seaborn as sns\n", + "import os, pathlib\n", + "import matplotlib.pyplot as plt\n", "\n", - "sample = \"test_epic_data\"\n", - "github = base / \".github\"\n", - "result_dir = base / \".test_steps/results\"\n", + "\"\"\"\n", + "This script takes a list of all filtered fasta files, combines e-value information \n", + "across samples, and visualizes the distribution of e-values using boxplots split \n", + "by part (ABR/16S) and sample.\n", + "\"\"\"\n", "\n", - "fastq = github / \"data/fastq/test_epic_data.fastq.gz\"\n", - "diamond_res = result_dir / \"card_results.txt\"\n", - "usearch_res = result_dir / \"SILVA_results.txt\"\n", - "integration_res = result_dir / \"integrated_result.csv\"\n", - "filtration_res = result_dir / \"filtered_result.csv\"\n", - "overview_table = result_dir / \"overview_table.txt\"\n", - "overview_plot = result_dir / \"overview_plot.png\"\n", + "PRETTY_LABELS = {\n", + " \"align_length\": \"Alignment length\",\n", + " \"perc_identity\": \"Percentage identity\",\n", + " \"evalue\": \"E-value\"\n", + "}\n", "\n", - "# === Utils ===\n", - "def count_lines(file):\n", - " if str(file).endswith(\".gz\"):\n", - " cmd = f\"zcat {file} | wc -l\"\n", - " result = subprocess.check_output(cmd, shell=True)\n", - " num_lines = int(result.strip())\n", - " return num_lines // 4\n", + "def read_and_process_partitioned_data(partition_files, sample, param):\n", + " \"\"\"Read and process partitioned files for a single sample.\"\"\"\n", + " data_frames = []\n", + " sample_name = sample\n", + " param = param\n", + " for part_file in partition_files:\n", + " if os.path.exists(part_file):\n", + " df = pd.read_csv(\n", + " part_file, header=0, sep=\",\"\n", + " )\n", + " #df[f\"{param}_ABR\"] = df[f\"{param}_ABR\"] * 3\n", + " long_df = pd.melt(\n", + " df,\n", + " id_vars=[\"query_id\"],\n", + " value_vars=[param + \"_ABR\", param + \"_16S\"],\n", + " var_name=\"part\",\n", + " value_name=param\n", + " )\n", + "\n", + " # Normalize part labels\n", + " long_df[\"part\"] = long_df[\"part\"].str.replace(param + \"_\", \"\")\n", + " long_df[\"sample\"] = sample_name\n", + " data_frames.append(long_df)\n", + " \n", + " if data_frames:\n", + " return pd.concat(data_frames)\n", " else:\n", - " cmd = f\"cat {file} | wc -l\"\n", - " return int(subprocess.check_output(cmd, shell=True))\n", - "\n", - "# === Prepare Overview File ===\n", - "lines_to_add = [\n", - " f\"{sample},fastq input reads,{count_lines(fastq)}\\n\",\n", - " f\"{sample},diamond output hits,{count_lines(diamond_res)}\\n\",\n", - " f\"{sample},usearch output hits,{count_lines(usearch_res)}\\n\",\n", - " f\"{sample},merge output,{count_lines(integration_res)}\\n\",\n", - "]\n", + " return None\n", + "\n", + "\n", + "def plot_boxplots(data, output_file):\n", + " \"\"\"\n", + " Generate and save boxplots of e-values across samples and parts (ABR vs. 16S).\n", + "\n", + " Args:\n", + " data (pd.DataFrame): Combined dataframe containing 'sample', 'evalue', and 'part'.\n", + " output_file (str): Path to save the resulting plot.\n", + " \"\"\"\n", + " plt.figure(figsize=(15, 10))\n", + " flierprops = dict(markerfacecolor=\"0.75\", markersize=2, linestyle=\"none\")\n", + " sns.boxplot(x=\"sample\", y=\"perc_identity\", hue=\"part\", data=data, flierprops=flierprops)\n", + " #plt.yscale(\"log\")\n", + " plt.title(\"Boxplot of e-values for ABR and 16S parts across samples -Filtered-\")\n", + " plt.xlabel(\"Sample\")\n", + " plt.ylabel(\"Percentage identity\")\n", + " plt.xticks(rotation=45)\n", + " plt.tight_layout()\n", + " plt.show()\n", + " plt.close()\n", "\n", - "# Read existing lines (if file exists)\n", - "existing_lines = set()\n", - "if os.path.exists(overview_table):\n", - " with open(overview_table, \"r\") as f:\n", - " existing_lines = set(f.readlines())\n", - "\n", - "# Append only missing lines\n", - "with open(overview_table, \"a\") as f:\n", - " for line in lines_to_add:\n", - " if line not in existing_lines:\n", - " f.write(line)\n", - "\n", - "# === plot overview script ===\n", - "MAIN_CATEGORIES = {\n", - " \"merge output\": \"Diamond and Usearch hits\",\n", - " \"filtration output\": \"Hits after filtration\",\n", - "}\n", "\n", - "FILTER_REASONS = {\n", - " \"filtered min similarity ABR\": \"Diamond hits < similarity threshold\",\n", - " \"filtered max identity ABR\": \"Diamond hits ≠ max identity for query ID\",\n", - " \"filtered min similarity 16S\": \"Usearch hits < similarity threshold\",\n", - " \"filtered max identity 16S\": \"Usearch hits ≠ max identity for query ID\",\n", - " \"filtered query id mismatch\": \"No overlap for hits in both databases\",\n", - "}\n", + "def main(filtered_fasta_files, sample_names, param, output_file):\n", + " \"\"\"Main function to process partitioned files for each sample and generate the plot.\"\"\"\n", + " all_data = []\n", + "\n", + " # Loop over each sample's partitioned CSV files\n", + " for sample in sample_names:\n", + " data = read_and_process_partitioned_data(\n", + " [file for file in filtered_fasta_files], sample, param\n", + " )\n", + " if data is not None:\n", + " all_data.append(data)\n", + "\n", + " if all_data:\n", + " combined_data = pd.concat(all_data)\n", + " plot_boxplots(combined_data, output_file)\n", + " else:\n", + " print(\"No data found.\")\n", "\n", "\n", - "def map_main_category(state):\n", - " return MAIN_CATEGORIES.get(state)\n", + "if __name__ == \"__main__\":\n", + " base = pathlib.Path(os.path.dirname(pathlib.Path().resolve()))\n", + " result_dir = base / \".test_steps/results\"\n", "\n", + " filter_result = result_dir / \"filtered_result.csv\"\n", + " boxplot = result_dir / f\"combine_boxplot.png\"\n", + " \n", + " filtered_fasta_files = filter_result\n", + " output_file = boxplot # Single output file for all panels\n", + " sample_names = \"test_epic_data\"\n", + " param = \"perc_identity\"\n", + " main([str(filtered_fasta_files)], [sample_names], param, output_file)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# 8. Create Attrition plot\n", + "# Selfwritten python scripts \"plot_attrition.py\"\n", + "# Input: overview table\n", + "# Output: plot of count overview throughout ERMA process with respect to rejection breakdown\n", "\n", - "def map_filter_reason(state):\n", - " return FILTER_REASONS.get(state)\n", + "import numpy as np\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "import os, pathlib\n", "\n", + "# === Paths ===\n", + "base = pathlib.Path(os.path.dirname(pathlib.Path().resolve()))\n", + "sample = \"test_epic_data\"\n", + "result_dir = base / \".test_steps/results\"\n", + "overview_table = result_dir / \"overview_table.txt\"\n", + "overview_plot = result_dir / \"overview_plot.png\"\n", "\n", - "def load_and_summarize_data(input_path):\n", - " \"\"\"Read the overview table and group by main and filtering categories.\"\"\"\n", - " df = pd.read_csv(input_path,names=[\"sample\",\"state\",\"total_count\"])\n", + "# === Category Definitions (now match the final labels directly) ===\n", + "MAIN_CATEGORIES = [\n", + " \"Number of FastQ input reads\",\n", + " \"Merged similarity hits\",\n", + " \"Filtered fusion reads\",\n", + "]\n", "\n", - " # Assign main and filter categories\n", - " df[\"category\"] = df[\"state\"].apply(map_main_category)\n", - " df[\"filter_reason\"] = df[\"state\"].apply(map_filter_reason)\n", - " df[\"total_count\"] = df[\"total_count\"].astype(int).abs()\n", + "FILTER_REASONS = {\n", + " \"Diamond hits < similarity threshold\": \"royalblue\",\n", + " \"Diamond hits NOT highest percentage identity per query\": \"purple\",\n", + " \"Usearch hits < similarity threshold\": \"#a6d854\",\n", + " \"Usearch hits NOT highest percentage identity per query\": \"#66c2a5\",\n", + " \"Query hit in only one of two databases\": \"#ffd92f\",\n", + "}\n", "\n", - " # Group main categories\n", - " main_summary = (\n", - " df.dropna(subset=[\"category\"])\n", - " .groupby([\"sample\", \"category\"])[\"total_count\"]\n", - " .sum()\n", - " .unstack()\n", - " .fillna(0)\n", - " )\n", + "MAIN_COLOR_MAP = {\n", + " \"Number of FastQ input reads\": \"seagreen\",\n", + " \"Merged similarity hits\": \"#fc8d62\",\n", + " \"Filtered fusion reads\": \"#8da0cb\",\n", + "}\n", "\n", - " # Group filtering reasons\n", - " overlay_summary = (\n", - " df.dropna(subset=[\"filter_reason\"])\n", - " .groupby([\"sample\", \"filter_reason\"])[\"total_count\"]\n", - " .sum()\n", - " .unstack()\n", - " .fillna(0)\n", - " )\n", + "# === Load and summarize the table ===\n", + "def load_and_summarize_data(path):\n", + " df = pd.read_csv(path, names=[\"sample\", \"state\", \"count\"])\n", + " df[\"count\"] = df[\"count\"].astype(int).abs()\n", "\n", - " return main_summary, overlay_summary\n", + " main_df = df[df[\"state\"].isin(MAIN_CATEGORIES)].pivot(index=\"sample\", columns=\"state\", values=\"count\").fillna(0)\n", + " filter_df = df[df[\"state\"].isin(FILTER_REASONS)].pivot(index=\"sample\", columns=\"state\", values=\"count\").fillna(0)\n", "\n", + " return main_df, filter_df\n", "\n", - "def plot_summary(main_summary, overlay_summary, output_path):\n", - " \"\"\"Generate and save a stacked bar plot showing filtering breakdown.\"\"\"\n", - " samples = main_summary.index\n", + "# === Plotting function ===\n", + "def plot_summary(main_df, filter_df, output_path):\n", + " samples = main_df.index\n", " x = np.arange(len(samples))\n", - " bar_width = 0.25\n", - " overlay_width = 0.125\n", + " bar_width = 0.18\n", + " overlay_width = 0.1\n", "\n", " fig, ax = plt.subplots(figsize=(12, 7))\n", "\n", - " # Define colors\n", - " main_colors = {\n", - " MAIN_CATEGORIES[\"merge output\"]: \"#fc8d62\",\n", - " MAIN_CATEGORIES[\"filtration output\"]: \"#8da0cb\",\n", - " }\n", - " filter_colors = {\n", - " FILTER_REASONS[k]: c\n", - " for k, c in zip(\n", - " FILTER_REASONS, [\"royalblue\", \"purple\", \"#a6d854\", \"#66c2a5\", \"#ffd92f\"]\n", + " # Plot main bars with offsets\n", + " offsets = np.linspace(-bar_width, bar_width, len(MAIN_CATEGORIES))\n", + " for i, col in enumerate(MAIN_CATEGORIES):\n", + " if col not in main_df.columns:\n", + " continue\n", + " ax.bar(\n", + " x + offsets[i],\n", + " main_df[col],\n", + " bar_width,\n", + " label=col,\n", + " color=MAIN_COLOR_MAP.get(col, \"gray\"),\n", " )\n", - " }\n", "\n", - " # Plot main bars\n", - " ax.bar(\n", - " x - bar_width / 2,\n", - " main_summary[MAIN_CATEGORIES[\"merge output\"]],\n", - " bar_width,\n", - " label=MAIN_CATEGORIES[\"merge output\"],\n", - " color=main_colors[MAIN_CATEGORIES[\"merge output\"]],\n", - " )\n", - " ax.bar(\n", - " x + bar_width / 2,\n", - " main_summary[MAIN_CATEGORIES[\"filtration output\"]],\n", - " bar_width,\n", - " label=MAIN_CATEGORIES[\"filtration output\"],\n", - " color=main_colors[MAIN_CATEGORIES[\"filtration output\"]],\n", - " )\n", + " # Plot filter stack bars *on top* of \"Filtered fusion reads\"\n", + " if \"Filtered fusion reads\" in main_df.columns:\n", + " bottom = main_df[\"Filtered fusion reads\"].values.copy()\n", + " else:\n", + " bottom = np.zeros_like(x)\n", "\n", - " # Stack filter bars on top of filtration bar\n", - " bottom = main_summary[MAIN_CATEGORIES[\"filtration output\"]].values.copy()\n", - " for reason in FILTER_REASONS.values():\n", - " heights = (\n", - " overlay_summary[reason]\n", - " if reason in overlay_summary\n", - " else np.zeros_like(bottom)\n", - " )\n", + " for reason in FILTER_REASONS:\n", + " heights = filter_df[reason].values if reason in filter_df.columns else np.zeros_like(x)\n", " ax.bar(\n", - " x + bar_width / 2.1,\n", + " x + bar_width,\n", " heights,\n", " overlay_width,\n", " bottom=bottom,\n", " label=reason,\n", - " color=filter_colors[reason],\n", + " color=FILTER_REASONS.get(reason, \"gray\"),\n", " )\n", " bottom += heights\n", "\n", @@ -2131,24 +1216,22 @@ " ax.set_xticklabels(samples, rotation=45)\n", " ax.set_ylabel(\"Similarity search hit count\")\n", " ax.set_xlabel(\"Sample\")\n", - " ax.set_title(\n", - " \"Similarity Search Processing with Rejection Breakdown on Filtration Hits\"\n", - " )\n", + " ax.set_title(\"Similarity Search Processing with Rejection Breakdown\")\n", "\n", - " # Split legend into main vs. filter categories\n", + " # Split legend into main vs. filter\n", " handles, labels = ax.get_legend_handles_labels()\n", - " main_labels = list(MAIN_CATEGORIES.values())\n", - " filter_labels = list(FILTER_REASONS.values())\n", + " main_labels = MAIN_CATEGORIES\n", + " filter_labels = FILTER_REASONS\n", "\n", " legend1 = ax.legend(\n", - " [handles[labels.index(l)] for l in main_labels],\n", + " [handles[labels.index(l)] for l in main_labels if l in labels],\n", " main_labels,\n", " loc=\"upper left\",\n", " bbox_to_anchor=(1.02, 1),\n", " title=\"Hit Process\",\n", " )\n", " legend2 = ax.legend(\n", - " [handles[labels.index(l)] for l in filter_labels],\n", + " [handles[labels.index(l)] for l in filter_labels if l in labels],\n", " filter_labels,\n", " loc=\"upper left\",\n", " bbox_to_anchor=(1.02, 0.55),\n", @@ -2160,8 +1243,417 @@ " plt.savefig(output_path)\n", " plt.show()\n", "\n", - "main_summary, overlay_summary = load_and_summarize_data(overview_table)\n", - "plot_summary(main_summary, overlay_summary, overview_plot)\n" + "# === Execute ===\n", + "main_df, filter_df = load_and_summarize_data(overview_table)\n", + "plot_summary(main_df, filter_df, overview_plot)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# 9. Create Abundance data\n", + "# Selfwritten python script \"single_genera_abundance_table.py\"\n", + "# Input: Overview table created iteritavely within the snakemake run\n", + "# Output: barplots for all samples showing generated and filtered similarity search hits\n", + "# Note: Overview Table is created here after the process while in the original snakemake run\n", + "# it's created iteratively within the workflow.\n", + "\n", + "import pandas as pd\n", + "import os, sys\n", + "\n", + "def write_dummy_line(sample_name):\n", + " dummy_line = {\n", + " \"sample\": sample_name,\n", + " \"AMR Gene Family\": \"NA\",\n", + " \"genus\": \"NA\",\n", + " \"genus_count\": 0,\n", + " \"total_count\": 0,\n", + " \"relative_genus_count\": 0,\n", + " }\n", + " merged_data = pd.DataFrame([dummy_line])\n", + " return merged_data\n", + "\n", + "def process_combined_data(combined_data, sample_name):\n", + "\n", + " combined_data[\"sample\"] = sample_name\n", + "\n", + " genus_counts = (\n", + " combined_data.groupby([\"sample\", \"AMR Gene Family\", \"genus\"])\n", + " .size()\n", + " .reset_index(name=\"genus_count\")\n", + " )\n", + "\n", + " total_counts = (\n", + " genus_counts.groupby([\"sample\", \"AMR Gene Family\"])[\"genus_count\"]\n", + " .sum()\n", + " .reset_index(name=\"total_count\")\n", + " )\n", + "\n", + " genus_counts = pd.merge(\n", + " genus_counts, total_counts, on=[\"sample\", \"AMR Gene Family\"], how=\"left\"\n", + " )\n", + " genus_counts[\"relative_genus_count\"] = round(\n", + " genus_counts[\"genus_count\"] / genus_counts[\"total_count\"], 4\n", + " )\n", + "\n", + " return genus_counts\n", + "\n", + "\n", + "def export_genera_abundance(input_files, sample_name, parts, output_path):\n", + " sample_input_files = [f for f in input_files]\n", + " part_dfs = []\n", + " for part in parts:\n", + " matching_files = [f for f in sample_input_files]\n", + " print(sample_input_files,matching_files)\n", + " if not matching_files:\n", + " continue\n", + " input_file = matching_files[0]\n", + " df = pd.read_csv(\n", + " input_file, sep=\",\", header=0\n", + " )\n", + " part_dfs.append(df)\n", + "\n", + " if not part_dfs:\n", + " print(f\"No valid parts found for sample: {sample_name}\")\n", + " dummy_df = write_dummy_line(sample_name)\n", + " dummy_df.to_csv(output_path, index=False)\n", + " return \n", + "\n", + " full_sample_df = pd.concat(part_dfs, ignore_index=True)\n", + " processed_data = process_combined_data(full_sample_df, sample_name)\n", + "\n", + " processed_data = processed_data.sort_values(\n", + " by=[\"sample\", \"genus_count\"], ascending=False\n", + " )\n", + "\n", + " display(processed_data)\n", + "\n", + "\n", + "if __name__ == \"__main__\":\n", + " base = pathlib.Path(os.path.dirname(pathlib.Path().resolve()))\n", + " result_dir = base / \".test_steps/results\"\n", + "\n", + " filter_result = result_dir / \"filtered_result.csv\"\n", + " table = result_dir / f\"single_abundance_table.csv\"\n", + " \n", + " filtered_fasta_files = filter_result\n", + " \n", + " input_file = filter_result\n", + " output_path = table\n", + " sample_name = \"test_epic_data\" \n", + " parts = [\"001\"]\n", + " export_genera_abundance([str(input_file)], sample_name, parts, output_path)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import pathlib\n", + "from IPython.core.display import HTML\n", + "\n", + "# === Paths ===\n", + "base = pathlib.Path().resolve()\n", + "result_dir = base / \"results\"\n", + "overview_table = result_dir / \"overview_table.txt\"\n", + "overview_html = \"overview_table.html\"\n", + "\n", + "# Read the input table\n", + "df = pd.read_csv(overview_table, sep=\",\", header=None, names=[\"sample\",\"step\",\"total_count\"])\n", + "\n", + "# Mapping step -> State\n", + "step_to_state = {\n", + " \"Number of FastQ input reads\": \"Input reads\",\n", + " \"Diamond output hits\": \"Similarity search\",\n", + " \"Usearch output hits\": \"Similarity search\",\n", + " \"Merged similarity hits\": \"Similarity search\",\n", + " \"Diamond hits < similarity threshold\": \"Filtration\",\n", + " \"Diamond hits NOT highest percentage identity per query\": \"Filtration\",\n", + " \"Usearch hits < similarity threshold\": \"Filtration\",\n", + " \"Usearch hits NOT highest percentage identity per query\": \"Filtration\",\n", + " \"Query hit in only one of two databases\": \"Filtration\",\n", + " \"Filtered fusion reads\": \"Output reads\"\n", + "}\n", + "\n", + "df[\"state\"] = df[\"step\"].map(step_to_state)\n", + "\n", + "# Reorder and sort\n", + "df = df[[\"sample\", \"state\", \"step\", \"total_count\"]]\n", + "state_order = [\"Input reads\", \"Similarity search\", \"Filtration\", \"Output reads\"]\n", + "df[\"state\"] = pd.Categorical(df[\"state\"], categories=state_order, ordered=True)\n", + "df = df.sort_values(by=[\"sample\", \"state\"])\n", + "\n", + "# === HTML with rowspan for merged cells ===\n", + "\n", + "html = \"\"\"\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + " \n", + "\n", + "\n", + "\"\"\"\n", + "\n", + "# Group and track rowspans\n", + "grouped = df.groupby([\"sample\", \"state\"])\n", + "for (sample, state), group in grouped:\n", + " sample_rowspan = len(df[df[\"sample\"] == sample])\n", + " state_rowspan = len(group)\n", + " \n", + " first_state = True\n", + " for i, row in group.iterrows():\n", + " html += \"\"\n", + " if i == df[df[\"sample\"] == sample].index[0]:\n", + " html += f''\n", + " if first_state:\n", + " html += f''\n", + " first_state = False\n", + " html += f\"\"\n", + " html += \"\"\n", + "\n", + "html += \"\"\"\n", + "\n", + "
SampleStateStepCount
{sample}{state}{row['step']}{row['total_count']}
\n", + "\n", + "\n", + "\"\"\"\n", + "display(HTML(html))\n", + "# Write to file\n", + "with open(overview_html, \"w\") as f:\n", + " f.write(html)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import pathlib\n", + "from IPython.core.display import HTML\n", + "\n", + "# === Paths ===\n", + "base = pathlib.Path().resolve()\n", + "result_dir = base / \"results\"\n", + "overview_table = result_dir / \"genera_abundance.csv\"\n", + "overview_html = \"\"\n", + "\n", + "# Read the input table\n", + "df = pd.read_csv(overview_table, sep=\",\", header=0)\n", + "\n", + "# === HTML with rowspan for merged cells ===\n", + "\n", + "html = \"\"\"\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + " \n", + "\n", + "\n", + "\"\"\"\n", + "\n", + "# Group and track rowspans\n", + "grouped = df.groupby([\"sample\", \"AMR Gene Family\"])\n", + "for (sample, family), group in grouped:\n", + " sample_rowspan = len(df[df[\"sample\"] == sample])\n", + " family_rowspan = len(group)\n", + " amr = df[(df[\"sample\"] == sample) & (df[\"AMR Gene Family\"] == family)]\n", + " reads_per_amr = amr[\"genus_count\"].sum()\n", + " amr_line = f\"{family}
Total Fusion Reads: {reads_per_amr}\"\n", + " first_family = True\n", + " for i, row in group.iterrows():\n", + " html += \"\"\n", + " if i == df[df[\"sample\"] == sample].index[0]:\n", + " html += f''\n", + " if first_family:\n", + " html += f''\n", + " first_family = False\n", + " html += f\"\"\n", + " html += \"\"\n", + "\n", + "html += \"\"\"\n", + "\n", + "
SampleAMR Gene FamilyGenusFusion Read CountRelative
{sample}{amr_line}{row['genus']}{row['genus_count']}{row['relative_genus_count']}
\n", + "\n", + "\n", + "\"\"\"\n", + "display(HTML(html))\n", + "# Write to file\n", + "#with open(overview_html, \"w\") as f:\n", + "# f.write(html)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "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", + "
query_idsubject_idperc_identityalign_lengthmismatchesgap_opensq_startq_ends_starts_endevaluebit_scorepartARO Namedistanceorientationgenus
0dummy.dummydummy10000000000016Sdummy0dummydummy
\n", + "
" + ], + "text/plain": [ + " query_id subject_id perc_identity align_length mismatches gap_opens \\\n", + "0 dummy.dummy dummy 100 0 0 0 \n", + "\n", + " q_start q_end s_start s_end evalue bit_score part ARO Name distance \\\n", + "0 0 0 0 0 0 0 16S dummy 0 \n", + "\n", + " orientation genus \n", + "0 dummy dummy " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "\"\"\"Write a dummy line to ensure compatibility with downstream analysis\"\"\"\n", + "part = \"16S\"\n", + "additional_columns = [\n", + " \"part\",\n", + " \"ARO Name\",\n", + " \"distance\",\n", + " \"orientation\",\n", + " \"genus\"\n", + "]\n", + "\n", + "if part == \"ABR\":\n", + " header = blast_columns + additional_columns\n", + " dummy_row = [\n", + " \"dummy.dummy\", \"dummy\", \"100\", + [\"0\"]*9,\n", + " \"ABR\", \"dummy\", \"0\", \"dummy\", + [\"0\"]*3, + [\"dummy\"]*8\n", + " ]\n", + "elif part == \"16S\":\n", + " header = blast_columns + additional_columns\n", + " dummy_row = [\"dummy.dummy\", \"dummy\", \"100\"] + [\"0\"]*9 + [\"16S\", \"dummy\", \"0\", \"dummy\", \"dummy\"]\n", + "else:\n", + " raise ValueError(\"Invalid part specified. Must be 'ABR' or '16S'.\")\n", + "dummy_df = pd.DataFrame([dummy_row], columns=header)\n", + "display(dummy_df)" ] }, { diff --git a/config/config.yaml b/config/config.yaml index de55b8f..87dcb65 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -7,6 +7,7 @@ fastq_dir: "data/fastq" # Copy target fastq.gz files in ERMA/data/fastq or chang outdir: "results" # Output directory of the final report min_similarity: "0.8" # threshold to pre-filter blast hits by percentage identity +min_abundance: "0.01" # genera with lower abundance will be binned as "Other" in stacked bar abundance plot silva: download_path_seq: "https://www.arb-silva.de/fileadmin/silva_databases/release_138_2/Exports/SILVA_138.2_SSURef_NR99_tax_silva.fasta.gz" @@ -18,7 +19,7 @@ card: num_parts: 1 # number of chunks the fastqs are split into max_threads: 16 -similarity_search_mode: "test" # Put here "test" or "full" for strand/s to be included in the similarity search +similarity_search_mode: "full" # Put here "test" or "full" for strand/s to be included in the similarity search ### Preprocessing ### # if data is already in format 'one fastq.gz per sample', this section can be ignored diff --git a/report/abundance_bubble_plot.rst b/report/abundance_bubble_plot.rst index a650283..334f993 100644 --- a/report/abundance_bubble_plot.rst +++ b/report/abundance_bubble_plot.rst @@ -1,7 +1,8 @@ Abundance Bubble Plot The Abundance Bubble Plot visualizes the distribution and abundance of genera linked to antimicrobial resistance genes. -Each bubble represents a genus-AMR gene pair, with the bubble size indicating the relative count of similarity search hits found for this genus-AMR pair per sample. The colour of the bubble indicates the total counts of AMR-genus hits after filtering. +Each bubble represents a genus-AMR gene pair, with the bubble size indicating the relative count of similarity search hits found for this genus-AMR pair per sample. +The colour of the bubble indicates the number of assigned fusion reads after filtering. The position of bubbles allows comparisons of genera and resistance genes across samples. The bubble plot is generated once for every AMR with more than 100 total respective similarity search hits. For representability, only 20 genera are shown. diff --git a/report/abundance_data.rst b/report/abundance_data.rst new file mode 100644 index 0000000..8958d55 --- /dev/null +++ b/report/abundance_data.rst @@ -0,0 +1,7 @@ +Abundance data + +The Abundance data table shows a breakdown of the number of Fusion Reads per Genus and the assignment to AMR Gene Family and Sample. +Additionally, a relative distribution is shown related to the Total Fusion Read sum per sample. +The Total Fusion Read sum per AMR Gene Family is shown beneath the AMR name. + +This data is the basis for the abundance bubble plot and the Stacked bar abundance plot. \ No newline at end of file diff --git a/report/attrition_plot.rst b/report/attrition_plot.rst new file mode 100644 index 0000000..b6270c7 --- /dev/null +++ b/report/attrition_plot.rst @@ -0,0 +1,23 @@ +Attrition Plot + +The Plot shows the number for: + +- all fasta files (Number of FastQ input reads), + +- the sum of generated hits through similarity search before (Merged similarity hits), + +- and after filtering (Filtered fusion reads) + +In front of the second plot, there is a thinner stacked bar showing the count discrepancy between unfiltered and filtered hits. +The reasons for filtering are explained as following: +- "Diamond hits < similarity threshold": All hits that have a smaller query/subject - percentage identity as the minimum threshold defined in the config file + +- "Diamond hits NOT highest percentage identity per query": Only the hits with the maximum percentage identity per query ID are accepted. The number of hits filtered for this reason can be seen here + +- "Usearch hits > similarity threshold": analogous to "ABR < similarity threshold" + +- "Usearch hits NOT highest percentage identity per query": analogous to "ABR Hit not max identity for query ID" + +- "Query hit in only one of two databases": In the final filtering step, only those hits gets accepted for which querys can be found in both databases after the previous filterings + +Raw data used for this plot can be found in "4. QC/Count Overview Table" \ No newline at end of file diff --git a/report/count_overview_per_sample.rst b/report/count_overview_per_sample.rst deleted file mode 100644 index 7a94c45..0000000 --- a/report/count_overview_per_sample.rst +++ /dev/null @@ -1,23 +0,0 @@ -Count Overview per sample - -The Table shows an count overview of input, intermediate and output files: - -- fasta input: number of reads of the raw fastq/fasta file, - -- diamond output: number of hits, the diamond (ABR) similarity search generated on this sample, - -- usearch output: number of hits, the usearch (16S) similarity search generated on this sample, - -- integration output: number of lines resulting from the merging of diamond and usearch results - -- filtered min similarity ABR: lines filtered through user defined minimum percentage identity threshold - -- filtered max identity ABR: lines filtered since only the maximum percentage identity per query ID is accepted - -- filtered min similarity 16S: lines filtered through user defined minimum percentage identity threshold - -- filtered max identity 16S: lines filtered since only the maximum percentage identity per query ID is accepted - -- filtered query id mismatch: hits filtered because query ID is not found in both databases - -- filtration output: number of lines in the final fight \ No newline at end of file diff --git a/report/reads_per_AMR.rst b/report/reads_per_AMR.rst deleted file mode 100644 index 8728199..0000000 --- a/report/reads_per_AMR.rst +++ /dev/null @@ -1,4 +0,0 @@ -Reads per AMR Gene Family - -The Reads per AMR Gene Family table shows the number of reads mapped to each found antimicrobial resistance (AMR) gene family. -It provides insights into the general AMR distribution in the sample set. \ No newline at end of file diff --git a/report/stacked_bar_abundance_plot.rst b/report/stacked_bar_abundance_plot.rst new file mode 100644 index 0000000..b0f461e --- /dev/null +++ b/report/stacked_bar_abundance_plot.rst @@ -0,0 +1,7 @@ +Stacked bar abundance plot + +The Interactive plot visualizes the relative abundance of genera per sample according to assigned fusion reads after filtration. + +According to the 'min_abundance'-parameter in the config file, genera with a lower abundance than this threshold are binned to "Other". + +If several AMR's are found with a Fusion Read count of more than 1% to the total, one plot is created per AMR. \ No newline at end of file diff --git a/workflow/Snakefile b/workflow/Snakefile index 403fc40..0235e39 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -7,12 +7,12 @@ configfile: "config/config.yaml" include: "rules/common.smk" -include: "rules/qc.smk" include: "rules/load_db.smk" +include: "rules/preprocess.smk" +include: "rules/qc.smk" include: "rules/simsearch_and_process.smk" include: "rules/boxplots.smk" -include: "rules/abundance.smk" -include: "rules/preprocess.smk" +include: "rules/visualizing.smk" # Build a list of all "fastq.gz" files in "ERMA/data/fastq" which will be processed subsequently samples = [ @@ -48,15 +48,15 @@ report: "../report/workflow.rst" rule snakemake_report: input: + local("results/abundance/stacked_bar_abundance_plot.html"), local("results/abundance/combined_genus_abundance_bubbleplot.html"), - local("results/abundance/reads_per_found_AMR.html"), + local("results/abundance/abundance_data.html"), local("results/boxplots/combined_allength_boxplot.png"), local("results/boxplots/combined_evalue_boxplot.png"), local("results/boxplots/combined_percidt_boxplot.png"), local("results/qc/multiqc.html"), - local("results/qc/overview_plot.png"), - local(expand("results/{sample}/overview_table.html", sample=samples)), - local(expand("results/{sample}/genus_abundance.html", sample=samples)), + local("results/qc/attrition_plot.png"), + local("results/qc/overview_table.html"), local(expand("results/{sample}/genus_idt_per_genus_plot.png", sample=samples)), log: local("logs/report/report.log"), diff --git a/workflow/rules/abundance.smk b/workflow/rules/abundance.smk deleted file mode 100644 index 39b211c..0000000 --- a/workflow/rules/abundance.smk +++ /dev/null @@ -1,92 +0,0 @@ -# Copyright 2024 Adrian Dörr. -# Licensed under the MIT License (https://opensource.org/license/mit) -# This file may not be copied, modified, or distributed -# except according to those terms. - - -rule single_genera_abundance_table: - input: - filtered_data=local( - expand( - "results/{{sample}}/{part}/filtered_results.csv.gz", - part=get_numpart_list(), - ) - ), - output: - report( - local("results/{sample}/genus_abundance.html"), - caption="../../report/genus_abundance_table.rst", - category="2. Single Sample Abundance Data", - subcategory="{sample}", - labels={"sample": "{sample}", "table": "Genera Abundance"}, - ), - params: - sample_name="{sample}", - parts=get_numpart_list(), - log: - local("logs/{sample}/genera_abundance_table.log"), - conda: - "../envs/python.yaml" - threads: config["max_threads"] - script: - "../scripts/single_genera_abundance_table.py" - - -rule combined_genera_abundance_table: - input: - filtered_data=local( - expand( - "results/{sample}/{part}/filtered_results.csv.gz", - sample=samples, - part=get_numpart_list(), - ) - ), - output: - csv=local("results/abundance/combined_genus_abundance.csv"), - params: - sample_name=samples, - log: - local("logs/genera_abundance_table.log"), - conda: - "../envs/python.yaml" - threads: config["max_threads"] - script: - "../scripts/combined_genera_abundance_table.py" - - -rule abundance_bubble_plot: - input: - abundance_data=local("results/abundance/combined_genus_abundance.csv"), - output: - report( - local("results/abundance/combined_genus_abundance_bubbleplot.html"), - caption="../../report/abundance_bubble_plot.rst", - category="1. Combined Abundance Data", - labels={"figure": "Abundance Bubble Plot"}, - ), - log: - local("logs/genera_abundance_plot.log"), - conda: - "../envs/python.yaml" - threads: config["max_threads"] - script: - "../scripts/combined_genera_abundance_plot.py" - - -rule reads_per_AMR: - input: - abundance_data=local("results/abundance/combined_genus_abundance.csv"), - output: - report( - local("results/abundance/reads_per_found_AMR.html"), - caption="../../report/reads_per_AMR.rst", - category="1. Combined Abundance Data", - labels={"table": "Reads per AMR"}, - ), - log: - local("logs/genera_abundance_plot.log"), - conda: - "../envs/python.yaml" - threads: config["max_threads"] - script: - "../scripts/combined_reads_per_amr.py" diff --git a/workflow/rules/boxplots.smk b/workflow/rules/boxplots.smk index 27869e2..d6b0c5f 100644 --- a/workflow/rules/boxplots.smk +++ b/workflow/rules/boxplots.smk @@ -16,9 +16,9 @@ rule generate_percidt_genus: report( local("results/{sample}/genus_idt_per_genus_plot.png"), caption="../../report/identity_read_count_per_genus.rst", - category="2. Single Sample Abundance Data", + category="2. Samplewise Genus per Percentage Identity", subcategory="{sample}", - labels={"sample": "{sample}", "figure": "Identity/Read Count per Genus"}, + labels={"sample": "{sample}", "Plot": "Identity/Read Count per Genus"}, ), log: local("logs/generate_percidt_genus/{sample}.log"), diff --git a/workflow/rules/qc.smk b/workflow/rules/qc.smk index 1f11c7d..feaed95 100644 --- a/workflow/rules/qc.smk +++ b/workflow/rules/qc.smk @@ -37,7 +37,7 @@ rule multiqc_report: local("results/qc/multiqc.html"), caption="../../report/multiqc.rst", category="4. QC", - labels={"File": "MultiQC Report"}, + labels={"HTML": "MultiQC Report"}, ), log: local("logs/multiqc/multiqc.log"), @@ -45,39 +45,7 @@ rule multiqc_report: "v6.0.1/bio/multiqc" -rule merge_overview_per_sample: - input: - checkpoint=local( - expand( - "results/{sample}/{part}/checkpoint.txt", - sample=samples, - part=get_numpart_list(), - ) - ), - overview_tables=local( - expand( - "results/{{sample}}/{part}/overview_table.txt", part=get_numpart_list() - ) - ), - output: - report( - local("results/{sample}/overview_table.html"), - caption="../../report/count_overview_per_sample.rst", - category="2. Single Sample Abundance Data", - subcategory="{sample}", - labels={"sample": "{sample}", "table": "Count Overview"}, - ), - params: - sample_name="{sample}", - log: - local("logs/merge_overview/{sample}/combined.log"), - conda: - "../envs/python.yaml" - script: - "../scripts/merge_overview_smpl.py" - - -rule merge_overview_to_one: +rule table_overview_to_one: input: checkpoint=local( expand( @@ -98,28 +66,8 @@ rule merge_overview_to_one: params: sample_name=samples, log: - local("logs/merge_overview/combined.log"), - conda: - "../envs/python.yaml" - script: - "../scripts/merge_overview_all.py" - - -rule plot_overview: - input: - overview_table=local("results/qc/overview_table.txt"), - output: - report( - local("results/qc/overview_plot.png"), - caption="../../report/count_overview.rst", - category="4. QC", - labels={"File": "Count Overview"}, - ), - params: - sample_name=samples, - log: - local("logs/plot_overview/combined.log"), + local("logs/table_overview/combined.log"), conda: "../envs/python.yaml" script: - "../scripts/plot_overview.py" + "../scripts/table_overview_all.py" diff --git a/workflow/rules/simsearch_and_process.smk b/workflow/rules/simsearch_and_process.smk index cfb6c09..95ae5d0 100644 --- a/workflow/rules/simsearch_and_process.smk +++ b/workflow/rules/simsearch_and_process.smk @@ -21,8 +21,8 @@ rule diamond_card: shell: """ diamond blastx -d {input.card} -q {input.fasta} -o {output.card_results} --outfmt 6 --evalue 1e-5 --quiet --threads {params.internal_threads} 2> {log} - echo -ne "fastq input reads,{wildcards.sample},{wildcards.part},$(cat {input.fasta}|grep -c '^>')\n" >> {output.overview_table} - echo -ne "diamond output hits,{wildcards.sample},{wildcards.part},$(cat {output.card_results}|wc -l)\n" >> {output.overview_table} + echo -ne "Number of FastQ input reads,{wildcards.sample},{wildcards.part},$(cat {input.fasta}|grep -c '^>')\n" >> {output.overview_table} + echo -ne "Diamond output hits,{wildcards.sample},{wildcards.part},$(cat {output.card_results}|wc -l)\n" >> {output.overview_table} """ @@ -46,7 +46,7 @@ if config["similarity_search_mode"] == "test": shell: """ usearch -usearch_local {input.fasta} -db {input.silva} -blast6out {output.silva_results} -evalue 1e-5 -threads {params.internal_threads} -strand plus -mincols 200 > {log} 2>&1 - echo -ne "usearch output hits,{wildcards.sample},{wildcards.part},$(cat {output.silva_results}|wc -l)\n" >> {input.overview_table} + echo -ne "Usearch output hits,{wildcards.sample},{wildcards.part},$(cat {output.silva_results}|wc -l)\n" >> {input.overview_table} """ @@ -70,7 +70,7 @@ if config["similarity_search_mode"] == "full": shell: """ usearch -usearch_local {input.fasta} -db {input.silva} -blast6out {output.silva_results} -evalue 1e-5 -threads {params.internal_threads} -strand both -mincols 200 2> {log} - echo -ne "usearch output hits,{wildcards.sample},{wildcards.part},$(cat {output.silva_results}|wc -l)\n" >> {input.overview_table} + echo -ne "Usearch output hits,{wildcards.sample},{wildcards.part},$(cat {output.silva_results}|wc -l)\n" >> {input.overview_table} """ @@ -80,8 +80,6 @@ rule integrate_blast_data: card_results=local("results/{sample}/{part}/card_results.txt"), silva_results=local("results/{sample}/{part}/SILVA_results.txt"), aro_mapping=local("data/card_db/aro_index.tsv"), - dummy_ABR=local("data/dummy/ABR.dummy"), - dummy_16S=local("data/dummy/16S.dummy"), output: intermed_card_results=local( temp("results/{sample}/{part}/intermed_card_results.csv") @@ -139,3 +137,25 @@ rule gzip_intermediates: gzip {input.filt_data} 2>> {log} touch {output.checkpoint} """ + + +rule table_combined_genera_abundance: + input: + filtered_data=local( + expand( + "results/{sample}/{part}/filtered_results.csv.gz", + sample=samples, + part=get_numpart_list(), + ) + ), + output: + csv=local("results/abundance/combined_genus_abundance.csv"), + params: + sample_name=samples, + log: + local("logs/genera_abundance_table.log"), + conda: + "../envs/python.yaml" + threads: config["max_threads"] + script: + "../scripts/table_combined_genera_abundance.py" diff --git a/workflow/rules/visualizing.smk b/workflow/rules/visualizing.smk new file mode 100644 index 0000000..b94d8af --- /dev/null +++ b/workflow/rules/visualizing.smk @@ -0,0 +1,99 @@ +# Copyright 2024 Adrian Dörr. +# Licensed under the MIT License (https://opensource.org/license/mit) +# This file may not be copied, modified, or distributed +# except according to those terms. + +rule plot_abundance_data: + input: + abundance_data=local("results/abundance/combined_genus_abundance.csv"), + output: + report( + local("results/abundance/abundance_data.html"), + caption="../../report/abundance_data.rst", + category="1. Combined Abundance Data", + labels={"HTML": "Abundance data"}, + ), + log: + local("logs/genera_abundance_plot.log"), + conda: + "../envs/python.yaml" + threads: config["max_threads"] + script: + "../scripts/plot_abundance_data.py" + + +rule plot_abundance_bubble: + input: + abundance_data=local("results/abundance/combined_genus_abundance.csv"), + output: + report( + local("results/abundance/combined_genus_abundance_bubbleplot.html"), + caption="../../report/abundance_bubble_plot.rst", + category="1. Combined Abundance Data", + labels={"figure": "Abundance Bubble Plot"}, + ), + log: + local("logs/genera_abundance_plot.log"), + conda: + "../envs/python.yaml" + threads: config["max_threads"] + script: + "../scripts/plot_abundance_bubble.py" + + +rule plot_stacked_bar_abundance: + input: + abundance_data=local("results/abundance/combined_genus_abundance.csv"), + output: + report( + local("results/abundance/stacked_bar_abundance_plot.html"), + caption="../../report/stacked_bar_abundance_plot.rst", + category="1. Combined Abundance Data", + labels={"figure": "Stacked Bar Abundance Plot"}, + ), + log: + local("logs/stacked_bar_abundance_plot.log"), + params: + min_abundance = config["min_abundance"] + conda: + "../envs/python.yaml" + threads: config["max_threads"] + script: + "../scripts/plot_stacked_bar_abundance.py" + + +rule plot_overview_table: + input: + overview_table=local("results/qc/overview_table.txt"), + output: + report( + local("results/qc/overview_table.html"), + caption="../../report/count_overview.rst", + category="4. QC", + labels={"HTML": "Count Overview Table"}, + ), + log: + local("logs/plot_overview_table/combined.log"), + conda: + "../envs/python.yaml" + script: + "../scripts/plot_overview_table.py" + +rule plot_attrition: + input: + overview_table=local("results/qc/overview_table.txt"), + output: + report( + local("results/qc/attrition_plot.png"), + caption="../../report/attrition_plot.rst", + category="4. QC", + labels={"Plot": "Attrition"}, + ), + params: + sample_name=samples, + log: + local("logs/plot_attrition/combined.log"), + conda: + "../envs/python.yaml" + script: + "../scripts/plot_attrition.py" diff --git a/workflow/scripts/boxplot_align_lengths.py b/workflow/scripts/boxplot_align_lengths.py index d48db79..42c6429 100644 --- a/workflow/scripts/boxplot_align_lengths.py +++ b/workflow/scripts/boxplot_align_lengths.py @@ -6,72 +6,90 @@ import os import sys -import gzip import pandas as pd import seaborn as sns import matplotlib.pyplot as plt +""" +This script takes a list of all filtered fasta files, combines param information +across samples, and visualizes the distribution of param using boxplots split +by part (ABR/16S) and sample. +""" -def read_and_process_partitioned_data(partition_files, sample): +PRETTY_LABELS = { + "align_length": "Alignment length", + "perc_identity": "Percentage identity", + "evalue": "E-value", +} + + +def read_and_process_partitioned_data(partition_files, sample, param): """Read and process partitioned files for a single sample.""" data_frames = [] sample_name = sample - + param = param for part_file in partition_files: if os.path.exists(part_file): - fields = ["query_id", "align_length", "part"] - df = pd.read_csv( - part_file, header=0, sep=",", usecols=fields, compression="gzip" + df = pd.read_csv(part_file, header=0, sep=",") + df[f"{param}_ABR"] = df[f"{param}_ABR"] * 3 + long_df = pd.melt( + df, + id_vars=["query_id"], + value_vars=[param + "_ABR", param + "_16S"], + var_name="part", + value_name=param, ) - df = df.drop_duplicates() - df["sample"] = sample_name - abr = df[df["part"] == "ABR"].copy() - sixts = df[df["part"] == "16S"] - abr["align_length"] *= 3 - merged_df = pd.concat([abr, sixts]) - data_frames.append(merged_df) - else: - print(f"File {part_file} does not exist.") + # Normalize part labels + long_df["part"] = long_df["part"].str.replace(param + "_", "") + long_df["sample"] = sample_name + data_frames.append(long_df) if data_frames: return pd.concat(data_frames) else: return None -def plot_boxplots(data, output_file): - """Plot boxplots based on the alignment lengths for ABR and 16S parts across samples.""" +def plot_boxplots(data, param, output_file): + """ + Generate and save boxplots of param across samples and parts (ABR vs. 16S). + + Args: + data (pd.DataFrame): Combined dataframe containing 'sample', param, and 'part'. + output_file (str): Path to save the resulting plot. + """ plt.figure(figsize=(15, 10)) flierprops = dict(markerfacecolor="0.75", markersize=2, linestyle="none") - sns.boxplot( - x="sample", y="align_length", hue="part", data=data, flierprops=flierprops - ) + sns.boxplot(x="sample", y=param, hue="part", data=data, flierprops=flierprops) plt.title( - "Boxplot of alignment lengths for ABR and 16S parts across samples -Filtered-" + f"Boxplot of {PRETTY_LABELS[param]} for ABR and 16S parts across samples -Filtered-" ) plt.xlabel("Sample") - plt.ylabel("Alignment length") + plt.ylabel(f"{PRETTY_LABELS[param]}") plt.xticks(rotation=45) plt.tight_layout() plt.savefig(output_file) plt.close() -def main(filtered_fasta_files, sample_names, output_file): +def main(filtered_fasta_files, sample_names, param, output_file): """Main function to process partitioned files for each sample and generate the plot.""" all_data = [] # Loop over each sample's partitioned CSV files for sample in sample_names: data = read_and_process_partitioned_data( - [file for file in filtered_fasta_files if str(sample) in file], sample + [file for file in filtered_fasta_files if str(sample) in file], + sample, + param, ) + data = data[data[param] > 0] if data is not None: all_data.append(data) if all_data: combined_data = pd.concat(all_data) - plot_boxplots(combined_data, output_file) + plot_boxplots(combined_data, param, output_file) else: print("No data found.") @@ -83,4 +101,5 @@ def main(filtered_fasta_files, sample_names, output_file): output_file = snakemake.output[0] # Path to save the output boxplot sample_name = sorted(snakemake.params.sample_name) # Minimum similarity filter sys.stderr = open(snakemake.log[0], "w") - main(filtered_fasta_files, sample_name, output_file) + param = "align_length" + main(filtered_fasta_files, sample_name, param, output_file) diff --git a/workflow/scripts/boxplot_evalue.py b/workflow/scripts/boxplot_evalue.py index 296724b..fcdbd59 100644 --- a/workflow/scripts/boxplot_evalue.py +++ b/workflow/scripts/boxplot_evalue.py @@ -11,37 +11,38 @@ import matplotlib.pyplot as plt """ -This script takes a list of all filtered fasta files, combines e-value information -across samples, and visualizes the distribution of e-values using boxplots split +This script takes a list of all filtered fasta files, combines param information +across samples, and visualizes the distribution of param using boxplots split by part (ABR/16S) and sample. """ +PRETTY_LABELS = { + "align_length": "Alignment length", + "perc_identity": "Percentage identity", + "evalue": "E-value", +} -def read_and_process_partitioned_data(partition_files, sample): - """ - Read and process filtered data files for a given sample. - - Args: - partition_files (list of str): Paths to CSV files corresponding to sample parts. - sample (str): Sample name. - Returns: - pd.DataFrame or None: Combined DataFrame of filtered entries or None if files missing. - """ +def read_and_process_partitioned_data(partition_files, sample, param): + """Read and process partitioned files for a single sample.""" data_frames = [] sample_name = sample - + param = param for part_file in partition_files: if os.path.exists(part_file): - fields = ["query_id", "evalue", "part"] - df = pd.read_csv( - part_file, header=0, sep=",", usecols=fields, compression="gzip" + df = pd.read_csv(part_file, header=0, sep=",") + long_df = pd.melt( + df, + id_vars=["query_id"], + value_vars=[param + "_ABR", param + "_16S"], + var_name="part", + value_name=param, ) - df = df.drop_duplicates() - df["sample"] = sample_name - data_frames.append(df) - else: - print(f"File {part_file} does not exist.") + + # Normalize part labels + long_df["part"] = long_df["part"].str.replace(param + "_", "") + long_df["sample"] = sample_name + data_frames.append(long_df) if data_frames: return pd.concat(data_frames) @@ -49,42 +50,47 @@ def read_and_process_partitioned_data(partition_files, sample): return None -def plot_boxplots(data, output_file): +def plot_boxplots(data, param, output_file): """ - Generate and save boxplots of e-values across samples and parts (ABR vs. 16S). + Generate and save boxplots of param across samples and parts (ABR vs. 16S). Args: - data (pd.DataFrame): Combined dataframe containing 'sample', 'evalue', and 'part'. + data (pd.DataFrame): Combined dataframe containing 'sample', param, and 'part'. output_file (str): Path to save the resulting plot. """ plt.figure(figsize=(15, 10)) flierprops = dict(markerfacecolor="0.75", markersize=2, linestyle="none") - sns.boxplot(x="sample", y="evalue", hue="part", data=data, flierprops=flierprops) + sns.boxplot(x="sample", y=param, hue="part", data=data, flierprops=flierprops) plt.yscale("log") - plt.title("Boxplot of e-values for ABR and 16S parts across samples -Filtered-") + plt.title( + f"Boxplot of {PRETTY_LABELS[param]} for ABR and 16S parts across samples -Filtered-" + ) plt.xlabel("Sample") - plt.ylabel("E-value (log scale)") + plt.ylabel(f"{PRETTY_LABELS[param]}") plt.xticks(rotation=45) plt.tight_layout() plt.savefig(output_file) plt.close() -def main(filtered_fasta_files, sample_names, output_file): +def main(filtered_fasta_files, sample_names, param, output_file): """Main function to process partitioned files for each sample and generate the plot.""" all_data = [] # Loop over each sample's partitioned CSV files for sample in sample_names: data = read_and_process_partitioned_data( - [file for file in filtered_fasta_files if str(sample) in file], sample + [file for file in filtered_fasta_files if str(sample) in file], + sample, + param, ) + data = data[data[param] > 0] if data is not None: all_data.append(data) if all_data: combined_data = pd.concat(all_data) - plot_boxplots(combined_data, output_file) + plot_boxplots(combined_data, param, output_file) else: print("No data found.") @@ -93,7 +99,8 @@ def main(filtered_fasta_files, sample_names, output_file): filtered_fasta_files = sorted( snakemake.input.filtered_data ) # List of all filtered fasta files files - output_file = snakemake.output[0] # Path to save the output plot + output_file = snakemake.output[0] # Path to save the output boxplot sample_name = sorted(snakemake.params.sample_name) # Minimum similarity filter sys.stderr = open(snakemake.log[0], "w") - main(filtered_fasta_files, sample_name, output_file) + param = "evalue" + main(filtered_fasta_files, sample_name, param, output_file) diff --git a/workflow/scripts/boxplot_percidt.py b/workflow/scripts/boxplot_percidt.py index 5c7d41d..e118a8f 100644 --- a/workflow/scripts/boxplot_percidt.py +++ b/workflow/scripts/boxplot_percidt.py @@ -10,23 +10,39 @@ import seaborn as sns import matplotlib.pyplot as plt +""" +This script takes a list of all filtered fasta files, combines param information +across samples, and visualizes the distribution of param using boxplots split +by part (ABR/16S) and sample. +""" -def read_and_process_partitioned_data(partition_files, sample): +PRETTY_LABELS = { + "align_length": "Alignment length", + "perc_identity": "Percentage identity", + "evalue": "E-value", +} + + +def read_and_process_partitioned_data(partition_files, sample, param): """Read and process partitioned files for a single sample.""" data_frames = [] sample_name = sample - + param = param for part_file in partition_files: if os.path.exists(part_file): - fields = ["query_id", "perc_identity", "part"] - df = pd.read_csv( - part_file, header=0, sep=",", usecols=fields, compression="gzip" + df = pd.read_csv(part_file, header=0, sep=",") + long_df = pd.melt( + df, + id_vars=["query_id"], + value_vars=[param + "_ABR", param + "_16S"], + var_name="part", + value_name=param, ) - df = df.drop_duplicates() - df["sample"] = sample_name - data_frames.append(df) - else: - print(f"File {part_file} does not exist.") + + # Normalize part labels + long_df["part"] = long_df["part"].str.replace(param + "_", "") + long_df["sample"] = sample_name + data_frames.append(long_df) if data_frames: return pd.concat(data_frames) @@ -34,39 +50,47 @@ def read_and_process_partitioned_data(partition_files, sample): return None -def plot_boxplots(data, output_file): - """Plot boxplots based on the e-values for ABR and 16S parts across samples.""" +def plot_boxplots(data, param, output_file): + """ + Generate and save boxplots of param across samples and parts (ABR vs. 16S). + + Args: + data (pd.DataFrame): Combined dataframe containing 'sample', param, and 'part'. + output_file (str): Path to save the resulting plot. + """ plt.figure(figsize=(15, 10)) flierprops = dict(markerfacecolor="0.75", markersize=2, linestyle="none") - sns.boxplot( - x="sample", y="perc_identity", hue="part", data=data, flierprops=flierprops - ) + sns.boxplot(x="sample", y=param, hue="part", data=data, flierprops=flierprops) + # plt.yscale("log") plt.title( - "Boxplot of percentage identities for ABR and 16S parts across samples -Filtered-" + f"Boxplot of {PRETTY_LABELS[param]} for ABR and 16S parts across samples -Filtered-" ) plt.xlabel("Sample") - plt.ylabel("Percentage identity") + plt.ylabel(f"{PRETTY_LABELS[param]}") plt.xticks(rotation=45) plt.tight_layout() plt.savefig(output_file) plt.close() -def main(filtered_fasta_files, sample_names, output_file): +def main(filtered_fasta_files, sample_names, param, output_file): """Main function to process partitioned files for each sample and generate the plot.""" all_data = [] # Loop over each sample's partitioned CSV files for sample in sample_names: data = read_and_process_partitioned_data( - [file for file in filtered_fasta_files if str(sample) in file], sample + [file for file in filtered_fasta_files if str(sample) in file], + sample, + param, ) + data = data[data[param] > 0] if data is not None: all_data.append(data) if all_data: combined_data = pd.concat(all_data) - plot_boxplots(combined_data, output_file) + plot_boxplots(combined_data, param, output_file) else: print("No data found.") @@ -78,4 +102,5 @@ def main(filtered_fasta_files, sample_names, output_file): output_file = snakemake.output[0] # Path to save the output plot sample_name = sorted(snakemake.params.sample_name) # Minimum similarity filter sys.stderr = open(snakemake.log[0], "w") - main(filtered_fasta_files, sample_name, output_file) + param = "perc_identity" + main(filtered_fasta_files, sample_name, param, output_file) diff --git a/workflow/scripts/boxplot_percidt_per_genus.py b/workflow/scripts/boxplot_percidt_per_genus.py index 8184291..95373a2 100644 --- a/workflow/scripts/boxplot_percidt_per_genus.py +++ b/workflow/scripts/boxplot_percidt_per_genus.py @@ -9,22 +9,6 @@ import seaborn as sns import sys -necessary_columns = [ - "query_id", - "part", - "genus", - "AMR Gene Family", - "perc_identity", -] - -dtype_dict = { - "query_id": "string", - "part": "string", - "genus": "string", - "AMR Gene Family": "string", - "perc_identity": "float", -} - def generate_percentage_idt_per_genus(input_files, output_file): all_data = [] # List to hold DataFrames from all input files @@ -33,9 +17,7 @@ def generate_percentage_idt_per_genus(input_files, output_file): df = pd.read_csv( input_file, sep=",", - usecols=necessary_columns, header=0, - dtype=dtype_dict, compression="gzip", ) all_data.append(df) @@ -69,7 +51,7 @@ def generate_percentage_idt_per_genus(input_files, output_file): fig, ax1 = plt.subplots(figsize=(15, 8)) sns.boxplot( x="genus", - y="perc_identity", + y="perc_identity_16S", data=combined_data, ax=ax1, order=genus_order, @@ -94,7 +76,7 @@ def generate_percentage_idt_per_genus(input_files, output_file): color="purple", order=genus_order, ) - ax2.set_ylabel("Number of hits (bar)", color="violet") + ax2.set_ylabel("Number of fusion reads (bar)", color="violet") plt.tight_layout() plt.savefig(output_file) diff --git a/workflow/scripts/combined_reads_per_amr.py b/workflow/scripts/combined_reads_per_amr.py deleted file mode 100644 index 1d77e9e..0000000 --- a/workflow/scripts/combined_reads_per_amr.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright 2024 Adrian Dörr. -# Licensed under the MIT License (https://opensource.org/license/mit) -# This file may not be copied, modified, or distributed -# except according to those terms. - - -import pandas as pd -import plotly.express as px -import sys - -""" -This script takes the combined genus abundance table as input and counts -the number of hits in relation to respective AMR families. -""" - - -def create_bubble_plots(df, output): - """Group by unique AMR Gene Families and sum respective genus count""" - # Filter data for the current AMR Gene Family - df = pd.read_csv(df, header=0, sep=",") - total_counts_per_abr = ( - df.groupby("AMR Gene Family")["genus_count"].sum().reset_index() - ) - total_counts_per_abr.to_html(output) - - -if __name__ == "__main__": - input_files = snakemake.input.abundance_data - output_html = snakemake.output[0] - sys.stderr = open(snakemake.log[0], "w") - create_bubble_plots(input_files, output_html) diff --git a/workflow/scripts/filter_blast_results.py b/workflow/scripts/filter_blast_results.py index 2a511c9..cbe9c0e 100644 --- a/workflow/scripts/filter_blast_results.py +++ b/workflow/scripts/filter_blast_results.py @@ -34,12 +34,14 @@ def write_dummy_line(output_file): print("Detected only a dummy 16S line — generating merged dummy output.") dummy_line = { "query_id": "dummy", - "perc_identity": 0, - "align_length": 0, - "evalue": 0, - "part": "16S", - "genus": "Unclassified", "AMR Gene Family": "NA", + "perc_identity_ABR": 0, + "align_length_ABR": 0, + "evalue_ABR": 0, + "genus": "Unclassified", + "perc_identity_16S": 0, + "align_length_16S": 0, + "evalue_16S": 0, } dummy_df = pd.DataFrame(dummy_line, index=[0]) dummy_df.to_csv(output_file, index=False) @@ -66,6 +68,13 @@ def keep_max_identity_per_query(df): return merged +def keep_best_per_query(df): + """For each query_id, keep the row with the highest perc_identity and lowest evalue""" + return df.sort_values( + by=["query_id"] + ["perc_identity", "evalue"], ascending=[True, False, True] + ).drop_duplicates(subset="query_id", keep="first") + + def clean_16s_query_ids(df): """Remove anything after the first whitespace in 16S query IDs""" df["query_id"] = df["query_id"].str.split().str[0] @@ -81,6 +90,17 @@ def merge_parts_on_query_id(abr_data, s16_data): ) +def rename_for_merge(df, part): + df_renamed = df.rename( + columns={ + "perc_identity": "perc_identity_" + part, + "align_length": "align_length_" + part, + "evalue": "evalue_" + part, + } + ) + return df_renamed + + def write_summary(overview_table, sample, part, stats): """Write all filtering summary statistics to the overview file""" with open(overview_table, "a") as file: @@ -95,21 +115,29 @@ def filter_blast_results(input_file, output_file, min_similarity, overview_table overview_table, names=["state", "sample", "No", "total_count"] ) # ABR filtering - abr_filtered, abr_removed_identity = filter_by_identity(df, "ABR", min_similarity) - abr_final = keep_max_identity_per_query(abr_filtered) - abr_removed_max = len(abr_filtered) - len(abr_final) + abr_threshold_filtered, abr_removed_identity = filter_by_identity( + df, "ABR", min_similarity + ) + abr_best_identity = keep_max_identity_per_query(abr_threshold_filtered) + abr_best_query = keep_best_per_query(abr_best_identity) + abr_final = rename_for_merge(abr_best_query, "ABR") + abr_removed_max = len(abr_threshold_filtered) - len(abr_final) # 16S filtering - s16_filtered, s16_removed_identity = filter_by_identity(df, "16S", min_similarity) - s16_filtered = clean_16s_query_ids(s16_filtered) - s16_final = keep_max_identity_per_query(s16_filtered) - s16_removed_max = len(s16_filtered) - len(s16_final) + s16_threshold_filtered, s16_removed_identity = filter_by_identity( + df, "16S", min_similarity + ) + s16_cleaned = clean_16s_query_ids(s16_threshold_filtered) + s16_best_identity = keep_max_identity_per_query(s16_cleaned) + s16_best_query = keep_best_per_query(s16_best_identity) + s16_final = rename_for_merge(s16_best_query, "16S") + s16_removed_max = len(s16_threshold_filtered) - len(s16_final) # Handle dummy 16S result if len(s16_final) == 1 and s16_final.iloc[0]["query_id"] == "dummy.dummy": write_dummy_line(output_file) merge_output = df_overview.loc[ - df_overview["state"] == "merge output", "total_count" + df_overview["state"] == "Merged similarity hits", "total_count" ].values[0] filtered = ( abr_removed_identity @@ -121,24 +149,39 @@ def filter_blast_results(input_file, output_file, min_similarity, overview_table sample, part = os.path.normpath(input_file).split(os.sep)[-3:-1] # Write summary in case of dummy stats = { - "filtered min similarity ABR": "-" + str(abr_removed_identity), - "filtered max identity ABR": "-" + str(abr_removed_max), - "filtered min similarity 16S": "-" + str(s16_removed_identity), - "filtered max identity 16S": "-" + str(s16_removed_max), - "filtered query id mismatch": "-" + str(remaining), - "filtration output": 1, + "Diamond hits < similarity threshold": "-" + str(abr_removed_identity), + "Diamond hits NOT highest percentage identity per query": "-" + + str(abr_removed_max), + "Usearch hits < similarity threshold": "-" + str(s16_removed_identity), + "Usearch hits NOT highest percentage identity per query": "-" + + str(s16_removed_max), + "Query hit in only one of two databases": "-" + str(remaining), + "Filtered fusion reads": 0, } write_summary(overview_table, sample, part, stats) return # Match ABR and 16S by query_id abr_common, s16_common = merge_parts_on_query_id(abr_final, s16_final) - removed_query_id_mismatch = (len(abr_final) + len(s16_final)) - ( - len(abr_common) + len(s16_common) + removed_query_id_mismatch = (len(abr_final) + len(s16_final)) - (len(abr_common)) + + # Merge side-by-side on query_id + merged = pd.merge( + abr_final[ + [ + "query_id", + "AMR Gene Family", + "perc_identity_ABR", + "align_length_ABR", + "evalue_ABR", + ] + ], + s16_final[ + ["query_id", "genus", "perc_identity_16S", "align_length_16S", "evalue_16S"] + ], + on="query_id", + how="inner", ) - - # Merge and write final output - merged = pd.concat([abr_common, s16_common]) merged.to_csv(output_file, index=False) # Extract sample and part from file path @@ -146,12 +189,14 @@ def filter_blast_results(input_file, output_file, min_similarity, overview_table # Write summary stats = { - "filtered min similarity ABR": "-" + str(abr_removed_identity), - "filtered max identity ABR": "-" + str(abr_removed_max), - "filtered min similarity 16S": "-" + str(s16_removed_identity), - "filtered max identity 16S": "-" + str(s16_removed_max), - "filtered query id mismatch": "-" + str(removed_query_id_mismatch), - "filtration output": len(merged), + "Diamond hits < similarity threshold": "-" + str(abr_removed_identity), + "Diamond hits NOT highest percentage identity per query": "-" + + str(abr_removed_max), + "Usearch hits < similarity threshold": "-" + str(s16_removed_identity), + "Usearch hits NOT highest percentage identity per query": "-" + + str(s16_removed_max), + "Query hit in only one of two databases": "-" + str(removed_query_id_mismatch), + "Filtered fusion reads": len(merged), } write_summary(overview_table, sample, part, stats) diff --git a/workflow/scripts/integrate_blast_data.py b/workflow/scripts/integrate_blast_data.py index f6e850a..54830e2 100644 --- a/workflow/scripts/integrate_blast_data.py +++ b/workflow/scripts/integrate_blast_data.py @@ -21,11 +21,18 @@ def write_dummy_line(output_file, part): """Write a dummy line to ensure compatibility with downstream analysis""" - if part == "ABR": - dummy_data = pd.read_csv(snakemake.input.dummy_ABR) - elif part == "16S": - dummy_data = pd.read_csv(snakemake.input.dummy_16S) - pd.DataFrame(dummy_data).to_csv(output_file, index=False) + + additional_columns = ["part", "ARO Name", "distance", "orientation", "genus"] + header = blast_columns + additional_columns + dummy_row = ["dummy.dummy", "dummy", "100"] + ["0"] * 9 + if part == "16S": + dummy_row = dummy_row + ["16S", "dummy", "0", "dummy", "dummy"] + elif part == "ABR": + dummy_row = dummy_row + ["ABR", "dummy"] + ["0"] * 3 + ["dummy"] * 8 + else: + raise ValueError("Invalid part specified. Must be 'ABR' or '16S'.") + dummy_df = pd.DataFrame([dummy_row], columns=header) + dummy_df.to_csv(output_file, index=False) def process_card_results( @@ -95,7 +102,7 @@ def merge_results(card_output, silva_output, final_output, overview_table): count = len(combined_df) with open(overview_table, "a") as file: - line = f"merge output,{sample},{part},{count}\n" + line = f"Merged similarity hits,{sample},{part},{count}\n" file.write(line) @@ -132,6 +139,7 @@ def merge_results(card_output, silva_output, final_output, overview_table): process_silva_results, silva_results, blast_columns, silva_output ) + # Wait for both processes to complete future_card.result() future_silva.result() diff --git a/workflow/scripts/merge_overview_smpl.py b/workflow/scripts/merge_overview_smpl.py deleted file mode 100644 index bb31880..0000000 --- a/workflow/scripts/merge_overview_smpl.py +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright 2024 Adrian Dörr. -# Licensed under the MIT License (https://opensource.org/license/mit) -# This file may not be copied, modified, or distributed -# except according to those terms. - - -import pandas as pd -from collections import defaultdict - -""" -This script merges overview table files for one sample, effectively -creating a table for all metrics between input and output. -""" - - -def merge_overview_tables(input_paths, sample_name, output_path): - # Dictionary of dictionaries: sample -> state -> total count - state_counts = defaultdict(int) - - for path in input_paths: - try: - df = pd.read_csv( - path, header=None, names=["state", "sample", "part", "count"] - ) - except Exception as e: - print(f"Could not read {path}: {e}") - continue - - for _, row in df.iterrows(): - state = row["state"] - count = int(row["count"]) - state_counts[state] += count - - # Write summary grouped by sample - summary_df = pd.DataFrame( - [ - {"sample": sample_name, "state": state, "total_count": total} - for state, total in state_counts.items() - ] - ) - - # Save as HTML - summary_df.to_html(output_path, index=False) - - -if __name__ == "__main__": - input_paths = snakemake.input.overview_tables - sample_name = snakemake.params.sample_name - output_path = snakemake.output[0] - merge_overview_tables(input_paths, sample_name, output_path) diff --git a/workflow/scripts/combined_genera_abundance_plot.py b/workflow/scripts/plot_abundance_bubble.py similarity index 96% rename from workflow/scripts/combined_genera_abundance_plot.py rename to workflow/scripts/plot_abundance_bubble.py index f7d1dae..5ecd28c 100644 --- a/workflow/scripts/combined_genera_abundance_plot.py +++ b/workflow/scripts/plot_abundance_bubble.py @@ -9,6 +9,7 @@ from plotly.subplots import make_subplots import plotly.io as pio import sys +import numpy as np """ This script creates interactive bubble plots showing the top genera per AMR Gene Family @@ -111,6 +112,7 @@ def add_amr_family_subplot( def create_bubble_plot_grid(df, max_genera, min_overlap, top_per_sample): """Create the full multi-subplot bubble chart""" + samples = df["sample"].unique() families = df["AMR Gene Family"].unique() num_cols = len(families) if len(df) > 1 else 1 @@ -130,8 +132,8 @@ def create_bubble_plot_grid(df, max_genera, min_overlap, top_per_sample): title="Bubble Plots of Top Genera for Each AMR Gene Family", plot_bgcolor="lightgrey", height=900, - width=500 * num_cols, - coloraxis_colorbar=dict(title="Filtered Hit Count"), + width=250 * np.log(len(samples)) if len(samples) > 2 else 600, + coloraxis_colorbar=dict(title="Fusion Read Count"), ) fig.update_yaxes(categoryorder="category descending") fig.update_xaxes(categoryorder="category ascending") diff --git a/workflow/scripts/plot_abundance_data.py b/workflow/scripts/plot_abundance_data.py new file mode 100644 index 0000000..7d03c3a --- /dev/null +++ b/workflow/scripts/plot_abundance_data.py @@ -0,0 +1,94 @@ +# Copyright 2024 Adrian Dörr. +# Licensed under the MIT License (https://opensource.org/license/mit) +# This file may not be copied, modified, or distributed +# except according to those terms. + + +import pandas as pd +import plotly.express as px +import sys + +""" +This script takes the combined genus abundance table as input and counts +the number of hits in relation to respective AMR families. +""" + +# === HTML with rowspan for merged cells === + +html = """ + + + + + + + + + + +""" + + +def plot_abundance_data(input_file, html, output_html): + """Group by unique AMR Gene Families and sum respective genus count""" + # Filter data for the current AMR Gene Family + df = pd.read_csv(input_file, sep=",", header=0) + grouped = df.groupby(["sample", "AMR Gene Family"]) + for (sample, family), group in grouped: + sample_rowspan = len(df[df["sample"] == sample]) + family_rowspan = len(group) + amr = df[(df["sample"] == sample) & (df["AMR Gene Family"] == family)] + reads_per_amr = amr["genus_count"].sum() + amr_line = f"{family}
Total Fusion Reads: {reads_per_amr}" + first_family = True + for i, row in group.iterrows(): + html += "" + if i == df[df["sample"] == sample].index[0]: + html += f'' + if first_family: + html += f'' + first_family = False + html += f"" + html += "" + + html += """ + +
SampleAMR Gene FamilyGenusFusion Read CountRelative
{sample}{amr_line}{row['genus']}{row['genus_count']}{row['relative_genus_count']}
+ + + """ + # Write to file + with open(output_html, "w") as f: + f.write(html) + + +if __name__ == "__main__": + input_file = snakemake.input.abundance_data + output_html = snakemake.output[0] + sys.stderr = open(snakemake.log[0], "w") + plot_abundance_data(input_file, html, output_html) diff --git a/workflow/scripts/plot_attrition.py b/workflow/scripts/plot_attrition.py new file mode 100644 index 0000000..33c6eb7 --- /dev/null +++ b/workflow/scripts/plot_attrition.py @@ -0,0 +1,142 @@ +# Copyright 2024 Adrian Dörr. +# Licensed under the MIT License (https://opensource.org/license/mit) +# This file may not be copied, modified, or distributed +# except according to those terms. + + +import pandas as pd +import matplotlib.pyplot as plt +import numpy as np + +""" +This script generates a summary bar plot illustrating the impact of filtration steps +on BLAST similarity hits (from Diamond and Usearch) across multiple samples. + +It reads a summary CSV with hit counts and states, +groups the data by sample and category, and visualizes both retained and +filtered hits with color-coded stacked bars. +""" + +# === Constants for category mapping === +MAIN_CATEGORIES = [ + "Number of FastQ input reads", + "Merged similarity hits", + "Filtered fusion reads", +] + +FILTER_REASONS = { + "Diamond hits < similarity threshold": "royalblue", + "Diamond hits NOT highest percentage identity per query": "purple", + "Usearch hits < similarity threshold": "#a6d854", + "Usearch hits NOT highest percentage identity per query": "#66c2a5", + "Query hit in only one of two databases": "#ffd92f", +} + +MAIN_COLOR_MAP = { + "Number of FastQ input reads": "seagreen", + "Merged similarity hits": "#fc8d62", + "Filtered fusion reads": "#8da0cb", +} + + +# === Load and summarize the table === +def load_and_summarize_data(path): + df = pd.read_csv(path, header=0) + df["total_count"] = df["total_count"].astype(int).abs() + + main_df = ( + df[df["step"].isin(MAIN_CATEGORIES)] + .pivot(index="sample", columns="step", values="total_count") + .fillna(0) + ) + filter_df = ( + df[df["step"].isin(FILTER_REASONS)] + .pivot(index="sample", columns="step", values="total_count") + .fillna(0) + ) + + return main_df, filter_df + + +# === Plotting function === +def plot_summary(main_df, filter_df, output_path): + samples = main_df.index + x = np.arange(len(samples)) + bar_width = 0.18 + overlay_width = 0.1 + + fig, ax = plt.subplots(figsize=(12, 7)) + + # Plot main bars with offsets + offsets = np.linspace(-bar_width, bar_width, len(MAIN_CATEGORIES)) + for i, col in enumerate(MAIN_CATEGORIES): + if col not in main_df.columns: + continue + ax.bar( + x + offsets[i], + main_df[col], + bar_width, + label=col, + color=MAIN_COLOR_MAP.get(col, "gray"), + ) + + # Plot filter stack bars *on top* of "Filtered fusion reads" + if "Filtered fusion reads" in main_df.columns: + bottom = main_df["Filtered fusion reads"].values.copy() + else: + bottom = np.zeros_like(x) + + for reason in FILTER_REASONS: + heights = ( + filter_df[reason].values + if reason in filter_df.columns + else np.zeros_like(x) + ) + ax.bar( + x + bar_width, + heights, + overlay_width, + bottom=bottom, + label=reason, + color=FILTER_REASONS.get(reason, "gray"), + ) + bottom += heights + + # Axis formatting + ax.set_xticks(x) + ax.set_xticklabels(samples, rotation=45) + ax.set_ylabel("Hit count") + ax.set_xlabel("Sample") + ax.set_title("Similarity Search Processing with Rejection Breakdown") + + # Split legend into main vs. filter + handles, labels = ax.get_legend_handles_labels() + main_labels = MAIN_CATEGORIES + filter_labels = FILTER_REASONS + + legend1 = ax.legend( + [handles[labels.index(l)] for l in main_labels if l in labels], + main_labels, + loc="upper left", + bbox_to_anchor=(1.02, 1), + title="Hit Process", + ) + legend2 = ax.legend( + [handles[labels.index(l)] for l in filter_labels if l in labels], + filter_labels, + loc="upper left", + bbox_to_anchor=(1.02, 0.55), + title="Filtering Reasons", + ) + ax.add_artist(legend1) + + plt.tight_layout() + plt.savefig(output_path) + + +if __name__ == "__main__": + input_path = snakemake.input.overview_table + output_path = snakemake.output[0] + + main_summary, overlay_summary = load_and_summarize_data(input_path) + plot_summary(main_summary, overlay_summary, output_path) diff --git a/workflow/scripts/plot_overview.py b/workflow/scripts/plot_overview.py deleted file mode 100644 index ea5ed5a..0000000 --- a/workflow/scripts/plot_overview.py +++ /dev/null @@ -1,167 +0,0 @@ -# Copyright 2024 Adrian Dörr. -# Licensed under the MIT License (https://opensource.org/license/mit) -# This file may not be copied, modified, or distributed -# except according to those terms. - - -import pandas as pd -import matplotlib.pyplot as plt -import numpy as np - -""" -This script generates a summary bar plot illustrating the impact of filtration steps -on BLAST similarity hits (from Diamond and Usearch) across multiple samples. - -It reads a summary CSV with hit counts and states, -groups the data by sample and category, and visualizes both retained and -filtered hits with color-coded stacked bars. -""" - -# === Constants for category mapping === -MAIN_CATEGORIES = { - "merge output": "Diamond and Usearch hits", - "filtration output": "Hits after filtration", -} - -FILTER_REASONS = { - "filtered min similarity ABR": "Diamond hits < similarity threshold", - "filtered max identity ABR": "Diamond hits ≠ max identity for query ID", - "filtered min similarity 16S": "Usearch hits < similarity threshold", - "filtered max identity 16S": "Usearch hits ≠ max identity for query ID", - "filtered query id mismatch": "No overlap for hits in both databases", -} - - -def map_main_category(state): - return MAIN_CATEGORIES.get(state) - - -def map_filter_reason(state): - return FILTER_REASONS.get(state) - - -def load_and_summarize_data(input_path): - """Read the overview table and group by main and filtering categories.""" - df = pd.read_csv(input_path) - - # Assign main and filter categories - df["category"] = df["state"].apply(map_main_category) - df["filter_reason"] = df["state"].apply(map_filter_reason) - df["total_count"] = df["total_count"].astype(int).abs() - - # Group main categories - main_summary = ( - df.dropna(subset=["category"]) - .groupby(["sample", "category"])["total_count"] - .sum() - .unstack() - .fillna(0) - ) - - # Group filtering reasons - overlay_summary = ( - df.dropna(subset=["filter_reason"]) - .groupby(["sample", "filter_reason"])["total_count"] - .sum() - .unstack() - .fillna(0) - ) - - return main_summary, overlay_summary - - -def plot_summary(main_summary, overlay_summary, output_path): - """Generate and save a stacked bar plot showing filtering breakdown.""" - samples = main_summary.index - x = np.arange(len(samples)) - bar_width = 0.25 - overlay_width = 0.125 - - fig, ax = plt.subplots(figsize=(12, 7)) - - # Define colors - main_colors = { - MAIN_CATEGORIES["merge output"]: "#fc8d62", - MAIN_CATEGORIES["filtration output"]: "#8da0cb", - } - filter_colors = { - FILTER_REASONS[k]: c - for k, c in zip( - FILTER_REASONS, ["royalblue", "purple", "#a6d854", "#66c2a5", "#ffd92f"] - ) - } - - # Plot main bars - ax.bar( - x - bar_width / 2, - main_summary[MAIN_CATEGORIES["merge output"]], - bar_width, - label=MAIN_CATEGORIES["merge output"], - color=main_colors[MAIN_CATEGORIES["merge output"]], - ) - ax.bar( - x + bar_width / 2, - main_summary[MAIN_CATEGORIES["filtration output"]], - bar_width, - label=MAIN_CATEGORIES["filtration output"], - color=main_colors[MAIN_CATEGORIES["filtration output"]], - ) - - # Stack filter bars on top of filtration bar - bottom = main_summary[MAIN_CATEGORIES["filtration output"]].values.copy() - for reason in FILTER_REASONS.values(): - heights = ( - overlay_summary[reason] - if reason in overlay_summary - else np.zeros_like(bottom) - ) - ax.bar( - x + bar_width / 2.1, - heights, - overlay_width, - bottom=bottom, - label=reason, - color=filter_colors[reason], - ) - bottom += heights - - # Axis formatting - ax.set_xticks(x) - ax.set_xticklabels(samples, rotation=45) - ax.set_ylabel("Similarity search hit count") - ax.set_xlabel("Sample") - ax.set_title( - "Similarity Search Processing with Rejection Breakdown on Filtration Hits" - ) - - # Split legend into main vs. filter categories - handles, labels = ax.get_legend_handles_labels() - main_labels = list(MAIN_CATEGORIES.values()) - filter_labels = list(FILTER_REASONS.values()) - - legend1 = ax.legend( - [handles[labels.index(l)] for l in main_labels], - main_labels, - loc="upper left", - bbox_to_anchor=(1.02, 1), - title="Hit Process", - ) - legend2 = ax.legend( - [handles[labels.index(l)] for l in filter_labels], - filter_labels, - loc="upper left", - bbox_to_anchor=(1.02, 0.55), - title="Filtering Reasons", - ) - ax.add_artist(legend1) - - plt.tight_layout() - plt.savefig(output_path) - - -if __name__ == "__main__": - input_path = snakemake.input.overview_table - output_path = snakemake.output[0] - - main_summary, overlay_summary = load_and_summarize_data(input_path) - plot_summary(main_summary, overlay_summary, output_path) diff --git a/workflow/scripts/plot_overview_table.py b/workflow/scripts/plot_overview_table.py new file mode 100644 index 0000000..bc84f37 --- /dev/null +++ b/workflow/scripts/plot_overview_table.py @@ -0,0 +1,116 @@ +# Copyright 2024 Adrian Dörr. +# Licensed under the MIT License (https://opensource.org/license/mit) +# This file may not be copied, modified, or distributed +# except according to those terms. + + +import pandas as pd +import matplotlib.pyplot as plt +import numpy as np + +""" +This script generates a summary bar plot illustrating the impact of filtration steps +on BLAST similarity hits (from Diamond and Usearch) across multiple samples. + +It reads a summary CSV with hit counts and states, +groups the data by sample and category, and visualizes both retained and +filtered hits with color-coded stacked bars. +""" + +# Mapping step -> State +step_to_state = { + "Number of FastQ input reads": "Input reads", + "Diamond output hits": "Similarity search", + "Usearch output hits": "Similarity search", + "Merged similarity hits": "Similarity search", + "Diamond hits < similarity threshold": "Filtration", + "Diamond hits NOT highest percentage identity per query": "Filtration", + "Usearch hits < similarity threshold": "Filtration", + "Usearch hits NOT highest percentage identity per query": "Filtration", + "Query hit in only one of two databases": "Filtration", + "Filtered fusion reads": "Output reads", +} + + +# === Load and summarize the table === +def table_to_html(input_path, output_path): + df = pd.read_csv(input_path, header=0) + + df["state"] = df["step"].map(step_to_state) + + # Reorder and sort + df = df[["sample", "state", "step", "total_count"]] + state_order = ["Input reads", "Similarity search", "Filtration", "Output reads"] + df["state"] = pd.Categorical(df["state"], categories=state_order, ordered=True) + df = df.sort_values(by=["sample", "state"]) + + # === HTML with rowspan for merged cells === + + html = """ + + + + + + + + + + + """ + + # Group and track rowspans + grouped = df.groupby(["sample", "state"], observed=False) + for (sample, state), group in grouped: + sample_rowspan = len(df[df["sample"] == sample]) + state_rowspan = len(group) + + first_state = True + for i, row in group.iterrows(): + html += "" + if i == df[df["sample"] == sample].index[0]: + html += f'' + if first_state: + html += f'' + first_state = False + html += f"" + html += "" + + html += """ + +
SampleStateStepCount
{sample}{state}{row['step']}{row['total_count']}
+ + + """ + # Write to file + with open(output_path, "w") as f: + f.write(html) + + +if __name__ == "__main__": + input_path = snakemake.input.overview_table + output_path = snakemake.output[0] + + table_to_html(input_path, output_path) diff --git a/workflow/scripts/plot_stacked_bar_abundance.py b/workflow/scripts/plot_stacked_bar_abundance.py new file mode 100644 index 0000000..055e59c --- /dev/null +++ b/workflow/scripts/plot_stacked_bar_abundance.py @@ -0,0 +1,172 @@ +import os, pathlib +import numpy as np +import pandas as pd +import plotly.express as px +import plotly.graph_objects as go +from plotly.subplots import make_subplots + +# ─── Constants ───────────────────────────────────────────────────────── +RESERVED_COLOR = "rgb(217,217,217)" +AMR_MIN_FRACTION = 0.01 + + +def get_genus_colors(all_genera): + """Assign consistent, distinguishable colors to each genus.""" + top_colors = [ + "#D62728", # dark red + "#FF7F0E", # orange + "#8B4513", # brown + "#1F77B4", # dark blue + "#800080", # purple + "#7F7F7F", # gray + "#2CA02C", # dark green + "#1E90FF", # blue + "#BA55D3", # medium orchid + "#BCBD22", # yellow-green + ] + + fallback_palette = ( + px.colors.qualitative.Pastel + + px.colors.qualitative.Set3 + + px.colors.qualitative.Alphabet + + px.colors.qualitative.Light24 + + px.colors.qualitative.Bold + ) + + # Remove duplicates and reserved color from palette + color_pool = list(dict.fromkeys(top_colors + fallback_palette)) + if RESERVED_COLOR in color_pool: + color_pool.remove(RESERVED_COLOR) + + # Assign genera with a unique color each + genus_list = [g for g in all_genera if g != "Others"] + if len(genus_list) > len(color_pool): + raise ValueError( + f"Too many genera ({len(genus_list)}) for available color pool." + ) + genus_colors = {g: color_pool[i] for i, g in enumerate(genus_list)} + genus_colors["Others"] = RESERVED_COLOR + return genus_colors + + +def preprocess_abundance(df, amr, min_genus_abundance, force_include, force_exclude): + """Filter and aggregate genus abundance data for a given AMR family.""" + df_amr = df[df["AMR Gene Family"] == amr].copy() + + # Determine low-abundance or excluded genera + low_abundance = df_amr[ + ( + (df_amr["relative_genus_count"] <= min_genus_abundance) + & (~df_amr["genus"].isin(force_include)) + ) + | (df_amr["genus"].isin(force_exclude)) + ] + others = ( + low_abundance.groupby(["sample", "total_count"], as_index=False) + .agg({"relative_genus_count": "sum"}) + .assign(genus="Others") + ) + others["sample_label"] = ( + others["sample"] + " (" + others["total_count"].astype(str) + ")" + ) + + # Remove excluded genera + df_amr = df_amr[~df_amr["genus"].isin(force_exclude)] + df_amr = df_amr.sort_values( + by=["sample", "AMR Gene Family", "genus_count"], ascending=[True, False, False] + ) + # plot high abundance or forced-includes + df_amr_filtered = df_amr[ + (df_amr["relative_genus_count"] > min_genus_abundance) + | (df_amr["genus"].isin(force_include)) + ] + + # Add "Others" + df_final = pd.concat([df_amr_filtered, others], ignore_index=True) + df_final["sample_label"] = ( + df_final["sample"] + " (" + df_final["total_count"].astype(str) + ")" + ) + return df_final + + +def plot_stacked_abundance( + observed_csv, + output_html, + min_genus_abundance, + force_include=None, + force_exclude=None, +): + """Main function to generate a stacked bar plot of genus abundance by AMR family.""" + force_include = force_include or [] + force_exclude = force_exclude or [] + + df = pd.read_csv(observed_csv) + df = df.sort_values(["sample", "genus_count"], ascending=[True, False]) + + # ─── Filter AMR families by total count ───────────────────────────── + amr_totals = df.groupby("AMR Gene Family")["total_count"].sum() + total_all = amr_totals.sum() + amrs_to_plot = amr_totals[amr_totals >= total_all * AMR_MIN_FRACTION].index.tolist() + + if not amrs_to_plot: + print("No AMR Gene Families meet the abundance threshold.") + return + + df = df[df["AMR Gene Family"].isin(amrs_to_plot)] + amrs = sorted(df["AMR Gene Family"].unique()) + samples = df["sample"].nunique() + + # ─── Set up subplots ──────────────────────────────────────────────── + fig = make_subplots( + rows=len(amrs), + cols=1, + subplot_titles=amrs, + shared_xaxes=True, + vertical_spacing=0.2, + ) + + for i, amr in enumerate(amrs, start=1): + df_amr = preprocess_abundance( + df, amr, min_genus_abundance, force_include, force_exclude + ) + genus_colors = get_genus_colors(df_amr["genus"].unique()) + + genera = df_amr["genus"].unique() + for genus in genera: + genus_data = df_amr[df_amr["genus"] == genus] + fig.add_trace( + go.Bar( + x=genus_data["sample_label"], + y=genus_data["relative_genus_count"], + name=genus, + marker_color=genus_colors[genus], + showlegend=True, + ), + row=i, + col=1, + ) + + # ─── Layout ──────────────────────────────────────────────────────── + fig.update_layout( + barmode="stack", + title="Relative Genus Abundance per AMR Gene Family", + height=800 * len(amrs), + width=1000 * np.log10(samples) if samples > 2 else 500, + plot_bgcolor="white", + yaxis=dict(tickformat=".0%"), + legend_title="Genus", + ) + + fig.update_xaxes(tickangle=45) + fig.update_yaxes(title_text="Relative Abundance") + + # Save and show + fig.write_html(output_html) + + +if __name__ == "__main__": + input_csv = snakemake.input.abundance_data + output_html = snakemake.output[0] + min_abundance = snakemake.params[0] + sys.stderr = open(snakemake.log[0], "w") + plot_stacked_abundance(input_csv, output_html, float(min_abundance)) diff --git a/workflow/scripts/single_genera_abundance_table.py b/workflow/scripts/single_genera_abundance_table.py deleted file mode 100644 index ec9c003..0000000 --- a/workflow/scripts/single_genera_abundance_table.py +++ /dev/null @@ -1,134 +0,0 @@ -# Copyright 2024 Adrian Dörr. -# Licensed under the MIT License (https://opensource.org/license/mit) -# This file may not be copied, modified, or distributed -# except according to those terms. - - -import pandas as pd -import os, sys - -""" -This script processes epicPCR data (ABR + 16S) distingly for one sample and its part -to compute genus-level total and relative abundance per AMR Gene Family in a table. -This table is later be imported as sample-wise information in the report. -""" - -# Necessary columns to load in each dataframe -necessary_columns = [ - "query_id", - "part", - "genus", - "AMR Gene Family", - "perc_identity", -] - - -def write_dummy_line(sample_name): - # Create a dummy row when either ABR or 16S data is missing for a sample - # Returns Single-row dataframe with placeholder values - dummy_line = { - "sample": sample_name, - "AMR Gene Family": "NA", - "genus": "NA", - "genus_count": 0, - "total_count": 0, - "relative_genus_count": 0, - } - merged_data = pd.DataFrame([dummy_line]) - return merged_data - - -def process_combined_data(combined_data, sample_name): - # Separate ABR and 16S data for merging by query_id - abr_data = combined_data[combined_data["part"] == "ABR"] - sixteen_s_data = combined_data[combined_data["part"] == "16S"] - - # Prepare to merge only unique hits - unique_abr_data = abr_data[["query_id", "AMR Gene Family"]].drop_duplicates() - unique_sixteen_s_data = sixteen_s_data[["query_id", "genus"]].drop_duplicates() - - if sixteen_s_data.iloc[0]["query_id"] == "dummy": - return write_dummy_line(sample_name) - - # Merge on query_id to associate AMR Gene Family with genus information from 16S data - merged_data = pd.merge( - unique_abr_data[["query_id", "AMR Gene Family"]], - unique_sixteen_s_data[["query_id", "genus"]], - on="query_id", - how="inner", - ) - - # Add the sample name - merged_data["sample"] = sample_name - - # Calculate genus counts per AMR Gene Family and genus for the sample - genus_counts = ( - merged_data.groupby(["sample", "AMR Gene Family", "genus"]) - .size() - .reset_index(name="genus_count") - ) - - # Calculate total genus count per AMR Gene Family within each sample - total_counts = ( - genus_counts.groupby(["sample", "AMR Gene Family"])["genus_count"] - .sum() - .reset_index(name="total_count") - ) - - # Merge to get total counts for each genus entry and calculate relative counts - genus_counts = pd.merge( - genus_counts, total_counts, on=["sample", "AMR Gene Family"], how="left" - ) - genus_counts["relative_genus_count"] = round( - genus_counts["genus_count"] / genus_counts["total_count"], 4 - ) - - return genus_counts - - -def combine_blast_data(input_file, sample_name): - # Load and combine data from all parts for the given sample - df = pd.read_csv( - input_file, sep=",", usecols=necessary_columns, header=0, compression="gzip" - ) - - # Process combined data to get genus counts and relative values - genus_counts = process_combined_data(df, sample_name) - return genus_counts - - -def export_genera_abundance(input_files, sample_name, parts, output_path): - sample_input_files = [f for f in input_files if f"/{sample_name}/" in f] - # Load and combine all parts for the current sample - part_dfs = [] - for part in parts: - matching_files = [f for f in sample_input_files if f"/{part}/" in f] - if not matching_files: - continue - input_file = matching_files[0] - df = pd.read_csv( - input_file, sep=",", usecols=necessary_columns, header=0, compression="gzip" - ) - part_dfs.append(df) - - if not part_dfs: - print(f"No valid parts found for sample: {sample_name}") - - full_sample_df = pd.concat(part_dfs, ignore_index=True) - processed_data = process_combined_data(full_sample_df, sample_name) - - processed_data = processed_data.sort_values( - by=["total_count", "genus_count"], ascending=False - ) - - # Write to HTML - processed_data.to_html(output_path, index=False) - - -if __name__ == "__main__": - input_file = snakemake.input.filtered_data - output_path = snakemake.output[0] - sample_name = snakemake.params.sample_name - parts = snakemake.params.parts - sys.stderr = open(snakemake.log[0], "w") - export_genera_abundance(input_file, sample_name, parts, output_path) diff --git a/workflow/scripts/combined_genera_abundance_table.py b/workflow/scripts/table_combined_genera_abundance.py similarity index 72% rename from workflow/scripts/combined_genera_abundance_table.py rename to workflow/scripts/table_combined_genera_abundance.py index 8a26fdb..988cef0 100644 --- a/workflow/scripts/combined_genera_abundance_table.py +++ b/workflow/scripts/table_combined_genera_abundance.py @@ -14,13 +14,6 @@ """ # Necessary columns to load in each dataframe -necessary_columns = [ - "query_id", - "part", - "genus", - "AMR Gene Family", - "perc_identity", -] def write_dummy_line(sample_name): @@ -37,24 +30,11 @@ def write_dummy_line(sample_name): def process_combined_data(combined_data, sample_name): - """Separate ABR and 16S data for merging by query_id""" - abr_data = combined_data[combined_data["part"] == "ABR"] - sixteen_s_data = combined_data[combined_data["part"] == "16S"] + combined_data["sample"] = sample_name - # Dummy handling - if sixteen_s_data.empty or abr_data.empty: - return write_dummy_line(sample_name) - - # Prepare to merge only unique hits - abr_unique = abr_data[["query_id", "AMR Gene Family"]].drop_duplicates() - sixteen_unique = sixteen_s_data[["query_id", "genus"]].drop_duplicates() - - merged = pd.merge(abr_unique, sixteen_unique, on="query_id", how="inner") - merged["sample"] = sample_name - - # Calculate genus counts per AMR Gene Family and genus for the sample + # Count genus occurrences per AMR Gene Family genus_counts = ( - merged.groupby(["sample", "AMR Gene Family", "genus"]) + combined_data.groupby(["sample", "AMR Gene Family", "genus"]) .size() .reset_index(name="genus_count") ) @@ -80,14 +60,14 @@ def load_and_merge_parts(file_list): data_frames = [] for file in file_list: try: - df = pd.read_csv(file, usecols=necessary_columns, compression="gzip") + df = pd.read_csv(file, compression="gzip") data_frames.append(df) except Exception as e: print(f"Skipping file due to read error [{file}]: {repr(e)}") if data_frames: merged_df = pd.concat(data_frames, ignore_index=True) else: - merged_df = pd.DataFrame(columns=necessary_columns) + merged_df = pd.DataFrame() return merged_df @@ -107,7 +87,9 @@ def export_genera_abundance(input_files, output_path): all_data.append(sample_data) final_df = pd.concat(all_data, ignore_index=True) - final_df = final_df.sort_values(by=["genus_count"], ascending=False) + final_df = final_df.sort_values( + by=["sample", "AMR Gene Family", "genus_count"], ascending=False + ) # Export the final aggregated data to a CSV file final_df.to_csv(output_path, index=False) diff --git a/workflow/scripts/merge_overview_all.py b/workflow/scripts/table_overview_all.py similarity index 89% rename from workflow/scripts/merge_overview_all.py rename to workflow/scripts/table_overview_all.py index 2f9f285..d742f25 100644 --- a/workflow/scripts/merge_overview_all.py +++ b/workflow/scripts/table_overview_all.py @@ -20,7 +20,7 @@ def merge_overview_tables(input_paths, output_path): for path in input_paths: try: df = pd.read_csv( - path, header=None, names=["state", "sample", "part", "count"] + path, header=None, names=["step", "sample", "part", "count"] ) except Exception as e: print(f"Could not read {path}: {e}") @@ -28,7 +28,7 @@ def merge_overview_tables(input_paths, output_path): for _, row in df.iterrows(): sample = row["sample"] - state = row["state"] + state = row["step"] count = int(row["count"]) sample_state_counts[sample][state] += count @@ -37,7 +37,7 @@ def merge_overview_tables(input_paths, output_path): header_written = False for sample, state_counts in sample_state_counts.items(): if header_written == False: - out_file.write("sample,state,total_count\n") + out_file.write("sample,step,total_count\n") header_written = True for state, total in state_counts.items(): out_file.write(f"{sample},{state},{total}\n")