pythonflex 0.1.2__py3-none-any.whl → 0.1.3__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.
@@ -0,0 +1,221 @@
1
+ import os
2
+ import pandas as pd
3
+ import numpy as np
4
+ from .utils import dsave, dload
5
+ from tqdm import tqdm
6
+ from .logging_config import log
7
+ from importlib import resources
8
+ from pathlib import Path
9
+ tqdm.pandas()
10
+
11
+
12
+ def get_example_data_path(filename: str):
13
+ return resources.files("pyflex.data").joinpath("dataset").joinpath(filename)
14
+
15
+
16
+ def _load_file(filepath, ext):
17
+ loaders = {
18
+ ".csv": lambda f: pd.read_csv(f, index_col=0),
19
+ ".xlsx": lambda f: pd.read_excel(f, index_col=0),
20
+ ".parquet": pd.read_parquet,
21
+ ".p": pd.read_parquet
22
+ }
23
+ if ext not in loaders:
24
+ raise ValueError(f"Unsupported file extension: {ext}")
25
+
26
+ return loaders[ext](filepath)
27
+
28
+
29
+ def load_datasets(files, continue_with_common_genes=False):
30
+ preprocessing = dload("config")["preprocessing"]
31
+ data_dict= {}
32
+
33
+ for filename, meta in files.items():
34
+ if isinstance(meta, pd.DataFrame):
35
+ df = meta
36
+ elif isinstance(meta, dict):
37
+ filepath = meta["path"]
38
+ if isinstance(filepath, pd.DataFrame):
39
+ df = filepath
40
+ else:
41
+ ext = os.path.splitext(filepath)[1]
42
+ df = _load_file(filepath, ext)
43
+ else:
44
+ raise ValueError(f"Unsupported data structure for '{filename}': {type(meta)}")
45
+
46
+ df.index = df.index.str.split().str[0]
47
+ if preprocessing.get('normalize'):
48
+ log.info(f"{filename}: Normalization.")
49
+ df = (df - df.mean()) / df.std(ddof=0)
50
+
51
+ if preprocessing.get('drop_na'):
52
+ log.info(f"{filename}: Dropping missing values.")
53
+ df = df.dropna(how="any")
54
+
55
+ if preprocessing.get('fill_na'):
56
+ log.info(f"{filename}: Filling missing values with column mean.")
57
+ #df = df.T.fillna(df.mean(axis=1)).T
58
+ df = data_imputation(df)
59
+
60
+
61
+ data_dict[filename] = df
62
+
63
+ common_genes = get_common_genes(data_dict)
64
+ if continue_with_common_genes:
65
+ log.info(f"Continuing with common genes: {len(common_genes)}")
66
+ for filename, df in data_dict.items():
67
+ if df.index.isin(common_genes).any():
68
+ data_dict[filename] = df.loc[common_genes]
69
+
70
+ dsave({
71
+ "datasets": data_dict,
72
+ "sorting": {
73
+ k: v.get("sort", "high") if isinstance(v, dict) else "high"
74
+ for k, v in files.items()
75
+ }
76
+ }, "input")
77
+ log.done(f"Datasets loaded.")
78
+ return data_dict, common_genes
79
+
80
+
81
+
82
+
83
+ def drop_bad_samples(df, max_na=0.1):
84
+ total_elements = df.shape[0] * df.shape[1]
85
+ percent_nan = np.isnan(df.values).sum() / total_elements if total_elements > 0 else 0
86
+ has_nan_per_sample = np.isnan(df.values).any(axis=0) # how many samples has NA.
87
+
88
+ log.info(f"Total: {total_elements}, Percent NaN: {percent_nan:.2%}, Samples with NaN: {np.sum(has_nan_per_sample)} / {df.shape[1]}")
89
+
90
+ num_genes = df.shape[0] # E.g., 1178 (total rows/genes)
91
+ na_per_sample = np.isnan(df.values).sum(axis=0) / num_genes # Fraction NA per sample (column)
92
+ good_samples = na_per_sample <= max_na # Keep if <=10% NA
93
+ data_filtered = df.loc[:, good_samples] # Drop bad samples (those >10% NA)
94
+
95
+ log.info(f"Filtered samples: {data_filtered.shape[1]} (removed {df.shape[1] - data_filtered.shape[1]} samples with >{max_na*100:.0f}% NAs)")
96
+ return data_filtered
97
+
98
+
99
+
100
+ def data_imputation(df):
101
+ log.info("Imputing missing values with gene means ...")
102
+ gene_means = np.nanmean(df.values, axis=1) # 1D array: means per gene
103
+ data_values = df.values.copy()
104
+ rows, cols = np.where(np.isnan(data_values))
105
+ if len(rows) > 0:
106
+ data_values[rows, cols] = np.take(gene_means, rows)
107
+
108
+ df = pd.DataFrame(data_values, index=df.index, columns=df.columns)
109
+ log.info(f"Data after imputation: {df.shape[0]} genes, {df.shape[1]} samples")
110
+ return df
111
+
112
+
113
+
114
+
115
+ def get_common_genes(datasets):
116
+ log.started("Finding common genes across datasets.")
117
+ gene_sets = [set(df.index) for df in datasets.values()]
118
+ common_genes = set.intersection(*gene_sets)
119
+ log.done(f"Common genes found: {len(common_genes)}")
120
+ dsave(common_genes, "common", "common_genes")
121
+ return list(common_genes)
122
+
123
+
124
+ def filter_matrix_by_genes(matrix, genes_present_in_terms):
125
+ log.started("Filtering matrix using genes present in terms.")
126
+ genes = matrix.index.intersection(genes_present_in_terms)
127
+ matrix = matrix.loc[genes, genes]
128
+ log.done(f"Filtering matrix: {matrix.shape}")
129
+ return matrix.loc[genes, genes]
130
+
131
+
132
+
133
+
134
+ def load_gold_standard():
135
+
136
+ config = dload("config")
137
+ common_genes = dload("common", "common_genes")
138
+
139
+ gold_standard_source = config['gold_standard']
140
+ log.started(f"Loading gold standard: {gold_standard_source}, Min complex size: {config['min_genes_in_complex']}, Jaccard filtering: {config['jaccard']}")
141
+ if not common_genes:
142
+ raise ValueError("Common genes not found.")
143
+
144
+ # Define gold standard file paths for predefined sources
145
+ gold_standard_files = {
146
+ "CORUM": "gold_standard/CORUM.parquet",
147
+ "GOBP": "gold_standard/GOBP.parquet",
148
+ "PATHWAY": "gold_standard/PATHWAY.parquet"
149
+ }
150
+
151
+ if gold_standard_source in gold_standard_files:
152
+ # Load predefined gold standard from package resources
153
+ filename = gold_standard_files[gold_standard_source]
154
+ filename_path = resources.files("pyflex.data").joinpath(filename)
155
+ if not filename_path.exists(): # Check if the file exists
156
+ raise ValueError(f"Invalid Gold Standard type: {gold_standard_source}. File not found.")
157
+ terms = pd.read_parquet(filename_path) # type: ignore
158
+ elif Path(gold_standard_source).suffix.lower() == '.csv':
159
+ # Load user-provided custom gold standard from CSV file
160
+ filename_path = Path(gold_standard_source)
161
+ if not filename_path.exists():
162
+ raise ValueError(f"Custom gold standard CSV file not found: {gold_standard_source}")
163
+ log.done(f"Loading custom gold standard from CSV: {gold_standard_source}")
164
+ terms = pd.read_csv(filename_path)
165
+ else:
166
+ raise ValueError(f"Invalid gold standard source: {gold_standard_source}. Must be one of {list(gold_standard_files.keys())} or a path to a .csv file.")
167
+
168
+ common_genes_set = set(common_genes)
169
+ terms["used_genes"] = terms["Genes"].apply(lambda x: list(set(x.split(";")) & common_genes_set))
170
+ terms["n_used_genes"] = terms["used_genes"].apply(len)
171
+ log.info(f"Applying min_genes_in_complex filtering: {config['min_genes_in_complex']}")
172
+ terms = terms[terms["n_used_genes"] >= config['min_genes_in_complex']]
173
+ terms["hash"] = terms["used_genes"].apply(lambda x: [hash(i) for i in x])
174
+
175
+ if config['jaccard']:
176
+ log.info("Applying Jaccard filtering. Remove terms with identical gene sets.")
177
+ terms = filter_duplicate_terms(terms)
178
+
179
+
180
+ genes_present_in_terms = list(set(terms["used_genes"].explode().unique()) & common_genes_set)
181
+ # if there is column called "ID", set it as index
182
+ if "ID" in terms.columns:
183
+ terms = terms.set_index("ID")
184
+
185
+ dsave(terms, "common", "terms")
186
+ dsave(genes_present_in_terms, "common", "genes_present_in_terms")
187
+ log.done("Gold standard loading completed.")
188
+ return terms, genes_present_in_terms
189
+
190
+
191
+
192
+
193
+
194
+ def filter_duplicate_terms(terms):
195
+ log.started("Filtering duplicate terms using optimized method.")
196
+
197
+ # Precompute frozen gene sets and hash them
198
+ terms = terms.copy()
199
+ terms["gene_set"] = terms["used_genes"].map(lambda x: frozenset(x))
200
+
201
+ # Group by identical gene sets
202
+ grouped = terms.groupby("gene_set", sort=False)
203
+
204
+ # Identify duplicate clusters (groups with >1 term)
205
+ duplicate_clusters = []
206
+ for _, group in grouped:
207
+ if len(group) > 1:
208
+ duplicate_clusters.append(group["ID"].values)
209
+
210
+ # Determine which IDs to keep (smallest ID in each duplicate cluster)
211
+ keep_ids = set(terms["ID"])
212
+ for cluster in duplicate_clusters:
213
+ sorted_ids = sorted(cluster)
214
+ keep_ids.difference_update(sorted_ids[1:]) # Remove all but smallest ID
215
+
216
+ # Filter and clean up
217
+ filtered = terms[terms["ID"].isin(keep_ids)].copy()
218
+ filtered.drop(columns=["gene_set"], inplace=True)
219
+
220
+ log.done(f"{len(terms) - len(filtered)} terms removed due to identical gene sets.")
221
+ return filtered
pythonflex/utils.py ADDED
@@ -0,0 +1,100 @@
1
+ import os
2
+ import re # For sanitization (built-in, minimal regex)
3
+ import tempfile
4
+ import joblib
5
+ import numpy as np
6
+ import pandas as pd
7
+
8
+ # Constants
9
+ TMP_ROOT = ".tmp"
10
+ VALID_EXTS = {".feather", ".npy", ".pkl"}
11
+
12
+ # Helper to sanitize names (make filesystem-safe)
13
+ def _sanitize(name):
14
+ if not name:
15
+ return "data"
16
+ # Replace forbidden/problematic chars with '_', collapse multiples, strip edges
17
+ safe = re.sub(r'[<>:"/\\|?*$,\s]+', '_', str(name).strip())
18
+ safe = re.sub(r'_+', '_', safe).strip('_')
19
+ return safe if safe else "data"
20
+
21
+ # Helper to get safe path
22
+ def _safe_path(category, name=None, ext=".pkl"):
23
+ safe_category = _sanitize(category)
24
+ dir_path = os.path.join(TMP_ROOT, safe_category)
25
+ os.makedirs(dir_path, exist_ok=True)
26
+ safe_name = _sanitize(name) if name else "data"
27
+ return os.path.join(dir_path, f"{safe_name}{ext}")
28
+
29
+ # Save function
30
+ def dsave(data, category, name=None, path=None): # 'path' ignored for compatibility with old code
31
+ # If data is dict and no name, recurse on each item
32
+ if name is None and isinstance(data, dict):
33
+ for k, v in data.items():
34
+ dsave(v, category, k)
35
+ return
36
+
37
+ # Choose best extension based on type
38
+ if isinstance(data, pd.DataFrame):
39
+ ext = ".feather"
40
+ save_func = lambda p: data.to_feather(p)
41
+ elif isinstance(data, np.ndarray):
42
+ ext = ".npy"
43
+ save_func = lambda p: np.save(p, data, allow_pickle=False)
44
+ else:
45
+ ext = ".pkl"
46
+ save_func = lambda p: joblib.dump(data, p, compress=0) # Add compress=3 if needed
47
+
48
+ target = _safe_path(category, name, ext)
49
+
50
+ # Atomic save: Write to temp file, then rename
51
+ with tempfile.NamedTemporaryFile(dir=os.path.dirname(target), delete=False) as tf:
52
+ tmp_path = tf.name
53
+ tf.close() # Close so save_func can write
54
+ save_func(tmp_path)
55
+ os.replace(tmp_path, target) # Atomic move
56
+
57
+ # Load function
58
+ def dload(category, name=None, path=None): # 'path' ignored for compatibility
59
+ dir_path = os.path.join(TMP_ROOT, _sanitize(category))
60
+
61
+ if not os.path.exists(dir_path):
62
+ return {}
63
+
64
+ if name is None:
65
+ # Load all in category as dict
66
+ out = {}
67
+ for filename in os.listdir(dir_path):
68
+ if not any(filename.endswith(ext) for ext in VALID_EXTS):
69
+ continue
70
+ k = os.path.splitext(filename)[0] # Key from filename (without ext)
71
+ full_path = os.path.join(dir_path, filename)
72
+ try:
73
+ if filename.endswith(".feather"):
74
+ out[k] = pd.read_feather(full_path)
75
+ elif filename.endswith(".npy"):
76
+ out[k] = np.load(full_path, mmap_mode="r") # MMap for perf
77
+ elif filename.endswith(".pkl"):
78
+ out[k] = joblib.load(full_path, mmap_mode="r") # MMap for perf
79
+ except (EOFError, ValueError, OSError):
80
+ print(f"Warning: '{full_path}' is corrupted. Skipping...")
81
+ os.remove(full_path) # Delete corrupted file
82
+ return out
83
+
84
+ # Load specific name (try extensions in order)
85
+ for ext in VALID_EXTS:
86
+ target = _safe_path(category, name, ext)
87
+ if os.path.exists(target):
88
+ try:
89
+ if ext == ".feather":
90
+ return pd.read_feather(target)
91
+ elif ext == ".npy":
92
+ return np.load(target, mmap_mode="r") # MMap for perf
93
+ elif ext == ".pkl":
94
+ return joblib.load(target, mmap_mode="r") # MMap for perf
95
+ except (EOFError, ValueError, OSError):
96
+ print(f"Warning: '{target}' is corrupted. Deleting and returning {{}}...")
97
+ os.remove(target)
98
+ return {}
99
+ return {}
100
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythonflex
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: pythonFLEX is a benchmarking toolkit for evaluating CRISPR screen results against biological gold standards. The toolkit computes gene-level and complex-level performance metrics, helping researchers systematically assess the biological relevance and resolution of their CRISPR screening data.
5
5
  Author-email: Yasir Demirtaş <tyasird@hotmail.com>
6
6
  Requires-Python: >=3.9
@@ -0,0 +1,21 @@
1
+ pythonflex/__init__.py,sha256=rz-8y-zEJQR3ThbhlIUV0N1q7Z-4UNHpHbOglXFp52c,1292
2
+ pythonflex/analysis.py,sha256=Bu4wK7bnSlaXFRoqUjAGlQP5CoGYsrr1x_ZJRE1AuSc,52966
3
+ pythonflex/logging_config.py,sha256=QrwI9NSpi026rGKgitky_zP67aH6lo7rKA2TTAyrm1E,2011
4
+ pythonflex/plotting.py,sha256=ydC5CHIFl9GAGkYFyoXZ9v4ESKxTL_4vvoOkSR8hvCU,19508
5
+ pythonflex/preprocessing.py,sha256=wBuN3qfe7-qczzLtugOtnV3Un4QK4Kc_G8AZ1MMlhNk,8575
6
+ pythonflex/utils.py,sha256=nyVlGu5OXpz5YPj48hXueL5ja88sQ2PUiJ76c4USg4A,3886
7
+ pythonflex/data/dataset/liver_cell_lines_500_genes.csv,sha256=qfKsqPjL41Y1GuxxAhc-MfaNO0mX6Qju_SeynKSpEiM,238639
8
+ pythonflex/data/dataset/melanoma_cell_lines_500_genes.csv,sha256=ByxcaDDqLlRtAyuCKhHeFQCitIBk2-Q4Hn6k8BNUF6c,620887
9
+ pythonflex/data/dataset/neuroblastoma_cell_lines_500_genes.csv,sha256=IxJI8E-smagbxHRvTjvQLZxuu89MuCw8XrReMSsViUI,365992
10
+ pythonflex/data/gold_standard/CORUM.parquet,sha256=AkLiflQAeQ6K3HG-PIdLbZ8vEF9GtNObtlY7TkxHyaw,131858
11
+ pythonflex/data/gold_standard/GOBP.parquet,sha256=YQGhRcHSiN_cMKytCUYNCfcDwYj9L3TLFNwobiS2f3M,1025099
12
+ pythonflex/data/gold_standard/PATHWAY.parquet,sha256=bFRDe3PQ_TFc7B1uZuynwOGcgxESLaOy1Zt5gpQ1Oso,277386
13
+ pythonflex/data/gold_standard/corum.csv,sha256=2rZeyr2Ghm7f-gFxCZnhPtxI2jxRoiZMUEH2EJwAgsI,208889
14
+ pythonflex/data/gold_standard/gobp.csv,sha256=TO9yfx9mO8WkXvWfSB-pFId9T8xYfqdZpshAXC0Fyj8,1739167
15
+ pythonflex/data/gold_standard/pathway.csv,sha256=J3HKVLUZ_Oxucmn_14ieYp3Wr2lcKtp0nIl4_8_K2Yc,489424
16
+ pythonflex/examples/basic_usage.py,sha256=RBtJFphAE6NpCmSm9tMYN6FSdcpbWQzwkTlpX2ZejvI,2387
17
+ pythonflex/examples/dataset_filtering.py,sha256=YBHgkbHj9SpIj-qLxQhHS4UWjERv4MHyb2t_fR6AONE,971
18
+ pythonflex-0.1.3.dist-info/METADATA,sha256=U9S0qymqzZ0NUxAAy7Vjn9YXqRaSxdcSm3zI2wd-aoI,3928
19
+ pythonflex-0.1.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
20
+ pythonflex-0.1.3.dist-info/entry_points.txt,sha256=37liK1baI_CRVDivpjsn8JDClL9_YeTTuSMAZ3Ty7oE,47
21
+ pythonflex-0.1.3.dist-info/RECORD,,
@@ -1,4 +0,0 @@
1
- pythonflex-0.1.2.dist-info/METADATA,sha256=LGYUDuQ3o_ANpHA6qovfk1w1VqNuz0pb-YwBs8tMNO0,3928
2
- pythonflex-0.1.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
3
- pythonflex-0.1.2.dist-info/entry_points.txt,sha256=37liK1baI_CRVDivpjsn8JDClL9_YeTTuSMAZ3Ty7oE,47
4
- pythonflex-0.1.2.dist-info/RECORD,,