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.
|
8
|
-
# Last update: 2025-07-
|
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(
|
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
|
-
|
785
|
-
|
786
|
-
|
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[
|
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[
|
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
|
822
|
-
if attrac_var_key == 0:
|
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 (
|
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 (
|
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
|
-
|
889
|
-
|
890
|
-
|
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(
|
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
|
-
|
908
|
-
|
909
|
-
|
910
|
-
|
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[
|
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[
|
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
|
-
|
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
|
-
|
2272
|
-
|
2273
|
-
|
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
|
-
|
2376
|
-
|
2377
|
-
|
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 (
|
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
|
-
|
2397
|
-
|
2398
|
-
|
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.
|
8
|
-
# Last update: 2025-07-
|
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.
|
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.
|
31
|
+
## Updates v1.5.4
|
32
32
|
- Bugfixes:
|
33
|
-
-
|
34
|
-
-
|
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=
|
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=
|
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.
|
28
|
-
huff-1.5.
|
29
|
-
huff-1.5.
|
30
|
-
huff-1.5.
|
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
|
File without changes
|