Pandas: Series, DataFrame və index
Pandas: Series, DataFrame və index
NumPy fəslinin sonunda structured array-lərin sərhədini gördük: sütunların adı var, amma sətirlərin yox; missing data üçün rahat yol yoxdur; müxtəlif tipli sütunlar üzərində əməliyyatlar yöndəmsizdir. Real data isə məhz belədir: cədvəl şəklində, etiketli, boşluqlu, qarışıq tipli. pandas NumPy array-inin üzərinə bu üç şeyi əlavə edir: etiketlər, heterogen sütunlar və missing data-nın birinci dərəcəli dəstəyi. Bu fəsil pandas-ın iki əsas strukturunu, Series və DataFrame-i, və onları birləşdirən Index obyektini araşdırır. Əsas ideya birdir: pandas-da əməliyyatlar mövqeyə görə yox, etiketə görə uyğunlaşdırılır. Bunu anlamaq pandas-da qarşılaşacağınız "qəribə" davranışların çoxunu izah edir.
Series: etiketli 1D array
Series bir ölçülü, indekslənmiş data massividir. İki tərəfi var: dəyərlərin NumPy array-i (.values) və etiketlərin Index obyekti (.index):
import pandas as pd
s = pd.Series([0.25, 0.5, 0.75, 1.0], index=['a', 'b', 'c', 'd'])
s['b'] # 0.5 — etiketə görə
s.values # NumPy array
s.index # Index(['a', 'b', 'c', 'd'])
Series-ə iki cür baxmaq olar. Birincisi, ümumiləşdirilmiş NumPy array: NumPy-da indeks həmişə gizli tam ədəddir (0, 1, 2...), Series-də isə istənilən tipdə, hətta qeyri-ardıcıl və ya təkrarlanan açıq etiketdir. İkincisi, ixtisaslaşmış dict: açarları dəyərlərə map edir, amma Python dict-indən fərqli olaraq açarlar sıralıdır və dəyərlər eyni tipli olduğundan vectorized əməliyyatlar mümkündür. Elə buna görə Series birbaşa dict-dən yaradıla bilir: pd.Series({'Bakı': 2.3, 'Gəncə': 0.3}).
DataFrame: Series-lərin dict-i
DataFrame iki ölçülü strukturdur və ona da iki cür baxmaq olar: həm sətir, həm sütun etiketi olan 2D array, və ya eyni index-i paylaşan Series-lərin dict-i. Praktikada ikinci baxış daha faydalıdır, çünki df['pop'] dəqiq bir Series qaytarır və sütun əlavə etmək dict-ə açar əlavə etmək qədər sadədir:
pop = pd.Series({'Bakı': 2300, 'Gəncə': 330, 'Sumqayıt': 340})
area = pd.Series({'Bakı': 2140, 'Gəncə': 110, 'Sumqayıt': 83})
df = pd.DataFrame({'pop': pop, 'area': area})
df['density'] = df['pop'] / df['area']
DataFrame dict-of-Series ilə yanaşı dict-lərin list-indən (hər dict bir sətir; çatışmayan açarlar NaN olur), 2D NumPy array-dən (columns= və index= ilə) və structured array-dən də qurulur. Hər halda nəticə eynidir: df.index sətirlərin, df.columns sütunların Index obyektidir, df.values isə alt NumPy array-dir.
Index: dəyişməz, sıralı çoxluq
Index obyekti sadəcə etiket list-i deyil. O, immutable-dır: idx[0] = 5 xəta verir. Bu, bir neçə DataFrame-in eyni index-i təhlükəsiz paylaşa bilməsi üçündür; index dəyişilə bilsəydi, bir cədvəldə dəyişiklik digərini sakitcə pozardı. Eyni zamanda Index sıralı çoxluq kimi davranır və çoxluq əməliyyatlarını dəstəkləyir:
a = pd.Index([1, 3, 5, 7, 9])
b = pd.Index([2, 3, 5, 7, 11])
a.intersection(b) # [3, 5, 7]
a.union(b) # [1, 2, 3, 5, 7, 9, 11]
a.symmetric_difference(b)
Bu çoxluq əməliyyatları aşağıda görəcəyimiz index alignment-in əsasıdır: iki Series toplananda nəticənin index-i onların index-lərinin union-udur.
Seçim: loc və iloc
Series dict kimi davranır (s['a'], 'a' in s, s.keys()) və eyni zamanda array kimi (slicing, mask, fancy indexing). Problem bu iki baxışın toqquşduğu yerdə başlayır. Index tam ədədlərdən ibarət olsun: pd.Series(['a', 'b', 'c'], index=[1, 3, 5]). s[1] nə qaytarmalıdır: etiketi 1 olan 'a', yoxsa 1-ci mövqedəki 'b'? pandas burada etiketi seçir. Bəs s[1:3]? Slicing-də isə mövqe seçilir və ['b', 'c'] qaytarır. Eyni sintaksis, iki fərqli məntiq. Bu, pandas-da ən çox bug yaradan yerlərdən biridir.
Həll qeyri-müəyyənliyi aradan qaldıran iki indexer atributudur:
lochəmişə explicit etiketə görə işləyir:s.loc[1]→'a',s.loc[1:3]→['a', 'b'](son etiket daxildir!).ilochəmişə implicit mövqeyə görə işləyir:s.iloc[1]→'b',s.iloc[1:3]→['b', 'c'](Python stili, son mövqe xaricdir).
Qayda sadədir: kodda s[...] əvəzinə həmişə loc və ya iloc yazın. Açıq olmaq bir neçə simvol artıqdır, amma oxuyan adam niyyətinizi dərhal görür.
DataFrame-də df['col'] sütun qaytarır, df.col də işləyir, amma sütun adı metod adı ilə üst-üstə düşəndə (df.pop!) və ya boşluq olanda yox. Sətir-sütun seçimi isə loc və iloc ilə 2D NumPy sintaksisində gedir və mask ilə sütun list-i birləşdirilə bilir:
df.iloc[:2, :1] # ilk 2 sətir, ilk sütun
df.loc['Gəncə':, ['pop', 'density']] # etiketlə
df.loc[df['density'] > 1, ['pop']] # mask + sütun list-i
df[df['pop'] > 500] # qısa forma: sətirləri mask ilə seç
Son iki sətir bir uyğunsuzluğu göstərir: df['pop'] sütun seçir, df[mask] isə sətir. Bu sintaksis tarixi səbəbdən belədir; loc ilə yazanda qeyri-müəyyənlik yoxdur.
Əməliyyatlar: index alignment
NumPy ufunc-ları Series və DataFrame üzərində işləyir və index-i saxlayır: np.exp(s) eyni etiketli yeni Series qaytarır. Amma iki pandas obyekti arasında əməliyyat NumPy-dan fərqlənir. Elementlər mövqeyə görə yox, etiketə görə cütləşdirilir:
area = pd.Series({'Bakı': 2140, 'Gəncə': 110})
pop = pd.Series({'Gəncə': 330, 'Sumqayıt': 340})
pop / area
# Bakı NaN
# Gəncə 3.0
# Sumqayıt NaN
Nəticənin index-i iki index-in union-udur və yalnız bir tərəfdə olan etiketlər NaN alır. Bu davranış real data-nı əks etdirir: iki fərqli mənbədən gələn cədvəllər nadir hallarda eyni sırada və eyni uzunluqda olur. NumPy-da bunu düzgün etmək üçün əl ilə sort və uyğunlaşdırma lazım olardı; pandas-da bu avtomatikdir və səhv sətirlərin toplanması mümkün deyil. NaN istəmirsinizsə, operator əvəzinə metod çağırıb fill_value verin: pop.div(area, fill_value=0). DataFrame-lər arasında eyni alignment həm sətir, həm sütun üzrə gedir.
DataFrame ilə Series arasında əməliyyat NumPy broadcasting-i kimi default olaraq sətir-sətir gedir, yəni Series-in index-i DataFrame-in sütunları ilə uyğunlaşdırılır: df - df.iloc[0] hər sətirdən birinci sətri çıxır. Sütun-sütun əməliyyat üçün axis lazımdır: df.sub(df['pop'], axis=0).
Missing data
Missing dəyəri necə təmsil etməli? İki yol var. Mask yanaşması ayrıca boolean array saxlayır: hər dəyər üçün "varmı?" biti. Dəqiqdir, amma hər array-in yanında ikinci array deməkdir. Sentinel yanaşması data-nın içində xüsusi dəyər seçir: -9999, boş string və ya float-ların NaN-ı. Yaddaşa qənaət edir, amma bir dəyəri "qadağan" edir. pandas tarixən sentinel yolunu seçib və iki sentinel istifadə edir.
None Python obyektidir və yalnız object dtype-lı array-də yaşaya bilir; belə array-lər yavaşdır və None ilə arifmetika xəta verir. NaN isə IEEE float standartının bir hissəsidir: hardware səviyyəsində dəstəklənir, arifmetika xətasız gedir (nəticə yenə NaN olur), amma yalnız float-dur. Buna görə int Series-ə bir NaN daxil edəndə pandas bütün sütunu float-a çevirir; int-in NaN üçün yeri yoxdur:
pd.Series([1, 2, 3]).dtype # int64
pd.Series([1, np.nan, 3]).dtype # float64 — upcast
pd.Series([1, None, 3]).dtype # float64 — None NaN-a çevrilir
Bu upcasting-in qarşısını almaq üçün pandas-ın nullable dtype-ları var: Int64 (böyük I), boolean, string. Onlar daxildə mask yanaşmasını istifadə edir və missing dəyəri pd.NA ilə göstərir: pd.Series([1, None, 3], dtype='Int64') int olaraq qalır.
None | NaN | pd.NA | |
|---|---|---|---|
| Nədir | Python obyekti | IEEE float dəyəri | pandas sentinel-i |
| Hansı dtype-da | yalnız object | yalnız float | nullable dtype-lar |
| Arifmetika | xəta | nəticə NaN | nəticə pd.NA |
Müqayisə == | False | False (hətta özü ilə) | pd.NA |
Cədvəldəki son sətir vacibdir: x == np.nan heç vaxt True olmur, ona görə missing dəyəri müqayisə ilə yox, aşağıdakı metodlarla axtarın.
Missing data ilə işləmək üçün dörd metod kifayətdir. isnull() və notnull() boolean mask qaytarır. dropna() missing olan sətirləri atır; DataFrame-də how='all' yalnız tamamilə boş sətirləri, thresh=3 ən azı 3 dolu dəyəri olmayanları atır, axis=1 isə sütunlara tətbiq edir. fillna(0) boşluğu sabit dəyərlə doldurur, ffill() əvvəlki, bfill() isə sonrakı dəyərlə; DataFrame-də axis doldurma istiqamətini seçir. Time series-də ffill ən təbii seçimdir: sensor cavab verməyəndə son ölçüsü qüvvədə qalır.
Hierarchical indexing: MultiIndex
Bəzən data iki ölçüdən çox ölçülüdür: şəhər × il × göstərici. MultiIndex bu əlavə ölçüləri index-in level-ləri kimi kodlaşdırır; hər etiket tuple-dır. Ən birbaşa yaradılma yolu tuple-lardan, array-lərdən və ya level-lərin Cartesian hasilindən keçir:
idx = pd.MultiIndex.from_product([['Bakı', 'Gəncə'], [2020, 2024]],
names=['city', 'year'])
pop = pd.Series([2250, 2300, 320, 330], index=idx)
pop['Bakı'] # yalnız Bakı: year üzrə Series
pop[:, 2024] # bütün şəhərlər, 2024
pop.unstack() # year → sütunlara: 2×2 DataFrame
unstack() bir index level-ini sütunlara, stack() isə sütunları index level-inə çevirir. Deməli MultiIndex-li Series ilə adi DataFrame eyni data-nın iki görünüşüdür və stack/unstack onlar arasında itkisiz keçid edir. Level-lərə ad vermək (names=) sonrakı əməliyyatları oxunaqlı edir. MultiIndex sütunlarda da ola bilər: hər xəstə üçün bir neçə ölçü, hər ölçünün bir neçə ziyarəti kimi 4D data 2D cədvəldə yerləşir.
Slicing MultiIndex-də iki tələ ilə gəlir. Birincisi: index sıralı olmalıdır. Level-lər leksikoqrafik sıralanmayıbsa, pop['Bakı':'Gəncə'] xəta verir; əvvəlcə pop.sort_index() çağırın. Səbəb odur ki, sıralanmamış index-də aralığın harada başlayıb bitdiyini müəyyən etmək mümkün deyil. İkincisi: daxili level üzrə slicing-in adi sintaksisi yoxdur, çünki pop.loc[('Bakı', 2020):('Gəncə', 2024)] tuple-ın içində slice yazmağa imkan vermir. Bunun üçün pd.IndexSlice var:
idx = pd.IndexSlice
df.loc[idx[:, 2024], idx['pop', :]] # bütün şəhərlər, 2024, pop sütunları
MultiIndex ilə adi sütunlar arasında keçid də iki metoddur: reset_index() level-ləri adi sütunlara, set_index(['city', 'year']) isə sütunları index-ə çevirir. Real data çox vaxt "uzun" formada, yəni sütunlarla gəlir və set_index + unstack onu analiz üçün əlverişli formaya salır.
Aggregation-lar level üzrə işləyir: pop.groupby(level='year').mean() hər il üçün şəhərlərin ortalamasını verir. Köhnə mean(level=...) forması pandas-dan çıxarılıb; groupby(level=...) standart yoldur. Növbəti fəsildə groupby-ı daha geniş görəcəyik, amma ideya artıq buradadır: MultiIndex ölçüləri adlandırır, aggregation isə o adlara istinad edir.
Tapşırıqlar
pd.Series([10, 20, 30], index=[2, 0, 1])yaradın.s[0],s.loc[0],s.iloc[0],s[0:2],s.loc[0:2],s.iloc[0:2]ifadələrinin hər birinin nəticəsini icra etmədən əvvəl kağızda yazın, sonra yoxlayın. Səhv etdiyiniz hallar üçün explicit/implicit fərqini izah edin.- İki fərqli mənbədən gələn satış cədvəlini (fərqli sırada, qismən üst-üstə düşən məhsul adları, bəzi boş dəyərlər) iki DataFrame kimi qurun. Onları toplayın və nəticədəki NaN-ların hər birinin səbəbini müəyyən edin: alignment-dən gəlir, yoxsa orijinal missing data-dan? Sonra
fill_valuevəfillnailə məqsədinizə uyğun cədvəl alın. - Üç şəhər, dörd il və iki göstəricidən (population, area) ibarət long-format DataFrame yaradın.
set_indexilə MultiIndex qurun,sort_indexetmədən slicing-in xəta verdiyini göstərin, sonraunstackilə hər il üçün sütun alın vəgroupby(level=...)ilə hər şəhərin illər üzrə orta sıxlığını hesablayın.