<jupyter_start><jupyter_text>EDA - Nulové hodnoty a odlehlá pozorování Dnes se společně podíváme, jak to vypadá, když data nejsou tak úplně v pořádku. K analýze použijeme data o filmech z let 2016 až 2018 a jejich hodnocení z [Internet movie database](https://www.imdb.com/).> V datech máme opět náhodně upravené některé hodnoty, aby příklady hezky vycházely.Data jsou k dispozici v následujících souborech:* [movies_2016.tsv](static/movies_2016.tsv)* [movies_2017.tsv](static/movies_2017.tsv)* [movies_2018.tsv](static/movies_2018.tsv)* [ratings.tsv](static/ratings.tsv)<jupyter_code>import pandas as pd

%matplotlib inline<jupyter_output><empty_output><jupyter_text>Načtení, kontrola a příprava dat Data jsou distribuována ve vice souborech a tak je budeme muset načíst samostatně a pak z nich vytvořit jednu datovou sadu k dalšímu zpracování. To se stává nejčastěji u historických dat, kterých může být velké množství a často by bylo zbytečné stahovat celou historii. V této podobě si může analytik vybrat časové období, které ho zajímá nebo jen část dat, kterou potřebuje.Začneme daty o filmech z roku 2016.<jupyter_code>movies_2016 = pd.read_csv("static/movies_2016.tsv")
movies_2016<jupyter_output><empty_output><jupyter_text>Koncovka `tsv` naznačuje, že budeme potřebovat jiný způsob načtení. Zatímco u CSV je oddělovačem hodnot čárka, TSV používá tabulátor. Je to naštěstí také známý formát a tak jej umí pandas snadno zpracovat.<jupyter_code>movies_2016 = pd.read_table("static/movies_2016.tsv")
movies_2016<jupyter_output><empty_output><jupyter_text>Stejným způsobem si načteme i další soubory.<jupyter_code>movies_2017 = pd.read_table("static/movies_2017.tsv")
movies_2017
movies_2018 = pd.read_table("static/movies_2018.tsv")
movies_2018<jupyter_output><empty_output><jupyter_text>V poslední tabulce se nachází hodnocení filmů.<jupyter_code>ratings = pd.read_table("static/ratings.tsv")
ratings<jupyter_output><empty_output><jupyter_text>Spojení v jednu datovou sadu (do délky)Nejprve budeme potřebovat spojit primární data o filmech do jedné datové sady. Naštěstí pro nás mají všechny zdroje stejné sloupce, což také nebývá pravidlem, především když se v čase mění způsob zpracování a publikace dat.Náš příklad je však jednoduchý a tak i spojení více zdrojů do jednoho nevyžaduje žádné mezikroky.<jupyter_code>movies = pd.concat((movies_2016, movies_2017, movies_2018))
movies<jupyter_output><empty_output><jupyter_text>`concat` umožňuje provádět i složitější magii a spojovat různé zdroje do jedné datové sady na základě různých kritérií. Složitost je nejčastěji závislá na společných prvcích jednotlivých zdrojů, jako jsou indexy či názvy sloupců. V základním nastavení spojuje zdroje do délky a protože náš index je automatický a nemá pro nás význam, je ignorován a ve výsledku přepočítán. Spojení v jednu datovou sadu (do šířky)Nadešel čas k existující datové sadě připojit informace o hodnocení jednotlivých filmů. Každý film má jednoznačný identifikátor ve sloupci tconst, který je obsažen i v datech o hodnocení, takže bude snadné identifikovat, které hodnocení patří ke kterému filmu. Je tu ovšem jedna potíž, na které závisí naše další počínání.<jupyter_code>movies.shape
ratings.shape<jupyter_output><empty_output><jupyter_text>Zatímco hlavní sada obsahuje základní informace o více než 50 000 filmech, informaci o hodnocení máme jen pro cca 26 000 z nich. Teď se musíme rozhodnout pro jednu ze čtyř možných strategií spojování, která ovlivní výsledek. Kdo někdy spojoval tabulky v SQL dotazech, bude mít hned jasno.Grafická reprezentace zmíněných čtyř možností pomocí Vénnových diagramů vypadá následovně:![joins](static/joins.svg)*Obrázek adaptován z [Wikimedia](https://upload.wikimedia.org/wikipedia/commons/9/9d/SQL_Joins.svg) (autor: <NAME>)* InnerInner vloží do výsledku jen ty řádky, které mají zastoupení v obou zdrojových tabulkách. V našem případě to znamená, že by ve výsledku byly jen ty filmy, pro které existuje hodnocení. Jinak řečeno se nad sloupcem pro identifikaci řádků, které k sobě patří, provede průnik a teprve pak dojde ke spojení.<jupyter_code>pd.merge(movies, ratings, how="inner", left_on="tconst", right_on="tconst")<jupyter_output><empty_output><jupyter_text>Parametry `left_on` a `right_on` říkáme, které sloupce slouží pro identifikaci k sobě patřících záznamů v *levé* a *pravé* tabulce. Použít pro to lze samozřejmě i index v kterékoli z nich.Na množství záznamů ve výsledku je vidět, že touto strategií spojení jsme přišli o několik hodnocení, ke kterým se nepodařilo najít film, a spoustu filmů, pro které se nepodařilo najít hodnocení. OuterOuter způsobí, že se zdroje spojí dohromady v plné velikosti, protože se nad sloupcem pro identifikaci provede nejdříve sjednocení. Pokud jedna z tabulek nebude mít ve druhé řádek, se kterým se bude moci spojit, budou hodnoty nahrazeny nulovými hodnotami.<jupyter_code>pd.merge(movies, ratings, how="outer", left_on="tconst", right_on="tconst")<jupyter_output><empty_output><jupyter_text>V tomto případě nepřijdeme o žádné informace o filmech, ale u spousty z nich nebudeme mít informaci o hodnocení a u několika hodnocení zase budou chybět základní informace o filmech. Left a RightStrategie left a right berou jako základ pro spojení levou/pravou tabulku, která tedy zůstane v původní podobě. Druhá tabulka do dvojice se použije jen pro doplnění informací tam, kde je to možné.<jupyter_code>pd.merge(movies, ratings, how="left", left_on="tconst", right_on="tconst")<jupyter_output><empty_output><jupyter_text>Tady máme k dispozici všechny filmy a doplněné hodnocení tam, kde to bylo možné.<jupyter_code>pd.merge(movies, ratings, how="right", left_on="tconst", right_on="tconst")<jupyter_output><empty_output><jupyter_text>Tady máme naopak všechna hodnocení a informace o filmu jen tam, kde je bylo možné dohledat.Finální strategie vždy záleží na tom, která data potřebujeme a která si můžeme dovolit vypustit. Více si toto rozebereme v dalších kapitolách. Pro tuto chvíli pro nás budou důležitější informace o filmech a tak zůstaneme u strategie, která použije filmy jako základ pro spojení.<jupyter_code>data = pd.merge(movies, ratings, how="left", left_on="tconst", right_on="tconst")<jupyter_output><empty_output><jupyter_text>Nulové hodnoty Nulové (neboli chybějící) hodnoty jsou snadno identifikovatelné a znamenají v podstatě chybějící data v datasetu. Nejčastěji jsou označeny jako `NaN`, což je ekvivalent pythoního `None` a může být použit i přímo z knihovny numpy.<jupyter_code>import numpy as np

np.nan<jupyter_output><empty_output><jupyter_text>Pandas identifikuje chybějící hodnoty automaticky, pokud je buňka ve zdrojovém souboru zcela prázdná. Pokud zdrojová data obsahují nějakou jinou reprezentaci (např.: `N/A`, `\\N` a podobně). je potřeba je na `np.nan` nejdříve převézt. Po převodu se nám bude s daty mnohem lépe pracovat.Informace o chybějících hodnotách lze vyčíst z informací o datech, deskriptivních statistikách a pak také přímo jejich součtem napříč daty.<jupyter_code>data.info()
data.describe()
data.isnull().sum()<jupyter_output><empty_output><jupyter_text>Poslední možnost je sice nejdelší na psaní, ale výsledek je nejlépe vidět a nevyžaduje odečítání od celkového počtu záznamů. Pro práci s nulovými hodnotami neexistuje jedna správná a univerzální cesta. V podstatě si musíme velmi uváženě vybrat jednu ze dvou možností:1. Mazání2. Doplnění MazáníMazání je velmi jednoduché. Smazat můžeme celé sloupce nebo řádky, ale přijdeme tím i o potencionálně užitečná data a snížíme tím jejich reprezentativnost.V našem případě můžeme smazat sloupec endYear, který neobsahuje ani jednu hodnotu a tak jeho smazáním o nic nepřijdeme.<jupyter_code>data = data.drop(columns=["endYear"])<jupyter_output><empty_output><jupyter_text>Pro mazání související přímo s chybějícími hodnotami máme k dispozici metodu `dropna`, která umí mazat řádky či sloupce obsahující alespoň jednu chybějící hodnotu nebo zcela naplněné chybějícími hodnotami. Nic dalšího mazat nebudeme, ale výsledek jednotlivých strategií si přes to ukážeme.<jupyter_code>data.shape<jupyter_output><empty_output><jupyter_text>Aktuálně máme v datech 53096 záznamů. Takto by to vypadalo, pokud bychom nechali smazat všechny řádky, které obsahují alespoň jednu chybějící hodnotu.<jupyter_code>data.dropna(how="any")<jupyter_output><empty_output><jupyter_text>A takto, pokud bychom smazali řádky, které mají pouze chybějící hodnoty:<jupyter_code>data.dropna(how="all")<jupyter_output><empty_output><jupyter_text>Z počtu zbylých řádků je jasné, že se žádný nesmazal a každý řádek tedy obsahuje alespoň nějakou hodnotu.Mazání by mohlo způsobit ztrátu potencionálně užitečných dat. Smazáním filmů bez hodnocení bychom přišli o polovinu filmů, což by pro analýzu některých jejich vlastností mohlo být nežádoucí. DoplněníBez mazání musíme v dalších krocích buď počítat s tím, že máme v datech chybějící hodnoty, nebo je do dat nějak doplnit.Doplnit data lze mnoha způsoby a záleží hlavně na proměnné, kterou budeme doplňovat. Je třeba mít také na paměti, že doplněná data nejsou reálná a měla by plnit jen pomocnou funkci. Možnosti jsou následující:* Nahradit chybějící hodnoty průměrem, mediánem či modem (nejčastěji se vyskytující hodnotou). Dopočtená hodnota přitom může vycházet z celého zbytku datasetu nebo jen několika řádků okolo chybějící hodnoty (KNN - K Nearest Neighbors).* Pokud se jedná o data sbíraná v čase, můžeme chybějící hodnotu nahradit poslední hodnotou naměřenou před tou chybějící nebo tou první následující po chybějící hodnotě.* Pokud je v datech viditelný trend, můžeme zkusit chybějící data dopočítat např. pomocí lineární regrese, o které si budeme povídat v lekcích o machine learningu.U více než 12 000 filmů nám chybí informace o jejich stopáži. Pojďme zkusit najít nejvhodnější cestu k doplnění.<jupyter_code>data.runtimeMinutes.hist(bins=30);<jupyter_output><empty_output><jupyter_text>Histogram trpí tím, že v datech máme i film dlouhý 36 hodin. Jak na to zareagují popisné statistiky?<jupyter_code>data.runtimeMinutes.describe()<jupyter_output><empty_output><jupyter_text>75 % filmů je kratších než 100 minut a medián s průměrem příliš nereflektují extrémní hodnoty, takže bychom jednu z těchto hodnot mohli použít. Pro úplnost se ještě podíváme, která hodnota se vyskytuje v datech úplně nejčastěji.<jupyter_code>data.runtimeMinutes.mode()
data.runtimeMinutes.value_counts()<jupyter_output><empty_output><jupyter_text>Vzhledem k malým rozdílům moc nesejde na tom, zda použijeme mean, medián nebo mode. Zkusme tedy medián.<jupyter_code>data.runtimeMinutes = data.runtimeMinutes.fillna(data.runtimeMinutes.median())<jupyter_output><empty_output><jupyter_text>Nový sloupec s doplněnými chybějícími hodnotami uložíme zpět do původních dat a máme hotovo.<jupyter_code>data.isnull().sum()<jupyter_output><empty_output><jupyter_text>U kategoriální proměnné genres nemáme tolik možností jako u numerických proměnných a navíc tento sloupec obsahuje různé kombinace žánrů. V neposlední řadě se s chybějícími hodnotami nepracuje u textových sloupců tak snadno.<jupyter_code>data.genres.value_counts()[:]<jupyter_output><empty_output><jupyter_text>Data obsahují celkem 814 unikátních kombinací žánrů a více než třetina filmů jsou dokumenty. Doplnit takovou proměnnou o nejčastější hodnotu by asi nebyl nejlepší nápad a tak budeme předpokládat, že chybějící hodnota znamená, že se film zkrátka nepodařilo zařadit do žádné z připravených škatulek.<jupyter_code>data[data.genres.isnull()].shape
data[(data.genres.isnull()) & (data.averageRating.isnull())].shape<jupyter_output><empty_output><jupyter_text>Navíc je vidět, že jen okolo 200 filmů z těch s chybějícím žánrem má nějaké hodnocení, což podporuje pocit, že se nejedná o běžné snímky. Nicméně je to stále jen pocit.Chybějící hodnoty ve sloupci s originálním názvem nepůjde doplnit tak snadno a doplňovat hodnocení filmů jakýmkoli způsobem také není dobrý nápad, protože by to mohlo ovlivnit výsledky případné navazující analýzy.Jak vidno, mazání či doplnění hodnot je vždy rizikové a takovému kroku by mělo předcházet dobré zvážení a zdůvodnění. Je také více než rozumné si na konci analýz sesumírovat své výsledky a zkusit odhadnout, jaký vliv na ně mohla mít strategie nakládání s chybějícími hodnotami.  Odlehlá měřeníChybějící hodnoty na nás v ideálním případě vyskočí hned po načtení dat, případně je objevíme záhy při pohledu na jednotlivé proměnné. S odlehlými měřeními je to složitější, protože na ně v tom nejhorším případě nemusíme narazit vůbec a jejich výskyt ovlivní výsledky naší analýzy.Že nám nějaká hodnota nesedí do zbytku dat, ještě nemusí nic zásadního znamenat a hlavně nemusí jít vždy o chybu. Stejně jako v případě chybějících hodnot je i zde před opravou či jiným zásahem třeba dobře zvážit jeho následky. Nejčastější příčiny výskytu odlehlých měření jsou:* Lidská chyba (např. při zápisu dat v dotazníku)* Chyba měření (např. závada na měřícím přístroji)* Chyba při manipulace s daty, agregaci, zpracování* Validní leč extrémní hodnotaPři hledání odlehlých měření se nejdříve zaměříme na jednotlivé proměnné a pak na jejich kombinace. Základní přehled a představu o tom, co můžeme dále očekávat nám poskytne známy krabicový graf. IQRPrvní metodou pro detekci odlehlých měření je inter-quartile range, který jsme si popsali při analýze jedné proměnné u popisu krabicového grafu. Pro zopakování: vezme se rozsah mezi prvním a třetím quartilem a vynásobí se 1,5×. Tím se vytvoří horní a spodní hranice (vodorovné čárky v grafu) a co se mezi ně nevejde, je označeno jako odlehlé měření.<jupyter_code>data.plot.box(figsize=(15,15), subplots=True, layout=(3,3));<jupyter_output><empty_output><jupyter_text>S daty už umíme lépe pracovat, takže si můžeme zkusit vypočítat tuto metodu i ručně.<jupyter_code>q1 = data.runtimeMinutes.quantile(0.25)
q3 = data.runtimeMinutes.quantile(0.75)
iqr = q3 - q1
dolni_hranice = q1-1.5*iqr
horni_hranice = q3+1.5*iqr
print(f"Q1: {q1}, Q3: {q3}, IQR: {iqr}, dolní hranice: {dolni_hranice}, horní hranice: {horni_hranice}")<jupyter_output>Q1: 77.0, Q3: 95.0, IQR: 18.0, dolní hranice: 50.0, horní hranice: 122.0<jupyter_text>Vypočtené hranice nám poslouží pro manuální filtraci dat, takže uvidíme i v tabulce to, co je vidět v grafu.<jupyter_code>data[data.runtimeMinutes < dolni_hranice]
data[data.runtimeMinutes > horni_hranice]<jupyter_output><empty_output><jupyter_text>Skoro 7 tisíc filmů je z pohledu své stopáže označeno jako odlehlá měření. Výpočet pomocí IQR nabízí jednoduchou cestu k výsledkům, ale není jednoduše možné si rozsah hodnot upravit podle svých představ. To je možné v druhé populární metodě detekce odlehlých měření - Z score. Z scorez score je metoda detekce odlehlých měření definovaná jednoduchým vzorcem a závislá na směrodatné odchylce. Právě díky jednoduchému vzorečku a nastavitelné hranici je možné si určit, co pro nás znamená odlehlé měření. Vzorec vypadá následovně:$$ z = \frac{x - \mu}{\sigma} $$`x` označuje konkrétní hodnotu proměnné, `𝜇` označuje průměr pro danou proměnnou a `𝜎` směrodatnou odchylku. Z score uvádí, kolik násobků směrodatné odchylky je daná hodnota vzdálená od průměru. Čím blíže bude zkoumaná hodnota průměru, tím blíže bude Z score nule. Výhodou je, že si pro Z score můžeme sami určit hranice pro detekci odlehlých měření a také to, že díky násobkům směrodatné odchylky, ve kterých se tato hranice stanovuje, víme, kolik procent záznamů máme pokryto. O směrodatné odchylce a normálním rozložení si budeme ještě povídat v lekci o reprezentativnosti dat.> U proměnných s normálním rozložením je nejvíce výskytů kolem průměru a s rostoucí vzdáleností od průměru klesá počet výskytů. Do vzdálenosti jedné směrodatné odchylky se vejde 68 % všech hodnot, dvojnásobek směrodatné odchylky pak pokrývá 95 % všech hodnot a trojnásobek 99 %.Je čas to vyzkoušet:<jupyter_code>abs(20 - data.runtimeMinutes.mean()) / data.runtimeMinutes.std()
abs(2 - data.runtimeMinutes.mean()) / data.runtimeMinutes.std()<jupyter_output><empty_output><jupyter_text>Dvacetiminutový film je od průměrné hodnoty vzdálen přibližně dvojnásobek směrodatné odchylky. Striktně vzato by se nezařadil mezi 95 % nejčastějších hodnot.<jupyter_code>abs(180 - data.runtimeMinutes.mean()) / data.runtimeMinutes.std()<jupyter_output><empty_output><jupyter_text>Tři hodiny trvající film je od průměru vzdálen ještě o něco více to skoro o trojnásobek směrodatné odchylky.Z score vypočtené pro všechny záznamy nám může také posloužit pro filtraci dat.<jupyter_code>zs = abs((data.runtimeMinutes - data.runtimeMinutes.mean()) / data.runtimeMinutes.std())
data[zs > 3].sort_values(by="runtimeMinutes")<jupyter_output><empty_output><jupyter_text>Z score nám na spodní hranici nedetekovalo žádná odlehlá měření a na té horní pak 180 filmů delších než tři hodiny. Více proměnných najednouPro kompletní detekci odlehlých měření je ještě potřeba se na data podívat z pohledu více dimenzí. Kombinace více vlastností totiž může být zcela mimo očekávanou hodnotu, i když každá z vlastností samostatně vůbec nemusí vybočovat z řady.Pro detekci takových případů použijeme stejně jako pro hledání trendů a korelací scatter plot.<jupyter_code>from pandas.plotting import scatter_matrix
scatter_matrix(data, figsize=(20, 20));<jupyter_output><empty_output><jupyter_text>Ve scatter plotech není na první pohled nic vidět, resp. jsou zde patrná odlehlá měření, která jsme již detekovali dříve.Jeden zajímavý bod zde ale přeci jen máme. Je vidět v grafu závislosti celkové délky stopáže a počtu hodnocení.<jupyter_code>data.plot.scatter(x="runtimeMinutes", y="numVotes", grid=True);<jupyter_output><empty_output><jupyter_text>Že se v počtu hlasování objeví extrémně často hodnocené filmy, to je očekávané a víme to z předchozích kroků analýzy. Stejná je situace u extrémních hodnot délky filmů, kde se nějaké extrémy na obě strany dají očekávat. Kombinace obou těchto vlastností má také více méně očekávaný charakter a lidé nejčastěji hodnotí filmy s průměrnou délkou. Až na jednu výjimku, jak je patrné z grafu.I když film mající 300 000 hodnocení není nijak výjimečný a délka 400 minut také není z nejextrémnějších, společná kombinace těchto hodnot je přinejmenším podezřelá a zasloužila by si v běžné analýze další zkoumání.Stejně jako v předchozích krocích u IQR a Z score, můžeme i zde použít algoritmy pro detekci konkrétních odlehlých pozorování ve dvou a více dimenzích. Jejich rozbor a porovnání je mimo záběr této lekce, ale jeden jednoduchý a celkem populární si přeci jen ukážeme. DBSCANDBSCAN (Density-Based Spatial Clustering of Applications with Noise) je algoritmus, který dokáže shlukovat body v prostoru (o libovolném počtu dimenzí) k sobě do skupin podle toho, jak moc jsou body blízko u sebe. Jednoduše řečeno prohledá okolí bodů v prostoru a pokud najde dostatek sousedů, přidá je do společné skupinky. Pokud ovšem body nemají dostatek sousedů, jsou označeny jako šum (noise) a v našem případě je lze považovat za odlehlá měření, protože se zkrátka vyskytují příliš daleko od ostatních a není jich dost, aby si vytvořili vlastní skupinku.Velikost prohledávaného okolí a nezbytný počet sousedů jsou nastavitelné parametry, takže i výsledky tohoto algoritmu budou stejně jako u Z score záviset na správném nastavení a interpretaci. Pro jednoduchost se budeme pohybovat jen v prostoru o dvou dimenzích, což nám umožní výsledky pěkně vizualizovat.Celý proces bude trošku komplikovaný, ale podstatné pro nás je porozumnět všem krokům než si hned zapamatovat celý postup a umět jej naprogramovat.Nejprve musíme z dat odebrat nulové hodnoty, protože s těmi si algoritmy často neumí poradit. Nechceme při tom zasahovat do originálních dat, takže si výsledek uložíme do nové proměnné. Při odstraňování chybějících dat jsme zvolili strategii `any` takže stačí jedna nulová hodnota pro odstranění řádku a `subset` nastavil sloupce, kam se bude pandas dívat, protože např. chybějící hodnota ve sloupci s originálním názvem nás aktuálně nezajímá a není třeba, aby způsobila odstranění celého řádku.<jupyter_code>no_null_values = data.dropna(how="any", subset=["runtimeMinutes", "numVotes"])
no_null_values.isnull().sum()<jupyter_output><empty_output><jupyter_text>Nyní potřebujeme upravit škálu našich dat. Problém je v tom, že délka filmu v minutách se pohybuje v dost odlišných cifrách než počet hlasujících, což by nám komplikovalo správné nastavení algoritmu a výpočet vzdálenosti mezi jednotlivými body. Více o škálování bude řeč v dalších lekcích, protože je to celkem běžná součást přípravy dat pro strojové učení.Škálování na menší hodnoty se provede pro oba sloupce najednou, takže vztah mezi nimi zůstane zachován.<jupyter_code>from sklearn.preprocessing import StandardScaler

sc = StandardScaler()
transformed = sc.fit_transform(no_null_values[["runtimeMinutes", "numVotes"]])
transformed<jupyter_output><empty_output><jupyter_text>Výsledkem je dvourozměrná matice, kterou si pro ověření můžeme také vizualizovat. Převodem z pandas `DataFrame` na numpy matici jsme přišli o názvy sloupců, ale ke sloupcům samotným se dokážeme dostat díky rozšířené indexaci.<jupyter_code>from matplotlib import pyplot as plt

plt.scatter(x=transformed[:, 0], y=transformed[:, 1]);<jupyter_output><empty_output><jupyter_text>V následujícím kroku si vytvoříme model, který bude prohledávat okolí o velikosti 4 a za skupinku bude považovat shluk minimálně dvou bodů.<jupyter_code>from sklearn.cluster import DBSCAN

model = DBSCAN(eps=4, min_samples=2)<jupyter_output><empty_output><jupyter_text>Necháme model naučit se naše transformovaná data.<jupyter_code>result = model.fit(transformed)<jupyter_output><empty_output><jupyter_text>Výsledek obsahuje mimo jiné `labels_`, což je seznam tzv. značek, kterými označil jednotlivé řádky z našich dat a tím je zařadil do skupin.<jupyter_code>result.labels_<jupyter_output><empty_output><jupyter_text>Převodem na množinu (`set`) dokážeme zjistit, kolik takových skupinek vytvořil.<jupyter_code>len(set(result.labels_))<jupyter_output><empty_output><jupyter_text>A teď už k vizualizaci. K tomu budeme potřebovat vykreslit do scatter plotu různé barvy, abychom mezi skupinkami dokázali rozlišit. U takto malého počtu bychom si dokázali barvy určit i ručně, ale pomoci nám může barevná mapa, která hodnoty pro jednotlivé skupiny rozloží na vybrané barevné škále a my je tím pádem dokážeme rozeznat v grafu.<jupyter_code>from matplotlib import cm

cmap = cm.get_cmap("cividis")

no_null_values.plot.scatter(x="runtimeMinutes", y="numVotes", c=result.labels_, cmap=cmap);<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.057534489642017475, 'fraction_numerical': 0.009670407665350662, 'mean_word_length': 6.293432546101585, 'pattern_counts': {'":': 0, '<': 166, '<?xml version=': 0, '>': 169, 'https://': 2, 'lorem ipsum': 0, 'www.': 1, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '41', 'dir': 'jupyter-structured-clean-dedup', 'id': '441997', 'license': 'MIT', 'n_tokens_mistral': 9674, 'n_tokens_neox': 9932, 'n_words': 2723, 'path': 'lessons/pydata/null_and_outliers/index.ipynb', 'repo_name': 'nappex/naucse.python.cz', 'size': '22549'}