Tutorial about tracking LocData objects#

Tracking refers to link localizations that are close in space over multiple frames and collect those localizations in individual tracks. We here make use of the trackpy package through wrapper functions to deal with LocData objects.

%matplotlib inline

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

import locan as lc
/tmp/ipykernel_2021/3049569888.py:4: DeprecationWarning: 
Pyarrow will become a required dependency of pandas in the next major release of pandas (pandas 3.0),
(to allow more performant data types, such as the Arrow string type, and better interoperability with other libraries)
but was not found to be installed on your system.
If this would cause problems for you,
please provide us feedback at https://github.com/pandas-dev/pandas/issues/54466
        
  import pandas as pd
lc.show_versions(system=False, dependencies=False, verbose=False)
Locan:
   version: 0.20.0.dev41+g755b969

Python:
   version: 3.11.6

Synthetic data#

A random dataset is created.

dat = lc.simulate_tracks(n_walks=5, n_steps=100, ranges=((0,1000),(0,1000)),
                      diffusion_constant=1, seed=1)

dat.print_meta()
Jupyter environment detected. Enabling Open3D WebVisualizer.
[Open3D INFO] WebRTC GUI backend enabled.
[Open3D INFO] WebRTCWindowSystem: HTTP handshake server disabled.
identifier: "1"
source: SIMULATION
state: RAW
history {
  name: "simulate_tracks"
  parameter: "{\'n_walks\': 5, \'n_steps\': 100, \'ranges\': ((0, 1000), (0, 1000)), \'diffusion_constant\': 1, \'time_step\': 10, \'seed\': 1}"
}
element_count: 500
frame_count: 100
creation_time {
  2024-03-14T11:10:03.274708Z
}
dat.data.head()
position_x position_y frame
0 518.146180 429.651004 0
1 524.470735 435.975560 1
2 530.795291 429.651004 2
3 524.470735 435.975560 3
4 518.146180 429.651004 4
fig, ax = plt.subplots(nrows=1, ncols=1)
dat.data.plot.scatter(x='position_x', y='position_y', ax=ax, color='Blue', label='locdata')
plt.show()
../../_images/01445020f08500f6d256f6f84abe698e7d896571d4af356f3df92877bbcb12de.png

Track locdata#

The track function collects tracks in a new locdata object.

tracks, track_numbers = lc.track(dat, search_range=500)
Frame 99: 5 trajectories present.
tracks.print_summary()
identifier: "7"
comment: ""
source: DESIGN
state: RAW
element_count: 5
frame_count: 1
creation_time {
  2024-03-14T11:10:03.783495Z
}
tracks.data
localization_count position_x uncertainty_x position_y uncertainty_y frame region_measure_bb localization_density_bb subregion_measure_bb
0 100 882.368107 4.321878 504.562854 2.382224 0 14720.0 0.006793 493.315315
1 100 169.710816 1.293090 381.371093 3.679042 0 6840.0 0.014620 354.175098
2 100 505.497069 2.098581 456.720101 3.047211 0 8640.0 0.011574 379.473319
3 100 307.151281 1.787996 19.463682 1.378273 0 4800.0 0.020833 278.280434
4 100 972.599640 3.900272 805.187177 1.217117 0 7560.0 0.013228 379.473319
tracks.references[0].data.head()
position_x position_y frame
300 954.974002 543.269132 0
301 961.298558 549.593688 1
302 967.623113 555.918243 2
303 961.298558 549.593688 3
304 967.623113 543.269132 4
fig, ax = plt.subplots(nrows=1, ncols=1)
dat.data.plot.scatter(x='position_x', y='position_y', ax=ax, color='Blue', label='locdata')
tracks.references[0].data.plot.scatter(x='position_x', y='position_y', ax=ax, color='Red', label='track 0')
plt.show()
../../_images/60e7278d88c24934e8755c8a51cd759bceb0e8345e38a8356e6eabc14692d064.png
fig, ax = plt.subplots(nrows=1, ncols=1)
dat.data.plot.scatter(x='position_x', y='position_y', ax=ax, color='Blue', label='locdata')
tracks.data.plot.scatter(x='position_x', y='position_y', ax=ax, color='Red', label='tracks')
plt.show()
../../_images/72dcb5e38f4014b24aa5e5e3cbe61e05f8acd7f5a90c41e22469e3a8b3bf2e07.png

To show individual tracks make use of the reference attribute.

fig, ax = plt.subplots(nrows=1, ncols=1)

jet= plt.get_cmap('jet')
colors = iter(jet(np.linspace(0, 1, len(tracks))))

for ref in tracks.references:
    c=next(colors)
    ref.data.plot.scatter(x='position_x', y='position_y', ax=ax, color=(c,), label=ref.meta.identifier)

plt.show()
../../_images/ff598a0da45f652a1f05a24896515aa591ae9af1749f069ee0c7df2890ffab9c.png

Alternatively to a new locdata object, a pandas DataFrame can be generated using the link_locdata method.

links = lc.link_locdata(dat, search_range=10)
Frame 99: 5 trajectories present.
links.head()
300    0
200    1
0      2
400    3
100    4
Name: track, dtype: int64

Use the following to add particle column to original locdata dataset.

dat.data.loc[links.index,'track']=links
dat.data.head()
position_x position_y frame track
0 518.146180 429.651004 0 2.0
1 524.470735 435.975560 1 2.0
2 530.795291 429.651004 2 2.0
3 524.470735 435.975560 3 2.0
4 518.146180 429.651004 4 2.0

Track using trackpy with locdata.data as input#

For a detailed tracking analysis you might want to use trackpy functions with the pandas DataFrame carrying localization data as input. The following examples will igve a short illustration of using trackpy functions.

import trackpy as tp
t = tp.link_df(dat.data, search_range=100, memory=1, pos_columns=['position_x', 'position_y'], t_column='frame')

t.head(10)
Frame 99: 5 trajectories present.
position_x position_y frame track particle
300 954.974002 543.269132 0 0.0 0
200 150.484168 402.874581 0 1.0 1
0 518.146180 429.651004 0 2.0 2
400 318.156007 33.883669 0 3.0 3
100 944.139141 821.378039 0 4.0 4
1 524.470735 435.975560 1 2.0 2
201 156.808723 409.199136 1 1.0 1
301 961.298558 549.593688 1 0.0 0
401 324.480563 27.559113 1 3.0 3
101 937.814586 815.053483 1 4.0 4
plt.figure()
tp.plot_traj(t, pos_columns=['position_x', 'position_y'], t_column='frame');
../../_images/964df309e0f08182c0a2935cbe18921682c43af1ca07d085dbf523133edd09c2.png

filter#

t.head()
position_x position_y frame track particle
300 954.974002 543.269132 0 0.0 0
200 150.484168 402.874581 0 1.0 1
0 518.146180 429.651004 0 2.0 2
400 318.156007 33.883669 0 3.0 3
100 944.139141 821.378039 0 4.0 4
t1 = tp.filter_stubs(t, 10).reset_index(drop=True)
len(t1)
500
plt.figure()
tp.plot_traj(t1, pos_columns=['position_x', 'position_y']);
../../_images/964df309e0f08182c0a2935cbe18921682c43af1ca07d085dbf523133edd09c2.png

drift#

d = tp.compute_drift(t1, pos_columns=['position_x', 'position_y'])
d.plot()
plt.show()
../../_images/a6ce9cbfe1f28bc93345536694e2a18410260e3f428760bddbd2da76fb84ba8d.png

Mean square displacement (msd)#

em = tp.emsd(t1,0.1, 100, pos_columns=['position_x', 'position_y']) # microns per pixel = 100/285., frames per second = 24
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[25], line 1
----> 1 em = tp.emsd(t1,0.1, 100, pos_columns=['position_x', 'position_y']) # microns per pixel = 100/285., frames per second = 24

File ~/checkouts/readthedocs.org/user_builds/locan/envs/latest/lib/python3.11/site-packages/trackpy/motion.py:235, in emsd(traj, mpp, fps, max_lagtime, detail, pos_columns)
    233     ids.append(pid)
    234 msds = pandas_concat(msds, keys=ids, names=['particle', 'frame'])
--> 235 results = msds.mul(msds['N'], axis=0).mean(level=1)  # weighted average
    236 results = results.div(msds['N'].mean(level=1), axis=0)  # weights normalized
    237 # Above, lagt is lumped in with the rest for simplicity and speed.
    238 # Here, rebuild it from the frame index.

File ~/checkouts/readthedocs.org/user_builds/locan/envs/latest/lib/python3.11/site-packages/pandas/core/frame.py:11666, in DataFrame.mean(self, axis, skipna, numeric_only, **kwargs)
  11658 @doc(make_doc("mean", ndim=2))
  11659 def mean(
  11660     self,
   (...)
  11664     **kwargs,
  11665 ):
> 11666     result = super().mean(axis, skipna, numeric_only, **kwargs)
  11667     if isinstance(result, Series):
  11668         result = result.__finalize__(self, method="mean")

File ~/checkouts/readthedocs.org/user_builds/locan/envs/latest/lib/python3.11/site-packages/pandas/core/generic.py:12413, in NDFrame.mean(self, axis, skipna, numeric_only, **kwargs)
  12406 def mean(
  12407     self,
  12408     axis: Axis | None = 0,
   (...)
  12411     **kwargs,
  12412 ) -> Series | float:
> 12413     return self._stat_function(
  12414         "mean", nanops.nanmean, axis, skipna, numeric_only, **kwargs
  12415     )

File ~/checkouts/readthedocs.org/user_builds/locan/envs/latest/lib/python3.11/site-packages/pandas/core/generic.py:12366, in NDFrame._stat_function(self, name, func, axis, skipna, numeric_only, **kwargs)
  12355 @final
  12356 def _stat_function(
  12357     self,
   (...)
  12363     **kwargs,
  12364 ):
  12365     assert name in ["median", "mean", "min", "max", "kurt", "skew"], name
> 12366     nv.validate_func(name, (), kwargs)
  12368     validate_bool_kwarg(skipna, "skipna", none_allowed=False)
  12370     return self._reduce(
  12371         func, name=name, axis=axis, skipna=skipna, numeric_only=numeric_only
  12372     )

File ~/checkouts/readthedocs.org/user_builds/locan/envs/latest/lib/python3.11/site-packages/pandas/compat/numpy/function.py:416, in validate_func(fname, args, kwargs)
    413     return validate_stat_func(args, kwargs, fname=fname)
    415 validation_func = _validation_funcs[fname]
--> 416 return validation_func(args, kwargs)

File ~/checkouts/readthedocs.org/user_builds/locan/envs/latest/lib/python3.11/site-packages/pandas/compat/numpy/function.py:88, in CompatValidator.__call__(self, args, kwargs, fname, max_fname_arg_count, method)
     86     validate_kwargs(fname, kwargs, self.defaults)
     87 elif method == "both":
---> 88     validate_args_and_kwargs(
     89         fname, args, kwargs, max_fname_arg_count, self.defaults
     90     )
     91 else:
     92     raise ValueError(f"invalid validation method '{method}'")

File ~/checkouts/readthedocs.org/user_builds/locan/envs/latest/lib/python3.11/site-packages/pandas/util/_validators.py:223, in validate_args_and_kwargs(fname, args, kwargs, max_fname_arg_count, compat_args)
    218         raise TypeError(
    219             f"{fname}() got multiple values for keyword argument '{key}'"
    220         )
    222 kwargs.update(args_dict)
--> 223 validate_kwargs(fname, kwargs, compat_args)

File ~/checkouts/readthedocs.org/user_builds/locan/envs/latest/lib/python3.11/site-packages/pandas/util/_validators.py:164, in validate_kwargs(fname, kwargs, compat_args)
    142 """
    143 Checks whether parameters passed to the **kwargs argument in a
    144 function `fname` are valid parameters as specified in `*compat_args`
   (...)
    161 map to the default values specified in `compat_args`
    162 """
    163 kwds = kwargs.copy()
--> 164 _check_for_invalid_keys(fname, kwargs, compat_args)
    165 _check_for_default_values(fname, kwds, compat_args)

File ~/checkouts/readthedocs.org/user_builds/locan/envs/latest/lib/python3.11/site-packages/pandas/util/_validators.py:138, in _check_for_invalid_keys(fname, kwargs, compat_args)
    136 if diff:
    137     bad_arg = next(iter(diff))
--> 138     raise TypeError(f"{fname}() got an unexpected keyword argument '{bad_arg}'")

TypeError: mean() got an unexpected keyword argument 'level'
fig, ax = plt.subplots()
ax.plot(em.index, em, 'o')
#ax.set_xscale('log')
#ax.set_yscale('log')
ax.set(ylabel=r'$\langle \Delta r^2 \rangle$ [$\mu$m$^2$]',
       xlabel='lag time $t$')
#ax.set(ylim=(1e-2, 10));