Агрегирование большого xml-файла в словарь в Python занимает слишком много времени с использованием lxml etree - PullRequest
1 голос
/ 12 октября 2019

У меня проблемы с итерацией и суммированием значений большого XML-файла (~ 300 МБ) в словарь 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>

В каждом временном шаге растет число транспортных средств. В этом файле около 11800 временных шагов.

Теперь я хочу суммировать значения для всех транспортных средств на основе их местоположения. Имеются значения x, y, которые я могу преобразовать в lat, long.

Мой текущий подход состоит в том, чтобы перебирать файл с помощью lxml etree iterparse и суммировать значения, используя lat, long как dict key.

Я использую fast_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 минут для анализа всего файла. Я не уверен, как это сделать по-другому. Я знаю, что глобальная переменная ужасна, но я подумала, что это сделает ее чище?

Можете ли вы придумать что-нибудь еще? Я знаю, что это из-за словаря. Без функции агрегирования fast_iter занимает около 25 секунд.

1 Ответ

2 голосов
/ 12 октября 2019

Ваш код работает медленно по 2 причинам:

  • Вы выполняете ненужную работу и используете неэффективные операторы Python. Вы не используете veh_id, но все еще используете int() для его преобразования. Вы создаете пустой словарь только для установки в нем 4 ключей в отдельных операторах, вы используете отдельные вызовы str() и round() вместе с конкатенацией строк, где форматирование строк может выполнять всю эту работу за один шаг, вы постоянно ссылаетесь на .attrib,поэтому Python должен постоянно находить этот словарный атрибут для вас.

  • Реализация sumolib.net.convertXY2LonLat() очень неэффективна, когда используется для каждого отдельного человека (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) пару отдельно.

Это зависит от точной используемой проекции, но вы можете просто квантовать координаты x и y сами, чтобы выполнить группировку,Сначала вы примените смещение, а затем «округлите» координаты на ту же сумму и получите округление. При проецировании (1, 0) и (0, 0) и использовании разницы по долготе мы знаем приблизительный коэффициент конверсии, используемый проекцией, и делим его на 10.000, чтобы получить размер вашей области агрегирования в виде значений x и y:

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

Для стандартной проекции UTM дает мне значение 11.5, поэтому умножение и умножение на пол на координаты x и y на этот коэффициент должно дать вам примерно такое же количество группировок безнеобходимость полного преобразования координат для каждой точки данных временного шага:

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 / 10000th градусов долготы и широты, то вы можете сделать преобразование из (x, y) пар в пары long / lat намного быстрее, если вместо этого будете кормить целые массивы numpy до net.convertXY2LonLat(). Это связано с тем, что pyproj.Proj() принимает массивы для преобразования координат в большом количестве , экономя значительное количество времени, избегая сотен тысяч отдельных вызовов преобразования, вместо этого мы сделали бы всего один вызов,

Вместо того, чтобы обрабатывать это с помощью словаря Python и объектов с плавающей запятой, вам действительно следует использовать DataFrame Pandas здесь. Он может тривиально принимать строки, взятые из каждого словаря атрибутов элемента (используя operator.itemgetter() объект со всеми необходимыми ключами, дает вам эти значения очень быстро) и превращать все эти строковые значения в числа с плавающей запятой, когда он принимает их. данные. Эти значения хранятся в компактной двоичной форме в непрерывной памяти, 11800 строк координат и ввод данных здесь не займет много памяти.

Итак,загрузите ваши данные в DataFrame сначала , затем из этого объекта преобразуйте ваши (x, y) координаты за один шаг и только затем агрегируйте значения по области, используя Функциональность группировки Pandas :

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, образец файла определения сумо-сети и восстановленный XML-файл, повторяя ваши 2 образца записей временного шага 5900 раз, я могу проанализировать весь набор данных примерно за1 секунда, общее время. Тем не менее, я подозреваю, что число ваших 11800 временных наборов слишком мало (так как это менее 10 МБ данных XML), поэтому я записал 11800 * 20 == 236000 раз ваш образец в файл, и это заняло 22 секунды для обработки с Pandas.

Вы также можете посмотреть на GeoPandas , который позволит вам агрегировать по географическим областям .

...