huff 1.5.4__py3-none-any.whl → 1.5.6__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/gistools.py CHANGED
@@ -4,22 +4,30 @@
4
4
  # Author: Thomas Wieland
5
5
  # ORCID: 0000-0001-5168-9846
6
6
  # mail: geowieland@googlemail.com
7
- # Version: 1.4.1
8
- # Last update: 2025-06-16 17:44
7
+ # Version: 1.4.2
8
+ # Last update: 2025-07-31 18:31
9
9
  # Copyright (c) 2025 Thomas Wieland
10
10
  #-----------------------------------------------------------------------
11
11
 
12
12
 
13
+ import os
13
14
  import geopandas as gp
14
15
  import pandas as pd
15
16
  from pandas.api.types import is_numeric_dtype
16
17
  from math import pi, sin, cos, acos
18
+ from matplotlib.patches import Patch
19
+ import matplotlib.pyplot as plt
20
+ from shapely.geometry import LineString, box
21
+ import contextily as cx
22
+ from PIL import Image
23
+ from huff.osm import get_basemap
17
24
 
18
25
 
19
26
  def distance_matrix(
20
27
  sources: list,
21
28
  destinations: list,
22
29
  unit: str = "m",
30
+ lines_gdf: bool = False
23
31
  ):
24
32
 
25
33
  def euclidean_distance (
@@ -48,18 +56,36 @@ def distance_matrix(
48
56
 
49
57
  matrix = []
50
58
 
59
+ if lines_gdf:
60
+ line_data = []
61
+
51
62
  for source in sources:
63
+
52
64
  row = []
53
65
  for destination in destinations:
66
+
54
67
  dist = euclidean_distance(
55
68
  source,
56
69
  destination,
57
70
  unit
58
71
  )
59
72
  row.append(dist)
73
+
74
+ if lines_gdf:
75
+ line = LineString([source, destination])
76
+ line_data.append({
77
+ "source": source,
78
+ "destination": destination,
79
+ "distance": dist,
80
+ "geometry": line
81
+ })
82
+
60
83
  matrix.append(row)
61
84
 
62
- return matrix
85
+ if lines_gdf:
86
+ return line_data
87
+ else:
88
+ return matrix
63
89
 
64
90
 
65
91
  def buffers(
@@ -72,7 +98,13 @@ def buffers(
72
98
  output_crs: str = "EPSG:4326"
73
99
  ):
74
100
 
75
- all_buffers_gdf = gp.GeoDataFrame(columns=[unique_id_col, "segment", "geometry"])
101
+ all_buffers_gdf = gp.GeoDataFrame(
102
+ columns=[
103
+ unique_id_col,
104
+ "segment",
105
+ "geometry"
106
+ ]
107
+ )
76
108
 
77
109
  for idx, row in point_gdf.iterrows():
78
110
 
@@ -115,7 +147,9 @@ def buffers(
115
147
  all_buffers_gdf = all_buffers_gdf.to_crs(output_crs)
116
148
 
117
149
  if save_output:
150
+
118
151
  all_buffers_gdf.to_file(output_filepath)
152
+
119
153
  print ("Saved as", output_filepath)
120
154
 
121
155
  return all_buffers_gdf
@@ -218,4 +252,175 @@ def point_spatial_join(
218
252
  return [
219
253
  shp_points_gdf_join,
220
254
  spatial_join_stat
221
- ]
255
+ ]
256
+
257
+ def map_with_basemap(
258
+ layers: list,
259
+ osm_basemap: bool = True,
260
+ zoom: int = 15,
261
+ figsize=(10, 10),
262
+ bounds_factor = [0.9999, 0.9999, 1.0001, 1.0001],
263
+ styles: dict = {},
264
+ save_output: bool = True,
265
+ output_filepath: str = "osm_map_with_basemap.png",
266
+ output_dpi=300,
267
+ legend: bool = True,
268
+ show_plot: bool = True,
269
+ verbose: bool = False
270
+ ):
271
+
272
+ if not isinstance(layers, list):
273
+ raise ValueError("Param 'layers' must be a list")
274
+
275
+ if not layers:
276
+ raise ValueError("List layers is empty")
277
+
278
+ if verbose:
279
+ print("Combining layers ...", end = " ")
280
+
281
+ layers_combined = gp.GeoDataFrame(
282
+ pd.concat(
283
+ layers,
284
+ ignore_index=True
285
+ ),
286
+ crs=layers[0].crs
287
+ )
288
+
289
+ layers_combined_wgs84 = layers_combined.to_crs(epsg=4326)
290
+
291
+ if verbose:
292
+ print("OK")
293
+ print("Retrieving total bounds ...", end = " ")
294
+
295
+ bounds = layers_combined_wgs84.total_bounds
296
+
297
+ sw_lon, sw_lat, ne_lon, ne_lat = bounds[0]*bounds_factor[0], bounds[1]*bounds_factor[1], bounds[2]*bounds_factor[2], bounds[3]*bounds_factor[3]
298
+
299
+ if verbose:
300
+ print("OK")
301
+
302
+ if osm_basemap:
303
+
304
+ if verbose:
305
+ print("Retrieving OSM basemap ...", end = " ")
306
+
307
+ get_basemap(sw_lat, sw_lon, ne_lat, ne_lon, zoom=zoom)
308
+
309
+ fig, ax = plt.subplots(figsize=figsize)
310
+
311
+ if osm_basemap:
312
+
313
+ img = Image.open("osm_map.png")
314
+ extent_img = [sw_lon, ne_lon, sw_lat, ne_lat]
315
+ ax.imshow(img, extent=extent_img, origin="upper")
316
+
317
+ if verbose:
318
+ print("OK")
319
+
320
+ if verbose:
321
+ print("Inserting layers and plotting map ...", end = " ")
322
+
323
+ i = 0
324
+ legend_handles = []
325
+
326
+ for layer in layers:
327
+
328
+ layer_3857 = layer.to_crs(epsg=3857)
329
+
330
+ if styles != {}:
331
+
332
+ layer_style = styles[i]
333
+ layer_color = layer_style["color"]
334
+ layer_alpha = layer_style["alpha"]
335
+ layer_name = layer_style["name"]
336
+
337
+ if isinstance(layer_color, str):
338
+ layer_3857.plot(
339
+ ax=ax,
340
+ color=layer_color,
341
+ alpha=layer_alpha,
342
+ label=layer_name
343
+ )
344
+ if legend:
345
+ patch = Patch(
346
+ facecolor=layer_color,
347
+ alpha=layer_alpha,
348
+ label=layer_name
349
+ )
350
+ legend_handles.append(patch)
351
+
352
+ elif isinstance(layer_color, dict):
353
+ color_key = list(layer_color.keys())[0]
354
+ color_mapping = layer_color[color_key]
355
+
356
+ if color_key not in layer_3857.columns:
357
+ raise KeyError("Column " + color_key + " not in layer.")
358
+
359
+ for value, color in color_mapping.items():
360
+
361
+ subset = layer_3857[layer_3857[color_key].astype(str) == str(value)]
362
+
363
+ if not subset.empty:
364
+
365
+ subset.plot(
366
+ ax=ax,
367
+ color=color,
368
+ alpha=layer_alpha,
369
+ label=str(value)
370
+ )
371
+
372
+ if legend:
373
+ patch = Patch(facecolor=color, alpha=layer_alpha, label=str(value))
374
+ legend_handles.append(patch)
375
+
376
+ else:
377
+
378
+ layer_3857.plot(ax=ax, alpha=0.6, label=f"Layer {i+1}")
379
+
380
+ if legend:
381
+
382
+ patch = Patch(
383
+ facecolor="gray",
384
+ alpha=0.6,
385
+ label=f"Layer {i+1}"
386
+ )
387
+
388
+ legend_handles.append(patch)
389
+
390
+ i += 1
391
+
392
+ bbox = box(sw_lon, sw_lat, ne_lon, ne_lat)
393
+ extent_geom = gp.GeoSeries([bbox], crs=4326).to_crs(epsg=3857).total_bounds
394
+ ax.set_xlim(extent_geom[0], extent_geom[2])
395
+ ax.set_ylim(extent_geom[1], extent_geom[3])
396
+
397
+ if osm_basemap:
398
+ cx.add_basemap(
399
+ ax,
400
+ source=cx.providers.OpenStreetMap.Mapnik,
401
+ zoom=zoom
402
+ )
403
+
404
+ plt.axis('off')
405
+
406
+ if legend and legend_handles:
407
+ ax.legend(handles=legend_handles, loc='lower right', fontsize='small', frameon=True)
408
+
409
+ if verbose:
410
+ print("OK")
411
+
412
+ if show_plot:
413
+ plt.show()
414
+
415
+ if save_output:
416
+ plt.savefig(
417
+ output_filepath,
418
+ dpi=output_dpi,
419
+ bbox_inches="tight"
420
+ )
421
+ plt.close()
422
+
423
+ if os.path.exists("osm_map.png"):
424
+ os.remove("osm_map.png")
425
+
426
+ return fig
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.4
8
- # Last update: 2025-07-18 18:07
7
+ # Version: 1.5.5
8
+ # Last update: 2025-07-26 13:42
9
9
  # Copyright (c) 2025 Thomas Wieland
10
10
  #-----------------------------------------------------------------------
11
11
 
@@ -944,15 +944,21 @@ class InteractionMatrix:
944
944
 
945
945
  def hansen(
946
946
  self,
947
- from_origins: bool = True
947
+ from_origins: bool = True,
948
+ exclude_self: bool = True,
949
+ check_df_vars: bool = True
948
950
  ):
949
951
 
950
952
  interaction_matrix_df = self.interaction_matrix_df
951
953
 
954
+ if exclude_self:
955
+
956
+ interaction_matrix_df = interaction_matrix_df[interaction_matrix_df["i"] != interaction_matrix_df["j"]]
957
+
952
958
  if from_origins:
953
959
 
954
- if interaction_matrix_df["U_ij"].isna().all():
955
- self.utility()
960
+ if "U_ij" not in interaction_matrix_df.columns or interaction_matrix_df["U_ij"].isna().all():
961
+ self.utility(check_df_vars = check_df_vars)
956
962
  interaction_matrix_df = self.interaction_matrix_df
957
963
 
958
964
  hansen_df = pd.DataFrame(interaction_matrix_df.groupby("i")["U_ij"].sum()).reset_index()
huff/osm.py CHANGED
@@ -4,24 +4,17 @@
4
4
  # Author: Thomas Wieland
5
5
  # ORCID: 0000-0001-5168-9846
6
6
  # mail: geowieland@googlemail.com
7
- # Version: 1.4.1
8
- # Last update: 2025-06-16 17:44
7
+ # Version: 1.4.2
8
+ # Last update: 2025-07-31 18:24
9
9
  # Copyright (c) 2025 Thomas Wieland
10
10
  #-----------------------------------------------------------------------
11
11
 
12
12
 
13
- import pandas as pd
14
- import geopandas as gpd
15
13
  import math
16
14
  import requests
17
15
  import tempfile
18
16
  import time
19
- import os
20
17
  from PIL import Image
21
- import matplotlib.pyplot as plt
22
- from matplotlib.patches import Patch
23
- import contextily as cx
24
- from shapely.geometry import box
25
18
 
26
19
 
27
20
  class Client:
@@ -79,8 +72,9 @@ def get_basemap(
79
72
  lat,
80
73
  lon,
81
74
  zoom
75
+ # https://wiki.openstreetmap.org/wiki/Zoom_levels
82
76
  ):
83
-
77
+
84
78
  n = 2 ** zoom
85
79
  x = int(n * ((lon + 180) / 360))
86
80
  y = int(n * (1 - (math.log(math.tan(math.radians(lat)) + 1 / math.cos(math.radians(lat))) / math.pi)) / 2)
@@ -133,135 +127,4 @@ def get_basemap(
133
127
  stitched_image.save(stitched_image_path)
134
128
 
135
129
  else:
136
- print("Error while building stitched images")
137
-
138
-
139
- def map_with_basemap(
140
- layers: list,
141
- osm_basemap: bool = True,
142
- zoom: int = 15,
143
- styles: dict = {},
144
- save_output: bool = True,
145
- output_filepath: str = "osm_map_with_basemap.png",
146
- output_dpi=300,
147
- legend: bool = True
148
- ):
149
- if not layers:
150
- raise ValueError("List layers is empty")
151
-
152
- combined = gpd.GeoDataFrame(
153
- pd.concat(layers, ignore_index=True),
154
- crs=layers[0].crs
155
- )
156
-
157
- combined_wgs84 = combined.to_crs(epsg=4326)
158
- bounds = combined_wgs84.total_bounds
159
-
160
- sw_lon, sw_lat, ne_lon, ne_lat = bounds[0]*0.9999, bounds[1]*0.9999, bounds[2]*1.0001, bounds[3]*1.0001
161
-
162
- if osm_basemap:
163
- get_basemap(sw_lat, sw_lon, ne_lat, ne_lon, zoom=zoom)
164
-
165
- fig, ax = plt.subplots(figsize=(10, 10))
166
-
167
- if osm_basemap:
168
- img = Image.open("osm_map.png")
169
- extent_img = [sw_lon, ne_lon, sw_lat, ne_lat]
170
- ax.imshow(img, extent=extent_img, origin="upper")
171
-
172
- i = 0
173
- legend_handles = []
174
-
175
- for layer in layers:
176
- layer_3857 = layer.to_crs(epsg=3857)
177
-
178
- if styles != {}:
179
- layer_style = styles[i]
180
- layer_color = layer_style["color"]
181
- layer_alpha = layer_style["alpha"]
182
- layer_name = layer_style["name"]
183
-
184
- if isinstance(layer_color, str):
185
- layer_3857.plot(
186
- ax=ax,
187
- color=layer_color,
188
- alpha=layer_alpha,
189
- label=layer_name
190
- )
191
- if legend:
192
- patch = Patch(
193
- facecolor=layer_color,
194
- alpha=layer_alpha,
195
- label=layer_name
196
- )
197
- legend_handles.append(patch)
198
-
199
- elif isinstance(layer_color, dict):
200
- color_key = list(layer_color.keys())[0]
201
- color_mapping = layer_color[color_key]
202
-
203
- if color_key not in layer_3857.columns:
204
- raise KeyError("Column " + color_key + " not in layer.")
205
-
206
- for value, color in color_mapping.items():
207
-
208
- subset = layer_3857[layer_3857[color_key].astype(str) == str(value)]
209
-
210
- if not subset.empty:
211
-
212
- subset.plot(
213
- ax=ax,
214
- color=color,
215
- alpha=layer_alpha,
216
- label=str(value)
217
- )
218
-
219
- if legend:
220
- patch = Patch(facecolor=color, alpha=layer_alpha, label=str(value))
221
- legend_handles.append(patch)
222
-
223
- else:
224
-
225
- layer_3857.plot(ax=ax, alpha=0.6, label=f"Layer {i+1}")
226
-
227
- if legend:
228
-
229
- patch = Patch(
230
- facecolor="gray",
231
- alpha=0.6,
232
- label=f"Layer {i+1}"
233
- )
234
-
235
- legend_handles.append(patch)
236
-
237
- i += 1
238
-
239
- bbox = box(sw_lon, sw_lat, ne_lon, ne_lat)
240
- extent_geom = gpd.GeoSeries([bbox], crs=4326).to_crs(epsg=3857).total_bounds
241
- ax.set_xlim(extent_geom[0], extent_geom[2])
242
- ax.set_ylim(extent_geom[1], extent_geom[3])
243
-
244
- if osm_basemap:
245
- cx.add_basemap(
246
- ax,
247
- source=cx.providers.OpenStreetMap.Mapnik,
248
- zoom=zoom
249
- )
250
-
251
- plt.axis('off')
252
-
253
- if legend and legend_handles:
254
- ax.legend(handles=legend_handles, loc='lower right', fontsize='small', frameon=True)
255
-
256
- plt.show()
257
-
258
- if save_output:
259
- plt.savefig(
260
- output_filepath,
261
- dpi=output_dpi,
262
- bbox_inches="tight"
263
- )
264
- plt.close()
265
-
266
- if os.path.exists("osm_map.png"):
267
- os.remove("osm_map.png")
130
+ print("Error while building stitched images")
huff/tests/tests_huff.py CHANGED
@@ -4,15 +4,14 @@
4
4
  # Author: Thomas Wieland
5
5
  # ORCID: 0000-0001-5168-9846
6
6
  # mail: geowieland@googlemail.com
7
- # Version: 1.5.4
8
- # Last update: 2025-07-18 18:06
7
+ # Version: 1.5.6
8
+ # Last update: 2025-07-31 18:33
9
9
  # Copyright (c) 2025 Thomas Wieland
10
10
  #-----------------------------------------------------------------------
11
11
 
12
12
  from huff.models import create_interaction_matrix, get_isochrones, load_geodata, load_interaction_matrix, load_marketareas, market_shares, modelfit
13
13
  from huff.models import HuffModel
14
- from huff.osm import map_with_basemap
15
- from huff.gistools import buffers, point_spatial_join
14
+ from huff.gistools import buffers, point_spatial_join, map_with_basemap
16
15
 
17
16
 
18
17
  # Dealing with customer origins (statistical districts):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: huff
3
- Version: 1.5.4
3
+ Version: 1.5.6
4
4
  Summary: huff: Huff Model Market Area Analysis
5
5
  Author: Thomas Wieland
6
6
  Author-email: geowieland@googlemail.com
@@ -28,10 +28,11 @@ Thomas Wieland [ORCID](https://orcid.org/0000-0001-5168-9846) [EMail](mailto:geo
28
28
  See the /tests directory for usage examples of most of the included functions.
29
29
 
30
30
 
31
- ## Updates v1.5.4
32
- - Bugfixes:
33
- - Use of check_vars() is now optional (default: True)
34
- - Correction of args argument when calling minimize() in huff_ml_fit()
31
+ ## Updates v1.5.6
32
+ - Extensions:
33
+ - gistools.distance_matrix() now includes line geometry output
34
+ - Other
35
+ - map_with_basemap() now belongs to huff.gistools
35
36
 
36
37
 
37
38
  ## Features
@@ -1,10 +1,10 @@
1
1
  huff/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- huff/gistools.py,sha256=fgeE1IsUO7UIaawb23kuiz_Rlxn7T18iLLTA5yvgp74,7038
3
- huff/models.py,sha256=2s-NQoPuP8q7iwJK3tA6S9-qDGMgRGah9vgL4aJerrU,134607
2
+ huff/gistools.py,sha256=cffGqNRaCaIE0f52j5bGpgbdxMxy8zmeMQqPhrVSfOE,12890
3
+ huff/models.py,sha256=yrV4enajAG0bHCgrGh3w-SNX5kJDJnuE28i6RXN30HE,134903
4
4
  huff/ors.py,sha256=JlO2UEishQX87PIiktksOrVT5QdB-GEWgjXcxoR_KuA,11929
5
- huff/osm.py,sha256=9A-7hxeZyjA2r8w2_IqqwH14qq2Y9AS1GxVKOD7utqs,7747
5
+ huff/osm.py,sha256=1a74HkKgAJYLWdthnAslOEJiHYz7qI3dk7u4ku6Jq0o,3563
6
6
  huff/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- huff/tests/tests_huff.py,sha256=np0YvzmGYqVFCPFBsdFuUWtGlwX6HKUUlWkYlfWzGPY,13116
7
+ huff/tests/tests_huff.py,sha256=B7C74NhXPtuwWY8X-5dxrB5GQLnT4OqgId4ZSGaTBQo,13095
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=H4rxCFlctn44-O6mIyeFf67FlgvznLX7xZqpoWYS41A,25788
27
- huff-1.5.4.dist-info/METADATA,sha256=Dq-uRGKbKUqNSvB9xUGbox7tRLNB0PDL7QBA_NLfAds,6025
28
- huff-1.5.4.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
29
- huff-1.5.4.dist-info/top_level.txt,sha256=nlzX-PxZNFmIxANIJMySuIFPihd6qOBkRlhIC28NEsQ,5
30
- huff-1.5.4.dist-info/RECORD,,
27
+ huff-1.5.6.dist-info/METADATA,sha256=4ZRQx28t5HIXwoHSlNG-S2Q0hocMBAnhmgkYEkV95PI,6023
28
+ huff-1.5.6.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
29
+ huff-1.5.6.dist-info/top_level.txt,sha256=nlzX-PxZNFmIxANIJMySuIFPihd6qOBkRlhIC28NEsQ,5
30
+ huff-1.5.6.dist-info/RECORD,,
File without changes