2025, Nov 01 11:31

pandas से नहीं हुआ? रिकर्सिव फ्लैटनर से डीप JSON को सचमुच सपाट बनाएं

Elasticsearch से आए डीप नेस्टेड JSON को एक ही पंक्ति में बदलें। pandas.json_normalize/explode की सीमाएँ समझें और रिकर्सिव Python फ्लैटनर से इंडेक्स्ड कॉलम बनाएं.

गहराई से नेस्टेड JSON को एक ही पंक्ति की संरचना में बदलना सुनने में आसान लगता है, लेकिन जैसे ही ऑब्जेक्ट्स की सूचियाँ और बहु-स्तरीय नेस्टिंग आती हैं, चुनौती बढ़ जाती है। pandas.json_normalize और explode जैसे सामान्य टूल कई स्थितियों में मदद करते हैं, पर जब पेलोड में डिक्शनरी की सूचियाँ और उनके भीतर आगे सूचियाँ मिली हों, तो अक्सर कुछ कॉलम सूची रूप में ही रह जाते हैं। यदि लक्ष्य हर नेस्टेड मान के लिए इंडेक्स वाली कुंजियों के साथ एक सपाट, एकल-रिकॉर्ड ऑब्जेक्ट पाना है, तो संरचना को पार करके खोलने का एक निश्चित (deterministic) तरीका चाहिए।

समस्या का संदर्भ

यह JSON Elasticsearch के रेस्पॉन्स से आता है: एक रिकॉर्ड जिसमें कई स्केलर फ़ील्ड हैं और कुछ ऑब्जेक्ट्स की एरेज़, जैसे icdDiagnosisCodes, serviceProcedures, procedureCodeModifiers और serviceDiagnoses। json_normalize को सीमित max_level के साथ चलाने या explode को चेन करने पर भी एकल सपाट पंक्ति नहीं मिली; सूची-मान वाले कॉलम वैसे ही बने रहे।

import requests
import pandas as pd
import json
resp = requests.get(
    ELASTICSEARCH_URL,
    data=QUERY,
    auth=(config.get('username'), config.get('password')),
    verify=False,
    headers={'Content-Type': 'application/json'}
)
blob = resp.json()
slice_ = blob["hits"]["hits"][0]["_source"]["response"]["data"][CLAIMTYPE]
frame_main = pd.json_normalize(slice_, max_level=2).fillna('')
frame_nested = pd.json_normalize(frame_main['icdDiagnosisCodes'])
print(frame_nested.head(10).to_string())

normalize के बाद भी icdDiagnosisCodes और serviceProcedures जैसे फ़ील्ड सूचियाँ ही हैं, और explode की आगे की कोशिशें भी हर नेस्टेड एट्रीब्यूट के लिए अलग कॉलम के साथ एक सपाट पंक्ति नहीं बनातीं।

असल में हो क्या रहा है

json_normalize डिक्शनरी को कॉलम में समतल करता है, लेकिन वह हर list-of-dicts को स्वतः अलग-अलग, स्पष्ट (disambiguated) स्केलर कॉलम में नहीं फैलाता। max_level के साथ एक कॉल किसी गहराई पर ट्रैवर्सल को रोक देता है; उस बिंदु पर सूची-मान वाले फ़ील्ड सूची ही रहते हैं। explode किसी सूची जैसे कॉलम को कई पंक्तियों या कई प्रविष्टियों में तो बदल सकता है, पर यदि आपको एक ही पंक्ति में ऐसा निरूपण चाहिए जहाँ हर नेस्टेड मान अपना अलग फ़ील्ड बने, तो आपको स्वयं संरचना पर चलकर स्थिर और अद्वितीय कुंजियाँ सौंपनी होंगी।

डेटा मॉडल की एक व्यावहारिक मांग भी है: सभी मान बनाए रखने हैं, भले वे दोहराए जाएँ; और procedureCodeModifiers जैसे फ़ील्ड में कई आइटम (25 तक) हो सकते हैं। इसका मतलब है कि डिडुप्लिकेशन नहीं किया जा सकता और एक ऐसी कुंजी-योजना चाहिए जो आइटम्स को क्रम में इंडेक्स करे।

एक केंद्रित, पुनरावर्ती (recursive) फ्लैटनर

नीचे दिया तरीका डिक्शनरी को समतल करते समय नेस्टेड कुंजियों को पढ़ने योग्य बनाने हेतु एक विभाजक के साथ जोड़ता है और सूची की स्थितियों के लिए संख्यात्मक इंडेक्स जोड़ता है। यह सभी मान सुरक्षित रखता है और स्थान के आधार पर डुप्लिकेट्स को अलग पहचान देता है, जिससे procedureCodeModifier_1, procedureCodeModifier_2 जैसे फ़ील्ड सहज रूप से बन जाते हैं।

def squash_map(obj_map, base_key=None, glue="___"):
    flat = {}
    for k, v in obj_map.items():
        new_key = k if base_key is None else base_key + glue + k
        if isinstance(v, str):
            flat[new_key] = v
        elif isinstance(v, list):
            for idx, item in enumerate(v):
                flat.update(squash_map(item, base_key=f"{new_key}_{idx}", glue=glue))
    return flat

यहाँ डेटा का एक छोटा हिस्सा दिखाया गया है, ताकि समझ आए कि सूची इंडेक्स और कुंजी संयोजन कैसे लागू होते हैं:

sample_payload = {
    "providerCity": "SOME CITY",
    "providerSpecialtyDescription": "PHYSICAL/OCCUPATIONAL THERAPY",
    "updateDate": "YYYY-MM-DD",
    "providerNpi": "XXXXXXXXXXX",
    "icdDiagnosisCodes": [
        {
            "icdDiagnosisCode": "M25551",
            "icdDiagnosisDecimalCode": "M25.551",
            "icdDiagnosisCodeDescription": "PAIN IN RIGHT HIP"
        },
        {
            "icdDiagnosisCode": "M545",
            "icdDiagnosisDecimalCode": "M54.5",
            "icdDiagnosisCodeDescription": "LOW BACK PAIN"
        }
    ],
    "dateOfBirth": "YYYY-MM-DD"
}
from pprint import pprint
pprint(squash_map(sample_payload))
{'dateOfBirth': 'YYYY-MM-DD',
 'icdDiagnosisCodes_0___icdDiagnosisCode': 'M25551',
 'icdDiagnosisCodes_0___icdDiagnosisCodeDescription': 'PAIN IN RIGHT HIP',
 'icdDiagnosisCodes_0___icdDiagnosisDecimalCode': 'M25.551',
 'icdDiagnosisCodes_1___icdDiagnosisCode': 'M545',
 'icdDiagnosisCodes_1___icdDiagnosisCodeDescription': 'LOW BACK PAIN',
 'icdDiagnosisCodes_1___icdDiagnosisCodeDecimalCode': 'M54.5',
 'providerCity': 'SOME CITY',
 'providerNpi': 'XXXXXXXXXXX',
 'providerSpecialtyDescription': 'PHYSICAL/OCCUPATIONAL THERAPY',
 'updateDate': 'YYYY-MM-DD'}

स्निपेट में दिख रहा क्रम pretty printer के कारण है; लौटाई गई डिक्शनरी में प्रविष्टियों का इंसर्शन-ऑर्डर स्वयं बना रहता है।

यह सीमाओं को कैसे संबोधित करता है

यह रणनीति एक सपाट डिक्शनरी देती है जिसे आप एक रिकॉर्ड की तरह मान सकते हैं। हर नेस्टेड सूची आइटम क्रम में इंडेक्स होता है, इसलिए डुप्लिकेट स्वभावतः सुरक्षित रहते हैं। procedureCodeModifier जैसे फ़ील्ड नैसर्गिक रूप से procedureCodeModifiers_0___procedureCodeModifier, procedureCodeModifiers_1___procedureCodeModifier आदि के रूप में नज़र आएँगे, जिससे अधिक मॉडिफ़ायर्स वाले मामलों को कवर किया जा सके। यही बात serviceProcedures के अंदर की serviceDiagnoses जैसी नेस्टेड एरेज़ पर भी लागू होती है, क्योंकि पथ के हर स्तर पर इंडेक्सिंग दोहरती है।

जानना क्यों उपयोगी है

जब आपको किसी जटिल JSON दस्तावेज़ का एकल-पंक्ति प्रतिनिधित्व चाहिए—खासकर जहाँ सूचियों के भीतर ऑब्जेक्ट्स की सूचियाँ हों—तो सामान्य फ्लैटनिंग अक्सर कम पड़ जाती है। एक छोटा, स्पष्ट ट्रैवर्सल आपको पूर्वानुमेय कॉलम-नेमिंग स्कीम, इंडेक्स के जरिए स्थिर क्रम और बिना किसी मान के नुकसान के देता है। यह तब अहम है जब डाउनस्ट्रीम प्रणालियाँ फ़ील्ड्स की उपस्थिति की अपेक्षा करती हैं, चाहे डुप्लिकेट हों या गिनती बदले।

मुख्य बातें

यदि आपका लक्ष्य सचमुच सपाट ऑब्जेक्ट है, तो पहले तय करें कि कुंजियाँ कैसे बनेंगी और सूचियों को कैसे इंडेक्स करेंगे। उस आकार को पाने के लिए समर्पित ट्रैवर्सल अपनाएँ, फिर परिणाम को अपने DataFrame वर्कफ़्लो में दें। इससे नेस्टेड सूचियाँ कॉलम तक नहीं पहुँचतीं और “सभी मान सुरक्षित रखें” तथा “कई मॉडिफ़ायर्स का समर्थन करें” जैसी आवश्यकताएँ बिना किसी अस्पष्टता के पूरी होती हैं।