json เป็นรูปแบบการจัดเก็บข้อมูลแบบหนึ่งที่นิยมใช้ในภาษา javascript
เกี่ยวกับการจัดการกับ json ภายในภาษาไพธอนได้อธิบายในนี้ไว้แล้ว
https://phyblas.hinaboshi.com/20190427 สำหรับในหน้านี้จะพูดถึงการใช้ pandas ในการจัดการกับ json
pandas มีคำสั่งที่ใช้อ่านและเขียน json ซึ่งสะดวกมาก ใช้งานได้ง่าย
สำหรับการอ่าน json มาเป็นเดตาเฟรมให้ใช้ pd.read_json และในทางกลับกันเมื่อจะเปลี่ยนเดตาเฟรมเป็น json ให้ใช้เมธอด to_json ที่ตัวเดตาเฟรมนั้น
อ่านไฟล์ json ด้วยฟังก์ชัน pd.read_json ฟังก์ชัน pd.read_json สามารถอ่าน json จากไฟล์โดยตรง หรือจากสายอักขระข้อความที่เป็น json ก็ได้
ปกติถ้าใส่ชื่อไฟล์ในคำสั่ง pd.read_json จะเป็นการอ่าน json จากไฟล์ตามชื่อนั้น แต่ถ้าใส่ข้อความที่เขียนเป็น json ลงไปก็จะเป็นการแปลงข้อความ json นั้นเป็นเดตาเฟรม
เช่นถ้ามีไฟล์ json อยู่ก็ใส่ชื่อไฟล์ลงไป ก็จะเป็นการอ่าน json จากไฟล์นั้น
df = pd.read_json('pokemon.json')
นอกจากนี้ยังสามารถอ่าน json จากไฟล์ที่อยู่ในรูปที่ถูกบีบอัดอยู่ได้ด้วย
เช่นอ่านไฟล์บีบอัด gzip (.gz)
df = pd.read_json('pokemon.json.gz')
ไฟล์ที่ใส่เข้าไปจะเป็นไฟล์ json เปล่าๆหรือเป็น โปรแกรมจะตัดสินเอาจากสกุลไฟล์ แต่จะใส่ตัวเลือก compression เพื่อระบุชัดก็ได้
df = pd.read_json('pokemon.json.gz',compression='gzip')
ไฟล์บีบอัดชนิดต่างๆที่ใช้ได้คือ 'bz2', 'gzip', 'xz', 'zip'
อ่าน json จากสายอักขระ หากมีสายอักขระที่เขียนข้อมูลในรูปแบบ json อยู่ ก็สามารถใช้ pd.read_json อ่านขึ้นมาได้
json ที่ป้อนเข้าไปจะอยู่ในรูปแบบออบเจ็กต์หรืออาเรย์ก็ได้ แต่จำเป็นต้องซ้อนสองชั้น ถ้าเป็นอาเรย์ก็จะไม่มีชื่อคอลัมน์และดัชนี ก็จะกลายเป็น 0,1,2,...
ตัวอย่าง
import pandas as pd
# ออบเจ็กต์ของออบเจ็กต์
js = '{"สายพันธุ์":{"1": "ฟุชิงิดาเนะ","2": "ฟุชิงิโซว"},"หนัก":{"1":6.9, "2": 13}}'
df = pd.read_json(js)
print(df)
# ออบเจ็กต์ของอาเรย์
js = '{"สายพันธุ์":["ฟุชิงิดาเนะ","ฟุชิงิโซว"],"หนัก":[6.9,13]}'
df = pd.read_json(js)
print(df)
# อาเรย์ของอาเรย์
js = '[["ฟุชิงิดาเนะ",6.9],["ฟุชิงิโซว",13]]'
df = pd.read_json(js)
print(df)
# อาเรย์ของออบเจ็กต์
js = '[{"สายพันธุ์": "ฟุชิงิดาเนะ","หนัก": 6.9},{"สายพันธุ์":"ฟุชิงิโซว", "หนัก": 13}]'
df = pd.read_json(js)
print(df)
ได้
|
สายพันธุ์ |
หนัก |
1 |
ฟุชิงิดาเนะ |
6.9 |
2 |
ฟุชิงิโซว |
13.0 |
|
สายพันธุ์ |
หนัก |
0 |
ฟุชิงิดาเนะ |
6.9 |
1 |
ฟุชิงิโซว |
13.0 |
|
0 |
1 |
0 |
ฟุชิงิดาเนะ |
6.9 |
1 |
ฟุชิงิโซว |
13.0 |
|
สายพันธุ์ |
หนัก |
0 |
ฟุชิงิดาเนะ |
6.9 |
1 |
ฟุชิงิโซว |
13.0 |
ถ้าข้อมูลออบเจ็กต์มีแค่บางส่วน ส่วนที่ขาดก็จะกลายเป็น NaN
js = '[{"สายพันธุ์": "ฟุชิงิดาเนะ","หนัก": 6.9},{"สายพันธุ์":"ฟุชิงิโซว", "สูง": 1}]'
df = pd.read_json(js)
print(df)
ได้
|
สายพันธุ์ |
สูง |
หนัก |
0 |
ฟุชิงิดาเนะ |
NaN |
6.9 |
1 |
ฟุชิงิโซว |
1.0 |
NaN |
อ่าน json เป็นซีรีส์ ปกติคำสั่ง pd.read_json จะอ่าน json ออกมาเป็นเดตาเฟรม แต่ถ้าหากแค่ต้องการอ่านเป็นซีรีส์ก็ทำได้โดยใส่ตัวเลือก typ='series'
กรณีที่เป็นซีรีส์แบบนี้ข้อมูลที่ใส่ก็คือ json ของอาเรย์หรือออบเจ็กต์ชั้นเดียว
เช่น
# อาเรย์
js = '["ฟุชิงิดาเนะ","ฟุชิงิโซว"]'
sr = pd.read_json(js,typ='series')
print(sr)
# ออบเจ็กต์
js = '{"1": "ฟุชิงิดาเนะ","2": "ฟุชิงิโซว"}'
sr = pd.read_json(js,typ='series')
print(sr)
ได้
0 ฟุชิงิดาเนะ
1 ฟุชิงิโซว
dtype: object
1 ฟุชิงิดาเนะ
2 ฟุชิงิโซว
dtype: object
เขียนเดตาเฟรมเป็น json ด้วยเมธอด to_json เดตาเฟรมมีเมธอด to_json ใช้สำหรับแปลงเป็น json
df = pd.DataFrame([
['fushigidane',0.7,6.9],
['hitokage',0.6,8.5],
['zenigame',0.5,9.0]])
print(df)
print(df.to_json())
ได้
|
0 |
1 |
2 |
0 |
fushigidane |
0.7 |
6.9 |
1 |
hitokage |
0.6 |
8.5 |
2 |
zenigame |
0.5 |
9.0 |
{"0":{"0":"fushigidane","1":"hitokage","2":"zenigame"},"1":{"0":0.7,"1":0.6,"2":0.5},"2":{"0":6.9,"1":8.5,"2":9.0}}
หากใส่ชื่อไฟล์ลงไปจะเป็นการเขียนบันทึกลงไฟล์
df.to_json('pokemon.json')
สามารถใส่ตัวเลือกเสริม compression ได้หากต้องการให้มีการบีบอัดไฟล์ โดยใส่ชนิดไฟล์ที่ต้องการ ซึ่งอาจเป็น 'gzip', 'bz2', 'zip', 'xz'
df.to_json('pokemon.json.gz',compression='gzip')
เวลาที่แปลงเป็น json โดยปกติแล้วถ้ามีอักษรที่ไม่ใช่ ascii เช่นตัวอักษรภาษาไทยก็จะถูกเปลี่ยนเป็นรหัส
เช่น
df = pd.DataFrame([
['ฟุชิงิดาเนะ','พืช/พิษ',0.7,6.9],
['ฟุชิงิโซว','พืช/พิษ',1.0,13.0],
['ฟุชิงิบานะ','พืช/พิษ',2.4,155.5]
],
columns=['สายพันธุ์','ชนิด','ส่วนสูง','น้ำหนัก'],
index=[1,2,3]
)
print(df)
print(df.to_json())
ได้
|
สายพันธุ์ |
ชนิด |
ส่วนสูง |
น้ำหนัก |
1 |
ฟุชิงิดาเนะ |
พืช/พิษ |
0.7 |
6.9 |
2 |
ฟุชิงิโซว |
พืช/พิษ |
1.0 |
13.0 |
3 |
ฟุชิงิบานะ |
พืช/พิษ |
2.4 |
155.5 |
{"\u0e2a\u0e32\u0e22\u0e1e\u0e31\u0e19\u0e18\u0e38\u0e4c":{"1":"\u0e1f\u0e38\u0e0a\u0e34\u0e07\u0e34\u0e14\u0e32\u0e40\u0e19\u0e30","2":"\u0e1f\u0e38\u0e0a\u0e34\u0e07\u0e34\u0e42\u0e0b\u0e27","3":"\u0e1f\u0e38\u0e0a\u0e34\u0e07\u0e34\u0e1a\u0e32\u0e19\u0e30"},"\u0e0a\u0e19\u0e34\u0e14":{"1":"\u0e1e\u0e37\u0e0a\/\u0e1e\u0e34\u0e29","2":"\u0e1e\u0e37\u0e0a\/\u0e1e\u0e34\u0e29","3":"\u0e1e\u0e37\u0e0a\/\u0e1e\u0e34\u0e29"},"\u0e2a\u0e48\u0e27\u0e19\u0e2a\u0e39\u0e07":{"1":0.7,"2":1.0,"3":2.4},"\u0e19\u0e49\u0e33\u0e2b\u0e19\u0e31\u0e01":{"1":6.9,"2":13.0,"3":155.5}}
เพื่อที่จะให้ยังคงเป็นอักษรตามเดิม ให้ใส่ตัวเลือก force_ascii=False เข้าไป
print(df.to_json(force_ascii=0))
ได้
{"สายพันธุ์":{"1":"ฟุชิงิดาเนะ","2":"ฟุชิงิโซว","3":"ฟุชิงิบานะ"},"ชนิด":{"1":"พืช\/พิษ","2":"พืช\/พิษ","3":"พืช\/พิษ"},"ส่วนสูง":{"1":0.7,"2":1.0,"3":2.4},"น้ำหนัก":{"1":6.9,"2":13.0,"3":155.5}}
ตำแหน่งทศนิยมสำหรับข้อมูลที่เป็นทศนิยม หากไม่ได้กำหนดจะมี ๑๐ ตัว หากต้องการกำหนดเองสามารถใส่ double_precision
df = pd.DataFrame([1/3.])
print(df.to_json()) # ได้ {"0":{"0":0.3333333333}}
print(df.to_json(double_precision=3)) # ได้ {"0":{"0":0.333}}
ตัวเลือก orient รูปแบบในการเขียน json นั้นจริงๆแล้วทำได้หลายแบบ โดยรูปแบบตั้งต้นคือการแบ่งตามคอลัมน์ แล้วระบุค่าแต่ละแถวในคอลัมน์
หากต้องการเปลี่ยนรูปแบบการเขียนสามารถทำได้โดยใช้ตัวเลือก orient
หากไม่ใส่ตัวเลือก orient จะเป็นค่าตั้งต้น คือ 'columns' คือแบ่งตามคอลัมส์ก่อน
แต่หากใส่ orient='index' จะแบ่งตามแถวก่อน
ตัวอย่างเปรียบเทียบ
df = pd.DataFrame([
['ฟุชิงิดาเนะ',0.7,6.9],
['ฮิโตคาเงะ',0.6,8.5],
['เซนิงาเมะ',0.5,9.0]
],
columns=['สายพันธุ์','ส่วนสูง','น้ำหนัก'],
index=[1,4,7]
)
print(df.to_json(orient='columns',force_ascii=0))
print(df.to_json(orient='index',force_ascii=0))
ได้
{"สายพันธุ์":{"1":"ฟุชิงิดาเนะ","4":"ฮิโตคาเงะ","7":"เซนิงาเมะ"},"ส่วนสูง":{"1":0.7,"4":0.6,"7":0.5},"น้ำหนัก":{"1":6.9,"4":8.5,"7":9.0}}
{"1":{"สายพันธุ์":"ฟุชิงิดาเนะ","ส่วนสูง":0.7,"น้ำหนัก":6.9},"4":{"สายพันธุ์":"ฮิโตคาเงะ","ส่วนสูง":0.6,"น้ำหนัก":8.5},"7":{"สายพันธุ์":"เซนิงาเมะ","ส่วนสูง":0.5,"น้ำหนัก":9.0}}
ถ้า orient='records' จะแยกตามแถว คล้าย orient='index' เพียงแต่ดัชนีแถวจะถูกละทิ้งไป
print(df.to_json(orient='records',force_ascii=0))
ได้
[{"สายพันธุ์":"ฟุชิงิดาเนะ","ส่วนสูง":0.7,"น้ำหนัก":6.9},{"สายพันธุ์":"ฮิโตคาเงะ","ส่วนสูง":0.6,"น้ำหนัก":8.5},{"สายพันธุ์":"เซนิงาเมะ","ส่วนสูง":0.5,"น้ำหนัก":9.0}]
เมื่อใช้ orient='records' หากใส่ตัวเลือก lines=True จะทำให้แบ่งเป็นบรรทัดแทน แทนที่จะกั้นด้วยจุลภาค ,
print(df.to_json(orient='records',lines=1,force_ascii=0))
ได้
{"สายพันธุ์":"ฟุชิงิดาเนะ","ส่วนสูง":0.7,"น้ำหนัก":6.9}
{"สายพันธุ์":"ฮิโตคาเงะ","ส่วนสูง":0.6,"น้ำหนัก":8.5}
{"สายพันธุ์":"เซนิงาเมะ","ส่วนสูง":0.5,"น้ำหนัก":9.0}
ถ้า orient='values' ทั้งชื่อคอลัมน์และชื่อแถวจะถูกละทิ้งทั้งหมด
print(df.to_json(orient='values',force_ascii=0))
ได้
[["ฟุชิงิดาเนะ",0.7,6.9],["ฮิโตคาเงะ",0.6,8.5],["เซนิงาเมะ",0.5,9.0]]
หาก orients='split' ชื่อคอลัมน์และชื่อแถวจะถูกแยกออกมาเก็บไว้ต่างหาก ส่วนข้อมูลก็จะบันทึกเหมือนกับเมื่อใช้ orient='values'
print(df.to_json(orient='split',force_ascii=0))
ได้
{"columns":["สายพันธุ์","ส่วนสูง","น้ำหนัก"],"index":[1,4,7],"data":[["ฟุชิงิดาเนะ",0.7,6.9],["ฮิโตคาเงะ",0.6,8.5],["เซนิงาเมะ",0.5,9.0]]}
ถ้า orient='table' จะเป็นตารางแบบที่บันทึกข้อมูลใน sql มีระบุชนิดข้อมูลไว้ชัดเจนด้วย
ในส่วนข้อมูลจะแยกตามแถว คล้าย orient='index'
เพียงแต่ว่าชื่อคอลัมน์ในส่วนของ fields จะถูกแปลงเป็นโค้ด แม้ว่าจะกำหนด force_ascii=0 ก็ตาม
print(df.to_json(orient='table',force_ascii=0))
ได้
{"schema": {"fields":[{"name":"index","type":"integer"},{"name":"\u0e2a\u0e32\u0e22\u0e1e\u0e31\u0e19\u0e18\u0e38\u0e4c","type":"string"},{"name":"\u0e2a\u0e48\u0e27\u0e19\u0e2a\u0e39\u0e07","type":"number"},{"name":"\u0e19\u0e49\u0e33\u0e2b\u0e19\u0e31\u0e01","type":"number"}],"primaryKey":["index"],"pandas_version":"0.20.0"}, "data": [{"index":1,"สายพันธุ์":"ฟุชิงิดาเนะ","ส่วนสูง":0.7,"น้ำหนัก":6.9},{"index":4,"สายพันธุ์":"ฮิโตคาเงะ","ส่วนสูง":0.6,"น้ำหนัก":8.5},{"index":7,"สายพันธุ์":"เซนิงาเมะ","ส่วนสูง":0.5,"น้ำหนัก":9.0}]}
เวลาที่ใช้ pd.read_json เพื่ออ่านข้อมูลที่บันทึกด้วยตัวเลือก orient แบบนี้ จะต้องกำหนด orient ตามให้ตรงด้วย
js = '{"columns":["ชื่อ"],"index":[25,26],"data":[["พีคาชู"],["ไรชู"]]}'
print(pd.read_json(js,orient='split'))
ได้
การใช้ json_normalize มีฟังก์ชันหนึ่งที่เกียวข้องกับ json ซึ่งค่อนข้างเข้าใจยากสักหน่อย แต่มีประโยชน์ นั่นคือ json_normalize
สมมุติว่ามีข้อมูล json ที่มีการซ้อนกันซับซ้อนหน่อย เช่นแบบนี้
jdata1.json
[
{
"เลข": 152,
"ชื่อ": {
"ไทย": "ชิโครีตา",
"ญี่ปุ่น": "チコリータ"
}
},
{
"เลข": 155,
"ชื่อ": {
"ไทย": "ฮิโนอาราชิ",
"ญี่ปุ่น": "ヒノアラシ"
}
},
{
"เลข": 158,
"ชื่อ": {
"ไทย": "วานิโนโกะ",
"ญี่ปุ่น": "ワニノコ"
}
}
]
ถ้าหากนำมาแปลงเป็นเดตาเฟรมด้วยวิธีปกติก็จะพบว่าดิกชันนารีด้านในอยู่รวมตาราง
print(pd.read_json('jdata1.json'))
ได้
|
ชื่อ |
เลข |
0 |
{'ไทย': 'ชิโครีตา', 'ญี่ปุ่น': 'チコリータ'} |
152 |
1 |
{'ไทย': 'ฮิโนอาราชิ', 'ญี่ปุ่น': 'ヒノアラシ'} |
155 |
2 |
{'ไทย': 'วานิโนโกะ', 'ญี่ปุ่น': 'ワニノコ'} |
158 |
แต่หากเริ่มอ่านมาเป็นดิกชันนารีก่อนแล้วใช้ json_normalize เพื่อแปลง โครงสร้างภายในจะถูกอ่านแล้วได้ชื่อคอลัมน์ซ้อนเข้าไป
import json
from pandas.io.json import json_normalize
with open('jdata1.json') as f:
jdata = json.load(f)
print(json_normalize(jdata))
ได้
|
ชื่อ.ญี่ปุ่น |
ชื่อ.ไทย |
เลข |
0 |
チコリータ |
ชิโครีตา |
152 |
1 |
ヒノアラシ |
ฮิโนอาราชิ |
155 |
2 |
ワニノコ |
วานิโนโกะ |
158 |
โครงสร้างซ้อน ๓ ชั้นแบบนี้ก็สามารถอ่านได้
jdata2.json
[
{
"เลข": 152,
"ข้อมูล": {
"ชื่อ": {
"ไทย": "ชิโครีตา",
"ญี่ปุ่น": "チコリータ"
},
"ส่วนสูง": 0.9,
"น้ำหนัก": 6.4
}
},
{
"เลข": 155,
"ข้อมูล": {
"ชื่อ": {
"ไทย": "ฮิโนอาราชิ",
"ญี่ปุ่น": "ヒノアラシ"
},
"ส่วนสูง": 0.5
}
},
{
"เลข": 158,
"ข้อมูล": {
"ชื่อ": {
"ไทย": "วานิโนโกะ",
"ญี่ปุ่น": "ワニノコ"
},
"น้ำหนัก": 9.5
}
}
]
with open('jdata2.json') as f:
jdata = json.load(f)
print(json_normalize(jdata))
ได้
|
ข้อมูล.ชื่อ.ญี่ปุ่น |
ข้อมูล.ชื่อ.ไทย |
ข้อมูล.น้ำหนัก |
ข้อมูล.ส่วนสูง |
เลข |
0 |
チコリータ |
ชิโครีตา |
6.4 |
0.9 |
152 |
1 |
ヒノアラシ |
ฮิโนอาราชิ |
NaN |
0.5 |
155 |
2 |
ワニノコ |
วานิโนโกะ |
9.5 |
NaN |
158 |
ต่อมาลองดูข้อมูลที่ซับซ้อนขึ้นไปอีก แบบนี้
jdata3.json
[
{
"เทรนเนอร์": "โกลด์",
"โปเกมอน": [
{
"เลข": 152,
"ชื่อ": "ชิโครีตา"
},
{
"เลข": 161,
"ชื่อ": "โอตาจิ"
},
{
"เลข": 167,
"ชื่อ": "อิโตมารุ"
}
]
},
{
"เทรนเนอร์": "ซิลเวอร์",
"โปเกมอน": [
{
"เลข": 155,
"ชื่อ": "ฮิโนอาราชิ"
},
{
"เลข": 163,
"ชื่อ": "โฮโฮ"
}
]
},
{
"เทรนเนอร์": "คริสตัล",
"โปเกมอน": [
{
"เลข": 158,
"ชื่อ": "วานิโนโกะ"
},
{
"เลข": 165,
"ชื่อ": "เรดีบา"
}
]
}
]
ในที่นี้มีข้อมูล ๓ กลุ่มของเทรนเนอร์ ๓ คน โดย "เทรนเนอร์" คือชื่อของเทรนเนอร์ ส่วน "โปเกมอน" บอกข้อมูลโปเกมอนที่มี
กรณีนี้ก็สามารถใช้ json_normalize เพื่ออ่านได้ แต่ต้องระบุชื่อคีย์สำหรับข้อมูลในแต่ละกลุ่ม ในที่นี้คือ "โปเกมอน" และระบุค่าอื่นๆที่จะใส่ด้วย ในที่นี้คือ "เทรนเนอร์"
with open('jdata3.json') as f:
jdata = json.load(f)
df = json_normalize(jdata,'โปเกมอน','เทรนเนอร์')
print(df)
ได้
|
ชื่อ |
เลข |
เทรนเนอร์ |
0 |
ชิโครีตา |
152 |
โกลด์ |
1 |
โอตาจิ |
161 |
โกลด์ |
2 |
อิโตมารุ |
167 |
โกลด์ |
3 |
ฮิโนอาราชิ |
155 |
ซิลเวอร์ |
4 |
โฮโฮ |
163 |
ซิลเวอร์ |
5 |
วานิโนโกะ |
158 |
คริสตัล |
6 |
เรดีบา |
165 |
คริสตัล |
ส่วนการสร้างข้อมูล json ในลักษณะนี้จากเดตาเฟรมอาจทำได้โดยวิธีการในลักษณะนี้
lis = []
for tr,p in df.groupby('เทรนเนอร์'):
pokemon = []
for _,pk in p.iterrows():
pokemon.append({'เลข':pk['เลข'],'ชื่อ':pk['ชื่อ']})
lis.append({"เทรนเนอร์":tr,"โปเกมอน":pokemon})
with open('jdata4.json','w') as f:
json.dump(lis,f,indent=2,ensure_ascii=0)
groupby จะช่วยแบ่งกลุ่มให้ตามเทรนเนอร์ แล้วเราจะได้ชื่อเทรนเนอร์และโปเกมอนที่เป็นของเทรนเนอร์นั้น เอามาแยกสร้างดิกชันนารี แล้วสุดท้ายก็แปลงเป็น json
หรืออาจย่อเหลือในบรรทัดเดียวได้แบบนี้
with open('jdata4.json','w') as f:
json.dump([{"เทรนเนอร์":tr,"โปเกมอน":[{c:pk[c] for c in ['เลข','ชื่อ']} for _,pk in p.iterrows()]} for tr,p in df.groupby('เทรนเนอร์')],f,indent=2,ensure_ascii=0)
กรณีที่มีคอลัมน์ไหนมีค่าซ้ำกันแบ่งเป็นกลุ่มชัดเจน การเก็บข้อมูลในลักษณะนี้จะประหยัดที่ไปได้ เพราะไม่ต้องเก็บชื่อคอลัมน์เดิมๆหลายครั้ง อีกทั้งยังเป็นการจัดแบ่งข้อมูลอย่างเป็นระเบียบขึ้นด้วย
อ้างอิง