24 сентября 2019

Как обработать большие датасеты в pandas. Работаем с базой ФИАС, используя python и 8Гб памяти

Python
Tutorial
Особо представлять базу ФИАС нет необходимости:



Скачать ее можно перейдя по ссылке, данная база является открытой и содержит все адреса объектов по России (адресный реестр). Интерес к этой базе вызван тем, что файлы, которые в ней содержатся достаточно объемны. Так, например, самый маленький составляет 2,9 Гб. Предлагается остановиться на нем и посмотреть, справится ли с ним pandas, если работать на машине, располагая только 8 Гб оперативной памяти. А если не справится, какие есть опции, для того, чтобы скормить pandas данный файл.

Положа руку на сердце, не разу не сталкивался с данной базой и это дополнительное препятствие, т.к. абсолютно не ясен формат данных, представленных в ней.

Скачав архив fias_xml.rar с базой, достанем из него файл — AS_ADDROBJ_20190915_9b13b2a6-b3bd-4866-bd1c-7ab966fafcf0.XML. Файл имеем формат xml.

Для более удобной работы в pandas рекомендуется конвертировать xml в csv или json.
Однако все попытки конвертации сторонними программами и самим python приводят к ошибке «MemoryError» либо зависанию.

Хм. Что, если разрезать файл и частями конвертировать? Хорошая идея, но все «резчики» также пытаются считать файл в память целиком и виснут, не режет его и сам python, идущий по пути «резчиков». 8 Гб явно маловато? Что ж, посмотрим.

Программа Vedit


Придется воспользоваться сторонней программой vedit.

Данная программа позволяет считать файл xml размером 2,9 Гб и поработать с ним.
Также она позволяет его разделить. Но тут есть небольшая хитрость.

Как видно при считывании файла, в нем, помимо прочего, есть открывающий тег AddressObjects:



Значит, создавая части данного большого файла, надо не забывать его(тег) закрывать.

То есть начало каждого xml файла будет таким:

<?xml version="1.0" encoding="utf-8"?><AddressObjects>

а окончание:

</AddressObjects>

Теперь отрежем первую часть файла (для остальных частей шаги те же).

В программе vedit:



Далее выбираем Goto и Line#. В открывшемся окне пишем номер строки, например 1000000:



Далее надо подкорректировать выделенный блок, чтобы он захватил до конца объект в базе до закрывающего тега:



Ничего страшного, если будет небольшой нахлест на последующий объект.

Далее в программе vedit сохраняем выделенный фрагмент — File, Save as.

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

В итоге должно получиться 4-е xml файла размером примерно по 610 Мб.

Доработаем xml-части


Теперь надо во вновь созданных файлах xml добавить теги, чтобы они читались как xml.

Откроем поочередно файлы в vedit и добавим в начале каждого файла:

<?xml version="1.0" encoding="utf-8"?><AddressObjects>

и в конце:

</AddressObjects>

Таким образом, теперь у нас 4 xml части разделенного первоначального файла.

Xml-to-csv


Теперь переведем xml в csv, написав программу на python.

Код программы

здесь
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import codecs,os
import xml.etree.ElementTree as ET
import csv
from datetime import datetime

parser = ET.XMLParser(encoding="utf-8")
tree = ET.parse("add-30-40.xml",parser=parser)
root = tree.getroot()

Resident_data = open('fias-30-40.csv', 'w',encoding='UTF8')
csvwriter = csv.writer(Resident_data)

start = datetime.now()
for member in root.findall('Object'):
    object = []
    
    object.append(member.attrib['AOID'])            
    object.append(member.attrib['AOGUID'])
    try:
        object.append(member.attrib['PARENTGUID'])
    except:
        object.append(None)
    try:
        object.append(member.attrib['PREVID'])
    except:
        object.append(None)
    #try:
    #    object.append(member.attrib['NEXTID'])
    #except:
    #    object.append(None)
    
    object.append(member.attrib['FORMALNAME'])
    object.append(member.attrib['OFFNAME'])
    object.append(member.attrib['SHORTNAME'])
    object.append(member.attrib['AOLEVEL'])
    object.append(member.attrib['REGIONCODE'])
    object.append(member.attrib['AREACODE'])
    object.append(member.attrib['AUTOCODE'])    
    object.append(member.attrib['CITYCODE'])
    object.append(member.attrib['CTARCODE'])
    object.append(member.attrib['PLACECODE'])
    object.append(member.attrib['STREETCODE'])
    object.append(member.attrib['EXTRCODE'])
    object.append(member.attrib['SEXTCODE'])
    try:
        object.append(member.attrib['PLAINCODE'])
    except:
        object.append(None)
    try:
        object.append(member.attrib['CODE'])
    except:
        object.append(None)
    object.append(member.attrib['CURRSTATUS'])
    object.append(member.attrib['ACTSTATUS'])
    object.append(member.attrib['LIVESTATUS'])
    object.append(member.attrib['CENTSTATUS'])
    object.append(member.attrib['OPERSTATUS'])
    try:
        object.append(member.attrib['IFNSFL'])
    except:
        object.append(None)
    try:
        object.append(member.attrib['IFNSUL'])
    except:
        object.append(None)
    try:
        object.append(member.attrib['OKATO'])
    except:
        object.append(None)
    try:
        object.append(member.attrib['OKTMO'])
    except:
        object.append(None)
    try:
        object.append(member.attrib['POSTALCODE'])
    except:
        object.append(None)
    #print(len(object))
    csvwriter.writerow(object)    
Resident_data.close()
print(datetime.now()- start)
#0:00:21.122437

.
С помощью программы надо конвертировать все 4-е файла в csv.
Размер файлов уменьшится, каждый будет по 236 Мб (сравните с 610 Мб в xml).
В принципе, теперь с ними уже можно работать, через excel или notepad++.
Однако файлов пока 4-е вместо одного, и мы не добрались до цели — обработка файла в pandas.

Склеим файлы в один


В Windows это может оказаться непростым занятием, поэтому воспользуемся консольной утилитой на python, которая называется csvkit. Устанавливается как модуль python:

pip install csvkit

*На самом деле это целый набор утилит, но оттуда потребуется одна.

Зайдя в папку с файлами для склейки в консоли, выполним склейку в один файл. Так как все файлы без заголовков, то назначим при склейке стандартные названия столбцов: a,b,c и т.д.:

csvstack -H fias-0-10.csv fias-10-20.csv fias-20-30.csv  fias-30-40.csv > joined2.csv

На выходе получаем готовый csv файл.

Поработаем в pandas над оптимизацией использования памяти


Если сразу загрузить в pandas файл


import pandas as pd
import numpy as np
gl = pd.read_csv('joined2.csv',encoding='ANSI',index_col='a')
print (gl.info(memory_usage='deep')) # использование памяти
def mem_usage(pandas_obj):
    if isinstance(pandas_obj,pd.DataFrame):
        usage_b = pandas_obj.memory_usage(deep=True).sum()
    else: # предположим, что если это не датафрейм, то серия
        usage_b = pandas_obj.memory_usage(deep=True)
    usage_mb = usage_b / 1024 ** 2 # преобразуем байты в мегабайты
    return "{:03.2f} МВ" .format(usage_mb)

и проверить сколько он займет памяти, результат может неприятно удивить:



3 Гб! И это при том, что при считывании данных первый столбец «пошел» в качестве индекс-столбца*, а так объем был бы еще больше.
*По умолчанию pandas задает свой индекс-столбец.

Проведем оптимизацию, используя методы из предыдущего поста и статьи:
— object в category;
— int64 в uint8;
— float64 в float32.

Для этого при считывании файла добавим dtypes и считывание столбцов в коде будет выглядеть так:


gl = pd.read_csv('joined2.csv',encoding='ANSI',index_col='a', dtype ={
    'b':'category', 'c':'category','d':'category','e':'category',
    'f':'category','g':'category',

    'h':'uint8','i':'uint8','j':'uint8',
    'k':'uint8','l':'uint8','m':'uint8','n':'uint16',
    'o':'uint8','p':'uint8','q':'uint8','t':'uint8',
    'u':'uint8','v':'uint8','w':'uint8','x':'uint8',

    'r':'float32','s':'float32',
    'y':'float32','z':'float32','aa':'float32','bb':'float32',
    'cc':'float32'    
    })

Теперь, открыв файл pandas использование памяти будет разумным:



Осталось добавить в csv файл, при желании, строку-фактические названия столбцов, чтобы данные обрели смысл:

AOID,AOGUID,PARENTGUID,PREVID,FORMALNAME,OFFNAME,SHORTNAME,AOLEVEL,REGIONCODE,AREACODE,AUTOCODE,CITYCODE,CTARCODE,PLACECODE,STREETCODE,EXTRCODE,SEXTCODE,PLAINCODE,CODE,CURRSTATUS,ACTSTATUS,LIVESTATUS,CENTSTATUS,OPERSTATUS,IFNSFL,IFNSUL,OKATO,OKTMO,POSTALCODE

*Этой строкой можно заменить названия столбцов, но тогда придется поменять код.
Сохраним первые строки файла из pandas


gl.head().to_csv('out.csv', encoding='ANSI',index_label='a')

и посмотрим, что получилось в excel:



Код программы для оптимизированного открытия csv файла с базой:

код
import os
import time
import pandas as pd
import numpy as np

#используем оптимизацию памяти при считывании датафрейма: для object-category,для float64-float32,для int64-int
gl = pd.read_csv('joined2.csv',encoding='ANSI',index_col='a', dtype ={
    'b':'category', 'c':'category','d':'category','e':'category',
    'f':'category','g':'category',

    
    'h':'uint8','i':'uint8','j':'uint8',
    'k':'uint8','l':'uint8','m':'uint8','n':'uint16',
    'o':'uint8','p':'uint8','q':'uint8','t':'uint8',
    'u':'uint8','v':'uint8','w':'uint8','x':'uint8',


    'r':'float32','s':'float32',
    'y':'float32','z':'float32','aa':'float32','bb':'float32',
    'cc':'float32'
    
    })

pd.set_option('display.notebook_repr_html', False)
pd.set_option('display.max_columns', 8)
pd.set_option('display.max_rows', 10)
pd.set_option('display.width', 80)

#print (gl.head())
print (gl.info(memory_usage='deep')) # использование памяти
def mem_usage(pandas_obj):
    if isinstance(pandas_obj,pd.DataFrame):
        usage_b = pandas_obj.memory_usage(deep=True).sum()
    else: # предположим, что если это не датафрейм, то серия
        usage_b = pandas_obj.memory_usage(deep=True)
    usage_mb = usage_b / 1024 ** 2 # преобразуем байты в мегабайты
    return "{:03.2f} МВ" .format(usage_mb)


В завершение посмотрим размер датасета:

gl.shape

(3348644, 28)

3,3 млн строк, 28 столбцов.

Итог: при первоначальном объеме файла csv 890 Мб, «оптимизированный» для целей работы с pandas он занимает в памяти 1,2 Гб.
Таким образом, при грубом расчете можно предположить что файл размером 7,69 Гб можно будет открыть в pandas, предварительно его «оптимизировав».
Теги:pandasxml-to-csvфиас
Хабы: Python
+2
6,9k 61
Комментарии 30
Похожие публикации
Python Developer(Relocation to Riga)
от 2 500 до 4 000 €BETBYСанкт-Петербург
Разработчик Python
до 170 000 ₽ВСКМоскваМожно удаленно
Python Developer
от 80 000 до 200 000 ₽kt.teamМожно удаленно
Программист Python
от 80 000 до 120 000 ₽FITTINВоронеж
Python Разработчик (Python Backend Developer)
от 150 000 ₽Правое полушарие ИнтровертаМожно удаленно
▇▅▄▅▅▄ ▇▄▅