Files
thrillwiki_django_no_react/docs/ride-ranking-system.md
pacnpal dcf890a55c feat: Implement Entity Suggestion Manager and Modal components
- Added EntitySuggestionManager.vue to manage entity suggestions and authentication.
- Created EntitySuggestionModal.vue for displaying suggestions and adding new entities.
- Integrated AuthManager for user authentication within the suggestion modal.
- Enhanced signal handling in start-servers.sh for graceful shutdown of servers.
- Improved server startup script to ensure proper cleanup and responsiveness to termination signals.
- Added documentation for signal handling fixes and usage instructions.
2025-08-25 10:46:54 -04:00

17 KiB

Ride Ranking System Documentation

Overview

The ThrillWiki ride ranking system implements the Internet Roller Coaster Poll (IRCP) algorithm to calculate fair, accurate rankings for all rides based on user ratings. This system provides daily-updated rankings that reflect the collective preferences of the community.

Algorithm Description

Original Internet Roller Coaster Poll Algorithm

The system implements the exact methodology from the Internet Roller Coaster Poll:

"Each coaster is compared one at a time to every other coaster to see whether more people who have ridden both of them preferred one or the other. A coaster is given a 'Win' for each coaster that more mutual riders ranked behind it, given a 'Loss' for each coaster that more mutual riders ranked ahead of it, and given a 'Tie' for each coaster that the same number of mutual riders ranked above it and below it. Coasters are ranked by their overall winning percentage (where ties count as half of a win and half of a loss). In the event that two coasters end up with identical winning percentages, the tie is broken (if possible) by determining which of the two won the mutual rider comparison between those two coasters."

Our Implementation

The ThrillWiki implementation adapts this algorithm to work with our existing rating system:

  1. "Have You Ridden" Detection: A user is considered to have ridden a ride if they have submitted a rating/review for it
  2. Preference Determination: Higher ratings indicate preference (e.g., a user rating Ride A as 8/10 and Ride B as 6/10 prefers Ride A)
  3. Pairwise Comparisons: Every ride is compared to every other ride based on mutual riders
  4. Winning Percentage: Calculated as (wins + 0.5 * ties) / total_comparisons
  5. Tie Breaking: Head-to-head comparisons resolve ties in winning percentage

System Architecture

Database Models

RideRanking

Stores the calculated ranking for each ride:

  • rank: Overall rank position (1 = best)
  • wins: Number of rides this ride beats in pairwise comparisons
  • losses: Number of rides that beat this ride
  • ties: Number of rides with equal preference
  • winning_percentage: Win percentage where ties count as 0.5
  • mutual_riders_count: Total users who have rated this ride
  • average_rating: Average rating from all users
  • last_calculated: Timestamp of last calculation

RidePairComparison

Caches pairwise comparison results between two rides:

  • ride_a, ride_b: The two rides being compared
  • ride_a_wins: Number of mutual riders who rated ride_a higher
  • ride_b_wins: Number of mutual riders who rated ride_b higher
  • ties: Number of mutual riders who rated both equally
  • mutual_riders_count: Total users who rated both rides

RankingSnapshot

Historical tracking of rankings:

  • ride: The ride being tracked
  • rank: Rank on the snapshot date
  • winning_percentage: Win percentage on the snapshot date
  • snapshot_date: Date of the snapshot

Service Layer

The RideRankingService (apps/rides/services/ranking_service.py) implements the core algorithm:

service = RideRankingService()
result = service.update_all_rankings(category='RC')  # Optional category filter

Key methods:

  • update_all_rankings(): Main entry point for ranking calculation
  • _calculate_pairwise_comparison(): Compares two rides
  • _calculate_rankings_from_comparisons(): Converts comparisons to rankings
  • _apply_tiebreakers(): Resolves ties using head-to-head comparisons

Usage

Manual Ranking Update

Run the management command to update rankings:

# Update all ride rankings
uv run python manage.py update_ride_rankings

# Update only roller coaster rankings
uv run python manage.py update_ride_rankings --category RC

Scheduled Updates (Cron)

Add to crontab for daily updates at 2 AM:

0 2 * * * cd /Users/talor/thrillwiki_django_no_react/backend && uv run python manage.py update_ride_rankings

Accessing Rankings

Django Admin

View rankings at /admin/rides/rideranking/

In Code

from apps.rides.models import RideRanking

# Get top 10 rides
top_rides = RideRanking.objects.select_related('ride').order_by('rank')[:10]

# Get ranking for specific ride
ranking = RideRanking.objects.get(ride__slug='millennium-force')
print(f"Rank: #{ranking.rank}, Win%: {ranking.winning_percentage:.1%}")

Web Interface

Views

The ranking system provides comprehensive web views for browsing and analyzing rankings.

Rankings List View

URL: /rides/rankings/

Features:

  • Paginated list of all ranked rides (50 per page)
  • Filter by ride category (Roller Coasters, Dark Rides, etc.)
  • Filter by minimum mutual riders to ensure statistical significance
  • HTMX support for dynamic updates without page refresh
  • Shows key metrics: rank, ride name, park, wins/losses/ties, winning percentage

Query Parameters:

  • category: Filter by ride type (RC, DR, FR, WR, TR, OT)
  • min_riders: Minimum number of mutual riders required (e.g., 100)
  • page: Page number for pagination

Example URLs:

/rides/rankings/                           # All rankings
/rides/rankings/?category=RC               # Only roller coasters
/rides/rankings/?category=RC&min_riders=50 # Roller coasters with 50+ mutual riders

Ranking Detail View

URL: /rides/rankings/<ride-slug>/

Features:

  • Comprehensive ranking metrics for a specific ride
  • Head-to-head comparison results with other rides
  • Historical ranking chart showing trends over time
  • Rank movement indicators (up/down from previous calculation)
  • Detailed breakdown of wins, losses, and ties

HTMX Endpoints:

  • /rides/rankings/<ride-slug>/history-chart/ - Returns ranking history chart data
  • /rides/rankings/<ride-slug>/comparisons/ - Returns detailed comparison table

Template Integration

The views use Django templates with HTMX for dynamic updates:

{# rankings.html - Main rankings page #}
{% extends "base.html" %}
{% block content %}
<div class="rankings-container">
  <h1>Ride Rankings</h1>
  
  {# Filters #}
  <form hx-get="/rides/rankings/" hx-target="#rankings-table">
    <select name="category">
      <option value="all">All Rides</option>
      <option value="RC">Roller Coasters</option>
      <option value="DR">Dark Rides</option>
      <!-- etc. -->
    </select>
    <input type="number" name="min_riders" placeholder="Min riders">
    <button type="submit">Filter</button>
  </form>
  
  {# Rankings table #}
  <div id="rankings-table">
    {% include "rides/partials/rankings_table.html" %}
  </div>
</div>
{% endblock %}

API Endpoints

RESTful API

The ranking system exposes a comprehensive REST API for programmatic access.

List Rankings

Endpoint: GET /api/v1/rankings/

Description: Get paginated list of ride rankings

Query Parameters:

  • category (string): Filter by ride category (RC, DR, FR, WR, TR, OT)
  • min_riders (integer): Minimum mutual riders required
  • park (string): Filter by park slug
  • ordering (string): Sort order (rank, -rank, winning_percentage, -winning_percentage)
  • page (integer): Page number
  • page_size (integer): Results per page (default: 20)

Response Example:

{
  "count": 523,
  "next": "http://api.example.com/api/v1/rankings/?page=2",
  "previous": null,
  "results": [
    {
      "id": 1,
      "rank": 1,
      "ride": {
        "id": 123,
        "name": "Steel Vengeance",
        "slug": "steel-vengeance",
        "park": {
          "id": 45,
          "name": "Cedar Point",
          "slug": "cedar-point"
        },
        "category": "RC"
      },
      "wins": 487,
      "losses": 23,
      "ties": 13,
      "winning_percentage": 0.9405,
      "mutual_riders_count": 1543,
      "comparison_count": 523,
      "average_rating": 9.4,
      "last_calculated": "2024-01-15T02:00:00Z",
      "rank_change": 0,
      "previous_rank": 1
    }
  ]
}

Get Ranking Details

Endpoint: GET /api/v1/rankings/<ride-slug>/

Description: Get detailed ranking information for a specific ride

Response: Includes full ride details, head-to-head comparisons, and ranking history

{
  "id": 1,
  "rank": 1,
  "ride": {
    "id": 123,
    "name": "Steel Vengeance",
    "slug": "steel-vengeance",
    "description": "Hybrid roller coaster...",
    "park": {
      "id": 45,
      "name": "Cedar Point",
      "slug": "cedar-point",
      "location": {
        "city": "Sandusky",
        "state": "Ohio",
        "country": "United States"
      }
    },
    "category": "RC",
    "manufacturer": {
      "id": 12,
      "name": "Rocky Mountain Construction"
    },
    "opening_date": "2018-05-05",
    "status": "OPERATING"
  },
  "wins": 487,
  "losses": 23,
  "ties": 13,
  "winning_percentage": 0.9405,
  "mutual_riders_count": 1543,
  "comparison_count": 523,
  "average_rating": 9.4,
  "last_calculated": "2024-01-15T02:00:00Z",
  "calculation_version": "1.0",
  "head_to_head_comparisons": [
    {
      "opponent": {
        "id": 124,
        "name": "Fury 325",
        "slug": "fury-325",
        "park": "Carowinds"
      },
      "wins": 234,
      "losses": 189,
      "ties": 23,
      "result": "win",
      "mutual_riders": 446
    }
  ],
  "ranking_history": [
    {
      "date": "2024-01-15",
      "rank": 1,
      "winning_percentage": 0.9405
    },
    {
      "date": "2024-01-14",
      "rank": 1,
      "winning_percentage": 0.9398
    }
  ]
}

Get Ranking History

Endpoint: GET /api/v1/rankings/<ride-slug>/history/

Description: Get historical ranking data for a specific ride (last 90 days)

Response: Array of ranking snapshots

Get Head-to-Head Comparisons

Endpoint: GET /api/v1/rankings/<ride-slug>/comparisons/

Description: Get detailed head-to-head comparison data for a ride

Response: Array of comparison results with all rides

Get Ranking Statistics

Endpoint: GET /api/v1/rankings/statistics/

Description: Get system-wide ranking statistics

Response Example:

{
  "total_ranked_rides": 523,
  "total_comparisons": 136503,
  "last_calculation_time": "2024-01-15T02:00:00Z",
  "calculation_duration": 45.23,
  "top_rated_ride": {
    "id": 123,
    "name": "Steel Vengeance",
    "slug": "steel-vengeance",
    "park": "Cedar Point",
    "rank": 1,
    "winning_percentage": 0.9405,
    "average_rating": 9.4
  },
  "most_compared_ride": {
    "id": 456,
    "name": "Millennium Force",
    "slug": "millennium-force",
    "park": "Cedar Point",
    "comparison_count": 521
  },
  "biggest_rank_change": {
    "ride": {
      "id": 789,
      "name": "Iron Gwazi",
      "slug": "iron-gwazi"
    },
    "current_rank": 3,
    "previous_rank": 8,
    "change": 5
  }
}

Trigger Ranking Calculation (Admin)

Endpoint: POST /api/v1/rankings/calculate/

Description: Manually trigger ranking calculation (requires admin authentication)

Request Body:

{
  "category": "RC"  // Optional - filter to specific category
}

Response:

{
  "status": "success",
  "rides_ranked": 523,
  "comparisons_made": 136503,
  "duration": 45.23,
  "timestamp": "2024-01-15T02:00:45Z"
}

API Authentication

  • Read endpoints (GET): No authentication required
  • Calculation endpoint (POST): Requires admin authentication via token or session

API Rate Limiting

  • Anonymous users: 100 requests per hour
  • Authenticated users: 1000 requests per hour
  • Admin users: No rate limiting

Python Client Example

import requests

# Get top 10 rankings
response = requests.get(
    "https://api.thrillwiki.com/api/v1/rankings/",
    params={"page_size": 10}
)
rankings = response.json()["results"]

for ranking in rankings:
    print(f"#{ranking['rank']}: {ranking['ride']['name']} - {ranking['winning_percentage']:.1%}")

# Get specific ride ranking
response = requests.get(
    "https://api.thrillwiki.com/api/v1/rankings/steel-vengeance/"
)
details = response.json()
print(f"{details['ride']['name']} is ranked #{details['rank']}")
print(f"Wins: {details['wins']}, Losses: {details['losses']}, Ties: {details['ties']}")

JavaScript/TypeScript Client Example

// Using fetch API
async function getTopRankings(limit: number = 10) {
  const response = await fetch(
    `https://api.thrillwiki.com/api/v1/rankings/?page_size=${limit}`
  );
  const data = await response.json();
  return data.results;
}

// Using axios
import axios from 'axios';

const api = axios.create({
  baseURL: 'https://api.thrillwiki.com/api/v1',
});

async function getRideRanking(slug: string) {
  const { data } = await api.get(`/rankings/${slug}/`);
  return data;
}

Calculation Example

Scenario

Three rides with the following ratings from users:

Ride A (Millennium Force):

  • User 1: 10/10
  • User 2: 9/10
  • User 3: 8/10

Ride B (Maverick):

  • User 1: 9/10
  • User 2: 10/10
  • User 4: 8/10

Ride C (Top Thrill 2):

  • User 1: 8/10
  • User 3: 9/10
  • User 4: 10/10

Pairwise Comparisons

A vs B (mutual riders: Users 1, 2):

  • User 1: A(10) > B(9) → A wins
  • User 2: A(9) < B(10) → B wins
  • Result: 1 win each, 0 ties

A vs C (mutual riders: Users 1, 3):

  • User 1: A(10) > C(8) → A wins
  • User 3: A(8) < C(9) → C wins
  • Result: 1 win each, 0 ties

B vs C (mutual riders: Users 1, 4):

  • User 1: B(9) > C(8) → B wins
  • User 4: B(8) < C(10) → C wins
  • Result: 1 win each, 0 ties

Final Rankings

All three rides have:

  • Wins: 1
  • Losses: 1
  • Ties: 0
  • Winning Percentage: 50%

Since all have identical winning percentages, the system would use additional criteria (mutual rider count, average rating) to determine final order.

Performance Considerations

Optimization Strategies

  1. Caching: Pairwise comparisons are cached in RidePairComparison table
  2. Batch Processing: Comparisons calculated in bulk to minimize database queries
  3. Indexes: Database indexes on frequently queried fields:
    • rank for ordering
    • winning_percentage for sorting
    • ride_id for lookups
    • Composite indexes for complex queries

Scalability

The algorithm complexity is O(n²) where n is the number of rides, as each ride must be compared to every other ride. For 1,000 rides:

  • Comparisons needed: 499,500
  • Estimated processing time: 30-60 seconds
  • Database storage: ~20MB for comparison cache

Monitoring and Maintenance

Admin Interface

Access Django admin to:

  • View current rankings
  • Inspect pairwise comparisons
  • Track historical changes
  • Monitor calculation performance

Logging

The system logs:

  • Calculation start/end times
  • Number of rides ranked
  • Number of comparisons made
  • Any errors or warnings

Example log output:

INFO: Starting ranking calculation for category: RC
INFO: Found 523 rides to rank
DEBUG: Processed 100/136503 comparisons
INFO: Ranking calculation completed in 45.23 seconds

Data Cleanup

Old ranking snapshots are automatically cleaned up after 365 days to manage database size.

Algorithm Validity

Mathematical Properties

  1. Transitivity: Not enforced (A > B and B > C doesn't guarantee A > C)
  2. Consistency: Same input always produces same output
  3. Fairness: Every mutual rider's opinion counts equally
  4. Completeness: All possible comparisons are considered

Advantages Over Simple Averaging

  1. Reduces Selection Bias: Only compares rides among users who've experienced both
  2. Fair Comparisons: Popular rides aren't advantaged over less-ridden ones
  3. Head-to-Head Logic: Direct comparisons matter for tie-breaking
  4. Robust to Outliers: One extremely high/low rating doesn't skew results

Future Enhancements

Planned Features

  1. Real-time Updates: Update rankings immediately after new reviews
  2. Category-Specific Rankings: Separate rankings for different ride types
  3. Regional Rankings: Rankings by geographic region
  4. Time-Period Rankings: Best new rides, best classic rides
  5. API Endpoints: RESTful API for ranking data
  6. Ranking Trends: Visualizations of ranking changes over time

Potential Optimizations

  1. Incremental Updates: Only recalculate affected comparisons
  2. Parallel Processing: Distribute comparison calculations
  3. Machine Learning: Predict rankings for new rides
  4. Weighted Ratings: Consider recency or reviewer credibility

Technical Details

File Structure

apps/rides/
├── models/
│   └── rankings.py          # Ranking data models
├── services/
│   └── ranking_service.py   # Algorithm implementation
├── management/
│   └── commands/
│       └── update_ride_rankings.py  # CLI command
└── admin.py                 # Admin interface configuration

Database Tables

  • rides_rideranking: Current rankings
  • rides_ridepaircomparison: Cached comparisons
  • rides_rankingsnapshot: Historical data
  • rides_riderankingevent: Audit trail (pghistory)
  • rides_ridepaircomparisonevent: Audit trail (pghistory)

Dependencies

  • Django 5.2+
  • PostgreSQL with pghistory
  • Python 3.11+

References