Cosinor analysis with pyActigraphy¶
This notebook illustrates how to perform a single-component Cosinor analysis on actigraphy data.
The idea of a Cosinor analysis is to estimate some key parameters of the actigraphy count series by fitting these data with a (co)sine curve with a period of 24h:
where M is the MESOR (Midline Statistic Of Rhythm), A is the amplitude of the oscillations, T is the period and \(\phi\) is the acrophase.
Then, the fit procedure provides estimates of these parameters which can then help to characterize the 24h rest-activity rhythm of an individual.
However, despite its popularity in circadian rhythm analyses, the Cosinor analysis exhibits some short-comings when dealing with non-stationary signals such as actigraphy signals. Intuitively, such model with a fixed period, amplitude and offset cannot accomodate signals with:
rest-activity cycles that do not follow a perfect 24h period;
an overall daily activity that changes on a day-by-day basis;
an overall activity trend that evolves with time.
For more informations on the topic:
Leise, T. L., & Harrington, M. E. (2011). Wavelet-Based Time Series Analysis of Circadian Rhythms. Journal of Biological Rhythms, 26(5), 454–463. https://doi.org/10.1177/0748730411416330
For a nice comparison of several techniques, including Cosinor and SSA (available in pyActigraphy):
Fossion, R., Rivera, A. L., Toledo-Roy, J. C., Ellis, J., & Angelova, M. (2017). Multiscale adaptive analysis of circadian rhythms and intradaily variability: Application to actigraphy time series in acute insomnia subjects. PLOS ONE, 12(7), e0181762. https://doi.org/10.1371/journal.pone.0181762
Import packages and input data¶
First, load your favourite python packages:
[1]:
import pyActigraphy
[2]:
import plotly.graph_objects as go
Let us now define the path to the example data files contained in the pyActigraphy package:
[3]:
import os
fpath = os.path.join(os.path.dirname(pyActigraphy.__file__),'tests/data/')
Cosinor analysis¶
Now, simply import the Cosinor module:
[4]:
from pyActigraphy.analysis import Cosinor
… and define a Cosinor object:
[5]:
cosinor = Cosinor()
Initial fit values¶
By default, the initial values of the cosine fit functions are the following:
[6]:
cosinor.fit_initial_params.pretty_print()
Name Value Min Max Stderr Vary Expr Brute_Step
Acrophase 3.142 0 6.283 None True None None
Amplitude 50 0 inf None True None None
Mesor 50 0 inf None True None None
Period 1440 0 inf None True None None
These parameters will be estimated from the data. However, in order to ease the fit procedure, it makes sense to start from reasonable values. For example, the initial value for the period is set to 1440, the number of minutes per day. It makes only sense for data with a 1-min sampling frequency.
However, it is trivial to change these intial values.
For example, let’s change the initial value of the period to 2880, in case you would like to analyse data with a 30-sec sampling frequency:
[7]:
cosinor.fit_initial_params['Period'].value = 2880
[8]:
cosinor.fit_initial_params.pretty_print()
Name Value Min Max Stderr Vary Expr Brute_Step
Acrophase 3.142 0 6.283 None True None None
Amplitude 50 0 inf None True None None
Mesor 50 0 inf None True None None
Period 2880 0 inf None True None None
Free and fixed fit parameters¶
By default, none of the fit parameters are fixed and will be estimated from the data. However, in some cases, it might be convenient to fix the initial of a parameter and not let it vary during the fit procedure.
It might be the case of the period for instance. Let us fix it to 1440.
[9]:
cosinor.fit_initial_params['Period'].value = 1440
[10]:
cosinor.fit_initial_params['Period'].vary = False
[11]:
cosinor.fit_initial_params.pretty_print()
Name Value Min Max Stderr Vary Expr Brute_Step
Acrophase 3.142 0 6.283 None True None None
Amplitude 50 0 inf None True None None
Mesor 50 0 inf None True None None
Period 1440 0 inf None False None None
It is also possible to apply lower and upper bounds to these parameters.
For more informations about these parameters, please see: https://lmfit.github.io/lmfit-py/parameters.html
Analysis of a single subject¶
Read data from an example file…
[12]:
raw = pyActigraphy.io.read_raw_awd(fpath+'example_01.AWD', start_time='1918-01-24 08:30:00', period="7D")
… and plot it for visual inspection:
[13]:
fig = go.Figure(go.Scatter(x=raw.data.index.astype(str),y=raw.data))
[14]:
fig.show()
To perform a cosinor analysis with the pyActigraphy package, it is as simple as:
[15]:
results = cosinor.fit(raw.data, verbose=True) # Set verbose to True to print the fit output
[[Fit Statistics]]
# fitting method = leastsq
# function evals = 28
# data points = 10081
# variables = 3
chi-square = 6.8103e+08
reduced chi-square = 67576.1845
Akaike info crit = 112113.910
Bayesian info crit = 112135.566
[[Variables]]
Amplitude: 156.998940 +/- 3.66168149 (2.33%) (init = 50)
Acrophase: 4.86541767 +/- 0.02332300 (0.48%) (init = 3.141593)
Period: 1440 (fixed)
Mesor: 174.191952 +/- 2.58907736 (1.49%) (init = 50)
Fit results¶
To access the best fit parameter values:
[16]:
results.params['Mesor'].value
[16]:
174.19195195975644
It is also possible to transform them to a dictionary:
[17]:
results.params.valuesdict()
[17]:
OrderedDict([('Amplitude', 156.99894000312472),
('Acrophase', 4.865417670453099),
('Period', 1440),
('Mesor', 174.19195195975644)])
The results
object contains also informations about the goodness-of-fit:
[18]:
results.aic # Akaike information criterium
[18]:
112113.91042765231
[19]:
results.redchi # Reduced Chi^2
[19]:
67576.1844639616
More informations about the list of the informations accessible via the results
object: https://lmfit.github.io/lmfit-py/fitting.html#minimizerresult-the-optimization-result
Fit result visualization¶
If you are interested in visualizing the cosinor fit that best approximates the data, the Cosinor
class implements a convinient function:
[20]:
help(cosinor.best_fit)
Help on method best_fit in module pyActigraphy.analysis.cosinor:
best_fit(ts, params) method of pyActigraphy.analysis.cosinor.Cosinor instance
Best fit function of the data.
Parameters
----------
ts : pandas.Series
Originally fitted time series.
params: instance of Parameters [1]_
Best fit parameters.
Returns
-------
bestfit_data : pandas.Series
Time series of the best fit data.
References
----------
.. [1] Non-Linear Least-Squares Minimization and Curve-Fitting for
Python.
https://lmfit.github.io/lmfit-py/index.html
[21]:
best_fit = cosinor.best_fit(raw.data, results.params)
[22]:
fig = go.Figure(
data=[
go.Scatter(x=raw.data.index.astype(str),y=raw.data,name='Raw data'),
go.Scatter(x=best_fit.index.astype(str),y=best_fit,name='Best fit')
]
)
[23]:
fig.show()
Batch analysis¶
While it is sometimes necessary to perform a cosinor analysis of a single subject to get used with the procedure and get “a feeling” about the data, it is often not pratical to follow the same procedure to analyse multiple subjects.
Fortunately, pyActigraphy
implements several convenient functions to ease a batch analysis.
Let us first read a batch of files, using the example files included the pyActigraphy
package for demonstration purposes:
[24]:
readers = pyActigraphy.io.read_raw(fpath+'example_*.AWD', reader_type='AWD')
[25]:
len(readers.readers)
[25]:
6
6 files have been found and read by the readers
object.
Read and apply a SST log (See https://ghammad.github.io/pyActigraphy/pyActigraphy-SSt-log.html for more informations):
[26]:
readers.read_sst_log(fpath+'example_sstlog.csv')
[27]:
readers.apply_sst(verbose=True)
Could not find an entry in SST log file for example_04
Could not find an entry in SST log file for example_05
Could not find an entry in SST log file for example_02
Found an entry in SST log file for example_03
Found an entry in SST log file for example_01
Could not find an entry in SST log file for example_01_mask
Now, let perform a cosinor analysis on each of these files, in parallel, using 3 cpu’s in order to speed up the computations.
[28]:
results_batch = cosinor.fit_reader(readers, n_jobs=3, prefer='threads') # prefer='threads': add that parameter if running on Mac OS.
For convenience, the output results are formatted as a Pandas.DataFrame (https://pandas.pydata.org/pandas-docs/stable/getting_started/dsintro.html#dataframe) in order to ease further analysis.
[29]:
results_batch
[29]:
Amplitude | Acrophase | Period | Mesor | BIC | RedChiSq | |
---|---|---|---|---|---|---|
example_04 | 7.345742e+01 | 0.927315 | 1440 | 81.733231 | 321025.485636 | 28448.100307 |
example_05 | 1.352039e+02 | 5.472072 | 1440 | 120.821941 | 225115.967014 | 31930.970729 |
example_02 | 5.711733e-08 | 3.032968 | 1440 | 183.388579 | 207392.482663 | 77803.026821 |
example_03 | 3.381232e+02 | 6.240036 | 1440 | 346.219095 | 171305.166394 | 146324.160952 |
example_01 | 1.577049e+02 | 4.750332 | 1440 | 171.007640 | 143499.394878 | 64194.285808 |
example_01_mask | 1.266343e-07 | 2.986621 | 1440 | 127.565387 | 202689.738565 | 60700.671139 |
To write these results to an output file, it is as simple as: results_batch.to_csv('myfile.csv')
.
More info on to_csv
: https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.to_csv.html?highlight=to_csv#pandas-dataframe-to-csv
Et voilà! Easy, isn’t it?