Files
thrillwiki_django_no_react/docs/ride-ranking-implementation.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

19 KiB

Ride Ranking System - Complete Implementation Documentation

Table of Contents

  1. Overview
  2. Backend Implementation
  3. Frontend Implementation
  4. API Reference
  5. Usage Examples
  6. Deployment & Maintenance

Overview

The ThrillWiki Ride Ranking System implements the Internet Roller Coaster Poll (IRCP) algorithm to provide fair, data-driven rankings of theme park rides based on user ratings. This document covers the complete implementation across both backend (Django) and frontend (Vue.js/TypeScript) components.

Key Features

  • Pairwise Comparison Algorithm: Compares every ride against every other ride based on mutual riders
  • Web Interface: Browse rankings with filtering and detailed views
  • REST API: Comprehensive API for programmatic access
  • Historical Tracking: Track ranking changes over time
  • Statistical Analysis: Head-to-head comparisons and win/loss records

Backend Implementation

Database Models

Location: apps/rides/models/rankings.py

# Core ranking models
- RideRanking: Current ranking data for each ride
- RidePairComparison: Cached pairwise comparison results
- RankingSnapshot: Historical ranking data

Key Fields

RideRanking Model:

  • rank (Integer): Overall ranking position
  • wins (Integer): Number of head-to-head wins
  • losses (Integer): Number of head-to-head losses
  • ties (Integer): Number of tied comparisons
  • winning_percentage (Decimal): Win percentage (ties count as 0.5)
  • mutual_riders_count (Integer): Total users who rated this ride
  • average_rating (Decimal): Average user rating
  • last_calculated (DateTime): Timestamp of last calculation

Service Layer

Location: apps/rides/services/ranking_service.py

The RideRankingService class implements the core ranking algorithm:

class RideRankingService:
    def update_all_rankings(category=None):
        """Main entry point for ranking calculation"""
        
    def _calculate_pairwise_comparison(ride_a, ride_b):
        """Compare two rides based on mutual riders"""
        
    def _calculate_rankings_from_comparisons():
        """Convert comparisons to rankings"""
        
    def _apply_tiebreakers():
        """Resolve ties using head-to-head comparisons"""

Django Views

Location: apps/rides/views.py

Web Views:

class RideRankingsView(ListView):
    """Main rankings list page with filtering"""
    template_name = 'rides/rankings.html'
    paginate_by = 50
    
class RideRankingDetailView(DetailView):
    """Detailed ranking view for a specific ride"""
    template_name = 'rides/ranking_detail.html'

HTMX Endpoints:

  • ranking_history_chart: Returns chart data for ranking history
  • ranking_comparisons: Returns head-to-head comparison data

URL Configuration

Location: apps/rides/urls.py

urlpatterns = [
    path('rankings/', RideRankingsView.as_view(), name='ride-rankings'),
    path('rankings/<slug:ride_slug>/', RideRankingDetailView.as_view(), name='ride-ranking-detail'),
    path('rankings/<slug:ride_slug>/history-chart/', ranking_history_chart, name='ranking-history-chart'),
    path('rankings/<slug:ride_slug>/comparisons/', ranking_comparisons, name='ranking-comparisons'),
]

API Implementation

Serializers

Location: apps/api/v1/serializers_rankings.py

class RideRankingSerializer(serializers.ModelSerializer):
    """Basic ranking data serialization"""
    
class RideRankingDetailSerializer(serializers.ModelSerializer):
    """Detailed ranking with relationships"""
    
class RankingSnapshotSerializer(serializers.ModelSerializer):
    """Historical ranking data"""
    
class RankingStatsSerializer(serializers.Serializer):
    """System-wide statistics"""

ViewSets

Location: apps/api/v1/viewsets_rankings.py

class RideRankingViewSet(viewsets.ReadOnlyModelViewSet):
    """
    REST API endpoint for ride rankings
    Supports filtering, ordering, and custom actions
    """
    
    @action(detail=True, methods=['get'])
    def history(self, request, pk=None):
        """Get historical ranking data"""
        
    @action(detail=True, methods=['get'])
    def comparisons(self, request, pk=None):
        """Get head-to-head comparisons"""
        
    @action(detail=False, methods=['get'])
    def statistics(self, request):
        """Get system-wide statistics"""

class TriggerRankingCalculationView(APIView):
    """Admin endpoint to trigger manual calculation"""

API URLs

Location: apps/api/v1/urls.py

router.register(r'rankings', RideRankingViewSet, basename='ranking')

urlpatterns = [
    path('', include(router.urls)),
    path('rankings/calculate/', TriggerRankingCalculationView.as_view()),
]

Management Commands

Location: apps/rides/management/commands/update_ride_rankings.py

# Update all rankings
python manage.py update_ride_rankings

# Update specific category
python manage.py update_ride_rankings --category RC

Admin Interface

Location: apps/rides/admin.py

@admin.register(RideRanking)
class RideRankingAdmin(admin.ModelAdmin):
    list_display = ['rank', 'ride', 'winning_percentage', 'wins', 'losses']
    list_filter = ['ride__category', 'last_calculated']
    search_fields = ['ride__name']
    ordering = ['rank']

Frontend Implementation

TypeScript Type Definitions

Location: frontend/src/types/index.ts

// Core ranking types
export interface RideRanking {
  id: number
  rank: number
  ride: {
    id: number
    name: string
    slug: string
    park: {
      id: number
      name: string
      slug: string
    }
    category: 'RC' | 'DR' | 'FR' | 'WR' | 'TR' | 'OT'
  }
  wins: number
  losses: number
  ties: number
  winning_percentage: number
  mutual_riders_count: number
  comparison_count: number
  average_rating: number
  last_calculated: string
  rank_change?: number
  previous_rank?: number | null
}

export interface RideRankingDetail extends RideRanking {
  ride: {
    // Extended ride information
    description?: string
    manufacturer?: { id: number; name: string }
    opening_date?: string
    status: string
  }
  calculation_version?: string
  head_to_head_comparisons?: HeadToHeadComparison[]
  ranking_history?: RankingSnapshot[]
}

export interface HeadToHeadComparison {
  opponent: {
    id: number
    name: string
    slug: string
    park: string
  }
  wins: number
  losses: number
  ties: number
  result: 'win' | 'loss' | 'tie'
  mutual_riders: number
}

export interface RankingSnapshot {
  date: string
  rank: number
  winning_percentage: number
}

export interface RankingStatistics {
  total_ranked_rides: number
  total_comparisons: number
  last_calculation_time: string
  calculation_duration: number
  top_rated_ride?: RideInfo
  most_compared_ride?: RideInfo
  biggest_rank_change?: RankChangeInfo
}

API Service Class

Location: frontend/src/services/api.ts

export class RankingsApi {
  // Core API methods
  async getRankings(params?: RankingParams): Promise<ApiResponse<RideRanking>>
  async getRankingDetail(rideSlug: string): Promise<RideRankingDetail>
  async getRankingHistory(rideSlug: string): Promise<RankingSnapshot[]>
  async getHeadToHeadComparisons(rideSlug: string): Promise<HeadToHeadComparison[]>
  async getRankingStatistics(): Promise<RankingStatistics>
  async calculateRankings(category?: string): Promise<CalculationResult>
  
  // Convenience methods
  async getTopRankings(limit: number, category?: string): Promise<RideRanking[]>
  async getParkRankings(parkSlug: string, params?: Params): Promise<ApiResponse<RideRanking>>
  async searchRankings(query: string): Promise<RideRanking[]>
  async getRankChange(rideSlug: string): Promise<RankChangeInfo>
}

Integration with Main API

// Singleton instance with all API services
export const api = new ThrillWikiApi()

// Direct access to rankings API
export const rankingsApi = api.rankings

API Reference

REST Endpoints

Get Rankings List

GET /api/v1/rankings/

Query Parameters:

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

Response:

{
  "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"
    }
  ]
}

Get Ranking Details

GET /api/v1/rankings/{ride-slug}/

Response: Extended ranking data with full ride details, comparisons, and history

Get Ranking History

GET /api/v1/rankings/{ride-slug}/history/

Response: Array of ranking snapshots (last 90 days)

Get Head-to-Head Comparisons

GET /api/v1/rankings/{ride-slug}/comparisons/

Response: Array of comparison results with all other rides

Get Statistics

GET /api/v1/rankings/statistics/

Response: System-wide ranking statistics

Trigger Calculation (Admin)

POST /api/v1/rankings/calculate/

Request Body:

{
  "category": "RC"  // Optional
}

Response:

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

Usage Examples

Frontend (Vue.js/TypeScript)

Display Top Rankings

import { rankingsApi } from '@/services/api'

export default {
  async mounted() {
    try {
      // Get top 10 rankings
      const topRides = await rankingsApi.getTopRankings(10)
      this.rankings = topRides
      
      // Get roller coasters only
      const response = await rankingsApi.getRankings({
        category: 'RC',
        page_size: 20,
        ordering: 'rank'
      })
      this.rollerCoasters = response.results
    } catch (error) {
      console.error('Failed to load rankings:', error)
    }
  }
}

Display Ranking Details

// In a Vue component
async loadRankingDetails(rideSlug: string) {
  const [details, history, comparisons] = await Promise.all([
    rankingsApi.getRankingDetail(rideSlug),
    rankingsApi.getRankingHistory(rideSlug),
    rankingsApi.getHeadToHeadComparisons(rideSlug)
  ])
  
  this.rankingDetails = details
  this.chartData = this.prepareChartData(history)
  this.comparisons = comparisons
}

Search Rankings

async searchRides(query: string) {
  const results = await rankingsApi.searchRankings(query)
  this.searchResults = results
}

Backend (Python/Django)

Access Rankings in Views

from apps.rides.models import RideRanking

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

# Get rankings for a specific category
coaster_rankings = RideRanking.objects.filter(
    ride__category='RC'
).order_by('rank')

# Get ranking with change indicator
ranking = RideRanking.objects.get(ride__slug='millennium-force')
if ranking.previous_rank:
    change = ranking.previous_rank - ranking.rank
    direction = 'up' if change > 0 else 'down' if change < 0 else 'same'

Trigger Ranking Update

from apps.rides.services.ranking_service import RideRankingService

# Update all rankings
service = RideRankingService()
result = service.update_all_rankings()

# Update specific category
result = service.update_all_rankings(category='RC')

print(f"Ranked {result['rides_ranked']} rides")
print(f"Made {result['comparisons_made']} comparisons")
print(f"Duration: {result['duration']:.2f} seconds")

Command Line

# Update rankings via management command
uv run python manage.py update_ride_rankings

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

# Schedule daily updates with cron
0 2 * * * cd /path/to/project && uv run python manage.py update_ride_rankings

Deployment & Maintenance

Initial Setup

  1. Run Migrations:
uv run python manage.py migrate
  1. Initial Ranking Calculation:
uv run python manage.py update_ride_rankings
  1. Verify in Admin:
  • Navigate to /admin/rides/rideranking/
  • Verify rankings are populated

Scheduled Updates

Add to crontab for daily updates:

# Update rankings daily at 2 AM
0 2 * * * cd /path/to/thrillwiki && uv run python manage.py update_ride_rankings

# Optional: Update different categories at different times
0 2 * * * cd /path/to/thrillwiki && uv run python manage.py update_ride_rankings --category RC
0 3 * * * cd /path/to/thrillwiki && uv run python manage.py update_ride_rankings --category DR

Monitoring

  1. Check Logs:
tail -f /path/to/logs/ranking_updates.log
  1. Monitor Performance:
  • Track calculation duration via API statistics endpoint
  • Monitor database query performance
  • Check comparison cache hit rates
  1. Data Validation:
# Check for ranking anomalies
from apps.rides.models import RideRanking

# Verify all ranks are unique
ranks = RideRanking.objects.values_list('rank', flat=True)
assert len(ranks) == len(set(ranks))

# Check winning percentage calculation
for ranking in RideRanking.objects.all():
    expected = (ranking.wins + 0.5 * ranking.ties) / ranking.comparison_count
    assert abs(ranking.winning_percentage - expected) < 0.001

Performance Optimization

  1. Database Indexes:
-- Ensure these indexes exist
CREATE INDEX idx_ranking_rank ON rides_rideranking(rank);
CREATE INDEX idx_ranking_ride ON rides_rideranking(ride_id);
CREATE INDEX idx_comparison_rides ON rides_ridepaircomparison(ride_a_id, ride_b_id);
  1. Cache Configuration:
# settings.py
CACHES = {
    'rankings': {
        'BACKEND': 'django.core.cache.backends.redis.RedisCache',
        'LOCATION': 'redis://127.0.0.1:6379/2',
        'TIMEOUT': 3600,  # 1 hour
    }
}
  1. Batch Processing:
  • Process comparisons in batches of 1000
  • Use bulk_create for database inserts
  • Consider parallel processing for large datasets

Troubleshooting

Common Issues:

  1. Rankings not updating:

    • Check cron job is running
    • Verify database connectivity
    • Check for lock files preventing concurrent runs
  2. Incorrect rankings:

    • Clear comparison cache and recalculate
    • Verify rating data integrity
    • Check for duplicate user ratings
  3. Performance issues:

    • Analyze slow queries with Django Debug Toolbar
    • Consider increasing database resources
    • Implement incremental updates for large datasets

API Rate Limiting

Configure in settings.py:

REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': [
        'rest_framework.throttling.AnonRateThrottle',
        'rest_framework.throttling.UserRateThrottle'
    ],
    'DEFAULT_THROTTLE_RATES': {
        'anon': '100/hour',
        'user': '1000/hour'
    }
}

Architecture Decisions

Why Pairwise Comparison?

  • Fairness: Only compares rides among users who've experienced both
  • Reduces Bias: Popular rides aren't advantaged over less-ridden ones
  • Head-to-Head Logic: Direct comparisons matter for tie-breaking
  • Robust to Outliers: One extreme rating doesn't skew results

Caching Strategy

  • Comparison Cache: Store pairwise results to avoid recalculation
  • Snapshot History: Keep 365 days of historical data
  • API Response Cache: Cache ranking lists for 1 hour

Scalability Considerations

  • O(n²) Complexity: Scales quadratically with number of rides
  • Batch Processing: Process in chunks to manage memory
  • Incremental Updates: Future enhancement for real-time updates

Testing

Unit Tests

# apps/rides/tests/test_ranking_service.py
class RankingServiceTestCase(TestCase):
    def test_pairwise_comparison(self):
        """Test comparison logic between two rides"""
        
    def test_ranking_calculation(self):
        """Test overall ranking calculation"""
        
    def test_tiebreaker_logic(self):
        """Test head-to-head tiebreaker"""

Integration Tests

# apps/api/tests/test_ranking_api.py
class RankingAPITestCase(APITestCase):
    def test_get_rankings_list(self):
        """Test ranking list endpoint"""
        
    def test_filtering_and_ordering(self):
        """Test query parameters"""
        
    def test_calculation_trigger(self):
        """Test admin calculation endpoint"""

Frontend Tests

// frontend/tests/api/rankings.test.ts
describe('Rankings API', () => {
  it('should fetch top rankings', async () => {
    const rankings = await rankingsApi.getTopRankings(10)
    expect(rankings).toHaveLength(10)
    expect(rankings[0].rank).toBe(1)
  })
  
  it('should filter by category', async () => {
    const response = await rankingsApi.getRankings({ category: 'RC' })
    expect(response.results.every(r => r.ride.category === 'RC')).toBe(true)
  })
})

Future Enhancements

Planned Features

  1. Real-time Updates: Update rankings immediately after new reviews
  2. Regional Rankings: Rankings by geographic region
  3. Time-Period Rankings: Best new rides, best classic rides
  4. User Preferences: Personalized rankings based on user history
  5. Confidence Intervals: Statistical confidence for rankings
  6. Mobile App API: Optimized endpoints for mobile applications

Potential Optimizations

  1. Incremental Updates: Only recalculate affected comparisons
  2. Parallel Processing: Distribute calculation across workers
  3. Machine Learning: Predict rankings for new rides
  4. GraphQL API: More flexible data fetching
  5. WebSocket Updates: Real-time ranking changes

Support & Documentation

Additional Resources

Contact

For questions or issues related to the ranking system:

  • Create an issue in the project repository
  • Contact the development team
  • Check the troubleshooting section above

Last Updated: January 2025 Version: 1.0