diff --git a/.buildinfo b/.buildinfo index fd9192ac..d3282abb 100644 --- a/.buildinfo +++ b/.buildinfo @@ -1,4 +1,4 @@ # Sphinx build info version 1 # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. -config: a6ac25651f02e78abf3a4ac84ba4923e +config: 713ec541478fa44eebb19d03a9a7ca96 tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/.doctrees/api/movement.analysis.kinematics.compute_acceleration.doctree b/.doctrees/api/movement.analysis.kinematics.compute_acceleration.doctree index 0e411518..c85d5acc 100644 Binary files a/.doctrees/api/movement.analysis.kinematics.compute_acceleration.doctree and b/.doctrees/api/movement.analysis.kinematics.compute_acceleration.doctree differ diff --git a/.doctrees/api/movement.analysis.kinematics.compute_displacement.doctree b/.doctrees/api/movement.analysis.kinematics.compute_displacement.doctree index 17934bea..6e1620af 100644 Binary files a/.doctrees/api/movement.analysis.kinematics.compute_displacement.doctree and b/.doctrees/api/movement.analysis.kinematics.compute_displacement.doctree differ diff --git a/.doctrees/api/movement.analysis.kinematics.compute_velocity.doctree b/.doctrees/api/movement.analysis.kinematics.compute_velocity.doctree index e48ae047..78f4ca95 100644 Binary files a/.doctrees/api/movement.analysis.kinematics.compute_velocity.doctree and b/.doctrees/api/movement.analysis.kinematics.compute_velocity.doctree differ diff --git a/.doctrees/api/movement.analysis.kinematics.doctree b/.doctrees/api/movement.analysis.kinematics.doctree new file mode 100644 index 00000000..4dd6f1c8 Binary files /dev/null and b/.doctrees/api/movement.analysis.kinematics.doctree differ diff --git a/.doctrees/api/movement.filtering.doctree b/.doctrees/api/movement.filtering.doctree new file mode 100644 index 00000000..c4348291 Binary files /dev/null and b/.doctrees/api/movement.filtering.doctree differ diff --git a/.doctrees/api/movement.filtering.filter_by_confidence.doctree b/.doctrees/api/movement.filtering.filter_by_confidence.doctree index 60ab551e..c9de1dc4 100644 Binary files a/.doctrees/api/movement.filtering.filter_by_confidence.doctree and b/.doctrees/api/movement.filtering.filter_by_confidence.doctree differ diff --git a/.doctrees/api/movement.filtering.interpolate_over_time.doctree b/.doctrees/api/movement.filtering.interpolate_over_time.doctree index b903f755..f7a8461c 100644 Binary files a/.doctrees/api/movement.filtering.interpolate_over_time.doctree and b/.doctrees/api/movement.filtering.interpolate_over_time.doctree differ diff --git a/.doctrees/api/movement.filtering.median_filter.doctree b/.doctrees/api/movement.filtering.median_filter.doctree index 8b0315f8..6213fa7d 100644 Binary files a/.doctrees/api/movement.filtering.median_filter.doctree and b/.doctrees/api/movement.filtering.median_filter.doctree differ diff --git a/.doctrees/api/movement.filtering.report_nan_values.doctree b/.doctrees/api/movement.filtering.report_nan_values.doctree deleted file mode 100644 index 3918f0b5..00000000 Binary files a/.doctrees/api/movement.filtering.report_nan_values.doctree and /dev/null differ diff --git a/.doctrees/api/movement.filtering.savgol_filter.doctree b/.doctrees/api/movement.filtering.savgol_filter.doctree index f1f29db5..e4d2a65c 100644 Binary files a/.doctrees/api/movement.filtering.savgol_filter.doctree and b/.doctrees/api/movement.filtering.savgol_filter.doctree differ diff --git a/.doctrees/api/movement.io.load_poses.doctree b/.doctrees/api/movement.io.load_poses.doctree new file mode 100644 index 00000000..acf7c20e Binary files /dev/null and b/.doctrees/api/movement.io.load_poses.doctree differ diff --git a/.doctrees/api/movement.io.load_poses.from_dlc_file.doctree b/.doctrees/api/movement.io.load_poses.from_dlc_file.doctree index 14ef626b..4462f534 100644 Binary files a/.doctrees/api/movement.io.load_poses.from_dlc_file.doctree and b/.doctrees/api/movement.io.load_poses.from_dlc_file.doctree differ diff --git a/.doctrees/api/movement.io.load_poses.from_dlc_style_df.doctree b/.doctrees/api/movement.io.load_poses.from_dlc_style_df.doctree index 188c5cf0..66a711f1 100644 Binary files a/.doctrees/api/movement.io.load_poses.from_dlc_style_df.doctree and b/.doctrees/api/movement.io.load_poses.from_dlc_style_df.doctree differ diff --git a/.doctrees/api/movement.io.load_poses.from_file.doctree b/.doctrees/api/movement.io.load_poses.from_file.doctree index eaa91318..14d140d0 100644 Binary files a/.doctrees/api/movement.io.load_poses.from_file.doctree and b/.doctrees/api/movement.io.load_poses.from_file.doctree differ diff --git a/.doctrees/api/movement.io.load_poses.from_lp_file.doctree b/.doctrees/api/movement.io.load_poses.from_lp_file.doctree index a9e22daa..6ae7b74d 100644 Binary files a/.doctrees/api/movement.io.load_poses.from_lp_file.doctree and b/.doctrees/api/movement.io.load_poses.from_lp_file.doctree differ diff --git a/.doctrees/api/movement.io.load_poses.from_numpy.doctree b/.doctrees/api/movement.io.load_poses.from_numpy.doctree index 9e4996b6..b063b314 100644 Binary files a/.doctrees/api/movement.io.load_poses.from_numpy.doctree and b/.doctrees/api/movement.io.load_poses.from_numpy.doctree differ diff --git a/.doctrees/api/movement.io.load_poses.from_sleap_file.doctree b/.doctrees/api/movement.io.load_poses.from_sleap_file.doctree index 1aaf85b0..3a1140ca 100644 Binary files a/.doctrees/api/movement.io.load_poses.from_sleap_file.doctree and b/.doctrees/api/movement.io.load_poses.from_sleap_file.doctree differ diff --git a/.doctrees/api/movement.io.save_poses.doctree b/.doctrees/api/movement.io.save_poses.doctree new file mode 100644 index 00000000..4cf8e205 Binary files /dev/null and b/.doctrees/api/movement.io.save_poses.doctree differ diff --git a/.doctrees/api/movement.io.save_poses.to_dlc_file.doctree b/.doctrees/api/movement.io.save_poses.to_dlc_file.doctree index 1d555985..a28600d8 100644 Binary files a/.doctrees/api/movement.io.save_poses.to_dlc_file.doctree and b/.doctrees/api/movement.io.save_poses.to_dlc_file.doctree differ diff --git a/.doctrees/api/movement.io.save_poses.to_dlc_style_df.doctree b/.doctrees/api/movement.io.save_poses.to_dlc_style_df.doctree index 8ea93860..4922942d 100644 Binary files a/.doctrees/api/movement.io.save_poses.to_dlc_style_df.doctree and b/.doctrees/api/movement.io.save_poses.to_dlc_style_df.doctree differ diff --git a/.doctrees/api/movement.io.save_poses.to_lp_file.doctree b/.doctrees/api/movement.io.save_poses.to_lp_file.doctree index 7559ba1a..87b28c5d 100644 Binary files a/.doctrees/api/movement.io.save_poses.to_lp_file.doctree and b/.doctrees/api/movement.io.save_poses.to_lp_file.doctree differ diff --git a/.doctrees/api/movement.io.save_poses.to_sleap_analysis_file.doctree b/.doctrees/api/movement.io.save_poses.to_sleap_analysis_file.doctree index 7220ec30..0470eb16 100644 Binary files a/.doctrees/api/movement.io.save_poses.to_sleap_analysis_file.doctree and b/.doctrees/api/movement.io.save_poses.to_sleap_analysis_file.doctree differ diff --git a/.doctrees/api/movement.io.validators.ValidDeepLabCutCSV.doctree b/.doctrees/api/movement.io.validators.ValidDeepLabCutCSV.doctree deleted file mode 100644 index 9c77cdb5..00000000 Binary files a/.doctrees/api/movement.io.validators.ValidDeepLabCutCSV.doctree and /dev/null differ diff --git a/.doctrees/api/movement.io.validators.ValidFile.doctree b/.doctrees/api/movement.io.validators.ValidFile.doctree deleted file mode 100644 index ba3c0773..00000000 Binary files a/.doctrees/api/movement.io.validators.ValidFile.doctree and /dev/null differ diff --git a/.doctrees/api/movement.io.validators.ValidHDF5.doctree b/.doctrees/api/movement.io.validators.ValidHDF5.doctree deleted file mode 100644 index 93acfb8f..00000000 Binary files a/.doctrees/api/movement.io.validators.ValidHDF5.doctree and /dev/null differ diff --git a/.doctrees/api/movement.io.validators.ValidPosesDataset.doctree b/.doctrees/api/movement.io.validators.ValidPosesDataset.doctree deleted file mode 100644 index f1d0d310..00000000 Binary files a/.doctrees/api/movement.io.validators.ValidPosesDataset.doctree and /dev/null differ diff --git a/.doctrees/api/movement.move_accessor.MovementDataset.doctree b/.doctrees/api/movement.move_accessor.MovementDataset.doctree index fc61aed3..f8164a72 100644 Binary files a/.doctrees/api/movement.move_accessor.MovementDataset.doctree and b/.doctrees/api/movement.move_accessor.MovementDataset.doctree differ diff --git a/.doctrees/api/movement.move_accessor.doctree b/.doctrees/api/movement.move_accessor.doctree new file mode 100644 index 00000000..1bc6d232 Binary files /dev/null and b/.doctrees/api/movement.move_accessor.doctree differ diff --git a/.doctrees/api/movement.sample_data.doctree b/.doctrees/api/movement.sample_data.doctree new file mode 100644 index 00000000..22dfa2f5 Binary files /dev/null and b/.doctrees/api/movement.sample_data.doctree differ diff --git a/.doctrees/api/movement.sample_data.fetch_dataset.doctree b/.doctrees/api/movement.sample_data.fetch_dataset.doctree index e7658a5c..8766380c 100644 Binary files a/.doctrees/api/movement.sample_data.fetch_dataset.doctree and b/.doctrees/api/movement.sample_data.fetch_dataset.doctree differ diff --git a/.doctrees/api/movement.sample_data.fetch_dataset_paths.doctree b/.doctrees/api/movement.sample_data.fetch_dataset_paths.doctree index bb8896ba..91accc1c 100644 Binary files a/.doctrees/api/movement.sample_data.fetch_dataset_paths.doctree and b/.doctrees/api/movement.sample_data.fetch_dataset_paths.doctree differ diff --git a/.doctrees/api/movement.sample_data.list_datasets.doctree b/.doctrees/api/movement.sample_data.list_datasets.doctree index b45123d9..8137f5be 100644 Binary files a/.doctrees/api/movement.sample_data.list_datasets.doctree and b/.doctrees/api/movement.sample_data.list_datasets.doctree differ diff --git a/.doctrees/api/movement.logging.configure_logging.doctree b/.doctrees/api/movement.utils.logging.configure_logging.doctree similarity index 70% rename from .doctrees/api/movement.logging.configure_logging.doctree rename to .doctrees/api/movement.utils.logging.configure_logging.doctree index 6bf702d5..546d9ecf 100644 Binary files a/.doctrees/api/movement.logging.configure_logging.doctree and b/.doctrees/api/movement.utils.logging.configure_logging.doctree differ diff --git a/.doctrees/api/movement.utils.logging.doctree b/.doctrees/api/movement.utils.logging.doctree new file mode 100644 index 00000000..277a38f4 Binary files /dev/null and b/.doctrees/api/movement.utils.logging.doctree differ diff --git a/.doctrees/api/movement.logging.log_error.doctree b/.doctrees/api/movement.utils.logging.log_error.doctree similarity index 70% rename from .doctrees/api/movement.logging.log_error.doctree rename to .doctrees/api/movement.utils.logging.log_error.doctree index 4a61cfdc..56ec4104 100644 Binary files a/.doctrees/api/movement.logging.log_error.doctree and b/.doctrees/api/movement.utils.logging.log_error.doctree differ diff --git a/.doctrees/api/movement.utils.logging.log_to_attrs.doctree b/.doctrees/api/movement.utils.logging.log_to_attrs.doctree new file mode 100644 index 00000000..f9ef85b4 Binary files /dev/null and b/.doctrees/api/movement.utils.logging.log_to_attrs.doctree differ diff --git a/.doctrees/api/movement.logging.log_warning.doctree b/.doctrees/api/movement.utils.logging.log_warning.doctree similarity index 67% rename from .doctrees/api/movement.logging.log_warning.doctree rename to .doctrees/api/movement.utils.logging.log_warning.doctree index 4e5c124a..bf16f4e7 100644 Binary files a/.doctrees/api/movement.logging.log_warning.doctree and b/.doctrees/api/movement.utils.logging.log_warning.doctree differ diff --git a/.doctrees/api/movement.utils.reports.calculate_nan_stats.doctree b/.doctrees/api/movement.utils.reports.calculate_nan_stats.doctree new file mode 100644 index 00000000..6ade1b66 Binary files /dev/null and b/.doctrees/api/movement.utils.reports.calculate_nan_stats.doctree differ diff --git a/.doctrees/api/movement.utils.reports.doctree b/.doctrees/api/movement.utils.reports.doctree new file mode 100644 index 00000000..3d29b2ef Binary files /dev/null and b/.doctrees/api/movement.utils.reports.doctree differ diff --git a/.doctrees/api/movement.utils.reports.report_nan_values.doctree b/.doctrees/api/movement.utils.reports.report_nan_values.doctree new file mode 100644 index 00000000..8d390fcf Binary files /dev/null and b/.doctrees/api/movement.utils.reports.report_nan_values.doctree differ diff --git a/.doctrees/api/movement.utils.vector.cart2pol.doctree b/.doctrees/api/movement.utils.vector.cart2pol.doctree index 87578f47..11bbf2b9 100644 Binary files a/.doctrees/api/movement.utils.vector.cart2pol.doctree and b/.doctrees/api/movement.utils.vector.cart2pol.doctree differ diff --git a/.doctrees/api/movement.utils.vector.doctree b/.doctrees/api/movement.utils.vector.doctree new file mode 100644 index 00000000..7a5a4c40 Binary files /dev/null and b/.doctrees/api/movement.utils.vector.doctree differ diff --git a/.doctrees/api/movement.utils.vector.pol2cart.doctree b/.doctrees/api/movement.utils.vector.pol2cart.doctree index 5228be26..7e2d93f1 100644 Binary files a/.doctrees/api/movement.utils.vector.pol2cart.doctree and b/.doctrees/api/movement.utils.vector.pol2cart.doctree differ diff --git a/.doctrees/api/movement.validators.datasets.ValidBboxesDataset.doctree b/.doctrees/api/movement.validators.datasets.ValidBboxesDataset.doctree new file mode 100644 index 00000000..f6abcfa5 Binary files /dev/null and b/.doctrees/api/movement.validators.datasets.ValidBboxesDataset.doctree differ diff --git a/.doctrees/api/movement.validators.datasets.ValidPosesDataset.doctree b/.doctrees/api/movement.validators.datasets.ValidPosesDataset.doctree new file mode 100644 index 00000000..de71da6c Binary files /dev/null and b/.doctrees/api/movement.validators.datasets.ValidPosesDataset.doctree differ diff --git a/.doctrees/api/movement.validators.datasets.doctree b/.doctrees/api/movement.validators.datasets.doctree new file mode 100644 index 00000000..724692fa Binary files /dev/null and b/.doctrees/api/movement.validators.datasets.doctree differ diff --git a/.doctrees/api/movement.validators.files.ValidDeepLabCutCSV.doctree b/.doctrees/api/movement.validators.files.ValidDeepLabCutCSV.doctree new file mode 100644 index 00000000..9b1297e0 Binary files /dev/null and b/.doctrees/api/movement.validators.files.ValidDeepLabCutCSV.doctree differ diff --git a/.doctrees/api/movement.validators.files.ValidFile.doctree b/.doctrees/api/movement.validators.files.ValidFile.doctree new file mode 100644 index 00000000..870750a3 Binary files /dev/null and b/.doctrees/api/movement.validators.files.ValidFile.doctree differ diff --git a/.doctrees/api/movement.validators.files.ValidHDF5.doctree b/.doctrees/api/movement.validators.files.ValidHDF5.doctree new file mode 100644 index 00000000..713902b0 Binary files /dev/null and b/.doctrees/api/movement.validators.files.ValidHDF5.doctree differ diff --git a/.doctrees/api/movement.validators.files.doctree b/.doctrees/api/movement.validators.files.doctree new file mode 100644 index 00000000..0250c4b4 Binary files /dev/null and b/.doctrees/api/movement.validators.files.doctree differ diff --git a/.doctrees/api_index.doctree b/.doctrees/api_index.doctree index 73ac3be2..b057727e 100644 Binary files a/.doctrees/api_index.doctree and b/.doctrees/api_index.doctree differ diff --git a/.doctrees/community/contributing.doctree b/.doctrees/community/contributing.doctree index c1f616fe..c69f634f 100644 Binary files a/.doctrees/community/contributing.doctree and b/.doctrees/community/contributing.doctree differ diff --git a/.doctrees/environment.pickle b/.doctrees/environment.pickle index e79c3521..9be7f1c8 100644 Binary files a/.doctrees/environment.pickle and b/.doctrees/environment.pickle differ diff --git a/.doctrees/examples/compute_kinematics.doctree b/.doctrees/examples/compute_kinematics.doctree index 7cc16de7..0353296b 100644 Binary files a/.doctrees/examples/compute_kinematics.doctree and b/.doctrees/examples/compute_kinematics.doctree differ diff --git a/.doctrees/examples/compute_polar_coordinates.doctree b/.doctrees/examples/compute_polar_coordinates.doctree index dad3ad19..43d0439d 100644 Binary files a/.doctrees/examples/compute_polar_coordinates.doctree and b/.doctrees/examples/compute_polar_coordinates.doctree differ diff --git a/.doctrees/examples/filter_and_interpolate.doctree b/.doctrees/examples/filter_and_interpolate.doctree index 399b412f..93426522 100644 Binary files a/.doctrees/examples/filter_and_interpolate.doctree and b/.doctrees/examples/filter_and_interpolate.doctree differ diff --git a/.doctrees/examples/index.doctree b/.doctrees/examples/index.doctree index 078235b5..ea526bb0 100644 Binary files a/.doctrees/examples/index.doctree and b/.doctrees/examples/index.doctree differ diff --git a/.doctrees/examples/load_and_explore_poses.doctree b/.doctrees/examples/load_and_explore_poses.doctree index 1ffaf1b1..ce8dbc9f 100644 Binary files a/.doctrees/examples/load_and_explore_poses.doctree and b/.doctrees/examples/load_and_explore_poses.doctree differ diff --git a/.doctrees/examples/sg_execution_times.doctree b/.doctrees/examples/sg_execution_times.doctree index 520b2bab..75ed5ab6 100644 Binary files a/.doctrees/examples/sg_execution_times.doctree and b/.doctrees/examples/sg_execution_times.doctree differ diff --git a/.doctrees/examples/smooth.doctree b/.doctrees/examples/smooth.doctree index 3a3956b8..a6f770a0 100644 Binary files a/.doctrees/examples/smooth.doctree and b/.doctrees/examples/smooth.doctree differ diff --git a/.doctrees/getting_started/installation.doctree b/.doctrees/getting_started/installation.doctree index 64b69ed7..bf0a8131 100644 Binary files a/.doctrees/getting_started/installation.doctree and b/.doctrees/getting_started/installation.doctree differ diff --git a/.doctrees/getting_started/movement_dataset.doctree b/.doctrees/getting_started/movement_dataset.doctree index b80776cf..714ba199 100644 Binary files a/.doctrees/getting_started/movement_dataset.doctree and b/.doctrees/getting_started/movement_dataset.doctree differ diff --git a/.doctrees/getting_started/sample_data.doctree b/.doctrees/getting_started/sample_data.doctree index 9d261eee..7a4bb8d9 100644 Binary files a/.doctrees/getting_started/sample_data.doctree and b/.doctrees/getting_started/sample_data.doctree differ diff --git a/.doctrees/sg_execution_times.doctree b/.doctrees/sg_execution_times.doctree index 2e91e951..547084a9 100644 Binary files a/.doctrees/sg_execution_times.doctree and b/.doctrees/sg_execution_times.doctree differ diff --git a/_downloads/07f4b1b69497c93530bbe772d7eb535a/smooth.py b/_downloads/07f4b1b69497c93530bbe772d7eb535a/smooth.py index 3bb44162..f9b969b5 100644 --- a/_downloads/07f4b1b69497c93530bbe772d7eb535a/smooth.py +++ b/_downloads/07f4b1b69497c93530bbe772d7eb535a/smooth.py @@ -12,11 +12,6 @@ from scipy.signal import welch from movement import sample_data -from movement.filtering import ( - interpolate_over_time, - median_filter, - savgol_filter, -) # %% # Load a sample dataset @@ -30,9 +25,10 @@ print(ds_wasp) # %% -# We see that the dataset contains a single individual (a wasp) with two -# keypoints tracked in 2D space. The video was recorded at 40 fps and lasts for -# ~27 seconds. +# We see that the dataset contains the 2D pose tracks and confidence scores +# for a single wasp, generated with DeepLabCut. The wasp is tracked at two +# keypoints: "head" and "stinger" in a video that was recorded at 40 fps and +# lasts for approximately 27 seconds. # %% # Define a plotting function @@ -67,7 +63,7 @@ def plot_raw_and_smooth_timeseries_and_psd( fig, ax = plt.subplots(2, 1, figsize=(10, 6)) for ds, color, label in zip( - [ds_raw, ds_smooth], ["k", "r"], ["raw", "smooth"] + [ds_raw, ds_smooth], ["k", "r"], ["raw", "smooth"], strict=False ): # plot position time series pos = ds.position.sel(**selection) @@ -80,9 +76,10 @@ def plot_raw_and_smooth_timeseries_and_psd( label=f"{label} {space}", ) - # generate interpolated dataset to avoid NaNs in the PSD calculation - ds_interp = interpolate_over_time(ds, max_gap=None, print_report=False) - pos_interp = ds_interp.position.sel(**selection) + # interpolate data to remove NaNs in the PSD calculation + pos_interp = ds.sel(**selection).move.interpolate_over_time( + print_report=False + ) # compute and plot the PSD freq, psd = welch(pos_interp, fs=ds.fps, nperseg=256) ax[1].semilogy( @@ -111,12 +108,36 @@ def plot_raw_and_smooth_timeseries_and_psd( # %% # Smoothing with a median filter # ------------------------------ -# Here we use the :py:func:`movement.filtering.median_filter` function to -# apply a rolling window median filter to the wasp dataset. -# The ``window_length`` parameter is defined in seconds (according to the -# ``time_unit`` dataset attribute). +# Using the +# :py:meth:`median_filter()\ +# ` +# method of the ``move`` accessor, +# we apply a rolling window median filter over a 0.1-second window +# (4 frames) to the wasp dataset. +# As the ``window`` parameter is defined in *number of observations*, +# we can simply multiply the desired time window by the frame rate +# of the video. We will also create a copy of the dataset to avoid +# modifying the original data. + +window = int(0.1 * ds_wasp.fps) +ds_wasp_smooth = ds_wasp.copy() +ds_wasp_smooth.update({"position": ds_wasp_smooth.move.median_filter(window)}) -ds_wasp_medfilt = median_filter(ds_wasp, window_length=0.1) +# %% +# .. note:: +# The ``move`` accessor :py:meth:`median_filter()\ +# ` +# method is a convenience method that applies +# :py:func:`movement.filtering.median_filter` +# to the ``position`` data variable. +# The equivalent function call using the +# :py:mod:`movement.filtering` module would be: +# +# .. code-block:: python +# +# from movement.filtering import median_filter +# +# ds_wasp_smooth.update({"position": median_filter(position, window)}) # %% # We see from the printed report that the dataset has no missing values @@ -124,7 +145,7 @@ def plot_raw_and_smooth_timeseries_and_psd( # median filter in the time and frequency domains. plot_raw_and_smooth_timeseries_and_psd( - ds_wasp, ds_wasp_medfilt, keypoint="stinger" + ds_wasp, ds_wasp_smooth, keypoint="stinger" ) # %% @@ -142,8 +163,8 @@ def plot_raw_and_smooth_timeseries_and_psd( # %% # Choosing parameters for the median filter # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -# You can control the behaviour of :py:func:`movement.filtering.median_filter` -# via two parameters: ``window_length`` and ``min_periods``. +# We can control the behaviour of the median filter +# via two parameters: ``window`` and ``min_periods``. # To better understand the effect of these parameters, let's use a # dataset that contains missing values. @@ -154,10 +175,15 @@ def plot_raw_and_smooth_timeseries_and_psd( # The dataset contains a single mouse with six keypoints tracked in # 2D space. The video was recorded at 30 fps and lasts for ~616 seconds. We can # see that there are some missing values, indicated as "nan" in the -# printed dataset. Let's apply the median filter to this dataset, with -# the ``window_length`` set to 0.1 seconds. - -ds_mouse_medfilt = median_filter(ds_mouse, window_length=0.1) +# printed dataset. +# Let's apply the median filter over a 0.1-second window (3 frames) +# to the dataset. + +window = int(0.1 * ds_mouse.fps) +ds_mouse_smooth = ds_mouse.copy() +ds_mouse_smooth.update( + {"position": ds_mouse_smooth.move.median_filter(window)} +) # %% # The report informs us that the raw data contains NaN values, most of which @@ -172,7 +198,9 @@ def plot_raw_and_smooth_timeseries_and_psd( # For example, setting ``min_periods=2`` means that two non-NaN values in the # window are sufficient for the median to be calculated. Let's try this. -ds_mouse_medfilt = median_filter(ds_mouse, window_length=0.1, min_periods=2) +ds_mouse_smooth.update( + {"position": ds_mouse.move.median_filter(window, min_periods=2)} +) # %% # We see that this time the number of NaN values has decreased @@ -183,32 +211,36 @@ def plot_raw_and_smooth_timeseries_and_psd( # parts of the data. plot_raw_and_smooth_timeseries_and_psd( - ds_mouse, ds_mouse_medfilt, keypoint="snout", time_range=slice(0, 80) + ds_mouse, ds_mouse_smooth, keypoint="snout", time_range=slice(0, 80) ) # %% # The smoothing once again reduces the power of high-frequency components, but # the resulting time series stays quite close to the raw data. # -# What happens if we increase the ``window_length`` to 2 seconds? +# What happens if we increase the ``window`` to 2 seconds (60 frames)? -ds_mouse_medfilt = median_filter(ds_mouse, window_length=2, min_periods=2) +window = int(2 * ds_mouse.fps) +ds_mouse_smooth.update( + {"position": ds_mouse.move.median_filter(window, min_periods=2)} +) # %% -# The number of NaN values has decreased even further. That's because the -# chance of finding at least 2 valid values within a 2 second window is -# quite high. Let's plot the results for the same keypoint and time range +# The number of NaN values has decreased even further. +# That's because the chance of finding at least 2 valid values within +# a 2-second window (i.e. 60 frames) is quite high. +# Let's plot the results for the same keypoint and time range # as before. plot_raw_and_smooth_timeseries_and_psd( - ds_mouse, ds_mouse_medfilt, keypoint="snout", time_range=slice(0, 80) + ds_mouse, ds_mouse_smooth, keypoint="snout", time_range=slice(0, 80) ) # %% # We see that the filtered time series is much smoother and it has even # "bridged" over some small gaps. That said, it often deviates from the raw # data, in ways that may not be desirable, depending on the application. -# That means that our choice of ``window_length`` may be too large. -# In general, you should choose a ``window_length`` that is small enough to +# Here, our choice of ``window`` may be too large. +# In general, you should choose a ``window`` that is small enough to # preserve the original data structure, but large enough to remove # "spikes" and high-frequency noise. Always inspect the results to ensure # that the filter is not removing important features. @@ -216,18 +248,27 @@ def plot_raw_and_smooth_timeseries_and_psd( # %% # Smoothing with a Savitzky-Golay filter # -------------------------------------- -# Here we use the :py:func:`movement.filtering.savgol_filter` function, -# which is a wrapper around :py:func:`scipy.signal.savgol_filter`. +# Here we use the +# :py:meth:`savgol_filter()\ +# ` +# method of the ``move`` accessor, which is a convenience method that applies +# :py:func:`movement.filtering.savgol_filter` +# (a wrapper around :py:func:`scipy.signal.savgol_filter`), +# to the ``position`` data variable. # The Savitzky-Golay filter is a polynomial smoothing filter that can be -# applied to time series data on a rolling window basis. A polynomial of -# degree ``polyorder`` is fitted to the data in each window of length -# ``window_length``, and the value of the polynomial at the center of the -# window is used as the output value. +# applied to time series data on a rolling window basis. +# A polynomial with a degree specified by ``polyorder`` is applied to each +# data segment defined by the size ``window``. +# The value of the polynomial at the midpoint of each ``window`` is then +# used as the output value. # -# Let's try it on the mouse dataset. - -ds_mouse_savgol = savgol_filter(ds_mouse, window_length=0.2, polyorder=2) +# Let's try it on the mouse dataset, this time using a 0.2-second +# window (i.e. 6 frames) and the default ``polyorder=2`` for smoothing. +# As before, we first compute the corresponding number of observations +# to be used as the ``window`` size. +window = int(0.2 * ds_mouse.fps) +ds_mouse_smooth.update({"position": ds_mouse.move.savgol_filter(window)}) # %% # We see that the number of NaN values has increased after filtering. This is @@ -238,22 +279,21 @@ def plot_raw_and_smooth_timeseries_and_psd( # effects in the time and frequency domains. plot_raw_and_smooth_timeseries_and_psd( - ds_mouse, ds_mouse_savgol, keypoint="snout", time_range=slice(0, 80) + ds_mouse, ds_mouse_smooth, keypoint="snout", time_range=slice(0, 80) ) # %% # Once again, the power of high-frequency components has been reduced, but more # missing values have been introduced. # %% -# Now let's take a look at the wasp dataset. +# Now let's apply the same Savitzky-Golay filter to the wasp dataset. -ds_wasp_savgol = savgol_filter(ds_wasp, window_length=0.2, polyorder=2) +window = int(0.2 * ds_wasp.fps) +ds_wasp_smooth.update({"position": ds_wasp.move.savgol_filter(window)}) # %% plot_raw_and_smooth_timeseries_and_psd( - ds_wasp, - ds_wasp_savgol, - keypoint="stinger", + ds_wasp, ds_wasp_smooth, keypoint="stinger" ) # %% # This example shows two important limitations of the Savitzky-Golay filter. @@ -261,7 +301,7 @@ def plot_raw_and_smooth_timeseries_and_psd( # example, focus on what happens around the sudden drop in position # during the final second. Second, the PSD appears to have large periodic # drops at certain frequencies. Both of these effects vary with the -# choice of ``window_length`` and ``polyorder``. You can read more about these +# choice of ``window`` and ``polyorder``. You can read more about these # and other limitations of the Savitzky-Golay filter in # `this paper `_. @@ -269,33 +309,41 @@ def plot_raw_and_smooth_timeseries_and_psd( # %% # Combining multiple smoothing filters # ------------------------------------ -# You can also combine multiple smoothing filters by applying them +# We can also combine multiple smoothing filters by applying them # sequentially. For example, we can first apply the median filter with a small -# ``window_length`` to remove "spikes" and then apply the Savitzky-Golay filter -# with a larger ``window_length`` to further smooth the data. +# ``window`` to remove "spikes" and then apply the Savitzky-Golay filter +# with a larger ``window`` to further smooth the data. # Between the two filters, we can interpolate over small gaps to avoid the # excessive proliferation of NaN values. Let's try this on the mouse dataset. -# First, let's apply the median filter. +# First, we will apply the median filter. -ds_mouse_medfilt = median_filter(ds_mouse, window_length=0.1, min_periods=2) +window = int(0.1 * ds_mouse.fps) +ds_mouse_smooth.update( + {"position": ds_mouse.move.median_filter(window, min_periods=2)} +) # %% -# Next, let's linearly interpolate over gaps smaller than 1 second. +# Next, let's linearly interpolate over gaps smaller than 1 second (30 frames). -ds_mouse_medfilt_interp = interpolate_over_time(ds_mouse_medfilt, max_gap=1) +ds_mouse_smooth.update( + {"position": ds_mouse_smooth.move.interpolate_over_time(max_gap=30)} +) # %% -# Finally, let's apply the Savitzky-Golay filter. +# Finally, let's apply the Savitzky-Golay filter over a 0.4-second window +# (12 frames). -ds_mouse_medfilt_interp_savgol = savgol_filter( - ds_mouse_medfilt_interp, window_length=0.4, polyorder=2 +window = int(0.4 * ds_mouse.fps) +ds_mouse_smooth.update( + {"position": ds_mouse_smooth.move.savgol_filter(window)} ) # %% -# A record of all applied operations is stored in the dataset's ``log`` -# attribute. Let's inspect it to summarise what we've done. +# A record of all applied operations is stored in the ``log`` attribute of the +# ``ds_mouse_smooth.position`` data array. Let's inspect it to summarise +# what we've done. -for entry in ds_mouse_medfilt_interp_savgol.log: +for entry in ds_mouse_smooth.position.log: print(entry) # %% @@ -304,7 +352,7 @@ def plot_raw_and_smooth_timeseries_and_psd( plot_raw_and_smooth_timeseries_and_psd( ds_mouse, - ds_mouse_medfilt_interp_savgol, + ds_mouse_smooth, keypoint="snout", time_range=slice(0, 80), ) @@ -312,3 +360,9 @@ def plot_raw_and_smooth_timeseries_and_psd( # %% # Feel free to play around with the parameters of the applied filters and to # also look at other keypoints and time ranges. + +# %% +# .. seealso:: +# :ref:`examples/filter_and_interpolate:Filtering multiple data variables` +# in the +# :ref:`sphx_glr_examples_filter_and_interpolate.py` example. diff --git a/_downloads/085258163d1f42f74bf76da18a848b11/compute_kinematics.ipynb b/_downloads/085258163d1f42f74bf76da18a848b11/compute_kinematics.ipynb index 600b53fa..d787743a 100644 --- a/_downloads/085258163d1f42f74bf76da18a848b11/compute_kinematics.ipynb +++ b/_downloads/085258163d1f42f74bf76da18a848b11/compute_kinematics.ipynb @@ -83,7 +83,7 @@ }, "outputs": [], "source": [ - "fig, ax = plt.subplots(1, 1)\nfor mouse_name, col in zip(position.individuals.values, [\"r\", \"g\", \"b\"]):\n ax.plot(\n position.sel(individuals=mouse_name, space=\"x\"),\n position.sel(individuals=mouse_name, space=\"y\"),\n linestyle=\"-\",\n marker=\".\",\n markersize=2,\n linewidth=0.5,\n c=col,\n label=mouse_name,\n )\n ax.invert_yaxis()\n ax.set_xlabel(\"x (pixels)\")\n ax.set_ylabel(\"y (pixels)\")\n ax.axis(\"equal\")\n ax.legend()" + "fig, ax = plt.subplots(1, 1)\nfor mouse_name, col in zip(\n position.individuals.values, [\"r\", \"g\", \"b\"], strict=False\n):\n ax.plot(\n position.sel(individuals=mouse_name, space=\"x\"),\n position.sel(individuals=mouse_name, space=\"y\"),\n linestyle=\"-\",\n marker=\".\",\n markersize=2,\n linewidth=0.5,\n c=col,\n label=mouse_name,\n )\n ax.invert_yaxis()\n ax.set_xlabel(\"x (pixels)\")\n ax.set_ylabel(\"y (pixels)\")\n ax.axis(\"equal\")\n ax.legend()" ] }, { @@ -108,7 +108,7 @@ }, "outputs": [], "source": [ - "fig, axes = plt.subplots(3, 1, sharey=True)\nfor mouse_name, ax in zip(position.individuals.values, axes):\n sc = ax.scatter(\n position.sel(individuals=mouse_name, space=\"x\"),\n position.sel(individuals=mouse_name, space=\"y\"),\n s=2,\n c=position.time,\n cmap=\"viridis\",\n )\n ax.invert_yaxis()\n ax.set_title(mouse_name)\n ax.set_xlabel(\"x (pixels)\")\n ax.set_ylabel(\"y (pixels)\")\n ax.axis(\"equal\")\n fig.colorbar(sc, ax=ax, label=\"time (s)\")\nfig.tight_layout()" + "fig, axes = plt.subplots(3, 1, sharey=True)\nfor mouse_name, ax in zip(position.individuals.values, axes, strict=False):\n sc = ax.scatter(\n position.sel(individuals=mouse_name, space=\"x\"),\n position.sel(individuals=mouse_name, space=\"y\"),\n s=2,\n c=position.time,\n cmap=\"viridis\",\n )\n ax.invert_yaxis()\n ax.set_title(mouse_name)\n ax.set_xlabel(\"x (pixels)\")\n ax.set_ylabel(\"y (pixels)\")\n ax.axis(\"equal\")\n fig.colorbar(sc, ax=ax, label=\"time (s)\")\nfig.tight_layout()" ] }, { @@ -122,7 +122,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We can also easily plot the components of the position vector against time\nusing ``xarray``'s built-in plotting methods. We use ``squeeze()`` to\nremove the dimension of length 1 from the data (the keypoints dimension).\n\n" + "We can also easily plot the components of the position vector against time\nusing ``xarray``'s built-in plotting methods. We use\n:py:meth:`xarray.DataArray.squeeze` to\nremove the dimension of length 1 from the data (the ``keypoints`` dimension).\n\n" ] }, { @@ -172,7 +172,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Notice that we could also compute the displacement (and all the other\nkinematic variables) using the kinematics module:\n\n" + "Notice that we could also compute the displacement (and all the other\nkinematic variables) using the :py:mod:`movement.analysis.kinematics` module:\n\n" ] }, { @@ -315,7 +315,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We can plot the components of the velocity vector against time\nusing ``xarray``'s built-in plotting methods. We use ``squeeze()`` to\nremove the dimension of length 1 from the data (the keypoints dimension).\n\n" + "We can plot the components of the velocity vector against time\nusing ``xarray``'s built-in plotting methods. We use\n:py:meth:`xarray.DataArray.squeeze` to\nremove the dimension of length 1 from the data (the ``keypoints`` dimension).\n\n" ] }, { @@ -351,7 +351,7 @@ }, "outputs": [], "source": [ - "fig, axes = plt.subplots(3, 1, sharex=True, sharey=True)\nfor mouse_name, ax in zip(velocity.individuals.values, axes):\n # compute the norm of the velocity vector for one mouse\n speed_one_mouse = np.linalg.norm(\n velocity.sel(individuals=mouse_name, space=[\"x\", \"y\"]).squeeze(),\n axis=1,\n )\n # plot speed against time\n ax.plot(speed_one_mouse)\n ax.set_title(mouse_name)\n ax.set_xlabel(\"time (s)\")\n ax.set_ylabel(\"speed (px/s)\")\nfig.tight_layout()" + "fig, axes = plt.subplots(3, 1, sharex=True, sharey=True)\nfor mouse_name, ax in zip(velocity.individuals.values, axes, strict=False):\n # compute the norm of the velocity vector for one mouse\n speed_one_mouse = np.linalg.norm(\n velocity.sel(individuals=mouse_name, space=[\"x\", \"y\"]).squeeze(),\n axis=1,\n )\n # plot speed against time\n ax.plot(speed_one_mouse)\n ax.set_title(mouse_name)\n ax.set_xlabel(\"time (s)\")\n ax.set_ylabel(\"speed (px/s)\")\nfig.tight_layout()" ] }, { @@ -412,7 +412,7 @@ }, "outputs": [], "source": [ - "fig, axes = plt.subplots(3, 1, sharex=True, sharey=True)\nfor mouse_name, ax in zip(accel.individuals.values, axes):\n # plot x-component of acceleration vector\n ax.plot(\n accel.sel(individuals=mouse_name, space=[\"x\"]).squeeze(),\n label=\"ax\",\n )\n # plot y-component of acceleration vector\n ax.plot(\n accel.sel(individuals=mouse_name, space=[\"y\"]).squeeze(),\n label=\"ay\",\n )\n ax.set_title(mouse_name)\n ax.set_xlabel(\"time (s)\")\n ax.set_ylabel(\"speed (px/s**2)\")\n ax.legend(loc=\"center right\", bbox_to_anchor=(1.07, 1.07))\nfig.tight_layout()" + "fig, axes = plt.subplots(3, 1, sharex=True, sharey=True)\nfor mouse_name, ax in zip(accel.individuals.values, axes, strict=False):\n # plot x-component of acceleration vector\n ax.plot(\n accel.sel(individuals=mouse_name, space=[\"x\"]).squeeze(),\n label=\"ax\",\n )\n # plot y-component of acceleration vector\n ax.plot(\n accel.sel(individuals=mouse_name, space=[\"y\"]).squeeze(),\n label=\"ay\",\n )\n ax.set_title(mouse_name)\n ax.set_xlabel(\"time (s)\")\n ax.set_ylabel(\"speed (px/s**2)\")\n ax.legend(loc=\"center right\", bbox_to_anchor=(1.07, 1.07))\nfig.tight_layout()" ] }, { @@ -430,7 +430,7 @@ }, "outputs": [], "source": [ - "fig, axes = plt.subplots(3, 1, sharex=True, sharey=True)\nfor mouse_name, ax in zip(accel.individuals.values, axes):\n # compute norm of the acceleration vector for one mouse\n accel_one_mouse = np.linalg.norm(\n accel.sel(individuals=mouse_name, space=[\"x\", \"y\"]).squeeze(),\n axis=1,\n )\n\n # plot acceleration against time\n ax.plot(accel_one_mouse)\n ax.set_title(mouse_name)\n ax.set_xlabel(\"time (s)\")\n ax.set_ylabel(\"accel (px/s**2)\")\nfig.tight_layout()" + "fig, axes = plt.subplots(3, 1, sharex=True, sharey=True)\nfor mouse_name, ax in zip(accel.individuals.values, axes, strict=False):\n # compute norm of the acceleration vector for one mouse\n accel_one_mouse = np.linalg.norm(\n accel.sel(individuals=mouse_name, space=[\"x\", \"y\"]).squeeze(),\n axis=1,\n )\n\n # plot acceleration against time\n ax.plot(accel_one_mouse)\n ax.set_title(mouse_name)\n ax.set_xlabel(\"time (s)\")\n ax.set_ylabel(\"accel (px/s**2)\")\nfig.tight_layout()" ] } ], diff --git a/_downloads/122338c6db2328ed9eeb5e9961344704/filter_and_interpolate.ipynb b/_downloads/122338c6db2328ed9eeb5e9961344704/filter_and_interpolate.ipynb index bd2d7e2e..2e6eb7a4 100644 --- a/_downloads/122338c6db2328ed9eeb5e9961344704/filter_and_interpolate.ipynb +++ b/_downloads/122338c6db2328ed9eeb5e9961344704/filter_and_interpolate.ipynb @@ -22,7 +22,7 @@ }, "outputs": [], "source": [ - "from movement import sample_data\nfrom movement.filtering import filter_by_confidence, interpolate_over_time" + "from movement import sample_data" ] }, { @@ -47,14 +47,14 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We can see that this dataset contains the 2D pose tracks and confidence\nscores for a single wasp, generated with DeepLabCut. There are 2 keypoints:\n\"head\" and \"stinger\".\n\n" + "We see that the dataset contains the 2D pose tracks and confidence scores\nfor a single wasp, generated with DeepLabCut. The wasp is tracked at two\nkeypoints: \"head\" and \"stinger\" in a video that was recorded at 40 fps and\nlasts for approximately 27 seconds.\n\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Visualise the pose tracks\n\n" + "## Visualise the pose tracks\nSince the data contains only a single wasp, we use\n:py:meth:`xarray.DataArray.squeeze` to remove\nthe dimension of length 1 from the data (the ``individuals`` dimension).\n\n" ] }, { @@ -65,7 +65,7 @@ }, "outputs": [], "source": [ - "position = ds.position.sel(individuals=\"individual_0\")\nposition.plot.line(x=\"time\", row=\"keypoints\", hue=\"space\", aspect=2, size=2.5)" + "ds.position.squeeze().plot.line(\n x=\"time\", row=\"keypoints\", hue=\"space\", aspect=2, size=2.5\n)" ] }, { @@ -79,7 +79,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Visualise confidence scores\nThe confidence scores are stored in the ``confidence`` data variable.\nSince the predicted poses in this example have been generated by DeepLabCut,\nthe confidence scores should be likelihood values between 0 and 1.\nThat said, confidence scores are not standardised across pose\nestimation frameworks, and their ranges can vary. Therefore,\nit's always a good idea to inspect the actual confidence values in the data.\n\nLet's first look at a histogram of the confidence scores.\n\n" + "## Visualise confidence scores\nThe confidence scores are stored in the ``confidence`` data variable.\nSince the predicted poses in this example have been generated by DeepLabCut,\nthe confidence scores should be likelihood values between 0 and 1.\nThat said, confidence scores are not standardised across pose\nestimation frameworks, and their ranges can vary. Therefore,\nit's always a good idea to inspect the actual confidence values in the data.\n\nLet's first look at a histogram of the confidence scores. As before, we use\n:py:meth:`xarray.DataArray.squeeze` to remove the ``individuals`` dimension\nfrom the data.\n\n" ] }, { @@ -90,7 +90,7 @@ }, "outputs": [], "source": [ - "ds.confidence.plot.hist(bins=20)" + "ds.confidence.squeeze().plot.hist(bins=20)" ] }, { @@ -108,7 +108,7 @@ }, "outputs": [], "source": [ - "confidence = ds.confidence.sel(individuals=\"individual_0\")\nconfidence.plot.line(x=\"time\", row=\"keypoints\", aspect=2, size=2.5)" + "ds.confidence.squeeze().plot.line(\n x=\"time\", row=\"keypoints\", aspect=2, size=2.5\n)" ] }, { @@ -122,7 +122,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Filter out points with low confidence\nWe can filter out points with confidence scores below a certain threshold.\nHere, we use ``threshold=0.6``. Points in the ``position`` data variable\nwith confidence scores below this threshold will be converted to NaN.\nThe ``print_report`` argument, which is True by default, reports the number\nof NaN values in the dataset before and after the filtering operation.\n\n" + "## Filter out points with low confidence\nUsing the\n:py:meth:`filter_by_confidence()\\\n`\nmethod of the ``move`` accessor,\nwe can filter out points with confidence scores below a certain threshold.\nThe default ``threshold=0.6`` will be used when ``threshold`` is not\nprovided.\nThis method will also report the number of NaN values in the dataset before\nand after the filtering operation by default (``print_report=True``).\nWe will use :py:meth:`xarray.Dataset.update` to update ``ds`` in-place\nwith the filtered ``position``.\n\n" ] }, { @@ -133,7 +133,14 @@ }, "outputs": [], "source": [ - "ds_filtered = filter_by_confidence(ds, threshold=0.6, print_report=True)" + "ds.update({\"position\": ds.move.filter_by_confidence()})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "

Note

The ``move`` accessor :py:meth:`filter_by_confidence()\\\n `\n method is a convenience method that applies\n :py:func:`movement.filtering.filter_by_confidence`,\n which takes ``position`` and ``confidence`` as arguments.\n The equivalent function call using the\n :py:mod:`movement.filtering` module would be:\n\n```python\nfrom movement.filtering import filter_by_confidence\n\nds.update({\"position\": filter_by_confidence(position, confidence)})

\n```\n" ] }, { @@ -151,21 +158,46 @@ }, "outputs": [], "source": [ - "position_filtered = ds_filtered.position.sel(individuals=\"individual_0\")\nposition_filtered.plot.line(\n x=\"time\", row=\"keypoints\", hue=\"space\", aspect=2, size=2.5\n)" + "ds.position.squeeze().plot.line(\n x=\"time\", row=\"keypoints\", hue=\"space\", aspect=2, size=2.5\n)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here we can see that gaps (consecutive NaNs) have appeared in the\npose tracks, some of which are over the implausible jumps and spikes we had\nseen earlier. Moreover, most gaps seem to be brief,\nlasting < 1 second (or 40 frames).\n\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Interpolate over missing values\nUsing the\n:py:meth:`interpolate_over_time()\\\n`\nmethod of the ``move`` accessor,\nwe can interpolate over the gaps we've introduced in the pose tracks.\nHere we use the default linear interpolation method (``method=linear``)\nand interpolate over gaps of 40 frames or less (``max_gap=40``).\nThe default ``max_gap=None`` would interpolate over all gaps, regardless of\ntheir length, but this should be used with caution as it can introduce\nspurious data. The ``print_report`` argument acts as described above.\n\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "ds.update({\"position\": ds.move.interpolate_over_time(max_gap=40)})" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Here we can see that gaps have appeared in the pose tracks, some of which\nare over the implausible jumps and spikes we had seen earlier. Moreover,\nmost gaps seem to be brief, lasting < 1 second.\n\n" + "

Note

The ``move`` accessor :py:meth:`interpolate_over_time()\\\n `\n is also a convenience method that applies\n :py:func:`movement.filtering.interpolate_over_time`\n to the ``position`` data variable.\n The equivalent function call using the\n :py:mod:`movement.filtering` module would be:\n\n```python\nfrom movement.filtering import interpolate_over_time\n\nds.update({\"position\": interpolate_over_time(\n position_filtered, max_gap=40\n)})

\n```\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Interpolate over missing values\nWe can interpolate over the gaps we've introduced in the pose tracks.\nHere we use the default linear interpolation method and ``max_gap=1``,\nmeaning that we will only interpolate over gaps of 1 second or shorter.\nSetting ``max_gap=None`` would interpolate over all gaps, regardless of\ntheir length, which should be used with caution as it can introduce\nspurious data. The ``print_report`` argument acts as described above.\n\n" + "We see that all NaN values have disappeared, meaning that all gaps were\nindeed shorter than 40 frames.\nLet's visualise the interpolated pose tracks.\n\n" ] }, { @@ -176,14 +208,14 @@ }, "outputs": [], "source": [ - "ds_interpolated = interpolate_over_time(\n ds_filtered, method=\"linear\", max_gap=1, print_report=True\n)" + "ds.position.squeeze().plot.line(\n x=\"time\", row=\"keypoints\", hue=\"space\", aspect=2, size=2.5\n)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "We see that all NaN values have disappeared, meaning that all gaps were\nindeed shorter than 1 second. Let's visualise the interpolated pose tracks\n\n" + "## Log of processing steps\nSo, far we've processed the pose tracks first by filtering out points with\nlow confidence scores, and then by interpolating over missing values.\nThe order of these operations and the parameters with which they were\nperformed are saved in the ``log`` attribute of the ``position`` data array.\nThis is useful for keeping track of the processing steps that have been\napplied to the data. Let's inspect the log entries.\n\n" ] }, { @@ -194,14 +226,14 @@ }, "outputs": [], "source": [ - "position_interpolated = ds_interpolated.position.sel(\n individuals=\"individual_0\"\n)\nposition_interpolated.plot.line(\n x=\"time\", row=\"keypoints\", hue=\"space\", aspect=2, size=2.5\n)" + "for log_entry in ds.position.log:\n print(log_entry)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Log of processing steps\nSo, far we've processed the pose tracks first by filtering out points with\nlow confidence scores, and then by interpolating over missing values.\nThe order of these operations and the parameters with which they were\nperformed are saved in the ``log`` attribute of the dataset.\nThis is useful for keeping track of the processing steps that have been\napplied to the data.\n\n" + "## Filtering multiple data variables\nAll :py:mod:`movement.filtering` functions are available via the\n``move`` accessor. These ``move`` accessor methods operate on the\n``position`` data variable in the dataset ``ds`` by default.\nThere is also an additional argument ``data_vars`` that allows us to\nspecify which data variables in ``ds`` to filter.\nWhen multiple data variable names are specified in ``data_vars``,\nthe method will return a dictionary with the data variable names as keys\nand the filtered DataArrays as values, otherwise it will return a single\nDataArray that is the filtered data.\nThis is useful when we want to apply the same filtering operation to\nmultiple data variables in ``ds`` at the same time.\n\nFor instance, to filter both ``position`` and ``velocity`` data variables\nin ``ds``, based on the confidence scores, we can specify\n``data_vars=[\"position\", \"velocity\"]`` in the method call.\nAs the filtered data variables are returned as a dictionary, we can once\nagain use :py:meth:`xarray.Dataset.update` to update ``ds`` in-place\nwith the filtered data variables.\n\n" ] }, { @@ -212,7 +244,7 @@ }, "outputs": [], "source": [ - "for log_entry in ds_interpolated.log:\n print(log_entry)" + "ds[\"velocity\"] = ds.move.compute_velocity()\nfiltered_data_dict = ds.move.filter_by_confidence(\n data_vars=[\"position\", \"velocity\"]\n)\nds.update(filtered_data_dict)" ] } ], diff --git a/_downloads/844e74b700fac354e52a59dbe7329a84/smooth.ipynb b/_downloads/844e74b700fac354e52a59dbe7329a84/smooth.ipynb index c9d60c82..f77fcfd4 100644 --- a/_downloads/844e74b700fac354e52a59dbe7329a84/smooth.ipynb +++ b/_downloads/844e74b700fac354e52a59dbe7329a84/smooth.ipynb @@ -22,7 +22,7 @@ }, "outputs": [], "source": [ - "from matplotlib import pyplot as plt\nfrom scipy.signal import welch\n\nfrom movement import sample_data\nfrom movement.filtering import (\n interpolate_over_time,\n median_filter,\n savgol_filter,\n)" + "from matplotlib import pyplot as plt\nfrom scipy.signal import welch\n\nfrom movement import sample_data" ] }, { @@ -47,7 +47,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We see that the dataset contains a single individual (a wasp) with two\nkeypoints tracked in 2D space. The video was recorded at 40 fps and lasts for\n~27 seconds.\n\n" + "We see that the dataset contains the 2D pose tracks and confidence scores\nfor a single wasp, generated with DeepLabCut. The wasp is tracked at two\nkeypoints: \"head\" and \"stinger\" in a video that was recorded at 40 fps and\nlasts for approximately 27 seconds.\n\n" ] }, { @@ -65,14 +65,14 @@ }, "outputs": [], "source": [ - "def plot_raw_and_smooth_timeseries_and_psd(\n ds_raw,\n ds_smooth,\n individual=\"individual_0\",\n keypoint=\"stinger\",\n space=\"x\",\n time_range=None,\n):\n # If no time range is specified, plot the entire time series\n if time_range is None:\n time_range = slice(0, ds_raw.time[-1])\n\n selection = {\n \"time\": time_range,\n \"individuals\": individual,\n \"keypoints\": keypoint,\n \"space\": space,\n }\n\n fig, ax = plt.subplots(2, 1, figsize=(10, 6))\n\n for ds, color, label in zip(\n [ds_raw, ds_smooth], [\"k\", \"r\"], [\"raw\", \"smooth\"]\n ):\n # plot position time series\n pos = ds.position.sel(**selection)\n ax[0].plot(\n pos.time,\n pos,\n color=color,\n lw=2,\n alpha=0.7,\n label=f\"{label} {space}\",\n )\n\n # generate interpolated dataset to avoid NaNs in the PSD calculation\n ds_interp = interpolate_over_time(ds, max_gap=None, print_report=False)\n pos_interp = ds_interp.position.sel(**selection)\n # compute and plot the PSD\n freq, psd = welch(pos_interp, fs=ds.fps, nperseg=256)\n ax[1].semilogy(\n freq,\n psd,\n color=color,\n lw=2,\n alpha=0.7,\n label=f\"{label} {space}\",\n )\n\n ax[0].set_ylabel(f\"{space} position (px)\")\n ax[0].set_xlabel(\"Time (s)\")\n ax[0].set_title(\"Time Domain\")\n ax[0].legend()\n\n ax[1].set_ylabel(\"PSD (px$^2$/Hz)\")\n ax[1].set_xlabel(\"Frequency (Hz)\")\n ax[1].set_title(\"Frequency Domain\")\n ax[1].legend()\n\n plt.tight_layout()\n fig.show()" + "def plot_raw_and_smooth_timeseries_and_psd(\n ds_raw,\n ds_smooth,\n individual=\"individual_0\",\n keypoint=\"stinger\",\n space=\"x\",\n time_range=None,\n):\n # If no time range is specified, plot the entire time series\n if time_range is None:\n time_range = slice(0, ds_raw.time[-1])\n\n selection = {\n \"time\": time_range,\n \"individuals\": individual,\n \"keypoints\": keypoint,\n \"space\": space,\n }\n\n fig, ax = plt.subplots(2, 1, figsize=(10, 6))\n\n for ds, color, label in zip(\n [ds_raw, ds_smooth], [\"k\", \"r\"], [\"raw\", \"smooth\"], strict=False\n ):\n # plot position time series\n pos = ds.position.sel(**selection)\n ax[0].plot(\n pos.time,\n pos,\n color=color,\n lw=2,\n alpha=0.7,\n label=f\"{label} {space}\",\n )\n\n # interpolate data to remove NaNs in the PSD calculation\n pos_interp = ds.sel(**selection).move.interpolate_over_time(\n print_report=False\n )\n # compute and plot the PSD\n freq, psd = welch(pos_interp, fs=ds.fps, nperseg=256)\n ax[1].semilogy(\n freq,\n psd,\n color=color,\n lw=2,\n alpha=0.7,\n label=f\"{label} {space}\",\n )\n\n ax[0].set_ylabel(f\"{space} position (px)\")\n ax[0].set_xlabel(\"Time (s)\")\n ax[0].set_title(\"Time Domain\")\n ax[0].legend()\n\n ax[1].set_ylabel(\"PSD (px$^2$/Hz)\")\n ax[1].set_xlabel(\"Frequency (Hz)\")\n ax[1].set_title(\"Frequency Domain\")\n ax[1].legend()\n\n plt.tight_layout()\n fig.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Smoothing with a median filter\nHere we use the :py:func:`movement.filtering.median_filter` function to\napply a rolling window median filter to the wasp dataset.\nThe ``window_length`` parameter is defined in seconds (according to the\n``time_unit`` dataset attribute).\n\n" + "## Smoothing with a median filter\nUsing the\n:py:meth:`median_filter()\\\n`\nmethod of the ``move`` accessor,\nwe apply a rolling window median filter over a 0.1-second window\n(4 frames) to the wasp dataset.\nAs the ``window`` parameter is defined in *number of observations*,\nwe can simply multiply the desired time window by the frame rate\nof the video. We will also create a copy of the dataset to avoid\nmodifying the original data.\n\n" ] }, { @@ -83,7 +83,14 @@ }, "outputs": [], "source": [ - "ds_wasp_medfilt = median_filter(ds_wasp, window_length=0.1)" + "window = int(0.1 * ds_wasp.fps)\nds_wasp_smooth = ds_wasp.copy()\nds_wasp_smooth.update({\"position\": ds_wasp_smooth.move.median_filter(window)})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "

Note

The ``move`` accessor :py:meth:`median_filter()\\\n `\n method is a convenience method that applies\n :py:func:`movement.filtering.median_filter`\n to the ``position`` data variable.\n The equivalent function call using the\n :py:mod:`movement.filtering` module would be:\n\n```python\nfrom movement.filtering import median_filter\n\nds_wasp_smooth.update({\"position\": median_filter(position, window)})

\n```\n" ] }, { @@ -101,7 +108,7 @@ }, "outputs": [], "source": [ - "plot_raw_and_smooth_timeseries_and_psd(\n ds_wasp, ds_wasp_medfilt, keypoint=\"stinger\"\n)" + "plot_raw_and_smooth_timeseries_and_psd(\n ds_wasp, ds_wasp_smooth, keypoint=\"stinger\"\n)" ] }, { @@ -115,7 +122,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Choosing parameters for the median filter\nYou can control the behaviour of :py:func:`movement.filtering.median_filter`\nvia two parameters: ``window_length`` and ``min_periods``.\nTo better understand the effect of these parameters, let's use a\ndataset that contains missing values.\n\n" + "### Choosing parameters for the median filter\nWe can control the behaviour of the median filter\nvia two parameters: ``window`` and ``min_periods``.\nTo better understand the effect of these parameters, let's use a\ndataset that contains missing values.\n\n" ] }, { @@ -133,7 +140,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The dataset contains a single mouse with six keypoints tracked in\n2D space. The video was recorded at 30 fps and lasts for ~616 seconds. We can\nsee that there are some missing values, indicated as \"nan\" in the\nprinted dataset. Let's apply the median filter to this dataset, with\nthe ``window_length`` set to 0.1 seconds.\n\n" + "The dataset contains a single mouse with six keypoints tracked in\n2D space. The video was recorded at 30 fps and lasts for ~616 seconds. We can\nsee that there are some missing values, indicated as \"nan\" in the\nprinted dataset.\nLet's apply the median filter over a 0.1-second window (3 frames)\nto the dataset.\n\n" ] }, { @@ -144,7 +151,7 @@ }, "outputs": [], "source": [ - "ds_mouse_medfilt = median_filter(ds_mouse, window_length=0.1)" + "window = int(0.1 * ds_mouse.fps)\nds_mouse_smooth = ds_mouse.copy()\nds_mouse_smooth.update(\n {\"position\": ds_mouse_smooth.move.median_filter(window)}\n)" ] }, { @@ -162,7 +169,7 @@ }, "outputs": [], "source": [ - "ds_mouse_medfilt = median_filter(ds_mouse, window_length=0.1, min_periods=2)" + "ds_mouse_smooth.update(\n {\"position\": ds_mouse.move.median_filter(window, min_periods=2)}\n)" ] }, { @@ -180,14 +187,14 @@ }, "outputs": [], "source": [ - "plot_raw_and_smooth_timeseries_and_psd(\n ds_mouse, ds_mouse_medfilt, keypoint=\"snout\", time_range=slice(0, 80)\n)" + "plot_raw_and_smooth_timeseries_and_psd(\n ds_mouse, ds_mouse_smooth, keypoint=\"snout\", time_range=slice(0, 80)\n)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "The smoothing once again reduces the power of high-frequency components, but\nthe resulting time series stays quite close to the raw data.\n\nWhat happens if we increase the ``window_length`` to 2 seconds?\n\n" + "The smoothing once again reduces the power of high-frequency components, but\nthe resulting time series stays quite close to the raw data.\n\nWhat happens if we increase the ``window`` to 2 seconds (60 frames)?\n\n" ] }, { @@ -198,14 +205,14 @@ }, "outputs": [], "source": [ - "ds_mouse_medfilt = median_filter(ds_mouse, window_length=2, min_periods=2)" + "window = int(2 * ds_mouse.fps)\nds_mouse_smooth.update(\n {\"position\": ds_mouse.move.median_filter(window, min_periods=2)}\n)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "The number of NaN values has decreased even further. That's because the\nchance of finding at least 2 valid values within a 2 second window is\nquite high. Let's plot the results for the same keypoint and time range\nas before.\n\n" + "The number of NaN values has decreased even further.\nThat's because the chance of finding at least 2 valid values within\na 2-second window (i.e. 60 frames) is quite high.\nLet's plot the results for the same keypoint and time range\nas before.\n\n" ] }, { @@ -216,21 +223,21 @@ }, "outputs": [], "source": [ - "plot_raw_and_smooth_timeseries_and_psd(\n ds_mouse, ds_mouse_medfilt, keypoint=\"snout\", time_range=slice(0, 80)\n)" + "plot_raw_and_smooth_timeseries_and_psd(\n ds_mouse, ds_mouse_smooth, keypoint=\"snout\", time_range=slice(0, 80)\n)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "We see that the filtered time series is much smoother and it has even\n\"bridged\" over some small gaps. That said, it often deviates from the raw\ndata, in ways that may not be desirable, depending on the application.\nThat means that our choice of ``window_length`` may be too large.\nIn general, you should choose a ``window_length`` that is small enough to\npreserve the original data structure, but large enough to remove\n\"spikes\" and high-frequency noise. Always inspect the results to ensure\nthat the filter is not removing important features.\n\n" + "We see that the filtered time series is much smoother and it has even\n\"bridged\" over some small gaps. That said, it often deviates from the raw\ndata, in ways that may not be desirable, depending on the application.\nHere, our choice of ``window`` may be too large.\nIn general, you should choose a ``window`` that is small enough to\npreserve the original data structure, but large enough to remove\n\"spikes\" and high-frequency noise. Always inspect the results to ensure\nthat the filter is not removing important features.\n\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Smoothing with a Savitzky-Golay filter\nHere we use the :py:func:`movement.filtering.savgol_filter` function,\nwhich is a wrapper around :py:func:`scipy.signal.savgol_filter`.\nThe Savitzky-Golay filter is a polynomial smoothing filter that can be\napplied to time series data on a rolling window basis. A polynomial of\ndegree ``polyorder`` is fitted to the data in each window of length\n``window_length``, and the value of the polynomial at the center of the\nwindow is used as the output value.\n\nLet's try it on the mouse dataset.\n\n" + "## Smoothing with a Savitzky-Golay filter\nHere we use the\n:py:meth:`savgol_filter()\\\n`\nmethod of the ``move`` accessor, which is a convenience method that applies\n:py:func:`movement.filtering.savgol_filter`\n(a wrapper around :py:func:`scipy.signal.savgol_filter`),\nto the ``position`` data variable.\nThe Savitzky-Golay filter is a polynomial smoothing filter that can be\napplied to time series data on a rolling window basis.\nA polynomial with a degree specified by ``polyorder`` is applied to each\ndata segment defined by the size ``window``.\nThe value of the polynomial at the midpoint of each ``window`` is then\nused as the output value.\n\nLet's try it on the mouse dataset, this time using a 0.2-second\nwindow (i.e. 6 frames) and the default ``polyorder=2`` for smoothing.\nAs before, we first compute the corresponding number of observations\nto be used as the ``window`` size.\n\n" ] }, { @@ -241,7 +248,7 @@ }, "outputs": [], "source": [ - "ds_mouse_savgol = savgol_filter(ds_mouse, window_length=0.2, polyorder=2)" + "window = int(0.2 * ds_mouse.fps)\nds_mouse_smooth.update({\"position\": ds_mouse.move.savgol_filter(window)})" ] }, { @@ -259,7 +266,7 @@ }, "outputs": [], "source": [ - "plot_raw_and_smooth_timeseries_and_psd(\n ds_mouse, ds_mouse_savgol, keypoint=\"snout\", time_range=slice(0, 80)\n)" + "plot_raw_and_smooth_timeseries_and_psd(\n ds_mouse, ds_mouse_smooth, keypoint=\"snout\", time_range=slice(0, 80)\n)" ] }, { @@ -273,7 +280,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Now let's take a look at the wasp dataset.\n\n" + "Now let's apply the same Savitzky-Golay filter to the wasp dataset.\n\n" ] }, { @@ -284,7 +291,7 @@ }, "outputs": [], "source": [ - "ds_wasp_savgol = savgol_filter(ds_wasp, window_length=0.2, polyorder=2)" + "window = int(0.2 * ds_wasp.fps)\nds_wasp_smooth.update({\"position\": ds_wasp.move.savgol_filter(window)})" ] }, { @@ -295,21 +302,21 @@ }, "outputs": [], "source": [ - "plot_raw_and_smooth_timeseries_and_psd(\n ds_wasp,\n ds_wasp_savgol,\n keypoint=\"stinger\",\n)" + "plot_raw_and_smooth_timeseries_and_psd(\n ds_wasp, ds_wasp_smooth, keypoint=\"stinger\"\n)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "This example shows two important limitations of the Savitzky-Golay filter.\nFirst, the filter can introduce artefacts around sharp boundaries. For\nexample, focus on what happens around the sudden drop in position\nduring the final second. Second, the PSD appears to have large periodic\ndrops at certain frequencies. Both of these effects vary with the\nchoice of ``window_length`` and ``polyorder``. You can read more about these\nand other limitations of the Savitzky-Golay filter in\n[this paper](https://pubs.acs.org/doi/10.1021/acsmeasuresciau.1c00054).\n\n" + "This example shows two important limitations of the Savitzky-Golay filter.\nFirst, the filter can introduce artefacts around sharp boundaries. For\nexample, focus on what happens around the sudden drop in position\nduring the final second. Second, the PSD appears to have large periodic\ndrops at certain frequencies. Both of these effects vary with the\nchoice of ``window`` and ``polyorder``. You can read more about these\nand other limitations of the Savitzky-Golay filter in\n[this paper](https://pubs.acs.org/doi/10.1021/acsmeasuresciau.1c00054).\n\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Combining multiple smoothing filters\nYou can also combine multiple smoothing filters by applying them\nsequentially. For example, we can first apply the median filter with a small\n``window_length`` to remove \"spikes\" and then apply the Savitzky-Golay filter\nwith a larger ``window_length`` to further smooth the data.\nBetween the two filters, we can interpolate over small gaps to avoid the\nexcessive proliferation of NaN values. Let's try this on the mouse dataset.\nFirst, let's apply the median filter.\n\n" + "## Combining multiple smoothing filters\nWe can also combine multiple smoothing filters by applying them\nsequentially. For example, we can first apply the median filter with a small\n``window`` to remove \"spikes\" and then apply the Savitzky-Golay filter\nwith a larger ``window`` to further smooth the data.\nBetween the two filters, we can interpolate over small gaps to avoid the\nexcessive proliferation of NaN values. Let's try this on the mouse dataset.\nFirst, we will apply the median filter.\n\n" ] }, { @@ -320,14 +327,14 @@ }, "outputs": [], "source": [ - "ds_mouse_medfilt = median_filter(ds_mouse, window_length=0.1, min_periods=2)" + "window = int(0.1 * ds_mouse.fps)\nds_mouse_smooth.update(\n {\"position\": ds_mouse.move.median_filter(window, min_periods=2)}\n)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Next, let's linearly interpolate over gaps smaller than 1 second.\n\n" + "Next, let's linearly interpolate over gaps smaller than 1 second (30 frames).\n\n" ] }, { @@ -338,14 +345,14 @@ }, "outputs": [], "source": [ - "ds_mouse_medfilt_interp = interpolate_over_time(ds_mouse_medfilt, max_gap=1)" + "ds_mouse_smooth.update(\n {\"position\": ds_mouse_smooth.move.interpolate_over_time(max_gap=30)}\n)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Finally, let's apply the Savitzky-Golay filter.\n\n" + "Finally, let's apply the Savitzky-Golay filter over a 0.4-second window\n(12 frames).\n\n" ] }, { @@ -356,14 +363,14 @@ }, "outputs": [], "source": [ - "ds_mouse_medfilt_interp_savgol = savgol_filter(\n ds_mouse_medfilt_interp, window_length=0.4, polyorder=2\n)" + "window = int(0.4 * ds_mouse.fps)\nds_mouse_smooth.update(\n {\"position\": ds_mouse_smooth.move.savgol_filter(window)}\n)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "A record of all applied operations is stored in the dataset's ``log``\nattribute. Let's inspect it to summarise what we've done.\n\n" + "A record of all applied operations is stored in the ``log`` attribute of the\n``ds_mouse_smooth.position`` data array. Let's inspect it to summarise\nwhat we've done.\n\n" ] }, { @@ -374,7 +381,7 @@ }, "outputs": [], "source": [ - "for entry in ds_mouse_medfilt_interp_savgol.log:\n print(entry)" + "for entry in ds_mouse_smooth.position.log:\n print(entry)" ] }, { @@ -392,7 +399,7 @@ }, "outputs": [], "source": [ - "plot_raw_and_smooth_timeseries_and_psd(\n ds_mouse,\n ds_mouse_medfilt_interp_savgol,\n keypoint=\"snout\",\n time_range=slice(0, 80),\n)" + "plot_raw_and_smooth_timeseries_and_psd(\n ds_mouse,\n ds_mouse_smooth,\n keypoint=\"snout\",\n time_range=slice(0, 80),\n)" ] }, { @@ -401,6 +408,13 @@ "source": [ "Feel free to play around with the parameters of the applied filters and to\nalso look at other keypoints and time ranges.\n\n" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + ".. seealso::\n `examples/filter_and_interpolate:Filtering multiple data variables`\n in the\n `sphx_glr_examples_filter_and_interpolate.py` example.\n\n" + ] } ], "metadata": { diff --git a/_downloads/8616797cd8df925412b19674f364ffea/filter_and_interpolate.py b/_downloads/8616797cd8df925412b19674f364ffea/filter_and_interpolate.py index d3e52ca5..dbe33044 100644 --- a/_downloads/8616797cd8df925412b19674f364ffea/filter_and_interpolate.py +++ b/_downloads/8616797cd8df925412b19674f364ffea/filter_and_interpolate.py @@ -9,7 +9,6 @@ # Imports # ------- from movement import sample_data -from movement.filtering import filter_by_confidence, interpolate_over_time # %% # Load a sample dataset @@ -19,16 +18,21 @@ print(ds) # %% -# We can see that this dataset contains the 2D pose tracks and confidence -# scores for a single wasp, generated with DeepLabCut. There are 2 keypoints: -# "head" and "stinger". +# We see that the dataset contains the 2D pose tracks and confidence scores +# for a single wasp, generated with DeepLabCut. The wasp is tracked at two +# keypoints: "head" and "stinger" in a video that was recorded at 40 fps and +# lasts for approximately 27 seconds. # %% # Visualise the pose tracks # ------------------------- +# Since the data contains only a single wasp, we use +# :py:meth:`xarray.DataArray.squeeze` to remove +# the dimension of length 1 from the data (the ``individuals`` dimension). -position = ds.position.sel(individuals="individual_0") -position.plot.line(x="time", row="keypoints", hue="space", aspect=2, size=2.5) +ds.position.squeeze().plot.line( + x="time", row="keypoints", hue="space", aspect=2, size=2.5 +) # %% # We can see that the pose tracks contain some implausible "jumps", such @@ -46,70 +50,113 @@ # estimation frameworks, and their ranges can vary. Therefore, # it's always a good idea to inspect the actual confidence values in the data. # -# Let's first look at a histogram of the confidence scores. -ds.confidence.plot.hist(bins=20) +# Let's first look at a histogram of the confidence scores. As before, we use +# :py:meth:`xarray.DataArray.squeeze` to remove the ``individuals`` dimension +# from the data. + +ds.confidence.squeeze().plot.hist(bins=20) # %% # Based on the above histogram, we can confirm that the confidence scores # indeed range between 0 and 1, with most values closer to 1. Now let's see how # they evolve over time. -confidence = ds.confidence.sel(individuals="individual_0") -confidence.plot.line(x="time", row="keypoints", aspect=2, size=2.5) +ds.confidence.squeeze().plot.line( + x="time", row="keypoints", aspect=2, size=2.5 +) # %% # Encouragingly, some of the drops in confidence scores do seem to correspond # to the implausible jumps and spikes we had seen in the position. # We can use that to our advantage. - # %% # Filter out points with low confidence # ------------------------------------- -# We can filter out points with confidence scores below a certain threshold. -# Here, we use ``threshold=0.6``. Points in the ``position`` data variable -# with confidence scores below this threshold will be converted to NaN. -# The ``print_report`` argument, which is True by default, reports the number -# of NaN values in the dataset before and after the filtering operation. +# Using the +# :py:meth:`filter_by_confidence()\ +# ` +# method of the ``move`` accessor, +# we can filter out points with confidence scores below a certain threshold. +# The default ``threshold=0.6`` will be used when ``threshold`` is not +# provided. +# This method will also report the number of NaN values in the dataset before +# and after the filtering operation by default (``print_report=True``). +# We will use :py:meth:`xarray.Dataset.update` to update ``ds`` in-place +# with the filtered ``position``. + +ds.update({"position": ds.move.filter_by_confidence()}) -ds_filtered = filter_by_confidence(ds, threshold=0.6, print_report=True) +# %% +# .. note:: +# The ``move`` accessor :py:meth:`filter_by_confidence()\ +# ` +# method is a convenience method that applies +# :py:func:`movement.filtering.filter_by_confidence`, +# which takes ``position`` and ``confidence`` as arguments. +# The equivalent function call using the +# :py:mod:`movement.filtering` module would be: +# +# .. code-block:: python +# +# from movement.filtering import filter_by_confidence +# +# ds.update({"position": filter_by_confidence(position, confidence)}) # %% # We can see that the filtering operation has introduced NaN values in the # ``position`` data variable. Let's visualise the filtered data. -position_filtered = ds_filtered.position.sel(individuals="individual_0") -position_filtered.plot.line( +ds.position.squeeze().plot.line( x="time", row="keypoints", hue="space", aspect=2, size=2.5 ) # %% -# Here we can see that gaps have appeared in the pose tracks, some of which -# are over the implausible jumps and spikes we had seen earlier. Moreover, -# most gaps seem to be brief, lasting < 1 second. +# Here we can see that gaps (consecutive NaNs) have appeared in the +# pose tracks, some of which are over the implausible jumps and spikes we had +# seen earlier. Moreover, most gaps seem to be brief, +# lasting < 1 second (or 40 frames). # %% # Interpolate over missing values # ------------------------------- -# We can interpolate over the gaps we've introduced in the pose tracks. -# Here we use the default linear interpolation method and ``max_gap=1``, -# meaning that we will only interpolate over gaps of 1 second or shorter. -# Setting ``max_gap=None`` would interpolate over all gaps, regardless of -# their length, which should be used with caution as it can introduce +# Using the +# :py:meth:`interpolate_over_time()\ +# ` +# method of the ``move`` accessor, +# we can interpolate over the gaps we've introduced in the pose tracks. +# Here we use the default linear interpolation method (``method=linear``) +# and interpolate over gaps of 40 frames or less (``max_gap=40``). +# The default ``max_gap=None`` would interpolate over all gaps, regardless of +# their length, but this should be used with caution as it can introduce # spurious data. The ``print_report`` argument acts as described above. -ds_interpolated = interpolate_over_time( - ds_filtered, method="linear", max_gap=1, print_report=True -) +ds.update({"position": ds.move.interpolate_over_time(max_gap=40)}) + +# %% +# .. note:: +# The ``move`` accessor :py:meth:`interpolate_over_time()\ +# ` +# is also a convenience method that applies +# :py:func:`movement.filtering.interpolate_over_time` +# to the ``position`` data variable. +# The equivalent function call using the +# :py:mod:`movement.filtering` module would be: +# +# .. code-block:: python +# +# from movement.filtering import interpolate_over_time +# +# ds.update({"position": interpolate_over_time( +# position_filtered, max_gap=40 +# )}) # %% # We see that all NaN values have disappeared, meaning that all gaps were -# indeed shorter than 1 second. Let's visualise the interpolated pose tracks +# indeed shorter than 40 frames. +# Let's visualise the interpolated pose tracks. -position_interpolated = ds_interpolated.position.sel( - individuals="individual_0" -) -position_interpolated.plot.line( +ds.position.squeeze().plot.line( x="time", row="keypoints", hue="space", aspect=2, size=2.5 ) @@ -119,9 +166,37 @@ # So, far we've processed the pose tracks first by filtering out points with # low confidence scores, and then by interpolating over missing values. # The order of these operations and the parameters with which they were -# performed are saved in the ``log`` attribute of the dataset. +# performed are saved in the ``log`` attribute of the ``position`` data array. # This is useful for keeping track of the processing steps that have been -# applied to the data. +# applied to the data. Let's inspect the log entries. -for log_entry in ds_interpolated.log: +for log_entry in ds.position.log: print(log_entry) + +# %% +# Filtering multiple data variables +# --------------------------------- +# All :py:mod:`movement.filtering` functions are available via the +# ``move`` accessor. These ``move`` accessor methods operate on the +# ``position`` data variable in the dataset ``ds`` by default. +# There is also an additional argument ``data_vars`` that allows us to +# specify which data variables in ``ds`` to filter. +# When multiple data variable names are specified in ``data_vars``, +# the method will return a dictionary with the data variable names as keys +# and the filtered DataArrays as values, otherwise it will return a single +# DataArray that is the filtered data. +# This is useful when we want to apply the same filtering operation to +# multiple data variables in ``ds`` at the same time. +# +# For instance, to filter both ``position`` and ``velocity`` data variables +# in ``ds``, based on the confidence scores, we can specify +# ``data_vars=["position", "velocity"]`` in the method call. +# As the filtered data variables are returned as a dictionary, we can once +# again use :py:meth:`xarray.Dataset.update` to update ``ds`` in-place +# with the filtered data variables. + +ds["velocity"] = ds.move.compute_velocity() +filtered_data_dict = ds.move.filter_by_confidence( + data_vars=["position", "velocity"] +) +ds.update(filtered_data_dict) diff --git a/_downloads/b14755a378a13dd501d6f42af59fe770/compute_kinematics.py b/_downloads/b14755a378a13dd501d6f42af59fe770/compute_kinematics.py index b2e05c7b..8cea0912 100644 --- a/_downloads/b14755a378a13dd501d6f42af59fe770/compute_kinematics.py +++ b/_downloads/b14755a378a13dd501d6f42af59fe770/compute_kinematics.py @@ -52,7 +52,9 @@ # colouring them by individual. fig, ax = plt.subplots(1, 1) -for mouse_name, col in zip(position.individuals.values, ["r", "g", "b"]): +for mouse_name, col in zip( + position.individuals.values, ["r", "g", "b"], strict=False +): ax.plot( position.sel(individuals=mouse_name, space="x"), position.sel(individuals=mouse_name, space="y"), @@ -78,7 +80,7 @@ # %% # We can also color the data points based on their timestamps: fig, axes = plt.subplots(3, 1, sharey=True) -for mouse_name, ax in zip(position.individuals.values, axes): +for mouse_name, ax in zip(position.individuals.values, axes, strict=False): sc = ax.scatter( position.sel(individuals=mouse_name, space="x"), position.sel(individuals=mouse_name, space="y"), @@ -102,8 +104,9 @@ # %% # We can also easily plot the components of the position vector against time -# using ``xarray``'s built-in plotting methods. We use ``squeeze()`` to -# remove the dimension of length 1 from the data (the keypoints dimension). +# using ``xarray``'s built-in plotting methods. We use +# :py:meth:`xarray.DataArray.squeeze` to +# remove the dimension of length 1 from the data (the ``keypoints`` dimension). position.squeeze().plot.line(x="time", row="individuals", aspect=2, size=2.5) plt.gcf().show() @@ -128,7 +131,7 @@ # %% # Notice that we could also compute the displacement (and all the other -# kinematic variables) using the kinematics module: +# kinematic variables) using the :py:mod:`movement.analysis.kinematics` module: # %% import movement.analysis.kinematics as kin @@ -280,8 +283,9 @@ # %% # We can plot the components of the velocity vector against time -# using ``xarray``'s built-in plotting methods. We use ``squeeze()`` to -# remove the dimension of length 1 from the data (the keypoints dimension). +# using ``xarray``'s built-in plotting methods. We use +# :py:meth:`xarray.DataArray.squeeze` to +# remove the dimension of length 1 from the data (the ``keypoints`` dimension). velocity.squeeze().plot.line(x="time", row="individuals", aspect=2, size=2.5) plt.gcf().show() @@ -297,7 +301,7 @@ # %% # We can also visualise the speed, as the norm of the velocity vector: fig, axes = plt.subplots(3, 1, sharex=True, sharey=True) -for mouse_name, ax in zip(velocity.individuals.values, axes): +for mouse_name, ax in zip(velocity.individuals.values, axes, strict=False): # compute the norm of the velocity vector for one mouse speed_one_mouse = np.linalg.norm( velocity.sel(individuals=mouse_name, space=["x", "y"]).squeeze(), @@ -357,7 +361,7 @@ # and plot of the components of the acceleration vector ``ax``, ``ay`` per # individual: fig, axes = plt.subplots(3, 1, sharex=True, sharey=True) -for mouse_name, ax in zip(accel.individuals.values, axes): +for mouse_name, ax in zip(accel.individuals.values, axes, strict=False): # plot x-component of acceleration vector ax.plot( accel.sel(individuals=mouse_name, space=["x"]).squeeze(), @@ -379,7 +383,7 @@ # acceleration. # We can also represent this for each individual. fig, axes = plt.subplots(3, 1, sharex=True, sharey=True) -for mouse_name, ax in zip(accel.individuals.values, axes): +for mouse_name, ax in zip(accel.individuals.values, axes, strict=False): # compute norm of the acceleration vector for one mouse accel_one_mouse = np.linalg.norm( accel.sel(individuals=mouse_name, space=["x", "y"]).squeeze(), diff --git a/_downloads/bc82bea3a5dd7bdba60b65220891d9e5/examples_python.zip b/_downloads/bc82bea3a5dd7bdba60b65220891d9e5/examples_python.zip index eb7bb6b0..2ef134f7 100644 Binary files a/_downloads/bc82bea3a5dd7bdba60b65220891d9e5/examples_python.zip and b/_downloads/bc82bea3a5dd7bdba60b65220891d9e5/examples_python.zip differ diff --git a/_downloads/fb625db3c50d423b1b7881136ffdeec8/examples_jupyter.zip b/_downloads/fb625db3c50d423b1b7881136ffdeec8/examples_jupyter.zip index 1c09c3d2..e826e622 100644 Binary files a/_downloads/fb625db3c50d423b1b7881136ffdeec8/examples_jupyter.zip and b/_downloads/fb625db3c50d423b1b7881136ffdeec8/examples_jupyter.zip differ diff --git a/_images/sphx_glr_compute_kinematics_001.png b/_images/sphx_glr_compute_kinematics_001.png index 03557526..63bda70c 100644 Binary files a/_images/sphx_glr_compute_kinematics_001.png and b/_images/sphx_glr_compute_kinematics_001.png differ diff --git a/_images/sphx_glr_compute_kinematics_002.png b/_images/sphx_glr_compute_kinematics_002.png index 18205e1c..9a822b5c 100644 Binary files a/_images/sphx_glr_compute_kinematics_002.png and b/_images/sphx_glr_compute_kinematics_002.png differ diff --git a/_images/sphx_glr_compute_kinematics_003.png b/_images/sphx_glr_compute_kinematics_003.png index 96977e3b..2a2b32f1 100644 Binary files a/_images/sphx_glr_compute_kinematics_003.png and b/_images/sphx_glr_compute_kinematics_003.png differ diff --git a/_images/sphx_glr_compute_kinematics_004.png b/_images/sphx_glr_compute_kinematics_004.png index b4646b8b..7f9d93c8 100644 Binary files a/_images/sphx_glr_compute_kinematics_004.png and b/_images/sphx_glr_compute_kinematics_004.png differ diff --git a/_images/sphx_glr_compute_kinematics_005.png b/_images/sphx_glr_compute_kinematics_005.png index 4c686545..9e9cfa2f 100644 Binary files a/_images/sphx_glr_compute_kinematics_005.png and b/_images/sphx_glr_compute_kinematics_005.png differ diff --git a/_images/sphx_glr_compute_kinematics_006.png b/_images/sphx_glr_compute_kinematics_006.png index c3eb586e..cc3beacb 100644 Binary files a/_images/sphx_glr_compute_kinematics_006.png and b/_images/sphx_glr_compute_kinematics_006.png differ diff --git a/_images/sphx_glr_compute_kinematics_007.png b/_images/sphx_glr_compute_kinematics_007.png index 4b9c53d7..4f08c63c 100644 Binary files a/_images/sphx_glr_compute_kinematics_007.png and b/_images/sphx_glr_compute_kinematics_007.png differ diff --git a/_images/sphx_glr_compute_kinematics_008.png b/_images/sphx_glr_compute_kinematics_008.png index 73eca615..74ff7b84 100644 Binary files a/_images/sphx_glr_compute_kinematics_008.png and b/_images/sphx_glr_compute_kinematics_008.png differ diff --git a/_images/sphx_glr_compute_kinematics_009.png b/_images/sphx_glr_compute_kinematics_009.png index 7a1c48fd..23d62299 100644 Binary files a/_images/sphx_glr_compute_kinematics_009.png and b/_images/sphx_glr_compute_kinematics_009.png differ diff --git a/_images/sphx_glr_compute_kinematics_010.png b/_images/sphx_glr_compute_kinematics_010.png index 44f004c9..60bf0414 100644 Binary files a/_images/sphx_glr_compute_kinematics_010.png and b/_images/sphx_glr_compute_kinematics_010.png differ diff --git a/_images/sphx_glr_compute_polar_coordinates_001.png b/_images/sphx_glr_compute_polar_coordinates_001.png index f94db162..81a0592d 100644 Binary files a/_images/sphx_glr_compute_polar_coordinates_001.png and b/_images/sphx_glr_compute_polar_coordinates_001.png differ diff --git a/_images/sphx_glr_compute_polar_coordinates_002.png b/_images/sphx_glr_compute_polar_coordinates_002.png index 4e69f393..b4e0e45a 100644 Binary files a/_images/sphx_glr_compute_polar_coordinates_002.png and b/_images/sphx_glr_compute_polar_coordinates_002.png differ diff --git a/_images/sphx_glr_compute_polar_coordinates_003.png b/_images/sphx_glr_compute_polar_coordinates_003.png index 428b5940..bb2f3b54 100644 Binary files a/_images/sphx_glr_compute_polar_coordinates_003.png and b/_images/sphx_glr_compute_polar_coordinates_003.png differ diff --git a/_images/sphx_glr_compute_polar_coordinates_004.png b/_images/sphx_glr_compute_polar_coordinates_004.png index ea66e4ff..9f9f8fd0 100644 Binary files a/_images/sphx_glr_compute_polar_coordinates_004.png and b/_images/sphx_glr_compute_polar_coordinates_004.png differ diff --git a/_images/sphx_glr_compute_polar_coordinates_005.png b/_images/sphx_glr_compute_polar_coordinates_005.png index 2a1a5adf..9c9a5c7a 100644 Binary files a/_images/sphx_glr_compute_polar_coordinates_005.png and b/_images/sphx_glr_compute_polar_coordinates_005.png differ diff --git a/_images/sphx_glr_compute_polar_coordinates_006.png b/_images/sphx_glr_compute_polar_coordinates_006.png index 91fbb753..912c63cd 100644 Binary files a/_images/sphx_glr_compute_polar_coordinates_006.png and b/_images/sphx_glr_compute_polar_coordinates_006.png differ diff --git a/_images/sphx_glr_filter_and_interpolate_001.png b/_images/sphx_glr_filter_and_interpolate_001.png index d4814c01..98f89261 100644 Binary files a/_images/sphx_glr_filter_and_interpolate_001.png and b/_images/sphx_glr_filter_and_interpolate_001.png differ diff --git a/_images/sphx_glr_filter_and_interpolate_002.png b/_images/sphx_glr_filter_and_interpolate_002.png index fec27525..9312a95c 100644 Binary files a/_images/sphx_glr_filter_and_interpolate_002.png and b/_images/sphx_glr_filter_and_interpolate_002.png differ diff --git a/_images/sphx_glr_filter_and_interpolate_003.png b/_images/sphx_glr_filter_and_interpolate_003.png index e942cc19..63025e6b 100644 Binary files a/_images/sphx_glr_filter_and_interpolate_003.png and b/_images/sphx_glr_filter_and_interpolate_003.png differ diff --git a/_images/sphx_glr_filter_and_interpolate_004.png b/_images/sphx_glr_filter_and_interpolate_004.png index 6df3027e..2daabe5a 100644 Binary files a/_images/sphx_glr_filter_and_interpolate_004.png and b/_images/sphx_glr_filter_and_interpolate_004.png differ diff --git a/_images/sphx_glr_filter_and_interpolate_005.png b/_images/sphx_glr_filter_and_interpolate_005.png index b14915e2..46a15c71 100644 Binary files a/_images/sphx_glr_filter_and_interpolate_005.png and b/_images/sphx_glr_filter_and_interpolate_005.png differ diff --git a/_images/sphx_glr_load_and_explore_poses_001.png b/_images/sphx_glr_load_and_explore_poses_001.png index 42488555..38af6dbf 100644 Binary files a/_images/sphx_glr_load_and_explore_poses_001.png and b/_images/sphx_glr_load_and_explore_poses_001.png differ diff --git a/_images/sphx_glr_load_and_explore_poses_002.png b/_images/sphx_glr_load_and_explore_poses_002.png index 96977e3b..2a2b32f1 100644 Binary files a/_images/sphx_glr_load_and_explore_poses_002.png and b/_images/sphx_glr_load_and_explore_poses_002.png differ diff --git a/_images/sphx_glr_load_and_explore_poses_003.png b/_images/sphx_glr_load_and_explore_poses_003.png index baff7f04..fa43737c 100644 Binary files a/_images/sphx_glr_load_and_explore_poses_003.png and b/_images/sphx_glr_load_and_explore_poses_003.png differ diff --git a/_images/sphx_glr_smooth_001.png b/_images/sphx_glr_smooth_001.png index 0bffbe3f..9d0d2367 100644 Binary files a/_images/sphx_glr_smooth_001.png and b/_images/sphx_glr_smooth_001.png differ diff --git a/_images/sphx_glr_smooth_002.png b/_images/sphx_glr_smooth_002.png index 263458a5..3baea513 100644 Binary files a/_images/sphx_glr_smooth_002.png and b/_images/sphx_glr_smooth_002.png differ diff --git a/_images/sphx_glr_smooth_003.png b/_images/sphx_glr_smooth_003.png index 7ba87d87..9d40acaf 100644 Binary files a/_images/sphx_glr_smooth_003.png and b/_images/sphx_glr_smooth_003.png differ diff --git a/_images/sphx_glr_smooth_004.png b/_images/sphx_glr_smooth_004.png index a9cda89d..d71f1be9 100644 Binary files a/_images/sphx_glr_smooth_004.png and b/_images/sphx_glr_smooth_004.png differ diff --git a/_images/sphx_glr_smooth_005.png b/_images/sphx_glr_smooth_005.png index c42c1caf..badaedd9 100644 Binary files a/_images/sphx_glr_smooth_005.png and b/_images/sphx_glr_smooth_005.png differ diff --git a/_images/sphx_glr_smooth_006.png b/_images/sphx_glr_smooth_006.png index a53cbc90..97dc67bd 100644 Binary files a/_images/sphx_glr_smooth_006.png and b/_images/sphx_glr_smooth_006.png differ diff --git a/_modules/index.html b/_modules/index.html index 0bf76026..a0e61ae5 100644 --- a/_modules/index.html +++ b/_modules/index.html @@ -17,12 +17,12 @@ - - - + + + - + @@ -36,11 +36,11 @@ - - - + + + - + @@ -105,7 +105,7 @@