Skip to content
2 changes: 1 addition & 1 deletion Tests/test_geocode.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ def test_reverse_geocode_gsp(self):
"""
Test the `reverse_geocode_gsp` function with several test cases.
"""
gsp_regions = [("BRED_1", "_G"), ("DEWP", "_N")]
gsp_regions = ["BRED_1", "DEWP"]
latlons = [(53.33985, -2.051880), (55.950095, -3.178485)]
with Geocoder() as geo:
assert_equal(
Expand Down
31 changes: 10 additions & 21 deletions geocode/eurostat.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def force_setup(self):
Function to setup all lookup files.
"""
for l in range(0, 4):
self._load_nuts_boundaries(l)
self.load_nuts_boundaries(l)

def load_nuts_boundaries(
self,
Expand Down Expand Up @@ -114,29 +114,12 @@ def load_nuts_boundaries(
)
return nuts_regions

def _load_nuts_boundaries(self, level, year=2021):
"""
For backwards compatibility pending https://github.com/SheffieldSolar/Geocode/issues/6

Load the NUTS boundaries, either from local cache if available, else fetch from Eurostat
API.
"""
nuts_gdf = self.load_nuts_boundaries(level, year)
nuts_gdf["bounds"] = nuts_gdf.bounds.apply(tuple, axis=1)
nuts_dict = (
nuts_gdf[["NUTS_ID", "geometry", "bounds"]]
.set_index("NUTS_ID")
.to_dict("index")
)
for r in nuts_dict:
nuts_dict[r] = tuple(nuts_dict[r].values())
return nuts_dict

def reverse_geocode_nuts(
self,
latlons: List[Tuple[float, float]],
level: Literal[0, 1, 2, 3],
year: Literal[2003, 2006, 2010, 2013, 2016, 2021] = 2021,
**kwargs,
) -> List[str]:
"""
Reverse-geocode latitudes and longitudes to NUTS regions.
Expand All @@ -150,6 +133,8 @@ def reverse_geocode_nuts(
`year` : int
Specify the year of NUTS regulation, must be one of [2003,2006,2010,2013,2016,2021],
defaults to 2021.
`**kwargs` : dict
Options to pass to the underlying utilities.reverse_geocode method.

Returns
-------
Expand All @@ -158,8 +143,12 @@ def reverse_geocode_nuts(
do not fall inside a NUTS boundary will return None.
"""
if self.nuts_regions[(level, year)] is None:
self.nuts_regions[(level, year)] = self._load_nuts_boundaries(
self.nuts_regions[(level, year)] = self.load_nuts_boundaries(
level=level, year=year
)
results = utils.reverse_geocode(latlons, self.nuts_regions[(level, year)])
results = utils.reverse_geocode(
latlons,
self.nuts_regions[(level, year)].rename({"NUTS_ID": "region_id"}, axis=1),
**kwargs,
)
return results
42 changes: 26 additions & 16 deletions geocode/geocode.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ def geocode_llsoa(self, llsoa_boundaries):
"""
return self.geocode(llsoa_boundaries, "llsoa")

def reverse_geocode_llsoa(self, latlons, dz=True):
def reverse_geocode_llsoa(self, latlons, dz=True, **kwargs):
"""
Function to reverse geocode a collection of latlons into llsoa boundaries.

Expand All @@ -156,14 +156,21 @@ def reverse_geocode_llsoa(self, latlons, dz=True):
Specific latlons to geocode to llsoa boundaries.
`dz` : Boolean
Indication whether to consider datazones
`**kwargs`
Options to pass to the underlying utilities.reverse_geocode method.

See Also
--------
utlities.reverse_geocode : for more information on the kwargs.
"""
return self.reverse_geocode(latlons, "llsoa", datazones=dz)
return self.reverse_geocode(latlons, "llsoa", datazones=dz, **kwargs)

def reverse_geocode_nuts(
self,
latlons: List[Tuple[float, float]],
level: Literal[0, 1, 2, 3],
year: Literal[2003, 2006, 2010, 2013, 2016, 2021] = 2021,
**kwargs: Optional[Dict],
) -> List[str]:
"""
Function to reverse geocode a collection of latlons into NUTS boundaries.
Expand All @@ -177,8 +184,14 @@ def reverse_geocode_nuts(
`year` : int
Specify the year of NUTS regulation, must be one of [2003,2006,2010,2013,2016,2021],
defaults to 2021.
`**kwargs`
Options to pass to the underlying utilities.reverse_geocode method.

See Also
--------
utlities.reverse_geocode : for more information on the kwargs.
"""
return self.reverse_geocode(latlons, "nuts", level=level, year=year)
return self.reverse_geocode(latlons, "nuts", level=level, year=year, **kwargs)

def geocode_constituency(self, constituencies):
"""
Expand Down Expand Up @@ -211,7 +224,11 @@ def reverse_geocode_gsp(self, latlons, **kwargs):
`latlons` : iterable of strings
Specific latlons to geocode to gsp regions.
`**kwargs`
Options to pass to the underlying reverse_geocode_gsp method.
Options to pass to the underlying utilities.reverse_geocode method.

See Also
--------
utlities.reverse_geocode : for more information on the kwargs.
"""
return self.reverse_geocode(latlons, "gsp", **kwargs)

Expand Down Expand Up @@ -273,23 +290,16 @@ def reverse_geocode(self, latlons, entity, **kwargs):
`entity` : string
Specify the entity type to Geocode from i.e., gsp or llsoa.
`**kwargs`
Options to pass to the underlying reverse-geocode method.
Options to pass to the underlying reverse-geocode method, eg., `max_distance`
"""
entity = entity.lower()
if entity == "gsp":
version = kwargs.get("version", "20250109")
return self.neso.reverse_geocode_gsp(latlons, version)
version = kwargs.pop("version", "20250109")
return self.neso.reverse_geocode_gsp(latlons, version, **kwargs)
elif entity == "llsoa":
datazones = kwargs.get("datazones", False)
return self.ons_nrs.reverse_geocode_llsoa(
latlons=latlons, datazones=datazones
)
return self.ons_nrs.reverse_geocode_llsoa(latlons=latlons, **kwargs)
elif entity == "nuts":
level = kwargs.get("level")
year = kwargs.get("year", 2021)
return self.eurostat.reverse_geocode_nuts(
latlons=latlons, level=level, year=year
)
return self.eurostat.reverse_geocode_nuts(latlons=latlons, **kwargs)
else:
raise GenericException(f"Entity '{entity}' is not supported.")

Expand Down
51 changes: 13 additions & 38 deletions geocode/neso.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def __init__(self, cache_manager, proxies=None, ssl_verify=True):
self.gsp_boundaries_20220314_cache_file = "gsp_boundaries_20220314"
self.gsp_boundaries_20181031_cache_file = "gsp_boundaries_20181031"
self.dno_boundaries_cache_file = "dno_boundaries"
self.gsp_regions_dict = None
self.gsp_regions = None
self.gsp_regions_20181031 = None
self.dno_regions = None
self.gsp_lookup_20181031 = None
Expand Down Expand Up @@ -104,8 +104,6 @@ def _load_gsp_boundaries_20250109(self):
-------
gsp_regions: GeoPandas.GeoDataFrame
A geodataframe of MultiPolygons for the GSP boundaries.
gsp_regions_dict: Dict
GSP boundaries as a dictionary for backwards compatibility with utilities methods.
"""
gsp_boundaries_cache_contents = self.cache_manager.retrieve(
self.gsp_boundaries_20250109_cache_file
Expand Down Expand Up @@ -139,21 +137,12 @@ def _load_gsp_boundaries_20250109(self):
raise utils.GenericException(
"Encountered an error while extracting GSP region data from ESO " "API."
)
### For backwards compatibility pending https://github.com/SheffieldSolar/Geocode/issues/6
gsp_regions_ = gsp_regions.dissolve(by=["GSPs", "GSPGroup"])
gsp_regions_["bounds"] = gsp_regions_.bounds.apply(tuple, axis=1)
gsp_regions_dict = gsp_regions_.to_dict(orient="index")
for r in gsp_regions_dict:
gsp_regions_dict[r] = tuple(gsp_regions_dict[r].values())
######
self.cache_manager.write(
self.gsp_boundaries_20250109_cache_file, (gsp_regions, gsp_regions_dict)
)
self.cache_manager.write(self.gsp_boundaries_20250109_cache_file, gsp_regions)
logging.info(
"20250109 GSP boundaries extracted and pickled to '%s'",
self.gsp_boundaries_20250109_cache_file,
)
return gsp_regions, gsp_regions_dict
return gsp_regions

def _load_gsp_boundaries_20220314(self):
"""
Expand All @@ -164,8 +153,6 @@ def _load_gsp_boundaries_20220314(self):
-------
gsp_regions: GeoPandas.GeoDataFrame
A geodataframe of MultiPolygons for the GSP boundaries.
gsp_regions_dict: Dict
GSP boundaries as a dictionary for backwards compatibility with utilities methods.
"""
gsp_boundaries_cache_contents = self.cache_manager.retrieve(
self.gsp_boundaries_20220314_cache_file
Expand Down Expand Up @@ -194,20 +181,12 @@ def _load_gsp_boundaries_20220314(self):
raise utils.GenericException(
"Encountered an error while extracting GSP region data from ESO " "API."
)
### For backwards compatibility pending https://github.com/SheffieldSolar/Geocode/issues/6
gsp_regions["bounds"] = gsp_regions.bounds.apply(tuple, axis=1)
gsp_regions_dict = gsp_regions.set_index(["GSPs", "GSPGroup"]).to_dict("index")
for r in gsp_regions_dict:
gsp_regions_dict[r] = tuple(gsp_regions_dict[r].values())
######
self.cache_manager.write(
self.gsp_boundaries_20220314_cache_file, (gsp_regions, gsp_regions_dict)
)
self.cache_manager.write(self.gsp_boundaries_20220314_cache_file, gsp_regions)
logging.info(
"20220314 GSP boundaries extracted and pickled to '%s'",
self.gsp_boundaries_20220314_cache_file,
)
return gsp_regions, gsp_regions_dict
return gsp_regions

def load_gsp_boundaries(self, version: str):
"""
Expand All @@ -222,8 +201,6 @@ def load_gsp_boundaries(self, version: str):
-------
gsp_regions: GeoPandas.GeoDataFrame
A geodataframe of MultiPolygons for the GSP boundaries.
gsp_regions_dict: Dict
GSP boundaries as a dictionary for backwards compatibility with utilities methods.
"""
if version == "20250109":
return self._load_gsp_boundaries_20250109()
Expand Down Expand Up @@ -284,7 +261,7 @@ def _load_dno_boundaries(self):
return dno_regions, dno_names

def reverse_geocode_gsp(
self, latlons: List[Tuple[float, float]], version: str
self, latlons: List[Tuple[float, float]], version: str, **kwargs
) -> Tuple[List[int], List[List[Dict]]]:
"""
Reverse-geocode latitudes and longitudes to GSP using the 20220314 definitions.
Expand All @@ -294,6 +271,8 @@ def reverse_geocode_gsp(
`latlons` : list of tuples
A list of tuples containing (latitude, longitude).
`version` : string
`kwargs`: dict
Options to pass to the underlying utilities.reverse_geocode method.

Returns
-------
Expand All @@ -305,17 +284,13 @@ def reverse_geocode_gsp(
Return format needs some work, maybe switch to DataFrames in future release.
"""
logging.debug(f"Reverse geocoding {len(latlons)} latlons to {version} GSP")
if self.gsp_regions_dict is None:
_, self.gsp_regions_dict = self.load_gsp_boundaries(version=version)
lats = [l[0] for l in latlons]
lons = [l[1] for l in latlons]
# Rather than re-project the region boundaries, re-project the input lat/lons
# (easier, but slightly slower if reverse-geocoding a lot)
logging.debug("Converting latlons to BNG")
eastings, northings = utils.latlon2bng(lons, lats)
if self.gsp_regions is None:
self.gsp_regions = self.load_gsp_boundaries(version=version)
logging.debug("Reverse geocoding")
results = utils.reverse_geocode(
list(zip(northings, eastings)), self.gsp_regions_dict
latlons,
self.gsp_regions.rename({"GSPs": "region_id"}, axis=1),
**kwargs,
)
return results

Expand Down
Loading