Created a script to pull sighting data from

INatraulist in just the US based on species list
This commit is contained in:
Suppai_Neko committed 2026-03-27 13:51:36 -07:00
1 parent afa683b7e3
commit b6873c829c
3 files changed
+659

No files matched your search

+88
View File
@@ -0,0 +1,88 @@
"""
Visualize species sighting coordinates overlaid on a map.
Usage:
python dev_scripts/map_viewer.py # shows all species
python dev_scripts/map_viewer.py "Mallard" # filter by common name (partial match)
python dev_scripts/map_viewer.py --list # list all available species
Requires: pip install folium
"""
import csv
import json
import sys
import webbrowser
from pathlib import Path
SIGHTINGS_FILE = Path(__file__).parent.parent / "wildlife" / "portland_wildlife_sightings.csv"
OUTPUT_HTML = Path(__file__).parent.parent / "wildlife" / "sightings_map.html"
PORTLAND_CENTER = [45.5051, -122.6750]
def load_sightings(filter_name: str | None = None) -> list[dict]:
rows = []
with open(SIGHTINGS_FILE, newline="", encoding="utf-8") as f:
for row in csv.DictReader(f):
coords = json.loads(row["sighting_coordinates"])
if not coords:
continue
if filter_name and filter_name.lower() not in row["common_name"].lower():
continue
rows.append({
"taxon_id": row["taxon_id"],
"common_name": row["common_name"],
"scientific_name": row["scientific_name"],
"coords": coords,
})
return rows
def build_map(species_list: list[dict]) -> str:
try:
import folium
except ImportError:
print("folium is not installed. Run: pip install folium")
sys.exit(1)
m = folium.Map(location=PORTLAND_CENTER, zoom_start=11, tiles="OpenStreetMap")
for sp in species_list:
for lat, lng in sp["coords"]:
folium.CircleMarker(
location=[lat, lng],
radius=5,
color="red",
fill=True,
fill_color="red",
fill_opacity=0.7,
tooltip=f"{sp['common_name']}<br><i>{sp['scientific_name']}</i>",
).add_to(m)
return m._repr_html_()
def main():
if "--list" in sys.argv:
with open(SIGHTINGS_FILE, newline="", encoding="utf-8") as f:
for row in csv.DictReader(f):
coords = json.loads(row["sighting_coordinates"])
print(f" {row['common_name']:40s} {len(coords)} sightings")
return
filter_name = next((a for a in sys.argv[1:] if not a.startswith("--")), None)
species_list = load_sightings(filter_name)
if not species_list:
print(f"No species found matching '{filter_name}'")
return
print(f"Plotting {len(species_list)} species...")
html = build_map(species_list)
OUTPUT_HTML.write_text(html, encoding="utf-8")
print(f"Map saved to {OUTPUT_HTML}")
webbrowser.open(OUTPUT_HTML.as_uri())
if __name__ == "__main__":
main()
+112
View File
@@ -0,0 +1,112 @@
import csv
import json
import time
import requests
from pathlib import Path
PLACE_ID = 2143
BASE_URL = "https://api.inaturalist.org/v1/observations"
INPUT_FILE = Path(__file__).parent.parent / "wildlife" / "portland_wildlife.csv"
OUTPUT_FILE = Path(__file__).parent.parent / "wildlife" / "portland_wildlife_sightings.csv"
OUTPUT_FIELDS = ["taxon_id", "scientific_name", "common_name", "sighting_coordinates"]
def load_already_fetched(path: Path) -> set[int]:
"""Return set of taxon_ids already written to the output CSV."""
if not path.exists():
return set()
done = set()
try:
with open(path, newline="", encoding="utf-8") as f:
for row in csv.DictReader(f):
done.add(int(row["taxon_id"]))
except Exception:
pass
return done
def fetch_observations(taxon_id: int) -> list[list[float]]:
"""Fetch up to 600 random research-grade observations (3 pages x 200) and return [lat, lng] pairs."""
coords = []
for page in range(1, 4):
response = requests.get(
BASE_URL,
headers={"User-Agent": "WildDex (wildlife dex app; contact via colemanswenson@gmail.com)"},
params={
"taxon_id": taxon_id,
"quality_grade": "research",
"per_page": 200,
"page": page,
"order_by": "random",
"swlat": 14.5,
"swlng": -168.0,
"nelat": 72.0,
"nelng": -52.0,
},
)
response.raise_for_status()
results = response.json().get("results", [])
if not results:
break
for obs in results:
location = obs.get("location")
if not location:
continue
try:
lat, lng = location.split(",")
coords.append([float(lat), float(lng)])
except (ValueError, AttributeError):
continue
time.sleep(1)
return coords
def read_species(path: Path) -> list[dict]:
"""Read portland_wildlife.csv and return all rows with taxon_id cast to int."""
with open(path, newline="", encoding="utf-8") as f:
rows = list(csv.DictReader(f))
for row in rows:
row["taxon_id"] = int(row["taxon_id"])
return rows
def main() -> None:
species = read_species(INPUT_FILE)
done = load_already_fetched(OUTPUT_FILE)
is_new = not OUTPUT_FILE.exists() or OUTPUT_FILE.stat().st_size == 0
total = len(species)
with open(OUTPUT_FILE, "a", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=OUTPUT_FIELDS)
if is_new:
writer.writeheader()
for i, sp in enumerate(species, start=1):
taxon_id = sp["taxon_id"]
if taxon_id in done:
print(f"[{i}/{total}] Skipping {sp['common_name']} (already done)")
continue
try:
coords = fetch_observations(taxon_id)
except requests.HTTPError as e:
print(f"[{i}/{total}] {sp['common_name']} — HTTP error {e}, skipping")
coords = []
writer.writerow({
"taxon_id": taxon_id,
"scientific_name": sp["scientific_name"],
"common_name": sp["common_name"],
"sighting_coordinates": json.dumps(coords),
})
f.flush()
print(f"[{i}/{total}] {sp['common_name']} ({taxon_id}) — {len(coords)} coords")
time.sleep(1)
print(f"\nDone. Output: {OUTPUT_FILE}")
if __name__ == "__main__":
main()