mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-09-01 12:15:15 -04:00
Merge pull request #3833 from alphagov/vary-bleed-by-population-density
Vary bleed amount based on population density
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import math
|
||||
|
||||
from notifications_utils.formatters import formatted_list
|
||||
from notifications_utils.polygons import Polygons
|
||||
from notifications_utils.serialised_model import SerialisedModelCollection
|
||||
@@ -49,6 +51,10 @@ class BroadcastArea(SortableMixin):
|
||||
BroadcastAreasRepository().get_simple_polygons_for_area(self.id)
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def simple_polygons_with_bleed(self):
|
||||
return self.simple_polygons.bleed_by(self.estimated_bleed_in_degrees)
|
||||
|
||||
@cached_property
|
||||
def sub_areas(self):
|
||||
return [
|
||||
@@ -68,6 +74,27 @@ class BroadcastArea(SortableMixin):
|
||||
# https://www.pivotaltracker.com/story/show/174837293
|
||||
return self._count_of_phones or 0
|
||||
|
||||
@cached_property
|
||||
def phone_density(self):
|
||||
return self.count_of_phones / self.polygons.estimated_area
|
||||
|
||||
@property
|
||||
def estimated_bleed_in_m(self):
|
||||
'''
|
||||
Estimates the amount of bleed based on the population of an
|
||||
area. Higher density areas tend to have short range masts, so
|
||||
the bleed is low (down to 500m). Lower density areas have longer
|
||||
range masts, so the typical bleed will be high (up to 5,000m).
|
||||
'''
|
||||
if self.phone_density < 1:
|
||||
return Polygons.approx_bleed_in_degrees * Polygons.approx_metres_to_degree
|
||||
estimated_bleed = 5_900 - (math.log(self.phone_density, 10) * 1_250)
|
||||
return max(500, min(estimated_bleed, 5000))
|
||||
|
||||
@property
|
||||
def estimated_bleed_in_degrees(self):
|
||||
return self.estimated_bleed_in_m / Polygons.approx_metres_to_degree
|
||||
|
||||
@cached_property
|
||||
def parents(self):
|
||||
return list(filter(None, self._parents_iterator))
|
||||
@@ -109,6 +136,13 @@ class CustomBroadcastArea:
|
||||
|
||||
simple_polygons = polygons
|
||||
|
||||
@cached_property
|
||||
def simple_polygons_with_bleed(self):
|
||||
# We don’t yet have a way of working out the population density
|
||||
# of a custom area, so for now we have to use an average number
|
||||
# to estimate the amount of bleed
|
||||
return self.simple_polygons.bleed_by(Polygons.approx_bleed_in_degrees)
|
||||
|
||||
|
||||
class CustomBroadcastAreas(SerialisedModelCollection):
|
||||
model = CustomBroadcastArea
|
||||
|
||||
@@ -117,6 +117,17 @@ class BroadcastMessage(JSONModel):
|
||||
def simple_polygons(self):
|
||||
return self.get_simple_polygons(areas=self.areas)
|
||||
|
||||
@cached_property
|
||||
def simple_polygons_with_bleed(self):
|
||||
polygons = Polygons(
|
||||
list(itertools.chain(*(
|
||||
area.simple_polygons_with_bleed for area in self.areas
|
||||
)))
|
||||
)
|
||||
# If we’ve added multiple areas then we need to re-simplify the
|
||||
# combined shapes to keep the point count down
|
||||
return polygons.smooth.simplify if len(self.areas) > 1 else polygons
|
||||
|
||||
@property
|
||||
def reference(self):
|
||||
if self.template_id:
|
||||
@@ -163,7 +174,7 @@ class BroadcastMessage(JSONModel):
|
||||
@property
|
||||
def count_of_phones_likely(self):
|
||||
area_estimate = self.simple_polygons.estimated_area
|
||||
bleed_area_estimate = self.simple_polygons.bleed.estimated_area - area_estimate
|
||||
bleed_area_estimate = self.simple_polygons_with_bleed.estimated_area - area_estimate
|
||||
return round_to_significant_figures(
|
||||
self.count_of_phones + (self.count_of_phones * bleed_area_estimate / area_estimate),
|
||||
1
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</li>
|
||||
<li class="area-list-key area-list-key--likely">
|
||||
<span class="visually-hidden">
|
||||
An extra area of {{ "{:,.1f}".format(broadcast_message.simple_polygons.bleed.estimated_area - broadcast_message.simple_polygons.estimated_area) }} square miles is
|
||||
An extra area of {{ "{:,.1f}".format(broadcast_message.simple_polygons_with_bleed.estimated_area - broadcast_message.simple_polygons.estimated_area) }} square miles is
|
||||
</span>
|
||||
Likely to get
|
||||
<span class="visually-hidden">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<script>
|
||||
var polygons = []
|
||||
|
||||
{% for polygon in broadcast_message.simple_polygons.bleed.as_coordinate_pairs_lat_long %}
|
||||
{% for polygon in broadcast_message.simple_polygons_with_bleed.as_coordinate_pairs_lat_long %}
|
||||
polygons.push(
|
||||
L.polygon({{polygon}}, {
|
||||
opacity: 1,
|
||||
|
||||
@@ -24,7 +24,7 @@ Shapely==1.7.1
|
||||
awscli-cwlogs>=1.4,<1.5
|
||||
itsdangerous==1.1.0
|
||||
|
||||
git+https://github.com/alphagov/notifications-utils.git@43.8.3#egg=notifications-utils==43.8.3
|
||||
git+https://github.com/alphagov/notifications-utils.git@44.0.0#egg=notifications-utils==44.0.0
|
||||
git+https://github.com/alphagov/govuk-frontend-jinja.git@v0.5.8-alpha#egg=govuk-frontend-jinja==0.5.8-alpha
|
||||
|
||||
# gds-metrics requires prometheseus 0.2.0, override that requirement as later versions bring significant performance gains
|
||||
|
||||
@@ -110,7 +110,7 @@ mistune==0.8.4
|
||||
# via notifications-utils
|
||||
notifications-python-client==6.0.2
|
||||
# via -r requirements.in
|
||||
git+https://github.com/alphagov/notifications-utils.git@43.8.3#egg=notifications-utils==43.8.3
|
||||
git+https://github.com/alphagov/notifications-utils.git@44.0.0#egg=notifications-utils==44.0.0
|
||||
# via -r requirements.in
|
||||
openpyxl==3.0.6
|
||||
# via pyexcel-xlsx
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from math import isclose
|
||||
|
||||
import pytest
|
||||
|
||||
from app.broadcast_areas import (
|
||||
@@ -10,6 +12,10 @@ from app.broadcast_areas.populations import (
|
||||
)
|
||||
|
||||
|
||||
def close_enough(a, b):
|
||||
return isclose(a, b, rel_tol=0.001) # Within 0.1% difference
|
||||
|
||||
|
||||
def test_loads_libraries():
|
||||
assert [
|
||||
(library.id, library.name, library.is_group) for library in sorted(broadcast_area_libraries)
|
||||
@@ -272,3 +278,75 @@ def test_estimate_number_of_smartphones_for_population(
|
||||
assert estimate_number_of_smartphones_for_population(
|
||||
population
|
||||
) == expected_estimate
|
||||
|
||||
|
||||
@pytest.mark.parametrize('area, expected_phones_per_square_mile', (
|
||||
(
|
||||
# Islington (most dense in UK)
|
||||
'lad20-E09000019', 21_348
|
||||
),
|
||||
(
|
||||
# Cordwainer Ward (City of London)
|
||||
# This is higher than Islington because we inflate the
|
||||
# popualtion to account for daytime workers
|
||||
'wd20-E05009300', 310_674
|
||||
),
|
||||
(
|
||||
# Crewe East
|
||||
'wd20-E05008621', 2_078),
|
||||
(
|
||||
# Eden (Cumbria, least dense in England)
|
||||
'lad20-E07000030', 25.57
|
||||
),
|
||||
(
|
||||
# Highland (least dense in UK)
|
||||
'lad20-S12000017', 4.40
|
||||
),
|
||||
))
|
||||
def test_phone_density(
|
||||
area, expected_phones_per_square_mile,
|
||||
):
|
||||
assert close_enough(
|
||||
broadcast_area_libraries.get_areas(area)[0].phone_density,
|
||||
expected_phones_per_square_mile,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('area, expected_bleed_in_m, expected_bleed_in_degrees', (
|
||||
(
|
||||
# Islington (most dense in UK)
|
||||
'lad20-E09000019', 500, 0.00449
|
||||
),
|
||||
(
|
||||
# Cordwainer Ward (City of London)
|
||||
# Special case because of inflated daytime population
|
||||
'wd20-E05009300', 500, 0.00449
|
||||
),
|
||||
(
|
||||
# Crewe East
|
||||
'wd20-E05008621', 1_752, 0.01574
|
||||
),
|
||||
(
|
||||
# Eden (Cumbria, least dense in England)
|
||||
'lad20-E07000030', 4_140, 0.0372
|
||||
),
|
||||
(
|
||||
# Highland (least dense in UK)
|
||||
'lad20-S12000017', 5_000, 0.0449
|
||||
),
|
||||
(
|
||||
# No population data available
|
||||
'test-santa-claus-village-rovaniemi', 1_500, 0.01347
|
||||
)
|
||||
))
|
||||
def test_estimated_bleed(
|
||||
area, expected_bleed_in_m, expected_bleed_in_degrees,
|
||||
):
|
||||
assert close_enough(
|
||||
broadcast_area_libraries.get_areas(area)[0].estimated_bleed_in_m,
|
||||
expected_bleed_in_m,
|
||||
)
|
||||
assert close_enough(
|
||||
broadcast_area_libraries.get_areas(area)[0].estimated_bleed_in_degrees,
|
||||
expected_bleed_in_degrees,
|
||||
)
|
||||
|
||||
@@ -623,7 +623,7 @@ def test_broadcast_page(
|
||||
'Scotland remove',
|
||||
], [
|
||||
'An area of 177,439.8 square miles Will get the alert',
|
||||
'An extra area of 3,058.9 square miles is Likely to get the alert',
|
||||
'An extra area of 6,392.3 square miles is Likely to get the alert',
|
||||
'40,000,000 phones estimated',
|
||||
]),
|
||||
([
|
||||
@@ -640,8 +640,17 @@ def test_broadcast_page(
|
||||
'Penrith West remove',
|
||||
], [
|
||||
'An area of 6.3 square miles Will get the alert',
|
||||
'An extra area of 14.4 square miles is Likely to get the alert',
|
||||
'9,000 to 30,000 phones',
|
||||
'An extra area of 22.6 square miles is Likely to get the alert',
|
||||
'9,000 to 40,000 phones',
|
||||
]),
|
||||
([
|
||||
'lad20-E09000019',
|
||||
], [
|
||||
'Islington remove',
|
||||
], [
|
||||
'An area of 9.7 square miles Will get the alert',
|
||||
'An extra area of 4.7 square miles is Likely to get the alert',
|
||||
'200,000 to 300,000 phones',
|
||||
]),
|
||||
))
|
||||
def test_preview_broadcast_areas_page(
|
||||
@@ -726,7 +735,7 @@ def test_preview_broadcast_areas_page_with_custom_polygons(
|
||||
for item in page.select('ul li.area-list-key')
|
||||
] == [
|
||||
'An area of 722.3 square miles Will get the alert',
|
||||
'An extra area of 1,402.5 square miles is Likely to get the alert',
|
||||
'An extra area of 1,498.5 square miles is Likely to get the alert',
|
||||
'Unknown number of phones',
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user