1+
2+ import pandas as pd
3+ import numpy as np
4+
5+
6+
7+
8+
9+
10+
11+
12+
13+ def add_slope_to_df_sess (df_sess , df_slope , slope_col_name , channel_name ,
14+ session_date_col = 'session_date' , channel_col = 'channel' ,
15+ new_col_name = None ):
16+ """
17+ Add a slope column from df_slope into df_sess by matching session_date.
18+ - df_sess: sessions dataframe
19+ - df_slope: slopes dataframe (must contain channel_col and session_date_col)
20+ - slope_col_name: name of the slope column in df_slope to pull (e.g. 'slope (RPE >= 0)')
21+ - channel_name: channel string to filter df_slope by
22+ - new_col_name: optional name for the added column in df_sess (defaults to slope_col_name)
23+ Returns a new dataframe (does not modify inputs).
24+ """
25+ if new_col_name is None :
26+ new_col_name = channel_name .split ('dff' )[0 ] + slope_col_name
27+
28+ df_slope = df_slope .rename (columns = {'date' : session_date_col })
29+ # filter slope table to the requested channel and keep only date + slope column
30+ slope_filtered = (df_slope
31+ .loc [df_slope [channel_col ] == channel_name , [session_date_col , slope_col_name ]]
32+ .copy ())
33+
34+ # if there are duplicate session_date rows for the same channel take the mean (simple resolution)
35+ slope_filtered = slope_filtered .groupby (session_date_col , as_index = False ).mean ()
36+
37+ # merge into sessions on session date
38+ merged = df_sess .merge (slope_filtered , on = session_date_col , how = 'left' )
39+
40+ # if original slope column name collides with other columns, rename to new_col_name
41+ if slope_col_name != new_col_name and slope_col_name in merged .columns :
42+ merged = merged .rename (columns = {slope_col_name : new_col_name })
43+
44+ return merged
45+
46+ def add_all_slopes_to_df_sess (df_sess , df_slope , slope_type ,
47+ session_date_col = 'session_date' ,
48+ channel_col = 'channel' ):
49+
50+
51+ df_sess_slope = df_sess .copy ()
52+
53+ if isinstance (slope_type , str ):
54+ slope_type = [slope_type ]
55+ slope_cols = []
56+ slope_col_names = {'slope (RPE >= 0)' :'slope_pos' , 'slope (RPE < 0)' :'slope_neg' }
57+ if 'pos' in slope_type or 'positive' in slope_type or 'slope (RPE >= 0)' in slope_type :
58+ slope_cols .append ('slope (RPE >= 0)' )
59+ if 'neg' in slope_type or 'negative' in slope_type or 'slope (RPE < 0)' in slope_type :
60+ slope_cols .append ('slope (RPE < 0)' )
61+ if 'both' in slope_type or 'all' in slope_type :
62+ slope_cols .append (['slope (RPE >= 0)' , 'slope (RPE < 0)' ])
63+
64+ for channel in df_slope [channel_col ].unique ():
65+ for slope_col in slope_cols :
66+ df_sess_slope = add_slope_to_df_sess (df_sess_slope , df_slope , slope_col , channel ,
67+ session_date_col = session_date_col ,
68+ channel_col = channel_col ,
69+ new_col_name = f'{ channel .split ("dff" )[0 ]} { slope_col_names [slope_col ]} ' )
70+ return df_sess_slope
71+
72+ def enrich_df_sess_from_nwbs (nwb_list , df_sess , extractor_func , new_col_name ):
73+ """
74+ Call extractor_func for each nwb in nwb_list and merge results into df_sess.
75+ extractor_func may return:
76+ - (session_date, value)
77+ - dict {session_date: value}
78+ - pandas.Series indexed by session_date
79+ - scalar (in which case the function will try to obtain session_date from the nwb)
80+ The merged column will be named new_col_name.
81+ Returns a new dataframe (does not modify inputs).
82+ """
83+ rows = []
84+ for nwb in nwb_list :
85+ res = extractor_func (nwb )
86+ session_date = nwb .session_id .split ('_' )[1 ]
87+ rows .append ({'session_date' : str (session_date ) if session_date is not None else None , new_col_name : res })
88+
89+ df_new = pd .DataFrame (rows ).dropna (subset = ['session_date' ])
90+
91+ # align column name for merge
92+ merged = df_sess .merge (df_new , on = 'session_date' , how = 'left' )
93+ return merged
94+
95+ # a bunch of extractor functions
96+
97+
98+ def get_max_side_bias (nwb ):
99+
100+ return nwb .df_trials ['side_bias' ].max ()
101+
102+ def get_mean_side_bias (nwb ):
103+
104+ return nwb .df_trials ['side_bias' ].mean ()
105+
106+ def get_baited_rate (nwb ):
107+ df_trials = nwb .df_trials
108+ mask = ((df_trials ['bait_left' ] == True ) & (df_trials ['choice' ] == 0.0 )) | ((df_trials ['bait_right' ] == True ) & (df_trials ['choice' ] == 1.0 ))
109+ return float (mask .sum ()) / float (len (df_trials ))
110+
111+ def get_left_choice_rate (nwb ):
112+ df_trials = nwb .df_trials
113+ left_count = (df_trials ['choice' ] == 0.0 ).sum ()
114+ return float (left_count ) / float (len (df_trials ))
115+
116+ def get_ignore_choice_rate (nwb ):
117+ df_trials = nwb .df_trials
118+ ignore_count = (df_trials ['choice' ] == 2.0 ).sum ()
119+ return float (ignore_count ) / float (len (df_trials ))
120+
121+ def get_left_right_diff (nwb ):
122+ df = getattr (nwb , 'df_trials' , None )
123+ if df is None or len (df ) == 0 :
124+ return float ('nan' )
125+ left = (df ['choice' ] == 0.0 ).sum ()
126+ right = (df ['choice' ] == 1.0 ).sum ()
127+ return float (left - right ) / float (len (df )) # signed difference normalized by total trials
128+
129+ def get_left_right_abs_diff (nwb ):
130+ v = get_left_right_diff (nwb )
131+ return abs (v ) if not np .isnan (v ) else v # magnitude of bias (0..1)
132+
133+ def enrich_df_sess_with_all_getters (nwb_list , df_sess ):
134+ """
135+ Enrich df_sess by calling enrich_df_sess_from_nwbs for each get_* extractor
136+ defined in this module. Returns a new dataframe with added columns:
137+
138+ - 'max_side_bias'
139+ - 'mean_side_bias'
140+ - 'baited_rate'
141+ - 'left_choice_rate'
142+ - 'ignore_choice_rate'
143+ - 'left_right_diff'
144+ - 'left_right_abs_diff'
145+
146+ Parameters:
147+ - nwb_list: iterable of nwb objects
148+ - df_sess: sessions dataframe
149+
150+ This function does not modify the inputs; it returns an enriched copy.
151+ """
152+ enrichments = [
153+ ('max_side_bias' , get_max_side_bias ),
154+ ('mean_side_bias' , get_mean_side_bias ),
155+ ('baited_rate' , get_baited_rate ),
156+ ('left_choice_rate' , get_left_choice_rate ),
157+ ('ignore_choice_rate' , get_ignore_choice_rate ),
158+ ('left_right_diff' , get_left_right_diff ),
159+ ('left_right_abs_diff' , get_left_right_abs_diff ),
160+ ]
161+
162+ df_out = df_sess .copy ()
163+ for col_name , extractor in enrichments :
164+ df_out = enrich_df_sess_from_nwbs (nwb_list , df_out , extractor , col_name )
165+
166+ return df_out
0 commit comments