Repository navigation
Add lagged cross-correlation for TsdFrame - #640
Conversation
wulfdewolf
left a comment
There was a problem hiding this comment.
Hi @viranovskaya ,
Thanks for this! It looks good already.
I added a couple small comments, mostly about unneeded text or checks.
We are discussing whether we can allow a multiindex in TsdFrame, which could allow this function to return a TsdFrame instead of a pd.DataFrame.
For now the pd.DataFrame is fine though.
One thing I am uncertain about is what happens when there are nans in the continuous traces.
Can we document that in the docstring and state explicitly what happens?
|
|
||
| ## Lagged cross-correlations | ||
|
|
||
| Lagged cross-correlation measures how continuous signals vary together at different time offsets. The input is a regularly sampled `TsdFrame`; no binning is needed because its columns are already sampled on the same time axis. |
There was a problem hiding this comment.
'no binning is needed because its columns are already sampled on the same time axis.` can be dropped
| print(lagged_corr) | ||
| ``` | ||
|
|
||
| The output is a pandas DataFrame with lags in seconds as its index and signal pairs as its columns. Here the correlation peaks at `+0.1` seconds: positive lags mean that the second signal follows the first. |
There was a problem hiding this comment.
We definitely want a TsdFrame as output!
There was a problem hiding this comment.
Hmm scratch that for now, we can't yet handle a multiindex in a TsdFrame, we're discussing whether we should.
For now the DataFrame output is fine.
| raise TypeError("time_units must be a string.") | ||
|
|
||
| if np.iscomplexobj(data1.values) or np.iscomplexobj(data2.values): | ||
| raise TypeError("Lagged cross-correlation requires real-valued data.") |
There was a problem hiding this comment.
this can be dropped, should be handled by TsdFrame already
| ) | ||
|
|
||
| with pytest.raises(TypeError, match="real-valued data"): | ||
| nap.compute_lagged_crosscorrelation(frame, 1) |
|
Thanks for the review! I’ve shortened the user-guide sentence and added a Notes section to the docstring explaining the NaN behavior explicitly. NaNs are propagated, not omitted or interpolated. For each column pair and lag, the correlation is NaN if either signal contains a NaN among the overlapping observations pooled across the selected epochs. Pairs not involving the NaN-containing columns are unaffected, as are observations outside the selected epochs or lag overlap. I’ve added regression tests covering these cases for both single- and two-frame inputs. The DataFrame output is unchanged. I checked the complex-input validation before removing it. Locally, the full test suite passes: 5,330 passed and 274 skipped. The correlogram doctests and lint checks also pass. The full doctest run has one float-representation failure in |
| lagged_corr = nap.compute_lagged_crosscorrelation( | ||
| signals, windowsize=0.5, time_units="s" | ||
| ) | ||
| print(lagged_corr) |
There was a problem hiding this comment.
I would add a figure after with 2 panels. The first panel should show the signal data. The second panel should show the output correlation.
There was a problem hiding this comment.
Thanks, Guillaume — I added the two-panel figure in 69c1bb6, with the input signals above and the lagged correlation below. Does this address your suggestion?
|
I left one comment for adding figures. Otherwise it's good to me. |
|
Discussing with @wulfdewolf, we decided that it would make more sense now to merge |
|
Thank you for discussing this with @wulfdewolf. I’m happy to extend the PR to support both My plan is to preserve the existing Before proceeding, I’d like to confirm the parameter semantics for continuous inputs. I propose requiring I’ll add regression coverage for both input types and document the type-specific normalization, epoch handling, and zero-lag behavior. I’ll also address an unnecessary read of signal values during sampling-interval validation, so that this check operates on timestamps only. |
I am not sure I understand what do you mean here? |
|
Sorry, I combined two separate points in that paragraph. By regression coverage, I meant retaining the existing tests for The second point concerns input handling in the current continuous implementation. It calls This is a separate I/O optimization; it does not change the correlation calculation or the sampling requirements. |
|
Ok so for the |
|
@gviejo, I also found a separate issue in the Hilbert doctest. It compares the printed complex values exactly, so small floating-point differences make it fail locally. |
|
No I think this one can go in the same PR thanks. |
| ```{code-cell} ipython3 | ||
| rng = np.random.default_rng(1) | ||
| timestamps = np.arange(1000) / 100 | ||
| reference = rng.normal(size=len(timestamps)) | ||
| delayed = np.r_[np.zeros(10), reference[:-10]] | ||
| signals = nap.TsdFrame( | ||
| t=timestamps, | ||
| d=np.column_stack((reference, delayed)), | ||
| columns=["reference", "delayed"], | ||
| time_support=nap.IntervalSet(0, 10), | ||
| ) | ||
|
|
||
| lagged_corr = nap.compute_crosscorrelogram( | ||
| signals, windowsize=0.5, time_units="s" | ||
| ) | ||
| print(lagged_corr) |
There was a problem hiding this comment.
Can you split this in 2 parts? One part for the data generation, one part to show how to call the function?
| For continuous autocorrelation, pass the same frame to [`compute_autocorrelogram`](pynapple.process.correlograms.compute_autocorrelogram). It computes only same-column pairs; the zero-lag coefficient is one for nonconstant finite signals, not zero as in a spike autocorrelogram. | ||
|
|
||
| ```{code-cell} ipython3 | ||
| continuous_autocorr = nap.compute_autocorrelogram(signals, windowsize=0.5) | ||
| ``` | ||
|
|
| @@ -1,12 +1,13 @@ | |||
| """ | |||
| Functions to compute correlograms of timestamps data. | |||
| Functions to compute spike-train correlograms and continuous correlations. | |||
There was a problem hiding this comment.
| Functions to compute spike-train correlograms and continuous correlations. | |
| Functions to compute lagged correlations of continuous and timestamps data (correlograms). |
| >>> analytic_signal.shape | ||
| (200,) | ||
|
|
||
| Compare the first three complex values with an absolute tolerance for the | ||
| values rounded to six decimal places: | ||
|
|
||
| >>> expected = [0.0 + 0.168638j, 0.099833 - 0.424271j, 0.198669 - 0.397076j] | ||
| >>> np.allclose(analytic_signal.values[:3], expected, rtol=0, atol=1e-6) | ||
| True |
There was a problem hiding this comment.
This is not really useful. Can you remove it? We will fix the docstring in a different PR
| >>> analytic_signals.shape | ||
| (200, 2) | ||
| >>> expected = [ | ||
| ... [0.0 + 0.168638j, 1.0 - 0.109339j], | ||
| ... [0.099833 - 0.424271j, 0.995004 + 0.251168j], | ||
| ... [0.198669 - 0.397076j, 0.980067 + 0.292269j], | ||
| ... ] | ||
| >>> np.allclose(analytic_signals.values[:3], expected, rtol=0, atol=1e-6) | ||
| True |
| time_differences = data1.time_diff().values | ||
| if len(time_differences) == 0: | ||
| raise RuntimeError("The sampling interval could not be determined.") | ||
| sampling_interval = time_differences[0] |
There was a problem hiding this comment.
| sampling_interval = time_differences[0] | |
| sampling_interval = np.round(np.median(data1.time_diff(align="start").values), 9) |
Otherwise it gives a rounding error.
| Examples | ||
| -------- | ||
| >>> import pynapple as nap | ||
| >>> import numpy as np | ||
| >>> ts_group = nap.TsGroup({0: nap.Ts(t=np.sort(np.random.uniform(0, 10, 100)))}) | ||
| >>> autocorr = nap.compute_autocorrelogram(ts_group, binsize=0.01, windowsize=0.1) | ||
| >>> t = np.arange(100) / 10 | ||
| >>> frame = nap.TsdFrame(t=t, d=np.column_stack((np.sin(t), np.cos(t)))) | ||
| >>> continuous_autocorr = nap.compute_autocorrelogram(frame, windowsize=0.5) | ||
| >>> np.allclose(continuous_autocorr.loc[0], 1.0) | ||
| True |
There was a problem hiding this comment.
Can you split this in 2 parts?
| >>> crosscorr = nap.compute_crosscorrelogram(ts_group, binsize=0.01, windowsize=0.1) | ||
| >>> t = np.arange(100) / 10 | ||
| >>> frame = nap.TsdFrame(t=t, d=np.column_stack((np.sin(t), np.cos(t)))) | ||
| >>> continuous_crosscorr = nap.compute_crosscorrelogram(frame, windowsize=0.5) |
|
Thanks, Guillaume. The revisions are in a218cb1. I split the guide example into data generation and the function call, separated the spike-train and continuous-signal docstring examples, added the autocorrelation figure, and updated the module introduction. The Hilbert changes are removed; For the sampling interval, I used the median of start-aligned differences. I limited decimal rounding to timestamp-subtraction uncertainty: unconditional rounding to nine decimal places changes the lag spacing at 300 Hz and can make regularly sampled 30 kHz and 44.1 kHz inputs fail the regularity check. The regression tests cover those cases, shifted timestamp origins, and windows around a lag boundary. Local validation of the revised code: 5,466 tests passed, 274 skipped; all four correlogram doctests and the lint/spelling checks passed. The changed guide executes and the HTML build completes, with documentation diagnostics still present elsewhere. The core/process doctest run has one failure in the unchanged upstream Hilbert example because it compares exact printed complex values; I have left that separate, as requested. |
Closes #613.
This extends
compute_crosscorrelogramandcompute_autocorrelogramto accept regularly sampled, real-valuedTsdFrameinputs, as discussed in the review. It replaces the proposed standalonecompute_lagged_crosscorrelationfunction. ExistingTsGroupcalls and spike-correlogram behavior are preserved.Continuous signal behavior
binsizemust beNone(or omitted) andnormmust beTrue. An explicit bin size ornorm=FalseraisesValueError.The two functions share the continuous-correlation kernel. The sampling interval uses the median of start-aligned timestamp differences. Decimal rounding is limited to timestamp-subtraction uncertainty, so it does not change genuine fractional sampling intervals. Regression tests cover shifted timestamp origins, fractional sampling intervals, and windows on either side of a lag boundary.
The docstrings now separate spike-train and continuous-signal examples. In the user guide, data generation and the cross-correlation call are separate cells; the requested two-panel cross-correlation figure is retained, and a continuous-autocorrelation figure has been added.
Hilbert changes have been removed from this PR, as requested. The timestamp-only
time_diff()optimization remains in the separate PR #665. Sphinx configuration and CI workflows are unchanged.Local validation
apply_hilbert_transformexample, which compares exact printed complex values;signal.pymatches the base branch.These local results apply to commit
a218cb19587201ae4b69191471ff3cff97a6831a. Remote CI is tracked separately on that head.Companion documentation tracking: pynapple-org/claude-skills#11.