mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 11:51:10 -05:00
213 lines
7.1 KiB
Python
213 lines
7.1 KiB
Python
"""
|
|
Models for ride ranking system using Internet Roller Coaster Poll algorithm.
|
|
|
|
This system calculates rankings based on pairwise comparisons between rides,
|
|
where each ride is compared to every other ride to determine which one
|
|
more riders preferred.
|
|
"""
|
|
|
|
from django.db import models
|
|
from django.utils import timezone
|
|
from django.core.validators import MinValueValidator, MaxValueValidator
|
|
import pghistory
|
|
|
|
|
|
@pghistory.track()
|
|
class RideRanking(models.Model):
|
|
"""
|
|
Stores calculated rankings for rides using the Internet Roller Coaster Poll algorithm.
|
|
|
|
Rankings are recalculated daily based on user reviews/ratings.
|
|
Each ride's rank is determined by its winning percentage in pairwise comparisons.
|
|
"""
|
|
|
|
ride = models.OneToOneField(
|
|
"rides.Ride", on_delete=models.CASCADE, related_name="ranking"
|
|
)
|
|
|
|
# Core ranking metrics
|
|
rank = models.PositiveIntegerField(
|
|
db_index=True, help_text="Overall rank position (1 = best)"
|
|
)
|
|
wins = models.PositiveIntegerField(
|
|
default=0, help_text="Number of rides this ride beats in pairwise comparisons"
|
|
)
|
|
losses = models.PositiveIntegerField(
|
|
default=0,
|
|
help_text="Number of rides that beat this ride in pairwise comparisons",
|
|
)
|
|
ties = models.PositiveIntegerField(
|
|
default=0,
|
|
help_text="Number of rides with equal preference in pairwise comparisons",
|
|
)
|
|
winning_percentage = models.DecimalField(
|
|
max_digits=5,
|
|
decimal_places=4,
|
|
validators=[MinValueValidator(0), MaxValueValidator(1)],
|
|
db_index=True,
|
|
help_text="Win percentage where ties count as 0.5",
|
|
)
|
|
|
|
# Additional metrics
|
|
mutual_riders_count = models.PositiveIntegerField(
|
|
default=0, help_text="Total number of users who have rated this ride"
|
|
)
|
|
comparison_count = models.PositiveIntegerField(
|
|
default=0, help_text="Number of other rides this was compared against"
|
|
)
|
|
average_rating = models.DecimalField(
|
|
max_digits=3,
|
|
decimal_places=2,
|
|
null=True,
|
|
blank=True,
|
|
validators=[MinValueValidator(1), MaxValueValidator(10)],
|
|
help_text="Average rating from all users who have rated this ride",
|
|
)
|
|
|
|
# Metadata
|
|
last_calculated = models.DateTimeField(
|
|
default=timezone.now, help_text="When this ranking was last calculated"
|
|
)
|
|
calculation_version = models.CharField(
|
|
max_length=10, default="1.0", help_text="Algorithm version used for calculation"
|
|
)
|
|
|
|
class Meta:
|
|
ordering = ["rank"]
|
|
indexes = [
|
|
models.Index(fields=["rank"]),
|
|
models.Index(fields=["winning_percentage", "-mutual_riders_count"]),
|
|
models.Index(fields=["ride", "last_calculated"]),
|
|
]
|
|
constraints = [
|
|
models.CheckConstraint(
|
|
name="rideranking_winning_percentage_range",
|
|
check=models.Q(winning_percentage__gte=0)
|
|
& models.Q(winning_percentage__lte=1),
|
|
violation_error_message="Winning percentage must be between 0 and 1",
|
|
),
|
|
models.CheckConstraint(
|
|
name="rideranking_average_rating_range",
|
|
check=models.Q(average_rating__isnull=True)
|
|
| (models.Q(average_rating__gte=1) & models.Q(average_rating__lte=10)),
|
|
violation_error_message="Average rating must be between 1 and 10",
|
|
),
|
|
]
|
|
|
|
def __str__(self):
|
|
return f"#{self.rank} - {self.ride.name} ({self.winning_percentage:.1%})"
|
|
|
|
@property
|
|
def total_comparisons(self):
|
|
"""Total number of pairwise comparisons (wins + losses + ties)."""
|
|
return self.wins + self.losses + self.ties
|
|
|
|
|
|
@pghistory.track()
|
|
class RidePairComparison(models.Model):
|
|
"""
|
|
Caches pairwise comparison results between two rides.
|
|
|
|
This model stores the results of comparing two rides based on mutual riders
|
|
(users who have rated both rides). It's used to speed up ranking calculations.
|
|
"""
|
|
|
|
ride_a = models.ForeignKey(
|
|
"rides.Ride", on_delete=models.CASCADE, related_name="comparisons_as_a"
|
|
)
|
|
ride_b = models.ForeignKey(
|
|
"rides.Ride", on_delete=models.CASCADE, related_name="comparisons_as_b"
|
|
)
|
|
|
|
# Comparison results
|
|
ride_a_wins = models.PositiveIntegerField(
|
|
default=0, help_text="Number of mutual riders who rated ride_a higher"
|
|
)
|
|
ride_b_wins = models.PositiveIntegerField(
|
|
default=0, help_text="Number of mutual riders who rated ride_b higher"
|
|
)
|
|
ties = models.PositiveIntegerField(
|
|
default=0, help_text="Number of mutual riders who rated both rides equally"
|
|
)
|
|
|
|
# Metrics
|
|
mutual_riders_count = models.PositiveIntegerField(
|
|
default=0, help_text="Total number of users who have rated both rides"
|
|
)
|
|
ride_a_avg_rating = models.DecimalField(
|
|
max_digits=3,
|
|
decimal_places=2,
|
|
null=True,
|
|
blank=True,
|
|
help_text="Average rating of ride_a from mutual riders",
|
|
)
|
|
ride_b_avg_rating = models.DecimalField(
|
|
max_digits=3,
|
|
decimal_places=2,
|
|
null=True,
|
|
blank=True,
|
|
help_text="Average rating of ride_b from mutual riders",
|
|
)
|
|
|
|
# Metadata
|
|
last_calculated = models.DateTimeField(
|
|
auto_now=True, help_text="When this comparison was last calculated"
|
|
)
|
|
|
|
class Meta:
|
|
unique_together = [["ride_a", "ride_b"]]
|
|
indexes = [
|
|
models.Index(fields=["ride_a", "ride_b"]),
|
|
models.Index(fields=["last_calculated"]),
|
|
]
|
|
|
|
def __str__(self):
|
|
winner = "TIE"
|
|
if self.ride_a_wins > self.ride_b_wins:
|
|
winner = self.ride_a.name
|
|
elif self.ride_b_wins > self.ride_a_wins:
|
|
winner = self.ride_b.name
|
|
return f"{self.ride_a.name} vs {self.ride_b.name} - Winner: {winner}"
|
|
|
|
@property
|
|
def winner(self):
|
|
"""Returns the winning ride or None for a tie."""
|
|
if self.ride_a_wins > self.ride_b_wins:
|
|
return self.ride_a
|
|
elif self.ride_b_wins > self.ride_a_wins:
|
|
return self.ride_b
|
|
return None
|
|
|
|
@property
|
|
def is_tie(self):
|
|
"""Returns True if the comparison resulted in a tie."""
|
|
return self.ride_a_wins == self.ride_b_wins
|
|
|
|
|
|
class RankingSnapshot(models.Model):
|
|
"""
|
|
Stores historical snapshots of rankings for tracking changes over time.
|
|
|
|
This allows us to show ranking trends and movements.
|
|
"""
|
|
|
|
ride = models.ForeignKey(
|
|
"rides.Ride", on_delete=models.CASCADE, related_name="ranking_history"
|
|
)
|
|
rank = models.PositiveIntegerField()
|
|
winning_percentage = models.DecimalField(max_digits=5, decimal_places=4)
|
|
snapshot_date = models.DateField(
|
|
db_index=True, help_text="Date when this ranking snapshot was taken"
|
|
)
|
|
|
|
class Meta:
|
|
unique_together = [["ride", "snapshot_date"]]
|
|
ordering = ["-snapshot_date", "rank"]
|
|
indexes = [
|
|
models.Index(fields=["snapshot_date", "rank"]),
|
|
models.Index(fields=["ride", "-snapshot_date"]),
|
|
]
|
|
|
|
def __str__(self):
|
|
return f"{self.ride.name} - Rank #{self.rank} on {self.snapshot_date}"
|