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.
@@ -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.
@@ -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