# 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:

```python
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:

```bash
# 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:

```bash
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
```python
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:

```django
{# 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**:
```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",
      "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

```json
{
  "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**:
```json
{
  "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**:
```json
{
  "category": "RC"  // Optional - filter to specific category
}
```

**Response**:
```json
{
  "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

```python
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

```typescript
// 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

- [Internet Roller Coaster Poll](https://ushsho.com/ridesurvey.py) - Original algorithm source
- [Condorcet Method](https://en.wikipedia.org/wiki/Condorcet_method) - Similar voting system theory
- [Pairwise Comparison](https://en.wikipedia.org/wiki/Pairwise_comparison) - Mathematical foundation
