This notebook was developed by Keneth Garcia. Source and license info are on GitHub.
import time
import math
import numpy as np
import pandas as pd
from tqdm import trange
from sievecomp.io.sql import load_sql
from sievecomp.core.runner import Runner
Optimizing Duplicates Search¶
Actually, the SieveComP package includes a function to check for duplicates, which is based on the time and geographical distance between events. This notebook summarizes the optimization process developed on the duplicates search, by using a more efficient algorithm that can reduce the time complexity of the search.
Querying SeisComP6 Database¶
Let's start by querying the SeisComP6 database to get the events for a given time range. We will wrap the SQL contained in the seismic_revision_routine.toml config file into a function that can be called with different time ranges. The function will return a pandas dataframe with the events for the given time range (see more details at the Advanced Usage notebook).
Given that SieveComP returns a RunResult object, we can use the total_events attribute to get the events dataframe. For now, we will ignore the other attributes of the RunResult object, since we are only interested in the events dataframe, including the checks and duplicates.
def fetch_events_by_time_range(
runner: Runner,
query_name: str,
start_time: pd.Timestamp,
end_time: pd.Timestamp
) -> pd.DataFrame:
"""
Fetches events from a specific SeisComP database query within a dynamic time window.
Bypasses the engine's checks and duplicates logic.
"""
# 1. Retrieve the configured time column used for sorting and filtering
time_col = runner._cm.get_time_column()
# 2. Prepare the dynamic time bounds and SQL parameters
sql_params = {}
where_clauses = []
if start_time:
sql_params["start_time"] = start_time.strftime('%Y-%m-%d %H:%M:%S')
where_clauses.append(f"base_query.{time_col} >= %(start_time)s")
if end_time:
sql_params["end_time"] = end_time.strftime('%Y-%m-%d %H:%M:%S')
where_clauses.append(f"base_query.{time_col} < %(end_time)s")
where_sql = " AND ".join(where_clauses)
# 3. Load the base SQL text strictly as it is defined in the TOML configuration
query_cfg = runner._cm.select_query(name=query_name)
base_sql = load_sql(
query_cfg=query_cfg,
base_dir=runner._cm.base_dir,
config_name=runner._cm.config_path.name
)
# 4. Wrap the base query as a subquery, applying the new WHERE clauses and sorting
wrapped_sql = f"""SELECT * FROM ({base_sql}) AS base_query WHERE {where_sql} ORDER BY base_query.{time_col} ASC"""
# 5. Fetch the events (skipping the check/duplicate evaluation steps for raw data retrieval)
result = runner.run(
query_name=query_name,
multi_query=False,
perform_duplicates=False,
perform_checks=False,
sql_text_override=wrapped_sql,
sql_params=sql_params
)
# 6. Return the raw queried events DataFrame
return result.total_events
With this function, we can now fetch events from the SeisComP6 database for any given time range, and we can use this data to test the duplicates search function. Let's fetch the events for the time range from 2026-03-17 to 2026-10-09, and see how many events we have in this time range:
# Define the time range for the query
start_time = pd.Timestamp('2026-03-17 00:00:00')
end_time = pd.Timestamp('2026-10-09 00:00:00')
# Create a Runner instance (assuming the config file is in the current directory)
runner = Runner(config_path='../../examples/data/configs/seismic_revision_routine.toml')
# Fetch the events for the specified time range
event_df3 = fetch_events_by_time_range(runner, "SeisComP6", start_time, end_time)
# Describe the dataset by counting how many events we have in this time range by event type
event_counts = event_df3['event_type'].value_counts()
print("Event counts by type:")
print(event_counts)
Event counts by type: event_type not locatable 30241 earthquake 18812 outside of network interest 970 explosion 698 not existing 136 volcanic eruption 38 Name: count, dtype: int64
Checking for Duplicates - Initial Implementation¶
Let's start by defining the first basic function to check duplicates. This implementation is based on the adjacent-only logic, which means that it only compares each event with the next one in the list using a for loop. This is a good approach to reduce the time complexity of the search, given that the probability of having three or more earthquakes within a window of 10-12 s is low, but it can be further optimized by using some micro-optimizations as we will see later. The function will return a pandas dataframe with the duplicates found, including the observations for each event:
columns = ['time_value', 'publicID', 'text', 'depth_value', 'magnitude_value', 'magnitude_type',
'quality_standardError', 'depth_uncertainty', 'latitude_uncertainty', 'longitude_uncertainty',
'quality_associatedPhaseCount', 'creationInfo_author', 'event_type', 'creationInfo_agencyID']
def calculate_haversine_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
"""
Calculates the great-circle distance in kilometers between two points
on the Earth's surface using the Haversine formula.
"""
R = 6371.0 # Earth radius in kilometers
# Convert decimal degrees to radians
phi1 = math.radians(lat1)
phi2 = math.radians(lat2)
dphi = math.radians(lat2 - lat1)
dlambda = math.radians(lon2 - lon1)
# Haversine formula
a = math.sin(dphi / 2.0)**2 + math.cos(phi1) * math.cos(phi2) * math.sin(dlambda / 2.0)**2
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
return R * c
def check_duplicates(
events: pd.DataFrame
):
"""
Identifies duplicate events based on time and geographical distance.
Parameters:
-----------
events: pandas dataframe
Seismic data for the given time range. Generally obtained from the connect2mysql function.
Returns:
--------
duplicates: A pandas dataframe
Table with information about duplicate events.
"""
# Filter events with the following types
checks = ["earthquake", "volcanic eruption", "explosion", "outside of network interest"]
selections = events[events['event_type'].isin(checks)].reset_index(drop=True)
# Make a pandas dataframe to store the info of the duplicates
duplicates = pd.DataFrame()
# Loop over al earthquakes, ordered by time
for i in trange(len(selections) - 1, desc="Checking duplicates", unit=" events", leave=False):
event1 = selections.iloc[i] # Take the i-esim event on the list
event2 = selections.iloc[i + 1] # Compared to the next event
# Check if the events are within 4 seconds of each other
time_diff = abs((event2['time_value'] - event1['time_value']).total_seconds())
if time_diff <= 4:
# Estimate the distance between the two events using haversine formula
distance = calculate_haversine_distance(event1['latitude_value'], event1['longitude_value'],
event2['latitude_value'], event2['longitude_value'])
if distance <= 100: # If both events are within 100 km and 4 seconds, they can be duplicates
dup_1 = event1[columns].copy()
dup_1['Observations'] = f'Possible duplicate event of {event2["publicID"]}'
dup_2 = event2[columns].copy()
dup_2['Observations'] = f'Possible duplicate event of {event1["publicID"]}'
# Add the two events to the duplicates dataframe
duplicates = pd.concat([duplicates, dup_1.to_frame().T], ignore_index=True)
duplicates = pd.concat([duplicates, dup_2.to_frame().T], ignore_index=True)
return duplicates
As you can see from the function, there are two criteria to consider an event as a duplicate: the time difference between the two events must be less than or equal to 4 seconds, and the geographical distance between the two events must be less than or equal to 100 km. The function loops over all the events, and compares each event with the next one in the list. If both criteria are met, the two events are considered duplicates, and their information is stored in a new dataframe called duplicates.
The idea of checking only the next event in the list is based on the fact that usually the time difference between earthquakes is greater than 4 seconds, so it is unlikely that two events that are far apart in the list are duplicates. Let's test the results for all the earthquakes fetched and see how many duplicates we can find and the time it takes to run the function.
time1 = time.time()
duplicates_previous = check_duplicates(event_df3)
time2 = time.time()
print(f"Number of duplicates found: {len(duplicates_previous)}")
print(f"Time taken to run the function: {time2 - time1} seconds")
Number of duplicates found: 22 Time taken to run the function: 7.795006990432739 seconds
Checking for duplicates - Micro-optimization of the adjacent-only logic¶
The previous function is based on the adjacent-only logic, which means that it only compares each event with the next one in the list. This is a good approach to reduce the time complexity of the search, but it can be further optimized by using some micro-optimizations. For example, we can pre-extract the relevant columns as NumPy arrays to avoid the overhead of pandas indexing in the loop. We can also accumulate the duplicate rows in a list and create the DataFrame at the end, instead of concatenating in each iteration. Let's implement this micro-optimized version of the adjacent-only logic:
def check_duplicates_adjacent(events: pd.DataFrame) -> pd.DataFrame:
"""
Identifies duplicate events based on time and geographical distance.
Same adjacent-only logic as the original, but micro-optimized:
- Pre-extracted NumPy arrays (avoids per-row iloc overhead)
- Row accumulation in a list (avoids repeated pd.concat in loop)
- Single pd.concat at the end
Time complexity: O(n) comparisons — only i vs i+1
Parameters:
-----------
events : pd.DataFrame
Seismic data for the given time range.
Returns:
--------
duplicates : pd.DataFrame
Table with information about duplicate events.
"""
checks = ["earthquake", "volcanic eruption", "explosion", "outside of network interest"]
selections = events[events['event_type'].isin(checks)].reset_index(drop=True)
# Edge case: empty or single-event dataset
if len(selections) < 2:
return pd.DataFrame(columns=columns + ['Observations'])
# --- Pre-extract arrays for fast access ---
times = selections['time_value'].values # numpy datetime64
lats = selections['latitude_value'].values.astype(np.float64)
lons = selections['longitude_value'].values.astype(np.float64)
ids = selections['publicID'].values
TIME_WINDOW = np.timedelta64(4, 's')
DIST_THRESHOLD = 100 # km
dup_rows = [] # Accumulate rows here, build DF once at the end
for i in trange(len(selections) - 1, desc="Checking duplicates", unit=" events", leave=False):
# Time check using numpy (faster than .total_seconds() on Timedelta)
if abs(times[i + 1] - times[i]) <= TIME_WINDOW:
# Distance check using haversine formula
distance = calculate_haversine_distance(lats[i], lons[i], lats[i + 1], lons[i + 1])
if distance <= DIST_THRESHOLD:
row_i = selections.iloc[i][columns].copy()
row_i['Observations'] = f'Possible duplicate event of {ids[i + 1]}'
row_j = selections.iloc[i + 1][columns].copy()
row_j['Observations'] = f'Possible duplicate event of {ids[i]}'
dup_rows.extend([row_i, row_j])
# Concat duplicates to one single Dataframe
if dup_rows:
duplicates = pd.DataFrame(dup_rows, columns=columns + ['Observations']).reset_index(drop=True)
else:
duplicates = pd.DataFrame(columns=columns + ['Observations'])
return duplicates
# Repeat the test with the adjacent-only logic, but micro-optimized
time1 = time.time()
duplicates_adjacent = check_duplicates_adjacent(event_df3)
time2 = time.time()
print(f"Number of duplicates found: {len(duplicates_adjacent)}")
print(f"Time taken to run the function: {time2 - time1} seconds")
Number of duplicates found: 22 Time taken to run the function: 0.2341165542602539 seconds
# Check if the results are the same
print(f"Number of duplicates found with original adjacent-only: {len(duplicates_previous)}")
print(f"Number of duplicates found with micro-optimized adjacent-only: {len(duplicates_adjacent)}")
Number of duplicates found with original adjacent-only: 22 Number of duplicates found with micro-optimized adjacent-only: 22
# Find differences between the two results
differences = pd.concat([duplicates_previous, duplicates_adjacent, duplicates_adjacent]).drop_duplicates(keep=False)
print(f"Number of differences between original and micro-optimized adjacent-only: {len(differences)}")
Number of differences between original and micro-optimized adjacent-only: 0
As you can see, the results are the same, but the time taken to run the function is significantly reduced. This is because we have avoided the overhead of pandas indexing in the loop, and we have also avoided the repeated concatenation of dataframes in each iteration. The time complexity of this function is still O(n), since we are only comparing each event with the next one in the list, but the constant factors have been reduced, which can make a big difference when there are many events in the dataset.
There are a significant improvement in the time taken to run the function. This micro-optimized version of the adjacent-only logic can be used as a baseline for further optimizations, such as using a more efficient algorithm that can reduce the time complexity of the search even further.
Checking for duplicates - Vectorized adjacent-only logic¶
Further improving: We can just calculate all the temporal and spatial distances between all the events in one-single shot using vectorized NumPy operations, and then filter the results to find the duplicates. This will reduce the time complexity of the search to O(n^2), but it will be much faster than the previous versions, since we are using vectorized operations instead of loops. Let's implement this version:
def haversine_np(lat1, lon1, lat2, lon2):
"""
Fully vectorized haversine distance (km) between paired lat/lon arrays.
Replaces the scalar `hs.haversine()` calls inside the original loop.
"""
R = 6371.0088
lat1r, lat2r = np.radians(lat1), np.radians(lat2)
dlat = lat2r - lat1r
dlon = np.radians(lon2 - lon1)
a = np.sin(dlat / 2.0) ** 2 + np.cos(lat1r) * np.cos(lat2r) * np.sin(dlon / 2.0) ** 2
return 2 * R * np.arcsin(np.sqrt(a))
def check_duplicates_vectorized(
events: pd.DataFrame,
time_window: int,
dist_threshold: float,
) -> pd.DataFrame:
"""
Identifies duplicate events based on time proximity and geographical
distance between temporally-adjacent events (after sorting by time).
Time complexity: O(n) — all comparisons are vectorized NumPy array
operations; only the (typically rare) confirmed duplicate pairs are
iterated in Python to build per-row observation text.
Parameters
----------
events : pd.DataFrame
Seismic data for the given time range. Must contain 'time_value',
'latitude_value', 'longitude_value', and 'publicID' columns.
time_window : int
Time window in seconds to detect duplicated events.
dist_threshold : float
Distance threshold in kilometers to detect duplicates.
Returns
-------
pd.DataFrame
Subset of `events` (all original columns preserved) with an added
'Observations' column, containing only the flagged duplicate rows.
"""
if len(events) < 2:
empty = events.copy()
empty['Observations'] = pd.Series(dtype='object')
return empty.iloc[0:0]
sorted_events = events.sort_values('time_value').reset_index(drop=True)
times = sorted_events['time_value'].to_numpy()
lats = sorted_events['latitude_value'].to_numpy(dtype=np.float64)
lons = sorted_events['longitude_value'].to_numpy(dtype=np.float64)
ids = sorted_events['publicID'].to_numpy()
time_diff = np.abs(times[1:] - times[:-1])
within_time = time_diff <= np.timedelta64(time_window, 's')
distances = haversine_np(lats[:-1], lons[:-1], lats[1:], lons[1:])
within_dist = distances <= dist_threshold
adjacent_dup = within_time & within_dist
dup_idx = np.where(adjacent_dup)[0]
if dup_idx.size == 0:
empty = events.copy()
empty['Observations'] = pd.Series(dtype='object')
return empty.iloc[0:0]
obs_map: dict[int, list[str]] = {}
for i in dup_idx:
obs_map.setdefault(int(i), []).append(f'Possible duplicate event of {ids[i + 1]}')
obs_map.setdefault(int(i) + 1, []).append(f'Possible duplicate event of {ids[i]}')
flagged_positions = sorted(obs_map.keys())
flagged = sorted_events.iloc[flagged_positions].copy()
flagged['Observations'] = ['; '.join(obs_map[p]) for p in flagged_positions]
return flagged.reset_index(drop=True)
# Repeat the test with the adjacent-only logic, but vectorized
time1 = time.time()
# Filter events by label:
filtered = event_df3[event_df3['event_type'].isin(["earthquake", "volcanic eruption", "explosion", "outside of network interest"])].reset_index(drop=True)
duplicates_vectorized = check_duplicates_vectorized(filtered, time_window=4, dist_threshold=100)
time2 = time.time()
print(f"Number of duplicates found: {len(duplicates_vectorized)}")
print(f"Time taken to run the function: {time2 - time1} seconds")
Number of duplicates found: 22 Time taken to run the function: 0.02883005142211914 seconds
The apparent discrepancy between the results is due to the fact that the vectorized version joins the observations for each event into a single string, while the previous versions created separate rows for each observation. This means that if an event is flagged as a duplicate of multiple other events, it will appear only once in the vectorized result, with all relevant observations concatenated. In contrast, the previous versions would create multiple rows for that event, one for each duplicate relationship.
Ignoring this difference in representation, the underlying logic for identifying duplicates is consistent across all implementations. The vectorized approach is significantly faster due to its use of NumPy operations, which are optimized for performance and can handle large datasets more efficiently than iterative loops in Python.
Checking Duplicates - Sorted Sliding Window Approach¶
Now, let's use a improved version created using the Sorted Sliding Window approach. We will also compare the results with the previous version of the function, which was based on a nested loop that compared all events with each other, and see how much time we can save with the new algorithm.
def check_duplicates_2(events: pd.DataFrame) -> pd.DataFrame:
"""
Identifies duplicate events based on time and geographical distance. Optimized using a sorted sliding time window (O(n log n)).
Parameters:
-----------
events : pd.DataFrame
Seismic data for the given time range.
Returns:
--------
duplicates : pd.DataFrame
Table with information about duplicate events.
"""
checks = ["earthquake", "volcanic eruption", "explosion", "outside of network interest"]
selections = (
events[events['event_type'].isin(checks)]
.sort_values('time_value')
.reset_index(drop=True)
)
# Edge case: empty or single-event dataset
if len(selections) < 2:
return pd.DataFrame(columns=columns + ['Observations'])
# Pre-extract arrays for fast access (avoids per-row iloc overhead)
times = selections['time_value'].values
lats = selections['latitude_value'].values
lons = selections['longitude_value'].values
ids = selections['publicID'].values
TIME_WINDOW = 4 # seconds
DIST_THRESHOLD = 100 # km
# Track which (i, j) pairs were already flagged to avoid duplicate rows
flagged_pairs = set()
dup_rows = []
left = 0 # Left pointer of the sliding window
for right in trange(1, len(selections), desc="Checking duplicates", unit=" events", leave=False):
# Advance left pointer: drop events outside the 4-second window
while left < right:
delta = (times[right] - times[left]) / pd.Timedelta('1s')
if delta > TIME_WINDOW:
left += 1
else:
break
# Compare `right` against every event still inside the window [left, right-1]
for i in range(left, right):
pair = (i, right) if i < right else (right, i)
if pair in flagged_pairs:
continue
time_diff = abs(
(selections.iloc[right]['time_value'] - selections.iloc[i]['time_value'])
.total_seconds()
)
if time_diff > TIME_WINDOW:
continue
distance = calculate_haversine_distance(lats[i], lons[i], lats[right], lons[right])
if distance <= DIST_THRESHOLD:
flagged_pairs.add(pair)
row_i = selections.iloc[i][columns].copy()
row_i['Observations'] = f'Possible duplicate event of {ids[right]}'
row_j = selections.iloc[right][columns].copy()
row_j['Observations'] = f'Possible duplicate event of {ids[i]}'
dup_rows.extend([row_i, row_j])
# Build duplicates dataframe in one shot (avoids repeated pd.concat overhead)
if dup_rows:
duplicates = pd.DataFrame(dup_rows, columns=columns + ['Observations']).reset_index(drop=True)
else:
duplicates = pd.DataFrame(columns=columns + ['Observations'])
return duplicates
time1 = time.time()
duplicates_sstw = check_duplicates_2(event_df3)
time2 = time.time()
print(f"Number of duplicates found: {len(duplicates_sstw)}")
print(f"Time taken to run the function: {time2 - time1} seconds")
Number of duplicates found: 22 Time taken to run the function: 0.9724502563476562 seconds
As you can see from here, the time taken to run the function is slightly higher than the micro-optimized adjacent-only version (51% slower), but it is still significantly lower than the original nested loop version (90% faster). Moreover, this method can find duplicates that are not adjacent in the list, as long as they are within the 4-second time window. This can be useful in cases where there are multiple events that occur within a short time frame, but are not necessarily adjacent in the list.
The time complexity of this function is O(n log n) due to the initial sorting step, and O(n * k) for the sliding window comparisons, where k is the average number of events within the 4-second window. In practice, this can be much faster than the O(n^2) complexity of the original nested loop approach, especially when there are many events in the dataset. Let's check if the duplicates found with this new approach are the same as the ones found with the adjacent-only logic, and see if there are any additional duplicates that were not found with the previous method.
# Check if the results are the same
print(f"Number of duplicates found with adjacent-only: {len(duplicates_adjacent)}")
print(f"Number of duplicates found with sorted sliding window: {len(duplicates_sstw)}")
differences = pd.concat([duplicates_sstw, duplicates_adjacent, duplicates_adjacent]).drop_duplicates(keep=False)
print(f"Number of additional duplicates found with sorted sliding window: {len(differences)}")
Number of duplicates found with adjacent-only: 22 Number of duplicates found with sorted sliding window: 22 Number of additional duplicates found with sorted sliding window: 0
Checking Duplicates - Vectorized Sorted Sliding Window Approach¶
Now, let's implement a fully vectorized version of the sorted sliding window approach. This will allow us to leverage NumPy's efficient array operations to further reduce the time complexity of the search. The idea is to precompute the time and distance matrices for all events, and then filter the results based on the specified time and distance thresholds. This will allow us to identify duplicate events without the need for nested loops, resulting in a significant speedup for large datasets.
def check_duplicates_2_vectorized(
events: pd.DataFrame,
columns: list[str],
time_window: int,
dist_threshold: float,
) -> pd.DataFrame:
"""
Identifies duplicate events based on time proximity and geographical
distance, using a sliding time window (compares every event against
ALL others within time_window seconds, not just its direct neighbor).
Parameters
----------
events : pd.DataFrame
Seismic data for the given time range. Must contain the columns
referenced by `columns`.
columns : list[str]
Ordered list [time_col, lat_col, lon_col, id_col] identifying which
columns hold time, latitude, longitude, and event ID values.
time_window : int
Time window in seconds to detect duplicated events.
dist_threshold : float
Distance threshold in kilometers to detect duplicates.
Returns
-------
pd.DataFrame
Subset of `events` (all original columns preserved) with an added
'Observations' column, one row per flagged duplicate event —
chains of 3+ nearby events are merged into a single row per event
rather than duplicated across multiple pair-rows.
"""
time_col, lat_col, lon_col, id_col = columns
selections = events.copy()
if len(selections) < 2:
empty = events.copy()
empty['Observations'] = pd.Series(dtype='object')
return empty.iloc[0:0]
sorted_events = selections.sort_values(time_col).reset_index(drop=True)
times = sorted_events[time_col].to_numpy()
lats = sorted_events[lat_col].to_numpy(dtype=np.float64)
lons = sorted_events[lon_col].to_numpy(dtype=np.float64)
ids = sorted_events[id_col].to_numpy()
n = len(sorted_events)
window_delta = np.timedelta64(time_window, 's')
# For every i, right_bound[i] = first index j where times[j] > times[i] + window
right_bound = np.searchsorted(times, times + window_delta, side='right')
obs_map: dict[int, list[str]] = {}
for i in range(n - 1):
j_end = right_bound[i]
if j_end <= i + 1:
continue
window_lats = lats[i + 1:j_end]
window_lons = lons[i + 1:j_end]
distances = haversine_np(lats[i], lons[i], window_lats, window_lons)
matched_offsets = np.where(distances <= dist_threshold)[0]
if matched_offsets.size == 0:
continue
matched_js = i + 1 + matched_offsets
for j in matched_js:
obs_map.setdefault(i, []).append(f'Possible duplicate event of {ids[j]}')
obs_map.setdefault(int(j), []).append(f'Possible duplicate event of {ids[i]}')
if not obs_map:
empty = events.copy()
empty['Observations'] = pd.Series(dtype='object')
return empty.iloc[0:0]
flagged_positions = sorted(obs_map.keys())
flagged = sorted_events.iloc[flagged_positions].copy()
flagged['Observations'] = ['; '.join(obs_map[p]) for p in flagged_positions]
return flagged.reset_index(drop=True)
# Repeat the test with the vectorized sswa
time1 = time.time()
duplicates_vectorized_sswa = check_duplicates_2_vectorized(filtered, time_window=4, dist_threshold=100, columns=['time_value', 'latitude_value', 'longitude_value', 'publicID'])
time2 = time.time()
print(f"Number of duplicates found: {len(duplicates_vectorized_sswa)}")
print(f"Time taken to run the function: {time2 - time1} seconds")
Number of duplicates found: 22 Time taken to run the function: 0.03940939903259277 seconds
Overall results¶
Summarizing the results of the different duplicate detection methods, we have for 50987 events:
- Original, for loop version: 7.795 s
- Original, micro-optimized adjacent-only: 0.234 s
- Vectorized adjacent-only: 0.029 s
- Sorted Sliding Window: 0.972 s
- Vectorized Sorted Sliding Window: 0.039 s
Therefore, the overall recommendation is to use the vectorized adjacent-only version if the time window is similar to the mean temporal separation between events, since it is the fastest and most efficient method. However, if the time window is larger than the mean temporal separation between events, the sorted sliding window approach may be more appropriate, as it can detect duplicates that are not adjacent in the list.
At SieveComP, we are constantly working to improve the performance and efficiency of our algorithms, and we hope that this tutorial has provided some insight into the optimization process for duplicate detection in seismic event data.
NOTE: Actually we have implemented the vectorized versions of the adjacent-only and sorted sliding window approaches in the latest version of the package, which can be used directly by calling the
check_duplicatesfunction with the appropriate parameters.