circaPy 0.1.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- circaPy/.idea/actigraphy_analysis.iml +11 -0
- circaPy/.idea/misc.xml +4 -0
- circaPy/.idea/modules.xml +8 -0
- circaPy/.idea/vcs.xml +6 -0
- circaPy/.idea/workspace.xml +95 -0
- circaPy/__init__.py +0 -0
- circaPy/activity.py +391 -0
- circaPy/episodes.py +505 -0
- circaPy/periodogram.py +101 -0
- circaPy/plots.py +351 -0
- circaPy/preprocessing.py +261 -0
- circaPy/sleep_process.py +96 -0
- circapy-0.1.5.dist-info/METADATA +104 -0
- circapy-0.1.5.dist-info/RECORD +16 -0
- circapy-0.1.5.dist-info/WHEEL +4 -0
- circapy-0.1.5.dist-info/licenses/LICENSE +674 -0
circaPy/plots.py
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import pdb
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import numpy as np
|
|
5
|
+
import matplotlib.pyplot as plt
|
|
6
|
+
import matplotlib.gridspec as gs
|
|
7
|
+
from matplotlib.transforms import Bbox
|
|
8
|
+
import circaPy.activity as act
|
|
9
|
+
import circaPy.preprocessing as prep
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@prep.validate_input
|
|
13
|
+
@prep.invert_light_values
|
|
14
|
+
@prep.plot_kwarg_decorator
|
|
15
|
+
def plot_actogram(data,
|
|
16
|
+
subject_no=0,
|
|
17
|
+
light_col=-1,
|
|
18
|
+
ylim=[0, 120],
|
|
19
|
+
fig=False,
|
|
20
|
+
subplot=False,
|
|
21
|
+
ldralpha=0.5,
|
|
22
|
+
start_day=0,
|
|
23
|
+
day_label_size=5,
|
|
24
|
+
linewidth=0.5,
|
|
25
|
+
**kwargs):
|
|
26
|
+
"""
|
|
27
|
+
Plot an double plotted actogram of activity data over several days
|
|
28
|
+
with background shading set by the lights
|
|
29
|
+
|
|
30
|
+
Parameters:
|
|
31
|
+
----------
|
|
32
|
+
data : (pd.DataFrame)
|
|
33
|
+
time-indexed pandas dataframe with activity values in
|
|
34
|
+
columns for each subject and one column for the light levels.
|
|
35
|
+
WRONG - currently expecting list of dataframes, one for each animal
|
|
36
|
+
and single column for each day
|
|
37
|
+
subject_no : int
|
|
38
|
+
which column number to plot, defaults to 0
|
|
39
|
+
light_col : int
|
|
40
|
+
which columns contains light information, defaults to -1
|
|
41
|
+
ylim : list of two ints
|
|
42
|
+
set the minimum and maximum values to plot
|
|
43
|
+
fig : matplotlib figure object
|
|
44
|
+
Figure to create plot on, if not passed defaults to false and
|
|
45
|
+
new figure is passed
|
|
46
|
+
subplot : matplotlib subplotspec object
|
|
47
|
+
Subplotspec from larger figure on which to draw actogram.
|
|
48
|
+
Must be created from gridspec
|
|
49
|
+
If not passed
|
|
50
|
+
defaults to False, which requires a fig object to be provided
|
|
51
|
+
ldralpha : float
|
|
52
|
+
Set the alpha level for how opaque to have the light shading,
|
|
53
|
+
defaults to 0.5
|
|
54
|
+
startday : int
|
|
55
|
+
sets which day to start as day 0 in plot, defaults to 0
|
|
56
|
+
day_label_size : int
|
|
57
|
+
sets size of labels on bottom x axis, defaults to 5
|
|
58
|
+
|
|
59
|
+
Returns
|
|
60
|
+
-------
|
|
61
|
+
matplotlib.pyplot.figure
|
|
62
|
+
instance containing overall figure
|
|
63
|
+
matplotlib.pyplot.subplot
|
|
64
|
+
the final subplot so can manipulate for xaxis
|
|
65
|
+
dict
|
|
66
|
+
dict containing plotting kwargs
|
|
67
|
+
"""
|
|
68
|
+
# grab line plot constant
|
|
69
|
+
if "linewidth" in kwargs:
|
|
70
|
+
linewidth = kwargs["linewidth"]
|
|
71
|
+
|
|
72
|
+
# check if data is empty
|
|
73
|
+
if data.empty:
|
|
74
|
+
raise ValueError("Input Dataframe is empty. Cannot plot actogram")
|
|
75
|
+
|
|
76
|
+
# select the correct data to plot for activity and light
|
|
77
|
+
col_data = data.columns[subject_no]
|
|
78
|
+
ldr_col = data.columns[light_col]
|
|
79
|
+
data_plot = data.loc[:, col_data].copy()
|
|
80
|
+
data_light = data.loc[:, ldr_col].copy()
|
|
81
|
+
|
|
82
|
+
# add entire day of 0s at start and end by extending index
|
|
83
|
+
# grab values from current index
|
|
84
|
+
freq = pd.infer_freq(data_plot.index)
|
|
85
|
+
start = data_plot.index.min()
|
|
86
|
+
end = data_plot.index.max()
|
|
87
|
+
|
|
88
|
+
# check frequency works
|
|
89
|
+
try:
|
|
90
|
+
pd.Timedelta(freq)
|
|
91
|
+
except BaseException:
|
|
92
|
+
freq = pd.Timedelta(f"1{freq}")
|
|
93
|
+
|
|
94
|
+
# Extend the range by 1 day but make sure lines up with original index
|
|
95
|
+
# select the length of one day
|
|
96
|
+
day_length = len(data_plot.loc[str(data_plot.index[0].date())])
|
|
97
|
+
extended_start = start - (pd.Timedelta(freq) * day_length)
|
|
98
|
+
extended_end = end + (pd.Timedelta(freq) * day_length)
|
|
99
|
+
|
|
100
|
+
# create new index and set data to it
|
|
101
|
+
extended_index = pd.date_range(
|
|
102
|
+
start=extended_start, end=extended_end, freq=freq)
|
|
103
|
+
data_plot = data_plot.reindex(extended_index, fill_value=-100)
|
|
104
|
+
|
|
105
|
+
# select just the days
|
|
106
|
+
days = data_plot.index.normalize().unique()
|
|
107
|
+
|
|
108
|
+
# set all 0 values to be very low so not showing on y index starting at 0
|
|
109
|
+
for mask in data_plot, data_light:
|
|
110
|
+
mask[mask == 0] = -100
|
|
111
|
+
|
|
112
|
+
# Create figure and subplot for every day
|
|
113
|
+
# create a new figure if not passed one when called
|
|
114
|
+
if not fig:
|
|
115
|
+
fig, ax = plt.subplots(nrows=(len(days) - 1))
|
|
116
|
+
fig.subplots_adjust(hspace=0)
|
|
117
|
+
|
|
118
|
+
# add subplots to figure if passed when called
|
|
119
|
+
else:
|
|
120
|
+
# remove ticks so don't draw over when we add later
|
|
121
|
+
subplot.set(yticks=[], xticks=[])
|
|
122
|
+
|
|
123
|
+
# draw subplots for each day on the subplot given
|
|
124
|
+
subplot_spec = subplot.get_subplotspec()
|
|
125
|
+
subplot_grid = gs.GridSpecFromSubplotSpec(nrows=(len(days) - 1),
|
|
126
|
+
ncols=1,
|
|
127
|
+
subplot_spec=subplot_spec,
|
|
128
|
+
wspace=0,
|
|
129
|
+
hspace=0)
|
|
130
|
+
ax = []
|
|
131
|
+
for grid in subplot_grid:
|
|
132
|
+
sub_ax = plt.Subplot(fig, grid)
|
|
133
|
+
fig.add_subplot(sub_ax)
|
|
134
|
+
ax.append(sub_ax)
|
|
135
|
+
|
|
136
|
+
# select each day to then plot on separate axis
|
|
137
|
+
# plot two days on each row
|
|
138
|
+
for day_label, axis in zip(days, ax):
|
|
139
|
+
# get two days of data to plot
|
|
140
|
+
curr_day = str(day_label.date())
|
|
141
|
+
next_day = str(day_label.date() + pd.Timedelta("1d"))
|
|
142
|
+
curr_data = data_plot.loc[curr_day:next_day]
|
|
143
|
+
curr_data_light = data_light.loc[curr_day:next_day]
|
|
144
|
+
|
|
145
|
+
# create masked data for fill between to avoid horizontal lines
|
|
146
|
+
fill_data = curr_data.where(curr_data > 0)
|
|
147
|
+
fill_ldr = curr_data_light.where(curr_data_light > 0)
|
|
148
|
+
|
|
149
|
+
# plot the data and light_col
|
|
150
|
+
axis.fill_between(fill_ldr.index,
|
|
151
|
+
fill_ldr,
|
|
152
|
+
alpha=ldralpha,
|
|
153
|
+
facecolor="grey")
|
|
154
|
+
axis.plot(curr_data, linewidth=linewidth)
|
|
155
|
+
axis.fill_between(fill_data.index,
|
|
156
|
+
fill_data)
|
|
157
|
+
|
|
158
|
+
# need to hide all the axis to make visible
|
|
159
|
+
axis.set(xticks=[],
|
|
160
|
+
xlim=[curr_data.index[0],
|
|
161
|
+
curr_data.index[-1]],
|
|
162
|
+
yticks=[],
|
|
163
|
+
ylim=ylim)
|
|
164
|
+
spines = ["left", "right", "top", "bottom"]
|
|
165
|
+
for pos in spines:
|
|
166
|
+
axis.spines[pos].set_visible(False)
|
|
167
|
+
|
|
168
|
+
# create the y labels for every 10th row
|
|
169
|
+
day_markers = np.arange(0, len(days), 10)
|
|
170
|
+
day_markers = day_markers + start_day
|
|
171
|
+
for axis, day in zip(ax[::10], day_markers):
|
|
172
|
+
axis.set_ylabel(day,
|
|
173
|
+
rotation=0,
|
|
174
|
+
va='center',
|
|
175
|
+
ha='right',
|
|
176
|
+
fontsize=day_label_size)
|
|
177
|
+
|
|
178
|
+
# create defaults dict
|
|
179
|
+
params_dict = {
|
|
180
|
+
"xlabel": "Time",
|
|
181
|
+
"ylabel": "Days",
|
|
182
|
+
"interval": 6,
|
|
183
|
+
"title": "Double Plotted Actogram",
|
|
184
|
+
"timeaxis": True,
|
|
185
|
+
"subplot": subplot
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
# put axis as a controllable parameter
|
|
189
|
+
if "timeaxis" in kwargs:
|
|
190
|
+
params_dict['timeaxis'] = kwargs["timeaxis"]
|
|
191
|
+
|
|
192
|
+
return fig, ax, params_dict
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@prep.validate_input
|
|
196
|
+
@prep.invert_light_values
|
|
197
|
+
@prep.plot_kwarg_decorator
|
|
198
|
+
def plot_activity_profile(data,
|
|
199
|
+
col=0,
|
|
200
|
+
light_col=-1,
|
|
201
|
+
subplot=None,
|
|
202
|
+
resample=False,
|
|
203
|
+
resample_freq="h",
|
|
204
|
+
*args,
|
|
205
|
+
**kwargs):
|
|
206
|
+
"""
|
|
207
|
+
Plot the activity profile with mean and SEM (Standard Error of the Mean).
|
|
208
|
+
Optionally resample the data before plotting.
|
|
209
|
+
|
|
210
|
+
Parameters
|
|
211
|
+
----------
|
|
212
|
+
data : pd.DataFrame or pd.Series
|
|
213
|
+
Activity data indexed by time. If `data` is a DataFrame, the
|
|
214
|
+
function uses the column specified by `col` (default is the
|
|
215
|
+
first column).
|
|
216
|
+
col : int, optional
|
|
217
|
+
The index of the column to plot, used when `data` is a
|
|
218
|
+
DataFrame (default is 0).
|
|
219
|
+
subplot : matplotlib.axes._axes.Axes, optional
|
|
220
|
+
Subplot to plot on. If None, a new figure and axis are
|
|
221
|
+
created (default is None).
|
|
222
|
+
resample : bool, optional
|
|
223
|
+
Whether to resample the data before plotting.
|
|
224
|
+
If `True`, the data will be resampled to the frequency
|
|
225
|
+
specified by `resample_freq` (default is `False`).
|
|
226
|
+
resample_freq : str, optional
|
|
227
|
+
The frequency to resample the data to.
|
|
228
|
+
This can be any valid pandas offset string
|
|
229
|
+
(e.g., "h" for hourly, "min" for minutely).
|
|
230
|
+
The default is "h" (hourly).
|
|
231
|
+
*args, **kwargs : additional arguments
|
|
232
|
+
These are passed to the plotting function,
|
|
233
|
+
such as `timeaxis` to control the appearance of the x-axis.
|
|
234
|
+
|
|
235
|
+
Returns
|
|
236
|
+
-------
|
|
237
|
+
fig : matplotlib.figure.Figure
|
|
238
|
+
The figure containing the plot.
|
|
239
|
+
ax : matplotlib.axes._axes.Axes
|
|
240
|
+
The axis with the plot.
|
|
241
|
+
params_dict : dict
|
|
242
|
+
A dictionary containing the plot's parameters,
|
|
243
|
+
including labels, title, and xlim.
|
|
244
|
+
"""
|
|
245
|
+
# ability to resample if required
|
|
246
|
+
if resample:
|
|
247
|
+
data = data.resample(resample_freq).mean()
|
|
248
|
+
|
|
249
|
+
# select just the subject
|
|
250
|
+
curr_data = data.iloc[:, col]
|
|
251
|
+
light_data = data.iloc[:, light_col]
|
|
252
|
+
|
|
253
|
+
# Calculate mean activity and SEM
|
|
254
|
+
mean, sem = act.calculate_mean_activity(curr_data, sem=True)
|
|
255
|
+
light_mean = act.calculate_mean_activity(light_data)
|
|
256
|
+
|
|
257
|
+
# Convert the index of mean and sem to a DatetimeIndex starting 2001-01-01
|
|
258
|
+
start_date = "2001-01-01"
|
|
259
|
+
freq = pd.infer_freq(data.index)
|
|
260
|
+
datetime_index = pd.date_range(
|
|
261
|
+
start=start_date, periods=len(mean), freq=freq)
|
|
262
|
+
mean.index = datetime_index
|
|
263
|
+
sem.index = datetime_index
|
|
264
|
+
light_mean.index = datetime_index
|
|
265
|
+
|
|
266
|
+
# Ensure freq has a numeric component
|
|
267
|
+
if not any(char.isdigit() for char in freq):
|
|
268
|
+
freq = pd.Timedelta('1' + freq) # Prepend '1' if missing
|
|
269
|
+
# Extend the light_mean data by one extra period and forward fill
|
|
270
|
+
light_mean = pd.concat([light_mean, pd.Series(
|
|
271
|
+
[light_mean.iloc[-1]], index=[light_mean.index[-1] + pd.Timedelta(freq)])])
|
|
272
|
+
light_mean.ffill(inplace=True)
|
|
273
|
+
|
|
274
|
+
# Offset the mean and sem data to plot in the middle of the hour
|
|
275
|
+
offset_time = 0.5 * pd.Timedelta(freq)
|
|
276
|
+
mean.index += offset_time
|
|
277
|
+
sem.index += offset_time
|
|
278
|
+
light_mean.index += offset_time
|
|
279
|
+
|
|
280
|
+
# Create plot if no subplot is provided
|
|
281
|
+
if subplot is None:
|
|
282
|
+
fig, ax = plt.subplots(figsize=(10, 6))
|
|
283
|
+
else:
|
|
284
|
+
fig = plt.gcf()
|
|
285
|
+
ax = subplot
|
|
286
|
+
|
|
287
|
+
# Plot the mean line
|
|
288
|
+
ax.plot(
|
|
289
|
+
mean.index, mean, label="Mean Activity", color="blue", linewidth=2)
|
|
290
|
+
|
|
291
|
+
# Add shaded SEM region
|
|
292
|
+
ax.fill_between(
|
|
293
|
+
mean.index,
|
|
294
|
+
mean - sem,
|
|
295
|
+
mean + sem,
|
|
296
|
+
color="blue",
|
|
297
|
+
alpha=0.3,
|
|
298
|
+
label="± SEM"
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
# get ylims to set at this level later
|
|
302
|
+
ylim = ax.get_ylim()
|
|
303
|
+
|
|
304
|
+
# Find the min and max of light_mean
|
|
305
|
+
min_light_mean = light_mean.min()
|
|
306
|
+
max_light_mean = light_mean.max()
|
|
307
|
+
|
|
308
|
+
# Define the target range
|
|
309
|
+
target_max = 1000 * ylim[1]
|
|
310
|
+
target_min = -1 * target_max
|
|
311
|
+
|
|
312
|
+
# Scale the light_mean values to the target range
|
|
313
|
+
# The formula to scale the values is:
|
|
314
|
+
# scaled_value = (value - min_value) / (max_value - min_value)
|
|
315
|
+
# * (target_max - target_min) + target_min
|
|
316
|
+
|
|
317
|
+
scaled_light_mean = (light_mean - min_light_mean
|
|
318
|
+
) / (max_light_mean - min_light_mean
|
|
319
|
+
) * (target_max - target_min) + target_min
|
|
320
|
+
|
|
321
|
+
# Add lights region
|
|
322
|
+
ax.fill_between(
|
|
323
|
+
scaled_light_mean.index,
|
|
324
|
+
scaled_light_mean,
|
|
325
|
+
color='grey',
|
|
326
|
+
alpha=0.2
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
# Add labels, legend, and title
|
|
330
|
+
ax.set_xlabel("Time")
|
|
331
|
+
ax.set_ylabel("Activity")
|
|
332
|
+
ax.set_ylim([0, ylim[1]])
|
|
333
|
+
ax.set_title("Activity Profile with Mean and SEM")
|
|
334
|
+
ax.legend()
|
|
335
|
+
|
|
336
|
+
# create defaults dict
|
|
337
|
+
xlim = [mean.index[0], (mean.index[0] + pd.Timedelta("24h"))]
|
|
338
|
+
params_dict = {
|
|
339
|
+
"xlabel": "Time",
|
|
340
|
+
"ylabel": "Activity",
|
|
341
|
+
"interval": 6,
|
|
342
|
+
"title": "Mean activity profile",
|
|
343
|
+
"timeaxis": True,
|
|
344
|
+
"xlim": xlim,
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
# put axis as a controllable parameter
|
|
348
|
+
if "timeaxis" in kwargs:
|
|
349
|
+
params_dict['timeaxis'] = kwargs["timeaxis"]
|
|
350
|
+
|
|
351
|
+
return fig, ax, params_dict
|
circaPy/preprocessing.py
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import pdb
|
|
2
|
+
from functools import wraps
|
|
3
|
+
import pingouin as pg
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import numpy as np
|
|
6
|
+
import matplotlib.pyplot as plt
|
|
7
|
+
import matplotlib.dates as mdates
|
|
8
|
+
idx = pd.IndexSlice
|
|
9
|
+
# This script contains functions which are useful for preprocessing of
|
|
10
|
+
# actigraphy data
|
|
11
|
+
|
|
12
|
+
#### Decorators ####
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def plot_kwarg_decorator(func):
|
|
16
|
+
"""
|
|
17
|
+
Universal decorator for plot formatting and configuration.
|
|
18
|
+
Handles xlabels, ylabels, titles, legends, time formatting, saving,
|
|
19
|
+
and showing plots.
|
|
20
|
+
:param func: The plotting function to decorate.
|
|
21
|
+
:return: A decorated function that applies plot configurations.
|
|
22
|
+
"""
|
|
23
|
+
@wraps(func)
|
|
24
|
+
def wrapper(data, *args, **kwargs):
|
|
25
|
+
# Call the original plotting function
|
|
26
|
+
fig, ax, params_dict = func(data, *args, **kwargs)
|
|
27
|
+
|
|
28
|
+
# check if multiple subplots or not
|
|
29
|
+
if isinstance(ax, (np.ndarray, list)):
|
|
30
|
+
final_ax = ax[-1]
|
|
31
|
+
else:
|
|
32
|
+
final_ax = ax
|
|
33
|
+
|
|
34
|
+
# Configure x-axis limits
|
|
35
|
+
if "xlim" in kwargs or "xlim" in params_dict:
|
|
36
|
+
xlim = kwargs.get("xlim", params_dict.get("xlim", None))
|
|
37
|
+
if xlim:
|
|
38
|
+
final_ax.set_xlim(xlim)
|
|
39
|
+
|
|
40
|
+
# Configure x-axis time formatting
|
|
41
|
+
if "timeaxis" in params_dict and params_dict["timeaxis"]:
|
|
42
|
+
xfmt = kwargs.get("xfmt", mdates.DateFormatter("%H:%M"))
|
|
43
|
+
final_ax.xaxis.set_major_formatter(xfmt)
|
|
44
|
+
interval = kwargs.get("interval", params_dict.get("interval", 1))
|
|
45
|
+
final_ax.xaxis.set_major_locator(
|
|
46
|
+
mdates.HourLocator(interval=interval))
|
|
47
|
+
fig.autofmt_xdate()
|
|
48
|
+
|
|
49
|
+
# Set x-axis label
|
|
50
|
+
xlabel = kwargs.get("xlabel", params_dict.get("xlabel", ""))
|
|
51
|
+
if xlabel:
|
|
52
|
+
final_ax.set_xlabel(
|
|
53
|
+
xlabel,
|
|
54
|
+
labelpad=5,
|
|
55
|
+
ha='center',
|
|
56
|
+
va='center')
|
|
57
|
+
|
|
58
|
+
# Set y-axis label
|
|
59
|
+
ylabel = kwargs.get("ylabel", params_dict.get("ylabel", ""))
|
|
60
|
+
ylabelpos = kwargs.get("ylabelpos", (0.02, 0.5))
|
|
61
|
+
subplot = kwargs.get("subplot", params_dict.get("subplot", None))
|
|
62
|
+
if ylabel:
|
|
63
|
+
if subplot:
|
|
64
|
+
subplot.set_ylabel(
|
|
65
|
+
ylabel,
|
|
66
|
+
labelpad=5,
|
|
67
|
+
ha='center',
|
|
68
|
+
va='center',
|
|
69
|
+
rotation='vertical')
|
|
70
|
+
else:
|
|
71
|
+
fig.text(
|
|
72
|
+
ylabelpos[0],
|
|
73
|
+
ylabelpos[1],
|
|
74
|
+
ylabel,
|
|
75
|
+
ha="center",
|
|
76
|
+
va="center",
|
|
77
|
+
rotation="vertical"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Set plot title
|
|
81
|
+
title = kwargs.get("title", params_dict.get("title", ""))
|
|
82
|
+
if title:
|
|
83
|
+
fig.suptitle(title)
|
|
84
|
+
|
|
85
|
+
# Configure legend
|
|
86
|
+
if kwargs.get("legend", False):
|
|
87
|
+
legend_loc = kwargs.get("legend_loc", 1)
|
|
88
|
+
handles, labels = final_ax.get_legend_handles_labels()
|
|
89
|
+
fig.legend(handles, labels, loc=legend_loc)
|
|
90
|
+
|
|
91
|
+
# Configure figure size
|
|
92
|
+
if "figsize" in kwargs:
|
|
93
|
+
fig.set_size_inches(kwargs["figsize"])
|
|
94
|
+
|
|
95
|
+
# Save or show the plot
|
|
96
|
+
if kwargs.get("savefig", False):
|
|
97
|
+
fname = kwargs.get("fname", "plot.png")
|
|
98
|
+
plt.savefig(fname)
|
|
99
|
+
plt.close()
|
|
100
|
+
if kwargs.get("showfig", False):
|
|
101
|
+
plt.show()
|
|
102
|
+
|
|
103
|
+
return fig, ax, params_dict
|
|
104
|
+
|
|
105
|
+
return wrapper
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def validate_input(func):
|
|
109
|
+
"""
|
|
110
|
+
Decorator to validate DataFrames or Series passed to the function.
|
|
111
|
+
- Checks if any input consists only of zeros.
|
|
112
|
+
- Checks if any DataFrame is empty.
|
|
113
|
+
- Checks if the index of any DataFrame is a DatetimeIndex.
|
|
114
|
+
Raises a ValueError if any condition is not met.
|
|
115
|
+
"""
|
|
116
|
+
@wraps(func)
|
|
117
|
+
def wrapper(*args, **kwargs):
|
|
118
|
+
# Helper function to validate a DataFrame or Series
|
|
119
|
+
def _validate(input_data, name):
|
|
120
|
+
if isinstance(
|
|
121
|
+
input_data,
|
|
122
|
+
pd.DataFrame) or isinstance(
|
|
123
|
+
input_data,
|
|
124
|
+
pd.Series):
|
|
125
|
+
# Check if consists only of zeros
|
|
126
|
+
if (input_data.values == 0).all():
|
|
127
|
+
raise ValueError(f"Input {name} consists only of zeros.")
|
|
128
|
+
|
|
129
|
+
# Check if empty
|
|
130
|
+
if input_data.empty:
|
|
131
|
+
raise ValueError(f"Input {name} is empty.")
|
|
132
|
+
|
|
133
|
+
# Check if index is a DatetimeIndex (only for DataFrames)
|
|
134
|
+
if isinstance(
|
|
135
|
+
input_data,
|
|
136
|
+
pd.DataFrame) and not isinstance(
|
|
137
|
+
input_data.index,
|
|
138
|
+
pd.DatetimeIndex):
|
|
139
|
+
raise TypeError(
|
|
140
|
+
f"Input {name} does not have a DatetimeIndex.")
|
|
141
|
+
|
|
142
|
+
# Validate positional arguments
|
|
143
|
+
for i, arg in enumerate(args):
|
|
144
|
+
_validate(arg, f"arg[{i}]")
|
|
145
|
+
|
|
146
|
+
# Validate keyword arguments
|
|
147
|
+
for key, value in kwargs.items():
|
|
148
|
+
_validate(value, f"kwarg[{key}]")
|
|
149
|
+
|
|
150
|
+
# Call the original function
|
|
151
|
+
return func(*args, **kwargs)
|
|
152
|
+
|
|
153
|
+
return wrapper
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def invert_light_values(func):
|
|
157
|
+
"""
|
|
158
|
+
Decorator to invert the light values in the given light column.
|
|
159
|
+
Used to ensure that on plots, darkness is shaded grey, not the lights.
|
|
160
|
+
|
|
161
|
+
Parameters
|
|
162
|
+
----------
|
|
163
|
+
func : function
|
|
164
|
+
The function to wrap.
|
|
165
|
+
|
|
166
|
+
Returns
|
|
167
|
+
-------
|
|
168
|
+
function
|
|
169
|
+
The wrapped function with inverted light values in the specified column.
|
|
170
|
+
"""
|
|
171
|
+
@wraps(func)
|
|
172
|
+
def wrapper(data, *args, light_col=-1, **kwargs):
|
|
173
|
+
# Ensure light_col is a valid index
|
|
174
|
+
if isinstance(light_col, int): # If specified as column index
|
|
175
|
+
light_col_name = data.columns[light_col]
|
|
176
|
+
elif isinstance(light_col, str): # If specified as column name
|
|
177
|
+
light_col_name = light_col
|
|
178
|
+
else:
|
|
179
|
+
raise ValueError(
|
|
180
|
+
"light_col must be an integer index or a column name")
|
|
181
|
+
|
|
182
|
+
# Copy the data to avoid modifying the original DataFrame
|
|
183
|
+
data = data.copy()
|
|
184
|
+
|
|
185
|
+
# Invert the light values
|
|
186
|
+
max_value = data[light_col_name].max()
|
|
187
|
+
min_value = data[light_col_name].min()
|
|
188
|
+
data[light_col_name] = max_value - data[light_col_name] + min_value
|
|
189
|
+
|
|
190
|
+
# Call the original function with the modified data
|
|
191
|
+
return func(data, *args, **kwargs)
|
|
192
|
+
|
|
193
|
+
return wrapper
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
#### Functions ####
|
|
197
|
+
# function to set data by circadian period
|
|
198
|
+
@validate_input
|
|
199
|
+
def set_circadian_time(
|
|
200
|
+
data,
|
|
201
|
+
period='24h'):
|
|
202
|
+
"""
|
|
203
|
+
Reindexes current data to 24 hours CT instead of ZT by setting
|
|
204
|
+
frequency to the ratio of 24hrs/new period
|
|
205
|
+
|
|
206
|
+
Parameters
|
|
207
|
+
----------
|
|
208
|
+
data : pd.DataFrame
|
|
209
|
+
Dataframe with a pandas timeindex
|
|
210
|
+
period : str or float
|
|
211
|
+
The new period to set the data to.
|
|
212
|
+
Timedelta string (e.g., '24h', '1d', '72h')
|
|
213
|
+
|
|
214
|
+
Returns
|
|
215
|
+
-------
|
|
216
|
+
pd.DataFrame
|
|
217
|
+
Original data but with a new datetimeindex, starting at the same time
|
|
218
|
+
as the original but now 24 hours is equal to the given period instead
|
|
219
|
+
of real time.
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
# Convert period string to timedelta
|
|
223
|
+
if isinstance(period, str):
|
|
224
|
+
period = pd.to_timedelta(period)
|
|
225
|
+
else:
|
|
226
|
+
raise TypeError("Period must be in timedelta string format")
|
|
227
|
+
|
|
228
|
+
# Calculate the frequency ratio based on the period
|
|
229
|
+
freq_ratio = 24 / (period.total_seconds() / 3600)
|
|
230
|
+
|
|
231
|
+
# get data frequency as timedelta
|
|
232
|
+
base_freq = pd.infer_freq(data.index)
|
|
233
|
+
|
|
234
|
+
# Ensure base_freq has a numeric component
|
|
235
|
+
if not any(char.isdigit() for char in base_freq):
|
|
236
|
+
base_freq = '1' + base_freq # Prepend '1' if missing
|
|
237
|
+
|
|
238
|
+
# convert to timedelta
|
|
239
|
+
base_timedelta = pd.to_timedelta(base_freq)
|
|
240
|
+
|
|
241
|
+
# calculate ratio as a string
|
|
242
|
+
new_timedelta = base_timedelta * freq_ratio
|
|
243
|
+
new_freq_str = str(np.round(new_timedelta.total_seconds() * 1000)) + "ms"
|
|
244
|
+
|
|
245
|
+
# create new index based on this
|
|
246
|
+
start_time = data.index[0]
|
|
247
|
+
data_length = len(data)
|
|
248
|
+
new_index = pd.date_range(
|
|
249
|
+
start=start_time,
|
|
250
|
+
periods=data_length,
|
|
251
|
+
freq=new_freq_str
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# reindex the data
|
|
255
|
+
reindexed_data = pd.DataFrame(
|
|
256
|
+
data=data.values,
|
|
257
|
+
index=new_index,
|
|
258
|
+
columns=data.columns
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
return reindexed_data
|
circaPy/sleep_process.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# functions for sleep processing
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import numpy as np
|
|
5
|
+
import circaPy.preprocessing as prep
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def sleep_process(data, window=4):
|
|
9
|
+
"""
|
|
10
|
+
Function to score activity data as sleep given
|
|
11
|
+
a certain window of activity
|
|
12
|
+
Future development implement thresholds for breaking
|
|
13
|
+
sleep episodes.
|
|
14
|
+
Returns scored dataframe
|
|
15
|
+
:param data:
|
|
16
|
+
:param window:
|
|
17
|
+
:return:
|
|
18
|
+
"""
|
|
19
|
+
# score > window as inactivity score of 1
|
|
20
|
+
rolling_sum_data = data.rolling(window).sum()
|
|
21
|
+
bool_scored_data = rolling_sum_data == 0
|
|
22
|
+
scored_data = bool_scored_data.astype(int)
|
|
23
|
+
return scored_data
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def create_scored_df(data, **kwargs):
|
|
27
|
+
"""
|
|
28
|
+
Function to take dataframe as input and return the same data
|
|
29
|
+
and labels but scored for sleep -> then appropriate to save
|
|
30
|
+
:param data:
|
|
31
|
+
:return:
|
|
32
|
+
"""
|
|
33
|
+
# remove object columns, score, return columns to df
|
|
34
|
+
sleep_df = _score_active_times(data, **kwargs)
|
|
35
|
+
return sleep_df
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _score_active_times(data,
|
|
39
|
+
ldr_col=-1,
|
|
40
|
+
test_col=0,
|
|
41
|
+
threshold=1,
|
|
42
|
+
drop_level=True):
|
|
43
|
+
"""
|
|
44
|
+
Scores all times between start and end of activity as sleep, sets all
|
|
45
|
+
other values to 0
|
|
46
|
+
:param data:
|
|
47
|
+
:param ldr_col:
|
|
48
|
+
:param test_col:
|
|
49
|
+
:param threshold:
|
|
50
|
+
:param drop_level:
|
|
51
|
+
:return:
|
|
52
|
+
"""
|
|
53
|
+
if drop_level:
|
|
54
|
+
data = data.reset_index(0)
|
|
55
|
+
label_name = data.columns[0]
|
|
56
|
+
label_col = data.pop(label_name)
|
|
57
|
+
|
|
58
|
+
# score the df minus the LDR
|
|
59
|
+
ldr_label = data.columns[ldr_col]
|
|
60
|
+
ldr_data = data.pop(ldr_label)
|
|
61
|
+
scored_df = sleep_process(data)
|
|
62
|
+
|
|
63
|
+
# find start and end of activity
|
|
64
|
+
mask = data.iloc[:, test_col] > threshold
|
|
65
|
+
start = data.where(mask).first_valid_index()
|
|
66
|
+
end = data.where(mask)[::-1].first_valid_index()
|
|
67
|
+
|
|
68
|
+
# set scored df times outside of start and end to be 0
|
|
69
|
+
scored_df.loc[:start] = 0
|
|
70
|
+
scored_df.loc[end:] = 0
|
|
71
|
+
scored_df[ldr_label] = ldr_data
|
|
72
|
+
|
|
73
|
+
if drop_level:
|
|
74
|
+
scored_df[label_name] = label_col
|
|
75
|
+
new_cols = [scored_df.columns[-1], scored_df.index]
|
|
76
|
+
scored_df.set_index(new_cols, inplace=True)
|
|
77
|
+
|
|
78
|
+
return scored_df
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def alter_file_name(file_name,
|
|
82
|
+
suffix,
|
|
83
|
+
remove_slice_after=-9):
|
|
84
|
+
"""
|
|
85
|
+
Function to take in the file name and remove part of the
|
|
86
|
+
name, replace with "suffix" and rename the file
|
|
87
|
+
:param file_name:
|
|
88
|
+
:param suffix:
|
|
89
|
+
:param slice_range:
|
|
90
|
+
:return:
|
|
91
|
+
"""
|
|
92
|
+
new_file_name = file_name.stem[:remove_slice_after] + \
|
|
93
|
+
suffix + \
|
|
94
|
+
file_name.suffix
|
|
95
|
+
new_file_path = file_name.parent / new_file_name
|
|
96
|
+
os.rename(file_name, new_file_path)
|