From 1e810e178210a9f3bafd3cabc4702b18dead7acc Mon Sep 17 00:00:00 2001 From: bilalgursen Date: Sun, 9 Aug 2026 00:11:53 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20unitur=20NULL=20backfill=20scripti=20+?= =?UTF-8?q?=20refresh=20ingest=20onar=C4=B1m=C4=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit YÖK Atlas API'sinin universiteTuru döndürmediği ~2.081 satır (Nişantaşı, Beykent gibi vakıflar + KKTC kampüsleri) devlet/vakıf filtrelerinden ve sihirbaz sayaçlarından sessizce düşüyordu. unitur-onar üç aşamada doldurur: aynı üniversitenin dolu satırından kopya → KKTC ili kuralı → manuel eşleme (Cerrahpaşa, MSB/İçişleri SBÜ, Alanya, Manas, Yesevi, Az. Pedagoji). "ÜNİVERSİTESİ- CERRAHPAŞA" tarzı tire etrafı boşluklu duplikat adlar da normalize edilir; refresh.ts hem insert yolunda adı normalize eder hem her tazeleme sonunda onarımı idempotent olarak koşar. Co-Authored-By: Claude Fable 5 --- scripts/refresh.ts | 10 ++- scripts/unitur-onar.ts | 158 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 scripts/unitur-onar.ts diff --git a/scripts/refresh.ts b/scripts/refresh.ts index d6b2ee0..ec9cbc0 100644 --- a/scripts/refresh.ts +++ b/scripts/refresh.ts @@ -12,6 +12,7 @@ */ import path from "node:path"; import Database from "better-sqlite3"; +import { uniAdiNormalize, uniturOnar } from "./unitur-onar"; const API = "https://yokatlas.yok.gov.tr/api/tercih-kilavuz/search"; const PAGE_SIZE = 500; @@ -169,7 +170,9 @@ async function main() { insert.run( id, r.birimAdi, - r.universiteAdi, + // "ÜNİVERSİTESİ- CERRAHPAŞA" gibi tire etrafı boşluklu varyantlar + // duplikat kurum adı üretmesin + uniAdiNormalize(r.universiteAdi), r.universiteTuru ?? null, r.ilAdi ?? null, r.fymkAdi ?? null, @@ -205,6 +208,11 @@ async function main() { console.log( `DB toplam: ${stats.toplam} program, 2025 sıralaması olan: ${stats.sirali}` ); + + // API'nin universiteTuru döndürmediği kayıtlar devlet/vakıf filtrelerinden + // düşmesin: her tazelemeden sonra unitur onarımı koşulur (idempotent). + db.transaction(() => uniturOnar(db))(); + db.close(); } diff --git a/scripts/unitur-onar.ts b/scripts/unitur-onar.ts new file mode 100644 index 0000000..f192d52 --- /dev/null +++ b/scripts/unitur-onar.ts @@ -0,0 +1,158 @@ +/** + * unitur (üniversite tipi) NULL onarımı + üniversite adı normalizasyonu. + * + * YÖK Atlas API'si bazı kayıtlarda universiteTuru döndürmüyor; DB'de ~2.081 + * satır unitur=NULL kalıyor ve bunlar hem devlet/vakıf filtrelerinden hem + * sihirbaz sayaçlarından sessizce düşüyor. Onarım üç aşamalı: + * + * A) Aynı üniversitenin dolu satırından kopyala (Nişantaşı, Beykent…) + * B) Kalanlarda il KKTC yerleşimiyse unitur='KKTC' + * C) Küçük manuel eşleme (Cerrahpaşa, MSB/İçişleri, Alanya, Manas…) + * + * Ayrıca "İSTANBUL ÜNİVERSİTESİ- CERRAHPAŞA" gibi tire etrafı boşluklu + * duplikat adlar normalize edilir; refresh.ts insert yolunda da aynı + * normalizasyon uygulanır ki duplikat bir daha oluşmasın. + * + * İdempotenttir; yalnızca NULL satırlara dokunur. + * + * Kullanım: npx tsx scripts/unitur-onar.ts (dev sunucu KAPALIYKEN — + * uygulama DB'yi readonly açar ama WAL checkpoint için tek yazar olmalı) + */ +import path from "node:path"; +import Database from "better-sqlite3"; + +/** Tire etrafı boşluk + çift boşluk temizliği (üniversite adı için). */ +export function uniAdiNormalize(ad: string): string { + return ad.replace(/\s*-\s*/g, "-").replace(/\s{2,}/g, " ").trim(); +} + +const KKTC_ILLERI = [ + "GİRNE", + "LEFKOŞA", + "GAZİMAĞUSA", + "LEFKE", + "GÜZELYURT", + "KIBRIS", +]; + +// Hiçbir satırında unitur olmayan ve il'den de çıkarılamayan kurumlar. +const MANUEL_UNITUR: Record = { + "İSTANBUL ÜNİVERSİTESİ-CERRAHPAŞA": "DEVLET", + "İÇİŞLERİ BAKANLIĞI VE MİLLİ SAVUNMA BAKANLIĞI ADINA SAĞLIK BİLİMLERİ ÜNİVERSİTESİNDE EĞİTİM ALACAKLAR": + "DEVLET", + "ALANYA ÜNİVERSİTESİ": "VAKIF", + "KIRGIZİSTAN-TÜRKİYE MANAS ÜNİVERSİTESİ": "YURTDISI KAMU", + "HOCA AHMET YESEVİ ULUSLARARASI TÜRK-KAZAK ÜNİVERSİTESİ": "YURTDISI KAMU", + "AZERBAYCAN DEVLET PEDAGOJİ ÜNİVERSİTESİ": "YURTDISI KAMU", +}; + +// İl'i hiç olmayan kampüsler için ad → il eşlemesi (yalnızca kesin olanlar). +const MANUEL_IL: Record = { + "İSTANBUL ÜNİVERSİTESİ-CERRAHPAŞA": "İSTANBUL", +}; + +export function uniturOnar(db: Database.Database): void { + // 0) Üniversite adı normalizasyonu (Cerrahpaşa duplikatını birleştirir) + const adlar = db + .prepare("SELECT DISTINCT universite FROM programs") + .all() as { universite: string }[]; + const adDuzelt = db.prepare( + "UPDATE programs SET universite = ? WHERE universite = ?" + ); + let duzeltilen = 0; + for (const { universite } of adlar) { + const temiz = uniAdiNormalize(universite); + if (temiz !== universite) { + adDuzelt.run(temiz, universite); + duzeltilen++; + } + } + if (duzeltilen > 0) console.log(`Ad normalizasyonu: ${duzeltilen} kurum`); + + // A) Aynı üniversitenin dolu satırından kopyala. Birden fazla FARKLI dolu + // değer varsa sessizce seçim yapma — kaynak veride tutarsızlık demektir. + const celiskiler = db + .prepare( + `SELECT universite, COUNT(DISTINCT unitur) AS n FROM programs + WHERE unitur IS NOT NULL GROUP BY universite HAVING n > 1` + ) + .all() as { universite: string; n: number }[]; + if (celiskiler.length > 0) { + throw new Error( + `Aynı üniversitede birden fazla unitur: ${celiskiler + .map((c) => c.universite) + .join(", ")} — önce kaynak veriyi kontrol et.` + ); + } + const aKopya = db + .prepare( + `UPDATE programs SET unitur = ( + SELECT p2.unitur FROM programs p2 + WHERE p2.universite = programs.universite AND p2.unitur IS NOT NULL + LIMIT 1 + ) + WHERE unitur IS NULL AND EXISTS ( + SELECT 1 FROM programs p3 + WHERE p3.universite = programs.universite AND p3.unitur IS NOT NULL + )` + ) + .run(); + console.log(`Aşama A (üniversiteden kopya): ${aKopya.changes} satır`); + + // B) Kalan NULL'larda il KKTC yerleşimiyse + const bKktc = db + .prepare( + `UPDATE programs SET unitur = 'KKTC' + WHERE unitur IS NULL AND il IN (${KKTC_ILLERI.map(() => "?").join(",")})` + ) + .run(...KKTC_ILLERI); + console.log(`Aşama B (KKTC ili): ${bKktc.changes} satır`); + + // C) Manuel eşlemeler + const cUnitur = db.prepare( + "UPDATE programs SET unitur = ? WHERE unitur IS NULL AND universite = ?" + ); + let cToplam = 0; + for (const [ad, tur] of Object.entries(MANUEL_UNITUR)) { + cToplam += cUnitur.run(tur, ad).changes; + } + console.log(`Aşama C (manuel): ${cToplam} satır`); + + const ilDuzelt = db.prepare( + "UPDATE programs SET il = ? WHERE il IS NULL AND universite = ?" + ); + let ilToplam = 0; + for (const [ad, il] of Object.entries(MANUEL_IL)) { + ilToplam += ilDuzelt.run(il, ad).changes; + } + if (ilToplam > 0) console.log(`İl doldurma (manuel): ${ilToplam} satır`); + + // Rapor: kalan NULL'lar + const kalan = db + .prepare( + `SELECT universite, COUNT(*) AS adet FROM programs + WHERE unitur IS NULL GROUP BY universite ORDER BY adet DESC` + ) + .all() as { universite: string; adet: number }[]; + if (kalan.length === 0) { + console.log("Kalan NULL unitur: 0 ✓"); + } else { + console.log(`Kalan NULL unitur (${kalan.length} kurum):`); + for (const k of kalan) console.log(` ${k.universite}: ${k.adet}`); + } +} + +// Doğrudan koşulduğunda (npx tsx scripts/unitur-onar.ts) +if (process.argv[1]?.endsWith("unitur-onar.ts")) { + const db = new Database(path.join(process.cwd(), "data", "yokatlas.db")); + db.pragma("journal_mode = WAL"); + db.transaction(() => uniturOnar(db))(); + db.pragma("wal_checkpoint(TRUNCATE)"); + const dagilim = db + .prepare( + "SELECT COALESCE(unitur,'(NULL)') AS t, COUNT(*) AS n FROM programs GROUP BY unitur ORDER BY n DESC" + ) + .all() as { t: string; n: number }[]; + console.log("Dağılım:", dagilim.map((d) => `${d.t}=${d.n}`).join(" ")); + db.close(); +}