Skip to content

Conversation

@qian-chu
Copy link
Collaborator

Developers and users have found the old Epochs class quite unintuitive and operationally hard to use (e.g., nested DataFrame). This PR aims to reorganize the class.

Current features:

  • Slight change of Events class, where now event IDs (e.g., "fixation id") are now used as index. For messages and custom events, the index is name "event id"
  • event_times is renamed to epochs_info for easier comprehension
  • Creating an Epochs instance will not compute the epochs or annotated data automatically.
  • epochs is now a cached property in the form of a dictionary. Keys are indices of epochs_info and values are Stream/Events instances.
  • Annotated data now available through annotate_epochs.
  • (in progress) added a pupillary light reflex (PLR) dataset (2 participants, data in cloud and native formats). Currently exploring options to host data on FigShare vs OSF

qian-chu and others added 15 commits December 19, 2025 15:34
Refactored the Epochs class to use a dictionary of epochs indexed by epoch index, added properties for empty epochs, and improved overlap checking and warnings. Updated the to_numpy method to support flexible sampling rates and interpolation, and improved baseline correction error handling. Modified plot_epochs and its helpers to work with the new Epochs API, and updated the pupil_size_and_epoching tutorial to use the new sample data and API.
Expanded and clarified docstrings for epoching functions and the Epochs class, including detailed parameter and return value descriptions. Refactored Dataset to automatically handle native data without requiring a 'custom' flag, and improved section construction for native recordings. Updated tutorials to use consistent event naming and native data examples. Minor bugfixes and doc improvements in export and utility modules.
@qian-chu qian-chu requested a review from JGHartel December 29, 2025 13:54
@qian-chu
Copy link
Collaborator Author

@JGHartel Currently the baseline correction method is not updated yet. What do you think would be the best API/code for it? For operability with other methods such as plot(), I think it makes the most sense to modify the data in epochs property

Copy link
Collaborator

@JGHartel JGHartel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, I welcome the suggested changes. In particular, the rename towards epoch_info and the unified access to epochs data as epoch.epochs, rather than the previous mixed access, is appreciated. Still, I would like to invite some discussions on the assumed datatypes, e.g. epochs.epochs and epochs.annotate being a dictionary. This would especially futureproof the development, if dataset level operations should be introduced

``t_after``: Time after the reference time to end the epoch.\n
``description``: Description or label associated with the epoch.
"""

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will defer to you on this, but I personally think it a bit over the top to use decorators for the docstring. Besides generating the API doc, sometimes one just wants to look on the functions/methods themselves. I would not want to have to dig through a dictionary of docstring defaults for that.
That being said, the current ones seem to be simple enough to be redundant in code

pyneon/epochs.py Outdated
# Create epochs
self.epochs, self.data = _create_epochs(source, times_df)
@cached_property
def epochs(self) -> dict[int, Stream | Events | None]:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

flagging this in case we ever want to support multi-recording functionality. It would be good to have a Unique ID for each event, so that one can easily concat these dicts between recordings. Alternatively, we could then construct an implicit multiindex (recording, epoch_number), which would require a custom concat function.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems a bit of an advanced user case and also in principle should apply to other classes (e.g. streams and events). My evaluation would be it would require another PR if we see value in supporting multi-recording concatenation

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a major refactoring of the Epochs class to make it more intuitive and easier to use. The key changes include moving away from nested DataFrames to a dictionary-based structure, renaming methods for clarity, and introducing lazy computation of epochs using cached properties.

Changes:

  • Refactored Epochs class to use epochs_dict (cached property returning a dictionary) instead of nested DataFrames
  • Renamed event_times to epochs_info and events_to_times_df to events_to_epochs_info for better comprehension
  • Changed Events class to use event IDs as DataFrame index instead of separate columns
  • Added new filter_by_name method to Events class and updated filter_by_duration behavior
  • Added PLR sample dataset support with Figshare hosting
  • Consolidated CI workflows (removed separate tests.yml, integrated into main.yml)

Reviewed changes

Copilot reviewed 19 out of 22 changed files in this pull request and generated 17 comments.

Show a summary per file
File Description
pyneon/epochs.py Major refactoring of Epochs class with new dictionary-based structure, cached properties, and helper functions renamed
pyneon/events.py Modified to use event IDs as index, added filter_by_name method, updated filter_by_duration
pyneon/stream.py Added stub for detect_events_from_derivative method, updated docstrings with reusable doc snippets
pyneon/vis/vis.py Updated plot_epochs to work with new epochs_dict structure
pyneon/dataset.py Made sections.csv optional, improved error handling for missing recordings
pyneon/utils/doc_decorators.py Added reusable documentation snippets for common return types and parameters
pyneon/utils/sample_data.py Added PLR dataset URL and test
tests/conftest.py Added simple_events fixture for testing Events functionality
tests/test_events.py Added (empty) test stub for crop functionality
tests/test_streams.py Added tests for interpolation and concatenation methods
.github/workflows/main.yml Consolidated CI workflows, added test job dependencies
source/tutorials/*.ipynb Updated tutorial notebooks to use new API names
README.md Added information about sample datasets on Figshare
Comments suppressed due to low confidence (1)

pyneon/epochs.py:223

  • The properties columns and dtypes (lines 216-223) reference self.data which no longer exists in the new Epochs class structure. These properties need to be updated or removed.
    @property
    def columns(self) -> pd.Index:
        return self.data.columns[:-3]

    @property
    def dtypes(self) -> pd.Series:
        """The data types of the epoched data."""
        return self.data.dtypes[:-3]

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +389 to 438
@fill_doc
def filter_by_name(
self,
names: str | list[str],
col_name: str = "name",
reset_id: bool = False,
inplace: bool = False,
) -> Optional["Events"]:
"""
Filter events by matching values in a specified column.
This method selects only the events whose value in ``col_name`` matches
one or more of the provided ``names``. If no events match, a
``ValueError`` is raised.
Parameters
----------
names : str or list of str
Event name or list of event names to keep. Matching is exact
and case-sensitive.
col_name : str, optional
Name of the column in ``self.data`` to use for filtering.
Must exist in the ``Events`` instance's DataFrame.
Defaults to ``"name"``.
reset_id: bool = False, optional
Whether to reset event IDs after filtering.
Defaults to ``False``.
%(inplace)s
Returns
-------
%(events_or_none)s
"""
if col_name not in self.data.columns:
raise KeyError(f"No `{col_name}` column found in the instance.")

names = [names] if isinstance(names, str) else names
mask = self.data[col_name].isin(names)
if not mask.any():
raise ValueError(
f"No data found matching the specified event names {names}"
)

inst = self if inplace else self.copy()
inst.data = self.data[mask].copy()
if reset_id:
inst.data.index = pd.RangeIndex(
start=0, stop=len(inst.data), name=inst.data.index.name
)
return None if inplace else inst
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new method filter_by_name in the Events class lacks test coverage. Since other similar files in the tests directory have test coverage (e.g., test_streams.py tests interpolation and concatenation), this new filtering functionality should also be tested.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants