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 +563 -133
- huff/tests/tests_huff.py +49 -23
- {huff-1.5.0.dist-info → huff-1.5.1.dist-info}/METADATA +8 -9
- {huff-1.5.0.dist-info → huff-1.5.1.dist-info}/RECORD +6 -6
- {huff-1.5.0.dist-info → huff-1.5.1.dist-info}/WHEEL +0 -0
- {huff-1.5.0.dist-info → huff-1.5.1.dist-info}/top_level.txt +0 -0
huff/models.py
CHANGED
@@ -4,8 +4,8 @@
|
|
4
4
|
# Author: Thomas Wieland
|
5
5
|
# ORCID: 0000-0001-5168-9846
|
6
6
|
# mail: geowieland@googlemail.com
|
7
|
-
# Version: 1.5.
|
8
|
-
# Last update: 2025-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
558
|
-
|
559
|
-
|
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
|
-
|
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
|
-
|
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
|
1123
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
1263
|
-
|
1264
|
-
|
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
|
-
|
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
|
1276
|
-
|
1277
|
-
|
1278
|
-
"
|
1279
|
-
"
|
1280
|
-
|
1281
|
-
"
|
1282
|
-
|
1283
|
-
|
1284
|
-
|
1285
|
-
|
1286
|
-
|
1287
|
-
|
1288
|
-
|
1289
|
-
|
1290
|
-
|
1291
|
-
|
1292
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
1523
|
-
|
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
|
-
|
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
|
-
|
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 = "
|
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
|
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
|
-
|
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
|
-
|
1666
|
-
|
1667
|
-
|
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
|
-
|
2061
|
+
try:
|
1671
2062
|
|
1672
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
2808
|
-
|
2809
|
-
|
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
|
-
|
2812
|
-
|
2813
|
-
|
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.
|
8
|
-
# Last update: 2025-
|
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
|
-
#
|
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
|
-
|
43
|
-
#
|
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
|
-
#
|
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
|
-
|
105
|
-
network=
|
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
|
-
|
136
|
-
initial_params=[1, 9, -0.6],
|
130
|
+
initial_params=[1, -2],
|
137
131
|
method="trust-constr",
|
138
|
-
|
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, -
|
293
|
-
bounds=[(0.5, 1), (-
|
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="
|
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.
|
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.
|
31
|
+
## Updates v1.5.1
|
32
32
|
- Extensions:
|
33
|
-
- Huff model
|
34
|
-
-
|
35
|
-
- Loading
|
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.
|
39
|
-
- InteractionMatrix.
|
40
|
-
|
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=
|
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=
|
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.
|
28
|
-
huff-1.5.
|
29
|
-
huff-1.5.
|
30
|
-
huff-1.5.
|
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
|
File without changes
|