huff 1.3.3__py3-none-any.whl → 1.3.5__py3-none-any.whl
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.
- huff/gistools.py +2 -2
- huff/models.py +138 -7
- huff/ors.py +2 -2
- huff/osm.py +3 -3
- huff/tests/tests_huff.py +12 -4
- {huff-1.3.3.dist-info → huff-1.3.5.dist-info}/METADATA +2 -8
- {huff-1.3.3.dist-info → huff-1.3.5.dist-info}/RECORD +9 -9
- {huff-1.3.3.dist-info → huff-1.3.5.dist-info}/WHEEL +1 -1
- {huff-1.3.3.dist-info → huff-1.3.5.dist-info}/top_level.txt +0 -0
huff/gistools.py
CHANGED
@@ -4,8 +4,8 @@
|
|
4
4
|
# Author: Thomas Wieland
|
5
5
|
# ORCID: 0000-0001-5168-9846
|
6
6
|
# mail: geowieland@googlemail.com
|
7
|
-
# Version: 1.3.
|
8
|
-
# Last update: 2025-06-
|
7
|
+
# Version: 1.3.5
|
8
|
+
# Last update: 2025-06-03 17:24
|
9
9
|
# Copyright (c) 2025 Thomas Wieland
|
10
10
|
#-----------------------------------------------------------------------
|
11
11
|
|
huff/models.py
CHANGED
@@ -4,8 +4,8 @@
|
|
4
4
|
# Author: Thomas Wieland
|
5
5
|
# ORCID: 0000-0001-5168-9846
|
6
6
|
# mail: geowieland@googlemail.com
|
7
|
-
# Version: 1.3.
|
8
|
-
# Last update: 2025-06-
|
7
|
+
# Version: 1.3.5
|
8
|
+
# Last update: 2025-06-03 17:23
|
9
9
|
# Copyright (c) 2025 Thomas Wieland
|
10
10
|
#-----------------------------------------------------------------------
|
11
11
|
|
@@ -13,7 +13,9 @@
|
|
13
13
|
import pandas as pd
|
14
14
|
import geopandas as gp
|
15
15
|
import numpy as np
|
16
|
+
from math import sqrt
|
16
17
|
import time
|
18
|
+
from pandas.api.types import is_numeric_dtype
|
17
19
|
from statsmodels.formula.api import ols
|
18
20
|
from shapely.geometry import Point
|
19
21
|
from shapely import wkt
|
@@ -232,6 +234,9 @@ class SupplyLocations:
|
|
232
234
|
|
233
235
|
print("Supply Locations")
|
234
236
|
print("No. locations " + str(metadata["no_points"]))
|
237
|
+
|
238
|
+
if metadata["attraction_col"][0] is None or metadata["attraction_col"] == []:
|
239
|
+
print("Attraction column(s) not defined")
|
235
240
|
else:
|
236
241
|
print("Attraction column(s) " + ",".join(metadata["attraction_col"]))
|
237
242
|
|
@@ -410,8 +415,7 @@ class SupplyLocations:
|
|
410
415
|
|
411
416
|
self.buffers_gdf = buffers_gdf
|
412
417
|
|
413
|
-
return self
|
414
|
-
|
418
|
+
return self
|
415
419
|
|
416
420
|
class InteractionMatrix:
|
417
421
|
|
@@ -442,6 +446,7 @@ class InteractionMatrix:
|
|
442
446
|
|
443
447
|
print("Interaction Matrix")
|
444
448
|
print("----------------------------------")
|
449
|
+
|
445
450
|
print("Supply locations " + str(supply_locations_metadata["no_points"]))
|
446
451
|
if supply_locations_metadata["attraction_col"][0] is None:
|
447
452
|
print("Attraction column not defined")
|
@@ -993,6 +998,24 @@ class MCIModel:
|
|
993
998
|
|
994
999
|
return self.market_areas_df
|
995
1000
|
|
1001
|
+
def modelfit(self):
|
1002
|
+
|
1003
|
+
interaction_matrix = self.interaction_matrix
|
1004
|
+
interaction_matrix_df = interaction_matrix.get_interaction_matrix_df()
|
1005
|
+
|
1006
|
+
if ("p_ij" in interaction_matrix_df.columns and "p_ij_emp" in interaction_matrix_df.columns):
|
1007
|
+
|
1008
|
+
mci_modelfit = modelfit(
|
1009
|
+
interaction_matrix_df["p_ij_emp"],
|
1010
|
+
interaction_matrix_df["p_ij"]
|
1011
|
+
)
|
1012
|
+
|
1013
|
+
return mci_modelfit
|
1014
|
+
|
1015
|
+
else:
|
1016
|
+
|
1017
|
+
return None
|
1018
|
+
|
996
1019
|
def summary(self):
|
997
1020
|
|
998
1021
|
interaction_matrix = self.interaction_matrix
|
@@ -1029,7 +1052,28 @@ class MCIModel:
|
|
1029
1052
|
print (coefficients_df)
|
1030
1053
|
|
1031
1054
|
print("--------------------------------------------")
|
1032
|
-
|
1055
|
+
|
1056
|
+
mci_modelfit = self.modelfit()
|
1057
|
+
if mci_modelfit is not None:
|
1058
|
+
|
1059
|
+
print ("Goodness-of-fit with respect to probabilities")
|
1060
|
+
|
1061
|
+
print("Sum of squared residuals ", round(mci_modelfit[1]["SQR"], 2))
|
1062
|
+
print("Sum of squares ", round(mci_modelfit[1]["SQT"], 2))
|
1063
|
+
print("R-squared ", round(mci_modelfit[1]["Rsq"], 2))
|
1064
|
+
print("Mean squared error ", round(mci_modelfit[1]["MSE"], 2))
|
1065
|
+
print("Root mean squared error ", round(mci_modelfit[1]["RMSE"], 2))
|
1066
|
+
print("Mean absolute error ", round(mci_modelfit[1]["MAE"], 2))
|
1067
|
+
print("Mean absolute percentage error ", round(mci_modelfit[1]["MAPE"], 2))
|
1068
|
+
print("Absolute percentage errors")
|
1069
|
+
print("< 5 % ", round(mci_modelfit[1]["APE"]["resid_below5"], 2))
|
1070
|
+
print("< 10 % ", round(mci_modelfit[1]["APE"]["resid_below10"], 2))
|
1071
|
+
print("< 15 % ", round(mci_modelfit[1]["APE"]["resid_below15"], 2))
|
1072
|
+
print("< 20 % ", round(mci_modelfit[1]["APE"]["resid_below20"], 2))
|
1073
|
+
print("< 25 % ", round(mci_modelfit[1]["APE"]["resid_below25"], 2))
|
1074
|
+
|
1075
|
+
print("--------------------------------------------")
|
1076
|
+
|
1033
1077
|
def utility(
|
1034
1078
|
self,
|
1035
1079
|
transformation = "LCT"
|
@@ -1091,6 +1135,14 @@ class MCIModel:
|
|
1091
1135
|
|
1092
1136
|
interaction_matrix = self.interaction_matrix
|
1093
1137
|
interaction_matrix_df = interaction_matrix.get_interaction_matrix_df()
|
1138
|
+
|
1139
|
+
if "p_ij" in interaction_matrix_df.columns:
|
1140
|
+
interaction_matrix_df["p_ij_emp"] = interaction_matrix_df["p_ij"]
|
1141
|
+
|
1142
|
+
if "U_ij" not in interaction_matrix_df.columns:
|
1143
|
+
self.utility(transformation = transformation)
|
1144
|
+
interaction_matrix = self.interaction_matrix
|
1145
|
+
interaction_matrix_df = interaction_matrix.get_interaction_matrix_df()
|
1094
1146
|
|
1095
1147
|
if interaction_matrix_df["U_ij"].isna().all():
|
1096
1148
|
self.utility(transformation = transformation)
|
@@ -1296,6 +1348,10 @@ def create_interaction_matrix(
|
|
1296
1348
|
|
1297
1349
|
customer_origins_geodata_gpd = pd.DataFrame(customer_origins.get_geodata_gpd())
|
1298
1350
|
customer_origins_geodata_gpd_original = pd.DataFrame(customer_origins.get_geodata_gpd_original())
|
1351
|
+
|
1352
|
+
customer_origins_geodata_gpd[customer_origins_unique_id] = customer_origins_geodata_gpd[customer_origins_unique_id].astype(str)
|
1353
|
+
customer_origins_geodata_gpd_original[customer_origins_unique_id] = customer_origins_geodata_gpd_original[customer_origins_unique_id].astype(str)
|
1354
|
+
|
1299
1355
|
customer_origins_data = pd.merge(
|
1300
1356
|
customer_origins_geodata_gpd,
|
1301
1357
|
customer_origins_geodata_gpd_original[[customer_origins_unique_id, customer_origins_marketsize]],
|
@@ -1662,7 +1718,7 @@ def get_isochrones(
|
|
1662
1718
|
|
1663
1719
|
isochrone_gdf[unique_id_col] = unique_id_values[i]
|
1664
1720
|
|
1665
|
-
isochrone_gdf["
|
1721
|
+
isochrone_gdf["segm_min"] = isochrone_gdf["segment"]/60
|
1666
1722
|
|
1667
1723
|
isochrones_gdf = pd.concat(
|
1668
1724
|
[
|
@@ -1675,7 +1731,7 @@ def get_isochrones(
|
|
1675
1731
|
i = i+1
|
1676
1732
|
|
1677
1733
|
isochrones_gdf["segment"] = isochrones_gdf["segment"].astype(int)
|
1678
|
-
isochrones_gdf["
|
1734
|
+
isochrones_gdf["segm_min"] = isochrones_gdf["segm_min"].astype(int)
|
1679
1735
|
|
1680
1736
|
isochrones_gdf.set_crs(
|
1681
1737
|
output_crs,
|
@@ -1690,6 +1746,81 @@ def get_isochrones(
|
|
1690
1746
|
return isochrones_gdf
|
1691
1747
|
|
1692
1748
|
|
1749
|
+
def modelfit(
|
1750
|
+
observed,
|
1751
|
+
expected
|
1752
|
+
):
|
1753
|
+
|
1754
|
+
observed_no = len(observed)
|
1755
|
+
expected_no = len(expected)
|
1756
|
+
|
1757
|
+
if not observed_no == expected_no:
|
1758
|
+
raise ValueError("Observed and expected differ in length")
|
1759
|
+
|
1760
|
+
if not isinstance(observed, np.number):
|
1761
|
+
if not is_numeric_dtype(observed):
|
1762
|
+
raise ValueError("Observed column is not numeric")
|
1763
|
+
if not isinstance(expected, np.number):
|
1764
|
+
if not is_numeric_dtype(expected):
|
1765
|
+
raise ValueError("Expected column is not numeric")
|
1766
|
+
|
1767
|
+
residuals = np.array(observed)-np.array(expected)
|
1768
|
+
residuals_sq = residuals**2
|
1769
|
+
residuals_abs = abs(residuals)
|
1770
|
+
|
1771
|
+
APE = abs(observed-expected)/observed*100
|
1772
|
+
|
1773
|
+
data_residuals = pd.DataFrame({
|
1774
|
+
"observed": observed,
|
1775
|
+
"expected": expected,
|
1776
|
+
"residuals": residuals,
|
1777
|
+
"residuals_sq": residuals_sq,
|
1778
|
+
"residuals_abs": residuals_abs,
|
1779
|
+
"APE": APE
|
1780
|
+
})
|
1781
|
+
|
1782
|
+
SQR = float(np.sum(residuals_sq))
|
1783
|
+
SAR = float(np.sum(residuals_abs))
|
1784
|
+
observed_mean = float(np.sum(observed)/observed_no)
|
1785
|
+
SQT = float(np.sum((observed-observed_mean)**2))
|
1786
|
+
Rsq = float(1-(SQR/SQT))
|
1787
|
+
MSE = float(SQR/observed_no)
|
1788
|
+
RMSE = float(sqrt(MSE))
|
1789
|
+
MAE = float(SAR/observed_no)
|
1790
|
+
MAPE = float(np.mean(APE))
|
1791
|
+
|
1792
|
+
resid_below5 = float(len([APE < 5])/expected_no*100)
|
1793
|
+
resid_below10 = float(len([APE < 10])/expected_no*100)
|
1794
|
+
resid_below15 = float(len([APE < 15])/expected_no*100)
|
1795
|
+
resid_below20 = float(len([APE < 20])/expected_no*100)
|
1796
|
+
resid_below25 = float(len([APE < 25])/expected_no*100)
|
1797
|
+
|
1798
|
+
data_lossfunctions = {
|
1799
|
+
"SQR": SQR,
|
1800
|
+
"SAR": SAR,
|
1801
|
+
"SQT": SQT,
|
1802
|
+
"Rsq": Rsq,
|
1803
|
+
"MSE": MSE,
|
1804
|
+
"RMSE": RMSE,
|
1805
|
+
"MAE": MAE,
|
1806
|
+
"MAPE": MAPE,
|
1807
|
+
"APE": {
|
1808
|
+
"resid_below5": resid_below5,
|
1809
|
+
"resid_below10": resid_below10,
|
1810
|
+
"resid_below15": resid_below15,
|
1811
|
+
"resid_below20": resid_below20,
|
1812
|
+
"resid_below25": resid_below25
|
1813
|
+
}
|
1814
|
+
}
|
1815
|
+
|
1816
|
+
modelfit_results = [
|
1817
|
+
data_residuals,
|
1818
|
+
data_lossfunctions
|
1819
|
+
]
|
1820
|
+
|
1821
|
+
return modelfit_results
|
1822
|
+
|
1823
|
+
|
1693
1824
|
def check_vars(
|
1694
1825
|
df: pd.DataFrame,
|
1695
1826
|
cols: list
|
huff/ors.py
CHANGED
@@ -4,8 +4,8 @@
|
|
4
4
|
# Author: Thomas Wieland
|
5
5
|
# ORCID: 0000-0001-5168-9846
|
6
6
|
# mail: geowieland@googlemail.com
|
7
|
-
# Version: 1.3.
|
8
|
-
# Last update: 2025-06-
|
7
|
+
# Version: 1.3.5
|
8
|
+
# Last update: 2025-06-03 17:24
|
9
9
|
# Copyright (c) 2025 Thomas Wieland
|
10
10
|
#-----------------------------------------------------------------------
|
11
11
|
|
huff/osm.py
CHANGED
@@ -4,8 +4,8 @@
|
|
4
4
|
# Author: Thomas Wieland
|
5
5
|
# ORCID: 0000-0001-5168-9846
|
6
6
|
# mail: geowieland@googlemail.com
|
7
|
-
# Version: 1.3.
|
8
|
-
# Last update: 2025-06-
|
7
|
+
# Version: 1.3.5
|
8
|
+
# Last update: 2025-06-03 17:24
|
9
9
|
# Copyright (c) 2025 Thomas Wieland
|
10
10
|
#-----------------------------------------------------------------------
|
11
11
|
|
@@ -198,7 +198,7 @@ def map_with_basemap(
|
|
198
198
|
color_mapping = layer_color[color_key]
|
199
199
|
|
200
200
|
if color_key not in layer_3857.columns:
|
201
|
-
raise KeyError ("Column"
|
201
|
+
raise KeyError ("Column " + color_key + " not in layer.")
|
202
202
|
|
203
203
|
for value, color in color_mapping.items():
|
204
204
|
|
huff/tests/tests_huff.py
CHANGED
@@ -4,13 +4,13 @@
|
|
4
4
|
# Author: Thomas Wieland
|
5
5
|
# ORCID: 0000-0001-5168-9846
|
6
6
|
# mail: geowieland@googlemail.com
|
7
|
-
# Version: 1.3.
|
8
|
-
# Last update: 2025-
|
7
|
+
# Version: 1.3.5
|
8
|
+
# Last update: 2025-06-03 17:24
|
9
9
|
# Copyright (c) 2025 Thomas Wieland
|
10
10
|
#-----------------------------------------------------------------------
|
11
11
|
|
12
12
|
|
13
|
-
from huff.models import create_interaction_matrix, get_isochrones, load_geodata, load_interaction_matrix
|
13
|
+
from huff.models import create_interaction_matrix, get_isochrones, load_geodata, load_interaction_matrix, modelfit
|
14
14
|
from huff.osm import map_with_basemap
|
15
15
|
from huff.gistools import buffers, point_spatial_join
|
16
16
|
|
@@ -179,6 +179,14 @@ Wieland2015_fit = Wieland2015_interaction_matrix.mci_fit(
|
|
179
179
|
Wieland2015_fit.summary()
|
180
180
|
# MCI model summary
|
181
181
|
|
182
|
+
Wieland2015_fit.probabilities()
|
183
|
+
|
184
|
+
Wieland2015_fit_interactionmatrix = Wieland2015_fit.get_interaction_matrix_df()
|
185
|
+
# Export interaction matrix
|
186
|
+
|
187
|
+
Wieland2015_fit.summary()
|
188
|
+
# MCI model summary
|
189
|
+
|
182
190
|
|
183
191
|
# Buffer analysis:
|
184
192
|
|
@@ -240,7 +248,7 @@ map_with_basemap(
|
|
240
248
|
styles={
|
241
249
|
0: {
|
242
250
|
"color": {
|
243
|
-
"
|
251
|
+
"segm_min": {
|
244
252
|
"3": "green",
|
245
253
|
"6": "yellow",
|
246
254
|
"9": "orange",
|
@@ -1,6 +1,6 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.1
|
2
2
|
Name: huff
|
3
|
-
Version: 1.3.
|
3
|
+
Version: 1.3.5
|
4
4
|
Summary: huff: Huff Model Market Area Analysis
|
5
5
|
Author: Thomas Wieland
|
6
6
|
Author-email: geowieland@googlemail.com
|
@@ -15,12 +15,6 @@ Requires-Dist: matplotlib
|
|
15
15
|
Requires-Dist: pillow
|
16
16
|
Requires-Dist: contextily
|
17
17
|
Requires-Dist: openpyxl
|
18
|
-
Dynamic: author
|
19
|
-
Dynamic: author-email
|
20
|
-
Dynamic: description
|
21
|
-
Dynamic: description-content-type
|
22
|
-
Dynamic: requires-dist
|
23
|
-
Dynamic: summary
|
24
18
|
|
25
19
|
# huff: Huff Model Market Area Analysis
|
26
20
|
|
@@ -1,10 +1,10 @@
|
|
1
1
|
huff/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
-
huff/gistools.py,sha256=
|
3
|
-
huff/models.py,sha256=
|
4
|
-
huff/ors.py,sha256=
|
5
|
-
huff/osm.py,sha256=
|
2
|
+
huff/gistools.py,sha256=jvS1IOi1cIO4X1i8n3NeyGlufaobBtAb0nI8aNs1jrk,6857
|
3
|
+
huff/models.py,sha256=T16UellqoDuEKcAq96Cn_TzEqihv9USIGivCOnbLZmw,66576
|
4
|
+
huff/ors.py,sha256=_qMBsgoEzZMbagxcF3sUjQ-jFZrNjuNF23v8mlmhbZI,11929
|
5
|
+
huff/osm.py,sha256=K_acRNHlKTOHVmMX-bHbA2_06QwfPLXpYKJcn03_gik,6862
|
6
6
|
huff/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
7
|
-
huff/tests/tests_huff.py,sha256=
|
7
|
+
huff/tests/tests_huff.py,sha256=v5ockdMFpq4C2hS6l3VAuWPtsKhWYI8l5AJC3wo-xZE,7795
|
8
8
|
huff/tests/data/Haslach.cpg,sha256=OtMDH1UDpEBK-CUmLugjLMBNTqZoPULF3QovKiesmCQ,5
|
9
9
|
huff/tests/data/Haslach.dbf,sha256=GVPIt05OzDO7UrRDcsMhiYWvyXAPg6Z-qkiysFzj-fc,506
|
10
10
|
huff/tests/data/Haslach.prj,sha256=2Jy1Vlzh7UxQ1MXpZ9UYLs2SxfrObj2xkEkZyLqmGTY,437
|
@@ -18,7 +18,7 @@ huff/tests/data/Haslach_supermarkets.qmd,sha256=JlcOYzG4vI1NH1IuOpxwIPnJsCyC-pDR
|
|
18
18
|
huff/tests/data/Haslach_supermarkets.shp,sha256=X7QbQ0BTMag_B-bDRbpr-go2BQIXo3Y8zMAKpYZmlps,324
|
19
19
|
huff/tests/data/Haslach_supermarkets.shx,sha256=j23QHX-SmdAeN04rw0x8nUOran-OCg_T6r_LvzzEPWs,164
|
20
20
|
huff/tests/data/Wieland2015.xlsx,sha256=SaVM-Hi5dBTmf2bzszMnZ2Ec8NUE05S_5F2lQj0ayS0,19641
|
21
|
-
huff-1.3.
|
22
|
-
huff-1.3.
|
23
|
-
huff-1.3.
|
24
|
-
huff-1.3.
|
21
|
+
huff-1.3.5.dist-info/METADATA,sha256=tj7dFjaEWkaa3eLHLTu8QDb51L5Nlv1bKO-mUSra1Cg,4558
|
22
|
+
huff-1.3.5.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
|
23
|
+
huff-1.3.5.dist-info/top_level.txt,sha256=nlzX-PxZNFmIxANIJMySuIFPihd6qOBkRlhIC28NEsQ,5
|
24
|
+
huff-1.3.5.dist-info/RECORD,,
|
File without changes
|