wormcat3 0.1.11__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
wormcat3/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+
2
+ __version__ = "0.1.11"
3
+
4
+ from .wormcat import Wormcat
5
+ from .annotations_manger import AnnotationsManager
6
+ from .constants import PAdjustMethod
7
+ from .wormcat_error import WormcatError, ErrorCode
@@ -0,0 +1,210 @@
1
+ import pandas as pd
2
+ import os
3
+ from pathlib import Path
4
+ from wormcat3 import file_util
5
+ import wormcat3.constants as cs
6
+ from wormcat3.wormcat_error import WormcatError, ErrorCode
7
+
8
+
9
+ class AnnotationsManager:
10
+ """ Manages gene annotations and preprocessing. """
11
+
12
+ def __init__(self, annotation_file=cs.DEFAULT_ANNOTATION_FILE_NAME):
13
+ """Initialize with the path to the annotation file."""
14
+ if file_util.is_file_path(annotation_file):
15
+ self.annotation_file_path = annotation_file
16
+ else:
17
+ self.annotation_file_path = file_util.find_file_path(annotation_file)
18
+ if not self.annotation_file_path:
19
+ raise WormcatError(f"Annotation file not found: {annotation_file}", ErrorCode.FILE_NOT_FOUND.to_dict())
20
+
21
+ self.annotations_df = self._load_annotations()
22
+
23
+
24
+
25
+ def _load_annotations(self):
26
+ """ Load annotations from file. """
27
+ try:
28
+ df = pd.read_csv(self.annotation_file_path) # type: ignore
29
+ df.columns = df.columns.str.replace(' ', '.')
30
+ if df.empty:
31
+ raise WormcatError(f"Annotation file '{self.annotation_file_path}' is empty.", ErrorCode.FILE_LOAD_FAILED.to_dict())
32
+ return df
33
+ except Exception as e:
34
+ raise WormcatError(f"Failed to load annotation file: {e}",
35
+ ErrorCode.FILE_LOAD_FAILED.to_dict(),
36
+ origin="AnnotationsManager._load_annotations")
37
+
38
+ @staticmethod
39
+ def available_annotation_files():
40
+ """
41
+ Return a list of .csv file names located in:
42
+ - the 'extdata' directory (next to this script), and
43
+ - the directory defined by the WORMCAT_DATA_PATH environment variable (if set).
44
+ """
45
+ file_path = Path(__file__).resolve()
46
+ extdata_path = file_path.parent / "extdata"
47
+
48
+ search_dirs = [extdata_path]
49
+
50
+ env_path = os.environ.get("WORMCAT_DATA_PATH")
51
+ if env_path:
52
+ env_path = Path(env_path)
53
+ if env_path.exists() and env_path.is_dir():
54
+ search_dirs.append(env_path)
55
+
56
+ found_files = set()
57
+
58
+ for directory in search_dirs:
59
+ if directory.exists():
60
+ for f in directory.glob("*.csv"):
61
+ if f.is_file():
62
+ found_files.add(f.name)
63
+
64
+ return sorted(found_files)
65
+
66
+
67
+ def annotation_name(self):
68
+ # Get the base name of the file (with extension, no directory)
69
+ if isinstance(self.annotation_file_path, (str, os.PathLike)) and self.annotation_file_path:
70
+ return os.path.basename(self.annotation_file_path)
71
+ else:
72
+ return "[NO_ANNOTATION_NAME]"
73
+
74
+ def get_gene_id_type(self, gene_set):
75
+ # Skip the first row (possible header), then filter valid gene entries
76
+ valid_genes = [
77
+ g for g in gene_set[1:] # Skip the first row
78
+ if isinstance(g, str) and len(g.strip()) > 1
79
+ ]
80
+
81
+ if len(valid_genes) < 2:
82
+ raise WormcatError(
83
+ "Not enough valid gene entries to determine gene type.",
84
+ ErrorCode.INVALID_VALUE.to_dict()
85
+ )
86
+
87
+ # Check if both start with 'WBGene' or not
88
+ if valid_genes[0].startswith("WBGene") and valid_genes[1].startswith("WBGene"):
89
+ return cs.GENE_TYPE_WORMBASE_ID
90
+ elif not valid_genes[0].startswith("WBGene") and not valid_genes[1].startswith("WBGene"):
91
+ return cs.GENE_TYPE_SEQUENCE_ID
92
+ else:
93
+ raise WormcatError(
94
+ "Invalid gene data: One gene starts with 'WBGene', but the other does not.",
95
+ ErrorCode.INVALID_VALUE.to_dict()
96
+ )
97
+
98
+ @staticmethod
99
+ def dedup_list(input_list):
100
+ """ Deduplicate a list while preserving order. """
101
+
102
+ seen = set()
103
+ deduped_list = []
104
+ for item in input_list:
105
+ if item not in seen:
106
+ deduped_list.append(item)
107
+ seen.add(item)
108
+ return deduped_list
109
+
110
+ def add_annotations(self, gene_set_list, gene_type):
111
+ """ Add annotations to the gene set. """
112
+
113
+ gene_set_df = pd.DataFrame(gene_set_list, columns=[gene_type])
114
+
115
+ # Verify if 'gene_type' is a column in the DataFrame
116
+ if gene_type not in self.annotations_df.columns:
117
+ raise WormcatError(f"Column '{gene_type}' not found in the DataFrame.", ErrorCode.INVALID_VALUE.to_dict())
118
+
119
+ return pd.merge(gene_set_df, self.annotations_df, on=gene_type, how='left')
120
+
121
+
122
+ def segment_genes_by_annotation_match(self, gene_set_list, gene_type):
123
+ """ Split genes into those with and without annotations. """
124
+
125
+ gene_set_df = pd.DataFrame(gene_set_list, columns=[gene_type])
126
+ gene_set_df = gene_set_df.fillna('') # It is rare but possible that we get NaN in the input
127
+
128
+ # Check if gene_type is in both dataframes
129
+ if gene_type not in gene_set_df.columns:
130
+ raise WormcatError(f"'{gene_type}' MUST be a column in the Gene Set DataFrame.", ErrorCode.MISSING_FIELD.to_dict())
131
+ if gene_type not in self.annotations_df.columns:
132
+ raise WormcatError(f"Incorrect '{gene_type}' name. Column not found in {self.annotation_name()} file.", ErrorCode.MISSING_FIELD.to_dict())
133
+
134
+ # Perform the left merge
135
+ merged_df = pd.merge(gene_set_df, self.annotations_df, on=gene_type, how='left')
136
+
137
+ # Split based on presence of annotation (assuming at least one non-key column in annotations_df)
138
+ annotation_columns = [col for col in self.annotations_df.columns if col != gene_type]
139
+
140
+ genes_matched_df = merged_df.dropna(subset=annotation_columns)
141
+ genes_not_matched_df = merged_df[merged_df[annotation_columns].isnull().all(axis=1)]
142
+ genes_not_matched_df = genes_not_matched_df[[gene_type]]
143
+
144
+ return genes_matched_df, genes_not_matched_df
145
+
146
+ def create_gmt_for_annotations(self, output_dir_path, id_col_nm="Wormbase.ID", output_file_nm_prefix="wormcat"):
147
+ """ Create GMT formatted files for all categories. """
148
+ for category in [1,2,3]:
149
+ gmt_format = self.category_to_gmt_format(category, id_col_nm)
150
+ output_file_path = f"{output_dir_path}/{output_file_nm_prefix}_cat_{category}.gmt"
151
+ self._save_gmt_to_file(gmt_format, output_file_path)
152
+
153
+ def category_to_gmt_format(self, category, id_col_nm="Wormbase.ID"):
154
+ """ Convert an annotation dataframe category to GMT format. """
155
+ category_col = f"Category.{category}"
156
+ id_col = "Function.ID"
157
+
158
+ category_df = self.annotations_df[[id_col_nm, category_col]]
159
+ category_df = category_df.rename(columns={category_col: id_col})
160
+
161
+ # Validate that required columns exist
162
+ required_cols = [id_col, id_col_nm]
163
+ missing_cols = [col for col in required_cols if col not in category_df.columns]
164
+ if missing_cols:
165
+ raise WormcatError(f"Incorrect column names: {', '.join(missing_cols)} {self.annotation_name()} file does not have these columns.",
166
+ ErrorCode.MISSING_FIELD.to_dict())
167
+
168
+ if category_df[id_col].isna().any():
169
+ raise WormcatError(f"{self.annotation_name()} dataframe Column '{id_col}' contains NaN values",
170
+ ErrorCode.INVALID_VALUE.to_dict())
171
+
172
+ # Use ID column as description if desc_col is None
173
+ grouped = category_df.groupby([id_col])[id_col_nm].apply(list).reset_index()
174
+
175
+ if len(grouped) == 0:
176
+ raise WormcatError("No gene sets found after grouping", ErrorCode.INVALID_VALUE.to_dict())
177
+
178
+ # Create GMT formatted dictionary
179
+ gmt_format = {}
180
+ for _, row in grouped.iterrows():
181
+ # Filter out any None or NaN values from gene list
182
+ gene_list = [str(gene) for gene in row[id_col_nm] if pd.notna(gene)]
183
+
184
+ # Only include if there are genes in the set
185
+ if gene_list:
186
+ gmt_format[row[id_col]] = gene_list
187
+
188
+ return gmt_format
189
+
190
+ def _save_gmt_to_file(self, gmt_format, output_file_path='wormcat.gmt'):
191
+ """ Write GMT formatted dictionary to disk. """
192
+ # Ensure the output directory exists
193
+ output_dir_path = os.path.dirname(output_file_path)
194
+ file_util.validate_directory_path(output_dir_path, not_empty_check = False)
195
+
196
+ # Write to GMT file
197
+ with open(output_file_path, 'w') as file:
198
+ for gene_id, gene_list in gmt_format.items():
199
+ description = gene_id
200
+ line = f"{gene_id}\t{description}\t" + '\t'.join(gene_list)
201
+ file.write(line + '\n')
202
+
203
+ if not os.path.exists(output_file_path):
204
+ print(f"[Warning] Output file {output_file_path} was not created.")
205
+ elif os.path.getsize(output_file_path) == 0:
206
+ print(f"[Warning] Output file {output_file_path} is empty.")
207
+ else:
208
+ print(f"Successfully created GMT file: {output_file_path}")
209
+
210
+ return output_file_path
@@ -0,0 +1,240 @@
1
+ # cSpell:ignore ggplot ggtitle ylim darkred orangered dimgray
2
+
3
+ import matplotlib
4
+ matplotlib.use('Agg')
5
+
6
+ import pandas as pd
7
+ import numpy as np
8
+ from plotnine import (
9
+ ggplot, aes, geom_point, ggtitle, labs, ylim, coord_flip,
10
+ scale_color_manual, scale_size_manual,
11
+ theme, element_blank, element_rect, element_text
12
+ )
13
+ from pathlib import Path
14
+ import wormcat3.constants as cs
15
+ import warnings
16
+ from wormcat3.wormcat_error import WormcatError, ErrorCode
17
+
18
+ # Suppress UserWarnings and DeprecationWarnings from plotnine
19
+ warnings.filterwarnings('ignore', category=UserWarning, module='plotnine')
20
+ warnings.filterwarnings('ignore', category=DeprecationWarning, module='plotnine')
21
+ warnings.filterwarnings('ignore', module="matplotlib.font_manager")
22
+
23
+ def preprocess_bubble_data(data_file_path, add_calibration=False):
24
+ """
25
+ Preprocess CSV data for bubble plot visualization.
26
+
27
+ Args:
28
+ csv_file_name (str): Path to the CSV file containing the data.
29
+
30
+ Returns:
31
+ pd.DataFrame: Processed DataFrame ready for plotting.
32
+ """
33
+ try:
34
+ # Read the CSV file
35
+ bubbles_data = pd.read_csv(data_file_path)
36
+ column_names = bubbles_data.columns.tolist()
37
+ correction_method = 'Bonferroni' if 'Bonferroni' in column_names else 'FDR'
38
+
39
+ # Remove rows with missing values
40
+ bubbles_data = bubbles_data.dropna()
41
+
42
+ # # Add calibration points
43
+ if add_calibration:
44
+ calibration_points = pd.DataFrame({
45
+ "Category": ["calibration high", "calibration low"],
46
+ "RGS": [250, 1],
47
+ "AC": [0, 0],
48
+ "PValue": [1.00E-50, 1],
49
+ correction_method: [1.00E-50, 1]
50
+ })
51
+ bubbles_data = pd.concat([bubbles_data, calibration_points], ignore_index=True)
52
+
53
+ # Compute normalized value as a placeholder
54
+ scale_for_visualization=0.001
55
+ bubbles_data['bubbles_z'] = round(scale_for_visualization * (bubbles_data['PValue'] - bubbles_data['PValue'].mean()) / bubbles_data['PValue'].std(), 2)
56
+
57
+ # Create color coding based on Bonferroni-corrected p-values
58
+ conditions = [
59
+ (bubbles_data[correction_method] < 1e-40),
60
+ (bubbles_data[correction_method] < 1e-20),
61
+ (bubbles_data[correction_method] < 1e-10),
62
+ (bubbles_data[correction_method] < 1e-5),
63
+ (bubbles_data[correction_method] < 1e-2),
64
+ (bubbles_data[correction_method] < 5e-2),
65
+ (bubbles_data[correction_method] >= 5e-2)
66
+ ]
67
+
68
+ values = ["Col1", "Col2", "Col3", "Col4", "Col5", "Col6", "NS"]
69
+ bubbles_data['p_value_type'] = np.select(conditions, values, default='')
70
+
71
+
72
+ # Create size coding based on RGS values
73
+ size_conditions = [
74
+ (bubbles_data['RGS'] > 150),
75
+ (bubbles_data['RGS'] > 100),
76
+ (bubbles_data['RGS'] > 75),
77
+ (bubbles_data['RGS'] > 50),
78
+ (bubbles_data['RGS'] > 25),
79
+ (bubbles_data['RGS'] > 10),
80
+ (bubbles_data['RGS'] > 5),
81
+ (bubbles_data['RGS'] > 2),
82
+ (bubbles_data['RGS'] <= 2)
83
+ ]
84
+
85
+ size_values = ["Size1", "Size2", "Size3", "Size4", "Size5", "Size6", "Size7", "Size8", "Size9"]
86
+ bubbles_data['RGS_size'] = np.select(size_conditions, size_values, default='')
87
+
88
+ bubbles_data = order_categories_by_column(
89
+ bubbles_data,
90
+ category_column = 'Category',
91
+ order_by_column = correction_method,
92
+ ascending=False
93
+ )
94
+
95
+ return bubbles_data
96
+
97
+ except FileNotFoundError:
98
+ raise WormcatError(f"Error/Warning in create Bubble Chart: CSV file not found: {data_file_path}", ErrorCode.FILE_NOT_FOUND.to_dict())
99
+ except pd.errors.EmptyDataError:
100
+ raise WormcatError(f"Error/Warning in create Bubble Chart: The CSV file is empty: {data_file_path}", ErrorCode.FILE_LOAD_FAILED.to_dict())
101
+ except pd.errors.ParserError:
102
+ raise WormcatError(f"Error/Warning in create Bubble Chart: Error parsing CSV file: {data_file_path}", ErrorCode.FILE_LOAD_FAILED.to_dict())
103
+ except KeyError as e:
104
+ raise WormcatError(f"Error/Warning in create Bubble Chart: Required column missing in CSV file: {e}", ErrorCode.FILE_LOAD_FAILED.to_dict())
105
+
106
+
107
+ def order_categories_by_column(df, category_column='Category', order_by_column='PValue', ascending=True):
108
+ """
109
+ Convert a column to a categorical type ordered by another column.
110
+
111
+ Args:
112
+ df (pd.DataFrame): DataFrame with category and ordering columns.
113
+ category_column (str): Name of the column to be converted to categorical.
114
+ order_by_column (str): Name of the column to order by.
115
+ ascending (bool): Sort order - True for ascending, False for descending.
116
+
117
+ Returns:
118
+ pd.DataFrame: DataFrame with category column converted to categorical type.
119
+ """
120
+ if df is None or df.empty:
121
+ raise WormcatError("While creating bubble plot found DataFrame empty or None", ErrorCode.INVALID_STATE.to_dict())
122
+
123
+ if category_column not in df.columns:
124
+ raise WormcatError(f"While creating bubble plot required column '{category_column}' not found in DataFrame",ErrorCode.INVALID_STATE.to_dict())
125
+
126
+ if order_by_column not in df.columns:
127
+ raise WormcatError(f"While creating bubble plot required column '{order_by_column}' not found in DataFrame",ErrorCode.INVALID_STATE.to_dict())
128
+
129
+ # Sort the dataframe by the specified column
130
+ sorted_df = df.sort_values(by=order_by_column, ascending=ascending)
131
+
132
+ # Get the categories in the order determined by the sorting column
133
+ category_order = sorted_df[category_column].unique()
134
+
135
+ # Convert category column to categorical type with the specified order
136
+ df[category_column] = pd.Categorical(
137
+ df[category_column],
138
+ categories=category_order,
139
+ ordered=True
140
+ )
141
+
142
+ return df
143
+
144
+ def generate_bubble_plot(df, svg_file_path, plot_title=cs.DEFAULT_TITLE, width=cs.DEFAULT_WIDTH, height=cs.DEFAULT_HEIGHT):
145
+ """
146
+ Generate and save a bubble plot as an SVG using plotnine.
147
+
148
+ """
149
+ if df is None or df.empty:
150
+ raise WormcatError("While Generating Bubble Plot found DataFrame empty or None", ErrorCode.INVALID_STATE.to_dict())
151
+
152
+ required_columns = ['Category', 'bubbles_z', 'RGS_size', 'p_value_type']
153
+ missing_columns = [col for col in required_columns if col not in df.columns]
154
+ if missing_columns:
155
+ raise WormcatError(f"While Generating Bubble Plot found required columns missing in DataFrame: {missing_columns}", ErrorCode.MISSING_FIELD.to_dict())
156
+
157
+ # Define color and size mapping dictionaries
158
+ color_mapping = {
159
+ "Col1": "darkred",
160
+ "Col2": "orangered",
161
+ "Col3": "tomato",
162
+ "Col4": "orange",
163
+ "Col5": "gold",
164
+ "Col6": "yellow",
165
+ "NS": "dimgray"
166
+ }
167
+
168
+ size_mapping = {
169
+ "Size1": 10,
170
+ "Size2": 9,
171
+ "Size3": 8,
172
+ "Size4": 7,
173
+ "Size5": 6,
174
+ "Size6": 5,
175
+ "Size7": 2.5,
176
+ "Size8": 1,
177
+ "Size9": 0.1
178
+ }
179
+
180
+ # Manual color and size scales
181
+ color_scale = scale_color_manual(
182
+ name="P value",
183
+ labels=["10-40", "10-20", "10-10", "10-5", "0.001", "0.05", "NS"],
184
+ values=color_mapping,
185
+ limits=list(color_mapping.keys())
186
+ )
187
+
188
+ size_scale = scale_size_manual(
189
+ name="Count",
190
+ labels=["150", "100", "75", "50", "25", "10", "5", "2", "Zero"],
191
+ values=size_mapping,
192
+ limits=list(size_mapping.keys())
193
+ )
194
+
195
+ # Build the plot
196
+ p = (
197
+ ggplot(df, aes(x='Category', y='bubbles_z', size='RGS_size', color='p_value_type')) +
198
+ ggtitle(title=plot_title) +
199
+ geom_point(stat='identity') +
200
+ color_scale +
201
+ size_scale +
202
+ labs(y=' ') +
203
+ ylim(-1.0, 1.0) +
204
+ coord_flip() +
205
+ theme(
206
+ panel_grid=element_blank(),
207
+ panel_background=element_blank(),
208
+ legend_key=element_rect(fill="white", color="white"),
209
+ text=element_text(family="Arial", size=14),
210
+ axis_text_x=element_blank(), # Remove x-axis tick labels
211
+ axis_ticks_x=element_blank(), # Remove x-axis tick marks
212
+ plot_title=element_text(margin={"b": 3, "units": "lines"})
213
+ )
214
+ )
215
+
216
+ try:
217
+ p.save(filename=svg_file_path, format = "svg", width = width, height = height)
218
+ except Exception as e:
219
+ raise WormcatError(f"Failed to save plot: {e}", ErrorCode.INTERNAL_ERROR.to_dict())
220
+
221
+
222
+ def create_bubble_chart(dir_path: str, data_file_nm: str, plot_title="RGS", add_calibration=False) -> None:
223
+ try:
224
+ data_file_path = Path(dir_path) / data_file_nm
225
+ svg_file_nm = data_file_nm[:-3] + 'svg'
226
+ svg_file_path = Path(dir_path) / svg_file_nm
227
+
228
+ bubbles_data = preprocess_bubble_data(data_file_path, add_calibration = add_calibration)
229
+
230
+ # Scale the height of the bubble chart based on the number of items
231
+ bubbles_data_len = len(bubbles_data)
232
+ height_fun = lambda x: min(0.1 * x + 6.3, 25)
233
+ height = height_fun(bubbles_data_len)
234
+
235
+ generate_bubble_plot(bubbles_data, svg_file_path, plot_title = plot_title, height=height, width=9)
236
+ except Exception as e:
237
+ print(f"Error: {e}")
238
+
239
+
240
+
wormcat3/constants.py ADDED
@@ -0,0 +1,33 @@
1
+ from enum import Enum
2
+
3
+ # Enum for p-value adjustment methods
4
+ class PAdjustMethod(Enum):
5
+ BONFERRONI = 'bonferroni'
6
+ FDR = 'fdr_bh'
7
+
8
+ @classmethod
9
+ def from_str(cls, value: str) -> "PAdjustMethod":
10
+ for method in cls:
11
+ if method.value == value:
12
+ return method
13
+ raise ValueError(f"Invalid PAdjustMethod: {value}")
14
+
15
+
16
+ # Wormcat Configuration
17
+ DEFAULT_WORKING_DIR_PATH = "./wormcat_out"
18
+ DEFAULT_RUN_PREFIX = "run"
19
+
20
+ # Annotations Management Configuration
21
+ DEFAULT_P_ADJUST_THRESHOLD = 0.05
22
+ DEFAULT_ANNOTATION_FILE_NAME = "whole_genome_v2_nov-11-2021.csv"
23
+
24
+ # Gene Set Enrichment Analysis
25
+ DEFAULT_GSEA_RESULTS_DIR = "./gsea_results"
26
+
27
+ GENE_TYPE_WORMBASE_ID = "Wormbase.ID"
28
+ GENE_TYPE_SEQUENCE_ID = "Sequence.ID"
29
+
30
+ # Bubble Chart Configuration
31
+ DEFAULT_TITLE = "RGS"
32
+ DEFAULT_WIDTH = 6
33
+ DEFAULT_HEIGHT = 5.5