huff 1.5.2__py3-none-any.whl → 1.5.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/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.5.2
8
- # Last update: 2025-07-02 21:09
7
+ # Version: 1.5.4
8
+ # Last update: 2025-07-18 18:07
9
9
  # Copyright (c) 2025 Thomas Wieland
10
10
  #-----------------------------------------------------------------------
11
11
 
@@ -105,7 +105,7 @@ class CustomerOrigins:
105
105
  metadata = self.metadata
106
106
 
107
107
  if marketsize_col not in geodata_gpd_original.columns:
108
- raise KeyError ("Column " + marketsize_col + " not in data")
108
+ raise KeyError ("Error while defining market size variable: Column " + marketsize_col + " not in data")
109
109
  else:
110
110
  metadata["marketsize_col"] = marketsize_col
111
111
 
@@ -140,13 +140,13 @@ class CustomerOrigins:
140
140
  metadata = self.metadata
141
141
 
142
142
  if func not in ["power", "exponential", "logistic"]:
143
- raise ValueError("Parameter 'func' must be 'power', 'exponential' or 'logistic'")
143
+ raise ValueError("Error while defining transport costs weighting: Parameter 'func' must be 'power', 'exponential' or 'logistic'")
144
144
 
145
145
  if isinstance(param_lambda, list) and func != "logistic":
146
- raise ValueError("Function type "+ func + " requires one single parameter value")
146
+ raise ValueError("Error while defining transport costs weighting: Function type "+ func + " requires one single parameter value")
147
147
 
148
148
  if isinstance(param_lambda, (int, float)) and func == "logistic":
149
- raise ValueError("Function type "+ func + " requires two parameters in a list")
149
+ raise ValueError("Error while defining transport costs weighting: Function type "+ func + " requires two parameters in a list")
150
150
 
151
151
  metadata["weighting"][0]["name"] = "t_ij"
152
152
  metadata["weighting"][0]["func"] = func
@@ -299,7 +299,7 @@ class SupplyLocations:
299
299
  metadata = self.metadata
300
300
 
301
301
  if attraction_col not in geodata_gpd_original.columns:
302
- raise KeyError ("Column " + attraction_col + " not in data")
302
+ raise KeyError ("Error while defining attraction variable: Column " + attraction_col + " not in data")
303
303
  else:
304
304
  metadata["attraction_col"][0] = attraction_col
305
305
 
@@ -316,7 +316,7 @@ class SupplyLocations:
316
316
  metadata = self.metadata
317
317
 
318
318
  if metadata["attraction_col"] is None:
319
- raise ValueError ("Attraction column is not yet defined. Use SupplyLocations.define_attraction()")
319
+ raise ValueError ("Error while defining attraction weighting: Attraction column is not yet defined. Use SupplyLocations.define_attraction()")
320
320
 
321
321
  metadata["weighting"][0]["name"] = "A_j"
322
322
  metadata["weighting"][0]["func"] = func
@@ -336,7 +336,7 @@ class SupplyLocations:
336
336
  metadata = self.metadata
337
337
 
338
338
  if metadata["attraction_col"] is None:
339
- raise ValueError ("Attraction column is not yet defined. Use SupplyLocations.define_attraction()")
339
+ raise ValueError ("Error while adding utility variable: Attraction column is not yet defined. Use SupplyLocations.define_attraction()")
340
340
 
341
341
  no_attraction_vars = len(metadata["attraction_col"])
342
342
  new_key = no_attraction_vars
@@ -371,9 +371,9 @@ class SupplyLocations:
371
371
  new_destinations_metadata = new_destinations.get_metadata()
372
372
 
373
373
  if list(new_destinations_gpd_original.columns) != list(geodata_gpd_original.columns):
374
- raise KeyError("Supply locations and new destinations data have different column names.")
374
+ raise KeyError("Error while adding new destinations: Supply locations and new destinations data have different column names.")
375
375
  if list(new_destinations_gpd.columns) != list(geodata_gpd.columns):
376
- raise KeyError("Supply locations and new destinations data have different column names.")
376
+ raise KeyError("Error while adding new destinations: Supply locations and new destinations data have different column names.")
377
377
 
378
378
  geodata_gpd_original = pd.concat(
379
379
  [
@@ -644,7 +644,7 @@ class InteractionMatrix:
644
644
  )
645
645
 
646
646
  if time_distance_matrix.get_metadata() is None:
647
- raise ValueError ("No transport costs matrix was built.")
647
+ raise ValueError ("Error in transport costs calculation: No transport costs matrix was built.")
648
648
 
649
649
  transport_costs_matrix = time_distance_matrix.get_matrix()
650
650
  transport_costs_matrix_config = time_distance_matrix.get_config()
@@ -766,25 +766,29 @@ class InteractionMatrix:
766
766
  self.supply_locations.metadata = supply_locations_metadata
767
767
  self.customer_origins.metadata = customer_origins_metadata
768
768
 
769
- def utility(self):
769
+ def utility(
770
+ self,
771
+ check_df_vars: bool = True
772
+ ):
770
773
 
771
774
  interaction_matrix_df = self.interaction_matrix_df
772
775
 
773
776
  interaction_matrix_metadata = self.get_metadata()
774
777
 
775
778
  if "t_ij" not in interaction_matrix_df.columns:
776
- raise ValueError ("No transport cost variable in interaction matrix")
779
+ raise ValueError ("Error in utility calculation: No transport cost variable in interaction matrix")
777
780
  if "A_j" not in interaction_matrix_df.columns:
778
- raise ValueError ("No attraction variable in interaction matrix")
781
+ raise ValueError ("Error in utility calculation: No attraction variable in interaction matrix")
779
782
  if interaction_matrix_df["t_ij"].isna().all():
780
- raise ValueError ("Transport cost variable is not defined")
783
+ raise ValueError ("Error in utility calculation: Transport cost variable is not defined")
781
784
  if interaction_matrix_df["A_j"].isna().all():
782
- raise ValueError ("Attraction variable is not defined")
785
+ raise ValueError ("Error in utility calculation: Attraction variable is not defined")
783
786
 
784
- check_vars(
785
- df = interaction_matrix_df,
786
- cols = ["A_j", "t_ij"]
787
- )
787
+ if check_df_vars:
788
+ check_vars(
789
+ df = interaction_matrix_df,
790
+ cols = ["A_j", "t_ij"]
791
+ )
788
792
 
789
793
  customer_origins = self.customer_origins
790
794
  customer_origins_metadata = customer_origins.get_metadata()
@@ -793,11 +797,11 @@ class InteractionMatrix:
793
797
  if tc_weighting["func"] == "power":
794
798
  interaction_matrix_df["t_ij_weighted"] = interaction_matrix_df["t_ij"] ** tc_weighting["param"]
795
799
  elif tc_weighting["func"] == "exponential":
796
- interaction_matrix_df["t_ij_weighted"] = np.exp(tc_weighting["param"] * interaction_matrix_df['t_ij'])
800
+ interaction_matrix_df["t_ij_weighted"] = np.exp(tc_weighting["param"] * interaction_matrix_df["t_ij"])
797
801
  elif tc_weighting["func"] == "logistic":
798
- interaction_matrix_df["t_ij_weighted"] = 1+np.exp(tc_weighting["param"][0] + tc_weighting["param"][1] * interaction_matrix_df['t_ij'])
802
+ interaction_matrix_df["t_ij_weighted"] = 1+np.exp(tc_weighting["param"][0] + tc_weighting["param"][1] * interaction_matrix_df["t_ij"])
799
803
  else:
800
- raise ValueError ("Transport costs weighting is not defined.")
804
+ raise ValueError ("Error in utility calculation: Transport costs weighting is not defined.")
801
805
 
802
806
  supply_locations = self.supply_locations
803
807
  supply_locations_metadata = supply_locations.get_metadata()
@@ -808,7 +812,7 @@ class InteractionMatrix:
808
812
  elif tc_weighting["func"] == "exponential":
809
813
  interaction_matrix_df["A_j_weighted"] = np.exp(attraction_weighting["param"] * interaction_matrix_df["A_j"])
810
814
  else:
811
- raise ValueError ("Attraction weighting is not defined.")
815
+ raise ValueError ("Error in utility calculation: Attraction weighting is not defined.")
812
816
 
813
817
  attrac_vars = supply_locations_metadata["attraction_col"]
814
818
  attrac_vars_no = len(attrac_vars)
@@ -818,8 +822,8 @@ class InteractionMatrix:
818
822
 
819
823
  for key, attrac_var in enumerate(attrac_vars):
820
824
 
821
- attrac_var_key = key #+1
822
- if attrac_var_key == 0: #1:
825
+ attrac_var_key = key
826
+ if attrac_var_key == 0:
823
827
  continue
824
828
 
825
829
  name = supply_locations_metadata["weighting"][attrac_var_key]["name"]
@@ -831,7 +835,7 @@ class InteractionMatrix:
831
835
  elif func == "exponential":
832
836
  interaction_matrix_df[name+"_weighted"] = np.exp(param * interaction_matrix_df[name])
833
837
  else:
834
- raise ValueError ("Weighting for " + name + " is not defined.")
838
+ raise ValueError ("Error in utility calculation: Weighting for " + name + " is not defined.")
835
839
 
836
840
  interaction_matrix_df["A_j_weighted"] = interaction_matrix_df["A_j_weighted"]*interaction_matrix_df[name+"_weighted"]
837
841
 
@@ -850,7 +854,10 @@ class InteractionMatrix:
850
854
 
851
855
  return self
852
856
 
853
- def probabilities (self):
857
+ def probabilities (
858
+ self,
859
+ check_df_vars: bool = True
860
+ ):
854
861
 
855
862
  interaction_matrix_df = self.interaction_matrix_df
856
863
 
@@ -858,6 +865,12 @@ class InteractionMatrix:
858
865
  self.utility()
859
866
  interaction_matrix_df = self.interaction_matrix_df
860
867
 
868
+ if check_df_vars:
869
+ check_vars(
870
+ df = interaction_matrix_df,
871
+ cols = ["U_ij"]
872
+ )
873
+
861
874
  utility_i = pd.DataFrame(interaction_matrix_df.groupby("i")["U_ij"].sum())
862
875
  utility_i = utility_i.rename(columns = {"U_ij": "U_i"})
863
876
 
@@ -876,19 +889,23 @@ class InteractionMatrix:
876
889
 
877
890
  return self
878
891
 
879
- def flows (self):
892
+ def flows (
893
+ self,
894
+ check_df_vars: bool = True
895
+ ):
880
896
 
881
897
  interaction_matrix_df = self.interaction_matrix_df
882
898
 
883
899
  if "C_i" not in interaction_matrix_df.columns:
884
- raise ValueError ("No market size variable in interaction matrix")
900
+ raise ValueError ("Error in flows calculation: No market size variable in interaction matrix")
885
901
  if interaction_matrix_df["C_i"].isna().all():
886
- raise ValueError ("Market size column in customer origins not defined. Use CustomerOrigins.define_marketsize()")
902
+ raise ValueError ("Error in flows calculation: Market size column in customer origins not defined. Use CustomerOrigins.define_marketsize()")
887
903
 
888
- check_vars(
889
- df = interaction_matrix_df,
890
- cols = ["C_i"]
891
- )
904
+ if check_df_vars:
905
+ check_vars(
906
+ df = interaction_matrix_df,
907
+ cols = ["C_i"]
908
+ )
892
909
 
893
910
  if interaction_matrix_df["p_ij"].isna().all():
894
911
  self.probabilities()
@@ -900,15 +917,19 @@ class InteractionMatrix:
900
917
 
901
918
  return self
902
919
 
903
- def marketareas(self):
920
+ def marketareas(
921
+ self,
922
+ check_df_vars: bool = True
923
+ ):
904
924
 
905
925
  interaction_matrix_df = self.interaction_matrix_df
906
926
 
907
- check_vars(
908
- df = interaction_matrix_df,
909
- cols = ["E_ij"],
910
- check_zero = False
911
- )
927
+ if check_df_vars:
928
+ check_vars(
929
+ df = interaction_matrix_df,
930
+ cols = ["E_ij"],
931
+ check_zero = False
932
+ )
912
933
 
913
934
  market_areas_df = pd.DataFrame(interaction_matrix_df.groupby("j")["E_ij"].sum())
914
935
  market_areas_df = market_areas_df.reset_index(drop=False)
@@ -940,18 +961,18 @@ class InteractionMatrix:
940
961
  else:
941
962
 
942
963
  if "C_i" not in interaction_matrix_df.columns or interaction_matrix_df["C_i"].isna().all():
943
- raise ValueError("Customer origins market size is not available")
964
+ raise ValueError("Error in hansen accessibility calculation: Customer origins market size is not available")
944
965
 
945
966
  customer_origins_metadata = self.customer_origins.get_metadata()
946
967
  tc_weighting = customer_origins_metadata["weighting"][0]
947
968
  if tc_weighting["func"] == "power":
948
969
  interaction_matrix_df["t_ij_weighted"] = interaction_matrix_df["t_ij"] ** tc_weighting["param"]
949
970
  elif tc_weighting["func"] == "exponential":
950
- interaction_matrix_df["t_ij_weighted"] = np.exp(tc_weighting["param"] * interaction_matrix_df['t_ij'])
971
+ interaction_matrix_df["t_ij_weighted"] = np.exp(tc_weighting["param"] * interaction_matrix_df["t_ij"])
951
972
  elif tc_weighting["func"] == "logistic":
952
- interaction_matrix_df["t_ij_weighted"] = 1+np.exp(tc_weighting["param"][0] + tc_weighting["param"][1] * interaction_matrix_df['t_ij'])
973
+ interaction_matrix_df["t_ij_weighted"] = 1+np.exp(tc_weighting["param"][0] + tc_weighting["param"][1] * interaction_matrix_df["t_ij"])
953
974
  else:
954
- raise ValueError ("Transport costs weighting is not defined.")
975
+ raise ValueError ("Error in hansen accessibility calculation: Transport costs weighting is not defined.")
955
976
 
956
977
  interaction_matrix_df["U_ji"] = interaction_matrix_df["C_i"]*interaction_matrix_df["t_ij_weighted"]
957
978
  hansen_df = pd.DataFrame(interaction_matrix_df.groupby("j")["U_ji"].sum()).reset_index()
@@ -1073,20 +1094,21 @@ class InteractionMatrix:
1073
1094
  def loglik(
1074
1095
  self,
1075
1096
  params,
1076
- fit_by = "probabilities"
1097
+ fit_by = "probabilities",
1098
+ check_df_vars: bool = True
1077
1099
  ):
1078
1100
 
1079
1101
  if fit_by not in ["probabilities", "flows"]:
1080
- raise ValueError ("Parameter 'fit_by' must be 'probabilities' or 'flows'")
1102
+ raise ValueError ("Error in loglik: Parameter 'fit_by' must be 'probabilities' or 'flows'")
1081
1103
 
1082
1104
  if not isinstance(params, list):
1083
1105
  if isinstance(params, np.ndarray):
1084
1106
  params = params.tolist()
1085
1107
  else:
1086
- raise ValueError("Parameter 'params' must be a list or np.ndarray with at least 2 parameter values")
1108
+ raise ValueError("Error in loglik: Parameter 'params' must be a list or np.ndarray with at least 2 parameter values")
1087
1109
 
1088
1110
  if len(params) < 2:
1089
- raise ValueError("Parameter 'params' must be a list or np.ndarray with at least 2 parameter values")
1111
+ raise ValueError("Error in loglik: Parameter 'params' must be a list or np.ndarray with at least 2 parameter values")
1090
1112
 
1091
1113
  customer_origins = self.customer_origins
1092
1114
  customer_origins_metadata = customer_origins.get_metadata()
@@ -1096,7 +1118,7 @@ class InteractionMatrix:
1096
1118
  if customer_origins_metadata["weighting"][0]["func"] == "logistic":
1097
1119
 
1098
1120
  if len(params) < 3:
1099
- raise ValueError("When using logistic weighting, parameter 'params' must be a list or np.ndarray with at least 3 parameter values")
1121
+ raise ValueError("Error in loglik: When using logistic weighting, parameter 'params' must be a list or np.ndarray with at least 3 parameter values")
1100
1122
 
1101
1123
  param_gamma, param_lambda, param_lambda2 = params[0], params[1], params[2]
1102
1124
 
@@ -1116,7 +1138,7 @@ class InteractionMatrix:
1116
1138
 
1117
1139
  else:
1118
1140
 
1119
- raise ValueError ("Huff Model with transport cost weighting of type " + customer_origins_metadata["weighting"][0]["func"] + " must have >= 2 input parameters")
1141
+ raise ValueError ("Error in loglik: Huff Model with transport cost weighting of type " + customer_origins_metadata["weighting"][0]["func"] + " must have >= 2 input parameters")
1120
1142
 
1121
1143
  elif customer_origins_metadata["weighting"][0]["func"] == "logistic":
1122
1144
 
@@ -1126,7 +1148,7 @@ class InteractionMatrix:
1126
1148
 
1127
1149
  else:
1128
1150
 
1129
- raise ValueError("Huff Model with transport cost weightig of type " + customer_origins_metadata["weighting"][0]["func"] + " must have >= 3 input parameters")
1151
+ raise ValueError("Error in loglik: Huff Model with transport cost weightig of type " + customer_origins_metadata["weighting"][0]["func"] + " must have >= 3 input parameters")
1130
1152
 
1131
1153
  if (customer_origins_metadata["weighting"][0]["func"] in ["power", "exponential"] and len(params) > 2):
1132
1154
 
@@ -1160,9 +1182,9 @@ class InteractionMatrix:
1160
1182
 
1161
1183
  interaction_matrix_copy = copy.deepcopy(self)
1162
1184
 
1163
- interaction_matrix_copy.utility()
1164
- interaction_matrix_copy.probabilities()
1165
- interaction_matrix_copy.flows()
1185
+ interaction_matrix_copy.utility(check_df_vars = check_df_vars)
1186
+ interaction_matrix_copy.probabilities(check_df_vars = check_df_vars)
1187
+ interaction_matrix_copy.flows(check_df_vars = check_df_vars)
1166
1188
 
1167
1189
  interaction_matrix_df_copy = interaction_matrix_copy.get_interaction_matrix_df()
1168
1190
 
@@ -1196,7 +1218,8 @@ class InteractionMatrix:
1196
1218
  bounds: list = [(0.5, 1), (-3, -1)],
1197
1219
  constraints: list = [],
1198
1220
  fit_by = "probabilities",
1199
- update_estimates: bool = True
1221
+ update_estimates: bool = True,
1222
+ check_df_vars: bool = True
1200
1223
  ):
1201
1224
 
1202
1225
  supply_locations = self.supply_locations
@@ -1224,15 +1247,15 @@ class InteractionMatrix:
1224
1247
  params_metadata = params_metadata_customer_origins+params_metadata_supply_locations
1225
1248
 
1226
1249
  if len(initial_params) < 2 or len(initial_params) != params_metadata:
1227
- raise ValueError("Parameter 'initial_params' must be a list with " + str(params_metadata) + " entries (Attaction: " + str(params_metadata_supply_locations) + ", Transport costs: " + str(params_metadata_customer_origins) + ")")
1250
+ raise ValueError("Error in huff_ml_fit: Parameter 'initial_params' must be a list with " + str(params_metadata) + " entries (Attaction: " + str(params_metadata_supply_locations) + ", Transport costs: " + str(params_metadata_customer_origins) + ")")
1228
1251
 
1229
1252
  if len(bounds) != len(initial_params):
1230
- raise ValueError("Parameter 'bounds' must have the same length as parameter 'initial_params' (" + str(len(bounds)) + ", " + str(len(initial_params)) + ")")
1253
+ raise ValueError("Error in huff_ml_fit: Parameter 'bounds' must have the same length as parameter 'initial_params' (" + str(len(bounds)) + ", " + str(len(initial_params)) + ")")
1231
1254
 
1232
1255
  ml_result = minimize(
1233
1256
  self.loglik,
1234
1257
  initial_params,
1235
- args=fit_by,
1258
+ args=(fit_by, check_df_vars),
1236
1259
  method = method,
1237
1260
  bounds = bounds,
1238
1261
  constraints = constraints,
@@ -1344,7 +1367,7 @@ class InteractionMatrix:
1344
1367
  "update_estimates": update_estimates
1345
1368
  }
1346
1369
 
1347
- return self
1370
+ return self
1348
1371
 
1349
1372
  def update(self):
1350
1373
 
@@ -1360,12 +1383,12 @@ class InteractionMatrix:
1360
1383
  supply_locations_geodata_gpd_new = supply_locations_geodata_gpd[supply_locations_geodata_gpd["j_update"] == 1]
1361
1384
 
1362
1385
  if len(supply_locations_geodata_gpd_new) < 1:
1363
- raise ValueError("There are no new destinations for an interaction matrix update. Use SupplyLocations.add_new_destinations()")
1386
+ raise ValueError("Error in InteractionMatrix update: There are no new destinations for an interaction matrix update. Use SupplyLocations.add_new_destinations()")
1364
1387
 
1365
1388
  supply_locations_geodata_gpd_original = supply_locations.get_geodata_gpd_original().copy()
1366
1389
  supply_locations_geodata_gpd_original_new = supply_locations_geodata_gpd_original[supply_locations_geodata_gpd_original["j_update"] == 1]
1367
1390
  if len(supply_locations_geodata_gpd_original_new) < 1:
1368
- raise ValueError("There are no new destinations for an interaction matrix update. Use SupplyLocations.add_new_destinations()")
1391
+ raise ValueError("Error in InteractionMatrix update: There are no new destinations for an interaction matrix update. Use SupplyLocations.add_new_destinations()")
1369
1392
 
1370
1393
  supply_locations_new = SupplyLocations(
1371
1394
  geodata_gpd=supply_locations_geodata_gpd_new,
@@ -1458,7 +1481,7 @@ class MarketAreas:
1458
1481
  ):
1459
1482
 
1460
1483
  if not isinstance(model_object, (HuffModel, MCIModel, InteractionMatrix)):
1461
- raise ValueError("Parameter 'interaction_matrix' must be of class HuffModel, MCIModel, or InteractionMatrix")
1484
+ raise ValueError("Error while adding MarketAreas to model: Parameter 'interaction_matrix' must be of class HuffModel, MCIModel, or InteractionMatrix")
1462
1485
 
1463
1486
  if isinstance(model_object, MCIModel):
1464
1487
 
@@ -1479,7 +1502,7 @@ class MarketAreas:
1479
1502
  elif isinstance(model_object, InteractionMatrix):
1480
1503
 
1481
1504
  if output_model not in ["Huff", "MCI"]:
1482
- raise ValueError("Parameter 'output_model' must be either 'Huff' or 'MCI'")
1505
+ raise ValueError("Error while adding MarketAreas to model: Parameter 'output_model' must be either 'Huff' or 'MCI'")
1483
1506
 
1484
1507
  if output_model == "Huff":
1485
1508
 
@@ -1597,10 +1620,19 @@ class HuffModel:
1597
1620
  else:
1598
1621
 
1599
1622
  name = supply_locations_metadata["weighting"][key]["name"]
1600
- param = supply_locations_metadata["weighting"][key]["param"]
1601
1623
  func = supply_locations_metadata["weighting"][key]["func"]
1602
1624
 
1603
- print(f"{name[:16]:16} {round(param, 3)} ({func})")
1625
+ if "param" in supply_locations_metadata["weighting"][key]:
1626
+
1627
+ param = supply_locations_metadata["weighting"][key]["param"]
1628
+
1629
+ if param is not None:
1630
+
1631
+ print(f"{name[:16]:16} {round(param, 3)} ({func})")
1632
+
1633
+ else:
1634
+
1635
+ print(f"{attrac_vars[key][:16]:16} NA ({func})")
1604
1636
 
1605
1637
  print("----------------------------------")
1606
1638
 
@@ -1736,17 +1768,18 @@ class HuffModel:
1736
1768
 
1737
1769
  def loglik(
1738
1770
  self,
1739
- params
1771
+ params,
1772
+ check_df_vars: bool = True
1740
1773
  ):
1741
1774
 
1742
1775
  if not isinstance(params, list):
1743
1776
  if isinstance(params, np.ndarray):
1744
1777
  params = params.tolist()
1745
1778
  else:
1746
- raise ValueError("Parameter 'params' must be a list or np.ndarray with at least 2 parameter values")
1779
+ raise ValueError("Error in loglik: Parameter 'params' must be a list or np.ndarray with at least 2 parameter values")
1747
1780
 
1748
1781
  if len(params) < 2:
1749
- raise ValueError("Parameter 'params' must be a list or np.ndarray with at least 2 parameter values")
1782
+ raise ValueError("Error in loglik: Parameter 'params' must be a list or np.ndarray with at least 2 parameter values")
1750
1783
 
1751
1784
  market_areas_df = self.market_areas_df
1752
1785
 
@@ -1758,7 +1791,7 @@ class HuffModel:
1758
1791
  if customer_origins_metadata["weighting"][0]["func"] == "logistic":
1759
1792
 
1760
1793
  if len(params) < 3:
1761
- raise ValueError("When using logistic weighting, parameter 'params' must be a list or np.ndarray with at least 3 parameter values")
1794
+ raise ValueError("Error in loglik: When using logistic weighting, parameter 'params' must be a list or np.ndarray with at least 3 parameter values")
1762
1795
 
1763
1796
  param_gamma, param_lambda, param_lambda2 = params[0], params[1], params[2]
1764
1797
 
@@ -1776,7 +1809,7 @@ class HuffModel:
1776
1809
 
1777
1810
  else:
1778
1811
 
1779
- raise ValueError ("Huff Model with transport cost weighting of type " + customer_origins_metadata["weighting"][0]["func"] + " must have >= 2 input parameters")
1812
+ raise ValueError ("Error in loglik: Huff Model with transport cost weighting of type " + customer_origins_metadata["weighting"][0]["func"] + " must have >= 2 input parameters")
1780
1813
 
1781
1814
  elif customer_origins_metadata["weighting"][0]["func"] == "logistic":
1782
1815
 
@@ -1786,7 +1819,7 @@ class HuffModel:
1786
1819
 
1787
1820
  else:
1788
1821
 
1789
- raise ValueError("Huff Model with transport cost weightig of type " + customer_origins_metadata["weighting"][0]["func"] + " must have >= 3 input parameters")
1822
+ raise ValueError("Error in loglik: Huff Model with transport cost weightig of type " + customer_origins_metadata["weighting"][0]["func"] + " must have >= 3 input parameters")
1790
1823
 
1791
1824
  if (customer_origins_metadata["weighting"][0]["func"] in ["power", "exponential"] and len(params) > 2):
1792
1825
 
@@ -1818,9 +1851,9 @@ class HuffModel:
1818
1851
 
1819
1852
  interaction_matrix_copy = copy.deepcopy(huff_model_copy.interaction_matrix)
1820
1853
 
1821
- interaction_matrix_copy = interaction_matrix_copy.utility()
1822
- interaction_matrix_copy = interaction_matrix_copy.probabilities()
1823
- interaction_matrix_copy = interaction_matrix_copy.flows()
1854
+ interaction_matrix_copy = interaction_matrix_copy.utility(check_df_vars = check_df_vars)
1855
+ interaction_matrix_copy = interaction_matrix_copy.probabilities(check_df_vars = check_df_vars)
1856
+ interaction_matrix_copy = interaction_matrix_copy.flows(check_df_vars = check_df_vars)
1824
1857
 
1825
1858
  huff_model_copy = interaction_matrix_copy.marketareas()
1826
1859
 
@@ -1846,7 +1879,8 @@ class HuffModel:
1846
1879
  constraints: list = [],
1847
1880
  fit_by = "probabilities",
1848
1881
  update_estimates: bool = True,
1849
- check_numbers: bool = True
1882
+ check_numbers: bool = True,
1883
+ check_df_vars: bool = True
1850
1884
  ):
1851
1885
 
1852
1886
  if fit_by in ["probabilities", "flows"]:
@@ -1857,7 +1891,8 @@ class HuffModel:
1857
1891
  bounds = bounds,
1858
1892
  constraints = constraints,
1859
1893
  fit_by = fit_by,
1860
- update_estimates = update_estimates
1894
+ update_estimates = update_estimates,
1895
+ check_df_vars = check_df_vars
1861
1896
  )
1862
1897
 
1863
1898
  elif fit_by == "totals":
@@ -1897,14 +1932,15 @@ class HuffModel:
1897
1932
  params_metadata = params_metadata_customer_origins+params_metadata_supply_locations
1898
1933
 
1899
1934
  if len(initial_params) < 2 or len(initial_params) != params_metadata:
1900
- raise ValueError("Parameter 'initial_params' must be a list with " + str(params_metadata) + " entries (Attaction: " + str(params_metadata_supply_locations) + ", Transport costs: " + str(params_metadata_customer_origins) + ")")
1935
+ raise ValueError("Error in ml_fit: Parameter 'initial_params' must be a list with " + str(params_metadata) + " entries (Attaction: " + str(params_metadata_supply_locations) + ", Transport costs: " + str(params_metadata_customer_origins) + ")")
1901
1936
 
1902
1937
  if len(bounds) != len(initial_params):
1903
- raise ValueError("Parameter 'bounds' must have the same length as parameter 'initial_params' (" + str(len(bounds)) + ", " + str(len(initial_params)) + ")")
1938
+ raise ValueError("Error in ml_fit: Parameter 'bounds' must have the same length as parameter 'initial_params' (" + str(len(bounds)) + ", " + str(len(initial_params)) + ")")
1904
1939
 
1905
1940
  ml_result = minimize(
1906
1941
  self.loglik,
1907
1942
  initial_params,
1943
+ args=check_df_vars,
1908
1944
  method = method,
1909
1945
  bounds = bounds,
1910
1946
  constraints = constraints,
@@ -1990,11 +2026,11 @@ class HuffModel:
1990
2026
 
1991
2027
  else:
1992
2028
 
1993
- self.interaction_matrix.utility()
1994
- self.interaction_matrix.probabilities()
1995
- self.interaction_matrix.flows()
2029
+ self.interaction_matrix.utility(check_df_vars = check_df_vars)
2030
+ self.interaction_matrix.probabilities(check_df_vars = check_df_vars)
2031
+ self.interaction_matrix.flows(check_df_vars = check_df_vars)
1996
2032
 
1997
- huff_model_new_marketareas = self.interaction_matrix.marketareas()
2033
+ huff_model_new_marketareas = self.interaction_matrix.marketareas(check_df_vars = check_df_vars)
1998
2034
  self.market_areas_df["T_j"] = huff_model_new_marketareas.get_market_areas_df()["T_j"]
1999
2035
 
2000
2036
  self.interaction_matrix.metadata["fit"] = {
@@ -2011,9 +2047,116 @@ class HuffModel:
2011
2047
 
2012
2048
  else:
2013
2049
 
2014
- raise ValueError("Parameter 'fit_by' must be 'probabilities', 'flows' or 'totals'")
2050
+ raise ValueError("Error in ml_fit: Parameter 'fit_by' must be 'probabilities', 'flows' or 'totals'")
2015
2051
 
2016
2052
  return self
2053
+
2054
+ def confint(
2055
+ self,
2056
+ alpha = 0.05,
2057
+ repeats = 3,
2058
+ sample_size = 0.75,
2059
+ replace = True
2060
+ ):
2061
+
2062
+ if self.interaction_matrix.metadata["fit"] is None or self.interaction_matrix.metadata["fit"] == {}:
2063
+ raise ValueError("Error while estimating confidence intervals: Model object does not contain information towards fit procedure")
2064
+
2065
+ keys_necessary = [
2066
+ "function",
2067
+ "fit_by",
2068
+ "initial_params",
2069
+ "method",
2070
+ "bounds",
2071
+ "constraints"
2072
+ ]
2073
+
2074
+ for key_necessary in keys_necessary:
2075
+ if key_necessary not in self.interaction_matrix.metadata["fit"]:
2076
+ raise KeyError(f"Error while estimating confidence intervals: Model object does not contain full information towards fit procedure. Missing key {key_necessary}")
2077
+
2078
+ fitted_params_repeats = []
2079
+
2080
+ alpha_lower = alpha/2
2081
+ alpha_upper = 1-alpha/2
2082
+
2083
+ huff_model_copy = copy.deepcopy(self)
2084
+
2085
+ if self.interaction_matrix.metadata["fit"]["fit_by"] in ["probabilities", "flows"]:
2086
+
2087
+ for i in range(repeats):
2088
+
2089
+ try:
2090
+
2091
+ n_samples = int(len(huff_model_copy.interaction_matrix.interaction_matrix_df)*sample_size)
2092
+
2093
+ huff_model_copy.interaction_matrix.interaction_matrix_df = huff_model_copy.interaction_matrix.interaction_matrix_df.sample(
2094
+ n = n_samples,
2095
+ replace = replace
2096
+ )
2097
+
2098
+ huff_model_copy.ml_fit(
2099
+ initial_params = self.interaction_matrix.metadata["fit"]["initial_params"],
2100
+ method = self.interaction_matrix.metadata["fit"]["method"],
2101
+ bounds = self.interaction_matrix.metadata["fit"]["bounds"],
2102
+ constraints = self.interaction_matrix.metadata["fit"]["constraints"],
2103
+ fit_by = self.interaction_matrix.metadata["fit"]["fit_by"],
2104
+ update_estimates = True,
2105
+ check_numbers = True
2106
+ )
2107
+
2108
+ minimize_fittedparams = huff_model_copy.interaction_matrix.metadata["fit"]["minimize_fittedparams"]
2109
+
2110
+ fitted_params_repeats.append(minimize_fittedparams)
2111
+
2112
+ except Exception as err:
2113
+
2114
+ print (f"Error in repeat {str(i)}: {err}")
2115
+
2116
+ elif self.metadata["fit"]["fit_by"] == "totals":
2117
+
2118
+ for i in range(repeats):
2119
+
2120
+ n_samples = int(len(huff_model_copy.market_areas_df)*sample_size)
2121
+
2122
+ huff_model_copy.market_areas_df = huff_model_copy.market_areas_df.sample(
2123
+ n = n_samples,
2124
+ replace = replace
2125
+ )
2126
+
2127
+ huff_model_copy.interaction_matrix.interaction_matrix_df = huff_model_copy.interaction_matrix.interaction_matrix_df[
2128
+ huff_model_copy.interaction_matrix.interaction_matrix_df["j"].isin(huff_model_copy.market_areas_df["j"])
2129
+ ]
2130
+
2131
+ huff_model_copy.ml_fit(
2132
+ initial_params = self.interaction_matrix.metadata["fit"]["initial_params"],
2133
+ method = self.interaction_matrix.metadata["fit"]["method"],
2134
+ bounds = self.interaction_matrix.metadata["fit"]["bounds"],
2135
+ constraints = self.interaction_matrix.metadata["fit"]["constraints"],
2136
+ fit_by = self.interaction_matrix.metadata["fit"]["fit_by"],
2137
+ update_estimates = True,
2138
+ check_numbers = True
2139
+ )
2140
+
2141
+ minimize_fittedparams = huff_model_copy.interaction_matrix.metadata["fit"]["minimize_fittedparams"]
2142
+
2143
+ fitted_params_repeats.append(minimize_fittedparams)
2144
+
2145
+ else:
2146
+
2147
+ raise ValueError("Error while estimating confidence intervals: Parameter 'fit_by' must be 'probabilities', 'flows' or 'totals'")
2148
+
2149
+ fitted_params_repeats_array = np.array(fitted_params_repeats)
2150
+ fitted_params_repeats_array_transposed = fitted_params_repeats_array.T
2151
+
2152
+ param_ci = pd.DataFrame(columns=["lower", "upper"])
2153
+
2154
+ for i, col in enumerate(fitted_params_repeats_array_transposed):
2155
+
2156
+ param_ci.loc[i, "lower"] = np.quantile(col, alpha_lower)
2157
+ param_ci.loc[i, "upper"] = np.quantile(col, alpha_upper)
2158
+
2159
+ return param_ci
2017
2160
 
2018
2161
  def update(self):
2019
2162
 
@@ -2110,7 +2253,7 @@ class HuffModel:
2110
2253
 
2111
2254
  else:
2112
2255
 
2113
- raise ValueError("Parameter 'by' must be 'probabilities', 'flows', or 'totals'")
2256
+ raise ValueError("Error in HuffModel.modelfit: Parameter 'by' must be 'probabilities', 'flows', or 'totals'")
2114
2257
 
2115
2258
  class MCIModel:
2116
2259
 
@@ -2256,7 +2399,8 @@ class MCIModel:
2256
2399
 
2257
2400
  def utility(
2258
2401
  self,
2259
- transformation = "LCT"
2402
+ transformation = "LCT",
2403
+ check_df_vars: bool = True
2260
2404
  ):
2261
2405
 
2262
2406
  interaction_matrix = self.interaction_matrix
@@ -2264,14 +2408,15 @@ class MCIModel:
2264
2408
  interaction_matrix_metadata = interaction_matrix.get_metadata()
2265
2409
 
2266
2410
  if interaction_matrix_df["t_ij"].isna().all():
2267
- raise ValueError ("Transport cost variable is not defined")
2411
+ raise ValueError ("Error in utility calculation: Transport cost variable is not defined")
2268
2412
  if interaction_matrix_df["A_j"].isna().all():
2269
- raise ValueError ("Attraction variable is not defined")
2413
+ raise ValueError ("Error in utility calculation: Attraction variable is not defined")
2270
2414
 
2271
- check_vars(
2272
- df = interaction_matrix_df,
2273
- cols = ["A_j", "t_ij"]
2274
- )
2415
+ if check_df_vars:
2416
+ check_vars(
2417
+ df = interaction_matrix_df,
2418
+ cols = ["A_j", "t_ij"]
2419
+ )
2275
2420
 
2276
2421
  customer_origins = interaction_matrix.get_customer_origins()
2277
2422
  customer_origins_metadata = customer_origins.get_metadata()
@@ -2360,22 +2505,24 @@ class MCIModel:
2360
2505
 
2361
2506
  def flows (
2362
2507
  self,
2363
- transformation = "LCT"
2508
+ transformation = "LCT",
2509
+ check_df_vars: bool = True
2364
2510
  ):
2365
2511
 
2366
2512
  interaction_matrix = self.interaction_matrix
2367
2513
  interaction_matrix_df = interaction_matrix.get_interaction_matrix_df()
2368
2514
 
2369
2515
  if "C_i" not in interaction_matrix_df.columns:
2370
- raise ValueError ("No market size column defined in interaction matrix.")
2516
+ raise ValueError ("Error in flows calculation: No market size column defined in interaction matrix.")
2371
2517
 
2372
2518
  if interaction_matrix_df["C_i"].isna().all():
2373
- raise ValueError ("Market size column in customer origins not defined. Use CustomerOrigins.define_marketsize()")
2519
+ raise ValueError ("Error in flows calculation: Market size column in customer origins not defined. Use CustomerOrigins.define_marketsize()")
2374
2520
 
2375
- check_vars(
2376
- df = interaction_matrix_df,
2377
- cols = ["C_i"]
2378
- )
2521
+ if check_df_vars:
2522
+ check_vars(
2523
+ df = interaction_matrix_df,
2524
+ cols = ["C_i"]
2525
+ )
2379
2526
 
2380
2527
  if interaction_matrix_df["p_ij"].isna().all():
2381
2528
  self.probabilities(transformation=transformation)
@@ -2388,15 +2535,19 @@ class MCIModel:
2388
2535
 
2389
2536
  return self
2390
2537
 
2391
- def marketareas (self):
2538
+ def marketareas (
2539
+ self,
2540
+ check_df_vars: bool = True
2541
+ ):
2392
2542
 
2393
2543
  interaction_matrix = self.interaction_matrix
2394
2544
  interaction_matrix_df = interaction_matrix.get_interaction_matrix_df()
2395
2545
 
2396
- check_vars(
2397
- df = interaction_matrix_df,
2398
- cols = ["E_ij"]
2399
- )
2546
+ if check_df_vars:
2547
+ check_vars(
2548
+ df = interaction_matrix_df,
2549
+ cols = ["E_ij"]
2550
+ )
2400
2551
 
2401
2552
  market_areas_df = pd.DataFrame(interaction_matrix_df.groupby("j")["E_ij"].sum())
2402
2553
  market_areas_df = market_areas_df.reset_index(drop=False)
@@ -2425,12 +2576,12 @@ def load_geodata (
2425
2576
  ):
2426
2577
 
2427
2578
  if location_type is None or (location_type != "origins" and location_type != "destinations"):
2428
- raise ValueError ("Argument location_type must be either 'origins' or 'destinations'")
2579
+ raise ValueError ("Error while loading geodata: Argument location_type must be either 'origins' or 'destinations'")
2429
2580
 
2430
2581
  if isinstance(data, gp.GeoDataFrame):
2431
2582
  geodata_gpd_original = data
2432
2583
  if not all(geodata_gpd_original.geometry.geom_type == "Point"):
2433
- raise ValueError ("Input geopandas.GeoDataFrame must be of type 'Point'")
2584
+ raise ValueError ("Error while loading geodata: Input geopandas.GeoDataFrame must be of type 'Point'")
2434
2585
  crs_input = geodata_gpd_original.crs
2435
2586
  elif isinstance(data, pd.DataFrame):
2436
2587
  geodata_tab = data
@@ -2438,13 +2589,13 @@ def load_geodata (
2438
2589
  if data_type == "shp":
2439
2590
  geodata_gpd_original = gp.read_file(data)
2440
2591
  if not all(geodata_gpd_original.geometry.geom_type == "Point"):
2441
- raise ValueError ("Input shapefile must be of type 'Point'")
2592
+ raise ValueError ("Error while loading geodata: Input shapefile must be of type 'Point'")
2442
2593
  crs_input = geodata_gpd_original.crs
2443
2594
  elif data_type == "csv" or data_type == "xlsx":
2444
2595
  if x_col is None:
2445
- raise ValueError ("Missing value for X coordinate column")
2596
+ raise ValueError ("Error while loading geodata: Missing value for X coordinate column")
2446
2597
  if y_col is None:
2447
- raise ValueError ("Missing value for Y coordinate column")
2598
+ raise ValueError ("Error while loading geodata: Missing value for Y coordinate column")
2448
2599
  elif data_type == "csv":
2449
2600
  geodata_tab = pd.read_csv(
2450
2601
  data,
@@ -2455,9 +2606,9 @@ def load_geodata (
2455
2606
  elif data_type == "xlsx":
2456
2607
  geodata_tab = pd.read_excel(data)
2457
2608
  else:
2458
- raise TypeError("Unknown type of data")
2609
+ raise TypeError("Error while loading geodata: Unknown type of data")
2459
2610
  else:
2460
- raise TypeError("data must be pandas.DataFrame, geopandas.GeoDataFrame or file (.csv, .xlsx, .shp)")
2611
+ raise TypeError("Error while loading geodata: Param 'data' must be pandas.DataFrame, geopandas.GeoDataFrame or file (.csv, .xlsx, .shp)")
2461
2612
 
2462
2613
  if data_type == "csv" or data_type == "xlsx" or (isinstance(data, pd.DataFrame) and not isinstance(data, gp.GeoDataFrame)):
2463
2614
 
@@ -2528,17 +2679,17 @@ def create_interaction_matrix(
2528
2679
  ):
2529
2680
 
2530
2681
  if not isinstance(customer_origins, CustomerOrigins):
2531
- raise ValueError ("customer_origins must be of class CustomerOrigins")
2682
+ raise ValueError ("Error while creating interaction matrix: customer_origins must be of class CustomerOrigins")
2532
2683
  if not isinstance(supply_locations, SupplyLocations):
2533
- raise ValueError ("supply_locations must be of class SupplyLocations")
2684
+ raise ValueError ("Error while creating interaction matrix: supply_locations must be of class SupplyLocations")
2534
2685
 
2535
2686
  customer_origins_metadata = customer_origins.get_metadata()
2536
2687
  if customer_origins_metadata["marketsize_col"] is None:
2537
- raise ValueError("Market size column in customer origins not defined. Use CustomerOrigins.define_marketsize()")
2688
+ raise ValueError("Error while creating interaction matrix: Market size column in customer origins not defined. Use CustomerOrigins.define_marketsize()")
2538
2689
 
2539
2690
  supply_locations_metadata = supply_locations.get_metadata()
2540
2691
  if supply_locations_metadata["attraction_col"][0] is None:
2541
- raise ValueError("Attraction column in supply locations not defined. Use SupplyLocations.define_attraction()")
2692
+ raise ValueError("Error while creating interaction matrix: Attraction column in supply locations not defined. Use SupplyLocations.define_attraction()")
2542
2693
 
2543
2694
  customer_origins_unique_id = customer_origins_metadata["unique_id"]
2544
2695
  customer_origins_marketsize = customer_origins_metadata["marketsize_col"]
@@ -2619,14 +2770,14 @@ def load_interaction_matrix(
2619
2770
  xlsx_sheet: str = None,
2620
2771
  crs_input = "EPSG:4326",
2621
2772
  crs_output = "EPSG:4326",
2622
- check_df_vars = True
2773
+ check_df_vars: bool = True
2623
2774
  ):
2624
2775
 
2625
2776
  if isinstance(data, pd.DataFrame):
2626
2777
  interaction_matrix_df = data
2627
2778
  elif isinstance(data, str):
2628
2779
  if data_type not in ["csv", "xlsx"]:
2629
- raise ValueError ("data_type must be 'csv' or 'xlsx'")
2780
+ raise ValueError ("Error while loading interaction matrix: param 'data_type' must be 'csv' or 'xlsx'")
2630
2781
  if data_type == "csv":
2631
2782
  interaction_matrix_df = pd.read_csv(
2632
2783
  data,
@@ -2643,14 +2794,14 @@ def load_interaction_matrix(
2643
2794
  else:
2644
2795
  interaction_matrix_df = pd.read_excel(data)
2645
2796
  else:
2646
- raise TypeError("Unknown type of data")
2797
+ raise TypeError("Error while loading interaction matrix: Unknown type of data")
2647
2798
  else:
2648
- raise TypeError("data must be pandas.DataFrame or file (.csv, .xlsx)")
2799
+ raise TypeError("Error while loading interaction matrix: param 'data' must be pandas.DataFrame or file (.csv, .xlsx)")
2649
2800
 
2650
2801
  if customer_origins_col not in interaction_matrix_df.columns:
2651
- raise KeyError ("Column " + customer_origins_col + " not in data")
2802
+ raise KeyError ("Error while loading interaction matrix: Column " + customer_origins_col + " not in data")
2652
2803
  if supply_locations_col not in interaction_matrix_df.columns:
2653
- raise KeyError ("Column " + supply_locations_col + " not in data")
2804
+ raise KeyError ("Error while loading interaction matrix: Column " + supply_locations_col + " not in data")
2654
2805
 
2655
2806
  cols_check = attraction_col + [transport_costs_col]
2656
2807
  if flows_col is not None:
@@ -2671,7 +2822,7 @@ def load_interaction_matrix(
2671
2822
  if isinstance(customer_origins_coords_col, str):
2672
2823
 
2673
2824
  if customer_origins_coords_col not in interaction_matrix_df.columns:
2674
- raise KeyError ("Column " + customer_origins_coords_col + " not in data.")
2825
+ raise KeyError ("Error while loading interaction matrix: Column " + customer_origins_coords_col + " not in data.")
2675
2826
 
2676
2827
  customer_origins_geodata_tab = interaction_matrix_df[[customer_origins_col, customer_origins_coords_col]]
2677
2828
  customer_origins_geodata_tab = customer_origins_geodata_tab.drop_duplicates()
@@ -2687,7 +2838,7 @@ def load_interaction_matrix(
2687
2838
  elif isinstance(customer_origins_coords_col, list):
2688
2839
 
2689
2840
  if len(customer_origins_coords_col) != 2:
2690
- raise ValueError ("Column " + customer_origins_coords_col + " must be a geometry column OR TWO columns with X and Y")
2841
+ raise ValueError ("Error while loading interaction matrix: Column " + customer_origins_coords_col + " must be a geometry column OR TWO columns with X and Y")
2691
2842
 
2692
2843
  check_vars (
2693
2844
  df = interaction_matrix_df,
@@ -2742,7 +2893,7 @@ def load_interaction_matrix(
2742
2893
  if isinstance(supply_locations_coords_col, str):
2743
2894
 
2744
2895
  if supply_locations_coords_col not in interaction_matrix_df.columns:
2745
- raise KeyError ("Column " + supply_locations_coords_col + " not in data.")
2896
+ raise KeyError ("Error while loading interaction matrix: Column " + supply_locations_coords_col + " not in data.")
2746
2897
 
2747
2898
  supply_locations_geodata_tab = interaction_matrix_df[[supply_locations_col, supply_locations_coords_col]]
2748
2899
  supply_locations_geodata_tab = supply_locations_geodata_tab.drop_duplicates()
@@ -2758,7 +2909,7 @@ def load_interaction_matrix(
2758
2909
  if isinstance(supply_locations_coords_col, list):
2759
2910
 
2760
2911
  if len(supply_locations_coords_col) != 2:
2761
- raise ValueError ("Column " + supply_locations_coords_col + " must be a geometry column OR TWO columns with X and Y")
2912
+ raise ValueError ("Error while loading interaction matrix: Column " + supply_locations_coords_col + " must be a geometry column OR TWO columns with X and Y")
2762
2913
 
2763
2914
  check_vars (
2764
2915
  df = interaction_matrix_df,
@@ -2860,14 +3011,14 @@ def load_marketareas(
2860
3011
  csv_decimal = ",",
2861
3012
  csv_encoding="unicode_escape",
2862
3013
  xlsx_sheet: str = None,
2863
- check_df_vars = True
3014
+ check_df_vars: bool = True
2864
3015
  ):
2865
3016
 
2866
3017
  if isinstance(data, pd.DataFrame):
2867
3018
  market_areas_df = data
2868
3019
  elif isinstance(data, str):
2869
3020
  if data_type not in ["csv", "xlsx"]:
2870
- raise ValueError ("data_type must be 'csv' or 'xlsx'")
3021
+ raise ValueError ("Error while loading market areas: data_type must be 'csv' or 'xlsx'")
2871
3022
  if data_type == "csv":
2872
3023
  market_areas_df = pd.read_csv(
2873
3024
  data,
@@ -2884,14 +3035,14 @@ def load_marketareas(
2884
3035
  else:
2885
3036
  market_areas_df = pd.read_excel(data)
2886
3037
  else:
2887
- raise TypeError("Unknown type of data")
3038
+ raise TypeError("Error while loading market areas: Unknown type of data")
2888
3039
  else:
2889
- raise TypeError("data must be pandas.DataFrame or file (.csv, .xlsx)")
3040
+ raise TypeError("Error while loading market areas: data must be pandas.DataFrame or file (.csv, .xlsx)")
2890
3041
 
2891
3042
  if supply_locations_col not in market_areas_df.columns:
2892
- raise KeyError ("Column " + supply_locations_col + " not in data")
3043
+ raise KeyError ("Error while loading market areas: Column " + supply_locations_col + " not in data")
2893
3044
  if total_col not in market_areas_df.columns:
2894
- raise KeyError ("Column " + supply_locations_col + " not in data")
3045
+ raise KeyError ("Error while loading market areas: Column " + supply_locations_col + " not in data")
2895
3046
 
2896
3047
  if check_df_vars:
2897
3048
  check_vars(
@@ -2934,7 +3085,7 @@ def market_shares(
2934
3085
  if ref_col is not None:
2935
3086
 
2936
3087
  if ref_col not in df.columns:
2937
- raise KeyError(f"Column '{ref_col}' not in dataframe.")
3088
+ raise KeyError(f"Error while calculating market shares: Column '{ref_col}' not in dataframe.")
2938
3089
 
2939
3090
  ms_refcol = pd.DataFrame(df.groupby(ref_col)[turnover_col].sum())
2940
3091
  ms_refcol = ms_refcol.rename(columns = {turnover_col: "total"})
@@ -2982,7 +3133,7 @@ def log_centering_transformation(
2982
3133
  )
2983
3134
 
2984
3135
  if ref_col not in df.columns:
2985
- raise KeyError(f"Column '{ref_col}' not in dataframe.")
3136
+ raise KeyError(f"Error in log-centering transformation: Column '{ref_col}' not in dataframe.")
2986
3137
 
2987
3138
  def lct (x):
2988
3139
 
@@ -3110,17 +3261,20 @@ def modelfit(
3110
3261
  expected_no = len(expected)
3111
3262
 
3112
3263
  if not observed_no == expected_no:
3113
- raise ValueError("Observed and expected differ in length")
3264
+ raise ValueError("Error while calculating fit metrics: Observed and expected differ in length")
3114
3265
 
3115
3266
  if not isinstance(observed, np.number):
3116
3267
  if not is_numeric_dtype(observed):
3117
- raise ValueError("Observed column is not numeric")
3268
+ raise ValueError("Error while calculating fit metrics: Observed column is not numeric")
3118
3269
  if not isinstance(expected, np.number):
3119
3270
  if not is_numeric_dtype(expected):
3120
- raise ValueError("Expected column is not numeric")
3271
+ raise ValueError("Error while calculating fit metrics: Expected column is not numeric")
3121
3272
 
3122
3273
  if remove_nan:
3123
3274
 
3275
+ observed = observed.reset_index(drop=True)
3276
+ expected = expected.reset_index(drop=True)
3277
+
3124
3278
  obs_exp = pd.DataFrame(
3125
3279
  {
3126
3280
  "observed": observed,
@@ -3140,9 +3294,9 @@ def modelfit(
3140
3294
  else:
3141
3295
 
3142
3296
  if np.isnan(observed).any():
3143
- raise ValueError("Vector with observed data contains NaN")
3297
+ raise ValueError("Error while calculating fit metrics: Vector with observed data contains NaN and 'remove_nan' is False")
3144
3298
  if np.isnan(expected).any():
3145
- raise ValueError("Vector with expected data contains NaN")
3299
+ raise ValueError("Error while calculating fit metrics: Vector with expected data contains NaN and 'remove_nan' is False")
3146
3300
 
3147
3301
  residuals = np.array(observed)-np.array(expected)
3148
3302
  residuals_sq = residuals**2
huff/tests/tests_huff.py CHANGED
@@ -4,12 +4,13 @@
4
4
  # Author: Thomas Wieland
5
5
  # ORCID: 0000-0001-5168-9846
6
6
  # mail: geowieland@googlemail.com
7
- # Version: 1.5.2
8
- # Last update: 2025-07-02 21:10
7
+ # Version: 1.5.4
8
+ # Last update: 2025-07-18 18:06
9
9
  # Copyright (c) 2025 Thomas Wieland
10
10
  #-----------------------------------------------------------------------
11
11
 
12
12
  from huff.models import create_interaction_matrix, get_isochrones, load_geodata, load_interaction_matrix, load_marketareas, market_shares, modelfit
13
+ from huff.models import HuffModel
13
14
  from huff.osm import map_with_basemap
14
15
  from huff.gistools import buffers, point_spatial_join
15
16
 
@@ -140,6 +141,10 @@ huff_model_fit = haslach_interactionmatrix.marketareas()
140
141
  # Calculcation of total market areas
141
142
  # Result of class HuffModel
142
143
 
144
+ bootstrap_cis = huff_model_fit.confint(repeats=10)
145
+ print(bootstrap_cis)
146
+ # Confidence intervals for estimated parameters
147
+
143
148
  huff_model_fit.summary()
144
149
  # Huff model summary
145
150
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: huff
3
- Version: 1.5.2
3
+ Version: 1.5.4
4
4
  Summary: huff: Huff Model Market Area Analysis
5
5
  Author: Thomas Wieland
6
6
  Author-email: geowieland@googlemail.com
@@ -18,7 +18,7 @@ Requires-Dist: openpyxl
18
18
 
19
19
  # huff: Huff Model Market Area Analysis
20
20
 
21
- This Python library is designed for performing market area analyses with the Huff Model (Huff 1962, 1964) and/or the Multiplicative Competitive Interaction (MCI) Model (Nakanishi and Cooper 1974, 1982). Users may load point shapefiles (or CSV, XLSX) of customer origins and supply locations and conduct a market area analysis step by step. The package also includes supplementary GIS functions, including clients for OpenRouteService(1) for network analysis (e.g., transport cost matrix) and OpenStreetMap(2) for simple maps. See Huff and McCallum (2008) or Wieland (2017) for a description of the models and their practical application.
21
+ This Python library is designed for performing market area analyses with the Huff Model (Huff 1962, 1964) and/or the Multiplicative Competitive Interaction (MCI) Model (Nakanishi and Cooper 1974, 1982). Users may load point shapefiles (or CSV, XLSX) of customer origins and supply locations and conduct a market area analysis step by step. The library supports parameter estimation based on empirical customer data using the MCI model and Maximum Likelihood. The package also includes supplementary GIS functions, including clients for OpenRouteService(1) for network analysis (e.g., transport cost matrix) and OpenStreetMap(2) for simple maps. See Huff and McCallum (2008) or Wieland (2017) for a description of the models and their practical application.
22
22
 
23
23
 
24
24
  ## Author
@@ -28,10 +28,10 @@ Thomas Wieland [ORCID](https://orcid.org/0000-0001-5168-9846) [EMail](mailto:geo
28
28
  See the /tests directory for usage examples of most of the included functions.
29
29
 
30
30
 
31
- ## Updates v1.5.2
31
+ ## Updates v1.5.4
32
32
  - Bugfixes:
33
- - HuffModel.ml_fit(): Correct values of expected T_j, corrected calculation of model fit metrices when fit_by="totals"
34
- - HuffModel.ml_fit(): Check if sum of E_ij != sum of T_j
33
+ - Use of check_vars() is now optional (default: True)
34
+ - Correction of args argument when calling minimize() in huff_ml_fit()
35
35
 
36
36
 
37
37
  ## Features
@@ -1,10 +1,10 @@
1
1
  huff/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  huff/gistools.py,sha256=fgeE1IsUO7UIaawb23kuiz_Rlxn7T18iLLTA5yvgp74,7038
3
- huff/models.py,sha256=e8aILi45qcJ9tvHJfKIFKWfD-DYXjZQ0gXOS4MpG7Ks,125430
3
+ huff/models.py,sha256=2s-NQoPuP8q7iwJK3tA6S9-qDGMgRGah9vgL4aJerrU,134607
4
4
  huff/ors.py,sha256=JlO2UEishQX87PIiktksOrVT5QdB-GEWgjXcxoR_KuA,11929
5
5
  huff/osm.py,sha256=9A-7hxeZyjA2r8w2_IqqwH14qq2Y9AS1GxVKOD7utqs,7747
6
6
  huff/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- huff/tests/tests_huff.py,sha256=xHCR087rqLNWDFfZhi1giKDzffCx3IemWQmHrAUYxFw,12956
7
+ huff/tests/tests_huff.py,sha256=np0YvzmGYqVFCPFBsdFuUWtGlwX6HKUUlWkYlfWzGPY,13116
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
@@ -24,7 +24,7 @@ huff/tests/data/Haslach_supermarkets.qmd,sha256=JlcOYzG4vI1NH1IuOpxwIPnJsCyC-pDR
24
24
  huff/tests/data/Haslach_supermarkets.shp,sha256=X7QbQ0BTMag_B-bDRbpr-go2BQIXo3Y8zMAKpYZmlps,324
25
25
  huff/tests/data/Haslach_supermarkets.shx,sha256=j23QHX-SmdAeN04rw0x8nUOran-OCg_T6r_LvzzEPWs,164
26
26
  huff/tests/data/Wieland2015.xlsx,sha256=H4rxCFlctn44-O6mIyeFf67FlgvznLX7xZqpoWYS41A,25788
27
- huff-1.5.2.dist-info/METADATA,sha256=XnlmcfscK8c1P3EN40W8JcQnFE7AkWDT4NqLR9skTIY,5956
28
- huff-1.5.2.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
29
- huff-1.5.2.dist-info/top_level.txt,sha256=nlzX-PxZNFmIxANIJMySuIFPihd6qOBkRlhIC28NEsQ,5
30
- huff-1.5.2.dist-info/RECORD,,
27
+ huff-1.5.4.dist-info/METADATA,sha256=Dq-uRGKbKUqNSvB9xUGbox7tRLNB0PDL7QBA_NLfAds,6025
28
+ huff-1.5.4.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
29
+ huff-1.5.4.dist-info/top_level.txt,sha256=nlzX-PxZNFmIxANIJMySuIFPihd6qOBkRlhIC28NEsQ,5
30
+ huff-1.5.4.dist-info/RECORD,,
File without changes