3 kurum (Kıbrıs Amerikan, Uluslararası Balkan, Uluslararası Saraybosna)
kaynak CSV'den `unitur='DEVLET'` geliyordu. `db.ts:79-82` filtreleri
yüzünden "devlet üniversitesi istiyorum" diyen öğrencinin aday havuzuna
27 KKTC/yurt dışı programı sızıyor, "vakıf" diyende aynı 27'si eksik
kalıyordu. Kullanıcının fark edemeyeceği bir doğruluk hatasıydı.
unitur-onar.ts — Aşama D (KURUM_DUZELT):
A/B/C yalnız `unitur IS NULL` satırlara dokunuyordu; bu satırlar NULL
değil, DOLU ve YANLIŞ. D koşulsuz çalışır ve Aşama A'dan ÖNCE koşar ki
yanlış değer başka satırlara kopyalanmasın. İdempotent (2. koşu: 0/0).
Her düzeltmenin kanıtı kurumun YÖK Atlas'taki kendi parantezli ikinci
yazımı: "(ÜSKÜP-MAKEDONYA)" → YURTDISI KAMU, "(KKTC-LEFKOŞA)" → KKTC.
refresh.ts:519 `uniturSec` DB'deki değeri kopyaladığı için düzeltme
buraya yazılınca her tazelemede kalıcı olur.
veri-kalite.ts — iki yeni ZORUNLU kontrol (dosyanın sonuna):
13. "baz ad başına tek unitur" — kök neden buydu: unitur-onar.ts'in
eski çelişki koruması GROUP BY universite (TAM ad) ile çalıştığı
için aynı kurumun iki yazımı arasındaki DEVLET↔YURTDISI KAMU
çelişkisini hiç görmüyordu.
14. "DEVLET etiketi Türkiye ili ile destekli" — 13'ün kör noktası:
Kıbrıs Amerikan'ın İKİ yazımı da yanlış etiketli olduğu için
ortada çelişki yok. 13 tek başına 100 satırın yalnız 58'ini
koruyordu. Şartnamenin ötesinde, gerekçesi raporun §4'ünde.
Doğrulama: düzeltme öncesi DB'de iki kontrol de KALDI (çıkış kodu 1);
sonrasında 0 ZORUNLU kaldı, 3 uyarı (üçü de önceden vardı). Tek satır
bilerek bozulduğunda ikisi de yakaladı. tsc + eslint temiz.
Veri (126 satır) ayrı commit'te. Rapor: docs/gece-vardiyasi/2026-09-22/
09-p2-unitur.md
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
222 lines
8.6 KiB
TypeScript
222 lines
8.6 KiB
TypeScript
/**
|
||
* 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)
|
||
* 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 NULL satırlara dokunur; D (KURUM_DUZELT)
|
||
* NULL koşulundan bağımsızdır — düzelttiği alanlar dolu ama yanlış.
|
||
*
|
||
* 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();
|
||
}
|
||
|
||
/** 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<string, string> = {
|
||
"İ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<string, string> = {
|
||
"İ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.
|
||
*/
|
||
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.
|
||
const dUnitur = db.prepare(
|
||
"UPDATE programs SET unitur = ? WHERE universite = ? AND unitur IS NOT ?"
|
||
);
|
||
const dIl = db.prepare(
|
||
"UPDATE programs 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ı): 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)");
|
||
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();
|
||
}
|