huff 1.3.3__tar.gz → 1.3.4__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.
- {huff-1.3.3 → huff-1.3.4}/PKG-INFO +2 -18
- {huff-1.3.3 → huff-1.3.4}/huff/gistools.py +2 -2
- {huff-1.3.3 → huff-1.3.4}/huff/models.py +134 -7
- {huff-1.3.3 → huff-1.3.4}/huff/ors.py +2 -2
- {huff-1.3.3 → huff-1.3.4}/huff/osm.py +3 -3
- {huff-1.3.3 → huff-1.3.4}/huff/tests/tests_huff.py +12 -4
- {huff-1.3.3 → huff-1.3.4}/huff.egg-info/PKG-INFO +2 -18
- {huff-1.3.3 → huff-1.3.4}/setup.py +1 -1
- {huff-1.3.3 → huff-1.3.4}/MANIFEST.in +0 -0
- {huff-1.3.3 → huff-1.3.4}/README.md +0 -0
- {huff-1.3.3 → huff-1.3.4}/huff/__init__.py +0 -0
- {huff-1.3.3 → huff-1.3.4}/huff/tests/__init__.py +0 -0
- {huff-1.3.3 → huff-1.3.4}/huff/tests/data/Haslach.cpg +0 -0
- {huff-1.3.3 → huff-1.3.4}/huff/tests/data/Haslach.dbf +0 -0
- {huff-1.3.3 → huff-1.3.4}/huff/tests/data/Haslach.prj +0 -0
- {huff-1.3.3 → huff-1.3.4}/huff/tests/data/Haslach.qmd +0 -0
- {huff-1.3.3 → huff-1.3.4}/huff/tests/data/Haslach.shp +0 -0
- {huff-1.3.3 → huff-1.3.4}/huff/tests/data/Haslach.shx +0 -0
- {huff-1.3.3 → huff-1.3.4}/huff/tests/data/Haslach_supermarkets.cpg +0 -0
- {huff-1.3.3 → huff-1.3.4}/huff/tests/data/Haslach_supermarkets.dbf +0 -0
- {huff-1.3.3 → huff-1.3.4}/huff/tests/data/Haslach_supermarkets.prj +0 -0
- {huff-1.3.3 → huff-1.3.4}/huff/tests/data/Haslach_supermarkets.qmd +0 -0
- {huff-1.3.3 → huff-1.3.4}/huff/tests/data/Haslach_supermarkets.shp +0 -0
- {huff-1.3.3 → huff-1.3.4}/huff/tests/data/Haslach_supermarkets.shx +0 -0
- {huff-1.3.3 → huff-1.3.4}/huff/tests/data/Wieland2015.xlsx +0 -0
- {huff-1.3.3 → huff-1.3.4}/huff.egg-info/SOURCES.txt +0 -0
- {huff-1.3.3 → huff-1.3.4}/huff.egg-info/dependency_links.txt +0 -0
- {huff-1.3.3 → huff-1.3.4}/huff.egg-info/requires.txt +0 -0
- {huff-1.3.3 → huff-1.3.4}/huff.egg-info/top_level.txt +0 -0
- {huff-1.3.3 → huff-1.3.4}/setup.cfg +0 -0
@@ -1,26 +1,10 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.1
|
2
2
|
Name: huff
|
3
|
-
Version: 1.3.
|
3
|
+
Version: 1.3.4
|
4
4
|
Summary: huff: Huff Model Market Area Analysis
|
5
5
|
Author: Thomas Wieland
|
6
6
|
Author-email: geowieland@googlemail.com
|
7
7
|
Description-Content-Type: text/markdown
|
8
|
-
Requires-Dist: geopandas
|
9
|
-
Requires-Dist: pandas
|
10
|
-
Requires-Dist: numpy
|
11
|
-
Requires-Dist: statsmodels
|
12
|
-
Requires-Dist: shapely
|
13
|
-
Requires-Dist: requests
|
14
|
-
Requires-Dist: matplotlib
|
15
|
-
Requires-Dist: pillow
|
16
|
-
Requires-Dist: contextily
|
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
8
|
|
25
9
|
# huff: Huff Model Market Area Analysis
|
26
10
|
|
@@ -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.4
|
8
|
+
# Last update: 2025-06-02 17:17
|
9
9
|
# Copyright (c) 2025 Thomas Wieland
|
10
10
|
#-----------------------------------------------------------------------
|
11
11
|
|
@@ -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.4
|
8
|
+
# Last update: 2025-06-02 17:17
|
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)
|
@@ -1662,7 +1714,7 @@ def get_isochrones(
|
|
1662
1714
|
|
1663
1715
|
isochrone_gdf[unique_id_col] = unique_id_values[i]
|
1664
1716
|
|
1665
|
-
isochrone_gdf["
|
1717
|
+
isochrone_gdf["segm_min"] = isochrone_gdf["segment"]/60
|
1666
1718
|
|
1667
1719
|
isochrones_gdf = pd.concat(
|
1668
1720
|
[
|
@@ -1675,7 +1727,7 @@ def get_isochrones(
|
|
1675
1727
|
i = i+1
|
1676
1728
|
|
1677
1729
|
isochrones_gdf["segment"] = isochrones_gdf["segment"].astype(int)
|
1678
|
-
isochrones_gdf["
|
1730
|
+
isochrones_gdf["segm_min"] = isochrones_gdf["segm_min"].astype(int)
|
1679
1731
|
|
1680
1732
|
isochrones_gdf.set_crs(
|
1681
1733
|
output_crs,
|
@@ -1690,6 +1742,81 @@ def get_isochrones(
|
|
1690
1742
|
return isochrones_gdf
|
1691
1743
|
|
1692
1744
|
|
1745
|
+
def modelfit(
|
1746
|
+
observed,
|
1747
|
+
expected
|
1748
|
+
):
|
1749
|
+
|
1750
|
+
observed_no = len(observed)
|
1751
|
+
expected_no = len(expected)
|
1752
|
+
|
1753
|
+
if not observed_no == expected_no:
|
1754
|
+
raise ValueError("Observed and expected differ in length")
|
1755
|
+
|
1756
|
+
if not isinstance(observed, np.number):
|
1757
|
+
if not is_numeric_dtype(observed):
|
1758
|
+
raise ValueError("Observed column is not numeric")
|
1759
|
+
if not isinstance(expected, np.number):
|
1760
|
+
if not is_numeric_dtype(expected):
|
1761
|
+
raise ValueError("Expected column is not numeric")
|
1762
|
+
|
1763
|
+
residuals = np.array(observed)-np.array(expected)
|
1764
|
+
residuals_sq = residuals**2
|
1765
|
+
residuals_abs = abs(residuals)
|
1766
|
+
|
1767
|
+
APE = abs(observed-expected)/observed*100
|
1768
|
+
|
1769
|
+
data_residuals = pd.DataFrame({
|
1770
|
+
"observed": observed,
|
1771
|
+
"expected": expected,
|
1772
|
+
"residuals": residuals,
|
1773
|
+
"residuals_sq": residuals_sq,
|
1774
|
+
"residuals_abs": residuals_abs,
|
1775
|
+
"APE": APE
|
1776
|
+
})
|
1777
|
+
|
1778
|
+
SQR = float(np.sum(residuals_sq))
|
1779
|
+
SAR = float(np.sum(residuals_abs))
|
1780
|
+
observed_mean = float(np.sum(observed)/observed_no)
|
1781
|
+
SQT = float(np.sum((observed-observed_mean)**2))
|
1782
|
+
Rsq = float(1-(SQR/SQT))
|
1783
|
+
MSE = float(SQR/observed_no)
|
1784
|
+
RMSE = float(sqrt(MSE))
|
1785
|
+
MAE = float(SAR/observed_no)
|
1786
|
+
MAPE = float(np.mean(APE))
|
1787
|
+
|
1788
|
+
resid_below5 = float(len([APE < 5])/expected_no*100)
|
1789
|
+
resid_below10 = float(len([APE < 10])/expected_no*100)
|
1790
|
+
resid_below15 = float(len([APE < 15])/expected_no*100)
|
1791
|
+
resid_below20 = float(len([APE < 20])/expected_no*100)
|
1792
|
+
resid_below25 = float(len([APE < 25])/expected_no*100)
|
1793
|
+
|
1794
|
+
data_lossfunctions = {
|
1795
|
+
"SQR": SQR,
|
1796
|
+
"SAR": SAR,
|
1797
|
+
"SQT": SQT,
|
1798
|
+
"Rsq": Rsq,
|
1799
|
+
"MSE": MSE,
|
1800
|
+
"RMSE": RMSE,
|
1801
|
+
"MAE": MAE,
|
1802
|
+
"MAPE": MAPE,
|
1803
|
+
"APE": {
|
1804
|
+
"resid_below5": resid_below5,
|
1805
|
+
"resid_below10": resid_below10,
|
1806
|
+
"resid_below15": resid_below15,
|
1807
|
+
"resid_below20": resid_below20,
|
1808
|
+
"resid_below25": resid_below25
|
1809
|
+
}
|
1810
|
+
}
|
1811
|
+
|
1812
|
+
modelfit_results = [
|
1813
|
+
data_residuals,
|
1814
|
+
data_lossfunctions
|
1815
|
+
]
|
1816
|
+
|
1817
|
+
return modelfit_results
|
1818
|
+
|
1819
|
+
|
1693
1820
|
def check_vars(
|
1694
1821
|
df: pd.DataFrame,
|
1695
1822
|
cols: list
|
@@ -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-01
|
7
|
+
# Version: 1.3.4
|
8
|
+
# Last update: 2025-06-01 17:18
|
9
9
|
# Copyright (c) 2025 Thomas Wieland
|
10
10
|
#-----------------------------------------------------------------------
|
11
11
|
|
@@ -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.4
|
8
|
+
# Last update: 2025-06-02 17:18
|
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
|
|
@@ -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.4
|
8
|
+
# Last update: 2025-06-02 17:17
|
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,26 +1,10 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.1
|
2
2
|
Name: huff
|
3
|
-
Version: 1.3.
|
3
|
+
Version: 1.3.4
|
4
4
|
Summary: huff: Huff Model Market Area Analysis
|
5
5
|
Author: Thomas Wieland
|
6
6
|
Author-email: geowieland@googlemail.com
|
7
7
|
Description-Content-Type: text/markdown
|
8
|
-
Requires-Dist: geopandas
|
9
|
-
Requires-Dist: pandas
|
10
|
-
Requires-Dist: numpy
|
11
|
-
Requires-Dist: statsmodels
|
12
|
-
Requires-Dist: shapely
|
13
|
-
Requires-Dist: requests
|
14
|
-
Requires-Dist: matplotlib
|
15
|
-
Requires-Dist: pillow
|
16
|
-
Requires-Dist: contextily
|
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
8
|
|
25
9
|
# huff: Huff Model Market Area Analysis
|
26
10
|
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|