Skip to content

Commit

Permalink
Merge pull request #4 from IATI/add-normalisation
Browse files Browse the repository at this point in the history
Add normalisation
  • Loading branch information
leilafarsani authored Dec 4, 2024
2 parents c48e3ff + f2b1373 commit 611f857
Show file tree
Hide file tree
Showing 2 changed files with 137 additions and 11 deletions.
47 changes: 36 additions & 11 deletions iati_activity_details_split_by_fields/iati_activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,17 +72,42 @@ def get_transactions_split_as_json(self):
return [x.get_as_json() for x in self.get_transactions_split()]

def _get_recipient_countries_with_normalised_percentages(self):
# TODO
return self.recipient_countries
"""Normalise country percentages to ensure they sum to 100%"""
if not self.recipient_countries:
return []
total_percentage = sum(
country.percentage or 0 for country in self.recipient_countries
)
if total_percentage == 0:
return self.recipient_countries

normalized_countries = copy.deepcopy(self.recipient_countries)

for country in normalized_countries:
if country.percentage:
country.percentage = (country.percentage / total_percentage) * 100

return normalized_countries

def _get_sectors_grouped_by_vocab_with_normalised_percentages(self) -> dict:
output: dict = {}
# Group by vocab
"""Group sectors by vocabulary and normalise percentages within each group"""
if not self.sectors:
return {}

# First group by vocab
grouped: dict = {}
for sector in self.sectors:
if sector.vocabulary not in output:
output[sector.vocabulary] = []
output[sector.vocabulary].append(sector)
# Now normalise percentages
# TODO
# Ready!
return output
vocab = sector.vocabulary or "default"
if vocab not in grouped:
grouped[vocab] = []
grouped[vocab].append(copy.deepcopy(sector))

# Now normalise percentages within each vocab group
for vocab, sectors in grouped.items():
total = sum(sector.percentage or 0 for sector in sectors)
if total > 0: # we only normalise if we have valid percentages
for sector in sectors:
if sector.percentage:
sector.percentage = (sector.percentage / total) * 100

return grouped
101 changes: 101 additions & 0 deletions tests/test_at_activity_level.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,28 @@ def test_split_by_country():
] == results


def test_split_by_country_with_incorrect_percentages():
"""Test that country splits work correctly even with percentages that don't sum to 100"""

iati_activity = IATIActivity(
transactions=[IATIActivityTransaction(value=1000)],
recipient_countries=[
IATIActivityRecipientCountry(code="FR", percentage=30), # 30%
IATIActivityRecipientCountry(code="GB", percentage=40), # 40%
],
)

results = iati_activity.get_transactions_split_as_json()

# Check results with ranges
assert len(results) == 2
assert results[0]["recipient_country_code"] == "FR"
assert 428.57 <= results[0]["value"] <= 428.58
assert results[1]["recipient_country_code"] == "GB"
assert 571.42 <= results[1]["value"] <= 571.43
assert all(r["sectors"] == [] for r in results)


def test_no_split_but_sector_set():
"""If a activity only has one sector (per vocab), then no split should happen but the sectors should appear on the transactions"""

Expand Down Expand Up @@ -131,6 +153,42 @@ def test_split_by_sector():
] == results


def test_split_by_sector_with_incorrect_percentages():
"""Test that sector splits work correctly even with percentages that don't sum to 100"""

iati_activity = IATIActivity(
transactions=[IATIActivityTransaction(value=1000)],
sectors=[
IATIActivitySector(vocabulary="cats", code="Henry", percentage=30), # 30%
IATIActivitySector(vocabulary="cats", code="Linda", percentage=40), # 40%
IATIActivitySector(vocabulary="dogs", code="Rover", percentage=100), # 100%
],
)

results = iati_activity.get_transactions_split_as_json()

# Check results with ranges
assert len(results) == 3

# Check Henry (cats)
assert results[0]["sectors"][0]["code"] == "Henry"
assert results[0]["sectors"][0]["vocabulary"] == "cats"
assert 428.57 <= results[0]["value"] <= 428.58
assert results[0]["recipient_country_code"] is None

# Check Linda (cats)
assert results[1]["sectors"][0]["code"] == "Linda"
assert results[1]["sectors"][0]["vocabulary"] == "cats"
assert 571.42 <= results[1]["value"] <= 571.43
assert results[1]["recipient_country_code"] is None

# Check Rover (dogs)
assert results[2]["sectors"][0]["code"] == "Rover"
assert results[2]["sectors"][0]["vocabulary"] == "dogs"
assert results[2]["value"] == 1000
assert results[2]["recipient_country_code"] is None


def test_split_by_everything():
iati_activity = IATIActivity(
transactions=[IATIActivityTransaction(value=1000)],
Expand Down Expand Up @@ -186,3 +244,46 @@ def test_split_by_everything():
# It's possible to verify this by hand,
# but may as well get Python to check for us and avoid extra work and the possibility of mistakes
# (Can use in other tests too)
# Note: This is now implemented in test_no_double_counting test (with one sector vocab)


def test_no_double_counting():
"""Test that split transactions sum up to original amount"""

# Create activity with both country and sector splits
iati_activity = IATIActivity(
transactions=[IATIActivityTransaction(value=1000)],
recipient_countries=[
IATIActivityRecipientCountry(code="FR", percentage=60),
IATIActivityRecipientCountry(code="GB", percentage=40),
],
sectors=[
IATIActivitySector(vocabulary="cats", code="Health", percentage=70),
IATIActivitySector(vocabulary="cats", code="Education", percentage=30),
],
)

results = iati_activity.get_transactions_split_as_json()

# Checking total value matches original
total_value = sum(r["value"] for r in results)
assert total_value == 1000, f"Total {total_value} should equal original 1000"

# Checking country totals
country_totals = {}
for r in results:
country = r["recipient_country_code"]
country_totals[country] = country_totals.get(country, 0) + r["value"]

assert country_totals["FR"] == 600 # 60% of 1000
assert country_totals["GB"] == 400 # 40% of 1000

# Checking sector totals
sector_totals = {}
for r in results:
if r["sectors"]:
sector = r["sectors"][0]["code"]
sector_totals[sector] = sector_totals.get(sector, 0) + r["value"]

assert sector_totals["Health"] == 700 # 70% of 1000
assert sector_totals["Education"] == 300 # 30% of 1000

0 comments on commit 611f857

Please sign in to comment.