Pandas

Kategória: Python.
Alkategória: Adatkezelés Pythonban.

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
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License