Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Compute neighbor distance #392

Merged
merged 2 commits into from
Mar 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 75 additions & 6 deletions notebooks/user_guide.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -3172,18 +3172,25 @@
"source": [
"### Neighborhood\n",
"\n",
"To analyze, which pedestrians are close to each other, it is possible to compute the neighbors of each pedestrian.\n",
"To analyze which pedestrians are close to each other, it is possible to compute the neighbors of each pedestrian.\n",
"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",
"As basis for the computation one can either use the uncut or cut Voronoi polygons.\n",
"When using the uncut Voronoi polygons, pedestrian may be detected as neighbors even when their distance is quite large in low density situation.\n",
"Therefor, 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",
"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."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Neighborhood computation\n",
"\n",
"To compute the neighbors in *PedPy* use:"
]
},
{
"cell_type": "code",
"execution_count": 316,
"execution_count": null,
"metadata": {
"collapsed": false,
"jupyter": {
Expand All @@ -3194,7 +3201,8 @@
"source": [
"from pedpy import compute_neighbors\n",
"\n",
"neighbors = compute_neighbors(individual_cutoff)"
"neighbors = compute_neighbors(individual_cutoff, as_list=False)\n",
"neighbors[0:5]"
]
},
{
Expand Down Expand Up @@ -3225,6 +3233,67 @@
"plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
":::{important}\n",
"For legacy reasons the function {func}`compute_neighbors <method_utils.compute_neighbors>` works also without specifing {code}`as_list` (defaults to {code}`True`). \n",
"We highly discourage using this, as its result is harder to be used in further computations.\n",
"Use 'as_list=False' instead.\n",
"The default value may change in future versions of *PedPy*.\n",
":::"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"tags": [
"hide-input",
"hide-cell"
]
},
"outputs": [],
"source": [
"neighbors_as_list = compute_neighbors(individual_cutoff)\n",
"neighbors_as_list[0:5]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Distance to neighbors\n",
"\n",
"For computing the distance between neighbors, *PedPy* offers a dedicated function:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from pedpy import compute_neighbor_distance\n",
"\n",
"neighbor_distance = compute_neighbor_distance(\n",
" traj_data=traj, neighborhood=neighbors\n",
")\n",
"neighbor_distance[0:5]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
":::{note}\n",
"The resulting {class}`~pandas.DataFrame` is symmetric. \n",
"If pedestrian A is a neighbor of pedestrian B, then pedestrian B is also a neighbor of pedestrian A.\n",
"Consequently, the distance between both appears twice in the DataFrame.\n",
":::"
]
},
{
"cell_type": "markdown",
"metadata": {},
Expand Down Expand Up @@ -4649,7 +4718,7 @@
],
"metadata": {
"kernelspec": {
"display_name": ".venv",
"display_name": "pedpy-313-venv",
"language": "python",
"name": "python3"
},
Expand All @@ -4663,7 +4732,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.7"
"version": "3.13.0"
}
},
"nbformat": 4,
Expand Down
4 changes: 4 additions & 0 deletions pedpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
MID_FRAME_COL,
MID_POSITION_COL,
NEIGHBORS_COL,
NEIGHBOR_ID_COL,
POINT_COL,
POLYGON_COL,
SPEED_COL,
Expand Down Expand Up @@ -77,6 +78,7 @@
compute_frame_range_in_area,
compute_individual_voronoi_polygons,
compute_intersecting_polygons,
compute_neighbor_distance,
compute_neighbors,
compute_time_distance_line,
get_invalid_trajectory,
Expand Down Expand Up @@ -154,6 +156,7 @@
"compute_individual_voronoi_polygons",
"compute_intersecting_polygons",
"compute_neighbors",
"compute_neighbor_distance",
"compute_time_distance_line",
"get_invalid_trajectory",
"is_individual_speed_valid",
Expand Down Expand Up @@ -224,6 +227,7 @@
"MID_FRAME_COL",
"LAST_FRAME_COL",
"NEIGHBORS_COL",
"NEIGHBOR_ID_COL",
"DISTANCE_COL",
"CROSSING_FRAME_COL",
"START_POSITION_COL",
Expand Down
1 change: 1 addition & 0 deletions pedpy/column_identifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
MID_FRAME_COL: Final = "mid_frame"
LAST_FRAME_COL: Final = "leaving_frame"
NEIGHBORS_COL: Final = "neighbors"
NEIGHBOR_ID_COL: Final = "neighbor_id"
DISTANCE_COL: Final = "distance"
CROSSING_FRAME_COL: Final = "crossing_frame"
START_POSITION_COL: Final = "start_position"
Expand Down
149 changes: 147 additions & 2 deletions pedpy/methods/method_utils.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
"""Helper functions for the analysis methods."""
# pylint: disable=C0302

# pylint: disable=C0302
import itertools
import logging
import warnings
from collections import defaultdict
from dataclasses import dataclass
from enum import Enum, auto
Expand All @@ -25,6 +26,7 @@
LAST_FRAME_COL,
MID_POSITION_COL,
NEIGHBORS_COL,
NEIGHBOR_ID_COL,
POINT_COL,
POLYGON_COL,
SPECIES_COL,
Expand Down Expand Up @@ -275,23 +277,57 @@ def compute_frame_range_in_area(

def compute_neighbors(
individual_voronoi_data: pd.DataFrame,
as_list: bool = True,
) -> pd.DataFrame:
"""Compute the neighbors of each pedestrian based on the Voronoi cells.

Computation of the neighborhood of each pedestrian per frame. Every other
pedestrian is a neighbor if the Voronoi cells of both pedestrian touch
and some point. The threshold for touching is set to 1mm.

Important:
For legacy reasons the function :func:`~method_utils.compute_neighbors`
works also without specifing :code:`as_list` (defaults to
:code:`True`). We highly discourage using this, as its result is
harder to be used in further computations. Use 'as_list=False' instead.
The default value may change in future versions of *PedPy*.

Args:
individual_voronoi_data (pandas.DataFrame): individual voronoi data,
needs to contain a column 'polygon', which holds a
:class:`shapely.Polygon` (result from
:func:`~method_utils.compute_individual_voronoi_polygons`)
as_list (bool): Return the neighbors as a list per pedestrian and frame,
if :code:`True`, otherwise each neighbor is in a single row.

Returns:
DataFrame containing the columns 'id', 'frame' and 'neighbors', where
neighbors are a list of the neighbor's IDs
neighbors are a list of the neighbor's IDs if as_list is :code:`True`.
Otherwise the DataFrame contains the columns 'id', 'frame',
'neighbor_id'.
"""
if as_list:
warnings.warn(
"The parameter 'as_list=True' is deprecated and may change in a "
"future version. It is kept for backwards compatibility. We "
"highly discourage using this, as its result is harder to be "
"used in further computations. Use 'as_list=False' instead.",
category=DeprecationWarning,
stacklevel=2, # Makes the warning appear at the caller level
)

return _compute_neighbors_list(
individual_voronoi_data=individual_voronoi_data
)
else:
return _compute_neighbors_single(
individual_voronoi_data=individual_voronoi_data
)


def _compute_neighbors_list(
individual_voronoi_data: pd.DataFrame,
) -> pd.DataFrame:
neighbor_df = []

for frame, frame_data in individual_voronoi_data.groupby(FRAME_COL):
Expand Down Expand Up @@ -330,9 +366,118 @@ def compute_neighbors(
)
neighbor_df.append(frame_df)

if not neighbor_df:
return pd.DataFrame(columns=[ID_COL, FRAME_COL, NEIGHBORS_COL])

return pd.concat(neighbor_df)


def _compute_neighbors_single(
individual_voronoi_data: pd.DataFrame,
) -> pd.DataFrame:
neighbor_df = []

for frame, frame_data in individual_voronoi_data.groupby(FRAME_COL):
polygons = frame_data[POLYGON_COL].to_numpy()

touching = shapely.dwithin(
polygons[:, np.newaxis], polygons[np.newaxis, :], 1e-9
)

# the peds are not neighbors of themselves
np.fill_diagonal(touching, False)

# Filter neighbor relationships based on the touching matrix
ids = frame_data[ID_COL].to_numpy()
row_idx, col_idx = np.where(
touching
) # Get row and column indices of True values
id_column = ids[row_idx] # Extract original IDs
neighbor_column = ids[col_idx] # Extract corresponding neighbor IDs

# Create DataFrame for this frame's neighbors
frame_neighbors = pd.DataFrame(
{
ID_COL: id_column,
FRAME_COL: frame,
NEIGHBOR_ID_COL: neighbor_column,
}
)

# Append to the result list
neighbor_df.append(frame_neighbors)

if not neighbor_df:
return pd.DataFrame(columns=[ID_COL, FRAME_COL, NEIGHBOR_ID_COL])

# Concatenate all frames' data into a single DataFrame
return (
pd.concat(neighbor_df, ignore_index=True)
.sort_values(by=[FRAME_COL, ID_COL])
.reset_index(drop=True)
)


def compute_neighbor_distance(
*,
traj_data: TrajectoryData,
neighborhood: pd.DataFrame,
) -> pd.DataFrame:
"""Compute the distance between the neighbors.

Computes the distance between the position of neighbors. As neighbors the
result of :func:`~compute_neighbors` with parameter :code:`as_list=False`.

Note:
The resulting :class:`~pandas.DataFrame` is symmetric. If pedestrian A
is a neighbor of pedestrian B, then pedestrian B is also a neighbor of
pedestrian A. Consequently, the distance between both appears twice in
the :class:`~pandas.DataFrame`.

Args:
traj_data (TrajectoryData): trajectory data
neighborhood (pd.DataFrame): DataFrame containing the columns 'id',
'frame' and 'neighbor_id'. The result of :func:`~compute_neighbors`
with parameter :code:`as_list=False` can be used here as input.

Raises:
ValueError: When passing a result of :func:`~compute_neighbors`
with parameter :code:`as_list=True`.

Returns:
DataFrame containing the columns 'id', 'frame', 'neighbor_id' and
'distance'.
"""
if NEIGHBORS_COL in neighborhood.columns:
raise ValueError(
"Cannot compute distance between neighbors with list-format data. "
"Please use the result of compute_neighbors with parameter "
"as_list=False."
)

neighbors_with_position = neighborhood.merge(
traj_data.data[[ID_COL, FRAME_COL, POINT_COL]],
on=[ID_COL, FRAME_COL],
how="left",
)

neighbors_with_position = neighbors_with_position.merge(
traj_data.data[[ID_COL, FRAME_COL, POINT_COL]],
left_on=[NEIGHBOR_ID_COL, FRAME_COL],
right_on=[ID_COL, FRAME_COL],
suffixes=("", "_neighbor"),
)

neighbors_with_position[DISTANCE_COL] = shapely.distance(
neighbors_with_position[POINT_COL],
neighbors_with_position["point_neighbor"],
)

return neighbors_with_position[
[ID_COL, FRAME_COL, NEIGHBOR_ID_COL, DISTANCE_COL]
]


def compute_time_distance_line(
*, traj_data: TrajectoryData, measurement_line: MeasurementLine
) -> pd.DataFrame:
Expand Down
Loading