geofusion 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- geofusion-0.1.0/LICENSE +21 -0
- geofusion-0.1.0/PKG-INFO +176 -0
- geofusion-0.1.0/README.md +124 -0
- geofusion-0.1.0/geofusion/__init__.py +139 -0
- geofusion-0.1.0/geofusion/clustering/__init__.py +13 -0
- geofusion-0.1.0/geofusion/clustering/kmeans_clustering.py +81 -0
- geofusion-0.1.0/geofusion/clustering/kmedoids_clustering.py +57 -0
- geofusion-0.1.0/geofusion/clustering/sdsgc_clustering.py +413 -0
- geofusion-0.1.0/geofusion/clustering/ukmeans_clustering.py +209 -0
- geofusion-0.1.0/geofusion/clustering/ukmedoids_clustering.py +299 -0
- geofusion-0.1.0/geofusion/core.py +841 -0
- geofusion-0.1.0/geofusion/estimators/__init__.py +13 -0
- geofusion-0.1.0/geofusion/estimators/dnn_estimator.py +479 -0
- geofusion-0.1.0/geofusion/estimators/ekf_estimator.py +190 -0
- geofusion-0.1.0/geofusion/estimators/kalman_estimator.py +129 -0
- geofusion-0.1.0/geofusion/estimators/particle_filter_estimator.py +227 -0
- geofusion-0.1.0/geofusion.egg-info/PKG-INFO +176 -0
- geofusion-0.1.0/geofusion.egg-info/SOURCES.txt +25 -0
- geofusion-0.1.0/geofusion.egg-info/dependency_links.txt +1 -0
- geofusion-0.1.0/geofusion.egg-info/requires.txt +9 -0
- geofusion-0.1.0/geofusion.egg-info/top_level.txt +1 -0
- geofusion-0.1.0/pyproject.toml +54 -0
- geofusion-0.1.0/setup.cfg +4 -0
- geofusion-0.1.0/tests/test_metrics.py +158 -0
- geofusion-0.1.0/tests/test_pipeline.py +165 -0
- geofusion-0.1.0/tests/test_sigma.py +134 -0
- geofusion-0.1.0/tests/test_validation.py +122 -0
geofusion-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Basant Mounir, Amr T. Abdel-Hamid
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
geofusion-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: geofusion
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Uncertain machine learning framework for GPS/GNSS sensor fusion
|
|
5
|
+
Author-email: Basant Mounir <basant.mounir@gmail.com>, "Amr T. Abdel-Hamid" <amr.abdelhamid@kaust.edu.sa>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Basant Mounir, Amr T. Abdel-Hamid
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Dataset, https://www.kaggle.com/code/basantmounir/geofusion-dataset
|
|
29
|
+
Keywords: GPS,GNSS,sensor fusion,uncertain clustering,machine learning,positioning,GeoFusion
|
|
30
|
+
Classifier: Development Status :: 3 - Alpha
|
|
31
|
+
Classifier: Intended Audience :: Science/Research
|
|
32
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
33
|
+
Classifier: Programming Language :: Python :: 3
|
|
34
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
38
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
39
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
40
|
+
Requires-Python: >=3.9
|
|
41
|
+
Description-Content-Type: text/markdown
|
|
42
|
+
License-File: LICENSE
|
|
43
|
+
Requires-Dist: numpy>=1.24
|
|
44
|
+
Requires-Dist: pandas>=1.5
|
|
45
|
+
Requires-Dist: scipy>=1.10
|
|
46
|
+
Requires-Dist: scikit-learn>=1.2
|
|
47
|
+
Requires-Dist: torch>=2.0
|
|
48
|
+
Provides-Extra: dev
|
|
49
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
50
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
51
|
+
Dynamic: license-file
|
|
52
|
+
|
|
53
|
+
# GeoFusion
|
|
54
|
+
|
|
55
|
+
An uncertain machine learning framework for GPS/GNSS sensor fusion.
|
|
56
|
+
|
|
57
|
+
GeoFusion estimates true location events from noisy phone-reported GPS coordinates by modelling each observation as an uncertain object, clustering by location event, and fusing the cluster into a refined position estimate.
|
|
58
|
+
|
|
59
|
+
## Installation
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install geofusion
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Quick start
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
import pandas as pd
|
|
69
|
+
from geofusion import run_geofusion
|
|
70
|
+
|
|
71
|
+
df = pd.read_csv("sample_top100_groups.csv")
|
|
72
|
+
|
|
73
|
+
# UK-medoids (mountain uncertainty model) + Kalman filter
|
|
74
|
+
result = run_geofusion(
|
|
75
|
+
df = df,
|
|
76
|
+
model = "mountain",
|
|
77
|
+
algorithm = "ukmedoids",
|
|
78
|
+
algo_params = dict(k=100, random_state=42, n_init=10, n_samples=50),
|
|
79
|
+
estimator = "kf",
|
|
80
|
+
)
|
|
81
|
+
print(result)
|
|
82
|
+
# GeoFusionResult(
|
|
83
|
+
# model='mountain' algorithm='ukmedoids' estimator='kf'
|
|
84
|
+
# nc=100 V=2.75m H=1.54m MAE=3.43m
|
|
85
|
+
# runtime=4.5s peak_RAM=152.1MB
|
|
86
|
+
# )
|
|
87
|
+
|
|
88
|
+
# Access the output dataframe and metrics
|
|
89
|
+
df_out = result.df_out # original columns + predicted_cluster + predicted_location
|
|
90
|
+
metrics = result.metrics # {'V': ..., 'H': ..., 'MAE': ...}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Uncertainty models
|
|
94
|
+
|
|
95
|
+
| `model` | Description | Compatible algorithms |
|
|
96
|
+
|------------|-------------|----------------------|
|
|
97
|
+
| `certain` | Raw phone coordinates, no uncertainty | `kmeans`, `kmedoids`, `sdsgc` |
|
|
98
|
+
| `volcano` | Isotropic sigma from satellite geometry cost J_avg (El Abbous & Samanta, 2017) | `ukmeans`, `ukmedoids` |
|
|
99
|
+
| `mountain` | Directional Student-t scale from multivariate regression on GSDC dataset | `ukmeans`, `ukmedoids` |
|
|
100
|
+
|
|
101
|
+
## Clustering algorithms
|
|
102
|
+
|
|
103
|
+
| `algorithm` | Description |
|
|
104
|
+
|--------------|-------------|
|
|
105
|
+
| `kmeans` | k-means++ (sklearn) |
|
|
106
|
+
| `kmedoids` | k-medoids with k-medoids++ initialisation |
|
|
107
|
+
| `ukmeans` | UK-means (Chau et al., 2006) — provably equivalent to k-means on GPS data |
|
|
108
|
+
| `ukmedoids` | UK-medoids (Gullo et al., 2008) with Monte Carlo expected-distance estimation |
|
|
109
|
+
| `sdsgc` | Structured Doubly Stochastic Graph-Based Clustering (Wang et al., TNNLS 2025) |
|
|
110
|
+
|
|
111
|
+
## Estimators
|
|
112
|
+
|
|
113
|
+
| `estimator` | Description |
|
|
114
|
+
|-------------|-------------|
|
|
115
|
+
| `rep` | Cluster representative (centroid or medoid) |
|
|
116
|
+
| `kf` | Linear Kalman filter (static target, degree space) |
|
|
117
|
+
| `ekf` | Extended Kalman filter (local metre space — corrects degree-space distortion) |
|
|
118
|
+
| `pf` | Sequential Importance Resampling particle filter |
|
|
119
|
+
| `dnn` | Post-clustering MLP predicting a position correction from cluster-level GNSS features |
|
|
120
|
+
|
|
121
|
+
## Required dataset columns
|
|
122
|
+
|
|
123
|
+
| Column | Description |
|
|
124
|
+
|--------|-------------|
|
|
125
|
+
| `collectionName` | Drive identifier (used for DNN drive-level split) |
|
|
126
|
+
| `latDeg_gt` | NovAtel reference latitude (degrees) |
|
|
127
|
+
| `lngDeg_gt` | NovAtel reference longitude (degrees) |
|
|
128
|
+
| `latDeg_phone` | Phone-reported latitude (degrees) |
|
|
129
|
+
| `lngDeg_phone` | Phone-reported longitude (degrees) |
|
|
130
|
+
| `j_avg` | Average satellite geometry cost |
|
|
131
|
+
| `speedMps` | Vehicle speed (m/s) |
|
|
132
|
+
| `n_signals` | Number of satellite signals |
|
|
133
|
+
| `avg_rawPrUnc` | Average pseudorange uncertainty (m) |
|
|
134
|
+
| `hDop` | Horizontal dilution of precision |
|
|
135
|
+
| `vDop` | Vertical dilution of precision |
|
|
136
|
+
| `avg_iono` | Average ionospheric delay (m) |
|
|
137
|
+
| `avg_tropo` | Average tropospheric delay (m) |
|
|
138
|
+
|
|
139
|
+
The dataset is publicly available on [Kaggle](https://www.kaggle.com/code/basantmounir/geofusion-dataset).
|
|
140
|
+
|
|
141
|
+
## algo_params reference
|
|
142
|
+
|
|
143
|
+
All algorithms accept `k`, `random_state`, `n_init`, `max_iter`.
|
|
144
|
+
|
|
145
|
+
Additional parameters:
|
|
146
|
+
- **`ukmedoids`**: `n_samples` (Monte Carlo samples for expected distance, default 50)
|
|
147
|
+
- **`sdsgc`**: `nn` (nearest neighbours, default 5 for k≤50, 4 for k≥100), `strategy` (`early_stop` / `best_of_n` / `threshold`), `threshold` (W-matrix threshold for component extraction), `eigsh_tol`, `eigsh_maxiter`
|
|
148
|
+
|
|
149
|
+
## estimator_params reference
|
|
150
|
+
|
|
151
|
+
- **`pf`**: `n_particles` (default 500)
|
|
152
|
+
- **`dnn`**: `test_drives` (required), `val_drives` (required), `max_epochs` (200), `patience` (20), `random_state` (42), `subprocess` (False — set True for clean RAM measurement)
|
|
153
|
+
|
|
154
|
+
## DNN example
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
result = run_geofusion(
|
|
158
|
+
df = df,
|
|
159
|
+
model = "mountain",
|
|
160
|
+
algorithm = "ukmedoids",
|
|
161
|
+
algo_params = dict(k=300, random_state=42, n_init=10, n_samples=50),
|
|
162
|
+
estimator = "dnn",
|
|
163
|
+
estimator_params = dict(
|
|
164
|
+
test_drives = ["2020-08-03-US-MTV-1", "2020-07-08-US-MTV-1", "2021-04-15-US-MTV-1"],
|
|
165
|
+
val_drives = ["2021-04-28-US-MTV-1", "2021-04-28-US-SJC-1"],
|
|
166
|
+
max_epochs = 200,
|
|
167
|
+
patience = 20,
|
|
168
|
+
),
|
|
169
|
+
)
|
|
170
|
+
# DNN metrics are evaluated on test-drive clusters only
|
|
171
|
+
print(result.metrics) # {'V': 2.05, 'H': 1.40, 'MAE': 2.71, 'n_test_clusters': 69}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## License
|
|
175
|
+
|
|
176
|
+
MIT
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# GeoFusion
|
|
2
|
+
|
|
3
|
+
An uncertain machine learning framework for GPS/GNSS sensor fusion.
|
|
4
|
+
|
|
5
|
+
GeoFusion estimates true location events from noisy phone-reported GPS coordinates by modelling each observation as an uncertain object, clustering by location event, and fusing the cluster into a refined position estimate.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install geofusion
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
import pandas as pd
|
|
17
|
+
from geofusion import run_geofusion
|
|
18
|
+
|
|
19
|
+
df = pd.read_csv("sample_top100_groups.csv")
|
|
20
|
+
|
|
21
|
+
# UK-medoids (mountain uncertainty model) + Kalman filter
|
|
22
|
+
result = run_geofusion(
|
|
23
|
+
df = df,
|
|
24
|
+
model = "mountain",
|
|
25
|
+
algorithm = "ukmedoids",
|
|
26
|
+
algo_params = dict(k=100, random_state=42, n_init=10, n_samples=50),
|
|
27
|
+
estimator = "kf",
|
|
28
|
+
)
|
|
29
|
+
print(result)
|
|
30
|
+
# GeoFusionResult(
|
|
31
|
+
# model='mountain' algorithm='ukmedoids' estimator='kf'
|
|
32
|
+
# nc=100 V=2.75m H=1.54m MAE=3.43m
|
|
33
|
+
# runtime=4.5s peak_RAM=152.1MB
|
|
34
|
+
# )
|
|
35
|
+
|
|
36
|
+
# Access the output dataframe and metrics
|
|
37
|
+
df_out = result.df_out # original columns + predicted_cluster + predicted_location
|
|
38
|
+
metrics = result.metrics # {'V': ..., 'H': ..., 'MAE': ...}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Uncertainty models
|
|
42
|
+
|
|
43
|
+
| `model` | Description | Compatible algorithms |
|
|
44
|
+
|------------|-------------|----------------------|
|
|
45
|
+
| `certain` | Raw phone coordinates, no uncertainty | `kmeans`, `kmedoids`, `sdsgc` |
|
|
46
|
+
| `volcano` | Isotropic sigma from satellite geometry cost J_avg (El Abbous & Samanta, 2017) | `ukmeans`, `ukmedoids` |
|
|
47
|
+
| `mountain` | Directional Student-t scale from multivariate regression on GSDC dataset | `ukmeans`, `ukmedoids` |
|
|
48
|
+
|
|
49
|
+
## Clustering algorithms
|
|
50
|
+
|
|
51
|
+
| `algorithm` | Description |
|
|
52
|
+
|--------------|-------------|
|
|
53
|
+
| `kmeans` | k-means++ (sklearn) |
|
|
54
|
+
| `kmedoids` | k-medoids with k-medoids++ initialisation |
|
|
55
|
+
| `ukmeans` | UK-means (Chau et al., 2006) — provably equivalent to k-means on GPS data |
|
|
56
|
+
| `ukmedoids` | UK-medoids (Gullo et al., 2008) with Monte Carlo expected-distance estimation |
|
|
57
|
+
| `sdsgc` | Structured Doubly Stochastic Graph-Based Clustering (Wang et al., TNNLS 2025) |
|
|
58
|
+
|
|
59
|
+
## Estimators
|
|
60
|
+
|
|
61
|
+
| `estimator` | Description |
|
|
62
|
+
|-------------|-------------|
|
|
63
|
+
| `rep` | Cluster representative (centroid or medoid) |
|
|
64
|
+
| `kf` | Linear Kalman filter (static target, degree space) |
|
|
65
|
+
| `ekf` | Extended Kalman filter (local metre space — corrects degree-space distortion) |
|
|
66
|
+
| `pf` | Sequential Importance Resampling particle filter |
|
|
67
|
+
| `dnn` | Post-clustering MLP predicting a position correction from cluster-level GNSS features |
|
|
68
|
+
|
|
69
|
+
## Required dataset columns
|
|
70
|
+
|
|
71
|
+
| Column | Description |
|
|
72
|
+
|--------|-------------|
|
|
73
|
+
| `collectionName` | Drive identifier (used for DNN drive-level split) |
|
|
74
|
+
| `latDeg_gt` | NovAtel reference latitude (degrees) |
|
|
75
|
+
| `lngDeg_gt` | NovAtel reference longitude (degrees) |
|
|
76
|
+
| `latDeg_phone` | Phone-reported latitude (degrees) |
|
|
77
|
+
| `lngDeg_phone` | Phone-reported longitude (degrees) |
|
|
78
|
+
| `j_avg` | Average satellite geometry cost |
|
|
79
|
+
| `speedMps` | Vehicle speed (m/s) |
|
|
80
|
+
| `n_signals` | Number of satellite signals |
|
|
81
|
+
| `avg_rawPrUnc` | Average pseudorange uncertainty (m) |
|
|
82
|
+
| `hDop` | Horizontal dilution of precision |
|
|
83
|
+
| `vDop` | Vertical dilution of precision |
|
|
84
|
+
| `avg_iono` | Average ionospheric delay (m) |
|
|
85
|
+
| `avg_tropo` | Average tropospheric delay (m) |
|
|
86
|
+
|
|
87
|
+
The dataset is publicly available on [Kaggle](https://www.kaggle.com/code/basantmounir/geofusion-dataset).
|
|
88
|
+
|
|
89
|
+
## algo_params reference
|
|
90
|
+
|
|
91
|
+
All algorithms accept `k`, `random_state`, `n_init`, `max_iter`.
|
|
92
|
+
|
|
93
|
+
Additional parameters:
|
|
94
|
+
- **`ukmedoids`**: `n_samples` (Monte Carlo samples for expected distance, default 50)
|
|
95
|
+
- **`sdsgc`**: `nn` (nearest neighbours, default 5 for k≤50, 4 for k≥100), `strategy` (`early_stop` / `best_of_n` / `threshold`), `threshold` (W-matrix threshold for component extraction), `eigsh_tol`, `eigsh_maxiter`
|
|
96
|
+
|
|
97
|
+
## estimator_params reference
|
|
98
|
+
|
|
99
|
+
- **`pf`**: `n_particles` (default 500)
|
|
100
|
+
- **`dnn`**: `test_drives` (required), `val_drives` (required), `max_epochs` (200), `patience` (20), `random_state` (42), `subprocess` (False — set True for clean RAM measurement)
|
|
101
|
+
|
|
102
|
+
## DNN example
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
result = run_geofusion(
|
|
106
|
+
df = df,
|
|
107
|
+
model = "mountain",
|
|
108
|
+
algorithm = "ukmedoids",
|
|
109
|
+
algo_params = dict(k=300, random_state=42, n_init=10, n_samples=50),
|
|
110
|
+
estimator = "dnn",
|
|
111
|
+
estimator_params = dict(
|
|
112
|
+
test_drives = ["2020-08-03-US-MTV-1", "2020-07-08-US-MTV-1", "2021-04-15-US-MTV-1"],
|
|
113
|
+
val_drives = ["2021-04-28-US-MTV-1", "2021-04-28-US-SJC-1"],
|
|
114
|
+
max_epochs = 200,
|
|
115
|
+
patience = 20,
|
|
116
|
+
),
|
|
117
|
+
)
|
|
118
|
+
# DNN metrics are evaluated on test-drive clusters only
|
|
119
|
+
print(result.metrics) # {'V': 2.05, 'H': 1.40, 'MAE': 2.71, 'n_test_clusters': 69}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## License
|
|
123
|
+
|
|
124
|
+
MIT
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GeoFusion
|
|
3
|
+
=========
|
|
4
|
+
|
|
5
|
+
An uncertain machine learning framework for GPS/GNSS sensor fusion.
|
|
6
|
+
|
|
7
|
+
Implements the two-phase pipeline from Mounir & Abdel-Hamid (IEEE Access):
|
|
8
|
+
|
|
9
|
+
1. **Representation** — each GPS observation is modelled as an uncertain
|
|
10
|
+
object (volcano model or mountain model) reflecting the positional
|
|
11
|
+
uncertainty given satellite geometry, speed, and signal quality.
|
|
12
|
+
|
|
13
|
+
2. **Clustering** — observations are grouped by location event using
|
|
14
|
+
either certain or uncertain clustering algorithms.
|
|
15
|
+
|
|
16
|
+
3. **Estimation** — observations within each cluster are fused into a
|
|
17
|
+
single refined position estimate.
|
|
18
|
+
|
|
19
|
+
Quick start
|
|
20
|
+
-----------
|
|
21
|
+
import pandas as pd
|
|
22
|
+
from geofusion import run_geofusion
|
|
23
|
+
|
|
24
|
+
df = pd.read_csv("sample_top100_groups.csv")
|
|
25
|
+
|
|
26
|
+
# UK-medoids (mountain) + Kalman filter
|
|
27
|
+
result = run_geofusion(
|
|
28
|
+
df = df,
|
|
29
|
+
model = "mountain",
|
|
30
|
+
algorithm = "ukmedoids",
|
|
31
|
+
algo_params = dict(k=100, random_state=42, n_init=10, n_samples=50),
|
|
32
|
+
estimator = "kf",
|
|
33
|
+
)
|
|
34
|
+
print(result)
|
|
35
|
+
# GeoFusionResult(
|
|
36
|
+
# model='mountain' algorithm='ukmedoids' estimator='kf'
|
|
37
|
+
# nc=100 V=2.75m H=1.54m MAE=3.43m
|
|
38
|
+
# runtime=4.5s peak_RAM=152.1MB
|
|
39
|
+
# )
|
|
40
|
+
|
|
41
|
+
# SDSGC (certain) + particle filter
|
|
42
|
+
result = run_geofusion(
|
|
43
|
+
df = df,
|
|
44
|
+
model = "certain",
|
|
45
|
+
algorithm = "sdsgc",
|
|
46
|
+
algo_params = dict(k=100, nn=4, strategy="threshold", threshold=0.1),
|
|
47
|
+
estimator = "pf",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Post-clustering DNN (test-drive evaluation)
|
|
51
|
+
result = run_geofusion(
|
|
52
|
+
df = df,
|
|
53
|
+
model = "mountain",
|
|
54
|
+
algorithm = "ukmedoids",
|
|
55
|
+
algo_params = dict(k=100, random_state=42, n_init=10, n_samples=50),
|
|
56
|
+
estimator = "dnn",
|
|
57
|
+
estimator_params = dict(
|
|
58
|
+
test_drives = ["2020-08-03-US-MTV-1", "2020-07-08-US-MTV-1"],
|
|
59
|
+
val_drives = ["2021-04-28-US-MTV-1"],
|
|
60
|
+
),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
Uncertainty models
|
|
64
|
+
------------------
|
|
65
|
+
certain No uncertainty. Raw phone coordinates.
|
|
66
|
+
Algorithms: kmeans, kmedoids, sdsgc
|
|
67
|
+
|
|
68
|
+
volcano Model A — isotropic sigma from satellite geometry cost J_avg
|
|
69
|
+
(El Abbous & Samanta, 2017). Zero-shot transfer, no fitting.
|
|
70
|
+
sigma = max(0.787 * j_avg + 2.192, 0.01)
|
|
71
|
+
Algorithms: ukmeans, ukmedoids
|
|
72
|
+
|
|
73
|
+
mountain Model B — directional Student-t scale from multivariate
|
|
74
|
+
regression on the GSDC dataset (Eqs 11-12 in the paper).
|
|
75
|
+
Sigma floor: max(s, 0.01) — NOT abs(s).
|
|
76
|
+
Algorithms: ukmeans, ukmedoids
|
|
77
|
+
|
|
78
|
+
Clustering algorithms
|
|
79
|
+
---------------------
|
|
80
|
+
kmeans k-means++ (sklearn) — certain only
|
|
81
|
+
kmedoids k-medoids (Lloyd-style, k-medoids++ init) — certain only
|
|
82
|
+
ukmeans UK-means (Chau et al., 2006) — volcano or mountain
|
|
83
|
+
Note: provably equivalent to k-means on GPS data
|
|
84
|
+
ukmedoids UK-medoids (Gullo et al., 2008) — volcano or mountain
|
|
85
|
+
sdsgc Structured Doubly Stochastic Graph-Based Clustering
|
|
86
|
+
(Wang et al., TNNLS 2025) — certain only
|
|
87
|
+
|
|
88
|
+
Estimators
|
|
89
|
+
----------
|
|
90
|
+
rep Cluster representative (centroid or medoid) — no post-processing
|
|
91
|
+
kf Linear Kalman filter (degree-space, static target)
|
|
92
|
+
ekf Extended Kalman filter (local metre-space, corrects degree distortion)
|
|
93
|
+
pf Sequential Importance Resampling particle filter
|
|
94
|
+
dnn Post-clustering MLP (Siemuri et al., 2021, adapted for GeoFusion)
|
|
95
|
+
Requires 'test_drives' and 'val_drives' in estimator_params.
|
|
96
|
+
|
|
97
|
+
Required dataset columns
|
|
98
|
+
------------------------
|
|
99
|
+
collectionName drive identifier (used for DNN drive-level split)
|
|
100
|
+
latDeg_gt NovAtel reference latitude (degrees)
|
|
101
|
+
lngDeg_gt NovAtel reference longitude (degrees)
|
|
102
|
+
latDeg_phone phone-reported latitude (degrees)
|
|
103
|
+
lngDeg_phone phone-reported longitude (degrees)
|
|
104
|
+
j_avg average satellite geometry cost
|
|
105
|
+
speedMps vehicle speed (m/s)
|
|
106
|
+
n_signals number of satellite signals
|
|
107
|
+
avg_rawPrUnc average pseudorange uncertainty (m)
|
|
108
|
+
hDop horizontal dilution of precision
|
|
109
|
+
vDop vertical dilution of precision
|
|
110
|
+
avg_iono average ionospheric delay (m)
|
|
111
|
+
avg_tropo average tropospheric delay (m)
|
|
112
|
+
|
|
113
|
+
References
|
|
114
|
+
----------
|
|
115
|
+
Mounir, B. & Abdel-Hamid, A.T. (2025). GeoFusion: An Uncertain Machine
|
|
116
|
+
Learning Framework for Sensor Fusion. IEEE Access.
|
|
117
|
+
El Abbous, A. & Samanta, N. (2017). A Modeling of GPS Error Distributions.
|
|
118
|
+
EURONAV. DOI: 10.1109/EURONAV.2017.7954200
|
|
119
|
+
Chau, M. et al. (2006). Uncertain Data Mining: An Example in Clustering
|
|
120
|
+
Location Data. PAKDD. DOI: 10.1007/11731139_24
|
|
121
|
+
Gullo, F. et al. (2008). Clustering Uncertain Data Via K-Medoids. MDAI.
|
|
122
|
+
DOI: 10.1007/978-3-540-87993-0_19
|
|
123
|
+
Wang, N. et al. (2025). Structured Doubly Stochastic Graph-Based
|
|
124
|
+
Clustering. IEEE TNNLS. DOI: 10.1109/TNNLS.2025.3531987
|
|
125
|
+
Siemuri, A. et al. (2021). Improving Precision GNSS Positioning and
|
|
126
|
+
Navigation Accuracy on Smartphones Using Machine Learning. ION GNSS+.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
from .core import run_geofusion, GeoFusionResult
|
|
130
|
+
from .core import _REQUIRED_COLS as REQUIRED_COLS
|
|
131
|
+
|
|
132
|
+
__all__ = [
|
|
133
|
+
"run_geofusion",
|
|
134
|
+
"GeoFusionResult",
|
|
135
|
+
"REQUIRED_COLS",
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
__version__ = "0.1.0"
|
|
139
|
+
__author__ = "Basant Mounir, Amr T. Abdel-Hamid"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from .kmeans_clustering import run_kmeans
|
|
2
|
+
from .kmedoids_clustering import run_kmedoids
|
|
3
|
+
from .ukmeans_clustering import run_ukmeans
|
|
4
|
+
from .ukmedoids_clustering import run_ukmedoids
|
|
5
|
+
from .sdsgc_clustering import (
|
|
6
|
+
run_sdsgc,
|
|
7
|
+
_l2_distance, _sym_neighbors, _eig_laplacian, _proj_simplex,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"run_kmeans", "run_kmedoids", "run_ukmeans", "run_ukmedoids", "run_sdsgc",
|
|
12
|
+
"_l2_distance", "_sym_neighbors", "_eig_laplacian", "_proj_simplex",
|
|
13
|
+
]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""
|
|
2
|
+
k-means clustering for GeoFusion.
|
|
3
|
+
|
|
4
|
+
Wraps sklearn's KMeans with k-means++ initialization and the standard
|
|
5
|
+
GeoFusion function signature, for a clean like-for-like comparison with
|
|
6
|
+
all other clustering methods in the framework.
|
|
7
|
+
|
|
8
|
+
The cluster representative is the centroid (mean) of reported locations
|
|
9
|
+
within each cluster -- a computed average, not an actual data point.
|
|
10
|
+
This is what distinguishes k-means from k-medoids in terms of the
|
|
11
|
+
representative: centroids may not correspond to any real observation.
|
|
12
|
+
|
|
13
|
+
Usage (in a notebook cell):
|
|
14
|
+
df_out = run_kmeans(
|
|
15
|
+
"real_data_sample_10events_5mapart.csv",
|
|
16
|
+
"real_data_sample_10events_kmeans.csv",
|
|
17
|
+
columns=("reported_location_lat", "reported_location_lon"),
|
|
18
|
+
k=10,
|
|
19
|
+
random_state=42,
|
|
20
|
+
)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
import pandas as pd
|
|
25
|
+
from sklearn.cluster import KMeans
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def run_kmeans(
|
|
29
|
+
input_path: str,
|
|
30
|
+
output_path: str,
|
|
31
|
+
columns=("reported_location_lat", "reported_location_lon"),
|
|
32
|
+
k: int = 10,
|
|
33
|
+
random_state: int = 42,
|
|
34
|
+
n_init: int = 10,
|
|
35
|
+
) -> pd.DataFrame:
|
|
36
|
+
"""
|
|
37
|
+
Run k-means clustering on the given columns of a CSV file and write
|
|
38
|
+
the result (original data + predicted_cluster + predicted_location)
|
|
39
|
+
to output_path.
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
input_path : str
|
|
44
|
+
Path to the input CSV file.
|
|
45
|
+
output_path : str
|
|
46
|
+
Path to write the output CSV file to.
|
|
47
|
+
columns : tuple of str
|
|
48
|
+
Column names to cluster on.
|
|
49
|
+
k : int
|
|
50
|
+
Number of clusters, default 10.
|
|
51
|
+
random_state : int
|
|
52
|
+
Seed for reproducibility (passed to sklearn KMeans).
|
|
53
|
+
n_init : int
|
|
54
|
+
Number of k-means++ initializations; best run by inertia is kept.
|
|
55
|
+
Default 10, matching sklearn's default and all other GeoFusion
|
|
56
|
+
algorithms for a fair comparison.
|
|
57
|
+
|
|
58
|
+
Returns
|
|
59
|
+
-------
|
|
60
|
+
pd.DataFrame
|
|
61
|
+
The output dataframe (also written to output_path).
|
|
62
|
+
"""
|
|
63
|
+
df = pd.read_csv(input_path)
|
|
64
|
+
|
|
65
|
+
missing = [c for c in columns if c not in df.columns]
|
|
66
|
+
if missing:
|
|
67
|
+
raise ValueError(f"Column(s) not found in input data: {missing}")
|
|
68
|
+
|
|
69
|
+
X = df[list(columns)].to_numpy(dtype=float)
|
|
70
|
+
|
|
71
|
+
model = KMeans(n_clusters=k, random_state=random_state, n_init=n_init)
|
|
72
|
+
labels = model.fit_predict(X)
|
|
73
|
+
centroids = model.cluster_centers_
|
|
74
|
+
|
|
75
|
+
df["predicted_cluster"] = labels
|
|
76
|
+
df["predicted_location"] = [
|
|
77
|
+
tuple(float(v) for v in centroids[lbl]) for lbl in labels
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
df.to_csv(output_path, index=False)
|
|
81
|
+
return df
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from scipy.spatial.distance import cdist
|
|
4
|
+
|
|
5
|
+
def _kmeans_plusplus_init(X, k, rng):
|
|
6
|
+
n = X.shape[0]
|
|
7
|
+
first_idx = rng.integers(n)
|
|
8
|
+
center_indices = [first_idx]
|
|
9
|
+
closest_sq_dist = np.sum((X - X[first_idx])**2, axis=1)
|
|
10
|
+
for _ in range(1, k):
|
|
11
|
+
probs = closest_sq_dist / closest_sq_dist.sum()
|
|
12
|
+
idx = rng.choice(n, p=probs)
|
|
13
|
+
center_indices.append(idx)
|
|
14
|
+
new_sq = np.sum((X - X[idx])**2, axis=1)
|
|
15
|
+
closest_sq_dist = np.minimum(closest_sq_dist, new_sq)
|
|
16
|
+
return np.array(center_indices)
|
|
17
|
+
|
|
18
|
+
def _lloyd_kmedoids_single(X, k, rng, max_iter):
|
|
19
|
+
n = X.shape[0]
|
|
20
|
+
medoid_idx = _kmeans_plusplus_init(X, k, rng)
|
|
21
|
+
labels = np.zeros(n, dtype=int)
|
|
22
|
+
for _ in range(max_iter):
|
|
23
|
+
d_to_medoids = cdist(X, X[medoid_idx], metric='euclidean')
|
|
24
|
+
new_labels = np.argmin(d_to_medoids, axis=1)
|
|
25
|
+
new_medoid_idx = medoid_idx.copy()
|
|
26
|
+
for c in range(k):
|
|
27
|
+
members = np.where(new_labels == c)[0]
|
|
28
|
+
if len(members) == 0: continue
|
|
29
|
+
if len(members) == 1: new_medoid_idx[c] = members[0]; continue
|
|
30
|
+
sub_dist = cdist(X[members], X[members], metric='euclidean')
|
|
31
|
+
new_medoid_idx[c] = members[np.argmin(sub_dist.sum(axis=1))]
|
|
32
|
+
if np.array_equal(new_labels, labels) and np.array_equal(np.sort(new_medoid_idx), np.sort(medoid_idx)):
|
|
33
|
+
labels = new_labels; medoid_idx = new_medoid_idx; break
|
|
34
|
+
labels = new_labels; medoid_idx = new_medoid_idx
|
|
35
|
+
d_final = cdist(X, X[medoid_idx], metric='euclidean')
|
|
36
|
+
total_cost = d_final[np.arange(n), labels].sum()
|
|
37
|
+
return labels, medoid_idx, total_cost
|
|
38
|
+
|
|
39
|
+
def _lloyd_kmedoids(X, k, random_state, n_init, max_iter):
|
|
40
|
+
rng = np.random.default_rng(random_state)
|
|
41
|
+
best_labels, best_medoid_idx, best_cost = None, None, np.inf
|
|
42
|
+
for _ in range(n_init):
|
|
43
|
+
labels, medoid_idx, cost = _lloyd_kmedoids_single(X, k, rng, max_iter)
|
|
44
|
+
if cost < best_cost:
|
|
45
|
+
best_labels, best_medoid_idx, best_cost = labels, medoid_idx, cost
|
|
46
|
+
return best_labels, best_medoid_idx
|
|
47
|
+
|
|
48
|
+
def run_kmedoids(input_path, output_path, columns=("reported_location_lat","reported_location_lon"),
|
|
49
|
+
k=10, random_state=42, n_init=10, max_iter=300):
|
|
50
|
+
df = pd.read_csv(input_path)
|
|
51
|
+
X = df[list(columns)].to_numpy(dtype=float)
|
|
52
|
+
labels, medoid_indices = _lloyd_kmedoids(X, k=k, random_state=random_state, n_init=n_init, max_iter=max_iter)
|
|
53
|
+
medoids = X[medoid_indices]
|
|
54
|
+
df["predicted_cluster"] = labels
|
|
55
|
+
df["predicted_location"] = [tuple(float(v) for v in medoids[lbl]) for lbl in labels]
|
|
56
|
+
df.to_csv(output_path, index=False)
|
|
57
|
+
return df
|