From 42aee455ba6ac3976c98d299922522112f76ab4f Mon Sep 17 00:00:00 2001 From: ITQ Date: Fri, 21 Feb 2025 07:12:46 +0300 Subject: [PATCH] feat: implemented algorythm and indexed more fields --- .../apps/campaign/migrations/0001_initial.py | 18 +-- .../services/backend/apps/campaign/models.py | 122 +++++++++++++++--- 2 files changed, 113 insertions(+), 27 deletions(-) diff --git a/solution/services/backend/apps/campaign/migrations/0001_initial.py b/solution/services/backend/apps/campaign/migrations/0001_initial.py index ba9800f..88d5664 100644 --- a/solution/services/backend/apps/campaign/migrations/0001_initial.py +++ b/solution/services/backend/apps/campaign/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.6 on 2025-02-17 18:16 +# Generated by Django 5.1.6 on 2025-02-21 03:50 import apps.campaign.models import django.core.validators @@ -28,12 +28,12 @@ class Migration(migrations.Migration): ('ad_title', models.TextField()), ('ad_text', models.TextField()), ('ad_image', models.ImageField(blank=True, max_length=256, null=True, upload_to=apps.campaign.models.Campaign.ad_image_directory_path)), - ('start_date', models.PositiveIntegerField()), - ('end_date', models.PositiveIntegerField()), - ('gender', models.CharField(blank=True, choices=[('MALE', 'MALE'), ('FEMALE', 'FEMALE'), ('ALL', 'ALL')], max_length=6, null=True)), - ('age_from', models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MaxValueValidator(100)])), - ('age_to', models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MaxValueValidator(100)])), - ('location', models.TextField(blank=True, null=True, validators=[django.core.validators.MinLengthValidator(1)])), + ('start_date', models.PositiveIntegerField(db_index=True)), + ('end_date', models.PositiveIntegerField(db_index=True)), + ('gender', models.CharField(blank=True, choices=[('MALE', 'MALE'), ('FEMALE', 'FEMALE'), ('ALL', 'ALL')], db_index=True, max_length=6, null=True)), + ('age_from', models.PositiveSmallIntegerField(blank=True, db_index=True, null=True, validators=[django.core.validators.MaxValueValidator(100)])), + ('age_to', models.PositiveSmallIntegerField(blank=True, db_index=True, null=True, validators=[django.core.validators.MaxValueValidator(100)])), + ('location', models.TextField(blank=True, db_index=True, null=True, validators=[django.core.validators.MinLengthValidator(1)])), ('advertiser', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='campaigns', to='advertiser.advertiser')), ], options={ @@ -45,7 +45,7 @@ class Migration(migrations.Migration): fields=[ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), ('price', models.FloatField()), - ('date', models.PositiveIntegerField()), + ('date', models.PositiveIntegerField(db_index=True)), ('campaign', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='clicks', to='campaign.campaign')), ('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='clicks', to='client.client')), ], @@ -58,7 +58,7 @@ class Migration(migrations.Migration): fields=[ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), ('price', models.FloatField()), - ('date', models.PositiveIntegerField()), + ('date', models.PositiveIntegerField(db_index=True)), ('campaign', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='impressions', to='campaign.campaign')), ('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='impressions', to='client.client')), ], diff --git a/solution/services/backend/apps/campaign/models.py b/solution/services/backend/apps/campaign/models.py index 195dc2e..870d2a7 100644 --- a/solution/services/backend/apps/campaign/models.py +++ b/solution/services/backend/apps/campaign/models.py @@ -48,32 +48,36 @@ class Campaign(BaseModel): ad_text = models.TextField() ad_image = models.ImageField( max_length=256, - null=True, blank=True, + null=True, upload_to=ad_image_directory_path, ) - start_date = models.PositiveIntegerField() - end_date = models.PositiveIntegerField() + start_date = models.PositiveIntegerField(db_index=True) + end_date = models.PositiveIntegerField(db_index=True) gender = models.CharField( max_length=6, - null=True, blank=True, + null=True, + db_index=True, choices=GenderChoices, ) age_from = models.PositiveSmallIntegerField( - null=True, blank=True, + null=True, + db_index=True, validators=[MaxValueValidator(100)], ) age_to = models.PositiveSmallIntegerField( - null=True, blank=True, + null=True, + db_index=True, validators=[MaxValueValidator(100)], ) location = models.TextField( - null=True, blank=True, + null=True, + db_index=True, validators=[MinLengthValidator(1)], ) @@ -273,19 +277,101 @@ class Campaign(BaseModel): | models.Q(age_from__isnull=True) ) & (models.Q(age_to__gte=client.age) | models.Q(age_to__isnull=True)) - queryset = cls.objects.filter( - date_filter, - location_filter, - gender_filter, - age_filter, - ).prefetch_related("clicks", "impressions") + queryset = ( + cls.objects.filter( + date_filter, + location_filter, + gender_filter, + age_filter, + ) + .prefetch_related("clicks", "impressions") + .select_related("advertiser") + ) return queryset @classmethod - def suggest(cls, client: Client) -> Self: - aboba = cls.get_revelant(client).all() - return aboba[0] + def suggest(cls, client: Client) -> None | Self: + available_campaigns = Campaign.get_available_campaigns(client) + if not available_campaigns or available_campaigns == []: + return None + + campaign_ids = [c.id for c in available_campaigns] + + impressions_counts = ( + CampaignImpression.objects.filter(campaign_id__in=campaign_ids) + .values("campaign_id") + .annotate(total=models.Count("id")) + ) + impressions_dict = { + item["campaign_id"]: item["total"] for item in impressions_counts + } + + clicks_counts = ( + CampaignClick.objects.filter(campaign_id__in=campaign_ids) + .values("campaign_id") + .annotate(total=models.Count("id")) + ) + clicks_dict = { + item["campaign_id"]: item["total"] for item in clicks_counts + } + + existing_impressions = CampaignImpression.objects.filter( + client=client, campaign_id__in=campaign_ids + ).values_list("campaign_id", flat=True) + existing_impressions_set = set(existing_impressions) + + advertisers = {c.advertiser_id for c in available_campaigns} + ml_scores = models.Mlscore.objects.filter( + client=client, advertiser_id__in=advertisers + ).values("advertiser_id", "score") + ml_score_dict = {ms["advertiser_id"]: ms["score"] for ms in ml_scores} + max_ml = max(ml_score_dict.values(), default=0) + + valid_campaigns = [] + for campaign in available_campaigns: + current_impressions = impressions_dict.get(campaign.id, 0) + if current_impressions >= campaign.impressions_limit: + continue + if campaign.id in existing_impressions_set: + continue + valid_campaigns.append(campaign) + + prioritized = [] + for campaign in valid_campaigns: + ml_score = ml_score_dict.get(campaign.advertiser_id, 0) + + remaining_impressions = ( + campaign.impressions_limit + - impressions_dict.get(campaign.id, 0) + ) + weight_imp = ( + remaining_impressions / campaign.impressions_limit + if campaign.impressions_limit > 0 + else 1.0 + ) + + current_clicks = clicks_dict.get(campaign.id, 0) + remaining_clicks = campaign.clicks_limit - current_clicks + if remaining_clicks <= 0: + cpc_contribution = 0.0 + else: + click_availability = ( + (remaining_clicks / campaign.clicks_limit) + if campaign.clicks_limit > 0 + else 1.0 + ) + prob_click = (ml_score / max_ml) if max_ml != 0 else 0.0 + cpc_contribution = ( + campaign.cost_per_click * prob_click * click_availability + ) + + expected_profit = campaign.cost_per_impression + cpc_contribution + priority = ml_score * expected_profit * weight_imp + prioritized.append((campaign, priority)) + + prioritized.sort(key=lambda x: -x[1]) + return prioritized[0] class CampaignImpression(BaseModel): @@ -300,7 +386,7 @@ class CampaignImpression(BaseModel): related_name="impressions", ) price = models.FloatField() - date = models.PositiveIntegerField() + date = models.PositiveIntegerField(db_index=True) def __str__(self) -> str: return f"{self.client.login} > {self.campaign.ad_title}" @@ -324,7 +410,7 @@ class CampaignClick(BaseModel): related_name="clicks", ) price = models.FloatField() - date = models.PositiveIntegerField() + date = models.PositiveIntegerField(db_index=True) def __str__(self) -> str: return f"{self.client.login} > {self.campaign.ad_title}"