huff 1.5.0__py3-none-any.whl → 1.5.2__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.0
8
- # Last update: 2025-06-25 18:32
7
+ # Version: 1.5.2
8
+ # Last update: 2025-07-02 21:09
9
9
  # Copyright (c) 2025 Thomas Wieland
10
10
  #-----------------------------------------------------------------------
11
11
 
@@ -528,15 +528,23 @@ class InteractionMatrix:
528
528
  if supply_locations_metadata["weighting"][0]["func"] is None and supply_locations_metadata["weighting"][0]["param"] is None:
529
529
  print("Attraction not defined")
530
530
  else:
531
- print("Attraction " + str(round(supply_locations_metadata["weighting"][0]["param"],3)) + " (" + supply_locations_metadata["weighting"][0]["func"] + ")")
531
+ if supply_locations_metadata["weighting"][0]["param"] is not None:
532
+ print("Attraction " + str(round(supply_locations_metadata["weighting"][0]["param"],3)) + " (" + supply_locations_metadata["weighting"][0]["func"] + ")")
533
+ else:
534
+ print("Attraction NA" + " (" + supply_locations_metadata["weighting"][0]["func"] + ")")
532
535
 
533
536
  if customer_origins_metadata["weighting"][0]["func"] is None and customer_origins_metadata["weighting"][0]["param"] is None:
534
537
  print("Transport costs not defined")
535
538
  elif customer_origins_metadata["weighting"][0]["func"] in ["power", "exponential"]:
536
- print("Transport costs " + str(round(customer_origins_metadata["weighting"][0]["param"],3)) + " (" + customer_origins_metadata["weighting"][0]["func"] + ")")
539
+ if customer_origins_metadata["weighting"][0]["param"] is not None:
540
+ print("Transport costs " + str(round(customer_origins_metadata["weighting"][0]["param"],3)) + " (" + customer_origins_metadata["weighting"][0]["func"] + ")")
541
+ else:
542
+ print("Transport costs NA" + " (" + customer_origins_metadata["weighting"][0]["func"] + ")")
537
543
  elif customer_origins_metadata["weighting"][0]["func"] == "logistic":
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
-
544
+ if customer_origins_metadata["weighting"][0]["param"] is not None:
545
+ 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"] + ")")
546
+ else:
547
+ print("Transport costs NA" + " (" + customer_origins_metadata["weighting"][0]["func"] + ")")
540
548
 
541
549
  attrac_vars = supply_locations_metadata["attraction_col"]
542
550
  attrac_vars_no = len(attrac_vars)
@@ -553,12 +561,25 @@ class InteractionMatrix:
553
561
  print(f"{attrac_vars[key][:16]:16} not defined")
554
562
 
555
563
  else:
564
+
565
+ if supply_locations_metadata["weighting"][key]["func"] is None and supply_locations_metadata["weighting"][key]["param"]:
566
+
567
+ print(f"{attrac_vars[key][:16]:16} not defined")
556
568
 
557
- name = supply_locations_metadata["weighting"][key]["name"]
558
- param = supply_locations_metadata["weighting"][key]["param"]
559
- func = supply_locations_metadata["weighting"][key]["func"]
569
+ else:
570
+
571
+ if supply_locations_metadata["weighting"][key]["param"] is not None:
572
+
573
+ name = supply_locations_metadata["weighting"][key]["name"]
574
+ param = supply_locations_metadata["weighting"][key]["param"]
575
+ func = supply_locations_metadata["weighting"][key]["func"]
576
+
577
+ print(f"{name[:16]:16} {round(param, 3)} ({func})")
578
+
579
+ else:
580
+
581
+ print(f"{attrac_vars[key][:16]:16} NA" + " (" + supply_locations_metadata["weighting"][0]["func"] + ")")
560
582
 
561
- print(f"{name[:16]:16} {round(param, 3)} ({func})")
562
583
 
563
584
  print("----------------------------------")
564
585
 
@@ -566,6 +587,8 @@ class InteractionMatrix:
566
587
  print("Parameter estimation")
567
588
  print("Fit function " + interaction_matrix_metadata["fit"]["function"])
568
589
  print("Fit by " + interaction_matrix_metadata["fit"]["fit_by"])
590
+ if interaction_matrix_metadata["fit"]["function"] == "huff_ml_fit":
591
+ print("Fit method " + interaction_matrix_metadata["fit"]["method"] + " (Converged: " + str(interaction_matrix_metadata["fit"]["minimize_success"]) + ")")
569
592
 
570
593
  def transport_costs(
571
594
  self,
@@ -688,7 +711,8 @@ class InteractionMatrix:
688
711
  vars_funcs = {
689
712
  0: {
690
713
  "name": "A_j",
691
- "func": "power"
714
+ "func": "power",
715
+ "param": 1
692
716
  },
693
717
  1: {
694
718
  "name": "t_ij",
@@ -711,9 +735,13 @@ class InteractionMatrix:
711
735
 
712
736
  supply_locations_metadata["weighting"][0]["name"] = vars_funcs[0]["name"]
713
737
  supply_locations_metadata["weighting"][0]["func"] = vars_funcs[0]["func"]
738
+ if "param" in vars_funcs[0]:
739
+ supply_locations_metadata["weighting"][0]["param"] = vars_funcs[0]["param"]
714
740
 
715
741
  customer_origins_metadata["weighting"][0]["name"] = vars_funcs[1]["name"]
716
742
  customer_origins_metadata["weighting"][0]["func"] = vars_funcs[1]["func"]
743
+ if "param" in vars_funcs[1]:
744
+ customer_origins_metadata["weighting"][0]["param"] = vars_funcs[1]["param"]
717
745
 
718
746
  if len(vars_funcs) > 2:
719
747
 
@@ -731,7 +759,9 @@ class InteractionMatrix:
731
759
 
732
760
  supply_locations_metadata["weighting"][key-1]["name"] = var["name"]
733
761
  supply_locations_metadata["weighting"][key-1]["func"] = var["func"]
734
- supply_locations_metadata["weighting"][key-1]["param"] = None
762
+
763
+ if "param" in var:
764
+ supply_locations_metadata["weighting"][key-1]["param"] = var["param"]
735
765
 
736
766
  self.supply_locations.metadata = supply_locations_metadata
737
767
  self.customer_origins.metadata = customer_origins_metadata
@@ -876,7 +906,8 @@ class InteractionMatrix:
876
906
 
877
907
  check_vars(
878
908
  df = interaction_matrix_df,
879
- cols = ["E_ij"]
909
+ cols = ["E_ij"],
910
+ check_zero = False
880
911
  )
881
912
 
882
913
  market_areas_df = pd.DataFrame(interaction_matrix_df.groupby("j")["E_ij"].sum())
@@ -1057,7 +1088,8 @@ class InteractionMatrix:
1057
1088
  if len(params) < 2:
1058
1089
  raise ValueError("Parameter 'params' must be a list or np.ndarray with at least 2 parameter values")
1059
1090
 
1060
- customer_origins_metadata = self.customer_origins.get_metadata()
1091
+ customer_origins = self.customer_origins
1092
+ customer_origins_metadata = customer_origins.get_metadata()
1061
1093
 
1062
1094
  param_gamma, param_lambda = params[0], params[1]
1063
1095
 
@@ -1071,10 +1103,7 @@ class InteractionMatrix:
1071
1103
  interaction_matrix_df = self.interaction_matrix_df
1072
1104
 
1073
1105
  supply_locations = self.supply_locations
1074
- supply_locations_metadata = supply_locations.get_metadata()
1075
-
1076
- customer_origins = self.customer_origins
1077
- customer_origins_metadata = customer_origins.get_metadata()
1106
+ supply_locations_metadata = supply_locations.get_metadata()
1078
1107
 
1079
1108
  supply_locations_metadata["weighting"][0]["param"] = float(param_gamma)
1080
1109
  supply_locations.metadata = supply_locations_metadata
@@ -1119,8 +1148,15 @@ class InteractionMatrix:
1119
1148
 
1120
1149
  customer_origins.metadata = customer_origins_metadata
1121
1150
 
1122
- p_ij_emp = interaction_matrix_df["p_ij"]
1123
- E_ij_emp = interaction_matrix_df["E_ij"]
1151
+ if "p_ij_emp" not in interaction_matrix_df.columns:
1152
+ p_ij_emp = interaction_matrix_df["p_ij"]
1153
+ else:
1154
+ p_ij_emp = interaction_matrix_df["p_ij_emp"]
1155
+
1156
+ if "E_ij_emp" not in interaction_matrix_df.columns:
1157
+ E_ij_emp = interaction_matrix_df["E_ij"]
1158
+ else:
1159
+ E_ij_emp = interaction_matrix_df["E_ij_emp"]
1124
1160
 
1125
1161
  interaction_matrix_copy = copy.deepcopy(self)
1126
1162
 
@@ -1144,10 +1180,12 @@ class InteractionMatrix:
1144
1180
  observed = p_ij_emp
1145
1181
  expected = p_ij
1146
1182
 
1147
- LL = loglik(
1183
+ modelfit_metrics = modelfit(
1148
1184
  observed = observed,
1149
1185
  expected = expected
1150
- )
1186
+ )
1187
+
1188
+ LL = modelfit_metrics[1]["LL"]
1151
1189
 
1152
1190
  return -LL
1153
1191
 
@@ -1171,7 +1209,10 @@ class InteractionMatrix:
1171
1209
  params_metadata_customer_origins = 1
1172
1210
  else:
1173
1211
  if customer_origins_metadata["weighting"][0]["param"] is not None:
1174
- params_metadata_customer_origins = len(customer_origins_metadata["weighting"][0]["param"])
1212
+ if isinstance(customer_origins_metadata["weighting"][0]["param"], (int, float)):
1213
+ params_metadata_customer_origins = 1
1214
+ else:
1215
+ params_metadata_customer_origins = len(customer_origins_metadata["weighting"][0]["param"])
1175
1216
 
1176
1217
  if customer_origins_metadata["weighting"][0]["func"] == "logistic":
1177
1218
  params_metadata_customer_origins = 2
@@ -1251,45 +1292,57 @@ class InteractionMatrix:
1251
1292
  print(f"Optimization via {method} algorithm succeeded with parameters: {', '.join(str(round(par, 3)) for par in param_results)}.")
1252
1293
 
1253
1294
  else:
1254
-
1255
- param_gamma = None
1256
- param_lambda = None
1257
-
1258
- supply_locations_metadata["weighting"][0]["param"] = param_gamma
1259
-
1260
- if customer_origins_metadata["weighting"][0]["func"] == "logistic":
1261
-
1262
- param_lambda2 = None
1263
- customer_origins_metadata["weighting"][0]["param"][0] = param_lambda
1264
- customer_origins_metadata["weighting"][0]["param"][1] = param_lambda2
1265
-
1266
- else:
1267
-
1268
- customer_origins_metadata["weighting"][0]["param"] = param_lambda
1269
1295
 
1270
1296
  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.")
1271
1297
 
1272
1298
  self.supply_locations.metadata = supply_locations_metadata
1273
- self.customer_origins.metadata = customer_origins_metadata
1299
+ self.customer_origins.metadata = customer_origins_metadata
1274
1300
 
1275
- if ml_result.success and update_estimates:
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
- }
1301
+ if update_estimates:
1302
+
1303
+ if "p_ij_emp" not in self.interaction_matrix_df.columns:
1285
1304
 
1286
-
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
-
1290
- self = self.utility()
1291
- self = self.probabilities()
1292
- self = self.flows()
1305
+ self.interaction_matrix_df["p_ij_emp"] = self.interaction_matrix_df["p_ij"]
1306
+
1307
+ print("NOTE: Probabilities in interaction matrix are treated as empirical probabilities")
1308
+
1309
+ else:
1310
+
1311
+ print("NOTE: Interaction matrix contains empirical probabilities")
1312
+
1313
+ if "E_ij_emp" not in self.interaction_matrix_df.columns:
1314
+
1315
+ self.interaction_matrix_df["E_ij_emp"] = self.interaction_matrix_df["E_ij"]
1316
+
1317
+ print("NOTE: Customer interactions in interaction matrix are treated as empirical interactions")
1318
+
1319
+ else:
1320
+
1321
+ print("NOTE: Interaction matrix contains empirical customer interactions")
1322
+
1323
+ if np.isnan(ml_result.x).any():
1324
+
1325
+ print("WARNING: No update of estimates because fit parameters contain NaN")
1326
+
1327
+ update_estimates = False
1328
+
1329
+ else:
1330
+
1331
+ self = self.utility()
1332
+ self = self.probabilities()
1333
+ self = self.flows()
1334
+
1335
+ self.metadata["fit"] = {
1336
+ "function": "huff_ml_fit",
1337
+ "fit_by": fit_by,
1338
+ "initial_params": initial_params,
1339
+ "method": method,
1340
+ "bounds": bounds,
1341
+ "constraints": constraints,
1342
+ "minimize_success": ml_result.success,
1343
+ "minimize_fittedparams": ml_result.x,
1344
+ "update_estimates": update_estimates
1345
+ }
1293
1346
 
1294
1347
  return self
1295
1348
 
@@ -1331,7 +1384,7 @@ class InteractionMatrix:
1331
1384
 
1332
1385
  if "transport_costs" not in interaction_matrix_metadata:
1333
1386
 
1334
- print("New destination(s) included. No transport costs calculation because not defined in original interaction matrix.")
1387
+ print("WARNING: New destination(s) included. No transport costs calculation because not defined in original interaction matrix.")
1335
1388
 
1336
1389
  interaction_matrix_df = pd.concat(
1337
1390
  [
@@ -1400,11 +1453,12 @@ class MarketAreas:
1400
1453
 
1401
1454
  def add_to_model(
1402
1455
  self,
1403
- model_object
1456
+ model_object,
1457
+ output_model = "Huff"
1404
1458
  ):
1405
1459
 
1406
- if not isinstance(model_object, (HuffModel, MCIModel)):
1407
- raise ValueError("Parameter 'interaction_matrix' must be of class HuffModel or MCIModel")
1460
+ if not isinstance(model_object, (HuffModel, MCIModel, InteractionMatrix)):
1461
+ raise ValueError("Parameter 'interaction_matrix' must be of class HuffModel, MCIModel, or InteractionMatrix")
1408
1462
 
1409
1463
  if isinstance(model_object, MCIModel):
1410
1464
 
@@ -1415,15 +1469,35 @@ class MarketAreas:
1415
1469
  market_areas_df = self.market_areas_df
1416
1470
  )
1417
1471
 
1418
- if isinstance(model_object, HuffModel):
1472
+ elif isinstance(model_object, HuffModel):
1419
1473
 
1420
1474
  model = HuffModel(
1421
1475
  interaction_matrix = model_object.interaction_matrix,
1422
1476
  market_areas_df = self.market_areas_df
1423
1477
  )
1478
+
1479
+ elif isinstance(model_object, InteractionMatrix):
1480
+
1481
+ if output_model not in ["Huff", "MCI"]:
1482
+ raise ValueError("Parameter 'output_model' must be either 'Huff' or 'MCI'")
1424
1483
 
1484
+ if output_model == "Huff":
1485
+
1486
+ model = HuffModel(
1487
+ interaction_matrix=model_object,
1488
+ market_areas_df=self.market_areas_df
1489
+ )
1490
+
1491
+ if output_model == "MCI":
1492
+
1493
+ model = MCIModel(
1494
+ coefs=model_object.coefs,
1495
+ mci_ols_model=model_object.mci_ols_model,
1496
+ market_areas_df=self.market_areas_df
1497
+ )
1498
+
1425
1499
  return model
1426
-
1500
+
1427
1501
  class HuffModel:
1428
1502
 
1429
1503
  def __init__(
@@ -1457,14 +1531,16 @@ class HuffModel:
1457
1531
  return customer_origins
1458
1532
 
1459
1533
  def get_market_areas_df(self):
1534
+
1460
1535
  return self.market_areas_df
1461
-
1536
+
1462
1537
  def summary(self):
1463
1538
 
1464
1539
  interaction_matrix = self.interaction_matrix
1465
1540
 
1466
1541
  customer_origins_metadata = interaction_matrix.get_customer_origins().get_metadata()
1467
1542
  supply_locations_metadata = interaction_matrix.get_supply_locations().get_metadata()
1543
+ interaction_matrix_metadata = interaction_matrix.get_metadata()
1468
1544
 
1469
1545
  print("Huff Model")
1470
1546
  print("----------------------------------")
@@ -1486,14 +1562,23 @@ class HuffModel:
1486
1562
  if supply_locations_metadata["weighting"][0]["func"] is None and supply_locations_metadata["weighting"][0]["param"] is None:
1487
1563
  print("Attraction not defined")
1488
1564
  else:
1489
- print("Attraction " + str(round(supply_locations_metadata["weighting"][0]["param"], 3)) + " (" + supply_locations_metadata["weighting"][0]["func"] + ")")
1565
+ if supply_locations_metadata["weighting"][0]["param"] is not None:
1566
+ print("Attraction " + str(round(supply_locations_metadata["weighting"][0]["param"],3)) + " (" + supply_locations_metadata["weighting"][0]["func"] + ")")
1567
+ else:
1568
+ print("Attraction NA" + " (" + supply_locations_metadata["weighting"][0]["func"] + ")")
1490
1569
 
1491
1570
  if customer_origins_metadata["weighting"][0]["func"] is None and customer_origins_metadata["weighting"][0]["param"] is None:
1492
1571
  print("Transport costs not defined")
1493
1572
  elif customer_origins_metadata["weighting"][0]["func"] in ["power", "exponential"]:
1494
- print("Transport costs " + str(round(customer_origins_metadata["weighting"][0]["param"],3)) + " (" + customer_origins_metadata["weighting"][0]["func"] + ")")
1573
+ if customer_origins_metadata["weighting"][0]["param"] is not None:
1574
+ print("Transport costs " + str(round(customer_origins_metadata["weighting"][0]["param"],3)) + " (" + customer_origins_metadata["weighting"][0]["func"] + ")")
1575
+ else:
1576
+ print("Transport costs NA" + " (" + customer_origins_metadata["weighting"][0]["func"] + ")")
1495
1577
  elif customer_origins_metadata["weighting"][0]["func"] == "logistic":
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"] + ")")
1578
+ if customer_origins_metadata["weighting"][0]["param"] is not None:
1579
+ 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"] + ")")
1580
+ else:
1581
+ print("Transport costs NA" + " (" + customer_origins_metadata["weighting"][0]["func"] + ")")
1497
1582
 
1498
1583
  attrac_vars = supply_locations_metadata["attraction_col"]
1499
1584
  attrac_vars_no = len(attrac_vars)
@@ -1518,39 +1603,46 @@ class HuffModel:
1518
1603
  print(f"{name[:16]:16} {round(param, 3)} ({func})")
1519
1604
 
1520
1605
  print("----------------------------------")
1521
-
1522
- huff_modelfit = self.modelfit()
1523
- if huff_modelfit is not None:
1606
+
1607
+ if interaction_matrix_metadata != {} and "fit" in interaction_matrix_metadata and interaction_matrix_metadata["fit"]["function"] is not None:
1608
+ print("Parameter estimation")
1609
+ print("Fit function " + interaction_matrix_metadata["fit"]["function"])
1610
+ print("Fit by " + interaction_matrix_metadata["fit"]["fit_by"])
1611
+ if interaction_matrix_metadata["fit"]["function"] == "huff_ml_fit":
1612
+ print("Fit method " + interaction_matrix_metadata["fit"]["method"] + " (Converged: " + str(interaction_matrix_metadata["fit"]["minimize_success"]) + ")")
1613
+
1614
+ huff_modelfit = self.modelfit(by = interaction_matrix_metadata["fit"]["fit_by"])
1524
1615
 
1525
- print ("Goodness-of-fit for probabilities")
1616
+ if huff_modelfit is not None:
1617
+
1618
+ print ("Goodness-of-fit for " + interaction_matrix_metadata["fit"]["fit_by"])
1619
+
1620
+ print("Sum of squared residuals ", round(huff_modelfit[1]["SQR"], 2))
1621
+ print("R-squared ", round(huff_modelfit[1]["Rsq"], 2))
1622
+ print("Mean squared error ", round(huff_modelfit[1]["MSE"], 2))
1623
+ print("Root mean squared error ", round(huff_modelfit[1]["RMSE"], 2))
1624
+ print("Mean absolute error ", round(huff_modelfit[1]["MAE"], 2))
1625
+ if huff_modelfit[1]["MAPE"] is not None:
1626
+ print("Mean absolute percentage error ", round(huff_modelfit[1]["MAPE"], 2))
1627
+ else:
1628
+ print("Mean absolute percentage error Not calculated")
1629
+ print("Symmetric MAPE ", round(huff_modelfit[1]["sMAPE"], 2))
1630
+ print("Absolute percentage errors")
1631
+
1632
+ APE_list = [
1633
+ ["< 5 % ", round(huff_modelfit[1]["APE"]["resid_below5"], 2), " < 30 % ", round(huff_modelfit[1]["APE"]["resid_below30"], 2)],
1634
+ ["< 10 % ", round(huff_modelfit[1]["APE"]["resid_below10"], 2), " < 35 % ", round(huff_modelfit[1]["APE"]["resid_below35"], 2)],
1635
+ ["< 15 % ", round(huff_modelfit[1]["APE"]["resid_below15"], 2), " < 40 % ", round(huff_modelfit[1]["APE"]["resid_below40"], 2)],
1636
+ ["< 20 % ", round(huff_modelfit[1]["APE"]["resid_below20"], 2), " < 45 % ", round(huff_modelfit[1]["APE"]["resid_below45"], 2)],
1637
+ ["< 25% ", round(huff_modelfit[1]["APE"]["resid_below25"], 2), " < 50 % ", round(huff_modelfit[1]["APE"]["resid_below50"], 2)]
1638
+ ]
1639
+ APE_df = pd.DataFrame(
1640
+ APE_list,
1641
+ columns=["Resid.", "%", "Resid.", "%"]
1642
+ )
1643
+ print(APE_df.to_string(index=False))
1526
1644
 
1527
- print("Sum of squared residuals ", round(huff_modelfit[1]["SQR"], 2))
1528
- print("Sum of squares ", round(huff_modelfit[1]["SQT"], 2))
1529
- print("R-squared ", round(huff_modelfit[1]["Rsq"], 2))
1530
- print("Mean squared error ", round(huff_modelfit[1]["MSE"], 2))
1531
- print("Root mean squared error ", round(huff_modelfit[1]["RMSE"], 2))
1532
- print("Mean absolute error ", round(huff_modelfit[1]["MAE"], 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))
1538
- print("Absolute percentage errors")
1539
-
1540
- APE_list = [
1541
- ["< 5 % ", round(huff_modelfit[1]["APE"]["resid_below5"], 2), " < 30 % ", round(huff_modelfit[1]["APE"]["resid_below30"], 2)],
1542
- ["< 10 % ", round(huff_modelfit[1]["APE"]["resid_below10"], 2), " < 35 % ", round(huff_modelfit[1]["APE"]["resid_below35"], 2)],
1543
- ["< 15 % ", round(huff_modelfit[1]["APE"]["resid_below15"], 2), " < 40 % ", round(huff_modelfit[1]["APE"]["resid_below40"], 2)],
1544
- ["< 20 % ", round(huff_modelfit[1]["APE"]["resid_below20"], 2), " < 45 % ", round(huff_modelfit[1]["APE"]["resid_below45"], 2)],
1545
- ["< 25% ", round(huff_modelfit[1]["APE"]["resid_below25"], 2), " < 50 % ", round(huff_modelfit[1]["APE"]["resid_below50"], 2)]
1546
- ]
1547
- APE_df = pd.DataFrame(
1548
- APE_list,
1549
- columns=["Resid.", "%", "Resid.", "%"]
1550
- )
1551
- print(APE_df.to_string(index=False))
1552
-
1553
- print("----------------------------------")
1645
+ print("----------------------------------")
1554
1646
 
1555
1647
  def mci_fit(
1556
1648
  self,
@@ -1642,6 +1734,287 @@ class HuffModel:
1642
1734
 
1643
1735
  return mci_model
1644
1736
 
1737
+ def loglik(
1738
+ self,
1739
+ params
1740
+ ):
1741
+
1742
+ if not isinstance(params, list):
1743
+ if isinstance(params, np.ndarray):
1744
+ params = params.tolist()
1745
+ else:
1746
+ raise ValueError("Parameter 'params' must be a list or np.ndarray with at least 2 parameter values")
1747
+
1748
+ if len(params) < 2:
1749
+ raise ValueError("Parameter 'params' must be a list or np.ndarray with at least 2 parameter values")
1750
+
1751
+ market_areas_df = self.market_areas_df
1752
+
1753
+ customer_origins = self.interaction_matrix.customer_origins
1754
+ customer_origins_metadata = customer_origins.get_metadata()
1755
+
1756
+ param_gamma, param_lambda = params[0], params[1]
1757
+
1758
+ if customer_origins_metadata["weighting"][0]["func"] == "logistic":
1759
+
1760
+ 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")
1762
+
1763
+ param_gamma, param_lambda, param_lambda2 = params[0], params[1], params[2]
1764
+
1765
+ supply_locations = self.interaction_matrix.supply_locations
1766
+ supply_locations_metadata = supply_locations.get_metadata()
1767
+
1768
+ supply_locations_metadata["weighting"][0]["param"] = float(param_gamma)
1769
+ supply_locations.metadata = supply_locations_metadata
1770
+
1771
+ if customer_origins_metadata["weighting"][0]["func"] in ["power", "exponential"]:
1772
+
1773
+ if len(params) >= 2:
1774
+
1775
+ customer_origins_metadata["weighting"][0]["param"] = float(param_lambda)
1776
+
1777
+ else:
1778
+
1779
+ raise ValueError ("Huff Model with transport cost weighting of type " + customer_origins_metadata["weighting"][0]["func"] + " must have >= 2 input parameters")
1780
+
1781
+ elif customer_origins_metadata["weighting"][0]["func"] == "logistic":
1782
+
1783
+ if len(params) >= 3:
1784
+
1785
+ customer_origins_metadata["weighting"][0]["param"] = [float(param_lambda), float(param_lambda2)]
1786
+
1787
+ else:
1788
+
1789
+ raise ValueError("Huff Model with transport cost weightig of type " + customer_origins_metadata["weighting"][0]["func"] + " must have >= 3 input parameters")
1790
+
1791
+ if (customer_origins_metadata["weighting"][0]["func"] in ["power", "exponential"] and len(params) > 2):
1792
+
1793
+ for key, param in enumerate(params):
1794
+
1795
+ if key <= 1:
1796
+ continue
1797
+
1798
+ supply_locations_metadata["weighting"][key-1]["param"] = float(param)
1799
+
1800
+ if (customer_origins_metadata["weighting"][0]["func"] == "logistic" and len(params) > 3):
1801
+
1802
+ for key, param in enumerate(params):
1803
+
1804
+ if key <= 2:
1805
+ continue
1806
+
1807
+ supply_locations_metadata["weighting"][key-2]["param"] = float(param)
1808
+
1809
+ customer_origins.metadata = customer_origins_metadata
1810
+
1811
+ if "T_j_emp" not in market_areas_df.columns:
1812
+ T_j_emp = market_areas_df["T_j"]
1813
+ else:
1814
+ T_j_emp = market_areas_df["T_j_emp"]
1815
+
1816
+
1817
+ huff_model_copy = copy.deepcopy(self)
1818
+
1819
+ interaction_matrix_copy = copy.deepcopy(huff_model_copy.interaction_matrix)
1820
+
1821
+ interaction_matrix_copy = interaction_matrix_copy.utility()
1822
+ interaction_matrix_copy = interaction_matrix_copy.probabilities()
1823
+ interaction_matrix_copy = interaction_matrix_copy.flows()
1824
+
1825
+ huff_model_copy = interaction_matrix_copy.marketareas()
1826
+
1827
+ market_areas_df_copy = huff_model_copy.market_areas_df
1828
+
1829
+ observed = T_j_emp
1830
+ expected = market_areas_df_copy["T_j"]
1831
+
1832
+ modelfit_metrics = modelfit(
1833
+ observed = observed,
1834
+ expected = expected
1835
+ )
1836
+
1837
+ LL = modelfit_metrics[1]["LL"]
1838
+
1839
+ return -LL
1840
+
1841
+ def ml_fit(
1842
+ self,
1843
+ initial_params: list = [1.0, -2.0],
1844
+ method: str = "L-BFGS-B",
1845
+ bounds: list = [(0.5, 1), (-3, -1)],
1846
+ constraints: list = [],
1847
+ fit_by = "probabilities",
1848
+ update_estimates: bool = True,
1849
+ check_numbers: bool = True
1850
+ ):
1851
+
1852
+ if fit_by in ["probabilities", "flows"]:
1853
+
1854
+ self.interaction_matrix.huff_ml_fit(
1855
+ initial_params = initial_params,
1856
+ method = method,
1857
+ bounds = bounds,
1858
+ constraints = constraints,
1859
+ fit_by = fit_by,
1860
+ update_estimates = update_estimates
1861
+ )
1862
+
1863
+ elif fit_by == "totals":
1864
+
1865
+ if check_numbers:
1866
+
1867
+ market_areas_df = self.market_areas_df
1868
+ interaction_matrix_df = self.get_interaction_matrix_df()
1869
+ T_j_market_areas_df = sum(market_areas_df["T_j"])
1870
+ T_j_interaction_matrix_df = sum(interaction_matrix_df["E_ij"])
1871
+
1872
+ if T_j_market_areas_df != T_j_interaction_matrix_df:
1873
+ print("WARNING: Sum of total market areas (" + str(int(T_j_market_areas_df)) + ") is not equal to sum of customer flows (" + str(int(T_j_interaction_matrix_df)) + ")")
1874
+
1875
+ supply_locations = self.interaction_matrix.supply_locations
1876
+ supply_locations_metadata = supply_locations.get_metadata()
1877
+
1878
+ customer_origins = self.interaction_matrix.customer_origins
1879
+ customer_origins_metadata = customer_origins.get_metadata()
1880
+
1881
+ if customer_origins_metadata["weighting"][0]["param"] is None:
1882
+ params_metadata_customer_origins = 1
1883
+ else:
1884
+ if customer_origins_metadata["weighting"][0]["param"] is not None:
1885
+ if isinstance(customer_origins_metadata["weighting"][0]["param"], (int, float)):
1886
+ params_metadata_customer_origins = 1
1887
+ else:
1888
+ params_metadata_customer_origins = len(customer_origins_metadata["weighting"][0]["param"])
1889
+
1890
+ if customer_origins_metadata["weighting"][0]["func"] == "logistic":
1891
+ params_metadata_customer_origins = 2
1892
+ else:
1893
+ params_metadata_customer_origins = 1
1894
+
1895
+ params_metadata_supply_locations = len(supply_locations_metadata["weighting"])
1896
+
1897
+ params_metadata = params_metadata_customer_origins+params_metadata_supply_locations
1898
+
1899
+ 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) + ")")
1901
+
1902
+ 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)) + ")")
1904
+
1905
+ ml_result = minimize(
1906
+ self.loglik,
1907
+ initial_params,
1908
+ method = method,
1909
+ bounds = bounds,
1910
+ constraints = constraints,
1911
+ options={'disp': 3}
1912
+ )
1913
+
1914
+ attrac_vars = len(supply_locations_metadata["weighting"])
1915
+
1916
+ if ml_result.success:
1917
+
1918
+ fitted_params = ml_result.x
1919
+
1920
+ param_gamma = fitted_params[0]
1921
+ supply_locations_metadata["weighting"][0]["param"] = float(param_gamma)
1922
+
1923
+ if customer_origins_metadata["weighting"][0]["func"] in ["power", "exponential"]:
1924
+
1925
+ param_lambda = fitted_params[1]
1926
+ param_results = [
1927
+ float(param_gamma),
1928
+ float(param_lambda)
1929
+ ]
1930
+
1931
+ customer_origins_metadata["weighting"][0]["param"] = float(param_lambda)
1932
+
1933
+ elif customer_origins_metadata["weighting"][0]["func"] == "logistic":
1934
+
1935
+ param_lambda = fitted_params[1]
1936
+ param_lambda2 = fitted_params[2]
1937
+ param_results = [
1938
+ float(param_gamma),
1939
+ float(param_lambda),
1940
+ float(param_lambda2)
1941
+ ]
1942
+
1943
+ customer_origins_metadata["weighting"][0]["param"][0] = float(param_lambda)
1944
+ customer_origins_metadata["weighting"][0]["param"][1] = float(param_lambda2)
1945
+
1946
+ if attrac_vars > 1:
1947
+
1948
+ if customer_origins_metadata["weighting"][0]["func"] == "logistic":
1949
+ fitted_params_add = 3
1950
+ else:
1951
+ fitted_params_add = 2
1952
+
1953
+ for key, var in supply_locations_metadata["weighting"].items():
1954
+
1955
+ if key > len(supply_locations_metadata["weighting"])-fitted_params_add:
1956
+ break
1957
+
1958
+ param = float(fitted_params[key+fitted_params_add])
1959
+
1960
+ param_results = param_results + [param]
1961
+
1962
+ supply_locations_metadata["weighting"][(key+1)]["param"] = float(param)
1963
+
1964
+ print(f"Optimization via {method} algorithm succeeded with parameters: {', '.join(str(round(par, 3)) for par in param_results)}.")
1965
+
1966
+ else:
1967
+
1968
+ 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.")
1969
+
1970
+ self.interaction_matrix.supply_locations.metadata = supply_locations_metadata
1971
+ self.interaction_matrix.customer_origins.metadata = customer_origins_metadata
1972
+
1973
+ if update_estimates:
1974
+
1975
+ if "T_j_emp" not in self.market_areas_df.columns:
1976
+
1977
+ self.market_areas_df["T_j_emp"] = self.market_areas_df["T_j"]
1978
+
1979
+ print("NOTE: Total values in market areas df are treated as empirical total values")
1980
+
1981
+ else:
1982
+
1983
+ print("NOTE: Total market areas df contains empirical total values")
1984
+
1985
+ if np.isnan(ml_result.x).any():
1986
+
1987
+ print("WARNING: No update of estimates because fit parameters contain NaN")
1988
+
1989
+ update_estimates = False
1990
+
1991
+ else:
1992
+
1993
+ self.interaction_matrix.utility()
1994
+ self.interaction_matrix.probabilities()
1995
+ self.interaction_matrix.flows()
1996
+
1997
+ huff_model_new_marketareas = self.interaction_matrix.marketareas()
1998
+ self.market_areas_df["T_j"] = huff_model_new_marketareas.get_market_areas_df()["T_j"]
1999
+
2000
+ self.interaction_matrix.metadata["fit"] = {
2001
+ "function": "huff_ml_fit",
2002
+ "fit_by": fit_by,
2003
+ "initial_params": initial_params,
2004
+ "method": method,
2005
+ "bounds": bounds,
2006
+ "constraints": constraints,
2007
+ "minimize_success": ml_result.success,
2008
+ "minimize_fittedparams": ml_result.x,
2009
+ "update_estimates": update_estimates
2010
+ }
2011
+
2012
+ else:
2013
+
2014
+ raise ValueError("Parameter 'fit_by' must be 'probabilities', 'flows' or 'totals'")
2015
+
2016
+ return self
2017
+
1645
2018
  def update(self):
1646
2019
 
1647
2020
  self.interaction_matrix = self.interaction_matrix.update()
@@ -1652,31 +2025,92 @@ class HuffModel:
1652
2025
 
1653
2026
  def modelfit(
1654
2027
  self,
1655
- by = "p_ij"
1656
- ):
1657
-
1658
- interaction_matrix = self.interaction_matrix
1659
- interaction_matrix_df = interaction_matrix.get_interaction_matrix_df()
2028
+ by = "probabilities"
2029
+ ):
1660
2030
 
1661
- if (by in interaction_matrix_df.columns and by+"_emp" in interaction_matrix_df.columns):
2031
+ if by == "probabilities":
2032
+
2033
+ interaction_matrix = self.interaction_matrix
2034
+ interaction_matrix_df = interaction_matrix.get_interaction_matrix_df()
2035
+
2036
+ if ("p_ij" in interaction_matrix_df.columns and "p_ij_emp" in interaction_matrix_df.columns):
2037
+
2038
+ try:
2039
+
2040
+ huff_modelfit = modelfit(
2041
+ interaction_matrix_df["p_ij_emp"],
2042
+ interaction_matrix_df["p_ij"]
2043
+ )
2044
+
2045
+ return huff_modelfit
2046
+
2047
+ except:
2048
+
2049
+ print("WARNING: Goodness-of-fit metrics could not be calculated due to NaN values.")
2050
+ return None
1662
2051
 
1663
- try:
2052
+ else:
2053
+
2054
+ print("WARNING: Goodness-of-fit metrics could not be calculated. No empirical values of probabilities in interaction matrix.")
2055
+
2056
+ return None
1664
2057
 
1665
- huff_modelfit = modelfit(
1666
- interaction_matrix_df[by+"_emp"],
1667
- interaction_matrix_df[by]
1668
- )
2058
+ elif by == "flows":
2059
+
2060
+ interaction_matrix = self.interaction_matrix
2061
+ interaction_matrix_df = interaction_matrix.get_interaction_matrix_df()
2062
+
2063
+ if ("E_ij" in interaction_matrix_df.columns and "E_ij_emp" in interaction_matrix_df.columns):
1669
2064
 
1670
- return huff_modelfit
2065
+ try:
1671
2066
 
1672
- except:
2067
+ huff_modelfit = modelfit(
2068
+ interaction_matrix_df["E_ij_emp"],
2069
+ interaction_matrix_df["E_ij"]
2070
+ )
2071
+
2072
+ return huff_modelfit
2073
+
2074
+ except:
2075
+
2076
+ print("WARNING: Goodness-of-fit metrics could not be calculated due to NaN values.")
2077
+ return None
2078
+
2079
+ else:
1673
2080
 
1674
- print("Goodness-of-fit metrics could not be calculated due to NaN values.")
2081
+ print("WARNING: Goodness-of-fit metrics could not be calculated. No empirical values of customer flows in interaction matrix.")
2082
+
1675
2083
  return None
1676
-
1677
- else:
1678
2084
 
1679
- return None
2085
+ elif by == "totals":
2086
+
2087
+ market_areas_df = self.market_areas_df
2088
+
2089
+ if ("T_j" in market_areas_df.columns and "T_j_emp" in market_areas_df.columns):
2090
+
2091
+ try:
2092
+
2093
+ huff_modelfit = modelfit(
2094
+ market_areas_df["T_j_emp"],
2095
+ market_areas_df["T_j"]
2096
+ )
2097
+
2098
+ return huff_modelfit
2099
+
2100
+ except:
2101
+
2102
+ print("WARNING: Goodness-of-fit metrics could not be calculated due to NaN values.")
2103
+ return None
2104
+
2105
+ else:
2106
+
2107
+ print("WARNING: Goodness-of-fit metrics could not be calculated. No empirical values of T_j in market areas data.")
2108
+
2109
+ return None
2110
+
2111
+ else:
2112
+
2113
+ raise ValueError("Parameter 'by' must be 'probabilities', 'flows', or 'totals'")
1680
2114
 
1681
2115
  class MCIModel:
1682
2116
 
@@ -1744,7 +2178,7 @@ class MCIModel:
1744
2178
 
1745
2179
  except:
1746
2180
 
1747
- print("Goodness-of-fit metrics could not be calculated due to NaN values.")
2181
+ print("WARNING: Goodness-of-fit metrics could not be calculated due to NaN values.")
1748
2182
  return None
1749
2183
 
1750
2184
  else:
@@ -1794,7 +2228,6 @@ class MCIModel:
1794
2228
  print ("Goodness-of-fit for probabilities")
1795
2229
 
1796
2230
  print("Sum of squared residuals ", round(mci_modelfit[1]["SQR"], 2))
1797
- print("Sum of squares ", round(mci_modelfit[1]["SQT"], 2))
1798
2231
  print("R-squared ", round(mci_modelfit[1]["Rsq"], 2))
1799
2232
  print("Mean squared error ", round(mci_modelfit[1]["MSE"], 2))
1800
2233
  print("Root mean squared error ", round(mci_modelfit[1]["RMSE"], 2))
@@ -1890,8 +2323,11 @@ class MCIModel:
1890
2323
  interaction_matrix = self.interaction_matrix
1891
2324
  interaction_matrix_df = interaction_matrix.get_interaction_matrix_df()
1892
2325
 
1893
- if "p_ij" in interaction_matrix_df.columns:
2326
+ if "p_ij" in interaction_matrix_df.columns and "p_ij_emp" not in interaction_matrix_df.columns:
2327
+ print("NOTE: Probabilities in interaction matrix are treated as empirical probabilities")
1894
2328
  interaction_matrix_df["p_ij_emp"] = interaction_matrix_df["p_ij"]
2329
+ else:
2330
+ print("NOTE: Interaction matrix contains empirical probabilities")
1895
2331
 
1896
2332
  if "U_ij" not in interaction_matrix_df.columns:
1897
2333
  self.utility(transformation = transformation)
@@ -2666,7 +3102,8 @@ def get_isochrones(
2666
3102
  def modelfit(
2667
3103
  observed,
2668
3104
  expected,
2669
- remove_nan: bool = True
3105
+ remove_nan: bool = True,
3106
+ verbose: bool = False
2670
3107
  ):
2671
3108
 
2672
3109
  observed_no = len(observed)
@@ -2692,6 +3129,10 @@ def modelfit(
2692
3129
  )
2693
3130
 
2694
3131
  obs_exp_clean = obs_exp.dropna(subset=["observed", "expected"])
3132
+
3133
+ if len(obs_exp_clean) < len(observed) or len(obs_exp_clean) < len(expected):
3134
+ if verbose:
3135
+ print("NOTE: Vectors 'observed' and/or 'expected' contain zeros which are dropped.")
2695
3136
 
2696
3137
  observed = obs_exp_clean["observed"].to_numpy()
2697
3138
  expected = obs_exp_clean["expected"].to_numpy()
@@ -2708,7 +3149,8 @@ def modelfit(
2708
3149
  residuals_abs = abs(residuals)
2709
3150
 
2710
3151
  if any(observed == 0):
2711
- print ("Vector 'observed' contains values equal to zero. No APE/MAPE calculated.")
3152
+ if verbose:
3153
+ print ("Vector 'observed' contains values equal to zero. No APE/MAPE calculated.")
2712
3154
  APE = np.full_like(observed, np.nan)
2713
3155
  MAPE = None
2714
3156
  else:
@@ -2735,6 +3177,7 @@ def modelfit(
2735
3177
  MSE = float(SQR/observed_no)
2736
3178
  RMSE = float(sqrt(MSE))
2737
3179
  MAE = float(SAR/observed_no)
3180
+ LL = np.sum(np.log(residuals_sq))
2738
3181
 
2739
3182
  sMAPE = float(np.mean(sAPE))
2740
3183
 
@@ -2759,6 +3202,7 @@ def modelfit(
2759
3202
  "MAE": MAE,
2760
3203
  "MAPE": MAPE,
2761
3204
  "sMAPE": sMAPE,
3205
+ "LL": -LL,
2762
3206
  "APE": {
2763
3207
  "resid_below5": resid_below5,
2764
3208
  "resid_below10": resid_below10,
@@ -2780,34 +3224,23 @@ def modelfit(
2780
3224
 
2781
3225
  return modelfit_results
2782
3226
 
2783
- def loglik(
2784
- observed,
2785
- expected
2786
- ):
2787
-
2788
- model_fit = modelfit(
2789
- observed,
2790
- expected
2791
- )
2792
- residuals_sq = model_fit[0]["residuals_sq"]
2793
-
2794
- LL = np.sum(np.log(residuals_sq))
2795
-
2796
- return -LL
2797
-
2798
3227
  def check_vars(
2799
3228
  df: pd.DataFrame,
2800
- cols: list
3229
+ cols: list,
3230
+ check_numeric: bool = True,
3231
+ check_zero: bool = True
2801
3232
  ):
2802
3233
 
2803
3234
  for col in cols:
2804
3235
  if col not in df.columns:
2805
3236
  raise KeyError(f"Column '{col}' not in dataframe.")
2806
3237
 
2807
- for col in cols:
2808
- if not pd.api.types.is_numeric_dtype(df[col]):
2809
- raise ValueError(f"Column '{col}' is not numeric. All stated columns must be numeric.")
3238
+ if check_numeric:
3239
+ for col in cols:
3240
+ if not pd.api.types.is_numeric_dtype(df[col]):
3241
+ raise ValueError(f"Column '{col}' is not numeric. All stated columns must be numeric.")
2810
3242
 
2811
- for col in cols:
2812
- if (df[col] <= 0).any():
2813
- raise ValueError(f"Column '{col}' includes values <= 0. All values must be numeric and positive.")
3243
+ if check_zero:
3244
+ for col in cols:
3245
+ if (df[col] <= 0).any():
3246
+ raise ValueError(f"Column '{col}' includes values <= 0. All values must be numeric and positive.")
Binary file
huff/tests/tests_huff.py CHANGED
@@ -4,18 +4,17 @@
4
4
  # Author: Thomas Wieland
5
5
  # ORCID: 0000-0001-5168-9846
6
6
  # mail: geowieland@googlemail.com
7
- # Version: 1.5.0
8
- # Last update: 2025-06-25 18:32
7
+ # Version: 1.5.2
8
+ # Last update: 2025-07-02 21:10
9
9
  # Copyright (c) 2025 Thomas Wieland
10
10
  #-----------------------------------------------------------------------
11
11
 
12
-
13
12
  from huff.models import create_interaction_matrix, get_isochrones, load_geodata, load_interaction_matrix, load_marketareas, market_shares, modelfit
14
13
  from huff.osm import map_with_basemap
15
14
  from huff.gistools import buffers, point_spatial_join
16
15
 
17
16
 
18
- # Customer origins (statistical districts):
17
+ # Dealing with customer origins (statistical districts):
19
18
 
20
19
  Haslach = load_geodata(
21
20
  "data/Haslach.shp",
@@ -39,10 +38,8 @@ Haslach.define_marketsize("pop")
39
38
  # Definition of market size variable
40
39
 
41
40
  Haslach.define_transportcosts_weighting(
42
- # param_lambda = -2.2,
43
- # # one weighting parameter for power function (default)
44
- param_lambda = [10, -0.5],
45
- func="logistic"
41
+ param_lambda = -2.2,
42
+ # one weighting parameter for power function (default)
46
43
  # two weighting parameters for logistic function
47
44
  )
48
45
  # Definition of transport costs weighting (lambda)
@@ -51,7 +48,7 @@ Haslach.summary()
51
48
  # Summary after update
52
49
 
53
50
 
54
- # Supply locations (supermarkets):
51
+ # Dealing with upply locations (supermarkets):
55
52
 
56
53
  Haslach_supermarkets = load_geodata(
57
54
  "data/Haslach_supermarkets.shp",
@@ -87,9 +84,7 @@ Haslach_supermarkets.summary()
87
84
  # Summary of updated customer origins
88
85
 
89
86
  Haslach_supermarkets_isochrones = Haslach_supermarkets.get_isochrones_gdf()
90
- # Extracting isochrones
91
-
92
- print(Haslach_supermarkets_isochrones)
87
+ # Extracting isochrones as gdf
93
88
 
94
89
 
95
90
  # Using customer origins and supply locations for building interaction matrix:
@@ -101,8 +96,8 @@ haslach_interactionmatrix = create_interaction_matrix(
101
96
  # Creating interaction matrix
102
97
 
103
98
  haslach_interactionmatrix.transport_costs(
104
- #ors_auth="5b3ce3597851110001cf62480a15aafdb5a64f4d91805929f8af6abd"
105
- network=False,
99
+ ors_auth="5b3ce3597851110001cf62480a15aafdb5a64f4d91805929f8af6abd",
100
+ network=True,
106
101
  #distance_unit="meters",
107
102
  # set network = True to calculate transport costs matrix via ORS API (default)
108
103
  )
@@ -132,11 +127,9 @@ print(huff_model.get_market_areas_df())
132
127
  # Maximum Likelihood fit for Huff Model:
133
128
 
134
129
  haslach_interactionmatrix.huff_ml_fit(
135
- #initial_params=[1, -2],
136
- initial_params=[1, 9, -0.6],
130
+ initial_params=[1, -2],
137
131
  method="trust-constr",
138
- #bounds = [(0.8, 0.9999),(-2.5, -1.5)],
139
- bounds = [(0.8, 0.9999),(7, 11),(-0.9, -0.4)],
132
+ bounds = [(0.8, 0.9999),(-2.5, -1.5)]
140
133
  )
141
134
  # Maximum Likelihood fit for Huff Model
142
135
 
@@ -218,10 +211,14 @@ Wieland2015_interaction_matrix = load_interaction_matrix(
218
211
  probabilities_col="MA_Anb1",
219
212
  data_type="xlsx"
220
213
  )
214
+ # Loading interaction matrix from XLSX file
221
215
 
222
216
  Wieland2015_interaction_matrix.summary()
223
217
  # Summary of interaction matrix
224
218
 
219
+
220
+ # Parameter estimation via MCI model:
221
+
225
222
  Wieland2015_fit = Wieland2015_interaction_matrix.mci_fit(
226
223
  cols=[
227
224
  "A_j",
@@ -289,15 +286,16 @@ Wieland2015_interaction_matrix2.define_weightings(
289
286
 
290
287
  Wieland2015_interaction_matrix2.huff_ml_fit(
291
288
  # 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)],
289
+ initial_params=[0.9, -1.5, 0.5, 0.3],
290
+ bounds=[(0.5, 1), (-2, -1), (0.2, 0.7), (0.2, 0.7)],
294
291
  # # Logistic TC function:
295
292
  # initial_params=[0.9, 10, -0.5, 0.5, 0.3],
296
293
  # 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"
294
+ fit_by="probabilities",
295
+ #method = "trust-constr"
299
296
  )
300
- # ML fit with power transport cost function
297
+ # ML fit with power transport cost function based on probabilities
298
+ # from InteractionMatrix object
301
299
 
302
300
  Wieland2015_interaction_matrix2.summary()
303
301
  # Summary of interaction matrix
@@ -305,6 +303,19 @@ Wieland2015_interaction_matrix2.summary()
305
303
  huff_model_fit2 = Wieland2015_interaction_matrix2.marketareas()
306
304
  # Calculation of market areas
307
305
 
306
+ huff_model_fit2 = huff_model_fit2.ml_fit(
307
+ # Power TC function
308
+ initial_params=[0.9, -1.5, 0.5, 0.3],
309
+ bounds=[(0.5, 1), (-2, -1), (0.2, 0.7), (0.2, 0.7)],
310
+ # # Logistic TC function:
311
+ # initial_params=[0.9, 10, -0.5, 0.5, 0.3],
312
+ # bounds=[(0.5, 1), (8, 12), (-0.7, -0.2), (0.2, 0.7), (0.2, 0.7)],
313
+ fit_by="probabilities",
314
+ #method = "trust-constr"
315
+ )
316
+ # ML fit with power transport cost function based on probabilities
317
+ # from HuffModel object
318
+
308
319
  huff_model_fit2.summary()
309
320
  # Summary of Hudd model
310
321
 
@@ -328,6 +339,24 @@ huff_model_fit2 = Wieland2025_totalmarketareas.add_to_model(
328
339
  print(huff_model_fit2.get_market_areas_df())
329
340
  # Showing total market areas of HuffModel object
330
341
 
342
+ huff_model_fit3 = huff_model_fit2.ml_fit(
343
+ # Power TC function
344
+ initial_params=[0.9, -1.5, 0.5, 0.3],
345
+ bounds=[(0.5, 1), (-2, -1), (0.2, 0.7), (0.2, 0.7)],
346
+ # # Logistic TC function:
347
+ # initial_params=[0.9, 10, -0.5, 0.5, 0.3],
348
+ # bounds=[(0.5, 1), (8, 12), (-0.7, -0.2), (0.2, 0.7), (0.2, 0.7)],
349
+ fit_by="totals",
350
+ #method = "trust-constr"
351
+ )
352
+ # Fit Huff model by totals
353
+
354
+ huff_model_fit3.summary()
355
+ # Show summary
356
+
357
+ print(huff_model_fit3.get_market_areas_df())
358
+ # Show market areas df
359
+
331
360
 
332
361
  # Buffer analysis:
333
362
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: huff
3
- Version: 1.5.0
3
+ Version: 1.5.2
4
4
  Summary: huff: Huff Model Market Area Analysis
5
5
  Author: Thomas Wieland
6
6
  Author-email: geowieland@googlemail.com
@@ -28,16 +28,11 @@ 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()
31
+ ## Updates v1.5.2
37
32
  - 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
33
+ - HuffModel.ml_fit(): Correct values of expected T_j, corrected calculation of model fit metrices when fit_by="totals"
34
+ - HuffModel.ml_fit(): Check if sum of E_ij != sum of T_j
35
+
41
36
 
42
37
  ## Features
43
38
 
@@ -45,7 +40,7 @@ See the /tests directory for usage examples of most of the included functions.
45
40
  - Defining origins and destinations with weightings
46
41
  - Creating interaction matrix from origins and destinations
47
42
  - Different function types: power, exponential, logistic
48
- - Huff model parameter estimation via Maximum Likelihood (ML) by probalities and customer flows
43
+ - Huff model parameter estimation via Maximum Likelihood (ML) by probalities, customer flows, and total market areas
49
44
  - Huff model market simulation
50
45
  - **Multiplicative Competitive Interaction Model**:
51
46
  - Log-centering transformation of interaction matrix
@@ -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=fGQP6eZOkV9wRVNw_0jYY8zIW74VAR0MMQ9YR8Vjcn4,105491
3
+ huff/models.py,sha256=e8aILi45qcJ9tvHJfKIFKWfD-DYXjZQ0gXOS4MpG7Ks,125430
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=xhJcUYQ6dJIg6cPMez8yRIcicvuXkww8NPHXmt-Qrjg,11955
7
+ huff/tests/tests_huff.py,sha256=xHCR087rqLNWDFfZhi1giKDzffCx3IemWQmHrAUYxFw,12956
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=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,,
26
+ huff/tests/data/Wieland2015.xlsx,sha256=H4rxCFlctn44-O6mIyeFf67FlgvznLX7xZqpoWYS41A,25788
27
+ huff-1.5.2.dist-info/METADATA,sha256=XnlmcfscK8c1P3EN40W8JcQnFE7AkWDT4NqLR9skTIY,5956
28
+ huff-1.5.2.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
29
+ huff-1.5.2.dist-info/top_level.txt,sha256=nlzX-PxZNFmIxANIJMySuIFPihd6qOBkRlhIC28NEsQ,5
30
+ huff-1.5.2.dist-info/RECORD,,
File without changes