Introducere în Pandas: Analiza temperaturilor din România

În exercițiul Statistici SARS-CoV-2 am analizat împreună niște date. Dacă vă gândiți la efortul depus, bună parte din muncă a fost dedicată citirii datelor din fișier. Acela era și scopul principal al exercițiului. Cu toate acestea, ne așteptăm ca procesul de citire a datelor să fie destul de similar pentru multe analize. De asemenea, ne putem gândi la o serie de operații comune, pe care le aplicăm destul de des încât să nu merite rescrise de fiecare dată.

Biblioteca pandas oferă funcționalități pentru manipularea facilă a datelor, ce ne vor face munca mult mai ușoară. În această lecție vom face un scurt tur prin funcționalitățile oferite, în timp ce vom încerca să aflăm lucruri noi analizând datele climatologice publicate de Guvernul României.

Documentația oficială pentru pandas poate fi gasită aici - acesta este locul unde veți găsi răspuns la toate întrebările legate de această bibliotecă. De fiecare dată când folosim o nouă funcție, investiți câteva secunde în a răsfoi documentația oficială pentru funcția respectivă. Vă veți mulțumi mai târziu!

Cum stocăm datele?

Formatul în care datele sunt colectate și stocate depinde foarte mult de tipul de date, sursa datelor, cantitatea de date și tipul de analiză pe care dorim să îl facem.

Pentru moment, vom presupune că operăm cu relativ putine date, ce pot fi scrise într-un fișier în format text. Un format popular pentru acest tip de date este CSV (Comma Separated Values). Este doar un fișier text în care putem scrie un tabel ale cărui celulele sunt separate prin virgule, similar cu un tabel Microsoft Excel. Motivul pentru care acest format este atât popular este simplitatea lui: este doar un fișier text, ce poate fi prelucrat de orice editor de text. Alte formate pentru fișiere, precum .xls, folosit de Excel, au nevoie de programe specializate, ce adesea necesită licențe scumpe.

Un exemplu de fișier CSV este:

CODST,ALT,LAT,LON,DATCLIM,TMED,TMAX,TMIN,R24
15015,503.00,47.7769444,23.9405556,2016/01/01 00, -10.0,  -5.7, -13.6,
15015,503.00,47.7769444,23.9405556,2016/01/02 00, -11.0,  -5.9, -14.4,
15015,503.00,47.7769444,23.9405556,2016/01/03 00, -12.2,  -5.5, -16.4,
15015,503.00,47.7769444,23.9405556,2016/01/04 00, -11.0,  -5.3, -16.6,    .0
15015,503.00,47.7769444,23.9405556,2016/01/05 00,  -5.7,  -4.6,  -8.1,   1.5

Acest fișier conține date climatologice de la 23 de stații esențiale de pe teritoriul României. Prima linie conține o descriere a coloanelor, iar valorile sunt separate prin virgulă.

Citirea datelor

Biblioteca pandas pune la dispoziție funcționalități pentru citirea datelor din multe formate comune, precum CSV sau Excel.

In [28]:
# Este prima dată când folosim pandas, deci trebuie să importăm biblioteca
# O importăm cu prescurtarea pd - o tehnică comună pentru a scurta codul
import pandas as pd

# Fișierul CSV poate fi citit folosind funcția read_csv
# Aruncați un ochi pe documentația funcției: https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.read_csv.html#pandas.read_csv
# După cum observați, are un număr impresionant de argumente ce permit o mare flexibilitate
climatic_2016 = pd.read_csv('Data/RoGovClimatic/climrbsn2016.csv')
In [29]:
# Tipul de date returnat de read_csv este DataFrame - tipul fundamental în Pandas pentru manipularea datelor
# Putem inspecta un DataFrame doar scriindu-i numele într-o celula
climatic_2016
Out[29]:
CODST ALT LAT LON DATCLIM TMED TMAX TMIN R24
0 15015 503.0 47.776944 23.940556 2016/01/01 00 -10.0 -5.7 -13.6 NaN
1 15015 503.0 47.776944 23.940556 2016/01/02 00 -11.0 -5.9 -14.4 NaN
2 15015 503.0 47.776944 23.940556 2016/01/03 00 -12.2 -5.5 -16.4 NaN
3 15015 503.0 47.776944 23.940556 2016/01/04 00 -11.0 -5.3 -16.6 0.0
4 15015 503.0 47.776944 23.940556 2016/01/05 00 -5.7 -4.6 -8.1 1.5
... ... ... ... ... ... ... ... ... ...
8413 15480 12.8 44.213889 28.645556 2016/12/27 00 2.1 5.6 -0.6 0.3
8414 15480 12.8 44.213889 28.645556 2016/12/28 00 0.7 2.1 -1.5 0.4
8415 15480 12.8 44.213889 28.645556 2016/12/29 00 1.6 2.7 0.5 0.0
8416 15480 12.8 44.213889 28.645556 2016/12/30 00 0.4 1.5 -0.5 0.6
8417 15480 12.8 44.213889 28.645556 2016/12/31 00 -1.5 1.5 -2.8 0.0

8418 rows × 9 columns

https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.shape.htmlÎnainte de orice analiză, trebuie să înțelegem datele. Uitându-ne la rezultatul celulei de mai sus, putem face multe observații despre structura datelor noastre.

Descrierea oficială ne spune că tabelul conține: Date climatologice de la cele 23 de stații meteorologice esențiale (flux RBSN: 15015-Ocna Sugatag, 15020-Botoșani, 15090-Iași, 15108-Ceahlău Toaca, 15120-Cluj-Napoca, 15150-Bacău, 15170-Miercurea Ciuc, 15200-Arad, 15230-Deva, 15260-Sibiu, 15280-Vârfu Omu, 15292-Caransebeș, 15310-Galați, 15335-Tulcea, 15346-Râmnicu Valcea, 15350-Buzău, 15360-Sulina, 15410-Drobeta Turnu Severin, 15420-București-Băneasa, 15450-Craiova, 15460-Călărași, 15470-Roșiorii de Vede, 15480-Constanța) care includ temperatura minimă zilnică, temperatura maximă zilnică, temperatura medie zilnică și cantitatea de precipitații zilnice pe intervalul 1961-prezent. Noi analizăm pentru moment doar datele din anul 2016.

Datele sunt aranjate într-un tabel cu 9 coloane, fiecare coloană reprezentând o valoare masurată, iar fiecare linie o măsurătoare. Există o coloană adițională, fără nume, care reprezintă un index. Orice tabel trebuie să aibă una sau mai multe coloane cu rol de index: un identificator unic pentru fiecare observație. În cazul acestor date, nu avem (momentan) un index natural, ca parte din date, așa că vom folosi index-ul construit automat de Pandas.

Pentru claritate, semificația coloanelor este:

CODST - Codul de identificare al stației meteo
ALT - Altitudine
LAT - Latitudine
LON - Longitudine
DATCLIM - Data și ora observației
TMED - Temperatura medie zilnică
TMAX - Temperatura maximă zilnică
TMIN - Temperatura minimă zilnică
R24 - Cantitatea de precipitații zilnice

Este important să înțelegem dimensiunea datelor pe care le avem disponibile și să verificăm dacă sunt date lipsă. Conform descrierii oficiale, ar trebui să avem câte o observație (linie) pe zi din 23 de stații meteorologice pentru fiecare din cele 366 de zile ale anului 2016 (an bisect). Așadar, ne așteptăm la $366 \times 23 = 8418$ linii în tabel:

In [30]:
print('Dimensiunea tabelului este de:', climatic_2016.shape)
Dimensiunea tabelului este de: (8418, 9)

Membrul shape este un tuple ce reprezintă dimensiunea tabelului, în formatul (linii, coloane).

Din fericire, nu ne lipsește nicio observație, întrucât numărul de linii returnat de shape este 8418 - valoarea calculată anterior. Dimensiunea tabelului poate fi văzută și la baza rezultatului celulei care inspectează tabelul.

Ca să aflăm dimensiunea totală a tabelului (numărul de celule din tabel), putem folosi membrul size pus la dispoziție de Pandas.

In [31]:
print('Numărul total de celule din tabel este de:', climatic_2016.size)
Numărul total de celule din tabel este de: 75762

Curățarea datelor

Din diverse motive, precum date lipsă (observați că valoarea pentru R24 în primele 3 randuri lipsește) sau date aflate în formate greșite, trebuie să prelucrăm datele înainte de a le analiza. Un început bun este să folosim metoda info, ce ne prezintă informații utile despre tabel:

In [32]:
climatic_2016.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 8418 entries, 0 to 8417
Data columns (total 9 columns):
 #   Column   Non-Null Count  Dtype  
---  ------   --------------  -----  
 0   CODST    8418 non-null   int64  
 1   ALT      8418 non-null   float64
 2   LAT      8418 non-null   float64
 3   LON      8418 non-null   float64
 4   DATCLIM  8418 non-null   object 
 5   TMED     8418 non-null   float64
 6   TMAX     8418 non-null   float64
 7   TMIN     8418 non-null   float64
 8   R24      3155 non-null   float64
dtypes: float64(7), int64(1), object(1)
memory usage: 592.0+ KB

Aici este descris intervalul index-ului (de la 0 la 8417), cele 9 coloane cu tipul de date asociat și numărul de valori nenule pentru fiecare coloană. Observăm că mai puțin de jumătate din numărul de observații au valori nenule în coloana R24, în timp ce toate celelate coloane sunt complete. Mai observăm că tabelul nostru necesită aproape 600 KB, o dimensiune mică pentru computerele moderne, ceea ce înseamnă că ar trebui să fie relativ ușor de procesat.

Tipuri de date corecte

Într-un DataFrame, toate valorile dintr-o coloană au același tip de date. Putem vizualiza aceste tipuri de date folosind membrul dtypes.

Haideți să ne asigurăm că tipurile de date pentru fiecare coloană sunt corecte.

In [33]:
climatic_2016.dtypes
Out[33]:
CODST        int64
ALT        float64
LAT        float64
LON        float64
DATCLIM     object
TMED       float64
TMAX       float64
TMIN       float64
R24        float64
dtype: object

Deși teoretic putem face o analiză folosind orice tipuri de date, dacă ne asigurăm de la început că avem tipurile corecte pentru fiecare coloană, ne va fi mult mai ușor în timpul analizei propriu-zise. De examplu, este destul de greu să adăugăm 30 de minute la o oră în format str sau să comparăm două date în format str pentru sortare cronologică. Cu toate acestea, dacă datele sunt stocate în formatul corespunzător, aceste operații devin extrem de facile.

Observăm că măsurile climatologice (temperaturi și precipitații) sunt în format float64. Ne asteptăm să fie numere reale, deci este un tip de date corespunzător. Putem dezbate faptul că nu avem nevoie de o precizie atât de mare, și am putea folosi float32, un tip de date pentru numere reale ce folosește doar jumătate din cei 64 de biți necesari stocării unei valori de tip float64. Deși în acest caz, la doar 600 KB, nu va face o diferență practică, vom schimba tipul de date la float32 din motive didactice.

Tipul de date al coloanelor poate fi schimbat folosind metoda astype:

In [34]:
climatic_2016 = climatic_2016.astype({'ALT': 'float32', 'TMED': 'float32', 'TMIN': 'float32', 'TMAX': 'float32', 'R24': 'float32'})

Daca ne uitam iar la informațiile despre tabel,

In [35]:
climatic_2016.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 8418 entries, 0 to 8417
Data columns (total 9 columns):
 #   Column   Non-Null Count  Dtype  
---  ------   --------------  -----  
 0   CODST    8418 non-null   int64  
 1   ALT      8418 non-null   float32
 2   LAT      8418 non-null   float64
 3   LON      8418 non-null   float64
 4   DATCLIM  8418 non-null   object 
 5   TMED     8418 non-null   float32
 6   TMAX     8418 non-null   float32
 7   TMIN     8418 non-null   float32
 8   R24      3155 non-null   float32
dtypes: float32(5), float64(2), int64(1), object(1)
memory usage: 427.6+ KB

, observăm că dimensiunea tabelului a scăzut la aproximativ 430 KB. Putem estima această valoare chiar înainte de a face conversia. Întrucât am schimbat 4 coloane cu 8418 elemente fiecare din float32 in float64, economisind 4 bytes pentru fiecare element:

In [36]:
print('Ar trebui să economism %d bytes, adică %d kilobytes.' % (4 * 8418 * 4, 4 * 8418 * 4 / 1024))
Ar trebui să economism 134688 bytes, adică 131 kilobytes.

Destul de precisă estimarea. Putem folosi astfel de estimări să evaluăm dacă merită efectuată operația sau nu.

Poate vă întrebați de ce nu am schimbat și tipul coloanelor LAT și LON. Pentru asta, trebuie să facem o mică pauză și să înțelegem cum sunt stocate numerele reale.

În formatul cunoscut ca IEEE754, numerele reale sunt memorate cu o tehnică de virgula mobilă (en: floating point): numărul este reținut ca un întreg și un exponent. Astfel, $1234000$ este reținut ca $1234e3$ (notație științifică) sau $1234 * 10 ^ 3$, în timp ce $123.45678$ este reținut ca $123456e-5$ sau $123456 * 10 ^ {-5}$. Tipurile de date float32 și float64 folosesc 32, respectiv 64 de biți pentru a stoca atât întregul cât și exponentul. În cazul valorilor pentru Longitudine și Latitudine, ele au nevoie de un număr întreg mare, cu 8 cifre. Acesta nu poate fi stocat precis în numarul de biți alocați de float32 (32 de biți), așa că vom avea niște aproximări. Haideți să vedem cât de mari ar fi erorile introduse, folosind Pandas!

Ideea e simplă: trebuie să calculăm diferențele, la fiecare element, dintre valoarea în float32 și cea in float64, apoi să vedem care ar fi cea mai mare eroare. Pentru asta, trebuie să accesăm coloana LAT și să scădem din ea valoarea coloanei cu noul tip de date.

In [37]:
# Putem scădea pur și simplu două coloane cu același număr de elemente
diff = climatic_2016.LAT - climatic_2016.LAT.astype('float32')

# Sintaxă echivalentă pentru accesul unei coloane
diff = climatic_2016['LAT'] - climatic_2016['LAT'].astype('float32')

La extragerea unei coloane, tipul de date returnat este pandas.Series, o listă indexată cu același index ca cel din DataFrame. Evident, după scădere, rămânem tot cu un obiect de tip Series:

In [38]:
diff
Out[38]:
0       0.000001
1       0.000001
2       0.000001
3       0.000001
4       0.000001
          ...   
8413   -0.000001
8414   -0.000001
8415   -0.000001
8416   -0.000001
8417   -0.000001
Name: LAT, Length: 8418, dtype: float64

Ne-ar interesa acum eroarea cu valoarea absolută maximă: cea mai mare eroare pe care o putem întâlni. Pentru asta, calculăm întâi modulul tututor valorilor folosind metoda abs():

In [39]:
diff.abs()
Out[39]:
0       0.000001
1       0.000001
2       0.000001
3       0.000001
4       0.000001
          ...   
8413    0.000001
8414    0.000001
8415    0.000001
8416    0.000001
8417    0.000001
Name: LAT, Length: 8418, dtype: float64

Apoi calculăm valoarea maximă, folosind metoda max():

In [40]:
diff.abs().max()
Out[40]:
1.8585449268471166e-06

, ceea ce se traduce într-o eroare de aproximativ 0.2 metri (doar pentru latitudine). În cazul acesta, probabil e acceptabilă eroarea, întrucât nu suntem interesați de localizarea atât de precisă a stațiilor meteo.

Observați că unele metode, precum abs(), aplică o funcție pe fiecare element al obiectului, fie el DataFrame sau Series, în timp ce altele, precum max(), returnează un scalar.

Observăm că, exceptând coloana DATCLIM, toate coloanele din tabelul nostru au un tip de date real (float32 sau float64). Pentru DATCLIM, în coloana DType este menționat object. Acesta nu este un tip propriu-zis, ci este un înlocuitor pentru orice fel de obiect nativ Python, precum list, dict, str etc.

Putem să aflăm tipul valorilor din coloana DATCLIM folosind funcția Python type:

In [41]:
# Aici accesăm elementul de la index-ul 0 din Seria climatic_2016.DATCLIM
type(climatic_2016.DATCLIM[0])
Out[41]:
str

Observăm că tipul valorilor este str. Acest lucru este datorită felului cum read_csv interpretează datele citite: datele numerice sunt convertite la tipuri numerice, în timp ce restul coloanelor sunt interpretate ca str, urmând ca în timpul procesării datelor să atribuim tipul corect acestor coloane.

Tipul corect pentru date calendaristice, precum și pentru ore, este datetime64. Așa că aplicăm o conversie:

In [42]:
climatic_2016 = climatic_2016.astype({'DATCLIM': 'datetime64'})
In [43]:
climatic_2016.head()
Out[43]:
CODST ALT LAT LON DATCLIM TMED TMAX TMIN R24
0 15015 503.0 47.776944 23.940556 2016-01-01 -10.0 -5.7 -13.6 NaN
1 15015 503.0 47.776944 23.940556 2016-01-02 -11.0 -5.9 -14.4 NaN
2 15015 503.0 47.776944 23.940556 2016-01-03 -12.2 -5.5 -16.4 NaN
3 15015 503.0 47.776944 23.940556 2016-01-04 -11.0 -5.3 -16.6 0.0
4 15015 503.0 47.776944 23.940556 2016-01-05 -5.7 -4.6 -8.1 1.5

Observăm că formatul în care este afișată data s-a schimat, datorită noului tip de date.

Date lipsă

În orice set de date trebuie să investigăm dacă avem date lipsă sau eronate. Așa cum am observat mai devreme, avem multe observații cărora le lipsește valoarea R24. Aceste valori lipsesc chiar din fișier, deci nu e o problemă cu modul de citire al datelor.

Acum trebuie să răspundem la două întrebari: de ce lipsesc aceste valori și cum le tratăm? De exemplu, am putea elimina observațiile incomplete. Din păcate, sunt multe observații fără aceasta valoare, și nu este clar motivul lipsei valorii. Am putea să le înlocuim cu valoarea 0, dar, întrucât există observații cu 0 precipitații, se pare că pur și simplu nu a fost înregistrată valoarea în zilele respective. A pune 0 ar însemna să alterăm datele, întrucât nu suntem siguri că lipsesc observații doar în zilele fără precipitații. Tot ce putem face e să păstrăm coloana așa cum e și să luăm în considerare acest lucru în timpul analizei.

Separarea stațiilor meteo

Observăm că valorile din coloanele ALT, LAT și LON descriu doar stația meteo, fiind copiate pentru fiecare observație de la fiecare stație. Pe lângă faptul că această copiere introduce redundanță în tabel, poate îngreuna anumite analize. Haideți să separăm aceste informații într-un alt DataFrame, ce descrie stațiile meteo, și în acest tabel să păstrăm doar indicativul din coloana CODST.

Începem prin a separa coloanele CODST, ALT, LAT și LON într-un alt DataFrame:

In [44]:
# Folosim paranteze duble pentru că accesam o listă de coloane
statii_meteo = climatic_2016[['CODST', 'ALT', 'LAT', 'LON']]

# Ștergem coloanele suplimentare - păstrăm doar codul stației meteo
climatic_2016 = climatic_2016.drop(columns=['ALT', 'LAT', 'LON'])

statii_meteo
Out[44]:
CODST ALT LAT LON
0 15015 503.0 47.776944 23.940556
1 15015 503.0 47.776944 23.940556
2 15015 503.0 47.776944 23.940556
3 15015 503.0 47.776944 23.940556
4 15015 503.0 47.776944 23.940556
... ... ... ... ...
8413 15480 12.8 44.213889 28.645556
8414 15480 12.8 44.213889 28.645556
8415 15480 12.8 44.213889 28.645556
8416 15480 12.8 44.213889 28.645556
8417 15480 12.8 44.213889 28.645556

8418 rows × 4 columns

In [54]:
# Eliminăm observațiile duplicate
# https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.drop_duplicates.html
statii_meteo = statii_meteo.drop_duplicates()

statii_meteo
Out[54]:
ALT LAT LON Nume
CODST
15015 503.000000 47.776944 23.940556 Ocna Sugatag
15020 161.000000 47.735556 26.645556 Botosani
15090 74.290001 47.163333 27.627222 Iasi
15108 1897.000000 46.977500 25.950000 Ceahlau Toaca
15120 410.000000 46.777778 23.571389 Cluj-Napoca
15150 174.000000 46.557778 26.896667 Bacau
15170 661.000000 46.371389 25.772500 Miercurea Ciuc
15200 116.589996 46.133611 21.353611 Arad
15230 240.000000 45.865000 22.898889 Deva
15260 443.000000 45.789444 24.091389 Sibiu
15280 2504.000000 45.445833 25.456667 Varfu Omu
15292 241.000000 45.416667 22.229167 Caransebes
15310 69.000000 45.473056 28.032222 Galati
15335 4.360000 45.190556 28.824167 Tulcea
15346 237.000000 45.088889 24.362778 Ramnicu Valcea
15350 97.000000 45.132778 26.851667 Buzau
15360 12.690000 45.162222 29.726944 Sulina
15410 77.000000 44.626389 22.626111 Drobeta Turnu Severin
15420 90.000000 44.510556 26.078056 Bucuresti-Baneasa
15450 192.000000 44.310278 23.866944 Craiova
15460 18.719999 44.205833 27.338333 Calarasi
15470 102.150002 44.107222 24.978611 Rosiorii de Vede
15480 12.800000 44.213889 28.645556 Constanta

Observați că indexul a rămas bazat pe indexul din vechiul DataFrame. L-am putea recalcula, folosind metoda reset_index:

In [46]:
statii_meteo.reset_index()
Out[46]:
index CODST ALT LAT LON
0 0 15015 503.000000 47.776944 23.940556
1 366 15020 161.000000 47.735556 26.645556
2 732 15090 74.290001 47.163333 27.627222
3 1098 15108 1897.000000 46.977500 25.950000
4 1464 15120 410.000000 46.777778 23.571389
5 1830 15150 174.000000 46.557778 26.896667
6 2196 15170 661.000000 46.371389 25.772500
7 2562 15200 116.589996 46.133611 21.353611
8 2928 15230 240.000000 45.865000 22.898889
9 3294 15260 443.000000 45.789444 24.091389
10 3660 15280 2504.000000 45.445833 25.456667
11 4026 15292 241.000000 45.416667 22.229167
12 4392 15310 69.000000 45.473056 28.032222
13 4758 15335 4.360000 45.190556 28.824167
14 5124 15346 237.000000 45.088889 24.362778
15 5490 15350 97.000000 45.132778 26.851667
16 5856 15360 12.690000 45.162222 29.726944
17 6222 15410 77.000000 44.626389 22.626111
18 6588 15420 90.000000 44.510556 26.078056
19 6954 15450 192.000000 44.310278 23.866944
20 7320 15460 18.719999 44.205833 27.338333
21 7686 15470 102.150002 44.107222 24.978611
22 8052 15480 12.800000 44.213889 28.645556

, dar acest DataFrame are un index natural: CODST. Așa că vom seta coloana CODST ca index, folosind metoda set_index:

In [47]:
statii_meteo = statii_meteo.set_index('CODST')

statii_meteo
Out[47]:
ALT LAT LON
CODST
15015 503.000000 47.776944 23.940556
15020 161.000000 47.735556 26.645556
15090 74.290001 47.163333 27.627222
15108 1897.000000 46.977500 25.950000
15120 410.000000 46.777778 23.571389
15150 174.000000 46.557778 26.896667
15170 661.000000 46.371389 25.772500
15200 116.589996 46.133611 21.353611
15230 240.000000 45.865000 22.898889
15260 443.000000 45.789444 24.091389
15280 2504.000000 45.445833 25.456667
15292 241.000000 45.416667 22.229167
15310 69.000000 45.473056 28.032222
15335 4.360000 45.190556 28.824167
15346 237.000000 45.088889 24.362778
15350 97.000000 45.132778 26.851667
15360 12.690000 45.162222 29.726944
15410 77.000000 44.626389 22.626111
15420 90.000000 44.510556 26.078056
15450 192.000000 44.310278 23.866944
15460 18.719999 44.205833 27.338333
15470 102.150002 44.107222 24.978611
15480 12.800000 44.213889 28.645556

Ar fi util să avem și numele statiei în acest tabel. Vom pleca de la datele din descriere și vom aplica niște transformări:

In [48]:
nume_statii_str = '15015-Ocna Sugatag, 15020-Botosani, 15090-Iasi, 15108-Ceahlau Toaca, 15120-Cluj-Napoca, 15150-Bacau, 15170-Miercurea Ciuc, 15200-Arad, 15230-Deva, 15260-Sibiu, 15280-Varfu Omu, 15292-Caransebes, 15310-Galati, 15335-Tulcea, 15346-Ramnicu Valcea, 15350-Buzau, 15360-Sulina, 15410-Drobeta Turnu Severin, 15420-Bucuresti-Baneasa, 15450-Craiova, 15460-Calarasi, 15470-Rosiorii de Vede, 15480-Constanta'

# Împărțim string-ul într-o listă de string-uri
nume_statii_list = [x.split('-', 1) for x in nume_statii_str.split(', ')]
nume_statii_list
Out[48]:
[['15015', 'Ocna Sugatag'],
 ['15020', 'Botosani'],
 ['15090', 'Iasi'],
 ['15108', 'Ceahlau Toaca'],
 ['15120', 'Cluj-Napoca'],
 ['15150', 'Bacau'],
 ['15170', 'Miercurea Ciuc'],
 ['15200', 'Arad'],
 ['15230', 'Deva'],
 ['15260', 'Sibiu'],
 ['15280', 'Varfu Omu'],
 ['15292', 'Caransebes'],
 ['15310', 'Galati'],
 ['15335', 'Tulcea'],
 ['15346', 'Ramnicu Valcea'],
 ['15350', 'Buzau'],
 ['15360', 'Sulina'],
 ['15410', 'Drobeta Turnu Severin'],
 ['15420', 'Bucuresti-Baneasa'],
 ['15450', 'Craiova'],
 ['15460', 'Calarasi'],
 ['15470', 'Rosiorii de Vede'],
 ['15480', 'Constanta']]
In [49]:
# Transformăm lista într-un mic DataFrame, avem grijă la numele coloanelor, la tipul lor și la index
# Tipul de date al CODST trebuie să se potrivească cu tipul de date al CODST din celălalt DataFrame
# Observați înlanțuirea operațiilor
nume_statii = pd.DataFrame(nume_statii_list, columns=['CODST', 'Nume']).astype({'CODST': 'int64'}).set_index('CODST')

# Combinăm DataFrame-urile, bazându-ne pe coloana CODST, detectată automat după nume
statii_meteo = statii_meteo.join(nume_statii)

statii_meteo
Out[49]:
ALT LAT LON Nume
CODST
15015 503.000000 47.776944 23.940556 Ocna Sugatag
15020 161.000000 47.735556 26.645556 Botosani
15090 74.290001 47.163333 27.627222 Iasi
15108 1897.000000 46.977500 25.950000 Ceahlau Toaca
15120 410.000000 46.777778 23.571389 Cluj-Napoca
15150 174.000000 46.557778 26.896667 Bacau
15170 661.000000 46.371389 25.772500 Miercurea Ciuc
15200 116.589996 46.133611 21.353611 Arad
15230 240.000000 45.865000 22.898889 Deva
15260 443.000000 45.789444 24.091389 Sibiu
15280 2504.000000 45.445833 25.456667 Varfu Omu
15292 241.000000 45.416667 22.229167 Caransebes
15310 69.000000 45.473056 28.032222 Galati
15335 4.360000 45.190556 28.824167 Tulcea
15346 237.000000 45.088889 24.362778 Ramnicu Valcea
15350 97.000000 45.132778 26.851667 Buzau
15360 12.690000 45.162222 29.726944 Sulina
15410 77.000000 44.626389 22.626111 Drobeta Turnu Severin
15420 90.000000 44.510556 26.078056 Bucuresti-Baneasa
15450 192.000000 44.310278 23.866944 Craiova
15460 18.719999 44.205833 27.338333 Calarasi
15470 102.150002 44.107222 24.978611 Rosiorii de Vede
15480 12.800000 44.213889 28.645556 Constanta

Unde a fost mai frig?

Acum, că am terminat curățarea datelor, putem să începem analizele propriu-zise. Haideți, pentru început, să vedem care au fost temperaturile minime și maxime în anul 2016.

Pentru asta, putem să ne uităm individual la coloane:

In [50]:
print('Minima înregistrată în anul 2016 a fost de %.1f grade Celsius' % climatic_2016.TMIN.min())
Minima înregistrată în anul 2016 a fost de -24.0 grade Celsius

Sau putem să folosim functia describe, care raportează valori statistice pentru fiecare coloana.

In [51]:
climatic_2016.describe()
Out[51]:
CODST TMED TMAX TMIN R24
count 8418.000000 8418.000000 8418.000000 8418.000000 3155.000000
mean 15275.043478 10.355523 15.862235 5.842493 5.341394
std 141.501830 9.670633 10.988680 8.798955 8.610003
min 15015.000000 -22.400000 -19.700001 -24.000000 0.000000
25% 15150.000000 3.700000 7.800000 0.000000 0.500000
50% 15292.000000 10.100000 15.500000 6.100000 2.000000
75% 15410.000000 18.500000 25.500000 12.800000 6.450000
max 15480.000000 29.500000 37.799999 26.100000 92.000000

Aici primim informații statistice despre fiecare coloană. Înainte de a intra în detalii, observăm că coloana CODST este tratată numeric: ni se raportează valoarea minimă, maximă, media, etc. Ce sens are media acestor ID-uri? Întrucât ele nu sunt valori numerice, sunt doar identificatori, trebuie tratați ca atare. Putem indica asta folosind tipul de date category.

In [52]:
climatic_2016 = climatic_2016.astype({'CODST': 'category'})
statii_meteo.index = statii_meteo.index.astype('category')

climatic_2016.describe()
Out[52]:
TMED TMAX TMIN R24
count 8418.000000 8418.000000 8418.000000 3155.000000
mean 10.355523 15.862235 5.842493 5.341394
std 9.670633 10.988680 8.798955 8.610003
min -22.400000 -19.700001 -24.000000 0.000000
25% 3.700000 7.800000 0.000000 0.500000
50% 10.100000 15.500000 6.100000 2.000000
75% 18.500000 25.500000 12.800000 6.450000
max 29.500000 37.799999 26.100000 92.000000

Acum coloana CODST nu mai este considerată numerică. Să vedem ce informații ne oferă acest tabel.

Prima linie indică numărul de valori nenule (valide) din fiecare coloană, unde vedem din nou valorile lipsă din coloana R24.

A doua linie indică media fiecarei coloane. Putem observa o medie a temperaturilor maxime zilnice de aproximativ 16 grade Celsius.

A treia linie este o valoare statistică, deviatia standard. O vom explica în detaliu în lecțiile următoare. Intuitiv, ea reprezintă o masură a cât de mult variază datele și este de obicei raportată alături de medie.

A patra linie reprezintă valorile minime. Cum era de așteptat, pentru precipitații minimul este 0, în zilele însorite. Cea mai mică temperatură înregistrată a fost de -24 grade Celsius.

Următoarele trei linii sunt iărași măsuri statistice ce dau informații despre cum sunt distribuite datele. Mai multe despre ele în lecția viitoare.

Ultima linie reprezintă, evident, valoarea maximă.

Haideți să căutăm stația care a înregistrat valoarea minimă:

In [53]:
# Putem selecta linii dintr-un DataFrame indexându-l cu un Series cu valori boolean
# Pentru a găsi linia ce conține temperatura minimă, obținem întâi un Series ce are True doar la indecșii unde întâlnim valoarea minimă
# și folosim acest Series pentru a indexa DataFrame-ul inițial.

temperatura_minima = climatic_2016[climatic_2016.TMIN == climatic_2016.TMIN.min()]

temperatura_minima
Out[53]:
CODST DATCLIM TMED TMAX TMIN R24
3682 15280 2016-01-23 -22.1 -19.5 -24.0 NaN

Temperatura minimă din 2016 a fost înregistrată pe 23 ianuarie. Haideți să aflăm și numele stației!

In [68]:
# Parametrul `on` specifică numele coloanei folosită când sunt combinate tabelele
temperatura_minima.join(statii_meteo, on='CODST')
Out[68]:
CODST DATCLIM TMED TMAX TMIN R24 ALT LAT LON Nume
3682 15280 2016-01-23 -22.1 -19.5 -24.0 NaN 2504.0 45.445833 25.456667 Varfu Omu

Temperatura minimă a fost pe Vârfu Omu, la 2504 metri altitudine. Era de așteptat! Haideți să vedem unde a fost temperatura minimă în afara munților, la altitudini mai mici de 1000 metri.

Avem doar 2 stații la peste 1000 de metri:

In [84]:
statii_meteo[statii_meteo.ALT > 1000]
Out[84]:
ALT LAT LON Nume
CODST
15108 1897.0 46.977500 25.950000 Ceahlau Toaca
15280 2504.0 45.445833 25.456667 Varfu Omu
In [87]:
# Selectăm doar stațiile meteo de deal și de câmpie, aflate la altitudini sub 1000 metri
statii_meteo_deal_campie = statii_meteo[statii_meteo.ALT < 1000]

# Trebuie să combinăm stațiile meteo selectate cu datele climatice, și să eliminăm observațiile din cele două stații meteo montane
# Tipul de join, sau felul în care sunt combinate cele două tabele, este specificat de parametrul `how`
#
# Într-un left join (varianta implicită), vor fi păstrate toate liniile din tabelul din stanga (aici climatic_2016),
# și coloanele adiționale (provenite de la statii_meteo_deal_campie) vor fi populate cu NaN dacă nu s-a găsit un corespondent.
# Practic, am fi păstrat valorile temperaturilor pentru observațiile de la stațiile montane, dar am fi avut NaN pentru ALT, LAT, LON, Nume.
#
# Într-un right join, totul funcționează identic, doar că sunt păstrate liniile tabelului din dreapta.
#
# Într-un outer join am păstra toate liniile și completa cu NaN valorile care lipsesc din oricare tabel.
#
# Într-un inner join păstrăm doar liniile complete, ce avem noi nevoie.
#
# Join-urile sunt operatii importante, și complexe. Nu vă faceți griji dacă nu ați înțeles totul din acest scurt sumar: le vom explora mai în detaliu în curând.
#
climatic_deal_campie = climatic_2016.join(statii_meteo_deal_campie, on='CODST', how='inner')
climatic_deal_campie[climatic_deal_campie.TMIN == climatic_deal_campie.TMIN.min()]
Out[87]:
CODST DATCLIM TMED TMAX TMIN R24 ALT LAT LON Nume
7706 15470 2016-01-21 -14.9 -9.6 -23.1 NaN 102.150002 44.107222 24.978611 Rosiorii de Vede

Minimul a fost înregistrat pe 21 ianuarie, la stația din Roșiorii de Vede.

Greșeli statistice

Să ne gândim la următorul scenariu. Suntem întrebați care a fost temperatura medie în anul 2016 pe teritoriul Romaniei. Găsim datele pe care le-am analizat împreună, oficiale, facem analiza de mai sus, și venim cu raspunsul: temperatura medie în 2016 a fost de 10.36 grade Celsius.

Doar că răspunsul nostru este greșit. Noi nu avem date de pe întreg teritoriul României, ci doar de la 23 de stații meteo. Pentru a putea calcula într-adevăr temperatura medie de pe întreg teritoriul țării ar trebui să acoperim întreaga țară cu termometre. Noi am calculat temperatura medie înregistrată în 2016 în principalele stații meteo. Din acelasi motiv, la rubrica meteo întălnim adesea sintagme precum a fost înregistrată o temperatură de - nu știm exact ce temperatură a fost, atât am înregistrat noi, folosind dispozitive ce au o oarecare eroare (orice dispozitiv de masură are o eroare asociată).

O problemă a analizei noastre este ca nu știm eroarea măsurătorilor, ce ar fi trebuit raportată. Ideal, am fi spus nu doar temperatura minimă, ci și un interval de eroare, calculat pe baza erorii observațiilor.

Nu numai că nu am avut destul de multe puncte de măsurare, dar nici nu știm exact cum s-au facut măsuratorile pentru temperatura medie zilnică. Temperatura poate fluctua, și noi să nu fi observat aceste fluctuații pentru că măsurăm temperatura prea rar.

În viața de zi cu zi, unele din aceste erori sunt considerate neglijabile. Da, măsurăm într-un număr mic de puncte, dar beneficiem de faptul că atmosfera regulează variațiile termice atât în spațiu cât și în timp. Așadar, nu observăm adesea variații extreme în perioade scurte de timp, de ordinul minutelor, și nici pe distanțe scurte, de ordinul kilometrilor. O implicație a acestui fapt este că mediile tind să fie mai precise decat valorile extreme.

Așadar, la fiecare analiză, nu trebuie doar să procesăm datele, trebuie de asemenea să ne gândim la sursa lor, la ce erori pot avea și cât de relvante sunt datele pentru analiza noastră.