Danışman araçları, TÜİK mezun istihdamı ve Jev çıktı denetimi

- Danışman: program_netleri, program_karsilastir ve bolum_istihdam araçları;
  program_ara'ya devlet/vakıf, öğretim türü ve sıralama parametreleri;
  program_detay'a trend cümlesi; bozuk araç argümanı modele hata döner.
- TÜİK Yükseköğretim İstihdam Göstergeleri 2025: bölüm bazında istihdam
  oranı, ilk işi bulma süresi ve kazanç grubu (src/lib/bolum-istihdam.ts,
  üretici scripts/tuik-istihdam-uret.py, kaynak Excel'ler data/kaynak/).
- Bölüm sayfasında "Mezun istihdamı" ayrıntı satırı ve alt sayfası;
  AkademikAyrinti başlık/ikon alır.
- Jev (TypeSafe AI) ile danışman cevabında garanti/olasılık dili denetimi;
  ihlalde cevabın sonuna düzeltme notu. Anahtar yoksa denetim atlanır.
- /gizlilik: TypeSafe AI (ABD) sağlayıcı satırı. KARARLAR.md: 5 Ekim kaydı.
- Üç katalog dosyasındaki bg-white → bg-card değişiklikleri de bu commit'te.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-10-05 00:15:05 +03:00
parent ab0b3a047f
commit 6564ec8948
19 changed files with 1189 additions and 25 deletions

View File

@@ -0,0 +1,168 @@
"""TÜİK Yükseköğretim İstihdam Göstergeleri Excel'lerinden src/lib/bolum-istihdam.ts sabitini üretir.
Kullanım (yılda bir, TÜİK bülteni Temmuz'da yayımlayınca):
python3 scripts/tuik-istihdam-uret.py 2025
Girdi: data/kaynak/tuik-istihdam-{lisans,onlisans}-bolum-{istihdam-orani,is-bulma-suresi,kazanc-gruplari}-<YIL>.xls
(veriportali.tuik.gov.tr → "Yükseköğretim İstihdam Göstergeleri, <YIL>" bülteni →
Tablolar; tarayıcıyla tek tek indirilir — portal komut satırını engelliyor ve
art arda istekte boş sayfa dönüyor). Bağımlılık: xlrd.
Kurallar:
- Veri ÜLKE GENELİ ve bölüm bazındadır; üniversite kırılımı TÜİK'te yok.
- Anahtar: /bolum/[slug] sayfa slug'ı (bolumBazAdi + turkishSlugify). Yalnız
yokatlas.db'de sayfası olan bölümler yazılır; eşleşmeyenler ekrana basılır.
Takma ad tablosu yok: eşleşmeyenler henüz mezun vermemiş yeni bölümler ya da
bizde olmayan özel yetenek / kapanmış bölümlerdir.
- "." = TÜİK'in yayımlamadığı hücre → null. Mezun sayısı medyanın altındaki
bölümler tablolarda hiç yok.
- Kazanç TL değil, TÜİK'in beş kademeli grubudur (Çok düşük … Çok yüksek).
"""
import re
import sqlite3
import sys
import unicodedata
from pathlib import Path
import xlrd
KOK = Path(__file__).resolve().parent.parent
YIL = sys.argv[1] if len(sys.argv) > 1 else "2025"
BULTEN_URL = "https://veriportali.tuik.gov.tr/tr/press/58027"
DUZEYLER = (("lisans", 0), ("onlisans", 1))
KAZANC_GRUPLARI = ("Çok düşük", "Düşük", "Orta", "Yüksek", "Çok yüksek")
def slugify(s: str) -> str:
# src/lib/slug.ts turkishSlugify ile aynı kural.
s = s.replace("I", "ı").replace("İ", "i").lower()
for a, b in [("ı", "i"), ("ş", "s"), ("ğ", "g"), ("ü", "u"), ("ö", "o"),
("ç", "c"), ("â", "a"), ("î", "i"), ("û", "u")]:
s = s.replace(a, b)
s = unicodedata.normalize("NFKD", s)
s = re.sub(r"[̀-ͯ]", "", s)
s = re.sub(r"[^a-z0-9]+", "-", s)
return s.strip("-")
def bolum_slug(isim: str) -> str:
# src/lib/slug.ts bolumSayfaSlug ile aynı kural.
return slugify(re.sub(r"\s+", " ", re.sub(r"\s*\([^)]*\)", "", isim)).strip())
def tr_satir(v) -> str:
"""Hücreler 'Türkçe\\r\\nEnglish' biçiminde; Türkçe satırı al."""
return str(v).replace("\r", "").split("\n")[0].strip()
def tablo(duzey: str, ad: str) -> tuple[dict[str, tuple[str, object]], object]:
"""slug → (TÜİK adı, son yıl değeri) ve 'Toplam' satırının değeri."""
yol = KOK / "data" / "kaynak" / f"tuik-istihdam-{duzey}-bolum-{ad}-{YIL}.xls"
s = xlrd.open_workbook(yol).sheet_by_index(0)
baslik = next(r for r in range(s.nrows) if str(s.cell_value(r, 0)).startswith("Bölüm"))
kolon = next(c for c in range(s.ncols) if str(s.cell_value(baslik, c)).startswith(YIL))
satirlar: dict[str, tuple[str, object]] = {}
toplam = None
for r in range(baslik + 1, s.nrows):
bolum = tr_satir(s.cell_value(r, 0))
if not bolum or bolum.startswith(("TÜİK", "TurkStat", ".")):
continue
v = s.cell_value(r, kolon)
if bolum == "Toplam":
toplam = v
continue
satirlar[slugify(bolum)] = (bolum, v)
return satirlar, toplam
def sayi(v) -> float | None:
return round(float(v), 1) if isinstance(v, (int, float)) else None
def kazanc(v) -> str | None:
g = tr_satir(v)
if g in KAZANC_GRUPLARI:
return g
if g not in (".", ""):
sys.exit(f"Bilinmeyen kazanç grubu: {g!r}")
return None
def ts(v) -> str:
if v is None:
return "null"
return f'"{v}"' if isinstance(v, str) else str(v)
db = sqlite3.connect(f"file:{KOK / 'data' / 'yokatlas.db'}?mode=ro", uri=True)
bloklar: list[str] = []
ortalamalar: list[str] = []
for duzey, onlisans in DUZEYLER:
sayfalar = {bolum_slug(i) for (i,) in db.execute("SELECT DISTINCT isim FROM programs WHERE onlisans = ?", (onlisans,))}
oran, oran_toplam = tablo(duzey, "istihdam-orani")
sure, sure_toplam = tablo(duzey, "is-bulma-suresi")
kaz, _ = tablo(duzey, "kazanc-gruplari")
hepsi = sorted(set(oran) | set(sure) | set(kaz))
eslesen = [k for k in hepsi if k in sayfalar]
eslesmeyen = [(oran.get(k) or sure.get(k) or kaz[k])[0] for k in hepsi if k not in sayfalar]
print(f"{duzey}: TÜİK {len(hepsi)} bölüm, sayfası olan {len(eslesen)}")
if eslesmeyen:
print(" sayfası olmayan (yazılmadı):", ", ".join(eslesmeyen))
satirlar = []
for k in eslesen:
o = sayi(oran[k][1]) if k in oran else None
s = sayi(sure[k][1]) if k in sure else None
g = kazanc(kaz[k][1]) if k in kaz else None
if o is None and s is None and g is None:
continue
satirlar.append(f' "{k}": {{ istihdam: {ts(o)}, isBulmaAy: {ts(s)}, kazanc: {ts(g)} }},')
bloklar.append(f" {duzey}: {{\n" + "\n".join(satirlar) + "\n },")
ortalamalar.append(f" {duzey}: {{ istihdam: {ts(sayi(oran_toplam))}, isBulmaAy: {ts(sayi(sure_toplam))} }},")
cikti = f"""// TÜİK Yükseköğretim İstihdam Göstergeleri {YIL}: bölüm başına mezun istihdamı.
//
// ÜRETİLMİŞ DOSYA — elle düzenleme. Kaynak Excel'ler data/kaynak/ altında;
// yeniden üretmek için: python3 scripts/tuik-istihdam-uret.py {YIL}
//
// Veri ÜLKE GENELİ ve bölüm bazındadır: aynı bölümün her üniversitesi için
// aynı sayı döner; üniversite kırılımı TÜİK'te yok. Kayıtlı istihdam = mezunun
// SGK kaydıyla çalışıyor olması. Kazanç TL değil, beş kademeli gruptur.
// Anahtar: /bolum/[slug] sayfa slug'ı. Burada olmayan bölüm ya henüz yeterli
// mezun vermemiştir ya da TÜİK tablosunda yoktur — "kötü" demek değildir.
export const BOLUM_ISTIHDAM_YIL = {YIL};
export const BOLUM_ISTIHDAM_KAYNAK = "TÜİK, Yükseköğretim İstihdam Göstergeleri, {YIL}";
export const BOLUM_ISTIHDAM_KAYNAK_URL = "{BULTEN_URL}";
export type KazancGrubu = {" | ".join(f'"{g}"' for g in KAZANC_GRUPLARI)};
export type IstihdamDuzeyi = "lisans" | "onlisans";
export type BolumIstihdam = {{
/** Kayıtlı istihdam oranı (%) */
istihdam: number | null;
/** Mezuniyetten sonra ilk işi bulma süresi (ay, ortalama) */
isBulmaAy: number | null;
/** Aylık ortalama kazanç grubu (bölümler arası göreli) */
kazanc: KazancGrubu | null;
}};
/** Tüm mezunların ortalaması — kıyas çizgisi. */
export const ISTIHDAM_ORTALAMA: Record<IstihdamDuzeyi, {{ istihdam: number | null; isBulmaAy: number | null }}> = {{
{chr(10).join(ortalamalar)}
}};
export const BOLUM_ISTIHDAM: Record<IstihdamDuzeyi, Record<string, BolumIstihdam>> = {{
{chr(10).join(bloklar)}
}};
/** Bölüm sayfa slug'ının TÜİK satırı; yoksa null. */
export function bolumIstihdamBul(slug: string, duzey: IstihdamDuzeyi): BolumIstihdam | null {{
return BOLUM_ISTIHDAM[duzey][slug] ?? null;
}}
"""
(KOK / "src" / "lib" / "bolum-istihdam.ts").write_text(cikti, encoding="utf-8")
print("yazıldı: src/lib/bolum-istihdam.ts")