Files
kolaytercih/scripts/tuik-istihdam-uret.py
bilalgursen 6564ec8948 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>
2026-10-05 00:15:05 +03:00

169 lines
6.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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")