PhoPro 0.5.0__tar.gz

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.
Files changed (33) hide show
  1. phopro-0.5.0/LICENSE +21 -0
  2. phopro-0.5.0/MANIFEST.in +3 -0
  3. phopro-0.5.0/PKG-INFO +99 -0
  4. phopro-0.5.0/PhoPro/__init__.py +14 -0
  5. phopro-0.5.0/PhoPro/analysis/FMM.py +504 -0
  6. phopro-0.5.0/PhoPro/analysis/__init__.py +20 -0
  7. phopro-0.5.0/PhoPro/analysis/artifact.py +650 -0
  8. phopro-0.5.0/PhoPro/analysis/comparison.py +571 -0
  9. phopro-0.5.0/PhoPro/analysis/peaks.py +1101 -0
  10. phopro-0.5.0/PhoPro/core/PhotometeryData.py +1370 -0
  11. phopro-0.5.0/PhoPro/core/PhotometryExperiment.py +1533 -0
  12. phopro-0.5.0/PhoPro/core/PhotometryLoader.py +374 -0
  13. phopro-0.5.0/PhoPro/core/PhotometryPipeline.py +587 -0
  14. phopro-0.5.0/PhoPro/core/__init__.py +15 -0
  15. phopro-0.5.0/PhoPro/sim/SimulatedPhotometry.py +924 -0
  16. phopro-0.5.0/PhoPro/sim/__init__.py +25 -0
  17. phopro-0.5.0/PhoPro/sim/kernels.py +305 -0
  18. phopro-0.5.0/PhoPro/sim/layers.py +717 -0
  19. phopro-0.5.0/PhoPro/utils/__init__.py +0 -0
  20. phopro-0.5.0/PhoPro/utils/equations.py +54 -0
  21. phopro-0.5.0/PhoPro/utils/graphing.py +548 -0
  22. phopro-0.5.0/PhoPro/utils/io.py +90 -0
  23. phopro-0.5.0/PhoPro/utils/operations.py +192 -0
  24. phopro-0.5.0/PhoPro/utils/reference_fitting.py +174 -0
  25. phopro-0.5.0/PhoPro/utils/window.py +331 -0
  26. phopro-0.5.0/PhoPro.egg-info/PKG-INFO +99 -0
  27. phopro-0.5.0/PhoPro.egg-info/SOURCES.txt +31 -0
  28. phopro-0.5.0/PhoPro.egg-info/dependency_links.txt +1 -0
  29. phopro-0.5.0/PhoPro.egg-info/requires.txt +21 -0
  30. phopro-0.5.0/PhoPro.egg-info/top_level.txt +4 -0
  31. phopro-0.5.0/README.md +63 -0
  32. phopro-0.5.0/pyproject.toml +58 -0
  33. phopro-0.5.0/setup.cfg +4 -0
phopro-0.5.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Griffin Golde
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,3 @@
1
+ include README.md
2
+ include LICENSE
3
+ exclude_dir pyFiberPhotometry.egg-info
phopro-0.5.0/PKG-INFO ADDED
@@ -0,0 +1,99 @@
1
+ Metadata-Version: 2.4
2
+ Name: PhoPro
3
+ Version: 0.5.0
4
+ Summary: Python package for processing, handling, and analyzing fiber photometry data
5
+ Author-email: Griffin Golde <ggolde@ufl.edu>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/ggolde/PhoPro
8
+ Project-URL: Source, https://github.com/ggolde/PhoPro
9
+ Project-URL: Issues, https://github.com/ggolde/PhoPro/issues
10
+ Project-URL: Documentation, https://ggolde.github.io/PhoPro/
11
+ Keywords: fiber photometry,photometry,neuroscience,analysis
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: numpy
16
+ Requires-Dist: pandas
17
+ Requires-Dist: matplotlib
18
+ Requires-Dist: anndata>=0.10
19
+ Requires-Dist: h5py
20
+ Requires-Dist: dask
21
+ Requires-Dist: scipy
22
+ Requires-Dist: statsmodels
23
+ Requires-Dist: scikit-learn
24
+ Requires-Dist: tdt
25
+ Requires-Dist: pyyaml
26
+ Requires-Dist: plotnine
27
+ Requires-Dist: pingouin
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest; extra == "dev"
30
+ Requires-Dist: mkdocs-material; extra == "dev"
31
+ Requires-Dist: mkdocstrings; extra == "dev"
32
+ Requires-Dist: nbconvert; extra == "dev"
33
+ Requires-Dist: twine; extra == "dev"
34
+ Requires-Dist: build; extra == "dev"
35
+ Dynamic: license-file
36
+
37
+ # **PhoPro**
38
+
39
+ *A comprehensive toolkit for the processing, analysis, and simulation of fiber photometry data*
40
+
41
+ ## Overview
42
+ This package provides extensive functionaly for processing, handling, analyzing, and bulk processing fiber photometry datasets while remaining highly customizable. The high level APIs only require basic programming experience. It is built around 4 cores modules:
43
+
44
+ * **[PhotometryLoader](https://ggolde.github.io/PhoPro/api/PhotometryLoader/)** - loads various photometry data formats.
45
+
46
+ * **[PhotometryExperiment](https://ggolde.github.io/PhoPro/api/PhotometryExperiment/)** - processes and windows photometry experiments.
47
+
48
+ * **[PhotometryData](https://ggolde.github.io/PhoPro/api/PhotometryData/)** - handles trial-wise signals and metadata with advanced filtering, averaging, windowing, and analysis functionality.
49
+
50
+ * **[PhotometryPipeline](https://ggolde.github.io/PhoPro/api/PhotometryPipeline/)** - handles highly-customizable bulk processing of photometry data.
51
+
52
+
53
+ With an additional module for rigorously simulating complex photometry data:
54
+
55
+ * **[SimulatedPhotometry](https://ggolde.github.io/PhoPro/sim/SimulatedPhotometry)** - simulates photometry traces with realistic photobleaching, movement artifacts, and custom event dynamics.
56
+
57
+ ---
58
+
59
+ ## [Documentation](https://ggolde.github.io/PhoPro/)
60
+
61
+ Documentation for this package can be found [here](https://ggolde.github.io/PhoPro/). It is built with mkdocs-material and hosted on GitHub pages.
62
+
63
+ ---
64
+
65
+ ## Installation
66
+ You can install directly from GitHub using the command:
67
+ ```
68
+ pip install git+https://github.com/ggolde/PhoPro.git
69
+ ```
70
+
71
+ See the [Installation](https://ggolde.github.io/PhoPro/installation/) page of the docs for more details.
72
+
73
+ ---
74
+
75
+ ## Planned Features
76
+ This package is in ongoing development, please suggest any features you would like to see implemented on the [issues page](https://github.com/ggolde/PhoPro/issues). Below are a few planned features:
77
+ * [ ] Implement import support for more formats.
78
+ * [ ] More methods for artifact detection and correction.
79
+ * [x] Easier APIs for the cluster based permutation test and functional mixed modeling.
80
+ * [x] Phase out "variants" modules.
81
+ * [x] Improve windowing in PhotometryData.
82
+
83
+ ## License
84
+
85
+ MIT License.
86
+
87
+ ## Authors
88
+ This package is developed and used internally at the [Bizon-Setlow Lab](https://burke.neuroscience.ufl.edu/profile/bizon-jennifer/) at the University of Florida.
89
+
90
+ **Griffin Golde**: University of Florida, contact: **[ggolde@ufl.edu](mailto:ggolde@ufl.edu)**
91
+
92
+ ## Citations
93
+
94
+ If you use this package in your work, and want to support the package, use the citation below (a proper publication is hopefully on the horizon):
95
+
96
+ * Golde G. *PhoPro: Python toolkit for simulating, processing, handling, and analyzing fiber photometry data*.
97
+ GitHub repository. Version 0.2.0. Available at: https://github.com/ggolde/PhoPro.
98
+
99
+ Additionally, if you used the FMM module, please review the [recommended reference(s)](https://github.com/gloewing/photometry_FLMM/blob/main/README.md#references) for the ``fast-fmm-rpy2`` package.
@@ -0,0 +1,14 @@
1
+ from .core import PhotometryData, PhotometryExperiment, PhotometryLoader, PhotometryPipeline, TDTLoader, CSVLoader
2
+ from .sim import SimulatedPhotometry
3
+
4
+ __version__ = "0.5.0"
5
+
6
+ __all__ = [
7
+ "PhotometryData",
8
+ "PhotometryExperiment",
9
+ "PhotometryLoader",
10
+ "PhotometryPipeline",
11
+ "TDTLoader",
12
+ "CSVLoader",
13
+ "SimulatedPhotometry",
14
+ ]
@@ -0,0 +1,504 @@
1
+ """Functional mixed-model helpers backed by fastFMM."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from os import PathLike
7
+ from typing import TYPE_CHECKING
8
+
9
+ import numpy as np
10
+ import pandas as pd
11
+ import re
12
+
13
+ from plotnine import ggplot
14
+ from ..utils import graphing
15
+
16
+ if TYPE_CHECKING:
17
+ from ..core.PhotometeryData import PhotometryData
18
+
19
+ ############################
20
+ #region --- RESULT CLASS ---
21
+ ############################
22
+ @dataclass(slots=True)
23
+ class FMMResult:
24
+ """Wrapper around fastFMM's multi-indexed result table."""
25
+
26
+ df: pd.DataFrame
27
+ formula: str | None = None
28
+
29
+ # --- dunders ---
30
+ def __post_init__(self) -> None:
31
+ """Validate the fastFMM result table."""
32
+ if not isinstance(self.df.columns, pd.MultiIndex):
33
+ raise ValueError("FMMResult.df must have MultiIndex columns.")
34
+
35
+ def __str__(self) -> str:
36
+ """Return the string representation of the result table."""
37
+ return self.df.__str__()
38
+
39
+ def __repr__(self) -> str:
40
+ """Return the interactive representation of the result table."""
41
+ return self.df.__repr__()
42
+
43
+ # --- properties ---
44
+ @property
45
+ def column_groups(self) -> list[str]:
46
+ """Top-level column groups in the result table."""
47
+ return self.df.columns.get_level_values(0).unique().to_list()
48
+
49
+ @property
50
+ def terms(self) -> list[str]:
51
+ """Model coefficient terms, excluding metadata groups."""
52
+ return [
53
+ col for col in self.column_groups
54
+ if col not in {"time", "AIC"}
55
+ ]
56
+
57
+ @property
58
+ def time(self) -> pd.Series:
59
+ """Time values indexed like the result table."""
60
+ if ("time", "time") in self.df.columns:
61
+ return self.df[("time", "time")]
62
+
63
+ if "time" in self.column_groups:
64
+ time_df = self.df["time"]
65
+ if time_df.shape[1] == 1:
66
+ out = time_df.iloc[:, 0]
67
+ out.name = "time"
68
+ return out
69
+
70
+ raise KeyError("FMMResult does not contain a time column.")
71
+
72
+ @property
73
+ def coefficients(self) -> pd.DataFrame:
74
+ """All coefficient-term columns."""
75
+ if not self.terms:
76
+ return self.df.iloc[:, 0:0]
77
+
78
+ return self.df.loc[:, pd.IndexSlice[self.terms, :]]
79
+
80
+ @property
81
+ def aic(self) -> pd.DataFrame:
82
+ """AIC columns returned by fastFMM."""
83
+ if "AIC" not in self.column_groups:
84
+ raise KeyError("FMMResult does not contain AIC columns.")
85
+ return pd.DataFrame(self.df["AIC"])
86
+
87
+ # --- I/O ---
88
+ def to_csv(
89
+ self,
90
+ path: str | PathLike[str],
91
+ **kwargs,
92
+ ) -> None:
93
+ """Write the underlying multi-indexed result table to CSV.
94
+
95
+ Parameters
96
+ ----------
97
+ path : str or PathLike[str]
98
+ Output CSV path.
99
+ **kwargs
100
+ Additional keyword arguments passed to ``DataFrame.to_csv``.
101
+ """
102
+ self.df.to_csv(path, **kwargs)
103
+
104
+ @classmethod
105
+ def from_csv(
106
+ cls,
107
+ path: str | PathLike[str],
108
+ formula: str | None = None,
109
+ **kwargs,
110
+ ) -> "FMMResult":
111
+ """Read an ``FMMResult`` from a CSV written by ``to_csv``.
112
+
113
+ Parameters
114
+ ----------
115
+ path : str or PathLike[str]
116
+ Input CSV path.
117
+ formula : str or None, default=None
118
+ Formula associated with the result.
119
+ **kwargs
120
+ Additional keyword arguments passed to ``pandas.read_csv``.
121
+
122
+ Returns
123
+ -------
124
+ FMMResult
125
+ Loaded result object.
126
+ """
127
+ read_kwargs = {"header": [0, 1], "index_col": 0} | kwargs
128
+ df = pd.read_csv(path, **read_kwargs)
129
+ return cls(df=df, formula=formula)
130
+
131
+ # --- access ---
132
+ def term(self, name: str, include_time: bool = True) -> pd.DataFrame:
133
+ """Return all statistics for one model term.
134
+
135
+ Parameters
136
+ ----------
137
+ name : str
138
+ Model term name.
139
+ include_time : bool, default=True
140
+ If ``True``, prepend a ``time`` column.
141
+
142
+ Returns
143
+ -------
144
+ pd.DataFrame
145
+ Statistics for the selected term.
146
+
147
+ Raises
148
+ ------
149
+ KeyError
150
+ If ``name`` is not a model term.
151
+ """
152
+ if name not in self.terms:
153
+ raise KeyError(f"Unknown FMM term: {name}")
154
+
155
+ out: pd.DataFrame = self.df[name].copy()
156
+ if include_time:
157
+ out.insert(0, "time", self.time.to_numpy())
158
+ return out
159
+
160
+ def stat(self, name: str) -> pd.DataFrame:
161
+ """Return one statistic across all model terms.
162
+
163
+ Parameters
164
+ ----------
165
+ name : str
166
+ Statistic name in the second column-index level.
167
+
168
+ Returns
169
+ -------
170
+ pd.DataFrame
171
+ Statistic values across all coefficient terms.
172
+ """
173
+ return pd.DataFrame(self.coefficients.xs(name, level=1, axis=1).copy())
174
+
175
+ # --- export ---
176
+ def to_long(
177
+ self,
178
+ only_terms: list[str] | None = None,
179
+ stat_map: dict[str, str] | None = None,
180
+ ) -> pd.DataFrame:
181
+ """Convert the FMM result table to long format.
182
+
183
+ Parameters
184
+ ----------
185
+ only_terms : list[str] or None, default=None
186
+ Model terms to include. If ``None``, all coefficient terms are
187
+ included.
188
+ stat_map : dict[str, str] or None, default=None
189
+ Mapping from output column names to source statistic names in the
190
+ fastFMM table.
191
+
192
+ Returns
193
+ -------
194
+ pd.DataFrame
195
+ Long dataframe with time, term, and selected statistics.
196
+ """
197
+ if stat_map is None:
198
+ stat_map = {
199
+ "value": "beta",
200
+ "lower": "lower",
201
+ "upper": "upper",
202
+ "lower_joint": "lower_joint",
203
+ "upper_joint": "upper_joint",
204
+ }
205
+
206
+ coefficients = self.coefficients.rename_axis(
207
+ index="time_idx",
208
+ columns=["term", "stat"],
209
+ )
210
+
211
+ try:
212
+ long_df = coefficients.stack(level="term", future_stack=True)
213
+ except TypeError:
214
+ long_df = coefficients.stack(level="term")
215
+
216
+ long_df = long_df.reset_index()
217
+ long_df.columns.name = None
218
+
219
+ rename_map = {
220
+ source: target
221
+ for target, source in stat_map.items()
222
+ if source in long_df.columns
223
+ }
224
+ long_df = long_df.rename(columns=rename_map)
225
+
226
+ time_df = self.time.rename("time").rename_axis("time_idx").reset_index()
227
+ long_df = long_df.merge(time_df, on="time_idx", how="left")
228
+
229
+ keep_cols = ["time_idx", "time", "term"] + [
230
+ col for col in stat_map
231
+ if col in long_df.columns
232
+ ]
233
+
234
+ only_terms = self.terms if only_terms is None else only_terms
235
+ keep_terms = long_df['term'].isin(only_terms)
236
+
237
+ return long_df.loc[keep_terms, keep_cols]
238
+
239
+ # --- plotting ---
240
+ def plot(
241
+ self,
242
+ only_terms: list[str] | None = None,
243
+ line_kwargs: dict = {},
244
+ hline_kwargs: dict = {},
245
+ ribbon_inner_kwargs: dict = {},
246
+ ribbon_outer_kwargs: dict = {},
247
+ theme_kwargs: dict = {},
248
+ ) -> ggplot:
249
+ """Plot model terms and confidence bands.
250
+
251
+ Parameters
252
+ ----------
253
+ only_terms : list[str] or None, default=None
254
+ Model terms to plot. If ``None``, all coefficient terms are plotted.
255
+ line_kwargs : dict, default={}
256
+ Keyword arguments forwarded to line geoms.
257
+ hline_kwargs : dict, default={}
258
+ Keyword arguments forwarded to horizontal reference-line geoms.
259
+ ribbon_inner_kwargs : dict, default={}
260
+ Keyword arguments forwarded to inner confidence-band ribbon geoms.
261
+ ribbon_outer_kwargs : dict, default={}
262
+ Keyword arguments forwarded to outer confidence-band ribbon geoms.
263
+ theme_kwargs : dict, default={}
264
+ Keyword arguments forwarded to the plot theme helper.
265
+
266
+ Returns
267
+ -------
268
+ ggplot
269
+ Plot object.
270
+ """
271
+
272
+ long_df = self.to_long(only_terms=only_terms)
273
+
274
+ p = graphing.plot_FMM_result(
275
+ long_df,
276
+ line_kwargs=line_kwargs,
277
+ hline_kwargs=hline_kwargs,
278
+ ribbon_inner_kwargs=ribbon_inner_kwargs,
279
+ ribbon_outer_kwargs=ribbon_outer_kwargs,
280
+ theme_kwargs=theme_kwargs,
281
+ )
282
+
283
+ return p
284
+
285
+ #endregion
286
+
287
+ #######################
288
+ #region --- FMM API ---
289
+ #######################
290
+ def run_fastFMM(
291
+ data: PhotometryData,
292
+ formula: str,
293
+ factor_cols: dict[str, str | None] = {},
294
+ parallel: bool = True,
295
+ family: str = "gaussian",
296
+ analytic: bool = True,
297
+ var: bool = True,
298
+ silent: bool = False,
299
+ argvals: list[int] | None = None,
300
+ nknots_min: int | None = None,
301
+ nknots_min_cov: int | None = 35,
302
+ smooth_method: str = "GCV.Cp",
303
+ splines: str = "tp",
304
+ design_mat: bool = False,
305
+ residuals: bool = False,
306
+ n_boots: int = 500,
307
+ seed: int = 1,
308
+ subj_id: str | None = None,
309
+ n_cores: int | None = None,
310
+ caic: bool = False,
311
+ randeffs: bool = False,
312
+ non_neg: int = 0,
313
+ MoM: int = 1,
314
+ concurrent: bool = False,
315
+ impute_outcome: bool = False,
316
+ override_zero_var: bool = False,
317
+ unsmooth: bool = False,
318
+ ) -> FMMResult:
319
+ """Run a fastFMM model on trial-wise photometry data.
320
+
321
+ This is a wrapper around ``fast-fmm-rpy2`` that converts
322
+ `PhotometryData` into the wide table expected by fastFMM and wraps the
323
+ multi-indexed output table in `FMMResult`.
324
+
325
+ Parameters
326
+ ----------
327
+ data : PhotometryData
328
+ Photometry data used to fit the functional mixed model.
329
+ formula : str
330
+ Formula passed to fastFMM. It must begin with a signal column prefix
331
+ present in the exported wide dataframe.
332
+ factor_cols : dict[str, str or None], default={}
333
+ Metadata columns to convert to R factors. Values specify optional
334
+ reference levels.
335
+ parallel : bool, default=True
336
+ Whether fastFMM should run in parallel.
337
+ family : str, default='gaussian'
338
+ Model family passed to fastFMM.
339
+ analytic : bool, default=True
340
+ Whether to use analytic inference instead of bootstrap inference.
341
+ var : bool, default=True
342
+ Whether to include within-timepoint variance in the model.
343
+ silent : bool, default=False
344
+ Whether to suppress model output.
345
+ argvals : list[int] or None, default=None
346
+ Functional-domain indexes used in the model. ``None`` uses all points.
347
+ nknots_min : int or None, default=None
348
+ Minimum knots for coefficient smoothing.
349
+ nknots_min_cov : int or None, default=35
350
+ Minimum knots for covariance smoothing.
351
+ smooth_method : str, default='GCV.Cp'
352
+ Smoothing-parameter selection method.
353
+ splines : str, default='tp'
354
+ Spline type used by fastFMM.
355
+ design_mat : bool, default=False
356
+ Whether to return the design matrix.
357
+ residuals : bool, default=False
358
+ Whether to save residuals from the unsmoothed LME.
359
+ n_boots : int, default=500
360
+ Number of bootstrap samples.
361
+ seed : int, default=1
362
+ Seed used by fastFMM bootstrap routines.
363
+ subj_id : str or None, default=None
364
+ Column containing subject IDs.
365
+ n_cores : int or None, default=None
366
+ Number of cores used for parallelization.
367
+ caic : bool, default=False
368
+ Whether to calculate CAIC.
369
+ randeffs : bool, default=False
370
+ Whether to return random-effect estimates.
371
+ non_neg : int, default=0
372
+ Non-negativity constraint mode passed to fastFMM.
373
+ MoM : int, default=1
374
+ Method-of-moments estimator setting.
375
+ concurrent : bool, default=False
376
+ Whether to fit a concurrent model.
377
+ impute_outcome : bool, default=False
378
+ Whether to impute missing outcome values with FPCA.
379
+ override_zero_var : bool, default=False
380
+ Whether to proceed when exported columns have zero variance.
381
+ unsmooth : bool, default=False
382
+ Whether to return raw unsmoothed coefficient and variance estimates.
383
+
384
+ Returns
385
+ -------
386
+ FMMResult
387
+ Wrapped fastFMM result table.
388
+
389
+ Raises
390
+ ------
391
+ ValueError
392
+ If ``formula`` does not begin with a signal prefix present in the
393
+ exported wide dataframe.
394
+ """
395
+ # lazy import fast-fmm-rpy2
396
+ from rpy2.robjects import r
397
+ from rpy2.rinterface import NULL # type: ignore
398
+ from fast_fmm_rpy2.ingest import pass_pandas_to_r
399
+ from fast_fmm_rpy2.fmm_run import fui
400
+ from fast_fmm_rpy2.plot_fui import plot_fui
401
+
402
+ # set up function
403
+ def _factor_r_var(r_var: str, col: str, ref_lvl: str | None) -> None:
404
+ """Factor one R dataframe column and optionally set a reference level."""
405
+ r(f'{r_var}[,"{col}"] = factor({r_var}[,"{col}"], ordered = "FALSE")')
406
+ if ref_lvl is not None:
407
+ r(f'{r_var}[,"{col}"] = relevel({r_var}[,"{col}"], ref = "{ref_lvl}")')
408
+
409
+ # step 0: convert Photometry data to dataframe
410
+ # get necessary cols from formula
411
+ keep_cols = (
412
+ pd.Series(re.split(r"[\+\*\|:~]", formula))
413
+ .str.replace(r'[\(\)]', '', regex=True)
414
+ .str.strip()
415
+ .unique()
416
+ .tolist()
417
+ )
418
+ obs_cols = [col for col in keep_cols if col in data.obs]
419
+
420
+ df = data.trials_to_wide_df(
421
+ layer=None,
422
+ obs_cols=obs_cols,
423
+ signal_prefix='photometry',
424
+ downsample=None,
425
+ )
426
+
427
+ # step 1: validate inputs
428
+ # convert some None types to R NULL type
429
+ def _none_to_null(val):
430
+ """Convert Python None to R NULL."""
431
+ if val is None: return NULL
432
+ else: return val
433
+
434
+ argvals = _none_to_null(argvals)
435
+ nknots_min = _none_to_null(nknots_min)
436
+ subj_id = _none_to_null(subj_id)
437
+ n_cores = _none_to_null(n_cores)
438
+
439
+ # ensure formula starts with signal prefix
440
+ potential_prefixes = (
441
+ df.columns[
442
+ df.columns.str.contains(".", regex=False)
443
+ ]
444
+ .str.split(".")
445
+ .str[0].unique()
446
+ .to_list()
447
+ )
448
+ formula_valid = np.asarray([formula.startswith(prefix) for prefix in potential_prefixes]).any()
449
+ if not formula_valid:
450
+ raise ValueError(f'Formula ({formula}) does not begin with any potential signal prefixes ({potential_prefixes})')
451
+
452
+ # step 2: pass dataframe to R and factor vars
453
+ pass_pandas_to_r(df, r_var_name='dat')
454
+ for col, ref_lvl in factor_cols.items():
455
+ _factor_r_var('dat', col, ref_lvl)
456
+
457
+ # step 3: run fastFMM
458
+ model = fui(
459
+ csv_filepath=None,
460
+ r_var_name='dat',
461
+ formula=formula,
462
+ parallel=parallel,
463
+ family=family,
464
+ analytic=analytic,
465
+ var=var,
466
+ silent=silent,
467
+ argvals=argvals,
468
+ nknots_min=nknots_min,
469
+ nknots_min_cov=nknots_min_cov,
470
+ smooth_method=smooth_method,
471
+ splines=splines,
472
+ design_mat=design_mat,
473
+ residuals=residuals,
474
+ n_boots=n_boots,
475
+ seed=seed,
476
+ subj_id=subj_id,
477
+ n_cores=n_cores,
478
+ caic=caic,
479
+ randeffs=randeffs,
480
+ non_neg=non_neg,
481
+ MoM=MoM,
482
+ concurrent=concurrent,
483
+ impute_outcome=impute_outcome,
484
+ override_zero_var=override_zero_var,
485
+ unsmooth=unsmooth,
486
+ )
487
+
488
+ # step 4: plot results and retrieve data
489
+ coeff_figs, model_res = plot_fui(model, return_data=True) # type: ignore
490
+
491
+ # step 5: coerce results into multi indexed dataframe
492
+ first_df = next(iter(model_res.values()))
493
+ shared_idx = first_df.index.astype(int)
494
+
495
+ combined: dict[str, pd.DataFrame] = {
496
+ coeff : data.set_index(shared_idx).drop(columns='s') for coeff, data in model_res.items()
497
+ }
498
+ combined['time'] = pd.DataFrame({'time':data.ts}).set_index(shared_idx)
499
+ combined['AIC'] = model.getbyname('aic').set_index(shared_idx)
500
+
501
+ out = pd.concat(combined, axis=1)
502
+ return FMMResult(out, formula=formula)
503
+
504
+ #endregion
@@ -0,0 +1,20 @@
1
+ """Analysis tools for artifacts, peaks, group comparisons, and FMM models."""
2
+
3
+ from .artifact import ArtifactResult, ODS_Detector, Spline_Corrector
4
+ from .comparison import ClusterTestResult, cluster_depth_test, cluster_permutation_test
5
+ from .FMM import FMMResult, run_fastFMM
6
+ from .peaks import PeakResult, StaticThresholdDetector, RollingThresholdDetector
7
+
8
+ __all__ = [
9
+ 'ArtifactResult',
10
+ 'ODS_Detector',
11
+ 'Spline_Corrector',
12
+ 'ClusterTestResult',
13
+ 'cluster_depth_test',
14
+ 'cluster_permutation_test',
15
+ 'FMMResult',
16
+ 'run_fastFMM',
17
+ 'PeakResult',
18
+ 'StaticThresholdDetector',
19
+ 'RollingThresholdDetector'
20
+ ]