Warstwa semantyczna danych – od sensu biznesowego do kodu

blog.prokulski.science 3 godzin temu

W wielu firmach te same dane prowadzą do różnych liczb i interpretacji, bo każdy zespół inaczej rozumie najważniejsze pojęcia biznesowe. Brak wspólnej warstwy semantycznej powoduje chaos, utratę zaufania do raportów i paraliż decyzyjny. Ten artykuł pokazuje, jak zapanować nad sensem danych - od źródła problemu po praktyczne rozwiązania.

Problem: kiedy liczby przestają mieć znaczenie

Jeśli pracujesz z danymi dłużej niż tydzień, znasz ten ból doskonale. Ten sam raport, trzy zespoły i... trzy różne wartości “sprzedaży". Dane źródłowe te same, Zapytania SQL poprawne, dashboardy w Power BI działają jak należy, joiny się zgadzają - a mimo to ktoś kłamie. Albo raczej: każdy ma swoją prawdę.

Manager regionu pokazuje 2,3 mln przychodu. CFO mówi o 1,8 mln. Analityk twierdzi, iż obie liczby są prawidłowe. Zarząd pyta, kto jest niekompetentny. I co zaraportować na giełdę?

Problem nie leży w danych. Baza jest jedna, źródła te same, pipeline działa. Problem leży w tym, że:

  • Analityk A liczy tylko zamówienia ze statusem PAID
  • Analityk B liczy PAID + SHIPPED
  • CFO liczy wszystko oprócz CANCELLED, bo “faktura wystawiona"

Każdy ma rację w swojej interpretacji. I każdy ma inną definicję tego, co znaczy słowo “sprzedaż".

Konsekwencje biznesowe

To nie jest tylko techniczny detal. To jest poważne biznesowe ryzyko, może choćby problem. Taka sytuacja prowadzi do:

  • paraliżu decyzyjnego - “sprawdźmy jeszcze raz dane" staje się mantrą każdego spotkania zarządu. Decyzje są odkładane, bo nikt nie ufa liczbom. Wszyscy mają więcej roboty (albo dłużej czekają na wynik).
  • utraty zaufania do BI - działy zaczynają robić własne Excele, bo “ten dashboard znowu coś źle pokazuje". Self-service BI zamienia się w self-destruction. I wcale to nie poprawia sytuacji - teraz manager regionu mówi 2,3 mln, CFO mówi 1,8 mln, zespół ze swoim Excelem mówi 2,1 mln... Czy jest lepiej? No jest “w środku", ale czy lepiej?
  • marnowania czasu - analitycy spędzają 60% czasu w wyjaśnianiu rozbieżności między raportami zamiast na analizie. IT poprawia “ostatni raz" któryś z kolei dashboard...
  • złych decyzji biznesowych - zarząd optymalizuje niewłaściwe wskaźniki, nakładane są niewłaściwe cele - każdy dział inaczej rozumie wskaźniki, więc dąży do czegoś innego.

Dlaczego to się dzieje?

Przyczyna jest dość banalna: brak jest jednego źródła prawdy - nie w sensie bazy danych, ale w sensie znaczenia. Mamy jedną bazę, ale dziesiątki interpretacji tego, co dane w niej zgromadzone oznaczają.

Logika biznesowa i znaczenia są rozproszone:

  • w SQL-ach analityków
  • w miarach Power BI
  • w Excelach handlowców (nie chcesz wiedzieć...)
  • w głowach ludzi

I każdy krok tego łańcuszka może wprowadzić inną definicję. Każdy swoją, każdy prawdziwą.

Warstwa semantyczna - tłumacz między danymi a ludźmi

Warstwa semantyczna powstała dokładnie po to, żeby ten cyrk zakończyć. To jest tłumacz między surowymi danymi a ludźmi, którzy chcą zadawać sensowne pytania i dostawać jedną, wspólną odpowiedź.

Czym adekwatnie jest warstwa semantyczna?

Najprościej mówiąc: to warstwa, która wie co dane znaczą, a nie tylko jakie są.

Baza danych wie, iż kolumna order_status zawiera wartości NEW, PAID, SHIPPED, CANCELLED. Wie, bo ta kolumna jest tak zdefiniowana (jako enum) albo inserty wkładające dane do bazy mają tylko takie możliwości.

Warstwa semantyczna wie, że: > Sprzedaż = suma kwot zamówień o statusie PAID lub SHIPPED

To jest kontrakt semantyczny. Jedna definicja, żadnych dyskusji. Nie dlatego, iż ktoś ma rację - tylko dlatego, iż wszyscy patrzą na to samo znaczenie.

Pięć kluczowych cech warstwy semantycznej

1. Jedno źródło znaczeń, nie tylko danych

Dane możesz mieć w jednej bazie, w jednej hurtowni. Ale jeżeli każdy raport je inaczej interpretuje, to masz dziesiątki źródeł prawdy. Warstwa semantyczna to jedno, kanoniczne miejsce, gdzie definiujesz co coś znaczy.

2. Oddzielenie “jak liczymy" od “co oglądamy"

Bez warstwy semantycznej logika biznesowa siedzi wszędzie: w dashboardach, SQL-ach, Excelach. Z warstwą semantyczną: logika liczenia jest w jednym miejscu, a raporty tylko z niej korzystają.

3. Wspólny język dla biznesu, analityków i IT

Biznes mówi: “Chcę marżę netto na kliencie". SQL odpowiada: 100 * (sum(revenue) - sum(costs)) / sum(revenue). Warstwa semantyczna tłumaczy to na Marża netto (%) - pojęcie, którym można rozmawiać, które ma opis, właściciela i jasne źródła.

4. Mniej SQL-owego spaghetti, więcej spójności

Jeśli zmiana definicji KPI wymaga poprawy 27 dashboardów, każdy ma własne zapytania, a każdy join jest “lekko inny" - to masz klasyczny brak warstwy semantycznej. Z nią: poprawiasz raz, działa wszędzie.

5. Skalowalność: więcej raportów bez chaosu

Na początku: 2 analityków, 5 raportów, wszystko “jakoś działa". Po roku: 20 raportów, self-service BI, każdy klika co chce. Bez warstwy semantycznej to się kończy pytaniem: “Dlaczego ten wykres pokazuje coś innego niż tamten?"

Po co mi to? - korzyści biznesowe

Dla managera

  • Koniec wojny o liczby. Zarząd nie pyta już “która liczba jest prawdziwa", bo jest tylko jedna definicja każdego KPI. Tylko jedna definicja - to bardzo ważne. Różne działy firmy pokażą tą samą wartość dla sprzedaży, kosztów i przede wszystkim zysku.
  • Szybsze decyzje. Nie tracisz tygodni na wyjaśnianie rozbieżności między raportami. Zaufanie do danych rośnie, bo wszyscy wiedzą, co oznaczają. Teraz można być data driven.
  • Kontrola nad self-service BI. Gdy dajesz ludziom możliwość tworzenia własnych raportów, nie zamienia się to w chaos - bo wszyscy korzystają z tych samych definicji. I teraz każdy może robić swoje przekroje i szukać zależności pod swoim kątem.

Dla analityka

  • Mniej czasu w gaszenie pożarów. Zamiast wyjaśniać rozbieżności między raportami, skupiasz się na analizie - sprawdzasz dlaczego biznes idzie tak albo inaczej, co można zmienić i poprawić.
  • Jeden raz definiujesz, wszędzie działa. Nowe KPI? Dodajesz do warstwy semantycznej. Wszystkie raporty automatycznie mają dostęp. Wszystkie działy widzą te same dane, nie muszą tworzyć KPI po swojemu (i lepiej żeby tego nie robiły).
  • Przejrzysta dokumentacja. Każda miara ma opis, właściciela, logikę liczenia. Nowy analityk nie musi zgadywać, co oznacza revenue_adjusted_v3. Możecie się sprzeczać o definicję i ją wspólnie ustalać czy też zmieniać, ale jest ona jedna - każdy ma tak samo.

Dla technika

  • Mniej duplikacji kodu. Ta sama logika nie jest przepisywana w 15 dashboardach, wszyscy czerpią z jednego źródła.
  • Łatwiejsze utrzymanie. Zmiana definicji w jednym miejscu, a nie refactoring całego BI, w różnych działach.
  • Lepsze testy. Testujesz logikę raz, a nie każdy dashboard osobno.

Przykłady z życia - kiedy brak semantyki boli
najbardziej

E-commerce: sprzedaż

Masz kolumnę order_status z wartościami: NEW, PAID, SHIPPED, CANCELLED.

Pytanie biznesowe: “Ile było sprzedaży?"

Bez warstwy semantycznej:

  • Marketing liczy NEW + PAID + SHIPPED (bo “potencjalna sprzedaż")
  • Finanse liczą tylko PAID (bo “zaksięgowane")
  • Logistyka liczy SHIPPED (bo “faktycznie wysłane")

Zarząd dostaje trzy różne liczby i nikt nie wie, której używać.

Z warstwą semantyczną masz jedną definicję: > Sprzedaż = suma kwot zamówień o statusie PAID lub SHIPPED.

Koniec dyskusji (dyskusja była wcześniej - prawdopodobnie długa - zanim ustalono powyższą definicję). Wszyscy patrzą na to samo.

Bankowość: aktywny klient

“Aktywny klient" to najważniejszy KPI. Ale co to adekwatnie znaczy?

  • Logowanie w ciągu 30 dni?
  • Albo transakcja kartą?
  • Albo saldo > 0?
  • A może wszystkie te warunki jednocześnie?

Bez semantyki każdy dashboard ma własną wersję tej definicji. Jeden pokazuje 100 tysięcy aktywnych klientów, drugi 85 tysięcy...

Z semantyką masz jedną miarę: active_customer = true/false z jasną definicją. Raporty są głupie, wyświetlają sumę tych co mają true w tej kolumnie. I bardzo dobrze, tak ma być!

Produkcja: czas przestoju

Dyrektor fabryki nie chce wiedzieć z jakiej tabeli pochodzi downtime_minutes, jaki ma typ danych czy jak wygląda join. Analityk (czy ktokolwiek odpowiadający na to pytanie) nie powinien w ogóle używać słów “baza danych", “tabela" czy “kolumna"!

Dyrektor chce informację: > “Czas przestoju linii produkcyjnej (minuty)"

Semantyka robi z technicznego języka pojęcia, którymi da się rozmawiać z biznesem.

Logistyka: terminowość dostaw

“Dostawa na czas" - brzmi prosto. Ale:

  • Czy liczymy względem planu?
  • Czy względem obiecanego terminu klientowi?
  • Co z dostawami w kilku częściach?
  • Co jeżeli klient przesunął termin?

Bez semantyki każdy raport to osobna interpretacja. Z semantyką masz miarę on_time_delivery_rate (true albo false) z precyzyjną definicją, poprawiasz ją raz i działa wszędzie.

Jak to wygląda w praktyce (jeszcze bez technologii)

Najpierw obalimy mit:

Warstwa semantyczna to niekoniecznie nowe tabele z kopiami danych.

Czasem tak. Częściej - nie.

Trzy główne podejścia

1. Warstwa semantyczna jako logika (nie dane)

Najczęstszy i najzdrowszy wariant.

  • Dane zostają tam, gdzie są (hurtownia, lakehouse)
  • Warstwa semantyczna to:
    • definicje miar
    • definicje wymiarów
    • reguły biznesowe
    • relacje między encjami

Przykłady narzędzi: Power
BI semantic model
, LookML (Looker), dbt metrics, Cube.dev, AtScale.

Nie kopiujesz danych. Tworzysz abstrakcję. SQL jest ukryty, biznes widzi czytelne pojęcia.

2. Warstwa semantyczna jako widoki/modele logiczne

Trochę bardziej “oldschool", ale wciąż skuteczne.

Budujesz widoki SQL, modele w dbt (staging → marts), czasem materializowane widoki.

Semantyka = to, co pokazujesz, a nie surowe tabele.

Minus: logika często się dubluje, trudniej zarządzać zmianami.

3. Warstwa semantyczna jako hybryda

Najczęstszy sposób budowania warstwy semantycznej w realnych firmach:

  • dbt: porządkuje dane i nadaje strukturę
  • BI tool / semantic engine: definiuje miary, relacje, KPI
  • dokumentacja: opisuje sens, a nie tylko kolumny

Czy dane są kopiowane? Czasem tak (marty, agregaty), czasem nie (tylko widoki i metadane).

Ale tu nie chodzi o kopiowanie, bo chodzi o kontrolę znaczeń. Kopiowanie (agregowanie, widoki) nie jest złe.

Kluczowa prawda o dokumentacji

I teraz coś, co wszyscy olewają, a potem płaczą:

Jeśli semantyka nie jest udokumentowana, to ona nie istnieje.

Tak - bo jak nie znamy znaczeń danych (znamy = mamy spisane; bo jak ktoś ma je w głowie to nie znaczy, iż znamy - jak ten ktoś wpadnie pod tramwaj to już nie znamy).

Co dokumentować?

Dla każdej miary i wymiaru:

  • Definicję biznesową (ludzkim językiem, co to jest, do czego używamy - możesz wrzucić do GPT mądry opis i dodać “opisz to jakbyś miał tłumaczyć uczniowi średniej szkoły" - będzie dobre, serio)
  • Logikę liczenia (wzór, warunki, założenia)
  • Źródło danych (skąd pochodzą dane, które służą do wyliczenia danej miary, jak często same dane oraz miary się przeliczają)
  • Właściciela (kto decyduje o definicji?)
  • Przykłady (jak interpretować wartości, na przykładach z danych)

Narzędzia do dokumentacji

  • dbt docs - niedoceniane, ale bardzo skuteczne
  • Data-katalogi: Atlan, DataHub, Alation
  • Confluence / Notion - lepsze to niż nic
  • Looker / Power BI - opisy pól i miar wbudowane w narzędzie

Złota zasada: Dokumentacja obok kodu, a nie w PDF-ie z 2021 roku.

Przykład domenowy: budujemy e-commerce

Tu zaczynają się technikalia i mięsko w SQL (i nie tylko).

Od tego momentu będziemy pracować na jednym, spójnym przykładzie. Klasyczny e-commerce:

Mamy dane

Mamy tabelki z kolumnami:

customers (customer_id, signup_date, segment)

orders (order_id, customer_id, order_date, status)

order_items (order_id, product_id, quantity, unit_price)

deliveries (order_id, promised_date, delivery_date)

Chcemy odpowiedzieć na pytania

  • Ile sprzedaliśmy?
  • Ilu mamy aktywnych klientów?
  • Jak terminowe są dostawy?
  • Jaka jest wartość klienta?
  • Jaka jest średnia wartość koszyka?

Definiujemy najważniejsze KPI

  • Revenue - Przychód
  • Number of orders - Liczba zamówień
  • Active customers - Aktywni klienci
  • On-time delivery rate - Terminowość dostaw %
  • Average order value - Średnia wartość zamówienia

Każda miara: jedna definicja, jeden właściciel, jeden wzór.

Semantyka w SQL - pierwszy kontakt z kodem

Zanim przejdziemy do zaawansowanych narzędzi, zacznijmy od fundamentów: jak wygląda semantyka w czystym SQL?

Raw data (punkt wyjścia)

-- To NIE jest warstwa semantyczna

-- To jest materiał budowlany

orders (order_id, customer_id, order_date, status)

order_items (order_id, product_id, quantity, unit_price)

customers (customer_id, signup_date, segment)

deliveries (order_id, promised_date, delivery_date)

Staging - czyszczenie, dostosowanie typów, bez
interpretacji

CREATE VIEW stg_orders AS

SELECT

  order_id,

  customer_id,

  CAST(order_date AS DATE) AS order_date,

  UPPER(status) AS status  -- normalizacja

FROM orders;

Tu:

  • normalizujesz dane
  • nie liczysz KPI
  • nie interpretujesz biznesu

Po prostu czyścisz i poprawiasz dane.

Fakty - tu zaczyna się semantyka

CREATE VIEW fct_orders AS

SELECT

  o.order_id,

  o.customer_id,

  o.order_date,

  SUM(oi.quantity * oi.unit_price) AS revenue

FROM stg_orders o

JOIN order_items oi ON o.order_id = oi.order_id

WHERE o.status IN ('PAID', 'SHIPPED')  -- decyzja semantyczna!

GROUP BY 1, 2, 3;

Decyzja semantyczna: > Sprzedaż = tylko PAID i SHIPPED

Jedna decyzja i jedno miejsce.

Wymiary - nudne, stabilne, konieczne

CREATE VIEW dim_customers AS

SELECT

  customer_id,

  signup_date,

  segment

FROM customers;

Im nudniej, tym lepiej. Wymiary nie powinny być miejscem niespodzianek.

Terminowość dostaw - pełny przykład

Ustalamy, iż definicja jest taka (już w SQLu) - jeżeli dostarczono przed obiecanym czasem to jest “on time":

CREATE VIEW fct_deliveries AS

SELECT

  order_id,

  promised_date,

  delivery_date,

  CASE

    WHEN delivery_date <= promised_date THEN 1

    ELSE 0

  END AS is_on_time

FROM deliveries;

Teraz możesz policzyć:

SELECT

  AVG(is_on_time) AS on_time_delivery_rate

FROM fct_deliveries;

Biznes widzi: “Terminowość dostaw (%)".

Nie widzi techniki: joinów, CASE WHEN, dat i wszystkich innych “surowych" elementów. Mają swoją wartość, którą choćby można na wykresie pokazać!

Semantyka w dbt - porządna inżynieria

SQL to dobry start, ale przy większej skali potrzebujesz struktury. Tu wkracza dbt.

Architektura dbt: staging → intermediate → marts

staging/      <- czyszczenie, normalizacja

intermediate/ <- łączenia, transformacje

marts/        <- to, co wystawiasz do BI

Przykład: fct_orders w dbt

models/marts/fct_orders.sql

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

{{

  -- zmaterializuj wynik tego SQL-a jako fizyczną tabelę w bazie

  config(

    materialized='table'

  )

}}

-- weź dane z modelu stg_orders

WITH orders AS (

  SELECT * FROM {{ ref('stg_orders') }}

),

-- weź dane z stg_order_items

order_items AS (

  SELECT * FROM {{ ref('stg_order_items') }}

),

-- tu zaczyna się semantyka

-- łączymy zamówienia z pozycjami

-- liczymy revenue (quantity*unit_price),

-- liczymy liczbę unikalnych produktów na zamówienie,

-- filtrujemy tylko opłacone/wysłane (biznesowa definicja "ważnego" zamówienia)

-- GROUP BY - dostajemy revenue per zamówienie.

final AS (

  SELECT

    o.order_id,                    -- Klucz zamówienia

    o.customer_id,                 -- Klient

    o.order_date,                  -- Data (do agregacji czasowych)

    SUM(oi.quantity*oi.unit_price) AS revenue,       -- METRYKA: przychód z zamówienia

    COUNT(DISTINCT oi.product_id) AS product_count   -- METRYKA: ile unikalnych SKU w zamówieniu

  FROM orders o

  JOIN order_items oi ON o.order_id = oi.order_id   -- Standardowy join 1:N (zamówienie pozycje)

  WHERE o.status IN ('PAID', 'SHIPPED')             -- BIZNESOWY FILTR: tylko "udane" zamówienia

  GROUP BY 1, 2, 3                                  -- Grupowanie po pierwszych 3 kolumnach (skrót SQL)

)

-- zwróć wynik z final CTE. dbt to zapisze jako tabelę

SELECT * FROM final

Miary semantyczne w dbt (YAML)

models/marts/schema.yml

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

version: 2

# SEKCJA 1: MODELE (tabele/marty)

# Dokumentacja tabeli fct_orders (tej z poprzedniego SQL-a).

# dbt używa tego do generowania docs (dbt docs generate) i autocomplete w narzędziach BI.

models:

  - name: fct_orders                    # Nazwa modelu/tabeli (pasuje do {{ ref('fct_orders') }} w SQLce)

    description: "Tabela faktów zamówień z przychodem"  # Biznesowy opis (widoczny w docs/UI)

    # Kolumny z opisami - semantyczny słownik! Pomaga unikać "co to jest revenue?"

    columns:

      - name: order_id

        description: "Unikalny identyfikator zamówienia"     # Klucz (dimension)

      - name: revenue

        description: "Przychód z zamówienia (tylko PAID i SHIPPED)"  # METRYKA bazowa

# SEKCJA 2: METRYKI

# Definicja KPI "revenue" - można ją ciągnąć po wymiarach, agregować, wersjonować...

metrics:

  - name: revenue                       # Unikalna nazwa metryki (używasz w query: revenue)

    label: "Przychód"                   # Ludzka nazwa (do dashboardów, UI)

    # Źródło: tabela z poprzedniego SQL-a. ref() = dependency jak w SQL

    model: ref('fct_orders')            # "Weź dane stąd", dbt obsłuży joiny/dependency

    description: "Przychód z opłaconych i wysłanych zamówień"  # Biznesowa definicja!

    # ALGORYTM OBLICZENIA - to jest semantyka jako kod!

    calculation_method: sum              # SUMUJ po expression (inny: count, avg, median...)

    expression: revenue                  # Która kolumna? (może być złożona: revenue * 1.23)

    # CZASOWOŚĆ - do time-series (daily/weekly revenue)

    timestamp: order_date                # Kolumna do grupowania czasowego (WHERE date_trunc(...))

    # WYMIARY - po czym agregujesz (WHERE/GROUP BY)

    dimensions:

      - customer_id                      # Revenue per customer

      - order_date                       # Revenue per day

      # Możesz dodać: product_category, region, channel...

Dlaczego to jest lepsze niż czysty SQL?

  • Jedno źródło prawdy - miara zdefiniowana raz, używana wszędzie
  • Dokumentacja obok kodu - opis przy definicji
  • Testy - możesz testować semantykę, nie tylko typy
  • Wersjonowanie - Git śledzi zmiany definicji KPI plus masz za darmo cały proces weryfikacji/akceptacji pull requestów
  • Lineage - widzisz, skąd pochodzi każda miara

Terminowość dostaw - kompletny przykład w dbt

models/marts/fct_deliveries.sql

WITH deliveries AS (

  SELECT * FROM {{ ref('stg_deliveries') }}

),

final AS (

  SELECT

    order_id,

    promised_date,

    delivery_date,

    delivery_date <= promised_date AS is_on_time

  FROM deliveries

)

SELECT * FROM final

schema.yml

metrics:

  - name: on_time_delivery_rate

    label: "Terminowość dostaw (%)"

    model: ref('fct_deliveries')

    description: "Odsetek dostaw zrealizowanych w obiecany terminie"

    calculation_method: average

    expression: is_on_time

    timestamp: delivery_date

Efekt:

  • Jedna definicja terminowości
  • Używana w BI, SQL, API, raportach
  • Zmiana w jednym miejscu = zmiana wszędzie

Semantyka poza BI: Python / DuckDB / ML

Najczęstszy błąd: myślenie, iż warstwa semantyczna to “rzecz od dashboardów". To nieprawda.

Ten sam sens w Pythonie

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

import duckdb

con = duckdb.connect('warehouse.db')

# Ta sama semantyka co w BI

df = con.execute("""

  SELECT

    DATE_TRUNC('month', order_date) AS month,

    SUM(revenue) AS revenue,

    COUNT(DISTINCT customer_id) AS unique_customers

  FROM fct_orders

  GROUP BY 1

  ORDER BY 1

""").df()

print(df.head())

Feature engineering oparty o semantykę

# Zamiast ad-hocowych obliczeń w Pythonie:

# ZŁE

revenue_bad = df[df['status'].isin(['PAID', 'SHIPPED'])].groupby('customer_id')['amount'].sum()

# DOBRE - korzystamy z semantyki

revenue_good = con.execute("""

  SELECT

    customer_id,

    SUM(revenue) AS total_revenue

  FROM fct_orders

  GROUP BY customer_id

""").df()

Dlaczego to ma znaczenie?

  • Zero duplikacji logiki - ten sam wzór w BI, Pythonie, ML
  • Spójność - model ML trenuje na tych samych definicjach co raporty
  • Łatwiejsze debugowanie - jak zmienisz definicję, zmienia się wszędzie

Tu uwaga - popularne jest też pojęcie feature store szczególnie w projektach machine learingowych. Gdzie różnica?
Feature store to system skoncentrowany na cechach używanych w ML: tworzeniu, wersjonowaniu, przechowywaniu i serwowaniu cech (feature’ów) do trenowania modeli. To jest taki “hub cech" dla data scientistów i MLOps, żeby nie odkrywać Ameryki za każdym razem przy budowie modelu. No i żeby wszyscy data scientiści mieli dostęp do już odkrytych cech.

Jak te pojęcia się łączą?

Te światy się coraz bardziej przenikają - semantic layer bywa źródłem lub wręcz fundamentem feature store’a.​ Typowy sensowny układ:

  • Warstwa semantyczna daje spójny, oczyszczony model biznesowy: tabele typu customer, order, subscription, oraz metryki jak revenue_30d, orders_90d, is_active_customer.​
  • Feature store używa tego modelu do budowy feature’ów: np. customer_ltv_365d, sessions_last_7d, discount_usage_ratio_90d, bazując na definicjach z warstwy semantycznej zamiast SQLa z dupy w każdym projekcie.​

Dzięki temu metryki w dashboardach i featury w modelach mają wspólną logikę; jak zmieniasz definicję “active customer", to wiesz, co się rozsypie - zarówno w BI, jak i w ML.​

Wróćmy jednak do wykorzystania warstwy semantycznej w ML:

Przykład: predykcja churn

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

import duckdb

import pandas as pd

from sklearn.ensemble import RandomForestClassifier

con = duckdb.connect('warehouse.db')

# features z warstwy semantycznej

features = con.execute("""

  SELECT

    c.customer_id,

    COUNT(DISTINCT o.order_id) AS total_orders,

    SUM(o.revenue) AS total_revenue,

    AVG(o.revenue) AS avg_order_value,

    MAX(o.order_date) AS last_order_date,

    DATEDIFF('day', MAX(o.order_date), CURRENT_DATE) AS days_since_last_order

  FROM dim_customers c

  LEFT JOIN fct_orders o ON c.customer_id = o.customer_id

  GROUP BY c.customer_id

""").df()

# target: czy klient zrobił zamówienie w ostatnich 90 dniach?

features['is_active'] = features['days_since_last_order'] <= 90

# trenujesz model

X = features[['total_orders', 'total_revenue', 'avg_order_value', 'days_since_last_order']]

y = features['is_active']

model = RandomForestClassifier()

model.fit(X, y)

Kluczowa obserwacja:

  • Definicja revenue ta sama co w BI
  • Definicja active customer ta sama co w raportach
  • Jak zmienisz semantykę, zmienia się model

Testy warstwy semantycznej - jak nie
dopuścić, żeby KPI zaczęły kłamać

Testy w warstwie semantycznej to nie jest QA na poziomie aplikacji i nie jest sprawdzanie, czy kolumna ma typ INT.

To są testy jednego prostego pytania: > “Czy to, co liczymy, przez cały czas znaczy to samo co wczoraj?"

Dlaczego w ogóle testować semantykę?

Bo zmiany w danych psują sens, a nie schemat.

Scenariusze:

  • Ktoś zmieni statusy zamówień (PAID → COMPLETED)
  • Ktoś doda nowy kanał sprzedaży
  • Ktoś “niewinnie" zmieni join

Pipeline przejdzie. Dashboard się odświeży. A liczby zaczną opowiadać inną historię.

Trzy typy testów, które wystarczą w 90% przypadków

1. Testy sensu biznesowego

Najważniejsze, a najczęściej ignorowane.

Revenue nigdy nie może być ujemne:

SELECT *

FROM fct_orders

WHERE revenue < 0;

Nie może być zamówień bez klienta:

SELECT *

FROM fct_orders o

LEFT JOIN dim_customers c USING (customer_id)

WHERE c.customer_id IS NULL;

Status musi być znaną wartością:

SELECT DISTINCT status

FROM orders

WHERE status NOT IN ('NEW', 'PAID', 'SHIPPED', 'CANCELLED');

Jeśli te testy się wywalą:

  • Pipeline działa
  • Ale biznesowo masz błąd

2. Testy zakresów i proporcji

Czyli: “czy świat przez cały czas wygląda jak świat, a nie jak bug".

Terminowość dostaw musi być w zakresie 0-1:

SELECT *

FROM (

  SELECT AVG(is_on_time) AS rate

  FROM fct_deliveries

) t

WHERE rate < 0 OR rate > 1;

Liczba aktywnych klientów nie może spaść o 90% z dnia na dzień:

WITH today AS (

  SELECT COUNT(*) AS cnt

  FROM dim_customers

  WHERE is_active = true

),

yesterday AS (

  SELECT COUNT(*) AS cnt

  FROM dim_customers_yesterday

  WHERE is_active = true

)

SELECT *

FROM today t, yesterday y

WHERE t.cnt < y.cnt * 0.1;

Nie złapiesz wszystkich błędów ora razu, ale możesz łapać najgłupsze i najdroższe na początek (a potem kolejne).

3. Testy kontraktowe

Semantyka zakłada konkretne wartości. jeżeli się zmienią “pod spodem", chcesz o tym wiedzieć.

Czy nie pojawiły się nowe statusy zamówień?

SELECT DISTINCT status

FROM orders

WHERE status NOT IN ('NEW', 'PAID', 'SHIPPED', 'CANCELLED');

Czy struktura tabeli się nie zmieniła?

SELECT column_name

FROM information_schema.columns

WHERE table_name = 'orders'

  AND column_name NOT IN ('order_id', 'customer_id', 'order_date', 'status');

To jest test typu: “Hej, ktoś zmienił rzeczywistość, a my o tym nie wiemy".

Testy w dbt

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

# models/schema.yml

models:

  - name: fct_orders

    tests:

      - dbt_utils.expression_is_true:

          expression: "revenue >= 0"

    columns:

      - name: customer_id

        tests:

          - not_null

          - relationships:

              to: ref('dim_customers')

              field: customer_id

      - name: status

        tests:

          - accepted_values:

              values: ['NEW', 'PAID', 'SHIPPED', 'CANCELLED']

Gdzie te testy żyją?

Muszą żyć. Ale nie są po to, żeby chwalić się nimi (że mamy) na slajdach w Power Poincie albo opisywać je na Wiki czy innym Confluence tylko:

  • w dbt tests
  • w SQL-u obok modeli
  • w CI/CD (nawet prostym)

Test semantyczny, który nie jest automatyczny, jest tylko ładnym życzeniem.

Minimalny setup (bez overengineeringu)

Na start wystarczy:

  • 1-2 testy sensu na kluczową miarę
  • 1 test zakresu
  • 1 test kontraktowy

To i tak jest więcej niż ma 80% zespołów...

Najważniejsza myśl

Testy semantyczne nie chronią danych. One chronią decyzje.

Bo błąd w danych to koszt techniczny. Błąd w semantyce to zła decyzja biznesowa. A to już zupełnie inna liga strat.

Pełny przepływ techniczny: SQL → dbt → Python / DuckDB

Połączmy wszystko w jeden, kompletny przykład. Od surowych danych do gotowego systemu semantycznego.

KROK 1: Raw data (DuckDB / hurtownia)

Mamy tabelki, w nich będą dane:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

CREATE TABLE orders (

  order_id INT,

  customer_id INT,

  order_date DATE,

  status VARCHAR

);

CREATE TABLE order_items (

  order_id INT,

  product_id INT,

  quantity INT,

  unit_price DECIMAL(10,2)

);

CREATE TABLE customers (

  customer_id INT,

  signup_date DATE,

  segment VARCHAR

);

CREATE TABLE deliveries (

  order_id INT,

  promised_date DATE,

  delivery_date DATE

);

KROK 2: Staging w dbt

Normalizujemy te dane:

models/staging/stg_orders.sql

SELECT

  order_id,

  customer_id,

  CAST(order_date AS DATE) AS order_date,

  UPPER(TRIM(status)) AS status

FROM {{ source('raw', 'orders') }}

Oraz czyścimy:
models/staging/stg_order_items.sql

SELECT

  order_id,

  product_id,

  quantity,

  unit_price

FROM {{ source('raw', 'order_items') }}

WHERE quantity > 0    -- co najmniej jeden produkt

  AND unit_price > 0  -- cena raczej dodatnia...

KROK 3: Fakty semantyczne

models/marts/fct_orders.sql

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

WITH orders AS (

  SELECT * FROM {{ ref('stg_orders') }}

),

order_items AS (

  SELECT * FROM {{ ref('stg_order_items') }}

),

aggregated AS (

  SELECT

    o.order_id,

    o.customer_id,

    o.order_date,

    SUM(oi.quantity * oi.unit_price) AS revenue,

    COUNT(DISTINCT oi.product_id) AS product_count

  FROM orders o

  JOIN order_items oi ON o.order_id = oi.order_id

  WHERE o.status IN ('PAID', 'SHIPPED')

  GROUP BY 1, 2, 3

)

SELECT * FROM aggregated;

models/marts/fct_deliveries.sql

WITH deliveries AS (

  SELECT * FROM {{ ref('stg_deliveries') }}

),

with_flag AS (

  SELECT

    order_id,

    promised_date,

    delivery_date,

    delivery_date <= promised_date AS is_on_time

  FROM deliveries

  WHERE delivery_date IS NOT NULL

)

SELECT * FROM with_flag;

KROK 4: Wymiary

models/marts/dim_customers.sql

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

WITH customers AS (

  SELECT * FROM {{ ref('stg_customers') }}

),

with_activity AS (

  SELECT

    c.customer_id,

    c.signup_date,

    c.segment,

    MAX(o.order_date) AS last_order_date,

    DATEDIFF('day', MAX(o.order_date), CURRENT_DATE) <= 90 AS is_active

  FROM customers c

  LEFT JOIN {{ ref('fctContinue3:03 PM_orders') }} o ON c.customer_id = o.customer_id

GROUP BY 1, 2, 3

)

SELECT * FROM with_activity;

KROK 5: Metryki w dbt

models/marts/schema.yml

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

metrics:

  - name: revenue

    label: "Przychód"

    model: ref('fct_orders')

    description: "Przychód z opłaconych i wysłanych zamówień"

    calculation_method: sum

    expression: revenue

    timestamp: order_date

    dimensions:

      - customer_id

  - name: on_time_delivery_rate

    label: "Terminowość dostaw (%)"

    model: ref('fct_deliveries')

    description: "Odsetek dostaw zrealizowanych w obiecany terminie"

    calculation_method: average

    expression: is_on_time

    timestamp: delivery_date

  - name: active_customers

    label: "Aktywni klienci"

    model: ref('dim_customers')

    description: "Liczba klientów z zamówieniem w ostatnich 90 dniach"

    calculation_method: count_distinct

    expression: customer_id

    filters:

      - field: is_active

        operator: '='

        value: 'true'

KROK 6: Python + DuckDB - ta sama semantyka

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

import duckdb

import pandas as pd

import matplotlib.pyplot as plt

# Połączenie z DuckDB

con = duckdb.connect('warehouse.db')

# Korzystamy z tej samej semantyki co BI

monthly_revenue = con.execute("""

  SELECT

    DATE_TRUNC('month', order_date) AS month,

    SUM(revenue) AS revenue,

    COUNT(DISTINCT customer_id) AS unique_customers

  FROM fct_orders

  GROUP BY 1

  ORDER BY 1

""").df()

# Wizualizacja

plt.figure(figsize=(12, 6))

plt.plot(monthly_revenue['month'], monthly_revenue['revenue'])

plt.title('Miesięczny przychód')

plt.xlabel('Miesiąc')

plt.ylabel('Przychód (PLN)')

plt.xticks(rotation=45)

plt.tight_layout()

plt.show()

# Terminowość dostaw

delivery_stats = con.execute("""

  SELECT

    DATE_TRUNC('week', delivery_date) AS week,

    AVG(is_on_time) AS on_time_rate

  FROM fct_deliveries

  GROUP BY 1

  ORDER BY 1

""").df()

print("Terminowość dostaw (ostatnie 4 tygodnie):")

print(delivery_stats.tail(4))

KROK 7: Ta sama logika w API

Tak, choćby w API możemy (powinniśmy!) zastosować semantykę:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

from fastapi import FastAPI

import duckdb

app = FastAPI()

con = duckdb.connect('warehouse.db')

@app.get("/metrics/revenue")

def get_revenue(start_date: str, end_date: str):

    result = con.execute("""

        SELECT

            SUM(revenue) AS total_revenue,

            COUNT(DISTINCT order_id) AS order_count

        FROM fct_orders

        WHERE order_date BETWEEN ? AND ?

    """, [start_date, end_date]).fetchone()

    return {

        "total_revenue": result[0],

        "order_count": result[1]

    }

@app.get("/metrics/delivery-performance")

def get_delivery_performance():

    result = con.execute("""

        SELECT

            AVG(is_on_time) AS on_time_rate

        FROM fct_deliveries

        WHERE delivery_date >= CURRENT_DATE - INTERVAL '30 days'

    """).fetchone()

    return {"on_time_rate": result[0]}

Kluczowa obserwacja:

  • Ten sam kod w BI, Pythonie, API
  • Zmiana definicji w jednym miejscu
  • Zero duplikacji logiki

Antywzorce - czyli jak wygląda brak semantyki

Rozpoznajesz któryś z tych scenariuszy? To znak, iż potrzebujesz warstwy semantycznej.

1. Revenue_final_v7_POPRAWKA2.xlsx

Excel jako “warstwa decyzyjna". Każdy dział ma swój arkusz z “prawdziwymi danymi". Dashboard pokazuje jedno, Excel drugie, zarząd otrzymuje trzecie. Na szczęście mamy już narzędzia pracy grupowej online, ale kiedyś... “A na której wersji to zmieniłeś?"

Dlaczego to boli:

  • Brak wersjonowania oraz kolejna wersja pliku binarnego (Excel, Word) nie pozwala na porównanie zmian
  • Nie wiadomo, która wersja jest aktualna
  • Logika ukryta w formułach, które rozumie tylko ich autor

2. “Ten raport jest prawdziwy"

Masz 5 raportów ze “sprzedażą". Wszyscy twierdzą, iż ich raport jest prawdziwy. Każdy ma inną liczbę.

Dlaczego to boli:

  • Zarząd nie wie, komu wierzyć
  • Decyzje oparte na intuicji, nie danych
  • Analitycy spędzają 60% czasu w wyjaśnianiu rozbieżności

3. Każdy dashboard ma własne CTE

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

-- Dashboard A

WITH revenue AS (

  SELECT

    SUM(amount)

  FROM orders

  WHERE

    status = 'PAID'

)

-- Dashboard B  

WITH revenue AS (

  SELECT

    SUM(amount)

  FROM orders

  WHERE

    status IN ('PAID', 'SHIPPED')

)

-- Dashboard C

WITH revenue AS (

  SELECT

    SUM(amount)

  FROM orders

  WHERE

    status != 'CANCELLED'

)

Trzy różne definicje tego samego pojęcia.

Dlaczego to boli:

  • Zmiana definicji czym jest “przychód" (z przykładu wyżej) oznacza refactoring 20 dashboardów... Chyba, iż masz ich więcej - to wtedy więcej niż 20.
  • Łatwo poprawić w jednym miejscu, zapomnieć w drugim
  • Testy? Jakie testy?

4. Brak właściciela definicji

Nikt nie wie, kto decyduje, co oznacza “aktywny klient" czy “terminowa dostawa". W efekcie każdy interpretuje po swojemu. Dlaczego to boli:

  • Nie ma kogo zapytać o wątpliwości
  • Brak odpowiedzialności za jakość definicji
  • Chaos przy zmianach

5. Logika w głowach ludzi

“Paweł wie, jak liczyć marżę". Problem: Paweł jest na urlopie. Albo odszedł z firmy.

Dlaczego to boli:

  • Knowledge silosing
  • Onboarding nowych pracowników trwa wieki, a i tak nie opowiesz wszystkiego. choćby Paweł nie opowie (o ile żyje).

6. Self-service zamienia się w self-destruction

Dałeś użytkownikom dostęp do danych. Teraz każdy tworzy własne raporty z własnymi definicjami. Zarząd dostaje 10 różnych wersji tej samej metryki.

Dlaczego to boli:

  • Chaos zamiast empowerment
  • Użytkownicy tracą zaufanie do narzędzi
  • IT wraca do robienia wszystkich raportów manualnie

Checklista wdrożeniowa - od czego zacząć

Faza 1: Zrozumienie

Do zrobienia:

  • Zidentyfikuj 3-5 najważniejszych KPI w firmie
  • Sprawdź, ile różnych definicji tych KPI aktualnie istnieje
  • Znajdź przykład, gdzie rozbieżności w definicjach spowodowały problem biznesowy
  • Ustal, kto jest właścicielem każdego KPI (biznesowo, nie technicznie)

Nie rób:

  • Nie projektuj całej architektury z góry
  • Nie kupuj drogich narzędzi od razu
  • Nie próbuj standaryzować wszystkiego naraz

Faza 2: MVP

Do zrobienia:

  • Wybierz JEDNO najważniejsze KPI (np. Revenue)
  • Stwórz jedną, kanoniczną definicję tego KPI
  • Zaimplementuj w najprostszy możliwy sposób (widok SQL wystarczy)
  • Zaktualizuj 2-3 najważniejsze raporty, żeby z niej korzystały
  • Udokumentuj: co liczy, dlaczego tak, kto zdecydował

Sukces = jeden KPI, jedna prawda, zero sporów o jego wartość

Faza 3: Skalowanie

Do zrobienia:

  • Dodaj kolejne 3-5 KPI
  • Wprowadź proces zmiany definicji (review, dokumentacja, komunikacja)
  • Dodaj podstawowe testy (sensu biznesowego, zakresów)
  • Przenieś logikę do dbt lub podobnego narzędzia
  • Przeprowadź szkolenie dla analityków

Mierniki sukcesu:

  • Czas na wyjaśnienie rozbieżności między raportami: <30% poprzedniego
  • Liczba wersji każdego KPI: dokładnie 1. Wszystkie udokumentowane w jednym miejscu!
  • Zaufanie zarządu do dashboardów: rośnie. A zarząd lubi jak rośnie (najlepsze są te wykresy co mają linię w prawo
    i do góry)

Faza 4: Dojrzałość

Do zrobienia:

  • Pokrycie testami wszystkich kluczowych miar
  • Data catalog z opisami wszystkich definicji
  • Self-service BI oparte o warstwę semantyczną
  • Metryki jakości samej warstwy semantycznej
  • Regularne review definicji (np. kwartalnie, najlepiej z właścicielami KPI)

Czego NIE robić

Overengineering na starcie:

  • Nie buduj hurtowni danych, żeby zacząć z semantyką
  • Nie twórz 50 miar “na zapas"
  • Nie wprowadzaj skomplikowanych procesów governance

Ignorowanie ludzi:

  • Nie narzucaj definicji bez konsultacji z biznesem
  • Nie zapomnij o szkoleniach
  • Nie traktuj tego jako “projektu IT" - to jest ciągła zmiana i ulepszanie produktu

Brak dokumentacji:

  • Nie zostawiaj definicji tylko w kodzie
  • Nie dokumentuj “kiedyś później"
  • Nie zakładaj, iż “to oczywiste" (pamiętasz, iż kwadrans temu padały słowa “jakbyś miał tłumaczyć uczniowi
    średniej szkoły"? przez cały czas obowiązują)

Red flags - kiedy przestać i przemyśleć

  • Jeśli implementacja trwa ponad 3 miesiące bez żadnej wartości
  • Jeśli nikt poza Tobą nie rozumie, co robisz
  • Jeśli biznes nie widzi różnicy
  • Jeśli dodajesz kolejną warstwę pośrednią zamiast upraszczać

Finałowa myśl

Jeśli:

  • każdy raport liczy coś “po swojemu"
  • BI jest szybkie, ale nikt mu nie ufa
  • zarząd pyta “która liczba jest prawdziwa?"
  • analitycy spędzają więcej czasu w wyjaśnianiu rozbieżności niż na analizie

...to problemem nie są dane. Problemem jest brak warstwy semantycznej.

Warstwa semantyczna nie służy do przechowywania danych. Ona służy do przechowywania sensu.

To nie jest luksus ani moda. To moment, w którym dane przestają być techniczne, a zaczynają być narzędziem podejmowania decyzji.

Jeśli dane mają wspierać decyzje, a nie wieczne dyskusje “dlaczego liczby się nie zgadzają", to prędzej czy później i tak do tego dojdziesz. Pytanie tylko: czy świadomie - czy po trzecim kryzysie na zarządzie?

I nie, nie da się tego naprawić jednym dashboardem final_v7_POPRAWKA2_z_rana2_final.

Idź do oryginalnego materiału