2025, Nov 01 11:17
Рекурсивное сплющивание JSON в одну строку с индексами
Сделайте вложенный JSON плоским: когда pandas.json_normalize и explode бессильны, выручит рекурсивный обход с индексами и стабильными ключами для одной строки.
Превратить сильно вложенный JSON в структуру из одной строки кажется задачей простой — пока не появляются списки объектов и многоуровневая вложенность. Стандартные инструменты вроде pandas.json_normalize и explode справляются со многими случаями, но когда в данных перемешаны списки словарей с вложенными списками, они нередко оставляют столбцы, в которых по‑прежнему лежат списки. Если цель — полностью плоский объект одной записи, где у каждого вложенного значения есть индексированный ключ, нужен детерминированный способ обхода и «раскрутки» структуры.
Проблема в контексте
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())
Даже после нормализации такие поля, как icdDiagnosisCodes и serviceProcedures, остаются списками, а дальнейшие попытки explode не приводят к получению одной плоской строки с отдельными столбцами для каждого вложенного атрибута.
Что на самом деле происходит
json_normalize раскладывает словари по столбцам, но не раскрывает автоматически каждый список словарей в набор однозначных скалярных столбцов. Один вызов с max_level обрывает обход на заданной глубине; на этом уровне поля‑списки остаются списками. explode может превратить столбец со списком в несколько строк или записей, но если нужна однострочная репрезентация, где каждое вложенное значение становится отдельным полем, придётся пройтись по структуре вручную и назначить стабильные, уникальные ключи.
Есть и практическое требование модели данных: нужно сохранить все значения, даже повторяющиеся, а такие поля, как procedureCodeModifiers, могут содержать много элементов (до 25). Это исключает дедупликацию и требует схемы ключей с индексами по порядку.
Рекурсивный подход к «сплющиванию»
Подход ниже «сплющивает» словарь, склеивая вложенные ключи разделителем для наглядности и добавляя числовые индексы для позиций в списках. Он сохраняет все значения и различает дубликаты по позиции, что покрывает требование к полям вроде 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 и т. п., что покрывает сценарий с множеством модификаторов. То же действует и для вложенных массивов, например serviceDiagnoses внутри serviceProcedures, поскольку индексация повторяется на каждом участке пути.
Зачем это знать
Когда нужна однострочная репрезентация сложного JSON — особенно с «списками в списках» объектов — универсальное «сплющивание» часто не справляется. Небольшой явный обход даёт предсказуемую схему именования столбцов, стабильный порядок через индексы и отсутствие потерь значений. Это важно, когда потребители дальше по цепочке ждут наличие полей независимо от дублей и количества, а разные полезные нагрузки отличаются числом вложенных элементов.
Выводы
Если цель — по‑настоящему плоский объект, начните с определения правил построения ключей и индексации списков. Примените отдельный обход, чтобы получить нужную форму, и уже потом передайте результат в пайплайн с DataFrame. Так вложенные списки не «протекут» в столбцы, а требования вроде «сохранить все значения» и «поддерживать множество модификаторов» будут выполнены однозначно.
Статья основана на вопросе на StackOverflow от Bhavani Kumar Metla и ответе от Swifty.