ปกติแล้วซีรีส์ที่มีดัชนีตัวเดียวจะเป็นข้อมูลที่มีเพียงมิติเดียว คือระบุแค่ดัชนีเพื่อเข้าถึงข้อมูลข้างใน
แต่พอเป็นเดตาเฟรมมีคอลัมน์ด้วยจึงเพิ่มเป็นสองมิติ คือต้องระบุทั้งดัชนีและคอลัมน์
ซีรีส์ที่มีดัชนีซ้อนกันสองตัวก็ถือว่ามีสองมิติเช่นกัน เพราะต้องระบุดัชนีทั้ง ๒ ตัวเพื่อเข้าถึงข้อมูล
ดังนั้นหากมองเช่นนี้แล้ว ซีรีส์ที่มีดัชนีสองตัวกับเดตาเฟรมนั้นก็เป็นอะไรที่คล้ายๆกัน
ที่จริงแล้วเดตาเฟรมนั้นสามารถแปลงให้กลายเป็นซีรีส์ที่มีดัชนีสองตัวได้ไม่ยาก นั่นคือทำโดยใช้เมธอด stack
ตัวอย่าง สร้างตารางข้อมูลของโปเกมอนซึ่งแต่ละแถวแสดงชนิดของโปเกมอน แต่ละคอลัมน์แสดงระดับวิวัฒนาการ (ร่าง) ของโปเกมอน
import pandas as pd
pokemon = pd.DataFrame([
['ชิโครีตา','เบย์ลีฟ','เมกาเนียม'],
['ฮิโนอาราชิ','แม็กมาราชิ','บักฟูน'],
['วานิโนโกะ','อาลิเกตซ์','ออร์ไดล์']],
index=pd.Series(['พืช','ไฟ','น้ำ'],name='ชนิด'),
columns=pd.Series([1,2,3],name='ร่าง'))
print(pokemon)
ได้
ร่าง |
1 |
2 |
3 |
ชนิด |
|
|
|
พืช |
ชิโครีตา |
เบย์ลีฟ |
เมกาเนียม |
ไฟ |
ฮิโนอาราชิ |
แม็กมาราชิ |
บักฟูน |
น้ำ |
วานิโนโกะ |
อาลิเกตซ์ |
ออร์ไดล์ |
จากนั้นลองใช้ stack เพื่อแปลงเป็นซีรีส์ที่มีดัชนี ๒ ตัว
print(pokemon.stack())
ได้
ชนิด ร่าง
พืช 1 ชิโครีตา
2 เบย์ลีฟ
3 เมกาเนียม
ไฟ 1 ฮิโนอาราชิ
2 แม็กมาราชิ
3 บักฟูน
น้ำ 1 วานิโนโกะ
2 อาลิเกตซ์
3 ออร์ไดล์
dtype: object
เท่านี้คอลัมน์ของเดตาเฟรมก็กลายมาเป็นดัชนีตัวที่สองของซีรีส์ที่เกิดขึ้นมาใหม่
ส่วนกระบวนการตรงกันข้าม ก็คือการแปลงจากซีรีส์ที่มีดัชนีสองตัวมาเป็นเดตาเฟรมจะใช้เมธอด unstack
ถ้าลอง
print(pokemon.stack().unstack())
แบบนี้ผลที่ได้ก็คือเดตาเฟรมตัวเดิม
แต่ unstack นั้นสามารถเลือกว่าจะดึงดัชนีตัวไหนมาได้โดยใส่หมายเลขของดัชนีนั้นหรือใส่ชื่อก็ได้ ปกติถ้าไม่ใส่จะเป็นการเลือกดัชนีตัวท้ายสุด ในที่นี้คือ "ร่าง"
ดังนั้นถ้าจะเลือกดึง "ชนิด" ขึ้นมาก็ใส่เป็น
print(pokemon.stack().unstack('ชนิด'))
# หรือ print(pokemon.stack().unstack(0))
ได้
ชนิด |
พืช |
ไฟ |
น้ำ |
ร่าง |
|
|
|
1 |
ชิโครีตา |
ฮิโนอาราชิ |
วานิโนโกะ |
2 |
เบย์ลีฟ |
แม็กมาราชิ |
อาลิเกตซ์ |
3 |
เมกาเนียม |
บักฟูน |
ออร์ไดล์ |
คอลัมน์กับดัชนีจะกลับกันจากข้อมูลตอนแรก
กรณีข้อมูลสามมิติ stack นั้นไม่ได้ใช้แค่กับเดตาเฟรมที่มีดัชนีแค่ตัวเดียว แต่กับแบบหลายดัชนีก็สามารถใช้ได้ด้วย
และ unstack นั้นไม่ใช่แค่ใช้กับซีรีส์แต่ยังใช้กับเดตาเฟรมได้ด้วย
ลองดูตัวอย่างโดยใช้โปเกมอนชุดเดิม แต่เพิ่มข้อมูลหมายเลขเข้าไป
pokemon = pd.DataFrame({
'สายพันธุ์':[
'ชิโครีตา','เบย์ลีฟ','เมกาเนียม',
'ฮิโนอาราชิ','แม็กมาราชิ','บักฟูน',
'วานิโนโกะ','อาลิเกตซ์','ออร์ไดล์'],
'หมายเลข':[152,153,154,155,156,157,158,159,160]},
index=[pd.Series(['พืช','พืช','พืช','ไฟ','ไฟ','ไฟ','น้ำ','น้ำ','น้ำ'],name='ชนิด'),
pd.Series([1,2,3,1,2,3,1,2,3],name='ร่าง')])
print(pokemon)
แบบนี้จะเป็นข้อมูลที่มีมิติเป็นสามมิติ คือมี "ชนิด", "ร่าง" แล้วก็มีแยกว่าเป็นข้อมูลสายพันธุ์หรือหมายเลข (ไม่ได้ตั้งชื่อ)
|
|
สายพันธุ์ |
หมายเลข |
ชนิด |
ร่าง |
|
|
พืช |
1 |
ชิโครีตา |
152 |
2 |
เบย์ลีฟ |
153 |
3 |
เมกาเนียม |
154 |
ไฟ |
1 |
ฮิโนอาราชิ |
155 |
2 |
แม็กมาราชิ |
156 |
3 |
บักฟูน |
157 |
น้ำ |
1 |
วานิโนโกะ |
158 |
2 |
อาลิเกตซ์ |
159 |
3 |
ออร์ไดล์ |
160 |
ลอง stack ดู
print(pokemon.stack())
จะได้
ชนิด ร่าง
พืช 1 สายพันธุ์ ชิโครีตา
หมายเลข 152
2 สายพันธุ์ เบย์ลีฟ
หมายเลข 153
3 สายพันธุ์ เมกาเนียม
หมายเลข 154
ไฟ 1 สายพันธุ์ ฮิโนอาราชิ
หมายเลข 155
2 สายพันธุ์ แม็กมาราชิ
หมายเลข 156
3 สายพันธุ์ บักฟูน
หมายเลข 157
น้ำ 1 สายพันธุ์ วานิโนโกะ
หมายเลข 158
2 สายพันธุ์ อาลิเกตซ์
หมายเลข 159
3 สายพันธุ์ ออร์ไดล์
หมายเลข 160
dtype: object
ตอนนี้กลายเป็นซีรีส์ที่มีดัชนีถึง ๓ ตัวไปแล้ว และข้อมูลทั้งหมดถูกกางแผ่ออกในแนวนอนทั้งหมดกลายเป็น 3*3*2=18 แถว
ในทางกลับกันลองใช้ unstack ดู
print(pokemon.unstack())
จะได้
|
สายพันธุ์ |
หมายเลข |
ร่าง |
1 |
2 |
3 |
1 |
2 |
3 |
ชนิด |
|
|
|
|
|
|
น้ำ |
วานิโนโกะ |
อาลิเกตซ์ |
ออร์ไดล์ |
158 |
159 |
160 |
พืช |
ชิโครีตา |
เบย์ลีฟ |
เมกาเนียม |
152 |
153 |
154 |
ไฟ |
ฮิโนอาราชิ |
แม็กมาราชิ |
บักฟูน |
155 |
156 |
157 |
กลายเป็นเดตาเฟรมที่มีดัชนีตัวเดียว แต่กลายเป็นว่าคอลัมน์เพิ่มเป็น ๒ ตัว
นอกจากนี้แล้วก็ยัง unstack ซ้ำได้อีก
print(pokemon.unstack().unstack())
และผลที่ได้ก็กลายเป็นซีรีส์เหมือนกัน แต่ลำดับจะต่างจากซีรีส์ที่ได้จากการ stack
ร่าง ชนิด
สายพันธุ์ 1 น้ำ วานิโนโกะ
พืช ชิโครีตา
ไฟ ฮิโนอาราชิ
2 น้ำ อาลิเกตซ์
พืช เบย์ลีฟ
ไฟ แม็กมาราชิ
3 น้ำ ออร์ไดล์
พืช เมกาเนียม
ไฟ บักฟูน
หมายเลข 1 น้ำ 158
พืช 152
ไฟ 155
2 น้ำ 159
พืช 153
ไฟ 156
3 น้ำ 160
พืช 154
ไฟ 157
dtype: object
กรณีที่ข้อมูลไม่ครบสมบูรณ์ ตัวอย่างที่กล่าวมาข้างต้นนั้นเป็นกรณีที่ข้อมูลมีครบถ้วน กล่าวคือไม่ว่าจะโปเกมอนทั้ง ๓ ร่างครบ ๓ ชนิด และทุกตัวก็มีข้อมูลหมายเลขและสายพันธุ์อยู่ครบ
แต่ถ้าข้อมูลไม่ครบสมบูรณ์แบบนั้น เมื่อมีการแปลงไปมาก็จะเกิด NaN ขึ้นได้ ตัวอย่างเช่นถ้าข้อมูลเป็นแบบนี้
pokemon = pd.Series(
['คิโมริ','จุปเทิล','จูไคน์',
'อาชาโม','วากะชาโม','บาชาโม',
'มิซึโงโรว','นุมาครอว์','ลากลาร์จ'],
index=[
pd.Series(
['พืช','พืช','พืช',
'ไฟ','ไฟ/ต่อสู้','ไฟ/ต่อสู้',
'น้ำ','น้ำ/ดิน','น้ำ/ดิน'],
name='ชนิด'),
pd.Series([1,2,3,1,2,3,1,2,3],name='ร่าง')])
print(pokemon)
ได้
ชนิด ร่าง
พืช 1 คิโมริ
2 จุปเทิล
3 จูไคน์
ไฟ 1 อาชาโม
ไฟ/ต่อสู้ 2 วากะชาโม
3 บาชาโม
น้ำ 1 มิซึโงโรว
น้ำ/ดิน 2 นุมาครอว์
3 ลากลาร์จ
dtype: object
ในตัวอย่างนี้หากจับคู่ระหว่าง "ร่าง" กับ "ชนิด" แล้วละก็จะพบว่าไม่สามารถจับคู่กันได้ครบ
เช่นมี "ไฟ" ร่าง 1 แต่ไม่มี "ไฟ" ร่าง 2 มี "ไฟ/ต่อสู้" ร่าง 2 แต่ไม่มี "ไฟ/ต่อสู้" ร่าง 1
เมื่อใช้ unstack
print(pokemon.unstack())
ผลที่ได้จะออกมาเป็น
ร่าง |
1 |
2 |
3 |
ชนิด |
|
|
|
น้ำ |
มิซึโงโรว |
None |
None |
น้ำ/ดิน |
None |
นุมาครอว์ |
ลากลาร์จ |
พืช |
คิโมริ |
จุปเทิล |
จูไคน์ |
ไฟ |
อาชาโม |
None |
None |
ไฟ/ต่อสู้ |
None |
วากะชาโม |
บาชาโม |
จะเห็นได้ว่ามี None อยู่หลายจุดในส่วนที่ข้อมูลขาดไป
แต่ถ้าไม่อยากให้เป์น None เราสามารถเพิ่มคีย์เวิร์ด fill_value ลงไปเพื่อกำหนดค่าที่มาแทนข้อมูลที่ว่างเปล่าได้ เช่น
print(pokemon.unstack(fill_value='---'))
ได้
ร่าง |
1 |
2 |
3 |
ชนิด |
|
|
|
น้ำ |
มิซึโงโรว |
--- |
--- |
น้ำ/ดิน |
--- |
นุมาครอว์ |
ลากลาร์จ |
พืช |
คิโมริ |
จุปเทิล |
จูไคน์ |
ไฟ |
อาชาโม |
--- |
--- |
ไฟ/ต่อสู้ |
--- |
วากะชาโม |
บาชาโม |
พอเป็นแบบนี้หากใช้ stack ซ้อนลงไปก็จะกลายเป็นซีรีส์ที่มีแถวเพิ่มมาเป็น ๑๕ แถว
print(pokemon.unstack(fill_value='---').stack())
ได้
ชนิด ร่าง
น้ำ 1 มิซึโงโรว
2 ---
3 ---
น้ำ/ดิน 1 ---
2 นุมาครอว์
3 ลากลาร์จ
พืช 1 คิโมริ
2 จุปเทิล
3 จูไคน์
ไฟ 1 อาชาโม
2 ---
3 ---
ไฟ/ต่อสู้ 1 ---
2 วากะชาโม
3 บาชาโม
dtype: object
แต่หากปล่อยให้เป็น None อยู่แบบนี้แล้ว stack ซ้อนเข้าไปอีกก็จะได้ซีรีส์อันเดิมกลับมา โดยที่ None ที่เกิดขึ้นมานี้ก็หายไปด้วย เพราะปกติเวลา stack ข้อมูลที่เป็น None หรือ NaN จะละไป
print(pokemon.unstack().stack())
แต่หากไม่ต้องการให้ตัด None หรือ NaN ทิ้งก็ให้เพิ่มคีย์เวิร์ด dropna=0 (จากที่ปกติเป็น 1 คือลบ NaN ทิ้งเสมอ)
ตัวอย่าง
print(pokemon.unstack().stack(dropna=0))
ได้
ชนิด ร่าง
น้ำ 1 มิซึโงโรว
2 None
3 None
น้ำ/ดิน 1 None
2 นุมาครอว์
3 ลากลาร์จ
พืช 1 คิโมริ
2 จุปเทิล
3 จูไคน์
ไฟ 1 อาชาโม
2 None
3 None
ไฟ/ต่อสู้ 1 None
2 วากะชาโม
3 บาชาโม
dtype: object
แต่ในกรณีที่ใช้ unstack จะไม่มีการตัดข้อมูลที่เป็น None และไม่มีตัวเลือกให้ตัดทิ้งด้วย
print(pokemon.unstack().unstack())
ได้
ร่าง ชนิด
1 น้ำ มิซึโงโรว
น้ำ/ดิน None
พืช คิโมริ
ไฟ อาชาโม
ไฟ/ต่อสู้ None
2 น้ำ None
น้ำ/ดิน นุมาครอว์
พืช จุปเทิล
ไฟ None
ไฟ/ต่อสู้ วากะชาโม
3 น้ำ None
น้ำ/ดิน ลากลาร์จ
พืช จูไคน์
ไฟ None
ไฟ/ต่อสู้ บาชาโม
dtype: object
อ้างอิง