mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-09-08 08:28: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.formatters import formatted_list
|
||||||
from notifications_utils.polygons import Polygons
|
from notifications_utils.polygons import Polygons
|
||||||
from notifications_utils.serialised_model import SerialisedModelCollection
|
from notifications_utils.serialised_model import SerialisedModelCollection
|
||||||
@@ -49,6 +51,10 @@ class BroadcastArea(SortableMixin):
|
|||||||
BroadcastAreasRepository().get_simple_polygons_for_area(self.id)
|
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
|
@cached_property
|
||||||
def sub_areas(self):
|
def sub_areas(self):
|
||||||
return [
|
return [
|
||||||
@@ -68,6 +74,27 @@ class BroadcastArea(SortableMixin):
|
|||||||
# https://www.pivotaltracker.com/story/show/174837293
|
# https://www.pivotaltracker.com/story/show/174837293
|
||||||
return self._count_of_phones or 0
|
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
|
@cached_property
|
||||||
def parents(self):
|
def parents(self):
|
||||||
return list(filter(None, self._parents_iterator))
|
return list(filter(None, self._parents_iterator))
|
||||||
@@ -109,6 +136,13 @@ class CustomBroadcastArea:
|
|||||||
|
|
||||||
simple_polygons = polygons
|
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):
|
class CustomBroadcastAreas(SerialisedModelCollection):
|
||||||
model = CustomBroadcastArea
|
model = CustomBroadcastArea
|
||||||
|
|||||||
@@ -117,6 +117,17 @@ class BroadcastMessage(JSONModel):
|
|||||||
def simple_polygons(self):
|
def simple_polygons(self):
|
||||||
return self.get_simple_polygons(areas=self.areas)
|
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
|
@property
|
||||||
def reference(self):
|
def reference(self):
|
||||||
if self.template_id:
|
if self.template_id:
|
||||||
@@ -163,7 +174,7 @@ class BroadcastMessage(JSONModel):
|
|||||||
@property
|
@property
|
||||||
def count_of_phones_likely(self):
|
def count_of_phones_likely(self):
|
||||||
area_estimate = self.simple_polygons.estimated_area
|
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(
|
return round_to_significant_figures(
|
||||||
self.count_of_phones + (self.count_of_phones * bleed_area_estimate / area_estimate),
|
self.count_of_phones + (self.count_of_phones * bleed_area_estimate / area_estimate),
|
||||||
1
|
1
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li class="area-list-key area-list-key--likely">
|
<li class="area-list-key area-list-key--likely">
|
||||||
<span class="visually-hidden">
|
<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>
|
</span>
|
||||||
Likely to get
|
Likely to get
|
||||||
<span class="visually-hidden">
|
<span class="visually-hidden">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<script>
|
<script>
|
||||||
var polygons = []
|
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(
|
polygons.push(
|
||||||
L.polygon({{polygon}}, {
|
L.polygon({{polygon}}, {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ Shapely==1.7.1
|
|||||||
awscli-cwlogs>=1.4,<1.5
|
awscli-cwlogs>=1.4,<1.5
|
||||||
itsdangerous==1.1.0
|
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
|
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
|
# 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
|
# via notifications-utils
|
||||||
notifications-python-client==6.0.2
|
notifications-python-client==6.0.2
|
||||||
# via -r requirements.in
|
# 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
|
# via -r requirements.in
|
||||||
openpyxl==3.0.6
|
openpyxl==3.0.6
|
||||||
# via pyexcel-xlsx
|
# via pyexcel-xlsx
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from math import isclose
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from app.broadcast_areas import (
|
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():
|
def test_loads_libraries():
|
||||||
assert [
|
assert [
|
||||||
(library.id, library.name, library.is_group) for library in sorted(broadcast_area_libraries)
|
(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(
|
assert estimate_number_of_smartphones_for_population(
|
||||||
population
|
population
|
||||||
) == expected_estimate
|
) == 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',
|
'Scotland remove',
|
||||||
], [
|
], [
|
||||||
'An area of 177,439.8 square miles Will get the alert',
|
'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',
|
'40,000,000 phones estimated',
|
||||||
]),
|
]),
|
||||||
([
|
([
|
||||||
@@ -640,8 +640,17 @@ def test_broadcast_page(
|
|||||||
'Penrith West remove',
|
'Penrith West remove',
|
||||||
], [
|
], [
|
||||||
'An area of 6.3 square miles Will get the alert',
|
'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',
|
'An extra area of 22.6 square miles is Likely to get the alert',
|
||||||
'9,000 to 30,000 phones',
|
'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(
|
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')
|
for item in page.select('ul li.area-list-key')
|
||||||
] == [
|
] == [
|
||||||
'An area of 722.3 square miles Will get the alert',
|
'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',
|
'Unknown number of phones',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user