Skip to article frontmatterSkip to article content

Pandas - spojování tabulek

Tato lekce se nese ve znamení mnohosti a propojování - naučíš se pracovat s více tabulkami najednou. Při tom společně projdeme (ne poprvé a ne naposledy) čištění reálných datových sad.

# Importy jako obvykle
import pandas as pd

Spojování tabulek

V lekci, kde jsme zpracovávali data o počasí, jsme ti ukázali, že je pomocí funkce concat možné slepit dohromady několik objektů DataFrame či Series, pokud mají “kompatibilní” index. Nyní se na problematiku podíváme trochu blíže a ukážeme si, jak spojovat tabulky na základě různých sloupců, a co dělat, když řádky z jedné tabulky nepasují přesně na tabulku druhou.

Obecně pro spojování pandas nabízí tři funkce / metody, z nichž každá má svoje typické využití (možnostmi se ovšem překrývají):

  • concat je univerzální funkce pro slepování dvou či více tabulek / sloupců - pod sebe, vedle sebe, s přihlédnutím k indexům i bez něj.
  • merge je univerzální funkce pro spojování tabulek na základě vazby mezi indexy nebo sloupci.
  • join (metoda) zjednodušuje práci, když chceš spojit dvě tabulky na základě indexu.

Detailní rozbor toho, co která umí, najdeš v dokumentaci. My si je také postupně ukážeme.

Jednoduché skládání

Pod sebou

To je asi ten nejjednodušší případ - máme dva objekty Series nebo dva kusy tabulky se stejnými sloupci a chceme je spojit pod sebou. Na to se používá funkce concat:

a = pd.Series(["jedna", "dvě", "tři"])
b = pd.Series(["čtyři", "pět", "šest"])
pd.concat([a, b])
0 jedna 1 dvě 2 tři 0 čtyři 1 pět 2 šest dtype: object

💡 Vidíš, že se index opakuje? Vytvořili jsme dvě Series, u kterých jsme index neřešili. Jenže pandas na rozdíl od nás ano, a tak poslušně oba indexy spojil, i za cenu duplicitních hodnot. Za cenu použití dodatečného argumentu ignore_index=True se tomu lze vyhnout, což si ukážeme na příklady spojování dvou tabulek o stejných sloupcích:

pd.concat([a, a, a, a, a], ignore_index=True)
0 jedna 1 dvě 2 tři 3 jedna 4 dvě 5 tři 6 jedna 7 dvě 8 tři 9 jedna 10 dvě 11 tři 12 jedna 13 dvě 14 tři dtype: object

Vedle sebe

Toto asi použijete zřídka, ale když chceme “lepit” doprava (třeba deset Series), stačí přidat nám dobře známý argument axis:

pd.concat([a, a, a, a, a], axis="columns")
Loading...

Příklad: Jak co nejrychleji “nakreslit prázdnou šachovnici” (obě slova jsou v uvozovkách)?

sachy = pd.concat(
    [
        pd.concat(   
            [pd.DataFrame([["⬜", "⬛"], ["⬛", "⬜"]])] * 4,
            axis=1)
    ] * 4
)
sachy.index = list(range(8, 0, -1))
sachy.columns = list("ABCDEFGH")
sachy
Loading...

Spojování různorodých tabulek

🎦 Pro spojování heterogenních dat (v datové hantýrce “joinování”) sáhneme po trochu komplexnějších filmových datech...

Máme staženo několik souborů, načteme si je (zatím hrubě, “raw”) - s přihlédnutím k tomu, že první dva nejsou v pravém slova smyslu “comma-separated”, ale používají k oddělení hodnot tabulátor (tady pomůže argument sep). Také zohledníme, že v nich řetězec "\N" představuje chybějící hodnoty (pomůže argument na_values):

imdb_titles_raw = pd.read_csv("title.basics.tsv.gz", sep="\t", na_values="\\N")
imdb_ratings_raw = pd.read_csv("title.ratings.tsv.gz", sep="\t", na_values="\\N")
boxoffice_raw = pd.read_csv("boxoffice_march_2019.csv.gz")
rotten_tomatoes_raw = pd.read_csv("rotten_tomatoes_top_movies_2019-01-15.csv")

Co který soubor obsahuje?

  • První dva soubory obsahují volně dostupná (byť “jen” pro nekomerční použití) data o filmech z IMDb (Internet Movie Database). My jsme si zvolili obecné informace a uživatelská (číselná) hodnocení. Detailní popis souborů, stejně jako odkazy na další soubory, najdeš na https://www.imdb.com/interfaces/. Z důvodů paměťové náročnosti jsme datovou sadu ořezali o epizody seriálů, protože nás nebudou zajímat a s trochu štěstí přežijeme i na počítačích s menší operační pamětí.

  • Soubor boxoffice_march_2019.csv.gz obsahuje informace o výdělcích jednotlivých filmů. Pochází z ukázkového datasetu pro soutěž “TMDB Box Office Prediction” na serveru Kaggle: https://www.kaggle.com/c/tmdb-box-office-prediction/data

  • Soubor rotten_tomatoes_top_movies_2019-01-15.csv obsahuje procentuální hodnocení filmů ze serveru Rotten Tomatoes, které se počítá jako podíl pozitivních hodnoceních od filmových kritiku (je to tedy jiný princip než na IMDb). Staženo z: https://data.world/prasert/rotten-tomatoes-top-movies-by-genre

Pojďme se podívat na nedostatky těchto souborů a postupně je skládat dohromady. Zajímalo by nás (a snad i tebe!), jak souvisí hodnocení s komerční úspěšností filmu, jak se liší hodnocení rotten tomatoes od těch na IMDb.

imdb_titles_raw
Loading...
# Kolik tabulka zabírá megabajtů paměti? (1 MB = 2**20 bajtů)
imdb_titles_raw.memory_usage(deep=True).sum() / 2**20 
648.8971881866455

Jistě budeme chtít převést sloupce na správné typy. Jaké jsou v základu?

imdb_titles_raw.dtypes
tconst object titleType object primaryTitle object originalTitle object isAdult int64 startYear float64 endYear float64 runtimeMinutes float64 genres object dtype: object

Na co budeme převádět?

  • tconst je řetězec, který posléze použijeme jako index, protože představuje unikátní identifikátor v databázi IMDb.
  • titleType:
imdb_titles_raw["titleType"].value_counts()
titleType short 676930 movie 514654 video 227582 tvSeries 162781 tvMovie 126507 tvMiniSeries 25574 videoGame 23310 tvSpecial 17007 tvShort 9171 Name: count, dtype: int64

Jen devět různých hodnot ve skoro 2 milionech řádků? To je ideální kandidát na převedení na typ "category".

  • primaryTitle a originalTitle vypadají jako obyčejné řetězce (pokud možno anglický a pokud možno původní název)
  • isAdult určuje, zda se jedná o dílo pro dospělé. Tento sloupec bychom nejspíše měli převést na bool.
imdb_titles_raw["isAdult"].value_counts()
isAdult 0 1692292 1 91224 Name: count, dtype: int64
  • startYear a endYear obsahují roky, t.j. celá čísla, ovšem kvůli chybějícím hodnotám je pro ně zvolen typ float64. V pandas raději zvolíme tzv. “nullable integer”, který se zapisuje s velkým “I”. Když nevíš, jaký podtyp konkrétně, sáhni po Int64.
  • totéž platí pro runtimeMinutes.
imdb_titles_raw[["startYear", "endYear", "runtimeMinutes"]].max()
startYear 2115.0 endYear 2027.0 runtimeMinutes 125156.0 dtype: float64

Mimochodem všimli jste si, že máme díla z budoucnosti (rok 2115)?

imdb_titles_raw["startYear"].plot.hist()
imdb_titles_raw.query("startYear > 2019")["startYear"].value_counts().sort_index()
startYear 2020.0 340 2021.0 36 2022.0 14 2023.0 1 2024.0 2 2025.0 1 2115.0 1 Name: count, dtype: int64
<Figure size 640x480 with 1 Axes>

Takhle nějak by přetypování mohlo vypadat:

(
    imdb_titles_raw
    .assign(
        titleType=imdb_titles_raw["titleType"].astype("category"),
        startYear=imdb_titles_raw["startYear"].astype("Int64"),
        endYear=imdb_titles_raw["endYear"].astype("Int64"),
        isAdult=imdb_titles_raw["isAdult"].astype(bool),
        runtimeMinutes=imdb_titles_raw["runtimeMinutes"].astype("Int64")
    )
).dtypes
tconst object titleType category primaryTitle object originalTitle object isAdult bool startYear Int64 endYear Int64 runtimeMinutes Int64 genres object dtype: object

Takhle už by to mohlo být. Jen si ještě:

  • pro přehlednost přejmenujeme některé sloupce (pomocí metody rename)
  • použijeme tconst jako index

A tabulka imdb_titles bude připravená k použití!

imdb_titles = (
    imdb_titles_raw
    .assign(
        titleType=imdb_titles_raw["titleType"].astype("category"),
        startYear=imdb_titles_raw["startYear"].astype("Int64"),
        endYear=imdb_titles_raw["endYear"].astype("Int64"),
        isAdult=imdb_titles_raw["isAdult"].astype(bool),
        runtimeMinutes=imdb_titles_raw["runtimeMinutes"].astype("Int64")
    )
    .rename({
        "primaryTitle": "title",
        "originalTitle": "original_title",
        "titleType": "title_type",
        "runtimeMinutes": "length",
        "startYear": "start_year",
        "endYear": "end_year",
        "isAdult": "is_adult",
    }, axis="columns")
    .set_index("tconst")
)
imdb_titles
Loading...
# Kolik tabulka zabírá megabajtů paměti?
imdb_titles.memory_usage(deep=True).sum() / 2**20   # O chlup méně, zase tolik jsme si ale nepomohli.
537.2908029556274

Připravíme si ještě speciální tabulku jenom pro filmy, protože další datové sady se zabývají jenom jimi.

U této tabulky navíc vyhodíme zbytečné sloupce title_type, end_year a přejmenujeme start_year prostě na year:

movies = (
    imdb_titles
    .query("title_type == 'movie'")
    .drop(["title_type", "end_year"], axis="columns")
    .rename({"start_year": "year"}, axis="columns")
)
movies
Loading...
print(movies.shape)
print(movies.dtypes)
(514654, 6)
title             object
original_title    object
is_adult            bool
year               Int64
length             Int64
genres            object
dtype: object

Nyní se podíváme na zoubek hodnocením z IMDb, na tabulky imdb_ratings_raw:

imdb_ratings_raw
Loading...
imdb_ratings_raw.dtypes
tconst object averageRating float64 numVotes int64 dtype: object

To by vlastně skoro mohlo být!

Tak jen nastavíme index (opět tconst) a přejmenujeme sloupce:

ratings = (imdb_ratings_raw
    .rename({
        "averageRating": "imdb_rating",
        "numVotes": "imdb_votes"
    }, axis="columns")
    .set_index("tconst")
)
ratings
Loading...

První join

Máme připravené dvě krásné tabulky, které sdílejí stejný index, a můžeme vesele spojovat. Protože pomocí join, merge a concat lze volbou vhodných parametrů dosáhnout identického výsledku (což je jedním z nešvarů knihovny pandas), ukážeme si všechny tři alternativy podle subjektivního pořadí vhodnosti.

movies.join(ratings)
Loading...

K tabulce se nenápadně přidaly dva sloupce z tabulky ratings, a to takovým způsobem, že se porovnaly hodnoty indexu (tedy tconst) a spárovaly se ty části řádku, kde se tento index shoduje.

💡 Uvědom si (ačkoliv z volání funkcí v pandas to není úplně zřejmé), že se tady děje něco fundamentálně odlišného od “nalepení doprava” - tabulky tu nejsou chápány jako čtverečky, které jde skládat jako lego, nýbrž jako zdroj údajů o jednotlivých objektech, které je potřeba spojit sémanticky.

Jak ale vidíš, tabulka obsahuje spoustu řádků, kde ve sloupcích s hodnocením chybí hodnoty (respektive nachází se NaN). To vychází ze způsobu, jakým metoda join ve výchozím nastavení “joinuje” - použije všechny řádky z levé tabulky bez ohledu na to, jestli jim odpovídá nějaký protějšek v tabulce pravé. Naštěstí lze pomocí argumentu how specifikovat i jiné způsoby spojování:

  • left (výchozí pro metodu join) - vezmou se všechny prvky z levé tabulky a jim odpovídající prvky z pravé tabulky (kde nejsou, doplní se NaN)
  • right - vezmou se všechny prvky z pravé tabulky a jim odpovídající prvky z levé tabulky (kde nejsou, doplní se NaN)
  • inner (výchozí pro funkci merge) - vezmou se jen ty prvky, které jsou v levé i pravé tabulce.
  • outer (výchozí pro funkci concat) - vezmou se všechny prvky, z levé i pravé tabulky, kde něco chybí, doplní se NaN.

V podobě Vennově diagramu, kde kruhy představují množiny řádků v obou zdrojových tabulkách a modrou barvou jsou zvýrazněny řádky v tabulce cílové:

Typy joinů

Obrázek adaptován z https://upload.wikimedia.org/wikipedia/commons/9/9d/SQL_Joins.svg (autor: Arbeck)

💡 Až budeme probírat databáze, tyto čtyři typu joinů se nám znovu vynoří.

Následující výpis ukáže, kolik řádků bychom dostali při použití různých hodnot how:

for how in ["left", "right", "inner", "outer"]:
    print(f"movies.join(ratings, how=\"{how}\"):", movies.join(ratings, how=how).shape[0], "řádků.")
movies.join(ratings, how="left"): 514654 řádků.
movies.join(ratings, how="right"): 923696 řádků.
movies.join(ratings, how="inner"): 232496 řádků.
movies.join(ratings, how="outer"): 1205854 řádků.

A teď tedy ty tři alternativy:

# Alternativa 1 (preferovaná)
movies_with_rating = movies.join(ratings, how="inner")
movies_with_rating
Loading...
# Alternativa 2 (taky dobrá)
pd.merge(movies, ratings, left_index=True, right_index=True)
Loading...
# Alternativa 3 (méně "sémantická")
pd.concat([movies, ratings], axis="columns", join="inner")
Loading...

Zkusme si zreprodukovat pořadí 250 nejlepších filmů z IMDb (viz https://www.imdb.com/chart/top/?ref_=nv_mv_250):

# Ty nejlepší (do června 2019)
(movies_with_rating
    .query("imdb_votes > 25000")                  # Berou se jen filmy s více než 25000 hlasy
    .sort_values("imdb_rating", ascending=False)  # IMDb tu použivá i váhu jednotlivých hlasů (kterou neznáme)
    .reset_index(drop=True)
).iloc[:250]
Loading...

Do výčtu se nám dostaly filmy, které hranici hlasů nepřekračují o moc. Máme důvodné podezření, že toto kritérium dávno změnili. S požadovanými 250 000 hlasy se už blížíme:

(movies_with_rating
    .query("imdb_votes > 250000")
    .sort_values("imdb_rating", ascending=False)
    .reset_index(drop=True)
).iloc[:250]
Loading...

Druhý join

Co tabulka s výdělky (boxoffice_raw)?

boxoffice_raw
Loading...
boxoffice_raw.dtypes
rank int64 title object studio object lifetime_gross int64 year int64 dtype: object

S tím bychom v podstatně mohli být spokojení, jen přejmenujeme rank, abychom při joinování věděli, odkud daný sloupec pochází.

boxoffice = (boxoffice_raw
    .rename({
        "rank": "boxoffice_rank"
    }, axis="columns")
)

A zkusíme joinovat. V tomto případě se nemůžeme opřít o index (boxoffice pochází z jiného zdroje a o nějakém ID filmu z IMDb nemá ani tuchy), ale explicitně specifikujeme, který sloupec (či sloupce) se musí shodovat - na to slouží argument on:

pd.merge(
    movies_with_rating,
    boxoffice,
    suffixes=[" (imdb)", " (boxoffice)"],
    on="title"
).query("title == 'Pinocchio'")  # "Jeden" ukázkový film
Loading...

Jejda, to jsme asi nechtěli. Existuje spousta různých Pinocchiů a ke každému z nich se připojili vždy oba snímky tohoto jména z boxoffice. Z toho vyplývá poučení, že při joinování je dobré se zamyslet nad jedinečností hodnot ve sloupci, který používáme jako klíč. Jméno filmu takové očividně není.

V našem konkrétním případě jsme si problému všimli sami, ale pokud bude duplikátní klíč utopen někde v milionech hodnot, rádi bychom, aby to počítač poznal za nás. K tomu slouží argument validate - podle toho, jaký vztah mezi tabulkami očekáš, jsou přípustné hodnoty "one_to_one", "one_to_many", "many_to_one" nebo "many_to_many":

pd.merge(
    movies_with_rating,
    boxoffice,
    on="title",
    suffixes=[" (imdb)", " (boxoffice)"],
#    validate="one_to_one"    # Odkomentuj a vyskočí chyba!
)
Loading...

Řešení je jednoduché - budeme joinovat přes dva různé sloupce (argument on to unese ;-)). Při té příležitosti navíc zjišťujeme, že nedává smysl spojovat filmy, které rok vůbec uvedený nemají, a proto je vyhodíme:

(
    pd.merge(
        movies_with_rating.dropna(subset=["year"]),  # Vyhoď všechny řádky bez roku
        boxoffice,
        on=["title", "year"],
        validate="many_to_one",      # movies_with_rating pořád nejsou unikátní!
    )
).query("title == 'Playback'")
Loading...

Pořád nejsou unikátní! Co s tím?

Hypotéza: Vstupujeme na nebezpečnou půdu a zkusíme spekulovat, že informace o ziscích budeme mít nejspíš jen o nejpopulárnějších filmech. Možná máme pravdu, možná ne a nejspíš nějakou drobnou nepřesnost zaneseme, ale dobrat se tady skutečné pravdy je “drahé” (a možná i skutečně drahé), z nabízených datových sad to věrohodně možné není.

Abychom se co nejvíc přiblížili realitě, z každé opakující se dvojice (název, rok) vybereme film s nejvyšším imdb_votes. Nejdříve si pomocí sort_values srovnáme všechny filmy a pak zavoláme drop_duplicates(..., keep="first"), což nám ponechá vždy jen jeden z řady duplikátů:

movies_with_rating_and_boxoffice = (
    pd.merge(
        movies_with_rating
            .dropna(subset=["year"])
            .sort_values("imdb_votes", ascending=False)
            .drop_duplicates(
                subset=["title", "year"],
                keep="first"
            ),
        boxoffice,
        on=["title", "year"],
        validate="one_to_one",
    )
)
movies_with_rating_and_boxoffice
Loading...
# To už by šlo!
movies_with_rating_and_boxoffice.query("title == 'Playback'")
Loading...

Úkol: Seřaď filmy podle toho, kolik vydělaly (nabízí se hned dvě možnosti).

Otázka: Které filmy nám vypadly a proč?

Třetí join

rotten_tomatoes_raw
Loading...
rotten_tomatoes_raw["Title"].value_counts()
Title Yellow Submarine (1968) 6 101 Dalmatians (1961) 5 Fantasia (1940) 5 Harry Potter and the Deathly Hallows - Part 2 (2011) 5 Miracle on 34th Street (1947) 5 .. Misery (1990) 1 The Dead Zone (1983) 1 The Conjuring (2013) 1 The Exorcist (1973) 1 Texas Rangers (2001) 1 Name: count, Length: 947, dtype: int64

A zase duplicity, některé názvy se nám opakují :-(

Otázka: Dokážeš zjistit proč? Nápověda: podívej se na nějaký konkrétní film.

Naštěstí už víme, jak na to - použijeme metodu drop_duplicates, tentokrát přes sloupec "Title". (Poznámka: druhou možností by bylo sloučit všechny různé žánry daného filmu do jedné buňky).

rotten_tomatoes_nodup = (
    rotten_tomatoes_raw
    .drop_duplicates(
        subset="Title",
        keep="first"                   # Vybereme první výskyt, lze i "last" (anebo False => vyhodit všechny)
    )
    .drop("Genres", axis="columns")    # Informační hodnotu jsme už ztratili
    .drop("Rank", axis="columns")      # Mělo smysl jen v rámci žánru
)
rotten_tomatoes_nodup
Loading...
# Ready to merge?
pd.merge(imdb_titles, rotten_tomatoes_nodup, left_on="title", right_on="Title")
Loading...

0 řádků!

Dosud jsme manipulovali s řádky a sloupci jako celky, nicméně teď musíme zasahovat přímo do hodnot v buňkách. I to se při slučování dat z různých zdrojů nezřídka stává. Stojíme před úkolem převést řetězce typu “Black Panther (2018)” na dvě hodnoty: název “Black Panther” a rok 2018 (jeden sloupec na dva).

Naštěstí si ty sloupce umíme jednoduše vyrobit pomocí řetězcové metody .str.slice, která z každého řetězce vyřízne nějakou jeho část (a zase pracuje na celém sloupci - výsledkem bude nový sloupec s funkcí aplikovanou na každou z hodnot). Budeme věřit, že předposlední čtyři znaky představují rok a zbytek, až na nějaké ty závorky, tvoří skutečný název:

rotten_tomatoes_beta = (rotten_tomatoes_nodup
    .assign(
        title=rotten_tomatoes_nodup["Title"].str.slice(0, -7),             
        year=rotten_tomatoes_nodup["Title"].str.slice(-5, -1).astype(int)
    )
    .rename({
        "RatingTomatometer": "tomatoes_rating",
        "No. of Reviews": "tomatoes_votes",
    }, axis="columns")
    .drop(["Title"], axis="columns")
)
rotten_tomatoes_beta
Loading...

Závorková odysea nekončí, někdo nám proaktivně do závorek nacpal i originální název naanglickojazyčných filmů. Pojďme se o tom přesvědčit pomocí metody .str.contains (protože tato metoda ve výchozím stavu používá pro vyhledávání regulární výrazy, které jsme se zatím nenaučili používat, musíme jí to explicitně zakázat argumentem regex=False):

rotten_tomatoes_beta[rotten_tomatoes_beta["title"].str.contains(")", regex=False)]
Loading...

V rámci zjednodušení proto ještě odstraníme všechny takové závorky. K tomu pomůže funkce .str.rsplit, která rozdělí zprava řetězec na několik částí podle oddělovače a vloží je do seznamu - my za ten oddělovač zvolíme levou závorku "(", omezíme počet částí na jednu až dvě (n=1):

split_title = (
    rotten_tomatoes_beta["title"]
    .str.rsplit("(", n=1)
)
split_title.loc[[41, 61, 81]]   # Některé seznamy obsahují jeden prvek, jiné dva
41 [Marvel's The Avengers] 61 [Ghostbusters , 1984 Original)] 81 [Mad Max 2: The Road Warrior] Name: title, dtype: object

A jak teď vybrat první prvek z každého seznamu?

💡 Metoda apply umožňuje použít libovolnou transformaci (definovanou jako funkci) na každý řádek v tabulce či hodnotu v Series. Obvykle se bez ní obejdeme a měli bychom (proto se jí tolik speciálně nevěnujeme), protože není příliš výpočetně efektivní. Tady nám ale usnadní pochopení, co se vlastně dělá, t.j. vybírá první prvek nějakého seznamu:

def take_first(a_list):   # Funkce, kterou použijeme v apply
    return a_list[0]

rotten_tomatoes = (rotten_tomatoes_beta
    .assign(
        title=split_title.apply(take_first)
    )
)
rotten_tomatoes
Loading...
# Zbavení se duplikátů hned na začátku nám zachovalo filmy se stejným jménem :-)
rotten_tomatoes.query("title == 'The Magnificent Seven'")
Loading...
pd.merge(
    movies.dropna(subset=["year"]),
    rotten_tomatoes,
    on=["title", "year"],
    how="inner"
)
Loading...

Když sloučíme filmy a hodnocení na Rotten Tomatoes, z 947 filmů se nám skoro tři sta ztratí. Bohužel zde je na vině především nestejnost zápisu názvu, různé uřčité členy, interpunkce, podnázvy apod. Coby řešení se tady nabízí spousta a spousta manuální práce, případně nějaká heuristika, která by na sebe pasovala “hodně podobné” názvy.

Mimochodem, obtížnost manuální práce se mezi vývojáři někdy přeceňuje: Opravit 288 názvů filmů může být práce na hodinu až dvě, zatímco psát algoritmus na “řešení problému” může trvat stejně dlouho, ne-li déle.

Čtvrtý (a poslední) join

Dokončíme slučování všech čtyř tabulek:

movies_complete = pd.merge(
    movies_with_rating_and_boxoffice,
    rotten_tomatoes,
    on=["title", "year"],
    how="inner"
)
movies_complete.sort_values("boxoffice_rank").reset_index(drop=True)
Loading...

A přišli jsme o dalších 175 filmů.

Co dál? Pokud by toto byl skutečný úkol, museli bychom se s tím nějak vypořádat - zkoumat, proč které řádky nesedí, v čem se liší názvy stejného filmu v různých datových sadách, jinými slovy manuální práce, práce, práce...

Naštěstí to je úkol jen ukázkový, a my můžeme být spokojeni, že máme sice neúplnou, ale přesto použitelnou datovou sadu :-)