Pandas: data-nı birləşdirmək və formalaşdırmaq
Pandas: data-nı birləşdirmək və formalaşdırmaq
Əvvəlki fəsildə DataFrame-in anatomiyasını öyrəndik: index, sütunlar, seçim, missing data. Amma real analizdə data heç vaxt bir cədvəldə gəlmir. Müştərilər bir faylda, sifarişlər başqasında, məhsul kataloqu üçüncüsündədir; log-lar gün-gün ayrı fayllara yazılır; sorğunun cavabı mətn sütununda gizlənir. Bu fəsil həmin parçaları bir yerə gətirmək və sualınıza cavab verəcək formaya salmaq haqqındadır. Burada öyrənəcəyiniz dörd mexanizm — birləşdirmə, qruplaşdırma, string əməliyyatları və time series — pandas ilə görülən praktik işin böyük hissəsini təşkil edir. API-nin hər metodunu əzbərləmək məqsəd deyil; məqsəd hər məsələdə hansı mexanizmin uyğun olduğunu dərhal tanımaqdır.
Cədvəlləri üst-üstə qoymaq: concat
Ən sadə birləşdirmə eyni strukturlu cədvəlləri bir-birinin ardınca düzməkdir. pd.concat bunu edir və NumPy-dakı np.concatenate-dən əsas fərqi index-ə münasibətidir: pandas index-i qoruyur, hətta təkrarlansa belə.
jan = pd.read_csv("sales_jan.csv")
feb = pd.read_csv("sales_feb.csv")
sales = pd.concat([jan, feb]) # index: 0,1,2,...,0,1,2,...
Təkrarlanan index çox vaxt istəmədiyiniz nəticədir, çünki sonra sales.loc[0] iki sətir qaytaracaq. Üç yol var: ignore_index=True yeni ardıcıl index yaradır; keys=["jan", "feb"] mənbəni göstərən hierarchical index qurur; verify_integrity=True isə təkrar olduqda xəta atır. Mənbəni itirmək istəmirsinizsə, keys ən faydalı seçimdir.
axis=1 cədvəlləri yan-yana qoyur. Sütunlar fərqli olanda default olaraq union alınır və çatışmayan yerlər NaN olur; join="inner" yalnız ortaq sütunları saxlayır. concat sütunları adla, sətirləri index-lə uyğunlaşdırır, mövqe ilə yox: bu, pandas-ın ümumi prinsipidir və NumPy-dan gələn adət səbəbindən ən çox səhv edilən yerdir.
Relational birləşdirmə: merge və join
Cədvəllər ortaq açar üzrə əlaqələndiriləndə pd.merge lazımdır. Bu, SQL-dəki JOIN-un pandas versiyasıdır və onun arxasındakı relational algebra düşüncəsi üç halı ayırır:
| Əlaqə | Nümunə | Nəticə |
|---|---|---|
| one-to-one | işçi → otaq nömrəsi | sətir sayı dəyişmir |
| many-to-one | sifariş → müştəri adı | sol cədvəlin sətirləri qorunur, sağdan dəyər köçürülür |
| many-to-many | işçi → bacarıq (hər ikisi təkrarlanır) | kombinasiyaların sayı qədər sətir |
Many-to-many ən təhlükəlisidir: nəticə gözlədiyinizdən dəfələrlə böyük ola bilər və siz bunu yalnız cəmlər qəribə görünəndə hiss edirsiniz. Merge-dən sonra len() yoxlamaq ucuz adətdir.
Açarı pandas default olaraq eyni adlı sütunlardan tapır, amma açıq yazmaq daha etibarlıdır:
orders.merge(customers, on="customer_id")
orders.merge(customers, left_on="cust", right_on="id") # fərqli adlar
orders.merge(prices, left_index=True, right_on="sku") # index ilə sütun
left_on/right_on ilə birləşdirəndə hər iki sütun nəticədə qalır; artıq olanı drop edin. DataFrame.join isə index üzrə merge-in qısa yazılışıdır.
how parametri uyğun gəlməyən açarlarla nə edəcəyinizi müəyyən edir: inner (default) yalnız hər ikisində olanları saxlayır, outer hamısını saxlayıb boşluqları NaN ilə doldurur, left və right bir tərəfi tam qoruyur. Praktikada left ən çox istifadə olunan seçimdir, çünki "əsas cədvəlimə məlumat əlavə edirəm" məntiqini dəqiq ifadə edir.
Hər iki cədvəldə açar olmayan eyni adlı sütun olanda pandas onlara _x və _y əlavə edir. Bu adlar bir həftə sonra heç nə demir; suffixes=("_order", "_catalog") kimi mənalı suffix verin.
Split-apply-combine: groupby
Sadə aggregate-lər — sum, mean, median, min, max, count, describe — bütün sütun üzərində işləyir. Amma analizdə sual demək olar həmişə "hər qrup üçün" formasındadır: hər region üçün orta satış, hər istifadəçi üçün ilk giriş tarixi. Bunun üçün groupby var və onu anlamağın ən yaxşı yolu split-apply-combine mental model-idir:
- split — cədvəl açarın dəyərlərinə görə qruplara bölünür;
- apply — hər qrupa eyni funksiya tətbiq olunur;
- combine — nəticələr bir cədvəldə birləşdirilir.
Bu model-in gücü ondadır ki, siz üç addımı ayrı-ayrı yazmırsınız; yalnız açarı və funksiyanı deyirsiniz, bölmə və birləşdirmə ilə pandas məşğul olur. Və bunu loop-la yazmaqdan dəfələrlə sürətli edir.
g = df.groupby("region") # hələ heç nə hesablanmayıb
g["revenue"].mean() # indi hesablanır
groupby çağırışı lazy obyekt qaytarır: nə qrup yaradır, nə hesablama aparır. Yalnız aggregate istənəndə iş görülür. Bu, böyük data-da əhəmiyyətlidir, çünki g["revenue"] kimi sütun seçimi lazımsız sütunların emalının qarşısını alır. Lazım olanda qruplar üzərində iterate də edə bilərsiniz: for name, group in g: hər qrupu ayrıca DataFrame kimi verir, amma bu yola yalnız vectorized həll olmayanda müraciət edin.
Groupby obyektinin dörd əsas metodu var və fərqi nəticənin formasıdır:
| Metod | Hər qrupa nə verir | Nəticə |
|---|---|---|
aggregate | qrup → bir dəyər (və ya bir neçə) | qrup başına bir sətir |
filter | qrup → bool | şərti ödəyən qrupların sətirləri |
transform | qrup → eyni ölçülü seriya | orijinalla eyni ölçü |
apply | qrup → istənilən şey | funksiyadan asılı |
df.groupby("region").aggregate({"revenue": ["sum", "mean"], "orders": "count"})
df.groupby("region").filter(lambda x: x["revenue"].sum() > 1e6)
df["rev_centered"] = df.groupby("region")["revenue"].transform(lambda x: x - x.mean())
transform xüsusilə vacibdir: qrup statistikasını hər sətirə qaytarır, bu isə "müştərinin bu alışı onun ortasından nə qədər fərqlənir" kimi feature-ləri bir sətirlə yaratmağa imkan verir. apply ən çevik, amma ən yavaş variantdır; digər üçü işi görürsə, onlara üstünlük verin.
Açar mütləq sütun olmalı deyil. Siyahı və ya Series verə bilərsiniz (uzunluğu sətir sayına bərabər olmalıdır), dict verəndə index dəyərləri qruplara map olunur, funksiya verəndə hər index dəyərinə tətbiq olunur, level= ilə isə MultiIndex-in səviyyəsi üzrə qruplaşdırılır. Bir neçə açar ötürəndə nəticə MultiIndex-li olur.
Pivot table: çoxölçülü groupby
İki açar üzrə groupby nəticəni uzun, iyerarxik cədvəl kimi verir. İnsan gözü isə iki ölçünü sətir və sütunda görməyə öyrəşib. Pivot table məhz bu çevirmədir: groupby-ın bir açarı sətirlərə, digəri sütunlara gedir.
df.pivot_table("revenue", index="region", columns="quarter", aggfunc="sum")
df.pivot_table("revenue", index=["region", "segment"],
columns=pd.cut(df["age"], [0, 30, 50, 80]),
aggfunc="mean", margins=True)
aggfunc default olaraq mean-dir, sütun başına fərqli funksiya üçün dict verilir. margins=True cəmi sətir və sütun əlavə edir. Hər iki tərəfdə çoxsəviyyəli açar mümkündür, amma hər əlavə səviyyə cədvəli oxunmaz edir; ikidən çox səviyyə lazımdırsa, adətən groupby ilə uzun formada qalmaq daha yaxşıdır. Pivot table yeni mexanizm deyil, groupby-ın təqdimat formasıdır; onu belə düşünsəniz, parametrləri təxmin etmək asanlaşır.
Vectorized string əməliyyatları
Python-un string metodları bir dəyər üzərində işləyir. Sütun üzərində loop yazmaq həm yavaşdır, həm də None dəyərlərdə xəta verir. Series-in .str accessor-u bu metodları bütün sütuna missing data-ya dözümlü şəkildə tətbiq edir:
names.str.lower() # yeni string Series
names.str.len() # ədəd Series
names.str.startswith("Dr") # bool Series → filter üçün
names.str.split() # hər element siyahı
names.str.split().str.get(-1) # soyad
Nəticənin tipi filter məntiqini müəyyən edir: bool qaytaranlar (contains, startswith, isnumeric) birbaşa df[...] içində işlənir; siyahı qaytaranlar .str.get və ya .str[0] ilə açılır. Regex metodları — extract capture group-u sütuna çıxarır, findall bütün uyğunluqları siyahı kimi qaytarır, contains pattern-in olub-olmadığını yoxlayır, replace regex ilə əvəz edir. get_dummies isə "a|b|c" kimi kodlaşdırılmış çoxdəyərli sütunu one-hot sütunlara çevirir.
emails.str.extract(r"@(\w+)\.") # domain
tags.str.get_dummies("|") # hər tag üçün 0/1 sütun
Mətni təmizləmək analizdən əvvəlki ən çox vaxt aparan addımdır və .str bunu həm oxunan, həm sürətli edir.
Time series
Vaxt data-sı pandas-ın ən güclü olduğu sahədir, çünki kitabxana maliyyə analizi üçün yaranıb. Üç vaxt tipi var və hər birinin yeri fərqlidir: Python-un datetime obyekti rahat, amma yavaşdır və vektor əməliyyatı bilmir; NumPy-ın datetime64 tipi sıx, vectorized, amma əlverişsiz; pandas-ın Timestamp tipi hər ikisinin yaxşı tərəflərini birləşdirir. Timestamp-lərdən ibarət index DatetimeIndex-dir. Bunlara əlavə olaraq Period vaxt intervalını (məsələn, "2024-ün mart ayı"), Timedelta isə iki vaxt arasındakı fərqi ifadə edir. İki Timestamp-i çıxanda Timedelta alırsınız.
idx = pd.date_range("2024-01-01", periods=90, freq="D")
ts = pd.Series(np.random.randn(90).cumsum(), index=idx)
ts["2024-02"] # bütün fevral
ts["2024-01-15":"2024-01-20"] # tarix aralığı
pd.date_range bərabər addımlı index yaradır; freq kodları (D, h, W, ME, QE, B iş günləri üçün) və pd.tseries.offsets bu addımları ifadə edir. Tarixi string kimi yazıb index-ləmək mümkündür və ay kimi qismən string bütün ayı seçir. Bu rahatlıq DatetimeIndex-in ən çox istifadə edilən xüsusiyyətidir.
İki əməliyyat tez-tez qarışdırılır. resample vaxt addımını dəyişib hər yeni intervalda aggregate hesablayır: gündəlikdən aylığa keçəndə ayın ortasını və ya cəmini götürür. asfreq isə sadəcə yeni addımdakı nöqtələrin dəyərini seçir, aralıq məlumatı atır. Qiymətin aylıq ortası istəyirsinizsə resample, ayın son günündəki qiyməti istəyirsinizsə asfreq.
ts.resample("ME").mean()
ts.asfreq("ME")
ts.shift(1) # dəyərlər bir addım irəli sürüşür
ts.rolling(7).mean() # 7 günlük hərəkətli ortalama
ts.pct_change() # shift ilə hesablanan gündəlik dəyişim
shift dəyərləri index-ə nisbətən sürüşdürür və "dünənlə müqayisə" tipli hesablamaların əsasıdır. Rolling window hər nöqtədə ondan əvvəlki n müşahidə üzərində statistika hesablayır: rolling(7).mean() küyü hamarlayır, rolling(30).std() volatility-ni göstərir. center=True pəncərəni nöqtənin ətrafına yerləşdirir, amma gələcəkdən məlumat istifadə etdiyi üçün proqnoz feature-lərində yolverilməzdir.
Yüksək performanslı pandas: eval və query
Pandas-ın vectorized ifadələri sürətlidir, amma (df.a + df.b) * df.c kimi ifadədə gizli xərc var: hər ara nəticə üçün tam ölçülü yeni massiv ayrılır. Yaddaşa sığan data-da bu hiss olunmur; yüz milyon sətirdə hər ara massiv gigabaytlardır və onları yazıb-oxumaq CPU-dan çox vaxt aparır.
pd.eval və onun DataFrame metodları eval və query ifadəni string kimi qəbul edib element-element bir keçiddə hesablayır, ara massiv yaratmadan:
df.eval("total = (price * qty) * (1 - discount)", inplace=True)
cheap = df.query("price < @threshold and region == 'EU'")
@ ilə Python dəyişənlərinə istinad etmək olur. Nə vaxt istifadə etməli? İki şərt eyni anda ödənəndə: data böyükdür (ara massivlər CPU cache-ə sığmır) və ifadə bir neçə sütunu əhatə edir. Kiçik data-da fərq yoxdur, hətta adi sintaksis daha sürətli ola bilər, çünki string-i parse etməyin öz xərci var. query isə ölçüdən asılı olmayaraq uzun filter şərtlərini oxunaqlı etdiyi üçün faydalıdır. Qayda sadədir: oxunaqlılıq üçün query istəyə görə, performans üçün eval yalnız ölçərək.
Tapşırıqlar
- Üç cədvəl düzəldin: müştərilər (
customer_id,city), sifarişlər (order_id,customer_id,amount) və şəhər-region xəritəsi. Onlarımergeilə bir cədvəldə birləşdirin,howparametrinin dörd dəyərini sınayıb sətir sayının necə dəyişdiyini izah edin. Sonra hər region üçün müştəri başına orta sifariş məbləğinigroupby+transformilə hesablayın. - Eyni data-nı
pivot_tableilə region × ay formasında göstərin,margins=Trueəlavə edin və bu cədvəligroupby(["region", "month"])nəticəsi ilə müqayisə edin: hansı məlumat eynidir, təqdimat necə fərqlənir? - Bir ilin gündəlik temperaturunu (sintetik olar) DatetimeIndex ilə Series-ə yığın. Aylıq ortalamanı
resamplevəasfreqilə hesablayıb fərqi izah edin; 7 günlük rolling ortalama iləshift(365)istifadə edərək "keçən ilin eyni günü ilə fərq" sütunu yaradın.