1  Wstęp

Sztuczna inteligencja i analiza danych opierają się na bardzo prostej zasadzie: na podstawie ograniczonej liczby przykładów (danych treningowych) chcemy wyciągać wnioski o zjawiskach, których nie widzimy w całości. Dane treningowe zawierają szum, błędy pomiarowe, przypadkowe odchylenia, braki oraz zależności, których często nie potrafimy bezpośrednio zaobserwować. Dlatego analiza danych nie jest jedynie rachunkiem na tabelach liczb. Jest próbą rozumowania w warunkach niepewności. Rachunek prawdopodobieństwa dostarcza języka, w którym tę niepewność można precyzyjnie opisać.

Mimo że poniższe notatki stanowią materiał do standardowego kursu z teorii prawdopodobieństwa postaramy się dobrze umotywować główny kierunek wykładu. Zaczniemy od przykładu pokazującego że losowe zaburzenia nie są problemem, a narzędziem, które można wykorzystać na swoją korzyść.

1.1 Boarding samolotu

Zaczniemy od zagadnienia wpuszczania pasażerów na pokład samolotu. Poniższa symulacja porównuje trzy kolejności wpuszczania pasażerów: uporządkowani zgodnie z numerami siedzeń, losowo oraz metodą z pracy Steffen (2008).

Pokaż kod
boarding = {
  const ROWS = 24;
  const COLS = ["A", "B", "C", "D", "E", "F"];
  const AISLE_AFTER = 2;
  const COLORS = {
    empty: "#d8dee8", seated: "#22c55e", walking: "#2563eb",
    stowing: "#f59e0b", waiting: "#ef4444", aisle: "#eef2f7",
    outline: "#7b8798", text: "#344054"
  };

  // Powtarzalny generator liczb pseudolosowych.
  function rng(seed) {
    let x = seed >>> 0;
    return () => ((x = (1664525 * x + 1013904223) >>> 0) / 4294967296);
  }
  function shuffled(xs, random) {
    const a = xs.slice();
    for (let i = a.length - 1; i > 0; --i) {
      const j = Math.floor(random() * (i + 1));
      [a[i], a[j]] = [a[j], a[i]];
    }
    return a;
  }
  function passengers() {
    return Array.from({length: ROWS}, (_, r) =>
      COLS.map((seat, c) => ({id: `${r + 1}${seat}`, row:r, col:c, seat}))
    ).flat();
  }
  function orderFor(method, random) {
    const all = passengers();
    if (method === "random") return shuffled(all, random);
    if (method === "back") {
      const ans = [];
      for (let r = ROWS - 1; r >= 0; --r)
        ans.push(...shuffled(all.filter(p => p.row === r), random));
      return ans;
    }
    // Metoda Steffena: najpierw okna, potem środek, na końcu przejście;
    // w każdej grupie naprzemienne rzędy od tyłu i obie strony kabiny.
    const ans = [];
    for (const pair of [[0,5],[1,4],[2,3]]) {
      for (const parity of [1,0]) {
        for (let r = ROWS - 1; r >= 0; --r) {
          if (r % 2 === parity)
            for (const c of pair) ans.push(all.find(p => p.row === r && p.col === c));
        }
      }
    }
    return ans;
  }
  function makeSim(method, seed) {
    const random = rng(seed);
    return {
      method, random, queue:orderFor(method, random), active:[], seated:new Set(),
      time:0, done:false, simultaneous:0
    };
  }
  function seatDelay(p, seated) {
    // 3 jednostki na bagaż oraz dodatkowy czas, gdy trzeba przepuścić
    // osobę siedzącą bliżej przejścia.
    let delay = 3;
    if (p.col === 0) delay += (seated.has(`${p.row}-2`) ? 2 : 0) + (seated.has(`${p.row}-1`) ? 2 : 0);
    if (p.col === 1) delay += seated.has(`${p.row}-2`) ? 2 : 0;
    if (p.col === 5) delay += (seated.has(`${p.row}-3`) ? 2 : 0) + (seated.has(`${p.row}-4`) ? 2 : 0);
    if (p.col === 4) delay += seated.has(`${p.row}-3`) ? 2 : 0;
    return delay;
  }
  function step(sim) {
    if (sim.done) return;
    sim.time++;

    const blocked = new Set(sim.active.filter(p => p.state === "stowing").map(p => p.pos));
    const occupied = new Set(sim.active.filter(p => p.state !== "seated").map(p => p.pos));

    // Aktualizujemy od najdalszego pasażera, aby nikt nie przeszedł przez inną osobę.
    const moving = sim.active.slice().sort((a,b) => b.pos - a.pos);
    for (const p of moving) {
      if (p.state === "stowing") {
        p.wait--;
        if (p.wait <= 0) {
          p.state = "seated";
          sim.seated.add(`${p.row}-${p.col}`);
          occupied.delete(p.pos);
        }
      } else if (p.state === "walking") {
        if (p.pos === p.row) {
          p.state = "stowing";
          p.wait = seatDelay(p, sim.seated);
        } else {
          const next = p.pos + 1;
          if (!occupied.has(next) && !blocked.has(next)) {
            occupied.delete(p.pos); p.pos = next; occupied.add(p.pos);
          }
        }
      }
    }
    sim.active = sim.active.filter(p => p.state !== "seated");

    // Jedna osoba może wejść w każdej jednostce czasu, jeśli początek przejścia jest wolny.
    if (sim.queue.length && !occupied.has(-1) && !occupied.has(0)) {
      const p = sim.queue.shift();
      sim.active.push({...p, pos:-1, state:"walking", wait:0});
    }
    sim.done = sim.queue.length === 0 && sim.active.length === 0;
  }

  const methods = [
    {id:"back", label:"Od tyłu do przodu"},
    {id:"random", label:"Kolejność losowa"},
    {id:"steffen", label:"Metoda Steffena"}
  ];
  const root = html`<div>
    <div class="boarding-controls">
      <button class="primary" data-action="start">▶ Start</button>
      <button data-action="pause">Ⅱ Pauza</button>
      <button data-action="reset">↺ Od początku</button>
      <label>Tempo <input data-speed type="range" min="1" max="10" value="5"></label>
      <label>Ziarno <input data-seed type="number" min="1" max="9999" value="2026" style="width:5.5rem"></label>
    </div>
    <div class="boarding-grid"></div>
    <div class="boarding-legend">
      <span class="boarding-key"><i class="boarding-dot" style="background:${COLORS.walking}"></i> idzie</span>
      <span class="boarding-key"><i class="boarding-dot" style="background:${COLORS.stowing}"></i> odkłada bagaż</span>
      <span class="boarding-key"><i class="boarding-dot" style="background:${COLORS.seated}"></i> siedzi</span>
    </div>
  </div>`;
  const grid = root.querySelector(".boarding-grid");
  const panels = new Map();
  for (const m of methods) {
    const panel = html`<section class="boarding-panel"><h3>${m.label}</h3><div class="boarding-status"></div><svg viewBox="0 0 250 666" role="img" aria-label="Symulacja: ${m.label}"></svg></section>`;
    grid.append(panel); panels.set(m.id, panel);
  }

  let sims, timer = null;
  function reset() {
    pause();
    const seed = +root.querySelector("[data-seed]").value || 2026;
    sims = methods.map((m,i) => makeSim(m.id, seed + 101 * i));
    render();
  }
  function start() {
    if (timer || sims.every(s => s.done)) return;
    const loop = () => {
      for (const s of sims) step(s);
      render();
      if (sims.every(s => s.done)) pause();
    };
    const speed = +root.querySelector("[data-speed]").value;
    timer = setInterval(loop, 310 - 27 * speed);
  }
  function pause() { if (timer) clearInterval(timer); timer = null; }

  function render() {
    for (const sim of sims) {
      const panel = panels.get(sim.method), svg = panel.querySelector("svg");
      const y = r => 625 - r * 24;
      let marks = `<rect x="64" y="32" width="122" height="630" rx="55" fill="none" stroke="${COLORS.outline}" stroke-width="2"/>`;
      marks += `<rect x="116" y="42" width="18" height="590" fill="${COLORS.aisle}"/>`;
      for (let r=0; r<ROWS; ++r) {
        marks += `<text x="51" y="${y(r)+5}" text-anchor="end" font-size="9" fill="${COLORS.text}">${r+1}</text>`;
        for (let c=0; c<6; ++c) {
          const x = 70 + c*16 + (c>AISLE_AFTER ? 18 : 0);
          const fill = sim.seated.has(`${r}-${c}`) ? COLORS.seated : COLORS.empty;
          marks += `<rect x="${x}" y="${y(r)-8}" width="12" height="16" rx="2" fill="${fill}"/>`;
        }
      }
      for (const p of sim.active) {
        const cy = p.pos < 0 ? 646 : y(p.pos);
        const fill = p.state === "stowing" ? COLORS.stowing : COLORS.walking;
        marks += `<circle cx="125" cy="${cy}" r="6" fill="${fill}" stroke="white" stroke-width="1"><title>${p.id}</title></circle>`;
      }
      svg.innerHTML = marks;
      const seated = sim.seated.size;
      panel.querySelector(".boarding-status").textContent = sim.done
        ? `Koniec: ${sim.time} jednostek czasu`
        : `czas ${sim.time} · siedzi ${seated}/144`;
    }
  }
  root.querySelector("[data-action=start]").onclick = start;
  root.querySelector("[data-action=pause]").onclick = pause;
  root.querySelector("[data-action=reset]").onclick = reset;
  root.querySelector("[data-speed]").onchange = () => { const wasRunning = !!timer; pause(); if (wasRunning) start(); };
  root.querySelector("[data-seed]").onchange = reset;
  invalidation.then(pause);
  reset();
  return root;
}

Czy potrafisz uzasadnić dlaczego metoda uporządkowana wypada słabiej niż losowa kolejność? Czy potrafisz uzasadnić, że metoda Steffena jest optymalna?

1.2 Półmaraton w Tokio

Kolejny przykład pochodzi z Tokio, gdzie w październiku 2024 roku odbył się Hakone Ekiden Yosenkai - półmaraton (bieg na 21.0975 km) kwalifikacyjny do wyścigu Hakone Ekiden (bieg sztafetowy). Oczywiście spodziewamy się, że znakomita większość zawodników uplasuje się w połowie stawki i kilka najszybszych lub najwolniejszych osób będzie odstawało odpowiednio na jej początku i końcu.

Okazuje się jednak, że dokładny rozkład biegaczy jest bardzo regularny. Zaobserwowana krzywa to funkcja dzwonowa, która jest równa \[\begin{equation*} \exp \left(-(x-\mu)^2/(2\sigma^2)\right)/\sqrt{2\pi\sigma^2} \end{equation*}\] dla pewnych parametrów \(\sigma>0\) oraz \(\mu \in \mathbb{R}\). W trakcie wykładu nauczymy się szukać tego typu regularności i zobaczymy dokładnie skąd bierze się zaobserwowany kształt. Parametry \(\mu\) oraz \(\sigma\) są odpowiednio odpowiedzialne za przesunięcie i kształt krzywej.

1.3 IMDb

Rozważmy filmy nakręcone w latach \(2000-2025\), które obejrzało co najmniej \(1000\) osób. Każdy film ma swoją ocenę w serwisie IMDb. Co zobaczymy, gdy zamiast pojedynczych tytułów przyjrzymy się tysiącom filmów jednocześnie? Jedna obserwacja to jeden film i jego ocena w IMDb.

Niebieskie słupki przedstawiają rzeczywiste oceny filmów. Pomarańczowa linia to krzywa normalna o tej samej średniej i odchyleniu standardowym co wybrana próba.

Ostateczny histogram przypomina dzwon, ale ma dłuższy lewy ogon. Krzywa normalna jest więc przybliżeniem. Zwiększanie liczby filmów pozwala lepiej zobaczyć rozkład ocen — nie zmienia go automatycznie w rozkład normalny.

1.4 Opóźnienia lotów

Przyjrzyjmy się lotom z nowojorskich lotnisk w 2013 roku. Możemy porównać odchylenia przylotów od rozkładu albo czasy spędzone w powietrzu na jednej trasie: JFK–Los Angeles.

Niebieskie słupki przedstawiają rzeczywiste dane w przedziałach szerokości 5 minut. Pomarańczowa linia to krzywa normalna o tej samej średniej i odchyleniu standardowym co wybrana próba.

W wykresach przylotów wartości ujemne oznaczają przybycie przed planowaną godziną. W wariancie z punktualnym wylotem uwzględniamy loty, które wystartowały o czasie lub wcześniej. W każdym wariancie pomijamy brakujące wartości badanej zmiennej.

1.5 Deska Galtona

Kolejny przykład to deska Galtona zaprezentowana poniżej. Kule spadają po kołkach za każdym razem odbijając się losowo w lewo bądź prawo. Poniżej widzimy histogram (znormalizowany wykres słupkowy) liczby kul, które opuściły deskę przez dane miejsce.

1.6 Jeszcze jeden przykład

Ideą twierdzeń granicznych jest wyodrębnienie deterministycznego stwierdzenia o losowym układzie. Nie każde takie twierdzenie prowadzi do krzywej dzwonowej. W losowych parkietażach zobaczymy okrąg.

Dla naturalnego \(n\) rozważmy diament aztecki rzędu \(n\). Powstaje on z szachownicy \(2n\times 2n\) przez usunięcie czterech schodkowych rogów. Długości kolejnych, wyśrodkowanych wierszy wynoszą

\[ 2,4,\ldots,2n,2n,\ldots,4,2. \]

Diament dla \(n=4\) wygląda następująco:

Chcemy pokryć cały diament kostkami domina o wymiarach \(2\times1\) lub \(1\times2\), bez nakładania kostek i bez pozostawiania pustych pól. Diament zawiera \(2n(n+1)\) pól, więc każdy parkietaż składa się z \(n(n+1)\) kostek. Spośród wszystkich dopuszczalnych parkietaży losujemy jeden. Każdy parkietaż wybieramy z jednakowym prawdopodobieństwem.

Wybierz \(n=100\) lub \(n=200\) i wykonaj kilka nowych losowań. Szczegóły za każdym razem się zmieniają, ale ogólny obraz pozostaje podobny. Przy czterech rogach pojawiają się zamarznięte obszary, w których kostki tworzą regularny wzór. W środku orientacje są wymieszane i tworzą okrąg.