Module calciumflexanalysis.calcium_flex

Expand source code
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import string, math
from platemapping import plate_map as pm
import matplotlib.patches as mpl_patches
from scipy.optimize import curve_fit
import copy
import itertools

# define custom errors
class Error(Exception):
    pass
class CompoundNameError(pm.PlateMapError):
    pass
class ProteinNameError(pm.PlateMapError):
    pass
class UnitsError(pm.PlateMapError):
    pass
class DataError(Error):
    pass
class DataReadInError(DataError):
    pass
class ControlError(pm.PlateMapError):
    pass

# define well plate dimensions
wells = {6:(2, 3), 12:(3, 4), 24:(4, 6), 48:(6, 8), 96:(8, 12), 384:(16, 24)} 

def read_in(raw_data):
    """Returns a dataframe of the old flex data."""
    df = pd.read_csv(raw_data, delimiter='\t', skiprows = 2, skipfooter=3, engine = 'python', encoding = 'mbcs') # update to check headers if there is no title?
    return df

def read_in_new(raw_data, skiprows, skipfooter):
    """Returns a dataframe of the new flex data."""
    df = pd.read_csv(raw_data, delimiter='\t', skiprows = skiprows, skipfooter=skipfooter, engine = 'python', encoding = "utf-16", skip_blank_lines=True) 
    return df

# curve fitting functions
def _ec50_func(x,top,bottom, ec50, hill):
    z=(ec50/x)**hill
    return (bottom + ((top-bottom)/(1+z)))   

def _ic50_func(x,top,bottom, ic50, hill):
    z=(ic50/x)**hill
    return (top - ((top-bottom)/(1+z)))
func_dict = {"ic50":_ic50_func, "ec50": _ec50_func} # dicitonary to access required curve fitting function


def plot_color(dataframe, cmap, iterrange):
    """Returns a color for each index value (accessed by iterrange) of a dataframe."""
    types = list(dataframe.index)
    cmap = plt.get_cmap(cmap)
    colors = cmap(np.linspace(0, 1, len(types)))
    colordict = dict(zip(types, colors))
    color = colordict.get(dataframe.index[iterrange])
    return color

def get_color(condition_list, cmap, name):
    """Returns a color for each condition plotted. 
    
    Similar to plot_color() but used for lists instead of dataframes - see plot_curve(combine = True)."""
    cmap = plt.get_cmap(cmap)
    colors = cmap(np.linspace(0, 1, len(condition_list)))
    colordict = dict(zip(condition_list, colors))
    color = colordict.get(name)
    return color

class CaFlexPlate:
    """Class used for the analysis of individual Calcium Flex well plates.
    
    :param raw_data: Raw, unprocessed data from experiment
    :type raw_data: .txt file
    :param plate_map_file: Filled template plate map that contains the information for each well of the well plate
    :type plate_map_file: .csv
    :param inject: Activator injection time point
    :type inject: float or int
    :param map_type: 'short' or 'long' - Denotes the type of plate map file used, default = 'short'
    :type map_type: str
    :param size: Size of well plate - 6, 12, 24, 48, 96 or 384. plate_map_file MUST have the appropriate dimensions, default = 96
    :type size: int
    :param data_type: 'new' or 'old' - denotes type of flex data, default = 'old'
    :type data_type: str
    :param valid: Validates every well - 'True' sets every well as valid, 'False' wells will not be used for analysis, default = True
    :type valid: bool
    :param processed_data: Dictionary containing separate dataframes of the time and flex data for every well
    :type processed_data: dictionary of pandas dataframes
    :param plate_map: plate_map_file converted as a dataframe
    :type plate_map: pandas dataframe
    :param title: Title of plate or assay 
    :type title: str
    :param skiprows: Number of rows to skip when reading in new data
    :type skiprows: int
    :param skipfooter: Number of rows from footer to skip when reading in new data
    :type skipfooter: int
    """
    def __init__(self, raw_data, plate_map_file, inject, map_type = 'short', data_type = 'old', valid = True, size = 96, title = "", skiprows = 2, skipfooter = 3):
        
        self.raw_data = raw_data
        self.plate_map_file = plate_map_file
        self.inject = inject
        self.map_type = map_type
        self.size = size
        self.data_type = data_type
        self.valid = valid # False invalidates all wells of plate
        self.skiprows = skiprows
        self.skipfooter = skipfooter
        self.processed_data = {'ratio':self._data_processed()}
        self.plate_map = self._give_platemap()
        self.grouplist = ['Protein','Type', 'Compound','Concentration', 'Concentration Units']
        
        # invalidate all wells if valid = False
        if self.valid == False:
            self.plate_map['Valid'] = valid
        if self.valid == True:
            self.plate_map['Valid'] = valid # ?????!!!
            
        # optional title param
        self.title = title
        # add default title
        if self.title == "":
            self.title = self.raw_data[:-4] # change to experiment title in plate map?
            
        # check params
        if wells.get(self.size) == None:
            raise pm.PlateMapError("Invalid size. Try 6, 12, 24, 48, 96 or 384.")
            
        print("Uploaded!")
        
    def _give_platemap(self):
        """Returns platemap dataframe."""
        if self.map_type == 'short':
             platemap = pm.short_map(self.plate_map_file, size = self.size, valid = self.valid)
        elif self.map_type == 'long':
            platemap = pm.plate_map(self.plate_map_file, size = self.size, valid = self.valid)
        else:
            raise pm.PlateMapError("Invalid map type. Try 'short' or 'long'.")
        return platemap
        
    def _data_processed(self):
        """Returns a timemap and datamap as a tuple."""
        if self.data_type == 'old':
            try:
                df = read_in(self.raw_data)
                # create new dataframe containing all time values for each well
                dftime = df.filter(regex = 'T$', axis = 1)
               # edit header names (this will come in handy in a second)
                dftime.columns = dftime.columns.str.replace('T', "")
                # extract list of header names 
                wellslist = list(dftime.columns.values)
                # transpose x and y axes of dataframe - generate time 'rows'
                dftime = dftime.transpose()
                # create new dataframe containing data measurements for each cell
                dfdata = df[wellslist]
                # transpose x and y axes
                dfdata = dfdata.transpose()
                # return timemap and datamap as a tuple
                return {'time':dftime, 'data':dfdata}
            
            except:
                print("Read in error")

        if self.data_type == 'new':
            try:
            
                newdata = read_in_new(self.raw_data, self.skiprows, self.skipfooter)
                # split the dataframe into the two data series
                data1 = newdata.iloc[:int(newdata.shape[0]/2), :]
                data1 = data1.reset_index(drop=True)
                data2 = newdata.iloc[int(newdata.shape[0]/2):, :]
                data2 = data2.reset_index(drop=True)
                newdatadict = {"data1":data1, "data2":data2}

                # check length
                if len(data1) != len(data2):
                    raise DataError("Read in error")
                    
            # try alternative read in if lengths not equal
            except DataError: 
                self.skipfooter = self.skipfooter+1
                newdata = read_in_new(self.raw_data, self.skiprows, self.skipfooter)
                # split the dataframe into the two data series
                data1 = newdata.iloc[:int(newdata.shape[0]/2), :]
                data1 = data1.reset_index(drop=True)
                data2 = newdata.iloc[int(newdata.shape[0]/2):, :]
                data2 = data2.reset_index(drop=True)
                newdatadict = {"data1":data1, "data2":data2}
                
                if len(data1) != len(data2):
                    raise DataError("Read in error")
                else:
                    print("self.skiprows updated. This is data type requires skipfooter equal {}.".format(self.skipfooter))
            except DataReadInError:
                print("Check your data. Read in is faulty.")
                
            # initiate empty dictionary to house preprocessed data and time values
            preprocessed = {}
            # loop produces two dictionaries containing preprocessed flux data and their corresponding time values
            for key, value in newdatadict.items():
                # reset indexes
                value = value.reset_index(drop=True)
                dftime = value.filter(regex = 'T$', axis = 1)
                # edit header names (this will come in handy in a second)
                dftime.columns = dftime.columns.str.replace('T', "")
                # extract list of header names 
                wellslist = list(dftime.columns.values)
                # transpose x and y axes of dataframe - generate time 'rows'
                dftime = dftime.transpose()
                # create new dataframe containing data measurements for each cell
                dfdata = value[wellslist]
                # transpose x and y axes
                dfdata = dfdata.transpose()
                # return timemap and datamap as a tuple
                tempdict = {'time':dftime, 'data':dfdata}
                # append dictionary
                preprocessed[key] = tempdict

            # take means of the time in new dataframe
            mean_time = pd.concat((preprocessed["data1"]['time'], preprocessed["data2"]["time"]))
            mean_time = mean_time.groupby(mean_time.index).mean()
            mean_time = mean_time.reindex(wellslist)

            # take difference of data to get change in flux
            difference = preprocessed["data1"]['data'].divide(preprocessed["data2"]["data"])
            return {'time':mean_time, 'data':difference}

        elif self.data_type not in ('old', 'new'):
            raise DataError("Incorrect data type")
            
    def visualise_assay(self, share_y, export = False, title = "", cmap = 'Dark2_r',
             colorby = 'Type', labelby = 'Type', dpi = 200):
        """Returns color-coded and labelled plots of the data collected for each well of the well plate.
        
        :param share_y: 'True' sets y axis the same for all plots
        :type share_y: bool
        :param export: If 'True' a .png file of the figure is saved, default = False
        :type export: bool
        :param title: Sets the title of the figure, optional
        :type title: str
        :param cmap: Sets the colormap for the color-coding, default = 'Dark2_r'
        :type cmap: str
        :param colorby: Chooses the parameter to color code by, for example 'Type', 'Contents', 'Concentration', 'Compound', 'Protein', 'Concentration Units', default = 'Type'
        :type colorby: str
        :param labelby: Chooses the parameter to label code by, for example 'Type', 'Contents', 'Concentration', 'Compound', 'Protein', 'Concentration Units', default = 'Type'
        :type labelby: str
        :param dpi: Size of the figure, default = 200
        :type dpi: int
        :return: Figure of plotted data for each well of the well plate described in plate_map_file
        :rtype: figure
        """
        self.title = title
        
        # check labelby and colorby
        if colorby not in self.plate_map.columns:
            raise pm.HeaderError("colorby parameter not in plate map")
        if labelby not in self.plate_map.columns:
            raise pm.HeaderError("labelby parameter not in plate map")
        
        pm.visualise_all_series(x = self.processed_data['ratio']['time'], y = self.processed_data['ratio']['data'], 
                            share_y = share_y, platemap = self.plate_map, size = self.size, 
                            export = export, cmap = cmap,
                            colorby = colorby, labelby = labelby, 
                            dpi = dpi, title = self.title)
        
        plt.suptitle(self.title, y = 0.95)
        
    def see_plate(self, title = "", export = False, cmap = 'Paired',
             colorby = 'Type', labelby = 'Type', dpi = 150):
        """Returns a visual representation of the plate map.
    
        The label and colour for each well can be customised to be a variable, for example 'Compound', 'Protein', 'Concentration', 'Concentration Units', 'Contents' or 'Type'. The size of the plate map used to generate the figure can be either 6, 12, 24, 48, 96 or 384. 
        :param size: Size of platemap, 6, 12, 24, 48, 96 or 384, default = 96
        :type size: int    
        :param export: If 'True' a .png file of the figure is saved, default = False
        :type export: bool
        :param title: Sets the title of the figure, optional
        :type title: str
        :param cmap: Sets the colormap for the color-coding, default = 'Paired'
        :type cmap: str
        :param colorby: Chooses the parameter to color code by, for example 'Type', 'Contents', 'Concentration', 'Compound', 'Protein', 'Concentration Units', default = 'Type'
        :type colorby: str
        :param labelby: Chooses the parameter to label code by, for example 'Type', 'Contents', 'Concentration', 'Compound', 'Protein', 'Concentration Units', default = 'Type'
        :type labelby: str
        :param dpi: Size of the figure, default = 150
        :type dpi: int
        :return: Visual representation of the plate map.
        :rtype: figure
        """
        pm.visualise(self.plate_map, title = title, size = self.size, export = export, cmap = cmap,
             colorby = colorby, labelby = labelby, dpi = dpi)
    
       
    def see_wells(self, to_plot, share_y = True, colorby = 'Type', labelby = 'Type', cmap = 'Dark2_r'):
        """Returns plotted data from stipulated wells.

        :param size: Size of platemap, 6, 12, 24, 48, 96 or 384, default = 96
        :type size: int   
        :param to_plot: Wells to plot
        :type to_plot: string or list of strings (well ID's), e.g. "A1", "A2", "A3"
        :param share_y: 'True' sets y axis the same for all plots, default = 'True'
        :type share_y: bool
        :param cmap: Sets the colormap for the color-coding, optional
        :type cmap: str
        :param colorby: Chooses the parameter to color code by, for example 'Type', 'Contents', 'Concentration', 'Compound', 'Protein', 'Concentration Units', default = 'Type'
        :type colorby: str
        :param labelby: Chooses the parameter to label code by, for example 'Type', 'Contents', 'Concentration', 'Compound', 'Protein', 'Concentration Units', default = 'Type'
        :type labelby: str
        :return: Plotted data for the stipulated wells of the well plate  
        :rtype: figure
        """
        # check labelby and colorby
        if colorby not in self.plate_map.columns:
            raise pm.HeaderError("colorby parameter not in plate map")
        if labelby not in self.plate_map.columns:
            raise pm.HeaderError("labelby parameter not in plate map")

        # check to_plot
        if type(to_plot) == str:
            # plot individual well:
            try:
                label = self.plate_map.loc[to_plot][labelby]
                
                fig, ax = plt.subplots()
                ax.plot(self.processed_data['ratio']['time'].loc[to_plot], self.processed_data['ratio']['data'].loc[to_plot], lw = 3, color = 'black', label = "{} {}".format(to_plot, label))

            # add label for each well
                ax.legend(loc = 'best', frameon = True, fancybox = True)
                ax.set_title("{} {}".format(to_plot, pm.labelwell(self.plate_map, labelby, 0)))
                ax.set_facecolor('0.95')
                ax.set_xlabel("time / s")
                ax.set_ylabel("$\mathrm{\Delta Ca^{2+} \ _i}$ (Ratio Units F340/F380)")
                ax.set_title('Flex data versus time for {}'.format(to_plot), y = 1.05, size = 20)
                plt.show()

            except:
                print("Plot error, does {} contain data?".format(to_plot))

        else:
            # plot multiple wells
            fig, axs = plt.subplots(len(to_plot), 1, figsize = (2*len(to_plot), 3*len(to_plot)), constrained_layout = True, sharey = share_y)

            for i in range(len(to_plot)):
                try:
                    label =  label = self.plate_map.loc[to_plot[i]][labelby] 
                    axs[i].plot(self.processed_data['ratio']['time'].loc[to_plot[i]], self.processed_data['ratio']['data'].loc[to_plot[i]], 
                                lw = 3, color = pm.wellcolour2(self.plate_map, colorby, cmap, i, to_plot), 
                               label = "{} {}".format(to_plot[i], label))

                # add label for each well
                    axs[i].legend(loc = 'best', frameon = True, fancybox = True)
                    axs[i].set_title("{} {}".format(to_plot[i], label))
                    axs[i].set_facecolor('0.95')
                    axs[i].set_xlabel("time / s")
                    axs[i].set_ylabel("$\mathrm{\Delta Ca^{2+} \ _i}$ (Ratio Units F340/F380)")
                except:
                    print("Plot error, does {} contain data?".format(to_plot[i]))

            # post try/except mods 
            fig.suptitle('Flex data versus time for the wells {}'.format(', '.join(to_plot)), y = 1.05, size = '20')
            
            plt.show()
    
    def invalidate_wells(self, wells):
        """Invalidates specified wells and updates plate_map
        
        :param wells: Wells to invalidate
        :type wells: list of strings, e.g. ("A1", "A2", "A3")
        """
        
        self.plate_map = pm.invalidate_wells(self.plate_map, wells = wells, valid = False)
        print("{} invalidated".format(wells))
        
    def invalidate_rows(self, rows):
        """Invalidates specified rows and updates plate_map
        
        :param wells: Rows to invalidate
        :type wells: list of strings, e.g. ("A", "B", "C")
        """
        platemap = pm.invalidate_rows(self.plate_map, rows, valid = False)
        self.plate_map = platemap
        print("Row {} invalidated".format(rows))
    
    def invalidate_cols(self, cols):
        """Invalidates specified wells and updates plate_map
        
        :param wells: Wells to invalidate
        :type wells: list of ints, e.g. (1, 2, 3)
        """
        platemap = pm.invalidate_cols(self.plate_map, cols, valid = False)
        self.plate_map = platemap
        print("Columns {} invalidated".format(cols))
        
    def baseline_correct(self):
        """Baseline corrects 'ratio' data using the pre-injection time points."""
        try:
            time_cut = self.inject - 5
            data_source = self.processed_data['ratio']
            # convert to numpy arrays
            time = data_source['time'].to_numpy()
            data = data_source['data'].to_numpy()
            # create mask from mean time values
            time_filter = np.nanmean(time,axis=0)<time_cut
            # # average over these times
            baseline = np.mean(data[:,time_filter],axis=1)
            # add dimension to enable broadcasting
            baseline = np.expand_dims(baseline, axis=1)
            # rewrite values back to dataframes
            self.processed_data['baseline_corrected'] = {}
            data_source = self.processed_data['baseline_corrected']['data'] = pd.DataFrame(data-baseline, index = data_source['data'].index)
            data_source = self.processed_data['baseline_corrected']['time'] = data_source = self.processed_data['ratio']['time']

            print("Baseline corrected! See self.processed_data['baseline_corrected']")
        except:
            print("baseline correction failed.")
        
    def get_gradients(self, data_type):
        """Returns the mean gradient over every 10 time points post injection.
        
        :param data_type: Data series to calculate plateau
        :type data_type: str
        :return: Dict containing a tuple corresponding to the start and end index and the corresponding mean gradient
        :rtype: dict[tuple, float]
        """        
        # filter for valid wells
        valid_filter = self.plate_map.Valid == True

        # add opposite time filter to extract data after injection
        time_cut = self.inject + 5
        data_source = self.processed_data[data_type]

        # convert to numpy arrays
        time = data_source['time'][valid_filter].to_numpy()
        data = data_source['data'][valid_filter].to_numpy()
        # create mask from mean time values
        post_inject_filter = np.nanmean(time,axis=0) > time_cut

        # get absolute gradient for each well along series
        gradient = abs(np.gradient(data[:, post_inject_filter], axis = 1))

        gradient_dict = {}

        index = np.array(list(data_source['data'].columns))[post_inject_filter]

        # mean gradient every ten measurements
        for i in range(gradient.shape[1]-9):

            # average of average gradients for every ten measurements post injection
            mean_gradient = np.nanmean(np.mean(gradient[:, i:(i+10)], axis=1), axis = 0)
            gradient_dict[(index[i]), (index[i]+10)] = mean_gradient
            
        return gradient_dict
    
    def get_window(self, data_type):
        """Updates self.window with the lowest mean gradient for each ten time point window post injection.
        
        :param data_type: Data series to calculate plateau
        :type data_type: str
        """
        gradient_dict = self.get_gradients(data_type)
        
        # get minimum gradient index window
        min_gradient = (min(gradient_dict, key = gradient_dict.get))

        self.window = min_gradient
        
    def def_window(self, time, data_type):
        """Manually set the plateau window.
        
        :param time: Time point at start of window
        :type time: int
        :param data_type: Data to set window on, either 'ratio' or 'baseline_corrected'
        :type data_type: str
        :return: Tuple of start and end index of plateau window
        :rtype: (int, int)
        """
        valid_filter = self.plate_map.Valid == True
        data_source = self.processed_data[data_type]
        time_df = data_source['time'][valid_filter]
        
        # create mask from mean time values
        window_filter = np.nanmean(time_df,axis=0) >= time
        index = np.array(list(data_source['data'].columns))[window_filter]
        self.window =  (index[0], index[10])
        
    def plot_conditions(self, data_type, activator = " ", show_window = False, dpi = 120, title = " ", error = False, control = ['control'], cmap = "winter_r", window_color = 'hotpink', proteins = [], compounds = [], marker = 'o', unique_markers = False, marker_list = ["o", "^", "s", "D", "p", "*", "v"], show_control = True):
        """Plots each mean condition versus time.
        
        :param data_type: Data to be plotted, either 'ratio' or 'baseline_corrected'
        :type data_type: str
        :param activator: Activator injected into assay, default = ""
        :type activator: str
        :param show_window: If 'True', shows the window from which the plateau for each condition is calculated, default = False 
        :type show_window: bool
        :param dpi: Size of figure, default = 120
        :type dpi: int
        :param title: Title of plot ADD LIST OF TITLES?
        :type title: str
        :param error: If True, plots error bars for each mean condition, default = False
        :type error: bool
        :param control: List of control conditions, default = 'control'
        :type control: bool
        :param cmap: Colormap to use as the source of plot colors
        :type cmap: str
        :param window_color: Color of the plateau window, default = 'hotpink'
        :type window_color: str
        :param marker: Marker type, default = '-o'
        :type marker: str
        :param unique_markers: If True, plots each condition as a black line with a unique marker, default = False
        :type unique_markers: bool
        :param marker_list: List of marker symbols to use when unique_markers = True, default = ["o", "^", "s", "D", "p", "*", "v"]
        :type marker_list: [str]
        :param show_control: If True, plots each control condition for each protein, default = True
        :return: Figure displaying each mean condition versus time
        :rtype: fig
        """

        platemap = self.plate_map
        grouplist = self.grouplist
        groupdct = {}
        
        # check control
        # check controls are correct
        for c in control:
            if self.plate_map['Type'].isin([c]).any() == False:
                raise ControlError("{} not in plate map".format(c))
        
        for key, val in self.processed_data[data_type].items():
            data_length = val.shape[1]
            mapped = platemap.fillna('none').join(val)
            group = mapped[mapped.Valid == True].groupby(grouplist)[val.columns]
            # update dictionary
            groupdct[key] = group

        # get data, time and error values for each condition
        data = groupdct['data'].mean().reset_index()
        time = groupdct['time'].mean().reset_index()
        yerr = groupdct['data'].sem().reset_index()

        # get names of proteins and compounds, excluding control
        if proteins == []:
            proteins = data[data['Type'].str.contains('control') == False]['Protein'].unique()
            
        # iterate through proteins list
        for pkey, pval in enumerate(proteins):
            # get number of compounds for each protein
            if compounds == []:
                compounds = data[(data['Type'].str.contains('control') == False) & (data['Protein'] == pval)]['Compound'].unique()
                
            # iterate through compounds for each protein
            for ckey, cval in enumerate(compounds):                
                # try each different combo
#                 try:
                # extract data for each protein and compound, excluding control. 

                data_temp = data[data['Type'].isin(control) == False]
                data_temp = data_temp[(data_temp['Protein'] == pval) & (data_temp['Compound'] == cval)]

                time_temp = time[time['Type'].isin(control) == False]
                time_temp = time_temp[(time_temp['Protein'] == pval) & (time_temp['Compound'] == cval)]

                yerr_temp = yerr[yerr['Type'].isin(control) == False]
                yerr_temp = yerr_temp[(yerr_temp['Protein'] == pval) & (yerr_temp['Compound'] == cval)]

                templist = [x for x in grouplist if x != 'Concentration'] # get columns to remove

                # extract just the data with conc as the index
                index = data_temp.iloc[:, :-data_length]
                data_temp = data_temp.set_index('Concentration').iloc[:, -data_length:]
                time_temp = time_temp.set_index('Concentration').iloc[:, -data_length:]
                yerr_temp = yerr_temp.set_index('Concentration').iloc[:, -data_length:]

                ########## PLOT ##########
                if (pval != 'none') & (cval != 'none'):
                    fig, ax = plt.subplots(dpi = 120)

                    # check units!
                    if len(index['Concentration Units'].unique()) > 1 :
                        raise UnitsError
                    # if units are fine, get units!
                    unique_units = index['Concentration Units'].unique()[0]

                    ########## control ##########
                    ctrl_max_vals = []
                    if show_control == True:
                        for c in control:

                            control_data = data[data['Type'].str.contains(c) == True]
                            control_data = control_data[(control_data['Protein'] == pval)].iloc[:, -data_length:]

                            control_time = time[time['Type'].str.contains(c) == True]
                            control_time = control_time[(control_time['Protein'] == pval)].iloc[:, -data_length:]
                            ctrl_max_vals.append(control_data.max().max())
                            
                            # convert u to mu
                            if unique_units[0] == 'u':
                                ctrl_label = "0 " + "$\mathrm{\mu }$" + unique_units[1:] + " ({})".format(c)
                            else:
                                ctrl_label = "0 {} ({})".format(unique_units, c)

                            ax.plot(control_time.iloc[0], control_data.iloc[0], marker, linestyle = '-', color = 'black', 
                                    mfc = get_color(control, 'binary', c), lw = 1, zorder = 2, label = ctrl_label)

                    ########## control end ##########

                    markers = itertools.cycle(marker_list) # define unique markers for each line (unique_markers = True)

                    # plot series, iterating down rows
                    for i in range(len(time_temp)):
                        # get legend labels
                        if unique_units[0] == 'u':
                            legend_label = "{} ".format(data_temp.index[i]) + "$\mathrm{\mu }$" + unique_units[1:]
                        else:
                            legend_label = "{} {}".format(data_temp.index[i], unique_units)

                        # plot each condition
                        if error == True:
                            ax.errorbar(x = time_temp.iloc[i], y = data_temp.iloc[i], yerr = yerr_temp.iloc[i],
                                       capsize = 3, zorder=1, color = plot_color(data_temp, cmap, i),
                                       label = legend_label)
                        else: 
                            if unique_markers == False:
                                ax.plot(time_temp.iloc[i], data_temp.iloc[i], marker = marker, linestyle = '-', zorder=1, label = legend_label, ms = 5, color = plot_color(data_temp, cmap, i), lw = 1)
                            else:
                                ax.plot(time_temp.iloc[i], data_temp.iloc[i], marker = next(markers), linestyle = '-', zorder=1, label = legend_label, ms = 4, color = 'black', lw = 1)

                        # add legend    
                        ax.legend(loc = "upper left", bbox_to_anchor = (1.0, 1.0), frameon = False, title = "{}".format(cval))

                    # white background makes the exported figure look a lot nicer
                    fig.patch.set_facecolor('white')

                    # spines
                    ax.spines['right'].set_visible(False)
                    ax.spines['top'].set_visible(False)

                    # add line representing the activator
                    times = self.processed_data[data_type]['time'].mean() # get times
                    time_filter = times > (self.inject - 5) # mean time series that contains activator

                    # get start and end points
                    injection_start = times[time_filter].iloc[0]
                    injection_end = times[time_filter].iloc[-1]

                    # add line indicating presence of activator
                    if len(ctrl_max_vals) > 0: # make sure line is above plotted data!! 
                        if max(ctrl_max_vals) > data_temp.max().max():
                            ymax = control_data.max().max() + control_data.max().max()*0.1
                    else:
                        ymax = data_temp.max().max() + data_temp.max().max()*0.1 # add a bit extra to prevent clash

                    ax.plot([injection_start, injection_end], [ymax, ymax], c = 'black')

                    # activator title
                    ax.text((injection_start+injection_end)/2, (ymax+ymax*0.05), activator, ha = 'center')

                    # assay title
                    ax.set_title(title, x = 0, fontweight = '550')

                    # axes labels
                    ax.set_xlabel("time (s)")
                    ax.set_ylabel("$\mathrm{\Delta Ca^{2+} \ _i}$ (Ratio Units F340/F380)")

                    # display plateau window
                    if show_window == True:
                        # x min and x max for axvspan 
                        xmin = time_temp.loc[:, self.window[0]].mean()
                        xmax = time_temp.loc[:, self.window[1]].mean()
                        ax.axvspan(xmin, xmax, facecolor = window_color, alpha = 0.5)

                    ########## PLOT END ##########

                plt.show()

                    # catch errors and notify user
#                 except UnitsError:
#                     print("{}, {} failed".format(pval, cval))
#                     print("too many units: {}".format(index['Concentration Units'].unique()))
#                 except ControlError:
#                     for c in control:
#                         if self.plate_map['Type'].isin([c]).any() == False:
#                             print("{} not in plate map".format(c))
#                 except:
#                     print("{}, {} failed".format(pval, cval))

    def amplitude(self, data_type):
        """Calculates response amplitude for each condition, updates processed_data dictionary with 'plateau' and plate_map with amplitude column. 
        
        :param data_type: Data to use to calculate amplitudes, either 'ratio' or 'baseline_corrected'
        :type data_type: str

        """
        try:
            # get ampltitude for every condition
            amp = (self.processed_data[data_type]['data'].iloc[:, self.window[0]:self.window[1]]).to_numpy()
            # get mean amplitude for every condition
            amp_mean = np.mean(amp, axis = 1)
            # update processed_data with response amplitude
            self.processed_data['plateau'] = {}
            self.processed_data['plateau']['data'] = pd.DataFrame(amp_mean, index = self.processed_data['ratio']['data'].index, columns = ['Amplitude'])

            print("Amplitudes calculated from {}. See self.processed_data['plateau']['data']".format(data_type))
        except:
            print("Ampltitude calculation failed.")
        
    def mean_amplitude(self, use_normalised = False):
        """Returns mean amplitudes and error for each condition.
        
        The user must run the normalise method before attempting to get the mean amplitudes of the normalised amplitudes.
        
        :param use_normalised: If True, uses normalised amplitudes, default = False
        :type use_normalised: bool
        :return: Mean amplitudes and error for each condition
        :rtype: Pandas DataFrame
        """
        mapped = self.plate_map.fillna(-1).join(self.processed_data['plateau']['data'])
        if use_normalised == True:
            mapped = self.plate_map.fillna(-1).join(self.processed_data['plateau']['data_normed'])
        # group by grouplist and take mean amplitude for each condition
        # filter for valid wells
        group = mapped[mapped.Valid == True] 
        # drop columns which can cause errors w/ groupby operations
        group.drop(['Valid', 'Column'], axis = 1, inplace = True)
        mean_response = group.groupby(self.grouplist).mean().reset_index()
        
        if use_normalised == False:
            mean_response['Amplitude Error'] = group.groupby(self.grouplist).sem().reset_index().loc[:, 'Amplitude']
        else:
            mean_response['amps_normed_error'] = group.groupby(self.grouplist).sem().reset_index().loc[:, 'amps_normed']
        # drop empty rows
        mean_response.drop(mean_response[mean_response['Type'] == 'empty'].index)

        # update data dict
        self.processed_data['mean_amplitudes'] = mean_response
        
        return mean_response
    
    def normalise(self):
        """Normalises amplitudes to mean control amplitude."""
        mean_amps = self.mean_amplitude(use_normalised = False) # get mean control amplitude
        amps = self.processed_data['plateau']['data']
        control_amp = float(mean_amps[mean_amps['Type'] == 'control']['Amplitude'])
        self.processed_data['plateau']['data_normed'] = "test"
        self.processed_data['plateau']['data_normed'] = ((amps * 100) / control_amp).rename(columns = {'Amplitude':'amps_normed'})

        return self.processed_data['plateau']['data_normed']
    
    @staticmethod
    def _gen_curve_data(mean_amps, plot_func, use_normalised, n, proteins, compounds, **kwargs):
        """Generates data required for a fitted plot of IC50's or EC50's."""
        # check proteins and compounds 
            
        curve_data = {}
        # filter amps 
        amps = mean_amps[mean_amps.Type == 'compound']
        
        # get names of proteins
        if proteins == []:
            prots = list(amps['Protein'].unique())
        else:
            prots = proteins
        # separate proteins 
        for pkey, pval in enumerate(prots):
            try:
                # check protein
                if mean_amps['Protein'].str.contains(pval, regex = False).any() == False:
                    raise ProteinNameError()
                # get compounds for each proteins
                if compounds == []:
                    comps = amps[amps['Protein'] == pval]['Compound'].unique()
                # get number of compounds for each protein
                for ckey, cval in enumerate(comps):
                    # check compound
                    if mean_amps['Compound'].str.contains(cval, regex = False).any() == False:
                        raise CompoundNameError()
                    # filter dataframe for each compound in each protein
                    temp = amps[(amps['Protein'] == pval) & (amps['Compound'] == cval)]
                    # check there is only 1 conc unit
                    if len(temp['Concentration Units'].unique()) > 1:
                        raise ValueError["One unit per condition please!"]
                        # check there is an adequate number of concs
                    if len(temp['Concentration']) < n:
                        raise ValueError("Not enough concs! You've only got {} for {}, compound {}. You really need at least {} to do a fit.".format(len(temp['Concentration']), pval, cval, n))

                    # get x, y and error values, c50 units, compound and protein names to use for plot
                    x = temp['Concentration'].to_numpy()
                    y = temp.iloc[:, -2].to_numpy()
                    yerr = temp.iloc[:, -1].to_numpy()
                    c50units = temp['Concentration Units'].unique()[0]

                    # get popt values for logistic regression
                    popt, pcov = curve_fit(func_dict[plot_func], x, y, **kwargs) 

                    # update curve_data dict
                    curve_data["{}_{}".format(pval, cval)] = {'x':x, 'y':y, 'yerr':yerr, 'c50units':c50units, 'compound':cval, 
                                                              'protein':pval, 'popt':popt}
            except CompoundNameError:
                print("Compound not found. Available compounds are: {}".format(amps['Compound'].unique()))

            except ProteinNameError:
                print("Protein not found. Available compounds are: {}".format(amps['Protein'].unique()))
                
        return curve_data  
        
    def _get_curve_data(self, plot_func, use_normalised, n, proteins, compounds, **kwargs): 
        """Updates and returns self.plot_data with data required for a fitted plot of IC50's or EC50's."""
        mean_amps = self.mean_amplitude(use_normalised)
        
        curve_data = self._gen_curve_data(mean_amps, plot_func, use_normalised, n, proteins, compounds, **kwargs)
        
        self.plot_data = curve_data
        
        return self.plot_data
        
    @staticmethod
    def _plot_curve(curve_data, plot_func, use_normalised, n, proteins, compounds, error_bar, cmap, combine, activator, title, dpi, show_top_bot, conc_units):
        
        legend_label = {"ic50":"IC$_{{50}}$", "ec50":"EC$_{{50}}$"}
        if combine == True:
            fig, ax = plt.subplots(dpi = dpi)
        
        for key, val in curve_data.items():
            
            
            ########## TRY EXCEPT ##########
            try:
                if combine == False:
                    fig, ax = plt.subplots(dpi = dpi)
                # unpack curve_data:
                x = val['x']
                y = val['y']
                yerr = val['yerr']
                c50units = val['c50units']
                popt = val['popt']
                protein = val['protein']
                compound = val['compound']
                tb_str = ""
                if show_top_bot == True:
                    tb_str = "\nTop = {:.2f}\nBottom = {:.2f}".format(popt[0], popt[1])

                # get x and y fits
                fit_x = np.logspace(np.log10(x.min())-0.5, np.log10(x.max())+0.5, 300)
                fit = func_dict[plot_func](fit_x, *popt)
                ############################# types of plot: combine True OR False ####################
                # TRUE
                if combine == True:
                    # get label for legend
                    
                    # convert u to mu 
                    if c50units[0] == 'u':
                        label = r"$\bf{}\ {}$ ".format(compound, "") + "\n{} = {:.2f}".format(legend_label[plot_func], popt[2]) + "$\mathrm{\mu }$" 
                        + "{}".format(c50units[1:]) + "\nHill Slope = {:.2f}".format(popt[3])+tb_str
                        
                    else:
                        label = r"$\bf{}\ {}$".format(compound, "") +"\n{} = {:.2f} {}\nHill Slope = {:.2f}".format(legend_label[plot_func], popt[2], c50units, popt[3])+tb_str

                    # plot fit
                    ax.plot(fit_x, fit, lw = 1.2, label = label, color = get_color(list(curve_data.keys()), cmap, "{}_{}".format(protein, compound)))

                    # plot errors
                    if error_bar == True:
                        ax.errorbar(x, y, yerr, fmt='ko',capsize=3,ms=3, color = get_color(list(curve_data.keys()), cmap, "{}_{}".format(protein, compound)))
                # FALSE        
                if combine == False:
                    # get label for legend
                    
                    # convert u to mu 
                    if c50units[0] == 'u':
                        label = "\n{} = {:.2f} ".format(legend_label[plot_func], popt[2]) + "$\mathrm{\mu }$" + "{}".format(c50units[1:]) + "\nHill Slope = {:.2f}".format(popt[3])+tb_str
                    else:
                        label = "{} = {:.2f} {}\nHill Slope = {:.2f} ".format(legend_label[plot_func], popt[2], c50units, popt[3])+tb_str

                    # plot fit
                    ax.plot(fit_x, fit, lw = 1.2, label = label, color = 'black')

                    # plot errors
                    if error_bar == True:
                        ax.errorbar(x, y, yerr, fmt='ko',capsize=3,ms=3, color = 'black')

                    # add legend
                    ax.legend(loc = 'best', frameon = False,framealpha=0.7, handlelength=0, handletextpad=0)

                ############# end of combine differences   ############# 
                ############# in loop modifications for both types of plot (combine = True OR False) #############   
                plt.xscale('log')    

                # spines
                ax.spines['right'].set_visible(False)
                ax.spines['top'].set_visible(False)

                # labels
                ax.set_xlabel("[{}] / {}".format(compound, conc_units))
                ax.set_title(title, x = 0, fontweight = '550')

                if use_normalised == False:
                    ax.set_ylabel("$\mathrm{\Delta Ca^{2+} \ _i}$ (Ratio Units F340/F380)")
                else:
                    if activator == "":
                        ax.set_ylabel("% of activation")
                    else:
                        ax.set_ylabel("% of activation by {}".format(activator))

                # title
                ax.set_title(title, x = 0, fontweight = '550')

                plt.minorticks_off()
            except:
                print("Did not plot {}, {}".format(protein, compound))
        ###################################### end of plotting loop ##########################        
        # post loop mods
        
        
        # using plt legend allows use of loc = 'best' to prevent annotation clashing with line
        if combine == True:
            leg = ax.legend(loc = 'center left', frameon = False, bbox_to_anchor = [1.0, 0.5])
            ax.set_xlabel("Concentration / {}".format(conc_units))
        
        # set background
        fig.patch.set_facecolor('white')
        
        plt.show()
        
        
    def plot_curve(self, plot_func, use_normalised = False, n = 5, proteins = [], compounds = [], error_bar = True, cmap = "Dark2", combine = False, activator = " ", title = " ", dpi = 120, show_top_bot = False, conc_units = 'M', **kwargs):
        """Plots fitted curve using logistic regression with errors and IC50/EC50 values.
        
        :param plot_func: Plot function to use, either ic50 or ec50
        :type plot_func: str
        :param use_normalised: If True, uses normalised amplitudes, default = False
        :type use_normalised: bool
        :param n: Number of concentrations required for plot
        :type n: int
        :param proteins: Proteins to plot, defaults to all
        :type proteins: list
        :param compounds: Compounds to plot, defaults to all
        :type compounds: list
        :param error_bar: If True, reveals error bars, default = True
        :type error_bar = bool
        :param cmap: Color map that sets each line colour in a combined plot, default = "Dark2"
        :type cmap: bool
        :param activator: Activator injected into assay, default = " "
        :type activator: str
        :param title: Choose between automatic title or insert string to use, default = " "
        :type title: str
        :param dpi: Size of figure
        :type dpi: int
        :param show_top_bot: 'True' shows the top and bottom curve fitting values in the legend
        :type show_top_bot: bool
        :param conc_units: Units displayed on x axis of graph
        :type conc_units: str
        :param **kwargs: Additional curve fitting arguments
        """         
        curve_data =  self._get_curve_data(plot_func, use_normalised, n, proteins, compounds, **kwargs)
        
        # update object with curve data to access when exporting
        self.curve_data = curve_data
        for key in self.curve_data.keys():
            self.curve_data[key]['plot_func'] = plot_func # curve data maximal conc plot function

    # do plots
        self._plot_curve(curve_data, plot_func, use_normalised, n, proteins, compounds, error_bar, cmap, combine, activator, title, dpi, show_top_bot, conc_units)
        
        
        
    def export_data(self, title = ""):
        """Exports raw and processed data to an excel file. 
        
        The excel file will be named after the 'self.title' parameter unless otherwise specified.
        
        :param title: Title of the excel file, default = self.title + '_data_export.xlsx'
        :type title: str
        :return: Excel document
        :rtype: .xlsx
        """
        if title == "":
            title = self.title + '_data_export.xlsx'
        if title[-5:] != '.xlsx':
            title = title + '.xlsx'
            
        dfs = self.processed_data
        
        # get curve data
        curve_list = {}
        
        if hasattr(self, 'curve_data'):
            for key, val in self.curve_data.items(): # get curve data for each protein/compound combo
                curve = copy.deepcopy(val) # get deep copy - prevents original dict from being edited
                curve['popt_keys'] = ['top', 'bottom', curve['plot_func'], 'hill'] # add popt index
                for i in ['compound', 'protein', 'c50units', 'plot_func']: # delete irrelevent keys
                    curve.pop(i)
                curve_df = pd.DataFrame.from_dict(curve, orient = 'index')
                
                curve_list[key] = curve_df
    
    
        # create separate sheet for each data set
        with pd.ExcelWriter(title) as writer:
            dfs['ratio']['time'].to_excel(writer, sheet_name = 'time')
            dfs['ratio']['data'].to_excel(writer, sheet_name = 'ratio')
            dfs['baseline_corrected']['data'].to_excel(writer, sheet_name = 'baseline_corrected')
            if 'data' in dfs['plateau'].keys():
                dfs['plateau']['data'].to_excel(writer, sheet_name = 'plateau')
            if 'data_normed' in dfs['plateau'].keys():
                dfs['plateau']['data'].to_excel(writer, sheet_name = 'normalised_plateau')
            if 'mean_amplitudes' in dfs.keys():
                dfs['mean_amplitudes'].to_excel(writer, sheet_name = 'mean_amplitudes')
            self.plate_map.to_excel(writer, sheet_name = 'plate_map')
            
            # export curve data for each protein/compund combination
            for key, val in curve_list.items():
                val.to_excel(writer, sheet_name = key + '_curve_data')
                
        print("Data exported to excel as {} Check your folder.".format(title))

Functions

def get_color(condition_list, cmap, name)

Returns a color for each condition plotted.

Similar to plot_color() but used for lists instead of dataframes - see plot_curve(combine = True).

Expand source code
def get_color(condition_list, cmap, name):
    """Returns a color for each condition plotted. 
    
    Similar to plot_color() but used for lists instead of dataframes - see plot_curve(combine = True)."""
    cmap = plt.get_cmap(cmap)
    colors = cmap(np.linspace(0, 1, len(condition_list)))
    colordict = dict(zip(condition_list, colors))
    color = colordict.get(name)
    return color
def plot_color(dataframe, cmap, iterrange)

Returns a color for each index value (accessed by iterrange) of a dataframe.

Expand source code
def plot_color(dataframe, cmap, iterrange):
    """Returns a color for each index value (accessed by iterrange) of a dataframe."""
    types = list(dataframe.index)
    cmap = plt.get_cmap(cmap)
    colors = cmap(np.linspace(0, 1, len(types)))
    colordict = dict(zip(types, colors))
    color = colordict.get(dataframe.index[iterrange])
    return color
def read_in(raw_data)

Returns a dataframe of the old flex data.

Expand source code
def read_in(raw_data):
    """Returns a dataframe of the old flex data."""
    df = pd.read_csv(raw_data, delimiter='\t', skiprows = 2, skipfooter=3, engine = 'python', encoding = 'mbcs') # update to check headers if there is no title?
    return df
def read_in_new(raw_data, skiprows, skipfooter)

Returns a dataframe of the new flex data.

Expand source code
def read_in_new(raw_data, skiprows, skipfooter):
    """Returns a dataframe of the new flex data."""
    df = pd.read_csv(raw_data, delimiter='\t', skiprows = skiprows, skipfooter=skipfooter, engine = 'python', encoding = "utf-16", skip_blank_lines=True) 
    return df

Classes

class CaFlexPlate (raw_data, plate_map_file, inject, map_type='short', data_type='old', valid=True, size=96, title='', skiprows=2, skipfooter=3)

Class used for the analysis of individual Calcium Flex well plates.

:param raw_data: Raw, unprocessed data from experiment :type raw_data: .txt file :param plate_map_file: Filled template plate map that contains the information for each well of the well plate :type plate_map_file: .csv :param inject: Activator injection time point :type inject: float or int :param map_type: 'short' or 'long' - Denotes the type of plate map file used, default = 'short' :type map_type: str :param size: Size of well plate - 6, 12, 24, 48, 96 or 384. plate_map_file MUST have the appropriate dimensions, default = 96 :type size: int :param data_type: 'new' or 'old' - denotes type of flex data, default = 'old' :type data_type: str :param valid: Validates every well - 'True' sets every well as valid, 'False' wells will not be used for analysis, default = True :type valid: bool :param processed_data: Dictionary containing separate dataframes of the time and flex data for every well :type processed_data: dictionary of pandas dataframes :param plate_map: plate_map_file converted as a dataframe :type plate_map: pandas dataframe :param title: Title of plate or assay :type title: str :param skiprows: Number of rows to skip when reading in new data :type skiprows: int :param skipfooter: Number of rows from footer to skip when reading in new data :type skipfooter: int

Expand source code
class CaFlexPlate:
    """Class used for the analysis of individual Calcium Flex well plates.
    
    :param raw_data: Raw, unprocessed data from experiment
    :type raw_data: .txt file
    :param plate_map_file: Filled template plate map that contains the information for each well of the well plate
    :type plate_map_file: .csv
    :param inject: Activator injection time point
    :type inject: float or int
    :param map_type: 'short' or 'long' - Denotes the type of plate map file used, default = 'short'
    :type map_type: str
    :param size: Size of well plate - 6, 12, 24, 48, 96 or 384. plate_map_file MUST have the appropriate dimensions, default = 96
    :type size: int
    :param data_type: 'new' or 'old' - denotes type of flex data, default = 'old'
    :type data_type: str
    :param valid: Validates every well - 'True' sets every well as valid, 'False' wells will not be used for analysis, default = True
    :type valid: bool
    :param processed_data: Dictionary containing separate dataframes of the time and flex data for every well
    :type processed_data: dictionary of pandas dataframes
    :param plate_map: plate_map_file converted as a dataframe
    :type plate_map: pandas dataframe
    :param title: Title of plate or assay 
    :type title: str
    :param skiprows: Number of rows to skip when reading in new data
    :type skiprows: int
    :param skipfooter: Number of rows from footer to skip when reading in new data
    :type skipfooter: int
    """
    def __init__(self, raw_data, plate_map_file, inject, map_type = 'short', data_type = 'old', valid = True, size = 96, title = "", skiprows = 2, skipfooter = 3):
        
        self.raw_data = raw_data
        self.plate_map_file = plate_map_file
        self.inject = inject
        self.map_type = map_type
        self.size = size
        self.data_type = data_type
        self.valid = valid # False invalidates all wells of plate
        self.skiprows = skiprows
        self.skipfooter = skipfooter
        self.processed_data = {'ratio':self._data_processed()}
        self.plate_map = self._give_platemap()
        self.grouplist = ['Protein','Type', 'Compound','Concentration', 'Concentration Units']
        
        # invalidate all wells if valid = False
        if self.valid == False:
            self.plate_map['Valid'] = valid
        if self.valid == True:
            self.plate_map['Valid'] = valid # ?????!!!
            
        # optional title param
        self.title = title
        # add default title
        if self.title == "":
            self.title = self.raw_data[:-4] # change to experiment title in plate map?
            
        # check params
        if wells.get(self.size) == None:
            raise pm.PlateMapError("Invalid size. Try 6, 12, 24, 48, 96 or 384.")
            
        print("Uploaded!")
        
    def _give_platemap(self):
        """Returns platemap dataframe."""
        if self.map_type == 'short':
             platemap = pm.short_map(self.plate_map_file, size = self.size, valid = self.valid)
        elif self.map_type == 'long':
            platemap = pm.plate_map(self.plate_map_file, size = self.size, valid = self.valid)
        else:
            raise pm.PlateMapError("Invalid map type. Try 'short' or 'long'.")
        return platemap
        
    def _data_processed(self):
        """Returns a timemap and datamap as a tuple."""
        if self.data_type == 'old':
            try:
                df = read_in(self.raw_data)
                # create new dataframe containing all time values for each well
                dftime = df.filter(regex = 'T$', axis = 1)
               # edit header names (this will come in handy in a second)
                dftime.columns = dftime.columns.str.replace('T', "")
                # extract list of header names 
                wellslist = list(dftime.columns.values)
                # transpose x and y axes of dataframe - generate time 'rows'
                dftime = dftime.transpose()
                # create new dataframe containing data measurements for each cell
                dfdata = df[wellslist]
                # transpose x and y axes
                dfdata = dfdata.transpose()
                # return timemap and datamap as a tuple
                return {'time':dftime, 'data':dfdata}
            
            except:
                print("Read in error")

        if self.data_type == 'new':
            try:
            
                newdata = read_in_new(self.raw_data, self.skiprows, self.skipfooter)
                # split the dataframe into the two data series
                data1 = newdata.iloc[:int(newdata.shape[0]/2), :]
                data1 = data1.reset_index(drop=True)
                data2 = newdata.iloc[int(newdata.shape[0]/2):, :]
                data2 = data2.reset_index(drop=True)
                newdatadict = {"data1":data1, "data2":data2}

                # check length
                if len(data1) != len(data2):
                    raise DataError("Read in error")
                    
            # try alternative read in if lengths not equal
            except DataError: 
                self.skipfooter = self.skipfooter+1
                newdata = read_in_new(self.raw_data, self.skiprows, self.skipfooter)
                # split the dataframe into the two data series
                data1 = newdata.iloc[:int(newdata.shape[0]/2), :]
                data1 = data1.reset_index(drop=True)
                data2 = newdata.iloc[int(newdata.shape[0]/2):, :]
                data2 = data2.reset_index(drop=True)
                newdatadict = {"data1":data1, "data2":data2}
                
                if len(data1) != len(data2):
                    raise DataError("Read in error")
                else:
                    print("self.skiprows updated. This is data type requires skipfooter equal {}.".format(self.skipfooter))
            except DataReadInError:
                print("Check your data. Read in is faulty.")
                
            # initiate empty dictionary to house preprocessed data and time values
            preprocessed = {}
            # loop produces two dictionaries containing preprocessed flux data and their corresponding time values
            for key, value in newdatadict.items():
                # reset indexes
                value = value.reset_index(drop=True)
                dftime = value.filter(regex = 'T$', axis = 1)
                # edit header names (this will come in handy in a second)
                dftime.columns = dftime.columns.str.replace('T', "")
                # extract list of header names 
                wellslist = list(dftime.columns.values)
                # transpose x and y axes of dataframe - generate time 'rows'
                dftime = dftime.transpose()
                # create new dataframe containing data measurements for each cell
                dfdata = value[wellslist]
                # transpose x and y axes
                dfdata = dfdata.transpose()
                # return timemap and datamap as a tuple
                tempdict = {'time':dftime, 'data':dfdata}
                # append dictionary
                preprocessed[key] = tempdict

            # take means of the time in new dataframe
            mean_time = pd.concat((preprocessed["data1"]['time'], preprocessed["data2"]["time"]))
            mean_time = mean_time.groupby(mean_time.index).mean()
            mean_time = mean_time.reindex(wellslist)

            # take difference of data to get change in flux
            difference = preprocessed["data1"]['data'].divide(preprocessed["data2"]["data"])
            return {'time':mean_time, 'data':difference}

        elif self.data_type not in ('old', 'new'):
            raise DataError("Incorrect data type")
            
    def visualise_assay(self, share_y, export = False, title = "", cmap = 'Dark2_r',
             colorby = 'Type', labelby = 'Type', dpi = 200):
        """Returns color-coded and labelled plots of the data collected for each well of the well plate.
        
        :param share_y: 'True' sets y axis the same for all plots
        :type share_y: bool
        :param export: If 'True' a .png file of the figure is saved, default = False
        :type export: bool
        :param title: Sets the title of the figure, optional
        :type title: str
        :param cmap: Sets the colormap for the color-coding, default = 'Dark2_r'
        :type cmap: str
        :param colorby: Chooses the parameter to color code by, for example 'Type', 'Contents', 'Concentration', 'Compound', 'Protein', 'Concentration Units', default = 'Type'
        :type colorby: str
        :param labelby: Chooses the parameter to label code by, for example 'Type', 'Contents', 'Concentration', 'Compound', 'Protein', 'Concentration Units', default = 'Type'
        :type labelby: str
        :param dpi: Size of the figure, default = 200
        :type dpi: int
        :return: Figure of plotted data for each well of the well plate described in plate_map_file
        :rtype: figure
        """
        self.title = title
        
        # check labelby and colorby
        if colorby not in self.plate_map.columns:
            raise pm.HeaderError("colorby parameter not in plate map")
        if labelby not in self.plate_map.columns:
            raise pm.HeaderError("labelby parameter not in plate map")
        
        pm.visualise_all_series(x = self.processed_data['ratio']['time'], y = self.processed_data['ratio']['data'], 
                            share_y = share_y, platemap = self.plate_map, size = self.size, 
                            export = export, cmap = cmap,
                            colorby = colorby, labelby = labelby, 
                            dpi = dpi, title = self.title)
        
        plt.suptitle(self.title, y = 0.95)
        
    def see_plate(self, title = "", export = False, cmap = 'Paired',
             colorby = 'Type', labelby = 'Type', dpi = 150):
        """Returns a visual representation of the plate map.
    
        The label and colour for each well can be customised to be a variable, for example 'Compound', 'Protein', 'Concentration', 'Concentration Units', 'Contents' or 'Type'. The size of the plate map used to generate the figure can be either 6, 12, 24, 48, 96 or 384. 
        :param size: Size of platemap, 6, 12, 24, 48, 96 or 384, default = 96
        :type size: int    
        :param export: If 'True' a .png file of the figure is saved, default = False
        :type export: bool
        :param title: Sets the title of the figure, optional
        :type title: str
        :param cmap: Sets the colormap for the color-coding, default = 'Paired'
        :type cmap: str
        :param colorby: Chooses the parameter to color code by, for example 'Type', 'Contents', 'Concentration', 'Compound', 'Protein', 'Concentration Units', default = 'Type'
        :type colorby: str
        :param labelby: Chooses the parameter to label code by, for example 'Type', 'Contents', 'Concentration', 'Compound', 'Protein', 'Concentration Units', default = 'Type'
        :type labelby: str
        :param dpi: Size of the figure, default = 150
        :type dpi: int
        :return: Visual representation of the plate map.
        :rtype: figure
        """
        pm.visualise(self.plate_map, title = title, size = self.size, export = export, cmap = cmap,
             colorby = colorby, labelby = labelby, dpi = dpi)
    
       
    def see_wells(self, to_plot, share_y = True, colorby = 'Type', labelby = 'Type', cmap = 'Dark2_r'):
        """Returns plotted data from stipulated wells.

        :param size: Size of platemap, 6, 12, 24, 48, 96 or 384, default = 96
        :type size: int   
        :param to_plot: Wells to plot
        :type to_plot: string or list of strings (well ID's), e.g. "A1", "A2", "A3"
        :param share_y: 'True' sets y axis the same for all plots, default = 'True'
        :type share_y: bool
        :param cmap: Sets the colormap for the color-coding, optional
        :type cmap: str
        :param colorby: Chooses the parameter to color code by, for example 'Type', 'Contents', 'Concentration', 'Compound', 'Protein', 'Concentration Units', default = 'Type'
        :type colorby: str
        :param labelby: Chooses the parameter to label code by, for example 'Type', 'Contents', 'Concentration', 'Compound', 'Protein', 'Concentration Units', default = 'Type'
        :type labelby: str
        :return: Plotted data for the stipulated wells of the well plate  
        :rtype: figure
        """
        # check labelby and colorby
        if colorby not in self.plate_map.columns:
            raise pm.HeaderError("colorby parameter not in plate map")
        if labelby not in self.plate_map.columns:
            raise pm.HeaderError("labelby parameter not in plate map")

        # check to_plot
        if type(to_plot) == str:
            # plot individual well:
            try:
                label = self.plate_map.loc[to_plot][labelby]
                
                fig, ax = plt.subplots()
                ax.plot(self.processed_data['ratio']['time'].loc[to_plot], self.processed_data['ratio']['data'].loc[to_plot], lw = 3, color = 'black', label = "{} {}".format(to_plot, label))

            # add label for each well
                ax.legend(loc = 'best', frameon = True, fancybox = True)
                ax.set_title("{} {}".format(to_plot, pm.labelwell(self.plate_map, labelby, 0)))
                ax.set_facecolor('0.95')
                ax.set_xlabel("time / s")
                ax.set_ylabel("$\mathrm{\Delta Ca^{2+} \ _i}$ (Ratio Units F340/F380)")
                ax.set_title('Flex data versus time for {}'.format(to_plot), y = 1.05, size = 20)
                plt.show()

            except:
                print("Plot error, does {} contain data?".format(to_plot))

        else:
            # plot multiple wells
            fig, axs = plt.subplots(len(to_plot), 1, figsize = (2*len(to_plot), 3*len(to_plot)), constrained_layout = True, sharey = share_y)

            for i in range(len(to_plot)):
                try:
                    label =  label = self.plate_map.loc[to_plot[i]][labelby] 
                    axs[i].plot(self.processed_data['ratio']['time'].loc[to_plot[i]], self.processed_data['ratio']['data'].loc[to_plot[i]], 
                                lw = 3, color = pm.wellcolour2(self.plate_map, colorby, cmap, i, to_plot), 
                               label = "{} {}".format(to_plot[i], label))

                # add label for each well
                    axs[i].legend(loc = 'best', frameon = True, fancybox = True)
                    axs[i].set_title("{} {}".format(to_plot[i], label))
                    axs[i].set_facecolor('0.95')
                    axs[i].set_xlabel("time / s")
                    axs[i].set_ylabel("$\mathrm{\Delta Ca^{2+} \ _i}$ (Ratio Units F340/F380)")
                except:
                    print("Plot error, does {} contain data?".format(to_plot[i]))

            # post try/except mods 
            fig.suptitle('Flex data versus time for the wells {}'.format(', '.join(to_plot)), y = 1.05, size = '20')
            
            plt.show()
    
    def invalidate_wells(self, wells):
        """Invalidates specified wells and updates plate_map
        
        :param wells: Wells to invalidate
        :type wells: list of strings, e.g. ("A1", "A2", "A3")
        """
        
        self.plate_map = pm.invalidate_wells(self.plate_map, wells = wells, valid = False)
        print("{} invalidated".format(wells))
        
    def invalidate_rows(self, rows):
        """Invalidates specified rows and updates plate_map
        
        :param wells: Rows to invalidate
        :type wells: list of strings, e.g. ("A", "B", "C")
        """
        platemap = pm.invalidate_rows(self.plate_map, rows, valid = False)
        self.plate_map = platemap
        print("Row {} invalidated".format(rows))
    
    def invalidate_cols(self, cols):
        """Invalidates specified wells and updates plate_map
        
        :param wells: Wells to invalidate
        :type wells: list of ints, e.g. (1, 2, 3)
        """
        platemap = pm.invalidate_cols(self.plate_map, cols, valid = False)
        self.plate_map = platemap
        print("Columns {} invalidated".format(cols))
        
    def baseline_correct(self):
        """Baseline corrects 'ratio' data using the pre-injection time points."""
        try:
            time_cut = self.inject - 5
            data_source = self.processed_data['ratio']
            # convert to numpy arrays
            time = data_source['time'].to_numpy()
            data = data_source['data'].to_numpy()
            # create mask from mean time values
            time_filter = np.nanmean(time,axis=0)<time_cut
            # # average over these times
            baseline = np.mean(data[:,time_filter],axis=1)
            # add dimension to enable broadcasting
            baseline = np.expand_dims(baseline, axis=1)
            # rewrite values back to dataframes
            self.processed_data['baseline_corrected'] = {}
            data_source = self.processed_data['baseline_corrected']['data'] = pd.DataFrame(data-baseline, index = data_source['data'].index)
            data_source = self.processed_data['baseline_corrected']['time'] = data_source = self.processed_data['ratio']['time']

            print("Baseline corrected! See self.processed_data['baseline_corrected']")
        except:
            print("baseline correction failed.")
        
    def get_gradients(self, data_type):
        """Returns the mean gradient over every 10 time points post injection.
        
        :param data_type: Data series to calculate plateau
        :type data_type: str
        :return: Dict containing a tuple corresponding to the start and end index and the corresponding mean gradient
        :rtype: dict[tuple, float]
        """        
        # filter for valid wells
        valid_filter = self.plate_map.Valid == True

        # add opposite time filter to extract data after injection
        time_cut = self.inject + 5
        data_source = self.processed_data[data_type]

        # convert to numpy arrays
        time = data_source['time'][valid_filter].to_numpy()
        data = data_source['data'][valid_filter].to_numpy()
        # create mask from mean time values
        post_inject_filter = np.nanmean(time,axis=0) > time_cut

        # get absolute gradient for each well along series
        gradient = abs(np.gradient(data[:, post_inject_filter], axis = 1))

        gradient_dict = {}

        index = np.array(list(data_source['data'].columns))[post_inject_filter]

        # mean gradient every ten measurements
        for i in range(gradient.shape[1]-9):

            # average of average gradients for every ten measurements post injection
            mean_gradient = np.nanmean(np.mean(gradient[:, i:(i+10)], axis=1), axis = 0)
            gradient_dict[(index[i]), (index[i]+10)] = mean_gradient
            
        return gradient_dict
    
    def get_window(self, data_type):
        """Updates self.window with the lowest mean gradient for each ten time point window post injection.
        
        :param data_type: Data series to calculate plateau
        :type data_type: str
        """
        gradient_dict = self.get_gradients(data_type)
        
        # get minimum gradient index window
        min_gradient = (min(gradient_dict, key = gradient_dict.get))

        self.window = min_gradient
        
    def def_window(self, time, data_type):
        """Manually set the plateau window.
        
        :param time: Time point at start of window
        :type time: int
        :param data_type: Data to set window on, either 'ratio' or 'baseline_corrected'
        :type data_type: str
        :return: Tuple of start and end index of plateau window
        :rtype: (int, int)
        """
        valid_filter = self.plate_map.Valid == True
        data_source = self.processed_data[data_type]
        time_df = data_source['time'][valid_filter]
        
        # create mask from mean time values
        window_filter = np.nanmean(time_df,axis=0) >= time
        index = np.array(list(data_source['data'].columns))[window_filter]
        self.window =  (index[0], index[10])
        
    def plot_conditions(self, data_type, activator = " ", show_window = False, dpi = 120, title = " ", error = False, control = ['control'], cmap = "winter_r", window_color = 'hotpink', proteins = [], compounds = [], marker = 'o', unique_markers = False, marker_list = ["o", "^", "s", "D", "p", "*", "v"], show_control = True):
        """Plots each mean condition versus time.
        
        :param data_type: Data to be plotted, either 'ratio' or 'baseline_corrected'
        :type data_type: str
        :param activator: Activator injected into assay, default = ""
        :type activator: str
        :param show_window: If 'True', shows the window from which the plateau for each condition is calculated, default = False 
        :type show_window: bool
        :param dpi: Size of figure, default = 120
        :type dpi: int
        :param title: Title of plot ADD LIST OF TITLES?
        :type title: str
        :param error: If True, plots error bars for each mean condition, default = False
        :type error: bool
        :param control: List of control conditions, default = 'control'
        :type control: bool
        :param cmap: Colormap to use as the source of plot colors
        :type cmap: str
        :param window_color: Color of the plateau window, default = 'hotpink'
        :type window_color: str
        :param marker: Marker type, default = '-o'
        :type marker: str
        :param unique_markers: If True, plots each condition as a black line with a unique marker, default = False
        :type unique_markers: bool
        :param marker_list: List of marker symbols to use when unique_markers = True, default = ["o", "^", "s", "D", "p", "*", "v"]
        :type marker_list: [str]
        :param show_control: If True, plots each control condition for each protein, default = True
        :return: Figure displaying each mean condition versus time
        :rtype: fig
        """

        platemap = self.plate_map
        grouplist = self.grouplist
        groupdct = {}
        
        # check control
        # check controls are correct
        for c in control:
            if self.plate_map['Type'].isin([c]).any() == False:
                raise ControlError("{} not in plate map".format(c))
        
        for key, val in self.processed_data[data_type].items():
            data_length = val.shape[1]
            mapped = platemap.fillna('none').join(val)
            group = mapped[mapped.Valid == True].groupby(grouplist)[val.columns]
            # update dictionary
            groupdct[key] = group

        # get data, time and error values for each condition
        data = groupdct['data'].mean().reset_index()
        time = groupdct['time'].mean().reset_index()
        yerr = groupdct['data'].sem().reset_index()

        # get names of proteins and compounds, excluding control
        if proteins == []:
            proteins = data[data['Type'].str.contains('control') == False]['Protein'].unique()
            
        # iterate through proteins list
        for pkey, pval in enumerate(proteins):
            # get number of compounds for each protein
            if compounds == []:
                compounds = data[(data['Type'].str.contains('control') == False) & (data['Protein'] == pval)]['Compound'].unique()
                
            # iterate through compounds for each protein
            for ckey, cval in enumerate(compounds):                
                # try each different combo
#                 try:
                # extract data for each protein and compound, excluding control. 

                data_temp = data[data['Type'].isin(control) == False]
                data_temp = data_temp[(data_temp['Protein'] == pval) & (data_temp['Compound'] == cval)]

                time_temp = time[time['Type'].isin(control) == False]
                time_temp = time_temp[(time_temp['Protein'] == pval) & (time_temp['Compound'] == cval)]

                yerr_temp = yerr[yerr['Type'].isin(control) == False]
                yerr_temp = yerr_temp[(yerr_temp['Protein'] == pval) & (yerr_temp['Compound'] == cval)]

                templist = [x for x in grouplist if x != 'Concentration'] # get columns to remove

                # extract just the data with conc as the index
                index = data_temp.iloc[:, :-data_length]
                data_temp = data_temp.set_index('Concentration').iloc[:, -data_length:]
                time_temp = time_temp.set_index('Concentration').iloc[:, -data_length:]
                yerr_temp = yerr_temp.set_index('Concentration').iloc[:, -data_length:]

                ########## PLOT ##########
                if (pval != 'none') & (cval != 'none'):
                    fig, ax = plt.subplots(dpi = 120)

                    # check units!
                    if len(index['Concentration Units'].unique()) > 1 :
                        raise UnitsError
                    # if units are fine, get units!
                    unique_units = index['Concentration Units'].unique()[0]

                    ########## control ##########
                    ctrl_max_vals = []
                    if show_control == True:
                        for c in control:

                            control_data = data[data['Type'].str.contains(c) == True]
                            control_data = control_data[(control_data['Protein'] == pval)].iloc[:, -data_length:]

                            control_time = time[time['Type'].str.contains(c) == True]
                            control_time = control_time[(control_time['Protein'] == pval)].iloc[:, -data_length:]
                            ctrl_max_vals.append(control_data.max().max())
                            
                            # convert u to mu
                            if unique_units[0] == 'u':
                                ctrl_label = "0 " + "$\mathrm{\mu }$" + unique_units[1:] + " ({})".format(c)
                            else:
                                ctrl_label = "0 {} ({})".format(unique_units, c)

                            ax.plot(control_time.iloc[0], control_data.iloc[0], marker, linestyle = '-', color = 'black', 
                                    mfc = get_color(control, 'binary', c), lw = 1, zorder = 2, label = ctrl_label)

                    ########## control end ##########

                    markers = itertools.cycle(marker_list) # define unique markers for each line (unique_markers = True)

                    # plot series, iterating down rows
                    for i in range(len(time_temp)):
                        # get legend labels
                        if unique_units[0] == 'u':
                            legend_label = "{} ".format(data_temp.index[i]) + "$\mathrm{\mu }$" + unique_units[1:]
                        else:
                            legend_label = "{} {}".format(data_temp.index[i], unique_units)

                        # plot each condition
                        if error == True:
                            ax.errorbar(x = time_temp.iloc[i], y = data_temp.iloc[i], yerr = yerr_temp.iloc[i],
                                       capsize = 3, zorder=1, color = plot_color(data_temp, cmap, i),
                                       label = legend_label)
                        else: 
                            if unique_markers == False:
                                ax.plot(time_temp.iloc[i], data_temp.iloc[i], marker = marker, linestyle = '-', zorder=1, label = legend_label, ms = 5, color = plot_color(data_temp, cmap, i), lw = 1)
                            else:
                                ax.plot(time_temp.iloc[i], data_temp.iloc[i], marker = next(markers), linestyle = '-', zorder=1, label = legend_label, ms = 4, color = 'black', lw = 1)

                        # add legend    
                        ax.legend(loc = "upper left", bbox_to_anchor = (1.0, 1.0), frameon = False, title = "{}".format(cval))

                    # white background makes the exported figure look a lot nicer
                    fig.patch.set_facecolor('white')

                    # spines
                    ax.spines['right'].set_visible(False)
                    ax.spines['top'].set_visible(False)

                    # add line representing the activator
                    times = self.processed_data[data_type]['time'].mean() # get times
                    time_filter = times > (self.inject - 5) # mean time series that contains activator

                    # get start and end points
                    injection_start = times[time_filter].iloc[0]
                    injection_end = times[time_filter].iloc[-1]

                    # add line indicating presence of activator
                    if len(ctrl_max_vals) > 0: # make sure line is above plotted data!! 
                        if max(ctrl_max_vals) > data_temp.max().max():
                            ymax = control_data.max().max() + control_data.max().max()*0.1
                    else:
                        ymax = data_temp.max().max() + data_temp.max().max()*0.1 # add a bit extra to prevent clash

                    ax.plot([injection_start, injection_end], [ymax, ymax], c = 'black')

                    # activator title
                    ax.text((injection_start+injection_end)/2, (ymax+ymax*0.05), activator, ha = 'center')

                    # assay title
                    ax.set_title(title, x = 0, fontweight = '550')

                    # axes labels
                    ax.set_xlabel("time (s)")
                    ax.set_ylabel("$\mathrm{\Delta Ca^{2+} \ _i}$ (Ratio Units F340/F380)")

                    # display plateau window
                    if show_window == True:
                        # x min and x max for axvspan 
                        xmin = time_temp.loc[:, self.window[0]].mean()
                        xmax = time_temp.loc[:, self.window[1]].mean()
                        ax.axvspan(xmin, xmax, facecolor = window_color, alpha = 0.5)

                    ########## PLOT END ##########

                plt.show()

                    # catch errors and notify user
#                 except UnitsError:
#                     print("{}, {} failed".format(pval, cval))
#                     print("too many units: {}".format(index['Concentration Units'].unique()))
#                 except ControlError:
#                     for c in control:
#                         if self.plate_map['Type'].isin([c]).any() == False:
#                             print("{} not in plate map".format(c))
#                 except:
#                     print("{}, {} failed".format(pval, cval))

    def amplitude(self, data_type):
        """Calculates response amplitude for each condition, updates processed_data dictionary with 'plateau' and plate_map with amplitude column. 
        
        :param data_type: Data to use to calculate amplitudes, either 'ratio' or 'baseline_corrected'
        :type data_type: str

        """
        try:
            # get ampltitude for every condition
            amp = (self.processed_data[data_type]['data'].iloc[:, self.window[0]:self.window[1]]).to_numpy()
            # get mean amplitude for every condition
            amp_mean = np.mean(amp, axis = 1)
            # update processed_data with response amplitude
            self.processed_data['plateau'] = {}
            self.processed_data['plateau']['data'] = pd.DataFrame(amp_mean, index = self.processed_data['ratio']['data'].index, columns = ['Amplitude'])

            print("Amplitudes calculated from {}. See self.processed_data['plateau']['data']".format(data_type))
        except:
            print("Ampltitude calculation failed.")
        
    def mean_amplitude(self, use_normalised = False):
        """Returns mean amplitudes and error for each condition.
        
        The user must run the normalise method before attempting to get the mean amplitudes of the normalised amplitudes.
        
        :param use_normalised: If True, uses normalised amplitudes, default = False
        :type use_normalised: bool
        :return: Mean amplitudes and error for each condition
        :rtype: Pandas DataFrame
        """
        mapped = self.plate_map.fillna(-1).join(self.processed_data['plateau']['data'])
        if use_normalised == True:
            mapped = self.plate_map.fillna(-1).join(self.processed_data['plateau']['data_normed'])
        # group by grouplist and take mean amplitude for each condition
        # filter for valid wells
        group = mapped[mapped.Valid == True] 
        # drop columns which can cause errors w/ groupby operations
        group.drop(['Valid', 'Column'], axis = 1, inplace = True)
        mean_response = group.groupby(self.grouplist).mean().reset_index()
        
        if use_normalised == False:
            mean_response['Amplitude Error'] = group.groupby(self.grouplist).sem().reset_index().loc[:, 'Amplitude']
        else:
            mean_response['amps_normed_error'] = group.groupby(self.grouplist).sem().reset_index().loc[:, 'amps_normed']
        # drop empty rows
        mean_response.drop(mean_response[mean_response['Type'] == 'empty'].index)

        # update data dict
        self.processed_data['mean_amplitudes'] = mean_response
        
        return mean_response
    
    def normalise(self):
        """Normalises amplitudes to mean control amplitude."""
        mean_amps = self.mean_amplitude(use_normalised = False) # get mean control amplitude
        amps = self.processed_data['plateau']['data']
        control_amp = float(mean_amps[mean_amps['Type'] == 'control']['Amplitude'])
        self.processed_data['plateau']['data_normed'] = "test"
        self.processed_data['plateau']['data_normed'] = ((amps * 100) / control_amp).rename(columns = {'Amplitude':'amps_normed'})

        return self.processed_data['plateau']['data_normed']
    
    @staticmethod
    def _gen_curve_data(mean_amps, plot_func, use_normalised, n, proteins, compounds, **kwargs):
        """Generates data required for a fitted plot of IC50's or EC50's."""
        # check proteins and compounds 
            
        curve_data = {}
        # filter amps 
        amps = mean_amps[mean_amps.Type == 'compound']
        
        # get names of proteins
        if proteins == []:
            prots = list(amps['Protein'].unique())
        else:
            prots = proteins
        # separate proteins 
        for pkey, pval in enumerate(prots):
            try:
                # check protein
                if mean_amps['Protein'].str.contains(pval, regex = False).any() == False:
                    raise ProteinNameError()
                # get compounds for each proteins
                if compounds == []:
                    comps = amps[amps['Protein'] == pval]['Compound'].unique()
                # get number of compounds for each protein
                for ckey, cval in enumerate(comps):
                    # check compound
                    if mean_amps['Compound'].str.contains(cval, regex = False).any() == False:
                        raise CompoundNameError()
                    # filter dataframe for each compound in each protein
                    temp = amps[(amps['Protein'] == pval) & (amps['Compound'] == cval)]
                    # check there is only 1 conc unit
                    if len(temp['Concentration Units'].unique()) > 1:
                        raise ValueError["One unit per condition please!"]
                        # check there is an adequate number of concs
                    if len(temp['Concentration']) < n:
                        raise ValueError("Not enough concs! You've only got {} for {}, compound {}. You really need at least {} to do a fit.".format(len(temp['Concentration']), pval, cval, n))

                    # get x, y and error values, c50 units, compound and protein names to use for plot
                    x = temp['Concentration'].to_numpy()
                    y = temp.iloc[:, -2].to_numpy()
                    yerr = temp.iloc[:, -1].to_numpy()
                    c50units = temp['Concentration Units'].unique()[0]

                    # get popt values for logistic regression
                    popt, pcov = curve_fit(func_dict[plot_func], x, y, **kwargs) 

                    # update curve_data dict
                    curve_data["{}_{}".format(pval, cval)] = {'x':x, 'y':y, 'yerr':yerr, 'c50units':c50units, 'compound':cval, 
                                                              'protein':pval, 'popt':popt}
            except CompoundNameError:
                print("Compound not found. Available compounds are: {}".format(amps['Compound'].unique()))

            except ProteinNameError:
                print("Protein not found. Available compounds are: {}".format(amps['Protein'].unique()))
                
        return curve_data  
        
    def _get_curve_data(self, plot_func, use_normalised, n, proteins, compounds, **kwargs): 
        """Updates and returns self.plot_data with data required for a fitted plot of IC50's or EC50's."""
        mean_amps = self.mean_amplitude(use_normalised)
        
        curve_data = self._gen_curve_data(mean_amps, plot_func, use_normalised, n, proteins, compounds, **kwargs)
        
        self.plot_data = curve_data
        
        return self.plot_data
        
    @staticmethod
    def _plot_curve(curve_data, plot_func, use_normalised, n, proteins, compounds, error_bar, cmap, combine, activator, title, dpi, show_top_bot, conc_units):
        
        legend_label = {"ic50":"IC$_{{50}}$", "ec50":"EC$_{{50}}$"}
        if combine == True:
            fig, ax = plt.subplots(dpi = dpi)
        
        for key, val in curve_data.items():
            
            
            ########## TRY EXCEPT ##########
            try:
                if combine == False:
                    fig, ax = plt.subplots(dpi = dpi)
                # unpack curve_data:
                x = val['x']
                y = val['y']
                yerr = val['yerr']
                c50units = val['c50units']
                popt = val['popt']
                protein = val['protein']
                compound = val['compound']
                tb_str = ""
                if show_top_bot == True:
                    tb_str = "\nTop = {:.2f}\nBottom = {:.2f}".format(popt[0], popt[1])

                # get x and y fits
                fit_x = np.logspace(np.log10(x.min())-0.5, np.log10(x.max())+0.5, 300)
                fit = func_dict[plot_func](fit_x, *popt)
                ############################# types of plot: combine True OR False ####################
                # TRUE
                if combine == True:
                    # get label for legend
                    
                    # convert u to mu 
                    if c50units[0] == 'u':
                        label = r"$\bf{}\ {}$ ".format(compound, "") + "\n{} = {:.2f}".format(legend_label[plot_func], popt[2]) + "$\mathrm{\mu }$" 
                        + "{}".format(c50units[1:]) + "\nHill Slope = {:.2f}".format(popt[3])+tb_str
                        
                    else:
                        label = r"$\bf{}\ {}$".format(compound, "") +"\n{} = {:.2f} {}\nHill Slope = {:.2f}".format(legend_label[plot_func], popt[2], c50units, popt[3])+tb_str

                    # plot fit
                    ax.plot(fit_x, fit, lw = 1.2, label = label, color = get_color(list(curve_data.keys()), cmap, "{}_{}".format(protein, compound)))

                    # plot errors
                    if error_bar == True:
                        ax.errorbar(x, y, yerr, fmt='ko',capsize=3,ms=3, color = get_color(list(curve_data.keys()), cmap, "{}_{}".format(protein, compound)))
                # FALSE        
                if combine == False:
                    # get label for legend
                    
                    # convert u to mu 
                    if c50units[0] == 'u':
                        label = "\n{} = {:.2f} ".format(legend_label[plot_func], popt[2]) + "$\mathrm{\mu }$" + "{}".format(c50units[1:]) + "\nHill Slope = {:.2f}".format(popt[3])+tb_str
                    else:
                        label = "{} = {:.2f} {}\nHill Slope = {:.2f} ".format(legend_label[plot_func], popt[2], c50units, popt[3])+tb_str

                    # plot fit
                    ax.plot(fit_x, fit, lw = 1.2, label = label, color = 'black')

                    # plot errors
                    if error_bar == True:
                        ax.errorbar(x, y, yerr, fmt='ko',capsize=3,ms=3, color = 'black')

                    # add legend
                    ax.legend(loc = 'best', frameon = False,framealpha=0.7, handlelength=0, handletextpad=0)

                ############# end of combine differences   ############# 
                ############# in loop modifications for both types of plot (combine = True OR False) #############   
                plt.xscale('log')    

                # spines
                ax.spines['right'].set_visible(False)
                ax.spines['top'].set_visible(False)

                # labels
                ax.set_xlabel("[{}] / {}".format(compound, conc_units))
                ax.set_title(title, x = 0, fontweight = '550')

                if use_normalised == False:
                    ax.set_ylabel("$\mathrm{\Delta Ca^{2+} \ _i}$ (Ratio Units F340/F380)")
                else:
                    if activator == "":
                        ax.set_ylabel("% of activation")
                    else:
                        ax.set_ylabel("% of activation by {}".format(activator))

                # title
                ax.set_title(title, x = 0, fontweight = '550')

                plt.minorticks_off()
            except:
                print("Did not plot {}, {}".format(protein, compound))
        ###################################### end of plotting loop ##########################        
        # post loop mods
        
        
        # using plt legend allows use of loc = 'best' to prevent annotation clashing with line
        if combine == True:
            leg = ax.legend(loc = 'center left', frameon = False, bbox_to_anchor = [1.0, 0.5])
            ax.set_xlabel("Concentration / {}".format(conc_units))
        
        # set background
        fig.patch.set_facecolor('white')
        
        plt.show()
        
        
    def plot_curve(self, plot_func, use_normalised = False, n = 5, proteins = [], compounds = [], error_bar = True, cmap = "Dark2", combine = False, activator = " ", title = " ", dpi = 120, show_top_bot = False, conc_units = 'M', **kwargs):
        """Plots fitted curve using logistic regression with errors and IC50/EC50 values.
        
        :param plot_func: Plot function to use, either ic50 or ec50
        :type plot_func: str
        :param use_normalised: If True, uses normalised amplitudes, default = False
        :type use_normalised: bool
        :param n: Number of concentrations required for plot
        :type n: int
        :param proteins: Proteins to plot, defaults to all
        :type proteins: list
        :param compounds: Compounds to plot, defaults to all
        :type compounds: list
        :param error_bar: If True, reveals error bars, default = True
        :type error_bar = bool
        :param cmap: Color map that sets each line colour in a combined plot, default = "Dark2"
        :type cmap: bool
        :param activator: Activator injected into assay, default = " "
        :type activator: str
        :param title: Choose between automatic title or insert string to use, default = " "
        :type title: str
        :param dpi: Size of figure
        :type dpi: int
        :param show_top_bot: 'True' shows the top and bottom curve fitting values in the legend
        :type show_top_bot: bool
        :param conc_units: Units displayed on x axis of graph
        :type conc_units: str
        :param **kwargs: Additional curve fitting arguments
        """         
        curve_data =  self._get_curve_data(plot_func, use_normalised, n, proteins, compounds, **kwargs)
        
        # update object with curve data to access when exporting
        self.curve_data = curve_data
        for key in self.curve_data.keys():
            self.curve_data[key]['plot_func'] = plot_func # curve data maximal conc plot function

    # do plots
        self._plot_curve(curve_data, plot_func, use_normalised, n, proteins, compounds, error_bar, cmap, combine, activator, title, dpi, show_top_bot, conc_units)
        
        
        
    def export_data(self, title = ""):
        """Exports raw and processed data to an excel file. 
        
        The excel file will be named after the 'self.title' parameter unless otherwise specified.
        
        :param title: Title of the excel file, default = self.title + '_data_export.xlsx'
        :type title: str
        :return: Excel document
        :rtype: .xlsx
        """
        if title == "":
            title = self.title + '_data_export.xlsx'
        if title[-5:] != '.xlsx':
            title = title + '.xlsx'
            
        dfs = self.processed_data
        
        # get curve data
        curve_list = {}
        
        if hasattr(self, 'curve_data'):
            for key, val in self.curve_data.items(): # get curve data for each protein/compound combo
                curve = copy.deepcopy(val) # get deep copy - prevents original dict from being edited
                curve['popt_keys'] = ['top', 'bottom', curve['plot_func'], 'hill'] # add popt index
                for i in ['compound', 'protein', 'c50units', 'plot_func']: # delete irrelevent keys
                    curve.pop(i)
                curve_df = pd.DataFrame.from_dict(curve, orient = 'index')
                
                curve_list[key] = curve_df
    
    
        # create separate sheet for each data set
        with pd.ExcelWriter(title) as writer:
            dfs['ratio']['time'].to_excel(writer, sheet_name = 'time')
            dfs['ratio']['data'].to_excel(writer, sheet_name = 'ratio')
            dfs['baseline_corrected']['data'].to_excel(writer, sheet_name = 'baseline_corrected')
            if 'data' in dfs['plateau'].keys():
                dfs['plateau']['data'].to_excel(writer, sheet_name = 'plateau')
            if 'data_normed' in dfs['plateau'].keys():
                dfs['plateau']['data'].to_excel(writer, sheet_name = 'normalised_plateau')
            if 'mean_amplitudes' in dfs.keys():
                dfs['mean_amplitudes'].to_excel(writer, sheet_name = 'mean_amplitudes')
            self.plate_map.to_excel(writer, sheet_name = 'plate_map')
            
            # export curve data for each protein/compund combination
            for key, val in curve_list.items():
                val.to_excel(writer, sheet_name = key + '_curve_data')
                
        print("Data exported to excel as {} Check your folder.".format(title))

Methods

def amplitude(self, data_type)

Calculates response amplitude for each condition, updates processed_data dictionary with 'plateau' and plate_map with amplitude column.

:param data_type: Data to use to calculate amplitudes, either 'ratio' or 'baseline_corrected' :type data_type: str

Expand source code
def amplitude(self, data_type):
    """Calculates response amplitude for each condition, updates processed_data dictionary with 'plateau' and plate_map with amplitude column. 
    
    :param data_type: Data to use to calculate amplitudes, either 'ratio' or 'baseline_corrected'
    :type data_type: str

    """
    try:
        # get ampltitude for every condition
        amp = (self.processed_data[data_type]['data'].iloc[:, self.window[0]:self.window[1]]).to_numpy()
        # get mean amplitude for every condition
        amp_mean = np.mean(amp, axis = 1)
        # update processed_data with response amplitude
        self.processed_data['plateau'] = {}
        self.processed_data['plateau']['data'] = pd.DataFrame(amp_mean, index = self.processed_data['ratio']['data'].index, columns = ['Amplitude'])

        print("Amplitudes calculated from {}. See self.processed_data['plateau']['data']".format(data_type))
    except:
        print("Ampltitude calculation failed.")
def baseline_correct(self)

Baseline corrects 'ratio' data using the pre-injection time points.

Expand source code
def baseline_correct(self):
    """Baseline corrects 'ratio' data using the pre-injection time points."""
    try:
        time_cut = self.inject - 5
        data_source = self.processed_data['ratio']
        # convert to numpy arrays
        time = data_source['time'].to_numpy()
        data = data_source['data'].to_numpy()
        # create mask from mean time values
        time_filter = np.nanmean(time,axis=0)<time_cut
        # # average over these times
        baseline = np.mean(data[:,time_filter],axis=1)
        # add dimension to enable broadcasting
        baseline = np.expand_dims(baseline, axis=1)
        # rewrite values back to dataframes
        self.processed_data['baseline_corrected'] = {}
        data_source = self.processed_data['baseline_corrected']['data'] = pd.DataFrame(data-baseline, index = data_source['data'].index)
        data_source = self.processed_data['baseline_corrected']['time'] = data_source = self.processed_data['ratio']['time']

        print("Baseline corrected! See self.processed_data['baseline_corrected']")
    except:
        print("baseline correction failed.")
def def_window(self, time, data_type)

Manually set the plateau window.

:param time: Time point at start of window :type time: int :param data_type: Data to set window on, either 'ratio' or 'baseline_corrected' :type data_type: str :return: Tuple of start and end index of plateau window :rtype: (int, int)

Expand source code
def def_window(self, time, data_type):
    """Manually set the plateau window.
    
    :param time: Time point at start of window
    :type time: int
    :param data_type: Data to set window on, either 'ratio' or 'baseline_corrected'
    :type data_type: str
    :return: Tuple of start and end index of plateau window
    :rtype: (int, int)
    """
    valid_filter = self.plate_map.Valid == True
    data_source = self.processed_data[data_type]
    time_df = data_source['time'][valid_filter]
    
    # create mask from mean time values
    window_filter = np.nanmean(time_df,axis=0) >= time
    index = np.array(list(data_source['data'].columns))[window_filter]
    self.window =  (index[0], index[10])
def export_data(self, title='')

Exports raw and processed data to an excel file.

The excel file will be named after the 'self.title' parameter unless otherwise specified.

:param title: Title of the excel file, default = self.title + '_data_export.xlsx' :type title: str :return: Excel document :rtype: .xlsx

Expand source code
def export_data(self, title = ""):
    """Exports raw and processed data to an excel file. 
    
    The excel file will be named after the 'self.title' parameter unless otherwise specified.
    
    :param title: Title of the excel file, default = self.title + '_data_export.xlsx'
    :type title: str
    :return: Excel document
    :rtype: .xlsx
    """
    if title == "":
        title = self.title + '_data_export.xlsx'
    if title[-5:] != '.xlsx':
        title = title + '.xlsx'
        
    dfs = self.processed_data
    
    # get curve data
    curve_list = {}
    
    if hasattr(self, 'curve_data'):
        for key, val in self.curve_data.items(): # get curve data for each protein/compound combo
            curve = copy.deepcopy(val) # get deep copy - prevents original dict from being edited
            curve['popt_keys'] = ['top', 'bottom', curve['plot_func'], 'hill'] # add popt index
            for i in ['compound', 'protein', 'c50units', 'plot_func']: # delete irrelevent keys
                curve.pop(i)
            curve_df = pd.DataFrame.from_dict(curve, orient = 'index')
            
            curve_list[key] = curve_df


    # create separate sheet for each data set
    with pd.ExcelWriter(title) as writer:
        dfs['ratio']['time'].to_excel(writer, sheet_name = 'time')
        dfs['ratio']['data'].to_excel(writer, sheet_name = 'ratio')
        dfs['baseline_corrected']['data'].to_excel(writer, sheet_name = 'baseline_corrected')
        if 'data' in dfs['plateau'].keys():
            dfs['plateau']['data'].to_excel(writer, sheet_name = 'plateau')
        if 'data_normed' in dfs['plateau'].keys():
            dfs['plateau']['data'].to_excel(writer, sheet_name = 'normalised_plateau')
        if 'mean_amplitudes' in dfs.keys():
            dfs['mean_amplitudes'].to_excel(writer, sheet_name = 'mean_amplitudes')
        self.plate_map.to_excel(writer, sheet_name = 'plate_map')
        
        # export curve data for each protein/compund combination
        for key, val in curve_list.items():
            val.to_excel(writer, sheet_name = key + '_curve_data')
            
    print("Data exported to excel as {} Check your folder.".format(title))
def get_gradients(self, data_type)

Returns the mean gradient over every 10 time points post injection.

:param data_type: Data series to calculate plateau :type data_type: str :return: Dict containing a tuple corresponding to the start and end index and the corresponding mean gradient :rtype: dict[tuple, float]

Expand source code
def get_gradients(self, data_type):
    """Returns the mean gradient over every 10 time points post injection.
    
    :param data_type: Data series to calculate plateau
    :type data_type: str
    :return: Dict containing a tuple corresponding to the start and end index and the corresponding mean gradient
    :rtype: dict[tuple, float]
    """        
    # filter for valid wells
    valid_filter = self.plate_map.Valid == True

    # add opposite time filter to extract data after injection
    time_cut = self.inject + 5
    data_source = self.processed_data[data_type]

    # convert to numpy arrays
    time = data_source['time'][valid_filter].to_numpy()
    data = data_source['data'][valid_filter].to_numpy()
    # create mask from mean time values
    post_inject_filter = np.nanmean(time,axis=0) > time_cut

    # get absolute gradient for each well along series
    gradient = abs(np.gradient(data[:, post_inject_filter], axis = 1))

    gradient_dict = {}

    index = np.array(list(data_source['data'].columns))[post_inject_filter]

    # mean gradient every ten measurements
    for i in range(gradient.shape[1]-9):

        # average of average gradients for every ten measurements post injection
        mean_gradient = np.nanmean(np.mean(gradient[:, i:(i+10)], axis=1), axis = 0)
        gradient_dict[(index[i]), (index[i]+10)] = mean_gradient
        
    return gradient_dict
def get_window(self, data_type)

Updates self.window with the lowest mean gradient for each ten time point window post injection.

:param data_type: Data series to calculate plateau :type data_type: str

Expand source code
def get_window(self, data_type):
    """Updates self.window with the lowest mean gradient for each ten time point window post injection.
    
    :param data_type: Data series to calculate plateau
    :type data_type: str
    """
    gradient_dict = self.get_gradients(data_type)
    
    # get minimum gradient index window
    min_gradient = (min(gradient_dict, key = gradient_dict.get))

    self.window = min_gradient
def invalidate_cols(self, cols)

Invalidates specified wells and updates plate_map

:param wells: Wells to invalidate :type wells: list of ints, e.g. (1, 2, 3)

Expand source code
def invalidate_cols(self, cols):
    """Invalidates specified wells and updates plate_map
    
    :param wells: Wells to invalidate
    :type wells: list of ints, e.g. (1, 2, 3)
    """
    platemap = pm.invalidate_cols(self.plate_map, cols, valid = False)
    self.plate_map = platemap
    print("Columns {} invalidated".format(cols))
def invalidate_rows(self, rows)

Invalidates specified rows and updates plate_map

:param wells: Rows to invalidate :type wells: list of strings, e.g. ("A", "B", "C")

Expand source code
def invalidate_rows(self, rows):
    """Invalidates specified rows and updates plate_map
    
    :param wells: Rows to invalidate
    :type wells: list of strings, e.g. ("A", "B", "C")
    """
    platemap = pm.invalidate_rows(self.plate_map, rows, valid = False)
    self.plate_map = platemap
    print("Row {} invalidated".format(rows))
def invalidate_wells(self, wells)

Invalidates specified wells and updates plate_map

:param wells: Wells to invalidate :type wells: list of strings, e.g. ("A1", "A2", "A3")

Expand source code
def invalidate_wells(self, wells):
    """Invalidates specified wells and updates plate_map
    
    :param wells: Wells to invalidate
    :type wells: list of strings, e.g. ("A1", "A2", "A3")
    """
    
    self.plate_map = pm.invalidate_wells(self.plate_map, wells = wells, valid = False)
    print("{} invalidated".format(wells))
def mean_amplitude(self, use_normalised=False)

Returns mean amplitudes and error for each condition.

The user must run the normalise method before attempting to get the mean amplitudes of the normalised amplitudes.

:param use_normalised: If True, uses normalised amplitudes, default = False :type use_normalised: bool :return: Mean amplitudes and error for each condition :rtype: Pandas DataFrame

Expand source code
def mean_amplitude(self, use_normalised = False):
    """Returns mean amplitudes and error for each condition.
    
    The user must run the normalise method before attempting to get the mean amplitudes of the normalised amplitudes.
    
    :param use_normalised: If True, uses normalised amplitudes, default = False
    :type use_normalised: bool
    :return: Mean amplitudes and error for each condition
    :rtype: Pandas DataFrame
    """
    mapped = self.plate_map.fillna(-1).join(self.processed_data['plateau']['data'])
    if use_normalised == True:
        mapped = self.plate_map.fillna(-1).join(self.processed_data['plateau']['data_normed'])
    # group by grouplist and take mean amplitude for each condition
    # filter for valid wells
    group = mapped[mapped.Valid == True] 
    # drop columns which can cause errors w/ groupby operations
    group.drop(['Valid', 'Column'], axis = 1, inplace = True)
    mean_response = group.groupby(self.grouplist).mean().reset_index()
    
    if use_normalised == False:
        mean_response['Amplitude Error'] = group.groupby(self.grouplist).sem().reset_index().loc[:, 'Amplitude']
    else:
        mean_response['amps_normed_error'] = group.groupby(self.grouplist).sem().reset_index().loc[:, 'amps_normed']
    # drop empty rows
    mean_response.drop(mean_response[mean_response['Type'] == 'empty'].index)

    # update data dict
    self.processed_data['mean_amplitudes'] = mean_response
    
    return mean_response
def normalise(self)

Normalises amplitudes to mean control amplitude.

Expand source code
def normalise(self):
    """Normalises amplitudes to mean control amplitude."""
    mean_amps = self.mean_amplitude(use_normalised = False) # get mean control amplitude
    amps = self.processed_data['plateau']['data']
    control_amp = float(mean_amps[mean_amps['Type'] == 'control']['Amplitude'])
    self.processed_data['plateau']['data_normed'] = "test"
    self.processed_data['plateau']['data_normed'] = ((amps * 100) / control_amp).rename(columns = {'Amplitude':'amps_normed'})

    return self.processed_data['plateau']['data_normed']
def plot_conditions(self, data_type, activator=' ', show_window=False, dpi=120, title=' ', error=False, control=['control'], cmap='winter_r', window_color='hotpink', proteins=[], compounds=[], marker='o', unique_markers=False, marker_list=['o', '^', 's', 'D', 'p', '*', 'v'], show_control=True)

Plots each mean condition versus time.

:param data_type: Data to be plotted, either 'ratio' or 'baseline_corrected' :type data_type: str :param activator: Activator injected into assay, default = "" :type activator: str :param show_window: If 'True', shows the window from which the plateau for each condition is calculated, default = False :type show_window: bool :param dpi: Size of figure, default = 120 :type dpi: int :param title: Title of plot ADD LIST OF TITLES? :type title: str :param error: If True, plots error bars for each mean condition, default = False :type error: bool :param control: List of control conditions, default = 'control' :type control: bool :param cmap: Colormap to use as the source of plot colors :type cmap: str :param window_color: Color of the plateau window, default = 'hotpink' :type window_color: str :param marker: Marker type, default = '-o' :type marker: str :param unique_markers: If True, plots each condition as a black line with a unique marker, default = False :type unique_markers: bool :param marker_list: List of marker symbols to use when unique_markers = True, default = ["o", "^", "s", "D", "p", "*", "v"] :type marker_list: [str] :param show_control: If True, plots each control condition for each protein, default = True :return: Figure displaying each mean condition versus time :rtype: fig

Expand source code
    def plot_conditions(self, data_type, activator = " ", show_window = False, dpi = 120, title = " ", error = False, control = ['control'], cmap = "winter_r", window_color = 'hotpink', proteins = [], compounds = [], marker = 'o', unique_markers = False, marker_list = ["o", "^", "s", "D", "p", "*", "v"], show_control = True):
        """Plots each mean condition versus time.
        
        :param data_type: Data to be plotted, either 'ratio' or 'baseline_corrected'
        :type data_type: str
        :param activator: Activator injected into assay, default = ""
        :type activator: str
        :param show_window: If 'True', shows the window from which the plateau for each condition is calculated, default = False 
        :type show_window: bool
        :param dpi: Size of figure, default = 120
        :type dpi: int
        :param title: Title of plot ADD LIST OF TITLES?
        :type title: str
        :param error: If True, plots error bars for each mean condition, default = False
        :type error: bool
        :param control: List of control conditions, default = 'control'
        :type control: bool
        :param cmap: Colormap to use as the source of plot colors
        :type cmap: str
        :param window_color: Color of the plateau window, default = 'hotpink'
        :type window_color: str
        :param marker: Marker type, default = '-o'
        :type marker: str
        :param unique_markers: If True, plots each condition as a black line with a unique marker, default = False
        :type unique_markers: bool
        :param marker_list: List of marker symbols to use when unique_markers = True, default = ["o", "^", "s", "D", "p", "*", "v"]
        :type marker_list: [str]
        :param show_control: If True, plots each control condition for each protein, default = True
        :return: Figure displaying each mean condition versus time
        :rtype: fig
        """

        platemap = self.plate_map
        grouplist = self.grouplist
        groupdct = {}
        
        # check control
        # check controls are correct
        for c in control:
            if self.plate_map['Type'].isin([c]).any() == False:
                raise ControlError("{} not in plate map".format(c))
        
        for key, val in self.processed_data[data_type].items():
            data_length = val.shape[1]
            mapped = platemap.fillna('none').join(val)
            group = mapped[mapped.Valid == True].groupby(grouplist)[val.columns]
            # update dictionary
            groupdct[key] = group

        # get data, time and error values for each condition
        data = groupdct['data'].mean().reset_index()
        time = groupdct['time'].mean().reset_index()
        yerr = groupdct['data'].sem().reset_index()

        # get names of proteins and compounds, excluding control
        if proteins == []:
            proteins = data[data['Type'].str.contains('control') == False]['Protein'].unique()
            
        # iterate through proteins list
        for pkey, pval in enumerate(proteins):
            # get number of compounds for each protein
            if compounds == []:
                compounds = data[(data['Type'].str.contains('control') == False) & (data['Protein'] == pval)]['Compound'].unique()
                
            # iterate through compounds for each protein
            for ckey, cval in enumerate(compounds):                
                # try each different combo
#                 try:
                # extract data for each protein and compound, excluding control. 

                data_temp = data[data['Type'].isin(control) == False]
                data_temp = data_temp[(data_temp['Protein'] == pval) & (data_temp['Compound'] == cval)]

                time_temp = time[time['Type'].isin(control) == False]
                time_temp = time_temp[(time_temp['Protein'] == pval) & (time_temp['Compound'] == cval)]

                yerr_temp = yerr[yerr['Type'].isin(control) == False]
                yerr_temp = yerr_temp[(yerr_temp['Protein'] == pval) & (yerr_temp['Compound'] == cval)]

                templist = [x for x in grouplist if x != 'Concentration'] # get columns to remove

                # extract just the data with conc as the index
                index = data_temp.iloc[:, :-data_length]
                data_temp = data_temp.set_index('Concentration').iloc[:, -data_length:]
                time_temp = time_temp.set_index('Concentration').iloc[:, -data_length:]
                yerr_temp = yerr_temp.set_index('Concentration').iloc[:, -data_length:]

                ########## PLOT ##########
                if (pval != 'none') & (cval != 'none'):
                    fig, ax = plt.subplots(dpi = 120)

                    # check units!
                    if len(index['Concentration Units'].unique()) > 1 :
                        raise UnitsError
                    # if units are fine, get units!
                    unique_units = index['Concentration Units'].unique()[0]

                    ########## control ##########
                    ctrl_max_vals = []
                    if show_control == True:
                        for c in control:

                            control_data = data[data['Type'].str.contains(c) == True]
                            control_data = control_data[(control_data['Protein'] == pval)].iloc[:, -data_length:]

                            control_time = time[time['Type'].str.contains(c) == True]
                            control_time = control_time[(control_time['Protein'] == pval)].iloc[:, -data_length:]
                            ctrl_max_vals.append(control_data.max().max())
                            
                            # convert u to mu
                            if unique_units[0] == 'u':
                                ctrl_label = "0 " + "$\mathrm{\mu }$" + unique_units[1:] + " ({})".format(c)
                            else:
                                ctrl_label = "0 {} ({})".format(unique_units, c)

                            ax.plot(control_time.iloc[0], control_data.iloc[0], marker, linestyle = '-', color = 'black', 
                                    mfc = get_color(control, 'binary', c), lw = 1, zorder = 2, label = ctrl_label)

                    ########## control end ##########

                    markers = itertools.cycle(marker_list) # define unique markers for each line (unique_markers = True)

                    # plot series, iterating down rows
                    for i in range(len(time_temp)):
                        # get legend labels
                        if unique_units[0] == 'u':
                            legend_label = "{} ".format(data_temp.index[i]) + "$\mathrm{\mu }$" + unique_units[1:]
                        else:
                            legend_label = "{} {}".format(data_temp.index[i], unique_units)

                        # plot each condition
                        if error == True:
                            ax.errorbar(x = time_temp.iloc[i], y = data_temp.iloc[i], yerr = yerr_temp.iloc[i],
                                       capsize = 3, zorder=1, color = plot_color(data_temp, cmap, i),
                                       label = legend_label)
                        else: 
                            if unique_markers == False:
                                ax.plot(time_temp.iloc[i], data_temp.iloc[i], marker = marker, linestyle = '-', zorder=1, label = legend_label, ms = 5, color = plot_color(data_temp, cmap, i), lw = 1)
                            else:
                                ax.plot(time_temp.iloc[i], data_temp.iloc[i], marker = next(markers), linestyle = '-', zorder=1, label = legend_label, ms = 4, color = 'black', lw = 1)

                        # add legend    
                        ax.legend(loc = "upper left", bbox_to_anchor = (1.0, 1.0), frameon = False, title = "{}".format(cval))

                    # white background makes the exported figure look a lot nicer
                    fig.patch.set_facecolor('white')

                    # spines
                    ax.spines['right'].set_visible(False)
                    ax.spines['top'].set_visible(False)

                    # add line representing the activator
                    times = self.processed_data[data_type]['time'].mean() # get times
                    time_filter = times > (self.inject - 5) # mean time series that contains activator

                    # get start and end points
                    injection_start = times[time_filter].iloc[0]
                    injection_end = times[time_filter].iloc[-1]

                    # add line indicating presence of activator
                    if len(ctrl_max_vals) > 0: # make sure line is above plotted data!! 
                        if max(ctrl_max_vals) > data_temp.max().max():
                            ymax = control_data.max().max() + control_data.max().max()*0.1
                    else:
                        ymax = data_temp.max().max() + data_temp.max().max()*0.1 # add a bit extra to prevent clash

                    ax.plot([injection_start, injection_end], [ymax, ymax], c = 'black')

                    # activator title
                    ax.text((injection_start+injection_end)/2, (ymax+ymax*0.05), activator, ha = 'center')

                    # assay title
                    ax.set_title(title, x = 0, fontweight = '550')

                    # axes labels
                    ax.set_xlabel("time (s)")
                    ax.set_ylabel("$\mathrm{\Delta Ca^{2+} \ _i}$ (Ratio Units F340/F380)")

                    # display plateau window
                    if show_window == True:
                        # x min and x max for axvspan 
                        xmin = time_temp.loc[:, self.window[0]].mean()
                        xmax = time_temp.loc[:, self.window[1]].mean()
                        ax.axvspan(xmin, xmax, facecolor = window_color, alpha = 0.5)

                    ########## PLOT END ##########

                plt.show()
def plot_curve(self, plot_func, use_normalised=False, n=5, proteins=[], compounds=[], error_bar=True, cmap='Dark2', combine=False, activator=' ', title=' ', dpi=120, show_top_bot=False, conc_units='M', **kwargs)

Plots fitted curve using logistic regression with errors and IC50/EC50 values.

:param plot_func: Plot function to use, either ic50 or ec50 :type plot_func: str :param use_normalised: If True, uses normalised amplitudes, default = False :type use_normalised: bool :param n: Number of concentrations required for plot :type n: int :param proteins: Proteins to plot, defaults to all :type proteins: list :param compounds: Compounds to plot, defaults to all :type compounds: list :param error_bar: If True, reveals error bars, default = True :type error_bar = bool :param cmap: Color map that sets each line colour in a combined plot, default = "Dark2" :type cmap: bool :param activator: Activator injected into assay, default = " " :type activator: str :param title: Choose between automatic title or insert string to use, default = " " :type title: str :param dpi: Size of figure :type dpi: int :param show_top_bot: 'True' shows the top and bottom curve fitting values in the legend :type show_top_bot: bool :param conc_units: Units displayed on x axis of graph :type conc_units: str :param **kwargs: Additional curve fitting arguments

Expand source code
def plot_curve(self, plot_func, use_normalised = False, n = 5, proteins = [], compounds = [], error_bar = True, cmap = "Dark2", combine = False, activator = " ", title = " ", dpi = 120, show_top_bot = False, conc_units = 'M', **kwargs):
    """Plots fitted curve using logistic regression with errors and IC50/EC50 values.
    
    :param plot_func: Plot function to use, either ic50 or ec50
    :type plot_func: str
    :param use_normalised: If True, uses normalised amplitudes, default = False
    :type use_normalised: bool
    :param n: Number of concentrations required for plot
    :type n: int
    :param proteins: Proteins to plot, defaults to all
    :type proteins: list
    :param compounds: Compounds to plot, defaults to all
    :type compounds: list
    :param error_bar: If True, reveals error bars, default = True
    :type error_bar = bool
    :param cmap: Color map that sets each line colour in a combined plot, default = "Dark2"
    :type cmap: bool
    :param activator: Activator injected into assay, default = " "
    :type activator: str
    :param title: Choose between automatic title or insert string to use, default = " "
    :type title: str
    :param dpi: Size of figure
    :type dpi: int
    :param show_top_bot: 'True' shows the top and bottom curve fitting values in the legend
    :type show_top_bot: bool
    :param conc_units: Units displayed on x axis of graph
    :type conc_units: str
    :param **kwargs: Additional curve fitting arguments
    """         
    curve_data =  self._get_curve_data(plot_func, use_normalised, n, proteins, compounds, **kwargs)
    
    # update object with curve data to access when exporting
    self.curve_data = curve_data
    for key in self.curve_data.keys():
        self.curve_data[key]['plot_func'] = plot_func # curve data maximal conc plot function

# do plots
    self._plot_curve(curve_data, plot_func, use_normalised, n, proteins, compounds, error_bar, cmap, combine, activator, title, dpi, show_top_bot, conc_units)
def see_plate(self, title='', export=False, cmap='Paired', colorby='Type', labelby='Type', dpi=150)

Returns a visual representation of the plate map.

The label and colour for each well can be customised to be a variable, for example 'Compound', 'Protein', 'Concentration', 'Concentration Units', 'Contents' or 'Type'. The size of the plate map used to generate the figure can be either 6, 12, 24, 48, 96 or 384. :param size: Size of platemap, 6, 12, 24, 48, 96 or 384, default = 96 :type size: int
:param export: If 'True' a .png file of the figure is saved, default = False :type export: bool :param title: Sets the title of the figure, optional :type title: str :param cmap: Sets the colormap for the color-coding, default = 'Paired' :type cmap: str :param colorby: Chooses the parameter to color code by, for example 'Type', 'Contents', 'Concentration', 'Compound', 'Protein', 'Concentration Units', default = 'Type' :type colorby: str :param labelby: Chooses the parameter to label code by, for example 'Type', 'Contents', 'Concentration', 'Compound', 'Protein', 'Concentration Units', default = 'Type' :type labelby: str :param dpi: Size of the figure, default = 150 :type dpi: int :return: Visual representation of the plate map. :rtype: figure

Expand source code
def see_plate(self, title = "", export = False, cmap = 'Paired',
         colorby = 'Type', labelby = 'Type', dpi = 150):
    """Returns a visual representation of the plate map.

    The label and colour for each well can be customised to be a variable, for example 'Compound', 'Protein', 'Concentration', 'Concentration Units', 'Contents' or 'Type'. The size of the plate map used to generate the figure can be either 6, 12, 24, 48, 96 or 384. 
    :param size: Size of platemap, 6, 12, 24, 48, 96 or 384, default = 96
    :type size: int    
    :param export: If 'True' a .png file of the figure is saved, default = False
    :type export: bool
    :param title: Sets the title of the figure, optional
    :type title: str
    :param cmap: Sets the colormap for the color-coding, default = 'Paired'
    :type cmap: str
    :param colorby: Chooses the parameter to color code by, for example 'Type', 'Contents', 'Concentration', 'Compound', 'Protein', 'Concentration Units', default = 'Type'
    :type colorby: str
    :param labelby: Chooses the parameter to label code by, for example 'Type', 'Contents', 'Concentration', 'Compound', 'Protein', 'Concentration Units', default = 'Type'
    :type labelby: str
    :param dpi: Size of the figure, default = 150
    :type dpi: int
    :return: Visual representation of the plate map.
    :rtype: figure
    """
    pm.visualise(self.plate_map, title = title, size = self.size, export = export, cmap = cmap,
         colorby = colorby, labelby = labelby, dpi = dpi)
def see_wells(self, to_plot, share_y=True, colorby='Type', labelby='Type', cmap='Dark2_r')

Returns plotted data from stipulated wells.

:param size: Size of platemap, 6, 12, 24, 48, 96 or 384, default = 96 :type size: int
:param to_plot: Wells to plot :type to_plot: string or list of strings (well ID's), e.g. "A1", "A2", "A3" :param share_y: 'True' sets y axis the same for all plots, default = 'True' :type share_y: bool :param cmap: Sets the colormap for the color-coding, optional :type cmap: str :param colorby: Chooses the parameter to color code by, for example 'Type', 'Contents', 'Concentration', 'Compound', 'Protein', 'Concentration Units', default = 'Type' :type colorby: str :param labelby: Chooses the parameter to label code by, for example 'Type', 'Contents', 'Concentration', 'Compound', 'Protein', 'Concentration Units', default = 'Type' :type labelby: str :return: Plotted data for the stipulated wells of the well plate
:rtype: figure

Expand source code
def see_wells(self, to_plot, share_y = True, colorby = 'Type', labelby = 'Type', cmap = 'Dark2_r'):
    """Returns plotted data from stipulated wells.

    :param size: Size of platemap, 6, 12, 24, 48, 96 or 384, default = 96
    :type size: int   
    :param to_plot: Wells to plot
    :type to_plot: string or list of strings (well ID's), e.g. "A1", "A2", "A3"
    :param share_y: 'True' sets y axis the same for all plots, default = 'True'
    :type share_y: bool
    :param cmap: Sets the colormap for the color-coding, optional
    :type cmap: str
    :param colorby: Chooses the parameter to color code by, for example 'Type', 'Contents', 'Concentration', 'Compound', 'Protein', 'Concentration Units', default = 'Type'
    :type colorby: str
    :param labelby: Chooses the parameter to label code by, for example 'Type', 'Contents', 'Concentration', 'Compound', 'Protein', 'Concentration Units', default = 'Type'
    :type labelby: str
    :return: Plotted data for the stipulated wells of the well plate  
    :rtype: figure
    """
    # check labelby and colorby
    if colorby not in self.plate_map.columns:
        raise pm.HeaderError("colorby parameter not in plate map")
    if labelby not in self.plate_map.columns:
        raise pm.HeaderError("labelby parameter not in plate map")

    # check to_plot
    if type(to_plot) == str:
        # plot individual well:
        try:
            label = self.plate_map.loc[to_plot][labelby]
            
            fig, ax = plt.subplots()
            ax.plot(self.processed_data['ratio']['time'].loc[to_plot], self.processed_data['ratio']['data'].loc[to_plot], lw = 3, color = 'black', label = "{} {}".format(to_plot, label))

        # add label for each well
            ax.legend(loc = 'best', frameon = True, fancybox = True)
            ax.set_title("{} {}".format(to_plot, pm.labelwell(self.plate_map, labelby, 0)))
            ax.set_facecolor('0.95')
            ax.set_xlabel("time / s")
            ax.set_ylabel("$\mathrm{\Delta Ca^{2+} \ _i}$ (Ratio Units F340/F380)")
            ax.set_title('Flex data versus time for {}'.format(to_plot), y = 1.05, size = 20)
            plt.show()

        except:
            print("Plot error, does {} contain data?".format(to_plot))

    else:
        # plot multiple wells
        fig, axs = plt.subplots(len(to_plot), 1, figsize = (2*len(to_plot), 3*len(to_plot)), constrained_layout = True, sharey = share_y)

        for i in range(len(to_plot)):
            try:
                label =  label = self.plate_map.loc[to_plot[i]][labelby] 
                axs[i].plot(self.processed_data['ratio']['time'].loc[to_plot[i]], self.processed_data['ratio']['data'].loc[to_plot[i]], 
                            lw = 3, color = pm.wellcolour2(self.plate_map, colorby, cmap, i, to_plot), 
                           label = "{} {}".format(to_plot[i], label))

            # add label for each well
                axs[i].legend(loc = 'best', frameon = True, fancybox = True)
                axs[i].set_title("{} {}".format(to_plot[i], label))
                axs[i].set_facecolor('0.95')
                axs[i].set_xlabel("time / s")
                axs[i].set_ylabel("$\mathrm{\Delta Ca^{2+} \ _i}$ (Ratio Units F340/F380)")
            except:
                print("Plot error, does {} contain data?".format(to_plot[i]))

        # post try/except mods 
        fig.suptitle('Flex data versus time for the wells {}'.format(', '.join(to_plot)), y = 1.05, size = '20')
        
        plt.show()
def visualise_assay(self, share_y, export=False, title='', cmap='Dark2_r', colorby='Type', labelby='Type', dpi=200)

Returns color-coded and labelled plots of the data collected for each well of the well plate.

:param share_y: 'True' sets y axis the same for all plots :type share_y: bool :param export: If 'True' a .png file of the figure is saved, default = False :type export: bool :param title: Sets the title of the figure, optional :type title: str :param cmap: Sets the colormap for the color-coding, default = 'Dark2_r' :type cmap: str :param colorby: Chooses the parameter to color code by, for example 'Type', 'Contents', 'Concentration', 'Compound', 'Protein', 'Concentration Units', default = 'Type' :type colorby: str :param labelby: Chooses the parameter to label code by, for example 'Type', 'Contents', 'Concentration', 'Compound', 'Protein', 'Concentration Units', default = 'Type' :type labelby: str :param dpi: Size of the figure, default = 200 :type dpi: int :return: Figure of plotted data for each well of the well plate described in plate_map_file :rtype: figure

Expand source code
def visualise_assay(self, share_y, export = False, title = "", cmap = 'Dark2_r',
         colorby = 'Type', labelby = 'Type', dpi = 200):
    """Returns color-coded and labelled plots of the data collected for each well of the well plate.
    
    :param share_y: 'True' sets y axis the same for all plots
    :type share_y: bool
    :param export: If 'True' a .png file of the figure is saved, default = False
    :type export: bool
    :param title: Sets the title of the figure, optional
    :type title: str
    :param cmap: Sets the colormap for the color-coding, default = 'Dark2_r'
    :type cmap: str
    :param colorby: Chooses the parameter to color code by, for example 'Type', 'Contents', 'Concentration', 'Compound', 'Protein', 'Concentration Units', default = 'Type'
    :type colorby: str
    :param labelby: Chooses the parameter to label code by, for example 'Type', 'Contents', 'Concentration', 'Compound', 'Protein', 'Concentration Units', default = 'Type'
    :type labelby: str
    :param dpi: Size of the figure, default = 200
    :type dpi: int
    :return: Figure of plotted data for each well of the well plate described in plate_map_file
    :rtype: figure
    """
    self.title = title
    
    # check labelby and colorby
    if colorby not in self.plate_map.columns:
        raise pm.HeaderError("colorby parameter not in plate map")
    if labelby not in self.plate_map.columns:
        raise pm.HeaderError("labelby parameter not in plate map")
    
    pm.visualise_all_series(x = self.processed_data['ratio']['time'], y = self.processed_data['ratio']['data'], 
                        share_y = share_y, platemap = self.plate_map, size = self.size, 
                        export = export, cmap = cmap,
                        colorby = colorby, labelby = labelby, 
                        dpi = dpi, title = self.title)
    
    plt.suptitle(self.title, y = 0.95)
class CompoundNameError (...)

Common base class for all non-exit exceptions.

Expand source code
class CompoundNameError(pm.PlateMapError):
    pass

Ancestors

  • platemapping.plate_map.PlateMapError
  • platemapping.plate_map.Error
  • builtins.Exception
  • builtins.BaseException
class ControlError (...)

Common base class for all non-exit exceptions.

Expand source code
class ControlError(pm.PlateMapError):
    pass

Ancestors

  • platemapping.plate_map.PlateMapError
  • platemapping.plate_map.Error
  • builtins.Exception
  • builtins.BaseException
class DataError (...)

Common base class for all non-exit exceptions.

Expand source code
class DataError(Error):
    pass

Ancestors

  • Error
  • builtins.Exception
  • builtins.BaseException

Subclasses

class DataReadInError (...)

Common base class for all non-exit exceptions.

Expand source code
class DataReadInError(DataError):
    pass

Ancestors

class Error (...)

Common base class for all non-exit exceptions.

Expand source code
class Error(Exception):
    pass

Ancestors

  • builtins.Exception
  • builtins.BaseException

Subclasses

class ProteinNameError (...)

Common base class for all non-exit exceptions.

Expand source code
class ProteinNameError(pm.PlateMapError):
    pass

Ancestors

  • platemapping.plate_map.PlateMapError
  • platemapping.plate_map.Error
  • builtins.Exception
  • builtins.BaseException
class UnitsError (...)

Common base class for all non-exit exceptions.

Expand source code
class UnitsError(pm.PlateMapError):
    pass

Ancestors

  • platemapping.plate_map.PlateMapError
  • platemapping.plate_map.Error
  • builtins.Exception
  • builtins.BaseException