Modelarea Datelor în Python
Autor: Rareș Stanciu, Nitro AI Workshops 2024
Descarcă fișierul folosit în exerciții de aici: filme.csv
Rezolvă în Colab: Modelarea Datelor în Python
Quick read:
Abilitatea de a prelucra si interpreta date este poate cea mai puternica arma din arsenalul fiecarui analist. Acest tutorial are rolul de a introduce notiunile de baza ale bibliotecilor NumPy, Pandas si Matplotlib.
1. NumPy
NumPy este o biblioteca fundamentala pentru Python, fiind folosita pentru calcule matematice pe array-uri multidimensionale si nu numai. Pe langa sintaxa eleganta ofera si o imbunatatire semnificativa a timpului de rulare, folosindu-se de vectorizarea operatiilor. Adesea, fara a profita de aceasta, calculele devin imposibile, avand timpi de rulare mult prea mari pentru a putea fi considerate practice.
1.1 Array-uri in NumPy
import numpy as np
Acest as se foloseste dupa un import pentru a defini un 'alias'
Cand vom folosi libraria vom scrie doar np, in loc de numpy, de fiecare data cand vom vrea sa apelam o functie
In numpy toate operatiile se fac pe array-uri.
x = np.array([4, 3, 8])
y = np.array([1, 2, 7])
print(x, y)
[4 3 8] [1 2 7]
Mai intai sa aflam cate ceva despre aceste array-uri
Putem vedea ca x este sir cu 3 elemente
print(x.shape)
(3,)
Putem vedea dimensiunile lui x
print(x.size)
3
Cate dimensiuni are x
print(x.ndim)
1
Momentan nu se observa nimic special la array-uri fata de listele obisnuite din python, insa numpy adauga multe functii folositoare
Putem aduna doua array-uri
suma = x + y
print(suma)
[ 5 5 15]
Putem face diferenta dintre ele
diferenta = x - y
print(diferenta)
[3 1 1]
Putem inmulti fiecare element in parte
produs_per_element = x * y
print(produs_per_element)
[ 4 6 56]
Putem face si dot productul dintre ei
dot_product = x.dot(y)
print(dot_product)
66
Numpy nu usureaza numai operatiile dintre doua array-uri, ci si prelucrarea individuala a lor
x = np.array([1, 2, 3, 4, 5])
Pot aduna o constanta la fiecare element al lui x
print(x + 7)
[ 8 9 10 11 12]
Pot inmulti fiecare element cu o constanta
print(x * 4.5)
[ 4.5 9. 13.5 18. 22.5]
Pot chiar ridica fiecare element la o putere
print(x ** 3)
[ 1 8 27 64 125]
Pot chiar si combina aceste operatii
print(x ** 2 + 2 * x + 1)
[ 4 9 16 25 36]
Numpy ofera si operatii deja implementate ce se pot aplica pe array, spre exemplu:
x = np.array([1, 3, 8, 10, 11])
Suma elementelor
suma = np.sum(x)
print(suma)
33
Media elementelor
medie = np.mean(x)
print(medie)
6.6
Elementul median
median = np.median(x)
print(median)
8.0
Deviatia standard
std = np.std(x)
print(std)
3.9293765408777004
Elementul minim si cel maxim
minn, maxx = np.min(x), np.max(x)
print(minn, maxx)
1 11
Pozitia elementului minim si pozitia elementului maxim
poz_min, poz_max = np.argmin(x), np.argmax(x)
print(poz_min, poz_max)
0 4
1.2 Matrice in NumPy
Poate cel mai important, numpy se descurca excelent si cu matrice sau orice container multidimensional
A = np.array([[5, 2, 4], [3, 2, 6]])
B = np.array([[1, 7], [9, 9], [2, 2]])
print(A, '\n -------- \n', B)
[[5 2 4]
[3 2 6]]
--------
[[1 7]
[9 9]
[2 2]]
Asa putem afla transpusa unei matrice
transpusa = A.T
print(transpusa)
[[5 3]
[2 2]
[4 6]]
Evident putem si inmulti matricele folosind operatorul '@' sau functia .dot()
prod = A @ B # sau prod = A.dot(B)
print(prod)
[[31 61]
[33 51]]
Functiile de la array-urile unidimensionale functioneaza si pe matrice
Suma tuturor elemenetelor matricei A
suma = np.sum(A)
print(suma)
22
Totusi adesea vom avea nevoie de suma pe coloane sau pe randuri, pentru asta vom folosi argumentul axis
suma_pe_randuri = np.sum(A, axis=1)
suma_pe_coloane = np.sum(A, axis=0)
print(suma_pe_randuri, suma_pe_coloane)
[11 11] [ 8 4 10]
Cum stiti ce sa puneti aici? Ei bine va ganditi ce axa vreti sa eliminati. In cazul coloanelor a trebuit sa eliminam randurile(axa 0) ca sa ramanem cu coloanele
In mod identic se procedeaza si cu celelalte functii
1.3 Alte funcii folositoare in NumPy
De multe ori va fi nevoie sa schimbam forma unui array in numpy Ei bine, libraria face acest proces foarte simplu
Sa zicem ca vrem sa il transformam pe x dintr-un sir cu 6 elemente intr-o matrice de forma (2, 3)
x = np.array([1, 2, 3, 4, 5, 6])
print(x, x.shape)
[1 2 3 4 5 6] (6,)
Atunci este nevoie de functia reshape si de tuple-ul (2, 3) ca argument
x = x.reshape((2, 3))
print(x, x.shape)
[[1 2 3]
[4 5 6]] (2, 3)
Poti folosi -1 pentru a scapa de dimensiunile ce nu au fost mentionate explicit anterior
x = x.reshape(-1)
print(x)
[1 2 3 4 5 6]
Numpy este folositor si pentru generarea rapida de date sintetice, spre exemplu
Functia linspace va genera 11 numere egal departate din intervalul [0, 10]
x = np.linspace(0, 5, 11)
print(x)
[0. 0.5 1. 1.5 2. 2.5 3. 3.5 4. 4.5 5. ]
Functia arange este fratele lui linspace, insa va primi ca argument pasul, nu cantitatea de numere si NU include sfarsitul intervalului
x = np.arange(0, 5, 0.5)
print(x)
[0. 0.5 1. 1.5 2. 2.5 3. 3.5 4. 4.5]
Asa generam un sir de 15 numere intregi random aflate in intervalul [0, 100)
x = np.random.randint(0, 100, 15)
print(x)
[13 35 69 13 72 51 58 74 27 23 31 26 64 13 81]
Astfel generam un sir de 15 numere de tip float random aflate in intervalul [0, 1)
x = np.random.rand(15)
print(x)
[0.6918043 0.83941958 0.43852082 0.95058614 0.54191751 0.28236312
0.66224165 0.12793607 0.5980543 0.46931181 0.20114682 0.3299257
0.96231553 0.24684537 0.29071311]
Media si deviatia standard
mu, sigma = 0, 0.1
Genereaza o distributie normala cu 1000 de elemente ce au media mu si deviatia standard sigma
s = np.random.normal(mu, sigma, 1000)
s[:10]
array([-0.07903164, -0.01650014, 0.04181418, -0.03856771, -0.04199796,
0.08980587, 0.04769363, 0.05129405, -0.03631649, -0.04437373])
Folosit pentru a genera un sir de 1-uri de lungime 10
unu = np.ones(10)
print(unu)
[1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
Folosit pentru a genera un sir de 0-uri de lungime 10
zero = np.zeros(10)
print(zero)
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
Adaugand like putem genera o matrice de 1-uri identice in forma cu argumentul
A = np.array([[1, 2, 3], [4, 5, 6]])
unu_A = np.ones_like(A)
print(unu_A)
[[1 1 1]
[1 1 1]]
1.4 Indexare si slicing
Este important sa intelegem cum functioneaza accesarea submatricelor in numpy, deoarece este un concept foarte des intalnit.
Pentru inceput vom face o matrice A de marime (5, 5) cu elemente numerele de la 1 la 25
A = np.arange(1, 26, 1).reshape((5, 5))
print(A)
[[ 1 2 3 4 5]
[ 6 7 8 9 10]
[11 12 13 14 15]
[16 17 18 19 20]
[21 22 23 24 25]]
Mai intai sa accesam elementul de pe al 3-lea rand si a 4-a coloana. Asa cum in python indexarea incepe de la 0, asa incepe si in numpy, astfel trebuie sa accesam elementul de pe pozitia (2, 3)
print(A[2][3])
14
Cum selectam randuri sau coloane in numpy? Ei bine selectarea unui rand se va face ca in python
primul_rand = A[0]
print(primul_rand)
[1 2 3 4 5]
La fel si cu coloanele. Acel ';' semnifica selectarea tuturor randurilor in cadrul celei de a treia coloana
a_treia_coloana = A[:, 2]
print(a_treia_coloana)
[ 3 8 13 18 23]
Desi nu l-am folosit si la rand, deoarece pythonul il considera implicit, il putem adauga si acolo pentru o intelegere mai clara
primul_rand = A[0, :] # pentru primul rand imi doresc fiecare coloana
print(primul_rand)
[1 2 3 4 5]
Cum selectam o submatrice? Ei bine va trebui sa specificam range-ul in jurul lui ':' Sa zicem ca vrem sa selctam submatricea cu coltul stanga sus (1, 2) si coltul dreapta jos (4, 3) atunci:
submatrice = A[1:5, 2:4] # ma intereseaza randurile 1, 2, 3 si 4 => 1:5 si pentru fiecare rand coloanele 2 si 3 => 2:4
print(submatrice)
[[ 8 9]
[13 14]
[18 19]
[23 24]]
Se poate realiza si selectarea mai multor randuri sau coloane 'fixe', spre exemplu:
B = A[[0, 1, 4]] # Voi pastra din A randurile 0, 1 si 4
print(B)
[[ 1 2 3 4 5]
[ 6 7 8 9 10]
[21 22 23 24 25]]
C = A[:, [1, 2, 4]] # Voi pastra din A coloanele 1, 2 si 4
print(C)
[[ 2 3 5]
[ 7 8 10]
[12 13 15]
[17 18 20]
[22 23 25]]
Pentru a combina selectarea si a unor randuri fixe, dar si a unor coloane fixe este nevoie de slicing avansat. Voi pastra elementele de pe randurile 0, 1 si 4, care se afla pe coloanele 1, 2 sau 4
randuri = [0, 1, 4]
coloane = [1, 2, 4]
M = A[np.ix_(randuri, coloane)]
print(M)
[[ 2 3 5]
[ 7 8 10]
[22 23 25]]
1.5 De ce NumPy?
Pana acum am explorat doar de ce ne face viata mai usoara, dar nu am zis nimic despre viteza bibliotecii. Mai jos, o sa demonstrez prin cateva exemple practice, de ce operatiile vectorizante sunt absolut necesare
Suma elementelor cu un for versus np.sum()
def suma_elementelor_fara_np(x):
suma = 0
for nr in x:
suma += nr
return suma
def suma_elementelor_np(x):
return np.sum(x)
x = np.random.rand(10000000)
%%timeit
suma_elementelor_fara_np(x)
1.21 s ± 24.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%%timeit
suma_elementelor_np(x)
10.1 ms ± 1.49 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)
def inmultire_matrice_fara_np(A, B):
if A.shape[1] != B.shape[0]:
return False
N, M, K = A.shape[0], B.shape[1], A.shape[1]
C = [[0 for i in range(N)] for j in range(M)]
for i in range(N):
for j in range(M):
for k in range(K):
C[i][j] += A[i][k] * B[k][j]
return C
def inmultire_matrice_np(A, B):
if A.shape[1] != B.shape[0]:
return False
return A @ B
A = np.random.rand(10000).reshape(100, 100)
B = np.random.rand(10000).reshape(100, 100)
%%timeit
inmultire_matrice_fara_np(A, B)
1.21 s ± 667 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%%timeit
inmultire_matrice_np(A, B)
113 µs ± 31.4 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
WOW! Suma elementelor a fost de aproximativ 100 de ori mai rapida si inmultirea matricelor a fost de aproape 10.000 de ori mai rapida. Pe langa factorul de viteza, codul a fost si mult muult mai scurt si elegant. Din acest motiv NumPy nu trebuie sa lipseasca din arsenalul niciunui utilizator de Python.
1.6 Exercitii
- Generati un sir 'x' de 20 de elemente intregi aflate in intervalul [10, 25] si aflati minimul, maximul, media si suma elementelor din sir
- Folosind sirul 'x' de la exercitiul 1, transformati-l intr-o matrice de marime (2, 5, 2) si afisati elementul de pe pozitia (0, 3, 1) iar apoi transformati-l intr-o matrice de marime (4, 5) si sa afisati suma elementelor de fiecare rand si de pe fiecare coloana
- Creati o matrice de marime (5, 6) cu elemente random de tip float cu valori aflate in intervalul (0, 1] si afisati submatricea cu coltul stanga sus (1, 3) si coltul dreapta jos (4, 5)
- Recreati aceasta matrice fara a scrie vreun for:
[[ 2 3 4 5 6 7] [ 6 7 8 9 10 11] [14 15 16 17 18 19] [18 19 20 21 22 23] [26 27 28 29 30 31] [30 31 32 33 34 35]] ``` ```python
2. Pandas
Pandas este o biblioteca de analiza a datelor, care ofera structuri precum DataFrame si Series pentru a organiza datele tabulare.
Pentru aceasta parte a tutorialului am pregatit un set de date despre filmele de pe IMDb. Pe aceste filme voi prezenta cateva functii si operatii pe care le puteti folosi pe un DataFrame. Fisierul filme.csv poate fi gasit in acelasi repo si trebuie adaugat in notebook, inainte sa fie folosit in cod.
Acest set de date este o versiune simplificata a acestuia de pe kaggle: https://www.kaggle.com/datasets/raedaddala/top-500-600-movies-of-each-year-from-1960-to-2024.
2.1 Familiarizarea cu setul de date
Inainte de ne apuca de orice proiect este necesar sa intelegem datele cu care va trebui sa lucram. Din fericire, Pandas ne face treaba usoara.
Folosim un alias asemanator ca la numpy
import pandas as pd
In pandas vom lucra de cele mai multe ori cu date citite dintr-un fisier de tip csv. Pentru a citi dintr-nu astfel de fisier folosim comanda read_csv
data = pd.read_csv('filme.csv')
Acum ca am introdus setul de date in notebook, haideti sa il si vedem
Folosind functia head() putem vedea primele 10 filme din setul de date
data.head(3)
Acum ca ne-am facut o prima impresie, a venit momentul sa studiem fiecare coloana in parte. Pentru a gasi coloanele, putem accesa atributul columns al lui data
data.columns
Index(['id', 'Title', 'Movie Link', 'Year', 'Duration', 'MPA', 'Rating',
'Votes', 'budget', 'grossWorldWide', 'opening_weekend_Gross', 'genres'],
dtype='object')
info() ne ofera informatii despre tipul de date si cate valori nu lipsesc
data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 33599 entries, 0 to 33598
Data columns (total 12 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 id 33599 non-null object
1 Title 33599 non-null object
2 Movie Link 33599 non-null object
3 Year 33599 non-null int64
4 Duration 33378 non-null object
5 MPA 25624 non-null object
6 Rating 33461 non-null float64
7 Votes 33461 non-null object
8 budget 11814 non-null float64
9 grossWorldWide 18221 non-null float64
10 opening_weekend_Gross 15522 non-null float64
11 genres 33217 non-null object
dtypes: float64(4), int64(1), object(7)
memory usage: 3.1+ MB
describe() ne ofera un rezumat matematic al coloanelor ce contin numere
data.describe()
2.2 Lucrul cu date
Putem accesa coloanele de parca lucram cu un dictionar si anume:
data['Year'] # asta va returna coloana ce contine aniii
Vedem ca o coloana este de fapt un Series, adica un sir
type(data['Year'])
pandas.core.series.Series
def __init__(data=None, index=None, dtype: Dtype | None=None, name=None, copy: bool | None=None, fastpath: bool | lib.NoDefault=lib.no_default) -> None
One-dimensional ndarray with axis labels (including time series). Labels need not be unique but must be a hashable type. The object supports both integer- and label-based indexing and provides a host of methods for performing operations involving the index. Statistical methods from ndarray have been overridden to automatically exclude missing data (currently represented as NaN). Operations between Series (+, -, /, \*, \*\*) align values based on their associated index values-- they need not be the same length. The result index will be the sorted union of the two indexes. Parameters ---------- data : array-like, Iterable, dict, or scalar value Contains data stored in Series. If data is a dict, argument order is maintained. index : array-like or Index (1d) Values must be hashable and have the same length as `data`. Non-unique index values are allowed. Will default to RangeIndex (0, 1, 2, ..., n) if not provided. If data is dict-like and index is None, then the keys in the data are used as the index. If the index is not None, the resulting Series is reindexed with the index values. dtype : str, numpy.dtype, or ExtensionDtype, optional Data type for the output Series. If not specified, this will be inferred from `data`. See the :ref:`user guide <basics.dtypes>` for more usages. name : Hashable, default None The name to give to the Series. copy : bool, default False Copy input data. Only affects Series or 1d ndarray input. See examples. Notes ----- Please reference the :ref:`User Guide <basics.series>` for more information. Examples -------- Constructing Series from a dictionary with an Index specified >>> d = {'a': 1, 'b': 2, 'c': 3} >>> ser = pd.Series(data=d, index=['a', 'b', 'c']) >>> ser a 1 b 2 c 3 dtype: int64 The keys of the dictionary match with the Index values, hence the Index values have no effect. >>> d = {'a': 1, 'b': 2, 'c': 3} >>> ser = pd.Series(data=d, index=['x', 'y', 'z']) >>> ser x NaN y NaN z NaN dtype: float64 Note that the Index is first build with the keys from the dictionary. After this the Series is reindexed with the given Index values, hence we get all NaN as a result. Constructing Series from a list with `copy=False`. >>> r = [1, 2] >>> ser = pd.Series(r, copy=False) >>> ser.iloc[0] = 999 >>> r [1, 2] >>> ser 0 999 1 2 dtype: int64 Due to input data type the Series has a `copy` of the original data even though `copy=False`, so the data is unchanged. Constructing Series from a 1d ndarray with `copy=False`. >>> r = np.array([1, 2]) >>> ser = pd.Series(r, copy=False) >>> ser.iloc[0] = 999 >>> r array([999, 2]) >>> ser 0 999 1 2 dtype: int64 Due to input data type the Series has a `view` on the original data, so the data is changed as well.
Putem afisa media unei coloane, fie cu mean() direct sau cu numpy
print(data['Year'].mean(), np.mean(data['Year']))
1992.3926902586386 1992.3926902586386
Pe serii putem aplica transformari asemanatoare cu cele de pe siruri
Spre exemplu:
data['Year'] ** 2 + data['Year'] + 5
ATENTIE! Cu aceasta linie doar afisez, nu si schimb coloana cu ani
Cand analizam date, de multe ori vrem sa scapam de numere prea mari sau prea mici
Asa ca una dintre cele mai comune metode de a scapa de acele valori cidudate este prin a normaliza
Poate cea mai simpla normalizare este Min-Max, care va reduce fiecare numar intre 0 si 1, adica:
v[i] = (v[i] - min(v)) / (max(v) + eps), unde eps este un numar foarte mic folosit pentru a evita impartirea la 0
Haideti sa normalizam coloana noastra de buget
(data['budget'] - data['budget'].min()) / (data['budget'].max() + 1e-6)
Adesea va fi nevoie sa cream coloane noi care sa ne ajute in analiza datelor
Sa zicem ca ne-a angajat un studio care se ocupa de filme de groaza sa facem o analiza de piata
Astfel, ar fi util sa vedem ce filme sunt Horror si daca au fost profitabile sau nu
O sa facem mai intai un 'feature' este_horror, care va dicta daca filmul este horror sau nu
data['este_horror'] = data['genres'].str.contains('Horror', case=False)
data.head(3)
Acum vom face o coloana este_profitabil, care ne va spune daca filmul a fost profitabil sau nu
Fie un film profitabil daca grossWorldWide > budget, atunci:
data['este_profitabil'] = data['grossWorldWide'] > data['budget']
data.head(3)
2.3 Filtrarea datelor
Cum putem aplica filtre usoare asupra setului nostru de date?
Pentru localizarea unor celule specifice ne folosim 2 metode: .loc si .iloc
Prima varianta este .loc si ea arata cam asa:
data.loc[randuri, coloane] => selecteaza din data toate randurile din 'randuri' si pastraza doar coloanele din 'coloane'
De retinut este ca .loc se foloseste atat de etichete cat si indexuri(similar cu ce avem la liste)
Spre exemplu, sa zicem ca vrem sa gasim primele 100 de randuri si sa pastram doar coloanele 'MPA' si 'Rating', atunci:
data.loc[:99, ['MPA', 'Rating']] # ATENTIE! .loc include si capatul range-ului
Haideti sa ne intoarcem la exemplul nostru cu filmele horror, care sunt si profitabile
Pentru a realiza filtrarile necesare pentru a le gasi va voi prezenta doua variante: .loc si apply
.loc este varianta mai simpla in opinia mea, dar si mai putin flexibila
filme_bune = data.loc[data['este_horror'] & data['este_profitabil']] # observatie! poate fi omis .loc
filme_bune.head(3)
.apply() este mai flexibila, intrucat foloseste o functie de filtrare, dar e si mai lunga de scris
def este_bun(film):
return film['este_horror'] and film['este_profitabil']
filme_bune = data[data.apply(este_bun, axis=1)]
filme_bune.head(3)
2.4 Exercitii
- Gasiti filmele care au fost lansate in anul 2000 sau mai tarziu
- Gasiti filmele care se incadreaza in a doua jumatate a castigurilor('grossWorldWide)
- Adaugati o coloana noua numita 'este_comedie' care sa verifice daca filmul este de comedie sau nu
- Pentru filmele de comedie sa se gaseasca cele cu rating mai mare ca 8
- Sa se calculeze profitul minim, maxim si media profiturilor pentru filmele gasite la exercitiul 4
unde profitul este definit ca (grossWorldWide - budget)
3. Matplotlib.pyplot
Matplotlib.pyplot sau plt pe scurt este cea mai populara biblioteca de vizualizare a datelor. In continuare va voi arata cateva tipuri de grafice.
import matplotlib.pyplot as plt
3.1 Grafice liniare
Graficul liniar este utilizat pentru a afisa o relatie continua intre doua variabile. De obicei, arta cum o variabila (pe axa y) se modifica in functie de alta (pe axa x).
x = np.linspace(0, 10, 100) # 100 de valori între 0 și 10
y = np.sin(x)
plt.figure(figsize=(6, 4))
plt.plot(x, y, label="sin(x)", color="blue")
plt.title("Grafic Liniar")
plt.xlabel("x")
plt.ylabel("sin(x)")
plt.legend()
plt.grid(True)
plt.show()

3.2 Grafic scatter
Graficul scatter utilizeaza puncte pentru a reprezenta valori dintr-un set de date. Fiecare punct reprezinta o observatie, coordonatele acesteia fiind definite de doua variabile.
x = np.random.rand(50)
y = np.random.rand(50)
plt.figure(figsize=(6, 4))
plt.scatter(x, y, color="red", alpha=0.7)
plt.title("Grafic Scatter")
plt.xlabel("x")
plt.ylabel("y")
plt.show()

3.3 Grafic de bare
Graficul de bare reprezinta datele categorice folosind bare dreptunghiulare, unde lungimea fiecarei bare este proportionala cu valoarea sa.
categories = ['A', 'B', 'C', 'D']
values = [5, 7, 3, 8]
plt.figure(figsize=(6, 4))
plt.bar(categories, values, color='green')
plt.title("Grafic de Bare")
plt.xlabel("Categorii")
plt.ylabel("Valori")
plt.show()

3.4 Grafic histograma
O histograma este utilizata pentru a repartiza distributia frecventei unei singure variabile. Datele sunt grupate in intervale (bins).
distributie = np.random.randn(1000) # 1000 de valori dintr-o distribuție normală
plt.figure(figsize=(6, 4))
plt.hist(distributie, bins=20, color='purple', alpha=0.8)
plt.title("Histogramă")
plt.xlabel("Valori")
plt.ylabel("Frecvență")
plt.show()

3.5 Exercitii
Pentru a le rezolva ne vom intoarce la setul de date din pandas, pentru a demonstra cat de bine se integreaza plt alaturi de pandas.
- Creeaza un grafic de bare care sa arate numarul de filme lansate in fiecare an
- Creeaza un scatter plot care sa arate relatia dintre buget(budget) si incasarile globale(grossWorldWide)
- Creeaza o histograma pentru a analiza cum sunt distribuite ratingurile(Rating) in setul de date
- Creeaza un grafic liniar care sa arate cum a evoluat bugetul mediu al filmelor de-a lungul anilor(Year)