Migration Guide: v0.1.x → v1.0
Linref v1.0 is a complete redesign of the library. The standalone class-based
API (EventsCollection, EventsMerge, EventsUnion) has been replaced
with a pandas accessor pattern (.lr). This guide provides side-by-side
comparisons to help you transition existing code.
Key Architectural Changes
Aspect |
v0.1.x |
v1.0 |
|---|---|---|
Entry point |
Wrap DataFrames in |
Use |
LRS config |
Embedded in each |
Separate |
Return types |
New |
Plain DataFrames (with LRS metadata preserved) |
Relational ops |
|
|
Unions |
|
|
Geometry |
|
|
Data access |
|
The object is a DataFrame — no unwrapping needed |
Import Changes
v0.1.x |
v1.0 |
|---|---|
|
|
|
Not needed — use |
|
Not needed — use |
Setup & LRS Configuration
v0.1.x — LRS columns were passed to the EventsCollection constructor
for every dataset:
# v0.1.x
import linref as lr
ec = lr.EventsCollection(
df,
keys=['route_id'],
beg='beg',
end='end',
geom='geometry'
)
# Point events
point_ec = lr.EventsCollection(
point_df,
keys=['route_id'],
beg='beg'
)
v1.0 — Set an LRS once per DataFrame, or define a project-wide default:
# v1.0 — per-DataFrame
import linref as lr
df = df.lr.set_lrs(
key_col=['route_id'],
chain_col='chain',
beg_col='beg',
end_col='end',
geom_col='geometry',
closed='left_mod'
)
# Point events
point_df = point_df.lr.set_lrs(
key_col=['route_id'],
loc_col='beg',
closed='left_mod'
)
# Or set a default LRS for all DataFrames at once
lr.set_default_lrs(lr.LRS(
key_col=['route_id'],
chain_col='chain',
beg_col='beg',
end_col='end',
loc_col='loc',
geom_col='geometry',
closed='left_mod'
))
Tip
With a default LRS set, any DataFrame with matching column names will automatically use the correct schema — no per-object setup needed.
Dissolving Events
v0.1.x:
# v0.1.x
ec = lr.EventsCollection(df, keys=['route_id'], beg='beg', end='end')
ec_dissolved = ec.dissolve(attr=['speed_limit'], aggs=['county'])
# Access the result
result_df = ec_dissolved.df
v1.0:
# v1.0
dissolved = df.lr.dissolve(retain=['speed_limit'])
# Result IS a DataFrame — no .df unwrapping
print(dissolved[['route_id', 'beg', 'end', 'speed_limit']])
# Geometries are merged automatically (with M-enabled geometry created)
print(dissolved.iloc[0].geometry_m)
Resegmentation (to_windows → resegment)
v0.1.x:
# v0.1.x
ec_dissolved = ec.dissolve()
ec_segments = ec_dissolved.to_windows(length=5)
result_df = ec_segments.df
v1.0:
# v1.0
dissolved = df.lr.dissolve()
segmented = dissolved.lr.resegment(length=5)
Merging / Relating Events
The EventsMerge class and its aggregators have been replaced by
df.lr.relate() which returns an EventsRelation with a similar but
modernized aggregation API.
v0.1.x — Point-on-linear merge:
# v0.1.x
roads_ec = lr.EventsCollection(roads_df, keys=['route_id'], beg='beg', end='end')
crash_ec = lr.EventsCollection(crashes_df, keys=['route_id'], beg='loc')
em = roads_ec.merge(crash_ec)
crash_count = em.count() # EventsMergeAttribute aggregator
avg_speed = em['speed'].mean()
v1.0 — Same operation:
# v1.0
relation = roads_df.lr.relate(crashes_df)
# DataFrame-level aggregation
roads_df['crash_count'] = relation.count()
# Column-level aggregation (groupby-like syntax)
roads_df['avg_severity'] = relation['severity'].mean()
roads_df['crash_ids'] = relation['crash_id'].list()
# Multiple columns at once
roads_df[['ids', 'severities']] = relation[['crash_id', 'severity']].list()
# Value counts across categories
categories = ['Fatal', 'Injury', 'Property Damage Only']
roads_df[categories] = relation['severity'].value_counts()[categories]
v0.1.x — Linear-on-linear merge:
# v0.1.x
road_ec = lr.EventsCollection(roads_df, keys=['route_id'], beg='beg', end='end')
pave_ec = lr.EventsCollection(pavement_df, keys=['route_id'], beg='beg', end='end')
em = road_ec.merge(pave_ec)
weighted_avg = em['condition'].mean() # length-weighted
v1.0:
# v1.0
relation = roads_df.lr.relate(pavement_df)
roads_df['condition'] = relation['condition_rating'].mean() # length-weighted
roads_df['surface'] = relation['surface_type'].mode() # length-weighted mode
Union / Integration
v0.1.x:
# v0.1.x
ec1 = lr.EventsCollection(ref_df, keys=['route_id'], beg='beg', end='end')
ec2 = lr.EventsCollection(input_df, keys=['route_id'], beg='beg', end='end')
eu = lr.EventsUnion([ec1, ec2])
union_ec = eu.union()
union_df = union_ec.df
# Manual pandas merge to carry over attributes
result = (union_df
.merge(ref_df[['val']], left_on='index_0', right_index=True, how='left')
.merge(input_df[['categ']], left_on='index_1', right_index=True, how='left')
)
v1.0:
# v1.0
integrated = lr.integrate([ref_df, input_df])
# Index columns (integrated_index_0, integrated_index_1) link back to sources
# Attributes can be joined the same way, or use relate() for aggregation
print(integrated[['route_id', 'beg', 'end', 'integrated_index_0', 'integrated_index_1']])
Generating Point Geometries from Mileposts
v0.1.x:
# v0.1.x
ref_ec = lr.EventsCollection(ref_gdf, keys=['route_id'], beg='beg', end='end', geom='geometry')
point_ec = lr.EventsCollection(point_df, keys=['route_id'], beg='beg')
ref_ec.build_routes()
em = point_ec.merge(ref_ec)
new_geoms = em.interpolate()
point_gdf = gpd.GeoDataFrame(point_df, geometry=new_geoms)
v1.0:
# v1.0
# Ensure reference has M-enabled geometries
ref_df = ref_df.lr.add_geom_m()
# Interpolate point geometries from the LRS relationship
crashes_df['geometry'] = crashes_df.lr.relate(ref_df).interpolate()
# Or use the convenience function interpolate_from
crashes_df['geometry'] = crashes_df.lr.interpolate_from(ref_df)
Projecting Points onto a Network (Spatial)
v0.1.x:
# v0.1.x
ref_ec = lr.EventsCollection(ref_gdf, keys=['route_id'], beg='beg', end='end', geom='geometry')
proj_ec = ref_ec.project(point_gdf, nearest=True)
result_df = proj_ec.df
v1.0:
# v1.0
ref_df = ref_df.lr.add_geom_m()
projected = ref_df.lr.project(point_gdf, buffer=2, nearest=True, dropna=True)
print(projected[['route_id', 'loc', 'project_distance']])
Projecting Linear Geometries onto a Network (Spatial Parallel Projection)
v0.1.x:
# v0.1.x
ref_ec = lr.EventsCollection(ref_gdf, keys=['route_id'], beg='beg', end='end', geom='geometry')
proj_ec = ref_ec.project_parallel(line_gdf)
line_gdf[['route_id', 'beg', 'end']] = proj_ec.df[['route_id', 'beg', 'end']]
v1.0:
# v1.0
# This utilizes an entirely new algorithm based on parallel Hausdorff distance,
# which is more robust to complex geometries and can handle cases where the
# projected line deviates from the reference.
from linref.ext.spatial import parallel_project_hausdorff
ref_df = ref_df.lr.add_geom_m()
projected = parallel_project_hausdorff(
target=ref_df,
projected=line_gdf,
buffer=2,
max_distance=1,
match=1,
densify=0.1
)
print(projected[['route_id', 'beg', 'end']])
Sorting
v0.1.x:
# v0.1.x
ec.sort()
v1.0:
# v1.0
df = df.lr.sort_standard()
Accessing Group Data
v0.1.x:
# v0.1.x
ec = lr.EventsCollection(df, keys=['route_id', 'year'], beg='beg', end='end')
# Single group
eg = ec['Route 50', 2018]
# Partial key
ec_2018 = ec[:, 2018]
# Intersection query
intersecting = eg.intersecting(0.5, 1.5, closed='left_mod')
v1.0:
# v1.0
# Single group
group_df = df.lr.get_group('Route 50')
# Iterate over groups
for name, group_df in df.lr.iter_groups():
print(name, len(group_df))
# Group counts
print(df.lr.group_counts())
Validation
v0.1.x — Warnings at construction time for missing data.
v1.0:
# v1.0
# Check for invalid events
print(df.lr.valid_events.sum(), "valid events")
print(df.lr.invalid_events.sum(), "invalid events")
# Drop invalid events
df = df.lr.drop_invalid_events()
Quick Reference Table
v0.1.x |
v1.0 Equivalent |
|---|---|
|
|
|
(just use the DataFrame directly) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|