ספריית pandas - חלק 1 - מבני נתונים - Series

Image result for pandas logo

מבוא

ספריית pandas היא python package שמאפשרת עבודה מהירה, גמישה ואינטואיטיבית עם מבני נתונים רלציונים. יוצרי הספריה התכוונו ליצור ספריה שתהיה ה-ספריה היסודית עבור data analysis ב-python. ספריית pandas היא פרוייקט קוד פתוח נפוץ מאוד בשימוש בפרוייקטים של Machine Learning ובפרוייקטים נוספים רבים. לספריית pandas יש ביצועים מהירים. חלק גדול מקוד ה-low level שלה נכתב ב-Cython (קומפיילר שמאפשר כתיבת הרחבות ל-python ב-C/C++). אבל כמובן כדי ליצור ספרייה גנרית יש צורך לפעמים להקריב ביצועים לשם כך. לכן אם הפרוייקט שלכם נצרך לפעולה מסויימת הרבה פעמים, יתכן שתוכלו לכתוב קוד מיוחד לצרכים שלכם שיהיה מהיר יותר.
pandas במושתת על NumPy לכן כדי להשתמש ב-Pandas צריך לייבא את NumPy ואת pandas:


>>> import numpy as np
>>> import pandas as pd

מבני נתונים

ל-pandas יש שני מבני נתונים עיקריים:
1.      Series – מבנה נתונים חד-ממדי
2.      DataFrame – מבנה נתונים דו-ממדי (df בקיצור)
דרך פשוטה לחשוב על מבני הנתונים היא שמבנה נתונים מורכב יותר מחזיק מבני נתונים פשוטים יותר. למשל DataFrame מכיל Series ו-Series מכיל סקלאר.
כשמשתמשים במערך של NumPy (נקרא ndarray) להחזיק מבנה דו-ממדי או תלת-ממדי, צריך לשים לב תמיד באיזה מימד נמצאים הנתונים ולהתאים את הפעולות לפי צורת שמירת הנתונים. לעומת זאת ב-Pandas המימדים הם יותר עניין סמנטי עבור המידע. במקום לדבר על עמודה ראשונה ועמודה שניה מדברים על אינדקס המידע ועל עמודות המידע. כך הקוד נהיה יותר קריא ודורש פחות השקעת מחשבה על צורת שמירת הנתונים. 
לדוגמה:

>>> for col in df.columns:
        series = df[col]

כל מבני הנתונים ב-Pandas הם value-mutable, ז"א שניתן לשנות ערכים בתוך מבנה נתונים. אבל לא תמיד ניתן לשנות את גודל מבנה הנתונים. למשל, אורך Series לא ניתן לשינוי. לעומת זאת, ניתן להוסיף עמודות ל-df.
עם זאת, רוב הפונקציות יוצרות אובייקטים חדשים ולא משנות את ה-Input שהן קיבלו. 

Series

ה-series הוא מבנה חד-ממדי הכולל תוויות (labeled), שיכול להכיל כל סוג מידע (integers, strings, floating point numbers, Python objects וכו'). התוויות של המידע נקראות גם אינדקס.
הדרך הפשוטה ביותר ליצור series היא:


>>> s = pd.Series(data, index=index)

כאשר:

  • data - הנתונים עצמם. יכול להיות Python dict, או ndarray או מספרים ממש.
  • index - תוויות הנתונים. רשימה של התוויות של המידע. וזה דבר שמשתנה לפי סוג המידע.

From ndarray

אם ה-data הוא ndarray, אז האינדקס חייב להיות באותו אורך של ה-data. אם לא שלחנו אינדקס הוא יווצר אוטומטית מאפס ועד לכמות הנתונים פחות אחד.
דוגמה:


>>> s = pd.Series(np.random.randn(5), index=['first','second','third','fourth','fifth'])
>>> s
first    -1.873184
second    1.041916
third     1.027184
fourth   -0.691705
fifth     1.291348
dtype: float64

>>> s.index
Index(['first', 'second', 'third', 'fourth', 'fifth'], dtype='object')

>>> r = pd.Series(np.random.randn(5))
>>> r
0   -1.512113
1    0.746104
2   -0.542947
3    0.356579
4   -0.803947
dtype: float64

הערה: pandas מאפשר גם אינדקסים לא ייחודיים (שני אינדקסים זהים). אם מנסים לבצע על מבנה שמכיל אינדקסים זהים פעולה שלא תומכת באינדקסים זהים, נקבל שגיאה.

From dict

ניתן ליצור Series מ-dict.
לדוגמה:


>>> d = {'c':1,'a':2,'b':3}
>>> pd.Series(d)
c    1
a    2
b    3
dtype: int64

הערה:
עבור python>=3.6 עם pandas>=0.23: כאשר ה-data הוא dict ולא מספקים אינדקס, האינדקס ב-series יהיה לפי הסדר שהופיע ב-dict
עבור python<3.6 או pandas<0.23: האינדקס ימויין בסדר לקסיקלי (לפי סדר האותיות והמספרים) לפי ה-keys שב-dict. בדוגמה לעיל ה-series היה בסדר [‘a’,’b’,’c’] במקום [‘c’,’a’,’b’].
כדי לבדוק את גרסת ה-python נכתוב בשורת הפקודה:
python --version

כדי לבדוק את גרסת ה-pandas נכתוב:
 pd.__version__

אם יחד עם ה-dict שולחים גם אינדקס מקבלים Series שמורכב מהערכים לפי האינדקסים המבוקשים.


>>> pd.Series(d,index=['a','b','a','d'])
a    2.0
b    3.0
a    2.0
d    NaN
dtype: float64

אם יש אינדקס שלא קיים pandas מכניס לו ערך של NaN שזה קיצור של Not a Number. זו הדרך של Pandas להשלמת ערכים חסרים.

From scalar

ניתן ליצור Series ממספר פשוט. אותו מספר יהיה הערך לכל האינדקסים.
לדוגמה:


>>> pd.Series(3 ,index=['a','b','c'])
a    3
b    3
c    3
dtype: int64


Series לעומת ndarray


Series פועל בצורה דומה ל-ndarray של NumPy. וניתן לשלוח אותו כפרמטר עבור רוב פונקציות NumPy. צריך לשים לב שחיתוך של Series יחתוך גם את האינקדסים. 

להלן כמה דוגמאות:


>>> s = pd.Series(np.random.randn(5), index=['first','second','third','fourth','fifth'])
>>> s
first    -0.613318
second    0.531901
third     0.750628
fourth   -2.339552
fifth     0.054086
dtype: float64

>>> s[1]
0.5319012043677321

>>> s[:2]
first    -0.613318
second    0.531901
dtype: float64

>>> s[s > s.median()]
second    0.531901
third     0.750628
dtype: float64

>>> s[s < s.mean()]
first    -0.613318
fourth   -2.339552
dtype: float64

>>> s[[0, 4, 1]]
first    -0.613318
fifth     0.054086
second    0.531901
dtype: float64

>>> np.exp(s)
first     0.541551
second    1.702165
third     2.118329
fourth    0.096371
fifth     1.055576
dtype: float64

בדומה ל-ndarray גם ל-Series יש dtype:


>>> s.dtype
dtype('float64')


בד"כ ה-dtype הוא מאחד הסוגים של NumPy. בנוסף, ב-pandas הוסיפו עוד כמה סוגים של נתונים. במקרים האלו ה-dtype יהיה ExtensionDtype.

אם יש צורך במערך עצמו שה-Series מחזיק ניתן לגשת אליו דרך Series.array:


>>> s.array
<PandasArray>
[ -0.6133182061989823,   0.5319012043677321,   0.7506275056129353,
  -2.3395522211220814, 0.054086401543235095]
Length: 5, dtype: float64

זה יכול להיות שימושי כשצריך לעשות פעולות ללא האינדקסים.
ה-Series.array יהיה תמיד מסוג ExtensionArray. שזה בעצם מעטפת סביב מערך אמיתי כמו ndarray.

אם אנחנו רוצים להמיר Series ל-ndarray ניתן להשתמש ב-()Series.to_numpy:


>>> s.to_numpy()
array([-0.61331821,  0.5319012 ,  0.75062751, -2.33955222,  0.0540864 ])

שימוש Series בדומה ל-dictionary

ה-Series הוא כמו dict בגודל קבוע וניתן לשמור בו נתונים ולקבל ממנו נתונים ע"י שימוש באינדקס שלו שמשמש כ-label:


>>> s = pd.Series(pd.np.random.randn(5), index=['a','b','c','d','e'])

>>> s
a   -2.690282
b   -1.830599
c   -0.648347
d   -0.556113
e    0.809609
dtype: float64

>>> s['b']
-1.8305993446632571

>>> s['b']=17
>>> s
a    -2.690282
b    17.000000
c    -0.648347
d    -0.556113
e     0.809609
dtype: float64

>>> 'c' in s
True

>>> 'g' in s
False

אם מנסים לקחת אינדקס שלא קיים נזרק exception:


>>> s['g']
KeyError: 'g'

לעומת זאת אם משתמשים ב-get עבור אינדקס שלא קיים מקבלים None ואפשר גם להגדיר ערך שיוחזר במקרה שאינדקס לא קיים:


>>> a = s.get('g')
>>> a is None
True

>>> s.get('g', 0)
0

פעולות וקטוריות

בדומה ל-NumPy נדיר שיהיה צורך לעשות פעולה על מערך בעזרת לולאה. ברוב המקרים ניתן לעשות פעולות וקטוריות ובכך להימנע מלולאות ולקבל תוכנה מהירה יותר.
לדוגמה:


>>> s+s
a    -5.380563
b    34.000000
c    -1.296693
d    -1.112225
e     1.619218
dtype: float64

>>> s * 3
a    -8.070845
b    51.000000
c    -1.945040
d    -1.668338
e     2.428827
dtype: float64

>>> np.exp(s)
a    6.786182e-02
b    2.415495e+07
c    5.229096e-01
d    5.734338e-01
e    2.247029e+00
dtype: float64


פעולות בין Series עם תויות שונות - label alignment


כאשר מבצעים פעולות בין שני Series, הפעולות יתבצעו בין הערכים בעלי אותו אינדקס. דבר כזה נקרא label alignment. אם אינדקס מסוים קיים ב-Series אחד אבל לא בשני התוצאה תהיה NaN.
לדוגמה:


>>> s = pd.Series(np.arange(5), index=['a','b','c','d','e'])
>>> r = pd.Series(np.arange(5), index=['b','c','d','e','f'])

>>> s
a    0
b    1
c    2
d    3
e    4
dtype: int32

>>> r
b    0
c    1
d    2
e    3
f    4
dtype: int32

>>> s + r
a    NaN
b    1.0
c    3.0
d    5.0
e    7.0
f    NaN
dtype: float64

אם רוצים להיפטר מכל האינדקסים שמכילים NaN ניתן להשתמש בפונקציה dropna:


>>> (s + r).dropna()
b    1.0
c    3.0
d    5.0
e    7.0


נתינת שם ל-Series

ניתן לתת שם ל-Series ע"י המשתנה name שיש לכל אובייקט מסוג Series.


>>> s = pd.Series(np.arange(3), index=['a','b','c'], name = 'mySeries')

>>> s.name
'mySeries'

אם לא נותנים שם אז name יהיה None.
כשיוצרים Series חדש מאחד קיים ניתן ליצור אותו עם שם חדש:


>>> s1 = s.rename('yourSeries')
>>> s1
a    0
b    1
c    2
Name: yourSeries, dtype: int32

עכשיו s ו-s1 הם שני Series שונים. ושינוי של אחד לא ישפיע על השני.

עד כאן להפעם.
אם אהבתם, תכתבו משהו למטה...

אין תגובות:

הוסף רשומת תגובה