Created
September 27, 2026 23:32
-
-
Save cjus/5704e25a222a59c86f4bd596a249fd39 to your computer and use it in GitHub Desktop.
build_birds.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """Build birds.db, the practice database for the AIE250 SQLite guide. | |
| Run it from the folder you want the database in: | |
| python build_birds.py | |
| It deletes any birds.db already there and builds a fresh one, so you can | |
| always get back to a clean copy by running it again. It uses only the Python | |
| standard library. | |
| The data is invented. The species names are real birds; the sites, the | |
| observers and every sighting are made up. The numbers come from a small | |
| arithmetic generator rather than the random module, so every copy of | |
| birds.db is identical, on any computer and any Python version. | |
| """ | |
| import os | |
| import sqlite3 | |
| DB_PATH = "birds.db" | |
| SCHEMA = """ | |
| CREATE TABLE species ( | |
| species_id INTEGER PRIMARY KEY, | |
| common_name TEXT NOT NULL UNIQUE, | |
| family TEXT NOT NULL | |
| ); | |
| CREATE TABLE sites ( | |
| site_code TEXT NOT NULL PRIMARY KEY, | |
| name TEXT NOT NULL, | |
| habitat TEXT NOT NULL, | |
| elevation_m INTEGER NOT NULL | |
| ); | |
| CREATE TABLE observers ( | |
| observer_id INTEGER PRIMARY KEY, | |
| name TEXT NOT NULL, | |
| joined_on TEXT NOT NULL | |
| ); | |
| CREATE TABLE sightings ( | |
| sighting_id INTEGER PRIMARY KEY, | |
| species_id INTEGER NOT NULL REFERENCES species(species_id), | |
| site_code TEXT NOT NULL REFERENCES sites(site_code), | |
| observer_id INTEGER NOT NULL REFERENCES observers(observer_id), | |
| seen_on TEXT NOT NULL, | |
| bird_count INTEGER NOT NULL CHECK (bird_count > 0) | |
| ); | |
| """ | |
| SPECIES = [ | |
| ("American Robin", "Turdidae"), | |
| ("Mountain Bluebird", "Turdidae"), | |
| ("Black-capped Chickadee", "Paridae"), | |
| ("Mountain Chickadee", "Paridae"), | |
| ("Steller's Jay", "Corvidae"), | |
| ("Black-billed Magpie", "Corvidae"), | |
| ("Common Raven", "Corvidae"), | |
| ("California Gull", "Laridae"), | |
| ("Red-tailed Hawk", "Accipitridae"), | |
| ("Golden Eagle", "Accipitridae"), | |
| ("Bald Eagle", "Accipitridae"), | |
| ("Great Blue Heron", "Ardeidae"), | |
| ("Mallard", "Anatidae"), | |
| ("Canada Goose", "Anatidae"), | |
| ("American Avocet", "Recurvirostridae"), | |
| ("Black-necked Stilt", "Recurvirostridae"), | |
| ("House Finch", "Fringillidae"), | |
| ("Pine Siskin", "Fringillidae"), | |
| ("Western Meadowlark", "Icteridae"), | |
| ("Red-winged Blackbird", "Icteridae"), | |
| ("Downy Woodpecker", "Picidae"), | |
| ("Northern Flicker", "Picidae"), | |
| ("Broad-tailed Hummingbird", "Trochilidae"), | |
| ("Black-chinned Hummingbird", "Trochilidae"), | |
| ("Dark-eyed Junco", "Passerellidae"), | |
| ("Song Sparrow", "Passerellidae"), | |
| ("Spotted Towhee", "Passerellidae"), | |
| ("Western Tanager", "Cardinalidae"), | |
| ("Lazuli Bunting", "Cardinalidae"), | |
| ("Cedar Waxwing", "Bombycillidae"), | |
| ] | |
| SITES = [ | |
| ("NMR", "North Marsh", "wetland", 1283), | |
| ("SMR", "South Marsh", "wetland", 1286), | |
| ("CTR", "Cottonwood Trail", "riparian", 1402), | |
| ("OAK", "Oak Hollow", "woodland", 1610), | |
| ("MDW", "Long Meadow", "grassland", 1455), | |
| ("RDG", "Ridge Overlook", "cliff", 2210), | |
| ("ASP", "Aspen Bowl", "forest", 2640), | |
| ("TRN", "Tarn Basin", "alpine", 2915), | |
| ("PRK", "City Park Pond", "urban", 1311), | |
| ("ORC", "Old Orchard", "urban", 1340), | |
| ] | |
| OBSERVERS = [ | |
| ("Ava Lindqvist", "2024-02-11"), | |
| ("Mateo Ruiz", "2024-03-02"), | |
| ("Priya Natarajan", "2024-03-19"), | |
| ("Jonah Whitfield", "2024-04-07"), | |
| ("Keiko Tanabe", "2024-05-23"), | |
| ("Samuel Okafor", "2024-06-14"), | |
| ("Lena Fischer", "2024-07-01"), | |
| ("Diego Castillo", "2024-08-30"), | |
| ("Hannah Brooks", "2024-09-12"), | |
| ("Tariq Haddad", "2024-10-05"), | |
| ("Mei Chen", "2024-11-18"), | |
| ("Owen Gallagher", "2025-01-09"), | |
| ("Rosa Delgado", "2025-02-27"), | |
| ("Ivan Petrov", "2025-04-15"), | |
| ("Nadia Rahman", "2025-06-03"), | |
| ("Caleb Morgan", "2025-08-21"), | |
| ] | |
| SIGHTING_COUNT = 100_000 | |
| # Days in each month of 2025, then of 2026 up to August. Sightings fall | |
| # between 2025-01-01 and 2026-08-31. | |
| MONTHS = [(2025, m, d) for m, d in enumerate([31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31], 1)] | |
| MONTHS += [(2026, m, d) for m, d in enumerate([31, 28, 31, 30, 31, 30, 31, 31], 1)] | |
| DATES = [f"{y}-{m:02d}-{day:02d}" for y, m, days in MONTHS for day in range(1, days + 1)] | |
| class Generator: | |
| """A linear congruential generator: the same numbers on every machine.""" | |
| def __init__(self, seed): | |
| self.state = seed | |
| def below(self, n): | |
| """Return a whole number from 0 up to, but not including, n.""" | |
| self.state = (1664525 * self.state + 1013904223) % 2**32 | |
| return (self.state * n) >> 32 | |
| def make_sightings(gen): | |
| rows = [] | |
| for _ in range(SIGHTING_COUNT): | |
| # Squaring a fraction makes low species ids more common than high | |
| # ones, so some birds are seen far more often than others. | |
| fraction = gen.below(10_000) / 10_000 | |
| species_id = 1 + int(fraction * fraction * len(SPECIES)) | |
| site_code = SITES[gen.below(len(SITES))][0] | |
| observer_id = 1 + gen.below(len(OBSERVERS)) | |
| seen_on = DATES[gen.below(len(DATES))] | |
| bird_count = 1 + gen.below(4) * gen.below(6) | |
| rows.append((species_id, site_code, observer_id, seen_on, bird_count)) | |
| return rows | |
| def main(): | |
| if os.path.exists(DB_PATH): | |
| os.remove(DB_PATH) | |
| con = sqlite3.connect(DB_PATH) | |
| con.execute("PRAGMA foreign_keys = ON") | |
| con.executescript(SCHEMA) | |
| with con: | |
| con.executemany( | |
| "INSERT INTO species (common_name, family) VALUES (?, ?)", SPECIES | |
| ) | |
| con.executemany( | |
| "INSERT INTO sites (site_code, name, habitat, elevation_m) VALUES (?, ?, ?, ?)", | |
| SITES, | |
| ) | |
| con.executemany( | |
| "INSERT INTO observers (name, joined_on) VALUES (?, ?)", OBSERVERS | |
| ) | |
| con.executemany( | |
| "INSERT INTO sightings (species_id, site_code, observer_id, seen_on, bird_count)" | |
| " VALUES (?, ?, ?, ?, ?)", | |
| make_sightings(Generator(seed=250)), | |
| ) | |
| for table in ("species", "sites", "observers", "sightings"): | |
| count = con.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0] | |
| print(f"{table:<10} {count:>7} rows") | |
| con.close() | |
| print(f"Built {DB_PATH} with SQLite {sqlite3.sqlite_version}") | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment