diff --git a/docs/img/score_10years.png b/docs/img/score_10years.png
new file mode 100644
index 0000000000000000000000000000000000000000..bef922b8906ac6410650601351336f4909c11b43
Binary files /dev/null and b/docs/img/score_10years.png differ
diff --git a/docs/img/score_1year.png b/docs/img/score_1year.png
new file mode 100644
index 0000000000000000000000000000000000000000..50d579aabb7e771c656500b97f7c1f6997ecbf1d
Binary files /dev/null and b/docs/img/score_1year.png differ
diff --git a/docs/img/score_exp1k.png b/docs/img/score_exp1k.png
new file mode 100644
index 0000000000000000000000000000000000000000..40a43e73ad235b16cec2ef562ddaae9fae9ae896
Binary files /dev/null and b/docs/img/score_exp1k.png differ
diff --git a/docs/img/score_exp6.png b/docs/img/score_exp6.png
new file mode 100644
index 0000000000000000000000000000000000000000..9262e8c05ca5e6c9a0b0262ff6bdc24946c09831
Binary files /dev/null and b/docs/img/score_exp6.png differ
diff --git a/docs/img/score_formula.png b/docs/img/score_formula.png
new file mode 100644
index 0000000000000000000000000000000000000000..131e4d8a5ed7b59c4ea92408f150cb7cdbe8f930
Binary files /dev/null and b/docs/img/score_formula.png differ
diff --git a/docs/index.rst b/docs/index.rst
index 05a6e669f3451c7c10c251928e4ae815a475d8c6..76d8172a76b2041530e95ebf3b6e986c82076ea3 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -14,6 +14,7 @@ Contents:
    usage
    deployment
    administration
+   scores
    api
    development
    hacker
diff --git a/docs/scores.rst b/docs/scores.rst
new file mode 100644
index 0000000000000000000000000000000000000000..529e74f7297b2f0410d63366676b0e6ce02fc688
--- /dev/null
+++ b/docs/scores.rst
@@ -0,0 +1,64 @@
+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
diff --git a/memopol/fixtures/one_representative.json b/memopol/fixtures/one_representative.json
index c260ea3bc586f77f7080d471becb1e3817954c0c..9bba397c9a9d817b4d5275946ff357ea58da4a5d 100644
--- a/memopol/fixtures/one_representative.json
+++ b/memopol/fixtures/one_representative.json
@@ -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
diff --git a/memopol/settings.py b/memopol/settings.py
index 227bba18f40cfb3002ac3e7df43d503e0046db5a..68f89dd22a26237ba14ce2a79f5ed05942e5e698 100644
--- a/memopol/settings.py
+++ b/memopol/settings.py
@@ -90,6 +90,7 @@ INSTALLED_APPS = (
     # ---
     'core',
     'memopol',
+    'memopol_settings',
     'representatives',
     'representatives_votes',
     'representatives_recommendations',
diff --git a/memopol_settings/__init__.py b/memopol_settings/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/memopol_settings/admin.py b/memopol_settings/admin.py
new file mode 100644
index 0000000000000000000000000000000000000000..88c9bfa1d901448becea8ed8e179ea64fe5d1f0d
--- /dev/null
+++ b/memopol_settings/admin.py
@@ -0,0 +1,11 @@
+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)
diff --git a/memopol_settings/fixtures/score_settings.json b/memopol_settings/fixtures/score_settings.json
new file mode 100644
index 0000000000000000000000000000000000000000..01ecf76ba05fc631c84cc81ec403bc0916089851
--- /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/0001_initial.py b/memopol_settings/migrations/0001_initial.py
new file mode 100644
index 0000000000000000000000000000000000000000..8c5caa8a631b5379b5cb7116e13001b39ef21473
--- /dev/null
+++ b/memopol_settings/migrations/0001_initial.py
@@ -0,0 +1,21 @@
+# -*- 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()),
+            ],
+        ),
+    ]
diff --git a/memopol_settings/migrations/0002_score_settings.py b/memopol_settings/migrations/0002_score_settings.py
new file mode 100644
index 0000000000000000000000000000000000000000..00c7098b6c631d63be16c797efa5a8858a64a9cf
--- /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/memopol_settings/migrations/__init__.py b/memopol_settings/migrations/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/memopol_settings/models.py b/memopol_settings/models.py
new file mode 100644
index 0000000000000000000000000000000000000000..70dc9682f7f41dccc7e1940d6d71be014d73332c
--- /dev/null
+++ b/memopol_settings/models.py
@@ -0,0 +1,7 @@
+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()
diff --git a/representatives_recommendations/migrations/0006_score_formula.py b/representatives_recommendations/migrations/0006_score_formula.py
new file mode 100644
index 0000000000000000000000000000000000000000..52039c8bbbd2e48a71c16a212df3b483dcb8c1b7
--- /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 7e887e036fda55d0e5731a7b9cd1081a35be61ea..c9a1895196ce932f02f4ad4d47f5d4793274c6c0 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']