Pandas DataFrame optimizavimas didelėms duomenų apimtims

Kodėl DataFrame’ai ima lėtėti su dideliais duomenimis

Kai pradedi dirbti su Pandas biblioteka, viskas atrodo paprasta ir greita. Bet kai tavo duomenų failas peršoka kelių gigabaitų ribą, staiga susiduri su realybe – kompiuteris pradeda lėtėti, RAM’as baigiasi, o paprastas groupby() užtrunka amžinybę. Tai nutinka ne todėl, kad Pandas yra prasta biblioteka, bet todėl, kad ji pagal nutylėjimą optimizuota patogumui, o ne našumui.

Pandas DataFrame’as saugo duomenis atmintyje kaip NumPy masyvus, ir čia slypi pagrindinė problema. Jei turi 5GB CSV failą, įkėlęs jį į DataFrame’ą gali pamatyti, kad programa naudoja 15-20GB RAM’o. Taip yra dėl to, kad Pandas kiekvienam stulpeliui priskiria duomenų tipą, ir dažnai pasirenka ne efektyviausią variantą. Pavyzdžiui, sveikieji skaičiai pagal nutylėjimą saugomi kaip int64, net jei visi tavo skaičiai telpa į int8 diapazoną.

Dar viena problema – Pandas operacijos dažnai kuria duomenų kopijas. Kai filtruoji DataFrame’ą ar atliekai transformacijas, neretai atmintyje atsiranda laikini objektai, kurie dar labiau didina RAM’o poreikį. O kai pradedi jungti kelis didelius DataFrame’us su merge(), situacija tampa visiškai nekontroliuojama.

Duomenų tipų optimizavimas – pirmasis žingsnis

Pats paprasčiausias būdas sumažinti DataFrame’o atminties naudojimą – tinkamai parinkti duomenų tipus. Štai konkrečios rekomendacijos, kurios gali sumažinti atminties naudojimą 5-10 kartų.

Pradėk nuo sveikųjų skaičių. Vietoj standartinio int64, kuris užima 8 baitus, galima naudoti:
int8 skaičiams nuo -128 iki 127 (1 baitas)
int16 skaičiams nuo -32,768 iki 32,767 (2 baitai)
int32 skaičiams nuo -2 milijardų iki 2 milijardų (4 baitai)

Praktiškai tai atrodo taip:


df['age'] = df['age'].astype('int8')
df['year'] = df['year'].astype('int16')
df['population'] = df['population'].astype('int32')

Su slankiojo kablelio skaičiais situacija panaši. Vietoj float64 dažnai pakanka float32, kuris užima dvigubai mažiau vietos. Jei tavo duomenyse nereikia didelio tikslumo (pavyzdžiui, kainos su dviem skaičiais po kablelio), float32 puikiai tinka.

Bet didžiausias laimėjimas slypi teksto stulpeliuose. Pagal nutylėjimą Pandas tekstą saugo kaip object tipą, kuris yra neįtikėtinai neefektyvus. Jei stulpelyje yra pasikartojančios reikšmės (pvz., šalių pavadinimai, kategorijos, statusai), naudok category tipą:


df['country'] = df['country'].astype('category')
df['status'] = df['status'].astype('category')

Kategorinis tipas saugo unikalias reikšmes vieną kartą ir naudoja sveikuosius skaičius kaip nuorodas. Jei turi milijoną eilučių su 10 unikalių šalių pavadinimų, vietoj milijono tekstinių eilučių saugosi tik 10 pavadinimų ir milijonas mažų skaičių.

Duomenų įkėlimo strategijos

Dažnai problema prasideda jau įkeliant duomenis. Jei tiesiog naudoji pd.read_csv('huge_file.csv'), Pandas įkels viską į atmintį ir pats spręs, kokius tipus naudoti. Geriau kontroliuoti šį procesą nuo pat pradžių.

Pirma, galima nurodyti duomenų tipus iš karto:


dtypes = {
'user_id': 'int32',
'age': 'int8',
'country': 'category',
'price': 'float32'
}
df = pd.read_csv('data.csv', dtype=dtypes)

Antra, jei nereikia visų stulpelių, įkelk tik tuos, kurių tikrai reikia:


df = pd.read_csv('data.csv', usecols=['user_id', 'price', 'date'])

Trečia, jei failas tikrai didelis, naudok chunksize parametrą ir apdorok duomenis dalimis:


chunk_size = 100000
chunks = []

for chunk in pd.read_csv('huge_file.csv', chunksize=chunk_size):
# Apdorok kiekvieną dalį
chunk = chunk[chunk['price'] > 0] # Filtruok
chunk['total'] = chunk['price'] * chunk['quantity'] # Skaičiuok
chunks.append(chunk)

df = pd.concat(chunks, ignore_index=True)

Dar vienas triukas – jei duomenys turi datos stulpelį, bet tau nereikia pilnos datos funkcionalumo, gali jį palikti kaip tekstą arba konvertuoti į efektyvesnį formatą. Pandas datetime64 objektai užima nemažai vietos.

Efektyvios operacijos su dideliais duomenimis

Kai jau turi optimizuotą DataFrame’ą, svarbu mokėti su juo efektyviai dirbti. Viena didžiausių klaidų – naudoti ciklus Python’e vietoj vektorizuotų operacijų.

Blogai:

for i in range(len(df)):
df.loc[i, 'total'] = df.loc[i, 'price'] * df.loc[i, 'quantity']

Gerai:

df['total'] = df['price'] * df['quantity']

Vektorizuotos operacijos yra šimtus kartų greitesnės, nes jos vykdomos C lygmenyje per NumPy. Jei negali išvengti sudėtingesnės logikos, naudok apply() su NumPy funkcijomis arba net geriau – eval() metodą:


df.eval('total = price * quantity', inplace=True)

Kai dirbi su grupavimais, būk atsargus su apply(). Jis patogus, bet lėtas. Jei įmanoma, naudok įtaisytas agregavimo funkcijas:


# Lėčiau
df.groupby('category').apply(lambda x: x['price'].sum())

# Greičiau
df.groupby('category')['price'].sum()

Dar vienas svarbus dalykas – išvengti nereikalingų kopijų. Kai filtruoji ar modifikuoji DataFrame’ą, naudok inplace=True parametrą, kur įmanoma, arba tiesiog perrašyk kintamąjį:


df = df[df['price'] > 0] # Sukuria naują objektą
df.drop('unnecessary_column', axis=1, inplace=True) # Modifikuoja esamą

Indeksavimas ir paieška

Tinkamas indeksavimas gali dramatiškai pagreitinti duomenų paiešką ir filtravimą. Jei dažnai ieškai duomenų pagal tam tikrą stulpelį, padaryk jį indeksu:


df.set_index('user_id', inplace=True)

Dabar paieška pagal user_id bus žymiai greitesnė:


user_data = df.loc[12345] # Labai greita

Jei reikia ieškoti pagal kelis stulpelius, naudok multi-indeksą:


df.set_index(['country', 'city'], inplace=True)
df.loc[('Lithuania', 'Vilnius')]

Bet atsimink – indeksavimas turi ir kainą. Indeksas užima papildomą atmintį, ir kai kurios operacijos (pvz., pridėti naują eilutę) tampa lėtesnės. Todėl naudok indeksus tik ten, kur jie tikrai duoda naudos.

Dar vienas naudingas dalykas – query() metodas, kuris leidžia rašyti filtrus kaip tekstines išraiškas ir yra optimizuotas našumui:


# Tradicinis būdas
df[(df['age'] > 18) & (df['country'] == 'Lithuania')]

# Su query()
df.query('age > 18 and country == "Lithuania"')

query() ne tik gražiau atrodo, bet ir veikia greičiau su dideliais duomenų kiekiais.

Alternatyvos ir papildomos priemonės

Kartais Pandas tiesiog nepakanka. Kai duomenų kiekis perauga 10-20GB, verta pažvelgti į kitas bibliotekas, kurios specialiai sukurtos dideliems duomenims.

Dask – tai praktiškai Pandas ant steroidų. Jis naudoja tą pačią API, bet apdoroja duomenis dalimis ir gali dirbti su duomenimis, kurie netelpa į atmintį:


import dask.dataframe as dd

ddf = dd.read_csv('huge_file.csv')
result = ddf.groupby('category')['price'].mean().compute()

Dask automatiškai skaido duomenis į mažesnes dalis ir apdoroja jas lygiagrečiai. compute() metodas paleidžia skaičiavimus tik tada, kai tikrai reikia rezultato.

Polars – naujesnė biblioteka, parašyta Rust kalba, kuri yra neįtikėtinai greita. Ji turi panašią į Pandas API, bet optimizuota našumui:


import polars as pl

df = pl.read_csv('data.csv')
result = df.groupby('category').agg(pl.col('price').mean())

Polars gali būti 5-10 kartų greitesnis už Pandas panašioms operacijoms, ypač su dideliais duomenų kiekiais.

Modin – dar viena biblioteka, kuri leidžia naudoti Pandas API, bet automatiškai paralelizuoja operacijas:


import modin.pandas as pd # Tiesiog pakeiti import'ą

df = pd.read_csv('data.csv') # Viskas kita lieka tas pats

Jei nori likti su Pandas sintakse, bet gauti geresnį našumą, Modin yra geras pasirinkimas.

Atminties stebėjimas ir profiliavimas

Kad suprastum, kur tiksliai yra problemos, reikia matuoti. Pandas turi įtaisytą metodą atminties naudojimui tikrinti:


df.info(memory_usage='deep')

Tai parodys, kiek atminties užima kiekvienas stulpelis. Gali pamatyti, kad vienas tekstinis stulpelis suryja pusę visos atminties – tada žinai, ką optimizuoti.

Dar detalesniam profiliavimui naudok memory_profiler biblioteką:


from memory_profiler import profile

@profile
def process_data():
df = pd.read_csv('data.csv')
# Tavo kodas čia
return df

process_data()

Tai parodys, kiek atminties naudoja kiekviena kodo eilutė. Labai naudinga ieškant atminties nutekėjimų ar neefektyvių operacijų.

Jei dirbi Jupyter notebook’e, gali naudoti %memit magic komandą:


%load_ext memory_profiler
%memit df.groupby('category')['price'].sum()

Tai parodys, kiek atminties sunaudojo konkreti operacija.

Ką daryti, kai nieko nebepadeda

Kartais optimizavai viską, ką galėjai, bet duomenys vis tiek per dideli. Tada reikia keisti požiūrį.

Pirmiausia, pagalvok, ar tikrai reikia visų duomenų vienu metu. Galbūt galima apdoroti duomenis dalimis ir saugoti tarpines išvadas? Pavyzdžiui, vietoj to, kad įkeltum visus 10 metų duomenis, gali apdoroti po metus ir sujungti tik galutinius rezultatus.

Antra, naudok duomenų bazes. Jei duomenys tikrai dideli, galbūt jie turėtų būti SQLite, PostgreSQL ar kitoje duomenų bazėje? Pandas puikiai integruojasi su SQL:


import sqlite3

conn = sqlite3.connect('data.db')
df = pd.read_sql_query('SELECT * FROM users WHERE age > 18', conn)

Taip galima filtruoti ir agreguoti duomenis duomenų bazėje, o į Pandas įkelti tik tai, ko reikia analizei.

Trečia, jei dirbi su laiko eilutėmis ar dideliais skaičių masyvais, pažvelk į specializuotas bibliotekas kaip Vaex arba PyArrow. Jos naudoja kitokius duomenų saugojimo formatus (pvz., Apache Parquet), kurie yra daug efektyvesni už CSV.

Parquet formato privalumai:
– Stulpelinė duomenų struktūra (greita analizė)
– Įtaisyta kompresija (mažesni failai)
– Metadata su duomenų tipais (greitas įkėlimas)

Konvertuoti į Parquet labai paprasta:


df.to_parquet('data.parquet', compression='snappy')
df = pd.read_parquet('data.parquet')

Parquet failas gali būti 5-10 kartų mažesnis už CSV ir įsikelia daug greičiau.

Kai optimizavimas tampa įpročiu

Darbas su dideliais duomenimis Pandas’e nėra raketų mokslas, bet reikalauja disciplinos ir supratimo, kaip biblioteka veikia po gaubtu. Svarbiausia – nepasikliauti nutylėtaisiais nustatymais ir galvoti apie efektyvumą nuo pat pradžių, o ne tada, kai programa jau lūžta.

Pradėk nuo paprastų dalykų: teisingų duomenų tipų, selektyvaus stulpelių įkėlimo, vektorizuotų operacijų. Tai duos didžiausią efektą mažiausiomis pastangomis. Jei to nepakanka, žiūrėk į alternatyvias bibliotekas – Dask dideliems duomenims, Polars našumui, Parquet formatą saugojimui.

Ir nepamirsk matuoti. Optimizavimas be matavimų – tai šaudymas tamsoje. Naudok profiliavimo įrankius, stebėk atminties naudojimą, testuok skirtingus sprendimus. Kartais rezultatai gali nustebinti – tai, kas atrodo efektyvu teorijoje, praktikoje gali būti lėčiau, ir atvirkščiai.

Galiausiai, pripažink, kada Pandas tiesiog nėra tinkamas įrankis. Jei dirbi su šimtais gigabaitų duomenų, galbūt reikia Apache Spark, duomenų bazės ar debesų sprendimų. Pandas puikus vidutinio dydžio duomenų analizei, bet jam yra ribos. Žinoti, kada pasiekei tą ribą ir kada reikia ieškoti kitų sprendimų – tai irgi svarbus įgūdis.

Daugiau

JAMstack architektūra: statinių svetainių renesansas