Kategória: Python.
Alkategória: Adatkezelés Pythonban.
Table of Contents
|
Bevezető
Az adat keret (data frame) egy olyan adatszerkezet, amely leginkább az adatbázis táblákra hasonlít: vannak benne adott nevű és típusú oszlopok, az egyes rekordok pedig a sorok. Az adattudomány egyik alapvető fontosságú adatszerkezete.
A Pythonban a Pandas csomag valósítja meg ezt az adatszerkezetet. Nem része az alap Python telepítésnek, azt külön kell telepíteni:
pip install pandas
Szokásos konvenció szerint az alábbi módon használjuk:
import pandas as pd
A Pandas egy óriási terület, melynek csak a felszínét karcoljuk. Itt - sok más rendszerrel ellentétben - jó szívvel ajánlom a hivatalos API referenciát; közérthető példákkal van alátámasztva: https://pandas.pydata.org/pandas-docs/stable/reference/index.html.
DataFrame létrehozása
Adatkeretet számos módon létrehozhatunk; egy - talán legegyszerűbb - lehetőség szótár segítségével:
my_fruits_df = pd.DataFrame({ 'fruit': ['apple', 'banana', 'orange'], 'pieces': [3, 2, 5], }) print(my_fruits_df) # fruit pieces # 0 apple 3 # 1 banana 2 # 2 orange 5
Az eredmény tehát egy olyan adatszerkezet, melynek 2+1 oszlopa van (a +1 az index), és 3+1 sora (a +1 a fejléc).
Az adatok áttekintése
Ezekben a példákban mi magunk hozunk létre adatokat, a gyakorlati adattudományban viszont az adatok "kívülről érkeznek". A legtöbb esetben az adat "nem tiszta", sőt, nem is feltétlenül tudjuk a szerkezetét. Kilistázni viszont nem biztos, hogy tudjuk; gondoljunk pl. egy több tucat oszlopból és több millió sorból álló táblára. Az adat belső szerkezetének a felgöngyölítését elősegítő néhány függvényt ismerünk meg most. A head() az első, míg a tail() a hátsó néhány rekordot jeleníti meg. Az info() az egyes oszlopokról ad áttekintést: hány elemet tartalmaz, mi a típusa stb.:
my_fruits_df = pd.DataFrame({ 'fruit': [ 'apple', 'banana', 'orange', 'plum', 'lemon', ], 'pieces': [ 3, 2, 5, 4, 6, ], }) print(my_fruits_df.info()) # <class 'pandas.core.frame.DataFrame'> # RangeIndex: 5 entries, 0 to 4 # Data columns (total 2 columns): # # Column Non-Null Count Dtype # --- ------ -------------- ----- # 0 fruit 5 non-null object # 1 pieces 5 non-null int64 # dtypes: int64(1), object(1) # memory usage: 208.0+ bytes # None print(my_fruits_df.head(2)) # fruit pieces # 0 apple 3 # 1 banana 2 print(my_fruits_df.tail(2)) # fruit pieces # 3 plum 4 # 4 lemon 6
Végiglépkedés az elemeken
Az adatkeret filozófiájával szembe megy, így ha csak lehet, el kell kerülni a sorokon történő végiglépkedés módszert, végső megoldásként viszont jó, ha ismerjük ezt a lehetőséget is:
my_fruits_df = pd.DataFrame({ 'fruit': ['apple', 'banana', 'orange'], 'pieces': [3, 2, 5], }) for index, row in my_fruits_df.iterrows(): print(index, row['fruit'], row['pieces']) # 0 apple 3 # 1 banana 2 # 2 orange 5
Oszlopok kiválasztása
Szögletes zárójellel hivatkozhatunk egy oszlopra a neve alapján:
fruits = my_fruits_df['fruit'] print(type(fruits)) # <class 'pandas.core.series.Series'> print(fruits) # 0 apple # 1 banana # 2 orange # Name: fruit, dtype: object
Érdemes megfigyelni, hogy az eredmény típusa Series. Ez a Pandas tömb megvalósítása. A Pandas tehát belül ily módon tárolja az adatokat. A Series egyébként sok mindenben hasonlít a Numpy-ra.
Ha több oszlopot választunk ki egyszerre,akkor az eredmény DataFrame lesz, pl.:
my_fruits_df = pd.DataFrame({ 'fruit': ['apple', 'banana', 'orange'], 'pieces': [3, 2, 5], 'level': [4, 5, 5], }) fruits = my_fruits_df[['fruit', 'level']] print(fruits) # fruit level # 0 apple 4 # 1 banana 5 # 2 orange 5
Figyeljük meg a [[…]] indexelési technikát. A külső szögletes zárójel magá az indexet jelenti, a belső pedig a listát.
Csoportosítás
Tegyük fel, hogy egy-egy gyümölcs többször szerepel az adatkeretben, és összegezni szeretnénk a hozzá tartozó darabszámot. Ezt a groupby() hívással tudjuk megtenni, hasonlóan az SQL GROUP BY lehetőséghez:
my_fruits_pieces = pd.DataFrame({ 'fruit': ['apple', 'banana', 'apple', 'orange', 'banana'], 'pieces': [3, 2, 4, 5, 3], }) print(my_fruits_pieces.groupby('fruit').sum()) # pieces # fruit # apple 7 # banana 5 # orange 5
Figyeljük meg azt, hogy a pieces-t egy oszloppal feljebb írja, mint a fruit-ot. Az eredmény oszlopai valójában csak a pieces, a gyümölcsnevek pedig indexekké váltak. Ha azt szeretnénk, hogy a gyümölcsnevek megmaradjanak oszlopnak, akkor az as_index=False paramétert kell megadnunk:
my_fruits_pieces = pd.DataFrame({ 'fruit': ['apple', 'banana', 'apple', 'orange', 'banana'], 'pieces': [3, 2, 4, 5, 3], }) print(my_fruits_pieces.groupby('fruit', as_index=False).sum()) # fruit pieces # 0 apple 7 # 1 banana 5 # 2 orange 5
Másik lehetőség:
print(my_fruits_pieces.groupby('fruit').agg('sum')) # pieces # fruit # apple 7 # banana 5 # orange 5
Ha több oszlopunk van, akkor alapból mindegyik esetben ugyanazt a műveletet hajtja végre. Ha különböző műveleteket szeretnénk, akkor az agg() híváson belül szótárként tudjuk megadni, hogy mit hajtson végre:
my_fruits_pieces = pd.DataFrame({ 'fruit': ['apple', 'banana', 'apple', 'orange', 'banana'], 'pieces': [3, 2, 4, 5, 3], 'level': [4, 5, 5, 3, 4], }) print(my_fruits_pieces.groupby('fruit', as_index=False).agg({'pieces': 'sum', 'level': 'mean'})) # fruit pieces level # 0 apple 7 4.5 # 1 banana 5 4.5 # 2 orange 5 3.0
Az előfordulás számát a size() hívással tudjuk lekérdezni
print(my_fruits_pieces.groupby('fruit', as_index=False).size()) # fruit size # 0 apple 2 # 1 banana 2 # 2 orange 1
A https://towardsdatascience.com/all-pandas-groupby-you-should-know-for-grouping-data-and-performing-operations-2a8ec1327b5 oldal jól összefoglalja a Pandas groupby() függvényének a további részleteit.
Rendezés
Rendezni a sort_values() hívással lehet:
my_fruits_pieces = pd.DataFrame({ 'fruit': ['apple', 'banana', 'apple', 'orange', 'banana'], 'pieces': [3, 2, 4, 5, 3], }) my_fruits_ordered = my_fruits_pieces.sort_values(by='fruit') print(my_fruits_ordered) # fruit pieces # 0 apple 3 # 2 apple 4 # 1 banana 2 # 4 banana 3 # 3 orange 5
Fordított sorrendet az ascending paraméterrel tudunk elérni:
my_fruits_ordered = my_fruits_pieces.sort_values(by='pieces', ascending=False) print(my_fruits_ordered) # fruit pieces # 3 orange 5 # 2 apple 4 # 0 apple 3 # 4 banana 3 # 1 banana 2
Érdemes megfigyelni azt, hogy az index megtartotta az eredeti értékét.
Sorok kiválasztása
Nem véletlenül vártunk idáig a sorok kiválasztásával, és nem néztük meg rögtön az oszlopok kiválasztása után. Ezt is számos módon tudjuk végrehajtani, melyek közül a két legfontosabb a loc[] és az iloc[].
A loc[] az eredeti index alapján választja ki az elemet:
fruit_loc1 = my_fruits_ordered.loc[1] print(fruit_loc1) # fruit banana # pieces 2 # Name: 1, dtype: object print(type(fruit_loc1)) # <class 'pandas.core.series.Series'>
Amint láthatjuk, az eredmény egy Pandas Series (ami talán kicsit szokatlan).
Az iloc[] az aktuális sorrend indexe alapján választja ki a sort:
fruit_iloc1 = my_fruits_ordered.iloc[1] print(fruit_iloc1) # fruit apple # pieces 4 # Name: 2, dtype: object
Talán kissé félrevezető, hogy az iloc[] nem az index alapján választ; hozzá kell szokni!
Szűrés
Szűrés azokra a gyümölcsökre, melyből több mint 3 van:
print(my_fruits_pieces) # fruit pieces # 0 apple 3 # 1 banana 2 # 2 apple 4 # 3 orange 5 # 4 banana 3 my_fruits_filtered = my_fruits_pieces[my_fruits_pieces['pieces'] > 3] print(my_fruits_filtered) # fruit pieces # 2 apple 4 # 3 orange 5
Összetett szűrés esetén a & (logikai és), | (logikai vagy) és ~ (logikai tagadás) műveleteket használhatjuk. Ezt soronként hajtja végre. (Általában a programozásban a &&, || és ! jelenti a logikai és, vagy és tagadás műveleteket, míg a &, a | és a ~ a bitenkéntit. Itt inkább hasonlít a bitenkéntire; erről lehet megjegyezni.)
my_fruits_filtered_complex = my_fruits_pieces[(my_fruits_pieces['pieces'] > 2) & ~(my_fruits_pieces['fruit'] == 'apple')] print(my_fruits_filtered_complex) # fruit pieces # 3 orange 5 # 4 banana 3
Hiányzó adatok kezelése
A gyakorlatban igen sok esetben előfordulnak hiányos adatok. A notna() függvény segítségével tudjuk kiszűrni. Ahogy az alábbi példa is mutatja, ez a None és a NaN értékeket is kiszűri:
my_fruits_df = pd.DataFrame({ 'fruit': ['apple', None, 'orange'], 'pieces': [None, 2, 5], }) print(my_fruits_df) # fruit pieces # 0 apple NaN # 1 None 2.0 # 2 orange 5.0 my_fruits_notna = my_fruits_df[my_fruits_df['fruit'].notna() & my_fruits_df['pieces'].notna()] print(my_fruits_notna) # fruit pieces # 2 orange 5.0
A hiányzó adatok alapértelmezett értékkel történő feltöltését az alábbi alfejezetben vizsgáljuk meg.
Adatkeretek összekapcsolása
Tegyük fel, hogy az egyik adatkeret a gyümölcsök darabszámát, míg a másik az egységárat adja meg, és ezt szeretnénk összekapcsolni. Ezt a merge utasítással tudjuk megtenni:
my_fruits_pieces = pd.DataFrame({ 'fruit': ['apple', 'banana', 'orange'], 'pieces': [3, 2, 5], }) my_fruits_prices = pd.DataFrame({ 'fruit': ['apple', 'banana', 'orange'], 'price': [20, 10, 50], }) my_fruits_merged = pd.merge(my_fruits_pieces, my_fruits_prices, on='fruit') print(my_fruits_merged) # fruit pieces price # 0 apple 3 20 # 1 banana 2 10 # 2 orange 5 50
Előfordulhat, hogy bizonyos gyümölcsök csak az egyikben vagy a másikban találhatóak. Alapértelmezésben a közös metszetet számolja ki:
my_fruits_pieces = pd.DataFrame({ 'fruit': ['apple', 'banana', 'orange'], 'pieces': [3, 2, 5], }) my_fruits_prices = pd.DataFrame({ 'fruit': ['orange', 'apple', 'plum'], 'price': [10, 20, 5], }) my_fruits_merged = pd.merge(my_fruits_pieces, my_fruits_prices, on='fruit') print(my_fruits_merged) # fruit pieces price # 0 apple 3 20 # 1 orange 5 10
Ez az INNER JOIN SQL utasításnak felel meg. Az OUTER JOIN-nak megfelelő:
my_fruits_merged = pd.merge(my_fruits_pieces, my_fruits_prices, on='fruit', how='outer') print(my_fruits_merged) # fruit pieces price # 0 apple 3.0 20.0 # 1 banana 2.0 NaN # 2 orange 5.0 10.0 # 3 plum NaN 5.0
Itt tehát az üres helyekre NaN kerül. Ha konkrét értékekkel szeretnénk feltölteni, azt a következőképpen tudjuk megtenni:
print(my_fruits_merged.fillna(0.0)) # fruit pieces price # 0 apple 3.0 20.0 # 1 banana 2.0 0.0 # 2 orange 5.0 10.0 # 3 plum 0.0 5.0
Csak adott oszlop NaN értékeinek feltöltése:
print(my_fruits_merged.fillna({'pieces': -1})) # fruit pieces price # 0 apple 3.0 20.0 # 1 banana 2.0 NaN # 2 orange 5.0 10.0 # 3 plum -1.0 5.0
A fillna() egyébként nem helyben tölti fel a hiányzó értékeket, hanem egy új adatkeretet hoz létre. Az inplace=True paraméterrel tudjuk megadni, hogy helyben töltse fel.
Két adatkeret Descartes-szorzatát a how='cross' paraméterrel hozhatjuk létre:
my_df1 = pd.DataFrame({ 'column1': [3, 2, 5], 'column2': ['a', 'b', 'c'], }) my_df2 = pd.DataFrame({ 'column3': [1, 4], }) print(pd.merge(my_df1, my_df2, how='cross')) # column1 column2 column3 # 0 3 a 1 # 1 3 a 4 # 2 2 b 1 # 3 2 b 4 # 4 5 c 1 # 5 5 c 4
Oszlopok hozzáadása
Más oszlopokból számított oszlopot a következőképpen tudunk hozzáadni:
my_fruits_merged['total_price'] = my_fruits_merged['pieces'] * my_fruits_merged['price'] print(my_fruits_merged[['fruit', 'total_price']]) # fruit total_price # 0 apple 60.0 # 1 banana NaN # 2 orange 50.0 # 3 plum NaN
Adott feltételtől függő oszlopot célszerűen a Numpy where() függvény segítségével tudunk hozzáadni. Az alábbi példában ezzel a módszerrel adjuk hozzá két oszlop közül a nagyobbat:
import numpy as np my_fruits_df = pd.DataFrame({ 'values1': [7, 2, 5], 'values2': [6, 4, 3], }) my_fruits_df['max'] = np.where(my_fruits_df['values1'] > my_fruits_df['values2'], my_fruits_df['values1'], my_fruits_df['values2']) print(my_fruits_df) # values1 values2 max # 0 7 6 7 # 1 2 4 4 # 2 5 3 5
Ha nem tudjuk zárt képletben kifejezni az új értéket, akkor az apply() függvényt használhatjuk, pl.:
my_fruits_df = pd.DataFrame({ 'values1': [7, 2, 5], 'values2': [6, 4, 3], }) def my_max(a, b): if a > b: return a else: return b my_fruits_df['max'] = my_fruits_df.apply(lambda row: my_max(row['values1'], row['values2']), axis='columns') print(my_fruits_df) # values1 values2 max # 0 7 6 7 # 1 2 4 4 # 2 5 3 5
Célszerű ezt a megoldást elkerülni, ha csak lehet.
Ezzel a módszerrel egyszerre több oszlopnak is adhatunk értéket:
my_df = pd.DataFrame({ 'values1': [7, 2, 5], 'values2': [6, 4, 3], }) def my_min_max(a, b): if a > b: return (b, a) else: return (a, b) my_df[['min', 'max']] = my_df.apply(lambda row: my_min_max(row['values1'], row['values2']), axis='columns').tolist() print(my_df) # values1 values2 min max # 0 7 6 6 7 # 1 2 4 2 4 # 2 5 3 3 5
Ld. a tolist()-et az értékadási sor végén.
Az oszlop hozzáadásánál előjöhet egy SettingWithCopyWarning figyelmeztetés:
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead
See the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
A hibát az alábbi kódrészlettel lehet előidézni:
import pandas as pd my_fruits_df = pd.DataFrame({ 'fruit': ['apple', 'orange', 'banana'], 'pieces': [3, 2, 5], }) my_fruits_df[my_fruits_df['pieces'] < 4]['pieces'] = my_fruits_df['pieces'] + 1 # SettingWithCopyWarning print(my_fruits_df) # fruit pieces # 0 apple 3 # 1 orange 2 # 2 banana 5
A problémát az okozza, hogy a my_fruits_df[my_fruits_df['pieces'] < 4] valójában egy másolat, így nem változtatja meg az eredeti DataFrame-et. A megoldás a .loc:
my_fruits_df.loc[my_fruits_df['pieces'] < 4, 'pieces'] = my_fruits_df[my_fruits_df['pieces'] < 4]['pieces'] + 1 print(my_fruits_df) # fruit pieces # 0 apple 4 # 1 orange 3 # 2 banana 5
Erről részletesebben a SettingsWithCopyWarning oldalamon olvashatunk, angolul.
Oszlop feltételes megváltoztatása
Tegyük fel, hogy meg szeretnénk változtatni egy oszlopot feltételesen; például ha kevesebb mint 4 gyümölcs van, akkor adjunk hozzá egyet. A megoldás:
my_fruits_df = pd.DataFrame({ 'fruit': ['apple', 'banana', 'orange'], 'pieces': [3, 2, 5], }) my_fruits_df.loc[my_fruits_df['pieces'] < 4, 'pieces'] = my_fruits_df['pieces'] + 1 print(my_fruits_df) # fruit pieces # 0 apple 4 # 1 banana 3 # 2 orange 5
Adatok mozgatása
A shift() függvénnyel lehet adatokat mozgatni. Az alábbi példában a pieces oszlopon belül mozgatjuk el az értékeket eggyel lefelé, nullával feltöltve az adatot.
my_fruits_df = pd.DataFrame({ 'fruit': ['apple', 'banana', 'orange'], 'pieces': [3, 2, 5], }) my_fruits_df['pieces'] = my_fruits_df['pieces'].shift(1, fill_value=0) print(my_fruits_df) # fruit pieces # 0 apple 0 # 1 banana 3 # 2 orange 2
Dátumkezelés
A Pandas-nak saját dátumformátuma van: a Timestamp. A Timedelta segítségével tudunk műveleteket végrehajtani.
Az alábbi példában az adatkeret két dátum oszlopot tartalmaz. Azt vizsgáljuk meg, hogy a második dátum az első után van-e, de nem több mint két nappal:
my_df = pd.DataFrame({ 'first_date': [ pd.Timestamp('2021-06-14'), pd.Timestamp('2021-06-17'), pd.Timestamp('2021-06-16'), ], 'second_date': [ pd.Timestamp('2021-06-15'), pd.Timestamp('2021-06-16'), pd.Timestamp('2021-06-20'), ] }) date_condition = (my_df['first_date'] < my_df['second_date']) & (my_df['second_date'] <= (my_df['first_date'] + pd.Timedelta(days=2))) print(date_condition) # 0 True # 1 False # 2 False # dtype: bool
Néhány további hasznos tudnivaló:
- Az időpontot is megadhatjuk, pl. pd.Timestamp('2021-06-14 16:43:12').
- A date() függvénnyel tudjuk lekérdezni a dátum részt.
- Hiányzó dátum: NaT (not a time)
Duplikált elemek
Beépített függvények segítik megtalálni a duplikátumokat. A duplicated() egy logikai tömböt ad vissza, amely azt mondja meg, hogy egy adott sor korábban előfordul-e már. A drop_duplicates() törli a duplikátumokat (nem helyben):
my_fruits_df = pd.DataFrame({ 'fruit': ['apple', 'banana', 'apple'], 'pieces': [3, 2, 3], }) print(my_fruits_df.duplicated()) # 0 False # 1 False # 2 True # dtype: bool print(my_fruits_df.drop_duplicates()) # fruit pieces # 0 apple 3 # 1 banana 2
Korreláció
A Pandas tartalmaz az adattudomány által alkalmazott statisztikai módszereket. Ezek részletes ismertetése túlmutat ennek a leírásnak a keretein, most csak az egyik leggyakrabban alkalmazott ilyen jellegű műveletet nézzük meg: a korrelációs számítást. Tegyük fel, hogy az adatkeret osztályzatokat tartalmaz (az oszlopok tantárgyakat jelentenek, míg a sorok adott diáknak a jegyeit adott tantárgyból), és azt szeretnénk megnézni, hogy mely tantárgyak mennyire korrelálnak a többiekkel. A corr() függvényt tudjuk erre a célra használni:
grades = pd.DataFrame({ 'name': ['John', 'Hanna', 'Jack', 'Mary', 'Steve'], 'math': [5, 5, 4, 3, 2], 'physics': [5, 4, 4, 3, 2], 'grammar': [5, 3, 4, 4, 2], 'literature': [5, 4, 3, 4, 2], }) print(grades.corr()) # math physics grammar literature # math 1.000000 0.941742 0.605406 0.773574 # physics 0.941742 1.000000 0.807692 0.807692 # grammar 0.605406 0.807692 1.000000 0.807692 # literature 0.773574 0.807692 0.807692 1.000000
Külső adatforrások
A Pandas számos adatforrásból tud többféle formátumú adatot olvasni: adatbázis, Excel, CSV, JSON stb., melyek ismertetése szintén túlmutat az oldal keretein. Itt most a JSON formátumot vizsgáljuk meg. Az alábbi példa megmutatja, hogy hogyan lehet JSON-ba konvertálni az adatkeretet, ill. JSON-ból vissza adatkeretté:
my_fruits_df = pd.DataFrame({ 'fruit': ['apple', 'banana', 'orange'], 'pieces': [3, 2, 5], }) my_fruits_json = my_fruits_df.to_json() print(my_fruits_json) # {"fruit":{"0":"apple","1":"banana","2":"orange"},"pieces":{"0":3,"1":2,"2":5}} my_fruits_df_restored = pd.read_json(my_fruits_json) print(my_fruits_df_restored) # fruit pieces # 0 apple 3 # 1 banana 2 # 2 orange 5
Ha a read_json()-nak fájlnevet adjunk meg, akkor azt beolvassa. Pl. hozzunk létre egy fájlt fruits.json néven az alábbi tartalommal:
{
"fruit": {
"0": "apple",
"1": "banana",
"2": "orange"
},
"pieces": {
"0": 3,
"1": 2,
"2": 5
}
}
Az alábbi kódrészlet beolvassa, Pandas adatkeretté konvertálja és kiírja:
my_fruits_df_restored_from_file = pd.read_json('fruits.json') print(my_fruits_df_restored_from_file) # fruit pieces # 0 apple 3 # 1 banana 2 # 2 orange 5