Commit 249a84df authored by njoyard's avatar njoyard Committed by GitHub

Merge pull request #96 from political-memory/configurable-scores

Configurable scores with exponential decay
parents a36fe829 fc76a189
......@@ -14,6 +14,7 @@ Contents:
usage
deployment
administration
scores
api
development
hacker
......
Memopol Scores
~~~~~~~~~~~~~~
Score computation
=================
In Memopol, each representative has a score that shows how their votes match
the recommendations made by administrators on the instance. The total score
of a representative is the sum of their score on each dossier, which in turn is
the sum of their score on each proposal with a recommendation on the dossier.
Each recommendation made by administrators has a weight: a number that tells how
important a specific proposal is. When the representative vote on a proposal
matches the recommendation, their score on the proposal is set to +weight.
Otherwise, it is set to -weight.
Score decay parameters
======================
Memopol allows to set decay parameters so that older votes have a lower
importance in the total representative score. By default, those parameters are
set to values that disable the score decay, so that each vote contributes
identically to the total score no matter how old it is.
The formula used to compute score is the following:
.. image:: img/score_formula.png
Where:
* ``baseScore`` is the base score for the vote computed as explained above;
* ``voteAge`` is the age of the vote in days;
* ``decayNum`` and ``decayDenom`` define the decay rate;
* ``exponent`` define the steepness of the decay.
The corresponding parameters can be set from the Memopol administration
interface (Memopol Settings > Settings); settings keys are ``SCORE_DECAY_NUM``,
``SCORE_DECAY_DENOM``, ``SCORE_EXPONENT``. Additionnaly, the ``SCORE_DECIMALS``
parameter sets how many decimal places are visible when scores are displayed.
The default values for those settings disable score decay by setting
``SCORE_DECAY_NUM`` to 0, ``SCORE_DECAY_DENOM`` and ``SCORE_EXPONENT`` to 1.
If you want to use score decay, start by setting ``SCORE_DECAY_NUM`` to 1, and
``SCORE_DECAY_DENOM`` to the number of days you want votes to matter. The graph
below shows how a score of 1.0 will decay with a 1-year decay (the X axis is in
days).
.. image:: img/score_1year.png
Increasing ``SCORE_DECAY_DENOM`` will make votes matter longer. Here is the
same example but with a 10-year decay.
.. image:: img/score_10years.png
Increasing ``SCORE_EXPONENT`` instead will make the decay cutoff steeper. Here
is an example with a 1-year decay and the exponent set to 6.
.. image:: img/score_exp6.png
Increasing it dramatically will create a brutal cutoff; here is the same example
with the exponent set to 1000:
.. image:: img/score_exp1k.png
......@@ -1159,13 +1159,13 @@
"app_label": "representatives_positions"
},
"model": "contenttypes.contenttype",
"pk": 27
"pk": 28
},
{
"fields": {
"tag": 1,
"object_id": 1,
"content_type": 27
"content_type": 28
},
"model": "taggit.taggeditem",
"pk": 1
......@@ -1174,7 +1174,7 @@
"fields": {
"tag": 2,
"object_id": 1,
"content_type": 27
"content_type": 28
},
"model": "taggit.taggeditem",
"pk": 2
......@@ -1183,7 +1183,7 @@
"fields": {
"tag": 1,
"object_id": 3,
"content_type": 27
"content_type": 28
},
"model": "taggit.taggeditem",
"pk": 5
......@@ -1192,7 +1192,7 @@
"fields": {
"tag": 1,
"object_id": 2,
"content_type": 27
"content_type": 28
},
"model": "taggit.taggeditem",
"pk": 6
......@@ -1201,7 +1201,7 @@
"fields": {
"tag": 3,
"object_id": 2,
"content_type": 27
"content_type": 28
},
"model": "taggit.taggeditem",
"pk": 7
......
......@@ -90,6 +90,7 @@ INSTALLED_APPS = (
# ---
'core',
'memopol',
'memopol_settings',
'representatives',
'representatives_votes',
'representatives_recommendations',
......
from django.contrib import admin
from .models import Setting
class SettingAdmin(admin.ModelAdmin):
list_display = ('key', 'value', 'comment')
list_editable = ('key', 'value', 'comment')
list_filter = ('key',)
admin.site.register(Setting, SettingAdmin)
[{
"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
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
]
operations = [
migrations.CreateModel(
name='Setting',
fields=[
('key', models.CharField(max_length=255, serialize=False, primary_key=True)),
('value', models.CharField(max_length=255)),
('comment', models.TextField()),
],
),
]
# -*- 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),
]
from django.db import models
class Setting(models.Model):
key = models.CharField(max_length=255, primary_key=True)
value = models.CharField(max_length=255)
comment = models.TextField()
# -*- 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"
"""
)
]
......@@ -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']
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment