huff 1.4.1__py3-none-any.whl → 1.5.0__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 +504 -77
- huff/tests/data/Wieland2015.xlsx +0 -0
- huff/tests/tests_huff.py +102 -10
- {huff-1.4.1.dist-info → huff-1.5.0.dist-info}/METADATA +14 -3
- {huff-1.4.1.dist-info → huff-1.5.0.dist-info}/RECORD +7 -7
- {huff-1.4.1.dist-info → huff-1.5.0.dist-info}/WHEEL +0 -0
- {huff-1.4.1.dist-info → huff-1.5.0.dist-info}/top_level.txt +0 -0
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.
|
8
|
-
# Last update: 2025-06-
|
7
|
+
# Version: 1.5.0
|
8
|
+
# Last update: 2025-06-25 18:32
|
9
9
|
# Copyright (c) 2025 Thomas Wieland
|
10
10
|
#-----------------------------------------------------------------------
|
11
11
|
|
@@ -119,6 +119,24 @@ class CustomerOrigins:
|
|
119
119
|
param_lambda = -2
|
120
120
|
):
|
121
121
|
|
122
|
+
"""
|
123
|
+
metadata["weighting"] = {
|
124
|
+
0: {
|
125
|
+
"name": "t_ij",
|
126
|
+
"func": "power",
|
127
|
+
"param": -2
|
128
|
+
}
|
129
|
+
}
|
130
|
+
|
131
|
+
metadata["weighting"] = {
|
132
|
+
0: {
|
133
|
+
"name": "t_ij",
|
134
|
+
"func": "logistic",
|
135
|
+
"param": [10, -0.5]
|
136
|
+
}
|
137
|
+
}
|
138
|
+
"""
|
139
|
+
|
122
140
|
metadata = self.metadata
|
123
141
|
|
124
142
|
if func not in ["power", "exponential", "logistic"]:
|
@@ -130,6 +148,7 @@ class CustomerOrigins:
|
|
130
148
|
if isinstance(param_lambda, (int, float)) and func == "logistic":
|
131
149
|
raise ValueError("Function type "+ func + " requires two parameters in a list")
|
132
150
|
|
151
|
+
metadata["weighting"][0]["name"] = "t_ij"
|
133
152
|
metadata["weighting"][0]["func"] = func
|
134
153
|
|
135
154
|
if isinstance(param_lambda, list):
|
@@ -299,8 +318,10 @@ class SupplyLocations:
|
|
299
318
|
if metadata["attraction_col"] is None:
|
300
319
|
raise ValueError ("Attraction column is not yet defined. Use SupplyLocations.define_attraction()")
|
301
320
|
|
321
|
+
metadata["weighting"][0]["name"] = "A_j"
|
302
322
|
metadata["weighting"][0]["func"] = func
|
303
323
|
metadata["weighting"][0]["param"] = float(param_gamma)
|
324
|
+
|
304
325
|
self.metadata = metadata
|
305
326
|
|
306
327
|
return self
|
@@ -323,6 +344,7 @@ class SupplyLocations:
|
|
323
344
|
metadata["attraction_col"] = metadata["attraction_col"] + [var]
|
324
345
|
|
325
346
|
metadata["weighting"][new_key] = {
|
347
|
+
"name": var,
|
326
348
|
"func": func,
|
327
349
|
"param": param
|
328
350
|
}
|
@@ -490,20 +512,19 @@ class InteractionMatrix:
|
|
490
512
|
else:
|
491
513
|
print("Market size column " + customer_origins_metadata["marketsize_col"])
|
492
514
|
|
493
|
-
if interaction_matrix_metadata != {}:
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
print("Transport cost unit " + interaction_matrix_metadata["transport_costs"]["distance_unit"])
|
515
|
+
if interaction_matrix_metadata != {} and "transport_costs" in interaction_matrix_metadata:
|
516
|
+
print("----------------------------------")
|
517
|
+
if interaction_matrix_metadata["transport_costs"]["network"]:
|
518
|
+
print("Transport cost type Time")
|
519
|
+
print("Transport cost unit " + interaction_matrix_metadata["transport_costs"]["time_unit"])
|
520
|
+
else:
|
521
|
+
print("Transport cost type Distance")
|
522
|
+
print("Transport cost unit " + interaction_matrix_metadata["transport_costs"]["distance_unit"])
|
502
523
|
|
503
524
|
print("----------------------------------")
|
504
525
|
print("Partial utilities")
|
505
526
|
print(" Weights")
|
506
|
-
|
527
|
+
|
507
528
|
if supply_locations_metadata["weighting"][0]["func"] is None and supply_locations_metadata["weighting"][0]["param"] is None:
|
508
529
|
print("Attraction not defined")
|
509
530
|
else:
|
@@ -515,9 +536,37 @@ class InteractionMatrix:
|
|
515
536
|
print("Transport costs " + str(round(customer_origins_metadata["weighting"][0]["param"],3)) + " (" + customer_origins_metadata["weighting"][0]["func"] + ")")
|
516
537
|
elif customer_origins_metadata["weighting"][0]["func"] == "logistic":
|
517
538
|
print("Transport costs " + str(round(customer_origins_metadata["weighting"][0]["param"][0],3)) + ", " + str(round(customer_origins_metadata["weighting"][0]["param"][1],3)) + " (" + customer_origins_metadata["weighting"][0]["func"] + ")")
|
539
|
+
|
540
|
+
|
541
|
+
attrac_vars = supply_locations_metadata["attraction_col"]
|
542
|
+
attrac_vars_no = len(attrac_vars)
|
543
|
+
|
544
|
+
if attrac_vars_no > 1:
|
545
|
+
|
546
|
+
for key, attrac_var in enumerate(attrac_vars):
|
547
|
+
|
548
|
+
if key == 0:
|
549
|
+
continue
|
550
|
+
|
551
|
+
if key not in supply_locations_metadata["weighting"].keys():
|
552
|
+
|
553
|
+
print(f"{attrac_vars[key][:16]:16} not defined")
|
554
|
+
|
555
|
+
else:
|
556
|
+
|
557
|
+
name = supply_locations_metadata["weighting"][key]["name"]
|
558
|
+
param = supply_locations_metadata["weighting"][key]["param"]
|
559
|
+
func = supply_locations_metadata["weighting"][key]["func"]
|
560
|
+
|
561
|
+
print(f"{name[:16]:16} {round(param, 3)} ({func})")
|
518
562
|
|
519
563
|
print("----------------------------------")
|
520
564
|
|
565
|
+
if interaction_matrix_metadata != {} and "fit" in interaction_matrix_metadata and interaction_matrix_metadata["fit"]["function"] is not None:
|
566
|
+
print("Parameter estimation")
|
567
|
+
print("Fit function " + interaction_matrix_metadata["fit"]["function"])
|
568
|
+
print("Fit by " + interaction_matrix_metadata["fit"]["fit_by"])
|
569
|
+
|
521
570
|
def transport_costs(
|
522
571
|
self,
|
523
572
|
network: bool = True,
|
@@ -630,12 +679,73 @@ class InteractionMatrix:
|
|
630
679
|
|
631
680
|
return self
|
632
681
|
|
682
|
+
def define_weightings(
|
683
|
+
self,
|
684
|
+
vars_funcs: dict
|
685
|
+
):
|
686
|
+
|
687
|
+
"""
|
688
|
+
vars_funcs = {
|
689
|
+
0: {
|
690
|
+
"name": "A_j",
|
691
|
+
"func": "power"
|
692
|
+
},
|
693
|
+
1: {
|
694
|
+
"name": "t_ij",
|
695
|
+
"func": "logistic"
|
696
|
+
},
|
697
|
+
2: {
|
698
|
+
"name": "second_attraction_variable",
|
699
|
+
"func": "power"
|
700
|
+
},
|
701
|
+
3: {
|
702
|
+
"name": "third_attraction_variable",
|
703
|
+
"func": "exponential"
|
704
|
+
},
|
705
|
+
...
|
706
|
+
}
|
707
|
+
"""
|
708
|
+
|
709
|
+
supply_locations_metadata = self.supply_locations.metadata
|
710
|
+
customer_origins_metadata = self.customer_origins.metadata
|
711
|
+
|
712
|
+
supply_locations_metadata["weighting"][0]["name"] = vars_funcs[0]["name"]
|
713
|
+
supply_locations_metadata["weighting"][0]["func"] = vars_funcs[0]["func"]
|
714
|
+
|
715
|
+
customer_origins_metadata["weighting"][0]["name"] = vars_funcs[1]["name"]
|
716
|
+
customer_origins_metadata["weighting"][0]["func"] = vars_funcs[1]["func"]
|
717
|
+
|
718
|
+
if len(vars_funcs) > 2:
|
719
|
+
|
720
|
+
for key, var in vars_funcs.items():
|
721
|
+
|
722
|
+
if key < 2:
|
723
|
+
continue
|
724
|
+
|
725
|
+
if key not in supply_locations_metadata["weighting"]:
|
726
|
+
supply_locations_metadata["weighting"][key-1] = {
|
727
|
+
"name": "attrac"+str(key),
|
728
|
+
"func": "power",
|
729
|
+
"param": None
|
730
|
+
}
|
731
|
+
|
732
|
+
supply_locations_metadata["weighting"][key-1]["name"] = var["name"]
|
733
|
+
supply_locations_metadata["weighting"][key-1]["func"] = var["func"]
|
734
|
+
supply_locations_metadata["weighting"][key-1]["param"] = None
|
735
|
+
|
736
|
+
self.supply_locations.metadata = supply_locations_metadata
|
737
|
+
self.customer_origins.metadata = customer_origins_metadata
|
738
|
+
|
633
739
|
def utility(self):
|
634
740
|
|
635
741
|
interaction_matrix_df = self.interaction_matrix_df
|
636
742
|
|
637
743
|
interaction_matrix_metadata = self.get_metadata()
|
638
744
|
|
745
|
+
if "t_ij" not in interaction_matrix_df.columns:
|
746
|
+
raise ValueError ("No transport cost variable in interaction matrix")
|
747
|
+
if "A_j" not in interaction_matrix_df.columns:
|
748
|
+
raise ValueError ("No attraction variable in interaction matrix")
|
639
749
|
if interaction_matrix_df["t_ij"].isna().all():
|
640
750
|
raise ValueError ("Transport cost variable is not defined")
|
641
751
|
if interaction_matrix_df["A_j"].isna().all():
|
@@ -670,6 +780,33 @@ class InteractionMatrix:
|
|
670
780
|
else:
|
671
781
|
raise ValueError ("Attraction weighting is not defined.")
|
672
782
|
|
783
|
+
attrac_vars = supply_locations_metadata["attraction_col"]
|
784
|
+
attrac_vars_no = len(attrac_vars)
|
785
|
+
attrac_var_key = 0
|
786
|
+
|
787
|
+
if attrac_vars_no > 1:
|
788
|
+
|
789
|
+
for key, attrac_var in enumerate(attrac_vars):
|
790
|
+
|
791
|
+
attrac_var_key = key #+1
|
792
|
+
if attrac_var_key == 0: #1:
|
793
|
+
continue
|
794
|
+
|
795
|
+
name = supply_locations_metadata["weighting"][attrac_var_key]["name"]
|
796
|
+
param = supply_locations_metadata["weighting"][attrac_var_key]["param"]
|
797
|
+
func = supply_locations_metadata["weighting"][attrac_var_key]["func"]
|
798
|
+
|
799
|
+
if func == "power":
|
800
|
+
interaction_matrix_df[name+"_weighted"] = interaction_matrix_df[name] ** param
|
801
|
+
elif func == "exponential":
|
802
|
+
interaction_matrix_df[name+"_weighted"] = np.exp(param * interaction_matrix_df[name])
|
803
|
+
else:
|
804
|
+
raise ValueError ("Weighting for " + name + " is not defined.")
|
805
|
+
|
806
|
+
interaction_matrix_df["A_j_weighted"] = interaction_matrix_df["A_j_weighted"]*interaction_matrix_df[name+"_weighted"]
|
807
|
+
|
808
|
+
interaction_matrix_df = interaction_matrix_df.drop(columns=[name+"_weighted"])
|
809
|
+
|
673
810
|
interaction_matrix_df["U_ij"] = interaction_matrix_df["A_j_weighted"]*interaction_matrix_df["t_ij_weighted"]
|
674
811
|
|
675
812
|
interaction_matrix_df = interaction_matrix_df.drop(columns=["A_j_weighted", "t_ij_weighted"])
|
@@ -687,7 +824,7 @@ class InteractionMatrix:
|
|
687
824
|
|
688
825
|
interaction_matrix_df = self.interaction_matrix_df
|
689
826
|
|
690
|
-
if interaction_matrix_df["U_ij"].isna().all():
|
827
|
+
if "U_ij" not in interaction_matrix_df.columns or interaction_matrix_df["U_ij"].isna().all():
|
691
828
|
self.utility()
|
692
829
|
interaction_matrix_df = self.interaction_matrix_df
|
693
830
|
|
@@ -713,6 +850,8 @@ class InteractionMatrix:
|
|
713
850
|
|
714
851
|
interaction_matrix_df = self.interaction_matrix_df
|
715
852
|
|
853
|
+
if "C_i" not in interaction_matrix_df.columns:
|
854
|
+
raise ValueError ("No market size variable in interaction matrix")
|
716
855
|
if interaction_matrix_df["C_i"].isna().all():
|
717
856
|
raise ValueError ("Market size column in customer origins not defined. Use CustomerOrigins.define_marketsize()")
|
718
857
|
|
@@ -875,6 +1014,15 @@ class InteractionMatrix:
|
|
875
1014
|
|
876
1015
|
customer_origins.metadata = customer_origins_metadata
|
877
1016
|
supply_locations.metadata = supply_locations_metadata
|
1017
|
+
|
1018
|
+
interaction_matrix_metadata = {
|
1019
|
+
"fit": {
|
1020
|
+
"function": "mci_fit",
|
1021
|
+
"fit_by": "probabilities",
|
1022
|
+
"method": "OLS"
|
1023
|
+
}
|
1024
|
+
}
|
1025
|
+
|
878
1026
|
interaction_matrix = InteractionMatrix(
|
879
1027
|
interaction_matrix_df,
|
880
1028
|
customer_origins,
|
@@ -891,23 +1039,34 @@ class InteractionMatrix:
|
|
891
1039
|
|
892
1040
|
return mci_model
|
893
1041
|
|
894
|
-
def
|
1042
|
+
def loglik(
|
895
1043
|
self,
|
896
|
-
params
|
1044
|
+
params,
|
1045
|
+
fit_by = "probabilities"
|
897
1046
|
):
|
1047
|
+
|
1048
|
+
if fit_by not in ["probabilities", "flows"]:
|
1049
|
+
raise ValueError ("Parameter 'fit_by' must be 'probabilities' or 'flows'")
|
898
1050
|
|
899
1051
|
if not isinstance(params, list):
|
900
1052
|
if isinstance(params, np.ndarray):
|
901
1053
|
params = params.tolist()
|
902
1054
|
else:
|
903
|
-
raise ValueError("Parameter 'params' must be a list or np.ndarray with
|
1055
|
+
raise ValueError("Parameter 'params' must be a list or np.ndarray with at least 2 parameter values")
|
904
1056
|
|
905
|
-
if len(params)
|
906
|
-
|
907
|
-
|
908
|
-
|
909
|
-
|
910
|
-
|
1057
|
+
if len(params) < 2:
|
1058
|
+
raise ValueError("Parameter 'params' must be a list or np.ndarray with at least 2 parameter values")
|
1059
|
+
|
1060
|
+
customer_origins_metadata = self.customer_origins.get_metadata()
|
1061
|
+
|
1062
|
+
param_gamma, param_lambda = params[0], params[1]
|
1063
|
+
|
1064
|
+
if customer_origins_metadata["weighting"][0]["func"] == "logistic":
|
1065
|
+
|
1066
|
+
if len(params) < 3:
|
1067
|
+
raise ValueError("When using logistic weighting, parameter 'params' must be a list or np.ndarray with at least 3 parameter values")
|
1068
|
+
|
1069
|
+
param_gamma, param_lambda, param_lambda2 = params[0], params[1], params[2]
|
911
1070
|
|
912
1071
|
interaction_matrix_df = self.interaction_matrix_df
|
913
1072
|
|
@@ -922,43 +1081,83 @@ class InteractionMatrix:
|
|
922
1081
|
|
923
1082
|
if customer_origins_metadata["weighting"][0]["func"] in ["power", "exponential"]:
|
924
1083
|
|
925
|
-
if len(params)
|
1084
|
+
if len(params) >= 2:
|
1085
|
+
|
926
1086
|
customer_origins_metadata["weighting"][0]["param"] = float(param_lambda)
|
1087
|
+
|
927
1088
|
else:
|
928
|
-
|
1089
|
+
|
1090
|
+
raise ValueError ("Huff Model with transport cost weighting of type " + customer_origins_metadata["weighting"][0]["func"] + " must have >= 2 input parameters")
|
929
1091
|
|
930
1092
|
elif customer_origins_metadata["weighting"][0]["func"] == "logistic":
|
931
1093
|
|
932
|
-
if len(params)
|
1094
|
+
if len(params) >= 3:
|
1095
|
+
|
933
1096
|
customer_origins_metadata["weighting"][0]["param"] = [float(param_lambda), float(param_lambda2)]
|
1097
|
+
|
934
1098
|
else:
|
935
|
-
|
936
|
-
|
1099
|
+
|
1100
|
+
raise ValueError("Huff Model with transport cost weightig of type " + customer_origins_metadata["weighting"][0]["func"] + " must have >= 3 input parameters")
|
1101
|
+
|
1102
|
+
if (customer_origins_metadata["weighting"][0]["func"] in ["power", "exponential"] and len(params) > 2):
|
1103
|
+
|
1104
|
+
for key, param in enumerate(params):
|
1105
|
+
|
1106
|
+
if key <= 1:
|
1107
|
+
continue
|
1108
|
+
|
1109
|
+
supply_locations_metadata["weighting"][key-1]["param"] = float(param)
|
1110
|
+
|
1111
|
+
if (customer_origins_metadata["weighting"][0]["func"] == "logistic" and len(params) > 3):
|
1112
|
+
|
1113
|
+
for key, param in enumerate(params):
|
1114
|
+
|
1115
|
+
if key <= 2:
|
1116
|
+
continue
|
1117
|
+
|
1118
|
+
supply_locations_metadata["weighting"][key-2]["param"] = float(param)
|
1119
|
+
|
937
1120
|
customer_origins.metadata = customer_origins_metadata
|
938
|
-
|
1121
|
+
|
939
1122
|
p_ij_emp = interaction_matrix_df["p_ij"]
|
1123
|
+
E_ij_emp = interaction_matrix_df["E_ij"]
|
940
1124
|
|
941
1125
|
interaction_matrix_copy = copy.deepcopy(self)
|
942
1126
|
|
943
1127
|
interaction_matrix_copy.utility()
|
944
1128
|
interaction_matrix_copy.probabilities()
|
1129
|
+
interaction_matrix_copy.flows()
|
945
1130
|
|
946
1131
|
interaction_matrix_df_copy = interaction_matrix_copy.get_interaction_matrix_df()
|
947
|
-
p_ij = interaction_matrix_df_copy["p_ij"]
|
948
1132
|
|
949
|
-
|
950
|
-
|
1133
|
+
if fit_by == "flows":
|
1134
|
+
|
1135
|
+
E_ij = interaction_matrix_df_copy["E_ij"]
|
1136
|
+
|
1137
|
+
observed = E_ij_emp
|
1138
|
+
expected = E_ij
|
1139
|
+
|
1140
|
+
else:
|
1141
|
+
|
1142
|
+
p_ij = interaction_matrix_df_copy["p_ij"]
|
1143
|
+
|
1144
|
+
observed = p_ij_emp
|
951
1145
|
expected = p_ij
|
952
|
-
)
|
953
1146
|
|
1147
|
+
LL = loglik(
|
1148
|
+
observed = observed,
|
1149
|
+
expected = expected
|
1150
|
+
)
|
1151
|
+
|
954
1152
|
return -LL
|
955
1153
|
|
956
|
-
def
|
1154
|
+
def huff_ml_fit(
|
957
1155
|
self,
|
958
1156
|
initial_params: list = [1.0, -2.0],
|
959
1157
|
method: str = "L-BFGS-B",
|
960
1158
|
bounds: list = [(0.5, 1), (-3, -1)],
|
961
1159
|
constraints: list = [],
|
1160
|
+
fit_by = "probabilities",
|
962
1161
|
update_estimates: bool = True
|
963
1162
|
):
|
964
1163
|
|
@@ -967,41 +1166,59 @@ class InteractionMatrix:
|
|
967
1166
|
|
968
1167
|
customer_origins = self.customer_origins
|
969
1168
|
customer_origins_metadata = customer_origins.get_metadata()
|
1169
|
+
|
1170
|
+
if customer_origins_metadata["weighting"][0]["param"] is None:
|
1171
|
+
params_metadata_customer_origins = 1
|
1172
|
+
else:
|
1173
|
+
if customer_origins_metadata["weighting"][0]["param"] is not None:
|
1174
|
+
params_metadata_customer_origins = len(customer_origins_metadata["weighting"][0]["param"])
|
1175
|
+
|
1176
|
+
if customer_origins_metadata["weighting"][0]["func"] == "logistic":
|
1177
|
+
params_metadata_customer_origins = 2
|
1178
|
+
else:
|
1179
|
+
params_metadata_customer_origins = 1
|
1180
|
+
|
1181
|
+
params_metadata_supply_locations = len(supply_locations_metadata["weighting"])
|
970
1182
|
|
971
|
-
|
972
|
-
|
1183
|
+
params_metadata = params_metadata_customer_origins+params_metadata_supply_locations
|
1184
|
+
|
1185
|
+
if len(initial_params) < 2 or len(initial_params) != params_metadata:
|
1186
|
+
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) + ")")
|
973
1187
|
|
974
1188
|
if len(bounds) != len(initial_params):
|
975
1189
|
raise ValueError("Parameter 'bounds' must have the same length as parameter 'initial_params' (" + str(len(bounds)) + ", " + str(len(initial_params)) + ")")
|
976
|
-
|
1190
|
+
|
977
1191
|
ml_result = minimize(
|
978
|
-
self.
|
1192
|
+
self.loglik,
|
979
1193
|
initial_params,
|
1194
|
+
args=fit_by,
|
980
1195
|
method = method,
|
981
1196
|
bounds = bounds,
|
982
1197
|
constraints = constraints,
|
983
1198
|
options={'disp': 3}
|
984
1199
|
)
|
985
1200
|
|
1201
|
+
attrac_vars = len(supply_locations_metadata["weighting"])
|
1202
|
+
|
986
1203
|
if ml_result.success:
|
987
1204
|
|
988
1205
|
fitted_params = ml_result.x
|
989
1206
|
|
990
|
-
|
1207
|
+
param_gamma = fitted_params[0]
|
1208
|
+
supply_locations_metadata["weighting"][0]["param"] = float(param_gamma)
|
991
1209
|
|
992
|
-
|
1210
|
+
if customer_origins_metadata["weighting"][0]["func"] in ["power", "exponential"]:
|
1211
|
+
|
993
1212
|
param_lambda = fitted_params[1]
|
994
1213
|
param_results = [
|
995
1214
|
float(param_gamma),
|
996
1215
|
float(param_lambda)
|
997
1216
|
]
|
998
|
-
|
999
|
-
supply_locations_metadata["weighting"][0]["param"] = float(param_gamma)
|
1217
|
+
|
1000
1218
|
customer_origins_metadata["weighting"][0]["param"] = float(param_lambda)
|
1001
1219
|
|
1002
|
-
elif
|
1220
|
+
elif customer_origins_metadata["weighting"][0]["func"] == "logistic":
|
1003
1221
|
|
1004
|
-
param_gamma = fitted_params[0]
|
1005
1222
|
param_lambda = fitted_params[1]
|
1006
1223
|
param_lambda2 = fitted_params[2]
|
1007
1224
|
param_results = [
|
@@ -1010,10 +1227,27 @@ class InteractionMatrix:
|
|
1010
1227
|
float(param_lambda2)
|
1011
1228
|
]
|
1012
1229
|
|
1013
|
-
supply_locations_metadata["weighting"][0]["param"] = float(param_gamma)
|
1014
1230
|
customer_origins_metadata["weighting"][0]["param"][0] = float(param_lambda)
|
1015
|
-
customer_origins_metadata["weighting"][0]["param"][1] = float(param_lambda2)
|
1231
|
+
customer_origins_metadata["weighting"][0]["param"][1] = float(param_lambda2)
|
1016
1232
|
|
1233
|
+
if attrac_vars > 1:
|
1234
|
+
|
1235
|
+
if customer_origins_metadata["weighting"][0]["func"] == "logistic":
|
1236
|
+
fitted_params_add = 3
|
1237
|
+
else:
|
1238
|
+
fitted_params_add = 2
|
1239
|
+
|
1240
|
+
for key, var in supply_locations_metadata["weighting"].items():
|
1241
|
+
|
1242
|
+
if key > len(supply_locations_metadata["weighting"])-fitted_params_add:
|
1243
|
+
break
|
1244
|
+
|
1245
|
+
param = float(fitted_params[key+fitted_params_add])
|
1246
|
+
|
1247
|
+
param_results = param_results + [param]
|
1248
|
+
|
1249
|
+
supply_locations_metadata["weighting"][(key+1)]["param"] = float(param)
|
1250
|
+
|
1017
1251
|
print(f"Optimization via {method} algorithm succeeded with parameters: {', '.join(str(round(par, 3)) for par in param_results)}.")
|
1018
1252
|
|
1019
1253
|
else:
|
@@ -1023,13 +1257,14 @@ class InteractionMatrix:
|
|
1023
1257
|
|
1024
1258
|
supply_locations_metadata["weighting"][0]["param"] = param_gamma
|
1025
1259
|
|
1026
|
-
if
|
1260
|
+
if customer_origins_metadata["weighting"][0]["func"] == "logistic":
|
1027
1261
|
|
1028
1262
|
param_lambda2 = None
|
1029
1263
|
customer_origins_metadata["weighting"][0]["param"][0] = param_lambda
|
1030
1264
|
customer_origins_metadata["weighting"][0]["param"][1] = param_lambda2
|
1031
1265
|
|
1032
1266
|
else:
|
1267
|
+
|
1033
1268
|
customer_origins_metadata["weighting"][0]["param"] = param_lambda
|
1034
1269
|
|
1035
1270
|
print(f"Optimiziation via {method} algorithm failed with error message: '{ml_result.message}'. See https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html for all available algorithms.")
|
@@ -1039,14 +1274,25 @@ class InteractionMatrix:
|
|
1039
1274
|
|
1040
1275
|
if ml_result.success and update_estimates:
|
1041
1276
|
|
1277
|
+
self.metadata["fit"] = {
|
1278
|
+
"function": "huff_ml_fit",
|
1279
|
+
"fit_by": fit_by,
|
1280
|
+
"initial_params": initial_params,
|
1281
|
+
"method": method,
|
1282
|
+
"bounds": bounds,
|
1283
|
+
"constraints": constraints
|
1284
|
+
}
|
1285
|
+
|
1286
|
+
|
1042
1287
|
self.interaction_matrix_df["p_ij_emp"] = self.interaction_matrix_df["p_ij"]
|
1288
|
+
self.interaction_matrix_df["E_ij_emp"] = self.interaction_matrix_df["E_ij"]
|
1289
|
+
|
1043
1290
|
self = self.utility()
|
1044
1291
|
self = self.probabilities()
|
1045
1292
|
self = self.flows()
|
1046
1293
|
|
1047
1294
|
return self
|
1048
1295
|
|
1049
|
-
|
1050
1296
|
def update(self):
|
1051
1297
|
|
1052
1298
|
interaction_matrix_df = self.get_interaction_matrix_df()
|
@@ -1135,6 +1381,49 @@ class InteractionMatrix:
|
|
1135
1381
|
|
1136
1382
|
return self
|
1137
1383
|
|
1384
|
+
class MarketAreas:
|
1385
|
+
|
1386
|
+
def __init__(
|
1387
|
+
self,
|
1388
|
+
market_areas_df,
|
1389
|
+
metadata
|
1390
|
+
):
|
1391
|
+
|
1392
|
+
self.market_areas_df = market_areas_df
|
1393
|
+
self.metadata = metadata
|
1394
|
+
|
1395
|
+
def get_market_areas_df(self):
|
1396
|
+
return self.market_areas_df
|
1397
|
+
|
1398
|
+
def get_metadata(self):
|
1399
|
+
return self.metadata
|
1400
|
+
|
1401
|
+
def add_to_model(
|
1402
|
+
self,
|
1403
|
+
model_object
|
1404
|
+
):
|
1405
|
+
|
1406
|
+
if not isinstance(model_object, (HuffModel, MCIModel)):
|
1407
|
+
raise ValueError("Parameter 'interaction_matrix' must be of class HuffModel or MCIModel")
|
1408
|
+
|
1409
|
+
if isinstance(model_object, MCIModel):
|
1410
|
+
|
1411
|
+
model = MCIModel(
|
1412
|
+
interaction_matrix = model_object.interaction_matrix,
|
1413
|
+
coefs = model_object.get_coefs_dict(),
|
1414
|
+
mci_ols_model = model_object.get_mci_ols_model(),
|
1415
|
+
market_areas_df = self.market_areas_df
|
1416
|
+
)
|
1417
|
+
|
1418
|
+
if isinstance(model_object, HuffModel):
|
1419
|
+
|
1420
|
+
model = HuffModel(
|
1421
|
+
interaction_matrix = model_object.interaction_matrix,
|
1422
|
+
market_areas_df = self.market_areas_df
|
1423
|
+
)
|
1424
|
+
|
1425
|
+
return model
|
1426
|
+
|
1138
1427
|
class HuffModel:
|
1139
1428
|
|
1140
1429
|
def __init__(
|
@@ -1179,33 +1468,55 @@ class HuffModel:
|
|
1179
1468
|
|
1180
1469
|
print("Huff Model")
|
1181
1470
|
print("----------------------------------")
|
1182
|
-
print("Supply locations
|
1471
|
+
print("Supply locations " + str(supply_locations_metadata["no_points"]))
|
1183
1472
|
if supply_locations_metadata["attraction_col"][0] is None:
|
1184
|
-
print("Attraction column
|
1473
|
+
print("Attraction column not defined")
|
1185
1474
|
else:
|
1186
|
-
print("Attraction column
|
1187
|
-
print("Customer origins
|
1475
|
+
print("Attraction column " + supply_locations_metadata["attraction_col"][0])
|
1476
|
+
print("Customer origins " + str(customer_origins_metadata["no_points"]))
|
1188
1477
|
if customer_origins_metadata["marketsize_col"] is None:
|
1189
|
-
print("Market size column
|
1478
|
+
print("Market size column not defined")
|
1190
1479
|
else:
|
1191
|
-
print("Market size column
|
1480
|
+
print("Market size column " + customer_origins_metadata["marketsize_col"])
|
1192
1481
|
print("----------------------------------")
|
1193
1482
|
|
1194
1483
|
print("Partial utilities")
|
1195
|
-
print("
|
1484
|
+
print(" Weights")
|
1196
1485
|
|
1197
1486
|
if supply_locations_metadata["weighting"][0]["func"] is None and supply_locations_metadata["weighting"][0]["param"] is None:
|
1198
|
-
print("Attraction
|
1487
|
+
print("Attraction not defined")
|
1199
1488
|
else:
|
1200
|
-
print("Attraction
|
1489
|
+
print("Attraction " + str(round(supply_locations_metadata["weighting"][0]["param"], 3)) + " (" + supply_locations_metadata["weighting"][0]["func"] + ")")
|
1201
1490
|
|
1202
1491
|
if customer_origins_metadata["weighting"][0]["func"] is None and customer_origins_metadata["weighting"][0]["param"] is None:
|
1203
|
-
print("Transport costs
|
1492
|
+
print("Transport costs not defined")
|
1204
1493
|
elif customer_origins_metadata["weighting"][0]["func"] in ["power", "exponential"]:
|
1205
|
-
print("Transport costs
|
1494
|
+
print("Transport costs " + str(round(customer_origins_metadata["weighting"][0]["param"],3)) + " (" + customer_origins_metadata["weighting"][0]["func"] + ")")
|
1206
1495
|
elif customer_origins_metadata["weighting"][0]["func"] == "logistic":
|
1207
|
-
print("Transport costs
|
1496
|
+
print("Transport costs " + str(round(customer_origins_metadata["weighting"][0]["param"][0],3)) + ", " + str(round(customer_origins_metadata["weighting"][0]["param"][1],3)) + " (" + customer_origins_metadata["weighting"][0]["func"] + ")")
|
1497
|
+
|
1498
|
+
attrac_vars = supply_locations_metadata["attraction_col"]
|
1499
|
+
attrac_vars_no = len(attrac_vars)
|
1500
|
+
|
1501
|
+
if attrac_vars_no > 1:
|
1502
|
+
|
1503
|
+
for key, attrac_var in enumerate(attrac_vars):
|
1504
|
+
|
1505
|
+
if key == 0:
|
1506
|
+
continue
|
1507
|
+
|
1508
|
+
if key not in supply_locations_metadata["weighting"].keys():
|
1509
|
+
|
1510
|
+
print(f"{attrac_vars[key][:16]:16} not defined")
|
1511
|
+
|
1512
|
+
else:
|
1208
1513
|
|
1514
|
+
name = supply_locations_metadata["weighting"][key]["name"]
|
1515
|
+
param = supply_locations_metadata["weighting"][key]["param"]
|
1516
|
+
func = supply_locations_metadata["weighting"][key]["func"]
|
1517
|
+
|
1518
|
+
print(f"{name[:16]:16} {round(param, 3)} ({func})")
|
1519
|
+
|
1209
1520
|
print("----------------------------------")
|
1210
1521
|
|
1211
1522
|
huff_modelfit = self.modelfit()
|
@@ -1219,7 +1530,11 @@ class HuffModel:
|
|
1219
1530
|
print("Mean squared error ", round(huff_modelfit[1]["MSE"], 2))
|
1220
1531
|
print("Root mean squared error ", round(huff_modelfit[1]["RMSE"], 2))
|
1221
1532
|
print("Mean absolute error ", round(huff_modelfit[1]["MAE"], 2))
|
1222
|
-
|
1533
|
+
if huff_modelfit[1]["MAPE"] is not None:
|
1534
|
+
print("Mean absolute percentage error ", round(huff_modelfit[1]["MAPE"], 2))
|
1535
|
+
else:
|
1536
|
+
print("Mean absolute percentage error Not calculated")
|
1537
|
+
print("Symmetric MAPE ", round(huff_modelfit[1]["sMAPE"], 2))
|
1223
1538
|
print("Absolute percentage errors")
|
1224
1539
|
|
1225
1540
|
APE_list = [
|
@@ -1302,6 +1617,15 @@ class HuffModel:
|
|
1302
1617
|
|
1303
1618
|
customer_origins.metadata = customer_origins_metadata
|
1304
1619
|
supply_locations.metadata = supply_locations_metadata
|
1620
|
+
|
1621
|
+
interaction_matrix_metadata = {
|
1622
|
+
"fit": {
|
1623
|
+
"function": "mci_fit",
|
1624
|
+
"fit_by": "probabilities",
|
1625
|
+
"method": "OLS"
|
1626
|
+
}
|
1627
|
+
}
|
1628
|
+
|
1305
1629
|
interaction_matrix = InteractionMatrix(
|
1306
1630
|
interaction_matrix_df,
|
1307
1631
|
customer_origins,
|
@@ -1326,18 +1650,21 @@ class HuffModel:
|
|
1326
1650
|
|
1327
1651
|
return self
|
1328
1652
|
|
1329
|
-
def modelfit(
|
1653
|
+
def modelfit(
|
1654
|
+
self,
|
1655
|
+
by = "p_ij"
|
1656
|
+
):
|
1330
1657
|
|
1331
1658
|
interaction_matrix = self.interaction_matrix
|
1332
1659
|
interaction_matrix_df = interaction_matrix.get_interaction_matrix_df()
|
1333
1660
|
|
1334
|
-
if (
|
1661
|
+
if (by in interaction_matrix_df.columns and by+"_emp" in interaction_matrix_df.columns):
|
1335
1662
|
|
1336
1663
|
try:
|
1337
1664
|
|
1338
1665
|
huff_modelfit = modelfit(
|
1339
|
-
interaction_matrix_df["
|
1340
|
-
interaction_matrix_df[
|
1666
|
+
interaction_matrix_df[by+"_emp"],
|
1667
|
+
interaction_matrix_df[by]
|
1341
1668
|
)
|
1342
1669
|
|
1343
1670
|
return huff_modelfit
|
@@ -1472,7 +1799,11 @@ class MCIModel:
|
|
1472
1799
|
print("Mean squared error ", round(mci_modelfit[1]["MSE"], 2))
|
1473
1800
|
print("Root mean squared error ", round(mci_modelfit[1]["RMSE"], 2))
|
1474
1801
|
print("Mean absolute error ", round(mci_modelfit[1]["MAE"], 2))
|
1475
|
-
|
1802
|
+
if mci_modelfit[1]["MAPE"] is not None:
|
1803
|
+
print("Mean absolute percentage error ", round(mci_modelfit[1]["MAPE"], 2))
|
1804
|
+
else:
|
1805
|
+
print("Mean absolute percentage error Not calculated")
|
1806
|
+
print("Symmetric MAPE ", round(mci_modelfit[1]["sMAPE"], 2))
|
1476
1807
|
|
1477
1808
|
print("Absolute percentage errors")
|
1478
1809
|
APE_list = [
|
@@ -1720,6 +2051,7 @@ def load_geodata (
|
|
1720
2051
|
"marketsize_col": None,
|
1721
2052
|
"weighting": {
|
1722
2053
|
0: {
|
2054
|
+
"name": None,
|
1723
2055
|
"func": None,
|
1724
2056
|
"param": None
|
1725
2057
|
}
|
@@ -1727,7 +2059,7 @@ def load_geodata (
|
|
1727
2059
|
"crs_input": crs_input,
|
1728
2060
|
"crs_output": crs_output,
|
1729
2061
|
"no_points": len(geodata_gpd)
|
1730
|
-
}
|
2062
|
+
}
|
1731
2063
|
|
1732
2064
|
if location_type == "origins":
|
1733
2065
|
|
@@ -1848,8 +2180,10 @@ def load_interaction_matrix(
|
|
1848
2180
|
csv_sep = ";",
|
1849
2181
|
csv_decimal = ",",
|
1850
2182
|
csv_encoding="unicode_escape",
|
2183
|
+
xlsx_sheet: str = None,
|
1851
2184
|
crs_input = "EPSG:4326",
|
1852
|
-
crs_output = "EPSG:4326"
|
2185
|
+
crs_output = "EPSG:4326",
|
2186
|
+
check_df_vars = True
|
1853
2187
|
):
|
1854
2188
|
|
1855
2189
|
if isinstance(data, pd.DataFrame):
|
@@ -1865,7 +2199,13 @@ def load_interaction_matrix(
|
|
1865
2199
|
encoding = csv_encoding
|
1866
2200
|
)
|
1867
2201
|
elif data_type == "xlsx":
|
1868
|
-
|
2202
|
+
if xlsx_sheet is not None:
|
2203
|
+
interaction_matrix_df = pd.read_excel(
|
2204
|
+
data,
|
2205
|
+
sheet_name=xlsx_sheet
|
2206
|
+
)
|
2207
|
+
else:
|
2208
|
+
interaction_matrix_df = pd.read_excel(data)
|
1869
2209
|
else:
|
1870
2210
|
raise TypeError("Unknown type of data")
|
1871
2211
|
else:
|
@@ -1884,10 +2224,11 @@ def load_interaction_matrix(
|
|
1884
2224
|
if market_size_col is not None:
|
1885
2225
|
cols_check = cols_check + [market_size_col]
|
1886
2226
|
|
1887
|
-
|
1888
|
-
|
1889
|
-
|
1890
|
-
|
2227
|
+
if check_df_vars:
|
2228
|
+
check_vars(
|
2229
|
+
interaction_matrix_df,
|
2230
|
+
cols = cols_check
|
2231
|
+
)
|
1891
2232
|
|
1892
2233
|
if customer_origins_coords_col is not None:
|
1893
2234
|
|
@@ -1942,6 +2283,7 @@ def load_interaction_matrix(
|
|
1942
2283
|
"marketsize_col": market_size_col,
|
1943
2284
|
"weighting": {
|
1944
2285
|
0: {
|
2286
|
+
"name": None,
|
1945
2287
|
"func": None,
|
1946
2288
|
"param": None
|
1947
2289
|
}
|
@@ -2009,6 +2351,7 @@ def load_interaction_matrix(
|
|
2009
2351
|
"marketsize_col": None,
|
2010
2352
|
"weighting": {
|
2011
2353
|
0: {
|
2354
|
+
"name": None,
|
2012
2355
|
"func": None,
|
2013
2356
|
"param": None
|
2014
2357
|
}
|
@@ -2056,7 +2399,12 @@ def load_interaction_matrix(
|
|
2056
2399
|
}
|
2057
2400
|
)
|
2058
2401
|
|
2059
|
-
metadata = {
|
2402
|
+
metadata = {
|
2403
|
+
"fit": {
|
2404
|
+
"function": None,
|
2405
|
+
"fit_by": None
|
2406
|
+
}
|
2407
|
+
}
|
2060
2408
|
|
2061
2409
|
interaction_matrix = InteractionMatrix(
|
2062
2410
|
interaction_matrix_df=interaction_matrix_df,
|
@@ -2067,6 +2415,74 @@ def load_interaction_matrix(
|
|
2067
2415
|
|
2068
2416
|
return interaction_matrix
|
2069
2417
|
|
2418
|
+
def load_marketareas(
|
2419
|
+
data,
|
2420
|
+
supply_locations_col: str,
|
2421
|
+
total_col: str,
|
2422
|
+
data_type = "csv",
|
2423
|
+
csv_sep = ";",
|
2424
|
+
csv_decimal = ",",
|
2425
|
+
csv_encoding="unicode_escape",
|
2426
|
+
xlsx_sheet: str = None,
|
2427
|
+
check_df_vars = True
|
2428
|
+
):
|
2429
|
+
|
2430
|
+
if isinstance(data, pd.DataFrame):
|
2431
|
+
market_areas_df = data
|
2432
|
+
elif isinstance(data, str):
|
2433
|
+
if data_type not in ["csv", "xlsx"]:
|
2434
|
+
raise ValueError ("data_type must be 'csv' or 'xlsx'")
|
2435
|
+
if data_type == "csv":
|
2436
|
+
market_areas_df = pd.read_csv(
|
2437
|
+
data,
|
2438
|
+
sep = csv_sep,
|
2439
|
+
decimal = csv_decimal,
|
2440
|
+
encoding = csv_encoding
|
2441
|
+
)
|
2442
|
+
elif data_type == "xlsx":
|
2443
|
+
if xlsx_sheet is not None:
|
2444
|
+
market_areas_df = pd.read_excel(
|
2445
|
+
data,
|
2446
|
+
sheet_name=xlsx_sheet
|
2447
|
+
)
|
2448
|
+
else:
|
2449
|
+
market_areas_df = pd.read_excel(data)
|
2450
|
+
else:
|
2451
|
+
raise TypeError("Unknown type of data")
|
2452
|
+
else:
|
2453
|
+
raise TypeError("data must be pandas.DataFrame or file (.csv, .xlsx)")
|
2454
|
+
|
2455
|
+
if supply_locations_col not in market_areas_df.columns:
|
2456
|
+
raise KeyError ("Column " + supply_locations_col + " not in data")
|
2457
|
+
if total_col not in market_areas_df.columns:
|
2458
|
+
raise KeyError ("Column " + supply_locations_col + " not in data")
|
2459
|
+
|
2460
|
+
if check_df_vars:
|
2461
|
+
check_vars(
|
2462
|
+
market_areas_df,
|
2463
|
+
cols = [total_col]
|
2464
|
+
)
|
2465
|
+
|
2466
|
+
market_areas_df = market_areas_df.rename(
|
2467
|
+
columns = {
|
2468
|
+
supply_locations_col: "j",
|
2469
|
+
total_col: "T_j"
|
2470
|
+
}
|
2471
|
+
)
|
2472
|
+
|
2473
|
+
metadata = {
|
2474
|
+
"unique_id": supply_locations_col,
|
2475
|
+
"total_col": total_col,
|
2476
|
+
"no_points": len(market_areas_df)
|
2477
|
+
}
|
2478
|
+
|
2479
|
+
market_areas = MarketAreas(
|
2480
|
+
market_areas_df,
|
2481
|
+
metadata
|
2482
|
+
)
|
2483
|
+
|
2484
|
+
return market_areas
|
2485
|
+
|
2070
2486
|
def market_shares(
|
2071
2487
|
df: pd.DataFrame,
|
2072
2488
|
turnover_col: str,
|
@@ -2291,7 +2707,15 @@ def modelfit(
|
|
2291
2707
|
residuals_sq = residuals**2
|
2292
2708
|
residuals_abs = abs(residuals)
|
2293
2709
|
|
2294
|
-
|
2710
|
+
if any(observed == 0):
|
2711
|
+
print ("Vector 'observed' contains values equal to zero. No APE/MAPE calculated.")
|
2712
|
+
APE = np.full_like(observed, np.nan)
|
2713
|
+
MAPE = None
|
2714
|
+
else:
|
2715
|
+
APE = abs(observed-expected)/observed*100
|
2716
|
+
MAPE = float(np.mean(APE))
|
2717
|
+
|
2718
|
+
sAPE = abs(observed-expected)/((abs(observed)+abs(expected))/2)*100
|
2295
2719
|
|
2296
2720
|
data_residuals = pd.DataFrame({
|
2297
2721
|
"observed": observed,
|
@@ -2299,7 +2723,8 @@ def modelfit(
|
|
2299
2723
|
"residuals": residuals,
|
2300
2724
|
"residuals_sq": residuals_sq,
|
2301
2725
|
"residuals_abs": residuals_abs,
|
2302
|
-
"APE": APE
|
2726
|
+
"APE": APE,
|
2727
|
+
"sAPE": sAPE
|
2303
2728
|
})
|
2304
2729
|
|
2305
2730
|
SQR = float(np.sum(residuals_sq))
|
@@ -2310,7 +2735,8 @@ def modelfit(
|
|
2310
2735
|
MSE = float(SQR/observed_no)
|
2311
2736
|
RMSE = float(sqrt(MSE))
|
2312
2737
|
MAE = float(SAR/observed_no)
|
2313
|
-
|
2738
|
+
|
2739
|
+
sMAPE = float(np.mean(sAPE))
|
2314
2740
|
|
2315
2741
|
resid_below5 = float(len(data_residuals[data_residuals["APE"] < 5])/expected_no*100)
|
2316
2742
|
resid_below10 = float(len(data_residuals[data_residuals["APE"] < 10])/expected_no*100)
|
@@ -2332,6 +2758,7 @@ def modelfit(
|
|
2332
2758
|
"RMSE": RMSE,
|
2333
2759
|
"MAE": MAE,
|
2334
2760
|
"MAPE": MAPE,
|
2761
|
+
"sMAPE": sMAPE,
|
2335
2762
|
"APE": {
|
2336
2763
|
"resid_below5": resid_below5,
|
2337
2764
|
"resid_below10": resid_below10,
|
@@ -2362,7 +2789,7 @@ def loglik(
|
|
2362
2789
|
observed,
|
2363
2790
|
expected
|
2364
2791
|
)
|
2365
|
-
residuals_sq = model_fit[0]["residuals_sq"]
|
2792
|
+
residuals_sq = model_fit[0]["residuals_sq"]
|
2366
2793
|
|
2367
2794
|
LL = np.sum(np.log(residuals_sq))
|
2368
2795
|
|
huff/tests/data/Wieland2015.xlsx
CHANGED
Binary file
|
huff/tests/tests_huff.py
CHANGED
@@ -4,13 +4,13 @@
|
|
4
4
|
# Author: Thomas Wieland
|
5
5
|
# ORCID: 0000-0001-5168-9846
|
6
6
|
# mail: geowieland@googlemail.com
|
7
|
-
# Version: 1.
|
8
|
-
# Last update: 2025-06-
|
7
|
+
# Version: 1.5.0
|
8
|
+
# Last update: 2025-06-25 18:32
|
9
9
|
# Copyright (c) 2025 Thomas Wieland
|
10
10
|
#-----------------------------------------------------------------------
|
11
11
|
|
12
12
|
|
13
|
-
from huff.models import create_interaction_matrix, get_isochrones, load_geodata, load_interaction_matrix, market_shares, modelfit
|
13
|
+
from huff.models import create_interaction_matrix, get_isochrones, load_geodata, load_interaction_matrix, load_marketareas, market_shares, modelfit
|
14
14
|
from huff.osm import map_with_basemap
|
15
15
|
from huff.gistools import buffers, point_spatial_join
|
16
16
|
|
@@ -101,8 +101,8 @@ haslach_interactionmatrix = create_interaction_matrix(
|
|
101
101
|
# Creating interaction matrix
|
102
102
|
|
103
103
|
haslach_interactionmatrix.transport_costs(
|
104
|
-
ors_auth="5b3ce3597851110001cf62480a15aafdb5a64f4d91805929f8af6abd"
|
105
|
-
|
104
|
+
#ors_auth="5b3ce3597851110001cf62480a15aafdb5a64f4d91805929f8af6abd"
|
105
|
+
network=False,
|
106
106
|
#distance_unit="meters",
|
107
107
|
# set network = True to calculate transport costs matrix via ORS API (default)
|
108
108
|
)
|
@@ -131,7 +131,7 @@ print(huff_model.get_market_areas_df())
|
|
131
131
|
|
132
132
|
# Maximum Likelihood fit for Huff Model:
|
133
133
|
|
134
|
-
haslach_interactionmatrix.
|
134
|
+
haslach_interactionmatrix.huff_ml_fit(
|
135
135
|
#initial_params=[1, -2],
|
136
136
|
initial_params=[1, 9, -0.6],
|
137
137
|
method="trust-constr",
|
@@ -212,8 +212,10 @@ Wieland2015_interaction_matrix = load_interaction_matrix(
|
|
212
212
|
"K",
|
213
213
|
"K_KKr"
|
214
214
|
],
|
215
|
+
market_size_col="Sum_Ek1",
|
216
|
+
flows_col="Anb_Eink1",
|
215
217
|
transport_costs_col="Dist_Min2",
|
216
|
-
probabilities_col="
|
218
|
+
probabilities_col="MA_Anb1",
|
217
219
|
data_type="xlsx"
|
218
220
|
)
|
219
221
|
|
@@ -240,6 +242,93 @@ Wieland2015_fit.summary()
|
|
240
242
|
# MCI model summary
|
241
243
|
|
242
244
|
|
245
|
+
# Parameter estimation via Maximum Likelihood:
|
246
|
+
|
247
|
+
Wieland2015_interaction_matrix2 = load_interaction_matrix(
|
248
|
+
data="data/Wieland2015.xlsx",
|
249
|
+
customer_origins_col="Quellort",
|
250
|
+
supply_locations_col="Zielort",
|
251
|
+
attraction_col=[
|
252
|
+
"VF",
|
253
|
+
"K",
|
254
|
+
"K_KKr"
|
255
|
+
],
|
256
|
+
market_size_col="Sum_Ek",
|
257
|
+
flows_col="Anb_Eink",
|
258
|
+
transport_costs_col="Dist_Min2",
|
259
|
+
probabilities_col="MA_Anb",
|
260
|
+
data_type="xlsx",
|
261
|
+
xlsx_sheet="interactionmatrix",
|
262
|
+
check_df_vars=False
|
263
|
+
)
|
264
|
+
# Loading empirical interaction matrix again
|
265
|
+
|
266
|
+
Wieland2015_interaction_matrix2.define_weightings(
|
267
|
+
vars_funcs = {
|
268
|
+
0: {
|
269
|
+
"name": "A_j",
|
270
|
+
"func": "power"
|
271
|
+
},
|
272
|
+
1: {
|
273
|
+
"name": "t_ij",
|
274
|
+
"func": "power",
|
275
|
+
#"func": "exponential"
|
276
|
+
#"func": "logistic"
|
277
|
+
},
|
278
|
+
2: {
|
279
|
+
"name": "K",
|
280
|
+
"func": "power"
|
281
|
+
},
|
282
|
+
3: {
|
283
|
+
"name": "K_KKr",
|
284
|
+
"func": "power"
|
285
|
+
}
|
286
|
+
}
|
287
|
+
)
|
288
|
+
# Defining weighting functions
|
289
|
+
|
290
|
+
Wieland2015_interaction_matrix2.huff_ml_fit(
|
291
|
+
# Power TC function
|
292
|
+
initial_params=[0.9, -0.5, 0.5, 0.3],
|
293
|
+
bounds=[(0.5, 1), (-0.7, -0.2), (0.2, 0.7), (0.2, 0.7)],
|
294
|
+
# # Logistic TC function:
|
295
|
+
# initial_params=[0.9, 10, -0.5, 0.5, 0.3],
|
296
|
+
# bounds=[(0.5, 1), (8, 12), (-0.7, -0.2), (0.2, 0.7), (0.2, 0.7)],
|
297
|
+
fit_by="flows",
|
298
|
+
method = "trust-constr"
|
299
|
+
)
|
300
|
+
# ML fit with power transport cost function
|
301
|
+
|
302
|
+
Wieland2015_interaction_matrix2.summary()
|
303
|
+
# Summary of interaction matrix
|
304
|
+
|
305
|
+
huff_model_fit2 = Wieland2015_interaction_matrix2.marketareas()
|
306
|
+
# Calculation of market areas
|
307
|
+
|
308
|
+
huff_model_fit2.summary()
|
309
|
+
# Summary of Hudd model
|
310
|
+
|
311
|
+
|
312
|
+
# Loading and including total market areas
|
313
|
+
|
314
|
+
Wieland2025_totalmarketareas = load_marketareas(
|
315
|
+
data="data/Wieland2015.xlsx",
|
316
|
+
supply_locations_col="Zielort",
|
317
|
+
total_col="Anb_Eink",
|
318
|
+
data_type="xlsx",
|
319
|
+
xlsx_sheet="total_marketareas"
|
320
|
+
)
|
321
|
+
# Loading empirical total market areas
|
322
|
+
|
323
|
+
huff_model_fit2 = Wieland2025_totalmarketareas.add_to_model(
|
324
|
+
huff_model_fit2
|
325
|
+
)
|
326
|
+
# Adding total market areas to HuffModel object
|
327
|
+
|
328
|
+
print(huff_model_fit2.get_market_areas_df())
|
329
|
+
# Showing total market areas of HuffModel object
|
330
|
+
|
331
|
+
|
243
332
|
# Buffer analysis:
|
244
333
|
|
245
334
|
Haslach_supermarkets_gdf = Haslach_supermarkets.get_geodata_gpd_original()
|
@@ -298,7 +387,8 @@ map_with_basemap(
|
|
298
387
|
Haslach_supermarkets_gdf
|
299
388
|
],
|
300
389
|
styles={
|
301
|
-
0: {
|
390
|
+
0: {
|
391
|
+
"name": "Isochrones",
|
302
392
|
"color": {
|
303
393
|
"segm_min": {
|
304
394
|
"3": "midnightblue",
|
@@ -310,11 +400,13 @@ map_with_basemap(
|
|
310
400
|
},
|
311
401
|
"alpha": 0.3
|
312
402
|
},
|
313
|
-
1: {
|
403
|
+
1: {
|
404
|
+
"name": "Districts",
|
314
405
|
"color": "black",
|
315
406
|
"alpha": 1
|
316
407
|
},
|
317
|
-
2: {
|
408
|
+
2: {
|
409
|
+
"name": "Supermarket chains",
|
318
410
|
"color": {
|
319
411
|
"Name": {
|
320
412
|
"Aldi S├╝d": "blue",
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: huff
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.5.0
|
4
4
|
Summary: huff: Huff Model Market Area Analysis
|
5
5
|
Author: Thomas Wieland
|
6
6
|
Author-email: geowieland@googlemail.com
|
@@ -28,14 +28,25 @@ 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.0
|
32
|
+
- Extensions:
|
33
|
+
- Huff model utility via InteractionMatrix.utility() extended to >2 variables
|
34
|
+
- Huff Model Maximum Likelihood fit via InteractionMatrix.huff_ml_fit() extended: >2 variables, fit by flows or probabilities
|
35
|
+
- Loading total market areas data as class MarketAreas
|
36
|
+
- Extended output of InteractionMatrix.summary()
|
37
|
+
- Bugfixes:
|
38
|
+
- InteractionMatrix.utility(): Tests for availability of relevant columns
|
39
|
+
- InteractionMatrix.flows(): Tests for availability of relevant columns
|
40
|
+
- modelfit(): Symmetrical (M)APE instead of (M)APE when observed contains zeros
|
41
|
+
|
31
42
|
## Features
|
32
43
|
|
33
44
|
- **Huff Model**:
|
34
45
|
- Defining origins and destinations with weightings
|
35
46
|
- Creating interaction matrix from origins and destinations
|
36
|
-
- Market simulation with basic Huff Model
|
37
47
|
- Different function types: power, exponential, logistic
|
38
|
-
- Huff model parameter estimation via Maximum Likelihood (ML)
|
48
|
+
- Huff model parameter estimation via Maximum Likelihood (ML) by probalities and customer flows
|
49
|
+
- Huff model market simulation
|
39
50
|
- **Multiplicative Competitive Interaction Model**:
|
40
51
|
- Log-centering transformation of interaction matrix
|
41
52
|
- Fitting MCI model with >= 2 independent variables
|
@@ -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=fGQP6eZOkV9wRVNw_0jYY8zIW74VAR0MMQ9YR8Vjcn4,105491
|
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=xhJcUYQ6dJIg6cPMez8yRIcicvuXkww8NPHXmt-Qrjg,11955
|
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
|
@@ -23,8 +23,8 @@ huff/tests/data/Haslach_supermarkets.prj,sha256=2Jy1Vlzh7UxQ1MXpZ9UYLs2SxfrObj2x
|
|
23
23
|
huff/tests/data/Haslach_supermarkets.qmd,sha256=JlcOYzG4vI1NH1IuOpxwIPnJsCyC-pDRAI00TzEvNf0,2522
|
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
|
-
huff/tests/data/Wieland2015.xlsx,sha256=
|
27
|
-
huff-1.
|
28
|
-
huff-1.
|
29
|
-
huff-1.
|
30
|
-
huff-1.
|
26
|
+
huff/tests/data/Wieland2015.xlsx,sha256=jUt9YcRrYL99AjxzXKMXD3o5erjd9r_jYfnALdrTQ3o,24333
|
27
|
+
huff-1.5.0.dist-info/METADATA,sha256=Ig_hu8ssyzbtuAUUxMw3ykAHpthSoyMeSxAgeSw3P9o,6319
|
28
|
+
huff-1.5.0.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
|
29
|
+
huff-1.5.0.dist-info/top_level.txt,sha256=nlzX-PxZNFmIxANIJMySuIFPihd6qOBkRlhIC28NEsQ,5
|
30
|
+
huff-1.5.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|