Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(patrons): update aura formula #361

Merged
merged 3 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 1 addition & 7 deletions app/controllers/pages_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,7 @@ def participation
end

def patrons
@users = User
.select("users.uid, users.username, COUNT(referees.id) AS referrals")
.select("CEIL(100 * LN(COUNT(referees.id) + 1)) AS aura")
.joins("LEFT JOIN users referees ON users.id = referees.referrer_id")
.group("users.id")
.having("CEIL(100 * LN(COUNT(referees.id) + 1)) > 0")
.order(aura: :desc, referrals: :desc)
@users = User.with_aura
end

def setup
Expand Down
26 changes: 26 additions & 0 deletions app/models/user.rb
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Juste pour info, le data analyste que je suis aurait écrit quelque comme ça :

with referrals as (
    select
        users.uid
        , users.username
        , referees.id as referee_id
        , count(distinct completions.id) as referee_completions
    from users as referees
    inner join users
        on referees.referrer_id = users.id
    left join completions
        on referees.id = completions.user_id
    group by 1, 2, 3
)

select
    uid
    , username
    , count(distinct referee_id) as referrals
    , ceil(100 * (
        1 * ln(count(distinct referee_id) + 1) +
        2 * ln(count(distinct case when referee_completions = 1 then referee_id end) + 1) +
        3 * ln(count(distinct case when referee_completions = 2 then referee_id end) + 1) +
        5 * ln(count(distinct case when referee_completions > 2 then referee_id end) + 1)
    )) as aura
from referrals
group by 1, 2

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

C'est quoi la difference ?
J'ai du mal a lire ton query mais au dela de ca, est ce qu'au niveau performance y a une difference ou c'est juste du formatage ?
Et si c'est juste du formatage, qu'est ce que tu prefere dans cette ecriture ?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sur le résultat normalement pas de différence.

Sur la lisibilité, c'est rarement une bonne pratique d'avoir des "subqueries" (requêtes imbriquées), du coup dans le milieu on préfère ce qu'on appelle les Common Table Expression, ou CTE, avec la notation with. En fonction des moteurs SQL ça peut avoir un impact sur les perfs (PostgreSQL pas à ma connaissance).

Sur les majuscules/pas majuscules, ça c'est vraiment une question de goût. Historiquement il n'y avait pas forcément de coloration syntaxique dans les éditeurs, donc l'usage des majuscules était une bonne pratique pour la lisibilité des requêtes. Par exemple je suis content qu'ActiveRecord log les requêtes avec des majuscules, vu que c'est unicolore.
Aujourd'hui, y a qu'à voir, la coloration syntaxique fait tout le taf. Les minuscules sont considérées comme une meilleure pratique, mais je considère que ça reste une question de goût.

Sur la performance, la mienne est légèrement meilleure en théorie (tu peux utiliser explain devant n'importe quelle requête pour avoir la liste des tâches séquentielles que le moteur SQL doit exécuter). Ça s'explique surtout grâce à mon inner join dès le début : je limite tout de suite mon nombre de lignes aux referees, alors que toi tu gardes tout même après l'agrégation pour ne filtrer qu'à la fin avec le having.
Avec 1000 utilisateurs, en pratique ça ne change rien. Avec 10M, ça peut faire la différence.

Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,32 @@ def self.find_by_referral_code(code)
User.find_by(uid: code.gsub(/R0*/, "").to_i)
end

def self.with_aura
query = <<~SQL.squish
SELECT
users.uid,
users.username,
COUNT(DISTINCT referees.id) AS referrals,
CEIL(100 * (
LN(COUNT(DISTINCT referees.id) + 1) +
2 * LN(COUNT(CASE WHEN referees_with_completion.completions_count = 1 THEN referees.id END) + 1) +
3 * LN(COUNT(CASE WHEN referees_with_completion.completions_count = 2 THEN referees.id END) + 1) +
5 * LN(COUNT(CASE WHEN referees_with_completion.completions_count > 2 THEN referees.id END) + 1)
)) AS aura
FROM users
LEFT JOIN users referees ON users.id = referees.referrer_id
LEFT JOIN (
SELECT user_id, COUNT(id) AS completions_count
FROM completions
GROUP BY user_id
) referees_with_completion ON referees.id = referees_with_completion.user_id
GROUP BY users.id
HAVING COUNT(referees.id) > 0;
SQL

ActiveRecord::Base.connection.exec_query(query, "SQL")
end

def admin?
uid.in?(ADMINS.values)
end
Expand Down
6 changes: 3 additions & 3 deletions app/views/pages/patrons.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,15 @@
<% @users.each do |user| %>
<tr class="hover:bg-hover">
<td class="text-center">
<%= link_to user[:username], profile_path(user[:uid]), class: "hover:text-gold" %>
<%= link_to user["username"], profile_path(user["uid"]), class: "hover:text-gold" %>
</td>

<td class="text-center">
<%= user[:referrals] %>
<%= user["referrals"] %>
</td>

<td class="text-center strong">
<%= user[:aura].to_i %>
<%= user["aura"].to_i %>
</td>
</tr>
<% end %>
Expand Down
28 changes: 0 additions & 28 deletions spec/controllers/pages_controller_spec.rb

This file was deleted.

4 changes: 2 additions & 2 deletions spec/factories/completions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

FactoryBot.define do
factory :completion do
day { (1..25).sample }
challenge { (1..2).to_a.sample }
day { rand(1..25) }
challenge { rand(1..2) }
association :user
end
end
32 changes: 32 additions & 0 deletions spec/models/user_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,36 @@
new_user = create :user, private_leaderboard: nil
expect(new_user.private_leaderboard).to be_in(Aoc.private_leaderboards)
end

describe "::with_aura" do
it "returns a ActiveRecord::Result" do
expect(User.with_aura.class).to eq ActiveRecord::Result
end

it "returns a list of users that have at least 1 referral" do
expect(User.with_aura.pluck("referrals")).to all be_positive
end

it "returns a list of users with attributes uid, username, referrals and aura" do
expect(User.with_aura.columns).to contain_exactly("uid", "username", "referrals", "aura")
end

it "computes the aura correctly" do
referrer = create(:user, uid: "1")

referee_one = create(:user, username: "Aquaj", uid: "2", referrer:)
referee_two = create(:user, username: "wJoenn", uid: "3", referrer:)
referee_three = create(:user, username: "Aurrou", uid: "4", referrer:)
create(:user, username: "Nikos", uid: "5", referrer: referee_one)

create_list(:completion, 1, user: referee_one)
create_list(:completion, 2, user: referee_two)
create_list(:completion, 3, user: referee_three)

expect(User.with_aura).to contain_exactly(
hash_including("uid" => "1", "username" => "pil0u", "referrals" => 3, "aura" => 832.0), # (100 * (ln(3 + 1) + 2 * (1 + 1) + 3 * (1 + 1) + 5 * (1 + 1))).ceil
hash_including("uid" => "2", "username" => "Aquaj", "referrals" => 1, "aura" => 70.0) # (100 * (ln(1 + 1) + 2 * (0 + 1) + 3 * (0 + 1) + 5 * (0 + 1))).ceil
)
end
end
end