Relating Events

This notebook covers events-based conflation, aggregation, geometry creation, and dataset integration.

[1]:
import linref as lr

# Load sample datasets with LRS pre-configured
roadways = lr.datasets.load('roadways', set_lrs=True)
crashes = lr.datasets.load('crashes', set_lrs=True)
pavement = lr.datasets.load('pavement', set_lrs=True)
[2]:
# Prepare data: dissolve and resegment roadways
dissolved = roadways.lr.dissolve()
resegmented = dissolved.lr.resegment(length=5, fill='cut')

Conflating Point Data onto Linear Data

An important functionality of linearly referenced data is events-based conflation. Linref makes this easy with the relate method, which builds dynamic relationships between two linearly referenced dataframes that can then be aggregated with a variety of methods.

[3]:
# Conflating point data onto linear data
#          left_df               right_df   <-- relations use terminology of left and right
relation = resegmented.lr.relate(crashes)

# Dataframe-level aggregations like count are easy
print(relation.count()) # Count the number of crashes on each roadway segment

# Aggregator results are designed to be assigned as new columns to the left dataframe
resegmented['crash_counts'] = relation.count()
print(resegmented.head(3)[['route', 'beg', 'end', 'crash_counts']])
[1 2 1 4 3 5 4 0]
  route   beg   end  crash_counts
0   I-5   0.0   5.0             1
1   I-5   5.0  10.0             2
2   I-5  10.0  12.0             1

Column-level aggregators can be accessed using column indexing on the relation instance, using a syntax similar to the pandas groupby syntax:

[4]:
# List of unique values (creates a single column)
resegmented['crash_ids'] = relation['crash_id'].list()
# Apply an aggregator to multiple columns at once
resegmented[['crash_ids', 'severities']] = relation[['crash_id', 'severity']].list()
# Counts of unique values (creates a number of columns equal to the number of unique values)
severities = ['Fatal', 'Injury', 'Property Damage Only']
resegmented[severities] = relation['severity'].value_counts()[severities] # Returned as a dataframe

print(resegmented.head(3)[['route', 'beg', 'end', 'crash_ids', 'severities'] + severities])
  route   beg   end crash_ids       severities  Fatal  Injury  \
0   I-5   0.0   5.0       [3]         [Injury]    0.0     1.0
1   I-5   5.0  10.0    [5, 6]  [Fatal, Injury]    1.0     1.0
2   I-5  10.0  12.0      [13]         [Injury]    0.0     1.0

   Property Damage Only
0                   0.0
1                   0.0
2                   0.0

Conflating Linear Data onto Linear Data

[5]:
# Conflating linear data onto linear data
relation = resegmented.lr.relate(pavement)

# Length-weighted mean of numerical data
resegmented['condition_rating'] = relation['condition_rating'].mean().round(3)
resegmented['condition_ratings'] = relation['condition_rating'].list()

print(resegmented.head(3)[['route', 'beg', 'end', 'condition_rating', 'condition_ratings']])

# Length-weighted mode of categorical data
resegmented['surface_type'] = relation['surface_type'].mode()
resegmented['surface_types'] = relation['surface_type'].list()

print(resegmented.head(3)[['route', 'beg', 'end', 'surface_type', 'surface_types']])
  route   beg   end  condition_rating condition_ratings
0   I-5   0.0   5.0            81.900          [84, 77]
1   I-5   5.0  10.0            81.086      [77, 83, 82]
2   I-5  10.0  12.0            82.000              [82]
  route   beg   end surface_type                  surface_types
0   I-5   0.0   5.0      Asphalt            [Asphalt, Concrete]
1   I-5   5.0  10.0     Concrete  [Concrete, Asphalt, Concrete]
2   I-5  10.0  12.0     Concrete                     [Concrete]

Creating New Geometries from Relations

We can create new geometries for linearly referenced point or linear dataframes by relating them to linearly referenced dataframes containing m-enabled geometries. When dissolving the roadways layer earlier, that method automatically created m-enabled geometries to retain dissolved event information. We can create them manually for other layers using the add_geom_m method, which applies event boundaries to the existing geometries.

[6]:
# Add m-enabled geometries to the roadways layer
roadways = roadways.lr.add_geom_m()

# Interpolate new crash geometries based on their LRS information
interpolated = crashes.lr.relate(roadways).interpolate()
print(list(crashes.geometry)[:3])
print(list(interpolated)[:3])

# Cut new roadway geometries from the parent LRS layer
cut = roadways.lr.relate(dissolved).cut()
print(list(roadways.geometry_m)[:3])
print(list(cut)[:3])
[<POINT (2.56 0.52)>, <POINT (8.18 2.15)>, <POINT (4.47 11.13)>]
[<POINT (2.56 0.517)>, <POINT (8.18 2.155)>, <POINT (4.47 11.132)>]
[LINESTRING M (0.0 0.0 0.0, 2.5 0.5 2.5) # linref compatibility approximation, LINESTRING M (2.5 0.5 2.5, 5.0 1.2 5.0) # linref compatibility approximation, LINESTRING M (5.0 1.2 5.0, 7.8 2.0 7.8) # linref compatibility approximation]
[LINESTRING M (0.0 0.0 0.0, 2.5 0.5 2.5) # linref compatibility approximation, LINESTRING M (2.5 0.5 2.5, 5.0 1.2 5.0) # linref compatibility approximation, LINESTRING M (5.0 1.2 5.0, 7.8 2.0 7.8) # linref compatibility approximation]

Integrating Multiple Datasets

We can combine multiple event datasets into a single, unified dataframe using the integrate function. This analyzes a list of passed linearly referenced dataframes, creating a single version featuring the least common event intervals among them. New events can be tabularly joined back to each of their source dataframes using the created integrated_index_[#] columns.

[7]:
# Combine multiple datasets with a matching LRS
integrated = lr.integrate([roadways, pavement])

print(integrated.lr.get_group('SR-1'))
  route  beg  end  integrated_index_0  integrated_index_1
4  SR-1  0.0  1.6                 4.0                 6.0
5  SR-1  1.6  3.2                 4.0                 7.0
6  SR-1  3.2  6.5                 5.0                 8.0
7  SR-1  6.5  9.8                 6.0                 9.0

New geometries can be cut for these intervals by relating the integrated dataframe back to spatial dataframe on the same LRS.

[8]:
# Relate the integrated dataset back to the roadways layer
integrated['geometry_m'] = integrated.lr.relate(roadways).cut()

# A short-hand version of this pattern is available with the cut_from method
# This retrieves both M-enabled and non-M-enabled geometries in a single line
integrated.lr.cut_from(roadways, inplace=True)

print(integrated.columns)
Index(['route', 'beg', 'end', 'integrated_index_0', 'integrated_index_1',
       'geometry_m', 'geometry'],
      dtype='object')