# House ELO Ranking A pairwise comparison tool that lets you rank houses using the [ELO rating system](https://en.wikipedia.org/wiki/Elo_rating_system). Built to rank [Funda](https://www.funda.nl/) listings during a personal house search, but adaptable to any listing dataset stored in PostgreSQL. Two listings are shown side-by-side. You pick the one you prefer, and both receive updated ELO ratings. Over time this produces a reliable ranking of all listings based on your personal preferences. ## Features - **Matchmaking** – weighted-random pairing that avoids recent duplicates and favours less-compared listings - **ELO ratings** – standard ELO formula with configurable K-factor - **Rankings** – sorted table of all sampled listings by ELO - **History** – browse every comparison you have made - **Statistics** – total comparisons, ELO distribution chart, aggregates - **Image carousel** – listing photos pulled from a JSON column in the database ## Architecture ``` ┌──────────┐ ┌──────────┐ ┌────────────┐ │ Frontend │─nginx──│ Backend │────────│ PostgreSQL │ │ React/TS │ :80 │ FastAPI │ :5432 │ │ └──────────┘ └──────────┘ └────────────┘ ``` | Layer | Stack | |----------|--------------------------------------------------| | Frontend | React 18, TypeScript, Vite, Tailwind CSS | | Backend | FastAPI, SQLAlchemy, Pydantic, Python 3.12, uv | | Database | PostgreSQL (external, not managed by this project)| | Infra | Docker Compose, nginx reverse proxy | ## Requirements - **Docker** and **Docker Compose** - An existing **PostgreSQL** database with the tables described below - **uv** (for local development outside Docker) ### Database schema The application reads listings from a configurable schema/table and manages its own ELO tables. The required structures are: #### Listings table (read-only, provided by you) The table configured via `LISTINGS_SCHEMA` / `LISTINGS_TABLE` must contain at least these columns: | Column | Type | Description | |--------------------|---------|----------------------------| | `global_id` | text | Unique listing identifier | | `tiny_id` | text | Short identifier | | `url` | text | Listing URL | | `title` | text | Listing title | | `city` | text | City name | | `postcode` | text | Postal code | | `province` | text | Province | | `neighbourhood` | text | Neighbourhood name | | `municipality` | text | Municipality | | `latitude` | numeric | Latitude | | `longitude` | numeric | Longitude | | `object_type` | text | e.g. apartment, house | | `house_type` | text | e.g. upstairs, detached | | `offering_type` | text | e.g. buy, rent | | `construction_type`| text | e.g. existing, new | | `construction_year`| text | Year of construction | | `energy_label` | text | Energy label (A, B, …) | | `living_area` | integer | Living area in m² | | `plot_area` | integer | Plot area in m² | | `bedrooms` | integer | Number of bedrooms | | `rooms` | integer | Total rooms | | `has_garden` | boolean | Garden present | | `has_balcony` | boolean | Balcony present | | `has_solar_panels` | boolean | Solar panels present | | `has_heat_pump` | boolean | Heat pump present | | `has_roof_terrace` | boolean | Roof terrace present | | `is_energy_efficient` | boolean | Energy efficient | | `is_monument` | boolean | Monument status | | `current_price` | integer | Price in euros | | `status` | text | e.g. available, sold | | `price_per_sqm` | numeric | Price per m² | | `publication_date` | text | Publication date | #### Sample listings table The schema configured via `ELO_SCHEMA` must contain a `sample_listings` table with a `global_id` column. Only listings present in this table are shown during comparisons and ranking. #### Images table (optional) The table configured via `IMAGES_SCHEMA` / `IMAGES_TABLE` must contain: | Column | Type | Description | |------------|-------|--------------------------------------| | `global_id`| text | Listing identifier | | `raw_json` | jsonb | Must contain a `photo_urls` key with a list of image URLs | #### ELO tables (auto-managed) The application creates and manages `ratings` and `comparisons` tables inside the `ELO_SCHEMA`. These are created automatically on first run via SQLAlchemy ORM. ## Getting started ### 1. Clone and configure ```bash git clone https://github.com//house-elo-ranking.git cd house-elo-ranking cp .env.example .env # Edit .env with your database credentials and schema names ``` ### 2. Run with Docker ```bash make build make up ``` The frontend is available at `http://localhost:8888` (or whatever `FRONTEND_PORT` you set). ### 3. Local development ```bash make install # install Python deps make test # run unit tests make lint # lint Python + SQL make format # auto-format code ``` ## Environment variables | Variable | Default | Description | |----------------------|------------------|------------------------------------------| | `POSTGRES_HOST` | `localhost` | PostgreSQL host | | `POSTGRES_PORT` | `5432` | PostgreSQL port | | `POSTGRES_USER` | `postgres` | Database user | | `POSTGRES_PASSWORD` | `postgres` | Database password | | `POSTGRES_DB` | `postgres` | Database name | | `DATABASE_URL` | — | Full connection string (overrides above) | | `LISTINGS_SCHEMA` | `marts` | Schema containing the listings table | | `LISTINGS_TABLE` | `funda_listings` | Table name for listings | | `ELO_SCHEMA` | `elo` | Schema for ELO rating tables | | `IMAGES_SCHEMA` | `raw_funda` | Schema for the images table | | `IMAGES_TABLE` | `listing_details`| Table containing `raw_json` with photos | | `ELO_K_FACTOR` | `32` | ELO K-factor (higher = bigger swings) | | `ELO_DEFAULT_RATING` | `1500` | Starting ELO for unrated listings | | `FRONTEND_PORT` | `8888` | Port the frontend is served on | ## Makefile commands ``` make help Show all available commands make install Install backend dependencies make lint Run all linters (ruff + sqlfluff) make format Auto-format Python and SQL make test Run unit tests make build Build Docker images make up Start services make down Stop services make logs Tail service logs make clean Remove caches and build artifacts ``` ## License [MIT](LICENSE)