Загрузка XML-документа серии Delft-FEWS во фрейм данных pandas

#python #pandas #xml

Вопрос:

У меня есть xml-документ, содержащий несколько временных рядов, полученных из веб-запроса к веб-PI Delft-FEWS. Пример реалистичного ответа этой конкретной веб-службы приведен в документации, но формат сводится к:

 <?xml version="1.0" encoding="UTF-8"?>
<TimeSeries xmlns="http://www.wldelft.nl/fews/PI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.wldelft.nl/fews/PI http://fews.wldelft.nl/schemas/version1.0/pi-schemas/pi_timeseries.xsd" version="1.23" xmlns:fs="http://www.wldelft.nl/fews/fs">
    <timeZone>0.0</timeZone>
    <series>
        <header>
        ...
        <locationId>63306260000</locationId>
        <parameterId>T.obs.mean</parameterId>
        ...
        </header>
        <event date="2013-01-01" time="00:00:00" value="2" flag="0"/>
        <event date="2013-02-01" time="00:00:00" value="1.7" flag="0"/>
        <event date="2013-03-01" time="00:00:00" value="2.5" flag="2"/>
        ...
    </series>
    <series>
        ...
    </series>
    ...
</TimeSeries>
 

Как загрузить этот XML-документ в pandas фрейм данных для дальнейшей обработки?

Примечание: Я публикую этот вопрос, поскольку он возник для коллеги, и я подумал, что это может принести пользу другим, так как было несколько ошибок. Я скоро опубликую ответ.

Ответ №1:

Начиная с версии 1.3.0, pandas имеет read_xml() функцию, которая может быть полезна здесь. Кроме того, при импорте lxml read_xml() поддерживается более полная реализация XPath.

Решение:

 from lxml import etree
from pandas import read_xml, DataFrame, MultiIndex, to_datetime


with open('time_series.xml') as f:
    # load the FEWS Series xml document
    tree = etree.parse(f)

# relabel the None namespace to 'fews', so we can refer to it in xpath expressions
nsmap = tree.getroot().nsmap.copy()
nsmap['fews'] = nsmap.pop(None)
# obtain the series in the document
series = tree.xpath('//fews:series', namespaces=nsmap)

# create and fill a new dataframe
df = DataFrame()
for s in series:
    # location and parameter names are in the header on the series
    loc_id = s.xpath('fews:header/fews:locationId/text()', namespaces=nsmap)[0]
    parameter_id = s.xpath('fews:header/fews:parameterId/text()', namespaces=nsmap)[0]
    # etree.tostring(s) is the literal xml for just the series,
    # which read_xml accepts as source data for a new dataframe
    series_df = read_xml(etree.tostring(s), xpath='//fews:series/fews:event', namespaces=nsmap)
    # combine the date and time columns into a single datetime and set it as the index for the dataframe
    series_df['datetime'] = to_datetime((series_df['date']   ' '   series_df['time']))
    series_df.set_index(['datetime'], inplace=True)
    # take the value and flag columns and add them to the dataframe being constructed
    df[loc_id, parameter_id, 'value'] = series_df['value']
    df[loc_id, parameter_id, 'flag'] = series_df['flag']

# turn the columns labeled with tuples into a proper multi-index on the dataframe
df.columns = MultiIndex.from_tuples(df.columns, names=['location', 'parameter', 'type'])
 

Это разбивает документ на отдельные серии, используется read_xml() для их разбора на фреймы данных, столбцы которых добавляются в результат в виде столбцов с соответствующими метками. И, наконец, индекс превращается в мультииндекс для легкого доступа.

Несколько примеров о том, как получить доступ к данным во фрейме данных:

 # now you can access df as needed, for example for all the values (not the flags):
print(df.xs('value', level='type', axis=1))

# ... or just the values and flags for '63306260000':
print(df.xs('63306260000', level='location', axis=1))

# ... or if you only need the for any of the parameters:
print(df.xs(('63306260000', 'value'), level=('location', 'type'), axis=1))

# ... of course, if you know exactly what you need, you can skip .xs:
print(df[('63306260000', 'T.obs.mean', 'value')])