huff 1.5.0__py3-none-any.whl → 1.5.1__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.1
8
+ # Last update: 2025-07-01 17:10
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
@@ -1252,44 +1293,63 @@ class InteractionMatrix:
1252
1293
 
1253
1294
  else:
1254
1295
 
1255
- param_gamma = None
1256
- param_lambda = None
1296
+ # param_gamma = None
1297
+ # param_lambda = None
1257
1298
 
1258
- supply_locations_metadata["weighting"][0]["param"] = param_gamma
1299
+ # supply_locations_metadata["weighting"][0]["param"] = param_gamma
1259
1300
 
1260
- if customer_origins_metadata["weighting"][0]["func"] == "logistic":
1301
+ # if customer_origins_metadata["weighting"][0]["func"] == "logistic":
1261
1302
 
1262
- param_lambda2 = None
1263
- customer_origins_metadata["weighting"][0]["param"][0] = param_lambda
1264
- customer_origins_metadata["weighting"][0]["param"][1] = param_lambda2
1303
+ # param_lambda2 = None
1304
+ # customer_origins_metadata["weighting"][0]["param"][0] = param_lambda
1305
+ # customer_origins_metadata["weighting"][0]["param"][1] = param_lambda2
1265
1306
 
1266
- else:
1307
+ # else:
1267
1308
 
1268
- customer_origins_metadata["weighting"][0]["param"] = param_lambda
1309
+ # customer_origins_metadata["weighting"][0]["param"] = param_lambda
1269
1310
 
1270
1311
  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
1312
 
1272
1313
  self.supply_locations.metadata = supply_locations_metadata
1273
- self.customer_origins.metadata = customer_origins_metadata
1314
+ self.customer_origins.metadata = customer_origins_metadata
1274
1315
 
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
- }
1285
-
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()
1316
+ if update_estimates:
1317
+
1318
+ if "p_ij_emp" not in self.interaction_matrix_df.columns:
1319
+ self.interaction_matrix_df["p_ij_emp"] = self.interaction_matrix_df["p_ij"]
1320
+ print("Probabilties in interaction matrix are treated as empirical probabilties")
1321
+ else:
1322
+ print("Interaction matrix contains empirical probabilties")
1323
+
1324
+ if "E_ij_emp" not in self.interaction_matrix_df.columns:
1325
+ self.interaction_matrix_df["E_ij_emp"] = self.interaction_matrix_df["E_ij"]
1326
+ print("Customer interactions in interaction matrix are treated as empirical interactions")
1327
+ else:
1328
+ print("Interaction matrix contains empirical customer interactions")
1329
+
1330
+ if np.isnan(ml_result.x).any():
1331
+
1332
+ print("No update of estimates because fit parameters contain NaN")
1333
+
1334
+ update_estimates = False
1335
+
1336
+ else:
1337
+
1338
+ self = self.utility()
1339
+ self = self.probabilities()
1340
+ self = self.flows()
1341
+
1342
+ self.metadata["fit"] = {
1343
+ "function": "huff_ml_fit",
1344
+ "fit_by": fit_by,
1345
+ "initial_params": initial_params,
1346
+ "method": method,
1347
+ "bounds": bounds,
1348
+ "constraints": constraints,
1349
+ "minimize_success": ml_result.success,
1350
+ "minimize_fittedparams": ml_result.x,
1351
+ "update_estimates": update_estimates
1352
+ }
1293
1353
 
1294
1354
  return self
1295
1355
 
@@ -1400,11 +1460,12 @@ class MarketAreas:
1400
1460
 
1401
1461
  def add_to_model(
1402
1462
  self,
1403
- model_object
1463
+ model_object,
1464
+ output_model = "Huff"
1404
1465
  ):
1405
1466
 
1406
- if not isinstance(model_object, (HuffModel, MCIModel)):
1407
- raise ValueError("Parameter 'interaction_matrix' must be of class HuffModel or MCIModel")
1467
+ if not isinstance(model_object, (HuffModel, MCIModel, InteractionMatrix)):
1468
+ raise ValueError("Parameter 'interaction_matrix' must be of class HuffModel, MCIModel, or InteractionMatrix")
1408
1469
 
1409
1470
  if isinstance(model_object, MCIModel):
1410
1471
 
@@ -1415,15 +1476,35 @@ class MarketAreas:
1415
1476
  market_areas_df = self.market_areas_df
1416
1477
  )
1417
1478
 
1418
- if isinstance(model_object, HuffModel):
1479
+ elif isinstance(model_object, HuffModel):
1419
1480
 
1420
1481
  model = HuffModel(
1421
1482
  interaction_matrix = model_object.interaction_matrix,
1422
1483
  market_areas_df = self.market_areas_df
1423
1484
  )
1485
+
1486
+ elif isinstance(model_object, InteractionMatrix):
1487
+
1488
+ if output_model not in ["Huff", "MCI"]:
1489
+ raise ValueError("Parameter 'output_model' must be either 'Huff' or 'MCI'")
1424
1490
 
1491
+ if output_model == "Huff":
1492
+
1493
+ model = HuffModel(
1494
+ interaction_matrix=model_object,
1495
+ market_areas_df=self.market_areas_df
1496
+ )
1497
+
1498
+ if output_model == "MCI":
1499
+
1500
+ model = MCIModel(
1501
+ coefs=model_object.coefs,
1502
+ mci_ols_model=model_object.mci_ols_model,
1503
+ market_areas_df=self.market_areas_df
1504
+ )
1505
+
1425
1506
  return model
1426
-
1507
+
1427
1508
  class HuffModel:
1428
1509
 
1429
1510
  def __init__(
@@ -1457,14 +1538,16 @@ class HuffModel:
1457
1538
  return customer_origins
1458
1539
 
1459
1540
  def get_market_areas_df(self):
1541
+
1460
1542
  return self.market_areas_df
1461
-
1543
+
1462
1544
  def summary(self):
1463
1545
 
1464
1546
  interaction_matrix = self.interaction_matrix
1465
1547
 
1466
1548
  customer_origins_metadata = interaction_matrix.get_customer_origins().get_metadata()
1467
1549
  supply_locations_metadata = interaction_matrix.get_supply_locations().get_metadata()
1550
+ interaction_matrix_metadata = interaction_matrix.get_metadata()
1468
1551
 
1469
1552
  print("Huff Model")
1470
1553
  print("----------------------------------")
@@ -1486,14 +1569,23 @@ class HuffModel:
1486
1569
  if supply_locations_metadata["weighting"][0]["func"] is None and supply_locations_metadata["weighting"][0]["param"] is None:
1487
1570
  print("Attraction not defined")
1488
1571
  else:
1489
- print("Attraction " + str(round(supply_locations_metadata["weighting"][0]["param"], 3)) + " (" + supply_locations_metadata["weighting"][0]["func"] + ")")
1572
+ if supply_locations_metadata["weighting"][0]["param"] is not None:
1573
+ print("Attraction " + str(round(supply_locations_metadata["weighting"][0]["param"],3)) + " (" + supply_locations_metadata["weighting"][0]["func"] + ")")
1574
+ else:
1575
+ print("Attraction NA" + " (" + supply_locations_metadata["weighting"][0]["func"] + ")")
1490
1576
 
1491
1577
  if customer_origins_metadata["weighting"][0]["func"] is None and customer_origins_metadata["weighting"][0]["param"] is None:
1492
1578
  print("Transport costs not defined")
1493
1579
  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"] + ")")
1580
+ if customer_origins_metadata["weighting"][0]["param"] is not None:
1581
+ print("Transport costs " + str(round(customer_origins_metadata["weighting"][0]["param"],3)) + " (" + customer_origins_metadata["weighting"][0]["func"] + ")")
1582
+ else:
1583
+ print("Transport costs NA" + " (" + customer_origins_metadata["weighting"][0]["func"] + ")")
1495
1584
  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"] + ")")
1585
+ if customer_origins_metadata["weighting"][0]["param"] is not None:
1586
+ 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"] + ")")
1587
+ else:
1588
+ print("Transport costs NA" + " (" + customer_origins_metadata["weighting"][0]["func"] + ")")
1497
1589
 
1498
1590
  attrac_vars = supply_locations_metadata["attraction_col"]
1499
1591
  attrac_vars_no = len(attrac_vars)
@@ -1518,39 +1610,47 @@ class HuffModel:
1518
1610
  print(f"{name[:16]:16} {round(param, 3)} ({func})")
1519
1611
 
1520
1612
  print("----------------------------------")
1521
-
1522
- huff_modelfit = self.modelfit()
1523
- if huff_modelfit is not None:
1613
+
1614
+ if interaction_matrix_metadata != {} and "fit" in interaction_matrix_metadata and interaction_matrix_metadata["fit"]["function"] is not None:
1615
+ print("Parameter estimation")
1616
+ print("Fit function " + interaction_matrix_metadata["fit"]["function"])
1617
+ print("Fit by " + interaction_matrix_metadata["fit"]["fit_by"])
1618
+ if interaction_matrix_metadata["fit"]["function"] == "huff_ml_fit":
1619
+ print("Fit method " + interaction_matrix_metadata["fit"]["method"] + " (Converged: " + str(interaction_matrix_metadata["fit"]["minimize_success"]) + ")")
1620
+
1621
+ huff_modelfit = self.modelfit(by = interaction_matrix_metadata["fit"]["fit_by"])
1524
1622
 
1525
- print ("Goodness-of-fit for probabilities")
1623
+ if huff_modelfit is not None:
1624
+
1625
+ print ("Goodness-of-fit for " + interaction_matrix_metadata["fit"]["fit_by"])
1626
+
1627
+ print("Sum of squared residuals ", round(huff_modelfit[1]["SQR"], 2))
1628
+ print("Sum of squares ", round(huff_modelfit[1]["SQT"], 2))
1629
+ print("R-squared ", round(huff_modelfit[1]["Rsq"], 2))
1630
+ print("Mean squared error ", round(huff_modelfit[1]["MSE"], 2))
1631
+ print("Root mean squared error ", round(huff_modelfit[1]["RMSE"], 2))
1632
+ print("Mean absolute error ", round(huff_modelfit[1]["MAE"], 2))
1633
+ if huff_modelfit[1]["MAPE"] is not None:
1634
+ print("Mean absolute percentage error ", round(huff_modelfit[1]["MAPE"], 2))
1635
+ else:
1636
+ print("Mean absolute percentage error Not calculated")
1637
+ print("Symmetric MAPE ", round(huff_modelfit[1]["sMAPE"], 2))
1638
+ print("Absolute percentage errors")
1639
+
1640
+ APE_list = [
1641
+ ["< 5 % ", round(huff_modelfit[1]["APE"]["resid_below5"], 2), " < 30 % ", round(huff_modelfit[1]["APE"]["resid_below30"], 2)],
1642
+ ["< 10 % ", round(huff_modelfit[1]["APE"]["resid_below10"], 2), " < 35 % ", round(huff_modelfit[1]["APE"]["resid_below35"], 2)],
1643
+ ["< 15 % ", round(huff_modelfit[1]["APE"]["resid_below15"], 2), " < 40 % ", round(huff_modelfit[1]["APE"]["resid_below40"], 2)],
1644
+ ["< 20 % ", round(huff_modelfit[1]["APE"]["resid_below20"], 2), " < 45 % ", round(huff_modelfit[1]["APE"]["resid_below45"], 2)],
1645
+ ["< 25% ", round(huff_modelfit[1]["APE"]["resid_below25"], 2), " < 50 % ", round(huff_modelfit[1]["APE"]["resid_below50"], 2)]
1646
+ ]
1647
+ APE_df = pd.DataFrame(
1648
+ APE_list,
1649
+ columns=["Resid.", "%", "Resid.", "%"]
1650
+ )
1651
+ print(APE_df.to_string(index=False))
1526
1652
 
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("----------------------------------")
1653
+ print("----------------------------------")
1554
1654
 
1555
1655
  def mci_fit(
1556
1656
  self,
@@ -1642,6 +1742,275 @@ class HuffModel:
1642
1742
 
1643
1743
  return mci_model
1644
1744
 
1745
+ def loglik(
1746
+ self,
1747
+ params
1748
+ ):
1749
+
1750
+ if not isinstance(params, list):
1751
+ if isinstance(params, np.ndarray):
1752
+ params = params.tolist()
1753
+ else:
1754
+ raise ValueError("Parameter 'params' must be a list or np.ndarray with at least 2 parameter values")
1755
+
1756
+ if len(params) < 2:
1757
+ raise ValueError("Parameter 'params' must be a list or np.ndarray with at least 2 parameter values")
1758
+
1759
+ market_areas_df = self.market_areas_df
1760
+
1761
+ customer_origins = self.interaction_matrix.customer_origins
1762
+ customer_origins_metadata = customer_origins.get_metadata()
1763
+
1764
+ param_gamma, param_lambda = params[0], params[1]
1765
+
1766
+ if customer_origins_metadata["weighting"][0]["func"] == "logistic":
1767
+
1768
+ if len(params) < 3:
1769
+ raise ValueError("When using logistic weighting, parameter 'params' must be a list or np.ndarray with at least 3 parameter values")
1770
+
1771
+ param_gamma, param_lambda, param_lambda2 = params[0], params[1], params[2]
1772
+
1773
+ supply_locations = self.interaction_matrix.supply_locations
1774
+ supply_locations_metadata = supply_locations.get_metadata()
1775
+
1776
+ supply_locations_metadata["weighting"][0]["param"] = float(param_gamma)
1777
+ supply_locations.metadata = supply_locations_metadata
1778
+
1779
+ if customer_origins_metadata["weighting"][0]["func"] in ["power", "exponential"]:
1780
+
1781
+ if len(params) >= 2:
1782
+
1783
+ customer_origins_metadata["weighting"][0]["param"] = float(param_lambda)
1784
+
1785
+ else:
1786
+
1787
+ raise ValueError ("Huff Model with transport cost weighting of type " + customer_origins_metadata["weighting"][0]["func"] + " must have >= 2 input parameters")
1788
+
1789
+ elif customer_origins_metadata["weighting"][0]["func"] == "logistic":
1790
+
1791
+ if len(params) >= 3:
1792
+
1793
+ customer_origins_metadata["weighting"][0]["param"] = [float(param_lambda), float(param_lambda2)]
1794
+
1795
+ else:
1796
+
1797
+ raise ValueError("Huff Model with transport cost weightig of type " + customer_origins_metadata["weighting"][0]["func"] + " must have >= 3 input parameters")
1798
+
1799
+ if (customer_origins_metadata["weighting"][0]["func"] in ["power", "exponential"] and len(params) > 2):
1800
+
1801
+ for key, param in enumerate(params):
1802
+
1803
+ if key <= 1:
1804
+ continue
1805
+
1806
+ supply_locations_metadata["weighting"][key-1]["param"] = float(param)
1807
+
1808
+ if (customer_origins_metadata["weighting"][0]["func"] == "logistic" and len(params) > 3):
1809
+
1810
+ for key, param in enumerate(params):
1811
+
1812
+ if key <= 2:
1813
+ continue
1814
+
1815
+ supply_locations_metadata["weighting"][key-2]["param"] = float(param)
1816
+
1817
+ customer_origins.metadata = customer_origins_metadata
1818
+
1819
+ if "T_j_emp" not in market_areas_df.columns:
1820
+ T_j_emp = market_areas_df["T_j"]
1821
+ else:
1822
+ T_j_emp = market_areas_df["T_j_emp"]
1823
+
1824
+
1825
+ huff_model_copy = copy.deepcopy(self)
1826
+
1827
+ interaction_matrix_copy = copy.deepcopy(huff_model_copy.interaction_matrix)
1828
+
1829
+ interaction_matrix_copy = interaction_matrix_copy.utility()
1830
+ interaction_matrix_copy = interaction_matrix_copy.probabilities()
1831
+ interaction_matrix_copy = interaction_matrix_copy.flows()
1832
+
1833
+ huff_model_copy = interaction_matrix_copy.marketareas()
1834
+
1835
+ market_areas_df_copy = huff_model_copy.market_areas_df
1836
+
1837
+ observed = T_j_emp
1838
+ expected = market_areas_df_copy["T_j"]
1839
+
1840
+ modelfit_metrics = modelfit(
1841
+ observed = observed,
1842
+ expected = expected
1843
+ )
1844
+
1845
+ LL = modelfit_metrics[1]["LL"]
1846
+
1847
+ return -LL
1848
+
1849
+ def ml_fit(
1850
+ self,
1851
+ initial_params: list = [1.0, -2.0],
1852
+ method: str = "L-BFGS-B",
1853
+ bounds: list = [(0.5, 1), (-3, -1)],
1854
+ constraints: list = [],
1855
+ fit_by = "probabilities",
1856
+ update_estimates: bool = True
1857
+ ):
1858
+
1859
+ if fit_by in ["probabilities", "flows"]:
1860
+
1861
+ self.interaction_matrix.huff_ml_fit(
1862
+ initial_params = initial_params,
1863
+ method = method,
1864
+ bounds = bounds,
1865
+ constraints = constraints,
1866
+ fit_by = fit_by,
1867
+ update_estimates = update_estimates
1868
+ )
1869
+
1870
+ elif fit_by == "totals":
1871
+
1872
+ supply_locations = self.interaction_matrix.supply_locations
1873
+ supply_locations_metadata = supply_locations.get_metadata()
1874
+
1875
+ customer_origins = self.interaction_matrix.customer_origins
1876
+ customer_origins_metadata = customer_origins.get_metadata()
1877
+
1878
+ if customer_origins_metadata["weighting"][0]["param"] is None:
1879
+ params_metadata_customer_origins = 1
1880
+ else:
1881
+ if customer_origins_metadata["weighting"][0]["param"] is not None:
1882
+ if isinstance(customer_origins_metadata["weighting"][0]["param"], (int, float)):
1883
+ params_metadata_customer_origins = 1
1884
+ else:
1885
+ params_metadata_customer_origins = len(customer_origins_metadata["weighting"][0]["param"])
1886
+
1887
+ if customer_origins_metadata["weighting"][0]["func"] == "logistic":
1888
+ params_metadata_customer_origins = 2
1889
+ else:
1890
+ params_metadata_customer_origins = 1
1891
+
1892
+ params_metadata_supply_locations = len(supply_locations_metadata["weighting"])
1893
+
1894
+ params_metadata = params_metadata_customer_origins+params_metadata_supply_locations
1895
+
1896
+ if len(initial_params) < 2 or len(initial_params) != params_metadata:
1897
+ 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) + ")")
1898
+
1899
+ if len(bounds) != len(initial_params):
1900
+ raise ValueError("Parameter 'bounds' must have the same length as parameter 'initial_params' (" + str(len(bounds)) + ", " + str(len(initial_params)) + ")")
1901
+
1902
+ ml_result = minimize(
1903
+ self.loglik,
1904
+ initial_params,
1905
+ method = method,
1906
+ bounds = bounds,
1907
+ constraints = constraints,
1908
+ options={'disp': 3}
1909
+ )
1910
+
1911
+ attrac_vars = len(supply_locations_metadata["weighting"])
1912
+
1913
+ if ml_result.success:
1914
+
1915
+ fitted_params = ml_result.x
1916
+
1917
+ param_gamma = fitted_params[0]
1918
+ supply_locations_metadata["weighting"][0]["param"] = float(param_gamma)
1919
+
1920
+ if customer_origins_metadata["weighting"][0]["func"] in ["power", "exponential"]:
1921
+
1922
+ param_lambda = fitted_params[1]
1923
+ param_results = [
1924
+ float(param_gamma),
1925
+ float(param_lambda)
1926
+ ]
1927
+
1928
+ customer_origins_metadata["weighting"][0]["param"] = float(param_lambda)
1929
+
1930
+ elif customer_origins_metadata["weighting"][0]["func"] == "logistic":
1931
+
1932
+ param_lambda = fitted_params[1]
1933
+ param_lambda2 = fitted_params[2]
1934
+ param_results = [
1935
+ float(param_gamma),
1936
+ float(param_lambda),
1937
+ float(param_lambda2)
1938
+ ]
1939
+
1940
+ customer_origins_metadata["weighting"][0]["param"][0] = float(param_lambda)
1941
+ customer_origins_metadata["weighting"][0]["param"][1] = float(param_lambda2)
1942
+
1943
+ if attrac_vars > 1:
1944
+
1945
+ if customer_origins_metadata["weighting"][0]["func"] == "logistic":
1946
+ fitted_params_add = 3
1947
+ else:
1948
+ fitted_params_add = 2
1949
+
1950
+ for key, var in supply_locations_metadata["weighting"].items():
1951
+
1952
+ if key > len(supply_locations_metadata["weighting"])-fitted_params_add:
1953
+ break
1954
+
1955
+ param = float(fitted_params[key+fitted_params_add])
1956
+
1957
+ param_results = param_results + [param]
1958
+
1959
+ supply_locations_metadata["weighting"][(key+1)]["param"] = float(param)
1960
+
1961
+ print(f"Optimization via {method} algorithm succeeded with parameters: {', '.join(str(round(par, 3)) for par in param_results)}.")
1962
+
1963
+ else:
1964
+
1965
+ 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.")
1966
+
1967
+ self.interaction_matrix.supply_locations.metadata = supply_locations_metadata
1968
+ self.interaction_matrix.customer_origins.metadata = customer_origins_metadata
1969
+
1970
+ if update_estimates:
1971
+
1972
+ if "T_j_emp" not in self.market_areas_df.columns:
1973
+
1974
+ self.market_areas_df["T_j_emp"] = self.market_areas_df["T_j"]
1975
+
1976
+ print("NOTE: Total values in market areas df are treated as empirical total values")
1977
+
1978
+ else:
1979
+
1980
+ print("NOTE: Total market areas df contains empirical total values")
1981
+
1982
+ if np.isnan(ml_result.x).any():
1983
+
1984
+ print("WARNING: No update of estimates because fit parameters contain NaN")
1985
+
1986
+ update_estimates = False
1987
+
1988
+ else:
1989
+
1990
+ self.interaction_matrix.utility()
1991
+ self.interaction_matrix.probabilities()
1992
+ self.interaction_matrix.flows()
1993
+
1994
+ self.interaction_matrix.marketareas()
1995
+
1996
+ self.interaction_matrix.metadata["fit"] = {
1997
+ "function": "huff_ml_fit",
1998
+ "fit_by": fit_by,
1999
+ "initial_params": initial_params,
2000
+ "method": method,
2001
+ "bounds": bounds,
2002
+ "constraints": constraints,
2003
+ "minimize_success": ml_result.success,
2004
+ "minimize_fittedparams": ml_result.x,
2005
+ "update_estimates": update_estimates
2006
+ }
2007
+
2008
+ else:
2009
+
2010
+ raise ValueError("Parameter 'fit_by' must be 'probabilities', 'flows' or 'totals'")
2011
+
2012
+ return self
2013
+
1645
2014
  def update(self):
1646
2015
 
1647
2016
  self.interaction_matrix = self.interaction_matrix.update()
@@ -1652,31 +2021,92 @@ class HuffModel:
1652
2021
 
1653
2022
  def modelfit(
1654
2023
  self,
1655
- by = "p_ij"
1656
- ):
1657
-
1658
- interaction_matrix = self.interaction_matrix
1659
- interaction_matrix_df = interaction_matrix.get_interaction_matrix_df()
2024
+ by = "probabilities"
2025
+ ):
1660
2026
 
1661
- if (by in interaction_matrix_df.columns and by+"_emp" in interaction_matrix_df.columns):
2027
+ if by == "probabilities":
2028
+
2029
+ interaction_matrix = self.interaction_matrix
2030
+ interaction_matrix_df = interaction_matrix.get_interaction_matrix_df()
2031
+
2032
+ if ("p_ij" in interaction_matrix_df.columns and "p_ij_emp" in interaction_matrix_df.columns):
2033
+
2034
+ try:
2035
+
2036
+ huff_modelfit = modelfit(
2037
+ interaction_matrix_df["p_ij_emp"],
2038
+ interaction_matrix_df["p_ij"]
2039
+ )
2040
+
2041
+ return huff_modelfit
2042
+
2043
+ except:
2044
+
2045
+ print("Goodness-of-fit metrics could not be calculated due to NaN values.")
2046
+ return None
1662
2047
 
1663
- try:
2048
+ else:
2049
+
2050
+ print("Goodness-of-fit metrics could not be calculated. No empirical values of probabilities in interaction matrix.")
2051
+
2052
+ return None
1664
2053
 
1665
- huff_modelfit = modelfit(
1666
- interaction_matrix_df[by+"_emp"],
1667
- interaction_matrix_df[by]
1668
- )
2054
+ elif by == "flows":
2055
+
2056
+ interaction_matrix = self.interaction_matrix
2057
+ interaction_matrix_df = interaction_matrix.get_interaction_matrix_df()
2058
+
2059
+ if ("E_ij" in interaction_matrix_df.columns and "E_ij_emp" in interaction_matrix_df.columns):
1669
2060
 
1670
- return huff_modelfit
2061
+ try:
1671
2062
 
1672
- except:
2063
+ huff_modelfit = modelfit(
2064
+ interaction_matrix_df["E_ij_emp"],
2065
+ interaction_matrix_df["E_ij"]
2066
+ )
2067
+
2068
+ return huff_modelfit
2069
+
2070
+ except:
2071
+
2072
+ print("Goodness-of-fit metrics could not be calculated due to NaN values.")
2073
+ return None
2074
+
2075
+ else:
1673
2076
 
1674
- print("Goodness-of-fit metrics could not be calculated due to NaN values.")
2077
+ print("Goodness-of-fit metrics could not be calculated. No empirical values of customer flows in interaction matrix.")
2078
+
1675
2079
  return None
1676
-
1677
- else:
1678
2080
 
1679
- return None
2081
+ elif by == "totals":
2082
+
2083
+ market_areas_df = self.market_areas_df
2084
+
2085
+ if ("T_j" in market_areas_df.columns and "T_j_emp" in market_areas_df.columns):
2086
+
2087
+ try:
2088
+
2089
+ huff_modelfit = modelfit(
2090
+ market_areas_df["T_j_emp"],
2091
+ market_areas_df["T_j"]
2092
+ )
2093
+
2094
+ return huff_modelfit
2095
+
2096
+ except:
2097
+
2098
+ print("Goodness-of-fit metrics could not be calculated due to NaN values.")
2099
+ return None
2100
+
2101
+ else:
2102
+
2103
+ print("Goodness-of-fit metrics could not be calculated. No empirical values of T_j in market areas data.")
2104
+
2105
+ return None
2106
+
2107
+ else:
2108
+
2109
+ raise ValueError("Parameter 'by' must be 'probabilities', 'flows', or 'totals'")
1680
2110
 
1681
2111
  class MCIModel:
1682
2112
 
@@ -1890,8 +2320,11 @@ class MCIModel:
1890
2320
  interaction_matrix = self.interaction_matrix
1891
2321
  interaction_matrix_df = interaction_matrix.get_interaction_matrix_df()
1892
2322
 
1893
- if "p_ij" in interaction_matrix_df.columns:
2323
+ if "p_ij" in interaction_matrix_df.columns and "p_ij_emp" not in interaction_matrix_df.columns:
2324
+ print("NOTE: Probabilities in interaction matrix are treated as empirical probabilities")
1894
2325
  interaction_matrix_df["p_ij_emp"] = interaction_matrix_df["p_ij"]
2326
+ else:
2327
+ print("NOTE: Interaction matrix contains empirical probabilities")
1895
2328
 
1896
2329
  if "U_ij" not in interaction_matrix_df.columns:
1897
2330
  self.utility(transformation = transformation)
@@ -2666,7 +3099,8 @@ def get_isochrones(
2666
3099
  def modelfit(
2667
3100
  observed,
2668
3101
  expected,
2669
- remove_nan: bool = True
3102
+ remove_nan: bool = True,
3103
+ verbose: bool = False
2670
3104
  ):
2671
3105
 
2672
3106
  observed_no = len(observed)
@@ -2692,6 +3126,10 @@ def modelfit(
2692
3126
  )
2693
3127
 
2694
3128
  obs_exp_clean = obs_exp.dropna(subset=["observed", "expected"])
3129
+
3130
+ if len(obs_exp_clean) < len(observed) or len(obs_exp_clean) < len(expected):
3131
+ if verbose:
3132
+ print("Vectors 'observed' and/or 'expected' contain zeros which are dropped.")
2695
3133
 
2696
3134
  observed = obs_exp_clean["observed"].to_numpy()
2697
3135
  expected = obs_exp_clean["expected"].to_numpy()
@@ -2708,7 +3146,8 @@ def modelfit(
2708
3146
  residuals_abs = abs(residuals)
2709
3147
 
2710
3148
  if any(observed == 0):
2711
- print ("Vector 'observed' contains values equal to zero. No APE/MAPE calculated.")
3149
+ if verbose:
3150
+ print ("Vector 'observed' contains values equal to zero. No APE/MAPE calculated.")
2712
3151
  APE = np.full_like(observed, np.nan)
2713
3152
  MAPE = None
2714
3153
  else:
@@ -2735,6 +3174,7 @@ def modelfit(
2735
3174
  MSE = float(SQR/observed_no)
2736
3175
  RMSE = float(sqrt(MSE))
2737
3176
  MAE = float(SAR/observed_no)
3177
+ LL = np.sum(np.log(residuals_sq))
2738
3178
 
2739
3179
  sMAPE = float(np.mean(sAPE))
2740
3180
 
@@ -2759,6 +3199,7 @@ def modelfit(
2759
3199
  "MAE": MAE,
2760
3200
  "MAPE": MAPE,
2761
3201
  "sMAPE": sMAPE,
3202
+ "LL": -LL,
2762
3203
  "APE": {
2763
3204
  "resid_below5": resid_below5,
2764
3205
  "resid_below10": resid_below10,
@@ -2780,34 +3221,23 @@ def modelfit(
2780
3221
 
2781
3222
  return modelfit_results
2782
3223
 
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
3224
  def check_vars(
2799
3225
  df: pd.DataFrame,
2800
- cols: list
3226
+ cols: list,
3227
+ check_numeric: bool = True,
3228
+ check_zero: bool = True
2801
3229
  ):
2802
3230
 
2803
3231
  for col in cols:
2804
3232
  if col not in df.columns:
2805
3233
  raise KeyError(f"Column '{col}' not in dataframe.")
2806
3234
 
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.")
3235
+ if check_numeric:
3236
+ for col in cols:
3237
+ if not pd.api.types.is_numeric_dtype(df[col]):
3238
+ raise ValueError(f"Column '{col}' is not numeric. All stated columns must be numeric.")
2810
3239
 
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.")
3240
+ if check_zero:
3241
+ for col in cols:
3242
+ if (df[col] <= 0).any():
3243
+ raise ValueError(f"Column '{col}' includes values <= 0. All values must be numeric and positive.")
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.1
8
+ # Last update: 2025-07-01 17: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,21 @@ 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
+
331
357
 
332
358
  # Buffer analysis:
333
359
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: huff
3
- Version: 1.5.0
3
+ Version: 1.5.1
4
4
  Summary: huff: Huff Model Market Area Analysis
5
5
  Author: Thomas Wieland
6
6
  Author-email: geowieland@googlemail.com
@@ -28,16 +28,15 @@ 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
31
+ ## Updates v1.5.1
32
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()
33
+ - HuffModel.ml_fit(): Fit Huff model parameters by empirical total market areas
34
+ - HuffModel.summary(): Goodnes-of-fit depends on fit_by
35
+ - load_market_areas(): Loading table with totals and including into InteractionMatrix, HuffModel and MCIModel objects
37
36
  - 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
37
+ - InteractionMatrix.summary(): NoneType parameter
38
+ - InteractionMatrix.huff_ml_fit(): Update estimates possible even if fit algorithm did not converge
39
+
41
40
 
42
41
  ## Features
43
42
 
@@ -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=IihaZmutJjdKqzTVXGVmcYbauFJImF-UPsZ2QCPzw8s,125182
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=eHnEiV8m7TjpAXnJqo1aZ0YlQCGHxK1iFXROEyhN5cU,12884
8
8
  huff/tests/data/Haslach.cpg,sha256=OtMDH1UDpEBK-CUmLugjLMBNTqZoPULF3QovKiesmCQ,5
9
9
  huff/tests/data/Haslach.dbf,sha256=GVPIt05OzDO7UrRDcsMhiYWvyXAPg6Z-qkiysFzj-fc,506
10
10
  huff/tests/data/Haslach.prj,sha256=2Jy1Vlzh7UxQ1MXpZ9UYLs2SxfrObj2xkEkZyLqmGTY,437
@@ -24,7 +24,7 @@ huff/tests/data/Haslach_supermarkets.qmd,sha256=JlcOYzG4vI1NH1IuOpxwIPnJsCyC-pDR
24
24
  huff/tests/data/Haslach_supermarkets.shp,sha256=X7QbQ0BTMag_B-bDRbpr-go2BQIXo3Y8zMAKpYZmlps,324
25
25
  huff/tests/data/Haslach_supermarkets.shx,sha256=j23QHX-SmdAeN04rw0x8nUOran-OCg_T6r_LvzzEPWs,164
26
26
  huff/tests/data/Wieland2015.xlsx,sha256=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,,
27
+ huff-1.5.1.dist-info/METADATA,sha256=BXRKyUp5qIEoYjNpo_w9zpUwMTDTO_aegNgr67qj8ns,6187
28
+ huff-1.5.1.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
29
+ huff-1.5.1.dist-info/top_level.txt,sha256=nlzX-PxZNFmIxANIJMySuIFPihd6qOBkRlhIC28NEsQ,5
30
+ huff-1.5.1.dist-info/RECORD,,
File without changes