Obsah
1. Knihovna Pandas: základy práce s datovými rámci
2. Načtení obsahu jednoduché tabulky ze souboru typu CSV
3. Zpracování prázdných hodnot v tabulce
4. Načtení tabulky obsahující časová razítka
5. Korektní parsing časových razítek
6. Problematika vlastního či specifického formátu data a/nebo času
7. Vlastní parsovací funkce pro časová razítka
8. Čtení tabulky uložené ve formátu TSV
9. Specifikace oddělovače sloupců
10. Import dat z textových souborů
11. Explicitní specifikace šířky sloupců
12. Zpracování souborů s nestandardním formátem
13. Pokus o načtení souboru s nestandardním formátem
15. Přeskok prvního řádku, který neobsahuje data tabulky ani hlavičku
16. Převod záznamů s desetinnou čárkou na číselné hodnoty
18. Repositář s demonstračními příklady a datovými soubory
19. Články s informacemi o různých způsobech validace datových struktur
1. Knihovna Pandas: základy práce s datovými rámci
V dnešním článku se – prozatím ve stručnosti – seznámíme s takzvanými datovými rámci (data frame) používanými v knihovně Pandas v programovacím jazyku Python. Jedná se o velmi důležitý datový typ používaný zejména (ale nejenom) při statistických výpočtech. Datové rámce se v určitém ohledu podobají tabulkám používaným v relačních databázích: jednotlivé sloupce jsou pojmenované a současně může být každý sloupec jiného datového typu (všechny prvky ve sloupci toto kritérium musí splňovat, což ovšem vede k určitým problémům, o nichž si řekneme v navazujících kapitolách). Tato vlastnost odlišuje datové rámce od dvourozměrných polí masivně využívaných například v knihovně Numpy; na druhou stranu je ovšem možné s jednotlivými sloupci datového rámce pracovat jako s jednorozměrným polem kompatibilním právě s knihovnou Numpy a jejími datovými typy.
Dnes si popíšeme především na způsob načtení dat do datového rámce. Knihovna Pandas podporuje využití různých datových zdrojů, především pak:
- Souborů CSV (Comma-Separated Values)
- Souborů TSV (Tab-Separated Values)
- Textových souborů s volitelným oddělovačem a formátem sloupců
- Tabulek z tabulkových procesorů (xls, xlsx, xlsm, xlsb, odf, ods, odt)
- Souborů JSON se strukturovanými daty
- Načítání z relačních databází s využitím SQL driverů
- Načítání z Parquet souborů
- atd.
Zaměříme se na první čtyři formáty, které sice vypadají primitivně, ovšem při načítání je mnohdy nutné řešit mnoho „maličkostí“ typu různé formáty dat, chybějící hodnoty ve sloupcích atd.
2. Načtení obsahu jednoduché tabulky ze souboru typu CSV
CSV neboli Comma-Separated Values [1] je jedním z nejčastěji používaných souborových formátů v této oblasti, a to přesto, že je export a import CSV v některých případech problematický (například některé české mutace Excelu namísto čárek používají středníky, problémy nastávají s buňkami obsahujícími znaky pro konec řádku atd.). Tyto soubory jsou mnohdy obrovské a i z tohoto důvodu se začínají v některých oblastech nahrazovat například za Parquet soubory atd. I přesto se ale s CSV setkáme, a to poměrně často. Příkladem může být export dat z Promethea atd.
Jeden z nejjednodušších příkladů používajících knihovnu Pandas bude načítat soubor CSV (Comma-Separated Values), jehož obsah lze najít na adrese https://github.com/tisnik/most-popular-python-libs/blob/master/pandas/integer_values.csv. V tomto souboru je uložena tabulka se dvěma sloupci, přičemž soubor má i řádek s hlavičkou (ta někdy může chybět):
Block size,Time to read 1,672512695 2,338152789 3,280886198 4,261732244 5,241726381 6,222869657 7,214296698 8,202491102 9,182263641 10,177141401
V příkladu provedeme načtení souboru s využitím funkce pandas.read_csv, které prozatím předáme jediný parametr obsahující název souboru. Vytvořený datový rámec zobrazíme funkcí print a navíc si necháme vypsat i datové typy přiřazené jednotlivým sloupcům:
#!/usr/bin/env python3 # vim: set fileencoding=utf-8 """Reading CSV file that contains column with integer values.""" import pandas df = pandas.read_csv("integer_values.csv") print("Data frame") print("---------------------------") print(df) print() print("Column types") print("---------------------------") print(df.dtypes)
Povšimněte si, že Pandas korektně vyhodnotila, že CSV obsahuje řádek s hlavičkou. Dokonce byl i korektně odvozen typ dat ve sloupcích (int64):
Data frame --------------------------- Block size Time to read 0 1 672512695 1 2 338152789 2 3 280886198 3 4 261732244 4 5 241726381 5 6 222869657 6 7 214296698 7 8 202491102 8 9 182263641 9 10 177141401 Column types --------------------------- Block size int64 Time to read int64 dtype: object
3. Zpracování prázdných hodnot v tabulce
Nyní se pokusme o načtení prakticky stejné tabulky, která se ovšem od první tabulky odlišuje v tom, že jedna hodnota chybí (viz též https://github.com/tisnik/most-popular-python-libs/blob/master/pandas/missing_integer_values.csv). V praxi se s takovými tabulkami pochopitelně setkáme velmi často:
Block size,Time to read 1,672512695 2,338152789 3,280886198 4,261732244 5, 6,222869657 7,214296698 8,202491102 9,182263641 10,177141401
Pokusme se použít stejný kód, jako tomu bylo v předchozí kapitole, pouze změníme jméno vstupního souboru:
#!/usr/bin/env python3 # vim: set fileencoding=utf-8 """Reading CSV file that contains column with integer values (some are missing).""" import pandas df = pandas.read_csv("missing_integer_values.csv") print("Data frame") print("---------------------------") print(df) print() print("Column types") print("---------------------------") print(df.dtypes)
Výsledek bude překvapivý – celý druhý sloupec byl převeden na hodnoty typu float64:
Data frame --------------------------- Block size Time to read 0 1 672512695.0 1 2 338152789.0 2 3 280886198.0 3 4 261732244.0 4 5 NaN 5 6 222869657.0 6 7 214296698.0 7 8 202491102.0 8 9 182263641.0 9 10 177141401.0 Column types --------------------------- Block size int64 Time to read float64 dtype: object
Proč tomu tak je? Knihovna Pandas musí nějakým způsobem reprezentovat chybějící hodnotu a pro tento účel lze (mj.) použít i datový typ float64 neboli double, který podporuje reprezentaci hodnoty NaN neboli Not a Number, viz též https://www.root.cz/clanky/norma-ieee-754-a-pribuzni-formaty-plovouci-radove-tecky/#k03. Což vlastně znamená, že Not a Number je number :-)
Ovšem toto chování nám nemusí z mnoha důvodů vyhovovat, už jen z toho důvodu, že u float64 není splněna vlastnost asociativity u aritmetických operací. Jednou z možností nápravy je použití typu Int64 (nikoli int64), která dokáže uložit i informaci o neexistující hodnotě. Musíme tedy explicitně specifikovat datový typ sloupce nazvaného „Time to read“:
#!/usr/bin/env python3 # vim: set fileencoding=utf-8 """Reading CSV file that contains column with integer values (some are missing).""" import pandas df = pandas.read_csv("missing_integer_values.csv", dtype={"Time to read": "Int64"}) print("Data frame") print("---------------------------") print(df) print() print("Column types") print("---------------------------") print(df.dtypes)
Nyní již dosáhneme očekávaného chování:
Data frame --------------------------- Block size Time to read 0 1 672512695 1 2 338152789 2 3 280886198 3 4 261732244 4 5 <NA> 5 6 222869657 6 7 214296698 7 8 202491102 8 9 182263641 9 10 177141401 Column types --------------------------- Block size int64 Time to read Int64 dtype: object
4. Načtení tabulky obsahující časová razítka
Pokusme se nyní načíst tabulku, která ve svém druhém sloupci obsahuje časová razítka, tedy jak plné datum, tak i čas. Jedná se konkrétně o tento soubor:
n,Timestamp 1,2020-01-15 03:59:47 2,2020-01-15 08:19:25 3,2020-01-15 11:42:07 4,2020-01-15 14:58:48 5,2020-01-15 18:21:56 6,2020-01-15 21:10:01 7,2020-01-15 23:13:58 8,2020-01-16 01:51:52 9,2020-01-16 05:55:55 10,2020-01-16 10:11:54 11,2020-01-16 14:02:32 12,2020-01-16 17:35:25 13,2020-01-16 19:35:43 14,2020-01-16 22:29:24
Prozatím při načtení nebudeme žádným způsobem specifikovat typy sloupců, takže vlastně zopakujeme zdrojový kód prvního příkladu:
#!/usr/bin/env python3 # vim: set fileencoding=utf-8 """Reading CSV with timestamps without parsing.""" import pandas df = pandas.read_csv("timestamps.csv") print("Data frame") print("---------------------------") print(df) print() print("Column types") print("---------------------------") print(df.dtypes)
Ovšem z výsledku je patrné, že knihovna Pandas hodnoty ve druhém sloupci načetla a zjistila, že se jedná o typ „objekt“ a nikoli časové razítko:
Data frame --------------------------- n Timestamp 0 1 2020-01-15 03:59:47 1 2 2020-01-15 08:19:25 2 3 2020-01-15 11:42:07 3 4 2020-01-15 14:58:48 4 5 2020-01-15 18:21:56 5 6 2020-01-15 21:10:01 6 7 2020-01-15 23:13:58 7 8 2020-01-16 01:51:52 8 9 2020-01-16 05:55:55 9 10 2020-01-16 10:11:54 10 11 2020-01-16 14:02:32 11 12 2020-01-16 17:35:25 12 13 2020-01-16 19:35:43 13 14 2020-01-16 22:29:24 Column types --------------------------- n int64 Timestamp object dtype: object
5. Korektní parsing časových razítek
Výše uvedené chování nám samozřejmě nebude v mnoha případech vyhovovat, protože budeme chtít s časovými údaji provádět různé operace. Jedno z možných řešení tohoto problému spočívá v tom, že při načítání tabulky funkcí pandas.read_csv použijeme parametr parse_dates, kterému předáme buď hodnotu True (nové verze Pandas) nebo explicitně názvy sloupců, u nichž se má datum zpracovat:
#!/usr/bin/env python3 # vim: set fileencoding=utf-8 """Reading CSV with timestamps with parsing.""" import pandas df = pandas.read_csv("timestamps.csv", parse_dates=["Timestamp"]) print("Data frame") print("---------------------------") print(df) print() print("Column types") print("---------------------------") print(df.dtypes)
Ze zobrazeného výsledku je patrné, že se nyní hodnoty ve druhém sloupci tabulky skutečně zpracovaly korektně a jsou reprezentovány typem datetime64 (viz též https://numpy.org/doc/stable/reference/arrays.datetime.html):
Data frame --------------------------- n Timestamp 0 1 2020-01-15 03:59:47 1 2 2020-01-15 08:19:25 2 3 2020-01-15 11:42:07 3 4 2020-01-15 14:58:48 4 5 2020-01-15 18:21:56 5 6 2020-01-15 21:10:01 6 7 2020-01-15 23:13:58 7 8 2020-01-16 01:51:52 8 9 2020-01-16 05:55:55 9 10 2020-01-16 10:11:54 10 11 2020-01-16 14:02:32 11 12 2020-01-16 17:35:25 12 13 2020-01-16 19:35:43 13 14 2020-01-16 22:29:24 Column types --------------------------- n int64 Timestamp datetime64[ns] dtype: object
6. Problematika vlastního či specifického formátu data a/nebo času
Nyní se pokusme vyřešit složitější, ale možná o to častější problém – načtení tabulky, v níž se používá specifický formát data a/nebo času. Budeme načítat následující tabulku, v níž je jako oddělovač měsíců a dnů použito lomítko a pro oddělení hodin a minut pak pomlčka (lidská vynalézavost v těchto oblastech je velká, nemluvě již o americkém formátu zápisu data):
n,Timestamp 1,2020/01/15 03-59-47 2,2020/01/15 08-19-25 3,2020/01/15 11-42-07 4,2020/01/15 14-58-48 5,2020/01/15 18-21-56 6,2020/01/15 21-10-01 7,2020/01/15 23-13-58 8,2020/01/16 01-51-52 9,2020/01/16 05-55-55 10,2020/01/16 10-11-54 11,2020/01/16 14-02-32 12,2020/01/16 17-35-25 13,2020/01/16 19-35-43 14,2020/01/16 22-29-24
První varianta skriptu, který se pokusí tuto tabulku načíst a vytvořit z ní datový rámec, může vypadat následovně:
#!/usr/bin/env python3 # vim: set fileencoding=utf-8 """Reading CSV with custom timestamps format.""" import pandas df = pandas.read_csv("custom_timestamps.csv", parse_dates=["Timestamp"]) pandas.to_datetime(df.Timestamp) print("Data frame") print("---------------------------") print(df) print() print("Column types") print("---------------------------") print(df.dtypes)
Výsledek ovšem v tomto případě nedopadne nejlépe, protože se Pandas sice pokusí o rozpoznání časových údajů (což jsme ostatně vyžadovali), ale specifický formát nedokáže správně rozkódovat:
Traceback (most recent call last): File "/home/ptisnovs/.local/lib/python3.6/site-packages/pandas/core/arrays/datetimes.py", line 2054, in objects_to_datetime64ns values, tz_parsed = conversion.datetime_to_datetime64(data) File "pandas/_libs/tslibs/conversion.pyx", line 335, in pandas._libs.tslibs.conversion.datetime_to_datetime64 ValueError: Array must be all same time zone During handling of the above exception, another exception occurred: Traceback (most recent call last): File "read_custom_timestamps_1.py", line 10, in <module> pandas.to_datetime(df.Timestamp) File "/home/ptisnovs/.local/lib/python3.6/site-packages/pandas/core/tools/datetimes.py", line 803, in to_datetime values = convert_listlike(arg._values, format) File "/home/ptisnovs/.local/lib/python3.6/site-packages/pandas/core/tools/datetimes.py", line 466, in _convert_listlike_datetimes allow_object=True, File "/home/ptisnovs/.local/lib/python3.6/site-packages/pandas/core/arrays/datetimes.py", line 2059, in objects_to_datetime64ns raise e File "/home/ptisnovs/.local/lib/python3.6/site-packages/pandas/core/arrays/datetimes.py", line 2050, in objects_to_datetime64ns require_iso8601=require_iso8601, File "pandas/_libs/tslib.pyx", line 352, in pandas._libs.tslib.array_to_datetime File "pandas/_libs/tslib.pyx", line 435, in pandas._libs.tslib.array_to_datetime ValueError: Tz-aware datetime.datetime cannot be converted to datetime64 unless utc=True
7. Vlastní parsovací funkce pro časová razítka
V případě specifických formátů musíme knihovně Pandas předat vlastní parsovací funkci. Ta bude prozatím velmi jednoduchá, protože v ní využijeme možností nabízených standardní knihovnou datetime:
def datetime_parser(raw_data): return datetime.datetime.strptime(raw_data, "%Y/%m/%d %H-%M-%S")
Funkce datetime_parser získá zdrojová data (poskytnutá přímo knihovnou Pandas) a pokusí se z těchto údajů vytvořit časové razítko.
Příklad použití této parsovací funkce při načítání datového rámce:
df = pandas.read_csv("custom_timestamps.csv", date_parser=datetime_parser, parse_dates=["Timestamp"])
Úplný zdrojový kód skriptu pro načtení tabulky:
#!/usr/bin/env python3 # vim: set fileencoding=utf-8 """Reading CSV with custom timestamps format using custom parser.""" import pandas import datetime def datetime_parser(raw_data): return datetime.datetime.strptime(raw_data, "%Y/%m/%d %H-%M-%S") df = pandas.read_csv("custom_timestamps.csv", date_parser=datetime_parser, parse_dates=["Timestamp"]) pandas.to_datetime(df.Timestamp) print("Data frame") print("---------------------------") print(df) print() print("Column types") print("---------------------------") print(df.dtypes)
Výsledek již vypadá použitelně:
Data frame --------------------------- n Timestamp 0 1 2020-01-15 03:59:47 1 2 2020-01-15 08:19:25 2 3 2020-01-15 11:42:07 3 4 2020-01-15 14:58:48 4 5 2020-01-15 18:21:56 5 6 2020-01-15 21:10:01 6 7 2020-01-15 23:13:58 7 8 2020-01-16 01:51:52 8 9 2020-01-16 05:55:55 9 10 2020-01-16 10:11:54 10 11 2020-01-16 14:02:32 11 12 2020-01-16 17:35:25 12 13 2020-01-16 19:35:43 13 14 2020-01-16 22:29:24 Column types --------------------------- n int64 Timestamp datetime64[ns] dtype: object
Alternativně je možné kód pro parsing zapsat formou lambda výrazu:
#!/usr/bin/env python3 # vim: set fileencoding=utf-8 """Reading CSV with custom timestamps format using custom parser.""" import pandas import datetime df = pandas.read_csv("custom_timestamps.csv", date_parser=lambda raw_data: datetime.datetime.strptime(raw_data, "%Y/%m/%d %H-%M-%S"), parse_dates=["Timestamp"]) pandas.to_datetime(df.Timestamp) print("Data frame") print("---------------------------") print(df) print() print("Column types") print("---------------------------") print(df.dtypes)
Výsledek by měl být totožný s příkladem předchozím:
Data frame --------------------------- n Timestamp 0 1 2020-01-15 03:59:47 1 2 2020-01-15 08:19:25 2 3 2020-01-15 11:42:07 3 4 2020-01-15 14:58:48 4 5 2020-01-15 18:21:56 5 6 2020-01-15 21:10:01 6 7 2020-01-15 23:13:58 7 8 2020-01-16 01:51:52 8 9 2020-01-16 05:55:55 9 10 2020-01-16 10:11:54 10 11 2020-01-16 14:02:32 11 12 2020-01-16 17:35:25 12 13 2020-01-16 19:35:43 13 14 2020-01-16 22:29:24 Column types --------------------------- n int64 Timestamp datetime64[ns] dtype: object
8. Čtení tabulky uložené ve formátu TSV
V dalším kroku se pokusíme načíst soubor https://github.com/tisnik/most-popular-python-libs/blob/master/pandas/tiobe.tsv. TSV neboli Tab-Separated Values [2] [3] je velmi podobným formátem jako CSV, ovšem s tím rozdílem, že oddělovačem jednotlivých buněk je znak tabulátoru (tím současně odpadají mnohé problémy CSV). Podobně jako v případě CSV i zde možnost ukládat na první řádek souboru hlavičku:
Sep 2020 Sep 2019 Change Language Ratings Changep 1 2 change C 15.95 +0.74 2 1 change Java 13.48 -3.18 3 3 Python 10.47 +0.59 4 4 C++ 7.11 +1.48 5 5 C# 4.58 +1.18 6 6 Visual Basic 4.12 +0.83 7 7 JavaScript 2.54 +0.41 8 9 change PHP 2.49 +0.62 9 19 change R 2.37 +1.33 10 8 change SQL 1.76 -0.19 11 14 change Go 1.46 +0.24 12 16 change Swift 1.38 +0.28 13 20 change Perl 1.30 +0.26 14 12 change Assembly language 1.30 -0.08 15 15 Ruby 1.24 +0.03 16 18 change MATLAB 1.10 +0.04 17 11 change Groovy 0.99 -0.52 18 33 change Rust 0.92 +0.55 19 10 change Objective-C 0.85 -0.99 20 24 change Dart 0.77 +0.13
Při běžném použití importní funkce pandas.read_csv není tento formát správně rozpoznán:
#!/usr/bin/env python3 # vim: set fileencoding=utf-8 """Reading TSV file that contains column with various values.""" import pandas df = pandas.read_csv("tiobe.tsv") print("Data frame") print("---------------------------") print(df) print() print("Column types") print("---------------------------") print(df.dtypes)
Povšimněte si, že se tabulka načetla do jediného sloupce, který obsahuje mj. i znaky TAB, které by měly sloužit jako oddělovače:
Data frame --------------------------- Sep 2020\tSep 2019\tChange\tLanguage\tRatings\tChangep 0 1\t2\tchange\tC\t15.95\t+0.74 1 2\t1\tchange\tJava\t13.48\t-3.18 2 3\t3\t\tPython\t10.47\t+0.59 3 4\t4\t\tC++\t7.11\t+1.48 4 5\t5\t\tC#\t4.58\t+1.18 5 6\t6\t\tVisual Basic\t4.12\t+0.83 6 7\t7\t\tJavaScript\t2.54\t+0.41 7 8\t9\tchange\tPHP\t2.49\t+0.62 8 9\t19\tchange\tR\t2.37\t+1.33 9 10\t8\tchange\tSQL\t1.76\t-0.19 10 11\t14\tchange\tGo\t1.46\t+0.24 11 12\t16\tchange\tSwift\t1.38\t+0.28 12 13\t20\tchange\tPerl\t1.30\t+0.26 13 14\t12\tchange\tAssembly language\t1.30\t-0.08 14 15\t15\t\tRuby\t1.24\t+0.03 15 16\t18\tchange\tMATLAB\t1.10\t+0.04 16 17\t11\tchange\tGroovy\t0.99\t-0.52 17 18\t33\tchange\tRust\t0.92\t+0.55 18 19\t10\tchange\tObjective-C\t0.85\t-0.99 19 20\t24\tchange\tDart\t0.77\t+0.13 Column types --------------------------- Sep 2020\tSep 2019\tChange\tLanguage\tRatings\tChangep object dtype: object
9. Specifikace oddělovače sloupců
Soubory TSV lze načíst tak, že nepovinným (pojmenovaným) parametrem sep specifikujeme oddělovač mezi záznamy. V tomto případě se jedná o znak „\t“ (Python používá céčkovský způsob zápisu řídicích znaků):
df = pandas.read_csv("tiobe.tsv", sep="\t")
Upravený příklad:
#!/usr/bin/env python3 # vim: set fileencoding=utf-8 """Reading TSV file that contains column with various values using custom separator.""" import pandas # separator/delimiter specification df = pandas.read_csv("tiobe.tsv", sep="\t") print("Data frame") print("---------------------------") print(df) print() print("Column types") print("---------------------------") print(df.dtypes)
Nyní již vypadá datový rámec použitelněji (i když bude nutné upravit minimálně obsah sloupce „Change“, například převodem na True/False):
Data frame --------------------------- Sep 2020 Sep 2019 Change Language Ratings Changep 0 1 2 change C 15.95 0.74 1 2 1 change Java 13.48 -3.18 2 3 3 NaN Python 10.47 0.59 3 4 4 NaN C++ 7.11 1.48 4 5 5 NaN C# 4.58 1.18 5 6 6 NaN Visual Basic 4.12 0.83 6 7 7 NaN JavaScript 2.54 0.41 7 8 9 change PHP 2.49 0.62 8 9 19 change R 2.37 1.33 9 10 8 change SQL 1.76 -0.19 10 11 14 change Go 1.46 0.24 11 12 16 change Swift 1.38 0.28 12 13 20 change Perl 1.30 0.26 13 14 12 change Assembly language 1.30 -0.08 14 15 15 NaN Ruby 1.24 0.03 15 16 18 change MATLAB 1.10 0.04 16 17 11 change Groovy 0.99 -0.52 17 18 33 change Rust 0.92 0.55 18 19 10 change Objective-C 0.85 -0.99 19 20 24 change Dart 0.77 0.13 Column types --------------------------- Sep 2020 int64 Sep 2019 int64 Change object Language object Ratings float64 Changep float64 dtype: object
10. Import dat z textových souborů
Existuje i mnoho aplikací, v nichž jsou tabulková data uložena ve formě běžných textových souborů s nějakými oddělovači odlišnými od výše zmíněného tabulátoru (relativně často se jedná o středníky, dvojtečky nebo o znak |). Buď se jedná o zobecnění formátů CSV a TSV [4], nebo může mít textový soubor podobu naformátovaných sloupců s pevnou délkou (a tedy bez problémů čitelných uživatelem).
A právě takový soubor je připraven na adrese https://github.com/tisnik/most-popular-python-libs/blob/master/pandas/tiobe.txt (povšimněte si toho, že nyní se skutečně mezi buňkami nachází proměnný počet mezer a některé buňky jsou nevyplněny):
Sep 2020 Sep 2019 Change Language Ratings Changep 1 2 change C 15.95 +0.74 2 1 change Java 13.48 -3.18 3 3 Python 10.47 +0.59 4 4 C++ 7.11 +1.48 5 5 C# 4.58 +1.18 6 6 Visual Basic 4.12 +0.83 7 7 JavaScript 2.54 +0.41 8 9 change PHP 2.49 +0.62 9 19 change R 2.37 +1.33 10 8 change SQL 1.76 -0.19 11 14 change Go 1.46 +0.24 12 16 change Swift 1.38 +0.28 13 20 change Perl 1.30 +0.26 14 12 change Assembly language 1.30 -0.08 15 15 Ruby 1.24 +0.03 16 18 change MATLAB 1.10 +0.04 17 11 change Groovy 0.99 -0.52 18 33 change Rust 0.92 +0.55 19 10 change Objective-C 0.85 -0.99 20 24 change Dart 0.77 +0.13
Pokusme se nyní tento soubor načíst, tentokrát však nikoli funkcí pandas.read_csv, ale funkcí pandas.read_fwf, kde „fwf“ znamená „fixed-width formatted“:
#!/usr/bin/env python3 # vim: set fileencoding=utf-8 """Reading text file that contains columns with fixed width.""" import pandas # separator/delimiter specification not needed there df = pandas.read_fwf("tiobe.txt") print("Data frame") print("---------------------------") print(df) print() print("Column types") print("---------------------------") print(df.dtypes)
Výsledek ovšem není zcela dokonalý, protože u sloupců, jejichž jména obsahují mezery, došlo k rozdělení na dva sloupce a tím pádem nám vznikly dvě série hodnot NaN:
Data frame --------------------------- Sep 2020 Sep.1 2019 Change Language Ratings Changep 0 1 NaN 2 NaN change C 15.95 0.74 1 2 NaN 1 NaN change Java 13.48 -3.18 2 3 NaN 3 NaN NaN Python 10.47 0.59 3 4 NaN 4 NaN NaN C++ 7.11 1.48 4 5 NaN 5 NaN NaN C# 4.58 1.18 5 6 NaN 6 NaN NaN Visual Basic 4.12 0.83 6 7 NaN 7 NaN NaN JavaScript 2.54 0.41 7 8 NaN 9 NaN change PHP 2.49 0.62 8 9 NaN 19 NaN change R 2.37 1.33 9 10 NaN 8 NaN change SQL 1.76 -0.19 10 11 NaN 14 NaN change Go 1.46 0.24 11 12 NaN 16 NaN change Swift 1.38 0.28 12 13 NaN 20 NaN change Perl 1.30 0.26 13 14 NaN 12 NaN change Assembly language 1.30 -0.08 14 15 NaN 15 NaN NaN Ruby 1.24 0.03 15 16 NaN 18 NaN change MATLAB 1.10 0.04 16 17 NaN 11 NaN change Groovy 0.99 -0.52 17 18 NaN 33 NaN change Rust 0.92 0.55 18 19 NaN 10 NaN change Objective-C 0.85 -0.99 19 20 NaN 24 NaN change Dart 0.77 0.13 Column types --------------------------- Sep int64 2020 float64 Sep.1 int64 2019 float64 Change object Language object Ratings float64 Changep float64 dtype: object
11. Explicitní specifikace šířky sloupců
Předchozí příklad můžeme opravit tak, že explicitně uvedeme šířky sloupců. To konkrétně znamená, že namísto:
df = pandas.read_fwf("tiobe.txt")
Použijeme nepovinný parametr widths, kde šířky nastavíme:
df = pandas.read_fwf("tiobe.txt", widths=(20, 20, 20, 20, 20, 20))
Ostatní části příkladu mohou zůstat nezměněné:
#!/usr/bin/env python3 # vim: set fileencoding=utf-8 """Reading text file that contains columns with fixed width.""" import pandas # separator/delimiter specification not needed there df = pandas.read_fwf("tiobe.txt", widths=(20, 20, 20, 20, 20, 20)) print("Data frame") print("---------------------------") print(df) print() print("Column types") print("---------------------------") print(df.dtypes)
Výsledek je již téměř dokonalý, pokud nám nebudou vadit hodnoty NaN ve sloupci „Change“:
Data frame --------------------------- Sep 2020 Sep 2019 Change Language Ratings Changep 0 1 2 change C 15.95 0.74 1 2 1 change Java 13.48 -3.18 2 3 3 NaN Python 10.47 0.59 3 4 4 NaN C++ 7.11 1.48 4 5 5 NaN C# 4.58 1.18 5 6 6 NaN Visual Basic 4.12 0.83 6 7 7 NaN JavaScript 2.54 0.41 7 8 9 change PHP 2.49 0.62 8 9 19 change R 2.37 1.33 9 10 8 change SQL 1.76 -0.19 10 11 14 change Go 1.46 0.24 11 12 16 change Swift 1.38 0.28 12 13 20 change Perl 1.30 0.26 13 14 12 change Assembly language 1.30 -0.08 14 15 15 NaN Ruby 1.24 0.03 15 16 18 change MATLAB 1.10 0.04 16 17 11 change Groovy 0.99 -0.52 17 18 33 change Rust 0.92 0.55 18 19 10 change Objective-C 0.85 -0.99 19 20 24 change Dart 0.77 0.13 Column types --------------------------- Sep 2020 int64 Sep 2019 int64 Change object Language object Ratings float64 Changep float64 dtype: object
12. Zpracování souborů s nestandardním formátem
Mnohé soubory s tabulkovými daty mají ještě jiný formát (což opět odkazuje na velkou lidskou tvořivost a znovuobjevování kola). Příkladem mohou být data, která nalezneme na adrese https://www.cnb.cz/cs/financni_trhy/devizovy_trh/kurzy_devizoveho_trhu/denni_kurz.txt. Evidentně se jedná o tabulková a velmi dobře strukturovaná data, která by bylo vhodné umět automaticky zpracovat:
20.11.2020 #224 země|měna|množství|kód|kurz Austrálie|dolar|1|AUD|16,231 Brazílie|real|1|BRL|4,160 Bulharsko|lev|1|BGN|13,467 Čína|žen-min-pi|1|CNY|3,381 Dánsko|koruna|1|DKK|3,536 EMU|euro|1|EUR|26,340 Filipíny|peso|100|PHP|46,038 Hongkong|dolar|1|HKD|2,864 Chorvatsko|kuna|1|HRK|3,481 Indie|rupie|100|INR|29,950 Indonesie|rupie|1000|IDR|1,567 Island|koruna|100|ISK|16,330 Izrael|nový šekel|1|ILS|6,649 Japonsko|jen|100|JPY|21,383 Jižní Afrika|rand|1|ZAR|1,445 Kanada|dolar|1|CAD|17,011 Korejská republika|won|100|KRW|1,990 Maďarsko|forint|100|HUF|7,328 Malajsie|ringgit|1|MYR|5,425 Mexiko|peso|1|MXN|1,104 MMF|ZPČ|1|XDR|31,598 Norsko|koruna|1|NOK|2,471 Nový Zéland|dolar|1|NZD|15,416 Polsko|zlotý|1|PLN|5,900 Rumunsko|leu|1|RON|5,405 Rusko|rubl|100|RUB|29,180 Singapur|dolar|1|SGD|16,530 Švédsko|koruna|1|SEK|2,577 Švýcarsko|frank|1|CHF|24,363 Thajsko|baht|100|THB|73,313 Turecko|lira|1|TRY|2,911 USA|dolar|1|USD|22,201 Velká Británie|libra|1|GBP|29,464
Problémy, které musíme vyřešit v rámci dalších kapitol:
- Ignorování prvního řádku
- Specifikace oddělovačů
- Zpracování hodnot s desetinnou čárkou (protože bývá zvykem používat zápis s tečkou, s čímž počítají i Pythonovské knihovny)
13. Pokus o načtení souboru s nestandardním formátem
Vzhledem k tomu, že soubor zmíněný v předchozí kapitole obsahuje znaky „|“ ve formě oddělovače, použijeme pro jeho načtení funkci pandas.read_csv a nikoli pandas.read_fwf (ta se totiž hodí pro načítání sloupců s pevnou šířkou). Pro začátek si vyzkoušejme, jak dokáže Pandas rozpoznat či naopak nerozpoznat neznámý formát:
#!/usr/bin/env python3 # vim: set fileencoding=utf-8 """Reading data file with custom format.""" import pandas df = pandas.read_csv("denni_kurz.txt") print("Data frame") print("---------------------------") print(df) print() print("Column types") print("---------------------------") print(df.dtypes)
Výsledek není v žádném případě dokonalý, což značí, že budeme muset lépe a přesněji specifikovat konkrétní použitý formát dat:
Traceback (most recent call last): File "read_custom_data_format_1.py", line 8, in <module> df = pandas.read_csv("denni_kurz.txt") File "/home/ptisnovs/.local/lib/python3.6/site-packages/pandas/io/parsers.py", line 688, in read_csv return _read(filepath_or_buffer, kwds) File "/home/ptisnovs/.local/lib/python3.6/site-packages/pandas/io/parsers.py", line 460, in _read data = parser.read(nrows) File "/home/ptisnovs/.local/lib/python3.6/site-packages/pandas/io/parsers.py", line 1198, in read ret = self._engine.read(nrows) File "/home/ptisnovs/.local/lib/python3.6/site-packages/pandas/io/parsers.py", line 2157, in read data = self._reader.read(nrows) File "pandas/_libs/parsers.pyx", line 847, in pandas._libs.parsers.TextReader.read File "pandas/_libs/parsers.pyx", line 862, in pandas._libs.parsers.TextReader._read_low_memory File "pandas/_libs/parsers.pyx", line 918, in pandas._libs.parsers.TextReader._read_rows File "pandas/_libs/parsers.pyx", line 905, in pandas._libs.parsers.TextReader._tokenize_rows File "pandas/_libs/parsers.pyx", line 2042, in pandas._libs.parsers.raise_parser_error pandas.errors.ParserError: Error tokenizing data. C error: Expected 1 fields in line 3, saw 2
14. Specifikace oddělovače
Nejprve se pokusme specifikovat oddělovač jednotlivých záznamů, kterým je znak „|“. Použijeme tedy pojmenovaný parametr sep předaný funkci pandas.read_csv:
df = pandas.read_csv("denni_kurz.txt", sep="|")
Úplný zdrojový kód příkladu:
#!/usr/bin/env python3 # vim: set fileencoding=utf-8 """Reading data file with custom format. separator specification.""" import pandas df = pandas.read_csv("denni_kurz.txt", sep="|") print("Data frame") print("---------------------------") print(df) print() print("Column types") print("---------------------------") print(df.dtypes)
Výsledek ovšem prozatím nebude uspokojivý, a to z toho důvodu, že první řádek „rozhodí“ logiku pro detekci začátku sloupců a všechna data budou uložena do jediného sloupce:
Data frame --------------------------- 20.11.2020 #224 země měna množství kód kurz Austrálie dolar 1 AUD 16,231 Brazílie real 1 BRL 4,160 Bulharsko lev 1 BGN 13,467 Čína žen-min-pi 1 CNY 3,381 Dánsko koruna 1 DKK 3,536 EMU euro 1 EUR 26,340 Filipíny peso 100 PHP 46,038 Hongkong dolar 1 HKD 2,864 Chorvatsko kuna 1 HRK 3,481 Indie rupie 100 INR 29,950 Indonesie rupie 1000 IDR 1,567 Island koruna 100 ISK 16,330 Izrael nový šekel 1 ILS 6,649 Japonsko jen 100 JPY 21,383 Jižní Afrika rand 1 ZAR 1,445 Kanada dolar 1 CAD 17,011 Korejská republika won 100 KRW 1,990 Maďarsko forint 100 HUF 7,328 Malajsie ringgit 1 MYR 5,425 Mexiko peso 1 MXN 1,104 MMF ZPČ 1 XDR 31,598 Norsko koruna 1 NOK 2,471 Nový Zéland dolar 1 NZD 15,416 Polsko zlotý 1 PLN 5,900 Rumunsko leu 1 RON 5,405 Rusko rubl 100 RUB 29,180 Singapur dolar 1 SGD 16,530 Švédsko koruna 1 SEK 2,577 Švýcarsko frank 1 CHF 24,363 Thajsko baht 100 THB 73,313 Turecko lira 1 TRY 2,911 USA dolar 1 USD 22,201 Velká Británie libra 1 GBP 29,464 Column types --------------------------- 20.11.2020 #224 object dtype: object
15. Přeskok prvního řádku, který neobsahuje data tabulky ani hlavičku
Aby byl textový soubor správně načten, musíme zcela přeskočit první řádek, jenž není součástí tabulky. Nejjednodušší způsob spočívá v použití parametru nazvaného skiprows, pochopitelně i s dříve použitým parametrem sep. Nové načtení by tedy mělo vypadat následovně:
df = pandas.read_csv("denni_kurz.txt", sep="|", skiprows=1)
Příklad pro načtení se až na další parametr nebude lišit od předchozího demonstračního příkladu, ovšem výsledky budou mnohem použitelnější:
#!/usr/bin/env python3 # vim: set fileencoding=utf-8 """Reading data file with custom format. separator + skip rows specification.""" import pandas df = pandas.read_csv("denni_kurz.txt", sep="|", skiprows=1) print("Data frame") print("---------------------------") print(df) print() print("Column types") print("---------------------------") print(df.dtypes)
Výsledkem po načtení je již plně použitelný datový rámec, jehož jedinou nevýhodou je, že poslední sloupec neobsahuje číselné hodnoty:
Data frame --------------------------- země měna množství kód kurz 0 Austrálie dolar 1 AUD 16,231 1 Brazílie real 1 BRL 4,160 2 Bulharsko lev 1 BGN 13,467 3 Čína žen-min-pi 1 CNY 3,381 4 Dánsko koruna 1 DKK 3,536 5 EMU euro 1 EUR 26,340 6 Filipíny peso 100 PHP 46,038 7 Hongkong dolar 1 HKD 2,864 8 Chorvatsko kuna 1 HRK 3,481 9 Indie rupie 100 INR 29,950 10 Indonesie rupie 1000 IDR 1,567 11 Island koruna 100 ISK 16,330 12 Izrael nový šekel 1 ILS 6,649 13 Japonsko jen 100 JPY 21,383 14 Jižní Afrika rand 1 ZAR 1,445 15 Kanada dolar 1 CAD 17,011 16 Korejská republika won 100 KRW 1,990 17 Maďarsko forint 100 HUF 7,328 18 Malajsie ringgit 1 MYR 5,425 19 Mexiko peso 1 MXN 1,104 20 MMF ZPČ 1 XDR 31,598 21 Norsko koruna 1 NOK 2,471 22 Nový Zéland dolar 1 NZD 15,416 23 Polsko zlotý 1 PLN 5,900 24 Rumunsko leu 1 RON 5,405 25 Rusko rubl 100 RUB 29,180 26 Singapur dolar 1 SGD 16,530 27 Švédsko koruna 1 SEK 2,577 28 Švýcarsko frank 1 CHF 24,363 29 Thajsko baht 100 THB 73,313 30 Turecko lira 1 TRY 2,911 31 USA dolar 1 USD 22,201 32 Velká Británie libra 1 GBP 29,464 Column types --------------------------- země object měna object množství int64 kód object kurz object dtype: object
16. Převod záznamů s desetinnou čárkou na číselné hodnoty
Poslední úpravou datového rámce, kterou musíme provést, je převod hodnot v posledním sloupci na numerické hodnoty. Tuto úpravu můžeme spustit po načtení datového rámce, a to tak, že de facto do rámce vložíme nový sloupec pojmenovaný stejně jako sloupec starý (tedy „kurz“). A hodnoty pro tento sloupec získáme nejprve řetězcovou záměnou desetinné čárky za desetinnou tečku a posléze převodem na numerické hodnoty, k čemuž použijeme konverzní funkci pandas.to_numeric:
df["kurz"] = pandas.to_numeric(df["kurz"].str.replace(',','.'), errors='coerce')
Upravený demonstrační příklad:
#!/usr/bin/env python3 # vim: set fileencoding=utf-8 """Reading data file with custom format. separator + skip rows specification.""" import pandas df = pandas.read_csv("denni_kurz.txt", sep="|", skiprows=1) df["kurz"] = pandas.to_numeric(df["kurz"].str.replace(',','.'), errors='coerce') print("Data frame") print("---------------------------") print(df) print() print("Column types") print("---------------------------") print(df.dtypes)
Výsledky nyní budou odpovídat požadavkům na datový rámec – čísla jsou skutečně reprezentována numerickými hodnotami atd.:
Data frame --------------------------- země měna množství kód kurz 0 Austrálie dolar 1 AUD 16.231 1 Brazílie real 1 BRL 4.160 2 Bulharsko lev 1 BGN 13.467 3 Čína žen-min-pi 1 CNY 3.381 4 Dánsko koruna 1 DKK 3.536 5 EMU euro 1 EUR 26.340 6 Filipíny peso 100 PHP 46.038 7 Hongkong dolar 1 HKD 2.864 8 Chorvatsko kuna 1 HRK 3.481 9 Indie rupie 100 INR 29.950 10 Indonesie rupie 1000 IDR 1.567 11 Island koruna 100 ISK 16.330 12 Izrael nový šekel 1 ILS 6.649 13 Japonsko jen 100 JPY 21.383 14 Jižní Afrika rand 1 ZAR 1.445 15 Kanada dolar 1 CAD 17.011 16 Korejská republika won 100 KRW 1.990 17 Maďarsko forint 100 HUF 7.328 18 Malajsie ringgit 1 MYR 5.425 19 Mexiko peso 1 MXN 1.104 20 MMF ZPČ 1 XDR 31.598 21 Norsko koruna 1 NOK 2.471 22 Nový Zéland dolar 1 NZD 15.416 23 Polsko zlotý 1 PLN 5.900 24 Rumunsko leu 1 RON 5.405 25 Rusko rubl 100 RUB 29.180 26 Singapur dolar 1 SGD 16.530 27 Švédsko koruna 1 SEK 2.577 28 Švýcarsko frank 1 CHF 24.363 29 Thajsko baht 100 THB 73.313 30 Turecko lira 1 TRY 2.911 31 USA dolar 1 USD 22.201 32 Velká Británie libra 1 GBP 29.464 Column types --------------------------- země object měna object množství int64 kód object kurz float64 dtype: object
O tom, že se jedná skutečně o číselné hodnoty, se můžeme přesvědčit i provedením nějakého výpočtu:
df.describe()
S výsledkem:
množství kurz count 33.000000 33.000000 mean 55.272727 14.879061 std 174.929141 15.649135 min 1.000000 1.104000 25% 1.000000 3.381000 50% 1.000000 7.328000 75% 100.000000 22.201000 max 1000.000000 73.313000
17. Načtení dat přímo z webu
Data, resp. celé tabulky je možné v případě potřeby načíst přímo z internetu, databáze atd. Ukažme si nejprve načtení z internetového zdroje. Opět se bude jednat o tabulku s denními převodními kurzy měn, která je dostupná na adrese https://www.cnb.cz/cs/financni_trhy/devizovy_trh/kurzy_devizoveho_trhu/denni_kurz.txt. Načtení provedeme se stejnými parametry, jaké jsme si uvedli v předchozí kapitole, pouze s tím rozdílem, že se namísto jména lokálního souboru použije URL:
#!/usr/bin/env python3 # vim: set fileencoding=utf-8 """Reading data file from internets.""" import pandas url = "https://www.cnb.cz/cs/financni_trhy/devizovy_trh/kurzy_devizoveho_trhu/denni_kurz.txt" df = pandas.read_csv(url, sep="|", skiprows=1) df["kurz"] = pandas.to_numeric(df["kurz"].str.replace(',','.'), errors='coerce') print("Data frame") print("---------------------------") print(df) print() print("Column types") print("---------------------------") print(df.dtypes)
S očekávaným výsledkem:
Data frame --------------------------- země měna množství kód kurz 0 Austrálie dolar 1 AUD 16.234 1 Brazílie real 1 BRL 4.107 2 Bulharsko lev 1 BGN 13.427 3 Čína žen-min-pi 1 CNY 3.360 4 Dánsko koruna 1 DKK 3.528 5 EMU euro 1 EUR 26.260 6 Filipíny peso 100 PHP 45.873 7 Hongkong dolar 1 HKD 2.855 8 Chorvatsko kuna 1 HRK 3.473 9 Indie rupie 100 INR 29.861 10 Indonesie rupie 1000 IDR 1.563 11 Island koruna 100 ISK 16.321 12 Izrael nový šekel 1 ILS 6.628 13 Japonsko jen 100 JPY 21.164 14 Jižní Afrika rand 1 ZAR 1.441 15 Kanada dolar 1 CAD 16.934 16 Korejská republika won 100 KRW 1.991 17 Maďarsko forint 100 HUF 7.267 18 Malajsie ringgit 1 MYR 5.413 19 Mexiko peso 1 MXN 1.103 20 MMF ZPČ 1 XDR 31.591 21 Norsko koruna 1 NOK 2.460 22 Nový Zéland dolar 1 NZD 15.436 23 Polsko zlotý 1 PLN 5.880 24 Rumunsko leu 1 RON 5.389 25 Rusko rubl 100 RUB 29.143 26 Singapur dolar 1 SGD 16.471 27 Švédsko koruna 1 SEK 2.573 28 Švýcarsko frank 1 CHF 24.240 29 Thajsko baht 100 THB 72.836 30 Turecko lira 1 TRY 2.779 31 USA dolar 1 USD 22.128 32 Velká Británie libra 1 GBP 29.498 Column types --------------------------- země object měna object množství int64 kód object kurz float64 dtype: object
18. Repositář s demonstračními příklady a datovými soubory
Zdrojové kódy všech dnes popsaných demonstračních příkladů určených pro Python 3 a nejnovější stabilní verzi knihovny Pandas byly uloženy do Git repositáře dostupného na adrese https://github.com/tisnik/most-popular-python-libs. V případě, že nebudete chtít klonovat celý repositář (ten je ovšem stále velmi malý, dnes má velikost zhruba několik desítek kilobajtů), můžete namísto toho použít odkazy na jednotlivé příklady, které naleznete v následující tabulce:
Demonstrační příklady načítají následující soubory s daty:
19. Články s informacemi o různých způsobech validace datových struktur
V této kapitole jsou uvedeny odkazy na články, v nichž jsme se zabývali různými způsoby validace datových struktur. Pravděpodobně nejlepší přístup nalezneme v knihovně clojure.spec určené pro jazyk Clojure, ovšem i pro Python existuje několik velmi užitečných knihoven:
- Validace dat s využitím knihovny spec v Clojure 1.9.0
https://www.root.cz/clanky/validace-dat-s-vyuzitim-knihovny-spec-v-clojure-1–9–0/ - Validace dat s využitím knihovny spec v Clojure 1.9.0 (dokončení)
https://www.root.cz/clanky/validace-dat-s-vyuzitim-knihovny-spec-v-clojure-1–9–0-dokonceni/ - Validace datových struktur v Pythonu pomocí knihoven Schemagic a Schema
https://www.root.cz/clanky/validace-datovych-struktur-v-pythonu-pomoci-knihoven-schemagic-a-schema/ - Validace datových struktur v Pythonu (2. část)
https://www.root.cz/clanky/validace-datovych-struktur-v-pythonu-2-cast/ - Validace datových struktur v Pythonu (dokončení)
https://www.root.cz/clanky/validace-datovych-struktur-v-pythonu-dokonceni/
20. Odkazy na Internetu
- Opulent-Pandas na PyPi
https://pypi.org/project/opulent-pandas/ - pandas_validator na PyPi
https://pypi.org/project/pandas_validator/ - pandas-validator (dokumentace)
https://pandas-validator.readthedocs.io/en/latest/ - 7 Best Python Libraries for Validating Data
https://www.yeahhub.com/7-best-python-libraries-validating-data/ - Universally unique identifier (Wikipedia)
https://en.wikipedia.org/wiki/Universally_unique_identifier - Nullable integer data type
https://pandas.pydata.org/pandas-docs/stable/user_guide/integer_na.html - pandas.read_csv
https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.read_csv.html - How to define format when using pandas to_datetime?
https://stackoverflow.com/questions/36848514/how-to-define-format-when-using-pandas-to-datetime - Pandas : skip rows while reading csv file to a Dataframe using read_csv() in Python
https://thispointer.com/pandas-skip-rows-while-reading-csv-file-to-a-dataframe-using-read_csv-in-python/ - Skip rows during csv import pandas
https://stackoverflow.com/questions/20637439/skip-rows-during-csv-import-pandas - Denni kurz
https://www.cnb.cz/cs/financni_trhy/devizovy_trh/kurzy_devizoveho_trhu/denni_kurz.txt - UUID objects according to RFC 4122 (knihovna pro Python)
https://docs.python.org/3.5/library/uuid.html#uuid.uuid4 - Object identifier (Wikipedia)
https://en.wikipedia.org/wiki/Object_identifier - Digital object identifier (Wikipedia)
https://en.wikipedia.org/wiki/Digital_object_identifier - voluptuous na (na PyPi)
https://pypi.python.org/pypi/voluptuous - Repositář knihovny voluptuous na GitHubu
https://github.com/alecthomas/voluptuous - pytest-voluptuous 1.0.2 (na PyPi)
https://pypi.org/project/pytest-voluptuous/ - pytest-voluptuous (na GitHubu)
https://github.com/F-Secure/pytest-voluptuous - schemagic 0.9.1 (na PyPi)
https://pypi.python.org/pypi/schemagic/0.9.1 - Schemagic / Schemagic.web (na GitHubu)
https://github.com/Mechrophile/schemagic - schema 0.6.7 (na PyPi)
https://pypi.python.org/pypi/schema - schema (na GitHubu)
https://github.com/keleshev/schema - XML Schema validator and data conversion library for Python
https://github.com/brunato/xmlschema - xmlschema 0.9.7
https://pypi.python.org/pypi/xmlschema/0.9.7 - jsonschema 2.6.0
https://pypi.python.org/pypi/jsonschema - warlock 1.3.0
https://pypi.python.org/pypi/warlock - Python Virtual Environments – A Primer
https://realpython.com/python-virtual-environments-a-primer/ - pip 1.1 documentation: Requirements files
https://pip.readthedocs.io/en/1.1/requirements.html - unittest.mock — mock object library
https://docs.python.org/3.5/library/unittest.mock.html - mock 2.0.0
https://pypi.python.org/pypi/mock - An Introduction to Mocking in Python
https://www.toptal.com/python/an-introduction-to-mocking-in-python - Unit testing (Wikipedia)
https://en.wikipedia.org/wiki/Unit_testing - Unit testing
https://cs.wikipedia.org/wiki/Unit_testing - Test-driven development (Wikipedia)
https://en.wikipedia.org/wiki/Test-driven_development - Pip (dokumentace)
https://pip.pypa.io/en/stable/ - 5 Differences between clojure.spec and Schema
https://lispcast.com/clojure.spec-vs-schema/ - Schema: Clojure(Script) library for declarative data description and validation
https://github.com/plumatic/schema - clojure.spec – Rationale and Overview
https://clojure.org/about/spec