ספריית pandas - חלק 2 - מבני נתונים - DataFrame - חלק א

Image result for pandas logo
לאחר שעסקנו בחלק 1 במבנה הנתונים Series שהוא מבנה חד ממדי, נעסוק בפוסט הזה במבנה הנתונים DataFrame שהוא מבנה דו-ממדי והוא המבנה הנפוץ יותר בשימוש. 

המבנה DataFrame (לפעמים נקרא לו בקיצור df) יכול להכיל עמודות שכל אחת מסוג שונה. ניתן לחשוב על זה כמו על טבלת SQL.
ניתן ליצור DataFrame מ:


כשיוצרים DataFrame ניתן להעביר יחד עם הערכים גם index שהוא התוויות של השורות, ו-columns שהם התוויות של העמודות. אם למשל יוצרים DataFrame  מ-Series ומעבירים גם אינדקסים, אז רק מהאינדקסים שצוינו יווצר ה-DataFrame וכל שאר המידע לא יועבר ל-DataFrame.
אם לא מעבירים אינדקסים ו-columns הם יווצרו אוטומטית על פי המידע הנכנס.

הערה:
עבור python>=3.6 עם pandas>=0.23: 
כאשר ה-data הוא dict ולא מספקים columns, ה-columns ב-DataFrame יהיו לפי הסדר שהופיע ב-dict
עבור python<3.6 או pandas<0.23: 
ה-columns ימויינו בסדר לקסיקלי (לפי סדר האותיות והמספרים) לפי ה-keys שב-dict.
כדי לבדוק את גרסת ה-python נכתוב בשורת הפקודה:
python --version

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

pd.__version__


כדי לראות מידע על ה-DataFrame ניתן להשתמש בפונקציה:
 df.info()

יצירת DataFrame מ-dict of Series

האינדקסים יהיו איחוד של האינדקסים של ה-Series שהיו ב-dict. ה-columns יהיו שמות המפתחות של ה-dicts.
לדוגמה:


>>> d = {'one': pd.Series([1., 2., 3.], index=['a', 'b', 'c']),
         'two': pd.Series([1., 2., 3., 4.], index=['a', 'b', 'c', 'd'])}
>>> df = pd.DataFrame(d)
>>> df
   one  two
a  1.0  1.0
b  2.0  2.0
c  3.0  3.0
d  NaN  4.0

>>> df.index
Index(['a', 'b', 'c', 'd'], dtype='object')

>>> df.columns
Index(['one', 'two'], dtype='object')

ניתן ליצור df מאינדקסים מסוימים מתוך ה-Sereis או מ-columns מסוימים:


>>> pd.DataFrame(d, index=['c', 'a', 'b'])
   one  two
c  3.0  3.0
a  1.0  1.0
b  2.0  2.0

>>> pd.DataFrame(d, columns=['one'])
   one
a  1.0
b  2.0
c  3.0

אם בוחרים אינדקסים או columns שלא קיימים נקבל NaN באותם המקומות:


>>> pd.DataFrame(d, index=['c', 'a', 'b', 'f'], columns=['one', 'two','three'])
   one  two three
c  3.0  3.0   NaN
a  1.0  1.0   NaN
b  2.0  2.0   NaN
f  NaN  NaN   NaN


יצירת DataFrame מ-dict of ndarrays / lists

כשיוצרים df ממערכים או רשימות חובה שהמערכים/הרשימות יהיו באותו אורך אחרת נקבל שגיאה. בנוסף, אם מציינים במפורש את האינדקסים, חובה שמספר האינדקסים יהיה כאורך המערכים.רשימות, אחרת גם פה נקבל שגיאה.


>>> d = {'one': [1., 2., 3., 4.],
         'two': [10., 20., 30., 40.]}

>>> pd.DataFrame(d)
   one   two
0  1.0  10.0
1  2.0  20.0
2  3.0  30.0
3  4.0  40.0

>>> pd.DataFrame(d, index =['a','b','c','d'])
   one   two
a  1.0  10.0
b  2.0  20.0
c  3.0  30.0
d  4.0  40.0


ניתן גם להשתמש בפונקציה DataFrame.from_dict כדי ליצור DataFrame מ-dict of dicts או dict of arrray-like. מה שמיוחד בפונקתיה הזו הוא שאפשר לקבוע האם ה-keys של ה-dict יהיו שמות העמודות או שמות השורות, וכך כיוון ה-DataFrame משתנה. קובעים זאת ע"י הפרמטר orient. בדיפולט הוא columns ואם נקבע אותו ל-index אז ה-keys יהיו שמות השורות.
לדוגמה:

>>> pd.DataFrame.from_dict(dict([('A', [1, 2, 3]), ('B', [4, 5, 6])]))
   A  B
0  1  4
1  2  5
2  3  6

>>> pd.DataFrame.from_dict(dict([('A', [1, 2, 3]), ('B', [4, 5, 6])]), orient='index')
   0  1  2
A  1  2  3
B  4  5  6

יצירת DataFrame מ-structured or record data

דומה למקרה של יצירת df מ-dict של מערכים:


>>> data = np.zeros((2, ), dtype=[('A', 'i4'), ('B', 'f4'), ('C', 'a10')])

>>> data[:] = [(1, 2., 'Hello'), (2, 3., "pandas")]

>>> pd.DataFrame(data)
   A    B          C
0  1  2.0   b'Hello'
1  2  3.0  b'pandas'

>>> pd.DataFrame(data, index=['aa','bb'])
    A    B          C
aa  1  2.0   b'Hello'
bb  2  3.0  b'pandas'

>>> pd.DataFrame(data, columns=['C','B','A'])
           C    B  A
0   b'Hello'  2.0  1
1  b'pandas'  3.0  2


יצירת DataFrame מ-list of dicts

דומה ליצירה של df מ-list of Series:

>>> data = [{'a': 1, 'b': 2}, {'a': 10, 'b': 20, 'c': 30}]
>>> pd.DataFrame(data)
    a   b     c
0   1   2   NaN
1  10  20  30.0

>>> pd.DataFrame(data, index=['one','two'])
      a   b     c
one   1   2   NaN
two  10  20  30.0

>>> pd.DataFrame(data, columns=['a','c'])
    a     c
0   


יצירת DataFrame מ-Series

אם יוצרים df מ-Series נקבל df עם אותם ערכים ואותם אינדקסים ויהיה לו עמודה אחת בשם של ה-Series. אם אין ל-Series שם העמודות יקבלו שמות מ-0 והלאה.


>>> s = pd.Series([12, 34, 56])
>>> pd.DataFrame(s)
    0
0  12
1  34
2  56

>>> s.name = 'mySeries'

>>> pd.DataFrame(s)
   mySeries
0        12
1        34
2        56


עבודה עם עמודות

העבודה עם עמודות היא די אינטואיטיבית. להלן כמה דוגמאות פשוטות:

>>> df = pd.DataFrame({'one':[1, 2, 3]}, index = ['a', 'b', 'c'])

>>> df
   one
a    1
b    2
c    3

>>> df['two'] = df['one'] * 2
>>> df
   one  two
a    1    2
b    2    4
c    3    6

>>> df['three'] = df['one'] ** 2
>>> df
   one  two  three
a    1    2      1
b    2    4      4
c    3    6      9

>>> df['flag'] = df['one'] > 2
>>> df
   one  two  three   flag
a    1    2      1  False
b    2    4      4  False
c    3    6      9   True


מחיקת עמודה

מחיקת עמודה מתבצעת ע"י del:

>>> del df['flag']
>>> df
   one  two  three
a    1    2      1
b    2    4      4
c    3    6      9

מחיקת עמודה ולקיחה של התוכן שלה למשתנה אחר מתבצעת ע"י pop:

>>> a = df.pop('two')
>>> a
a    2
b    4
c    6
Name: two, dtype: int64

>>> df
   one  three
a    1      1
b    2      4
c    3      9


יצירת עמודות


אם יוצרים עמודה עם ערך אחד הערך נכנס לכל תאי העמודה:

>>> df['four'] = 4
>>> df['txt'] = 'Israel'
>>> df
   one  three  four     txt
a    1      1     4  Israel
b    2      4     4  Israel
c    3      9     4  Israel

ניתן להכניס מערך numpy.ndarray לתוך עמודה.
אם המערך לא יהיה באורך של העמודה תתקבל שגיאה.
לעומת זאת, אם נכניס Series שהוא לא באורך של העמודה, לא תתקבל שגיאה. אם ה-Series קצר הערכים החסרים יהיו NaN. אם הוא ארוך, רק הערכים עם אותו אינדקסים יכנסו.

>>> d = np.array([10,20,30])
>>> e = pd.Series([1,2,3,4], index = ['a','b','c','d'])
>>> c = pd.Series([10,20], index = ['a','b'])

>>> df['small'] = c
>>> df['arr'] = d
>>> df['long'] = e

>>> df
   one  three  four     txt  small  arr  long
a    1      1     4  Israel   10.0   10     1
b    2      4     4  Israel   20.0   20     2
c    3      9     4  Israel    NaN   30     3

כשיוצרים עמודות הן מתווספות לסוף ה-DataFrame. ניתן להכניס עמודות במיקומים אחרים ע"י הפונקציה insert:


>>> df.insert(1,'two', df['one']*2)
>>> df
   one  two  three  four     txt  small  arr  long
a    1    2      1     4  Israel   10.0   10     1
b    2    4      4     4  Israel   20.0   20     2
c    3    6      9     4  Israel    NaN   30     3


עד כאן להפעם.


אם אהבתם, תכתבו משהו למטה...

ספריית 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 שונים. ושינוי של אחד לא ישפיע על השני.

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