Skip to content

Commit 3bbc4b7

Browse files
committed
Add function to compute distances between neighbors
1 parent ab81cf8 commit 3bbc4b7

File tree

4 files changed

+226
-2
lines changed

4 files changed

+226
-2
lines changed

notebooks/user_guide.ipynb

+42-1
Original file line numberDiff line numberDiff line change
@@ -3079,7 +3079,14 @@
30793079
"We define two pedestrians as neighbors if their Voronoi polygons ($V_i$, $V_j$) touch at some point, in case of *PedPy* they are touching if their distance is below 1mm.\n",
30803080
"As basis for the computation one can either use the uncut or cut Voronoi polygons.\n",
30813081
"When using the uncut Voronoi polygons, pedestrian may be detected as neighbors even when their distance is quite large in low density situation.\n",
3082-
"Therefore, it is recommended to use the cut Voronoi polygons, where the cut-off radius can be used to define a maximal distance between neighboring pedestrians.\n",
3082+
"Therefore, it is recommended to use the cut Voronoi polygons, where the cut-off radius can be used to define a maximal distance between neighboring pedestrians."
3083+
]
3084+
},
3085+
{
3086+
"cell_type": "markdown",
3087+
"metadata": {},
3088+
"source": [
3089+
"#### Neighborhood computation\n",
30833090
"\n",
30843091
"To compute the neighbors in *PedPy* use:"
30853092
]
@@ -3154,6 +3161,40 @@
31543161
"neighbors_as_list[0:5]"
31553162
]
31563163
},
3164+
{
3165+
"cell_type": "markdown",
3166+
"metadata": {},
3167+
"source": [
3168+
"#### Distance to neighbors\n",
3169+
"\n",
3170+
"For computing the distance between neighbors, *PedPy* offers a dedicated function:"
3171+
]
3172+
},
3173+
{
3174+
"cell_type": "code",
3175+
"execution_count": null,
3176+
"metadata": {},
3177+
"outputs": [],
3178+
"source": [
3179+
"from pedpy import compute_neighbor_distance\n",
3180+
"\n",
3181+
"neighbor_distance = compute_neighbor_distance(\n",
3182+
" traj_data=traj, neighborhood=neighbors\n",
3183+
")\n",
3184+
"neighbor_distance[0:5]"
3185+
]
3186+
},
3187+
{
3188+
"cell_type": "markdown",
3189+
"metadata": {},
3190+
"source": [
3191+
":::{note}\n",
3192+
"The resulting {class}`~pandas.DataFrame` is symmetric. \n",
3193+
"If pedestrian A is a neighbor of pedestrian B, then pedestrian B is also a neighbor of pedestrian A.\n",
3194+
"Consequently, the distance between both appears twice in the DataFrame.\n",
3195+
":::"
3196+
]
3197+
},
31573198
{
31583199
"cell_type": "markdown",
31593200
"metadata": {},

pedpy/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
compute_frame_range_in_area,
7979
compute_individual_voronoi_polygons,
8080
compute_intersecting_polygons,
81+
compute_neighbor_distance,
8182
compute_neighbors,
8283
compute_time_distance_line,
8384
get_invalid_trajectory,
@@ -155,6 +156,7 @@
155156
"compute_individual_voronoi_polygons",
156157
"compute_intersecting_polygons",
157158
"compute_neighbors",
159+
"compute_neighbor_distance",
158160
"compute_time_distance_line",
159161
"get_invalid_trajectory",
160162
"is_individual_speed_valid",

pedpy/methods/method_utils.py

+60
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,66 @@ def _compute_neighbors_single(
418418
)
419419

420420

421+
def compute_neighbor_distance(
422+
*,
423+
traj_data: TrajectoryData,
424+
neighborhood: pd.DataFrame,
425+
) -> pd.DataFrame:
426+
"""Compute the distance between the neighbors.
427+
428+
Computes the distance between the position of neighbors. As neighbors the
429+
result of :func:`~compute_neighbors` with parameter :code:`as_list=False`.
430+
431+
Note:
432+
The resulting :class:`~pandas.DataFrame` is symmetric. If pedestrian A
433+
is a neighbor of pedestrian B, then pedestrian B is also a neighbor of
434+
pedestrian A. Consequently, the distance between both appears twice in
435+
the :class:`~pandas.DataFrame`.
436+
437+
Args:
438+
traj_data (TrajectoryData): trajectory data
439+
neighborhood (pd.DataFrame): DataFrame containing the columns 'id',
440+
'frame' and 'neighbor_id'. The result of :func:`~compute_neighbors`
441+
with parameter :code:`as_list=False` can be used here as input.
442+
443+
Raises:
444+
ValueError: When passing a result of :func:`~compute_neighbors`
445+
with parameter :code:`as_list=True`.
446+
447+
Returns:
448+
DataFrame containing the columns 'id', 'frame', 'neighbor_id' and
449+
'distance'.
450+
"""
451+
if NEIGHBORS_COL in neighborhood.columns:
452+
raise ValueError(
453+
"Cannot compute distance between neighbors with list-format data. "
454+
"Please use the result of compute_neighbors with parameter "
455+
"as_list=False."
456+
)
457+
458+
neighbors_with_position = neighborhood.merge(
459+
traj_data.data[[ID_COL, FRAME_COL, POINT_COL]],
460+
on=[ID_COL, FRAME_COL],
461+
how="left",
462+
)
463+
464+
neighbors_with_position = neighbors_with_position.merge(
465+
traj_data.data[[ID_COL, FRAME_COL, POINT_COL]],
466+
left_on=[NEIGHBOR_ID_COL, FRAME_COL],
467+
right_on=[ID_COL, FRAME_COL],
468+
suffixes=("", "_neighbor"),
469+
)
470+
471+
neighbors_with_position[DISTANCE_COL] = shapely.distance(
472+
neighbors_with_position[POINT_COL],
473+
neighbors_with_position["point_neighbor"],
474+
)
475+
476+
return neighbors_with_position[
477+
[ID_COL, FRAME_COL, NEIGHBOR_ID_COL, DISTANCE_COL]
478+
]
479+
480+
421481
def compute_time_distance_line(
422482
*, traj_data: TrajectoryData, measurement_line: MeasurementLine
423483
) -> pd.DataFrame:

tests/unit_tests/methods/test_method_utils.py

+122-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
from unittest.mock import MagicMock
2+
13
import numpy as np
24
import pandas as pd
35
import pytest
4-
from shapely import Polygon
6+
from shapely import Point, Polygon
57

68
from pedpy.column_identifier import *
79
from pedpy.data.geometry import MeasurementLine
@@ -11,6 +13,7 @@
1113
_compute_orthogonal_speed_in_relation_to_proportion,
1214
_compute_partial_line_length,
1315
compute_crossing_frames,
16+
compute_neighbor_distance,
1417
compute_neighbors,
1518
is_individual_speed_valid,
1619
is_species_valid,
@@ -454,3 +457,121 @@ def test_compute_neighbors_deprecation_warning():
454457
DeprecationWarning, match="The parameter 'as_list=True' is deprecated"
455458
):
456459
compute_neighbors(dummy_data, as_list=True)
460+
461+
462+
def test_compute_neighbor_distance():
463+
traj_data = TrajectoryData(
464+
data=pd.DataFrame(
465+
{
466+
ID_COL: [1, 2, 3],
467+
FRAME_COL: [0, 0, 0],
468+
X_COL: [0, 3, 6],
469+
Y_COL: [0, 4, 8],
470+
}
471+
),
472+
frame_rate=1,
473+
)
474+
475+
neighborhood = pd.DataFrame(
476+
{
477+
ID_COL: [1, 2],
478+
FRAME_COL: [0, 0],
479+
NEIGHBOR_ID_COL: [2, 3],
480+
}
481+
)
482+
483+
result = compute_neighbor_distance(
484+
traj_data=traj_data, neighborhood=neighborhood
485+
)
486+
487+
expected_result = pd.DataFrame(
488+
{
489+
ID_COL: [1, 2],
490+
FRAME_COL: [0, 0],
491+
NEIGHBOR_ID_COL: [2, 3],
492+
DISTANCE_COL: [
493+
5.0,
494+
5.0,
495+
], # Euclidean distances: sqrt(3^2 + 4^2) = 5
496+
}
497+
)
498+
499+
pd.testing.assert_frame_equal(result, expected_result, check_dtype=False)
500+
501+
502+
def test_compute_neighbor_distance_invalid_list_input():
503+
traj_data = MagicMock(spec=TrajectoryData)
504+
neighborhood = pd.DataFrame(
505+
{
506+
ID_COL: [1, 2],
507+
FRAME_COL: [0, 0],
508+
NEIGHBORS_COL: [[2], [3]], # as_list=True adds this column
509+
}
510+
)
511+
512+
with pytest.raises(
513+
ValueError,
514+
match="Cannot compute distance between neighbors with list-format data.",
515+
):
516+
compute_neighbor_distance(
517+
traj_data=traj_data, neighborhood=neighborhood
518+
)
519+
520+
521+
def test_compute_neighbor_distance_empty_input():
522+
traj_data = TrajectoryData(
523+
data=pd.DataFrame(columns=[ID_COL, FRAME_COL, X_COL, Y_COL]).astype(
524+
{X_COL: "float64", Y_COL: "float64"}
525+
),
526+
frame_rate=1,
527+
)
528+
neighborhood = pd.DataFrame(columns=[ID_COL, FRAME_COL, NEIGHBOR_ID_COL])
529+
530+
result = compute_neighbor_distance(
531+
traj_data=traj_data, neighborhood=neighborhood
532+
)
533+
534+
expected_result = pd.DataFrame(
535+
columns=[ID_COL, FRAME_COL, NEIGHBOR_ID_COL, DISTANCE_COL]
536+
)
537+
538+
pd.testing.assert_frame_equal(result, expected_result, check_dtype=False)
539+
540+
541+
def test_compute_neighbor_distance_different_distances():
542+
traj_data = TrajectoryData(
543+
data=pd.DataFrame(
544+
{
545+
ID_COL: [1, 2, 3],
546+
FRAME_COL: [0, 0, 0],
547+
X_COL: [0, 3, 10],
548+
Y_COL: [0, 4, 10],
549+
}
550+
),
551+
frame_rate=1,
552+
)
553+
554+
neighborhood = pd.DataFrame(
555+
{
556+
ID_COL: [1, 2],
557+
FRAME_COL: [0, 0],
558+
NEIGHBOR_ID_COL: [2, 3],
559+
}
560+
)
561+
562+
result = compute_neighbor_distance(
563+
traj_data=traj_data, neighborhood=neighborhood
564+
)
565+
566+
expected_result = pd.DataFrame(
567+
{
568+
ID_COL: [1, 2],
569+
FRAME_COL: [0, 0],
570+
NEIGHBOR_ID_COL: [2, 3],
571+
DISTANCE_COL: [5.0, 9.21954445729], # sqrt(7^2 + 6^2) = 9.21
572+
}
573+
)
574+
575+
pd.testing.assert_frame_equal(
576+
result, expected_result, check_dtype=False, atol=1e-6
577+
)

0 commit comments

Comments
 (0)