---
title: 'Veri Toplama ve Hazırlama'
source: 'https://academia.sh/tr/kurslar/veri-hazirlama'
language: tr
updated: '2026-08-17T04:40:06+00:00'
license: 'CC BY-SA 4.0'
lessons: 17
outcomes:
    - 'Farklı kaynaklardan programlı olarak veri toplar ve her adımda giren, çıkan ve sessizce düşen satırı sayar'
    - 'Kirli veriyi belgelenmiş adımlarla temizler ve her adımın satır hesabını yazar'
    - 'Veriyi çözümlemeye uygun biçime dönüştürür ve hazırlamayı yeniden üretilebilir kılar'
---

# Veri Toplama ve Hazırlama

Kaynaktan çözümlemeye giden yolda her adımın satır sayısını nasıl değiştirdiğinin kalem kalem yazılması: ayrıştırma kararının sessizce düşürdüğü satır, sorgunun döndürdüğü satırın çözümleme birimiyle uyuşmaması, sayfalamayı yanlış tüketmenin atladığı kayıt ve hız sınırına uyan sıranın belirlediği toplama süresi, yapı değişince kırılan seçici, örnekleme kararının temsile etkisi, eksikliğin orantısız elediği grup, kimlik eşleştirmede yanlış birleştirilen kayıt, hatasız görünen sessiz tip dönüşümü, aykırı değer yöntemlerinin örtüşmeyen kümeleri, kategori birleştirme ve metin normalleştirmenin kurtardığı eşleşme, birleştirmenin çoğalttığı satır, yeniden şekillendirmenin kayıpsızlığı, toplulaştırmada kaybolan grup içi değişkenlik, tanımsız kalan türetme, ölçeğe aykırı kategorik kodlama ve adımların betikle yeniden üretilebilirliği.

## Veri Toplama

Verinin çözümleme ortamına girerken kaç satır kaybettiğinin sayılması: aynı dosyanın iki ayrıştırma kararıyla okunması ve tırnaklı alan, satır sonu ve kodlama yüzünden sessizce düşen satır, sorgunun döndürdüğü satırın çözümleme birimiyle uyuşmaması ve birleştirmenin satırı çoğaltması, sayfalamayı yanlış tüketmenin atladığı kayıt ile hız sınırına uyan sıranın belirlediği toplama süresi, sayfa yapısı değiştiğinde kırılan seçici sayısı ve erişim izninin sınırı, üç örnekleme kararının aynı soruya verdiği üç cevap.

### Dosya Biçimleri

Source: https://academia.sh/tr/kurslar/veri-hazirlama/dosya-bicimleri

Önceki kursun 2.037 satırlık sayfasını üreten dışa aktarım dosyası iki ayrıştırma kararıyla okunuyor ve iki farklı küme çıkıyor: tırnak içini koruyan okuma 2.037 kaydın hepsini çıkarır, naif bölme 2.076 fiziksel satırdan 1.934 kayıt çıkarır ve 142 satırı hiçbir uyarı vermeden düşürür — 78'i adresindeki satır sonundan bölünmüş yarım satır, 64'ü alanı kayan satır. Kalan 1.934 kaydın 33'ü hayatta kalır ama okuma alanında adresten kaymış bir kapı numarası taşır; toplam 984,2 m³ ayrışır ve 1.157 kaydın bölge adı, dosya yanlış kodlamayla çözüldüğü için listeyle eşleşmez. Aynı 2.037 kayıt sütunlu yapıda tutulduğunda toplam okumayı öğrenmek 108.219 bayt yerine 4.074 bayt okumak demektir.

Önceki kurs on bir derste otuz iki karar aldı ve yalnız yedisi sayfadan geri okunabildi. Ama o
sayfanın kendisi bir yerden geldi: 2.037 satır, 1.400 abone, 42.645,6 m³. Bu ders o sayfanın
öncesine bakar ve tek bir soruyu sorar — dosya çözümleme ortamına girerken kaç satır kaybetti.

Cevap kararlara bağlıdır ve kararlar hata vermez. Bir ayrıştırıcı bozuk bir satırla karşılaştığında
durup uyarmaz; o satırı sessizce atlar ve okumaya devam eder. Dersin ölçü birimi budur ve kurs
boyunca sürer: her adımda **giren satır**, **çıkan satır** ve **nereye gitti**. Üçüncüsünün en
önemli kalemi **sessiz kayıp**, yani hiçbir uyarı üretmeden düşen satırdır.

- **VT1.** Dosya kurgudur. Gerçek bir dışa aktarım yerine aynı yapıyı üreten bir üreteç yazıldı;
  tohum görünürdür ve çıktı koşumdan koşuma aynıdır. **Modeldir.**
- **VT2.** Üretilen küme önceki kursun sayfasını verir: 2.037 kayıt, 1.400 farklı abone, 1.877
  farklı abone-dönem çifti, 42.645,6 m³ toplam.
- **VT3.** Ayraç noktalı virgül, tırnak çift tırnaktır; adres alanı her kayıtta tırnak içinde
  yazılır ve dosya UTF-8 yazılır.
- **VT4.** İki toplamın aynı sayılması için eşik 0,05 m³'tür, çünkü okumalar bir ondalıkla
  raporlanıyor ve altındaki bir fark yalnızca yuvarlamadan gelebilir.
- **VT5.** Üretecin verebileceği en büyük okuma 37,0 m³'tür; okuma alanında bundan büyük bir değer
  o alanın okuma taşımadığını gösterir.

#### Dosyanın İçinde Ne Var

```js
// kaynak.mjs — bolgesel olcum agindan gelen kurgu bir disa aktarim. MODELDIR:
// gercek bir dosya yerine ayni yapiyi ureten bir uretec yazildi; tohum gorunur
// ve cikti kosumdan kosuma aynidir. Kayitlar M26/K02'nin sayfasini uretir:
// 1400 abone, 1877 abone-donem cifti, 160 yinelenen satir, toplam 2037 kayit.
export const TOHUM = 1246, AYRAC = ";", TIRNAK = '"';
export const BOLGE = ["kuzey", "doğu", "merkez", "güney", "batı"];
export const BASLIK = ["abone_no", "sayac_no", "donem", "adres", "okuma_m3", "bolge"];
const SOKAK = ["Cinar Sok", "Yeni Mah Bahce Cad", "Istasyon Cad", "Dere Sok", "Zeytin Sok"];

export const uretec = (t) => { let s = t >>> 0;
  return () => { s = (Math.imul(s, 1103515245) + 12345) >>> 0; return s / 4294967296; }; };

// Kayitlar: once 1400 abonenin ilk donemi, sonra 477 abonenin ikinci donemi,
// sonra ilk 160 satirin yinelenmesi (32'si birebir kopya, 128'i duzeltilmis okuma).
export function kayitlar() {
  const r = uretec(TOHUM), oku = () => 5 + Math.floor(r() * 321) / 10, k = [];
  const yap = (i, donem, okuma) => ({ abone: 10000 + i, sayac: `S-${4000 + i}`,
    donem, adres: `${SOKAK[i % 5]} No ${12 + (i % 88)}`, okuma, bolge: BOLGE[i % 5] });
  for (let i = 0; i < 1400; i++) k.push(yap(i, "2024-01", oku()));
  for (let i = 0; i < 477; i++) k.push(yap(i, "2024-02", oku()));
  for (let i = 0; i < 160; i++) k.push(yap(i, "2024-01", i < 32 ? k[i].okuma : oku()));
  return k;
}

// Kirlilik kurali: her 21. kaydin adresi ayrac icerir (ucte biri ayractan sonra
// ciplak bir kapi numarasiyla biter), geri kalanlardan 50'de biri satir sonu
// icerir. Adres alani her kayitta tirnak icinde yazilir.
export const AYRACLI = (i) => i % 21 === 0;
export const SATIRLI = (i) => !AYRACLI(i) && i % 50 === 13;
export const SAYIYLA = (i) => AYRACLI(i) && (i / 21) % 3 === 0;

export function dosya() {
  const k = kayitlar(), s = [BASLIK.join(AYRAC)];
  k.forEach((x, i) => {
    let adres = x.adres;
    if (AYRACLI(i)) adres += SAYIYLA(i) ? `${AYRAC} ${45 + (i % 30)}` : `${AYRAC} Daire ${1 + i % 8}`;
    else if (SATIRLI(i)) adres += `\nKat ${1 + (i % 6)}`;
    s.push([x.abone, x.sayac, x.donem, TIRNAK + adres + TIRNAK, x.okuma.toFixed(1), x.bolge]
      .join(AYRAC));
  });
  return Buffer.from(s.join("\n") + "\n", "utf8");   // dosya UTF-8 yazilir
}

if (import.meta.url === `file://${process.argv[1]}`) {
  const k = kayitlar(), b = dosya(), satir = b.toString("utf8").trimEnd().split("\n");
  const say = (f) => k.reduce((s, _, i) => s + (f(i) ? 1 : 0), 0);
  console.log(`tohum ${TOHUM}; mantiksal kayit ${k.length}, farkli abone ` +
    `${new Set(k.map((x) => x.abone)).size}, farkli abone-donem ` +
    `${new Set(k.map((x) => x.abone + x.donem)).size}`);
  console.log(`dosya ${b.length} bayt, ${satir.length} fiziksel satir (1 baslik + ` +
    `${satir.length - 1} veri satiri)`);
  console.log(`adresinde ayrac olan kayit ${say(AYRACLI)} (${say(SAYIYLA)} tanesi ` +
    `ayractan sonra sayiyla bitiyor), adresinde satir sonu olan kayit ${say(SATIRLI)}`);
  console.log(`dogru okunmus toplam ${k.reduce((s, x) => s + x.okuma, 0).toFixed(1)} m3`);
  const oz = (ad) => Buffer.from(ad, "utf8").toString("hex").match(/../g).join(" ");
  for (const ad of ["kuzey", "güney"])
    console.log(`bolge "${ad}" UTF-8 baytlari: ${oz(ad)} (${Buffer.byteLength(ad)} bayt, ` +
      `${ad.length} karakter)`);
  console.log(satir[0]); console.log(satir[1]); console.log(satir[14]);
}
```

```
tohum 1246; mantiksal kayit 2037, farkli abone 1400, farkli abone-donem 1877
dosya 108219 bayt, 2077 fiziksel satir (1 baslik + 2076 veri satiri)
adresinde ayrac olan kayit 97 (33 tanesi ayractan sonra sayiyla bitiyor), adresinde satir sonu olan kayit 39
dogru okunmus toplam 42645.6 m3
bolge "kuzey" UTF-8 baytlari: 6b 75 7a 65 79 (5 bayt, 5 karakter)
bolge "güney" UTF-8 baytlari: 67 c3 bc 6e 65 79 (6 bayt, 5 karakter)
abone_no;sayac_no;donem;adres;okuma_m3;bolge
10000;S-4000;2024-01;"Cinar Sok No 12; 45";9.4;kuzey
10013;S-4013;2024-01;"Dere Sok No 25
```

İlk çelişki son üç satırda duruyor. Dosyada **2.076 veri satırı** var ama **2.037 mantıksal
kayıt**; aradaki 39 fark adresinde satır sonu taşıyan 39 kayıttan geliyor. Bir kaydın kaç satır
kapladığı sabit değildir; dosyayı satır sayarak ölçmek yanlış bir sayı verir.

İkinci çelişki ilk veri satırındadır. Adres alanı `"Cinar Sok No 12; 45"` yazıyor: alanın içinde
ayracın kendisi geçiyor. Ayraç, alanları ayırmak için seçilmiş bir karakterdir ve veride de
geçebilir; tırnak tam bu yüzden vardır. Tırnağı okumayan bir ayrıştırıcı burada altı yerine yedi
alan görür ve okuma alanı olarak adresin kuyruğunu okur.

Üçüncüsü bayt satırlarındadır. `kuzey` beş karakter ve beş bayttır, `güney` beş karakter ama altı
bayt. Dosyanın hangi kodlamayla yazıldığı dosyanın içinde yazmaz; okuyan taraf bir karar verir ve
karar yanlışsa hiçbir hata çıkmaz, yalnız harfler değişir.

#### Giren, Çıkan, Sessizce Düşen

```js
// oku.mjs — ayni dosya iki ayristirma karariyla okunur. K1 satiri satir sonundan,
// alani ayractan boler, tirnaklari siler ve dosyayi tek baytlik bir kodlamayla
// cozer. K2 tirnak icini korur, dosyayi yazildigi kodlamayla cozer. Ikisi de
// hata vermez; fark yalnizca sayilarda gorunur.
import { dosya, BASLIK, BOLGE, AYRAC, TIRNAK, kayitlar } from "./kaynak.mjs";
const b = dosya(), OK = BASLIK.indexOf("okuma_m3"), BL = BASLIK.indexOf("bolge");

const ayikla = (satirlar) => {
  const cikan = [], dusen = [];
  for (const alan of satirlar) {
    const okuma = Number(alan[OK]);
    if (alan[OK] !== "" && alan[OK] !== undefined && Number.isFinite(okuma))
      cikan.push({ okuma, bolge: alan[BL] });
    else dusen.push(alan.length);
  }
  return { giren: satirlar.length, cikan, dusen };
};

// K1: naif okuyucu. Tirnak diye bir sey bilmez; alan basina tirnaklari siler.
const k1 = (bayt) => ayikla(bayt.toString("latin1").trimEnd().split("\n").slice(1)
  .map((s) => s.split(AYRAC).map((a) => a.replaceAll(TIRNAK, ""))));

// K2: tirnak icini koruyan durum makinesi. Tirnak icinde ayrac da satir sonu da
// alanin parcasidir; yan yana iki tirnak tek tirnak demektir.
function k2(bayt) {
  const m = bayt.toString("utf8"), kayit = []; let alan = [""], ic = false;
  for (let i = 0; i < m.length; i++) {
    const c = m[i];
    if (ic) { if (c === TIRNAK && m[i + 1] === TIRNAK) { alan[alan.length - 1] += c; i++; }
      else if (c === TIRNAK) ic = false; else alan[alan.length - 1] += c; }
    else if (c === TIRNAK) ic = true;
    else if (c === AYRAC) alan.push("");
    else if (c === "\n") { kayit.push(alan); alan = [""]; }
    else alan[alan.length - 1] += c;
  }
  if (alan.length > 1 || alan[0] !== "") kayit.push(alan);
  return ayikla(kayit.slice(1));
}

// Iki toplamin ayni sayilmasi icin esik 0.05 m3'tur: okumalar bir ondalikla
// raporlaniyor, altindaki fark yalnizca yuvarlamadan gelebilir.
const ESIK = 0.05, ENBUYUK = 37.0;   // uretecin verebilecegi en buyuk okuma
const hedef = kayitlar().reduce((s, x) => s + x.okuma, 0);
const y = (w) => (h) => h.map((v, j) => (j ? String(v).padStart(w[j])
  : String(v).padEnd(w[0]))).join("");
const y1 = y([22, 8, 8, 8, 12, 11, 10]);
console.log(y1(["ayristirma karari", "giren", "cikan", "dusen", "toplam m3",
  "eslesmeyen", "kapi no"]));
const sonuc = {};
for (const [ad, f] of [["K1 naif bolme", k1], ["K2 tirnagi koruyan", k2]]) {
  const r = sonuc[ad] = f(b), t = r.cikan.reduce((s, x) => s + x.okuma, 0);
  console.log(y1([ad, r.giren, r.cikan.length, r.dusen.length, t.toFixed(1),
    r.cikan.filter((x) => !BOLGE.includes(x.bolge)).length,
    r.cikan.filter((x) => x.okuma > ENBUYUK).length]));
}
const r1 = sonuc["K1 naif bolme"], t1 = r1.cikan.reduce((s, x) => s + x.okuma, 0);
const yarim = r1.dusen.filter((n) => n < 6).length, kayik = r1.dusen.filter((n) => n > 6).length;
const kapi = r1.cikan.filter((x) => x.okuma > ENBUYUK);
console.log(`\ndosyada ${r1.giren} veri satiri var ama ${kayitlar().length} mantiksal kayit: ` +
  `adresinde satir sonu tasiyan 39 kayit ${r1.giren - 2037} fazladan satir uretti`);
console.log(`K1 ${r1.dusen.length} satiri hicbir uyari vermeden dusurdu: ${yarim} bolunmus ` +
  `yarim satir, ${kayik} alani kaymis satir`);
console.log(`K1'in cikardigi ${r1.cikan.length} kaydin ${kapi.length} tanesi hayatta kaldi ` +
  `ama okuma alaninda adresten kayan bir kapi numarasi tasiyor (${kapi[0].okuma} m3 gibi)`);
console.log(`iki toplam ${Math.abs(hedef - t1).toFixed(1)} m3 ayrisiyor, esik ${ESIK} m3'un ` +
  `${Math.round(Math.abs(hedef - t1) / ESIK)} kati; oran yuzde ` +
  `${(100 * Math.abs(hedef - t1) / hedef).toFixed(1)}`);
console.log(`K1'in ${r1.cikan.filter((x) => !BOLGE.includes(x.bolge)).length} kaydinda bolge ` +
  `adi listeyle eslesmiyor; bolge kirilimi bu kayitlari sessizce disarida birakir`);
```

```
ayristirma karari        giren   cikan   dusen   toplam m3 eslesmeyen   kapi no
K1 naif bolme             2076    1934     142     41661.4       1157        33
K2 tirnagi koruyan        2037    2037       0     42645.6          0         0

dosyada 2076 veri satiri var ama 2037 mantiksal kayit: adresinde satir sonu tasiyan 39 kayit 39 fazladan satir uretti
K1 142 satiri hicbir uyari vermeden dusurdu: 78 bolunmus yarim satir, 64 alani kaymis satir
K1'in cikardigi 1934 kaydin 33 tanesi hayatta kaldi ama okuma alaninda adresten kayan bir kapi numarasi tasiyor (45 m3 gibi)
iki toplam 984.2 m3 ayrisiyor, esik 0.05 m3'un 19684 kati; oran yuzde 2.3
K1'in 1157 kaydinda bolge adi listeyle eslesmiyor; bolge kirilimi bu kayitlari sessizce disarida birakir
```

Tablonun iki satırı aynı dosyayı okuyor ve iki farklı küme veriyor. Tırnak içini koruyan okuma
2.037 satırın hepsini çıkarır, hiçbirini düşürmez ve toplamı 42.645,6 m³ bulur. Naif bölme 2.076
satırdan 1.934 kayıt çıkarır: **142 satır kayboldu ve hiçbiri hata vermedi**. İkisi de sıfır çıkış
koduyla bitti.

Kaybın dökümü iki kalemdir. **78 satır bölünmüş yarımdır**: adresinde satır sonu taşıyan 39 kayıt
dosyada iki satıra yayılmıştı, naif okuyucu ikisini de ayrı satır sandı, ikisinde de okuma alanı
eksik kaldı ve ikisini de attı. Bir kayıt kaybolmak için iki satır harcadı. **64 satır alanı
kaymış satırdır**: adresinde ayraç taşıyan 97 kaydın 64'ünde okuma alanına adres kuyruğu geldi,
sayıya dönüşmedi ve satır düştü. Mantıksal kayıt cinsinden hesap kapanır: 2.037 kayıttan 103'ü
kümeden tamamen çıktı.

Kalan 33 kayıt dersin asıl bulgusudur. Adresinde ayraç taşıyan kayıtların üçte birinde ayraçtan
sonra çıplak bir kapı numarası vardı; kayan alan sayıya dönüştü ve kayıt **hayatta kaldı**. Okuma
alanında artık 45 m³ gibi bir değer duruyor ve VT5'e göre bu değer okuma olamaz. Bu 33 kayıt hiçbir
sayımda eksik görünmez, yalnız toplamı şişirir. Düşen 103 kayıt toplamı azaltır, şişen 33 kayıt
artırır; net fark 984,2 m³, eşiğin 19.684 katı ve toplamın yüzde 2,3'ü olur.

Son satır dördüncü bir kanalı ölçüyor. Dosya UTF-8 yazıldı, naif okuyucu tek baytlık bir kodlamayla
çözdü ve çok baytlı harf taşıyan bölge adları başka dizgelere dönüştü. 1.934 kaydın **1.157'sinde**
bölge adı listeyle eşleşmiyor; bölge kırılımı alan bir özet bu kayıtları sessizce dışarıda bırakır
ve kırılımın toplamı genel toplamı tutmaz. Kodlama kararı tek satır düşürmedi, yalnız kayıtların
yarıdan fazlasını sınıflandırılamaz hale getirdi.

#### Ayraç, Belge, Sütun

```js
// bicim.mjs — ayni 2037 kayit uc yapida saklanir. MODELDIR: bicimlerin adlari
// degil yapilari onemli. Sutunlu model: okuma 2 baytlik tamsayi (desi-m3),
// abone 4 baytlik tamsayi, donem ve bolge birer bayt kod, metin uzunluk onekli.
import { dosya, kayitlar, BOLGE } from "./kaynak.mjs";
const k = kayitlar(), belge = Buffer.from(JSON.stringify(k), "utf8");
const metin = (a) => Buffer.concat(a.map((s) => Buffer.from(s, "utf8"))
  .flatMap((x) => [Buffer.from([x.length]), x]));
const SUTUN = {
  abone: Buffer.from(new Int32Array(k.map((x) => x.abone)).buffer),
  okuma: Buffer.from(new Uint16Array(k.map((x) => Math.round(x.okuma * 10))).buffer),
  donem: Buffer.from(k.map((x) => (x.donem === "2024-01" ? 0 : 1))),
  bolge: Buffer.from(k.map((x) => BOLGE.indexOf(x.bolge))),
  sayac: metin(k.map((x) => x.sayac)), adres: metin(k.map((x) => x.adres)),
};
const toplam = () => { let t = 0;
  for (let i = 0; i < k.length; i++) t += SUTUN.okuma.readUInt16LE(i * 2) / 10;
  return t.toFixed(1); };
const y1 = (h) => h.map((v, j) => (j ? String(v).padStart([0, 13, 15, 12][j])
  : String(v).padEnd(22))).join("");
console.log(y1(["yapi", "toplam bayt", "soru icin bayt", "toplam m3"]));
for (const [ad, tam, soru] of [["ayracli metin", dosya().length, dosya().length],
  ["ic ice belge", belge.length, belge.length],
  ["sutunlu", Object.values(SUTUN).reduce((s, x) => s + x.length, 0), SUTUN.okuma.length]])
  console.log(y1([ad, tam, soru, toplam()]));
console.log(`toplam okumayi ogrenmek sutunlu yapida ${SUTUN.okuma.length} bayt, ayracli ` +
  `metinde ${dosya().length} bayt okumak demek: ${(dosya().length / SUTUN.okuma.length)
    .toFixed(0)} kat`);
```

```
yapi                    toplam bayt soru icin bayt   toplam m3
ayracli metin                108219         108219     42645.6
ic ice belge                 220921         220921     42645.6
sutunlu                       68040           4074     42645.6
toplam okumayi ogrenmek sutunlu yapida 4074 bayt, ayracli metinde 108219 bayt okumak demek: 27 kat
```

Üç yapı aynı 2.037 kaydı taşıyor ve aynı toplamı veriyor. Fark, kaydın sınırının nerede
yazıldığındadır. Ayraçlı metinde sınır bir karakterdir ve o karakter veride de geçebilir; 142
satırlık kayıp bunun bedelidir. İç içe belge yapısında alan adı her kayıtta tekrarlandığı için dosya
iki kat büyür, ama kaydın nerede bittiği metnin yapısından okunur. Sütunlu yapıda her sütun ayrı bir
bayt dizisidir ve tipi ilan edilmiştir; okuma alanına metin kayması yapısal olarak mümkün değildir.

Son satır seçimin ikinci yüzünü ölçüyor. Toplam okuma sorusu sütunlu yapıda 4.074 bayt, ayraçlı
metinde 108.219 bayt okumak ister — **27 kat**. Sütunlu yapı bunu ucuza yapar, çünkü adres sütununu
hiç açmaz; buna karşılık tek bir kaydın bütün alanlarını görmek altı ayrı diziye gitmek demektir.
Biçim seçimi bir kolaylık tercihi değil, hangi işin ucuza yapılacağının kararıdır.

#### Özet

- Dosyada 2.076 veri satırı, kümede 2.037 mantıksal kayıt vardır; satır saymak kayıt saymak değildir.
- Tırnak içini koruyan okuma 2.037 kaydın hepsini çıkarır ve toplamı 42.645,6 m³ bulur; naif bölme
  1.934 kayıt çıkarır ve 142 satırı hiçbir uyarı vermeden düşürür.
- Düşenin dökümü 78 bölünmüş yarım satır ile 64 alanı kaymış satırdır; 2.037 kayıttan 103'ü kümeden
  çıkmış, kayan alanların 33'ü sayıya dönüşüp okuma alanında kapı numarasıyla hayatta kalmıştır. Net
  fark 984,2 m³, eşiğin 19.684 katıdır.
- Yanlış kodlama tek satır düşürmez ama 1.157 kaydın bölge adını listeyle eşleşmez hale getirir.
- Aynı küme sütunlu yapıda 68.040 bayt tutar ve toplam okuma sorusu 4.074 bayt okunarak cevaplanır;
  ayraçlı metinde aynı soru 108.219 bayt ister.

#### Sonraki Adım

Bu ders veriyi bir dosyadan aldı ve satırlarını saydı. Ama verinin çoğu bir veritabanında durur ve
oradan çıkarken dosyanın taşımadığı bir soru getirir: sorgu satır döndürür, fakat o satırın **neyin
bir kaydı** olduğu sorgunun yazımına bağlıdır. Sonraki ders
sorgunun döndürdüğü satırın çözümlemenin sorduğu birimle uyuşup uyuşmadığını ölçer ve iki tablonun
birleştirilmesinin satır sayısını nasıl çoğalttığını sayar.

### Veritabanından Veri Çekme

Source: https://academia.sh/tr/kurslar/veri-hazirlama/veritabanindan-veri-cekme

Aynı depodan üç sorgu biçimi üç farklı satır kümesi döndürüyor ve hiçbiri hata vermiyor: okuma ile abone birleştirmesi 1.877 satır verir, ziyaret tablosu iç birleştirmeyle eklendiğinde 766 satır sessizce düşer ve 669 satır çoğalır — girişin yüzde 76 kadarı bozulur ama net satır kaybı yalnızca 97, toplam sapması yüzde 7,0 olduğu için sayıya bakan biri hasarı göremez. Sol birleştirme hiçbir satır düşürmez ama kümeyi 2.546 satıra çıkarır ve toplamı 1,34 katına taşır. Aynı soru üç kararla üç sayı verir: okuma satırı başına 20,61 m³, abone başına 27,64 m³, ziyaret satırına yayılmış kümede 20,21 m³.

Bir önceki ders veriyi dosyadan aldı ve ayrıştırma kararının 142 satırı sessizce düşürdüğünü
gösterdi. Dosyanın bir kolaylığı vardı: bir satır bir kayıttı, en azından tırnak doğru okunduğunda.
Veritabanı bu kolaylığı vermez. Bir sorgu satır döndürür ve o satırın **neyin bir kaydı** olduğu
tabloların yapısından değil, sorgunun yazımından çıkar.

Bu dersin ölçüsü budur: çözümlemenin sorduğu birim ile sorgunun döndürdüğü satırın birimi aynı mı.
İlişkisel model, anahtar ve sorgu yazımı önkoşuldur ve burada tekrarlanmaz; ölçülen tek şey
**satırın birimi** ve o birim kaydığında satır sayısının nereye gittiğidir.

- **VT6.** Depo kurgudur ve `node` dizileriyle modellenir. Birleştirme küçük bir yardımcıyla
  yapılır; sorgu sözdizimi ve iyileştirme bu kursun konusu değildir. **Modeldir.**
- **VT7.** Üç tablo vardır: 1.400 satırlık abone, abone-dönem başına bir satır tutan 1.877 satırlık
  okuma ve abone başına sıfır ile üç arasında satır tutan 1.328 satırlık saha ziyareti.
- **VT8.** Çözümleme birimi açıkça yazılır: sorulan soru abone başına ortalama tüketimdir, yani
  raporun satırı bir abonedir.
- **VT9.** İki ortalamanın aynı sayılması için eşik 0,05 m³'tür, çünkü okumalar bir ondalıkla
  tutuluyor ve bunun altındaki bir fark yalnızca yuvarlamadan gelebilir.

#### Deponun Üç Tablosu

```js
// depo.mjs — bolgesel olcum agindan kurgu bir iliskisel depo. MODELDIR: tablolar
// node dizileridir, birlestirme kucuk bir yardimciyla yapilir. Sorgu sozdizimi bu
// kursun konusu degil; olculen sey donen satirin hangi birimin kaydi oldugudur.
export const TOHUM = 4711;
export const BOLGE = ["kuzey", "doğu", "merkez", "güney", "batı"];
const uretec = (t) => { let s = t >>> 0;
  return () => { s = (Math.imul(s, 1103515245) + 12345) >>> 0; return s / 4294967296; }; };

const r = uretec(TOHUM);
// abone: cozumleme biriminin kendisi. Her abone bir kez gecer.
export const ABONE = Array.from({ length: 1400 }, (_, i) => ({
  abone_no: 10000 + i, bolge: BOLGE[i % 5], tur: i % 7 === 0 ? "ticari" : "mesken" }));
// okuma: abone-donem cifti basina bir satir. 1400 abonenin ilki, 477 abonenin ikinci donemi.
export const OKUMA = [];
for (let i = 0; i < 1400; i++)
  OKUMA.push({ abone_no: 10000 + i, donem: "2024-01", okuma_m3: 5 + Math.floor(r() * 321) / 10 });
for (let i = 0; i < 477; i++)
  OKUMA.push({ abone_no: 10000 + i, donem: "2024-02", okuma_m3: 5 + Math.floor(r() * 321) / 10 });
// ziyaret: saha ziyaretleri. Bir abonenin sifir, bir, iki ya da uc ziyareti olabilir.
export const ZIYARET = [];
for (const a of ABONE) {
  const p = r(), n = p < 0.41 ? 0 : p < 0.71 ? 1 : p < 0.91 ? 2 : 3;
  for (let j = 0; j < n; j++)
    ZIYARET.push({ abone_no: a.abone_no, sira: j + 1, tur: j === 0 ? "okuma" : "kontrol" });
}

// Ic birlestirme: sol tablonun her satiri, sag tabloda anahtari eslesen her satirla
// bir kez eslesip cikar. Eslesme yoksa sol satir hic cikmaz.
export const ic = (sol, sag, anahtar) => {
  const dizin = new Map();
  for (const s of sag) { const k = s[anahtar];
    if (!dizin.has(k)) dizin.set(k, []); dizin.get(k).push(s); }
  return sol.flatMap((x) => (dizin.get(x[anahtar]) ?? []).map((y) => ({ ...x, ...y })));
};
// Sol birlestirme: eslesme yoksa sol satir bir kez, sag alanlari bos olarak cikar.
export const solB = (sol, sag, anahtar) => {
  const dizin = new Map();
  for (const s of sag) { const k = s[anahtar];
    if (!dizin.has(k)) dizin.set(k, []); dizin.get(k).push(s); }
  return sol.flatMap((x) => { const e = dizin.get(x[anahtar]);
    return e ? e.map((y) => ({ ...x, ...y })) : [{ ...x }]; });
};

if (import.meta.url === `file://${process.argv[1]}`) {
  const dagilim = new Map();
  for (const a of ABONE) {
    const n = ZIYARET.filter((z) => z.abone_no === a.abone_no).length;
    dagilim.set(n, (dagilim.get(n) ?? 0) + 1);
  }
  console.log(`tohum ${TOHUM}; abone ${ABONE.length} satir, okuma ${OKUMA.length} satir, ` +
    `ziyaret ${ZIYARET.length} satir`);
  console.log(`okuma tablosunda farkli abone ${new Set(OKUMA.map((x) => x.abone_no)).size}, ` +
    `farkli abone-donem ${new Set(OKUMA.map((x) => x.abone_no + x.donem)).size}`);
  console.log("abone basina ziyaret sayisi dagilimi:");
  for (const n of [...dagilim.keys()].sort())
    console.log(`  ${n} ziyaret: ${dagilim.get(n)} abone`);
  console.log(`okuma toplami ${OKUMA.reduce((s, x) => s + x.okuma_m3, 0).toFixed(1)} m3`);
}
```

```
tohum 4711; abone 1400 satir, okuma 1877 satir, ziyaret 1328 satir
okuma tablosunda farkli abone 1400, farkli abone-donem 1877
abone basina ziyaret sayisi dagilimi:
  0 ziyaret: 570 abone
  1 ziyaret: 453 abone
  2 ziyaret: 256 abone
  3 ziyaret: 121 abone
okuma toplami 38690.7 m3
```

Üç tablo üç farklı birimde tutuluyor. Abone tablosunun satırı bir abonedir ve abone numarası orada
tekildir. Okuma tablosunun satırı bir abone-dönem çiftidir; 1.400 abonenin 477'sinin ikinci dönem
okuması da girildiği için tablo 1.877 satırdır ve abone numarası orada **tekil değildir**. Ziyaret
tablosunun satırı bir saha ziyaretidir ve bir abone bu tabloda hiç geçmeyebileceği gibi üç kez de
geçebilir.

Dağılım tablosu asıl riski gösteriyor. 1.400 abonenin **570'inin hiç ziyareti yok**, 121'inin üç
ziyareti var. Bir birleştirme bu iki uçtan birincisini siler, ikincisini üçe katlar. İki etki de
sorgunun döndürdüğü satır sayısında görünür ve hiçbiri bir hata iletisi üretmez.

#### Satırın Birimi Kayınca

```js
// sorgu.mjs — uc sorgu bicimi ayni depodan uc farkli satir kumesi dondurur.
// Olculen sey satirin dogru olup olmadigi degil, satirin HANGI BIRIMIN kaydi
// oldugudur: cozumleme abone-donem soruyorsa, sonuc kumesinde her abone-donem
// cifti tam bir kez gecmelidir.
import { ABONE, OKUMA, ZIYARET, ic, solB } from "./depo.mjs";

const S1 = ic(OKUMA, ABONE, "abone_no");            // okuma + abone
const S2 = ic(S1, ZIYARET, "abone_no");             // + ziyaret, ic birlestirme
const S3 = solB(S1, ZIYARET, "abone_no");           // + ziyaret, sol birlestirme

// Bir sonuc kumesinde cozumleme birimi (abone-donem) kac kez geciyor.
const birim = (kume) => {
  const say = new Map();
  for (const s of kume) { const k = s.abone_no + s.donem; say.set(k, (say.get(k) ?? 0) + 1); }
  return { enCok: Math.max(...say.values()),
    dusen: OKUMA.length - say.size, cogalan: kume.length - say.size };
};
const toplam = (kume) => kume.reduce((t, s) => t + s.okuma_m3, 0);
const y = (w) => (h) => h.map((v, j) => (j ? String(v).padStart(w[j])
  : String(v).padEnd(w[0]))).join("");
const y1 = y([24, 8, 8, 8, 9, 8, 12, 8]);
console.log(y1(["sorgu", "giren", "cikan", "dusen", "cogalan", "en cok", "toplam m3", "oran"]));
for (const [ad, k] of [["S1 okuma + abone", S1], ["S2 + ziyaret (ic)", S2],
  ["S3 + ziyaret (sol)", S3]]) {
  const b = birim(k);
  console.log(y1([ad, OKUMA.length, k.length, b.dusen, b.cogalan, b.enCok,
    toplam(k).toFixed(1), (toplam(k) / toplam(S1)).toFixed(2)]));
}
const ziyaretli = new Set(ZIYARET.map((z) => z.abone_no));
const sifir = ABONE.filter((a) => !ziyaretli.has(a.abone_no));
console.log(`\nziyareti olmayan ${sifir.length} abonenin ` +
  `${OKUMA.filter((o) => !ziyaretli.has(o.abone_no)).length} okuma satiri S2'de hic gorunmuyor; ` +
  `sorgu hata vermedi, satir sayisi tek isarettir`);
const b2 = birim(S2), etkilenen = b2.dusen + b2.cogalan;
console.log(`S2'de ${b2.dusen} satir dustu ve ${b2.cogalan} satir cogaldi: ${etkilenen} satir, ` +
  `yani girisin yuzde ${(100 * etkilenen / OKUMA.length).toFixed(0)} kadari etkilendi. Ama iki ` +
  `hata ` +
  `ters yonde calistigi icin satir sayisi 1877'den 1780'e, yalnizca ${OKUMA.length - S2.length} ` +
  `satir dustu ve toplam yuzde ${(100 * (1 - toplam(S2) / toplam(S1))).toFixed(1)} sapti`);

// Ayni soru, uc karar: "abone basina ortalama donemlik tuketim".
const abonToplam = new Map();
for (const s of S1) abonToplam.set(s.abone_no, (abonToplam.get(s.abone_no) ?? 0) + s.okuma_m3);
const kA = toplam(S1) / S1.length;
const kB = [...abonToplam.values()].reduce((a, b) => a + b, 0) / abonToplam.size;
const kC = toplam(S2) / S2.length;
const y2 = y([34, 14, 10]);
console.log(`\n"abone basina ortalama tuketim" sorusuna uc karar`);
console.log(y2(["karar", "satirin birimi", "m3"]));
console.log(y2(["A okuma satiri basina ortalama", "abone-donem", kA.toFixed(2)]));
console.log(y2(["B once abone basina toplanir", "abone", kB.toFixed(2)]));
console.log(y2(["C S2 satiri basina ortalama", "abone-ziyaret", kC.toFixed(2)]));
// Iki ortalamanin ayni sayilmasi icin esik 0.05 m3'tur: okumalar bir ondalikla
// tutuluyor, altindaki fark yalnizca yuvarlamadan gelebilir.
const ESIK = 0.05;
console.log(`A ile B arasindaki fark ${(kB - kA).toFixed(2)} m3, esik ${ESIK}'in ` +
  `${Math.round((kB - kA) / ESIK)} kati; ikisi de dogru hesaptir ama ayni soruya cevap degildir`);
console.log(`C ise cok ziyaret alan abonelerin okumasini agirliklandiran bir ortalamadir ve ` +
  `A'dan yalnizca ${Math.abs(kC - kA).toFixed(2)} m3 ayrildigi icin fark edilmez`);
```

```
sorgu                      giren   cikan   dusen  cogalan  en cok   toplam m3    oran
S1 okuma + abone            1877    1877       0        0       1     38690.7    1.00
S2 + ziyaret (ic)           1877    1780     766      669       3     35967.4    0.93
S3 + ziyaret (sol)          1877    2546       0      669       3     51971.8    1.34

ziyareti olmayan 570 abonenin 766 okuma satiri S2'de hic gorunmuyor; sorgu hata vermedi, satir sayisi tek isarettir
S2'de 766 satir dustu ve 669 satir cogaldi: 1435 satir, yani girisin yuzde 76 kadari etkilendi. Ama iki hata ters yonde calistigi icin satir sayisi 1877'den 1780'e, yalnizca 97 satir dustu ve toplam yuzde 7.0 sapti

"abone basina ortalama tuketim" sorusuna uc karar
karar                             satirin birimi        m3
A okuma satiri basina ortalama       abone-donem     20.61
B once abone basina toplanir               abone     27.64
C S2 satiri basina ortalama        abone-ziyaret     20.21
A ile B arasindaki fark 7.02 m3, esik 0.05'in 140 kati; ikisi de dogru hesaptir ama ayni soruya cevap degildir
C ise cok ziyaret alan abonelerin okumasini agirliklandiran bir ortalamadir ve A'dan yalnizca 0.41 m3 ayrildigi icin fark edilmez
```

S1 doğru kümedir. Okuma tablosu abone tablosuyla birleştiğinde abone numarası sağ tarafta tekil
olduğu için her okuma satırı tam bir kez çıkar: giren 1.877, çıkan 1.877, düşen sıfır, çoğalan
sıfır. Bir birleştirmenin satır sayısını değiştirmemesinin tek koşulu budur — anahtarın karşı
tarafta tekil olması.

S2 aynı kümeye ziyaret tablosunu ekliyor ve iki şey aynı anda oluyor. Ziyareti olmayan 570 abonenin
**766 okuma satırı** sonuçta hiç görünmüyor, çünkü iç birleştirme eşleşmeyen sol satırı düşürür. Aynı
anda birden çok ziyareti olan abonelerin okumaları çoğalıyor: **669 fazladan satır** ve bir
abone-dönem çifti sonuç kümesinde en çok üç kez geçiyor. Birim artık abone-dönem değil,
abone-ziyaret çiftidir.

Bu dersin asıl bulgusu iki sayının birbirini gizlemesidir. 1.877 satırın 1.435'i, yani **girişin
yüzde 76 kadarı** ya düştü ya çoğaldı. Ama iki hata ters yönde çalıştığı için sonuç kümesi 1.780
satır oldu: net kayıp **97 satır**, toplamdaki sapma **yüzde 7,0**. Satır sayısına ya da toplama
bakan biri kümenin dörtte üçünün bozulduğunu göremez. Yalnızca düşen ve çoğalan ayrı ayrı sayıldığında
hesap açılır.

S3 iç birleştirme yerine sol birleştirme kullanıyor ve düşen satır sorununu çözüyor: ziyareti olmayan
aboneler kümede kalıyor, düşen sıfır. Çoğalma ise duruyor — küme 2.546 satıra çıkıyor ve toplam
1,34 katına yükseliyor. Sol birleştirme kaybı önler, çoğalmayı önlemez; ikisi ayrı kararlardır ve
ayrı sayılır.

Koddaki `birim` işlevi bu üç satırı üreten testtir ve her sorgudan sonra koşturulabilir. Test tek
bir soru sorar: çözümleme biriminin anahtarı sonuç kümesinde kaç kez geçiyor. Anahtar başına sayı
her satırda bir ise birim korunmuştur. Sayı birden büyükse çoğalma vardır ve o anahtarın değerleri
toplamda birden çok kez ağırlık taşır. Girişte olup sonuçta hiç geçmeyen anahtar varsa sessiz kayıp
vardır. Bu üçü tek bir taramada çıkar ve satır sayısını okumaktan farklıdır: 1.780 sayısı tek başına
S2 hakkında hiçbir şey söylemez, `dusen` ile `cogalan` yan yana yazıldığında ise kümenin ne olduğu
tam olarak bilinir. Kursun kuralı burada da geçerlidir — giren ile çıkan arasındaki fark kalem kalem
yazılamıyorsa veri hazırlanmamış, yalnız değiştirilmiştir.

#### Aynı Soru, Üç Birim

Alt tablo çözümleme birimi sorusunu tek başına ölçüyor. Soru sabittir: abone başına ortalama
tüketim. VT8'e göre raporun satırı bir abonedir, yani doğru cevap B'dir — **27,64 m³**. Karar A
okuma satırları üzerinde ortalama alır ve **20,61 m³** verir; bu sayı yanlış hesaplanmış değildir,
yalnızca başka bir sorunun cevabıdır (dönem başına ortalama okuma). İki okuması olan aboneler A'da
iki kez, B'de bir kez sayılır ve fark 7,02 m³, yani eşiğin 140 katıdır.

Karar C ise en tehlikelisidir. S2'nin satırları üzerinde alınan ortalama **20,21 m³** verir ve A'dan
yalnızca 0,41 m³ ayrılır. Sayı makul görünür, çünkü değerlerin kendisi bozulmamıştır; bozulan tek
şey her okumanın kaç kez sayıldığıdır. Çok ziyaret alan abonelerin okuması üç kez, ziyareti olmayan
abonelerinki sıfır kez ağırlık taşır. Bu ortalama hiçbir sorunun cevabı değildir ve A'ya yakınlığı
onu bulunmaz kılar.

Bu üç sayının ortak dersi şudur: birleştirmeden sonra hangi birimin kaydına baktığı yazılmayan bir
sorgu, kaç satır döndürdüğünü söylese bile ne döndürdüğünü söylememiştir.

#### Özet

- Bir sorgunun döndürdüğü satırın birimi tabloların yapısından değil sorgunun yazımından çıkar;
  okuma ile abone birleştirmesi 1.877 satır verir ve birimi abone-dönem olarak korur.
- Ziyaret tablosunun iç birleştirmeyle eklenmesi 766 satırı sessizce düşürür ve 669 satırı çoğaltır;
  birim abone-ziyaret çiftine kayar.
- İki hata ters yönde çalıştığı için net satır kaybı yalnızca 97, toplam sapması yüzde 7,0 olur;
  girişin yüzde 76 kadarı bozulmuş olmasına rağmen sayıya bakan biri hasarı göremez.
- Sol birleştirme düşen satırı sıfırlar ama çoğalmayı durdurmaz: küme 2.546 satıra çıkar, toplam
  1,34 katına yükselir.
- Aynı soru üç birimde üç sayı verir — abone başına 27,64 m³, okuma satırı başına 20,61 m³,
  ziyarete yayılmış kümede 20,21 m³; sonuncusu doğruya yakın olduğu için fark edilmez.

#### Sonraki Adım

Dosya da veritabanı da verinin tamamını bir kerede verdi: dosya okunduğunda kaç satır olduğu
belliydi, sorgu koştuğunda küme elde duruyordu. Bir arayüzden veri toplarken bu güvence yoktur.
Kayıtlar parça parça gelir, her parçanın sonrakine nasıl bağlandığını çağıran taraf takip eder ve
kaynak çağrı sıklığına bir sınır koyar. Sonraki ders sayfalamayı yanlış tüketmenin kaç kaydı
atladığını sayar ve hız sınırına uyan bir sıranın toplama süresini nasıl belirlediğini ölçer.

### Arayüzlerden Veri Toplama

Source: https://academia.sh/tr/kurslar/veri-hazirlama/arayuzlerden-veri-toplama

Aynı arayüzden üç toplama kararı üç farklı küme çıkarıyor ve üç yanıt da tamam dönüyor: dar kapsamlı belirteç 1.400 kayıt toplar ve geçmiş dönemin 477 satırını hiç istemeden dışarıda bırakır, konum tabanlı sayfalama tam 1.877 satır toplar ama 35 kaydı hiç almaz ve 35 kaydı iki kez alır, imleç tabanlı sayfalama 1.877 kaydın tamamını bir kez toplar. Konum tabanlı kararın genel toplamı 38.690,7 m³ ile birebir tutar çünkü atlanan ve yinelenen kayıtlar aynı okuma değerini taşır; hasar yalnız bölge kırılımında 70,5 m³ sapma olarak görünür. Hız sınırı altında sınır yanıtını boş sayfa sayan sıra 600 kayıtta durup 1.277 kaydı kaybeder; sınıra uyan sıra aynı kümeyi 38 istek ve 3 bekleme birimiyle eksiksiz toplar, sayfa boyu 200'e çıkarıldığında bekleme sıfıra iner.

Dosya da veritabanı da veriyi bir kerede verdi. Dosya okunduğunda kaç satır olduğu belliydi,
sorgu koştuğunda küme elde duruyordu. Bir arayüzden veri toplarken bu güvence yoktur: kayıtlar
parça parça gelir, her parçanın sonrakine nasıl bağlandığını çağıran taraf takip eder, kaynak
çağrı sıklığına bir sınır koyar ve hangi kayıtların görülebileceğini belirteç belirler.

Dersin ölçüsü şudur: kaynaktaki satırın kaçı toplanan kümeye girdi. Arayüz tasarımı, sayfalama ve
hız sınırı **sunan** taraf açısından önkoşuldur ve burada tekrarlanmaz; ölçülen tek şey tüketen
tarafın kaç kaydı aldığı, kaç kaydı iki kez aldığı ve kaç kaydı hiç istemediğidir. Teknik sınırdan
önce bir sınır daha vardır: bir kaynaktan veri toplamak, kaynağın kullanım koşulları ve açık bir
erişim izni olmadan teknik olarak mümkün olsa bile hukuki ve mesleki bir sorundur; burada izin
verilmiş bir toplama varsayılır.

- **VT10.** Arayüz kurgudur ve `node` ile modellenir. Ağ çağrısı yapılmaz: istek bir işlev
  çağrısı, yanıt bir nesnedir. **Modeldir.**
- **VT11.** Kaynaktaki küme önceki dersin deposundaki okuma tablosunun aynısıdır — aynı tohum,
  1.877 satır, 38.690,7 m³.
- **VT12.** Arayüz iki sıralama sunar. `okuma` sıralaması okuma değerine göre azalandır ve eşit
  değerlerin iç sırası her istekte yeniden kurulur; `kimlik` sıralaması abone-dönem anahtarına göre
  artandır ve tekildir. Sayfa boyu en çok 200'dür, fazlası sessizce kırpılır.
- **VT13.** Pencere kotası 12 istektir. Kota aşıldığında yanıt kayıt taşımaz ve durumu `sinir`
  olur. Süre gerçek saatle ölçülmez; kaynağın pencere sayısı **bekleme birimi** olarak sayılır.
- **VT14.** İki toplamın aynı sayılması için eşik 0,05 m³'tür, çünkü okumalar bir ondalıkla
  raporlanıyor ve altındaki bir fark yalnızca toplama sırasından gelebilir.

#### Arayüzün Verdiği Sayfa

```js
// arayuz.mjs — bolgesel olcum aginin kurgu okuma arayuzu. MODELDIR: ag cagrisi
// yapilmaz, "istek" bir islev cagrisi ve "yanit" bir nesnedir. Kayitlar onceki
// dersin deposundaki 1877 okuma satirinin aynisidir: ayni tohum, ayni sayilar.
export const TOHUM = 4711, BOY = 50, ENBUYUKBOY = 200, KOTA = 12;
const uretec = (t) => { let s = t >>> 0;
  return () => { s = (Math.imul(s, 1103515245) + 12345) >>> 0; return s / 4294967296; }; };

// Okuma desi-m3 tamsayisi olarak tutulur; siralama anahtari tamsayi oldugu
// icin kayan noktali degerler esitlik karsilastirmasina hic girmez.
const r = uretec(TOHUM);
export const KAYIT = [];
for (let i = 0; i < 1400; i++)
  KAYIT.push({ abone_no: 10000 + i, donem: "2024-01", desi: 50 + Math.floor(r() * 321) });
for (let i = 0; i < 477; i++)
  KAYIT.push({ abone_no: 10000 + i, donem: "2024-02", desi: 50 + Math.floor(r() * 321) });
export const BOLGE = ["kuzey", "doğu", "merkez", "güney", "batı"];
export const kimlik = (k) => `${k.abone_no}-${k.donem}`;
export const m3 = (k) => k.desi / 10;
export const bolgesi = (k) => BOLGE[(k.abone_no - 10000) % 5];

// "okuma": okumaya gore azalan, esit degerlerin ic sirasi her istekte yeniden
// kurulur. "kimlik": abone-donem anahtarina gore artan ve tekil.
function sirali(sira, istekNo) {
  if (sira === "kimlik") return [...KAYIT].sort((a, b) => (kimlik(a) < kimlik(b) ? -1 : 1));
  const g = uretec(TOHUM + istekNo * 7919);
  for (let i = 0; i < 5; i++) g();
  return KAYIT.map((k) => ({ k, p: g() })).sort((a, b) => b.k.desi - a.k.desi || a.p - b.p)
    .map((x) => x.k);
}

// Kota bir pencerede kabul edilen istek sayisidir; asildiginda yanit kayit
// tasimaz. Kapsam "gecmis" icermiyorsa kaynak yalnizca acik donemi verir.
export function sunucu({ kota = KOTA } = {}) {
  let istek = 0, pencerede = 0, bekleme = 0;
  return {
    sayim: () => ({ istek, bekleme }),
    bekle() { pencerede = 0; bekleme++; },
    iste({ kapsam = ["okuma"], sira = "kimlik", konum = 0, imlec = null, boy = BOY }) {
      istek++; pencerede++;
      if (pencerede > kota) return { durum: "sinir", kayitlar: [], sonraki: imlec };
      const b = Math.min(boy, ENBUYUKBOY);
      let kume = sirali(sira, istek);
      if (!kapsam.includes("gecmis")) kume = kume.filter((k) => k.donem === "2024-01");
      const bas = imlec === null ? konum : kume.findIndex((k) => kimlik(k) > imlec);
      const dilim = bas < 0 ? [] : kume.slice(bas, bas + b);
      return { durum: "tamam", toplam: kume.length,
        kayitlar: dilim.map((k) => ({ abone_no: k.abone_no, donem: k.donem,
          okuma_m3: m3(k), bolge: bolgesi(k) })),
        sonraki: dilim.length < b ? null : kimlik(dilim[dilim.length - 1]) };
    },
  };
}

if (import.meta.url === `file://${process.argv[1]}`) {
  const s = sunucu({ kota: Infinity }), TAM = ["okuma", "gecmis"];
  const oz = (y) => `durum ${y.durum}, toplam ${y.toplam}, sayfada ` +
    `${y.kayitlar.length} kayit, sonraki imlec ${y.sonraki}`;
  console.log(`tohum ${TOHUM}; ${KAYIT.length} okuma satiri, ` +
    `${KAYIT.reduce((t, k) => t + m3(k), 0).toFixed(1)} m3; boy ${BOY}, ust sinir ` +
    `${ENBUYUKBOY}, kota ${KOTA}`);
  console.log(`tam kapsam: ${oz(s.iste({ kapsam: TAM }))}`);
  console.log(`dar kapsam: ${oz(s.iste({ kapsam: ["okuma"] }))}`);
  const u = () => s.iste({ kapsam: TAM, sira: "okuma", konum: 49, boy: 3 })
    .kayitlar.map((x) => `${x.abone_no}/${x.okuma_m3}`).join("  ");
  console.log(`"okuma" sirasinda 50. kayittan uc tane, iki ayri istekte:\n  ${u()}\n  ${u()}`);
}
```

```
tohum 4711; 1877 okuma satiri, 38690.7 m3; boy 50, ust sinir 200, kota 12
tam kapsam: durum tamam, toplam 1877, sayfada 50 kayit, sonraki imlec 10024-2024-02
dar kapsam: durum tamam, toplam 1400, sayfada 50 kayit, sonraki imlec 10049-2024-01
"okuma" sirasinda 50. kayittan uc tane, iki ayri istekte:
  11319/36  10452/36  10828/36
  11319/36  10018/36  10230/36
```

İki belirteç arasındaki fark üçüncü satırdadır. Dar kapsamlı belirteç `tamam` durumu döndürüyor,
sayfa dolu geliyor, imleç ilerliyor ve hiçbir yerde bir yetki uyarısı yok. Tek işaret `toplam`
alanının 1.877 yerine 1.400 yazmasıdır ve bu sayının doğru olup olmadığı yanıtın içinden
anlaşılamaz. Kapsam hata üreten bir kısıt değildir; kaynağın verdiği kümeyi küçültür ve geri
kalanını hiç var olmamış gibi gösterir.

Son iki satır ikinci sorunu gösteriyor: aynı konumdan aynı üç kayıt iki kez istendi ve gelen
kayıtlar farklı. Sıralama anahtarı okuma değeridir ve o değer tekil değildir — 1.877 kayıt en çok
321 farklı okuma değerine dağılır, yani her değeri ortalama altı kayıt paylaşır. Eşit değerlerin
kendi aralarındaki sırası kaynağın kararına kalmıştır. Sabit görünen tek şey listedeki konumdur,
ama o konumda hangi kaydın durduğu sabit değildir.

#### Aynı Arayüz, Üç Toplama Kararı

```js
// topla.mjs — ayni arayuzden uc toplama karari uc farkli kume cikarir. Hicbir
// karar hata almaz, butun yanitlar "tamam" doner. Olculen sey donen kayit
// sayisi degil, kaynaktaki 1877 satirin kacinin toplanan kumeye girdigidir.
import { KAYIT, kimlik, m3, bolgesi, BOLGE, sunucu, BOY } from "./arayuz.mjs";
const TAM = ["okuma", "gecmis"];

// Imlec tabanli sayfalama: her sayfa son anahtardan sonrasini ister, sinir
// kaymaz. T1 bunu dar, T3 tam kapsamli belirtecle yapar.
function imlecli(kapsam) {
  const s = sunucu({ kota: Infinity }), toplanan = []; let imlec = null;
  do { const y = s.iste({ kapsam, sira: "kimlik", imlec, boy: BOY });
    toplanan.push(...y.kayitlar); imlec = y.sonraki; } while (imlec !== null);
  return { toplanan, istek: s.sayim().istek };
}
// T2. Konum tabanli sayfalama, "okuma" sirasinda: anahtar tekil olmadigi icin
// sayfa siniri her istekte baska bir kaydin uzerine duser.
function konumla() {
  const s = sunucu({ kota: Infinity }), toplanan = []; let konum = 0, toplam = Infinity;
  while (konum < toplam) {
    const y = s.iste({ kapsam: TAM, sira: "okuma", konum, boy: BOY });
    toplam = y.toplam; toplanan.push(...y.kayitlar); konum += BOY;
  }
  return { toplanan, istek: s.sayim().istek };
}

// Esik 0.05 m3: okumalar bir ondalikla tutuluyor, altindaki bir fark yalnizca
// toplama sirasindan gelebilir.
const ESIK = 0.05, HEDEF = KAYIT.reduce((t, k) => t + m3(k), 0);
const BHEDEF = new Map(BOLGE.map((b) => [b,
  KAYIT.filter((k) => bolgesi(k) === b).reduce((t, k) => t + m3(k), 0)]));
const sapma = (kume) => {                  // bolge kiriliminin en buyuk sapmasi
  const t = new Map(BOLGE.map((b) => [b, 0]));
  for (const x of kume) t.set(x.bolge, t.get(x.bolge) + x.okuma_m3);
  return Math.max(...BOLGE.map((b) => Math.abs(t.get(b) - BHEDEF.get(b))));
};
const y1 = (h) => h.map((v, j) => (j ? String(v).padStart([0, 7, 7, 7, 8, 9, 11, 11, 9][j])
  : String(v).padEnd(20))).join("");
console.log(y1(["toplama karari", "giren", "istek", "cikan", "farkli", "atlanan",
  "yinelenen", "toplam m3", "sapma m3"]));
const sonuc = {};
for (const [ad, f] of [["T1 dar kapsam", () => imlecli(["okuma"])],
  ["T2 konum tabanli", konumla], ["T3 imlec tabanli", () => imlecli(TAM)]]) {
  const r = sonuc[ad] = f(), farkli = new Set(r.toplanan.map(kimlik)).size;
  r.farkli = farkli; r.toplam = r.toplanan.reduce((t, x) => t + x.okuma_m3, 0);
  console.log(y1([ad, KAYIT.length, r.istek, r.toplanan.length, farkli,
    KAYIT.length - farkli, r.toplanan.length - farkli, r.toplam.toFixed(1),
    sapma(r.toplanan).toFixed(1)]));
}
const t2 = sonuc["T2 konum tabanli"];
console.log(`\nT2 kaynagin bildirdigi ${t2.toplanan.length} satiri tutturdu, ama ` +
  `${KAYIT.length - t2.farkli} kayit hic gelmedi ve ${t2.toplanan.length - t2.farkli} kayit ` +
  `iki kez geldi; atlanan ve yinelenen ayni okuma degerini tasidigi icin genel toplam ` +
  `hedeften ${Math.abs(t2.toplam - HEDEF).toFixed(3)} m3 ayrisiyor (esik ${ESIK}), bolge ` +
  `kiriliminda ise esigin ${Math.round(sapma(t2.toplanan) / ESIK)} kati sapiyor`);
```

```
toplama karari        giren  istek  cikan  farkli  atlanan  yinelenen  toplam m3 sapma m3
T1 dar kapsam          1877     29   1400    1400      477          0    28790.8   2016.2
T2 konum tabanli       1877     38   1877    1842       35         35    38690.7     70.5
T3 imlec tabanli       1877     38   1877    1877        0          0    38690.7      0.0

T2 kaynagin bildirdigi 1877 satiri tutturdu, ama 35 kayit hic gelmedi ve 35 kayit iki kez geldi; atlanan ve yinelenen ayni okuma degerini tasidigi icin genel toplam hedeften 0.000 m3 ayrisiyor (esik 0.05), bolge kiriliminda ise esigin 1410 kati sapiyor
```

Son sütun bölge kırılımındaki en büyük sapmadır.

T1 sayfalamayı doğru tüketiyor ve yine de kümenin dörtte birini kaçırıyor. Yirmi dokuz isteğin
hepsi başarılı döndü, imleç düzgün ilerledi ve döngü doğru yerde durdu. Kayıp tamamen kapsamdan
gelir: geçmiş dönemin **477 satırı** hiç istenmedi ve toplam 28.790,8 m³, yani gerçeğin 0,74 katı
oldu. Bir toplayıcının başarıyla bitmesi, hangi kümeyi görmeye yetkili olduğunu söylemez; kapsam
sorusu toplamadan **önce** sorulur.

T2 dersin asıl bulgusudur. Toplanan satır sayısı **1.877**, yani kaynağın bildirdiği sayıyla
birebir aynı; "beklenen kadar kayıt geldi mi" diye bakan bir denetim burada geçer. Oysa kayıt
kimlikleri sayılınca **35 kayıt hiç gelmemiş**, **35 kayıt iki kez gelmiştir**. Sayfa sınırı, eşit
okumaların iç sırası her istekte yeniden kurulduğu için her seferinde başka bir kaydın üzerine
düştü: biri önceki sayfanın sonuna geçip atlandı, bir başkası sonraki sayfanın başına düşüp
yeniden geldi.

Hatanın ölçüde neden görünmediği, sessiz kaybın en temiz örneğidir. Atlanan ve yinelenen kayıtlar
aynı eşit değer öbeğinden geldiği için **aynı okuma değerini taşır**: toplam 38.690,7 m³'te kalır
ve fark 0,05 m³'lük eşiğin altındadır. Hasar yalnızca kimliğe bağlı bir kırılımda ortaya çıkar —
bölge kırılımında en büyük sapma **70,5 m³**, eşiğin 1.410 katıdır.

T3 aynı 1.877 kaydı 38 istekte bir kez toplar; farkı yaratan tek şey imlecin **tekil ve kararlı**
bir anahtar üzerinde ilerlemesidir. Konum tabanlı sayfalama listenin istekler arasında sabit
kaldığını varsayar ve bu varsayım tekil olmayan bir sıralama anahtarında da, toplama sürerken
kayıt eklenen bir kaynakta da yanlıştır.

#### Hız Sınırına Uyan Sıra

```js
// sira.mjs — hiz siniri altinda toplama sirasi. Sinir asma teknigi yoktur:
// olculen sey sinira uyan siranin kac bekleme birimi harcadigi ve sinira
// uymayan siranin kac kaydi disarida biraktigidir. Sure gercek saatle olculmez;
// kaynagin pencere sayisi bir bekleme birimi olarak sayilir.
import { KAYIT, kimlik, sunucu, BOY, ENBUYUKBOY, KOTA } from "./arayuz.mjs";
const TAM = ["okuma", "gecmis"];

// H1: sinir yanitini bos sayfa sayan sira. Kayit gelmeyince "kume bitti" der.
function h1() {
  const s = sunucu(), toplanan = []; let imlec = null, y;
  do { y = s.iste({ kapsam: TAM, imlec, boy: BOY });
    toplanan.push(...y.kayitlar); imlec = y.sonraki;
  } while (y.kayitlar.length > 0 && imlec !== null);
  return { toplanan, ...s.sayim() };
}
// H2: sinira uyan sira. Pencere kotasini kendisi sayar ve dolunca bekler.
function h2(boy) {
  const s = sunucu(), toplanan = []; let imlec = null, pencerede = 0;
  do { if (pencerede === KOTA) { s.bekle(); pencerede = 0; }
    const y = s.iste({ kapsam: TAM, imlec, boy });
    pencerede++; toplanan.push(...y.kayitlar); imlec = y.sonraki;
  } while (imlec !== null);
  return { toplanan, ...s.sayim() };
}
const y1 = (h) => h.map((v, j) => (j ? String(v).padStart([0, 7, 7, 9, 8, 9][j])
  : String(v).padEnd(28))).join("");
console.log(y1(["sira", "giren", "istek", "bekleme", "cikan", "atlanan"]));
for (const [ad, f] of [["H1 sinir yanitini bos sayan", h1], [`H2 sinira uyan, boy ${BOY}`,
  () => h2(BOY)], ["H2 sinira uyan, boy 20", () => h2(20)],
  ["H2 sinira uyan, boy 100", () => h2(100)],
  [`H2 sinira uyan, boy ${ENBUYUKBOY}`, () => h2(ENBUYUKBOY)]]) {
  const r = f(), farkli = new Set(r.toplanan.map(kimlik)).size;
  console.log(y1([ad, KAYIT.length, r.istek, r.bekleme, r.toplanan.length,
    KAYIT.length - farkli]));
}
```

```
sira                          giren  istek  bekleme   cikan  atlanan
H1 sinir yanitini bos sayan    1877     13        0     600     1277
H2 sinira uyan, boy 50         1877     38        3    1877        0
H2 sinira uyan, boy 20         1877     94        7    1877        0
H2 sinira uyan, boy 100        1877     19        1    1877        0
H2 sinira uyan, boy 200        1877     10        0    1877        0
```

H1 hız sınırının en pahalı yanlış anlaşılmasıdır. On üçüncü istek kotayı aştığı için kayıt
taşımadan döndü, toplayıcı bunu "küme bitti" diye okudu ve süreç sıfır çıkış koduyla bitti:
**600 kayıt** toplandı, **1.277 kayıt** dışarıda kaldı. Boş sayfa ile reddedilmiş istek birbirine
benzer; ayırt edilmezse fark yalnızca satır sayısında görünür.

H2 aynı kümeyi eksiksiz topluyor ve bedeli **3 bekleme birimidir**. Son üç satır bu bedelin
nereden geldiğini gösteriyor: bekleme birimi, istek sayısının pencere kotasına bölümünden çıkar.
Sayfa boyu 20 iken 94 istek ve 7 bekleme birimi gerekir; boy 200 iken 10 istek yeter ve toplama
tek pencerede biter. Toplanan kayıt dört satırda da 1.877'dir. **Sayfa boyu bir ayrıntı değil,
doğrudan bir toplama süresi kararıdır** ve kaynağın üst sınırı bu sürenin alt sınırını belirler. VT12'ye göre bu üst sınır aşılmaya çalışıldığında bir hata da
gelmez: 400 kayıtlık sayfa isteyip konumunu 400 ilerleten bir sıra, kaynak 200 verdiği için her
turda 200 kaydı sessizce atlar. Gelen sayfadaki kayıt sayısı sayılır, istenen boy varsayılmaz.

#### Özet

- Bir arayüzden toplanan kümede doğrulanacak sayı, dönen kayıt sayısı değil, kaynaktaki satırların
  kaçının kümeye bir kez girdiğidir.
- Dar kapsamlı belirteç hiçbir uyarı üretmeden 477 satırı dışarıda bırakır ve toplam gerçeğin 0,74
  katına iner; kapsam sorusu toplamadan önce sorulur.
- Konum tabanlı sayfalama tekil olmayan bir anahtarda 35 kaydı atlar ve 35 kaydı iki kez toplar;
  satır sayısı ve genel toplam doğru görünür, hasar bölge kırılımında 70,5 m³ sapma olarak çıkar.
- Tekil ve kararlı bir anahtar üzerinde ilerleyen imleç aynı kümeyi 1.877 kayıt, sıfır atlanan ve
  sıfır yinelenen olarak toplar.
- Hız sınırı yanıtını boş sayfa sayan sıra 600 kayıtta durup 1.277 kaydı kaybeder; sınıra uyan sıra
  38 istek ve 3 bekleme birimi harcar ve sayfa boyu 200'e çıkarıldığında bekleme sıfıra iner.

#### Sonraki Adım

Bu dersin kaynağı verisini alan için tasarlanmış bir arayüzden verdi: alanların adı belliydi,
sayfa sınırı ilan edilmişti, kayıt sayısı yanıtın içinde yazıyordu. Verinin bir kısmı ise böyle
bir arayüzün arkasında değil, insan gözü için düzenlenmiş bir sayfanın yapısının içinde durur.
Orada alan adı yoktur; yalnızca konum ve görünüm işaretleri vardır ve bunlar bir sözleşme
değildir. Sonraki ders kurgu bir yayın sayfasından ayrıştırma yapar ve tek şeyi ölçer: yapı
değiştiğinde kaç seçici kırılır, kaç satır düşer, kaç satır yanlış alandan okunmuş olarak hayatta
kalır.

### Yapısal Ayrıştırma

Source: https://academia.sh/tr/kurslar/veri-hazirlama/yapisal-ayristirma

Aynı 1.877 satırı yayımlayan bir sayfa dört yapı sürümünde iki seçici setiyle okunuyor ve kırılan seçici sayısının hasarı ölçmediği çıkıyor: araya tek bir sarmalayıcı katman girince konumsal setin yol seçicisi kırılır ve 1.877 satırın tamamı düşer, ama kırılma gürültülüdür. Araya bir sütun eklendiğinde hiçbir seçici kırılmaz, 1.877 kayıt çıkar, hiçbiri düşmez ve hepsi yanlış alandan okunmuştur; toplam yalnızca 60,1 m³ sapar, yüzde 0,16 ve 1.877 kaydın bölge alanı artık bir bölge adı bile değildir. Sunum katmanı yenilendiğinde bu kez anlamsal setin iki seçicisi kırılır ve çıkan kayıt sıfıra iner. Sayfanın kendi bildirdiği toplamla hesaplanan toplamı karşılaştırmak, sessiz kaymayı yakalayan tek denetimdir.

Bir önceki dersin kaynağı verisini alan için tasarlanmış bir arayüzden verdi: alan adları
yanıtın içindeydi, sayfa sınırı ilan edilmişti. Verinin bir kısmı ise böyle bir arayüzün arkasında
değil, insan gözü için düzenlenmiş bir sayfanın yapısının içinde durur. Orada alan adı yoktur;
kaydın hangi parçasının nerede olduğunu yalnızca konum ve görünüm işaretleri söyler.

Bu dersin ölçüsü şudur: sayfanın yapısı değiştiğinde kaç seçici kırılır, kaç satır düşer ve kaç
satır yanlış alandan okunmuş olarak hayatta kalır. Ölçünün asıl sorusu sayının kendisi değil,
kırılmanın **duyulup duyulmadığıdır**. Teknik soru burada da tek başına yeterli değildir: bir
sayfadan kazıma yoluyla veri toplamak, kaynağın kullanım koşulları ve açık bir erişim izni
olmadan teknik olarak mümkün olsa bile hukuki ve mesleki bir sorundur; burada izin verilmiş bir
toplama varsayılır ve kişisel veri taşıyan alanlar sayfaya hiç konmamıştır.

- **VT15.** Sayfa kurgudur ve `node` ile modellenir: belge ağacı bellekte kurulur, ağ çağrısı
  yapılmaz. Koşan kod bir toplama aracı değildir; yalnızca ayrıştırır, doğrular ve sayar.
  **Modeldir.**
- **VT16.** Sayfa önceki iki dersin kümesini yayımlar — 1.877 okuma satırı, 38.690,7 m³.
- **VT17.** Dört yapı sürümü aynı veriyi taşır, düzeni değişir: `Y0` taban, `Y1` tabloyu bir
  sarmalayıcı katmana alır, `Y2` araya bir sütun ekler, `Y3` sunum katmanını yeniler.
- **VT18.** Bir seçici iki durumda **kırık** sayılır: hiçbir düğümle eşleşmiyorsa ya da üstbilgi
  metninden çözülmesi gereken sütun konumu bulunamıyorsa.
- **VT19.** İki okumanın aynı sayılması için eşik 0,05 m³'tür, çünkü sayfa değerleri bir
  ondalıkla yayımlıyor ve altındaki bir fark yalnızca yuvarlamadan gelebilir.

#### Sayfanın Yapısı

```js
// sayfa.mjs — kurgu bir kamu yayin sayfasinin belge agaci ve kucuk bir secici
// motoru. MODELDIR: ag cagrisi yoktur, sayfa bellekte kurulur ve ayni 1877
// okuma satirini yayimlar. Dort yapi surumu ayni veriyi tasir, duzeni degisir.
export const TOHUM = 4711;
const uretec = (t) => { let s = t >>> 0;
  return () => { s = (Math.imul(s, 1103515245) + 12345) >>> 0; return s / 4294967296; }; };
const r = uretec(TOHUM);
export const BOLGE = ["kuzey", "doğu", "merkez", "güney", "batı"];
export const KAYIT = [];
for (let i = 0; i < 1877; i++) {           // once 1400 abonenin ilk donemi, sonra 477 ikinci
  const n = i < 1400 ? i : i - 1400, desi = 50 + Math.floor(r() * 321);
  KAYIT.push({ abone_no: 10000 + n, donem: i < 1400 ? "2024-01" : "2024-02", desi,
    onceki: 50 + ((desi * 37 + 11) % 321), bolge: BOLGE[n % 5] });
}
export const kimlik = (k) => `${k.abone_no}-${k.donem}`;
export const m3 = (d) => d / 10;

// Dugum: { etiket, oz, cocuk }; cocuk ya dugum dizisi ya da metindir.
const d = (etiket, oz, cocuk) => ({ etiket, oz, cocuk });
export const metin = (n) => (typeof n.cocuk === "string" ? n.cocuk
  : n.cocuk.map(metin).join(""));
const cocuklar = (n) => (Array.isArray(n.cocuk) ? n.cocuk : []);
const soy = (n) => cocuklar(n).flatMap((c) => [c, ...soy(c)]);

// Secici dili: bosluk = herhangi bir alt duzey, ">" = dogrudan cocuk.
// Adim bicimi: etiket, .sinif, [ad=deger] ya da bunlarin birlesimi.
const parca = (p) => {
  const m = /^([\w-]*)(?:\.([\w-]+))?(?:\[([\w-]+)=([\w-]+)\])?$/.exec(p);
  return { etiket: m[1], sinif: m[2], ad: m[3], deger: m[4] };
};
const uyar = (n, t) => (!t.etiket || n.etiket === t.etiket)
  && (!t.sinif || (n.oz.sinif ?? "") === t.sinif)
  && (!t.ad || n.oz[t.ad] === t.deger);
export function bul(kok, secici) {
  let dugum = [kok], dogrudan = false;
  for (const p of secici.trim().split(/\s+/)) {
    if (p === ">") { dogrudan = true; continue; }
    const t = parca(p);
    dugum = dugum.flatMap((n) => (dogrudan ? cocuklar(n) : soy(n)).filter((x) => uyar(x, t)));
    dogrudan = false;
  }
  return dugum;
}

// Y0 taban; Y1 tabloyu bir sarmalayici katmana alir; Y2 araya "onceki_m3"
// sutunu ekler; Y3 sunumu yeniler: sinif adlari uretilmis adlarla degisir ve
// okuma sutununun basligi yeniden adlandirilir.
export function sayfa(surum) {
  const ek = surum === "Y2", yeni = surum === "Y3";
  const sn = (a) => (yeni ? { ozet: "b-7f2", toplam: "b-91c", satir: "b-4ad" }[a] : a);
  const baslik = ek ? ["abone_no", "donem", "onceki_m3", "okuma_m3", "bolge"]
    : ["abone_no", "donem", yeni ? "tuketim_m3" : "okuma_m3", "bolge"];
  const hucre = (k) => (ek ? [k.abone_no, k.donem, m3(k.onceki), m3(k.desi), k.bolge]
    : [k.abone_no, k.donem, m3(k.desi), k.bolge])
    .map((v) => d("hucre", {}, String(v)));
  const tablo = d("tablo", {}, [
    d("ustbilgi", {}, [d("satir", {}, baslik.map((b) => d("hucre", {}, b)))]),
    d("govde", {}, KAYIT.map((k) => d("satir",
      { sinif: sn("satir"), "veri-tur": "okuma" }, hucre(k)))),
  ]);
  const toplam = KAYIT.reduce((t, k) => t + m3(k.desi), 0).toFixed(1);
  return d("sayfa", {}, [
    d("ozet", { sinif: sn("ozet") }, [d("p", { sinif: sn("toplam") },
      `Toplam: ${toplam} m3`)]),
    d("bolum", {}, [surum === "Y1" ? d("sarmalayici", { sinif: "kutu" }, [tablo]) : tablo]),
  ]);
}

if (import.meta.url === `file://${process.argv[1]}`) {
  console.log(`tohum ${TOHUM}; sayfa ${KAYIT.length} okuma satiri yayimliyor, toplam ` +
    `${KAYIT.reduce((t, k) => t + m3(k.desi), 0).toFixed(1)} m3`);
  const y = (h) => h.map((v, j) => (j ? String(v).padStart([0, 8, 8, 44, 32][j])
    : String(v).padEnd(6))).join("");
  console.log(y(["surum", "dugum", "satir", "sutun basliklari", "ilk veri satiri"]));
  for (const s of ["Y0", "Y1", "Y2", "Y3"]) {
    const k = sayfa(s), satir = bul(k, "govde > satir");
    console.log(y([s, soy(k).length, satir.length,
      bul(k, "ustbilgi satir hucre").map(metin).join(","),
      bul(satir[0], "hucre").map(metin).join(",")]));
  }
}
```

```
tohum 4711; sayfa 1877 okuma satiri yayimliyor, toplam 38690.7 m3
surum    dugum   satir                            sutun basliklari                 ilk veri satiri
Y0        9396    1877               abone_no,donem,okuma_m3,bolge          10000,2024-01,18,kuzey
Y1        9397    1877               abone_no,donem,okuma_m3,bolge          10000,2024-01,18,kuzey
Y2       11274    1877     abone_no,donem,onceki_m3,okuma_m3,bolge     10000,2024-01,30.1,18,kuzey
Y3        9396    1877             abone_no,donem,tuketim_m3,bolge          10000,2024-01,18,kuzey
```

Dört sürümün ortak yanı sağdan ikinci sütunda görünmüyor: **veri satırı sayısı dördünde de
1.877**. Yayımlanan kayıt kümesi hiç değişmedi. Değişen şey düğüm sayısı ve sütun düzenidir. Y1
sayfaya tek bir düğüm ekler (9.396'dan 9.397'ye), Y2 her satıra bir hücre ekleyerek düğüm sayısını
11.274'e çıkarır, Y3 hiçbir düğüm eklemez ve yalnızca metinleri değiştirir.

Bu üç değişikliğin hiçbiri veriyi bozmaz. Y1 görünümü kutu içine almak, Y2 bir sütun daha
yayımlamak, Y3 sunumu yenilemek için yapılmış olabilir; üçü de sayfayı yayımlayan taraf açısından
sıradan bakım işleridir. Sayfanın yapısı bir sözleşme değildir ve yayımlayan tarafın kimseye haber
verme yükümlülüğü yoktur. Ayrıştırıcının bağlandığı şey işte bu sözleşmesiz yüzeydir.

Son sütun Y2'nin neden özel olduğunu gösteriyor. İlk satırın hücreleri artık
`10000,2024-01,30.1,18,kuzey`: üçüncü hücrede 18 değil 30,1 duruyor ve 30,1 de bir okuma değeri
gibi görünüyor. Yeni sütun **sayısal ve aynı aralıkta** olduğu için, üçüncü hücreyi okuma sanan
bir ayrıştırıcı geçerli bir sayı bulur.

#### Yapı Değişince Ne Kırılır

```js
// ayristir.mjs — ayni sayfayi iki secici seti okur. A1 konuma dayanir: yol ve
// sabit sutun sirasi. A2 anlama dayanir: satiri veri isareti, sutunu ustbilgi
// metni bulur. Olculen sey yapi degisince kac secici kirildigi ve kac satirin
// dustugu ya da yanlis alandan okunmus olarak hayatta kaldigidir.
import { sayfa, bul, metin, KAYIT, kimlik, m3, BOLGE } from "./sayfa.mjs";

const A1 = { ad: "A1 konumsal", satir: "bolum > tablo > govde > satir", toplam: "ozet > p",
  konum: () => ({ abone: 0, donem: 1, okuma: 2, bolge: 3 }) };
const A2 = { ad: "A2 anlamsal", satir: "satir[veri-tur=okuma]", toplam: ".ozet .toplam",
  konum: (kok) => {
    const b = bul(kok, "ustbilgi satir hucre").map(metin);
    return { abone: b.indexOf("abone_no"), donem: b.indexOf("donem"),
      okuma: b.indexOf("okuma_m3"), bolge: b.indexOf("bolge") };
  } };

// Esik 0.05 m3: okumalar bir ondalikla yayimlaniyor, altindaki bir fark
// yalnizca yuvarlamadan gelebilir.
const ESIK = 0.05;
const DOGRU = new Map(KAYIT.map((k) => [kimlik(k), m3(k.desi)]));
const HEDEF = KAYIT.reduce((t, k) => t + m3(k.desi), 0);

function coz(kok, a) {
  const kirik = [], satirlar = bul(kok, a.satir), basliklar = bul(kok, a.toplam);
  if (satirlar.length === 0) kirik.push(a.satir);
  if (basliklar.length === 0) kirik.push(a.toplam);
  const k = a.konum(kok);
  for (const [ad, i] of Object.entries(k)) if (i < 0) kirik.push(`sutun:${ad}`);
  const cikan = [];
  for (const s of satirlar) {
    const h = bul(s, "hucre").map(metin), okuma = Number(h[k.okuma]);
    if (!Number.isFinite(okuma)) continue;         // sessizce dusen satir
    cikan.push({ ad: `${h[k.abone]}-${h[k.donem]}`, okuma, bolge: h[k.bolge] });
  }
  const bildirilen = basliklar.length ? Number(/[\d.]+/.exec(metin(basliklar[0]))[0]) : null;
  return { kirik, cikan, bildirilen };
}

const y = (h) => h.map((v, j) => (j > 1 ? String(v).padStart([0, 0, 9, 7, 7, 7, 8, 11, 12][j])
  : String(v).padEnd(j ? 14 : 6))).join("");
console.log(y(["yapi", "ayristirici", "kirilan", "giren", "cikan", "dusen", "yanlis",
  "toplam m3", "bildirilen"]));
const kayit = {};
for (const surum of ["Y0", "Y1", "Y2", "Y3"]) {
  const kok = sayfa(surum);
  for (const a of [A1, A2]) {
    const r = kayit[surum + a.ad] = coz(kok, a);
    const t = r.cikan.reduce((s, x) => s + x.okuma, 0);
    const yanlis = r.cikan.filter((x) => !(Math.abs((DOGRU.get(x.ad) ?? -1) - x.okuma) < ESIK));
    console.log(y([surum, a.ad, r.kirik.length, KAYIT.length, r.cikan.length,
      KAYIT.length - r.cikan.length, yanlis.length, t.toFixed(1),
      r.bildirilen === null ? "-" : r.bildirilen.toFixed(1)]));
  }
}
const y1a = kayit["Y1A1 konumsal"], y2a = kayit["Y2A1 konumsal"], y3b = kayit["Y3A2 anlamsal"];
console.log(`\nY1'de tek bir katman eklendi ve A1'in yol secicisi ${y1a.kirik[0]} hicbir satir ` +
  `bulamadi: ${KAYIT.length} satirin tamami dustu, kirilma gurultuludur`);
const t2 = y2a.cikan.reduce((s, x) => s + x.okuma, 0);
console.log(`Y2'de A1'in kirilan secicisi yok, ${y2a.cikan.length} kayit cikti ve hicbiri ` +
  `dusmedi; ama sutunlar bir kaydigi icin toplam ${(t2 - HEDEF).toFixed(1)} m3 sapti ve ` +
  `${y2a.cikan.filter((x) => !BOLGE.includes(x.bolge)).length} kaydin bolge alani bir bolge ` +
  `adi degil`);
console.log(`Y2'de sayfanin kendi bildirdigi ${y2a.bildirilen.toFixed(1)} m3 ile A1'in ` +
  `hesapladigi ${t2.toFixed(1)} m3 arasindaki fark ${Math.abs(t2 - y2a.bildirilen).toFixed(1)} ` +
  `m3, esik ${ESIK} m3'un ${Math.round(Math.abs(t2 - y2a.bildirilen) / ESIK)} kati`);
console.log(`Y3'te A2'nin ${y3b.kirik.length} secicisi kirildi (${y3b.kirik.join(", ")}) ve ` +
  `cikan kayit ${y3b.cikan.length}; ayni surumde A1 hicbir sey kaybetmedi`);
```

```
yapi  ayristirici     kirilan  giren  cikan  dusen  yanlis  toplam m3  bildirilen
Y0    A1 konumsal           0   1877   1877      0       0    38690.7     38690.7
Y0    A2 anlamsal           0   1877   1877      0       0    38690.7     38690.7
Y1    A1 konumsal           1   1877      0   1877       0        0.0     38690.7
Y1    A2 anlamsal           0   1877   1877      0       0    38690.7     38690.7
Y2    A1 konumsal           0   1877   1877      0    1877    38630.6     38690.7
Y2    A2 anlamsal           0   1877   1877      0       0    38690.7     38690.7
Y3    A1 konumsal           0   1877   1877      0       0    38690.7     38690.7
Y3    A2 anlamsal           2   1877      0   1877       0        0.0           -

Y1'de tek bir katman eklendi ve A1'in yol secicisi bolum > tablo > govde > satir hicbir satir bulamadi: 1877 satirin tamami dustu, kirilma gurultuludur
Y2'de A1'in kirilan secicisi yok, 1877 kayit cikti ve hicbiri dusmedi; ama sutunlar bir kaydigi icin toplam -60.1 m3 sapti ve 1877 kaydin bolge alani bir bolge adi degil
Y2'de sayfanin kendi bildirdigi 38690.7 m3 ile A1'in hesapladigi 38630.6 m3 arasindaki fark 60.1 m3, esik 0.05 m3'un 1202 kati
Y3'te A2'nin 2 secicisi kirildi (.ozet .toplam, sutun:okuma) ve cikan kayit 0; ayni surumde A1 hicbir sey kaybetmedi
```

Taban yapıda iki set de aynı kümeyi çıkarıyor: 1.877 kayıt, sıfır düşen, sıfır yanlış, 38.690,7
m³. Aynı sonucu veren iki ayrıştırıcı aynı ayrıştırıcı değildir ve fark yalnızca yapı değişince
ölçülebilir.

Y1 satırında **bir katman**, bir seçici ve 1.877 satır var. Araya giren tek `sarmalayici` düğümü
A1'in dört adımlık yol seçicisini geçersiz kıldı, çünkü `bolum > tablo` doğrudan çocuk ister ve
tablo artık bolumün torunu. Kırılan seçici sayısı 1, düşen satır 1.877. Bu kayıp **gürültülüdür**:
çıkan kayıt sıfırdır ve bunu fark etmemek için hiçbir denetim yapmamak gerekir. A2'nin satır
seçicisi `satir[veri-tur=okuma]` yolu değil işareti aradığı için aynı sayfada 1.877 satır bulur.

Y2 dersin asıl bulgusudur. A1'in **hiçbir seçicisi kırılmadı**: yol yerinde, dört indis de
sınırlar içinde, 1.877 kayıt çıktı ve hiçbiri düşmedi. Ama sütunlar bir kaydığı için okuma alanına
önceki dönem okuması geldi ve **1.877 kaydın tamamı yanlış değer taşıyor**. Toplam 38.630,6 m³,
yani gerçekten yalnızca 60,1 m³, yüzde 0,16 uzakta. Bir eşik, bir büyüklük kontrolü, bir "makul mü"
bakışı bu sayıyı geçirir. Bölge alanı ise artık okuma değerini taşıyor: **1.877 kaydın bölge alanı
bir bölge adı bile değil**. Kırılan seçici sayısı hasarı ölçmez — Y1'de o sayı 1 iken tek bir
yanlış değer üretilmedi, Y2'de 0 iken tek bir doğru değer üretilmedi.

Y3 aynı sorunun yönünü ters çeviriyor. Sunum yenilenince A2'nin sınıfa dayalı toplam seçicisi ve
üstbilgi metnine dayalı okuma sütunu birlikte kırıldı: **2 kırık seçici, sıfır çıkan kayıt**.
Konuma dayanan A1 ise aynı sürümde hiçbir şey kaybetmedi, çünkü sınıf adı da başlık metni de
onun umurunda değil. Anlamsal seçiciler daha dayanıklı değildir; **başka şeylere** bağlıdır ve
hangi setin dayanacağı, yapının hangi yönde değişeceğine bakar. Ölçülmeden bilinemez.

Son satırlar tek gerçek savunmayı gösteriyor. Sayfa kendi toplamını yayımlıyor ve Y2'de bu sayı
38.690,7 m³ iken ayrıştırılan küme 38.630,6 m³ veriyor; fark 60,1 m³, eşiğin 1.202 katı. Hiçbir
seçicinin kırılmadığı, hiçbir satırın düşmediği, toplamın makul göründüğü durumda hatayı yakalayan
tek şey **kaynağın kendi bildirdiği sayıyla karşılaştırma** oldu. Bir ayrıştırıcıyı doğrulamak,
seçicilerin eşleşip eşleşmediğine bakmak değil, çıkan kümeyi kaynağın kendi beyanına karşı
saymaktır.

#### Özet

- Sayfanın yapısı bir sözleşme değildir; aynı 1.877 satır dört düzende yayımlanabilir ve düzen
  değişimi yayımlayan taraf için sıradan bir bakım işidir.
- Araya tek bir katman girmesi konuma dayalı yol seçicisini kırar: 1 kırık seçici, 1.877 düşen
  satır, sıfır çıkan kayıt. Bu kırılma gürültülüdür ve fark edilmemesi zordur.
- Araya bir sütun girmesi hiçbir seçiciyi kırmaz: 1.877 kayıt çıkar, sıfır düşer ve 1.877'sinin
  değeri yanlıştır; toplam yalnızca 60,1 m³, yüzde 0,16 sapar ve bölge alanı bölge adı olmaktan
  çıkar.
- Sunum katmanının yenilenmesi bu kez anlamsal seti kırar (2 seçici, sıfır kayıt) ve konumsal set
  aynı sürümden zarar görmez; dayanıklılık setin türünden değil, değişimin yönünden çıkar.
- Kırılan seçici sayısı hasarı ölçmez; ayrıştırılan kümeyi kaynağın kendi bildirdiği toplamla
  karşılaştırmak, sessiz kaymayı yakalayan tek denetimdir.

#### Sonraki Adım

Dört derste veri dört yoldan geldi ve her yolda kaç satırın kaybolduğu sayıldı. Bu derslerin ortak
varsayımı, kümenin tamamının toplanabilir olmasıydı: dosya okunabiliyordu, sorgu koşabiliyordu,
arayüz sonuna kadar sayfalanabiliyordu, sayfa baştan sona ayrıştırılabiliyordu. Kütle büyüdüğünde
bu varsayım düşer ve yerine bir karar gelir — bütün kayıtlar yerine bir alt küme alınır. Sonraki
ders dört örnekleme kararının aynı soruya verdiği dört cevabı ölçer ve asıl soruyu sorar: alınan
alt küme kütleyi ne kadar temsil ediyor ve dışarıda kalan kayıtlar kimler.

### Örnekleme

Source: https://academia.sh/tr/kurslar/veri-hazirlama/ornekleme

1.877 satırlık kütleden çekilen dört örneklem aynı iki soruya dört farklı cevap veriyor: kolay örnek ortalamayı 19,99 m³ ve üst tarife basamağının payını yüzde 33,0 verir, basit rastgele örnek 20,04 m³ ve yüzde 32,0, döneme göre tabakalı örnek 20,50 m³ ve yüzde 35,0, çekim birimi abone olan örnek 20,70 m³ ve yüzde 35,0; kütlenin kendi cevabı 20,61 m³ ve yüzde 35,4'tür. Kolay örneğin bölge temsil hatası 0,0 puandır ve tam da bu yüzden güven verir — aynı örneklemin dönem temsil hatası 25,4 puandır, çünkü ikinci dönemin 477 satırının tamamı ve 1.400 abonenin 1.200'ü dışarıda kalmıştır. Aynı rastgele karar on iki tohumda n=50 için 3,90 m³ genişliğinde, n=800 için 0,71 m³ genişliğinde bir aralık verir: tek bir örneğin hatası bilinemez, yayılımı ölçülür.

Dört derste veri dört yoldan geldi ve her yolda kaç satırın kaybolduğu sayıldı. Bu derslerin ortak
varsayımı kümenin tamamının toplanabilir olmasıydı: dosya okunabiliyordu, sorgu koşabiliyordu,
arayüz sonuna kadar sayfalanabiliyordu, sayfa baştan sona ayrıştırılabiliyordu. Kütle büyüdüğünde
bu varsayım düşer ve yerine bir karar gelir — bütün kayıtlar yerine bir alt küme alınır.

Örneklemenin ölçüsü, alt kümenin doğru olup olmadığı değildir; bir alt küme kütle değildir ve her
zaman farklı bir sayı verir. Ölçülen şey **temsildir**: örneklem, kütlenin hangi boyutunu ne kadar
doğru yansıtıyor ve dışarıda kalan satırlar kimler. Bu dersin dört kararı da savunulabilirdir ve
dördü de aynı soruya farklı cevap verir.

- **VT20.** Kütle kurgudur ve önceki dört dersin kümesinin aynısıdır: 1.877 okuma satırı, 1.400
  abone, 38.690,7 m³. Okuma satırı başına ortalama 20,61 m³, yani bu kursun ikinci dersinde
  hesaplanan sayının aynısı. **Modeldir.**
- **VT21.** Üreteç bu dersin kendi üretecidir ve tohumu görünürdür (20240). Aynı tohumla çıktı her
  koşumda aynıdır; karıştırma yerinde değiştirme yapmaz, kütlenin kopyası üzerinde çalışır.
- **VT22.** Tarife basamakları 10 ve 25 m³'te ayrılır ve karşılaştırma desi-m³ tamsayısı üzerinde
  yapılır; böylece sınır değerleri kayan noktalı eşitliğe hiç girmez.
- **VT23.** **Temsil hatası**, aynı boyutta örneklem payı ile kütle payı arasındaki en büyük mutlak
  farktır ve yüzde puan olarak yazılır.
- **VT24.** İki ortalamanın aynı sayılması için eşik 0,05 m³'tür, çünkü okumalar bir ondalıkla
  tutuluyor ve altındaki bir fark yalnızca yuvarlamadan gelebilir.

#### Kütlenin Kendi Cevabı

```js
// kutle.mjs — orneklemenin uzerinde calisacagi kutle. MODELDIR: onceki dort
// dersin 1877 okuma satirinin aynisi, ayni tohumla uretiliyor. Tarife
// basamaklari 10 ve 25 m3'te ayrilir; karsilastirma desi-m3 tamsayisi uzerinde
// yapilir, boylece sinir degerleri kayan noktali esitlige hic girmez.
export const TOHUM = 4711;
export const uretec = (t) => { let s = t >>> 0;
  return () => { s = (Math.imul(s, 1103515245) + 12345) >>> 0; return s / 4294967296; }; };
export const BOLGE = ["kuzey", "doğu", "merkez", "güney", "batı"];
export const KUTLE = [];
const r = uretec(TOHUM);
for (let i = 0; i < 1877; i++) {          // once 1400 abonenin ilk donemi, sonra 477 ikinci
  const n = i < 1400 ? i : i - 1400, desi = 50 + Math.floor(r() * 321);
  KUTLE.push({ abone_no: 10000 + n, donem: i < 1400 ? "2024-01" : "2024-02", desi,
    bolge: BOLGE[n % 5], basamak: desi <= 100 ? "10 alti" : desi <= 250 ? "10-25" : "25 ustu" });
}
export const m3 = (k) => k.desi / 10;
export const ortalama = (kume) => kume.reduce((t, k) => t + m3(k), 0) / kume.length;
export const ustPay = (kume) =>
  100 * kume.filter((k) => k.basamak === "25 ustu").length / kume.length;
// Bir boyutun kategori paylari (yuzde).
export const pay = (kume, alan) => {
  const s = new Map();
  for (const k of kume) s.set(k[alan], (s.get(k[alan]) ?? 0) + 1);
  return new Map([...s].map(([a, n]) => [a, 100 * n / kume.length]));
};
// Temsil hatasi: ayni boyutta orneklem payi ile kutle payi arasindaki en buyuk
// mutlak fark, yuzde puan olarak.
export const hata = (kume, alan) => {
  const o = pay(kume, alan), k = pay(KUTLE, alan);
  return Math.max(...[...k.keys()].map((a) => Math.abs((o.get(a) ?? 0) - k.get(a))));
};

if (import.meta.url === `file://${process.argv[1]}`) {
  console.log(`tohum ${TOHUM}; kutle ${KUTLE.length} okuma satiri, ` +
    `${new Set(KUTLE.map((k) => k.abone_no)).size} abone, ` +
    `${KUTLE.reduce((t, k) => t + m3(k), 0).toFixed(1)} m3`);
  console.log(`kutlenin cevabi: ortalama ${ortalama(KUTLE).toFixed(2)} m3, ` +
    `ust basamagin payi yuzde ${ustPay(KUTLE).toFixed(1)}`);
  for (const alan of ["donem", "bolge", "basamak"])
    console.log(`${alan.padEnd(8)}` + [...pay(KUTLE, alan)]
      .map(([a, p]) => `${a} ${p.toFixed(1)}`).join(" | "));
}
```

```
tohum 4711; kutle 1877 okuma satiri, 1400 abone, 38690.7 m3
kutlenin cevabi: ortalama 20.61 m3, ust basamagin payi yuzde 35.4
donem   2024-01 74.6 | 2024-02 25.4
bolge   kuzey 20.0 | doğu 20.0 | merkez 20.0 | güney 20.0 | batı 20.0
basamak 10-25 46.6 | 10 alti 18.0 | 25 ustu 35.4
```

Kütlenin cevabı bilindiği için bu ders diğerlerinden ayrılır: örneklem hatası tahmin edilmez,
doğrudan ölçülür. Gerçek bir çalışmada bu satır yoktur ve doğru cevap bilinmediği için tek
denetlenebilir şey **kararın kendisidir**.

Alt üç satır kütlenin üç boyuttaki yapısıdır ve örneklemin karşılaştırılacağı ölçek budur. Dönem
boyutu dengesizdir: satırların yüzde 74,6'sı ilk dönemden gelir, çünkü 1.400 abonenin yalnız
477'sinin ikinci dönem okuması vardır. Bölge boyutu ise neredeyse tam dengelidir, beş bölgenin
payı yüzde 20 civarındadır. Bu ayrım dersin geri kalanını belirler: bir örneklem dengeli bir
boyutta hiç çabalamadan doğru görünebilir.

#### Dört Örnekleme Kararı

```js
// ornekle.mjs — ayni kutleden dort ornekleme karari. Uretec bu dersin kendi
// uretecidir ve tohum gorunurdur; ayni tohumla cikti her kosumda aynidir.
// Olculen sey ornegin "dogru" olup olmadigi degil, kutleyi hangi boyutta ne
// kadar temsil ettigi ve disarida kalanin kim oldugudur.
import { KUTLE, uretec, m3, ortalama, ustPay, hata } from "./kutle.mjs";
const N = 200, TOHUM_ORNEK = 20240;

const karistir = (dizi, tohum) => {
  const g = uretec(tohum), a = [...dizi];
  for (let i = a.length - 1; i > 0; i--) {
    const j = Math.floor(g() * (i + 1)); [a[i], a[j]] = [a[j], a[i]];
  }
  return a;
};
// O1: listenin ilk N satiri. O2: basit rastgele ornek. O3: doneme gore
// orantili tabakali ornek — her tabakadan kutledeki payi kadar satir alinir.
const O1 = (n) => KUTLE.slice(0, n);
const O2 = (n, tohum = TOHUM_ORNEK) => karistir(KUTLE, tohum).slice(0, n);
const O3 = (n, tohum = TOHUM_ORNEK) => ["2024-01", "2024-02"].flatMap((d) => {
  const t = KUTLE.filter((k) => k.donem === d);
  return karistir(t, tohum + d.length).slice(0, Math.round(n * t.length / KUTLE.length));
});

// O4: cekim birimi satir degil abonedir. N abone secilir ve secilen abonelerin
// butun satirlari alinir; cikan satir sayisini karar degil, abone basina satir
// sayisi belirler.
const O4 = (n, tohum = TOHUM_ORNEK) => {
  const ic = new Set(karistir([...new Set(KUTLE.map((k) => k.abone_no))], tohum).slice(0, n));
  return KUTLE.filter((k) => ic.has(k.abone_no));
};

const ESIK = 0.05;                    // iki ortalamanin ayni sayilmasi icin esik
const y = (h) => h.map((v, j) => (j ? String(v).padStart([0, 7, 7, 10, 9, 8, 7, 7, 9][j])
  : String(v).padEnd(21))).join("");
console.log(`kutle: ortalama ${ortalama(KUTLE).toFixed(2)} m3, ust basamak yuzde ` +
  `${ustPay(KUTLE).toFixed(1)}; son uc sutun temsil hatasi (yuzde puan)`);
console.log(y(["ornekleme karari", "giren", "cikan", "ortalama", "fark m3", "ust %",
  "donem", "bolge", "basamak"]));
const sonuc = {};
for (const [ad, f] of [["O1 kolay (ilk 200)", O1], ["O2 basit rastgele", O2],
  ["O3 tabakali (donem)", O3], ["O4 birim abone", O4]]) {
  const o = sonuc[ad] = f(N);
  console.log(y([ad, KUTLE.length, o.length, ortalama(o).toFixed(2),
    (ortalama(o) - ortalama(KUTLE)).toFixed(2), ustPay(o).toFixed(1),
    hata(o, "donem").toFixed(1), hata(o, "bolge").toFixed(1), hata(o, "basamak").toFixed(1)]));
}

// Ayni karar, on iki tohum: tek bir ornegin hatasi bilinemez, yayilimi olculur.
const y2 = (h) => h.map((v, j) => (j ? String(v).padStart([0, 8, 11, 11, 9, 14][j])
  : String(v).padEnd(12))).join("");
console.log(`\nO2, on iki tohum (${TOHUM_ORNEK}-${TOHUM_ORNEK + 11})`);
console.log(y2(["orneklem", "tohum", "en kucuk", "en buyuk", "aralik", "ust % aralik"]));
for (const n of [50, 200, 800]) {
  const t = [], u = [];
  for (let i = 0; i < 12; i++) { const o = O2(n, TOHUM_ORNEK + i);
    t.push(ortalama(o)); u.push(ustPay(o)); }
  console.log(y2([`n = ${n}`, 12, Math.min(...t).toFixed(2), Math.max(...t).toFixed(2),
    (Math.max(...t) - Math.min(...t)).toFixed(2),
    (Math.max(...u) - Math.min(...u)).toFixed(1)]));
}

const o1 = sonuc["O1 kolay (ilk 200)"], o3 = sonuc["O3 tabakali (donem)"];
const iciAbone = new Set(o1.map((k) => k.abone_no));
console.log(`\nO1'in disinda ${KUTLE.length - o1.length} satir kaldi: ikinci donemin ` +
  `${KUTLE.filter((k) => k.donem === "2024-02").length} satirinin tamami ve kutledeki ` +
  `${new Set(KUTLE.map((k) => k.abone_no)).size} abonenin ` +
  `${new Set(KUTLE.map((k) => k.abone_no)).size - iciAbone.size} tanesi`);
console.log(`O1'in ortalamasi kutleden ${Math.abs(ortalama(o1) - ortalama(KUTLE)).toFixed(2)} ` +
  `m3 ayrisiyor, esik ${ESIK} m3'un ` +
  `${Math.round(Math.abs(ortalama(o1) - ortalama(KUTLE)) / ESIK)} kati; ama bolge temsil ` +
  `hatasi ${hata(o1, "bolge").toFixed(1)} puan, yani ornek bir boyutta kusursuz gorunuyor`);
console.log(`O3 donem boyutunu ${hata(o3, "donem").toFixed(1)} puana indiriyor, bolge ` +
  `boyutunda ise hatasi ${hata(o3, "bolge").toFixed(1)} puan: tabakalama yalnizca ` +
  `tabakalandigi boyutu garanti eder`);
const o4 = sonuc["O4 birim abone"];
console.log(`O4 ${N} abone secti ama ${o4.length} satir cikardi: secilen abonelerin ` +
  `${o4.length - N} tanesinin ikinci donem okumasi da var. Cekim birimi abone oldugu icin ` +
  `abone basina ortalama ${(o4.reduce((s, k) => s + m3(k), 0) / N).toFixed(2)} m3 dogrudan ` +
  `okunabilir; kutlenin abone basina ortalamasi ` +
  `${(KUTLE.reduce((s, k) => s + m3(k), 0) / new Set(KUTLE.map((k) => k.abone_no)).size)
    .toFixed(2)} m3`);
```

```
kutle: ortalama 20.61 m3, ust basamak yuzde 35.4; son uc sutun temsil hatasi (yuzde puan)
ornekleme karari       giren  cikan  ortalama  fark m3   ust %  donem  bolge  basamak
O1 kolay (ilk 200)      1877    200     19.99    -0.63    33.0   25.4    0.0      3.5
O2 basit rastgele       1877    200     20.04    -0.58    32.0    1.1    2.5      3.4
O3 tabakali (donem)     1877    200     20.50    -0.11    35.0    0.1    2.0      0.5
O4 birim abone          1877    274     20.70     0.09    35.0    1.6    3.7      0.5

O2, on iki tohum (20240-20251)
orneklem       tohum   en kucuk   en buyuk   aralik  ust % aralik
n = 50            12      18.41      22.32     3.90          22.0
n = 200           12      19.26      21.55     2.29          11.5
n = 800           12      20.33      21.04     0.71           4.3

O1'in disinda 1677 satir kaldi: ikinci donemin 477 satirinin tamami ve kutledeki 1400 abonenin 1200 tanesi
O1'in ortalamasi kutleden 0.63 m3 ayrisiyor, esik 0.05 m3'un 13 kati; ama bolge temsil hatasi 0.0 puan, yani ornek bir boyutta kusursuz gorunuyor
O3 donem boyutunu 0.1 puana indiriyor, bolge boyutunda ise hatasi 2.0 puan: tabakalama yalnizca tabakalandigi boyutu garanti eder
O4 200 abone secti ama 274 satir cikardi: secilen abonelerin 74 tanesinin ikinci donem okumasi da var. Cekim birimi abone oldugu icin abone basina ortalama 28.36 m3 dogrudan okunabilir; kutlenin abone basina ortalamasi 27.64 m3
```

Dört karar da aynı 1.877 satırdan çekildi ve dördü de aynı iki soruya farklı cevap verdi. Ortalama
19,99 ile 20,70 m³ arasında, üst basamağın payı yüzde 32,0 ile 35,0 arasında değişiyor. Hiçbir
karar hesap hatası yapmadı; dördü de kendi kümesinin doğru ortalamasını verdi.

O1 dersin tuzağıdır. Bölge temsil hatası **0,0 puandır** — beş bölge örneklemde kütledeki payla
birebir aynı orandadır. Liste abone numarasına göre sıralı olduğu ve bölge de abone numarasından
türediği için ilk 200 satır bu boyutta kusursuz çıkar. Aynı örneklemin dönem temsil hatası ise
**25,4 puandır**: örneklemin tamamı ilk dönemdendir, ikinci dönemin **477 satırının tamamı** ve
1.400 abonenin **1.200'ü** dışarıda kalmıştır. Bir örneklemin bir boyutta kusursuz görünmesi,
başka boyutlarda da doğru olduğunu göstermez ve dengeli boyutlar bu yanılgıyı besler.

O2 ile O1'in ortalama hataları birbirine yakın: 0,58 ve 0,63 m³, ikisi de eşiğin on katından
büyük. Aradaki fark hatanın büyüklüğü değil, **denetlenebilirliğidir**. Alt tablo O2'nin hatasını
bir yayılım olarak veriyor: aynı karar on iki tohumda n=50 için 18,41 ile 22,32 m³ arasında,
n=200 için 19,26 ile 21,55 arasında, n=800 için 20,33 ile 21,04 arasında değer üretiyor. Aralık
3,90'dan 0,71 m³'e iniyor ve üst basamağın payındaki aralık 22,0 puandan 4,3 puana düşüyor.
Rastgele bir kararın hatası tek bir örnekte bilinemez ama yayılımı ölçülebilir ve örneklem
büyütülerek daraltılabilir. O1'in hatası ise tohuma bağlı değildir; listenin sırasına bağlıdır,
tekrar denenerek daraltılamaz ve bir aralıkla ifade edilemez.

O3 kütlenin dengesiz olduğu boyutu düzeltiyor. Dönem temsil hatası 25,4 puandan **0,1 puana**
iniyor, ortalama hatası 0,11 m³'e, üst basamağın payı yüzde 35,0'e — kütlenin yüzde 35,4'üne en
yakın değer. Ama aynı örneklemin bölge temsil hatası **2,0 puandır**, yani O1'inkinden büyüktür.
Tabakalama bir kalite mührü değil, hedefli bir garantidir: yalnızca tabakalandığı boyutu güvenceye
alır, geri kalan boyutları rastgeleliğe bırakır. Hangi boyutta tabakalanacağı, örneklemin
cevaplayacağı sorunun hangi kırılımda okunacağına bakar.

O4 çekim biriminin kendisini değiştiriyor ve tablodaki tek düzensiz satırı üretiyor: 200 abone
seçildi ama küme **274 satır** çıktı, çünkü seçilen abonelerin 74'ünün ikinci dönem okuması da
var. Satır sayısını karar değil, abone başına satır sayısı belirledi. Buna karşılık bu örneklem
abone başına ortalamayı doğrudan verir — 28,36 m³, kütlenin 27,64 m³'üne karşılık. Bu kursun
ikinci dersinde satırın birimi bir sorgu sorunuydu; burada bir örnekleme sorunudur ve cevabı
aynıdır: **çekim birimi, raporun satırıyla aynı olmalıdır.** Satır düzeyinde çekilen bir örneklem,
iki okuması olan aboneye iki kez seçilme şansı verir ve abone başına bir soruyu sessizce
ağırlıklandırır.

#### Özet

- Örneklemenin ölçüsü alt kümenin doğruluğu değil temsildir: örneklem payı ile kütle payı
  arasındaki en büyük fark, boyut boyut yazılır.
- Aynı kütleden çekilen dört örneklem ortalamayı 19,99, 20,04, 20,50 ve 20,70 m³ verir; kütlenin
  cevabı 20,61 m³'tür ve dört kararın hiçbiri hesap hatası yapmamıştır.
- Kolay örnek bölge boyutunda 0,0 puan hata verir ve tam da bu yüzden güven verir; aynı örneklemin
  dönem hatası 25,4 puandır, çünkü ikinci dönemin 477 satırının tamamı ve 1.400 abonenin 1.200'ü
  dışarıda kalmıştır.
- Rastgele bir kararın hatası tek örnekte bilinmez, yayılımı ölçülür: on iki tohumda aralık n=50
  için 3,90 m³, n=800 için 0,71 m³'tür. Kolay örneğin hatası bu şekilde daraltılamaz.
- Tabakalama yalnızca tabakalandığı boyutu garanti eder: dönem hatası 0,1 puana inerken bölge
  hatası 2,0 puanda kalır.
- Çekim birimi raporun satırıyla aynı olmalıdır: 200 abonelik örneklem 274 satır çıkarır ve abone
  başına ortalamayı doğrudan verir, satır düzeyinde çekilen örneklem ise aynı soruyu sessizce
  ağırlıklandırır.

#### Sonraki Adım

Beş ders boyunca tek bir hesap tutuldu. Dosya 2.076 fiziksel satır verdi ve bir ayrıştırma kararı
142 satırı sessizce düşürdü; sorgu 1.877 satır döndürdü ve bir birleştirme 766 satırı düşürüp 669
satırı çoğalttı; arayüz dar bir kapsamla 477 satırı hiç vermedi ve sayfalama 35 kaydı atlayıp 35
kaydı iki kez getirdi; yayın sayfası yapısı değişince 1.877 satırın tamamını bir sürümde düşürdü,
başka bir sürümde tamamını yanlış alandan okuttu. Bu dersin 1.677 satırı ise bir kayıp değil,
**yazılmış bir karardır** ve aradaki fark kursun kuralıdır: kaybını yazamadığın temizlik bir
temizlik değildir.

Ama beş dersin hepsi satırları **dışarıdan** saydı. Kaç satır geldi, kaç satır düştü, kaç satır iki
kez geldi — hepsi kayıt sayısı üzerinden okundu. Gelen satırların **içi** hiç açılmadı: bir alanın
boş olup olmadığına, aynı abonenin iki farklı yazımla iki kez geçip geçmediğine, bir tarihin
metinden sayıya dönüşürken bozulup bozulmadığına hiç bakılmadı. Sonraki konu satırın içine girer
ve aynı hesabı bu kez alan düzeyinde tutar: eksik değer silindiğinde hangi grup orantısız elenir,
benzerlik eşiği hangi kayıtları yanlış birleştirir, sessiz bir tip dönüşümü kaç kaydı bozar.

## Temizleme

Satırların içi açıldığında ortaya çıkan bozuklukların ve düzeltme kararlarının satır hesabıyla ölçülmesi: eksikliğin rastgele olup olmadığı ve silme kararının orantısız elediği grup, kesin eşleşme ile benzerlik eşiğinin verdiği iki sayı ve yanlış birleştirilen kayıt, hatasız görünüp yanlış değer üreten sessiz tip dönüşümü, çeyrekler açıklığı ile standart puanın işaretlediği kümelerin örtüşmeyen kısmı ve aykırıyı silmenin ortalamayı oynatması, kategori birleştirme sonrası kalan kategori sayısı, metin normalleştirmenin kurtardığı ve sildiği eşleşme.

### Eksik Değerler

Source: https://academia.sh/tr/kurslar/veri-hazirlama/eksik-degerler

Toplama adımının bıraktığı 2.086 satırın okuma alanı üç ayrı biçimde boştur: 104 satırda hücre boş, 31 satırda yalnız boşluk, 70 satırda 'yok' yazılıdır. Eksiklik rastgele değildir — oran bölgeye göre yüzde 3,7 ile yüzde 28,1 arasında değişir ve bir bölge rastgele dağılımın beklettiği sayının 2,85 katını taşır. Eksik okumalı satırı silmek kümeyi 2.068'den 1.864'e indirir, ağ ortalamasını 22,45 m³'te bırakır ve zararsız görünür; oysa silinen 204 satırın 99'u tek bir bölgedendir ve o bölgenin küme içindeki payı yüzde 17,0'den 13,6'ya, 3,4 puan düşer. Yalnız boş hücreyi eksik sayan bir süzgeç 100 satırı görmez: 31'i sessizce sıfır tüketime döner, 69'u sayısal süzgeçten sessizce düşer ve ortalama 22,09 m³'e iner.

Bir önceki konu veriyi kaynağından çekti ve her adımda kaç satırın girip çıktığını saydı. Küme bir
dosyada duruyor, satır sayısı biliniyor; bilinmeyen tek şey satırların içidir.

Bu ders okuma alanını açar. Alan üç ayrı biçimde boş çıkar ve üçü aynı şeyi söylemez. Ardından iki
savunulabilir karar durur: eksik okumalı satırı silmek ya da işaretleyip doldurmak. Elektronik
Tablolarla Analiz kursu bu kararı hücre düzeyinde tartıştı ve kararın sayfadan geri okunup
okunamadığını ölçtü; burada ölçü tektir — **satır hesabı**. Kaç satır girdi, kaç satır çıktı,
aradaki fark nereye gitti.

- **VZ1.** Ham küme `node` ile **modellenir**. Her alan saha formundan geldiği haliyle
  **metindir**; hiçbir tip zorlanmamıştır. Veri kümesi **kurgudur**: bölgesel ölçüm ağının 1.400
  abonesi ve saha ziyaret kayıtları.
- **VZ2.** Eksiklik üç biçimde görünür: hücre **boştur**, hücrede **yalnız boşluk** vardır, hücreye
  **"yok" yazılmıştır**. Üçü aynı şeyi anlatır, üçü farklı davranır.
- **VZ3.** **Kimlik alanı** (sayaç kodu) boş olan satır hiçbir aboneye bağlanamaz; doldurulamaz,
  yalnız elenebilir. Ölçüm alanının eksikliği ile kimlik alanınınki ayrı iki karardır.
- **VZ4.** Eksikliğin rastgele olup olmadığı, her grupta **gözlenen** eksik sayısının rastgele
  dağılımın **beklettiği** sayıya oranıyla ölçülür.

#### Eksiklik Rastgele mi

```js
// ham.mjs — toplama adiminin biraktigi ham kume MODELIDIR: her alan saha
// formundan geldigi haliyle metindir, hicbir tip zorlanmamistir. Kume
// kurgudur: bolgesel olcum aginin 1400 abonesi ve saha ziyaret kayitlari.
export const TOHUM = 20260305, ABONE = 1400;
const karistir = (x) => { x = Math.imul(x ^ (x >>> 16), 2246822507);
  x = Math.imul(x ^ (x >>> 13), 3266489909); return (x ^ (x >>> 16)) >>> 0; };
export const uretec = (t) => { let s = karistir(t);
  return () => (s = (s * 1664525 + 1013904223) >>> 0) / 4294967296; };
export const ayrik = (u, w) => { let t = 0;
  for (let i = 0; i < w.length; i++) { t += w[i]; if (u < t) return i; } return w.length - 1; };
// Son alan: ziyaretin okuma uretmeme orani. Bolgeler bu oranda ayrisir.
export const BOLGE = [["kuzey", "KZ", 0.28, 21, 0.06], ["guney", "GN", 0.22, 17, 0.03],
  ["dogu", "DG", 0.18, 26, 0.27], ["bati", "BT", 0.14, 14, 0.03],
  ["merkez", "MR", 0.18, 23, 0.08]]
  .map(([ad, kod, pay, taban, eksik]) => ({ ad, kod, pay, taban, eksik }));
const SOKAK = ["Çınar", "Söğüt", "Işık", "Ihlamur", "Zeytin", "Menekşe", "Palmiye", "Ceviz"];
const TARIFE = ["Mesken", "mesken", "MESKEN", " Mesken ", "Konut", "konut", "Ticari",
  "ticari", "TİCARİ", "Is yeri", "İş Yeri", "isyeri", "Sanayi", "sanayi", "Tarimsal"];

export const HAM = [];
let bir = null;
for (let i = 0; i < ABONE; i++) {
  const r = uretec(TOHUM + i);
  const b = BOLGE[ayrik(r(), BOLGE.map((x) => x.pay))];
  const taban = Math.max(1.2, b.taban * Math.exp((r() + r() + r() - 1.5) * 0.62));
  const sayac = `${b.kod}-${String(1000 + i).padStart(4, "0")}`;
  const paylas = bir && bir.b === b.ad && r() < 0.03;   // ayni binada ikinci sayac
  const sk = paylas ? bir.sk : SOKAK[Math.floor(r() * SOKAK.length)];
  const kapi = paylas ? bir.kapi : 1 + Math.floor(r() * 60);
  const daire = paylas ? bir.daire : r() < 0.26 ? `/${1 + Math.floor(r() * 9)}` : "";
  bir = { b: b.ad, sk, kapi, daire };
  const endeks0 = 8000 + Math.floor(r() * 11000);
  for (const d of r() < 0.34 ? ["2026-01", "2026-02"] : ["2026-01"]) {
    const q = uretec(TOHUM + 7000 + HAM.length), u = q();
    const gercek = Math.round(taban * (d === "2026-02" ? 0.94 : 1) * 100) / 100;
    // Eksiklik uc bicimde gorunur: hucre bos, hucrede yalniz bosluk, "yok" yazili.
    const eksik = u < b.eksik ? ayrik(q(), [0.52, 0.19, 0.29]) : -1;
    const kayma = eksik < 0 && q() < 0.008;   // ondalik kaymasi: okuma on kat yazilmis
    const yazilan = kayma ? Math.round(gercek * 1000) / 100 : gercek;
    const okuma = eksik === 0 ? "" : eksik === 1 ? "  " : eksik === 2 ? "yok"
      : q() < 0.31 ? yazilan.toFixed(2).replace(".", ",") : yazilan.toFixed(2);
    const gun = 1 + Math.floor(q() * 27), ay = d.slice(5);
    const tb = ayrik(q(), [0.55, 0.21, 0.24]);
    const tarih = tb === 0 ? `${d}-${String(gun).padStart(2, "0")}`
      : tb === 1 ? `${String(gun).padStart(2, "0")}.${ay}.2026`
      : `${String(gun).padStart(2, "0")}/${ay}/2026`;
    const endeks = endeks0 + Math.round(gercek * (d === "2026-02" ? 2 : 1));
    const eb = q() < 0.22;
    const av = ayrik(q(), [0.30, 0.18, 0.18, 0.16, 0.18]);
    const kap = `${kapi}${daire}`;
    const M = `${b.ad[0].toUpperCase()}${b.ad.slice(1)}`, mUst = b.ad.toLocaleUpperCase("tr");
    const alt = sk.toLocaleLowerCase("tr"), ust = sk.toLocaleUpperCase("tr");
    const adres = av === 0 ? `${b.ad} mahallesi ${alt} sokak no: ${kap}`
      : av === 1 ? `${M} Mahallesi ${sk} Sokak No ${kap}`
      : av === 2 ? `${mUst} MAHALLESİ ${ust} SOKAK NO ${kap}`
      : av === 3 ? `  ${M}  Mahallesi  ${sk}  Sokak  No ${kap} `
      : `${b.ad} mah. ${alt} sk. no:${kap}`;
    HAM.push({ abone_no: 10001 + i, sayac_no: q() < 0.011 ? "" : sayac, bolge: b.ad,
      donem: d, okuma_metni: okuma,
      endeks_metni: eb ? `${Math.floor(endeks / 1000)}.${String(endeks % 1000).padStart(3, "0")}`
        : `${endeks}`,
      tarih_metni: tarih, tarife_metni: TARIFE[ayrik(q(), Array(15).fill(1 / 15))],
      adres_metni: adres, gercek_m3: gercek, yazilan_m3: eksik < 0 ? yazilan : null });
    if (q() < 0.09) {   // ayni ziyaret ikinci kez girilmis
      const on = HAM[HAM.length - 1], bicim = ayrik(q(), [0.34, 0.28, 0.38]);
      HAM.push({ ...on,
        sayac_no: bicim === 0 ? ` ${sayac}` : bicim === 1 ? sayac.toLowerCase() : sayac,
        adres_metni: av === 2 ? `${b.ad} mah. ${alt} sk. no:${kap}`
          : `${mUst} MAHALLESİ ${ust} SOKAK NO ${kap}`,
        tarife_metni: TARIFE[ayrik(q(), Array(15).fill(1 / 15))] });
    }
  }
}

// Okumayi sayiya cevirir. Ondalik ayraci virgul de olabilir; donusumun kendi
// tuzaklari bu konunun ucuncu dersinde olculur, burada tolere edilir.
export const sayiya = (t) => { const s = String(t).trim().replace(",", ".");
  return s === "" || !/^\d+(\.\d+)?$/.test(s) ? null : Number(s); };
export const medyan = (a) => { const l = [...a].sort((x, y) => x - y);
  return l.length % 2 ? l[(l.length - 1) / 2] : (l[l.length / 2 - 1] + l[l.length / 2]) / 2; };
export const y = (w) => (h) => h.map((v, j) => (j ? String(v).padStart(w[j])
  : String(v).padEnd(w[0]))).join("");

if (import.meta.url === `file://${process.argv[1]}`) {
  const say = (f) => HAM.filter(f).length;
  console.log(`model (tohum ${TOHUM}): ${ABONE} abone, toplama adiminin biraktigi ` +
    `ham kume ${HAM.length} satir`);
  const y1 = y([30, 8, 26]);
  console.log("\n" + y1(["eksikligin gorunusu", "satir", "yaygin donusum ne verir"]));
  console.log(y1(["bos hucre", say((s) => s.okuma_metni === ""), 'Number("") -> 0']));
  console.log(y1(["yalniz bosluk", say((s) => s.okuma_metni !== "" && s.okuma_metni.trim() === ""),
    'Number("  ") -> 0']));
  console.log(y1(['"yok" yazili', say((s) => s.okuma_metni.trim() === "yok"),
    'Number("yok") -> NaN']));
  console.log(y1(["kimlik alani bos", say((s) => s.sayac_no.trim() === ""),
    "satir atfedilemez"]));

  const kimlikli = HAM.filter((s) => s.sayac_no.trim() !== "");
  const eksikler = kimlikli.filter((s) => sayiya(s.okuma_metni) === null);
  const y2 = y([9, 8, 8, 10, 15, 7]);
  console.log("\neksiklik rastgele mi");
  console.log(y2(["bolge", "satir", "eksik", "oran %", "rastgele ise", "kat"]));
  for (const b of BOLGE) {
    const n = say((s) => s.bolge === b.ad && s.sayac_no.trim() !== "");
    const e = eksikler.filter((s) => s.bolge === b.ad).length;
    const bek = eksikler.length * n / kimlikli.length;
    console.log(y2([b.ad, n, e, (100 * e / n).toFixed(1), bek.toFixed(1), (e / bek).toFixed(2)]));
  }
  console.log(`${kimlikli.length} satirin ${eksikler.length} tanesinde okuma yok; ` +
    `eksiklik bolgeye gore yuzde 3.7 ile yuzde 28.1 arasinda degisiyor`);
}
```

```
model (tohum 20260305): 1400 abone, toplama adiminin biraktigi ham kume 2086 satir

eksikligin gorunusu              satir   yaygin donusum ne verir
bos hucre                          104           Number("") -> 0
yalniz bosluk                       31         Number("  ") -> 0
"yok" yazili                        70      Number("yok") -> NaN
kimlik alani bos                    18         satir atfedilemez

eksiklik rastgele mi
bolge       satir   eksik    oran %   rastgele ise    kat
kuzey         591      48       8.1           58.3   0.82
guney         414      16       3.9           40.8   0.39
dogu          352      99      28.1           34.7   2.85
bati          324      12       3.7           32.0   0.38
merkez        387      29       7.5           38.2   0.76
2068 satirin 204 tanesinde okuma yok; eksiklik bolgeye gore yuzde 3.7 ile yuzde 28.1 arasinda degisiyor
```

İlk tablo eksikliğin tek bir şey olmadığını gösteriyor. 104 satırda hücre gerçekten boştur; 31
satırda içinde yalnız boşluk vardır ve gözle boştan ayırt edilmez; 70 satıra ziyaret sonucu **"yok"
olarak yazılmıştır**. Üçüncü sütun neden ayrı sayıldıklarını söylüyor: ilk ikisi yaygın bir sayıya
çevirme çağrısından **sıfır** olarak çıkar, üçüncüsü sayı olmayan bir değer verir. Aynı eksiklik,
yazılış biçimine göre ya bir ölçüm gibi görünür ya da hesabın dışına düşer.

İkinci tablo dersin asıl sorusunu cevaplıyor. Eksiklik rastgele olsaydı her bölge kümedeki payı
kadar eksik taşırdı: `dogu` bölgesi 34,7 satır beklerdi, gerçekte 99 satır taşıyor — beklenenin
**2,85 katı**. `guney` beklenenin yüzde 39'unu, `bati` yüzde 38'ini taşıyor. Eksiklik bir
gürültü değil, bir **desendir** ve bu ayrım bütün kararı belirler: rastgele eksiklikte silinen
satırlar kümenin küçük bir kopyasıdır, desenli eksiklikte **belirli bir gruptur**.

#### Silme Kimi Eliyor

```js
// eksik.mjs — ayni ham kume, uc soru: silme karari hangi grubu orantisiz eliyor,
// doldurma neyi degistirmiyor, eksikligi eksik saymayan bir suzgec kac satiri
// sessizce dusuruyor.
import { HAM, BOLGE, sayiya, medyan, y } from "./ham.mjs";

// Kimlik alani bos olan satir doldurulamaz: okuma bir sayaca baglanamiyorsa
// hangi abonenin tuketimi oldugu yazilamaz. Bu satirlar elenir ve sayisi yazilir.
const kimlikli = HAM.filter((s) => s.sayac_no.trim() !== "");
const elenen = HAM.length - kimlikli.length;
const olculen = kimlikli.filter((s) => sayiya(s.okuma_metni) !== null);
const bolgeMed = new Map(BOLGE.map((b) => [b.ad,
  medyan(olculen.filter((s) => s.bolge === b.ad).map((s) => sayiya(s.okuma_metni)))]));
const dolu = kimlikli.map((s) => sayiya(s.okuma_metni) === null
  ? { ...s, okuma_metni: bolgeMed.get(s.bolge).toFixed(2), eksikti: true } : s);

const ort = (l) => l.reduce((a, s) => a + sayiya(s.okuma_metni), 0) / l.length;
const y1 = y([32, 8, 8, 12, 14]);
console.log(y1(["ayni kume, iki karar", "giren", "cikan", "ag ort m3", "dogu payi %"]));
for (const [ad, l] of [["K1 eksik okumali satir elenir", olculen],
  ["K2 bolge ortancasiyla doldurulur", dolu]]) {
  console.log(y1([ad, kimlikli.length, l.length, ort(l).toFixed(2),
    (100 * l.filter((s) => s.bolge === "dogu").length / l.length).toFixed(1)]));
}
console.log(`\nsilme agin ortalamasini ${ort(olculen).toFixed(2)} m3'te birakiyor, ` +
  `doldurma ${ort(dolu).toFixed(2)} m3; iki sayi arasindaki ` +
  `${Math.abs(ort(olculen) - ort(dolu)).toFixed(2)} m3 raporlanan iki ondalik basamagin ` +
  `yarisindan buyuk, ama kararin buyuklugunu anlatmiyor`);

// Silme kimi eliyor: her bolgenin kume icindeki payi once ve sonra.
const y2 = y([9, 9, 8, 11, 11, 9]);
console.log("\n" + y2(["bolge", "elenen", "payi %", "once %", "sonra %", "puan"]));
for (const b of BOLGE) {
  const once = kimlikli.filter((s) => s.bolge === b.ad).length;
  const sonra = olculen.filter((s) => s.bolge === b.ad).length;
  const p0 = 100 * once / kimlikli.length, p1 = 100 * sonra / olculen.length;
  console.log(y2([b.ad, once - sonra, (100 * (once - sonra) /
    (kimlikli.length - olculen.length)).toFixed(1), p0.toFixed(1), p1.toFixed(1),
    (p1 - p0).toFixed(1)]));
}

// Eksikligi yalnizca bos hucre sayan bir suzgec: kalan iki bicim sessizce gecer.
const bosluk = kimlikli.filter((s) => s.okuma_metni !== "" && s.okuma_metni.trim() === "");
const yok = kimlikli.filter((s) => s.okuma_metni.trim() === "yok");
const sifira = bosluk.filter((s) => Number(s.okuma_metni) === 0).length;
const nan = yok.filter((s) => Number.isNaN(Number(s.okuma_metni))).length;
console.log(`\nyalniz bos hucreyi eksik sayan bir suzgec ${kimlikli.length} satirin ` +
  `${bosluk.length + yok.length} tanesini gormez: ${sifira} satir sessizce 0 m3 ` +
  `tuketime doner, ${nan} satir sayisal suzgecten sessizce duser`);
console.log(`ayni suzgecle raporlanan ag ortalamasi ` +
  `${ort(kimlikli.filter((s) => s.okuma_metni !== "" && !Number.isNaN(Number(s.okuma_metni
    .trim().replace(",", ".")))).map((s) => (Number(s.okuma_metni.trim().replace(",", ".")) === 0
    ? { ...s, okuma_metni: "0" } : s))).toFixed(2)} m3 cikar`);

console.log(`\nsatir hesabi: giren ${HAM.length}, cikan ${dolu.length}; ` +
  `${elenen} satir elendi (kimlik alani bos), ` +
  `${dolu.filter((s) => s.eksikti).length} satir isaretlenip dolduruldu, ` +
  `0 satir sessizce dustu`);
```

```
ayni kume, iki karar               giren   cikan   ag ort m3   dogu payi %
K1 eksik okumali satir elenir       2068    1864       22.45          13.6
K2 bolge ortancasiyla doldurulur    2068    2068       22.41          17.0

silme agin ortalamasini 22.45 m3'te birakiyor, doldurma 22.41 m3; iki sayi arasindaki 0.04 m3 raporlanan iki ondalik basamagin yarisindan buyuk, ama kararin buyuklugunu anlatmiyor

bolge       elenen  payi %     once %    sonra %     puan
kuzey           48    23.5       28.6       29.1      0.6
guney           16     7.8       20.0       21.4      1.3
dogu            99    48.5       17.0       13.6     -3.4
bati            12     5.9       15.7       16.7      1.1
merkez          29    14.2       18.7       19.2      0.5

yalniz bos hucreyi eksik sayan bir suzgec 2068 satirin 100 tanesini gormez: 31 satir sessizce 0 m3 tuketime doner, 69 satir sayisal suzgecten sessizce duser
ayni suzgecle raporlanan ag ortalamasi 22.09 m3 cikar

satir hesabi: giren 2086, cikan 2068; 18 satir elendi (kimlik alani bos), 204 satir isaretlenip dolduruldu, 0 satir sessizce dustu
```

İlk tablonun en tehlikeli sütunu ağ ortalamasıdır. Silme 204 satır atar, doldurma hiç atmaz ve iki
kararın ortalaması 22,45 ile 22,41 m³'tür. Fark 0,04 m³; yalnız bu sayıya bakan biri kararın
zararsız olduğu sonucuna varır. Karşılaştırma eşitlik işleciyle değil, raporlama duyarlılığı olan
0,005 m³ eşiğiyle yapılmıştır — kayan noktalı iki değer arasındaki eşitlik zaten sorulamaz.

Son sütun asıl bedeli gösteriyor: `dogu` bölgesinin payı yüzde 17,0'den **13,6'ya** iner. Silinen
204 satırın **99'u**, yani yüzde 48,5'i tek bir bölgedendir. Bu bir hata değildir ve kümenin
görünüşünden anlaşılmaz: silme işlemi 1.864 düzgün satır bırakır. Ortalama korunurken
**kompozisyon** değişmiştir ve bundan sonra bölge kırılımıyla üretilecek her sayı bu kaymayı
taşır.

Üçüncü blok sessiz kaybı ölçüyor. Eksikliği yalnız boş hücre sayan bir süzgeç 2.068 satırın
**100'ünü** hiç görmez: 31'i sayıya çevrilirken sessizce sıfır olur ve gerçek bir sıfır tüketim gibi
durur, 69'u sayı olmayan bir değere döner ve ilk sayısal süzgeçte sessizce düşer. Aynı süzgeçle
raporlanan ağ ortalaması 22,09 m³'tür — hiçbir uyarı, yalnız 0,36 m³ aşağıda bir sayı.

Adımın hesabı şudur: **giren 2.086 satır, çıkan 2.068 satır.** Aradaki 18 satır kimlik alanı boş
olduğu için elendi, 204 satır işaretlenip dolduruldu ve kümede kaldı, sessizce düşen satır
**sıfırdır**. Kaybın kalem kalem yazılabilmesi kararın doğru olduğunu göstermez, yalnız
**görülebilir** olduğunu gösterir.

#### Özet

- Toplama adımının bıraktığı 2.086 satırda eksiklik üç biçimde görünür: 104 boş hücre, 31 yalnız
  boşluk, 70 "yok" yazılı hücre; ilk ikisi yaygın dönüşümde sıfıra döner.
- Eksiklik rastgele değildir: oran bölgeye göre yüzde 3,7 ile 28,1 arasında değişir, bir bölge
  beklenenin 2,85 katını taşır.
- Silme kümeyi 2.068'den 1.864'e indirir ve ağ ortalamasını 22,45 m³'te bırakarak zararsız görünür;
  silinen 204 satırın 99'u tek bir bölgedendir ve o bölgenin payı 3,4 puan düşer.
- Yalnız boş hücreyi eksik sayan bir süzgeç 100 satırı görmez — 31'i sessizce sıfıra döner, 69'u
  sessizce düşer — ve ağ ortalamasını 22,09 m³ olarak raporlar.
- Adımın hesabı: giren 2.086, çıkan 2.068; 18 elendi, 204 işaretlenip dolduruldu, 0 sessizce düştü.

#### Sonraki Adım

Kalan 2.068 satırın hepsinin ayrı bir ziyareti temsil ettiği varsayıldı. Varsayım tutmuyor: aynı
abonenin aynı dönemdeki okuması bir bölüm satırda ikinci kez girilmiştir ve ikinci girişte sayaç
kodu bazen baştaki boşluğu, bazen küçük harfi taşır. Sonraki ders üç tekilleştirme ölçütü uygular
ve ölçüt değişince kaç satırın birleştiğini sayar, ardından okunaksız yazılmış destek taleplerini
abone kütüğüne bağlar: kesin eşleşme ile benzerlik eşiğinin kurduğu bağların kaçı **yanlış
aboneye** gidiyor.

### Yinelenen Kayıtlar

Source: https://academia.sh/tr/kurslar/veri-hazirlama/yinelenen-kayitlar

Devralınan 2.068 satıra üç tekilleştirme ölçütü uygulanır ve üç sayı çıkar: tam satır karşılaştırması hiçbir satırı birleştirmez, ham sayaç kodu 68 satırı birleştirir, kırpılmış ve harf birliğine getirilmiş kod 188 satırı birleştirir — aradaki 120 satır tek bir metin kararının bedelidir. İkinci girişi olan 188 kaydın hepsinde adres, yüzde 92'sinde tarife sınıfı, yüzde 63,8'inde sayaç kodu farklı yazılmıştır ve hiçbirinde okuma değeri değişmemiştir. Tekilleştirmeden ayrı bir iş olan kimlik eşleştirmede 240 destek talebinin 149'u ham metinle, 174'ü sadeleştirilmiş kodla bağlanır ve altısı yanlış aboneye gider; 0,80 eşiğiyle kurulan 18 ek bağın 17'si yanlıştır. Adım 2.068 satırla girip 1.880 satırla çıkar.

Bir önceki ders kümeyi 2.068 satıra indirdi ve kalan satırların her birinin ayrı bir ziyareti
temsil ettiğini varsaydı. Varsayım tutmuyor: bir bölüm ziyaret forma iki kez geçirilmiştir.

Bu ders iki ayrı işi ölçer. **Tekilleştirme** aynı kümenin içinde çalışır ve aynı kaydın iki
kopyasını tek satıra indirir. **Kimlik eşleştirme** iki kaynak arasında çalışır ve bir kaydı
diğerindeki varlığa bağlar; bir şey silmez, bir bağ kurar. Yanlış cevabın bedeli farklıdır: birinde
satır kaybolur, diğerinde satır **yanlış yere yazılır**.

- **VZ5.** Küme bir önceki dersten devralınır: kimlik alanı boş 18 satır elenmiş, eksik okumalı 204
  satır işaretlenip doldurulmuştur. Aynı tohum, aynı satırlar; küme yine `node` ile **modellenir**
  ve **kurgudur**.
- **VZ6.** Yinelenme aynı ziyaretin ikinci kez girilmesidir; bir abonenin iki dönemde okunması
  yinelenme değildir. Bu ayrım Elektronik Tablolarla Analiz kursunda kurulmuştur, verili sayılır.
- **VZ7.** Tekilleştirme ölçütü satırdan üretilen bir **anahtar metnidir**. Ölçütün hangi alanları
  kapsadığı ve metni nasıl karşılaştırdığı iki ayrı karardır.
- **VZ8.** Benzerlik **eşiği** bir karardır. Örtüşme oranı kayan noktalı bir sayıdır ve eşitlik
  işleciyle değil, yazılı bir eşikle karşılaştırılır.

#### Ölçüt Değişince Kaç Satır Birleşiyor

```js
// tekil.mjs — bir onceki dersin ham kumesi MODELIDIR: her alan saha formundan
// geldigi haliyle metindir. Kume kurgudur: bolgesel olcum aginin 1400 abonesi
// ve saha ziyaret kayitlari; ayni tohum, ayni satirlar.
export const TOHUM = 20260305, ABONE = 1400;
const karistir = (x) => { x = Math.imul(x ^ (x >>> 16), 2246822507);
  x = Math.imul(x ^ (x >>> 13), 3266489909); return (x ^ (x >>> 16)) >>> 0; };
export const uretec = (t) => { let s = karistir(t);
  return () => (s = (s * 1664525 + 1013904223) >>> 0) / 4294967296; };
export const ayrik = (u, w) => { let t = 0;
  for (let i = 0; i < w.length; i++) { t += w[i]; if (u < t) return i; } return w.length - 1; };
// Son alan: ziyaretin okuma uretmeme orani. Bolgeler bu oranda ayrisir.
export const BOLGE = [["kuzey", "KZ", 0.28, 21, 0.06], ["guney", "GN", 0.22, 17, 0.03],
  ["dogu", "DG", 0.18, 26, 0.27], ["bati", "BT", 0.14, 14, 0.03],
  ["merkez", "MR", 0.18, 23, 0.08]]
  .map(([ad, kod, pay, taban, eksik]) => ({ ad, kod, pay, taban, eksik }));
const SOKAK = ["Çınar", "Söğüt", "Işık", "Ihlamur", "Zeytin", "Menekşe", "Palmiye", "Ceviz"];
const TARIFE = ["Mesken", "mesken", "MESKEN", " Mesken ", "Konut", "konut", "Ticari",
  "ticari", "TİCARİ", "Is yeri", "İş Yeri", "isyeri", "Sanayi", "sanayi", "Tarimsal"];

export const HAM = [];
let bir = null;
for (let i = 0; i < ABONE; i++) {
  const r = uretec(TOHUM + i);
  const b = BOLGE[ayrik(r(), BOLGE.map((x) => x.pay))];
  const taban = Math.max(1.2, b.taban * Math.exp((r() + r() + r() - 1.5) * 0.62));
  const sayac = `${b.kod}-${String(1000 + i).padStart(4, "0")}`;
  const paylas = bir && bir.b === b.ad && r() < 0.03;   // ayni binada ikinci sayac
  const sk = paylas ? bir.sk : SOKAK[Math.floor(r() * SOKAK.length)];
  const kapi = paylas ? bir.kapi : 1 + Math.floor(r() * 60);
  const daire = paylas ? bir.daire : r() < 0.26 ? `/${1 + Math.floor(r() * 9)}` : "";
  bir = { b: b.ad, sk, kapi, daire };
  const endeks0 = 8000 + Math.floor(r() * 11000);
  for (const d of r() < 0.34 ? ["2026-01", "2026-02"] : ["2026-01"]) {
    const q = uretec(TOHUM + 7000 + HAM.length), u = q();
    const gercek = Math.round(taban * (d === "2026-02" ? 0.94 : 1) * 100) / 100;
    // Eksiklik uc bicimde gorunur: hucre bos, hucrede yalniz bosluk, "yok" yazili.
    const eksik = u < b.eksik ? ayrik(q(), [0.52, 0.19, 0.29]) : -1;
    const kayma = eksik < 0 && q() < 0.008;   // ondalik kaymasi: okuma on kat yazilmis
    const yazilan = kayma ? Math.round(gercek * 1000) / 100 : gercek;
    const okuma = eksik === 0 ? "" : eksik === 1 ? "  " : eksik === 2 ? "yok"
      : q() < 0.31 ? yazilan.toFixed(2).replace(".", ",") : yazilan.toFixed(2);
    const gun = 1 + Math.floor(q() * 27), ay = d.slice(5);
    const tb = ayrik(q(), [0.55, 0.21, 0.24]);
    const tarih = tb === 0 ? `${d}-${String(gun).padStart(2, "0")}`
      : tb === 1 ? `${String(gun).padStart(2, "0")}.${ay}.2026`
      : `${String(gun).padStart(2, "0")}/${ay}/2026`;
    const endeks = endeks0 + Math.round(gercek * (d === "2026-02" ? 2 : 1));
    const eb = q() < 0.22;
    const av = ayrik(q(), [0.30, 0.18, 0.18, 0.16, 0.18]);
    const kap = `${kapi}${daire}`;
    const M = `${b.ad[0].toUpperCase()}${b.ad.slice(1)}`, mUst = b.ad.toLocaleUpperCase("tr");
    const alt = sk.toLocaleLowerCase("tr"), ust = sk.toLocaleUpperCase("tr");
    const adres = av === 0 ? `${b.ad} mahallesi ${alt} sokak no: ${kap}`
      : av === 1 ? `${M} Mahallesi ${sk} Sokak No ${kap}`
      : av === 2 ? `${mUst} MAHALLESİ ${ust} SOKAK NO ${kap}`
      : av === 3 ? `  ${M}  Mahallesi  ${sk}  Sokak  No ${kap} `
      : `${b.ad} mah. ${alt} sk. no:${kap}`;
    HAM.push({ abone_no: 10001 + i, sayac_no: q() < 0.011 ? "" : sayac, bolge: b.ad,
      donem: d, okuma_metni: okuma,
      endeks_metni: eb ? `${Math.floor(endeks / 1000)}.${String(endeks % 1000).padStart(3, "0")}`
        : `${endeks}`,
      tarih_metni: tarih, tarife_metni: TARIFE[ayrik(q(), Array(15).fill(1 / 15))],
      adres_metni: adres, gercek_m3: gercek, yazilan_m3: eksik < 0 ? yazilan : null });
    if (q() < 0.09) {   // ayni ziyaret ikinci kez girilmis
      const on = HAM[HAM.length - 1], bicim = ayrik(q(), [0.34, 0.28, 0.38]);
      HAM.push({ ...on,
        sayac_no: bicim === 0 ? ` ${sayac}` : bicim === 1 ? sayac.toLowerCase() : sayac,
        adres_metni: av === 2 ? `${b.ad} mah. ${alt} sk. no:${kap}`
          : `${mUst} MAHALLESİ ${ust} SOKAK NO ${kap}`,
        tarife_metni: TARIFE[ayrik(q(), Array(15).fill(1 / 15))] });
    }
  }
}

// Okumayi sayiya cevirir. Ondalik ayraci virgul de olabilir; donusumun kendi
// tuzaklari bu konunun ucuncu dersinde olculur, burada tolere edilir.
export const sayiya = (t) => { const s = String(t).trim().replace(",", ".");
  return s === "" || !/^\d+(\.\d+)?$/.test(s) ? null : Number(s); };
export const medyan = (a) => { const l = [...a].sort((x, y) => x - y);
  return l.length % 2 ? l[(l.length - 1) / 2] : (l[l.length / 2 - 1] + l[l.length / 2]) / 2; };
export const y = (w) => (h) => h.map((v, j) => (j ? String(v).padStart(w[j])
  : String(v).padEnd(w[0]))).join("");

// Onceki dersin karari: kimlik alani bos olan satirlar elendi, eksik okumali
// satirlar isaretlenip bolge ortancasiyla dolduruldu. Bu adima 2068 satir girer.
const kimlikli = HAM.filter((s) => s.sayac_no.trim() !== "");
const olculen = kimlikli.filter((s) => sayiya(s.okuma_metni) !== null);
const bolgeMed = new Map(BOLGE.map((b) => [b.ad, medyan(olculen
  .filter((s) => s.bolge === b.ad).map((s) => sayiya(s.okuma_metni)))]));
export const GIREN = kimlikli.map((s) => sayiya(s.okuma_metni) === null
  ? { ...s, okuma_metni: bolgeMed.get(s.bolge).toFixed(2) } : s);

// Tekillestirme olcutu: satirdan bir anahtar metni uretir. Olcut degisince
// ayni kumeden farkli sayida satir kalir.
export const OLCUT = [
  ["O1 tam satir", (s) => [s.abone_no, s.sayac_no, s.donem, s.okuma_metni,
    s.endeks_metni, s.tarih_metni, s.tarife_metni, s.adres_metni].join("|")],
  ["O2 abone + donem + sayac (ham)", (s) => `${s.abone_no}|${s.donem}|${s.sayac_no}`],
  ["O3 abone + donem + sayac (duzeltilmis)",
    (s) => `${s.abone_no}|${s.donem}|${s.sayac_no.trim().toUpperCase()}`],
];
export const tekille = (l, k) => { const m = new Map();
  for (const s of l) if (!m.has(k(s))) m.set(k(s), s); return [...m.values()]; };

if (import.meta.url === `file://${process.argv[1]}`) {
  console.log(`model (tohum ${TOHUM}): onceki adimdan devralinan kume ${GIREN.length} satir`);
  const y1 = y([41, 8, 11]);
  console.log("\n" + y1(["tekillestirme olcutu", "kalan", "birlesen"]));
  for (const [ad, f] of OLCUT) { const l = tekille(GIREN, f);
    console.log(y1([ad, l.length, GIREN.length - l.length])); }

  // Ikinci girisler: O3 altinda ayni gruba dusen satirlarda hangi alan farkli yazilmis.
  const grup = new Map();
  for (const s of GIREN) { const a = OLCUT[2][1](s);
    grup.set(a, (grup.get(a) ?? []).concat([s])); }
  const cift = [...grup.values()].filter((l) => l.length > 1);
  const fark = (alan) => cift.filter((l) => new Set(l.map((s) => s[alan])).size > 1).length;
  const y2 = y([26, 8, 9]);
  console.log("\n" + y2([`${cift.length} ikinci giriste fark`, "kayit", "oran %"]));
  for (const alan of ["sayac_no", "adres_metni", "tarife_metni", "okuma_metni"])
    console.log(y2([alan, fark(alan), (100 * fark(alan) / cift.length).toFixed(1)]));
}
```

```
model (tohum 20260305): onceki adimdan devralinan kume 2068 satir

tekillestirme olcutu                        kalan   birlesen
O1 tam satir                                 2068          0
O2 abone + donem + sayac (ham)               2000         68
O3 abone + donem + sayac (duzeltilmis)       1880        188

188 ikinci giriste fark      kayit   oran %
sayac_no                       120     63.8
adres_metni                    188    100.0
tarife_metni                   173     92.0
okuma_metni                      0      0.0
```

Üç ölçüt üç sayı verdi. **Ö1 tam satır** en güvenli görünen ölçüttür ve hiçbir işi yapmaz; ihtiyat
burada sorunun tamamını olduğu gibi bırakır. **Ö2** sayaç kodunu geldiği haliyle karşılaştırır ve 68
satır birleştirir, **Ö3** aynı ölçütü kırpılmış ve harf birliğine getirilmiş kodla uygular ve 188'e
çıkar. Aradaki **120 satır** hangi alanların ölçüte gireceği kararının değil, o alanların **nasıl
karşılaştırılacağı** kararının bedelidir.

İkinci tablo nedenini söylüyor. 188 ikinci girişin **hepsinde** adres farklı yazılmıştır, yüzde
92'sinde tarife sınıfı, yüzde 63,8'inde sayaç kodu; değişmeyen tek alan okumadır. Ölçüm aynı kalmış,
**onu çevreleyen her metin yeniden yazılmıştır**. Yinelenmeyi ölçütün kapsadığı alanlar belirler,
satırın tamamı değil.

#### Kimlik Eşleştirme

```js
// eslesme.mjs — tekillestirmeden ayri bir is: kimlik eslestirme. Destek
// talepleri MODELIDIR; sayac kodu cagri sirasinda elle yazilmistir. Talebi
// aboneye baglamanin iki yolu karsilastirilir: kesin eslesme ve benzerlik esigi.
import { GIREN, OLCUT, tekille, uretec, ayrik, y } from "./tekil.mjs";
const TEKIL = tekille(GIREN, OLCUT[2][1]);

// Sayac kutugu tekillestirilmis kumeden cikarilir; kodu hic okunakli yazilmamis
// aboneler kutukte yoktur.
const KOD = new Map();
for (const s of TEKIL) KOD.set(s.sayac_no.trim().toUpperCase(), s.abone_no);
const sade = (t) => t.trim().toLocaleUpperCase("tr").replace(/[^A-Z0-9]/g, "");
const SADE = new Map([...KOD.keys()].map((k) => [sade(k), k]));

// 240 destek talebi. Operatorun yazim hatasi bes tipte: dogru, kucuk harf ve
// bastaki bosluk, ayracsiz, iki rakam yer degistirmis, son rakam eksik.
const kodlar = [...KOD.keys()], TALEP = [], tr = uretec(20260305 + 50000);
for (let i = 0; i < 240; i++) {
  const dogru = kodlar[Math.floor(tr() * kodlar.length)];
  const tip = ayrik(tr(), [0.42, 0.18, 0.14, 0.14, 0.12]);
  const rk = dogru.slice(3), takas = rk.slice(0, 2) + rk[3] + rk[2];
  TALEP.push({ talep_no: 5001 + i, tip, gercek_kod: dogru,
    yazilan: tip === 0 ? dogru : tip === 1 ? ` ${dogru.toLowerCase()}`
      : tip === 2 ? dogru.replace("-", "") : tip === 3 ? `${dogru.slice(0, 3)}${takas}`
      : dogru.slice(0, -1) });
}
console.log(`model: ${TEKIL.length} tekil olcum satiri, ${KOD.size} tekil sayac kodu, ` +
  `${TALEP.length} destek talebi`);

const y1 = y([34, 9, 9, 12]);
console.log("\n" + y1(["kesin eslesme", "baglanan", "yanlis", "baglanmayan"]));
for (const [ad, f] of [["E1 ham metin", (t) => KOD.get(t.yazilan.trim().toUpperCase())],
  ["E2 sadelestirilmis kod", (t) => KOD.get(SADE.get(sade(t.yazilan)))]]) {
  const bag = TALEP.map((t) => [t, f(t)]).filter(([, a]) => a !== undefined);
  console.log(y1([ad, bag.length, bag.filter(([t, a]) => a !== KOD.get(t.gercek_kod)).length,
    TALEP.length - bag.length]));
}

// Benzerlik: uc harfli parcalarin ortusme orani. Esik bir karardir; kayan
// noktali oran esitlik islecine sokulmaz, esikle karsilastirilir.
const uclu = (t) => { const s = `  ${t}  `, r = new Set();
  for (let i = 0; i + 3 <= s.length; i++) r.add(s.slice(i, i + 3)); return r; };
const ortusme = (a, b) => { let o = 0; for (const g of a) if (b.has(g)) o++;
  return (2 * o) / (a.size + b.size); };
const sadeler = [...SADE.keys()], gram = new Map(sadeler.map((k) => [k, uclu(k)]));
const kalanlar = TALEP.filter((t) => !SADE.has(sade(t.yazilan)));
const y2 = y([9, 13, 9, 17, 10]);
console.log("\n" + y2(["esik", "ek baglanan", "yanlis", "birden cok aday", "bagsiz"]));
for (const esik of [0.60, 0.70, 0.80, 0.90]) {
  let ek = 0, yanlis = 0, belirsiz = 0;
  for (const t of kalanlar) {
    const g = uclu(sade(t.yazilan));
    let en = 0, aday = [];
    for (const k of sadeler) { const d = ortusme(g, gram.get(k));
      if (d > en + 1e-9) { en = d; aday = [k]; } else if (Math.abs(d - en) < 1e-9) aday.push(k); }
    if (en < esik) continue;
    if (aday.length > 1) { belirsiz++; continue; }
    ek++; if (SADE.get(aday[0]) !== t.gercek_kod) yanlis++;
  }
  console.log(y2([esik.toFixed(2), ek, yanlis, belirsiz, kalanlar.length - ek - belirsiz]));
}
console.log(`\ntek basina benzerlik esigi kesin eslesmenin biraktigi ${kalanlar.length} talebi ` +
  `cozmuyor: 0.80 esiginde kurdugu 18 bagin 17'si yanlis abonedir, cunku iki rakami yer ` +
  `degistirmis bir kod baska bir abonenin gecerli kodudur`);
console.log(`satir hesabi: giren ${GIREN.length}, cikan ${TEKIL.length}; ` +
  `${GIREN.length - TEKIL.length} satir birlesti, 0 satir sessizce dustu. Yan defter: ` +
  `240 talebin 192'si baglandi, 23'u yanlis aboneye, 44'u bagsiz, 4'u belirsiz kaldi`);
```

```
model: 1880 tekil olcum satiri, 1392 tekil sayac kodu, 240 destek talebi

kesin eslesme                      baglanan   yanlis baglanmayan
E1 ham metin                            149        6          91
E2 sadelestirilmis kod                  174        6          66

esik       ek baglanan   yanlis  birden cok aday    bagsiz
0.60                20       19               46         0
0.70                18       17                4        44
0.80                18       17                4        44
0.90                 2        2                0        64

tek basina benzerlik esigi kesin eslesmenin biraktigi 66 talebi cozmuyor: 0.80 esiginde kurdugu 18 bagin 17'si yanlis abonedir, cunku iki rakami yer degistirmis bir kod baska bir abonenin gecerli kodudur
satir hesabi: giren 2068, cikan 1880; 188 satir birlesti, 0 satir sessizce dustu. Yan defter: 240 talebin 192'si baglandi, 23'u yanlis aboneye, 44'u bagsiz, 4'u belirsiz kaldi
```

Kesin eşleşme iki kararla iki sayı veriyor: ham metinle 149, sadeleştirilmiş kodla **174** talep
bağlanır — yirmi beş talep yalnızca ayraç yüzünden bağlanamıyordu. Asıl uyarı ikinci sütundadır: iki
kararda da **altı talep yanlış aboneye bağlanır**. İki rakamı yer değiştirmiş bir sayaç kodu bozuk
bir metin değil, **başka bir abonenin geçerli kodudur**; kesin eşleşme onu tereddütsüz bağlar.

Benzerlik eşiği kalan 66 talebi çözmüyor. Eşik 0,80'de kurulan 18 bağın **17'si yanlıştır**; 0,90'a
çekilince bağ sayısı 2'ye iner ve **ikisi de yanlıştır**; 0,60'a gevşetilince yalnız iki bağ daha
kurulur, buna karşılık 46 talep birden çok eşit adayla belirsiz kalır. Kısa ve yapılı bir kimlik metninde benzerlik,
doğru kaydı bulmaktan çok **komşu kaydı** bulur.

Adımın hesabı şudur: **giren 2.068 satır, çıkan 1.880 satır.** Aradaki 188 satır birleşti, sessizce
düşen satır yoktur. Yan defterde 240 talebin 192'si bağlandı, **23'ü yanlış aboneye**, 44'ü bağsız
kaldı ve 4'ü birden çok eşit aday arasında belirsiz kaldı. Bu 23 kayıt satır sayısını hiç oynatmaz;
bundan sonraki her talep sayımında yanlış abonenin altında görünür.

#### Özet

- Tekilleştirme ölçütü satırdan üretilen bir anahtar metnidir; aynı 2.068 satırlık kümede üç ölçüt
  2.068, 2.000 ve 1.880 satır bırakır.
- Tam satır karşılaştırması hiçbir satırı birleştirmez: ikinci girişi olan 188 kaydın hepsinde adres,
  yüzde 92'sinde tarife sınıfı, yüzde 63,8'inde sayaç kodu farklı yazılmıştır ve okuma değeri hiç
  değişmemiştir. Alanların nasıl karşılaştırıldığı kararı tek başına 120 satır oynatır.
- Kimlik eşleştirme ayrı bir iştir: 240 talebin 149'u ham metinle, 174'ü sadeleştirilmiş kodla
  bağlanır ve iki kararda da altısı yanlış aboneye gider.
- Benzerlik eşiği kalan 66 talebi çözmez: 0,80'de 18 bağın 17'si, 0,90'da 2 bağın 2'si yanlıştır;
  0,60'ta 46 talep birden çok eşit adayla belirsiz kalır.
- Adımın hesabı: giren 2.068, çıkan 1.880; 188 birleşti, 0 sessizce düştü, yan defterde 23 kayıt
  yanlış aboneye yazıldı.

#### Sonraki Adım

Kalan 1.880 satırın okuma, endeks ve tarih alanları hâlâ metindir ve hesaba girmeden önce sayıya ve
tarihe çevrilmek zorundadır. Dönüşüm zararsız görünür: hiçbir uyarı vermez, hiçbir işlem durmaz.
Sonraki ders aynı kümeyi iki dönüşüm kararıyla çevirir ve yaygın çağrının kaç kaydı hatasız
görünerek bozduğunu, kaç satırı da hiç haber vermeden düşürdüğünü sayar.

### Tip Dönüşümleri

Source: https://academia.sh/tr/kurslar/veri-hazirlama/tip-donusumleri

Devralınan 1.880 satırda okuma alanının 542'si virgüllü, endeks alanının 407'si binlik ayraçlı yazılmıştır. Yaygın sayıya çevirme çağrısı hiçbir hata üretmez ve hiçbir işlemi durdurmaz, ama 827 satırı bozar: 541 okuma ile 407 endeks yanlış değere döner, raporlanan tüketim toplamı 263,6 m³ eksik çıkar ve ortalama endeks 13.481'den 10.558'e iner. Tarih alanında biçimi tanımayan bir ayrıştırıcı 1.880 satırın 1.054'ünü doğru çözer, 361'ini yanlış aya yazar, 465'ini hiç çözemez ve bu satırlar tarih süzgecinden sessizce düşer; ocak döneminde yapılan ziyaret sayısı 1.388 yerine 805 raporlanır. Biçimi önce tanıyan dönüşüm aynı kümede sıfır bozuk kayıt ve sıfır kayıp satır verir.

Bir önceki ders kümeyi 1.880 tekil satıra indirdi. Bu satırların okuma, endeks ve tarih alanları
hâlâ metindir ve hesaba girmeleri için çevrilmeleri gerekir.

Dönüşüm bu konudaki en sessiz adımdır. Silme satır sayısını düşürür ve bir şeyin eksildiği görülür.
Dönüşüm ise **hiçbir iz bırakmaz**: yaygın bir sayıya çevirme çağrısı her metinden bir sayı çıkarır,
hata vermez, işlemi durdurmaz ve sonuç kümesi düzgün görünür. Dersin sayısı budur: **kaç kayıt
hatasız görünerek bozuldu** ve **kaç satır hiç haber verilmeden düştü**.

- **VZ9.** Küme bir önceki dersten devralınır: 188 satır birleşmiş, 1.880 tekil satır kalmıştır.
  Aynı tohum, aynı satırlar; küme `node` ile **modellenir** ve **kurgudur**.
- **VZ10.** Aynı sayı iki ayraç geleneğiyle yazılabilir: okuma alanında ondalık ayracı nokta ya da
  virgül, endeks alanında binlik ayracı yok ya da noktadır. **Aynı karakter iki farklı iş** yapar.
- **VZ11.** **Sessiz dönüşüm**, hata üretmeden yanlış değer üreten dönüşümdür. Bozulan kayıt, iki
  dönüşümün sonucu arasındaki farkın 0,005 eşiğini aşmasıyla sayılır; kayan noktalı iki değer
  eşitlik işleciyle karşılaştırılmaz.
- **VZ12.** Çözülemeyen bir tarih hata değil, **süzgeçten düşen bir satırdır**: uyarı üretmez,
  yalnız sonraki sayımda görünmez.

#### Sayıya Çevirme

```js
// tip.mjs — ayni ham kume MODELIDIR: her alan saha formundan geldigi haliyle
// metindir. Kume kurgudur: bolgesel olcum aginin 1400 abonesi ve saha ziyaret
// kayitlari; ayni tohum, ayni satirlar.
export const TOHUM = 20260305, ABONE = 1400;
const karistir = (x) => { x = Math.imul(x ^ (x >>> 16), 2246822507);
  x = Math.imul(x ^ (x >>> 13), 3266489909); return (x ^ (x >>> 16)) >>> 0; };
export const uretec = (t) => { let s = karistir(t);
  return () => (s = (s * 1664525 + 1013904223) >>> 0) / 4294967296; };
export const ayrik = (u, w) => { let t = 0;
  for (let i = 0; i < w.length; i++) { t += w[i]; if (u < t) return i; } return w.length - 1; };
// Son alan: ziyaretin okuma uretmeme orani. Bolgeler bu oranda ayrisir.
export const BOLGE = [["kuzey", "KZ", 0.28, 21, 0.06], ["guney", "GN", 0.22, 17, 0.03],
  ["dogu", "DG", 0.18, 26, 0.27], ["bati", "BT", 0.14, 14, 0.03],
  ["merkez", "MR", 0.18, 23, 0.08]]
  .map(([ad, kod, pay, taban, eksik]) => ({ ad, kod, pay, taban, eksik }));
const SOKAK = ["Çınar", "Söğüt", "Işık", "Ihlamur", "Zeytin", "Menekşe", "Palmiye", "Ceviz"];
const TARIFE = ["Mesken", "mesken", "MESKEN", " Mesken ", "Konut", "konut", "Ticari",
  "ticari", "TİCARİ", "Is yeri", "İş Yeri", "isyeri", "Sanayi", "sanayi", "Tarimsal"];

export const HAM = [];
let bir = null;
for (let i = 0; i < ABONE; i++) {
  const r = uretec(TOHUM + i);
  const b = BOLGE[ayrik(r(), BOLGE.map((x) => x.pay))];
  const taban = Math.max(1.2, b.taban * Math.exp((r() + r() + r() - 1.5) * 0.62));
  const sayac = `${b.kod}-${String(1000 + i).padStart(4, "0")}`;
  const paylas = bir && bir.b === b.ad && r() < 0.03;   // ayni binada ikinci sayac
  const sk = paylas ? bir.sk : SOKAK[Math.floor(r() * SOKAK.length)];
  const kapi = paylas ? bir.kapi : 1 + Math.floor(r() * 60);
  const daire = paylas ? bir.daire : r() < 0.26 ? `/${1 + Math.floor(r() * 9)}` : "";
  bir = { b: b.ad, sk, kapi, daire };
  const endeks0 = 8000 + Math.floor(r() * 11000);
  for (const d of r() < 0.34 ? ["2026-01", "2026-02"] : ["2026-01"]) {
    const q = uretec(TOHUM + 7000 + HAM.length), u = q();
    const gercek = Math.round(taban * (d === "2026-02" ? 0.94 : 1) * 100) / 100;
    // Eksiklik uc bicimde gorunur: hucre bos, hucrede yalniz bosluk, "yok" yazili.
    const eksik = u < b.eksik ? ayrik(q(), [0.52, 0.19, 0.29]) : -1;
    const kayma = eksik < 0 && q() < 0.008;   // ondalik kaymasi: okuma on kat yazilmis
    const yazilan = kayma ? Math.round(gercek * 1000) / 100 : gercek;
    const okuma = eksik === 0 ? "" : eksik === 1 ? "  " : eksik === 2 ? "yok"
      : q() < 0.31 ? yazilan.toFixed(2).replace(".", ",") : yazilan.toFixed(2);
    const gun = 1 + Math.floor(q() * 27), ay = d.slice(5);
    const tb = ayrik(q(), [0.55, 0.21, 0.24]);
    const tarih = tb === 0 ? `${d}-${String(gun).padStart(2, "0")}`
      : tb === 1 ? `${String(gun).padStart(2, "0")}.${ay}.2026`
      : `${String(gun).padStart(2, "0")}/${ay}/2026`;
    const endeks = endeks0 + Math.round(gercek * (d === "2026-02" ? 2 : 1));
    const eb = q() < 0.22;
    const av = ayrik(q(), [0.30, 0.18, 0.18, 0.16, 0.18]);
    const kap = `${kapi}${daire}`;
    const M = `${b.ad[0].toUpperCase()}${b.ad.slice(1)}`, mUst = b.ad.toLocaleUpperCase("tr");
    const alt = sk.toLocaleLowerCase("tr"), ust = sk.toLocaleUpperCase("tr");
    const adres = av === 0 ? `${b.ad} mahallesi ${alt} sokak no: ${kap}`
      : av === 1 ? `${M} Mahallesi ${sk} Sokak No ${kap}`
      : av === 2 ? `${mUst} MAHALLESİ ${ust} SOKAK NO ${kap}`
      : av === 3 ? `  ${M}  Mahallesi  ${sk}  Sokak  No ${kap} `
      : `${b.ad} mah. ${alt} sk. no:${kap}`;
    HAM.push({ abone_no: 10001 + i, sayac_no: q() < 0.011 ? "" : sayac, bolge: b.ad,
      donem: d, okuma_metni: okuma,
      endeks_metni: eb ? `${Math.floor(endeks / 1000)}.${String(endeks % 1000).padStart(3, "0")}`
        : `${endeks}`,
      tarih_metni: tarih, tarife_metni: TARIFE[ayrik(q(), Array(15).fill(1 / 15))],
      adres_metni: adres, gercek_m3: gercek, yazilan_m3: eksik < 0 ? yazilan : null });
    if (q() < 0.09) {   // ayni ziyaret ikinci kez girilmis
      const on = HAM[HAM.length - 1], bicim = ayrik(q(), [0.34, 0.28, 0.38]);
      HAM.push({ ...on,
        sayac_no: bicim === 0 ? ` ${sayac}` : bicim === 1 ? sayac.toLowerCase() : sayac,
        adres_metni: av === 2 ? `${b.ad} mah. ${alt} sk. no:${kap}`
          : `${mUst} MAHALLESİ ${ust} SOKAK NO ${kap}`,
        tarife_metni: TARIFE[ayrik(q(), Array(15).fill(1 / 15))] });
    }
  }
}

// Okumayi sayiya cevirir. Ondalik ayraci virgul de olabilir; donusumun kendi
// tuzaklari bu konunun ucuncu dersinde olculur, burada tolere edilir.
export const sayiya = (t) => { const s = String(t).trim().replace(",", ".");
  return s === "" || !/^\d+(\.\d+)?$/.test(s) ? null : Number(s); };
export const medyan = (a) => { const l = [...a].sort((x, y) => x - y);
  return l.length % 2 ? l[(l.length - 1) / 2] : (l[l.length / 2 - 1] + l[l.length / 2]) / 2; };
export const y = (w) => (h) => h.map((v, j) => (j ? String(v).padStart(w[j])
  : String(v).padEnd(w[0]))).join("");

// Onceki dersin karari: kimlik alani bos olan satirlar elendi, eksik okumali
// satirlar isaretlenip bolge ortancasiyla dolduruldu. Bu adima 2068 satir girer.
const kimlikli = HAM.filter((s) => s.sayac_no.trim() !== "");
const olculen = kimlikli.filter((s) => sayiya(s.okuma_metni) !== null);
const bolgeMed = new Map(BOLGE.map((b) => [b.ad, medyan(olculen
  .filter((s) => s.bolge === b.ad).map((s) => sayiya(s.okuma_metni)))]));
export const GIREN = kimlikli.map((s) => sayiya(s.okuma_metni) === null
  ? { ...s, okuma_metni: bolgeMed.get(s.bolge).toFixed(2) } : s);

// Onceki dersin karari: abone, donem ve duzeltilmis sayac kodundan uretilen
// anahtarla tekillestirildi; 188 satir birlesti. Bu adima 1880 satir girer.
const tekille = (l, k) => { const m = new Map();
  for (const s of l) if (!m.has(k(s))) m.set(k(s), s); return [...m.values()]; };
export const GIREN2 = tekille(GIREN,
  (s) => `${s.abone_no}|${s.donem}|${s.sayac_no.trim().toUpperCase()}`);

// Iki donusum karari. D1 yaygin cagridir: hicbir hata uretmez, her metinden bir
// sayi cikarir. D2 bicimi once tanir, tanimadigi metni sayiya cevirmez.
export const D1 = { okuma: (t) => parseFloat(t), endeks: (t) => parseFloat(t) };
export const D2 = { okuma: (t) => Number(t.trim().replace(",", ".")),
  endeks: (t) => Number(t.replace(".", "")) };
export const topla = (a) => a.reduce((x, z) => x + z, 0);
export const ESIK = 0.005;   // raporlanan iki ondalik basamagin yarisi

if (import.meta.url === `file://${process.argv[1]}`) {
  const say = (f) => GIREN2.filter(f).length;
  console.log(`model (tohum ${TOHUM}): onceki adimdan devralinan kume ${GIREN2.length} satir`);
  const y1 = y([36, 8]);
  console.log("\n" + y1(["alanin yazim bicimi", "satir"]));
  console.log(y1(["okuma, ondalik ayraci nokta", say((s) => !s.okuma_metni.includes(","))]));
  console.log(y1(["okuma, ondalik ayraci virgul", say((s) => s.okuma_metni.includes(","))]));
  console.log(y1(["endeks, binlik ayraci yok", say((s) => !s.endeks_metni.includes("."))]));
  console.log(y1(["endeks, binlik ayraci nokta", say((s) => s.endeks_metni.includes("."))]));

  const y2 = y([32, 10, 9, 13, 12]);
  console.log("\n" + y2(["sayisal donusum", "bozulan", "hata", "toplam m3", "ort endeks"]));
  for (const [ad, d] of [["D1 yaygin cagri", D1], ["D2 bicim farkinda", D2]]) {
    const bozuk = GIREN2.filter((s) => Math.abs(d.okuma(s.okuma_metni)
      - D2.okuma(s.okuma_metni)) > ESIK || Math.abs(d.endeks(s.endeks_metni)
      - D2.endeks(s.endeks_metni)) > ESIK).length;
    console.log(y2([ad, bozuk, 0, topla(GIREN2.map((s) => d.okuma(s.okuma_metni))).toFixed(1),
      (topla(GIREN2.map((s) => d.endeks(s.endeks_metni))) / GIREN2.length).toFixed(0)]));
  }
  const fark = topla(GIREN2.map((s) => D2.okuma(s.okuma_metni) - D1.okuma(s.okuma_metni)));
  console.log(`\nD1 hicbir hata uretmedi ve hicbir islem durmadi; okuma alaninda ` +
    `${say((s) => Math.abs(D1.okuma(s.okuma_metni) - D2.okuma(s.okuma_metni)) > ESIK)} kaydi, ` +
    `endeks alaninda ${say((s) => Math.abs(D1.endeks(s.endeks_metni) -
      D2.endeks(s.endeks_metni)) > ESIK)} kaydi bozdu. Raporlanan tuketim toplami ` +
    `${fark.toFixed(1)} m3 eksik cikiyor (karsilastirma ${ESIK} m3 esigiyle yapilir, ` +
    `kayan noktali degerler esitlik islecine sokulmaz)`);
}
```

```
model (tohum 20260305): onceki adimdan devralinan kume 1880 satir

alanin yazim bicimi                    satir
okuma, ondalik ayraci nokta             1338
okuma, ondalik ayraci virgul             542
endeks, binlik ayraci yok               1473
endeks, binlik ayraci nokta              407

sayisal donusum                    bozulan     hata    toplam m3  ort endeks
D1 yaygin cagri                        827        0      41511.2       10558
D2 bicim farkinda                        0        0      41774.8       13481

D1 hicbir hata uretmedi ve hicbir islem durmadi; okuma alaninda 541 kaydi, endeks alaninda 407 kaydi bozdu. Raporlanan tuketim toplami 263.6 m3 eksik cikiyor (karsilastirma 0.005 m3 esigiyle yapilir, kayan noktali degerler esitlik islecine sokulmaz)
```

İlk tablo alanların nasıl yazıldığını sayıyor: okumaların 542'sinde ondalık ayracı virgüldür,
endekslerin 407'sinde binlik ayracı noktadır; ikisi de tek bir alanın içinde, formu dolduran
kişinin alışkanlığına göre karışır.

İkinci tablo dersin sayısıdır. **D1** yaygın çağrıdır ve **hiçbir hata üretmez** — hata sütunu iki
satırda da sıfırdır. Yine de 827 satırı bozar. Virgüllü bir okuma metninde çağrı virgüle kadar okur
ve durur: 12,74 değeri **12** olur, ondalık kısım sessizce atılır. Binlik ayraçlı bir endekste ise
nokta ondalık ayracı sanılır: 12.480 değeri **12,48** olur. Birincisi küçük bir yanlışlıktır,
ikincisi **bin kat** yanlıştır ve ikisi de aynı sessizlikle olur.

Toplamlar bunu doğruluyor. Raporlanan tüketim toplamı 263,6 m³ eksik çıkar — kimsenin fark
etmeyeceği bir sapma. Ortalama endeks ise 13.481'den **10.558'e** iner; endeks tüketim farkını
hesaplamakta kullanılan alandır ve bu kadar bozulmuş bir alanla yapılacak her fark hesabı
anlamsızdır. D2 sıfır bozuk kayıt verir; tek yaptığı, çevirmeden önce **biçimi tanımaktır**.

#### Tarihe Çevirme

```js
// tarih.mjs — ayni kume, bu kez tarih alani. Iki ayristirma karari: biri
// yaziliş bicimini bilmeden ilk alani ay sayar, digeri bicimi once tanir.
import { GIREN2, y } from "./tip.mjs";

// T1: bicim bilinmiyor, ilk alan ay sayiliyor (dort haneli ilk alan yil demektir).
// T2: bicim once taninir; ayracli yazimda alanlar gun, ay, yil sirasindadir.
const T1 = (t) => { const p = t.split(/[-./]/).map(Number);
  const [yil, ay, gun] = p[0] > 999 ? p : [p[2], p[0], p[1]];
  return ay >= 1 && ay <= 12 && gun >= 1 && gun <= 31 ? { yil, ay, gun } : null; };
const T2 = (t) => { const p = t.split(/[-./]/).map(Number);
  const [yil, ay, gun] = /^\d{4}-/.test(t) ? p : [p[2], p[1], p[0]];
  return ay >= 1 && ay <= 12 && gun >= 1 && gun <= 31 ? { yil, ay, gun } : null; };
const iki = (n) => String(n).padStart(2, "0");

const y1 = y([26, 9, 11, 11, 11]);
console.log(y1(["tarih ayristirma", "gecersiz", "yanlis ay", "dogru", "hata"]));
for (const [ad, f] of [["T1 bicim bilinmiyor", T1], ["T2 bicim taninir", T2]]) {
  let g = 0, ya = 0, d = 0;
  for (const s of GIREN2) { const t = f(s.tarih_metni);
    if (t === null) g++; else if (iki(t.ay) !== s.donem.slice(5)) ya++; else d++; }
  console.log(y1([ad, g, ya, d, 0]));
}

// Ayni kumeden raporlanan sayi: ocak doneminde yapilan ziyaret sayisi.
const ocak = GIREN2.filter((s) => s.donem === "2026-01").length;
const t1Ocak = GIREN2.filter((s) => { const t = T1(s.tarih_metni);
  return t !== null && t.ay === 1; }).length;
const t1Gecerli = GIREN2.filter((s) => T1(s.tarih_metni) !== null).length;
console.log(`\nocak doneminde yapilan ziyaret: donem alanina gore ${ocak}, ` +
  `T1'in cozdugu tarihe gore ${t1Ocak}; T1 ${GIREN2.length - t1Gecerli} satiri hic ` +
  `cozemedi ve bu satirlar tarih suzgecinden sessizce dustu`);
const y2 = y([22, 10, 16]);
console.log("\n" + y2(["tarihin yazim bicimi", "satir", "T1 cozer mi"]));
for (const [ad, f, c] of [["yil-ay-gun", (t) => /^\d{4}-/.test(t), "evet"],
  ["gun.ay.yil", (t) => t.includes("."), "gun <= 12 ise"],
  ["gun/ay/yil", (t) => t.includes("/"), "gun <= 12 ise"]])
  console.log(y2([ad, GIREN2.filter((s) => f(s.tarih_metni)).length, c]));

console.log(`\nsatir hesabi: giren ${GIREN2.length}, cikan ${GIREN2.length}; 0 satir elendi, ` +
  `0 satir sessizce dustu. Yaygin donusum secilseydi ${GIREN2.length - t1Gecerli} satir ` +
  `sessizce duser, 948 alan hatasiz gorunerek bozulur ve hicbir uyari cikmazdi`);
```

```
tarih ayristirma           gecersiz  yanlis ay      dogru       hata
T1 bicim bilinmiyor             465        361       1054          0
T2 bicim taninir                  0          0       1880          0

ocak doneminde yapilan ziyaret: donem alanina gore 1388, T1'in cozdugu tarihe gore 805; T1 465 satiri hic cozemedi ve bu satirlar tarih suzgecinden sessizce dustu

tarihin yazim bicimi       satir     T1 cozer mi
yil-ay-gun                  1022            evet
gun.ay.yil                   389   gun <= 12 ise
gun/ay/yil                   469   gun <= 12 ise

satir hesabi: giren 1880, cikan 1880; 0 satir elendi, 0 satir sessizce dustu. Yaygin donusum secilseydi 465 satir sessizce duser, 948 alan hatasiz gorunerek bozulur ve hicbir uyari cikmazdi
```

Tarih alanı aynı sorunun daha sert biçimidir, çünkü burada yanlış değer bazen **hiçbir değer**
olur. **T1** yazılış biçimini bilmez ve yaygın bir varsayımı uygular: ilk alan aydır. 1.880 satırın
1.054'ünü doğru çözer, 361'ini **yanlış aya** yazar ve 465'ini hiç çözemez.

Üç sonuç farklı davranır. Yanlış aya yazılan satır kümede kalır, düzgün bir tarih taşır ve yanlış
olduğu hiçbir yerden anlaşılmaz — 1 Şubat'ta yapılan bir ziyaret 2 Ocak'ta yapılmış görünür.
Çözülemeyen satır ise hata mesajı üretmez, yalnız tarihe göre süzen ilk işlemde **listeden çıkar**.

Raporlanan sayı bu üçünün toplamıdır. Ocak döneminde yapılan ziyaret sayısı dönem alanına göre
1.388, T1'in çözdüğü tarihlere göre **805**'tir. Aradaki 583 ziyaretin bir bölümü yanlış aya
yazılmış, bir bölümü görünmez olmuştur.

Adımın hesabı şudur: **giren 1.880 satır, çıkan 1.880 satır.** Hiçbir satır elenmedi, hiçbir satır
sessizce düşmedi, hiçbir kayıt bozulmadı. Bu adımın bedeli, yapılmadığında ortaya çıkan sayıdır:
yaygın dönüşüm seçilseydi **465 satır sessizce düşer, 948 alan hatasız görünerek bozulurdu**.

#### Özet

- Aynı alan iki ayraç geleneğiyle yazılabilir: 1.880 satırın 542'sinde ondalık ayracı virgül,
  407'sinde binlik ayracı noktadır.
- Yaygın sayıya çevirme çağrısı hiçbir hata üretmez ve 827 satırı bozar: virgüllü okuma ondalık
  kısmını yitirir, binlik ayraçlı endeks bin kat küçülür.
- Etkisi alana göre değişir: tüketim toplamı 263,6 m³ eksik çıkar, ortalama endeks 13.481'den
  10.558'e iner.
- Biçimi tanımayan bir tarih ayrıştırıcısı 1.054 satırı doğru çözer, 361'ini yanlış aya yazar,
  465'ini hiç çözemez; ocak ziyaretleri 1.388 yerine 805 raporlanır.
- Adımın hesabı: giren 1.880, çıkan 1.880; 0 elendi, 0 sessizce düştü, 0 kayıt bozuldu — yaygın
  dönüşüm seçilseydi 465 satır düşer ve 948 alan bozulurdu.

#### Sonraki Adım

Bütün alanlar artık doğru tiptedir ve 1.880 okumanın hepsi bir sayıdır. Sayı olmaları doğru
oldukları anlamına gelmiyor: bir bölümü ondalık noktası kayarak on kat büyük yazılmıştır ve dönüşüm
bunları sadakatle çevirmiştir. Sonraki ders bu değerleri çeyrekler açıklığı ve standart puanla
tarar, iki yöntemin işaretlediği kümelerin örtüşmeyen kısmını sayar ve işaretlenenleri ayırmanın
ortalamayı ne kadar oynattığını ölçer.

### Aykırı Değerler

Source: https://academia.sh/tr/kurslar/veri-hazirlama/aykiri-degerler

Devralınan 1.880 okumada çeyrekler açıklığı 64 satır, standart puan 14 satır işaretler; ikinci küme birincinin alt kümesidir ve örtüşmeyen kısım 50 satırdır. Fark yöntemlerin duyarlılığından değil, standart puanın kendi aykırısını gizlemesinden gelir: 1.880 satırda standart sapma 19,68 m³'tür, işaretli 64 satır ayrıldığında 6,35 m³'e iner ve aynı üç sigma ölçütü bu kez sıfır satır işaretler. Modelde ondalık noktası kaymış 15 okumanın 15'ini çeyrekler açıklığı, 14'ünü standart puan yakalar. İşaretlileri ayırmak ortalamayı 22,22 m³'ten 20,14 m³'e (2,08 m³, yüzde 9,4), medyanı 20,31'den 19,85'e, toplamı 41.774,8 m³'ten 36.578,3 m³'e çeker. Adım 1.880 satırla girip 1.816 satırla çıkar.

Bir önceki ders bütün alanları doğru tipe çevirdi ve 1.880 okumanın hepsi artık bir sayıdır. Sayı
olmak doğru olmak değildir: bir bölüm okuma forma ondalık noktası kayarak on kat büyük yazılmıştır
ve dönüşüm bunları sadakatle çevirmiştir. Bu değerler kümede uç noktalarda durur, hiçbir kural
ihlal etmez ve yalnız **komşularına göre** tuhaftır.

Bu ders o uçları iki yöntemle tarar. **Çeyrekler açıklığı** sıralamaya dayanır ve açıklığın bir
katsayısı kadar dışarıya çit çeker; **standart puan** ortalamaya ve standart sapmaya dayanır. İkisi
de aynı soruyu sorar ve farklı sayılar verir. Dersin sayısı bu iki kümenin **örtüşmeyen kısmı** ve
işaretlenenleri ayırmanın raporlanan sayıyı **ne kadar oynattığıdır**.

- **VZ13.** Küme önceki iki dersten devralınır: 188 satır birleşmiş, biçim farkında dönüşümle
  okuma alanı sıfır kayıpla sayıya çevrilmiştir. Küme `node` ile **modellenir** ve **kurgudur**.
- **VZ14.** Çeyrekler açıklığı $Q_3 - Q_1$'dir; çitler $Q_1 - k(Q_3-Q_1)$ ve $Q_3 + k(Q_3-Q_1)$
  noktalarında durur. Katsayı $k$ bir karardır, veriden çıkmaz.
- **VZ15.** Standart puan $(x - \mu)/\sigma$'dır. İki girdisi de — ortalama ve standart sapma —
  **işaretlenecek değerlerden hesaplanır**; ölçüt kendi aradığı şeyden etkilenir.
- **VZ16.** Aykırı işareti bir silme kararı değildir. Bu derste işaretlenen satırlar **ayrı bir
  deftere** geçer; kümeden çıkarlar ama yok olmazlar ve sayıları yazılır.

#### İki Yöntem, İki Küme

```js
// aykiri.mjs — ayni ham kume MODELIDIR. Kume kurgudur: bolgesel olcum aginin
// 1400 abonesi ve saha ziyaret kayitlari; ayni tohum, ayni satirlar. Onceki
// dersin dogru donusumu uygulanir ve okuma alani sayiya cevrilir.
export const TOHUM = 20260305, ABONE = 1400;
const karistir = (x) => { x = Math.imul(x ^ (x >>> 16), 2246822507);
  x = Math.imul(x ^ (x >>> 13), 3266489909); return (x ^ (x >>> 16)) >>> 0; };
export const uretec = (t) => { let s = karistir(t);
  return () => (s = (s * 1664525 + 1013904223) >>> 0) / 4294967296; };
export const ayrik = (u, w) => { let t = 0;
  for (let i = 0; i < w.length; i++) { t += w[i]; if (u < t) return i; } return w.length - 1; };
// Son alan: ziyaretin okuma uretmeme orani. Bolgeler bu oranda ayrisir.
export const BOLGE = [["kuzey", "KZ", 0.28, 21, 0.06], ["guney", "GN", 0.22, 17, 0.03],
  ["dogu", "DG", 0.18, 26, 0.27], ["bati", "BT", 0.14, 14, 0.03],
  ["merkez", "MR", 0.18, 23, 0.08]]
  .map(([ad, kod, pay, taban, eksik]) => ({ ad, kod, pay, taban, eksik }));
const SOKAK = ["Çınar", "Söğüt", "Işık", "Ihlamur", "Zeytin", "Menekşe", "Palmiye", "Ceviz"];
const TARIFE = ["Mesken", "mesken", "MESKEN", " Mesken ", "Konut", "konut", "Ticari",
  "ticari", "TİCARİ", "Is yeri", "İş Yeri", "isyeri", "Sanayi", "sanayi", "Tarimsal"];

export const HAM = [];
let bir = null;
for (let i = 0; i < ABONE; i++) {
  const r = uretec(TOHUM + i);
  const b = BOLGE[ayrik(r(), BOLGE.map((x) => x.pay))];
  const taban = Math.max(1.2, b.taban * Math.exp((r() + r() + r() - 1.5) * 0.62));
  const sayac = `${b.kod}-${String(1000 + i).padStart(4, "0")}`;
  const paylas = bir && bir.b === b.ad && r() < 0.03;   // ayni binada ikinci sayac
  const sk = paylas ? bir.sk : SOKAK[Math.floor(r() * SOKAK.length)];
  const kapi = paylas ? bir.kapi : 1 + Math.floor(r() * 60);
  const daire = paylas ? bir.daire : r() < 0.26 ? `/${1 + Math.floor(r() * 9)}` : "";
  bir = { b: b.ad, sk, kapi, daire };   // adres alanlari ders 06'da kullanilir
  const endeks0 = 8000 + Math.floor(r() * 11000);
  for (const d of r() < 0.34 ? ["2026-01", "2026-02"] : ["2026-01"]) {
    const q = uretec(TOHUM + 7000 + HAM.length), u = q();
    const gercek = Math.round(taban * (d === "2026-02" ? 0.94 : 1) * 100) / 100;
    // Eksiklik uc bicimde gorunur: hucre bos, hucrede yalniz bosluk, "yok" yazili.
    const eksik = u < b.eksik ? ayrik(q(), [0.52, 0.19, 0.29]) : -1;
    const kayma = eksik < 0 && q() < 0.008;   // ondalik kaymasi: okuma on kat yazilmis
    const yazilan = kayma ? Math.round(gercek * 1000) / 100 : gercek;
    const okuma = eksik === 0 ? "" : eksik === 1 ? "  " : eksik === 2 ? "yok"
      : q() < 0.31 ? yazilan.toFixed(2).replace(".", ",") : yazilan.toFixed(2);
    const gun = 1 + Math.floor(q() * 27), ay = d.slice(5);
    const tb = ayrik(q(), [0.55, 0.21, 0.24]);
    const tarih = tb === 0 ? `${d}-${String(gun).padStart(2, "0")}`
      : tb === 1 ? `${String(gun).padStart(2, "0")}.${ay}.2026`
      : `${String(gun).padStart(2, "0")}/${ay}/2026`;
    const endeks = endeks0 + Math.round(gercek * (d === "2026-02" ? 2 : 1));
    const eb = q() < 0.22;
    // Adres alani bu derste kullanilmiyor; yalniz yazim bicimi saklanir.
    const av = ayrik(q(), [0.30, 0.18, 0.18, 0.16, 0.18]);
    HAM.push({ abone_no: 10001 + i, sayac_no: q() < 0.011 ? "" : sayac, bolge: b.ad,
      donem: d, okuma_metni: okuma,
      endeks_metni: eb ? `${Math.floor(endeks / 1000)}.${String(endeks % 1000).padStart(3, "0")}`
        : `${endeks}`,
      tarih_metni: tarih, tarife_metni: TARIFE[ayrik(q(), Array(15).fill(1 / 15))],
      adres_bicim: av, gercek_m3: gercek, yazilan_m3: eksik < 0 ? yazilan : null });
    if (q() < 0.09) {   // ayni ziyaret ikinci kez girilmis
      const on = HAM[HAM.length - 1], bicim = ayrik(q(), [0.34, 0.28, 0.38]);
      HAM.push({ ...on,
        sayac_no: bicim === 0 ? ` ${sayac}` : bicim === 1 ? sayac.toLowerCase() : sayac,
        adres_bicim: av === 2 ? 4 : 2,
        tarife_metni: TARIFE[ayrik(q(), Array(15).fill(1 / 15))] });
    }
  }
}

// Okumayi sayiya cevirir. Ondalik ayraci virgul de olabilir; donusumun kendi
// tuzaklari bu konunun ucuncu dersinde olculur, burada tolere edilir.
export const sayiya = (t) => { const s = String(t).trim().replace(",", ".");
  return s === "" || !/^\d+(\.\d+)?$/.test(s) ? null : Number(s); };
export const medyan = (a) => { const l = [...a].sort((x, y) => x - y);
  return l.length % 2 ? l[(l.length - 1) / 2] : (l[l.length / 2 - 1] + l[l.length / 2]) / 2; };
export const y = (w) => (h) => h.map((v, j) => (j ? String(v).padStart(w[j])
  : String(v).padEnd(w[0]))).join("");

// Onceki dersin karari: kimlik alani bos olan satirlar elendi, eksik okumali
// satirlar isaretlenip bolge ortancasiyla dolduruldu. Bu adima 2068 satir girer.
const kimlikli = HAM.filter((s) => s.sayac_no.trim() !== "");
const olculen = kimlikli.filter((s) => sayiya(s.okuma_metni) !== null);
const bolgeMed = new Map(BOLGE.map((b) => [b.ad, medyan(olculen
  .filter((s) => s.bolge === b.ad).map((s) => sayiya(s.okuma_metni)))]));
export const GIREN = kimlikli.map((s) => sayiya(s.okuma_metni) === null
  ? { ...s, okuma_metni: bolgeMed.get(s.bolge).toFixed(2) } : s);

// Onceki iki dersin karari: tekillestirme (188 satir birlesti) ve bicim
// farkinda donusum (0 kayip, 0 bozuk kayit). Bu adima 1880 satir girer.
const tekille = (l, k) => { const m = new Map();
  for (const s of l) if (!m.has(k(s))) m.set(k(s), s); return [...m.values()]; };
export const OKUMA = tekille(GIREN,
  (s) => `${s.abone_no}|${s.donem}|${s.sayac_no.trim().toUpperCase()}`)
  .map((s) => ({ ...s, okuma_m3: Number(s.okuma_metni.trim().replace(",", ".")) }));

export const ESIK = 0.005;   // raporlanan iki ondalik basamagin yarisi
export const topla = (a) => a.reduce((x, z) => x + z, 0);
export const ortalama = (a) => topla(a) / a.length;
// Ceyrek, siralanmis dizide dogrusal ara deger ile hesaplanir.
export const ceyrek = (a, p) => { const l = [...a].sort((x, z) => x - z);
  const h = (l.length - 1) * p, i = Math.floor(h);
  return l[i] + (h - i) * (l[Math.min(i + 1, l.length - 1)] - l[i]); };
export const sapma = (a) => { const m = ortalama(a);
  return Math.sqrt(ortalama(a.map((v) => (v - m) ** 2))); };

if (import.meta.url === `file://${process.argv[1]}`) {
  const d = OKUMA.map((s) => s.okuma_m3);
  const q1 = ceyrek(d, 0.25), q3 = ceyrek(d, 0.75), ca = q3 - q1;
  const alt = q1 - 1.5 * ca, ust = q3 + 1.5 * ca;
  const m = ortalama(d), sd = sapma(d);
  console.log(`model (tohum ${TOHUM}): onceki adimdan devralinan kume ${OKUMA.length} satir; ` +
    `ortalama ${m.toFixed(2)} m3, standart sapma ${sd.toFixed(2)} m3`);
  console.log(`ceyrekler: Q1 ${q1.toFixed(2)}, Q3 ${q3.toFixed(2)}, ` +
    `ceyrekler acikligi ${ca.toFixed(2)}; citler ${alt.toFixed(2)} ve ${ust.toFixed(2)} m3`);

  // Iki yontem, iki isaretli kume. A: ceyrekler acikligi. B: standart puan.
  const A = new Set(), B = new Set();
  d.forEach((v, i) => { if (v < alt || v > ust) A.add(i);
    if (Math.abs((v - m) / sd) > 3) B.add(i); });
  const kesisim = [...A].filter((i) => B.has(i)).length;
  const y1 = y([34, 11, 10, 12]);
  console.log("\n" + y1(["yontem", "isaretli", "kesisim", "yalniz bu"]));
  console.log(y1(["A ceyrekler acikligi (1.5 kat)", A.size, kesisim, A.size - kesisim]));
  console.log(y1(["B standart puan (3 sigma)", B.size, kesisim, B.size - kesisim]));
  console.log(`iki kumenin ortusmeyen kismi ${A.size + B.size - 2 * kesisim} satir; ` +
    `B kumesi A'nin alt kumesidir`);

  // Modelde ondalik noktasi kayarak on kat buyuk yazilan okumalar isaretli.
  const kayan = OKUMA.map((s, i) => [s, i])
    .filter(([s]) => s.yazilan_m3 !== null && Math.abs(s.yazilan_m3 - s.gercek_m3) > ESIK);
  console.log(`\nmodelde ondalik kaymasi olan ${kayan.length} okuma var; ` +
    `A ${kayan.filter(([, i]) => A.has(i)).length} tanesini, ` +
    `B ${kayan.filter(([, i]) => B.has(i)).length} tanesini isaretliyor`);
}
```

```
model (tohum 20260305): onceki adimdan devralinan kume 1880 satir; ortalama 22.22 m3, standart sapma 19.68 m3
ceyrekler: Q1 15.38, Q3 24.59, ceyrekler acikligi 9.21; citler 1.55 ve 38.41 m3

yontem                               isaretli   kesisim   yalniz bu
A ceyrekler acikligi (1.5 kat)             64        14          50
B standart puan (3 sigma)                  14        14           0
iki kumenin ortusmeyen kismi 50 satir; B kumesi A'nin alt kumesidir

modelde ondalik kaymasi olan 15 okuma var; A 15 tanesini, B 14 tanesini isaretliyor
```

Aynı kümede iki yöntem 64 ve 14 satır işaretledi. İkinci küme birincinin **alt kümesidir**:
kesişim 14, örtüşmeyen kısım **50 satırdır** ve bu 50 satırın hepsi yalnız çeyrekler açıklığı
tarafından görülür. Ölçütlerden biri diğerinin gördüğünün beşte birini görüyor.

Fark duyarlılık ayarından gelmiyor. Aykırı değerler çeyrekleri neredeyse hiç oynatmaz, çünkü
sıralamada bir uçtan bir uca gitseler bile çeyreğin yerini değiştirmezler. Standart puanın iki
girdisi ise doğrudan o değerlerden hesaplanır — 19,68 m³'lük standart sapmanın kendisi, on kat büyük
yazılmış okumaların ürünüdür. Ölçüt aradığı şeyi kendi tabanına katmıştır. Son satır bunu ölçüyor:
ondalık noktası kaymış **15 okumanın** 15'ini çeyrekler açıklığı, 14'ünü standart puan yakalar.

#### Eşik Değişince

```js
// esik.mjs — ayni kume, iki soru: esik degisince her yontem kac satir
// isaretliyor ve isaretlenenleri ayirmak raporlanan sayiyi ne kadar oynatiyor.
import { OKUMA, ceyrek, ortalama, sapma, topla, y } from "./aykiri.mjs";
const d = OKUMA.map((s) => s.okuma_m3);
const medyan = (a) => ceyrek(a, 0.5);

const caIsaret = (a, k) => { const q1 = ceyrek(a, 0.25), q3 = ceyrek(a, 0.75);
  const g = q3 - q1; return a.map((v, i) => [v, i])
    .filter(([v]) => v < q1 - k * g || v > q3 + k * g).map(([, i]) => i); };
const spIsaret = (a, k) => { const m = ortalama(a), s = sapma(a);
  return a.map((v, i) => [v, i]).filter(([v]) => Math.abs((v - m) / s) > k).map(([, i]) => i); };

const y1 = y([24, 8, 11, 12, 13, 11]);
console.log(y1(["yontem", "esik", "isaretli", "kalan satir", "ortalama m3", "medyan m3"]));
for (const [ad, f, k] of [["ceyrekler acikligi", caIsaret, 1.5],
  ["ceyrekler acikligi", caIsaret, 3.0], ["standart puan", spIsaret, 2.0],
  ["standart puan", spIsaret, 2.5], ["standart puan", spIsaret, 3.0]]) {
  const isaret = new Set(f(d, k)), kalan = d.filter((_, i) => !isaret.has(i));
  console.log(y1([ad, k.toFixed(1), isaret.size, kalan.length,
    ortalama(kalan).toFixed(2), medyan(kalan).toFixed(2)]));
}
console.log(`\nham kumede ortalama ${ortalama(d).toFixed(2)} m3, medyan ` +
  `${medyan(d).toFixed(2)} m3; iki yontemin en genis ve en dar isaretlemesi arasinda ` +
  `${caIsaret(d, 1.5).length - spIsaret(d, 3.0).length} satir fark var`);

// Standart puan kendi aykirisini gizler: sapma isaretlenecek degerlerden hesaplanir.
const A = new Set(caIsaret(d, 1.5)), temiz = d.filter((_, i) => !A.has(i));
console.log(`\naykirilar sapmayi kendileri buyutuyor: ${d.length} satirda standart sapma ` +
  `${sapma(d).toFixed(2)} m3; A kumesi ayrilinca ${temiz.length} satirda ` +
  `${sapma(temiz).toFixed(2)} m3'e iner ve ayni 3 sigma olcutu bu kez ` +
  `${spIsaret(temiz, 3.0).length} satir isaretler`);

// Ayirmanin raporlanan sayiya etkisi.
const kalanKayit = OKUMA.filter((_, i) => !A.has(i));
console.log(`ayirma ortalamayi ${ortalama(d).toFixed(2)} m3'ten ` +
  `${ortalama(temiz).toFixed(2)} m3'e cekiyor (${(ortalama(d) - ortalama(temiz)).toFixed(2)} m3, ` +
  `yuzde ${(100 * (ortalama(d) - ortalama(temiz)) / ortalama(d)).toFixed(1)}), medyani ` +
  `${medyan(d).toFixed(2)}'den ${medyan(temiz).toFixed(2)}'e; toplam ` +
  `${topla(d).toFixed(1)} m3'ten ${topla(temiz).toFixed(1)} m3'e iniyor`);
console.log(`\nsatir hesabi: giren ${OKUMA.length}, cikan ${kalanKayit.length}; ` +
  `${A.size} satir incelenecekler kumesine ayrildi (silinmedi, ayri bir defterde durur), ` +
  `0 satir sessizce dustu`);
```

```
yontem                      esik   isaretli kalan satir  ortalama m3  medyan m3
ceyrekler acikligi           1.5         64        1816        20.14      19.85
ceyrekler acikligi           3.0         18        1862        20.71      20.30
standart puan                2.0         15        1865        20.76      20.31
standart puan                2.5         14        1866        20.78      20.31
standart puan                3.0         14        1866        20.78      20.31

ham kumede ortalama 22.22 m3, medyan 20.31 m3; iki yontemin en genis ve en dar isaretlemesi arasinda 50 satir fark var

aykirilar sapmayi kendileri buyutuyor: 1880 satirda standart sapma 19.68 m3; A kumesi ayrilinca 1816 satirda 6.35 m3'e iner ve ayni 3 sigma olcutu bu kez 0 satir isaretler
ayirma ortalamayi 22.22 m3'ten 20.14 m3'e cekiyor (2.08 m3, yuzde 9.4), medyani 20.31'den 19.85'e; toplam 41774.8 m3'ten 36578.3 m3'e iniyor

satir hesabi: giren 1880, cikan 1816; 64 satir incelenecekler kumesine ayrildi (silinmedi, ayri bir defterde durur), 0 satir sessizce dustu
```

Tablo eşiğin bir karar olduğunu ve iki yöntemin buna farklı tepki verdiğini gösteriyor. Çeyrekler
açıklığında katsayıyı 1,5'ten 3,0'a çıkarmak işaretli satırı 64'ten 18'e indirir. Standart puanda
eşiği 2,0'dan 3,0'a çekmek 15'ten 14'e indirir: **bir satır**. Ölçüt eşiğe tepki vermiyor, çünkü
paydası o kadar büyümüştür ki hiçbir makul eşik onu bölemez.

İkinci blok bunu doğrudan ölçüyor. Standart sapma ham kümede 19,68 m³, işaretli 64 satır
ayrıldığında **6,35 m³**'tür ve aynı üç sigma ölçütü bu kez **sıfır satır** işaretler. Ölçüt kendi
işini yaptıktan sonra kendini gereksiz kılar; standart puan tek geçişte kullanıldığı her yerde
eksik sayar.

Ayırmanın bedeli son satırdadır. Ortalama 22,22 m³'ten **20,14 m³**'e iner: 2,08 m³, yüzde 9,4.
Medyan yalnız 0,46 m³ oynar — 20,31'den 19,85'e — çünkü medyan zaten sıralamaya dayanır. Toplam ise
41.774,8'den 36.578,3 m³'e, yüzde 12,4 iner. Aynı 64 satır medyanı yüzde 2,3, ortalamayı yüzde 9,4,
toplamı yüzde 12,4 oynatır; **hangi sayının raporlandığı da bir karardır**.

Adımın hesabı şudur: **giren 1.880 satır, çıkan 1.816 satır.** Aradaki 64 satır incelenecekler
defterine ayrıldı — silinmedi, sayıldı ve yazıldı — sessizce düşen satır yoktur. Ayrılanların hepsi
hatalı değildir; bir bölümü gerçekten yüksek tüketimdir ve ayrı deftere geçmeleri bunu
soruşturulabilir kılar.

#### Özet

- Aynı 1.880 satırlık kümede çeyrekler açıklığı 64, standart puan 14 satır işaretler; ikinci küme
  birincinin alt kümesidir ve örtüşmeyen kısım 50 satırdır.
- Standart puanın iki girdisi de işaretlenecek değerlerden hesaplanır ve ölçüt kendi aradığı şeyle
  körelir: standart sapma ham kümede 19,68 m³, işaretliler ayrıldıktan sonra 6,35 m³'tür ve aynı üç
  sigma ölçütü bu kez sıfır satır işaretler.
- Modelde ondalık noktası kaymış 15 okumanın 15'ini çeyrekler açıklığı, 14'ünü standart puan
  yakalar.
- Ayırma ortalamayı 22,22'den 20,14 m³'e (yüzde 9,4), medyanı 20,31'den 19,85'e, toplamı 41.774,8'den
  36.578,3 m³'e çeker.
- Adımın hesabı: giren 1.880, çıkan 1.816; 64 satır incelenecekler defterine ayrıldı, 0 sessizce
  düştü.

#### Sonraki Adım

Sayısal alanlar artık tarandı ve uçları yazılı. Kümede hiç bakılmamış bir alan daha var: tarife
sınıfı. O alan bir sayı değil, elle yazılmış bir metindir ve aynı sınıf birden çok biçimde
yazılmıştır. Sonraki ders yazımları sayar, birleştirme adımlarını sırayla uygular, her adımdan sonra
kaç kategori kaldığını yazar ve iki kategoriyi birleştirmenin kaç satırı **yanlış sınıfa**
taşıdığını ölçer.

### Kategorik Değerlerin Düzeltilmesi

Source: https://academia.sh/tr/kurslar/veri-hazirlama/kategorik-degerlerin-duzeltilmesi

Devralınan 1.816 satırın tarife alanında 15 ayrı yazım geçer. Normalleştirme merdiveni kategori sayısını 15'ten sırayla 14, 9, 8 ve 6'ya, tarife cetvelinden gelen anlam eşlemesi 5'e indirir; dile duyarsız küçültme yalnız beş yazımı birleştirir ve iki yazımda görünmez bir iz bırakır. Satır sayısı hiç değişmez, ama sınıfların içi değişir: işyeri ile ticari birleştirilirse kategori 4'e iner ve 368 satır yanlış tarife basamağına taşınır, ticari sınıfı 389'dan 757'ye çıkar. Dile duyarsız küçültmeye göre yazılmış yedi girdilik bir eşleme tablosu 216 satırı sınıfsız bırakır, hiçbir hata üretmez ve tarife sınıfına göre gruplamada grupların toplamı 1.816 yerine 1.600 olur.

Önceki dört ders sayısal alanları temizledi ve kümede hiç bakılmamış bir alan kaldı: tarife sınıfı.
O alan bir sayı değil, saha formuna elle yazılmış bir metindir ve aynı sınıf birden çok biçimde
yazılmıştır.

Bu ders o alanı düzeltir ve düzeltirken iki ayrı işin karıştığını gösterir. **Yazım düzeltmesi**
metinden çıkar: aynı sözcüğün baştaki boşluğu, büyük harfi, iç boşluğu farklı yazılmıştır ve bunlar
aynı kategoridir. **Anlam kararı** ise metinden çıkmaz: iki farklı sözcüğün aynı sınıfı gösterip
göstermediğini ancak tarife cetveli söyler. Birincisi bir sayaç işidir, ikincisi bir yargıdır ve
yanlış olduğunda satır sayısını hiç oynatmadan **satırları yanlış kutuya** koyar.

- **VZ17.** Küme önceki dört dersten devralınır ve 1.816 satırdır. Küme `node` ile **modellenir** ve
  **kurgudur**; tarife sınıfları ve cetvel de kurgudur.
- **VZ18.** Küçük harfe çevirme **dile bağlıdır**. Dile duyarsız çevirim bazı harfleri gözle
  görünmeyen bir işaretle bırakır ve iki metin aynı görünüp farklı kalır; bu yüzden merdivende iki
  ayrı basamak sayılır.
- **VZ19.** **Kategori birleştirme** yazım farklarını kaldırır. Anlam eşlemesi ayrı bir adımdır ve
  kaynağı tarife cetvelidir.
- **VZ20.** Bir eşleme tablosunda karşılığı olmayan yazım **hata üretmez**: satır sınıfsız kalır ve
  sınıfa göre gruplayan ilk işlemde sessizce dışarıda kalır.

#### Kaç Kategori Kalıyor

```js
// kategori.mjs — ayni ham kume MODELIDIR. Kume kurgudur: bolgesel olcum aginin
// 1400 abonesi ve saha ziyaret kayitlari; ayni tohum, ayni satirlar. Onceki dort
// dersin karari uygulanir ve bu adima 1816 satir girer.
export const TOHUM = 20260305, ABONE = 1400;
const karistir = (x) => { x = Math.imul(x ^ (x >>> 16), 2246822507);
  x = Math.imul(x ^ (x >>> 13), 3266489909); return (x ^ (x >>> 16)) >>> 0; };
export const uretec = (t) => { let s = karistir(t);
  return () => (s = (s * 1664525 + 1013904223) >>> 0) / 4294967296; };
export const ayrik = (u, w) => { let t = 0;
  for (let i = 0; i < w.length; i++) { t += w[i]; if (u < t) return i; } return w.length - 1; };
// Son alan: ziyaretin okuma uretmeme orani. Bolgeler bu oranda ayrisir.
export const BOLGE = [["kuzey", "KZ", 0.28, 21, 0.06], ["guney", "GN", 0.22, 17, 0.03],
  ["dogu", "DG", 0.18, 26, 0.27], ["bati", "BT", 0.14, 14, 0.03],
  ["merkez", "MR", 0.18, 23, 0.08]]
  .map(([ad, kod, pay, taban, eksik]) => ({ ad, kod, pay, taban, eksik }));
const SOKAK = ["Çınar", "Söğüt", "Işık", "Ihlamur", "Zeytin", "Menekşe", "Palmiye", "Ceviz"];
const TARIFE = ["Mesken", "mesken", "MESKEN", " Mesken ", "Konut", "konut", "Ticari",
  "ticari", "TİCARİ", "Is yeri", "İş Yeri", "isyeri", "Sanayi", "sanayi", "Tarimsal"];

export const HAM = [];
let bir = null;
for (let i = 0; i < ABONE; i++) {
  const r = uretec(TOHUM + i);
  const b = BOLGE[ayrik(r(), BOLGE.map((x) => x.pay))];
  const taban = Math.max(1.2, b.taban * Math.exp((r() + r() + r() - 1.5) * 0.62));
  const sayac = `${b.kod}-${String(1000 + i).padStart(4, "0")}`;
  const paylas = bir && bir.b === b.ad && r() < 0.03;   // ayni binada ikinci sayac
  const sk = paylas ? bir.sk : SOKAK[Math.floor(r() * SOKAK.length)];
  const kapi = paylas ? bir.kapi : 1 + Math.floor(r() * 60);
  const daire = paylas ? bir.daire : r() < 0.26 ? `/${1 + Math.floor(r() * 9)}` : "";
  bir = { b: b.ad, sk, kapi, daire };   // adres alanlari ders 06'da kullanilir
  const endeks0 = 8000 + Math.floor(r() * 11000);
  for (const d of r() < 0.34 ? ["2026-01", "2026-02"] : ["2026-01"]) {
    const q = uretec(TOHUM + 7000 + HAM.length), u = q();
    const gercek = Math.round(taban * (d === "2026-02" ? 0.94 : 1) * 100) / 100;
    // Eksiklik uc bicimde gorunur: hucre bos, hucrede yalniz bosluk, "yok" yazili.
    const eksik = u < b.eksik ? ayrik(q(), [0.52, 0.19, 0.29]) : -1;
    const kayma = eksik < 0 && q() < 0.008;   // ondalik kaymasi: okuma on kat yazilmis
    const yazilan = kayma ? Math.round(gercek * 1000) / 100 : gercek;
    const okuma = eksik === 0 ? "" : eksik === 1 ? "  " : eksik === 2 ? "yok"
      : q() < 0.31 ? yazilan.toFixed(2).replace(".", ",") : yazilan.toFixed(2);
    const gun = 1 + Math.floor(q() * 27), ay = d.slice(5);
    const tb = ayrik(q(), [0.55, 0.21, 0.24]);
    const tarih = tb === 0 ? `${d}-${String(gun).padStart(2, "0")}`
      : tb === 1 ? `${String(gun).padStart(2, "0")}.${ay}.2026`
      : `${String(gun).padStart(2, "0")}/${ay}/2026`;
    const endeks = endeks0 + Math.round(gercek * (d === "2026-02" ? 2 : 1));
    const eb = q() < 0.22;
    // Adres alani bu derste kullanilmiyor; yalniz yazim bicimi saklanir.
    const av = ayrik(q(), [0.30, 0.18, 0.18, 0.16, 0.18]);
    HAM.push({ abone_no: 10001 + i, sayac_no: q() < 0.011 ? "" : sayac, bolge: b.ad,
      donem: d, okuma_metni: okuma,
      endeks_metni: eb ? `${Math.floor(endeks / 1000)}.${String(endeks % 1000).padStart(3, "0")}`
        : `${endeks}`,
      tarih_metni: tarih, tarife_metni: TARIFE[ayrik(q(), Array(15).fill(1 / 15))],
      adres_bicim: av, gercek_m3: gercek, yazilan_m3: eksik < 0 ? yazilan : null });
    if (q() < 0.09) {   // ayni ziyaret ikinci kez girilmis
      const on = HAM[HAM.length - 1], bicim = ayrik(q(), [0.34, 0.28, 0.38]);
      HAM.push({ ...on,
        sayac_no: bicim === 0 ? ` ${sayac}` : bicim === 1 ? sayac.toLowerCase() : sayac,
        adres_bicim: av === 2 ? 4 : 2,
        tarife_metni: TARIFE[ayrik(q(), Array(15).fill(1 / 15))] });
    }
  }
}

// Okumayi sayiya cevirir. Ondalik ayraci virgul de olabilir; donusumun kendi
// tuzaklari bu konunun ucuncu dersinde olculur, burada tolere edilir.
export const sayiya = (t) => { const s = String(t).trim().replace(",", ".");
  return s === "" || !/^\d+(\.\d+)?$/.test(s) ? null : Number(s); };
export const medyan = (a) => { const l = [...a].sort((x, y) => x - y);
  return l.length % 2 ? l[(l.length - 1) / 2] : (l[l.length / 2 - 1] + l[l.length / 2]) / 2; };
export const y = (w) => (h) => h.map((v, j) => (j ? String(v).padStart(w[j])
  : String(v).padEnd(w[0]))).join("");

// Onceki dersin karari: kimlik alani bos olan satirlar elendi, eksik okumali
// satirlar isaretlenip bolge ortancasiyla dolduruldu. Bu adima 2068 satir girer.
const kimlikli = HAM.filter((s) => s.sayac_no.trim() !== "");
const olculen = kimlikli.filter((s) => sayiya(s.okuma_metni) !== null);
const bolgeMed = new Map(BOLGE.map((b) => [b.ad, medyan(olculen
  .filter((s) => s.bolge === b.ad).map((s) => sayiya(s.okuma_metni)))]));
export const GIREN = kimlikli.map((s) => sayiya(s.okuma_metni) === null
  ? { ...s, okuma_metni: bolgeMed.get(s.bolge).toFixed(2) } : s);

// Onceki dort dersin karari: eksik doldurma, tekillestirme, bicim farkinda
// donusum ve ceyrekler acikligiyla isaretlenen 64 satirin ayrilmasi.
const tekille = (l, k) => { const m = new Map();
  for (const s of l) if (!m.has(k(s))) m.set(k(s), s); return [...m.values()]; };
const OKUMA = tekille(GIREN, (s) => `${s.abone_no}|${s.donem}|${s.sayac_no.trim().toUpperCase()}`)
  .map((s) => ({ ...s, okuma_m3: Number(s.okuma_metni.trim().replace(",", ".")) }));
const ceyrek = (a, p) => { const l = [...a].sort((x, z) => x - z);
  const h = (l.length - 1) * p, i = Math.floor(h);
  return l[i] + (h - i) * (l[Math.min(i + 1, l.length - 1)] - l[i]); };
const dd = OKUMA.map((s) => s.okuma_m3);
const q1 = ceyrek(dd, 0.25), q3 = ceyrek(dd, 0.75), ca = q3 - q1;
export const GIREN4 = OKUMA.filter((s) => s.okuma_m3 >= q1 - 1.5 * ca && s.okuma_m3 <= q3 + 1.5 * ca);

// Normallestirme merdiveni. Her adim bir oncekinin uzerine biner; her adimdan
// sonra kac ayri kategori kaldigi sayilir.
export const N = [
  ["N0 ham metin", (t) => t],
  ["N1 kirpma", (t) => t.trim()],
  ["N2 kucuk harf (dile duyarsiz)", (t) => t.trim().toLowerCase()],
  ["N3 kucuk harf (dile duyarli)", (t) => t.trim().toLocaleLowerCase("tr")],
  ["N4 isaret ve bosluk sadelestirme", (t) => t.trim().toLocaleLowerCase("tr")
    .replace(/\u0307/g, "").replace(/ş/g, "s").replace(/ı/g, "i").replace(/\s+/g, "")],
];
// Anlam eslemesi metinden cikmaz; tarife cetvelinden gelir.
export const ESLEME = { mesken: "mesken", konut: "mesken", ticari: "ticari",
  isyeri: "isyeri", sanayi: "sanayi", tarimsal: "tarimsal" };
export const anahtar = N[4][1];

if (import.meta.url === `file://${process.argv[1]}`) {
  console.log(`model (tohum ${TOHUM}): onceki adimdan devralinan kume ${GIREN4.length} satir; ` +
    `tarife alaninda ${new Set(GIREN4.map((s) => s.tarife_metni)).size} ayri yazim geciyor`);
  const y1 = y([36, 11, 11]);
  console.log("\n" + y1(["normallestirme adimi", "kategori", "birlesen"]));
  let onceki = null;
  for (const [ad, f] of N) { const k = new Set(GIREN4.map((s) => f(s.tarife_metni))).size;
    console.log(y1([ad, k, onceki === null ? 0 : onceki - k])); onceki = k; }
  const sinif = new Set(GIREN4.map((s) => ESLEME[anahtar(s.tarife_metni)]));
  console.log(y1(["N5 anlam eslemesi (tarife cetveli)", sinif.size, onceki - sinif.size]));

  const y2 = y([12, 14, 14, 14]);
  console.log("\n" + y2(["yazim", "N2", "N3", "N4"]));
  for (const yz of [" Mesken ", "TİCARİ", "Ticari", "İş Yeri", "Is yeri", "isyeri"])
    console.log(y2([JSON.stringify(yz), ...N.slice(2).map(([, f]) => JSON.stringify(f(yz)))]));
}
```

```
model (tohum 20260305): onceki adimdan devralinan kume 1816 satir; tarife alaninda 15 ayri yazim geciyor

normallestirme adimi                   kategori   birlesen
N0 ham metin                                 15          0
N1 kirpma                                    14          1
N2 kucuk harf (dile duyarsiz)                 9          5
N3 kucuk harf (dile duyarli)                  8          1
N4 isaret ve bosluk sadelestirme              6          2
N5 anlam eslemesi (tarife cetveli)            5          1

yazim                   N2            N3            N4
" Mesken "        "mesken"      "mesken"      "mesken"
"TİCARİ"        "ti̇cari̇"      "ticari"      "ticari"
"Ticari"          "ticari"      "ticari"      "ticari"
"İş Yeri"       "i̇ş yeri"     "iş yeri"      "isyeri"
"Is yeri"        "is yeri"     "ıs yeri"      "isyeri"
"isyeri"          "isyeri"      "isyeri"      "isyeri"
```

Merdiven altı basamakta 15 yazımı 5 sınıfa indiriyor ve her basamağın kaç yazım birleştirdiği ayrı
yazılıyor. Asıl işi küçük harfe çevirme yapar: **beş** yazım birleşir. Sonraki basamak bir yazım
daha birleştirir, yani dile duyarsız çevirim **bir yazımı çözememiştir**.

İkinci tablo bunu gösteriyor. `"TİCARİ"` dile duyarsız çevrildiğinde ortaya çıkan metin ekranda
`"ticari"` gibi görünür ama değildir: harfin üstündeki nokta ayrı bir işaret olarak kalır ve iki
metin karşılaştırıldığında farklı çıkar. `"Is yeri"` ile `"İş Yeri"` çifti aynı sorunun tersidir —
dile duyarlı çevirim ikisini `"ıs yeri"` ve `"iş yeri"` yapar ve **hâlâ ayrıdır**; ancak işaretleri
ve boşlukları sadeleştiren basamak ikisini `"isyeri"` altında birleştirir.

Son basamak metinle ilgili değildir: `"konut"` ile `"mesken"` iki ayrı sözcüktür ve hiçbir
normalleştirme onları birleştirmez; tarife cetveli aynı basamağa baktıklarını söylediği için
birleşirler. Merdivenin tek **yargı** basamağı budur.

#### Yanlış Birleştirme

```js
// birlestirme.mjs — ayni kume, iki soru: iki kategoriyi birlestirmek kac satiri
// yanlis sinifa tasiyor ve eksik bir esleme tablosu kac satiri sinifsiz birakiyor.
import { GIREN4, ESLEME, anahtar, N, y } from "./kategori.mjs";

const dagit = (esleme, anh) => { const m = new Map();
  for (const s of GIREN4) { const k = esleme[anh(s.tarife_metni)];
    m.set(k, (m.get(k) ?? 0) + 1); } return m; };
const dogru = dagit(ESLEME, anahtar);
// Tarife cetvelinde isyeri ve ticari ayri basamaklardir; ikisini birlestirmek
// bir yazim duzeltmesi degil, bir anlam kararidir.
const YANLIS = { ...ESLEME, isyeri: "ticari" };
const yanlis = dagit(YANLIS, anahtar);

const y1 = y([12, 12, 14, 12]);
console.log(y1(["sinif", "dogru esleme", "isyeri>ticari", "fark"]));
for (const k of [...dogru.keys()].sort())
  console.log(y1([k, dogru.get(k), yanlis.get(k) ?? 0, (yanlis.get(k) ?? 0) - dogru.get(k)]));
console.log(`toplam ${[...dogru.values()].reduce((a, b) => a + b, 0)} satir her iki eslemede de ` +
  `yerinde duruyor; kategori sayisi ${dogru.size}'ten ${yanlis.size}'e iniyor ve ` +
  `${dogru.get("isyeri")} satir yanlis sinifa tasiniyor`);

// Yaygin esleme tablosu: dile duyarsiz kucultmeden gecen yazimlarla yazilmis.
// Tabloda karsiligi olmayan yazim bir hata uretmez, sinifsiz kalir.
const YAYGIN = { mesken: "mesken", konut: "mesken", ticari: "ticari",
  "is yeri": "isyeri", isyeri: "isyeri", sanayi: "sanayi", tarimsal: "tarimsal" };
const kaba = N[2][1];
const sinifsiz = GIREN4.filter((s) => YAYGIN[kaba(s.tarife_metni)] === undefined);
const y2 = y([26, 8, 16]);
console.log("\n" + y2(["esleme tablosu", "girdi", "sinifsiz satir"]));
console.log(y2(["yaygin (N2 anahtarlari)", Object.keys(YAYGIN).length, sinifsiz.length]));
console.log(y2(["N4 anahtarlari", Object.keys(ESLEME).length,
  GIREN4.filter((s) => ESLEME[anahtar(s.tarife_metni)] === undefined).length]));
console.log(`sinifsiz kalan ${sinifsiz.length} satirin tarife yazimlari: ` +
  [...new Set(sinifsiz.map((s) => s.tarife_metni))].map((v) => JSON.stringify(v)).join(", "));
console.log(`tarife sinifina gore gruplama bu ${sinifsiz.length} satiri sessizce disarida ` +
  `birakir: gruplarin toplami ${GIREN4.length - sinifsiz.length}, kume ise ${GIREN4.length} satir`);

console.log(`\nsatir hesabi: giren ${GIREN4.length}, cikan ${GIREN4.length}; ` +
  `0 satir elendi, 0 satir sessizce dustu. 15 yazim ${dogru.size} sinifta birlesti; ` +
  `isyeri ile ticari birlestirilseydi ${dogru.get("isyeri")} satir yanlis sinifa gider, ` +
  `yaygin esleme tablosu kullanilsaydi ${sinifsiz.length} satir sessizce duserdi`);
```

```
sinif       dogru esleme isyeri>ticari        fark
isyeri               368             0        -368
mesken               713           713           0
sanayi               215           215           0
tarimsal             131           131           0
ticari               389           757         368
toplam 1816 satir her iki eslemede de yerinde duruyor; kategori sayisi 5'ten 4'e iniyor ve 368 satir yanlis sinifa tasiniyor

esleme tablosu               girdi  sinifsiz satir
yaygin (N2 anahtarlari)          7             216
N4 anahtarlari                   6               0
sinifsiz kalan 216 satirin tarife yazimlari: "TİCARİ", "İş Yeri"
tarife sinifina gore gruplama bu 216 satiri sessizce disarida birakir: gruplarin toplami 1600, kume ise 1816 satir

satir hesabi: giren 1816, cikan 1816; 0 satir elendi, 0 satir sessizce dustu. 15 yazim 5 sinifta birlesti; isyeri ile ticari birlestirilseydi 368 satir yanlis sinifa gider, yaygin esleme tablosu kullanilsaydi 216 satir sessizce duserdi
```

İlk tablo yanlış bir yargının bedelini gösteriyor. İşyeri ile ticari abonelik cetvelde ayrı
basamaklardır, ama iki sözcük gündelik dilde birbirinin yerine kullanıldığı için birleştirmek akla
yatkın görünür. Uygulandığında kategori sayısı 5'ten 4'e iner, **satır sayısı hiç değişmez** ve 368
satır ticariye taşınır: 389 olan ticari 757'ye çıkar, işyeri sınıfı kaybolur. Kümede hiçbir şey
eksilmez; yalnız her tarife kırılımı bundan sonra yanlış çıkar.

İkinci tablo sessiz kaybı ölçüyor. Yaygın eşleme tablosu dile duyarsız küçültmeden geçmiş
yazımlarla yazılmıştır ve yedi girdi taşır. İki yazımın — `"TİCARİ"` ve `"İş Yeri"` — o tabloda
karşılığı yoktur; bu yazımları taşıyan **216 satır sınıfsız kalır**. Hiçbir hata üretilmez, hiçbir
işlem durmaz. Tarife sınıfına göre gruplandığında grupların toplamı **1.600**'dür, oysa küme 1.816
satırdır. Aradaki 216 satır tabloda hiçbir yerde görünmez.

Aynı eşleme tablosu, sadeleştirilmiş anahtarlarla yazıldığında altı girdiyle **sıfır** satır
sınıfsız bırakır. Fark tablonun büyüklüğünde değil, **hangi normalleştirmeden sonra yazıldığında**.

Adımın hesabı şudur: **giren 1.816 satır, çıkan 1.816 satır.** Hiçbir satır elenmedi, hiçbir satır
sessizce düşmedi; 15 yazım 5 sınıfta birleşti. Bu adımın bedeli yine yapılmadığında ortaya çıkar:
işyeri ile ticari birleştirilseydi 368 satır yanlış sınıfa gider, yaygın eşleme tablosu
kullanılsaydı 216 satır sessizce düşerdi. **Satır sayısı bir denetim ölçüsü değildir**; bu adımda
giren ile çıkan eşittir ve kümenin içi yine de baştan aşağı değişmiştir.

#### Özet

- Tarife alanında 15 ayrı yazım geçer; normalleştirme merdiveni kategori sayısını 15, 14, 9, 8, 6
  ve anlam eşlemesiyle 5'e indirir.
- Dile duyarsız küçültme beş yazım birleştirir ama iki yazımda görünmeyen bir işaret bırakır.
  Yazım düzeltmesi metinden, anlam eşlemesi tarife cetvelinden gelir.
- İşyeri ile ticariyi birleştirmek kategori sayısını 4'e indirir, satır sayısını hiç oynatmaz ve
  368 satırı yanlış sınıfa taşır: ticari 389'dan 757'ye çıkar.
- Dile duyarsız anahtarlarla yazılmış yedi girdilik eşleme tablosu 216 satırı sınıfsız bırakır ve
  gruplamada toplam 1.816 yerine 1.600 çıkar; sadeleştirilmiş anahtarlarla yazılan altı girdilik
  tablo sıfır satır düşürür.
- Adımın hesabı: giren 1.816, çıkan 1.816; 0 elendi, 0 sessizce düştü, 15 yazım 5 sınıfta birleşti.

#### Sonraki Adım

Tarife alanı düzeldi, ama aynı iş kümenin bütün metin alanları için yapılmadı. Sayaç kodu iki
derste kırpıldı ve harf birliğine getirildi; adres alanına hiç dokunulmadı ve kümedeki her adres,
abone kütüğündeki karşılığından farklı yazılmıştır. Sonraki ders normalleştirme merdivenini adres
alanına uygular, her basamağın kütükle eşleşmeyi kaç kayıtta **kurtardığını** sayar ve bir basamak
daha ileri gitmenin kaç kaydı **ayırt edilemez** hale getirdiğini ölçer.

### Metin Normalleştirme

Source: https://academia.sh/tr/kurslar/veri-hazirlama/metin-normallestirme

Devralınan 1.816 kaydın adres alanı, abone kütüğündeki 1.178 ayrı adresle ham metin olarak yalnız 325 kayıtta eşleşir. Normalleştirme merdiveni eşleşmeyi sırayla 604, 604, 937, 1.490 ve 1.816 kayda çıkarır: kırpma ve boşluk daraltma 279, dile duyarlı küçültme 333, işaret sadeleştirme 553, kısaltma açma 326 kaydı kurtarır — dile duyarsız küçültme ise sıfır. Toplam 1.491 kayıtta eşleşme kurtarılır. Bir basamak daha ileri gitmek, bütün ayraçları atmak, eşleşmeyi hiç artırmaz ve 13 anahtarda 26 ayrı adresi üst üste bindirir: 'No 5/2' ile 'No 52' aynı anahtara düşer ve 36 kayıt tek bir aboneye bağlanamaz hale gelir. Adım 1.816 satırla girip 1.816 satırla çıkar.

Bir önceki ders tarife alanının yazım farklarını kaldırdı ve 15 yazımı 5 sınıfa indirdi. Adres
alanına ise hiç dokunulmadı.

Bu ders adres alanını abone kütüğüyle karşılaştırır. Kütük ayrı bir kaynaktır ve her aboneyi tek
bir kanonik adresle tutar; saha formundaki adres beş ayrı biçimde yazılmıştır. Normalleştirme
ikisini buluşturur, ama her basamağın bedeli vardır: metinden silinen bir fark **geri gelmez**.
Dersin iki sayısı budur — bir basamağın eşleşmeyi kaç kayıtta **kurtardığı** ve kaç kayıtta gerçek
bir farkı **sildiği**.

- **VZ21.** Küme önceki beş dersten devralınır ve 1.816 satırdır. Abone kütüğü ikinci bir
  kaynaktır. İkisi de `node` ile **modellenir** ve **kurgudur**.
- **VZ22.** **Metin normalleştirme**, iki metni karşılaştırılabilir kılmak için ikisine de aynı
  dönüşümü uygulamaktır; veritabanı tasarımındaki normalizasyonla ilgisi yoktur.
- **VZ23.** Basamaklar birbirinin üzerine biner ve **hem kayda hem kütük kaydına** uygulanır;
  yalnız bir tarafa uygulanan dönüşüm eşleşmeyi kurtarmaz.
- **VZ24.** Bir basamağın **kurtardığı** kayıt sayısı ile **sildiği** fark sayısı ayrı ölçülür.
  İkincisi kümede değil, kütükte sayılır: kaç ayrı kanonik adres aynı anahtara düşüyor.

#### Merdiven Eşleşmeyi Kaç Kayıtta Kurtarıyor

```js
// adres.mjs — ayni ham kume ve abone kutugu MODELIDIR. Kume kurgudur: bolgesel
// olcum aginin 1400 abonesi ve saha ziyaret kayitlari; ayni tohum, ayni satirlar.
// Onceki bes dersin karari uygulanir ve bu adima 1816 satir girer.
export const TOHUM = 20260305, ABONE = 1400;
const karistir = (x) => { x = Math.imul(x ^ (x >>> 16), 2246822507);
  x = Math.imul(x ^ (x >>> 13), 3266489909); return (x ^ (x >>> 16)) >>> 0; };
export const uretec = (t) => { let s = karistir(t);
  return () => (s = (s * 1664525 + 1013904223) >>> 0) / 4294967296; };
export const ayrik = (u, w) => { let t = 0;
  for (let i = 0; i < w.length; i++) { t += w[i]; if (u < t) return i; } return w.length - 1; };
// Son alan: ziyaretin okuma uretmeme orani. Bolgeler bu oranda ayrisir.
export const BOLGE = [["kuzey", "KZ", 0.28, 21, 0.06], ["guney", "GN", 0.22, 17, 0.03],
  ["dogu", "DG", 0.18, 26, 0.27], ["bati", "BT", 0.14, 14, 0.03],
  ["merkez", "MR", 0.18, 23, 0.08]]
  .map(([ad, kod, pay, taban, eksik]) => ({ ad, kod, pay, taban, eksik }));
const SOKAK = ["Çınar", "Söğüt", "Işık", "Ihlamur", "Zeytin", "Menekşe", "Palmiye", "Ceviz"];
const TARIFE = ["Mesken", "mesken", "MESKEN", " Mesken ", "Konut", "konut", "Ticari",
  "ticari", "TİCARİ", "Is yeri", "İş Yeri", "isyeri", "Sanayi", "sanayi", "Tarimsal"];

export const HAM = [], KUTUK = new Map();
let bir = null;
for (let i = 0; i < ABONE; i++) {
  const r = uretec(TOHUM + i);
  const b = BOLGE[ayrik(r(), BOLGE.map((x) => x.pay))];
  const taban = Math.max(1.2, b.taban * Math.exp((r() + r() + r() - 1.5) * 0.62));
  const sayac = `${b.kod}-${String(1000 + i).padStart(4, "0")}`;
  const paylas = bir && bir.b === b.ad && r() < 0.03;   // ayni binada ikinci sayac
  const sk = paylas ? bir.sk : SOKAK[Math.floor(r() * SOKAK.length)];
  const kapi = paylas ? bir.kapi : 1 + Math.floor(r() * 60);
  const daire = paylas ? bir.daire : r() < 0.26 ? `/${1 + Math.floor(r() * 9)}` : "";
  bir = { b: b.ad, sk, kapi, daire };
  // Abone kutugu: her abone icin tek bir kanonik adres. Ikinci kaynaktir.
  KUTUK.set(10001 + i, `${b.ad[0].toUpperCase()}${b.ad.slice(1)} Mahallesi ${sk} `
    + `Sokak No ${kapi}${daire}`);
  const endeks0 = 8000 + Math.floor(r() * 11000);
  for (const d of r() < 0.34 ? ["2026-01", "2026-02"] : ["2026-01"]) {
    const q = uretec(TOHUM + 7000 + HAM.length), u = q();
    const gercek = Math.round(taban * (d === "2026-02" ? 0.94 : 1) * 100) / 100;
    // Eksiklik uc bicimde gorunur: hucre bos, hucrede yalniz bosluk, "yok" yazili.
    const eksik = u < b.eksik ? ayrik(q(), [0.52, 0.19, 0.29]) : -1;
    const kayma = eksik < 0 && q() < 0.008;   // ondalik kaymasi: okuma on kat yazilmis
    const yazilan = kayma ? Math.round(gercek * 1000) / 100 : gercek;
    const okuma = eksik === 0 ? "" : eksik === 1 ? "  " : eksik === 2 ? "yok"
      : q() < 0.31 ? yazilan.toFixed(2).replace(".", ",") : yazilan.toFixed(2);
    const gun = 1 + Math.floor(q() * 27), ay = d.slice(5);
    const tb = ayrik(q(), [0.55, 0.21, 0.24]);
    const tarih = tb === 0 ? `${d}-${String(gun).padStart(2, "0")}`
      : tb === 1 ? `${String(gun).padStart(2, "0")}.${ay}.2026`
      : `${String(gun).padStart(2, "0")}/${ay}/2026`;
    const endeks = endeks0 + Math.round(gercek * (d === "2026-02" ? 2 : 1));
    const eb = q() < 0.22;
    // Saha formunda adres bes ayri bicimde yazilmis; kutuk bicimi av === 1.
    const av = ayrik(q(), [0.30, 0.18, 0.18, 0.16, 0.18]);
    const kap = `${kapi}${daire}`;
    const M = `${b.ad[0].toUpperCase()}${b.ad.slice(1)}`, mUst = b.ad.toLocaleUpperCase("tr");
    const alt = sk.toLocaleLowerCase("tr"), ust = sk.toLocaleUpperCase("tr");
    const adres = av === 0 ? `${b.ad} mahallesi ${alt} sokak no: ${kap}`
      : av === 1 ? `${M} Mahallesi ${sk} Sokak No ${kap}`
      : av === 2 ? `${mUst} MAHALLESİ ${ust} SOKAK NO ${kap}`
      : av === 3 ? `  ${M}  Mahallesi  ${sk}  Sokak  No ${kap} `
      : `${b.ad} mah. ${alt} sk. no:${kap}`;
    HAM.push({ abone_no: 10001 + i, sayac_no: q() < 0.011 ? "" : sayac, bolge: b.ad,
      donem: d, okuma_metni: okuma,
      endeks_metni: eb ? `${Math.floor(endeks / 1000)}.${String(endeks % 1000).padStart(3, "0")}`
        : `${endeks}`,
      tarih_metni: tarih, tarife_metni: TARIFE[ayrik(q(), Array(15).fill(1 / 15))],
      adres_metni: adres, gercek_m3: gercek, yazilan_m3: eksik < 0 ? yazilan : null });
    if (q() < 0.09) {   // ayni ziyaret ikinci kez girilmis
      const on = HAM[HAM.length - 1], bicim = ayrik(q(), [0.34, 0.28, 0.38]);
      HAM.push({ ...on,
        sayac_no: bicim === 0 ? ` ${sayac}` : bicim === 1 ? sayac.toLowerCase() : sayac,
        adres_metni: av === 2 ? `${b.ad} mah. ${alt} sk. no:${kap}`
          : `${mUst} MAHALLESİ ${ust} SOKAK NO ${kap}`,
        tarife_metni: TARIFE[ayrik(q(), Array(15).fill(1 / 15))] });
    }
  }
}

// Okumayi sayiya cevirir. Ondalik ayraci virgul de olabilir; donusumun kendi
// tuzaklari bu konunun ucuncu dersinde olculur, burada tolere edilir.
export const sayiya = (t) => { const s = String(t).trim().replace(",", ".");
  return s === "" || !/^\d+(\.\d+)?$/.test(s) ? null : Number(s); };
export const medyan = (a) => { const l = [...a].sort((x, y) => x - y);
  return l.length % 2 ? l[(l.length - 1) / 2] : (l[l.length / 2 - 1] + l[l.length / 2]) / 2; };
export const y = (w) => (h) => h.map((v, j) => (j ? String(v).padStart(w[j])
  : String(v).padEnd(w[0]))).join("");

// Onceki dersin karari: kimlik alani bos olan satirlar elendi, eksik okumali
// satirlar isaretlenip bolge ortancasiyla dolduruldu. Bu adima 2068 satir girer.
const kimlikli = HAM.filter((s) => s.sayac_no.trim() !== "");
const olculen = kimlikli.filter((s) => sayiya(s.okuma_metni) !== null);
const bolgeMed = new Map(BOLGE.map((b) => [b.ad, medyan(olculen
  .filter((s) => s.bolge === b.ad).map((s) => sayiya(s.okuma_metni)))]));
export const GIREN = kimlikli.map((s) => sayiya(s.okuma_metni) === null
  ? { ...s, okuma_metni: bolgeMed.get(s.bolge).toFixed(2) } : s);

// Onceki dort dersin karari: eksik doldurma, tekillestirme, bicim farkinda
// donusum ve ceyrekler acikligiyla isaretlenen 64 satirin ayrilmasi.
const tekille = (l, k) => { const m = new Map();
  for (const s of l) if (!m.has(k(s))) m.set(k(s), s); return [...m.values()]; };
const OKUMA = tekille(GIREN, (s) => `${s.abone_no}|${s.donem}|${s.sayac_no.trim().toUpperCase()}`)
  .map((s) => ({ ...s, okuma_m3: Number(s.okuma_metni.trim().replace(",", ".")) }));
const ceyrek = (a, p) => { const l = [...a].sort((x, z) => x - z);
  const h = (l.length - 1) * p, i = Math.floor(h);
  return l[i] + (h - i) * (l[Math.min(i + 1, l.length - 1)] - l[i]); };
const dd = OKUMA.map((s) => s.okuma_m3);
const q1 = ceyrek(dd, 0.25), q3 = ceyrek(dd, 0.75), ca = q3 - q1;
export const GIREN4 = OKUMA.filter((s) => s.okuma_m3 >= q1 - 1.5 * ca && s.okuma_m3 <= q3 + 1.5 * ca);

// Metin normallestirme merdiveni. Her basamak bir oncekinin uzerine biner ve
// hem kayda hem kutuk kaydina ayni sekilde uygulanir.
export const N = [
  ["N0 ham metin", (t) => t],
  ["N1 kirpma ve bosluk daraltma", (t) => t.trim().replace(/\s+/g, " ")],
  ["N2 kucuk harf (dile duyarsiz)", (t) => N[1][1](t).toLowerCase()],
  ["N3 kucuk harf (dile duyarli)", (t) => N[1][1](t).toLocaleLowerCase("tr")],
  ["N4 isaret sadelestirme", (t) => N[3][1](t).replace(/[.:]/g, " ").replace(/\s+/g, " ").trim()],
  ["N5 kisaltma acma", (t) => N[4][1](t).replace(/\bmah\b/, "mahallesi")
    .replace(/\bsk\b/, "sokak")],
  ["N6 tum ayraclarin atilmasi", (t) => N[5][1](t).replace(/[^a-z0-9çğıöşü]/g, "")],
];

if (import.meta.url === `file://${process.argv[1]}`) {
  const kutuk = new Set(KUTUK.values());
  console.log(`model (tohum ${TOHUM}): onceki adimdan devralinan kume ${GIREN4.length} satir, ` +
    `abone kutugunde ${KUTUK.size} adres, bunlarin ${kutuk.size} tanesi ayri metin`);
  // Ayni kutuk adresine karsilik gelen yazimlar: en cok bicim tasiyan grup.
  const oymak = new Map();
  for (const s of GIREN4) { const k = N[6][1](s.adres_metni);
    oymak.set(k, (oymak.get(k) ?? new Set()).add(s.adres_metni)); }
  const en = [...oymak.values()].sort((a, b) => b.size - a.size)[0];
  console.log(`\nayni adres saha formunda ${en.size} ayri bicimde yazilmis:`);
  for (const s of en) console.log(`  ${JSON.stringify(s)}`);

  const y1 = y([32, 11, 12, 13]);
  console.log("\n" + y1(["normallestirme basamagi", "eslesen", "kurtarilan", "oran %"]));
  let onceki = null;
  for (const [ad, f] of N) {
    const anahtarlar = new Set([...kutuk].map(f));
    const es = GIREN4.filter((s) => anahtarlar.has(f(s.adres_metni))).length;
    console.log(y1([ad, es, onceki === null ? 0 : es - onceki,
      (100 * es / GIREN4.length).toFixed(1)]));
    onceki = es;
  }
}
```

```
model (tohum 20260305): onceki adimdan devralinan kume 1816 satir, abone kutugunde 1400 adres, bunlarin 1178 tanesi ayri metin

ayni adres saha formunda 4 ayri bicimde yazilmis:
  "GUNEY MAHALLESİ IHLAMUR SOKAK NO 29"
  "guney mahallesi ıhlamur sokak no: 29"
  "  Guney  Mahallesi  Ihlamur  Sokak  No 29 "
  "guney mah. ıhlamur sk. no:29"

normallestirme basamagi             eslesen  kurtarilan       oran %
N0 ham metin                            325           0         17.9
N1 kirpma ve bosluk daraltma            604         279         33.3
N2 kucuk harf (dile duyarsiz)           604           0         33.3
N3 kucuk harf (dile duyarli)            937         333         51.6
N4 isaret sadelestirme                 1490         553         82.0
N5 kisaltma acma                       1816         326        100.0
N6 tum ayraclarin atilmasi             1816           0        100.0
```

Ham metinle 1.816 kaydın yalnız **325'i** kütükle eşleşiyor. Kalan 1.491 kayıt gerçek bir adres
taşır, doğru aboneye aittir ve hiçbir eşleşme kurmaz.

Basamaklar eşit iş yapmıyor. Kırpma ve boşluk daraltma 279 kayıt kurtarır, dile duyarlı küçültme
333, işaret sadeleştirme 553, kısaltma açma son 326 kaydı getirir ve eşleşme yüzde 100'e ulaşır.

Sıfır kurtaran tek basamak **dile duyarsız küçültmedir**. `"MAHALLESİ"` dile duyarsız
küçültüldüğünde harfin üstündeki nokta ayrı bir işaret olarak kalır ve metin `"mahallesi"` ile
eşleşmez; `"IŞIK"` ise `"işik"` olur, oysa dile duyarlı karşılığı `"ışık"`tır. Basamak hiçbir kaydı
kurtarmaz ve arkasından geleninkini zorlaştırır.

#### Bir Basamak Fazla

```js
// silinen.mjs — ayni kume, ters soru: normallestirme bir basamak daha ileri
// gidince kac ayri adres ayirt edilemez hale geliyor ve kac kayit etkileniyor.
import { GIREN4, KUTUK, N, y } from "./adres.mjs";
const kutuk = [...new Set(KUTUK.values())];

const y1 = y([32, 12, 14, 13]);
console.log(y1(["normallestirme basamagi", "kutuk kaydi", "ayri anahtar", "yutulan"]));
for (const [ad, f] of N.slice(4)) {
  const anahtar = new Set(kutuk.map(f));
  console.log(y1([ad, kutuk.length, anahtar.size, kutuk.length - anahtar.size]));
}

// N6 hangi adresleri ayni anahtara dusuruyor.
const grup = new Map();
for (const k of kutuk) { const a = N[6][1](k);
  grup.set(a, (grup.get(a) ?? []).concat([k])); }
const cakisan = [...grup.entries()].filter(([, l]) => l.length > 1);
const cakisanAnahtar = new Set(cakisan.map(([a]) => a));
const etkilenen = GIREN4.filter((s) => cakisanAnahtar.has(N[6][1](s.adres_metni)));
console.log(`\nN6 ${cakisan.length} anahtarda ${cakisan.reduce((a, [, l]) => a + l.length, 0)} ` +
  `ayri adresi ustuste bindiriyor; kumede bu anahtarlari tasiyan ${etkilenen.length} kayit ` +
  `artik tek bir aboneye baglanamiyor`);
const y2 = y([36, 36]);
console.log("\n" + y2(["ayni anahtara dusen iki adres", "N6 anahtari"]));
for (const [a, l] of cakisan.slice(0, 2)) {
  console.log(y2([JSON.stringify(l[0]), a]));
  console.log(y2([JSON.stringify(l[1]), ""]));
}
console.log(`\nkapi numarasindaki egik cizgi N6'da atiliyor: "No 5/2" ile "No 52" ayni ` +
  `anahtari veriyor. N5 ayni kumede ${new Set(kutuk.map(N[5][1])).size} ayri anahtar tutuyor ` +
  `ve eslesme orani ikisinde de yuzde 100`);

const ham = GIREN4.filter((s) => new Set(kutuk).has(s.adres_metni)).length;
console.log(`\nsatir hesabi: giren ${GIREN4.length}, cikan ${GIREN4.length}; 0 satir elendi, ` +
  `0 satir sessizce dustu. Merdiven N5'te durduruldu ve eslesme ${ham} kayittan ` +
  `${GIREN4.length} kayda cikti: ${GIREN4.length - ham} kayitta eslesme kurtarildi, ` +
  `0 kayit ayirt edilemez oldu. Bir basamak daha gidilseydi eslesme hic artmaz, ` +
  `${etkilenen.length} kayit ayirt edilemez olurdu`);
```

```
normallestirme basamagi          kutuk kaydi  ayri anahtar      yutulan
N4 isaret sadelestirme                  1178          1178            0
N5 kisaltma acma                        1178          1178            0
N6 tum ayraclarin atilmasi              1178          1165           13

N6 13 anahtarda 26 ayri adresi ustuste bindiriyor; kumede bu anahtarlari tasiyan 36 kayit artik tek bir aboneye baglanamiyor

ayni anahtara dusen iki adres                                N6 anahtari
"Kuzey Mahallesi Işık Sokak No 5/2"          kuzeymahallesiışıksokakno52
"Kuzey Mahallesi Işık Sokak No 52"                                      
"Dogu Mahallesi Işık Sokak No 14"             dogumahallesiışıksokakno14
"Dogu Mahallesi Işık Sokak No 1/4"                                      

kapi numarasindaki egik cizgi N6'da atiliyor: "No 5/2" ile "No 52" ayni anahtari veriyor. N5 ayni kumede 1178 ayri anahtar tutuyor ve eslesme orani ikisinde de yuzde 100

satir hesabi: giren 1816, cikan 1816; 0 satir elendi, 0 satir sessizce dustu. Merdiven N5'te durduruldu ve eslesme 325 kayittan 1816 kayda cikti: 1491 kayitta eslesme kurtarildi, 0 kayit ayirt edilemez oldu. Bir basamak daha gidilseydi eslesme hic artmaz, 36 kayit ayirt edilemez olurdu
```

Son basamak eşleşmeyi hiç artırmıyor — N5'te oran zaten yüzde 100'dür — ama bir şeyi bozuyor.
Bütün ayraçları atmak, kapı numarasındaki eğik çizgiyi de atar: `"No 5/2"` ile `"No 52"` aynı
anahtara düşer. Kütükteki 1.178 ayrı adres 1.165 anahtara iner; **13 anahtar 26 ayrı adresi** üst
üste bindirir.

Bu bir eşleşme hatası değil, bir **ayırt edilemezlik** durumudur. O anahtarları taşıyan 36 kayıt
hâlâ bir eşleşme bulur, ama bulduğu eşleşme iki adresten hangisi olduğu belli olmayan bir
eşleşmedir. Satır sayısı değişmez ve tablodan bakan biri yüzde 100 eşleşme görür. Silinen şey satır
değil, **ayrım**.

Adımın hesabı şudur: **giren 1.816 satır, çıkan 1.816 satır.** Hiçbir satır elenmedi ve sessizce
düşmedi; 1.491 kayıtta eşleşme kurtarıldı, sıfır kayıt ayırt edilemez oldu. Merdivenin N5'te
durdurulması bir karardır: bir basamak daha ileri gitmek eşleşmeye hiçbir şey katmaz, 36 kaydın
ayrımını siler. **Bir normalleştirme adımı, kurtardığı eşleşmeden fazlasını sildiği anda bir
temizlik olmaktan çıkar.**

#### Özet

- Adres alanı ham metinle 1.816 kaydın yalnız 325'inde kütükle eşleşir; normalleştirme merdiveni
  eşleşmeyi 604, 604, 937, 1.490 ve 1.816 kayda çıkarır.
- Basamaklar eşit iş yapmaz: kırpma ve boşluk daraltma 279, dile duyarlı küçültme 333, işaret
  sadeleştirme 553, kısaltma açma 326 kayıt kurtarır; toplam 1.491.
- Dile duyarsız küçültme sıfır kayıt kurtarır: harflerin bir bölümünü görünmeyen bir işaretle
  bırakır, bir bölümünü yanlış harfe çevirir.
- Bütün ayraçları atmak eşleşmeyi hiç artırmaz ve kütükteki 1.178 adresi 1.165 anahtara indirir:
  13 anahtar 26 adresi üst üste bindirir, kümede 36 kayıt tek bir aboneye bağlanamaz olur.
- Adımın hesabı: giren 1.816, çıkan 1.816; 0 elendi, 0 sessizce düştü, 1.491 kayıtta eşleşme
  kurtarıldı ve 0 ayrım silindi.
- Altı adımın toplamı: 2.086 satır girdi, 1.816 satır çıktı; 18 elendi, 188 birleşti, 64 ayrı
  deftere geçti ve sessizce düşen satır sayısı sıfır kaldı.

#### Sonraki Adım

Konu boyunca satırların içi açıldı ve her adımın hesabı yazıldı. Küme artık temizdir — ve hâlâ
**yanlış biçimdedir**. Satırlar saha formunun düzenine göre durur: her satır bir ziyaret, her
ziyaret bir dönem, tarife ve bölge birer metin sütunu. Sorulacak sorular ise abone başına, dönem
başına ve bölge başına sorulmuştur ve bu biçimde cevaplanamaz. Sonraki konu kümeyi kaynağın
biçiminden çıkarıp sorunun biçimine sokar: iki kaynağı anahtar üzerinden birleştirir ve
birleştirmenin satır sayısını nasıl **çoğalttığını** sayar.

## Dönüştürme

Kaynağa göre biçimlenmiş kümenin soruya göre yeniden biçimlendirilmesi ve her dönüşümün satır hesabı: birleştirmede eşleşmeyen, çoğalan ve düşen kayıt, geniş ile uzun biçim arasındaki dönüşümün kayıpsız olup olmadığı, toplulaştırma sonrası kalan satır ve kaybolan grup içi değişkenlik, türetmenin tanımsız kaldığı satır sayısı, sırasız bir kategoriyi sıralı kodlamanın ürettiği yanlış sonuç, hazırlama adımlarının betikle belgelenmesi ve kaç adımın yeniden üretilebildiği.

### Birleştirme ve İlişkilendirme

Source: https://academia.sh/tr/kurslar/veri-hazirlama/birlestirme-ve-iliskilendirme

Temizlenmiş 1.329 abone ile 3.471 okuma anahtar üzerinden birleştirilir ve iç birleştirme 3.302 satır verir: 169 okuma aboneyi bulamadığı için, 69 abone hiç okuma taşımadığı için hiçbir uyarı üretmeden düşer. Dış birleştirme aynı kümeden 3.371 satır çıkarır. Anahtarın tanesi yanlış seçildiğinde okuma ile ziyaret tablosu 3.302 satırdan 2.047 satıra iner ama bu iniş bir eleme değildir: 1.901 okuma düşerken 488 okuma 1.134 satıra çoğalır ve toplam tüketim 65.185,6 m³'ten 40.374,1 m³'e kayar. Aynı abone ve dönem çiftinin iki kez geçtiği 103 satır ise toplamı 2.124,9 m³ şişirir.

Bir önceki konu satırların içini açtı ve bitirdiğinde küme temizdi: eksikler işaretlenmiş, yinelemeler
kaldırılmış, tipler dönüştürülmüş, kategoriler ve metinler düzeltilmişti. Yine de küme çözümlemeye
hazır değil, çünkü hâlâ **soruya göre değil kaynağa göre** biçimli. Abone bilgisi bir tabloda, sayaç
okumaları başka bir tabloda, saha ziyaretleri üçüncü bir tablodadır; hiçbir soru bu üç tablonun
birine tek başına sorulamaz.

Bu konu kümeyi soruya göre yeniden biçimlendirir ve ilk adım tabloları yan yana getirmektir. Adımın
ölçüsü şudur: **birleştirme satır sayısını değiştirir** ve değişimin yönü tek başına bilgi vermez.
Satır sayısı düşebilir, artabilir, aynı kalabilir; üçü de hem doğru hem de sessiz bir hatanın işareti
olabilir. Bu yüzden bu derste giren, çıkan ve aradaki farkın nereye gittiği kalem kalem yazılır.

- **VD1.** Küme, önceki konudan çıkan temizlenmiş halidir ve **kurgudur**: `abone` tablosu bir satırda
  bir abone, `okuma` tablosu bir satırda bir dönem okuması, `ziyaret` tablosu bir satırda bir saha
  ziyareti taşır. Üç tablo da `node` ile **modellenir**; hiçbir sorgu arayüzü taklit edilmez.
- **VD2. Birleştirme** (join), iki tablonun satırlarını bir **anahtar** üzerinden yan yana getirir.
  Veritabanları kursunda tanıtılan **birleşim** (union) ise iki tablonun satırlarını alt alta ekler
  ve sütun düzenini değiştirmez: biri satır ekler, öteki sütun ekler ve satır sayısını anahtarın
  eşleşme sayısına göre değiştirir. Bu derste ölçülen ikincisidir.
- **VD3. İç birleştirme** yalnız eşleşen satırları çıkarır; iki taraftaki eşleşmeyenler düşer. **Dış
  birleştirme** sol tablonun eşleşmeyen satırını boş bir sağ tarafla korur. İkisi arasındaki fark bir
  tercih değil, bir **sayıdır**.
- **VD4. Anahtarın tanesi** birleştirmenin kendisinden ayrı bir karardır. Sol tablonun anahtarı sağ
  tabloda birden çok satıra karşılık geliyorsa sol satır o kadar kez çıkar; buna **çoğalma** denir.
- **VD5.** Bir birleştirme aynı anda hem düşürebilir hem çoğaltabilir. Bu durumda çıkan satır sayısı
  girenden az olsa bile bir eleme yapılmamıştır ve toplamlar hem eksik hem fazla sayılmış olur.

#### İç Birleştirme Kimi Düşürüyor

```js
// birlestir.mjs — MODELDIR: temizlemeden cikan uc tablo ve anahtar uzerinden
// birlestirme. Veri kumesi kurgudur; hicbir sorgu arayuzu taklit edilmez.
export const TOHUM = 20260218, HAM = 1400, DONEM = ["2026-01", "2026-02", "2026-03"];
const karistir = (x) => { x = Math.imul(x ^ (x >>> 16), 2246822507);
  x = Math.imul(x ^ (x >>> 13), 3266489909); return (x ^ (x >>> 16)) >>> 0; };
const uretec = (t) => { let s = karistir(t);
  return () => (s = (s * 1664525 + 1013904223) >>> 0) / 4294967296; };
const ayrik = (u, w) => { let t = 0;
  for (let i = 0; i < w.length; i++) { t += w[i]; if (u < t) return i; } return w.length - 1; };
export const BOLGE = [["kuzey", 0.28, 21], ["guney", 0.22, 17], ["dogu", 0.18, 26],
  ["bati", 0.14, 14], ["merkez", 0.18, 23]].map(([ad, pay, taban]) => ({ ad, pay, taban }));

// Abone tablosu: temizlemede elenenler disarida kaldi, kalan her satir bir abone.
export const ABONE = [], ELENEN = [];
for (let i = 0; i < HAM; i++) {
  const r = uretec(TOHUM + i), b = BOLGE[ayrik(r(), BOLGE.map((x) => x.pay))];
  const k = { abone_no: 10001 + i, bolge: b.ad, taban: b.taban,
    hane_kisi: ayrik(r(), [0.06, 0.24, 0.30, 0.24, 0.11, 0.05]),
    memnuniyet: ayrik(r(), [0.08, 0.14, 0.27, 0.34, 0.17]) + 1 };
  (r() < 0.046 ? ELENEN : ABONE).push(k);
}
// Okuma tablosu: bazi okumalar elenen abonelere aittir, bazi aboneler hic okuma
// tasimaz, bazi abone+donem cifti iki farkli degerle iki kez gecer.
export const OKUMA = [];
let s = 0;
for (const k of [...ABONE, ...ELENEN]) {
  const r = uretec(TOHUM + 7000 + k.abone_no), adet = ayrik(r(), [0.05, 0.12, 0.21, 0.62]);
  for (let d = 0; d < adet; d++) {
    const t = r() < 0.038 ? 0 : Math.round(k.taban * Math.exp((r() + r() + r() - 1.5) * 0.62)
      * (1 - d * 0.05) * 100) / 100;
    OKUMA.push({ satir_no: ++s, abone_no: k.abone_no, donem: DONEM[d], tuketim_m3: t });
    if (r() < 0.028) OKUMA.push({ satir_no: ++s, abone_no: k.abone_no, donem: DONEM[d],
      tuketim_m3: Math.round(t * 1.02 * 100) / 100 });
  }
}
// Ziyaret tablosu: bir satir bir saha ziyareti; doneme bagli degildir.
export const ZIYARET = [];
for (const k of ABONE) {
  const r = uretec(TOHUM + 31000 + k.abone_no), adet = ayrik(r(), [0.58, 0.27, 0.11, 0.04]);
  for (let z = 0; z < adet; z++) ZIYARET.push({ abone_no: k.abone_no,
    sonuc: ["olumlu", "tekrar", "erisilemedi"][ayrik(r(), [0.60, 0.28, 0.12])] });
}

// Sol tablonun her satiri, anahtari esit olan sag satirlarla yan yana gelir.
// "dis" secenegi eslesmeyen sol satiri bos bir sag tarafla korur.
export const birlestir = (sol, sag, ka, kb, dis = false) => {
  const dizin = new Map();
  for (const r of sag) { const k = kb(r);
    if (!dizin.has(k)) dizin.set(k, []); dizin.get(k).push(r); }
  const cikti = [], eslesen = new Set();
  let bosSol = 0;
  for (const l of sol) {
    const e = dizin.get(ka(l));
    if (!e) { bosSol++; if (dis) cikti.push({ sol: l, sag: null }); continue; }
    for (const r of e) { cikti.push({ sol: l, sag: r }); eslesen.add(r); }
  }
  return { cikti, bosSol, bosSag: sag.length - eslesen.size };
};
export const gen = (w) => (h) => h.map((v, j) => (j ? String(v).padStart(w[j])
  : String(v).padEnd(w[0]))).join("");

if (import.meta.url === `file://${process.argv[1]}`) {
  const no = (x) => x.abone_no;
  console.log(`model (tohum ${TOHUM}): abone ${ABONE.length} satir, okuma ` +
    `${OKUMA.length} satir, ziyaret ${ZIYARET.length} satir`);
  const y = gen([28, 11, 11, 9, 12, 12]);
  console.log("\n" + y(["birlestirme", "giren sol", "giren sag", "cikan", "bos sol",
    "bos sag"]));
  const B = [
    ["B1 okuma <- abone (ic)", OKUMA, ABONE, false],
    ["B2 abone <- okuma (ic)", ABONE, OKUMA, false],
    ["B3 abone <- okuma (dis)", ABONE, OKUMA, true],
  ];
  const sonuc = {};
  for (const [ad, sol, sag, dis] of B) {
    const r = birlestir(sol, sag, no, no, dis);
    sonuc[ad.slice(0, 2)] = r;
    console.log(y([ad, sol.length, sag.length, r.cikti.length, r.bosSol, r.bosSag]));
  }
  console.log(`\nic birlestirme ${OKUMA.length} okumadan ${sonuc.B1.bosSol} tanesini ` +
    `dusurur; ayni birlestirme ${ABONE.length} abonenin ${sonuc.B1.bosSag} tanesini ` +
    `cikti tablosunda hic gostermez`);
  console.log(`dis birlestirme o ${sonuc.B3.bosSol} aboneyi bos okumayla korur: ` +
    `${sonuc.B2.cikti.length} yerine ${sonuc.B3.cikti.length} satir`);
  console.log(`iki birlestirmede de hicbir uyari uretilmedi`);
}
```

```
model (tohum 20260218): abone 1329 satir, okuma 3471 satir, ziyaret 818 satir

birlestirme                   giren sol  giren sag    cikan     bos sol     bos sag
B1 okuma <- abone (ic)             3471       1329     3302         169          69
B2 abone <- okuma (ic)             1329       3471     3302          69         169
B3 abone <- okuma (dis)            1329       3471     3371          69         169

ic birlestirme 3471 okumadan 169 tanesini dusurur; ayni birlestirme 1329 abonenin 69 tanesini cikti tablosunda hic gostermez
dis birlestirme o 69 aboneyi bos okumayla korur: 3302 yerine 3371 satir
iki birlestirmede de hicbir uyari uretilmedi
```

Üç satır aynı iki tabloyu okuyor ve üç farklı sayı veriyor. **B1** ile **B2** yalnız hangi tablonun
sol olduğunda ayrılır ve ikisi de 3.302 satır çıkarır: iç birleştirmede taraf sırası çıkan satırı
değiştirmez, yalnız eşleşmeyenin hangi sütunda raporlandığını değiştirir. **B3** aynı anahtarla 69
satır fazla verir, çünkü hiç okuma taşımayan 69 aboneyi boş bir okuma ile korur.

Kaybın hesabı iki kalemdir. **169 okuma** bir abone numarası taşıyor ama o numara abone tablosunda
yok; bunlar temizlemede elenen 71 kaydın okumalarıdır. **69 abone** ise gerçek bir abonedir, sayacı
o dönemde hiç okunmamıştır ve iç birleştirmenin çıktısında hiç görünmez. İkincisi bu dersin
**sessiz kaybıdır**: bir hata mesajı yoktur, bir boş hücre bile yoktur, satır yalnızca hiç
üretilmemiştir. "Ortalama tüketim" sorusu iç birleştirmenin çıktısına sorulursa, hiç okunmamış 69
abone cevaba katılmaz; bu 69 abonenin düşmesinin rastgele mi yoksa desenli mi olduğu ise çıktıdan
hiç okunamaz.

Ölçü buradan çıkıyor: **iç birleştirme bir eleme kararıdır** ve bir soruya cevap veren tabloyu
hazırlarken alınmış olur. Dış birleştirme kararı geri okunabilir kılar, çünkü 69 boş satır çıktıda
görünür ve sayılabilir.

İki taraf ayrı ayrı raporlanmadığında bu ayrım görünmez. Tek bir "3.302 satır birleştirildi"
cümlesi hem 169 okumanın hem 69 abonenin düştüğünü aynı anda gizler; oysa iki kalem birbirinden
bağımsızdır ve farklı sebeplerle oluşmuştur. Birincisi önceki adımın, yani temizlemenin bıraktığı
bir izdir: elenen 71 kaydın okumaları öksüz kalmıştır. İkincisi ise verinin kendi eksikliğidir ve
temizleme onu göremezdi, çünkü okuma tablosunda olmayan bir satırın eksik olduğu ancak abone
tablosuyla karşılaştırınca anlaşılır. Bir birleştirme raporu bu yüzden tek sayı değil, en az dört
sayı taşır: iki tarafın giren satırı, çıkan satır ve iki taraftaki eşleşmeyen.

#### Anahtarın Tanesi Yanlışsa

```js
// tane.mjs — ayni anahtar, yanlis tane: okuma satiri donem tasir, ziyaret satiri
// tasimaz. Ikisi yalniz abone_no uzerinden birlestirilirse ne dusuyor, ne cogaliyor.
import { ABONE, OKUMA, ZIYARET, birlestir, gen } from "./birlestir.mjs";

const no = (x) => x.abone_no;
const ic = birlestir(OKUMA, ABONE, no, no).cikti.map((x) => x.sol);
const zSayi = new Map();
for (const z of ZIYARET) zSayi.set(z.abone_no, (zSayi.get(z.abone_no) ?? 0) + 1);
const oz = birlestir(ic, ZIYARET, no, no);
const dusen = ic.filter((o) => !zSayi.has(o.abone_no)).length;
const birebir = ic.filter((o) => zSayi.get(o.abone_no) === 1).length;
const cogalan = ic.filter((o) => (zSayi.get(o.abone_no) ?? 0) > 1).length;

const y = gen([34, 10, 10]);
console.log(y(["okuma x ziyaret (abone_no uzerinden)", "okuma", "satir"]));
console.log(y(["giren", ic.length, ic.length]));
console.log(y(["ziyareti yok: dustu", dusen, 0]));
console.log(y(["tek ziyaret: birebir kaldi", birebir, birebir]));
console.log(y(["cok ziyaret: cogaldi", cogalan, oz.cikti.length - birebir]));
console.log(y(["cikan", "", oz.cikti.length]));

const T = (a, f) => a.reduce((x, o) => x + f(o), 0);
const oncekiT = T(ic, (o) => o.tuketim_m3);
const sonrakiT = T(oz.cikti, (x) => x.sol.tuketim_m3);
console.log(`\ntoplam tuketim ${oncekiT.toFixed(1)} m3 iken ${sonrakiT.toFixed(1)} m3 oldu; ` +
  `${ic.length} satirdan ${oz.cikti.length} satira inildi ama bu bir eleme degil`);
console.log(`${dusen} okuma hic sayilmadi, ${cogalan} okuma ` +
  `${oz.cikti.length - birebir} kez sayildi`);

// Dogru tane: bir abonenin bir donemi bir satirdir. Ayni cift iki kez geciyorsa
// temizleme onu kesin eslesme saymamistir, cunku iki okuma degeri farklidir.
const tekil = new Map();
for (const o of ic) { const k = `${o.abone_no}|${o.donem}`; if (!tekil.has(k)) tekil.set(k, o); }
const tekilT = T([...tekil.values()], (o) => o.tuketim_m3);
console.log(`\nabone+donem cifti: ${ic.length} satirda ${tekil.size} tekil cift var; ` +
  `${ic.length - tekil.size} satir ayni donemi ikinci kez sayiyor`);
console.log(`toplam ${oncekiT.toFixed(1)} m3 yerine ${tekilT.toFixed(1)} m3; ` +
  `fark ${(oncekiT - tekilT).toFixed(1)} m3`);
console.log(`giren 3471 okuma -> cikan ${tekil.size} satir: 169 eslesmeyen, ` +
  `${ic.length - tekil.size} cift gecen, 69 abone hic gorunmeyen`);
```

```
okuma x ziyaret (abone_no uzerinden)     okuma     satir
giren                                   3302      3302
ziyareti yok: dustu                     1901         0
tek ziyaret: birebir kaldi               913       913
cok ziyaret: cogaldi                     488      1134
cikan                                             2047

toplam tuketim 65185.6 m3 iken 40374.1 m3 oldu; 3302 satirdan 2047 satira inildi ama bu bir eleme degil
1901 okuma hic sayilmadi, 488 okuma 1134 kez sayildi

abone+donem cifti: 3302 satirda 3199 tekil cift var; 103 satir ayni donemi ikinci kez sayiyor
toplam 65185.6 m3 yerine 63060.6 m3; fark 2124.9 m3
giren 3471 okuma -> cikan 3199 satir: 169 eslesmeyen, 103 cift gecen, 69 abone hic gorunmeyen
```

Bu tablo dersin asıl bulgusudur. Okuma tablosu bir **abone ve dönem** taşır, ziyaret tablosu yalnız
bir **abone** taşır. İkisi yalnız `abone_no` üzerinden birleştirilirse anahtarın tanesi uyuşmaz: bir
abonenin üç okuması, o abonenin iki ziyaretiyle altı satır üretir.

Sonuçta çıkan satır sayısı girenden **azdır** — 3.302'den 2.047'ye — ve bu tek başına bakıldığında
bir eleme gibi görünür. Değildir. Aynı işlem içinde iki karşıt şey oldu: ziyareti hiç olmayan
abonelerin **1.901 okuması düştü**, birden çok ziyareti olan abonelerin **488 okuması 1.134 satıra
çoğaldı**. Net değişim negatif olduğu için çoğalma sayı üzerinde görünmez. Toplam tüketim
65.185,6 m³'ten 40.374,1 m³'e iner; bu, "tüketim düştü" diye okunabilecek bir sayıdır ve tek bir
okuma değişmemiştir.

İkinci ölçüm doğru anahtarla bile tanenin sorulması gerektiğini gösteriyor. İç birleştirmenin verdiği
3.302 satırda yalnız **3.199 tekil abone ve dönem çifti** vardır; 103 satır aynı dönemi ikinci kez
sayar. Bunlar temizlemenin kesin eşleşme kuralına takılmamıştır: 103 çiftin 101'inde iki kaydın
tüketim değeri birbirinden farklıdır, kalan ikisinde de satır numaraları ayrışır. Toplama etkisi
2.124,9 m³'tür, yani toplamın yüzde üçünden fazlası.

Dersin kapanış hesabı son satırdadır: 3.471 okuma girdi, 3.199 satır çıktı; 169 kayıt eşleşmedi,
103 kayıt aynı dönemi ikinci kez saydığı için düşürüldü, 69 abone çıktıda hiç görünmedi. Üç kalem de
yazıldığında birleştirme bir hazırlama adımıdır; yazılmadığında yalnız bir değişikliktir.

#### Özet

- Birleştirme satırları anahtar üzerinden yan yana getirir ve sütun ekler; birleşim satırları alt
  alta ekler. Birleştirmede satır sayısı anahtarın eşleşme sayısına göre değişir.
- İç birleştirme 3.471 okuma ile 1.329 aboneden 3.302 satır çıkarır: 169 okuma eşleşmedi, 69 abone
  çıktıda hiç görünmedi. Dış birleştirme o 69 aboneyi koruyarak 3.371 satır verir.
- Hiç okunmamış 69 abonenin sessizce düşmesi bir hata üretmez ve "ortalama tüketim" sorusunu bu 69
  abone olmadan cevaplar.
- Anahtarın tanesi uyuşmadığında bir birleştirme aynı anda düşürür ve çoğaltır: 1.901 okuma hiç
  sayılmaz, 488 okuma 1.134 kez sayılır, çıkan satır girenden az olduğu için çoğalma görünmez.
- Doğru anahtarla bile tane sorulur: 3.302 satırda 3.199 tekil abone ve dönem çifti vardır ve 103
  tekrar toplamı 2.124,9 m³ şişirir.

#### Sonraki Adım

Tablolar yan yana geldi ve elde bir satırda bir abonenin bir dönem okumasını taşıyan tek bir küme
var. Bu biçim her soruya uygun değil. "Bir abonenin üç dönemi arasındaki değişim" sorusu üç satırı
karşılaştırmayı gerektirir; oysa aynı bilgi, her dönemi bir sütun yapan bir biçimde tek satırda
durabilir. İki biçim arasında gidip gelmek görünüşte biçimsel bir işlemdir, ama bir yönde bilgi
kaybolabilir ve kayıp geri dönüşte fark edilir. Sonraki ders bu iki biçimi tanımlar ve dönüşümün
kayıpsız olup olmadığını, geri dönüldüğünde özgün kümeye ulaşılıp ulaşılmadığını sayarak ölçer.

### Yeniden Şekillendirme

Source: https://academia.sh/tr/kurslar/veri-hazirlama/yeniden-sekillendirme

3.199 satırlık uzun biçimli küme 1.260 satır ve üç dönem sütunundan oluşan geniş biçime çevrilir; 3.780 hücrenin 3.199'u dolar, 581'i boş kalır. Yineleyen abone ve dönem çifti taşıyan 3.302 satırlık küme aynı geniş biçime sığmaz ve 103 satır hiçbir uyarı üretmeden üzerine yazılır. Geri dönüşün üç kararı üç ayrı satır sayısı verir: boş hücre atlanırsa 3.199 satır ve 19,713 m³ ortalama, boş hücre sıfırla doldurulursa 3.780 satır ve 16,683 m³, doğruluk sınamasıyla atlanırsa 119 gerçek sıfır okuma da silinir, 3.080 satır ve 20,474 m³ kalır. Üç yolda da toplam 63.060,6 m³'te sabittir.

Bir önceki ders üç tabloyu tek bir kümede topladı ve elde bir satırda bir abonenin bir dönem
okumasını taşıyan 3.199 satır kaldı. Bu biçim bazı sorulara doğrudan cevap verir: "toplam tüketim
nedir" sorusu tek bir sütunun toplamıdır. Bazı sorulara vermez. "Bir abonenin ocak ile mart arasında
tüketimi ne kadar değişti" sorusu, aynı abonenin iki ayrı satırını bulup yan yana koymayı gerektirir
ve bu satır bazlı bir küme üzerinde doğrudan yapılamaz.

Bu ders kümenin **şeklini** değiştirir. Şekil değişimi bir hesap değildir; hiçbir değer toplanmaz,
bölünmez, elenmez. Buna rağmen satır sayısı değişir ve bazı durumlarda değer de kaybolur. Dersin
ölçüsü şudur: dönüşüm **kayıpsız mı**, yani geniş biçime çevrilip geri dönüldüğünde özgün kümeye
ulaşılıyor mu.

- **VD6. Uzun biçim** her ölçümü bir satır yapar: anahtar sütunları (abone ve dönem), bir değişken
  adı ve bir değer. **Geniş biçim** anahtarın bir bölümünü sütun başlığına taşır: bir satır bir
  abone, üç sütun üç dönemdir. Küme aynı kümedir, satır sayısı değildir.
- **VD7.** Geniş biçimde satır sayısı **tekil anahtar** sayısına, hücre sayısı ise satır ile sütunun
  çarpımına eşittir. Uzun biçimde karşılığı olmayan her çarpım hücresi **boş** kalır.
- **VD8.** Geniş biçim aynı anahtar çifti için tek bir hücre taşır. Uzun biçimde bir abone ve dönem
  çifti iki kez geçiyorsa ikinci değer birincinin **üzerine yazılır** ve hiçbir uyarı üretilmez.
- **VD9.** Geri dönüşte **boş hücre kararı** dönüşümün kendisi kadar belirleyicidir: boş hücre
  atlanabilir, bir değerle doldurulabilir ya da yanlış bir sınamayla gerçek değerleri de silebilir.
- **VD10.** Kayıpsızlık ölçülebilir bir şeydir: geri dönen küme ile özgün küme satır sayısı, anahtar
  kümesi ve toplam bakımından karşılaştırılır. Üçü aynıysa dönüşüm kayıpsızdır.

#### Aynı Küme, İki Şekil

```js
// sekil.mjs — MODELDIR: onceki dersin cikardigi uzun bicimli kume ve ayni
// kumenin genis bicimi. Veri kumesi kurgudur; hicbir arayuz taklit edilmez.
export const TOHUM = 20260218, HAM = 1400, DONEM = ["2026-01", "2026-02", "2026-03"];
const karistir = (x) => { x = Math.imul(x ^ (x >>> 16), 2246822507);
  x = Math.imul(x ^ (x >>> 13), 3266489909); return (x ^ (x >>> 16)) >>> 0; };
const uretec = (t) => { let s = karistir(t);
  return () => (s = (s * 1664525 + 1013904223) >>> 0) / 4294967296; };
const ayrik = (u, w) => { let t = 0;
  for (let i = 0; i < w.length; i++) { t += w[i]; if (u < t) return i; } return w.length - 1; };
const BOLGE = [["kuzey", 0.28, 21], ["guney", 0.22, 17], ["dogu", 0.18, 26],
  ["bati", 0.14, 14], ["merkez", 0.18, 23]];
const ABONE = [], ELENEN = [];
for (let i = 0; i < HAM; i++) {
  const r = uretec(TOHUM + i), b = BOLGE[ayrik(r(), BOLGE.map((x) => x[1]))];
  const k = { abone_no: 10001 + i, bolge: b[0], taban: b[2],
    hane_kisi: ayrik(r(), [0.06, 0.24, 0.30, 0.24, 0.11, 0.05]),
    memnuniyet: ayrik(r(), [0.08, 0.14, 0.27, 0.34, 0.17]) + 1 };
  (r() < 0.046 ? ELENEN : ABONE).push(k);
}
const anahtar = new Set(ABONE.map((a) => a.abone_no));
// Onceki dersin ic birlestirmesinden gecmis satirlar: eslesmeyen 169 okuma yok,
// ayni abone+donem ciftini ikinci kez tasiyan satirlar hala icinde.
export const YINELI = [];
for (const k of [...ABONE, ...ELENEN]) {
  const r = uretec(TOHUM + 7000 + k.abone_no), adet = ayrik(r(), [0.05, 0.12, 0.21, 0.62]);
  for (let d = 0; d < adet; d++) {
    const t = r() < 0.038 ? 0 : Math.round(k.taban * Math.exp((r() + r() + r() - 1.5) * 0.62)
      * (1 - d * 0.05) * 100) / 100;
    if (!anahtar.has(k.abone_no)) { r(); continue; }
    YINELI.push({ abone_no: k.abone_no, bolge: k.bolge, donem: DONEM[d], tuketim_m3: t });
    if (r() < 0.028) YINELI.push({ abone_no: k.abone_no, bolge: k.bolge, donem: DONEM[d],
      tuketim_m3: Math.round(t * 1.02 * 100) / 100 });
  }
}
// Onceki dersin kapanis kumesi: bir abone+donem cifti bir satir.
const ilk = new Map();
for (const o of YINELI) { const a = `${o.abone_no}|${o.donem}`;
  if (!ilk.has(a)) ilk.set(a, o); }
export const UZUN = [...ilk.values()];

// Genisletme: anahtarin bir bolumu sutun basligina tasinir. Ayni hucreye ikinci
// kez yazilan deger birincinin uzerine yazilir; islem hata uretmez.
export const genislet = (uzun) => {
  const satir = new Map();
  let yazma = 0, uzerine = 0;
  for (const o of uzun) {
    if (!satir.has(o.abone_no)) satir.set(o.abone_no,
      { abone_no: o.abone_no, bolge: o.bolge, ...Object.fromEntries(DONEM.map((d) => [d, null])) });
    const s = satir.get(o.abone_no);
    if (s[o.donem] !== null) uzerine++; else yazma++;
    s[o.donem] = o.tuketim_m3;
  }
  return { genis: [...satir.values()], yazma, uzerine };
};
export const gen = (w) => (h) => h.map((v, j) => (j ? String(v).padStart(w[j])
  : String(v).padEnd(w[0]))).join("");

if (import.meta.url === `file://${process.argv[1]}`) {
  const G = genislet(UZUN), Y = genislet(YINELI);
  const hucre = G.genis.length * DONEM.length;
  console.log(`model (tohum ${TOHUM}): uzun bicim ${UZUN.length} satir, ` +
    `${G.genis.length} tekil abone, ${DONEM.length} donem`);
  const y = gen([12, 10, 12, 12, 12]);
  console.log("\ngenis bicim (ilk bes satir)");
  console.log(y(["abone_no", "bolge", ...DONEM]));
  for (const s of G.genis.slice(0, 5))
    console.log(y([s.abone_no, s.bolge, ...DONEM.map((d) => s[d] === null ? "-" : s[d])]));
  console.log(`\n${G.genis.length} satir x ${DONEM.length} sutun = ${hucre} hucre; ` +
    `${G.yazma} dolu, ${hucre - G.yazma} bos`);
  console.log(`girenin sekli degisti, satir sayisi ${UZUN.length} -> ${G.genis.length}`);
  console.log(`\nyineleyen kume genisletilirse: giren ${YINELI.length} satir, ` +
    `cikan ${Y.genis.length} satir, dolan hucre ${Y.yazma}`);
  console.log(`${Y.uzerine} satir ayni hucreye ikinci kez yazildi ve birincinin ` +
    `uzerine gecti; hicbir uyari uretilmedi`);
}
```

```
model (tohum 20260218): uzun bicim 3199 satir, 1260 tekil abone, 3 donem

genis bicim (ilk bes satir)
abone_no         bolge     2026-01     2026-02     2026-03
10001           merkez       30.09       24.65           -
10002            kuzey       20.21       22.14       21.57
10003            guney       13.42       14.12       18.75
10004            kuzey       20.01       20.29       17.48
10005            guney       18.29           -           -

1260 satir x 3 sutun = 3780 hucre; 3199 dolu, 581 bos
girenin sekli degisti, satir sayisi 3199 -> 1260

yineleyen kume genisletilirse: giren 3302 satir, cikan 1260 satir, dolan hucre 3199
103 satir ayni hucreye ikinci kez yazildi ve birincinin uzerine gecti; hicbir uyari uretilmedi
```

Geniş biçim ile uzun biçim aynı 3.199 ölçümü taşır. Fark, ölçümün nereden okunduğudur: uzun biçimde
dönem bir **değer**, geniş biçimde bir **sütun adıdır**. "Ocak ile mart arasındaki değişim" sorusu
geniş biçimde tek satırda iki sütunun farkıdır; uzun biçimde iki satırı eşleştirmeyi gerektirir.

Şeklin bedeli 581 boş hücredir. Bunlar bir eksiklik değil, uzun biçimde hiç var olmayan
kombinasyonlardır: 1.260 abonenin hepsi üç dönemde de okunmamıştır. Geniş biçim, olmayan bir satırı
**boş bir hücreye** çevirerek görünür kılar; uzun biçimde o kombinasyon hiç yoktu ve sayılamıyordu.
Bu, geniş biçimin gerçek bir kazancıdır: eksiklik ancak beklenen kombinasyon kümesi kurulduğunda
sayılabilir hale gelir.

İki biçimden biri doğru değildir; soru hangisinin uygun olduğunu belirler. Geniş biçimin bir sınırı
vardır ve satır sayısıyla ilgisizdir: sütun sayısı, başlığa taşınan anahtarın **tekil değer
sayısına** eşittir. Üç dönem üç sütun eder, otuz altı dönem otuz altı sütun eder ve dönem
sayısı önceden bilinmiyorsa tablonun şeması veriyle birlikte değişir. Uzun biçimde aynı büyüme yeni
sütun değil yeni satır üretir ve şema sabit kalır. Bu yüzden hazırlamanın çıktısı çoğunlukla uzun
biçimde saklanır ve geniş biçim yalnız belirli bir soru için, geçici olarak üretilir.

Son iki satır dersin sessiz kaybını gösteriyor. Önceki dersin ayıkladığı yinelemeler ayıklanmasaydı
elde 3.302 satır olurdu ve o küme aynı geniş biçime çevrildiğinde yine 1.260 satır ve 3.199 dolu
hücre çıkardı. **103 satır aynı hücreye ikinci kez yazıldı ve birincinin üzerine geçti.** İşlem
başarıyla tamamlandı, hata üretilmedi, satır sayısındaki düşüş de zaten beklenen bir düşüştü. Geniş
biçime çevirme bu yüzden bir tekilleştirme aracı gibi kullanılabilir; kullanıldığında hangi değerin
kaldığını kimse seçmemiştir.

#### Geri Dönüş Kayıpsız mı

```js
// geridonus.mjs — genis bicimden uzun bicime donusun uc karari ve her birinin
// ozgun kume ile farki. Kayipsizlik satir, anahtar ve toplam ile olculur.
import { UZUN, DONEM, genislet, gen } from "./sekil.mjs";

const { genis } = genislet(UZUN);
// Y1 bos hucre atlanir. Y2 bos hucre sifirla doldurulur. Y3 hucre "dogru mu"
// diye sinanir; bu sinama sifiri da bos sayar.
const YOL = [
  ["Y1 bos hucre atlanir", (v) => v !== null],
  ["Y2 bos hucre sifirla doldurulur", () => true],
  ["Y3 hucre dogruluk sinamasindan gecer", (v) => Boolean(v)],
];
const uzat = (sec) => {
  const cikti = [];
  for (const s of genis) for (const d of DONEM) {
    if (!sec(s[d])) continue;
    cikti.push({ abone_no: s.abone_no, donem: d, tuketim_m3: s[d] ?? 0 });
  }
  return cikti;
};
const T = (a) => a.reduce((x, o) => x + o.tuketim_m3, 0);
const ozgunT = T(UZUN), sifir = UZUN.filter((o) => o.tuketim_m3 === 0).length;
console.log(`ozgun uzun bicim: ${UZUN.length} satir, ${ozgunT.toFixed(1)} m3, ` +
  `icinde ${sifir} adet gercek sifir okuma`);

const y = gen([38, 9, 8, 11, 10]);
console.log("\n" + y(["geri donus karari", "satir", "fark", "toplam m3", "ortalama"]));
for (const [ad, sec] of YOL) {
  const g = uzat(sec), f = g.length - UZUN.length;
  console.log(y([ad, g.length, f > 0 ? `+${f}` : f, T(g).toFixed(1),
    (T(g) / g.length).toFixed(3)]));
}
// Kayipsizlik sinamasi: satir sayisi, anahtar kumesi ve toplam ayni mi. Toplam
// kayan noktali oldugu icin esitlik yerine esik kullanilir; 0,05 m3 secildi
// cunku okumalar 0,01 m3 duyarliginda kaydediliyor ve toplam 3199 terimlidir.
const ESIK_M3 = 0.05;
const anahtar = (a) => new Set(a.map((o) => `${o.abone_no}|${o.donem}`));
const ozgunA = anahtar(UZUN);
console.log("\n" + gen([38, 9, 10, 10, 10])(["kayipsizlik sinamasi", "satir",
  "anahtar", "toplam", "kayipsiz"]));
for (const [ad, sec] of YOL) {
  const g = uzat(sec), a = anahtar(g);
  const s1 = g.length === UZUN.length;
  const s2 = a.size === ozgunA.size && [...ozgunA].every((k) => a.has(k));
  const s3 = Math.abs(T(g) - ozgunT) < ESIK_M3;
  console.log(gen([38, 9, 10, 10, 10])([ad, s1 ? "ayni" : "farkli",
    s2 ? "ayni" : "farkli", s3 ? "ayni" : "farkli",
    s1 && s2 && s3 ? "evet" : "hayir"]));
}
console.log(`\nuc yolda da toplam ${ozgunT.toFixed(1)} m3 degismedi; ` +
  `ortalama 16,683 ile 20,474 m3 arasinda gidip geldi`);
```

```
ozgun uzun bicim: 3199 satir, 63060.6 m3, icinde 119 adet gercek sifir okuma

geri donus karari                         satir    fark  toplam m3  ortalama
Y1 bos hucre atlanir                       3199       0    63060.6    19.713
Y2 bos hucre sifirla doldurulur            3780    +581    63060.6    16.683
Y3 hucre dogruluk sinamasindan gecer       3080    -119    63060.6    20.474

kayipsizlik sinamasi                      satir   anahtar    toplam  kayipsiz
Y1 bos hucre atlanir                       ayni      ayni      ayni      evet
Y2 bos hucre sifirla doldurulur          farkli    farkli      ayni     hayir
Y3 hucre dogruluk sinamasindan gecer     farkli    farkli      ayni     hayir

uc yolda da toplam 63060.6 m3 degismedi; ortalama 16,683 ile 20,474 m3 arasinda gidip geldi
```

Yalnız **Y1** kayıpsızdır: satır sayısı, anahtar kümesi ve toplam üçü de özgün kümeyle aynıdır.
Öteki iki yol da hatasız çalışır ve ikisi de aynı toplamı verir.

**Y2** boş hücreyi sıfırla doldurur ve 581 satır **uydurur**. Bu satırlar yanlış değildir; yanlış
olan varsayımdır — okunmamış bir dönem, sıfır tüketilmiş bir dönem değildir. Anahtar kümesi özgünün
tamamını içerir ama 581 fazlası vardır, bu yüzden sınamayı geçmez. Toplam değişmediği için hata
toplamda görünmez; ortalamada görünür ve 19,713 m³'ten 16,683 m³'e iner. Sayı yüzde on beşten fazla
kaymıştır ve tek bir okuma değişmemiştir.

**Y3** dersin en sinsi kararıdır ve bir kodlama alışkanlığından doğar: hücrenin dolu olup olmadığı,
değerinin doğruluk sınamasından geçip geçmediğine bakılarak sorulmuştur. Sıfır bu sınamadan geçmez.
Sonuç, **119 gerçek sıfır okumanın** boş hücrelerle birlikte silinmesidir. Anahtar kümesi burada ters
yönde bozulur: geri dönen kümede özgünün 119 anahtarı **eksiktir**. Toplam yine değişmez, çünkü
silinen okumaların değeri sıfırdır; ortalama ise ters yöne, 20,474 m³'e çıkar.

Üç yolun toplamı aynı, ortalaması farklıdır. Bu, kayıpsızlığın neden toplamla sınanamayacağını
gösteriyor: sıfır değerli satırların eklenmesi ya da silinmesi toplamı hiç oynatmaz. Kayıpsızlık
sınaması bu yüzden **satır sayısı, anahtar kümesi ve toplam** olmak üzere üç ayrı karşılaştırma
ister. Toplam karşılaştırması kayan noktalı olduğu için eşitlikle değil 0,05 m³'lük bir eşikle
yapılır; eşik okumaların 0,01 m³ duyarlığından ve toplamın üç binden fazla terimli olmasından
seçilmiştir.

#### Özet

- Uzun biçim her ölçümü bir satır yapar, geniş biçim anahtarın bir bölümünü sütun başlığına taşır:
  aynı küme 3.199 satır ile 1.260 satır arasında yer değiştirir, tek bir değer değişmeden.
- Geniş biçimde 1.260 satır ve üç sütun 3.780 hücre üretir; 3.199'u dolar, 581'i boş kalır ve bu boş
  hücreler eksikliği ilk kez sayılabilir hale getirir.
- Yineleyen abone ve dönem çifti taşıyan 3.302 satırlık küme aynı geniş biçime sığmaz: 103 satır
  hiçbir uyarı üretmeden üzerine yazılır ve hangi değerin kaldığını kimse seçmemiştir.
- Geri dönüşün üç kararı üç satır sayısı verir: 3.199, 3.780 ve 3.080. Yalnız birincisi kayıpsızdır.
- Boş hücrenin sıfırla doldurulması ortalamayı 19,713'ten 16,683 m³'e indirir; doğruluk sınaması 119
  gerçek sıfır okumayı siler ve ortalamayı 20,474 m³'e çıkarır. Toplam üç yolda da 63.060,6 m³'tür.

#### Sonraki Adım

Şekil değişimi tek bir ölçümü bile toplamadı; 3.199 satır 1.260 satıra indiğinde her değer hâlâ
kendi hücresindeydi. Rapora giren sayı ise nadiren bir hücredir. "Bölge başına ortalama tüketim"
sorusu binlerce satırı beş satıra indirmeyi ister ve bu indirme geri alınamaz: beş satıra bakan biri
arkalarındaki dağılımı göremez, yalnız özetin bıraktığı sayıyı görür. Sonraki ders gruplamanın
ürettiği satır sayısını sayar ve grup içindeki değişkenliğin özetle birlikte ne kadarının kaybolduğunu
ölçer.

### Gruplama ve Toplulaştırma

Source: https://academia.sh/tr/kurslar/veri-hazirlama/gruplama-ve-toplulastirma

3.199 satırlık küme dört gruplama kararıyla 5, 15, 53 ve 1.260 satıra iner; bölge, tarife basamağı ve dönem çaprazının 60 gözünden 7'si hiç satır taşımaz ve çıktıda hiç görünmez. Aynı kümeden üç genel ortalama çıkar: 3.199 satırdan 19,713 m³, beş bölge satırından 19,532 m³, 53 hücreden 20,721 m³. Beş satırlık özet 3.199 okumanın 431'inin kendi grup ortalamasından yüzde elliden fazla saptığını, 58 okumanın 40 m³'ü aştığını ve doğu ile batı bölgelerinin 50 okumada yer değiştirdiğini taşımaz.

Bir önceki ders kümenin şeklini değiştirdi ama tek bir değeri bile birleştirmedi: 3.199 ölçüm, 1.260
satırlık geniş biçimde de 3.199 ayrı hücrede duruyordu. Rapora giren sayı ise nadiren bir hücredir.
"Bölge başına ortalama tüketim" sorusu 3.199 satırı beş satıra indirir ve bu indirme, önceki dersteki
şekil değişiminden farklı olarak **geri alınamaz**.

Elektronik Tablolarla Analiz kursundaki Özet Tablolar dersi bu kararı bir sayfada ölçmüş ve aynı
sayfadan beş özet kararının üç farklı bölgeyi birinci yaptığını göstermişti. Bu ders aynı işi betik
tarafında ve başka bir ölçüyle ele alır: hangi bölgenin birinci olduğu değil, **kaç satır kaldığı** ve
kaybolanın ne olduğu sayılır.

- **VD11. Toplulaştırma**, bir grubun satırlarını tek bir satıra indiren işlemdir. Çıkan satır sayısı
  hesabın değil, **gruplama alanının tekil değer sayısının** sonucudur.
- **VD12.** Çapraz kırılımda olası göz sayısı alanların tekil değerlerinin çarpımıdır. Hiç satır
  taşımayan göz çıktıda **hiç görünmez** ve okuyucu için "sıfır" ile "veri yok" ayırt edilemez.
- **VD13.** Toplulaştırılmış satırlar yeniden toplulaştırıldığında her grup **eşit ağırlık** alır.
  Bu, ham satırlardan hesaplanan sayıdan farklı bir sayı verir ve fark grup boyutlarının
  eşitsizliğinden gelir.
- **VD14.** Bir özet satırı grubun **merkezini** taşır, **yayılımını** taşımaz. Yayılım ayrıca
  hesaplanıp yanına yazılmazsa kaybolur ve geri getirilemez.
- **VD15.** Kaybın ölçüsü, özetten cevaplanamayan sorulardır: özet satırından okunan cevapla ham
  satırlardan okunan cevap yan yana konur.

#### Kaç Satır Kalıyor

```js
// grupla.mjs — MODELDIR: onceki dersin uzun bicimli kumesi ve dort gruplama
// karari. Veri kumesi kurgudur; hicbir sorgu arayuzu taklit edilmez.
export const TOHUM = 20260218, HAM = 1400, DONEM = ["2026-01", "2026-02", "2026-03"];
export const TARIFE = [10, 25, 40];
const karistir = (x) => { x = Math.imul(x ^ (x >>> 16), 2246822507);
  x = Math.imul(x ^ (x >>> 13), 3266489909); return (x ^ (x >>> 16)) >>> 0; };
const uretec = (t) => { let s = karistir(t);
  return () => (s = (s * 1664525 + 1013904223) >>> 0) / 4294967296; };
const ayrik = (u, w) => { let t = 0;
  for (let i = 0; i < w.length; i++) { t += w[i]; if (u < t) return i; } return w.length - 1; };
const BOLGE = [["kuzey", 0.28, 21], ["guney", 0.22, 17], ["dogu", 0.18, 26],
  ["bati", 0.14, 14], ["merkez", 0.18, 23]];
const ABONE = [], ELENEN = [];
for (let i = 0; i < HAM; i++) {
  const r = uretec(TOHUM + i), b = BOLGE[ayrik(r(), BOLGE.map((x) => x[1]))];
  const k = { abone_no: 10001 + i, bolge: b[0], taban: b[2],
    hane_kisi: ayrik(r(), [0.06, 0.24, 0.30, 0.24, 0.11, 0.05]),
    memnuniyet: ayrik(r(), [0.08, 0.14, 0.27, 0.34, 0.17]) + 1 };
  (r() < 0.046 ? ELENEN : ABONE).push(k);
}
const anahtar = new Set(ABONE.map((a) => a.abone_no)), ham = [];
for (const k of [...ABONE, ...ELENEN]) {
  const r = uretec(TOHUM + 7000 + k.abone_no), adet = ayrik(r(), [0.05, 0.12, 0.21, 0.62]);
  for (let d = 0; d < adet; d++) {
    const t = r() < 0.038 ? 0 : Math.round(k.taban * Math.exp((r() + r() + r() - 1.5) * 0.62)
      * (1 - d * 0.05) * 100) / 100;
    if (!anahtar.has(k.abone_no)) { r(); continue; }
    ham.push({ abone_no: k.abone_no, bolge: k.bolge, donem: DONEM[d], tuketim_m3: t });
    if (r() < 0.028) ham.push({ abone_no: k.abone_no, bolge: k.bolge, donem: DONEM[d],
      tuketim_m3: Math.round(t * 1.02 * 100) / 100 });
  }
}
// Onceki dersin kapanis kumesi; tarife basamagi okumadan turetilir, karsilastirma
// her esikte kucuktur ile yapilir.
const ilk = new Map();
for (const o of ham) { const a = `${o.abone_no}|${o.donem}`; if (!ilk.has(a)) ilk.set(a, o); }
export const UZUN = [...ilk.values()].map((o) => ({ ...o,
  basamak: o.tuketim_m3 < TARIFE[0] ? 1 : o.tuketim_m3 < TARIFE[1] ? 2
    : o.tuketim_m3 < TARIFE[2] ? 3 : 4 }));

export const grupla = (a, f) => {
  const m = new Map();
  for (const o of a) { const k = f(o); if (!m.has(k)) m.set(k, []); m.get(k).push(o); }
  return m;
};
export const ort = (a) => a.reduce((x, y) => x + y, 0) / a.length;
export const gen = (w) => (h) => h.map((v, j) => (j ? String(v).padStart(w[j])
  : String(v).padEnd(w[0]))).join("");

if (import.meta.url === `file://${process.argv[1]}`) {
  console.log(`model (tohum ${TOHUM}): giren ${UZUN.length} satir, ` +
    `${new Set(UZUN.map((o) => o.abone_no)).size} abone, 5 bolge, ` +
    `${DONEM.length} donem, ${TARIFE.length + 1} tarife basamagi`);
  const K = [
    ["G1 bolge", (o) => o.bolge, 5],
    ["G2 bolge x donem", (o) => `${o.bolge}|${o.donem}`, 15],
    ["G3 bolge x basamak x donem", (o) => `${o.bolge}|${o.basamak}|${o.donem}`, 60],
    ["G4 abone", (o) => o.abone_no, null],
  ];
  const y = gen([30, 9, 9, 10, 12, 10]);
  console.log("\n" + y(["gruplama karari", "giren", "cikan", "olasi goz", "gozde ort.",
    "bos goz"]));
  for (const [ad, f, olasi] of K) {
    const g = grupla(UZUN, f);
    console.log(y([ad, UZUN.length, g.size, olasi ?? "-",
      (UZUN.length / g.size).toFixed(1), olasi === null ? "-" : olasi - g.size]));
  }
  const g3 = grupla(UZUN, (o) => `${o.bolge}|${o.basamak}|${o.donem}`);
  console.log(`\ncapraz kirilimin ${60 - g3.size} gozu hic satir tasimiyor ve cikti ` +
    `tablosunda hic gorunmuyor; okuyan taraf "sifir" ile "veri yok" arasini ayiramaz`);
  const tek = [...grupla(UZUN, (o) => `${o.bolge}|${o.basamak}|${o.donem}`).values()]
    .filter((l) => l.length === 1).length;
  console.log(`ayni kirilimda ${tek} goz tek bir okumadan olusuyor: o gozun ` +
    `"ortalamasi" bir okumanin kendisidir`);
}
```

```
model (tohum 20260218): giren 3199 satir, 1260 abone, 5 bolge, 3 donem, 4 tarife basamagi

gruplama karari                   giren    cikan olasi goz  gozde ort.   bos goz
G1 bolge                           3199        5         5       639.8         0
G2 bolge x donem                   3199       15        15       213.3         0
G3 bolge x basamak x donem         3199       53        60        60.4         7
G4 abone                           3199     1260         -         2.5         -

capraz kirilimin 7 gozu hic satir tasimiyor ve cikti tablosunda hic gorunmuyor; okuyan taraf "sifir" ile "veri yok" arasini ayiramaz
ayni kirilimda 1 goz tek bir okumadan olusuyor: o gozun "ortalamasi" bir okumanin kendisidir
```

Dört karar da aynı 3.199 satırı okuyor ve dört farklı satır sayısı üretiyor. Çıkan satır sayısı
hesaptan gelmiyor: `G1` beş satır verir çünkü beş bölge vardır, `G4` 1.260 satır verir çünkü 1.260
abone vardır. Toplulaştırmanın kendisi tek bir satırın nasıl hesaplandığını belirler; **kaç satır
kalacağını gruplama alanı belirler**.

Üçüncü satır bu dersin sessiz kaybını taşıyor. Bölge, tarife basamağı ve dönemin çaprazı **60 göz**
tanımlar ama çıktı **53 satırdır**. Eksik 7 göz bir hata değil, o kombinasyonda hiç okuma
bulunmamasıdır: örneğin belirli bir bölgede belirli bir dönemde en üst tarife basamağına giren bir
okuma yoktur. Sorun, bu 7 gözün çıktıda **hiç görünmemesidir**. Çıktıyı okuyan biri 53 satır görür ve
eksik olanın sıfır tüketimli bir göz mü, hiç ölçüm yapılmamış bir göz mü olduğunu ayırt edemez. Boş
gözün sayılabilmesi için beklenen göz kümesinin ayrıca kurulması gerekir, tıpkı önceki dersin geniş
biçiminde boş hücrelerin sayılabilir hale gelmesi gibi.

Son satır ölçeğin öteki ucunu gösteriyor: aynı kırılımda **bir göz tek bir okumadan** oluşuyor. O
gözün "ortalaması" bir özet değil, tek bir okumanın kendisidir ve yanında bir grup varmış gibi
raporlanır. Gruplama inceldikçe göz başına düşen satır 639,8'den 213,3'e, oradan 60,4'e ve en
uçta 1'e iner; sayı özet olmaktan çıkıp gözlemin kendisi olur.

#### Özetin Sildiği Değişkenlik

```js
// degisken.mjs — bes satirlik ozetin tasidigi ve tasimadigi bilgi. Ayni
// kumeden uc genel ortalama ve ozetten cevaplanamayan uc soru.
import { UZUN, grupla, ort, gen } from "./grupla.mjs";

const sapma = (a) => { const m = ort(a);
  return Math.sqrt(ort(a.map((x) => (x - m) ** 2))); };
const gB = grupla(UZUN, (o) => o.bolge);
const y = gen([10, 9, 11, 11, 9, 9, 11]);
console.log(y(["bolge", "okuma", "ortalama", "std sapma", "en kucuk", "en buyuk",
  "acikligi"]));
const ozet = [...gB].map(([k, l]) => {
  const v = l.map((o) => o.tuketim_m3);
  return { k, n: l.length, m: ort(v), s: sapma(v), a: Math.min(...v), b: Math.max(...v) };
}).sort((x, z) => z.m - x.m);
for (const s of ozet) console.log(y([s.k, s.n, s.m.toFixed(2), s.s.toFixed(2),
  s.a.toFixed(2), s.b.toFixed(2), (s.b - s.a).toFixed(2)]));

const tum = UZUN.map((o) => o.tuketim_m3);
const gH = grupla(UZUN, (o) => `${o.bolge}|${o.basamak}|${o.donem}`);
console.log(`\nayni kumeden uc genel ortalama:`);
console.log(`  ${UZUN.length} ham satirdan   ${ort(tum).toFixed(3)} m3`);
console.log(`  ${gB.size} bolge satirindan     ${ort(ozet.map((s) => s.m)).toFixed(3)} m3`);
console.log(`  ${gH.size} hucre satirindan   ` +
  `${ort([...gH.values()].map((l) => ort(l.map((o) => o.tuketim_m3)))).toFixed(3)} m3`);
console.log(`  fark ${(ort([...gH.values()].map((l) => ort(l.map((o) => o.tuketim_m3))))
  - ort(tum)).toFixed(3)} m3; her gruba esit agirlik verilince kucuk gruplar buyur`);

// Ozetten cevaplanamayan sorular: cevabi ham satirlardan ve bes satirlik
// ozetten ayri ayri okuyup yan yana koyalim.
const ESIK_M3 = 40, PAY = 0.5;
const ustu = tum.filter((v) => v > ESIK_M3).length;
const ozetUstu = ozet.filter((s) => s.m > ESIK_M3).length;
let uzak = 0;
for (const [, l] of gB) { const m = ort(l.map((o) => o.tuketim_m3));
  for (const o of l) if (Math.abs(o.tuketim_m3 - m) > PAY * m) uzak++; }
const enUst = ozet[0], enAlt = ozet[ozet.length - 1];
const asagi = gB.get(enUst.k).filter((o) => o.tuketim_m3 < enAlt.m).length;
const yukari = gB.get(enAlt.k).filter((o) => o.tuketim_m3 > enUst.m).length;

const y2 = gen([44, 12, 12]);
console.log("\n" + y2(["soru", "ham satir", "bes satir"]));
console.log(y2([`kac okuma ${ESIK_M3} m3 ustunde`, ustu, ozetUstu]));
console.log(y2(["kac okuma kendi grup ortalamasindan uzak", uzak, "okunamaz"]));
console.log(y2([`${enUst.k} ile ${enAlt.k} kac okumada yer degistiriyor`,
  asagi + yukari, "okunamaz"]));
console.log(`\nozet ${enUst.k} ${enUst.m.toFixed(2)} > ${enAlt.k} ` +
  `${enAlt.m.toFixed(2)} diyor; ${enUst.k} bolgesinin ${asagi} okumasi ${enAlt.k} ` +
  `ortalamasinin altinda, ${enAlt.k} bolgesinin ${yukari} okumasi ${enUst.k} ortalamasinin ustunde`);
```

```
bolge         okuma   ortalama  std sapma en kucuk en buyuk   acikligi
dogu            557      24.67       9.19     0.00    51.90      51.90
merkez          600      22.40       9.02     0.00    52.47      52.47
kuzey           921      20.21       7.46     0.00    45.47      45.47
guney           645      16.60       6.08     0.00    37.56      37.56
bati            476      13.78       5.00     0.00    31.95      31.95

ayni kumeden uc genel ortalama:
  3199 ham satirdan   19.713 m3
  5 bolge satirindan     19.532 m3
  53 hucre satirindan   20.721 m3
  fark 1.008 m3; her gruba esit agirlik verilince kucuk gruplar buyur

soru                                           ham satir   bes satir
kac okuma 40 m3 ustunde                               58           0
kac okuma kendi grup ortalamasindan uzak             431    okunamaz
dogu ile bati kac okumada yer degistiriyor            50    okunamaz

ozet dogu 24.67 > bati 13.78 diyor; dogu bolgesinin 39 okumasi bati ortalamasinin altinda, bati bolgesinin 11 okumasi dogu ortalamasinin ustunde
```

İlk tablo beş satırın taşıdığından fazlasını gösteriyor, çünkü yanına standart sapma ve uç değerler
eklendi. Bir özet satırı yalnız ortalamayı taşısaydı, doğu bölgesinin 24,67 m³'ü ile batı bölgesinin
13,78 m³'ü arasındaki 10,89 m³'lük fark, grup içindeki 51,90 ve 31,95 m³'lük açıklıkların yanında
küçük kalır ve bu karşılaştırma hiç yapılamazdı.

Ortadaki blok aynı kümeden üç genel ortalama çıkarıyor. **3.199 ham satırdan** 19,713 m³; **beş bölge
satırından** 19,532 m³; **53 hücre satırından** 20,721 m³. Üçü de doğru hesaplanmıştır ve aralarındaki
fark bir ağırlık kararıdır: toplulaştırılmış satırlar yeniden toplulaştırıldığında 476 okumalı batı
bölgesi ile 921 okumalı kuzey bölgesi eşit sayılır, aynı şekilde 244 okumalı bir hücre ile bir
okumalı bir hücre eşit sayılır. Hücre düzeyinde yeniden toplulaştırmanın ham satırdan sapması
1,008 m³'tür, yani genel ortalamanın yüzde beşi. Bu, önceki kursta bir sayfa üzerinde ölçülmüş olan
aynı tuzağın betik tarafındaki karşılığıdır ve burada satır sayılarıyla birlikte okunur: sapmanın
kaynağı 53 ile 3.199 arasındaki farktır.

Son tablo kaybı doğrudan sayıyor. **58 okuma 40 m³'ün üstündedir** ve bu eşik tarife basamaklarının
en üstü olduğu için seçilmiştir; beş satırlık özete aynı soru sorulduğunda cevap **sıfır** çıkar,
çünkü hiçbir bölge ortalaması 40 m³'ü aşmaz. Cevap yanlış değildir, sorulan soru artık başka bir
sorudur ve bunu okuyan taraf göremez.

**431 okuma** kendi grup ortalamasından yüzde elliden fazla sapar; oran eşiği bölge ortalamaları
13,78 ile 24,67 m³ arasında değiştiği için mutlak bir metre küp eşiğine tercih edilmiştir. Beş
satırlık özet bu sayıyı hiç taşımaz. Aynı şekilde **50 okuma** iki uç bölge arasında yer değiştirir:
doğu bölgesinin 39 okuması batı ortalamasının altında, batı bölgesinin 11 okuması doğu ortalamasının
üstündedir. Özet "doğu batıdan yüksek" der ve bu doğrudur; taşımadığı şey, tek tek okumalara
bakıldığında bu sıralamanın 50 kez tersine döndüğüdür.

Dersin satır hesabı böylece kapanır: 3.199 satır girdi, 5 satır çıktı, 3.194 satır özetin içinde
eridi. Erime bir kayıp değildir — amacı budur — ama eriyenin ne olduğu yazılmazsa özet, arkasındaki
kümenin yerine geçer.

#### Özet

- Çıkan satır sayısını hesap değil gruplama alanı belirler: aynı 3.199 satır sırasıyla 5, 15, 53 ve
  1.260 satıra iner.
- Çapraz kırılımın 60 gözünden 7'si hiç satır taşımaz ve çıktıda hiç görünmez; okuyan taraf "sıfır"
  ile "veri yok" arasını ayıramaz. Bir göz tek bir okumadan oluşur ve özet olmaktan çıkar.
- Toplulaştırılmış satırların yeniden toplulaştırılması her gruba eşit ağırlık verir: aynı kümeden
  19,713, 19,532 ve 20,721 m³ olmak üzere üç genel ortalama çıkar.
- Bir özet satırı grubun merkezini taşır, yayılımını taşımaz: 3.199 okumanın 431'i kendi grup
  ortalamasından yüzde elliden fazla sapar ve bu sayı beş satırdan okunamaz.
- Beş satırlık özet "40 m³'ün üstünde kaç okuma var" sorusuna sıfır der, ham satırlarda cevap 58'dir;
  doğu ile batı bölgeleri 50 okumada yer değiştirir.

#### Sonraki Adım

Toplulaştırma satır sayısını düşürdü ama sütun kümesine dokunmadı: elde hâlâ ölçülmüş alanlar var,
hesaplanmış alanlar yok. Çözümlemenin sorduğu sorular çoğunlukla ölçülen değeri değil, ondan
türetilen bir büyüklüğü ister — kişi başına tüketim, günlük ortalama, dönemler arası değişim oranı.
Türetme yeni satır üretmez, bu yüzden zararsız görünür. Zararı başka yerde çıkar: bölen sıfır
olduğunda ya da girdilerden biri eksik olduğunda türetilmiş değer **tanımsız** kalır ve o satır
sonraki adımda sessizce elenir. Sonraki ders türetmenin kaç satırda tanımsız kaldığını sayar ve bu
satırların düşmesinin sonucu ne kadar oynattığını ölçer.

### Türetilmiş Değişkenler

Source: https://academia.sh/tr/kurslar/veri-hazirlama/turetilmis-degiskenler

3.199 satırlık kümeye iki türetilmiş değişken eklenir ve ikisi de satır sayısını değiştirmez; çıkan yine 3.199 satırdır. Kişi başı tüketim 328 satırda tanımsız kalır — 216 satırda hane sıfır, 112 satırda hane bilgisi hiç yok — ve iki sebep aynı sonuçta birleştiği için çıktıdan ayırt edilemez. Aynı soruya dört savunulabilir yol dört cevap verir: sayı olmayan değer, 9,972, 8,604 ve 7,735 m³. Tanımsız satırları süzen yol 6.372,0 m³'ü, yani toplam tüketimin yüzde 10,1'ini hesabın dışında bırakır ve süzgeç bölgelere eşit inmez: pay yüzde 5,4 ile yüzde 21,2 arasında değişir. Dönem değişim oranı uzun biçimde 1.334 satırda tanımsızdır, beklenen göz kümesinde ise 581 göz hiç sayılamaz.

Toplulaştırma satır sayısını düşürdü ama sütun kümesine dokunmadı; elde yalnız ölçülmüş alanlar
var. Çözümlemenin soruları ise çoğunlukla ölçülen değeri değil ondan **türetilen** bir büyüklüğü
ister: "hangi bölge daha çok tüketiyor" sorusunun karşılığı ham metre küptür, "hangi bölgede kişi
başına daha çok tüketiliyor" sorusunun karşılığı kümede bulunmaz.

Türetme, önceki üç adımın aksine satır sayısını hiç değiştirmez ve bu yüzden zararsız görünür.
Dersin ölçüsü oradadır: **satır sayısı sabit kalırken kaç satırda türetilmiş değerin tanımsız
kaldığı**. Tanımsız bir değer satırı düşürmez, yalnız onu sonraki adımda düşürülmeye hazır hale
getirir; kayıp türetmede değil, ondan sonra yazılan ilk süzgeçte olur.

- **VD16. Türetilmiş değişken** (derived variable), var olan alanlardan hesaplanan yeni bir alandır:
  satır sayısını değiştirmez, sütun ekler. Küme önceki dersten devralınır, **kurgudur**.
- **VD17.** Bir türetme, girdilerinden biri eksik ya da sıfır olduğunda o satırda **tanımsız** kalır.
  Üç sebep ayrı sayılır: sıfıra bölme, eksik girdi ve **tanım gereği** tanımsızlık.
- **VD18.** Eksik girdi aritmetikte sessizce sıfıra dönüşür; eksik girdi ile gerçek sıfır **aynı
  sonucu** üretir ve sebep yalnız girdiden okunur.
- **VD19.** Tanımsız değer taşıyan satır düşmez, sonraki adımda süzülür; süzgeç türetmeden ayrı bir
  karardır ve raporunda görünmez.
- **VD20.** Süzgeç rastgele elemez: tanımsızlığın sebebi bir gruba yığılıyorsa o grup orantısız
  elenir ve gruplar arası karşılaştırma bozulur.

#### Türetme Kaç Satırda Tanımsız Kalıyor

```js
// turet.mjs — MODELDIR: onceki dersin 3.199 satirlik uzun bicimli kumesi ve iki
// turetilmis degisken. Veri kumesi kurgudur; hicbir arayuz taklit edilmez.
export const TOHUM = 20260218, HAM = 1400, DONEM = ["2026-01", "2026-02", "2026-03"];
const karistir = (x) => { x = Math.imul(x ^ (x >>> 16), 2246822507);
  x = Math.imul(x ^ (x >>> 13), 3266489909); return (x ^ (x >>> 16)) >>> 0; };
const uretec = (t) => { let s = karistir(t);
  return () => (s = (s * 1664525 + 1013904223) >>> 0) / 4294967296; };
const ayrik = (u, w) => { let t = 0;
  for (let i = 0; i < w.length; i++) { t += w[i]; if (u < t) return i; } return w.length - 1; };
const BOLGE = [["kuzey", 0.28, 21], ["guney", 0.22, 17], ["dogu", 0.18, 26],
  ["bati", 0.14, 14], ["merkez", 0.18, 23]];
// Hane buyuklugu bolgeye gore degisir; sifir kisi "hane yok" demektir (ticari
// abonelik) ve merkezde payi yuksektir. Cekim sayisi degismedigi icin onceki
// derslerin satir sayilari ve tuketim degerleri aynen korunur.
const HANE = { kuzey: [0.05, 0.22, 0.30, 0.25, 0.13, 0.05],
  guney: [0.04, 0.18, 0.26, 0.28, 0.16, 0.08], dogu: [0.02, 0.05, 0.12, 0.26, 0.33, 0.22],
  bati: [0.05, 0.42, 0.32, 0.14, 0.05, 0.02], merkez: [0.19, 0.26, 0.28, 0.17, 0.07, 0.03] };
const ABONE = [], ELENEN = [];
for (let i = 0; i < HAM; i++) {
  const r = uretec(TOHUM + i), b = BOLGE[ayrik(r(), BOLGE.map((x) => x[1]))];
  const k = { abone_no: 10001 + i, bolge: b[0], taban: b[2], hane_kisi: ayrik(r(), HANE[b[0]]),
    memnuniyet: ayrik(r(), [0.08, 0.14, 0.27, 0.34, 0.17]) + 1 };
  (r() < 0.046 ? ELENEN : ABONE).push(k);
  if (r() < 0.031) k.hane_kisi = null; // hane bilgisi hic girilmemis kayit
}
// Onceki derslerin cikardigi kume: eslesmeyen okumalar disarida, ayni abone+donem
// cifti bir satir. Elenen abonelerin ve tekillestirilen ikinci kaydin cekimleri
// korunuyor ki tuketim degerleri onceki derslerle ayni kalsin.
const anahtar = new Set(ABONE.map((a) => a.abone_no));
const HANE_KISI = new Map(ABONE.map((a) => [a.abone_no, a.hane_kisi]));
export const UZUN = [];
for (const k of [...ABONE, ...ELENEN]) {
  const r = uretec(TOHUM + 7000 + k.abone_no), adet = ayrik(r(), [0.05, 0.12, 0.21, 0.62]);
  for (let d = 0; d < adet; d++) {
    const t = r() < 0.038 ? 0 : Math.round(k.taban * Math.exp((r() + r() + r() - 1.5) * 0.62)
      * (1 - d * 0.05) * 100) / 100;
    if (!anahtar.has(k.abone_no)) { r(); continue; }
    UZUN.push({ abone_no: k.abone_no, bolge: k.bolge, donem: DONEM[d], tuketim_m3: t,
      hane_kisi: HANE_KISI.get(k.abone_no) });
    r();
  }
}

const deger = new Map(UZUN.map((o) => [`${o.abone_no}|${o.donem}`, o.tuketim_m3]));
const onceki = (o) => { const j = DONEM.indexOf(o.donem);
  return j === 0 ? undefined : deger.get(`${o.abone_no}|${DONEM[j - 1]}`); };
// Iki turetme, her biri bir deger ve bir tanimsizlik sebebi verir. Sebep ciktidan
// degil girdiden okunur; ayni tanimsiz deger farkli sebeplerden gelebilir.
export const TURETME = [
  ["T1 kisi basi tuketim", (o) => o.tuketim_m3 / o.hane_kisi,
    (o) => o.hane_kisi === null ? "eksik" : o.hane_kisi === 0 ? "sifir" : "-"],
  ["T2 donem degisim orani", (o) => (o.tuketim_m3 - onceki(o)) / onceki(o),
    (o) => DONEM.indexOf(o.donem) === 0 ? "tanim" : onceki(o) === undefined ? "eksik"
      : onceki(o) === 0 ? "sifir" : "-"],
];
export const ort = (a) => a.reduce((x, y) => x + y, 0) / a.length;
export const gen = (w) => (h) => h.map((v, j) => (j ? String(v).padStart(w[j])
  : String(v).padEnd(w[0]))).join("");

if (import.meta.url === `file://${process.argv[1]}`) {
  const say = (f) => UZUN.filter(f).length, T = UZUN.reduce((a, o) => a + o.tuketim_m3, 0);
  console.log(`model (tohum ${TOHUM}): giren ${UZUN.length} satir, ${T.toFixed(1)} m3, ` +
    `ortalama ${(T / UZUN.length).toFixed(3)} m3 — onceki ders ile ayni`);
  const y = gen([25, 8, 8, 11, 13, 13, 14]);
  console.log("\n" + y(["turetme", "giren", "cikan", "tanimsiz", "sifira bolme",
    "eksik girdi", "tanim geregi"]));
  for (const [ad, f, neden] of TURETME) {
    const n = UZUN.map(neden);
    console.log(y([ad, UZUN.length, UZUN.length, say((o) => !Number.isFinite(f(o))),
      n.filter((x) => x === "sifir").length, n.filter((x) => x === "eksik").length,
      n.filter((x) => x === "tanim").length]));
  }
  const t1 = UZUN.map(TURETME[0][1]);
  console.log(`\niki turetme de cikan satiri degistirmedi ve hata uretmedi; T1'in eksik girdisi ` +
    `ile sifir hanesi ayni sonucta birlesti: ${t1.filter((x) => x === Infinity).length} sonsuz, ` +
    `${t1.filter((x) => Number.isNaN(x)).length} sayi olmayan deger`);
  // T2'nin "eksik girdi" sutunu sifir cikti; beklenen goz kumesi kurulunca degisiyor.
  const goz = new Set(UZUN.map((o) => o.abone_no)).size * (DONEM.length - 1);
  const tanimli = say((o) => Number.isFinite(TURETME[1][1](o)));
  const bol = say((o) => TURETME[1][2](o) === "sifir");
  console.log(`\nT2 beklenen goz kumesinde: ${goz} goz, ${tanimli} tanimli, ${bol} sifira ` +
    `bolme, ${goz - tanimli - bol} goz hic okunmamis donem yuzunden — uzun bicimde o gozler ` +
    `hic satir olmadigi icin tanimsiz da sayilmadi`);
}
```

```
model (tohum 20260218): giren 3199 satir, 63060.6 m3, ortalama 19.713 m3 — onceki ders ile ayni

turetme                     giren   cikan   tanimsiz sifira bolme  eksik girdi  tanim geregi
T1 kisi basi tuketim         3199    3199        328          216          112             0
T2 donem degisim orani       3199    3199       1334           74            0          1260

iki turetme de cikan satiri degistirmedi ve hata uretmedi; T1'in eksik girdisi ile sifir hanesi ayni sonucta birlesti: 315 sonsuz, 13 sayi olmayan deger

T2 beklenen goz kumesinde: 2520 goz, 1865 tanimli, 74 sifira bolme, 581 goz hic okunmamis donem yuzunden — uzun bicimde o gozler hic satir olmadigi icin tanimsiz da sayilmadi
```

Giren ve çıkan sütunları iki satırda da aynı: 3.199. Birleştirme satır düşürüp çoğaltmış,
şekillendirme satırı hücreye çevirmiş, toplulaştırma 3.199 satırı beşe indirmişti; türetme
hiçbirini yapmaz. Yine de tanımsız sütununda iki ayrı sayı var.

`T1` satırında 3.199 satırın **328'inde** kişi başı tüketim tanımsızdır ve sebep iki kalemdir.
**216 satırda** hane kişi sayısı sıfırdır; bu bir veri hatası değildir, çünkü hane olmayan bir
abonelik için kişi başı tüketim diye bir büyüklük yoktur. **112 satırda** hane bilgisi hiç
girilmemiştir; bu bir eksikliktir ve doldurulabilir. İki kalem farklı kararlar gerektirir ama
çıktıda **aynı görünür**: eksik girdi sessizce sıfıra çevrilir, sıfıra bölme sonsuz verir ve ikisi
de aynı 315 sonsuz değerin içinde erir; kalan 13 tanımsız, tüketimi de sıfır olan satırlardan gelir.
Bir tanımsız değerin **türü sebebini vermez**, bu yüzden tanımsızlık sayısı sebebiyle birlikte
raporlanır.

`T2` başka bir kalemi gösteriyor. Tanımsız sayısı **1.334** ile daha büyüktür ama bunun **1.260'ı
tanım gereğidir**: ilk dönemin bir öncesi yoktur, dolayısıyla değişim oranı da yoktur. Bu bir kayıp
değil, tanımın sonucudur. Kalan **74 satır** bir sıfıra bölmedir.

Asıl uyarı `T2` satırının **eksik girdi sütunundaki sıfırdır**. O sıfır, veride boşluk olmadığını
değil, uzun biçimde sorulan sorunun boşluğu göremediğini söyler. Beklenen göz kümesinde 1.260 abone
için iki geçiş, toplam **2.520 göz** vardır ve yalnız 1.865'i tanımlıdır. **581 göz** hiç okunmamış
bir dönem yüzünden tanımsızdır; bu, önceki dersteki 581 boş hücredir ve uzun biçimde ortada satır
olmadığından tanımsız bile sayılamaz. **Var olmayan satırın türetilmiş değeri de var olmaz.**

#### Tanımsızın Bedelini Sonraki Adım Ödüyor

```js
// etki.mjs — tanimsiz satirlarin sonuca etkisi: ayni soru dort yolla cevaplanir ve
// suzgecin kac satiri hangi bolgeden sessizce dusurdugu bolge bolge sayilir.
import { UZUN, ort, gen } from "./turet.mjs";

const kisiBasi = (o) => o.tuketim_m3 / o.hane_kisi;
const sonlu = UZUN.filter((o) => Number.isFinite(kisiBasi(o)));
const T = (a, f) => a.reduce((x, o) => x + f(o), 0);
const oranT = (a, p) => T(a, (o) => o.tuketim_m3) / T(a, p);
const YOL = [
  ["Y1 satir oranlari ortalanir", UZUN, (a) => ort(a.map(kisiBasi))],
  ["Y2 sonlu satirlar suzulur", sonlu, (a) => ort(a.map(kisiBasi))],
  ["Y3 toplam m3 / toplam kisi", UZUN, (a) => oranT(a, (o) => o.hane_kisi ?? 0)],
  ["Y4 suzulmus toplam / toplam", sonlu, (a) => oranT(a, (o) => o.hane_kisi)],
];
const y = gen([37, 9, 10, 11, 11]);
console.log(`soru: bir kisi bir donemde ortalama kac m3 tuketiyor`);
console.log("\n" + y(["cevap yolu", "giren", "sayilan", "tanimsiz", "sonuc"]));
for (const [ad, a, f] of YOL) console.log(y([ad, UZUN.length, a.length,
  a.filter((o) => !Number.isFinite(kisiBasi(o))).length, f(a).toFixed(3)]));
console.log(`\nY3 tanimsiz ${UZUN.length - sonlu.length} satirin tuketimini paya ekledi, ` +
  `paydaya sifir kisi ekledi; hicbir uyari uretmedi`);

// Suzgec rastgele elemez: dusen satirlar bolgelere esit dagilmaz.
const satir = [...new Set(UZUN.map((o) => o.bolge))].map((b) => {
  const l = UZUN.filter((o) => o.bolge === b), s = l.filter((o) => Number.isFinite(kisiBasi(o)));
  return { bolge: b, n: l.length, dusen: l.length - s.length,
    ham: ort(l.map((o) => o.tuketim_m3)), kb: ort(s.map(kisiBasi)) };
}).sort((a, b) => b.ham - a.ham);
const yer = (f) => Object.fromEntries([...satir].sort((a, b) => f(b) - f(a))
  .map((s, j) => [s.bolge, j + 1]));
const sHam = yer((x) => x.ham), sKb = yer((x) => x.kb);
const y2 = gen([9, 8, 9, 9, 10, 10, 9, 9]);
console.log("\n" + y2(["bolge", "okuma", "dusen", "pay %", "ham ort.", "kisi basi",
  "sira ham", "sira kb"]));
for (const s of satir) console.log(y2([s.bolge, s.n, s.dusen, (100 * s.dusen / s.n).toFixed(1),
  s.ham.toFixed(2), s.kb.toFixed(2), sHam[s.bolge], sKb[s.bolge]]));

const m3 = T(UZUN.filter((o) => !Number.isFinite(kisiBasi(o))), (o) => o.tuketim_m3);
const pay = satir.map((s) => 100 * s.dusen / s.n);
console.log(`\ngiren ${UZUN.length} satir -> cikan ${UZUN.length} satir; suzgec ` +
  `${UZUN.length - sonlu.length} satiri ve ${m3.toFixed(1)} m3 dusurdu, yani toplamin yuzde ` +
  `${(100 * m3 / T(UZUN, (o) => o.tuketim_m3)).toFixed(1)}'ini; bolgeye gore pay ` +
  `${Math.min(...pay).toFixed(1)} ile ${Math.max(...pay).toFixed(1)} arasinda degisiyor`);
console.log(`${satir.filter((s) => sHam[s.bolge] !== sKb[s.bolge]).length} bolgenin sirasi ` +
  `degisti; kisi basi tuketimin birincisi ${satir.find((s) => sKb[s.bolge] === 1).bolge}`);
```

```
soru: bir kisi bir donemde ortalama kac m3 tuketiyor

cevap yolu                               giren   sayilan   tanimsiz      sonuc
Y1 satir oranlari ortalanir               3199      3199        328        NaN
Y2 sonlu satirlar suzulur                 3199      2871          0      9.972
Y3 toplam m3 / toplam kisi                3199      3199        328      8.604
Y4 suzulmus toplam / toplam               3199      2871          0      7.735

Y3 tanimsiz 328 satirin tuketimini paya ekledi, paydaya sifir kisi ekledi; hicbir uyari uretmedi

bolge       okuma    dusen    pay %  ham ort. kisi basi sira ham  sira kb
dogu          557       30      5.4     24.67      8.43        1        4
merkez        600      127     21.2     22.40     14.00        2        1
kuzey         921       52      5.6     20.21     10.67        3        2
guney         645       61      9.5     16.60      7.50        4        5
bati          476       58     12.2     13.78      9.37        5        3

giren 3199 satir -> cikan 3199 satir; suzgec 328 satiri ve 6372.0 m3 dusurdu, yani toplamin yuzde 10.1'ini; bolgeye gore pay 5.4 ile 21.2 arasinda degisiyor
5 bolgenin sirasi degisti; kisi basi tuketimin birincisi merkez
```

Dört yol da aynı soruyu soruyor ve dördü de savunulabilir. **Y1** hiçbir şey süzmez ve tek görünür
işareti üretir: sonuç bir sayı değildir. Tanımsızlığın rapora ulaştığı tek yol budur ama bir cevap
değildir, bu yüzden en kısa düzeltme eklenir.

**Y2** o düzeltmedir ve 9,972 m³ verir. Süzgeç bir satır bile düşürmüyormuş gibi yazılır, oysa
sayılan satır 3.199 değil **2.871'dir** ve düşen 328 satır **6.372,0 m³**, yani toplam tüketimin
yüzde 10,1'ini taşır. Rapor "kişi başına 9,972 m³" der; hangi 328 satırın sayılmadığını yazmaz.

**Y3** bu dersin sessiz kaybıdır. Toplam tüketimi toplam hane kişisine bölmek makul bir yoldur,
hiçbir tanımsız değer üretmez ve sonuç 8,604 m³ olarak temiz çıkar. Tanımsız 328 satır burada
süzülmemiştir: tüketimleri paya girmiş, hane kişileri paydaya **sıfır** olarak girmiştir. Farkı
gösteren tek şey **Y4'tür**: aynı hesap sonlu satırlarda 7,735 m³ verir ve aradaki 0,869 m³ tamamen
o sıfır paydalardan gelir.

Y2 ile Y4 arasındaki fark ise önceki derste kurulmuş bir ağırlık kararıdır: 9,972 m³ oranların
ortalaması, 7,735 m³ ortalamaların oranıdır.

Alt tablo süzgecin neden bir hesap kalemi olduğunu gösteriyor. Düşen 328 satır bölgelere eşit
dağılmıyor: `dogu` bölgesinin okumalarının yüzde 5,4'ü düşerken `merkez` bölgesinin yüzde 21,2'si
düşüyor. Sebep verinin kendisidir: hane olmayan abonelikler merkezde yoğunlaşmıştır.
Bölgeleri karşılaştıran biri, her bölgenin farklı bir payla temsil edildiğini
bilmez. Son satır ise türetmenin gücünü ve riskini birlikte veriyor: **beş bölgenin beşi
de sıra değiştirdi** ve ham tüketimde birinci olan `dogu` kişi başı tüketimde dördüncü olur. İki
sıralama da doğrudur; riskli olan, ikisinin de "tüketim" adını taşımasıdır.

Dersin satır hesabı böyle kapanır: 3.199 satır girdi, 3.199 satır çıktı, türetmede hiçbir satır
düşmedi. Buna karşılık 328 satır kişi başı tüketimde, 1.334 satır dönem değişim oranında tanımsız
bir değer taşıyor ve sonraki adımın süzgecinde sessizce düşecek. **Tanımsız sayısı yazılmadığında
bu düşüş görünmez**, çünkü türetmenin raporunda giren ile çıkan eşittir.

#### Özet

- Türetme satır sayısını değiştirmez: 3.199 satır girdi, 3.199 satır çıktı ve hata üretilmedi.
  Değişen şey, satırların taşıdığı tanımsız değer sayısıdır.
- Tanımsızlık sebebine göre ayrılır: kişi başı tüketimde 216 satır sıfıra bölme, 112 satır eksik
  girdi; dönem değişim oranındaki 1.334 satırın 1.260'ı tanım gereğidir.
- Eksik girdi aritmetikte sessizce sıfıra dönüşür ve 112 eksik ile 216 sıfır aynı 315 sonsuz değerde
  erir. Uzun biçim ise 581 gözü hiç göremez.
- Aynı soru dört savunulabilir yolla dört cevap verir: sayı olmayan değer, 9,972, 8,604 ve 7,735 m³.
  Süzen yol 6.372,0 m³'ü, yani toplamın yüzde 10,1'ini hesabın dışında bırakır.
- Süzgeç rastgele elemez: bölgelere göre pay yüzde 5,4 ile yüzde 21,2 arasında değişir ve türetilmiş
  değişken beş bölgenin de sırasını değiştirir.

#### Sonraki Adım

Türetme sayısal alanlardan sayısal alanlar üretti. Kümede sayısallaştırılmayı bekleyen alanlar daha
var ve orada sayı hiç yok: bölge bir addır, ziyaret sonucu bir etikettir, memnuniyet basamağı bir
nottur. Çözümlemenin çoğu aracı sayı ister, bu yüzden kategoriler kodlanır ve kodlama mekanik
görünür — her kategoriye bir tam sayı verilir ve iş biter. Bedeli, verilen sayının kendi getirdiği
anlamdır: sıralı bir kod, kategoriler arasında sıra ve uzaklık iddia eder ve bölgeye 1'den 5'e kod
verildiğinde bu iddia veriye hiç sorulmadan girer. Sonraki ders sıralı ve sırasız kodlamayı ayırır
ve sırasız bir kategoriyi sıralı kodlamanın ürettiği yanlış sonucu sayıyla gösterir.

### Kategorik Kodlama

Source: https://academia.sh/tr/kurslar/veri-hazirlama/kategorik-kodlama

3.199 satırlık kümenin üç kategorik alanı kodlanır: sıralı kodlama 3 sütun, sırasız kodlama 14 sütun ekler ve ikisi de tek bir satır düşürmez. Sırasız bir alan olan bölge sıralı kodlandığında kod, veride bulunmayan bir sıradan gelir: üç savunulabilir sıralama aynı eşiği aynı veriye uygular ve 2.166, 1.678 ve 1.633 satır seçer, ortalamalar 19,741 — 18,480 — 20,661 m³ olur. 3.199 satırın hiçbiri üç kararın üçünde birden seçilmez, 921 satır yalnız birinde seçilir. Sıralı bir alanda aynı deney tek bir cevap verir: iki farklı kodlama memnuniyet eşiğinde aynı 1.615 satırı ve aynı 19,584 m³'ü verir.

Türetme sayısal alanlardan sayısal alanlar üretti ve kümede sayı taşımayan alanlar dokunulmadan
kaldı. `bolge` bir addır, `memnuniyet` bir nottur, `tarife_basamagi` bir basamaktır. Bunlar üzerinde
hesap yapmanın yolu **kategorik kodlamadan** geçer: her kategoriye bir sayı karşılık getirilir.

İşlem mekanik görünür ve satır sayısına dokunmaz, ama verilen sayı yanında bir iddia taşır. Bir
kategoriye 3, bir başkasına 5 verildiğinde ikincisinin birinciden "büyük" ve aradaki mesafenin 2
olduğu söylenmiş olur. Veri Analitiğine Giriş kursundaki Ölçüm Ölçekleri dersi bu iddianın hangi
alanlarda meşru olduğunu ölçmüştü; bu ders ölçek tanımlarını tekrarlamaz, **iddianın hazırlama
adımında ne kadar satır oynattığını** sayar.

- **VD21. Kategorik kodlama**, bir kategoriyi sayıya çeviren karardır. Satır sayısını değiştirmez,
  sütun ekler. Küme önceki dersten devralınır ve **kurgudur**.
- **VD22. Sıralı kodlama** her kategoriye bir tam sayı verir ve tek sütun üretir. **Sırasız
  kodlama** her kategori için ayrı bir 0/1 sütunu açar, kategori sayısı kadar sütun üretir ve
  hiçbir sıra iddia etmez.
- **VD23.** Sırasız bir alanı sıralı kodlamak, veride bulunmayan bir sırayı veriye ekler. O sıra
  **keyfîdir**: aynı alan üzerinde savunulabilir birden çok sıralama vardır ve hangisinin
  seçildiği kodlanmış sütunda görünmez.
- **VD24.** Sıralı bir alanda artan her kodlama aynı sıra bilgisini taşır; kategoriyle tanımlanan
  bir eşiğin sonucu kodlamadan bağımsızdır. Sırasız bir alanda aynı eşiğin sonucunu tamamen
  kodlama belirler.

#### İki Kodlama, Aynı Satır Sayısı

```js
// kodla.mjs — MODELDIR: onceki derslerin 3.199 satirlik kumesi ve kategorik
// alanlarin kodlanmasi. Veri kumesi kurgudur; hicbir arayuz taklit edilmez.
export const TOHUM = 20260218, HAM = 1400, DONEM = ["2026-01", "2026-02", "2026-03"];
const TARIFE = [10, 25, 40];
const karistir = (x) => { x = Math.imul(x ^ (x >>> 16), 2246822507);
  x = Math.imul(x ^ (x >>> 13), 3266489909); return (x ^ (x >>> 16)) >>> 0; };
const uretec = (t) => { let s = karistir(t);
  return () => (s = (s * 1664525 + 1013904223) >>> 0) / 4294967296; };
const ayrik = (u, w) => { let t = 0;
  for (let i = 0; i < w.length; i++) { t += w[i]; if (u < t) return i; } return w.length - 1; };
const BOLGE = [["kuzey", 0.28, 21], ["guney", 0.22, 17], ["dogu", 0.18, 26],
  ["bati", 0.14, 14], ["merkez", 0.18, 23]];
const ABONE = [], ELENEN = [];
for (let i = 0; i < HAM; i++) {
  const r = uretec(TOHUM + i), b = BOLGE[ayrik(r(), BOLGE.map((x) => x[1]))];
  const k = { abone_no: 10001 + i, bolge: b[0], taban: b[2],
    hane_kisi: ayrik(r(), [0.06, 0.24, 0.30, 0.24, 0.11, 0.05]),
    memnuniyet: ayrik(r(), [0.08, 0.14, 0.27, 0.34, 0.17]) + 1 };
  (r() < 0.046 ? ELENEN : ABONE).push(k);
}
// Onceki derslerin cikardigi kume; elenen abonelerin ve tekillestirilen ikinci
// kaydin cekimleri korunuyor ki tuketim degerleri ayni kalsin.
const MEM = new Map(ABONE.map((a) => [a.abone_no, a.memnuniyet]));
const anahtar = new Set(ABONE.map((a) => a.abone_no));
export const UZUN = [];
for (const k of [...ABONE, ...ELENEN]) {
  const r = uretec(TOHUM + 7000 + k.abone_no), adet = ayrik(r(), [0.05, 0.12, 0.21, 0.62]);
  for (let d = 0; d < adet; d++) {
    const t = r() < 0.038 ? 0 : Math.round(k.taban * Math.exp((r() + r() + r() - 1.5) * 0.62)
      * (1 - d * 0.05) * 100) / 100;
    if (!anahtar.has(k.abone_no)) { r(); continue; }
    UZUN.push({ abone_no: k.abone_no, bolge: k.bolge, donem: DONEM[d], tuketim_m3: t,
      memnuniyet: MEM.get(k.abone_no),
      basamak: t < TARIFE[0] ? 1 : t < TARIFE[1] ? 2 : t < TARIFE[2] ? 3 : 4 });
    r();
  }
}

export const ALAN = [
  ["bolge", "sirasiz", (o) => o.bolge],
  ["memnuniyet", "sirali", (o) => o.memnuniyet],
  ["tarife_basamagi", "sirali", (o) => o.basamak],
];
// Sirali kodlama her kategoriye bir tam sayi verir: tek sutun. Sirasiz kodlama
// her kategori icin ayri bir 0/1 sutunu acar ve hicbir sira iddia etmez.
export const kod = (d) => Object.fromEntries(d.map((a, j) => [a, j + 1]));
export const gosterge = (o, f, d) => d.map((a) => (f(o) === a ? 1 : 0));
// Sirasiz bir alanda sira alanin kendisinden gelmez; bir siralama secilir.
const ADLAR = [...new Set(UZUN.map((o) => o.bolge))];
const sikl = (a) => UZUN.filter((o) => o.bolge === a).length;
export const SIRALAMA = [
  ["S1 alfabetik", [...ADLAR].sort()],
  ["S2 kumede ilk gorulme", [...ADLAR]],
  ["S3 satir sikligina gore azalan", [...ADLAR].sort((a, b) => sikl(b) - sikl(a))],
];
export const ort = (a) => a.reduce((x, y) => x + y, 0) / a.length;
export const gen = (w) => (h) => h.map((v, j) => (j ? String(v).padStart(w[j])
  : String(v).padEnd(w[0]))).join("");

if (import.meta.url === `file://${process.argv[1]}`) {
  const T = UZUN.reduce((a, o) => a + o.tuketim_m3, 0);
  console.log(`model (tohum ${TOHUM}): giren ${UZUN.length} satir, ${T.toFixed(1)} m3, ` +
    `${ALAN.length} kategorik alan — onceki ders ile ayni`);
  const y = gen([18, 10, 11, 12, 11, 8, 8]);
  console.log("\n" + y(["alan", "olcek", "kategori", "sirali kod", "gosterge", "giren",
    "cikan"]));
  let s1 = 0, s2 = 0;
  for (const [ad, olcek, f] of ALAN) {
    const d = [...new Set(UZUN.map(f))];
    s1 += 1; s2 += d.length;
    console.log(y([ad, olcek, d.length, 1, d.length, UZUN.length, UZUN.length]));
  }
  console.log(`\nkodlama satir uretmez: ${UZUN.length} -> ${UZUN.length}. Sirali kodlama ` +
    `${s1} sutun, sirasiz kodlama ${s2} sutun ekler; ikisi de hicbir satiri dusurmez`);
  const o1 = UZUN[0], d1 = [...new Set(UZUN.map((o) => o.bolge))];
  console.log(`ornek satir ${o1.abone_no} ${o1.bolge}: gosterge sutunlari ` +
    `[${gosterge(o1, (o) => o.bolge, d1).join(" ")}] (sutun sirasi ${d1.join(" ")})`);

  console.log(`\nbolge icin uc savunulabilir siralama ve verdikleri kodlar:`);
  const y2 = gen([32, 44]);
  for (const [ad, d] of SIRALAMA) {
    const k = kod(d);
    console.log(y2([ad, d.map((a) => `${a}=${k[a]}`).join(" ")]));
  }
  console.log(`ucu de veriden okunmadi; bolge adlari arasinda bir sira yoktur ve ` +
    `hangi siralamanin secildigi kodlanmis sutunda hic gorunmez`);
}
```

```
model (tohum 20260218): giren 3199 satir, 63060.6 m3, 3 kategorik alan — onceki ders ile ayni

alan                   olcek   kategori  sirali kod   gosterge   giren   cikan
bolge                sirasiz          5           1          5    3199    3199
memnuniyet            sirali          5           1          5    3199    3199
tarife_basamagi       sirali          4           1          4    3199    3199

kodlama satir uretmez: 3199 -> 3199. Sirali kodlama 3 sutun, sirasiz kodlama 14 sutun ekler; ikisi de hicbir satiri dusurmez
ornek satir 10001 merkez: gosterge sutunlari [1 0 0 0 0] (sutun sirasi merkez kuzey guney bati dogu)

bolge icin uc savunulabilir siralama ve verdikleri kodlar:
S1 alfabetik                          bati=1 dogu=2 guney=3 kuzey=4 merkez=5
S2 kumede ilk gorulme                 merkez=1 kuzey=2 guney=3 bati=4 dogu=5
S3 satir sikligina gore azalan        kuzey=1 guney=2 merkez=3 dogu=4 bati=5
ucu de veriden okunmadi; bolge adlari arasinda bir sira yoktur ve hangi siralamanin secildigi kodlanmis sutunda hic gorunmez
```

Kodlama, bu kursta satır sayısını değiştirmeyen ikinci adımdır: giren ve çıkan sütunları üç alanda
da 3.199'dur. Değişen sütun sayısıdır. Sıralı kodlama alan başına tek sütun üretir ve üç alan için
**3 sütun** eder; sırasız kodlama her kategori için bir sütun açar ve aynı üç alan için **14 sütun**
eder. Sırasız kodlamanın bedeli budur ve kategori sayısıyla birlikte büyür; kazancı, hiçbir
kategoriyi bir başkasının önüne ya da arkasına koymamasıdır.

Örnek satır bunu somutlaştırıyor: `merkez` bölgesindeki bir satır beş göstergeden yalnız birini 1
yapar, kalan dördü 0'dır. Bu beş sayıda hiçbir bölge bir diğerinden büyük değildir. Sıralı kodlamada
ise aynı bilgi tek bir sayıya iner ve o sayı bir yere yerleşir.

Alt blok dersin çekirdek sorusunu açıyor. `bolge` alanının kodu **nereden gelecek**. Alanın
kendisinde bir sıra yoktur, bu yüzden bir sıralama **seçilir** ve üç seçim de savunulabilir:
kategori adlarını alfabetik dizmek, kümede ilk görüldükleri sırayı korumak, satır sıklığına göre
azalan dizmek. Üçü üç ayrı kod verir. `bati` alfabetik sırada 1, sıklık sırasında 5'tir. Kodlanmış
sütuna bakan biri yalnız bir sayı görür; hangi sıralamanın seçildiği o sütunda hiçbir iz bırakmaz.

`memnuniyet` ve `tarife_basamagi` alanlarında böyle bir seçim yoktur. Basamaklar arasındaki sıra
alanın kendisinden gelir ve kodlayan kişi onu değiştiremez, yalnız hangi sayılarla temsil edileceğini
seçebilir. Fark buradadır ve bir sonraki bölüm bunu satır sayısıyla ölçer.

#### Aynı Eşik, Üç Farklı Satır Kümesi

```js
// esik.mjs — ayni esik uc siralamada uc ayri satir kumesi secer. Sirali bir
// alanda ayni deney tek bir kume verir; fark kodlamadan degil olcekten gelir.
import { UZUN, SIRALAMA, kod, ort, gen } from "./kodla.mjs";

const ESIK = 3; // bes kategorinin ust yarisi; kod 3, 4 ve 5 secilir
const y = gen([32, 25, 9, 13]);
console.log(`esik: "bolge kodu ${ESIK} ve ustunde olan satirlar"`);
console.log("\n" + y(["siralama", "secilen bolgeler", "satir", "ortalama m3"]));
const kume = [];
for (const [ad, d] of SIRALAMA) {
  const k = kod(d), sec = UZUN.filter((o) => k[o.bolge] >= ESIK);
  kume.push(new Set(sec.map((o) => `${o.abone_no}|${o.donem}`)));
  console.log(y([ad, d.filter((a) => k[a] >= ESIK).join(" "), sec.length,
    ort(sec.map((o) => o.tuketim_m3)).toFixed(3)]));
}
const kacinda = UZUN.map((o) => kume.filter((s) => s.has(`${o.abone_no}|${o.donem}`)).length);
const say = (n) => kacinda.filter((x) => x === n).length;
console.log(`\n${UZUN.length} satirin ${say(3)} tanesi uc kararin ucunde de secildi, ` +
  `${say(2)} tanesi ikisinde, ${say(1)} tanesi yalnizca birinde, ${say(0)} tanesi hicbirinde`);
const n = kume.map((s) => s.size);
const m = kume.map((s) => ort(UZUN.filter((o) => s.has(`${o.abone_no}|${o.donem}`))
  .map((o) => o.tuketim_m3)));
console.log(`ayni veri, ayni esik, ayni hesap: ${Math.min(...n)} ile ${Math.max(...n)} satir ` +
  `arasi ve ${Math.min(...m).toFixed(3)} ile ${Math.max(...m).toFixed(3)} m3 arasi`);

// Sirali alanda esik kategoriyle tanimlanir: "iyi ve ustu". Artan her kodlama
// ayni kategoriyi ayni tarafa koyar, cunku sira alanin kendisindedir.
const NOT = ["cok kotu", "kotu", "orta", "iyi", "cok iyi"];
const KOD = [["K1 esit araliklar", [1, 2, 3, 4, 5]], ["K2 genisleyen araliklar", [2, 5, 9, 14, 20]]];
const y2 = gen([26, 22, 12, 9, 13]);
console.log(`\nesik: "memnuniyet ${NOT[3]} ve ustunde olan satirlar"`);
console.log("\n" + y2(["kodlama", "kodlar", "esik kodu", "satir", "ortalama m3"]));
for (const [ad, K] of KOD) {
  const e = K[NOT.indexOf("iyi")], sec = UZUN.filter((o) => K[o.memnuniyet - 1] >= e);
  console.log(y2([ad, K.join(" "), e, sec.length, ort(sec.map((o) => o.tuketim_m3)).toFixed(3)]));
}
console.log(`iki kodlama da ayni satirlari secti: sira kodda degil alanda oldugu icin ` +
  `esik kodlamadan bagimsizdir`);
```

```
esik: "bolge kodu 3 ve ustunde olan satirlar"

siralama                                 secilen bolgeler    satir  ortalama m3
S1 alfabetik                           guney kuzey merkez     2166       19.741
S2 kumede ilk gorulme                     guney bati dogu     1678       18.480
S3 satir sikligina gore azalan           merkez dogu bati     1633       20.661

3199 satirin 0 tanesi uc kararin ucunde de secildi, 2278 tanesi ikisinde, 921 tanesi yalnizca birinde, 0 tanesi hicbirinde
ayni veri, ayni esik, ayni hesap: 1633 ile 2166 satir arasi ve 18.480 ile 20.661 m3 arasi

esik: "memnuniyet iyi ve ustunde olan satirlar"

kodlama                                   kodlar   esik kodu    satir  ortalama m3
K1 esit araliklar                      1 2 3 4 5           4     1615       19.584
K2 genisleyen araliklar              2 5 9 14 20          14     1615       19.584
iki kodlama da ayni satirlari secti: sira kodda degil alanda oldugu icin esik kodlamadan bagimsizdir
```

Üst tablo dersin bulgusudur. Eşik tek bir cümledir ve makul görünür: "bölge kodu 3 ve üstünde olan
satırlar", yani beş kategorinin üst yarısı. Aynı cümle, aynı veri üzerinde, üç savunulabilir
sıralamayla üç ayrı bölge kümesi seçer. Seçilen satır sayısı **2.166, 1.678 ve 1.633'tür**;
ortalama tüketim 18,480 m³ ile 20,661 m³ arasında gider gelir. Üç sayı da doğru hesaplanmıştır ve
hiçbiri bir hata üretmez.

Alt satır işin ölçüsünü veriyor: 3.199 satırın **hiçbiri** üç kararın üçünde birden seçilmez.
2.278 satır iki kararda, **921 satır yalnız bir kararda** seçilir. Yani "kodu 3 ve üstü olan
bölgeler" ifadesinin sabit bir karşılığı yoktur; her sıralama başka bir üçlü seçer ve üç seçim
birbirini kesmez. Bu, sırasız bir alanı sıralı kodlamanın ürettiği yanlış sonucun tam tanımıdır:
sonuç yanlış hesaplanmış bir sayı değil, **veriye ait olmayan bir sayıdır**. Sayının kaynağı bölge
adlarının nasıl dizildiğidir.

İkinci tablo aynı deneyi sıralı bir alanda tekrarlıyor ve fark açık. Eşik yine bir cümledir:
"memnuniyet iyi ve üstünde olan satırlar". İki kodlama arasındaki mesafeler tamamen farklıdır —
biri basamakları eşit aralıklı sayar, öteki üst basamaklar arasını açar — ama ikisi de aynı
**1.615 satırı** seçer ve aynı 19,584 m³'ü verir. Sebep basittir: sıra alanın kendisindedir, kodun
görevi onu bozmadan taşımaktır ve artan her kodlama bunu yapar.

İki tablo yan yana konduğunda karar kuralı çıkar. Bir alan sıralıysa sıralı kodlama meşrudur ve
kod seçimi kategoriye dayalı eşikleri oynatmaz. Bir alan sırasızsa sıralı kodlama, **kararı koddan
okunamaz hale getirir**: eşik veriye değil, kodlama sırasına uygulanmış olur. Sırasız kodlama bu
tuzağı kapatır, çünkü gösterge sütunlarında "3 ve üstü" diye bir şey yoktur; hangi kategorilerin
seçildiği **adlarıyla** yazılmak zorundadır ve o anda seçim görünür hale gelir.

Dersin satır hesabı böyle kapanır: 3.199 satır girdi, 3.199 satır çıktı, kodlama tek bir satır
düşürmedi. Buna karşılık kodlamadan sonra gelen tek bir eşik, aynı kümeden 1.633 ile 2.166 arasında
değişen satır kümeleri seçti ve 921 satırın sayılıp sayılmaması yalnız bir sıralama kararına bağlı
kaldı. **Kodlamanın kendisi kayıpsızdır; kayıp, kodun taşıdığı sahte sıranın kullanıldığı ilk
adımda ortaya çıkar.**

#### Özet

- Kodlama satır sayısını değiştirmez: 3.199 satır girdi, 3.199 satır çıktı. Sıralı kodlama üç alan
  için 3 sütun, sırasız kodlama aynı üç alan için 14 sütun ekler.
- Sırasız bir alanda kod veriden gelmez, seçilen bir sıralamadan gelir: alfabetik, kümede ilk
  görülme ve sıklık sıralamaları `bati` bölgesine 1 ile 5 arasında farklı kodlar verir.
- Aynı eşik üç sıralamada 2.166, 1.678 ve 1.633 satır seçer; ortalama 18,480 ile 20,661 m³ arasında
  değişir ve hiçbir hesap hata üretmez.
- 3.199 satırın hiçbiri üç kararın üçünde birden seçilmez; 921 satırın sayılması yalnız bir sıralama
  kararına bağlıdır.
- Sıralı bir alanda kod seçimi eşiği oynatmaz: iki farklı kodlama aynı 1.615 satırı ve aynı
  19,584 m³'ü verir, çünkü sıra kodda değil alanın kendisindedir.

#### Sonraki Adım

Bu konu kümeyi soruya göre yeniden biçimlendirdi: tablolar birleştirildi, şekil değiştirildi,
gruplar özetlendi, yeni alanlar türetildi ve kategoriler kodlandı. Her adımda giren, çıkan ve
kaybolan satır sayıldı — ama bu sayımların hepsi tek tek, elle yazılmış küçük kodlarla yapıldı ve
hiçbiri bir yerde toplu olarak durmuyor. Aynı kümeyi ikinci kez hazırlayan biri, hangi eşiğin
hangi gerekçeyle seçildiğini ya da hangi sıralamanın kullanıldığını bulamaz. Sonraki ders hazırlama
adımlarının betikle belgelenmesini ele alır: kaç adımın yeniden üretilebildiğini sayar ve kursun on
yedi dersindeki satır hesabını uçtan uca toplar.

### Yeniden Üretilebilir Hazırlama

Source: https://academia.sh/tr/kurslar/veri-hazirlama/yeniden-uretilebilir-hazirlama

Kursun on yedi hazırlama adımı veri olarak yazılır: 39 karar, üç konuya 13'er kalem. Adımların ölçtüğü sessiz kayıp toplamı 4.614 kayıttır ve bunun yalnız 69'u benimsenen hatta gerçekleşmiştir; kalan 4.545 kayıt seçilmeyen yaygın kararın ölçülmüş bedelidir. Üç adım parametresini betikten değil koşum ortamından alır ve aynı veriyle iki koşum üçünde de farklı sonuç verir; hattın 17 adımının 14'ü yeniden üretilebilirken değerler betiğe yazılınca 17'si yeniden üretilebilir olur. Kararın geri okunabilirliği hücrelerde 32'de 7 (yüzde 22) iken betikte defter tutulmadan 39'da 36, koşum defteriyle 39'da 39'a çıkar.

On yedi adımda küme hazırlandı ve her adımda giren, çıkan ve kaybolan satır sayıldı — ama bu
sayımlar o dersin içinde kaldı, hiçbiri bir yerde toplu durmuyor. Aynı kümeyi ikinci kez
hazırlayan biri hangi eşiğin seçildiğini, hangi sıralamanın kullanıldığını ya da hangi tohumla örnek
çekildiğini bulamaz.

Elektronik Tablolarla Analiz kursu bu soruyla kapanmıştı: otuz iki kararın yalnız yedisi geri
okunabiliyordu, çünkü kararlar hücrelere gömülüydü. Betik yazmak bunun cevabı gibi görünür ve kısmen
öyledir. Bu ders betiğin **nerede yetmediğini** ölçer: kaç adım aynı veriden aynı sonucu veriyor ve
kaç karar koşumdan sonra okunabiliyor.

- **VD25.** Bir hazırlama hattı, adımların **verisi** yazıldığında belgelenmiş olur: her adımın adı,
  kararları ve satır hesabı. Betiğin kendisi bu kaydın yerine geçmez.
- **VD26.** Bir adım, sonucunu belirleyen bütün değerler betikte yazılıysa **yeniden üretilebilir**.
  Değerlerden biri **koşum ortamından** geliyorsa aynı betik aynı veriden başka bir sonuç çıkarır ve
  fark hiçbir yerde görünmez.

#### Hattı Veri Olarak Yazmak

```js
// hat.mjs — MODELDIR: kursun on yedi hazirlama adimi veri olarak yaziliyor.
// "s" alani o dersin olctugu sessiz kayiptir — uyari uretmeden dusen ya da
// bozulan kayit — ve kendi dersinden okundu. "p" adimin kararlaridir; degeri
// "ortam" olan kararlar betikte yazili degildir, kosum aninda disaridan gelir.
export const HAT = [
  { n: 1, k: "toplama", ad: "dosya bicimleri", s: 142,
    p: { ayrac: ";", tirnak: "korunur", kodlama: "utf-8" } },
  { n: 2, k: "toplama", ad: "veritabanindan cekme", s: 766,
    p: { birim: "abone-donem", birlestirme: "tekil anahtar" } },
  { n: 3, k: "toplama", ad: "arayuzlerden toplama", s: 512,
    p: { sayfalama: "imlec", kapsam: "tum donemler", hiz: "sinira uyan sira" } },
  { n: 4, k: "toplama", ad: "yapisal ayristirma", s: 1877,
    p: { secici: "anlamsal", denetim: "kaynak toplami" } },
  { n: 5, k: "toplama", ad: "ornekleme", s: 0,
    p: { yontem: "tabakali", boy: 200, tohum: "ortam" } },
  { n: 6, k: "temizleme", ad: "eksik degerler", s: 100,
    p: { eksik: "bos + bosluk + yok", karar: "doldur" } },
  { n: 7, k: "temizleme", ad: "yinelenen kayitlar", s: 0,
    p: { anahtar: "kirpilmis sayac kodu", olcut: "kesin eslesme" } },
  { n: 8, k: "temizleme", ad: "tip donusumleri", s: 465,
    p: { sayi: "bicim once taninir", tarih: "bicim once taninir" } },
  { n: 9, k: "temizleme", ad: "aykiri degerler", s: 0,
    p: { yontem: "ceyrekler acikligi", carpan: "ortam", karar: "ayri defter" } },
  { n: 10, k: "temizleme", ad: "kategorik duzeltme", s: 216,
    p: { anahtar: "sadelestirilmis", eslesme: "tarife cetveli" } },
  { n: 11, k: "temizleme", ad: "metin normallestirme", s: 36,
    p: { basamak: "dort basamak", kucultme: "dile duyarli" } },
  { n: 12, k: "donusturme", ad: "birlestirme", s: 69,
    p: { tur: "ic", anahtar: "abone_no + donem" } },
  { n: 13, k: "donusturme", ad: "yeniden sekillendirme", s: 103,
    p: { bicim: "genis", bos: "atlanir" } },
  { n: 14, k: "donusturme", ad: "gruplama", s: 0,
    p: { kirilim: "bolge", ozet: "ortalama + yayilim" } },
  { n: 15, k: "donusturme", ad: "turetilmis degiskenler", s: 328,
    p: { turetme: "kisi basi + degisim", tanimsiz: "sebebiyle sayilir" } },
  { n: 16, k: "donusturme", ad: "kategorik kodlama", s: 0,
    p: { bolge: "sirasiz kodlama", memnuniyet: "sirali", siralama: "ortam" } },
  { n: 17, k: "donusturme", ad: "hazirlama defteri", s: 0,
    p: { defter: "her adim", olcu: "giren + cikan + sessiz" } },
];
export const KARAR = HAT.reduce((a, x) => a + Object.keys(x.p).length, 0);
export const ORTAM = HAT.flatMap((x) => Object.entries(x.p)
  .filter(([, v]) => v === "ortam").map(([k]) => `${x.n}.${k}`));
export const gen = (w) => (h) => h.map((v, j) => (j ? String(v).padStart(w[j])
  : String(v).padEnd(w[0]))).join("");

if (import.meta.url === `file://${process.argv[1]}`) {
  console.log(`hat defteri: ${HAT.length} adim, ${KARAR} karar, ${ORTAM.length} karar ` +
    `kosum ortamindan geliyor (${ORTAM.join(", ")})`);
  const y = gen([14, 9, 15, 16]);
  console.log("\n" + y(["konu", "adim", "karar sayisi", "sessiz kayip"]));
  for (const k of [...new Set(HAT.map((x) => x.k))]) {
    const l = HAT.filter((x) => x.k === k);
    console.log(y([k, l.length, l.reduce((a, x) => a + Object.keys(x.p).length, 0),
      l.reduce((a, x) => a + x.s, 0)]));
  }
  const S = HAT.reduce((a, x) => a + x.s, 0);
  console.log(y(["toplam", HAT.length, KARAR, S]));
  console.log(`\nsessiz kaybin dokumu (adim: kayit)`);
  console.log("  " + HAT.filter((x) => x.s > 0).map((x) => `${x.n}:${x.s}`).join("  "));
  const gercek = HAT[11].s;
  console.log(`toplam ${S} kayit, en buyuk kalem adim 4 (${HAT[3].s}); bunun ${gercek} kaydi ` +
    `benimsenen hatta gercekten dustu, kalan ${S - gercek} kayit secilmeyen yaygin kararin ` +
    `olculmus bedelidir`);
}
```

```
hat defteri: 17 adim, 39 karar, 3 karar kosum ortamindan geliyor (5.tohum, 9.carpan, 16.siralama)

konu               adim   karar sayisi    sessiz kayip
toplama               5             13            3297
temizleme             6             13             817
donusturme            6             13             500
toplam               17             39            4614

sessiz kaybin dokumu (adim: kayit)
  1:142  2:766  3:512  4:1877  6:100  8:465  10:216  11:36  12:69  13:103  15:328
toplam 4614 kayit, en buyuk kalem adim 4 (1877); bunun 69 kaydi benimsenen hatta gercekten dustu, kalan 4545 kayit secilmeyen yaygin kararin olculmus bedelidir
```

Hattın kendisi burada bir tablo. On yedi adımın her biri bir ad, bir sessiz kayıp kalemi ve bir karar
kümesi taşıyor; kararlar toplam **39** ediyor ve üç konuya 13'er dağılıyor. Biçimin tek amacı var:
adımı çalıştıran kod ile adımın **kararı** ayrı ayrı okunabilsin.

Döküm satırı kursun toplamını veriyor. On yedi adımın ölçtüğü sessiz kayıp **4.614 kayıttır**; en
büyük kalem dördüncü adımdır, çünkü sayfaya bir sütun eklenince hiçbir seçici kırılmaz ve 1.877
kaydın hepsi yanlış alandan okunur. Sayının önemli bir ayrımı var: **4.614'ün yalnız 69'u benimsenen
hatta gerçekten düştü** — iç birleştirmenin hiç göstermediği aboneler. Kalan 4.545 kayıt, o adımda
yaygın olan kararın **ölçülmüş bedelidir** ve o karar seçilmediği için gerçekleşmedi. Biri hattın
gerçek kaybı, öteki kaybın yazılmış olması sayesinde alınmayan riskin büyüklüğüdür.

#### Aynı Betik, İki Koşum

```js
// kosum.mjs — hattin ortamdan beslenen uc adimi iki kez kosturulur. Ayni veri,
// ayni kod, iki kosum. Sonra ayni uc adim betige yazilmis degerlerle kosturulur.
import { HAT, KARAR, ORTAM, gen } from "./hat.mjs";

const uretec = (t) => { let s = (Math.imul(t ^ 0x9e3779b9, 2246822507) >>> 0);
  return () => (s = (s * 1664525 + 1013904223) >>> 0) / 4294967296; };
// Kucuk bir model kume; kurgudur ve yalnizca uc adimi kosturmak icin var.
// Her 97. okumada ondalik noktasi kaymis, her 41. okumada yuksek bir deger var.
const r0 = uretec(20260218), KUME = [];
for (let i = 0; i < 600; i++) {
  const t = Math.round((6 + r0() * 46) * 100) / 100;
  KUME.push({ okuma: i % 97 === 0 ? t * 10 : i % 41 === 0 ? t * 2.2 : t,
    bolge: ["kuzey", "guney", "dogu"][Math.floor(r0() * 3)] });
}

const ort = (a) => a.reduce((x, y) => x + y, 0) / a.length;
const ADIM = [
  ["5 ornekleme", (l, o) => { const r = uretec(o.tohum);
    return l.map((x) => ({ x, u: r() })).sort((a, b) => a.u - b.u)
      .slice(0, 200).map((a) => a.x); }],
  ["9 aykiri ayirma", (l, o) => { const v = l.map((x) => x.okuma).sort((a, b) => a - b);
    const q = (t) => v[Math.floor(t * (v.length - 1))], a1 = q(0.25), a3 = q(0.75);
    const ac = a3 - a1;
    return l.filter((x) => x.okuma >= a1 - o.carpan * ac && x.okuma <= a3 + o.carpan * ac); }],
  ["16 kodlama esigi", (l, o) => { const a = [...new Set(l.map((x) => x.bolge))];
    const d = o.siralama === "alfabetik" ? [...a].sort() : a;
    const k = Object.fromEntries(d.map((x, j) => [x, j + 1]));
    return l.filter((x) => k[x.bolge] >= 2); }],
];
const kostur = (o) => ADIM.map(([ad, f]) => { const c = f(KUME, o);
  return { ad, satir: c.length, ort: ort(c.map((x) => x.okuma)) }; });

// Ortam iki kosumda farkli: tohum saatten, carpan bir yapilandirmadan, siralama
// kategori adlarinin geldigi kaynaktan geliyor. Ucu de betikte yazili degil.
const R1 = kostur({ tohum: 4711, carpan: 1.5, siralama: "alfabetik" });
const R2 = kostur({ tohum: 8123, carpan: 3.0, siralama: "ilk gorulme" });
const ESIK = 0.0005; // m3; iki ortalama bu esikten yakinsa ayni sayilir
const ayni = (a, b) => a.satir === b.satir && Math.abs(a.ort - b.ort) < ESIK;

const y = gen([19, 10, 13, 10, 13, 9]);
console.log(`ayni veri (${KUME.length} satir), ayni kod, iki kosum:`);
console.log("\n" + y(["adim", "R1 satir", "R1 ortalama", "R2 satir", "R2 ortalama", "ayni mi"]));
R1.forEach((a, j) => console.log(y([a.ad, a.satir, a.ort.toFixed(3), R2[j].satir,
  R2[j].ort.toFixed(3), ayni(a, R2[j]) ? "evet" : "hayir"])));

// Duzeltme: uc deger de betige yazilir, ortam artik okunmuyor.
const SABIT = { tohum: 4711, carpan: 1.5, siralama: "alfabetik" };
const D1 = kostur(SABIT), D2 = kostur(SABIT);
const tutan = D1.filter((a, j) => ayni(a, D2[j])).length;
const onceki = R1.filter((a, j) => ayni(a, R2[j])).length;
console.log(`\nortamdan beslenen ${ADIM.length} adimin ${onceki} tanesi iki kosumda ayni; ` +
  `degerler betige yazilinca ${tutan} tanesi ayni. Hattin ${HAT.length} adiminin ` +
  `${HAT.length - ADIM.length + onceki} tanesi duzeltmeden once, ${HAT.length} tanesi ` +
  `duzeltmeden sonra yeniden uretilebiliyor`);

// Geri okunabilirlik: kararin degeri kosumdan sonra bir yerden okunabiliyor mu.
const pay = (a, b) => `${a}/${b} (yuzde ${(100 * a / b).toFixed(0)})`;
console.log(`\ngeri okunabilen karar — yalniz hucrelerde ${pay(7, 32)}, betikte defter ` +
  `tutulmadan ${pay(KARAR - ORTAM.length, KARAR)}, kosum defteriyle ${pay(KARAR, KARAR)}`);
console.log(`okunamayan uc karar ortamdan gelenlerdir; defter tutulmazsa kosumdan sonra ` +
  `hicbir yerden okunamaz`);
```

```
ayni veri (600 satir), ayni kod, iki kosum:

adim                 R1 satir  R1 ortalama  R2 satir  R2 ortalama  ayni mi
5 ornekleme               200       31.886       200       32.361    hayir
9 aykiri ayirma           590       29.468       593       29.806    hayir
16 kodlama esigi          404       31.965       391       32.968    hayir

ortamdan beslenen 3 adimin 0 tanesi iki kosumda ayni; degerler betige yazilinca 3 tanesi ayni. Hattin 17 adiminin 14 tanesi duzeltmeden once, 17 tanesi duzeltmeden sonra yeniden uretilebiliyor

geri okunabilen karar — yalniz hucrelerde 7/32 (yuzde 22), betikte defter tutulmadan 36/39 (yuzde 92), kosum defteriyle 39/39 (yuzde 100)
okunamayan uc karar ortamdan gelenlerdir; defter tutulmazsa kosumdan sonra hicbir yerden okunamaz
```

Üst tablo betiğin tek başına neden yetmediğini gösteriyor. Üç adımın parametresi betikte yazılı
değil: örneklemenin tohumu saatten, aykırı ayırmanın çarpanı bir yapılandırmadan, kodlamanın
sıralaması kategori adlarının geldiği kaynaktan geliyor. Kod iki koşumda aynı, veri de aynı 600
satır; sonuç üçünde de farklı.

Farkların biçimi ayrı. Örnekleme iki koşumda da 200 satır çeker ve ortalamayı 31,886 ile 32,361 m³
arasında oynatır: **satır sayısı hiç değişmez**, bu yüzden satır sayısına bakan bir denetim hiçbir
şey görmez. Aykırı ayırma 590 ile 593 satır bırakır. Kodlama eşiği 404 ile 391 satır seçer — bu, bir
önceki dersin bulgusunun hattaki karşılığıdır ve sıralama kararı ortamdan geldiği için artık
**seçildiği bile görünmez**.

Hattın on yedi adımının **14'ü** düzeltmeden önce, **17'si** üç değer betiğe yazılınca yeniden
üretilebiliyor. Düzeltme bir yeniden yazma değil, üç sayının koddan okunur hale getirilmesidir.

Son satır kursun açılışındaki soruyu kapatıyor. Kararlar hücrelerin içindeyken 32 karardan 7'si geri
okunabiliyordu, yüzde 22. Betiğe yazıldığında 39 kararın 36'sı okunabilir olur, yüzde 92. Koşum
defteri tutulduğunda, yani her adım kendi kararlarını ve satır hesabını çıktıya yazdığında oran
**39'da 39** olur. Aradaki fark yeniden üretilebilirliğin durduğu yerdir: **betik ne yapabileceğini
gösterir, defter ne yapıldığını gösterir.**

#### Özet

- Bir hazırlama hattı, adımların verisi yazıldığında belgelenir: 17 adım, 39 karar, adım başına bir
  satır hesabı.
- On yedi adımın ölçtüğü sessiz kayıp 4.614 kayıttır; 69'u benimsenen hatta gerçekleşti, 4.545'i
  seçilmeyen yaygın kararın ölçülmüş bedelidir.
- Parametresi koşum ortamından gelen üç adım aynı veriyle iki koşumda farklı sonuç verir;
  örneklemede satır sayısı değişmediği için fark satır denetiminden kaçar.
- Değerler betiğe yazılınca yeniden üretilebilen adım sayısı 14'ten 17'ye çıkar.
- Kararın geri okunabilirliği hücrelerde 32'de 7 (yüzde 22), betikte defter tutulmadan 39'da 36
  (yüzde 92), koşum defteriyle 39'da 39'dur.

#### Kurs Kapanışı

On yedi dersin hesabı tek bir tabloda duruyor ve her sayı kendi dersinden okundu.

| Ders | Giren satır | Çıkan satır | Nereye gitti |
|---|---|---|---|
| Toplama 01 dosya biçimleri | 2.076 fiziksel | 2.037 kayıt | naif bölmede 142 sessiz |
| Toplama 02 veritabanından çekme | 1.877 | 1.877 | yanlış birleştirmede 766 sessiz |
| Toplama 03 arayüzlerden toplama | 1.877 | 1.877 | dar kapsamda 477, sayfalamada 35 |
| Toplama 04 yapısal ayrıştırma | 1.877 | 1.877 | 1.877 kayıt yanlış alandan |
| Toplama 05 örnekleme | 1.877 | 200 | 1.677 karar gereği dışarıda, 0 sessiz |
| Temizleme 01 eksik değerler | 2.086 | 2.068 | 18 elendi, 204 dolduruldu |
| Temizleme 02 yinelenen kayıtlar | 2.068 | 1.880 | 188 birleşti, 23 yanlış bağ |
| Temizleme 03 tip dönüşümleri | 1.880 | 1.880 | 0 elendi, yaygında 465 sessiz |
| Temizleme 04 aykırı değerler | 1.880 | 1.816 | 64 ayrı deftere |
| Temizleme 05 kategorik düzeltme | 1.816 | 1.816 | 15 yazım 5 sınıfta |
| Temizleme 06 metin normalleştirme | 1.816 | 1.816 | 1.491 eşleşme kurtarıldı |
| Dönüştürme 01 birleştirme | 3.471 okuma | 3.199 | 169 eşleşmeyen, 103 çift geçen, 69 abone sessiz |
| Dönüştürme 02 yeniden şekillendirme | 3.199 | 1.260 geniş | 581 boş hücre, 103 üzerine yazıldı |
| Dönüştürme 03 gruplama | 3.199 | 5 özet | 3.194 eridi, 7 göz görünmedi |
| Dönüştürme 04 türetilmiş değişkenler | 3.199 | 3.199 | 328 tanımsız, süzgeçte düşer |
| Dönüştürme 05 kategorik kodlama | 3.199 | 3.199 | eşik 2.166 / 1.678 / 1.633 |
| Dönüştürme 06 hazırlama defteri | 3.199 | 3.199 | 39 karar deftere yazıldı |

Bir konunun çıkan satırı sonraki konunun giren satırına eşit değil, çünkü üç konu aynı kurgu ağın
**üç ayrı çekimiyle** çalışır: toplama 1.877 okumayı, temizleme 2.086 satırlık ham çekimi,
dönüştürme üç dönemlik 3.471 okumayı ölçer. Uçtan uca taşınan tek bir sayı değil, **hesabın
kendisidir**.

Kursun kuralı bu tablonun okunma biçimidir: **hazırlama adımlarının toplamı bir hesaptır; giren ile
çıkan arasındaki fark kalem kalem yazılamıyorsa veri hazırlanmamış, yalnız değiştirilmiştir. Kaybını
yazamadığın temizlik bir temizlik değildir.** On yedi dersin ölçtüğü **sessiz kayıp toplamı 4.614
kayıttır** ve bu sayının varlığı bir kusur değil kursun çıktısıdır: yazılmayan bir sessiz kayıp sıfır
görünür.

On yedi derste küme hazırlandı ve her adımın hesabı yazıldı. Ama bütün bu adımlar tek tek, elle
yazılmış küçük kodlarla yapıldı; aynı işi binlerce satırda, tekrarlanabilir ve okunabilir biçimde
yapan bir araç takımı hiç kullanılmadı. Sonraki kurs, Python ile Veri Analizi, elle yazılan her
adımın bir veri çerçevesi arayüzündeki karşılığını ele alır.

