Fair Allocation with Priority¶
Django REST API that takes meal ratios and a total portion count, then distributes portions fairly across meal types.
Problem¶
Given a dictionary of {meal_id: ratio} and a total number of portions, return a JSON response showing how many portions each meal type receives. The allocation must:
- Guarantee every meal type gets at least a minimum share.
- Distribute remaining portions to the highest-ratio meal types.
Approach¶
A two-pass rules engine:
-
Minimum allocation. Calculate the smallest equal portion that satisfies all meal types:
ceil((1 / sum_of_ratios) * total_portions). Assign this to every meal type with a non-zero ratio. Track the remainder. -
Remainder to top ratios. Sort meal types by ratio descending. Distribute leftover portions equally among the tied-for-highest ratio meals, repeating until nothing remains.
This greedy approach ensures fairness first (everyone gets a baseline), then rewards priority (high-ratio meals get the surplus). The same logic applies to resource allocation in ML — distributing GPU hours or batch slots across training jobs with different priorities.
Implementation¶
Product model:
from django.db import models
class Product(models.Model):
id = models.IntegerField(primary_key=True)
name = models.CharField(max_length=100)
Serializers:
class ProductSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Product
fields = ["id", "name"]
class PortionSerializer(serializers.Serializer):
id = serializers.IntegerField()
name = serializers.CharField(max_length=120)
portion = serializers.IntegerField()
Rules engine:
from collections import OrderedDict
from math import ceil
def run_rules(inp):
meal_ratios, portions = inp
if not meal_ratios:
return meal_ratios
portions_allocated, remainder = rule_allocate_minimum_ratio(meal_ratios, portions)
if remainder > 0:
portions_allocated = rule_allocate_remainder_to_high_ratios(
meal_ratios, portions_allocated, remainder
)
return portions_allocated
def rule_allocate_minimum_ratio(meal_ratios, portions):
min_to_allocate = min_allocation(meal_ratios, portions)
meal_counts = {}
remaining = portions
for meal_id in meal_ratios:
allocation = min_to_allocate if remaining > min_to_allocate else remaining
if meal_ratios[meal_id] != 0:
meal_counts[meal_id] = allocation
remaining -= min_to_allocate
else:
meal_counts[meal_id] = 0
return meal_counts, remaining
def rule_allocate_remainder_to_high_ratios(meal_ratios, allocated, remainder):
top_meals = find_top_remainder_meals(meal_ratios)
min_alloc = min_allocation(top_meals, remainder)
if min_alloc == 0:
min_alloc = 1
remaining = remainder
while remaining > 0:
for meal_id in top_meals:
if remaining <= 0:
break
alloc = min_alloc if remaining >= min_alloc else remaining
allocated[meal_id] += alloc
remaining -= alloc
return allocated
def min_allocation(meal_ratios, portions):
return ceil((1 / sum(meal_ratios.values())) * portions)
def find_top_remainder_meals(meal_ratios):
sorted_meals = OrderedDict(
sorted(meal_ratios.items(), key=lambda x: x[1], reverse=True)
)
max_val = sorted_meals[list(sorted_meals)[0]]
return {k: v for k, v in sorted_meals.items() if v == max_val}
Views:
class ProductViewSet(viewsets.ModelViewSet):
queryset = Product.objects.all().order_by("id")
serializer_class = ProductSerializer
class PortionDistribution(viewsets.ViewSet):
parser_classes = (JSONParser,)
def create(self, request, format=None):
distribution = request.data.get("distribution")
portions = distribution.pop("portions")
distribution = {int(k): int(v) for k, v in distribution.items()}
distributed_portions = response_value(distribution, portions)
serializer = PortionSerializer(distributed_portions, many=True)
return Response(serializer.data)
Takeaway¶
The two-pass approach -- guarantee minimums, then distribute surplus by priority -- is simple enough to reason about in production and general enough to apply anywhere you're dividing a fixed budget across competing consumers.