/** * 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 dört aşamalı: * * D) Kaynak veride DOLU ama YANLIŞ olan kurum alanları (KURUM_DUZELT) — * `programs` VE `programs_arsiv` üzerinde * 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. A/B/C yalnızca `programs` tablosunun NULL satırlarına dokunur; * D (KURUM_DUZELT) NULL koşulundan bağımsızdır — düzelttiği alanlar dolu ama * yanlış — ve `programs_arsiv`'i de tarar (varsa). * * 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"; import { tabloVar } from "./yokatlas-goc"; /** 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(); } /** KKTC il değerleri — tek kaynak; veri-kalite.ts de bunu okur. */ export 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", }; /** * Aşama D — kaynak veride DOLU ama YANLIŞ olan kurum alanları. * * MANUEL_UNITUR'dan farkı: burada değer NULL değil, yanlış. Bu yüzden bu * harita NULL koşulu OLMADAN çalışır. Her satırın kanıtı kurumun YÖK Atlas'ta * geçen parantezli ikinci yazımıdır (aynı kurum, doğru etiketli): * * "ULUSLARARASI BALKAN ÜNİVERSİTESİ (ÜSKÜP-MAKEDONYA)" → YURTDISI KAMU * "ULUSLARARASI SARAYBOSNA ÜNİVERSİTESİ (SARAYBOSNA-…)" → YURTDISI KAMU * "KIBRIS AMERİKAN ÜNİVERSİTESİ (KKTC-LEFKOŞA)" → ili KKTC * * Yanlış etiket ilk CSV ingest'inden (scripts/ingest.ts) geliyor; refresh.ts * `uniturSec` DB'deki değeri sadakatle kopyaladığı için her tazelemede * yeniden üretiliyordu. Düzeltme buraya yazılınca kalıcı olur. * * Etki: `src/lib/db.ts` devlet filtresi (`unitur = 'DEVLET'`) bu kurumların * programlarını sihirbaz havuzuna alıyordu; vakıf filtresi (`unitur <> * 'DEVLET'`) ise dışarıda bırakıyordu. * * `il` düzeltmesi yalnız YANLIŞ yazılmış değeri hedefler (yanlis → dogru); * boş il'ler kapsam dışı — eksik, eksik olarak görünmeli. * * Aynı yanlış etiket `programs_arsiv`'e de yazılmıştı (kapanan programlar oraya * `programs`'tan olduğu gibi taşınıyor). Bugün `src/` tarafında arşivi okuyan * SQL yok, ama tablo ileride okunursa sızıntı geri gelirdi; bu yüzden Aşama D * iki tabloyu da tarar. Dokunulan kolon yalnız `unitur` ve `il`. */ const KURUM_DUZELT: Record< string, { unitur?: string; il?: { yanlis: string; dogru: string } } > = { "KIBRIS AMERİKAN ÜNİVERSİTESİ": { unitur: "KKTC" }, "KIBRIS AMERİKAN ÜNİVERSİTESİ (KKTC-LEFKOŞA)": { unitur: "KKTC" }, "ULUSLARARASI BALKAN ÜNİVERSİTESİ": { unitur: "YURTDISI KAMU", // Kurum Üsküp'te; 26 satırda ili Saraybosna yazıyor. Doğru değerin yazımı // DB'deki diğer yurt dışı illerin biçimini izler (BİŞKEK-KIRGIZİSTAN…) // ve kurumun kendi parantezli adından alınmıştır. il: { yanlis: "SARAYBOSNA - BOSNA - HERSEK", dogru: "ÜSKÜP-MAKEDONYA" }, }, "ULUSLARARASI SARAYBOSNA ÜNİVERSİTESİ": { unitur: "YURTDISI KAMU" }, }; 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`); // D) Dolu ama yanlış kurum alanları. A'dan ÖNCE koşar: yanlış değer başka // satırlara kopyalanmadan düzelsin. NULL koşulu yok; değer zaten dolu. // `programs` ve (varsa) `programs_arsiv` aynı haritadan geçer — iki tablo // ayrışırsa arşiv ileride okunduğunda aynı sızıntı geri gelir. const dTablolar = tabloVar(db, "programs_arsiv") ? ["programs", "programs_arsiv"] : ["programs"]; for (const tablo of dTablolar) { const dUnitur = db.prepare( `UPDATE ${tablo} SET unitur = ? WHERE universite = ? AND unitur IS NOT ?` ); const dIl = db.prepare( `UPDATE ${tablo} SET il = ? WHERE universite = ? AND il = ?` ); let dUniturToplam = 0; let dIlToplam = 0; for (const [ad, duzelt] of Object.entries(KURUM_DUZELT)) { if (duzelt.unitur) { dUniturToplam += dUnitur.run(duzelt.unitur, ad, duzelt.unitur).changes; } if (duzelt.il) { dIlToplam += dIl.run(duzelt.il.dogru, ad, duzelt.il.yanlis).changes; } } console.log( `Aşama D (yanlış kurum alanı) ${tablo}: unitur ${dUniturToplam} satır, il ${dIlToplam} satır` ); } // 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)"); for (const tablo of tabloVar(db, "programs_arsiv") ? ["programs", "programs_arsiv"] : ["programs"]) { const dagilim = db .prepare( `SELECT COALESCE(unitur,'(NULL)') AS t, COUNT(*) AS n FROM ${tablo} GROUP BY unitur ORDER BY n DESC` ) .all() as { t: string; n: number }[]; console.log( `Dağılım (${tablo}):`, dagilim.map((d) => `${d.t}=${d.n}`).join(" ") ); } db.close(); }