Merge pull request #3833 from alphagov/vary-bleed-by-population-density

Vary bleed amount based on population density
This commit is contained in:
Chris Hill-Scott
2021-03-22 10:03:19 +00:00
committed by GitHub
8 changed files with 141 additions and 9 deletions

View File

@@ -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 dont 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

View File

@@ -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 weve 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

View File

@@ -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">

View File

@@ -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,

View File

@@ -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

View File

@@ -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

View File

@@ -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,
)

View File

@@ -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',
]