Změna kontextu řádku na kontext filtru v jazyku DAX

Úvodní obrázek

Změna kontextu řádku na kontext filtru je proces, který zneplatní všechny aktivní kontexty řádku a vytvoří nový kontext filtru, který bude obsahovat aktuální hodnoty z každého sloupce v původním kontextu řádku. Znalost principu kontextu řádku a znalost principu kontextu filtru je tedy základním předpokladem pro porozumění procesu změny kontextu.

K tomuto tématu je k dispozici také video:

Ke změně kontextu dochází při vyhodnocení funkcí CALCULATE() a CALCULATETABLE() v kontextu řádku. Ke změně kontextu řádku na kontext filtru ale dojde také při vyhodnocení měřítek v kontextu řádku, protože každé měřítko je na pozadí vyhodnoceno právě ve funkci CALCULATE(), i když ta nemusí být přímo v definici měřítka použita. Ke změně kontextu řádku na kontext filtru dochází i při vyhodnocení jiných funkcí, a to funkcí které interně používají v některém z argumentů funkce CALCULATE() a CALCULATETABLE(). Typickým příkladem mohou být Time intelligence funkce, ve kterých se v prvním argumentu obvykle používá odkaz na sloupec z datumové tabulky. Tento zápis s odkazem na sloupec z datumové tabulky je ale pouze syntaktická zkratka, a na pozadí je použitý sloupec vyhodnocen ve funkci DISTINCT() a ta je dále vyhodnocena ve funkci CALCULATETABLE(), takže i zde může docházet ke změně kontextu řádku na kontext filtru.

Funkce CALCULATE a změna kontextu

Jak již jsme si řekli, změna kontextu probíhá před vyhodnocením výrazu (prvního argumentu) ve funkci CALCULATE() a CALCULATETABLE(). Pro zjednodušení budeme v tomto příspěvku pracovat pouze s funkcí CALCULATE(). Všechny uvedené principy nicméně platí také pro funkci CALCULATETABLE() s tím rozdílem, že funkce CALCULATE() vrací skalární hodnotu, zatímco funkce CALCULATETABLE() vrací tabulku.

Při změně kontextu dochází k zneplatnění všech kontextů řádků, ve kterých je funkce CALCULATE() vyhodnocena. Následně je funkcí CALCULATE() vytvořen nový kontext filtru. Tento nový kontext filtru obsahuje všechny sloupce a jejich aktuálních hodnoty, které v aktuálním kroku iterace představovaly původní kontext řádku.

Velmi důležité je si uvědomit, že kontext řádku obsahuje vždy pouze jeden řádek, ale nový kontext filtru může po jeho aplikaci filtrovat více řádků tabulky, pokud tyto řádky mají ve všech sloupcích stejné hodnoty.

Dalším důležitým faktem je že kontext řádku iteruje tabulku, zatímco kontext filtru filtruje model. Kontext řádku tedy není propagován přes relace, zatímco kontext filtru ano.

Pokud při vyvolání změny kontextu existuje více kontextů řádku, například díky vnořeným iteračním funkcím, nový kontext filtru je vytvořen ze všech aktivních kontextů řádků.

Změna kontextu v počítaných sloupcích

Proces změny kontextu řádku na kontext filtru si můžeme jednoduše znázornit při vytvoření nového počítaného sloupce, protože výraz vyhodnocený v rámci definice počítaného sloupce je automaticky vyhodnocen v kontextu řádku.

V použitém Power BI souboru, který je k dispozici ke stažení níže pod tímto příspěvkem, je mimo jiné tabulka 'Produkty a prodeje', která obsahuje pouze čtyři sloupce.

Změna kontextu řádku na kontext filtru v jazyku DAX

Do této tabulky můžeme přidat nový počítaný sloupec, který má následující definici.

Počítaný sloupec:

Prodeje celkem = SUM('Produkty a prodeje'[Prodeje])

Výsledky funkce SUM() v kontextu řádku, který je tvořen automaticky v počítaných sloupcích, mohou být překvapivé.

Změna kontextu řádku na kontext filtru v jazyku DAX 2

Nový počítaný sloupec 'Produkty a prodeje'[Prodeje celkem] obsahuje v každém řádku tabulky stejnou hodnotu, která odpovídá součtu všech hodnot ze sloupce 'Produkty a prodeje'[Prodeje]. Funkce SUM(), stejně jako kterákoliv jiná sumarizační funkce, ignoruje kontext řádku, a naopak respektuje kontext filtru. Protože při vytvoření počítaného sloupce je aktivní pouze kontext řádku, a není k dispozici žádný kontext filtru, funkce SUM() jednoduše sečte všechny dostupné hodnoty ze sloupce 'Produkty a prodeje'[Prodeje] v každém řádku tabulky.

Situace se ale změní pokud vložíme funkci SUM() do funkce CALCULATE().

Počítaný sloupec:

Prodeje (2) = CALCULATE(SUM('Produkty a prodeje'[Prodeje]))

Výsledkem nového počítaného sloupce je nyní jiná hodnota v každém řádku tabulky.

Změna kontextu řádku na kontext filtru v jazyku DAX 3

Důvodem proč nová verze počítaného sloupce vrací jiné výsledky než původní výpočet je funkce CALCULATE(). Funkce CALCULATE() totiž mimo jiné změní před vyhodnocením prvního argumentu této funkce všechny aktivní kontexty řádku na kontext filtru. Při změně kontextu řádku na kontext filtru dojde nejdříve k zneplatnění všech kontextů řádku, a následně jsou aktuální hodnoty z každého sloupce v původním kontextu řádku přidány do kontextu filtru. Uvažujme například první řádek tabulky zobrazené na obrázku výše. V tomto řádku je výpočet ve sloupci 'Produkty a prodeje'[Prodeje (2)] vyhodnocen následujícím způsobem.

Počítaný sloupec:

CALCULATE
(
    SUM('Produkty a prodeje'[Prodeje]),
    'Produkty a prodeje'[Product] = "Sport-100 Helmet, Black",
    'Produkty a prodeje'[Color] = "Black",
    'Produkty a prodeje'[Category] = "Accessories",
    'Produkty a prodeje'[Prodeje] = 1608695189,
    'Produkty a prodeje'[Prodeje celkem] = 790074581535
)

Obdobným způsobem si můžeme představit vyhodnocení výpočtu ve sloupci 'Produkty a prodeje'[Prodeje 2] v každém řádku tabulky. Vždy dojde před vyhodnocením funkce SUM() v prvním argumentu funkce CALCULATE() k deaktivaci aktuálního kontextu řádku, a k přidání nového kontextu filtru s aktuálními hodnotami z každého sloupce.

Ačkoliv se na první pohled může zdát, že změnou kontextu řádku na kontext filtru dosáhneme vždy stejného výsledku, v našem příkladu aktuální hodnotu ze sloupce 'Produkty a prodeje'[Prodeje], nemusí to být vždy pravda. V kontextu řádku totiž vždy přistupujeme pouze k aktuální hodnotě v aktuálním řádku iterace. Nový kontext filtru ale může filtrovat více řádků tabulky. Tato situace může nastat v případě, kdy všem hodnotám v novém kontextu filtru vytvořeném změnou kontextu řádku na kontext filtru odpovídá více řádků v tabulce. To je i případ naší tabulky 'Produkty a prodeje'.

Změna kontextu řádku na kontext filtru v jazyku DAX 4

Pokud tabulku 'Produkty a prodeje' seřadíme vzestupně podle sloupce 'Produkty a prodeje'[Prodeje], můžeme vidět že první dva řádky tabulky jsou duplicitní. Výsledkem počítaného sloupce 'Produkty a prodeje'[Prodeje (2)] u těchto dvou záznamů není stejná hodnota jako ve sloupci 'Produkty a prodeje'[Prodeje], ale součet všech hodnot ze sloupce 'Produkty a prodeje'[Prodeje], kterým odpovídá nový kontext filtru vytvořený funkcí CALCULATE().

Jak již jsme si řekli, při vyhodnocení počítaného sloupce 'Produkty a prodeje'[Prodeje (2)] dojde před vyhodnocením funkce SUM() v prvním argumentu funkce CALCULATE() ke změně kontextu řádku na kontext filtru. Nový kontext filtru v prvním řádku tabulky zobrazené na obrázku výše opět obsahuje všechny hodnoty ze všech sloupců v daném řádku. Tyto hodnoty jsou pak použity jako filtr před vyhodnocením funkce SUM(). Pokud ale zafiltrujeme celou tabulku 'Produkty a prodeje' hodnotami z prvního řádku, zafiltrovaná tabulka bude obsahovat dva řádky, protože těmto filtrům odpovídají hodnoty ze dvou řádků. Funkce SUM() následně sečte hodnoty ze dvou řádků a v každém z duplicitních řádků proto vidíme ve sloupci 'Produkty a prodeje'[Prodeje (2)] dvojnásobnou částku prodejů.

Tato situace sice není úplně častá. V počítaných sloupcích se ji můžeme snadno vyhnout pokud tabulka obsahuje jednoznačný identifikátor řádku. Ve výpočtech v měřítku, kde také dochází v iteračních funkcích ke změně kontextu, používáme v prvním argumentu iteračních funkcí obvykle funkce jako VALUES() nebo SUMMARIZE(), které nevrací duplicitní řádky.  I přesto je ale vždy důležité myslet na to, že kontext řádku je něco jiného než kontext filtru. V kontextu řádku získáme vždy aktuální hodnotu z aktuálního řádku tabulky v rámci iterace. Na druhou stranu, kontext filtru filtruje model.

Měřítka a změna kontextu

V dalších příkladech se již přesuneme ke klasické databázi Adventure Works DW 2020.pbix. V této databázi často pracujeme s měřítkem [Prodeje], které má následující definici.

Měřítko:

Prodeje = SUM(Sales[Sales Amount])

Každé měřítko, které je vyhodnoceno v kontextu řádku také vyvolá změnu kontextu řádku na kontext filtru. To je dáno tím že měřítka jsou vždy na pozadí obaleny funkcí CALCULATE(), bez ohledu na to zda funkci CALCULATE() použijeme v definici měřítka nebo ne. 

Pokud tedy použijeme v definici měřítka funkci CALCULATE(), tak je celý výpočet na pozadí při jeho vyhodnocení obalen do další funkce CALUCLATE(). Pokud funkci CALCULATE() nepoužijeme, tak je celý výpočet také automaticky vyhodnocen ve funkci CALUCLATE(). Měřítko [Prodeje] je tedy ve skutečnosti vyhodnoceno následujícím způsobem.

Měřítko:

Prodeje =
CALCULATE
(
    SUM(Sales[Sales Amount])
)

Tento efekt si opět můžeme znázornit v počítaném sloupci, kde je aktivní pouze kontext řádku. Pokud bychom chtěli mít u každého produktu v tabulce 'Product' sumu za prodeje, můžeme použít následující definici počítaného sloupce.

  Počítaný sloupec:

Prodeje produktu = [Prodeje]

Výsledkem bude nový počítaný sloupec s částkou prodejů u každého produktu, právě díky změně kontextu řádku na kontext filtru, ke které dochází u všech měřítek automaticky, protože každé měřítko je na pozadí vyhodnoceno ve funkci CALCULATE().

Změna kontextu řádku na kontext filtru v jazyku DAX 5

K předchozímu příkladu je dobré poznamenat ještě jednu věc. Měřítko [Prodeje] sčítá hodnoty z jednoho konkrétního sloupce z tabulky 'Sales'. Nový kontext filtru vytvořený změnou kontextu řádku na kontext filtru ale obsahuje pouze filtry nastavené na sloupce z tabulky 'Product'. Z toho tedy vyplývá, že nový kontext filtru vytvořený změnou kontextu řádku na kontext filtru je kontext filtru jako kterýkoliv jiný, a je proto propagován pomocí relací do celého modelu, pokud to směr filtrace mezi tabulkami umožňuje, jako je tomu u relace mezi tabulkami 'Product' a 'Sales'.

Změna kontextu řádku na kontext filtru v jazyku DAX 6

Efekt změny kontextu řádku na kontext filtru můžeme také programově odstranit nebo upravit, jak si ukážeme na následujícím příkladu.

Změna kontextu a modifikátory filtrů ve funkci CALCULATE

Ke změně kontextu řádku na kontext filtru dochází ve funkci CALCULATE() před aplikováním modifikátorů filtrů a před aplikováním explicitních filtrů. Mezi modifikátory filtrů řadíme všechny ALL* funkce a dále funkci REMOVEFILTERS(). Díky tomuto pevnému pořadí vyhodnocení filtrů ve funkci CALCULATE() můžeme úplně nebo částečně odstraňovat filtry vzniklé změnou kontextu řádku na kontext filtru, a nebo je můžeme přepisovat pomocí explicitních filtrů. To si můžeme znázornit na jednoduchém příkladu. Uvažujme například situaci, kdy bychom v tabulce 'Product' chtěli v každém řádku získat procentuální podíl prodejů aktuálního produktu vůči prodejům všech produktů v kategorii, do které aktuální produkt patří. Jak získat prodeje za aktuální produkt už víme, stačí použít měřítko [Prodeje] v definici počítaného sloupce.  Pokud bychom ale chtěli získat prodeje za aktuální kategorii, do které daný produkt patří, musíme před vyhodnocením měřítka [Prodeje] odstranit všechny filtry vzniklé změnou kontextu řádku na kontext filtru, kromě filtru nastaveného na sloupec s kategoriemi produktů.

Počítaný sloupec:

Prodeje v kategorii =
CALCULATE
(
    [Prodeje],
    ALLEXCEPT('Product', 'Product'[Category])
)

Funkce CALCULATE() nyní nejdříve změní kontext řádku na kontext filtru, který bude v každém řádu tabulky 'Product' obsahovat aktuální hodnoty z každého sloupce. Následně funkce ALLEXEPT() odstraní nově vzniklé filtry ze všech sloupců kromě sloupce s kategoriemi produktů. Výsledný kontext filtru, který je aktivní před vyhodnocením prvního argumentu funkce CALCULATE() proto obsahuje pouze aktuální kategorii, do které produkt v aktuálním řádku tabulky patří.

Změna kontextu řádku na kontext filtru v jazyku DAX 7

Procentuální podíl prodejů aktuálního produktu vůči prodejům za všechny produkty v dané kategorii pak můžeme získat prostým vydělením, například následujícím způsobem.

Měřítko:

% Podíl v kategorii =
DIVIDE
(
    [Prodeje],
    CALCULATE
    (
        [Prodeje],
        ALLEXCEPT('Product', 'Product'[Category])
    )
)

Kontext řádku je aktivní také při vyhodnocení výrazů uvnitř iteračních funkcí. Na dalším příkladu si proto ukážeme efekt změny kontextu řádku na kontext filtru v iterační funkci použité v měřítku.

Změna kontextu řádku na kontext filtru v iteračních funkcích

V dalším příkladu budeme chtít získat hodnotu prodejů, které uskutečnil nejlepší zákazník v aktuálním roce. Začneme přípravou vizuálu, kde si vložíme do řádku roky z kalendářní tabulky a do hodnot měřítko [Prodeje].

Změna kontextu řádku na kontext filtru v jazyku DAX 8

Pro získání hodnoty prodejů za nejlepšího zákazníka v aktuálním roce budeme muset vyhodnotit sumu za prodeje produktů pro každého zákazníka, a následně získat ze všech těchto hodnot tu nejvyšší. První pokus by mohl vypadat například následovně.

Měřítko:

Prodeje (nejlepší zákazník špatně) =
MAXX
(
    Customer,
    SUM(Sales[Sales Amount])
)

Pokud nové měřítko vložíme do dříve připraveného vizuálu, zjistíme že vrací stejné výsledky jako měřítko [Prodeje].

Změna kontextu řádku na kontext filtru v jazyku DAX 9

Při vyhodnocení měřítka [Prodeje (nejlepší zákazník špatně)] působí na měřítko kontext filtru, který je dán aktuálním rokem v každém řádku vizuálu. Uvažujme například řádek s rokem 2020. 

V iteračních funkcích, mezi které patří také funkce MAXX(), dojde nejdříve k vyhodnocení prvního argumentu, který v našem výpočtu obsahuje tabulku 'Customer'. Mezi tabulkou 'Date' a tabulkou 'Customer' nedojde k propagaci filtrů, a proto je výsledkem prvního argumentu funkce MAXX() celá tabulka 'Customer'. Následně je pro každý řádek této tabulky vyhodnocena funkce SUM(), a to v kontextu řádku aktuálního zákazníka a v kontextu filtru roku 2020. Jak už jsme si řekli u prvního příkladu v tomto příspěvku, sumarizační funkce ignorují kontext řádku. Proto je funkce SUM() v každém řádku tabulky 'Customer' vyhodnocena pouze v kontextu filtru roku 2020, a v každém řádku vrací stejnou hodnotu, a to prodeje za rok 2020. Funkce MAXX() na závěr ze všech těchto hodnot vybere tu nejvyšší, což je opět suma za prodeje v roce 2020, a proto vrací měřítko [Prodeje] i měřítko [Prodeje (nejlepší zákazník špatně)] stejné hodnoty.

Abychom získali sumu za prodeje produktů pro každého zákazníka, musíme vyvolat změnu kontextu řádku na kontext filtru, a to například následujícím způsobem.

Měřítko:

Prodeje (nejlepší zákazník) =
MAXX
(
    Customer,
    [Prodeje]
)

Díky použití měřítka ve druhém argumentu funkce MAXX() již získáme v každém řádku tabulky 'Customer' prodeje v aktuálním roce a nově také prodeje za aktuálního zákazníka, protože každé měřítko vyvolá změnu kontextu řádku na kontext filtru. Stejného efektu bychom dosáhli vložením funkce SUM() do funkce CALCULATE().

Změna kontextu řádku na kontext filtru v jazyku DAX 10

Měřítko [Prodeje (nejlepší zákazník)] nyní vrací sumu za prodeje nejlepšího zákazníka v aktuálním kontextu vyhodnocení.

Shrnutí

Kontext řádku, kontext filtru a proces změny kontextu řádku na kontext filtru jsou zcela zásadní principy, které je třeba znát pro efektivní práci s jazykem DAX.

Ke změně kontextu řádku na kontext filtru dochází při vyhodnocení funkcí CALCULATE() a CALCULATETABLE(). Protože je každé měřítko na pozadí obalené skrytě do funkce CALCULATE(), dochází ke změně kontextu také při vyhodnocení měřítek, pokud jsou vyhodnoceny v kontextu řádku.

Mezi kontextem řádku a kontextem filtru je zásadní rozdíl. Kontextu řádku odpovídá vždy jeden jediný konkrétní řádek. Po změně kontextu řádku na kontext filtru může ve specifických situacích dojít k tomu, že po aplikaci nového kontextu filtru bude filtrovaná tabulka obsahovat více řádků. Se změnou kontextu souvisí také agregační funkce, které ignorují kontext řádku, ale kontext filtru reflektují.

č. 16

Komentáře