:orphan: Source: bactoscoop/curation.py ============================== .. raw:: html
1# -*- coding: utf-8 -*-
2"""
3Created on Mon Jul 31 16:46:21 2023
4
5@author: Bart Steemans. Govers Lab.
6"""
7import pickle
8import pandas as pd
9import numpy as np
10
11
12class Curation:
13
14 def __init__(self, svm_df):
15
16 self.svm_df = svm_df
17 self.features = None
18 self.curated_df = None
19 self.svm_model = None
20
21 def compiled_curation(self, path_to_model, cols):
22 """
23 Perform compiled cell curation using a trained SVM model.
24
25 This method curates the dataset by making predictions using a trained Support Vector Machine (SVM) model.
26 Cells that do not meet the criteria specified by the model are flagged for potential removal.
27
28 Parameters
29 ----------
30 path_to_model : str
31 The path to the trained SVM model file.
32
33 cols : int
34 The number of columns to exclude in the dataset during curation.
35
36 Returns
37 -------
38 pd.DataFrame
39 A DataFrame containing the curated dataset.
40
41 """
42 self.load_model(path_to_model)
43 self.prepare_dataframe(cols)
44 self.curated_df = self.make_predictions()
45 return self.curated_df
46
47 def load_model(self, path_to_model):
48 """
49 Load a trained SVM model from a file.
50
51 This method loads a trained Support Vector Machine (SVM) model from a file located at the given path.
52
53 Parameters
54 ----------
55 path_to_model : str
56 The path to the trained SVM model file.
57
58 """
59 with open(path_to_model, "rb") as f:
60 self.svm_model = pickle.load(f)
61
62 def prepare_dataframe(self, cols):
63 """
64 Prepare the feature dataset for SVM curation.
65
66 This method prepares the feature dataset for cell curation using a Support Vector Machine (SVM) model.
67 It extracts the relevant feature columns while ensuring that `cell_id` and `frame` are preserved.
68
69 Parameters
70 ----------
71 cols : int
72 Retained for backward compatibility; feature columns are selected
73 by name and this argument is currently unused.
74 """
75
76 # Identify non-feature columns (but keep cell_id & frame for tracking)
77 exclude_columns = ["image_name", "contour", "label"] # Keep cell_id & frame
78 cols_to_include = [col for col in self.svm_df.columns if col not in exclude_columns]
79 cols_to_include.sort()
80
81 # Preserve cell_id and frame separately for re-mapping
82 self.cell_id_frame = self.svm_df[["cell_id", "frame"]].copy()
83
84 # Select only feature columns for training
85 self.features = self.svm_df[cols_to_include].drop(columns=["cell_id", "frame"])
86
87 # Convert numeric columns to float
88 self.features[self.features.select_dtypes(include=[np.number]).columns] = (
89 self.features.select_dtypes(include=[np.number]).astype(float)
90 )
91
92 # Check for infinite and NaN values
93 mask = ~(
94 np.isinf(self.features).any(axis=1) | np.isnan(self.features).any(axis=1)
95 )
96
97 # Store discarded rows separately
98 self.svm_df_nan = self.svm_df[~mask].copy()
99 self.svm_df_nan["label"] = 0 # Assign label 0 to discarded cells
100
101 # Keep only valid rows for training
102 self.features = self.features[mask]
103 self.cell_id_frame = self.cell_id_frame[mask] # Ensure cell_id & frame match
104 self.svm_df = self.svm_df[mask]
105
106 # Print the number of removed rows
107 num_removed_rows = len(mask) - mask.sum()
108 print(f"\nNumber of removed rows: {num_removed_rows}")
109
110 def get_label_proportions(self):
111 """
112 Calculate the proportions of labels in the curated dataset.
113
114 This method calculates and returns the proportions of label 1 and label 0 in the curated dataset.
115 It counts the occurrences of each label and divides them by the total number of samples in the dataset to compute the proportions.
116
117 Returns
118 -------
119 float
120 The proportion of label 1 in the curated dataset.
121 float
122 The proportion of label 0 in the curated dataset.
123
124 """
125 label_counts = self.curated_df["label"].value_counts()
126 total_samples = len(self.curated_df)
127
128 proportion_label_1 = label_counts.get(1, 0) / total_samples
129 proportion_label_0 = label_counts.get(0, 0) / total_samples
130
131 return proportion_label_1, proportion_label_0
132
133 def make_predictions(self):
134 """
135 Make predictions using the SVM model and reattach `cell_id` and `frame`.
136
137 Returns
138 -------
139 pd.DataFrame
140 A DataFrame containing the predicted labels with corresponding `cell_id` and `frame`.
141 """
142
143 # Use the schema stored with the trained model. An unnamed model can
144 # only check width and could silently interpret reordered columns.
145 expected_columns = getattr(self.svm_model, "feature_names_in_", None)
146 if expected_columns is None:
147 raise ValueError(
148 "The SVM model has no feature_names_in_ schema. Train it on a "
149 "named pandas DataFrame before using it for curation."
150 )
151 expected_columns = list(expected_columns)
152 actual_columns = list(self.features.columns)
153 if actual_columns != expected_columns:
154 raise ValueError(
155 "SVM feature columns do not match the model's training schema. "
156 f"Expected {expected_columns}; got {actual_columns}."
157 )
158
159 # Predict labels
160 predictions = self.svm_model.predict(self.features)
161
162 # Create curated DataFrame
163 self.curated_df = self.svm_df.copy()
164 self.curated_df["label"] = predictions
165
166 # Reattach `cell_id` and `frame`
167 self.curated_df[["cell_id", "frame"]] = self.cell_id_frame.values
168
169 # Add back NaN/Inf-removed rows
170 self.curated_df = pd.concat([self.curated_df, self.svm_df_nan], ignore_index=True)
171
172 return self.curated_df
173
174 def get_control(self, num_pos=5, num_neg=5):
175 """
176 Get control cell IDs for positive and negative classes.
177
178 This method retrieves cell IDs for positive and negative classes based on the curated dataset.
179 It randomly selects a specified number of positive and negative cell IDs and returns them as lists, along with the corresponding 'frame' values.
180
181 Parameters
182 ----------
183 num_pos : int, optional
184 The number of positive class cell IDs to retrieve. The default is 5.
185 num_neg : int, optional
186 The number of negative class cell IDs to retrieve. The default is 5.
187
188 Returns
189 -------
190 Tuple
191 Two lists containing tuples of ('frame', 'cell_id') for positive and negative class control cell IDs.
192 If there are not enough samples for one class, the corresponding list will be empty.
193 """
194
195 positive_indices = self.curated_df[self.curated_df["label"] == 1].index.tolist()
196 negative_indices = self.curated_df[self.curated_df["label"] == 0].index.tolist()
197
198 # Check if there are not enough positive or negative samples
199 if len(positive_indices) < num_pos:
200 print("No positive labels")
201 return None, None # Return None for positive and available negative indices
202 elif len(negative_indices) < num_neg:
203 print("No negative labels")
204 return None, None # Return available positive indices and None for negative
205
206 positive_indices_random = np.random.choice(
207 positive_indices, size=num_pos, replace=False
208 )
209 negative_indices_random = np.random.choice(
210 negative_indices, size=num_neg, replace=False
211 )
212
213 # Retrieve cell_id and frame for selected indices
214 positive_cell_frames = [
215 (self.curated_df.loc[idx, "cell_id"], self.curated_df.loc[idx, "frame"])
216 for idx in positive_indices_random
217 ]
218 negative_cell_frames = [
219 (self.curated_df.loc[idx, "cell_id"], self.curated_df.loc[idx, "frame"])
220 for idx in negative_indices_random
221 ]
222
223 return positive_cell_frames, negative_cell_frames