huff 1.3.2__py3-none-any.whl → 1.3.4__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 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.2
8
- # Last update: 2025-05-30 11: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
 
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.2
8
- # Last update: 2025-05-30 11: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")
@@ -697,8 +702,6 @@ class InteractionMatrix:
697
702
 
698
703
  return hansen_df
699
704
 
700
- =======
701
- >>>>>>> e9583d0cd7f349f17f005df0fa75e70160655aa3
702
705
  def mci_transformation(
703
706
  self,
704
707
  cols: list = ["A_j", "t_ij"]
@@ -995,6 +998,24 @@ class MCIModel:
995
998
 
996
999
  return self.market_areas_df
997
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
+
998
1019
  def summary(self):
999
1020
 
1000
1021
  interaction_matrix = self.interaction_matrix
@@ -1031,7 +1052,28 @@ class MCIModel:
1031
1052
  print (coefficients_df)
1032
1053
 
1033
1054
  print("--------------------------------------------")
1034
-
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
+
1035
1077
  def utility(
1036
1078
  self,
1037
1079
  transformation = "LCT"
@@ -1093,6 +1135,14 @@ class MCIModel:
1093
1135
 
1094
1136
  interaction_matrix = self.interaction_matrix
1095
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()
1096
1146
 
1097
1147
  if interaction_matrix_df["U_ij"].isna().all():
1098
1148
  self.utility(transformation = transformation)
@@ -1664,7 +1714,7 @@ def get_isochrones(
1664
1714
 
1665
1715
  isochrone_gdf[unique_id_col] = unique_id_values[i]
1666
1716
 
1667
- isochrone_gdf["segment_minutes"] = isochrone_gdf["segment"]/60
1717
+ isochrone_gdf["segm_min"] = isochrone_gdf["segment"]/60
1668
1718
 
1669
1719
  isochrones_gdf = pd.concat(
1670
1720
  [
@@ -1677,7 +1727,7 @@ def get_isochrones(
1677
1727
  i = i+1
1678
1728
 
1679
1729
  isochrones_gdf["segment"] = isochrones_gdf["segment"].astype(int)
1680
- isochrones_gdf["segment_minutes"] = isochrones_gdf["segment_minutes"].astype(int)
1730
+ isochrones_gdf["segm_min"] = isochrones_gdf["segm_min"].astype(int)
1681
1731
 
1682
1732
  isochrones_gdf.set_crs(
1683
1733
  output_crs,
@@ -1692,6 +1742,81 @@ def get_isochrones(
1692
1742
  return isochrones_gdf
1693
1743
 
1694
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
+
1695
1820
  def check_vars(
1696
1821
  df: pd.DataFrame,
1697
1822
  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.2
8
- # Last update: 2025-05-30 11:07
7
+ # Version: 1.3.4
8
+ # Last update: 2025-06-01 17:18
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.2
8
- # Last update: 2025-05-30 11:07
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
 
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.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,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.1
2
2
  Name: huff
3
- Version: 1.3.2
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
@@ -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=Zcckofl4OWueSXZmUHRsbrLvAHZuxeI5Gwh4g7hi2H0,6857
3
- huff/models.py,sha256=gSVZims_7TvXr-pzUYAP19fnEtHmr0QmT8LFI_LsIVw,61452
4
- huff/ors.py,sha256=hjLJzBBQUP0frF-Aj8aCkOFVeCHc_3rGkJ8srOYIPOE,11929
5
- huff/osm.py,sha256=KTJo9NYDNZjDDYp4FKNH0MxMnY9A7kfg12FvCAvpmQw,6858
2
+ huff/gistools.py,sha256=PVeWvGzTmCXgIgzXGSElvCdaSV78_YcgYISgthyr6sI,6857
3
+ huff/models.py,sha256=3jESNI57oS6dEFvf3bFoq4txfP1nkm3aFY4wNrYQMt4,66284
4
+ huff/ors.py,sha256=2Fz-_gYmCt4U3Ic0MR7ws7XCPVotVJP3Hh1CkXuZL7g,11929
5
+ huff/osm.py,sha256=ARazUc2ciJbXwT6wFiXxR8JaGcYP4cYpb7VXWQR9NRI,6862
6
6
  huff/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- huff/tests/tests_huff.py,sha256=iITEaA96WrewlLyC3l8wWPMwVigAgG9OYsHbix7hI0A,7595
7
+ huff/tests/tests_huff.py,sha256=rXrZ_rQru_JWtBgFPb2KdkS4m9pudgldnQ7r08tTTkw,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.2.dist-info/METADATA,sha256=cf90Dvp0NVO4ZoUx1uBT90ahuAmKKHloGd_z4ne4-I8,4697
22
- huff-1.3.2.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
23
- huff-1.3.2.dist-info/top_level.txt,sha256=nlzX-PxZNFmIxANIJMySuIFPihd6qOBkRlhIC28NEsQ,5
24
- huff-1.3.2.dist-info/RECORD,,
21
+ huff-1.3.4.dist-info/METADATA,sha256=Mq4YbIg7Js2_DK-d9v8nH6Soq_yXXrLmiqZcExfiouo,4558
22
+ huff-1.3.4.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
23
+ huff-1.3.4.dist-info/top_level.txt,sha256=nlzX-PxZNFmIxANIJMySuIFPihd6qOBkRlhIC28NEsQ,5
24
+ huff-1.3.4.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.8.0)
2
+ Generator: bdist_wheel (0.45.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5