Merge branch 'gece/0922-unitur-kacagi' into is/0923-birlesik

# Conflicts:
#	scripts/veri-kalite.ts
This commit is contained in:
bilalgursen
2026-09-23 11:34:07 +03:00
4 changed files with 289 additions and 9 deletions

View File

@@ -3,8 +3,10 @@
*
* 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ı:
* 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…)
@@ -13,20 +15,24 @@
* 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.
* İ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();
}
const KKTC_ILLERI = [
/** KKTC il değerleri — tek kaynak; veri-kalite.ts de bunu okur. */
export const KKTC_ILLERI = [
"GİRNE",
"LEFKOŞA",
"GAZİMAĞUSA",
@@ -51,6 +57,49 @@ 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.
*
* 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
@@ -69,6 +118,35 @@ export function uniturOnar(db: Database.Database): void {
}
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
@@ -148,11 +226,18 @@ if (process.argv[1]?.endsWith("unitur-onar.ts")) {
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(" "));
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();
}

View File

@@ -21,6 +21,7 @@ import { EFEKTIF_SIRA_SQL, ILK_YIL } from "../src/lib/veri-yillari";
import { EFEKTIF_INDEKS, efektifSorguPlani } from "./db-index";
import { kimlikFarklari, onarimKayitlari, yilKolonlari, type DbKimlik } from "./kimlik";
import { hamOku, sonHamDosya, TUR_MAP } from "./refresh";
import { KKTC_ILLERI } from "./unitur-onar";
import {
argOku,
bayrakVar,
@@ -295,6 +296,53 @@ async function main() {
console.log(` şimdi, tüm satırlar (yeni dahil) : ${toplamlar(simdi)}`);
}
// 13. Aynı kurumun iki yazımı arasında unitur çelişkisi — BAZ AD üzerinden.
// unitur-onar.ts'in eski çelişki koruması GROUP BY universite (TAM ad) ile
// çalıştığı için "X ÜNİVERSİTESİ" ve "X ÜNİVERSİTESİ (ÜSKÜP-MAKEDONYA)" iki
// ayrı kurum sayılıyor, aralarındaki DEVLET↔YURTDISI KAMU çelişkisi hiç
// görünmüyordu. Baz ad = sondaki parantezli ek atılmış hâl.
const BAZ_AD =
"TRIM(CASE WHEN instr(universite, '(') > 0 THEN substr(universite, 1, instr(universite, '(') - 1) ELSE universite END)";
// 13 ve 14 arşivi de tarar: kapanan programlar `programs_arsiv`'e olduğu gibi
// taşındığı için yanlış etiket orada da duruyordu (BACKLOG #21). Bugün `src/`
// tarafında arşivi okuyan SQL yok, ama iki tablo ayrışırsa denetim bunu
// görmez ve tablo ileride okunduğunda sızıntı geri gelir.
const KURUMLAR = tabloVar(db, "programs_arsiv")
? `(SELECT universite, unitur, il FROM programs
UNION ALL SELECT universite, unitur, il FROM programs_arsiv)`
: "programs";
const bazAdCeliskileri = `(SELECT ${BAZ_AD} AS bazAd, GROUP_CONCAT(DISTINCT unitur) AS turler, COUNT(*) AS adet,
MAX(unitur = 'DEVLET') <> MIN(unitur = 'DEVLET') AS devletBolunmus
FROM ${KURUMLAR} WHERE unitur IS NOT NULL GROUP BY 1 HAVING COUNT(DISTINCT unitur) > 1)`;
const bolunmus = ornek(`SELECT bazAd, turler, adet FROM ${bazAdCeliskileri} WHERE devletBolunmus`);
zorunlu(
"baz ad başına tek unitur",
bolunmus === "",
bolunmus === ""
? "aynı kurumun iki yazımı devlet/vakıf filtresini bölmüyor"
: `devlet filtresine giren, vakıf filtresinden düşen kurum: ${bolunmus} → scripts/unitur-onar.ts KURUM_DUZELT`
);
const esdegerFark = ornek(`SELECT bazAd, turler, adet FROM ${bazAdCeliskileri} WHERE NOT devletBolunmus`);
bilgi("baz ad unitur farkı (devlet/vakıf ayrımını bölmeyen)", false, esdegerFark || "yok");
// 14. DEVLET etiketli ama hiçbir programı Türkiye ilinde olmayan kurum.
// 13'ün kör noktası: kurumun İKİ yazımı da yanlış etiketliyse çelişki yoktur.
// Türkiye illerinde tire geçmez; yurt dışı iller tireli (BİŞKEK-KIRGIZİSTAN…),
// KKTC illeri KKTC_ILLERI'nde. İli hiç olmayan kurumlar (MSB/İçişleri) hariç.
const yurtDisiDevlet = ornek(
`SELECT universite, GROUP_CONCAT(DISTINCT il), COUNT(*) FROM ${KURUMLAR}
WHERE unitur = 'DEVLET' GROUP BY universite
HAVING SUM(il IS NOT NULL) > 0
AND SUM(il NOT LIKE '%-%' AND il NOT IN (${KKTC_ILLERI.map(() => "?").join(",")})) = 0`,
...KKTC_ILLERI
);
zorunlu(
"DEVLET etiketi Türkiye ili ile destekli",
yurtDisiDevlet === "",
yurtDisiDevlet === ""
? "DEVLET etiketli her kurumun en az bir programı Türkiye ilinde"
: `tüm programları KKTC/yurt dışında olduğu hâlde DEVLET: ${yurtDisiDevlet}`
);
urapKontrolleri(db, zorunlu, bilgi);
db.close();