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.
Files changed (30) hide show
  1. {huff-1.3.3 → huff-1.3.4}/PKG-INFO +2 -18
  2. {huff-1.3.3 → huff-1.3.4}/huff/gistools.py +2 -2
  3. {huff-1.3.3 → huff-1.3.4}/huff/models.py +134 -7
  4. {huff-1.3.3 → huff-1.3.4}/huff/ors.py +2 -2
  5. {huff-1.3.3 → huff-1.3.4}/huff/osm.py +3 -3
  6. {huff-1.3.3 → huff-1.3.4}/huff/tests/tests_huff.py +12 -4
  7. {huff-1.3.3 → huff-1.3.4}/huff.egg-info/PKG-INFO +2 -18
  8. {huff-1.3.3 → huff-1.3.4}/setup.py +1 -1
  9. {huff-1.3.3 → huff-1.3.4}/MANIFEST.in +0 -0
  10. {huff-1.3.3 → huff-1.3.4}/README.md +0 -0
  11. {huff-1.3.3 → huff-1.3.4}/huff/__init__.py +0 -0
  12. {huff-1.3.3 → huff-1.3.4}/huff/tests/__init__.py +0 -0
  13. {huff-1.3.3 → huff-1.3.4}/huff/tests/data/Haslach.cpg +0 -0
  14. {huff-1.3.3 → huff-1.3.4}/huff/tests/data/Haslach.dbf +0 -0
  15. {huff-1.3.3 → huff-1.3.4}/huff/tests/data/Haslach.prj +0 -0
  16. {huff-1.3.3 → huff-1.3.4}/huff/tests/data/Haslach.qmd +0 -0
  17. {huff-1.3.3 → huff-1.3.4}/huff/tests/data/Haslach.shp +0 -0
  18. {huff-1.3.3 → huff-1.3.4}/huff/tests/data/Haslach.shx +0 -0
  19. {huff-1.3.3 → huff-1.3.4}/huff/tests/data/Haslach_supermarkets.cpg +0 -0
  20. {huff-1.3.3 → huff-1.3.4}/huff/tests/data/Haslach_supermarkets.dbf +0 -0
  21. {huff-1.3.3 → huff-1.3.4}/huff/tests/data/Haslach_supermarkets.prj +0 -0
  22. {huff-1.3.3 → huff-1.3.4}/huff/tests/data/Haslach_supermarkets.qmd +0 -0
  23. {huff-1.3.3 → huff-1.3.4}/huff/tests/data/Haslach_supermarkets.shp +0 -0
  24. {huff-1.3.3 → huff-1.3.4}/huff/tests/data/Haslach_supermarkets.shx +0 -0
  25. {huff-1.3.3 → huff-1.3.4}/huff/tests/data/Wieland2015.xlsx +0 -0
  26. {huff-1.3.3 → huff-1.3.4}/huff.egg-info/SOURCES.txt +0 -0
  27. {huff-1.3.3 → huff-1.3.4}/huff.egg-info/dependency_links.txt +0 -0
  28. {huff-1.3.3 → huff-1.3.4}/huff.egg-info/requires.txt +0 -0
  29. {huff-1.3.3 → huff-1.3.4}/huff.egg-info/top_level.txt +0 -0
  30. {huff-1.3.3 → huff-1.3.4}/setup.cfg +0 -0
@@ -1,26 +1,10 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.1
2
2
  Name: huff
3
- Version: 1.3.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.3
8
- # Last update: 2025-06-01 14:30
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.3
8
- # Last update: 2025-06-01 14:29
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["segment_minutes"] = isochrone_gdf["segment"]/60
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["segment_minutes"] = isochrones_gdf["segment_minutes"].astype(int)
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.3
8
- # Last update: 2025-06-01 14:30
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.3
8
- # Last update: 2025-06-01 14:30
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", color_key, "not in layer.")
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.2
8
- # Last update: 2025-05-30 09:37
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
- "segment_minutes": {
251
+ "segm_min": {
244
252
  "3": "green",
245
253
  "6": "yellow",
246
254
  "9": "orange",
@@ -1,26 +1,10 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.1
2
2
  Name: huff
3
- Version: 1.3.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
 
@@ -7,7 +7,7 @@ def read_README():
7
7
 
8
8
  setup(
9
9
  name='huff',
10
- version='1.3.3',
10
+ version='1.3.4',
11
11
  description='huff: Huff Model Market Area Analysis',
12
12
  packages=find_packages(include=["huff", "huff.tests"]),
13
13
  include_package_data=True,
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