# Ride Ranking System - Complete Implementation Documentation ## Table of Contents 1. [Overview](#overview) 2. [Backend Implementation](#backend-implementation) 3. [Frontend Implementation](#frontend-implementation) 4. [API Reference](#api-reference) 5. [Usage Examples](#usage-examples) 6. [Deployment & Maintenance](#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` ```python # 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: ```python 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**: ```python 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` ```python urlpatterns = [ path('rankings/', RideRankingsView.as_view(), name='ride-rankings'), path('rankings//', RideRankingDetailView.as_view(), name='ride-ranking-detail'), path('rankings//history-chart/', ranking_history_chart, name='ranking-history-chart'), path('rankings//comparisons/', ranking_comparisons, name='ranking-comparisons'), ] ``` ### API Implementation #### Serializers **Location**: `apps/api/v1/serializers_rankings.py` ```python 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` ```python 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` ```python 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` ```bash # 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` ```python @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` ```typescript // 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` ```typescript export class RankingsApi { // Core API methods async getRankings(params?: RankingParams): Promise> async getRankingDetail(rideSlug: string): Promise async getRankingHistory(rideSlug: string): Promise async getHeadToHeadComparisons(rideSlug: string): Promise async getRankingStatistics(): Promise async calculateRankings(category?: string): Promise // Convenience methods async getTopRankings(limit: number, category?: string): Promise async getParkRankings(parkSlug: string, params?: Params): Promise> async searchRankings(query: string): Promise async getRankChange(rideSlug: string): Promise } ``` ### Integration with Main API ```typescript // 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 ```http 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**: ```json { "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 ```http GET /api/v1/rankings/{ride-slug}/ ``` **Response**: Extended ranking data with full ride details, comparisons, and history #### Get Ranking History ```http GET /api/v1/rankings/{ride-slug}/history/ ``` **Response**: Array of ranking snapshots (last 90 days) #### Get Head-to-Head Comparisons ```http GET /api/v1/rankings/{ride-slug}/comparisons/ ``` **Response**: Array of comparison results with all other rides #### Get Statistics ```http GET /api/v1/rankings/statistics/ ``` **Response**: System-wide ranking statistics #### Trigger Calculation (Admin) ```http POST /api/v1/rankings/calculate/ ``` **Request Body**: ```json { "category": "RC" // Optional } ``` **Response**: ```json { "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 ```typescript 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 ```typescript // 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 ```typescript async searchRides(query: string) { const results = await rankingsApi.searchRankings(query) this.searchResults = results } ``` ### Backend (Python/Django) #### Access Rankings in Views ```python 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 ```python 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 ```bash # 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**: ```bash uv run python manage.py migrate ``` 2. **Initial Ranking Calculation**: ```bash uv run python manage.py update_ride_rankings ``` 3. **Verify in Admin**: - Navigate to `/admin/rides/rideranking/` - Verify rankings are populated ### Scheduled Updates Add to crontab for daily updates: ```bash # 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**: ```bash tail -f /path/to/logs/ranking_updates.log ``` 2. **Monitor Performance**: - Track calculation duration via API statistics endpoint - Monitor database query performance - Check comparison cache hit rates 3. **Data Validation**: ```python # 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**: ```sql -- 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); ``` 2. **Cache Configuration**: ```python # settings.py CACHES = { 'rankings': { 'BACKEND': 'django.core.cache.backends.redis.RedisCache', 'LOCATION': 'redis://127.0.0.1:6379/2', 'TIMEOUT': 3600, # 1 hour } } ``` 3. **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`: ```python 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 ```python # 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 ```python # 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 ```typescript // 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 - [Original IRCP Algorithm](https://ushsho.com/ridesurvey.py) - [Django REST Framework Documentation](https://www.django-rest-framework.org/) - [Vue.js Documentation](https://vuejs.org/) - [TypeScript Documentation](https://www.typescriptlang.org/) ### 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*