Python’da Büyük Hacimli Verilerle Çalışmak — 1.Bölüm

Volkan Yurtseven
Akbank Teknoloji
Published in
21 min readJul 10, 2024

Pandas, Jupyter, Memory Yönetimi ve daha fazlası.

Giriş

Python’da veriyle uğraşan herkes çok kısa sürede pandas ile tanışır ve hummalı çalışmalarına başlar. Bu kütüphanenin verimli kullanımına yönelik birçok blog/video bulunmakla birlikte kendilerinin 3000+ sayfalık dokümantasyonu da oldukça doyurucudur.

Ben bu yazı dizisinde sizlere kısmen bu kaynaklardaki önemli fakat az bilinen püf noktalarını ve aynı zamanda kendi çalışmalarımızda uyguladığımız yaklaşımları da aktarmaya çalışacağım. Ancak yazının başlığından da anlaşılacağı üzere odak noktamız Memory Yönetimi, hız, performans gibi konular olacak.

Medium standartlarına göre çok uzun bir yazı olduğu için 2 parçaya ayırmak zorunda kaldım. Keyifli okumalar dilerim.

Notlar:

  • Sayfa boyunca göreceğiniz kodlar içinde yer yer kendi yazdığım utility fonksiyonları göreceksiniz, bunları da sayfanın en altında (Kaynakların hemen üstünde) bulabilirsiniz.
  • Bazı kısımlar aşırı teknik gelebilir ve programlama dünyasında yeniyseniz bir şey anlamayabilirsiniz, göz aşinalığı olması adına yine de mutlaka okuyun, ilerde muhtemelen ihtiyacınız olacaktır.
  • Bazı çözüm önerileri hızlı çalışmak için, bazıları memory-efficient çalışmak için bazısı da her iki amaca da hizmet eden öneriler olabilir. İhtiyacınıza uygun olanı seçmeye çalışırsınız.
  • Bazı bilgileri sık sık tekrarlayabilirim, zira farklı bağlamlarda aynı bilginin tekrarlanmasının pekişme süreci için faydalı olduğunu düşünüyorum.
  • Tıp bilim dilinin Latince olduğu nasıl bir gerçekse, yazılım dünyasının dilinin de İngilizce olduğunu, o yüzden bazı ifadelerin İngilizce versiyonlarını kullanmanın yadırganmaması gerektiğini düşünenlerdenim. Bir doktorun meslektaşlarıyla konuşurken hastanın humerusunda sorun görmesiyle bizlerin de Memory’de sorun görmesi aynı şeydir. Okurken bu farkındalıkla okumanızı rica ederim.

İçindekiler

Part I

Part II

  • Veri Okuma
    ○ Flat file okuma
    ○ Veritabanından okuma
    — SQL ve Veritabanı mimarisi
    — Python & Pandas
    — Profilerlar ile gözlem
    — Pandas ile paralel veri okuma
    — Chunklar halinde paralel okumaya genel bakış
    — Chunklar halinde farklı okuma yöntemleri
  • Veri İşleme Üzerine Notlar
  • Kaynaklar

Büyük (Hacimli) Veri Kavramı

Şimdi öncelikle büyük hacimli veri denince ne anlamalıyız, kısaca buradaki terminolojiden bahsedeyim.

Büyük Veri (Big Data): Günümüzde artık Büyük Veri kavramını duymayan kalmamıştır: Bunun cluster, hadoop, hive, hdfs, mapreduce, spark v.s başta olmak üzere onlarca hatta yüzlerce alt kavramı/bileşeni var. Ama bizim odağımızda bunlar olmayacak. Kaldı ki, Big data ortamına erişmek (Yetkilendirme süreçlerini hiç saymıyorum bile), eriştikten sonra ne yapacağını bilmek (Hive mı, Spark mı, Impala mı vs. kullanacağını bilmek) ve tüm bunları öğrenmek oldukça zahmetli, tamamen ayrı bir dünya zaten. Tabi ki bunları uzun vadede bilmek faydanıza olacaktır ama kısa vadede de elinizdeki araçlarla (Cluster’sız bir ortamda) ne yapabileceğinizi bilmek de önemli. Bazen ihtiyacınız çok kompleks de olmayacaktır. Mesela Excel’de çalışırken de a1 ve a2 hücrelerini toplamak için ne sum() fonksiyonunu kullanırsınız ne de makro yazarsınız; direkt “=a1+a2” formülünü yazarsınız. Veri analizinde de birçok iş için pandas yeterli olmakta, Big Data teknolojilerine çok da gerek olmamakta. Ben de burada özellikle işlerini clustersız bir ortamda ve pandas’la görenlere yönelik bir içerik hazırlamaya çalıştım.

Büyük hacimli veri (Large dataset): Bu yazının odağında bu kavram olacak. Bu verinin hacmi birkaç gigabayt da olabilir, 1–2 terabayt da, daha fazlası için Big Data ortamlarının nimetlerinden faydalanmak lazım. Aslında veri hacminden ziyade burada bizim için asıl önemli olan verinin structured (Tabular formatta) olması ve gerçek büyük veride olduğu gibi cluster’lara dağıtılmamış olması, zira çoğunuzun dünyasında veri tek bir noktada bulunuyor.

Çalışma Verimini Etkileyen Hususlar

Büyük hacimli veriyle çalışırken göz önünde tutulması gereken birkaç husus var. Ancak öncelikle buradaki “çalışırken” ifadesini altını dolduralım. Ben burada hem veriyi okumayı/yazmayı hem de onu işlemeyi (Preprocessing, processing, data wrangling gibi birçok farklı terimle de ifade edilir) kapsama alıyorum. Zira bazen ikisini bir arada yapmak da gerekecek; ne demek istediğime sonra geleceğiz. Şimdi hangi hususlar bizim için önemli, bunlara bakalım:

Genel makine gücü/çalışma ortamı: Çalıştığınız makine local bir PC ise muhtemelen hem RAM hem Cpu açısından yetersiz bir makine olacaktır (Gerçekten büyük hacimli verilerle çalışıyorsanız). O yüzden kurumunuzda varsa jupyterhub benzeri bir ortamda çalışmanızı tavsiye ederim. Bunlardan özellikle RAM, kritik bir kaynaktır, zira memory hatası (Dead kernel) sıklıkla alınan hatalardandır, ama çekirdek (Cpu core) sayısı az diye genelde hata almazsınız. Endüstriyelleşmiş(Yaygınlaştırmaya hazır) çalışmalarınız içinse artık kullanımları iyice artmakta olan AutoML platformları daha uygun olacaktır.

Çalışma süresi (Runtime): Bazen elinizdeki makinenin Memory’sinden yana derdiniz yoktur. Kodun hızlı çalışmasını istersiniz. O yüzden yazdığınız kodun time complexity’si düşük olmalıdır. Öncelikle burada genel algoritma bilginizin iyi olması gerekiyor. Ayrıca Veri Yapılarını bilmek, doğru yerde doğru veri yapılarını kullanmak ve bu yapıları optimum içerikte tutmak, gereksiz kod tekrarı yapmamak, bottleneck’ler oluşturmamak, yeri geldiğinde paralelleştirme tekniklerini, uygun yerlerde vektörel işlemleri yapmak gibi birçok konu var. Bunların her birinde ve fazlasında kendinizi geliştirmeye çalışmalısınız, ki bu yazıda bu açığınızın bir kısmını kapatmayı umuyorum.

Memory kullanımı: Burada önemli olan ise Space complexity kavramı. RAM’iniz her ne kadar yüksek olsa da, Jupyterhub gibi paylaşımlı ortamlarda diğer kişilere de yeterince Memory bırakmanız gerekir, üstelik daha ekonomik yolları varken neden hoyratça sömüresiniz? Kaldı ki, yüksek RAM’li bu makinelerin bile RAM’inin tamamen tükendiği zamanlar olacaktır. Veri tipi optimizasyonu, gereken miktarda veriyi okumak, gereksiz objelerin zamanında Memory’den atılmasını sağlamak gibi yöntemleri öğrenmeniz gerekecek, ki bu yazıda bunlardan bolca bulacaksınız.

Python Implementasyonu: Çok yaygın kullanılan yanlış bir ifade vardır: “Python yorumlamalı (Interpereted) bir dildir”. Aslında Python sadece bir dil spesifikasyonudur; yorumlamalı olan şey dilin kendisi değil, onun en yaygın implementasyonu olan Cpython’dır. Zira, compiled çalışan c# implementasyonu, yani IronPython veya java versiyonu Jython da var, dolayısıyla bunlar çok daha hızlı çalışırlar. Ama bunlar sadece genel kültür olsun, zira bildiğim kadarıyla veriyle ilgili kütüphanelerin önemli bir kısmı sadece Cpython implementasyonuna göre hazırlanmıştır. Gerçi bir de pure python implementasyonu olan ve JIT compiled çalışan PyPy var ki, bunun hem çok hızlı hem de pandas/numpy/sklearn gibi kütüphanelerle uyumlu olduğu iddia edilmektedir. Denemeye değer, ben fırsat bulduğumda deneyeceğim. Siz de bu aşamada acele etmeyin, bu bilgiler aklınızda bulunsun, şimdilik Cpython ile devam edin, ilerde gerekirse bakabilirsiniz.

Paralelleştirme: Buradaki temel kavramlar “true parallelism”, threading, concurrency, asynchronization olup; core kütüphaneler (Multiprocessing, concurrent.futures, threading, asyncio) bize bu araçları sağlamaktadır. Bununla beraber bu paketlerin karmaşıklıklarından soyutlanmış hali olan joblib ve diğer 3rd party kütüphaneler de denemeye değer: pyspark(büyük veri ortamı lazım), dask, polars, ray, modin, tuplex. Her ne kadar 3rd party kütüphaneler paralel çalıştırma işini arka planda sizin adınıza yapsa da core olanları bilmeniz de önemli. Özellikle thread ve process/pool yöntemlerinden hangisinin nerede kullanılması gerektiğini bilmek (Örneğin:CPU-bound işler için multiprocessing, IO-bound işler için threading veya asyncio) önemli, zira 3rd party kütüphanelerin “Bunlardan hangisini seçeyim” şeklinde parametreleri de olabiliyor. Şurada güzel bi benchmark var. Bu yazı dizisinde bunlara değinmeyi düşünmüyorum ama belki iki part’lık bu seriye bir ek olarak bir üçüncü part veya bağımsız bir yazıyı da ilerleyen dönemde kaleme alabilirim.

Bu arada konu paralelleştirme olduğunda karşımıza bir de GIL konusu çıkar ki, çok tartışmalı bir kavramdır. Ve maalesef yine birçok kişi bu konuyu yanlış anlamaktadır. Öncelikle GIL sadece Cpython impletasyonunda bulunur ama yapılan esas hata GIL’in hep devrede olduğu ve Python’ın aslında multithreading’i desteklemediğini sanmaktır. Halbuki GIL’in serbest kaldığı, yani devreye girmediği birçok durum vardır. Ben tekrar detaylarına girmeyeceğim, en aşağıdaki kaynaklarda bolca bilgi bakabilirsiniz. Bu arada 2023'te yapılan bir duyuruyla GIL’in yakın gelecekte optional olacağı söylendi. Bunu da takip edip göreceğiz.

İlave optimizasyon yöntemleri: Cython (Cpythonla karışmasın) paketi ile yorumlamalı dil olan kurguyu compiled hale yaklaştırıp önemli hız kazanımları sağlayabilirsiniz. Yine buna benzer şekilde kodun hızlanması için numpy kullanımı olan yerlerde numba kütüphanesinden faydalanabilirsiniz. Numpy zaten vektörizasyon yaparak paralel işlem imkanı verirken numba ile JIT compilation yapıp ekstra performans sağlanabiliyor(her durumda değil, belli durumlarda). Ayrıca adını son zamanlarda duyduğum ve açıkçası henüz deneme fırsatım olmayan taichi kütüpahnesi de oldukça başarılı görünüyor.

Son olarak eğer c/rust dillerini biliyorsanız python extensionları da yazabilirsiniz. Siz doğrudan c/c++/rust ile extension yaz(a)masanız bile, bu şekilde yazılmış numpy gibi kütüphaneleri veya pandas’ın bazı vektörel işlemlerini kullanarak zaten önemli performans kazanımlarınız olacak. Hatta ilk başlarda bu Cython/numba v.s konularına hiç girmeyin derim, zaten numpy ve pandas yeterince optimize edilmiş paketler olduğu için bu anlamda bazı istisnalar dışında başka bir optimizasyona ihtiyacınız olmayacaktır. Bu arada bunlar tüm seçenekler değil, bu yazıyı hazırladığım günlerde varlığını yeni öğrendiğim bir başka kütüphane olan cuDF de harika birşeye benziyor, tabi Nvidia GPU’unuz varsa. Eminim daha birçok kütüphane orada bir yerde kullanmanızı bekliyordur. Ama dediğim gibi şimdi değil, belki ileride “Ekstra güç iyidir” bakışıyla bakmak isteyebilirsiniz. Son olarak yukarıda bahsettiğim birkaç yaklaşımın bir performans karşılaştırmasını ise şu videoda bulabilirsiniz.

Okunan veri kaynağı: Verinin flat-file olarak diskten (txt-based veya binary farketmez) mi veritabanından mı okunacağı önemli bir faktördür. Klasik flat file(csv, txt) kaynaklarını daha verimli dosya tiplerine dönüştürmek, chunk’lar halinde okumak gibi çözümler gerekebilir, bunlardan Part II’de bahsedeceğiz. Veritabanından okumak için ise farklı çözümler var, buna da yine Part II’de geniş bir şekilde değineceğiz.

İşletim sistemi: Windows’ta fork yapısının olmaması gibi nedenlerle multiprocessing çalışırken bazı küçük farklılıklar yapmak gerekebiliyor.

Python’da memory yönetimi

Değişkenler, Nesneler, Referanslar, Memory Adresleri

Eğer daha önce Python’dan başka bir dil kullanmadıysanız burada anlatılanlar biraz karışık gelebilir. c/c++, java, c# gibi dillerde programlama yaptıysanız da ters gelebilir.

Memory’deki işleyişi bilmek onu etkin kullanmak adına oldukça önemli. Ben burada bu detaylara girmeyeceğim, ama bir YZ aracına (Chatgpt, gemini v.s) aşina olduğunuz dil ve Python’da bir değişkene bir değer atandığında Memory’de kıyaslamalı olarak neler olduğunu anlatmasını isteyin, bunu yaparken “Variable, name, reference, object, heap, stack” kavramlarını da kullanmasını belirtebilirsiniz. Bilahare en alttaki referans kaynaklara da bakmanız iyi olacaktır.

Obje Silme ve Garbage Collection

Python’da Garbage Collection yapısı da diğer dillerden biraz farklı işliyor. Şurada güzel bir anlatım var, ayrıca Youtube’da da animasyonlu güzel anlatımlar var.

Özellikle jupyter notebookta çalışırken, büyük bir dataframe’i sildikten sonra Memory kullanımını gösteren resource_usage widget’ta (aşağıdaki görsel) neden Memory azalmadığını merak ediyorsanız bu kısmı ve verilen kaynakları dikkatli okumanızı öneririm.

jupyter-resource-usage

Bu arada kullanılabilir memory bilgisine CLI’dan da şu komutlarla ulaşabilirsiniz.

wmic OS get FreePhysicalMemory /Value #windows'ta
free #Linux'ta

veya benim en aşağıda verdiğim şu fonksiyonu kullanabilirsiniz.

Utility.getCurrentResourceInfo(None, None)

del ifadesi

Bir nesnenin Memory’den silinmesi için iki şarttan biri gerçekleşmeli diye söylenir:

  • local scope’ta (If blok, for döngüsü vs.) çalışan bir değişkendir ve scope’tan çıkınca hemen silinir.
  • del ifadesi veya None atamasıyla

Ancak buradaki ve şuradaki soru-cevaba bakarsanız del ifadesi hakkında önemli bir detayı daha öğrenmiş olacaksınız. Özetle bu komut, aslında bir objeyi Memory’den silmez, bunun yerine Memory alanındaki bu objeye verilen referansın adını siler. Eğer bir sızıntı nedeniyle buna başka bir referans kaldıysa, o zaman Memory’de yaşamaya devam eder, çünkü Garbage Collector sadece hiç referans kalmamış objeleri silmektedir.

Yani günün sonunda Garbage Collector’ın bir nesneyi Memory’den atması için bazı koşullar sağlanmalı, ancak söz konusu Python ise sürprizler bitmez, bütün bu şartlar sağlanıp da Memory’den silinse bile bu boşalan alan işletim sistemine hemen geri verilmeyebilir, onun yerine bu alan Python’ın kendisine verilebilir, “Bunu başka bir yerde kullanırsın” diye. Bir süre sonra işletim sistemi bunu geri alacaktır, ama bu, sizin müdahale edebileceğiniz bir akış değildir. Bu bilgiyi sadece kodunuzdaki olası darboğazları açıklamanız için vermiş oldum. Bununla birlikte bu durumun çok sık yaşanmadığını (En azından benim için) da söyleyebiliriz. O yüzden büyük bir dataframe sildiğinizde Memory tüketimi hala düşmüyorsa bu çok büyük ihtimalle Memory Leak’e (Sızıntı) delalettir.

Bu nedenle sizin esas yapmanız gereken şey, kodunuzda bir Memory Leak var mı bunu takip etmektir. Bunun için;

  • Mümkün mertebe local scope kullanın, yani büyük kod bloklarını fonksiyonlar içine yazıp yerelleştirin
  • objgraph ve profiler araçları ile sızıntı takibi yapın

Not: Amacınız Memory kazanımı değil de hız kazanımı ise gc.disable() metodu ile kodunuzu hızlandırabilirsiniz ama bu riskli bir yaklaşım olabilir, zira memory sorunlarına neden olabilir. Memory sorunu yaşamayacağınızı düşünüyorsanız kullanabilirsiniz, tabi kodun sonunda bunu tekrar enable edip bir de collect metodunu çağırmakta fayda var. Bu arada disable etmekle generation’lardaki threshold’ları artırmak benzer etkiye sahip. Bir de freeze işlemi var, kaynaklarda göreceğiniz üzere Instagram’daki yazılımcıların buluşu olan bu metot da hız kazanımı açısından fayda sağlayabilir.

Jupyter’de İşler Nasıl Oluyor?

Scope

Yukarıda belirttiğimiz gibi, if blok gibi local scope’ta olan değişkenlerin del ile silinmesine gerek yoktur. Program akışı bu scope’tan çıkınca bunlar zaten Memory’den direkt silinir, ama global scope’ta bulunan objelerin, işi bittiği noktada del ile silinmesi(veya None atanması) gerekir. None atamanın bir güzelliği profiler araçları ile ilgili değişken üzerinde hala işlem yapabiliyor olmanızdır, o yüzden denemeler yaparken del yapmak yerine None atamasını tercih edebilirsiniz.

def localscope():
localdf=joblib.load(r"....pkl")
print(localdf.shape)

%memit localscope()
Output->(132831947, 45)
peak memory: 36049.55 MiB, increment: 17329.62 MiB

Bu kod çalışırken bir yandan da resource-usage widget’ındaki değerin de önce yükselip (17 GB kadar artıp) sonra düştüğünü de görebildim.

Jupyter notebooklardaki her hücre kendi başına global scope’un bir parçası olduğundan sıklıkla del komutunu kullanmanızda fayda var. Ancak Jupyter’de işler biraz daha karmaşık olup arkada başka referanslar tutuyor olması nedeniyle beklediğinizden daha fazla Memory tüketimi görebilirsiniz.

Peki üstteki örneğimize geri dönelim. Diyelim ki bu fonksiyonda localdf nesnesine bir başka değişkenden erişelim, yani bu dataframe objesine olan referans sayısı 2'ye çıkaralım ve bunu yazdıralım.

def localscope2():
localdf=joblib.load(r"......pkl")
supinfo=localdf.super_info_() #büyük objeye hala ref olacak
print(f"id:{id(localdf)}, adres: {hex(id(localdf))}, refcount: {getrefcount(localdf)-1}") #-1 deme sebebimizi hatırlayın, getrefcount bi de kendisini de sayıyordu

#bunun çıktısı aşağıdaki gibi olacak
%memit localscope2()
id:140688781882704, adres: 0x7fb5192ff460, refcount: 2

Bu sefer widget’taki memory usage hemen düşmedi, çünkü 2. bir referans verdik. Ancak bir süre beklenirse veya gc.collect hemen çağrılırsa düşüş görülecektir.

Şimdi son olarak bu sefer sızıntı bırakalım.

liste=[]
def localscope3():
localdf=joblib.load(r"......pkl")
liste.append(localdf)

Ve beklediğim gibi widget’taki Memory Usage’ta düşme olmadı. listeyi silip hemen arkasından gc.collect() çağırırsak Memory tüketimi normale iner.

Peki kod hızlandırma gibi sebeplerle gc.disable() yapmış olsaydınız veya sebebini anlayamadığınız bir nedenden ötürü Memory serbest kalmıyorsa, nasıl hareket ederdiniz?

Çözüm önerileri

  • Bu bağlamda, Quora’da sorulmuş bir soru üzerine verilen ilk cevapta üstte bahsettiğimiz iki ana sebep söylenmiş, yani ‘’Memory serbest kalması hemen garanti değil, hemen olsa bile işletim sistemine hemen verilmeyebilir, Python onu kullanmaya devam edebilir v.s” denmiş, ama ikinci yanıt olan şu yanıttaki öneri oldukça güzel. Biz de bunu implemente edelim,
#bu fonksiyon, multiprocessinge gireceği için ayrı bir py dosyasına konur ve oradan çağırılır
import multiprocessing
def localscope4(dummy):
localdf=joblib.load(r"......pkl")
supinfo=localdf.super_info_()
print(f"adres: {hex(id(localdf))}, refcount: {getrefcount(localdf)-1}")
print(supinfo.shape)

#notebooktan da şunlar
from workers import localscope4
p = multiprocessing.Pool(1)
p.map(localscope4,[None])[0]
p.terminate()
p.join()
  • Bu arada gerçekten jupyter(IPython)’de işler biraz farklı gibi, Şuraya bir bakın, iki farklı öneri daha var, onları da deneyebilirsiniz.
  • Şuradaki öneri de güzel, tüm olayı global scope’tan çıkarıp yerelleştirdiği ve local değişkenler de scope sona erer ermez yok olduğu için güzel bir öneri, ki bizim yukarıda scope başlığı altında ilk yaptığımız da buydu, linkteki açıklamaları faydalı bulduğum için tekrar belirtmek istedim.
  • Şurada ise şöyle bir ifade var: If your notebook is following this type of pattern a simple del won’t work because ipython adds extra references to your big_data that you didnt add. These are things that enable features like _, __, ___, umong others. Bunu yorumlamayı size bırakıyorum.
  • Burada da gizli oluşan ve Out değişkenlerine el atmakla ilgili öneriler var.

Yarıda Kesilen İşler

Jupyter’de olur da büyük bir veri okuma işini yarıda keserseniz o ana kadar okunan veri memory’de kalacak, ancak henüz pandas api’sine paslanmadığı için, yani “df” değişkenimiz oluşamadığı için onu silemeyeceksiniz ve memory işgali devam edecektir. Böyle bir durumda kernel’i restart etmekten veya bu Memory tüketimine katlanmaktan başka çareniz yoktur.

Profilerlar İle Gözlem Yapma

Kodunuzla ilgili 3 farklı gözlem yapılabilir:

  • Ne kadar süre çalışıyor?,
  • Ne kadar memory tüketiyor?,
  • Ne kadar cpu kullanıyor?

Süre ölçümü: notebookta nbextensions’ın Execution Time widget’ı veya %time ve %timeit magic commandlarıyla süre ölçümü yapabilirsiniz. Alternatif olarak bazı profiler paketleri(ör:cProfiler, line_profiler v.s) de süre ölçümünü çeşitli kırılımlarda verebilmektedir.

nbextension’ın Execution Time widget’ı — 122 ms sürmüş bir kod

Bir şekilde nbextensions kullanamıyorsanız, mesela Google Colab’tasınız veya JupyterHub’ta çalışıyorsunuz, böyle bir durumda ipython-autotime kütüphanesini kurup %load_ext autotime yaparsanız her hücreniz otomatik süre ölçümü yapacaktır.

Aşağıdaki örnek fonksiyonlarda verdiğim timeElapse (Hem notebook hem py dosyalarında) gibi decoratorler kullanarak da süre ölçümü yapılabilir.

Memory Tüketimi: Python’ın kendi built-in profilerlarına(cProfiler, tracemalloc) ek olarak 3rd party paketler de söz konusudur. Bunlar arasında, memory_profiler, snakeviz(cProfiler’ın görsel hali), scalene, pyInstrument, pyspy, pyflame popüler olanlardır. Örnek bir görüntü aşağıda verilecektir.

Cpu kullanımı: cpu profilerlar hem daha az sayıda hem de biraz teferruatlı olduğu için bunun yerine psutil içindeki cpu_percent değerini de loglayabilirsiniz, tabi ki bir profiler kadar detaylı olmayacaktır. Burada scalene yine en iyi seçim gibi duruyor ama henüz kullanma fırsatım olmadı, onun yerine loglama yöntemi ile ilerliyorum.

Ben bunlar için ayrı bir benchmark çalışması yapmadım, ama aşağıdaki linklerde var, ayrıca scalene’in sayfasında da görebilirsiniz.

Özetle, bu profiler’larda dikkat edilmesi gereken şey şu: Yazıldığı dile göre (C, rust, python) veya çalışma şekline göre (Sampling, full) kimisinin overhead’i çok yüksekken kimisinin düşük, buna mukabil kimisinin doğruluğu tamken kimisininki yaklaşık, keza kimisi çok hızlı kimisi daha yavaş. Seçim size kalmış.

Pandas DataFrame’in İç Yapısı ve Genel Davranışı

Bilindiği üzere Pandas’ta veriler numpy array olarak tutuluyor. En azından pandas 2.0 gelene kadar böyleydi, hala (Bu yazının yayınlandığı Temmuz 2024'te) default davranış bu şekilde, ama isterseniz bunu pyarrow array olarak değiştirebiliyorsunuz. PySparktan bildiğim bu veri yapısı gerçekten efsanevi ama şuan hala numpy’lı versiyon kullanımı yaygın olduğu için burada da böyle devam edeceğiz.

Arka planda tutulan bu numpy arrayle dataframemiz arasında ara yapılar da bulunuyor, bunlar Block ve BlockManager olarak ifade edilirler. Burada ve şurada bu konuyla ilgili güzel bi yazı var.

Bilmeniz gereken önemli bir detay, pandastaki bazı operasyonların copy bazılarının view ürettiğidir. Bu anlamda konuyla alakalı copy_on_write (Cow) mekanizmasını bilmek de önemlidir. Yaratılan yeni objenin view olması sql’deki viewlar gibi bir izlenim yaratmasın. Bir objenin view olması bunun, orjinal objeyle aynı veriyi paylaşıyor olması anlamına gelir, daha da açmak gerekirse, birinde yapılan değişiklik diğerini de etkiler demektir ama siz yine de yukarıdaki cow linkine bakın derim, zira yakında bu davranışta değişiklikler olacak.

Şimdi bu Memory işgali konusunu biraz daha irdeleyelim. Bir dataframe’den loc/iloc ile bir kesit aldık diyelim(adı subframe olsun), bu kesit bir view’dur ve bu ne kadar büyük olursa olsun resource_usage widget’ında bir artış görmezsiniz, çünkü toplam Memory kullanımında bir artış yoktur, ancak sys.getsizeof ile baktığınızda bu subframe için view diye küçük bir değer görmezsiniz. Bunu şöyle yorumlamak lazım: bu subframe ayrı bir dataframe olması durumunda hacmi bu kadar olacaktı. Burada dikkat edilmesi gereken nokta, bu subframe orjinal df ile sadece veriyi paylaşır, Memory adresini değil, o yüzden id fonksiyonuyla bakarsak adresleri farklıdır. Şimdi önemli kısım geliyor; bu yüzden orijinal dataframe’i silsek bile Memory düşüşü gözlemlemeyiz, çünkü bu orjinal dataframe’e hala bir referans vardır, ve Memory düşüşü olması için bu subframe’in de silinmesi gerekir. Hadi bunlara bir de kod olarak bakalım.

resource_usage widgetımız (Bunan sonra kısaca widget diyeceğim) başlangıç anında 0.7 değerini gösteriyorken, şunları çalıştırıyorum.

df=joblib.load(dosya)
df.memory_usage(deep=True).sum()>>20
getsizeof(df)>>20
#4813
#4813

Şimdi widget’tan bakınca toplam RAM kullanımı 4.5 görünüyor. (Yani 3.8 artış oldu. Diyeceksiniz ki, üstteki kodlarda sadece bu dataframe 4.8 GB görünürken widget’ta neden 3.8 artış var. İşte burada, gerek python’ın gerek pandas’ın gerek jupyter’in öngörülemez davranışları devreye giriyor, chatgpt ve gemini’ye de sorduğumda “Lazy evaluation, extra overhead” gibi cevaplar verdiler, ama biz şuan buna odaklanmayalım, önemli olan bu fark değil, bir artış olup olmadığı, devam edelim)

Şimdi bir view yaratalım,

dftemp_v=df.iloc[:1000000,:]

Widget’tan bakınca hala 4.5 görünüyor, yani memory artışı olmadı, zira yukarıda dediğimiz gibi bu iki obje altta yatan veriyi paylaşıyorlar.

Şimdi bir de copy yaratalım,

dftemp_c=df.iloc[:1000000,:].copy()

Widget’taki rakamın 0.8 artığını görüyorum. Bu üç variable için memory kullanımına bakalım,

def getMemUsage(*args):
return list(map(sys.getsizeof, list(args)))

getMemUsage(df,dftemp_v,dftemp_c)
#[5047111464, 1850530974, 1850530974]

(Bir gariplik daha, widget’ta 0.8 artarkan burda 1.7 GB’lık bir hacim görüyoruz. Ama biz yine bu farka değil artış olup olmamasına odaklanalım.)

Şimdi durumu bir özetleyelim. Hem dftemp_v hem dftemp_c 1.8 GB’lık büyüklükte, ilki view ve orijinal dataframe’le aynı veriyi paylaşıyor, o yüzden toplam Memory artışına neden olmuyor, ikincisi ise kopya ve ayrı bir datası var.

Şimdi de bunların Memory adreslerine bakalım.

def printMemInfo(i:str):
if isinstance(eval(i),pd.core.frame.DataFrame):
print(f"{i} :df, id:{id(eval(i))}, adres: {hex(id(eval(i)))}, refcount: {getrefcount(eval(i))}")
else:
print(f"{i} :{eval(i)}, id:{id(eval(i))}, adres: {hex(id(eval(i)))}, refcount: {getrefcount(eval(i))}")

printMemInfo("df")
printMemInfo("dftemp_v")
printMemInfo("dftemp_c")
#df :df, id:139616022598512, adres: 0x7efae36e9370, refcount: 2
#dftemp_v :df, id:139616023376032, adres: 0x7efae37a70a0, refcount: 2
#dftemp_c :df, id:139616023442528, adres: 0x7efae37b7460, refcount: 2

Gördüğünüz gibi üçünün de Memory adresleri farklı.

Mevcut durumda ana dataframe’e hangi objeler referansta bulunuyor bir bakalım,

objgraph.show_backrefs(df)

Oldukça karışık gibi, pandas dataframeler’in iç yapısıyla ilgili linklere baktığınızda bunların ne kadar kompleks objeler olduğunu göreceksiniz.

Şimdi orijinal dataframe’i silelim ve neler olduğuna bakalım.

df = None
gc.collect()
printMemInfo("df") #None'a 135942 adet ref var, df'e değil.
#df :None, id:94066142561120, adres: 0x558d7b034360, refcount: 135942

Bu arada widget’da bir düşme olmadı, tekrar graph bakalım,

objgraph.show_backrefs(df,filename=”afterNone.png”)

Artık df’e bir referans yok gibi ama aslında var, o yüzden de Memory düşüşü gözlemlemedik. Şimdi öncelikle copy olanı da silelim,

del dftemp_c

Widget’ta 1.3’lük düşüş gördüm, ama bu zaten orijinal df’e referansta bulunmuyordu, ona hala referans var, o yüzden beklediğimiz düşüş hala yok.

Son olarak view olanı da silelim,

del dftemp_v

Artık orijinal df’e giden başka referans kalmayacağı için Memory kullanımı düşer ve gerçekten şu anda widget’ta başlangıçtaki 0.7 değerini görüyorum.

Pandas vs numpy

Şimdi de pandas dataframe ile bunun altındaki veri yapısı olan numpy arrayler arasındaki ilişki nedir diye bakalım.

#aynı dataframei tekrar okumuş olalım
>>> sys.getsizeof(df)
5047111464
>>> arr = df.to_numpy()
>>> sys.getsizeof(arr)
128

Şimdi şöyle düşünmeniz gayet normal. “arr’ın hacmi küçük, o zaman bu kesin view’dır” . Bir kere view’ları SQL’deki view’larla karıştırmamak lazım demiştik, nitekim pandas’ın view’ları yine hacim kaplarlar, bunların view olması ayna etkisi gibidir, ana objedeki değişikliği view’da da görürüz, tersi de doğrudur. Peki, burada neden sadece 128 bytes görüyoruz? Çünkü bu hacim sadece array nesnesinin kendi metadata hacmini verir, altında yatan data’nınkini değil. Zaten values’a çevirdiğinizde widget’ta Memory artışı görebilirsiniz, bu onun copy olarak yaratıldığını gösterir. Eğer view yaratılırsa widgetta memory artışı görmezdiniz. Ne zaman view ne zaman copy yaratılacağı dokümantasyonda belirtilmiş. Bu arada numpy’a çevrilmiş hali üzerinden başka kompleks bir işlem uygulanacaksa bunu otomatikman view’dan copy haline döndürebilir, bu da Memory şişmesine neden olabilir. Son olarak “peki bu arr nesnesinin gerçek hacmini nasıl görürüz?” diye sorarsanız, bunun için arr.nbytes değerine bakmak lazım, ben şuan bunun için yaklaşık 2 GB değerini görüyorum.

Şunu da söylemekte fayda var, bazı işlemler geçici ara yapılar oluşturur ve işlem bitince bunlar Memory’den atılır, bir de kalıcı ara yapılar var, bunları biz doğrudan göremiyoruz, ama Memory tüketiminin olduğundan daha fazla görünmesinin sebebi bunlar olabilmektedir.

Peki ne zaman pandas ne zaman numpy tercih edilmeli? Eğer pandas’ın desteklemediği vektör işlemleri olacaksa numpy yapmanızda fayda var, zaten genel olarak numpy, pandas’tan daha hızlıdır. Kolon başlıkları vs. ihtiyacınız yoksa, sadece sayısal işlemler için kullanacaksanız (Örneğin: ML modelinde) yani satır ve kolonların sayısal indeks bilgileri yeterliyse numpy hali ile ilerlenebilir, ama yukarıda bahsettiğimiz geçici memory artışına dikkat, eğer bir sıkıntı yaratmayacak gibiyse numpy ile devam edin, sonrasında df’i silebilirsiniz zaten. Tabiki numpy’a çevirmek sadece ek Memory maliyeti değil bir de süre maliyeti yaratacaktır, bu süreye katlanmaya da değip değmeyeceğini hesaba katmanız lazım.

Özet

Bu noktada hala “Bütün bunlar çok teknik, bunları bilmek ne işe yarar” diyorsanız alttaki kaynaklara bakmanızı tavsiye ederim, ama yine de küçük bir örnek vermek gerekirse: Diyelim ki RAM’iniz 16 GB. 14 GB’lık bir datayı pandas dataframe’e aldınız. Şimdi bir nunique işlemi yaparsanız muhtemelen dead kernel hatası alırsınız, çünkü belki bu de bu işlem sırasında oluşan ara yapılar nedeniyle ilave 3–4 GB Memory tüketimi olacaktır. O yüzden “Ne de olsa ben datayı Memory’e aldım, bundan sonra Memory üzerinden sorunsuz devam ederim” diye düşünmemek lazım. Bu ara yapılar gerçekten çok can sıkıcı olabilmekte.

Şimdi yukarıda da kullandığım super_info_ metoduna yakından bakalım. Burada hem süre performansı hem de Memory performansı açısından inceleme yapacağız.

Bu fonksiyon, çeşitli özet bilgileri tek bir dataframe olarak bize sunmaktadır. Ben bunu pandas_flavour kullanarak dataframeler için bir extension method haline getirdim. Örnek bir çıktı aşağıdaki gibidir:

super_info

Fonksiyonun mevcut hali şöyle,

@register_dataframe_method        
def super_info_(df)->pd.DataFrame:
dt=pd.DataFrame(df.dtypes, columns=["Type"])
dn=pd.DataFrame(df.nunique(dropna=True), columns=["Nunique(Excl.Nulls)"])
nulls=pd.DataFrame(df.isnull().sum(), columns=["#of Missing"])
dn["Nunique(Incl.Nulls)"]=np.where(nulls["#of Missing"].values>0,dn["Nunique(Excl.Nulls)"]+1,dn["Nunique(Excl.Nulls)"])
first_non_null = df.apply(pd.Series.first_valid_index, axis=0)
d=dict(first_non_null)
first_non_null_DF=pd.DataFrame([(k,df.loc[v,k]) for k,v in d.items() if not pd.isna(v)],columns=["i","FirstNonNullValue"]).set_index("i")
MostFreqI_C=pd.DataFrame([df[x].value_counts().getFirst_() for x in df.columns], columns=["MostFreqItem","MostFreqCount"],index=df.columns)
return pd.concat([dt,dn,nulls,MostFreqI_C,first_non_null_DF],axis=1)

#super_info_'da kullandığım başka bir extension metjodu
@register_series_method
def getFirst_(seri, ifNullWhat=None):
if len(seri)>0:
return seri.head(1).reset_index().values[0]
else:
return np.array([ifNullWhat,ifNullWhat]) #first for index second for value

Daha önceki hali ise şöyleydi;

@register_dataframe_method        
def super_info_(df, dropna=True):
dt=pd.DataFrame(df.dtypes, columns=["Type"])
if dropna:
col_="Nunique(Excl.Nulls)"
else:
col_="Nunique(Incl.Nulls)"
dn=pd.DataFrame(df.nunique(dropna=dropna), columns=[col_])
nulls=pd.DataFrame(df.isnull().sum(), columns=["#of Missing"])
firstT=df.head(1).T.rename(columns={0:"First"})
try:
NonNullT=pd.DataFrame(df.loc[~df.isnull().sum(1).astype(bool)].iloc[0])
except:
NonNullT=pd.DataFrame(["Yok"]*len(dt),index=dt.index)
NonNullT.columns=["NonNullValues"]
MostFreqI=pd.DataFrame([df[x].value_counts().head(1).index[0] if not df[x].isnull().all() else None for x in df.columns], columns=["MostFreqItem"],index=df.columns)
MostFreqC=pd.DataFrame([df[x].value_counts().head(1).values[0] if not df[x].isnull().all() else None for x in df.columns], columns=["MostFreqCount"],index=df.columns)
return pd.concat([dt,dn,nulls,MostFreqI,MostFreqC,firstT,NonNullT],axis=1)

Burada iki önemli sorun vardı:

○ nunique değerlerini null’lar dahil ve hariç olacak şekilde iki ayrı versiyonla gösteriyordu. Ancak bir zaman geldi ikisine de ihtiyacım oldu. Bu fonksiyonu bir False(Nunique(Incl.Nulls)), bir True(Nunique(Excl.Nulls)) ile çalıştırmak diğer tüm bilgilerin gereksize yere tekrar çalıştırılması anlamına gelecekti. Bunun yerine fonksiyonu modifiye ettim ama ederken bir optimizasyon daha yaptım, böylece nunique sadece bir kez çalışmış olacaktır. Yukarıda görüldüğü üzere ikinci bir nunique yapmak yerine zaten hesapladığım nulls üzerinden gittim, bunun total etkisi 44 sn kadar oldu.

○ Mostfreq kısmında iki sorun vardı: Gereksiz yere iki kez value_counts yapılıyordu, bu yetmezmiş gibi her ikisinde tüm kolon üzerinde isnull kontrolü yapılıyordu, onun yerine Series’ler için bir extension method daha yazdım, böylece isnull operasyonuna gerek kalmadı (3 sn), bu fonksiyon içinde index ve value’yu da tek seferde aldığım için 12 sn de buradan tasarruf olup toplam kazanım 15 sn oldu.

○ NonNulls da tam anlamıyla istediğim gibi çalışmıyordu, onu da düzeltmiş oldum.

Burada Memory anlamında bir kazanımım olamadı, ama süre olarak önemli bir tasarruf sağladığımı söyleyebilirim. Memory açısından da şunu söyleyebilirim, Memory profiler ile baktığımda ara gizli yapılar nedeniyle 2 GB kadar arttığını ve tekrar düştüğünü görüyorum. (Not: notebook içinden sadece gözüme kestirdiğim bir satır için memit yapınca ise 7 GB arttığını görüyorum, sanırım mprof ve memit arasında çalışma mantığı farkı var)

Line #    Mem usage    Increment  Occurrences   Line Contents
=============================================================
105 18765.2 MiB 18765.2 MiB 1 def super_info_(df)->pd.DataFrame:
106 """
107 Returns a dataframe consisting of datatypes, nuniques, #s of nulls head(1), most frequent item and its frequncy,
108 where the column names are indices.
109 First, pandas_flavor must be installed via https://pypi.org/project/pandas_flavor/
110 """
111 18765.2 MiB 0.0 MiB 1 dt=pd.DataFrame(df.dtypes, columns=["Type"])
112 18765.8 MiB 0.6 MiB 1 dn=pd.DataFrame(df.nunique(dropna=True), columns=["Nunique(Excl.Nulls)"])
113 18765.8 MiB 0.0 MiB 1 nulls=pd.DataFrame(df.isnull().sum(), columns=["#of Missing"])
114 18765.8 MiB 0.0 MiB 1 dn["Nunique(Incl.Nulls)"]=np.where(nulls["#of Missing"].values>0,dn["Nunique(Excl.Nulls)"]+1,dn["Nunique(Excl.Nulls)"])
115 18765.8 MiB 0.0 MiB 1 first_non_null = df.apply(pd.Series.first_valid_index, axis=0)
116 18765.8 MiB 0.0 MiB 1 d=dict(first_non_null)
117 18765.8 MiB 0.0 MiB 48 first_non_null_DF=pd.DataFrame([(k,df.loc[v,k]) for k,v in d.items() if not pd.isna(v)],columns=["i","FirstNonNullValue"]).set_index("i")
118 20748.4 MiB -5947.9 MiB 48 MostFreqI_C=pd.DataFrame([df[x].value_counts().getFirst_() for x in df.columns], columns=["MostFreqItem","MostFreqCount"],index=df.columns)
119 18765.8 MiB -1982.6 MiB 1 return pd.concat([dt,dn,nulls,MostFreqI_C,first_non_null_DF],axis=1)

Bu da line_profiler çıktısı

Total time: 83.3796 s
File: /data01/Userbooks/n35516/Various/largedata_memory/memorytest.py
Function: super_info_ at line 105

Line # Hits Time Per Hit % Time Line Contents
==============================================================
105 def super_info_(df)->pd.DataFrame:
106 """
107 Returns a dataframe consisting of datatypes, nuniques, #s of nulls head(1), most frequent item and its frequncy,
108 where the column names are indices.
109 First, pandas_flavor must be installed via https://pypi.org/project/pandas_flavor/
110 """
111 1 844970.0 844970.0 0.0 dt=pd.DataFrame(df.dtypes, columns=["Type"])
112 1 3e+10 3e+10 38.6 dn=pd.DataFrame(df.nunique(dropna=True), columns=["Nunique(Excl.Nulls)"])
113 1 4614724515.0 5e+09 5.5 nulls=pd.DataFrame(df.isnull().sum(), columns=["#of Missing"])
114 1 1157525.0 1e+06 0.0 dn["Nunique(Incl.Nulls)"]=np.where(nulls["#of Missing"].values>0,dn["Nunique(Excl.Nulls)"]+1,dn["Nunique(Excl.Nulls)"])
115 1 2728477750.0 3e+09 3.3 first_non_null = df.apply(pd.Series.first_valid_index, axis=0)
116 1 289619.0 289619.0 0.0 d=dict(first_non_null)
117 1 2142020.0 2e+06 0.0 first_non_null_DF=pd.DataFrame([(k,df.loc[v,k]) for k,v in d.items() if not pd.isna(v)],columns=["i","FirstNonNullValue"]).set_index("i")
118 1 4e+10 4e+10 52.6 MostFreqI_C=pd.DataFrame([df[x].value_counts().getFirst_() for x in df.columns], columns=["MostFreqItem","MostFreqCount"],index=df.columns)
119 1 756502.0 756502.0 0.0 return pd.concat([dt,dn,nulls,MostFreqI_C,first_non_null_DF],axis=1)

Diğer Memory Efficient öneriler

Öncelikle şuradakine benzer birçok sayfayı inceleyip genel bir fikir edinmenizde fayda var. Ben de kısaca bahsedip sonraki konuya geçeceğim.

Doğru veri yapılarını kullanmak

Ne zaman tuple, ne zaman list, ne zaman frozenset, ne zaman iterator/generator, ne zaman list comprehension, ne zaman generator expression, ne zaman class, ne zaman dataclass, ne zaman __slots__ implementasyonlu class kullanmanız gerektiğini bilmeniz önemli. Bunlar biraz daha advanced konular olup yeni başlayan biriyseniz şimdi değil ama ileride mutlaka bakmanızı öneririm. Yazı çok daha uzamasın diye, bunları araştırmayı size bırakıyorum, aşağıya sadece list ve iterator karşılaştırması koyuyorum, burada tabi süre ölçümü yapıldı, ancak memory kullanımı açısından da karşılaştırmalar yapılabilir.

%timeit sum(range(100000000)) #iterator
#4.66 s ± 172.8 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

%timeit sum(list(range(100000000))) #list
#7.41 s ± 167.8 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Fonksiyon kullanımı

Tekrar hatırlamak gerekirse, Python’da her şey bir objedir, fonksiyonlar bile, dolayısıyla bir memory overhead’i(maliyeti) vardır. Bir iki satırlık ve küçük işlevleri olan fonksiyonlar yazmayın, kullanmayın. İşlemi doğrudan ilgili bloğa koyun. Böyle bir fonksiyonu yaratmanın maliyeti, elde edeceğiniz kazanımdan daha fazla olabilir. Süre ölçümü yaparak bu testi yapabilirsiniz.

#bu şekilde 10 sn sürerken
def carp(a,b):
return a*b

toplam=0
for i in range(100000000):
toplam=toplam+carp(i,10)

#------------------------------
#bu şekilde 7 sn sürmektedir
toplam=0
for i in range(100000000):
toplam=toplam+i*10

Yardımcı Fonksiyonlar

Bunların bir kısmı mevcut bir tipin/sınıfın extension metodu olarak kullanılabilirken(kolay ayırdetmek için bunların sonuna _ koyuyorum), bir kısmı bir modülde direk kullanımda.

#birçok classtan oluşan MyUtility.py
from psutil import virtual_memory,cpu_percent
import multiprocessing
import threading
import dataanalysis as da

#başka Class'lar da var, anca biz burada sadece Utility classını kullandık
class Utility:
@staticmethod
def getCurrentResourceInfo(usr:str, pwd:str, isTablespace:bool=False)->str:
_mem = virtual_memory().available>>30 #as GB
_cpu = cpu_percent(interval=1) #block 1 sec
if isTablespace:
tablespace=Utility.getTableSpaceUsage(usr,pwd)
return f"{_mem} GB available memory and cpu usage:%{_cpu}, total tempspace :{tablespace[0]} GB and free tempspace :{tablespace[1]} GB"
else:
return f"Available memory: {_mem} GB and cpu usage: %{_cpu}"

@staticmethod
def getTableSpaceUsage(usr,pwd):
sql= "SELECT tablespace_size/ 1024 / 1024 /1024 TOTAL_GB , round(free_space / 1024 / 1024 /1024,0) FREE_GB
FROM dba_temp_free_space WHERE tablespace_name = '.....'"
df=da.oracleSql(sql,usr, pwd) #Part II'da kullanılacak
return df.iat[0,0], df.iat[0,1]

@staticmethod
def getThreadOrProcessInfo():
if threading.current_thread().name=="MainThread":#muhtemelen multicpu
return f"{multiprocessing.current_process().name} isimli process, threadno:{threading.current_thread().name}"
else:
return f"{threading.current_thread().name} isimli thread, prosesnono:{multiprocessing.current_process().name}"

@staticmethod
def timeElapse(func):
"""
alternative to %time/timeit. In case of using an IDE apart from jupyter.
usage:
@timeElapse
def somefunc():
...
somefunc()
"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
value = func(*args, **kwargs)
func()
finito = time.time()
print("Time elapsed:{}".format(finito - start))
return value

return wrapper

Buraya kadar sabredip okuduğunuz için teşekkür ederim. Part II’de görüşmek üzere.

Kaynaklar ve İlave Okumalar

Genel

Python’da memory yönetimi

3rd Party kütüphaneler

Pandas’ın iç yapısı

Extension yazma

--

--

Volkan Yurtseven
Akbank Teknoloji

Once self-taught-Data Science Enthusiast,now graduate one