Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Import overlapping constituencies #345

Merged
merged 3 commits into from
Nov 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion hub/management/commands/import_new_constituencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
from django.core.management.base import BaseCommand
from django.db.utils import IntegrityError

from mysoc_dataset import get_dataset_df
from tqdm import tqdm

from hub.models import Area, AreaType
from hub.models import Area, AreaOverlap, AreaType
from utils.constituency_mapping import get_overlap_df


class Command(BaseCommand):
Expand Down Expand Up @@ -57,3 +59,30 @@ def handle(self, quiet: bool = False, *args, **options):
con["properties"]["type"] = "WMC23"
a.geometry = json.dumps(con)
a.save()

constituency_lookup = (
get_dataset_df(
repo_name="2025-constituencies",
package_name="parliament_con_2025",
version_name="latest",
file_name="parl_constituencies_2025.csv",
)
.set_index("short_code")["gss_code"]
.to_dict()
)

df = get_overlap_df("PARL10", "PARL25")
for area in Area.objects.filter(area_type__code="WMC"):
overlap_constituencies = df.query("PARL10 == @area.gss")
for _, row in overlap_constituencies.iterrows():
new_area = Area.objects.get(
area_type__code="WMC23", gss=constituency_lookup[row["PARL25"]]
)
AreaOverlap.objects.get_or_create(
area_old=area,
area_new=new_area,
defaults={
"population_overlap": int(row["percentage_overlap_pop"] * 100),
"area_overlap": int(row["percentage_overlap_area"] * 100),
},
)
51 changes: 51 additions & 0 deletions hub/migrations/0058_areaoverlap_area_overlaps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Generated by Django 4.2.5 on 2023-11-14 11:15

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
("hub", "0057_populate_dataset_areas_available"),
]

operations = [
migrations.CreateModel(
name="AreaOverlap",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("population_overlap", models.SmallIntegerField(default=0)),
("area_overlap", models.SmallIntegerField(default=0)),
(
"area_new",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="new_overlaps",
to="hub.area",
),
),
(
"area_old",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="old_overlaps",
to="hub.area",
),
),
],
),
migrations.AddField(
model_name="area",
name="overlaps",
field=models.ManyToManyField(through="hub.AreaOverlap", to="hub.area"),
),
]
12 changes: 12 additions & 0 deletions hub/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,7 @@
name = models.CharField(max_length=200)
area_type = models.ForeignKey(AreaType, on_delete=models.CASCADE)
geometry = models.TextField(blank=True, null=True)
overlaps = models.ManyToManyField("self", through="AreaOverlap")

def __str__(self):
return self.name
Expand Down Expand Up @@ -535,17 +536,28 @@

@classmethod
def get_by_name(cls, name, area_type="WMC"):
try:
area = cls.objects.get(name__iexact=name, area_type__code=area_type)
except cls.DoesNotExist:
area = None

Check warning on line 542 in hub/models.py

View check run for this annotation

Codecov / codecov/patch

hub/models.py#L539-L542

Added lines #L539 - L542 were not covered by tests

return area

Check warning on line 544 in hub/models.py

View check run for this annotation

Codecov / codecov/patch

hub/models.py#L544

Added line #L544 was not covered by tests

class Meta:
unique_together = ["gss", "area_type"]


class AreaOverlap(models.Model):
area_old = models.ForeignKey(
Area, on_delete=models.CASCADE, related_name="old_overlaps"
)
area_new = models.ForeignKey(
Area, on_delete=models.CASCADE, related_name="new_overlaps"
)
population_overlap = models.SmallIntegerField(default=0)
area_overlap = models.SmallIntegerField(default=0)


class AreaData(CommonData):
area = models.ForeignKey(Area, on_delete=models.CASCADE)

Expand Down
49 changes: 34 additions & 15 deletions hub/templates/hub/area.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,24 +41,43 @@ <h1 class="m-0">{{ area.name }}</h1>
</div>
</div>
<div class="col-lg-9 offset-xl-1">
{% if area_type == "WMC" %}
<div class="card flex-grow-1 dataset-card">
<div class="dataset-card-header">
<h2 class="h5">This is a 2010 constituency</h2>
{% if area_type == "WMC" and area.overlaps %}
<div class="card flex-grow-1">
<div class="card-body">
{% if overlap_constituencies|length > 1 %}
<p>At the next election, this constituency will be divided into <b>{{ overlap_constituencies|length|apnumber }} new constituenc{% if overlap_constituencies|length_is:1 %}y{% else %}ies{% endif %}:</b></p>
{% else %}
<p>At the next election, this constituency will be replaced with:</p>
{% endif %}
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); grid-gap: 1rem 2rem;">
{% for overlap_constituency in overlap_constituencies %}
<div>
<h3 class="h5"><a href="{% url 'area' area_type=overlap_constituency.new_area.area_type.code name=overlap_constituency.new_area.name %}" class="text-decoration-none">{{ overlap_constituency.new_area }}</a></h3>
<p class="fs-7 text-muted mb-0">Will cover approximately {{ overlap_constituency.pop_overlap }}% of this constituency’s population, and {{ overlap_constituency.area_overlap }}% of this constituency’s area.</p>
</div>
{% endfor %}
</div>
</div>
</div>
{% elif area_type == "WMC23" and area.overlaps %}
<div class="card flex-grow-1">
{% if area.overlaps %}
<div class="card-body">
<p>At the next election, people from this constituency will be divided into <b>{{ overlap_constituencies|length|apnumber }} constituenc{% if overlap_constituencies|length_is:1 %}y{% else %}ies{% endif %}:</b></p>
<table class="table mb-0">
<tbody>
{% for overlap_constituency in overlap_constituencies %}
<tr>
<th><a href="{% url 'area' area_type=overlap_constituency.area.area_type.code name=overlap_constituency.area.name %}" class="text-decoration-none">{{ overlap_constituency.area }}</a></th>
<td class="text-muted">Covers approximately {{ overlap_constituency.pop_overlap }}% of this constituency’s population, and {{ overlap_constituency.area_overlap }}% of this constituency’s area.</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if overlap_constituencies|length > 1 %}
<p>This constituency will only exist at the next election, and replaces the previous <b>{{ overlap_constituencies|length|apnumber }} constituenc{% if overlap_constituencies|length_is:1 %}y{% else %}ies{% endif %}:</b></p>
{% else %}
<p>This constituency will only exist at the next election, and replaces:</p>
{% endif %}
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); grid-gap: 1rem 2rem;">
{% for overlap_constituency in overlap_constituencies %}
<div>
<h3 class="h5"><a href="{% url 'area' area_type=overlap_constituency.old_area.area_type.code name=overlap_constituency.old_area.name %}" class="text-decoration-none">{{ overlap_constituency.old_area }}</a></h3>
<p class="fs-7 text-muted mb-0">Covered approximately {{ overlap_constituency.pop_overlap }}% of this constituency’s population, and {{ overlap_constituency.area_overlap }}% of this constituency’s area.</p>
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
{% endif %}

Expand Down
31 changes: 11 additions & 20 deletions hub/views/area.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
from django.shortcuts import get_object_or_404, redirect
from django.views.generic import DetailView, TemplateView, View

from mysoc_dataset import get_dataset_df

from hub.mixins import TitleMixin
from hub.models import (
Area,
Expand All @@ -18,7 +16,6 @@
UserDataSets,
)
from utils import is_valid_postcode
from utils.constituency_mapping import get_overlap_df
from utils.mapit import (
BadRequestException,
ForbiddenException,
Expand Down Expand Up @@ -180,27 +177,21 @@
return tags

def get_overlap_info(self, **kwargs):
# Get lookup between short code and GSS
constituency_lookup = (
get_dataset_df(
repo_name="2025-constituencies",
package_name="parliament_con_2025",
version_name="latest",
file_name="parl_constituencies_2025.csv",
)
.set_index("short_code")["gss_code"]
.to_dict()
)
if self.object.area_type.code == "WMC":
overlaps = self.object.old_overlaps.all()
elif self.object.area_type.code == "WMC23":
overlaps = self.object.new_overlaps.all()

Check warning on line 183 in hub/views/area.py

View check run for this annotation

Codecov / codecov/patch

hub/views/area.py#L183

Added line #L183 was not covered by tests
else:
return []

Check warning on line 185 in hub/views/area.py

View check run for this annotation

Codecov / codecov/patch

hub/views/area.py#L185

Added line #L185 was not covered by tests

df = get_overlap_df("PARL10", "PARL25")
overlap_constituencies = df.query("PARL10 == @self.object.gss")
overlap_constituencies = [
{
"area": Area.objects.get(gss=constituency_lookup[row["PARL25"]]),
"pop_overlap": int(row["percentage_overlap_pop"] * 100),
"area_overlap": int(row["percentage_overlap_area"] * 100),
"new_area": overlap.area_new,
"old_area": overlap.area_old,
"pop_overlap": overlap.population_overlap,
"area_overlap": overlap.area_overlap,
}
for index, row in overlap_constituencies.iterrows()
for overlap in overlaps
]
return overlap_constituencies

Expand Down
Loading