: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