如何展开含单层冗余键的嵌套字典提取AIS数据的MessageID
问题背景
我在pandas数据帧dfAIS中存储了AIS自动识别系统的嵌套字典数据,结构如下:
dfAIS Out[18]: Message ... MetaData 0 {'ShipStaticData': {'AisVersion': 1, 'CallSign... ... {'MMSI': 255814000, 'MMSI_String': 255814000, ... 1 {'MultiSlotBinaryMessage': {'ApplicationID': {... ... {'MMSI': 2276003, 'MMSI_String': 2276003, 'Shi... 2 {'StandardClassBPositionReport': {'AssignedMod... ... {'MMSI': 503760500, 'MMSI_String': 503760500, ... 3 {'PositionReport': {'Cog': 25.2, 'Communicatio... ... {'MMSI': 211648000, 'MMSI_String': 211648000, ... 4 {'StaticDataReport': {'MessageID': 24, 'PartNu... ... {'MMSI': 338467989, 'MMSI_String': 338467989, ... ... ... ... 139625 {'PositionReport': {'Cog': 360, 'Communication... ... {'MMSI': 244730300, 'MMSI_String': 244730300, ... 139626 {'PositionReport': {'Cog': 231.5, 'Communicati... ... {'MMSI': 219025528, 'MMSI_String': 219025528, ... 139627 {'PositionReport': {'Cog': 360, 'Communication... ... {'MMSI': 273252100, 'MMSI_String': 273252100, ... 139628 {'UnknownMessage': {}} ... {'MMSI': 244730043, 'MMSI_String': 244730043, ... 139629 {'ShipStaticData': {'AisVersion': 1, 'CallSign... ... {'MMSI': 211666470, 'MMSI_String': 211666470, ... [139630 rows x 3 columns]
核心数据在Message列,每个元素是仅含一个键值对的字典:键(如ShipStaticData、MultiSlotBinaryMessage)可视为消息类型,我称其为一级字典;实际有效数据在该键对应的二级字典中,包含AisVersion、ApplicationID、Cog、MessageID等字段。
由于二级字典自带MessageID用于标识消息类型,且数据帧已有MessageType列存储与一级字典键相同的标签,因此一级字典的键属于冗余信息,无需保留。我想展开嵌套字典直接访问MessageID,但因不同消息类型的嵌套路径不一致(如Message.ShipStaticData.MessageID和Message.MultiSlotBinaryMessage.MessageID),无法直接用json_normalize;尝试用dfAIS.Message.apply(dict.items)后,得到的是嵌套元组序列,冗余的消息类型键仍存在:
df = dfAIS.Message.apply(dict.items) 0 ((ShipStaticData, {'AisVersion': 1, 'CallSign'... 1 ((MultiSlotBinaryMessage, {'ApplicationID': {'... 2 ((StandardClassBPositionReport, {'AssignedMode... 3 ((PositionReport, {'Cog': 25.2, 'Communication... 4 ((StaticDataReport, {'MessageID': 24, 'PartNum... 139625 ((PositionReport, {'Cog': 360, 'CommunicationS... 139626 ((PositionReport, {'Cog': 231.5, 'Communicatio... 139627 ((PositionReport, {'Cog': 360, 'CommunicationS... 139628 ((UnknownMessage, {})) 139629 ((ShipStaticData, {'AisVersion': 1, 'CallSign'... Name: Message, Length: 139630, dtype: object
解决方案
方案1:处理已加载的DataFrame
提取二级字典并提取MessageID
先把Message列的每个元素转换为二级字典(跳过冗余的一级键),再提取MessageID:
# 提取二级字典:取每个字典的第一个值 dfAIS['MessageContent'] = dfAIS['Message'].apply(lambda x: next(iter(x.values()))) # 提取MessageID,空字典或无该字段时返回NaN dfAIS['MessageID'] = dfAIS['MessageContent'].apply(lambda x: x.get('MessageID', pd.NA))
如果需要将二级字典的所有字段展开到数据帧中,可以用json_normalize:
message_df = pd.json_normalize(dfAIS['Message'].apply(lambda x: next(iter(x.values())))) # 合并回原数据帧 dfAIS = pd.concat([dfAIS.drop(columns=['Message']), message_df], axis=1)
高效批量处理(避免逐行apply)
针对13万行的大数据量,用列表推导式提升处理效率:
# 提取所有二级字典 message_contents = [next(iter(msg.values())) for msg in dfAIS['Message']] # 转成数据帧 message_df = pd.DataFrame(message_contents) # 合并回原数据帧 dfAIS = pd.concat([dfAIS.drop(columns=['Message']), message_df], axis=1)
方案2:从JSONL源文件读取时直接处理
如果可以重新加载数据,可在读取阶段跳过冗余的一级键,减少后续处理步骤:
import pandas as pd # 读取JSONL文件 df = pd.read_json('ais_data.jsonl', lines=True) # 替换Message列为二级字典内容 df['Message'] = df['Message'].apply(lambda x: next(iter(x.values()))) # 展开所有字段 final_df = pd.concat([df.drop(columns=['Message']), pd.json_normalize(df['Message'])], axis=1)
也可以在读取时用converters参数直接处理:
df = pd.read_json( 'ais_data.jsonl', lines=True, converters={'Message': lambda x: next(iter(x.values()))} ) final_df = pd.concat([df.drop(columns=['Message']), pd.json_normalize(df['Message'])], axis=1)
附言解答
术语表述建议
可以将一级字典的键称为消息类型标识键,对应的二级字典称为消息内容字典,能清晰区分两者,避免混淆。
3行JSONL样本
{"Message": {"ShipStaticData": {"AisVersion": 0, "CallSign": "GDNB ", "Destination": "AVONMOUTH DREDGING ", "Dimension": {"A": 30, "B": 5, "C": 7, "D": 3}, "Dte": false, "Eta": {"Day": 0, "Hour": 24, "Minute": 60, "Month": 0}, "FixType": 1, "ImoNumber": 702864, "MaximumStaticDraught": 14, "MessageID": 5, "Name": "MALAGO ", "RepeatIndicator": 0, "Spare": false, "Type": 33, "UserID": 235065329, "Valid": true}}, "MessageType": "ShipStaticData", "MetaData": {"MMSI": 235065329, "MMSI_String": 235065329, "ShipName": "MALAGO ", "latitude": 51.50296166666667, "longitude": -2.707621666666667, "time_utc": "2024-04-15 23:00:24.874950587 +0000 UTC"}} {"Message": {"PositionReport": {"Cog": 226.1, "CommunicationState": 59916, "Latitude": 33.74614666666667, "Longitude": -118.22303833333334, "MessageID": 1, "NavigationalStatus": 0, "PositionAccuracy": false, "Raim": false, "RateOfTurn": 0, "RepeatIndicator": 0, "Sog": 1.1, "Spare": 0, "SpecialManoeuvreIndicator": 0, "Timestamp": 24, "TrueHeading": 11, "UserID": 367693690, "Valid": true}}, "MessageType": "PositionReport", "MetaData": {"MMSI": 367693690, "MMSI_String": 367693690, "ShipName": "KELLY C ", "latitude": 33.74614666666667, "longitude": -118.22303833333334, "time_utc": "2024-04-15 23:00:24.875182234 +0000 UTC"}} {"Message": {"Interrogation": {"MessageID": 15, "RepeatIndicator": 3, "Spare": 0, "Station1Msg1": {"MessageID": 316004037, "SlotOffset": 0, "StationID": 316004037, "Valid": true}, "Station1Msg2": {"MessageID": 0, "SlotOffset": 0, "Spare": 0, "Valid": false}, "Station2": {"MessageID": 0, "SlotOffset": 0, "Spare1": 0, "Spare2": 0, "StationID": 0, "Valid": false}, "UserID": 3669987, "Valid": true}}, "MessageType": "Interrogation", "MetaData": {"MMSI": 3669987, "MMSI_String": 3669987, "ShipName": "", "latitude": 47.548895, "longitude": -122.78526333333333, "time_utc": "2024-04-15 23:00:24.875383071 +0000 UTC"}}
内容的提问来源于stack exchange,提问作者user2153235

