From efb3dcdc2841d0d4a005a834cd1138f6eec036df Mon Sep 17 00:00:00 2001 From: Nicolas Joyard <joyard.nicolas@gmail.com> Date: Wed, 29 Jun 2016 20:35:25 +0200 Subject: [PATCH] Change score calculation to exponential decay --- memopol_settings/fixtures/score_settings.json | 30 ++++++++ .../migrations/0002_score_settings.py | 34 +++++++++ .../migrations/0006_score_formula.py | 76 +++++++++++++++++++ representatives_recommendations/models.py | 8 +- 4 files changed, 144 insertions(+), 4 deletions(-) create mode 100644 memopol_settings/fixtures/score_settings.json create mode 100644 memopol_settings/migrations/0002_score_settings.py create mode 100644 representatives_recommendations/migrations/0006_score_formula.py diff --git a/memopol_settings/fixtures/score_settings.json b/memopol_settings/fixtures/score_settings.json new file mode 100644 index 00000000..01ecf76b --- /dev/null +++ b/memopol_settings/fixtures/score_settings.json @@ -0,0 +1,30 @@ +[{ + "model": "memopol_settings.setting", + "pk": "SCORE_DECAY_NUM", + "fields": { + "value": "0", + "comment": "Numerator for decay rate in score formula. Set to 0 for no decay, 1 otherwise. Score formula is base_score * exp( -(vote_age*DECAY_NUM/DECAY_DENOM)^(2*EXPONENT) )." + } +}, +{ + "model": "memopol_settings.setting", + "pk": "SCORE_DECAY_DENOM", + "fields": { + "value": "1", + "comment": "Denominator for decay rate in score formula. Must be nonzero. Set to higher value to delay the score decay. Score formula is base_score * exp( -(vote_age*DECAY_NUM/DECAY_DENOM)^(2*EXPONENT) )." + } +},{ + "model": "memopol_settings.setting", + "pk": "SCORE_EXPONENT", + "fields": { + "value": "1", + "comment": "Exponent for score formula. Set to higher value for a steeper decay around the cutoff. Score formula is base_score * exp( -(vote_age*DECAY_NUM/DECAY_DENOM)^(2*EXPONENT) )." + } +},{ + "model": "memopol_settings.setting", + "pk": "SCORE_DECIMALS", + "fields": { + "value": "0", + "comment": "Number of score decimals to display. Use 0 for integers." + } +}] \ No newline at end of file diff --git a/memopol_settings/migrations/0002_score_settings.py b/memopol_settings/migrations/0002_score_settings.py new file mode 100644 index 00000000..00c7098b --- /dev/null +++ b/memopol_settings/migrations/0002_score_settings.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import os + +from django.core import serializers +from django.db import migrations + +fixture_dir = os.path.abspath( + os.path.join( + os.path.dirname(__file__), + '../fixtures')) +fixture_filename = 'score_settings.json' + + +def load_fixture(apps, schema_editor): + fixture_file = os.path.join(fixture_dir, fixture_filename) + + fixture = open(fixture_file, 'rb') + objects = serializers.deserialize('json', fixture, ignorenonexistent=True) + for obj in objects: + obj.save() + fixture.close() + + +class Migration(migrations.Migration): + + dependencies = [ + ('memopol_settings', '0001_initial'), + ] + + operations = [ + migrations.RunPython(load_fixture), + ] diff --git a/representatives_recommendations/migrations/0006_score_formula.py b/representatives_recommendations/migrations/0006_score_formula.py new file mode 100644 index 00000000..52039c8b --- /dev/null +++ b/representatives_recommendations/migrations/0006_score_formula.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('representatives_recommendations', '0005_representativescore'), + ] + + operations = [ + migrations.RunSQL( + """ + DROP VIEW "representatives_recommendations_votescores" CASCADE; + """ + ), + migrations.AlterField( + model_name='recommendation', + name='weight', + field=models.FloatField(default=0), + ), + migrations.RunSQL( + """ + CREATE VIEW "representatives_recommendations_votescores" + AS SELECT + representatives_votes_vote.id, + representatives_votes_vote."position", + representatives_votes_vote.proposal_id, + representatives_votes_vote.representative_id, + ROUND(CAST(EXP(-((decay_num.value * EXTRACT(days FROM CURRENT_DATE - representatives_votes_proposal.datetime) / decay_denom.value) ^ (2 * exponent.value))) + * (CASE + WHEN representatives_votes_vote."position"::text = representatives_recommendations_recommendation.recommendation::text THEN representatives_recommendations_recommendation.weight + ELSE 0 - representatives_recommendations_recommendation.weight + END) AS NUMERIC), decimals.value) AS score + FROM representatives_votes_vote + JOIN (SELECT CAST(TO_NUMBER(value, '99999') AS FLOAT) AS value FROM memopol_settings_setting WHERE key = 'SCORE_DECAY_NUM') decay_num ON 1=1 + JOIN (SELECT CAST(TO_NUMBER(value, '99999') AS FLOAT) AS value FROM memopol_settings_setting WHERE key = 'SCORE_DECAY_DENOM') decay_denom ON 1=1 + JOIN (SELECT CAST(TO_NUMBER(value, '99999') AS FLOAT) AS value FROM memopol_settings_setting WHERE key = 'SCORE_EXPONENT') exponent ON 1=1 + JOIN (SELECT CAST(TO_NUMBER(value, '99999') AS INTEGER) AS value FROM memopol_settings_setting WHERE key = 'SCORE_DECIMALS') decimals ON 1=1 + JOIN representatives_votes_proposal ON representatives_votes_vote.proposal_id = representatives_votes_proposal.id + LEFT JOIN representatives_recommendations_recommendation ON representatives_votes_proposal.id = representatives_recommendations_recommendation.proposal_id + WHERE representatives_recommendations_recommendation.id IS NOT NULL; + """ + ), + migrations.RunSQL( + """ + CREATE VIEW "representatives_recommendations_dossierscores" + AS SELECT + "representatives_recommendations_votescores"."representative_id" || ':' || "representatives_votes_proposal"."dossier_id" AS "id", + "representatives_recommendations_votescores"."representative_id", + "representatives_votes_proposal"."dossier_id", + SUM("representatives_recommendations_votescores"."score") AS "score" + FROM "representatives_recommendations_votescores" + INNER JOIN "representatives_votes_proposal" + ON ( "representatives_recommendations_votescores"."proposal_id" = "representatives_votes_proposal"."id" ) + GROUP BY + "representatives_recommendations_votescores"."representative_id", + "representatives_votes_proposal"."dossier_id" + """ + ), + migrations.RunSQL( + """ + CREATE VIEW "representatives_recommendations_representativescore" + AS SELECT + "representatives_representative"."id" as "representative_id", + COALESCE(SUM("representatives_recommendations_votescores"."score"), 0) AS "score" + FROM + "representatives_representative" + LEFT OUTER JOIN "representatives_recommendations_votescores" + ON "representatives_recommendations_votescores"."representative_id" = "representatives_representative"."id" + GROUP BY "representatives_representative"."id" + """ + ) + ] diff --git a/representatives_recommendations/models.py b/representatives_recommendations/models.py index 7e887e03..c9a18951 100644 --- a/representatives_recommendations/models.py +++ b/representatives_recommendations/models.py @@ -12,7 +12,7 @@ class DossierScore(models.Model): representative = models.ForeignKey(Representative, on_delete=models.DO_NOTHING) dossier = models.ForeignKey(Dossier, on_delete=models.DO_NOTHING) - score = models.IntegerField(default=0) + score = models.FloatField(default=0) class Meta: managed = False @@ -25,7 +25,7 @@ class VoteScore(models.Model): representative = models.ForeignKey( Representative, related_name='votescores', null=True) position = models.CharField(max_length=10) - score = models.IntegerField(default=0) + score = models.FloatField(default=0) class Meta: managed = False @@ -36,7 +36,7 @@ class VoteScore(models.Model): class RepresentativeScore(models.Model): representative = models.OneToOneField('representatives.representative', primary_key=True, related_name='score') - score = models.IntegerField(default=0) + score = models.FloatField(default=0) class Meta: managed = False @@ -52,7 +52,7 @@ class Recommendation(models.Model): recommendation = models.CharField(max_length=10, choices=Vote.VOTECHOICES) title = models.CharField(max_length=1000, blank=True) description = models.TextField(blank=True) - weight = models.IntegerField(default=0) + weight = models.FloatField(default=0) class Meta: ordering = ['proposal__datetime'] -- GitLab