sgwr 0.0.11__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,14 @@
1
+ Copyright 2025 GIBD (M. Naser Lessani)
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8
+
9
+ ### Citation
10
+
11
+ If you use this tool in your research, please cite the following publications:
12
+
13
+ 1. Lessani, M. Naser, and Zhenlong Li. "SGWR: similarity and geographically weighted regression." International Journal of Geographical Information Science 38, no. 7 (2024): 1232-1255.
14
+ 2. Lessani, M. Naser, and Zhenlong Li. "Enhancing the Computational Efficiency of the SGWR Model and Introducing Its Software Implementation." Annals of GIS (2025).
sgwr-0.0.11/PKG-INFO ADDED
@@ -0,0 +1,122 @@
1
+ Metadata-Version: 2.4
2
+ Name: sgwr
3
+ Version: 0.0.11
4
+ Summary: Python implementation of SGWR and Fast SGWR (MPI-enhanced)
5
+ Home-page: https://github.com/Lessani252/FastSGWR
6
+ Author: M. Naser Lessani (GIBD)
7
+ Author-email: naserlessani252@gmail.com
8
+ License: MIT
9
+ Requires-Python: >=3.7
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE.txt
12
+ Requires-Dist: numpy
13
+ Requires-Dist: pandas
14
+ Requires-Dist: scikit-learn
15
+ Requires-Dist: click
16
+ Requires-Dist: mpi4py
17
+ Requires-Dist: scipy
18
+ Requires-Dist: spglm
19
+ Requires-Dist: argparse
20
+ Requires-Dist: matplotlib
21
+ Dynamic: author
22
+ Dynamic: author-email
23
+ Dynamic: description
24
+ Dynamic: description-content-type
25
+ Dynamic: home-page
26
+ Dynamic: license
27
+ Dynamic: license-file
28
+ Dynamic: requires-dist
29
+ Dynamic: requires-python
30
+ Dynamic: summary
31
+
32
+ FastSGWR: "Enhancing the Computational Efficiency of the SGWR Model and Introducing Its Software Implementation." This article along the python packages (parallel & sequential), and a Graphic User Interface (GUI) tool named 'SGWR Model' is developed based on this article :SGWR: similarity and geographically weighted regression (https://doi.org/10.1080/13658816.2024.2342319).
33
+
34
+ Author info
35
+ ------------
36
+ - Code Author: M. Naser Lessani (GIBD)
37
+ - Realeased Year: 2025
38
+ - Affiliation: Geoinformation and Big Data Research Laboratory (GIBD), Department of Geography, The Pennsylvania State University, University Park, PA, USA
39
+
40
+ Overview
41
+ --------
42
+ The SGWR (Similarity and Geographically Weighted Regression) model is a novel local spatial regression model that extends the conventional GWR by incorporating both geographical proximity and attribute similarity into a composite spatial weight matrix. The combination of spatial and attribute-based weights is governed by a parameter alpha, which is optimized based on AICc measure.
43
+
44
+ This Python package includes:
45
+ - MPI-enabled parallel implementations of the SGWR
46
+ - Serial version of the SGWR model
47
+ - Support for Gaussian and bi-square kernels
48
+ - Also, supports a combination of adaptive bisquare and gaussian
49
+
50
+ Key Features
51
+ ------------
52
+ - Parallel Computation with MPI: Achieve substantial runtime reductions using multiple CPU cores.
53
+ - Alpha Optimization: Automatically tune the contribution of similarity and spatial proximity.
54
+ - Kernel Flexibility: Choose between Gaussian (default) and bi-square kernels.
55
+ - Also users have the ability to either chose standardize or don't standardize their data
56
+ - Evaluation Metrics: Outputs include R², adjusted R², AICc, and other metrics
57
+
58
+ Installation Requirements
59
+ -------------------------
60
+ Ensure the following dependencies are installed:
61
+
62
+ Standard Python Libraries:
63
+ - os
64
+ - argparse
65
+ - datetime
66
+ - math
67
+ - copy
68
+ - typing (Optional)
69
+ - itertools (combinations)
70
+
71
+ MPI and CLI:
72
+ - mpi4py
73
+ - click
74
+
75
+ Numerical and Data Handling:
76
+ - numpy
77
+ - pandas
78
+ - scipy (stats, linalg, spatial.distance)
79
+
80
+ Machine Learning and Metrics:
81
+ - scikit-learn (metrics)
82
+
83
+ Specialized Geospatial Modeling:
84
+ - spglm (family, glm, iwls, utils)
85
+
86
+
87
+ Data Format
88
+ -----------
89
+ Input data must be a CSV file with the following column order:
90
+
91
+ longitude, latitude, dependent_variable, independent_variable_1, ..., independent_variable_n
92
+
93
+ Categorical variables must be pre-processed into dummy variables.
94
+ Example: For a 3-class variable ("urban", "peri-urban", "rural"), create:
95
+ - urban_dummy: 1 if urban, else 0
96
+ - peri_urban_dummy: 1 if peri-urban, else 0
97
+ - Rural becomes the reference class (excluded)
98
+
99
+ Usage
100
+ -----
101
+ After preparing your dataset and ensuring all dependencies are installed, the model can be run via the command line:
102
+
103
+ MPI Commands (parallel):
104
+ ----------------------------
105
+ - fastsgwr run -np x -data path_to_data (by default the kernel is Gaussian function, and doesn't standardize the data)
106
+ - fastsgwr run -np x -data path_to_data -standardize (using Gaussian function and standardize the input data)
107
+ - fastsgwr run -np x -data path_to_data -bisquare
108
+ - fastsgwr run -np x -data path_to_data -bisquare -standardize
109
+ - fastsgwr run -np x -data path_to_data -biga (adaptive bisquare and gaussian)
110
+ - fastsgwr run -np x -data path__to_data -gwr (run gwr as well in parallel)
111
+
112
+ - x: Number of cores
113
+ - path_to_data: Path to the CSV dataset
114
+
115
+ The output will be a CSV file saved in the same input directory, and containing local coefficients and performance metrics.
116
+
117
+ Citation
118
+ --------
119
+ If you use this package in your work, please cite the following articles:
120
+
121
+ 1. Lessani, M. Naser, and Zhenlong Li. "SGWR: similarity and geographically weighted regression." International Journal of Geographical Information Science 38, no. 7 (2024): 1232-1255.
122
+ 2. Lessani, M. Naser, and Zhenlong Li. "Enhancing the Computational Efficiency of the SGWR Model and Introducing Its Software Implementation." Annals of GIS (2025).
sgwr-0.0.11/README.md ADDED
@@ -0,0 +1,91 @@
1
+ FastSGWR: "Enhancing the Computational Efficiency of the SGWR Model and Introducing Its Software Implementation." This article along the python packages (parallel & sequential), and a Graphic User Interface (GUI) tool named 'SGWR Model' is developed based on this article :SGWR: similarity and geographically weighted regression (https://doi.org/10.1080/13658816.2024.2342319).
2
+
3
+ Author info
4
+ ------------
5
+ - Code Author: M. Naser Lessani (GIBD)
6
+ - Realeased Year: 2025
7
+ - Affiliation: Geoinformation and Big Data Research Laboratory (GIBD), Department of Geography, The Pennsylvania State University, University Park, PA, USA
8
+
9
+ Overview
10
+ --------
11
+ The SGWR (Similarity and Geographically Weighted Regression) model is a novel local spatial regression model that extends the conventional GWR by incorporating both geographical proximity and attribute similarity into a composite spatial weight matrix. The combination of spatial and attribute-based weights is governed by a parameter alpha, which is optimized based on AICc measure.
12
+
13
+ This Python package includes:
14
+ - MPI-enabled parallel implementations of the SGWR
15
+ - Serial version of the SGWR model
16
+ - Support for Gaussian and bi-square kernels
17
+ - Also, supports a combination of adaptive bisquare and gaussian
18
+
19
+ Key Features
20
+ ------------
21
+ - Parallel Computation with MPI: Achieve substantial runtime reductions using multiple CPU cores.
22
+ - Alpha Optimization: Automatically tune the contribution of similarity and spatial proximity.
23
+ - Kernel Flexibility: Choose between Gaussian (default) and bi-square kernels.
24
+ - Also users have the ability to either chose standardize or don't standardize their data
25
+ - Evaluation Metrics: Outputs include R², adjusted R², AICc, and other metrics
26
+
27
+ Installation Requirements
28
+ -------------------------
29
+ Ensure the following dependencies are installed:
30
+
31
+ Standard Python Libraries:
32
+ - os
33
+ - argparse
34
+ - datetime
35
+ - math
36
+ - copy
37
+ - typing (Optional)
38
+ - itertools (combinations)
39
+
40
+ MPI and CLI:
41
+ - mpi4py
42
+ - click
43
+
44
+ Numerical and Data Handling:
45
+ - numpy
46
+ - pandas
47
+ - scipy (stats, linalg, spatial.distance)
48
+
49
+ Machine Learning and Metrics:
50
+ - scikit-learn (metrics)
51
+
52
+ Specialized Geospatial Modeling:
53
+ - spglm (family, glm, iwls, utils)
54
+
55
+
56
+ Data Format
57
+ -----------
58
+ Input data must be a CSV file with the following column order:
59
+
60
+ longitude, latitude, dependent_variable, independent_variable_1, ..., independent_variable_n
61
+
62
+ Categorical variables must be pre-processed into dummy variables.
63
+ Example: For a 3-class variable ("urban", "peri-urban", "rural"), create:
64
+ - urban_dummy: 1 if urban, else 0
65
+ - peri_urban_dummy: 1 if peri-urban, else 0
66
+ - Rural becomes the reference class (excluded)
67
+
68
+ Usage
69
+ -----
70
+ After preparing your dataset and ensuring all dependencies are installed, the model can be run via the command line:
71
+
72
+ MPI Commands (parallel):
73
+ ----------------------------
74
+ - fastsgwr run -np x -data path_to_data (by default the kernel is Gaussian function, and doesn't standardize the data)
75
+ - fastsgwr run -np x -data path_to_data -standardize (using Gaussian function and standardize the input data)
76
+ - fastsgwr run -np x -data path_to_data -bisquare
77
+ - fastsgwr run -np x -data path_to_data -bisquare -standardize
78
+ - fastsgwr run -np x -data path_to_data -biga (adaptive bisquare and gaussian)
79
+ - fastsgwr run -np x -data path__to_data -gwr (run gwr as well in parallel)
80
+
81
+ - x: Number of cores
82
+ - path_to_data: Path to the CSV dataset
83
+
84
+ The output will be a CSV file saved in the same input directory, and containing local coefficients and performance metrics.
85
+
86
+ Citation
87
+ --------
88
+ If you use this package in your work, please cite the following articles:
89
+
90
+ 1. Lessani, M. Naser, and Zhenlong Li. "SGWR: similarity and geographically weighted regression." International Journal of Geographical Information Science 38, no. 7 (2024): 1232-1255.
91
+ 2. Lessani, M. Naser, and Zhenlong Li. "Enhancing the Computational Efficiency of the SGWR Model and Introducing Its Software Implementation." Annals of GIS (2025).
sgwr-0.0.11/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
sgwr-0.0.11/setup.py ADDED
@@ -0,0 +1,40 @@
1
+ from setuptools import setup, find_packages
2
+ from os import path
3
+ import pathlib
4
+
5
+ working_directory = pathlib.Path(__file__).parent
6
+
7
+ with open(path.join(working_directory, 'README.md'), encoding='utf-8') as f:
8
+ long_description = f.read()
9
+
10
+ setup(
11
+ name='sgwr',
12
+ version='0.0.11',
13
+ url='https://github.com/Lessani252/FastSGWR',
14
+ author='M. Naser Lessani (GIBD)',
15
+ author_email='naserlessani252@gmail.com',
16
+ license='MIT',
17
+ description='Python implementation of SGWR and Fast SGWR (MPI-enhanced)',
18
+ long_description=long_description,
19
+ long_description_content_type='text/markdown',
20
+ packages=find_packages(include=['sgwr', 'sgwr.*']),
21
+ include_package_data=True,
22
+ install_requires=[
23
+ 'numpy',
24
+ 'pandas',
25
+ 'scikit-learn',
26
+ 'click',
27
+ 'mpi4py',
28
+ 'scipy',
29
+ 'spglm',
30
+ 'argparse',
31
+ 'matplotlib'
32
+ ],
33
+ python_requires='>=3.7',
34
+
35
+ entry_points={
36
+ 'console_scripts': [
37
+ 'fastsgwr=sgwr.fastsgwr.__main__:main',
38
+ ]
39
+ },
40
+ )
@@ -0,0 +1,3 @@
1
+ from .alpha import ALPHA
2
+ from .sgwr import SGWR
3
+ from . import fastsgwr
@@ -0,0 +1,362 @@
1
+ """ Related to alpha optimization in SGWR model"""
2
+ """ Author: M. Naser Lessani. email:naserlessani252@gamil.com"""
3
+
4
+ import numpy as np
5
+ from spglm.family import Gaussian
6
+ from scipy.spatial.distance import cdist
7
+
8
+ class ALPHA(object):
9
+ def __init__(self, coords, y, X, data, family=Gaussian(),
10
+ offset=None, kernel='bisquare', bw=False, fixed=False, greedy=False, adaptiveBG=False, constant=True):
11
+
12
+ self.coords = coords
13
+ self.y = y
14
+ self.X = X
15
+ self.data = X
16
+ self.family = family
17
+ self.offset = offset
18
+ self.kernel = kernel
19
+ self.fixed = fixed
20
+ self.bw = bw
21
+ self.data = data
22
+ self.alphacurve = greedy
23
+ self.constant = constant
24
+ self.n = self.X.shape[0]
25
+ self.k = self.X.shape[1]
26
+ self.minbw = None
27
+ self.maxbw = None
28
+ self.adaptiveBG = adaptiveBG
29
+ self.search_params = {}
30
+
31
+ if self.adaptiveBG:
32
+ self.fixed = False
33
+ if self.constant:
34
+ self.X = np.hstack([np.ones((self.n, 1)), self.X]) ## adding the intercept
35
+
36
+ ########
37
+ def set_search_range(self):
38
+ """
39
+ Define the search range for the bandwidth in the golden section search.
40
+ """
41
+ if self.fixed:
42
+ max_dist = np.max(np.array([np.max(cdist([self.coords[i]], self.coords))
43
+ for i in range(self.n)]))
44
+ self.maxbw = max_dist * 2
45
+ if self.minbw is None:
46
+ min_dist = np.min(
47
+ np.array([np.min(np.delete(cdist(self.coords[[i]], self.coords), i))
48
+ for i in range(self.n)]))
49
+ self.minbw = min_dist / 2
50
+ if self.minbw < 0.01: ##added Naser
51
+ self.minbw = 0.01
52
+ else:
53
+ self.maxbw = self.n
54
+ if self.minbw is None:
55
+ self.minbw = 40 + 2 * self.k
56
+
57
+ def _local_fit(self, i, y, X, bw):
58
+ # Compute distances for observation i
59
+ dist = cdist([self.coords[i]], self.coords).reshape(-1)
60
+ if self.fixed:
61
+ wi = np.exp(-0.5 * (dist / bw) ** 2).reshape(-1, 1)
62
+ else:
63
+ maxdis = np.partition(dist, int(bw) - 1)[int(bw) - 1] * 1.0000001
64
+ wg = dist / maxdis
65
+ wg[wg >= 1] = 1
66
+ wi = ((1 - (wg) ** 2) ** 2).reshape(-1, 1)
67
+
68
+ # Form the weighted design matrix and response vector
69
+ X_new = X * np.sqrt(wi)
70
+ Y_new = y * np.sqrt(wi)
71
+
72
+ # Regularization: add a tiny constant to the diagonal before inverting
73
+ epsilon = 1e-8
74
+ A = np.dot(X_new.T, X_new) + epsilon * np.eye(X_new.shape[1])
75
+ invA = np.linalg.inv(A)
76
+ temp = np.dot(invA, X_new.T)
77
+ hat = np.dot(X_new[i], temp[:, i])
78
+ yhat = np.sum(np.dot(X_new, temp[:, i]).reshape(-1, 1) * Y_new)
79
+ err = Y_new[i][0] - yhat
80
+
81
+ return err ** 2, hat
82
+
83
+ def golden_section(self, a, c, function):
84
+ delta = 0.38197
85
+ b = a + delta * np.abs(c - a)
86
+ d = c - delta * np.abs(c - a)
87
+ opt_bw = None
88
+ score = None
89
+ diff = 1.0e9
90
+ iters = 0
91
+ dict = {}
92
+ while np.abs(diff) > 1.0e-6 and iters < 200:
93
+ iters += 1
94
+ if not self.fixed:
95
+ b = np.round(b)
96
+ d = np.round(d)
97
+
98
+ if b in dict:
99
+ score_b = dict[b]
100
+ else:
101
+ score_b = function(b)
102
+ dict[b] = score_b
103
+
104
+ if d in dict:
105
+ score_d = dict[d]
106
+ else:
107
+ score_d = function(d)
108
+ dict[d] = score_d
109
+
110
+ if score_b <= score_d:
111
+ opt_score = score_b
112
+ opt_bw = b
113
+ c = d
114
+ d = b
115
+ b = a + delta * np.abs(c - a)
116
+ else:
117
+ opt_score = score_d
118
+ opt_bw = d
119
+ a = b
120
+ b = d
121
+ d = c - delta * np.abs(c - a)
122
+
123
+ diff = score_b - score_d
124
+ score = opt_score
125
+ return opt_bw
126
+
127
+ def sgwr_fit(self, y, X, bw):
128
+ if self.fixed:
129
+ bw = round(bw, 3)
130
+ RSS = 0
131
+ trS = 0
132
+ for i in range(self.n):
133
+ err2, hat = self._local_fit(i, y, X, bw)
134
+ RSS += err2
135
+ trS += hat
136
+
137
+ aicc = self.n * np.log((RSS) / (self.n)) + self.n * np.log(2 * np.pi) + self.n * (self.n + trS) / (
138
+ self.n - trS - 2.0)
139
+
140
+ print(f"BW: {bw:.3f} >>> AICc Score: {aicc:.3f}", flush=True)
141
+
142
+ return aicc
143
+
144
+ def fit(self, y=None, X=None):
145
+ if self.bw: ## if the bw is given by the users
146
+ alpha_optimization = self.optimal_alpha()
147
+ else: ## if the model requires to optimize the bandwidth
148
+ print('Bandwidth optimization using golden section based on AICc values...', flush=True)
149
+ if y is None:
150
+ y = self.y
151
+ X = self.X
152
+
153
+ self.set_search_range()
154
+
155
+ sgwr_func = lambda bw: self.sgwr_fit(y, X, bw)
156
+
157
+ self.bw = self.golden_section(self.minbw, self.maxbw, sgwr_func)
158
+ print('')
159
+ print(f'Optimal BW: {self.bw:.3f}')
160
+ # Save search params for spatial_variability compatibility
161
+ # return round(self.bw, 3)
162
+ if self.adaptiveBG:
163
+ self.fixed = True
164
+ alpha_optimization = self.optimal_alpha()
165
+
166
+ return alpha_optimization
167
+
168
+ def fit(self, y=None, X=None):
169
+ # If user provides a fixed bandwidth, skip optimization
170
+ if self.bw:
171
+ alpha_optimization = self.optimal_alpha()
172
+ else:
173
+ print('Bandwidth optimization using golden section based on AICc values...', flush=True)
174
+
175
+ if y is None:
176
+ y = self.y
177
+ X = self.X
178
+
179
+ self.set_search_range()
180
+ sgwr_func = lambda bw: self.sgwr_fit(y, X, bw)
181
+
182
+ self.bw = self.golden_section(self.minbw, self.maxbw, sgwr_func)
183
+
184
+ print(f'\nOptimal BW: {self.bw:.3f}')
185
+
186
+ if self.adaptiveBG:
187
+ self.fixed = True
188
+
189
+ alpha_optimization = self.optimal_alpha()
190
+
191
+ return alpha_optimization
192
+
193
+ def alpha_fit(self, i, bw, bt_value):
194
+ ###
195
+ data_point = self.data.iloc[i, :].values # fix: use .iloc and convert to np array
196
+ diff = np.abs(self.data.values - data_point)
197
+ combined = np.mean(diff, axis=1)
198
+ ws = np.exp(-combined ** 2)
199
+ ws = ws.reshape(-1, 1)
200
+
201
+ ### Geographically weighted
202
+ dist = cdist([self.coords[i]], self.coords).reshape(-1)
203
+
204
+ # fixed gaussian
205
+ if self.fixed:
206
+ if self.adaptiveBG:
207
+ bw = np.partition(dist, int(bw) - 1)[int(bw) - 1] * 1.0000001
208
+ wg = np.exp(-0.5 * (dist / bw) ** 2).reshape(-1, 1)
209
+ else:
210
+ wg = np.exp(-0.5 * (dist / bw) ** 2).reshape(-1, 1)
211
+ # adaptive bisquare
212
+ else:
213
+ maxd = np.partition(dist, int(bw) - 1)[int(bw) - 1] * 1.0000001
214
+ zs = dist / maxd
215
+ zs[zs >= 1] = 1
216
+ wg = ((1 - (zs) ** 2) ** 2).reshape(-1, 1)
217
+
218
+ wi = bt_value * wg + (1 - bt_value) * ws
219
+
220
+ X_new = self.X * np.sqrt(wi)
221
+ Y_new = self.y * np.sqrt(wi)
222
+ temp = np.dot(np.linalg.inv(np.dot(X_new.T, X_new)), X_new.T)
223
+ hat = np.dot(X_new[i], temp[:, i])
224
+ yhat = np.sum(np.dot(X_new, temp[:, i]).reshape(-1, 1) * Y_new)
225
+ err = Y_new[i][0] - yhat
226
+
227
+ return err * err, hat
228
+
229
+ def alpha_optimization(self, bw, bt_value):
230
+ RSS = 0
231
+ trS = 0
232
+ for i in range(self.n):
233
+ err2, hat = self.alpha_fit(i, bw, bt_value)
234
+ RSS += err2
235
+ trS += hat
236
+
237
+ aicc = self.n * np.log((RSS) / (self.n)) + self.n * np.log(2 * np.pi) + self.n * (self.n + trS) / (
238
+ self.n - trS - 2.0)
239
+
240
+ return [aicc]
241
+
242
+ def greedy_fit(self):
243
+ print('')
244
+ print('Greedy optimization takes longer time than divide and conqur option!')
245
+ print('Alpha optimization using bandwidth:', round(self.bw, 3))
246
+ print('')
247
+ initial_candidates = [0.9, 0.01]
248
+ alpha_scores = {}
249
+ best_alpha = None
250
+ best_score = float('inf')
251
+
252
+ # Evaluate initial candidates
253
+ for alpha in initial_candidates:
254
+ if alpha < 0.02:
255
+ continue
256
+ aicc = self.alpha_optimization(self.bw, alpha)[0]
257
+ alpha_scores[alpha] = aicc
258
+ print(f'Alpha: {alpha:.3f} >>> AICc Score: {aicc:.3f}', flush=True)
259
+ if aicc < best_score:
260
+ best_score = aicc
261
+ best_alpha = alpha
262
+ else:
263
+ break
264
+
265
+ # Recursive search between best and neighbors
266
+ def recursive_search(low, high, depth=0):
267
+ if abs(high - low) < 0.01 or depth > 10:
268
+ return
269
+
270
+ mid = round((low + high) / 2, 3)
271
+ if mid in alpha_scores or mid < 0.02:
272
+ return
273
+
274
+ aicc = self.alpha_optimization(self.bw, mid)[0]
275
+ alpha_scores[mid] = aicc
276
+ print(f'Alpha: {mid:.3f} >>> AICc Score: {aicc:.3f}', flush=True)
277
+
278
+ nonlocal best_alpha, best_score
279
+ if aicc < best_score:
280
+ best_alpha = mid
281
+ best_score = aicc
282
+ # Keep searching both sides around this new best
283
+ recursive_search(low, mid, depth + 1)
284
+ recursive_search(mid, high, depth + 1)
285
+ else:
286
+ # Only search between previous best and mid
287
+ if mid < best_alpha:
288
+ recursive_search(mid, best_alpha, depth + 1)
289
+ else:
290
+ recursive_search(best_alpha, mid, depth + 1)
291
+
292
+ # Start search around neighbors
293
+ sorted_initial = sorted(initial_candidates)
294
+ best_index = sorted_initial.index(best_alpha)
295
+ if best_index > 0:
296
+ recursive_search(sorted_initial[best_index - 1], best_alpha)
297
+ if best_index < len(sorted_initial) - 1:
298
+ recursive_search(best_alpha, sorted_initial[best_index + 1])
299
+
300
+ return best_alpha
301
+
302
+ ### this is coarse divide-and-conquer strategy with a step size of 0.1
303
+ def divid_fit(self):
304
+ print('')
305
+ print('Alpha optimization using bandwidth:', round(self.bw, 3))
306
+ print('')
307
+ initial_candidates = [0.7, 0.5, 0.1]
308
+ alpha_scores = {}
309
+ best_alpha = None
310
+ best_score = float('inf')
311
+
312
+ # Step 1: Evaluate initial candidates
313
+ for alpha in initial_candidates:
314
+ aicc = self.alpha_optimization(self.bw, alpha)[0]
315
+ alpha_scores[alpha] = aicc
316
+ print(f'Alpha: {alpha:.3f} >>> AICc Score: {aicc:.3f}', flush=True)
317
+ if aicc < best_score:
318
+ best_score = aicc
319
+ best_alpha = alpha
320
+ else:
321
+ break
322
+
323
+ # Step 2: Greedy search (strictly stop if score worsens)
324
+ def greedy_direction_search(start, direction, min_alpha=0.01):
325
+ nonlocal best_alpha, best_score
326
+
327
+ current = start
328
+ while True:
329
+ step = 0.1 if current > 0.1 else 0.02
330
+ next_alpha = round(current + direction * step, 3)
331
+ if next_alpha < min_alpha or next_alpha > 1.0 or next_alpha in alpha_scores:
332
+ break
333
+
334
+ aicc = self.alpha_optimization(self.bw, next_alpha)[0]
335
+ alpha_scores[next_alpha] = aicc
336
+ print(f'Alpha: {next_alpha:.3f} >>> AICc Score: {aicc:.3f}', flush=True)
337
+
338
+ if aicc < best_score:
339
+ best_alpha = next_alpha
340
+ best_score = aicc
341
+ current = next_alpha
342
+ else:
343
+ break
344
+
345
+ # Step 3: Explore both directions around best_alpha
346
+ greedy_direction_search(best_alpha, direction=-1) # Lower alphas
347
+ greedy_direction_search(best_alpha, direction=1) # Higher alphas
348
+
349
+ return best_alpha
350
+
351
+ def optimal_alpha(self):
352
+ if self.alphacurve:
353
+ self.best_alpha = self.greedy_fit()
354
+ print('')
355
+ print('Best alpha value (greedy):', self.best_alpha)
356
+ return round(self.bw, 3), self.best_alpha
357
+
358
+ else:
359
+ self.best_alpha = self.divid_fit()
360
+ print('')
361
+ print('Best alpha value (divide and conquer):', self.best_alpha)
362
+ return round(self.bw, 3), self.best_alpha