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 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.4.1
8
- # Last update: 2025-06-16 17:43
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
- if "transport_costs" in interaction_matrix_metadata:
495
- print("----------------------------------")
496
- if interaction_matrix_metadata["transport_costs"]["network"]:
497
- print("Transport cost type Time")
498
- print("Transport cost unit " + interaction_matrix_metadata["transport_costs"]["time_unit"])
499
- else:
500
- print("Transport cost type Distance")
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 huff_loglik(
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 two or three parameter values")
1055
+ raise ValueError("Parameter 'params' must be a list or np.ndarray with at least 2 parameter values")
904
1056
 
905
- if len(params) == 2:
906
- param_gamma, param_lambda = params
907
- elif len(params) == 3:
908
- param_gamma, param_lambda, param_lambda2 = params
909
- else:
910
- raise ValueError("Parameter 'params' must be a list with two or three parameter values")
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) == 2:
1084
+ if len(params) >= 2:
1085
+
926
1086
  customer_origins_metadata["weighting"][0]["param"] = float(param_lambda)
1087
+
927
1088
  else:
928
- raise ValueError ("Huff Model with transport cost weightig of type " + customer_origins_metadata["weighting"][0]["func"] + " must have two input parameters")
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) == 3:
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
- raise ValueError("Huff Model with transport cost weightig of type " + customer_origins_metadata["weighting"][0]["func"] + " must have three input parameters")
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
- LL = loglik(
950
- observed = p_ij_emp,
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 ml_fit(
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
- if len(initial_params) > 3 or len(initial_params) < 2:
972
- raise ValueError("Parameter 'initial_params' must be a list with two or three entries")
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.huff_loglik,
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
- if len(initial_params) == 2:
1207
+ param_gamma = fitted_params[0]
1208
+ supply_locations_metadata["weighting"][0]["param"] = float(param_gamma)
991
1209
 
992
- param_gamma = fitted_params[0]
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 len (initial_params) == 3:
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 len(initial_params) == 3:
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 " + str(supply_locations_metadata["no_points"]))
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 not defined")
1473
+ print("Attraction column not defined")
1185
1474
  else:
1186
- print("Attraction column " + supply_locations_metadata["attraction_col"][0])
1187
- print("Customer origins " + str(customer_origins_metadata["no_points"]))
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 not defined")
1478
+ print("Market size column not defined")
1190
1479
  else:
1191
- print("Market size column " + customer_origins_metadata["marketsize_col"])
1480
+ print("Market size column " + customer_origins_metadata["marketsize_col"])
1192
1481
  print("----------------------------------")
1193
1482
 
1194
1483
  print("Partial utilities")
1195
- print(" Weights")
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 not defined")
1487
+ print("Attraction not defined")
1199
1488
  else:
1200
- print("Attraction " + str(round(supply_locations_metadata["weighting"][0]["param"], 3)) + " (" + supply_locations_metadata["weighting"][0]["func"] + ")")
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 not defined")
1492
+ print("Transport costs not defined")
1204
1493
  elif customer_origins_metadata["weighting"][0]["func"] in ["power", "exponential"]:
1205
- print("Transport costs " + str(round(customer_origins_metadata["weighting"][0]["param"],3)) + " (" + customer_origins_metadata["weighting"][0]["func"] + ")")
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 " + 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"] + ")")
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
- print("Mean absolute percentage error ", round(huff_modelfit[1]["MAPE"], 2))
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(self):
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 ("p_ij" in interaction_matrix_df.columns and "p_ij_emp" in interaction_matrix_df.columns):
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["p_ij_emp"],
1340
- interaction_matrix_df["p_ij"]
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
- print("Mean absolute percentage error ", round(mci_modelfit[1]["MAPE"], 2))
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
- interaction_matrix_df = pd.read_excel(data)
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
- check_vars(
1888
- interaction_matrix_df,
1889
- cols = cols_check
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
- APE = abs(observed-expected)/observed*100
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
- MAPE = float(np.mean(APE))
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
 
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.4.1
8
- # Last update: 2025-06-16 17:43
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
- #network=False,
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.ml_fit(
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="MA",
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: {"name": "Isochrones",
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: {"name": "Districts",
403
+ 1: {
404
+ "name": "Districts",
314
405
  "color": "black",
315
406
  "alpha": 1
316
407
  },
317
- 2: {"name": "Supermarket chains",
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.4.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=3IxZLUp8-sC-sy0qJ677-cYEi09cqNOuOw_QBvr-K5s,89975
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=nnOcZmhvEQMsnCf7YKnm-2vAY_h7FA7p7E2UPBDXLRU,9435
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=SaVM-Hi5dBTmf2bzszMnZ2Ec8NUE05S_5F2lQj0ayS0,19641
27
- huff-1.4.1.dist-info/METADATA,sha256=TMOldW_srTquKEghHkuMKyofG2MjUMUV4OKfdNUyFoU,5692
28
- huff-1.4.1.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
29
- huff-1.4.1.dist-info/top_level.txt,sha256=nlzX-PxZNFmIxANIJMySuIFPihd6qOBkRlhIC28NEsQ,5
30
- huff-1.4.1.dist-info/RECORD,,
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