YOUR ANSWER HERE
A labor célja egy rövid bevezetőt adni a manapság népszerű "data science" Python eszközeibe. A labor feladatai előtt mindenképp meg kell ismerkedni a Python nyelv alapjaival. A laborhoz tartozó rövid magyar Python bevezetőt itt találod.
A pandashoz egy rövid magyar bevezető itt.
A labort összeállította: Ács Judit
A labor teljes beadandó anyaga ez a notebook. A kérdések sorszámozva vannak és Q-val kezdődnek: Q1.1-Q5.5-ig. Néhány szorgalmi kérdés is van, ezekkel plusz pontot lehet szerezni a maximálison felül. Minden kiskérdés 2 pontot ér, a 25 kérdés összesen 50 pont. A jegyek a következőképpen alakulnak:
| pontszám | jegy |
|---|---|
| 40+ | 5 |
| 30+ | 4 |
| 20+ | 3 |
| 10+ | 2 |
| 9- | 1 |
A feladatokhoz tartoznak tesztek, amiket a megoldás elkészítése előtt érdemes elolvasni.
FIGYELEM! A vizualizációt nem tartalmazó feladatok javítása automatikusan történik. A notebookban szereplő tesztek a visszatérési értékek típusát ellenőrzik, a válaszok helyességét rejtett tesztek ellenőrzik. A teszteket tartalmazó cellákat nem lehet módosítani. A kitöltendő helyek YOUR CODE HERE commenttel vannak jelölve (az exception dobását értelemszerűen törölni kell).
Amennyiben az órán nem sikerült befejezned, otthon folytathatod a munkát és a labor hetének végéig (vasárnap éjfél) feltöltheted. Az Anaconda Python disztribúció tartalmazza a laboranyaghoz szükséges összes csomagot, beleértve a jupytert.
Kernel->Restart & Run All opcióval teheted meg. Ha nem minden feladatot oldasz meg, akkor a NotImplementedError-ok miatt nem fog végigfutni, de kézzel le tudod futtatni a cellákat (Shift+Enter lefuttatja és a következőre lép).A laborhoz külön jegyzőkönyvet nem kell készíteni, ezt a notebookot kell vasárnap éjfélig az AUT portálra feltölteni NEPTUN.ipynb néven. A fájlrendszerben pandas_labor.ipynb néven megtalálod a notebookot abban a könyvtárban, ahonnan indítottad a jupytert.
Az AUT portálra .zip-be csomagolva tudod feltölteni.
In [ ]:
import pandas as pd # konvenció szerint pd aliast használunk
%matplotlib inline
import matplotlib
import numpy as np
# tegyük szebbé a grafikonokat
matplotlib.style.use('ggplot')
matplotlib.pyplot.rcParams['figure.figsize'] = (15, 3)
matplotlib.pyplot.rcParams['font.family'] = 'sans-serif'
A MovieLens adatsorral fogunk dolgozni, de először le kell töltenünk. http://grouplens.org/datasets/movielens/
Csak akkor töltjük le a fájlt, ha még nem létezik.
In [ ]:
import os
data_dir = os.getenv("MOVIELENS")
if data_dir is None:
data_dir = ""
ml_path = os.path.join(data_dir, "ml.zip")
if not os.path.exists(ml_path):
print("Download data")
import urllib
u = urllib.request.URLopener()
u.retrieve("http://files.grouplens.org/datasets/movielens/ml-100k.zip", ml_path)
print("Data downloaded")
Kicsomagoljuk:
In [ ]:
unzip_path = os.path.join(data_dir, "ml-100k")
if not os.path.exists(unzip_path):
print("Extracting data")
from zipfile import ZipFile
with ZipFile(ml_path) as myzip:
myzip.extractall(data_dir)
print("Data extraction done")
data_dir = unzip_path
A pd.read_table függvény táblázatos adatok betöltésére alkalmas. Több tucat paraméterrel rendelkezik, de csak egy kötelező paramétere van: a fájl, amit beolvasunk.
A karakterkódolást is meg kell adnunk, mert a fájl nem az alapértelmezett (utf-8) kódolást használja, hanem az ISO-8859-1-et, vagy köznéven a latin1-et.
In [ ]:
# df = pd.read_table("ml-100k/u.item") # UnicodeDecodeErrort kapunk, mert rossz dekódert használ
df = pd.read_table(os.path.join(data_dir, "u.item"), encoding="latin1")
df.head()
Ez még elég rosszul néz ki. Hogyan tudnánk javítani?
sep paraméterrel tudjuk megadni.names paraméterben.index_col paraméter). Célszerű szóköz nélküli, kisbetűs oszlopneveket használni, mert akkor attribútumként is elérjük őket (df.release_date).
In [ ]:
column_names = [
"movie_id", "title", "release_date", "video_release_date", "imdb_url", "unknown", "action", "adventure", "animation",
"children", "comedy", "crime", "documentary", "drama", "fantasy", "film_noir", "horror", "musical", "mystery",
"romance", "sci_fi", "thriller", "war", "western"]
In [ ]:
df = pd.read_table(
os.path.join(data_dir, "u.item"), sep="|",
names=column_names, encoding="latin1", index_col='movie_id')
df.head()
Két oszlop is van, amik dátumot jelölnek: release_date, video_release_date. A pandas parszolni tudja a dátumokat többféle népszerű formátumban, ehhez csak a parse_dates paraméterben kell megadnunk a dátumot tartalmazó oszlopokat. Figyeljük meg, hogy ahol nincs dátum, az Nan (not a number)-ről NaT-ra (not a time) változik.
In [ ]:
df = pd.read_table(os.path.join(data_dir, "u.item"), sep="|",
names=column_names, encoding="latin1",
parse_dates=[2,3], index_col='movie_id')
df.head()
Még mindig nem tökéletes, hiszen a filmek címei után szerepel az évszám zárójelben, ami egyrészt redundáns, másrészt zaj. Tüntessük el!
A szokásos str műveletek egy része elérhető DataSeries objektumokra is (minden elemre végrehajtja). A függvényeket az str névtérben találjuk.
In [ ]:
df.title.str
Egy reguláris kifejezéssel eltüntetjük a két zárójel közti részt, majd eltávolítjuk az ott maradt whitespace-eket (a strip függvény a stringek elejéről és végéről is eltávolítja). Végül adjuk értékül a régi title oszlopnak a kezdő és záró whitespace-ektől megfosztott változatát.
In [ ]:
df.title = df.title.str.replace(r'\(.*\)', '').str.strip()
In [ ]:
df.head()
A video_release_date mező az első néhány sorban csak érvénytelen mezőket tartalmaz. Vajon igaz ez az egész DataFrame-re? Listázzuk ki azokat a mezőket, ahol nem NaT a video_release_date értéke, vagyis érvénytelen dátum.
In [ ]:
df[df.video_release_date.notnull()]
Nincs ilyen mező, ezért elhagyhatjuk az oszlopot.
In [ ]:
df = df.drop('video_release_date', axis=1)
df.head()
Van egy unknown oszlop, ettől is szabaduljunk meg!
In [ ]:
df = df.drop('unknown', axis=1)
A describe függvény oszloponként szolgáltat alapvető infomációkkal: darabszám, átlag, szórás stb.
Mivel a legtöbb mező bináris, most nem tudunk meg sok hasznos információt a mezőkről.
In [ ]:
df.describe()
In [ ]:
df.quantile(.9).head()
In [ ]:
df[df.release_date.dt.year == 1956]
In [ ]:
d = df[(df.release_date.dt.year >= 1980) & (df.release_date.dt.year < 1990)]
len(d)
107 film jelent meg a 80-as években, ezt már nem praktikus kiíni. Nézzük meg csak az első 3-at.
In [ ]:
d.head(3)
Többször fogjuk még használni a megjelenési évet, ezért praktikus külön év oszlopot létrehozni.
A DateTime mezőhöz használható metódusok és attribútumok a dt névtérben vannak, így tudjuk minden oszlopra egyszerre meghívni. Az eredményt egy új oszlopban tároljuk.
In [ ]:
df['year'] = df.release_date.dt.year
In [ ]:
df[df.title == 'Die Hard']
Sajnos csak teljes egyezésre tudunk így szűrni.
A szöveges mezőkre a pandas nyújt egy csomó műveletet, amik az str névtérben vannak (ahogy a dátum mezőkre a dt-ben voltak).
In [ ]:
df[df.title.str.contains('Die Hard')]
A Die Hard 4 és 5 hiányzik. Kilógnának az adatsorból? Nézzük meg még egyszer, hogy mikori filmek szerepelnek.
In [ ]:
df.release_date.describe()
A Die Hard 4 és 5 2007-ben, illetve 2013-ban jelentek meg, ezért nem szerepelnek az adatban.
In [ ]:
d = df[(df.action==1) & (df.romance==1)]
print(len(d))
d.head()
In [ ]:
d = df[(df.action==1) | (df.romance==1)]
print(len(d))
d.head()
In [ ]:
def count_movies_before_1985(df):
# YOUR CODE HERE
raise NotImplementedError()
def count_movies_after_1984(df):
# YOUR CODE HERE
raise NotImplementedError()
In [ ]:
before = count_movies_before_1985(df)
print(before)
assert type(before) == int
after = count_movies_after_1984(df)
print(after)
assert type(after) == int
In [ ]:
def child_thriller(df):
# YOUR CODE HERE
raise NotImplementedError()
In [ ]:
title = child_thriller(df)
assert type(title) == str
In [ ]:
def long_titles(df):
# YOUR CODE HERE
raise NotImplementedError()
In [ ]:
title_cnt = long_titles(df)
assert type(title_cnt) == int
In [ ]:
def oldest_movie(df):
# YOUR CODE HERE
raise NotImplementedError()
def newest_movie(df):
# YOUR CODE HERE
raise NotImplementedError()
In [ ]:
oldest = oldest_movie(df)
newest = newest_movie(df)
assert type(oldest) == str
assert type(newest) == str
In [ ]:
def newest_scifi(df):
# YOUR CODE HERE
raise NotImplementedError()
In [ ]:
newest = newest_scifi(df)
assert type(newest) == str
In [ ]:
df.groupby('year').size().plot()
Vonaldiagram az alapértelmezett, de oszlopdiagramként informatívabb lenne.
In [ ]:
df.groupby('year').size().plot(kind='bar')
Lásztik, hogy a 80-as évek végén nőtt meg a kiadott filmek száma, kicsit közelítsünk rá. Ehhez először szűrni fogjuk a 1985 utáni filmeket, majd csoportosítva ábrázolni.
In [ ]:
d = df[df.year > 1985]
d.groupby('year').size().plot(kind='bar')
# df[df.year > 1985].groupby('year').size().plot(kind='bar') # vagy egy sorban
Nem csak egy kategóriaértékű oszlop szerint csoportosíthatunk, hanem tetszőleges kifejezés szerint. Ezt kihasználva fogunk évtizedenként csoportosítani. A groupby-nak bármilyen kifejezést megadhatunk, ami diszkrét értékekre képezi le a sorokat, tehát véges sok csoport egyikébe helyezi (mint egy hash függvény).
Az évtizedet úgy kaphatjuk meg, ha az évet 10-zel osztjuk és csak az egészrészt tartjuk meg, hiszen 1983/10 és 1984/10 egészrésze ugyanúgy 198. Használjuk a Python egészosztás operátorát (//).
In [ ]:
d = df.groupby(df.year // 10 * 10)
d.groups.keys() # létrejött csoportok listázása
In [ ]:
d.size().plot(kind='bar')
In [ ]:
def comedy_by_year(df):
# YOUR CODE HERE
raise NotImplementedError()
In [ ]:
c = comedy_by_year(df)
assert type(c) == pd.core.groupby.DataFrameGroupBy
In [ ]:
# YOUR CODE HERE
raise NotImplementedError()
In [ ]:
col1 = 'children'
col2 = 'crime'
d = df[['year', col1, col2]].copy()
d['diff'] = d[col1] - d[col2]
d.groupby(d.year // 10 * 10).sum()
In [ ]:
d.groupby(d.year // 10 * 10).sum().plot(y='diff', kind='bar')
A 90-es években több filmet adtak ki, mint előtte összesen, nézzük meg azt az évtizedet közelebbről!
In [ ]:
def groupby_nineties(d):
# YOUR CODE HERE
raise NotImplementedError()
In [ ]:
nineties = groupby_nineties(d)
assert type(nineties) == pd.core.groupby.DataFrameGroupBy
# a diff oszlop szerepel
assert 'diff' in nineties.sum()
In [ ]:
# YOUR CODE HERE
raise NotImplementedError()
Tortadiagramot a plot függvény kind="pie" argumentumával tudsz készíteni.
A tortadiagramhoz érdemes megváltoztatni a diagram képarányát, amit a plot függvény figsize paraméterének megadásával tehetsz meg. figsize=(10,10). Százalékokat a autopct="%.0lf%%" opcióval lehet a diagramra írni.
A tortadiagramot szebbé teheted másik colormap választásával: dokumentáció és a colormapek listája.
In [ ]:
def groupby_release_day(df):
# YOUR CODE HERE
raise NotImplementedError()
In [ ]:
by_day = groupby_release_day(df)
assert type(by_day) == pd.core.groupby.DataFrameGroupBy
# legfeljebb 31 napos egy hónap
assert len(by_day) < 32
# nehogy a hét napjai szerint csoportosítsunk
assert len(by_day) > 7
In [ ]:
# YOUR CODE HERE
raise NotImplementedError()
In [ ]:
def groupby_initial_letter(df):
# YOUR CODE HERE
raise NotImplementedError()
In [ ]:
initial = groupby_initial_letter(df)
assert type(initial) == pd.core.groupby.DataFrameGroupBy
In [ ]:
# YOUR CODE HERE
raise NotImplementedError()
In [ ]:
def get_largest_group(df, groupby_columns):
# YOUR CODE HERE
raise NotImplementedError()
In [ ]:
genres = ["drama"]
drama_largest = get_largest_group(df, genres)
assert type(drama_largest) == pd.DataFrame
assert len(drama_largest) == 957
genres = ["drama", "comedy"]
both_largest = get_largest_group(df, genres)
# a csoportban minden film comedy es drama cimkeje azonos
assert both_largest[["comedy", "drama"]].nunique().loc["comedy"] == 1
assert both_largest[["comedy", "drama"]].nunique().loc["drama"] == 1
print(both_largest.shape)
Az adathalmaz lényegi része a 100000 értékelés, amit az u.data fájlból tudunk beolvasni. A README-ből kiolvashatjuk a fájl oszlopait.
In [ ]:
cols = ['user', 'movie_id', 'rating', 'timestamp']
ratings = pd.read_table(os.path.join(data_dir, "u.data"), names=cols)
In [ ]:
ratings.head()
A timestamp oszlop Unix timestampeket tartalmaz, konvertáljuk DateTime-má.
In [ ]:
ratings['timestamp'] = pd.to_datetime(ratings.timestamp, unit='s')
ratings.head()
Mivel már több DataFrame-mel dolgozunk, érdemes a filmeket tartalmazó táblának beszédesebb nevet adni.
In [ ]:
movies = df
Felülírjuk a ratings táblát:
In [ ]:
ratings = pd.merge(ratings, movies, left_on='movie_id', right_index=True)
ratings.head()
In [ ]:
len(ratings[ratings.timestamp <= ratings.release_date])
In [ ]:
ratings[ratings.timestamp <= ratings.release_date].title.value_counts()
In [ ]:
def count_greater_than_4(ratings):
# YOUR CODE HERE
raise NotImplementedError()
In [ ]:
greater = count_greater_than_4(ratings)
assert type(greater) == int
assert greater != 1160 # titles are NOT UNIQUE
In [ ]:
ratings.hist('rating', bins=5)
In [ ]:
def filter_old_crime_movies(ratings):
# YOUR CODE HERE
raise NotImplementedError()
In [ ]:
old_crime_movies = filter_old_crime_movies(ratings)
assert type(old_crime_movies) == pd.DataFrame
In [ ]:
# YOUR CODE HERE
raise NotImplementedError()
In [ ]:
def rating_mean_by_decade(ratings):
# YOUR CODE HERE
raise NotImplementedError()
In [ ]:
decade_mean = rating_mean_by_decade(ratings)
# csak az ertekeles oszlop atalga erdekel minket, nem az egesz DataFrame-e
assert not type(decade_mean) == pd.DataFrame
assert type(decade_mean) == pd.Series
assert 1920 in decade_mean.index
assert 1921 not in decade_mean.index
In [ ]:
def rating_mean_by_weekday(ratings):
# YOUR CODE HERE
raise NotImplementedError()
In [ ]:
weekday_mean = rating_mean_by_weekday(ratings)
assert type(weekday_mean) == pd.Series
assert type(weekday_mean) != pd.DataFrame # csak egy oszlop kell
In [ ]:
def adventure_monthly_std(ratings):
# YOUR CODE HERE
raise NotImplementedError()
In [ ]:
adventure = adventure_monthly_std(ratings)
assert type(adventure) == pd.Series
assert type(adventure) != pd.DataFrame
# legfeljebb 12 különböző hónapban érkezhettek értékelések
assert len(adventure) <= 12
In [ ]:
# users = ...
# YOUR CODE HERE
raise NotImplementedError()
In [ ]:
assert type(users) == pd.DataFrame
# user_id starts from 1
assert 0 not in users.index
In [ ]:
# ratings = ratings.merge...
# YOUR CODE HERE
raise NotImplementedError()
In [ ]:
assert type(ratings) == pd.DataFrame
assert ratings.shape == (100000, 30)
In [ ]:
def by_age_group(ratings):
# YOUR CODE HERE
raise NotImplementedError()
In [ ]:
r = by_age_group(ratings)
assert type(r) == pd.Series
assert 20 in r
In [ ]:
# YOUR CODE HERE
raise NotImplementedError()
Tipp:
timestamp mezőt,Készíts egy függvényt, ami egy adott szakma képviselőinek óránkénti értékelésszámát adja vissza.
In [ ]:
def occupation_cnt_by_hour(ratings, occupation):
# YOUR CODE HERE
raise NotImplementedError()
In [ ]:
marketing = occupation_cnt_by_hour(ratings, "marketing")
assert type(marketing) == pd.Series
# 24 órás egy nap
assert len(marketing) < 25
In [ ]:
# YOUR CODE HERE
raise NotImplementedError()
majd a programozók:
In [ ]:
programmer = occupation_cnt_by_hour(ratings, "programmer")
In [ ]:
# YOUR CODE HERE
raise NotImplementedError()
In [ ]:
# YOUR CODE HERE
raise NotImplementedError()
In [ ]:
# YOUR CODE HERE
raise NotImplementedError()
In [ ]:
# YOUR CODE HERE
raise NotImplementedError()
In [ ]:
# YOUR CODE HERE
raise NotImplementedError()
In [ ]:
# YOUR CODE HERE
raise NotImplementedError()
In [ ]:
# YOUR CODE HERE
raise NotImplementedError()
Ebben a feladatban a műfajok alapján fogjuk megkeresni minden filmhez a hozzá leghasonlóbb K filmet. Az eljárás neve k-nearest neighbor (KNN). A scikit-learn tartalmaz több KNN implementációt is, mi most a ball_tree-t fogjuk használni. Az osztály dokumentációja itt található: http://scikit-learn.org/stable/modules/neighbors.html
A DataFrame values attribútumával kérhetünk le mátrixként az értékeket (oszlopnév, index stb. nélkül). Most csak a műfajokat tartalmazó oszlopokat kell megtartani. Vigyázz, az utolsó oszlop az évet tartalmazza!
A mátrix neve legyen X.
In [ ]:
# YOUR CODE HERE
raise NotImplementedError()
In [ ]:
assert type(X) == np.ndarray
Ehhez bele kell nézned a NearestNeighbors dokumentációjába.
Az indexeket az indices változóban tárold.
A legközelebbi szomszédok számát a K változóban tárold. Először állítsd 4-re a K-t, később kísérletezhetsz más értékekkel is. A többi paramétert ne módosítsd, különben a tesztek nem biztos, hogy működnek.
In [ ]:
from sklearn.neighbors import NearestNeighbors
def run_knn(X, K):
# YOUR CODE HERE
raise NotImplementedError()
In [ ]:
K = 4
indices = run_knn(X, K)
assert type(indices) == np.ndarray
# K legközelebbi szomszédot keresünk
assert indices.shape[1] == K
Az indices változó tartalmazza az indexeket, ebből készítsünk DataFrame-et:
In [ ]:
ind = pd.DataFrame(indices)
ind.head()
Értelmezzük a táblázatot! Az index oszlop (első oszlop) azt mondja meg, hogy az X mátrix hányadik sorához tartozó szomszédok találhatók meg a sorban. A 0-3. nevű oszlopok a legközelebbi szomszédokat adják meg. Legtöbb film esetén saját maga a legközelebbi szomszédja, hiszen 0 a távolságuk, azonban nincs mindenhol így. Mit gondolsz, miért?
A táblázat indexe 0-val kezdődik, de a movies táblában a movie_id 1-től indul.
In [ ]:
def increment_table(df):
# YOUR CODE HERE
raise NotImplementedError()
In [ ]:
indices = increment_table(ind)
assert indices.shape[1] == 4
assert indices.index[0] == 1
assert indices.index[-1] == len(indices)
Az indices táblázatban filmcímek helyett indexek vannak, ami nem túl felhasználóbarát. A movies DataFrame tartalmazza a filmeket indexekkel együtt, ezzel kell merge-ölni K alkalommal.
Pl. az első sorban megjelenő 422-es index az Aladdin and the King of Thieves film indexe. Kerüljön ez a cím az index helyére a merge után.
Segítség:
indices tábla oszlopainak nevei most nem stringek, hanem integerek,rename metódussal.df = df.rename(columns={'regi': 'uj', 'masik regi': 'masik uj'})
A filmcímeken kívül minden oszlopot el lehet dobni.
Tipp: érdemes belenézni a kapott táblázatba, hogy reálisak-e az adatok. Pl. a Toy Story szomszédai szintén rajzfilmek lesznek.
In [ ]:
def find_neighbor_titles(movies, indices):
# YOUR CODE HERE
raise NotImplementedError()
In [ ]:
neighbors = find_neighbor_titles(movies, indices)
assert type(neighbors) == pd.DataFrame
assert neighbors.shape[1] == K
Most olyanok a soraink hogy: Jelenleg a táblázat indexe a filmek azonosítója, tehát egy szám:
| nearest1 | nearest 2 | |
|---|---|---|
| 1 | hasonló film címe 1 | hasonló film címe 2 |
Számok helyett a filmcím legyen az index.
| nearest1 | nearest 2 | |
|---|---|---|
| Filmcím | hasonló film címe 1 | hasonló film címe 2 |
Sok filmnek saját maga a legközelebbi szomszédja.
In [ ]:
def recover_titles(movies, neighbors):
# YOUR CODE HERE
raise NotImplementedError()
In [ ]:
most_similar = recover_titles(movies, neighbors)
assert type(most_similar) == pd.DataFrame
assert "Toy Story" in most_similar.index
A függvény visszatérési értéke legyen egy DataFrame, amely a hasonló filmeket tartalmazza (akkor is, ha 1 vagy 0 hasonló film van). A most_similar táblázat szintén a függvény paramétere.
In [ ]:
def recommend_similar_movies(most_similar, title):
# YOUR CODE HERE
raise NotImplementedError()
In [ ]:
die_hard = recommend_similar_movies(most_similar, "Die Hard")
assert type(die_hard) == pd.DataFrame
# there are more than one Die Hard movies
assert len(die_hard) > 1
In [ ]:
asdf_movies = recommend_similar_movies(most_similar, "asdf")
assert type(asdf_movies) == pd.DataFrame
Köszönöm, hogy végigcsináltad a labort, remélem, hogy hasznosnak találtad.
Minden visszajelzés fontos és be fogom építeni következő alkalmakba. Kérlek töltsd ki ezt a rövid kérdőívet
Ha kedvet kaptál a témához, érdemes megnézni a kaggle.com data science oldalt, ahol megnézheted mások kódját (általában egy Jupyter Notebook formájában :)), részt vehetsz versenyeken és sokat tanulhatsz a témáról.