- 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.
19 KiB
Ride Ranking System - Complete Implementation Documentation
Table of Contents
- Overview
- Backend Implementation
- Frontend Implementation
- API Reference
- Usage Examples
- 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 positionwins(Integer): Number of head-to-head winslosses(Integer): Number of head-to-head lossesties(Integer): Number of tied comparisonswinning_percentage(Decimal): Win percentage (ties count as 0.5)mutual_riders_count(Integer): Total users who rated this rideaverage_rating(Decimal): Average user ratinglast_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 historyranking_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 numberpage_size(integer): Results per page (default: 20)category(string): Filter by category (RC, DR, FR, WR, TR, OT)min_riders(integer): Minimum mutual riderspark(string): Filter by park slugordering(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
- Run Migrations:
uv run python manage.py migrate
- Initial Ranking Calculation:
uv run python manage.py update_ride_rankings
- 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
- Check Logs:
tail -f /path/to/logs/ranking_updates.log
- Monitor Performance:
- Track calculation duration via API statistics endpoint
- Monitor database query performance
- Check comparison cache hit rates
- 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
- 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);
- 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
}
}
- Batch Processing:
- Process comparisons in batches of 1000
- Use bulk_create for database inserts
- Consider parallel processing for large datasets
Troubleshooting
Common Issues:
-
Rankings not updating:
- Check cron job is running
- Verify database connectivity
- Check for lock files preventing concurrent runs
-
Incorrect rankings:
- Clear comparison cache and recalculate
- Verify rating data integrity
- Check for duplicate user ratings
-
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
- Real-time Updates: Update rankings immediately after new reviews
- Regional Rankings: Rankings by geographic region
- Time-Period Rankings: Best new rides, best classic rides
- User Preferences: Personalized rankings based on user history
- Confidence Intervals: Statistical confidence for rankings
- Mobile App API: Optimized endpoints for mobile applications
Potential Optimizations
- Incremental Updates: Only recalculate affected comparisons
- Parallel Processing: Distribute calculation across workers
- Machine Learning: Predict rankings for new rides
- GraphQL API: More flexible data fetching
- WebSocket Updates: Real-time ranking changes
Support & Documentation
Additional Resources
- Original IRCP Algorithm
- Django REST Framework Documentation
- Vue.js Documentation
- TypeScript Documentation
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