在Python中,使用lxm将大xml文件聚合到字典花费的时间太长

2024-10-06 11:43:05 发布

您现在位置:Python中文网/ 问答频道 /正文

我在将一个大xml文件(~300MB)的值迭代和求和到python字典中时遇到问题。我很快意识到不是lxml etrees iterparse减慢了速度,而是每次迭代时对字典的访问。你知道吗

以下是我的XML文件中的代码段:

    <timestep time="7.00">
        <vehicle id="1" eclass="HBEFA3/PC_G_EU4" CO2="0.00" CO="0.00" HC="0.00" NOx="0.00" PMx="0.00" fuel="0.00" electricity="0.00" noise="54.33" route="!1" type="DEFAULT_VEHTYPE" waiting="0.00" lane="-27444291_0" pos="26.79" speed="4.71" angle="54.94" x="3613.28" y="1567.25"/>
        <vehicle id="2" eclass="HBEFA3/PC_G_EU4" CO2="3860.00" CO="133.73" HC="0.70" NOx="1.69" PMx="0.08" fuel="1.66" electricity="0.00" noise="65.04" route="!2" type="DEFAULT_VEHTYPE" waiting="0.00" lane=":1785290_3_0" pos="5.21" speed="3.48" angle="28.12" x="789.78" y="2467.09"/>
    </timestep>
    <timestep time="8.00">
        <vehicle id="1" eclass="HBEFA3/PC_G_EU4" CO2="0.00" CO="0.00" HC="0.00" NOx="0.00" PMx="0.00" fuel="0.00" electricity="0.00" noise="58.15" route="!1" type="DEFAULT_VEHTYPE" waiting="0.00" lane="-27444291_0" pos="31.50" speed="4.71" angle="54.94" x="3617.14" y="1569.96"/>
        <vehicle id="2" eclass="HBEFA3/PC_G_EU4" CO2="5431.06" CO="135.41" HC="0.75" NOx="2.37" PMx="0.11" fuel="2.33" electricity="0.00" noise="68.01" route="!2" type="DEFAULT_VEHTYPE" waiting="0.00" lane="-412954611_0" pos="1.38" speed="5.70" angle="83.24" x="795.26" y="2467.99"/>
        <vehicle id="3" eclass="HBEFA3/PC_G_EU4" CO2="2624.72" CO="164.78" HC="0.81" NOx="1.20" PMx="0.07" fuel="1.13" electricity="0.00" noise="55.94" route="!3" type="DEFAULT_VEHTYPE" waiting="0.00" lane="22338220_0" pos="5.10" speed="0.00" angle="191.85" x="2315.21" y="2613.18"/>
    </timestep>

每个timestep中都有越来越多的车辆。这个文件中大约有11800个时间步。你知道吗

现在我要根据它们的位置总结所有车辆的值。提供了x,y值,我可以将其转换为lat,long。你知道吗

我目前的方法是使用lxml etree iterparse对文件进行迭代,并使用lat(long-as-dict-key)对值求和。你知道吗

我正在使用本文中的fast\u iter https://www.ibm.com/developerworks/xml/library/x-hiperfparse/

from lxml import etree

raw_pollution_data = {}

def fast_iter(context, func):
    for _, elem in context:
        func(elem)
        elem.clear()
        while elem.getprevious() is not None:
            del elem.getparent()[0]
    del context

def aggregate(vehicle):
    veh_id = int(vehicle.attrib["id"])
    veh_co2 = float(vehicle.attrib["CO2"])
    veh_co = float(vehicle.attrib["CO"])
    veh_nox = float(vehicle.attrib["NOx"]) 
    veh_pmx = float(vehicle.attrib["PMx"]) # mg/s
    lng, lat = net.convertXY2LonLat(float(vehicle.attrib["x"]), float(vehicle.attrib["y"]))

    coordinate = str(round(lat, 4)) + "," + str(round(lng, 4))

    if coordinate in raw_pollution_data:
        raw_pollution_data[coordinate]["CO2"] += veh_co2
        raw_pollution_data[coordinate]["NOX"] += veh_nox
        raw_pollution_data[coordinate]["PMX"] += veh_pmx
        raw_pollution_data[coordinate]["CO"] += veh_co
    else:
        raw_pollution_data[coordinate] = {}
        raw_pollution_data[coordinate]["CO2"] = veh_co2
        raw_pollution_data[coordinate]["NOX"] = veh_nox
        raw_pollution_data[coordinate]["PMX"] = veh_pmx
        raw_pollution_data[coordinate]["CO"] = veh_co

def parse_emissions():
    xml_file = "/path/to/emission_output.xml"
    context = etree.iterparse(xml_file, tag="vehicle")
    fast_iter(context, aggregate)
    print(raw_pollution_data)

但是,这种方法需要大约25分钟来解析整个文件。我不知道该怎么做。我知道全局变量是可怕的,但我认为这会使它更干净?你知道吗

你能想点别的吗?我知道是因为字典。如果没有聚合函数,快速迭代大约需要25秒。你知道吗


Tags: 文件idcoordinatedatarawxmlfloatco2
1条回答
网友
1楼 · 发布于 2024-10-06 11:43:05

您的代码运行缓慢有两个原因:

  • 您会做不必要的工作,并使用低效的Python语句。您不使用veh_id,但仍然使用int()来转换它。创建一个空字典只是为了在单独的语句中设置其中的4个键,使用单独的str()round()调用以及字符串连接,其中字符串格式可以在一个步骤中完成所有工作,重复引用.attrib,因此Python必须为您重复查找字典属性。

  • ^{} implementation用于每个单独的(x,y)坐标时效率非常低;它每次都从零开始加载偏移量和pyproj.Proj()对象。例如,我们可以通过缓存pyproj.Proj()实例来减少重复操作。或者我们可以避免使用它,或者只使用一次,通过在一个步骤中处理所有坐标。

第一个问题可以通过删除不必要的工作和缓存像属性字典这样的东西来避免,只使用一次,并且通过缓存函数参数中重复的全局名称查找(本地名称使用起来更快);这里的_...关键字纯粹是为了避免查找全局名称:

from operator import itemgetter

_fields = ('CO2', 'CO', 'NOx', 'PMx')

def aggregate(
    vehicle,
    _fields=_fields,
    _get=itemgetter(*_fields, 'x', 'y'),
    _conv=net.convertXY2LonLat,
):
    # convert all the fields we need to floats in one step
    *values, x, y = map(float, _get(vehicle.attrib))
    # convert the coordinates to latitude and longitude
    lng, lat = _conv(x, y)
    # get the aggregation dictionary (start with an empty one if missing)
    data = raw_pollution_data.setdefault(
        f"{lng:.4f},{lat:.4f}",
        dict.fromkeys(_fields, 0.0)
    )
    # and sum the numbers
    for f, v in zip(_fields, values):
        data[f] += v

为了解决第二个问题,我们可以将位置查找替换为至少重新使用Proj()实例的内容;在这种情况下,我们需要手动应用位置偏移:

proj = net.getGeoProj()
offset = net.getLocationOffset()
adjust = lambda x, y, _dx=offset[0], _dy=offset[1]: (x - _dx, y - _dy)

def longlat(x, y, _proj=proj, _adjust=adjust):
    return _proj(*_adjust(x, y), inverse=True)

然后在聚合函数中通过替换_conv本地名称来使用它:

def aggregate(
    vehicle,
    _fields=_fields,
    _get=itemgetter(*_fields, 'x', 'y'),
    _conv=longlat,
):
    # function body stays the same

这仍然会很慢,因为它要求我们分别转换每个(x, y)对。你知道吗

它取决于所使用的精确投影,但是您可以简单地量化xy坐标本身来进行分组。首先应用偏移量,然后将坐标“四舍五入”相同的转换量,四舍五入将实现。当投影(1, 0)(0, 0)并取经度的差值时,我们知道投影使用的粗略转换率,除以10.000,我们就xy值得出聚合区域的大小:

 (proj(1, 0)[0] - proj(0, 0)[0]) / 10000

对于一个标准的UTM投影,它给出了大约11.5,因此将xy坐标除以该因子,可以得到大致相同的分组量,而无需对每个timestep数据点进行完全的坐标转换:

proj = net.getGeoProj()
factor = abs(proj(1, 0)[0] - proj(0, 0)[0]) / 10000
dx, dy = net.getLocationOffset()

def quantise(v, _f=factor):
    return v * _f // _f

def aggregate(
    vehicle,
    _fields=_fields,
    _get=itemgetter(*_fields, 'x', 'y'),
    _dx=dx, _dy=dy,
    _quant=quantise,
):
    *values, x, y = map(float, _get(vehicle.attrib))
    key = _quant(x - _dx), _quant(y - _dy)
    data = raw_pollution_data.setdefault(key, dict.fromkeys(_fields, 0.0))
    for f, v in zip(_fields, values):
        data[f] += v

对于问题中共享的非常有限的数据集,这给了我相同的结果。你知道吗

然而,如果投影在经度上不同,这可能会导致地图上不同点的结果失真。我也不知道你需要如何准确地聚集一个区域的车辆坐标。你知道吗

如果您真的只能按1/10000度经纬度的区域进行聚合,那么如果您将整个numpy数组馈送到net.convertXY2LonLat(),则可以更快地将(x,y)对转换为long/lat对。这是因为pyproj.Proj()接受数组来批量转换坐标,节省了大量的时间,避免了成百上千个单独的转换调用,因此我们只需进行一次调用。你知道吗

与其用Python字典和float对象来处理这个问题,不如在这里真正使用Pandas数据帧。它可以简单地接收从每个元素属性字典中获取的字符串(使用带有所有所需键的^{} object可以非常快地提供这些值),并在接收数据时将所有这些字符串值转换为浮点数。这些值以紧凑的二进制形式存储在连续内存中,11800行坐标和数据项不会占用太多内存。你知道吗

因此,首先将数据加载到DataFrame中,然后从该对象在单个步骤中转换(x,y)坐标,然后使用Pandas grouping functionality按面积聚合值:

from lxml import etree
import pandas as pd
import numpy as np

from operator import itemgetter

def extract_attributes(context, fields):
    values = itemgetter(*fields)
    for _, elem in context:
        yield values(elem.attrib)
        elem.clear()
        while elem.getprevious() is not None:
            del elem.getparent()[0]
    del context

def parse_emissions(filename):
    context = etree.iterparse(filename, tag="vehicle")

    # create a dataframe from XML data a single call
    coords = ['x', 'y']
    entries = ['CO2', 'CO', 'NOx', 'PMx']
    df = pd.DataFrame(
        extract_attributes(context, coords + entries),
        columns=coords + entries, dtype=np.float)

    # convert *all coordinates together*, remove the x, y columns
    # note that the net.convertXY2LonLat() call *alters the 
    # numpy arrays in-place* so we don’t want to keep them anyway. 
    df['lng'], df['lat'] = net.convertXY2LonLat(df.x.to_numpy(), df.y.to_numpy())
    df.drop(coords, axis=1, inplace=True)

    # 'group' data by rounding the latitude and longitude
    # effectively creating areas of 1/10000th degrees per side
    lnglat = ['lng', 'lat']
    df[lnglat] = df[lnglat].round(4)

    # aggregate the results and return summed dataframe
    return df.groupby(lnglat)[entries].sum()

emissions = parse_emissions("/path/to/emission_output.xml")
print(emissions)

你知道吗使用Pandas、一个示例sumo net定义文件和一个重构的XML文件(通过重复您的2个示例timestep条目5900次),我可以在大约1秒的总时间内解析整个数据集。但是,我怀疑您的11800 timesets数字太低(因为这是小于10MB的XML数据),所以我将11800*20==236000倍的样本写到一个文件中,处理Pandas需要22秒。你知道吗

您还可以查看GeoPandas,这会让您aggregate by geographical areas。你知道吗

相关问题 更多 >