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.

