Як використовувати Python для «випасу» ваших неструктурованих даних

Здравствуйте, шановні читачі.

Останнім часом ми опрацьовуємо різні теми, пов'язані з мовою Python, в тому числі, проблеми отримання та аналізу даних. Наприклад, нас зацікавила книга «Data Wrangling with Python: Tips and Tools to Make Your Life Easier»:



Тому якщо ви ще не знаєте, що таке скрепінг, витяг неструктурованих даних, і як привести хаос в порядок, пропонуємо почитати переклад цікавої статті Піта Тамисина (Pete Tamisin), який розповідає, як це робиться на Python. Оскільки стаття відкриває цілу серію постів автора, а ми вирішили поки що обмежитися тільки нею, текст трохи скорочений.

Якщо хтось сам мріє підготувати та видати книгу на цю тему — пишіть, обговоримо.

Я – продакт-менеджер в компанії Rittman Mead, але при цьому я ще вважаю себе фріком 80 лвл. Я полюбив комікси, фентезі, наукову фантастику та інше подібне чтиво в ті часи, коли слово «Старк» ще аж ніяк не асоціювалося з Робертом Дауні-молодшим або з мемами про насування зими. Тому мені дуже сподобалася перспектива об'єднати приємне з корисним, тобто, хобі з роботою. Я вирішив написати у себе в блозі кілька статей про побудові прогностичної моделі на підставі даних, пов'язаних з продажами коміксів. Кінцева мета — запропонувати таку модель, яка дозволяла б достовірно судити, чи читачі змітати новий комікс з полиць як гарячі пиріжки, або він заляже на складі. У цій публікації я розповім про деякі підводні камені, які можуть підстерігати вас при підготовці даних для аналізу. Підготовка даних, також іменована «випасом» (wrangling) – це недосконалий процес, зазвичай виконується в кілька ітерацій і включає перетворення, інтерпретацію і рефакторинг – після чого дані можна буде аналізувати.

Хоча етапи випасу даних можуть відрізнятися в залежності від стану та доступності «сирих» даних, в цій статті я вирішив зосередитися на зборі інформації з різних джерел, збагаченні даних шляхом злиття їх атрибутів і реструктуруванні даних для спрощення аналізу. Серії книг з коміксами легко знайти в Інтернеті, однак виявилося, що не так просто добути їх у удобоваримом форматі. У підсумку я вирішив вступити дешево і сердито – зайнявся екранним скрепингом інформації з сайту, присвяченого дослідженню коміксів. Для тих щасливчиків, яким поки не довелося мати справу з екранним скрепингом, пояснюю: при скрепинге програмно завантажуєте HTML-дані і видалити з них всяке форматування, щоб ці дані можна було використовувати. Як правило, цей прийом використовується, коли нічого іншого не залишається, оскільки сайт – штука непостійна, вміст на ньому змінюється не рідше, ніж дитина-підліток намыливается втекти з дому.



Отже, перша проблема, з якою можна зіткнутися при випасі даних. У вас є доступ до масі даних, але вони неакуратні. Треба їх причесати. Робота з сирими даними нагадує роботу різьбяра по дереву. Ваше завдання – не змінити структуру даних таким чином, щоб підігнати їх під свої цілі, а відсікти все зайве, щоб від чурки залишилася гарна конячка… я маю на увазі, щоб ви могли зробити висновки. Вибачте, захопився метафорами. До речі, продовжуючи цю аналогію: для роботи над цим проектом я першим ділом витягнув з столярного скриньки Python. Для програміста Python – реальний мультитул. Він швидкий, добре поєднується з іншими технологіями і, що найбільш важливо в даному випадку, він повсюдно поширений. Python використовується для вирішення різноманітних задач – від автоматизації процесів і ETL до програмування ігор і академічних досліджень. Python – по-справжньому багатоцільовий мову. Таким чином, зіткнувшись з конкретним завданням, ви цілком можете знайти в Python спеціально призначена для неї нативний модуль, або хто-то вже міг написати загальнодоступну бібліотеку, що володіє потрібним функціоналом. Мені були потрібні певні скрипти для «скрепинга» HTML-таблиць з даними про продажі коміксів. Далі мені потрібно скомбінувати цю інформацію за іншими даними по коміксах, здобутими в іншому місці. «Інша» інформація представляла собою метадані про кожного з випусків. Метадані – це просто інформація, що описує інші дані. В даному випадку до метаданих ставилася інформація про автора, про продажі, про час публікації тощо… Докладніше про це нижче.



На щастя, ті дані, які я скріпив, були в табличному форматі, тому витягати їх і перетворювати в об'єкти Python було відносно просто – потрібно лише перебрати рядка таблиці і пов'язати кожен стовпець таблиці зі спеціально призначеним для цього полем об'єкта Python. На сторінці все-таки залишалося досить багато непотрібного контенту, який доводилось ігнорувати – наприклад, тег title й інші структурні елементи. Але, як тільки я знайшов потрібну таблицю з даними, мені вдалося її ізолювати. На даному етапі я записав об'єкти в CSV-файл, щоб дані було легко передавати, а також щоб їх було простіше використовувати з застосуванням інших мов і/або процесів.

Вся складна робота в даному випадку виконувалася за допомогою трьох модулів Python: urllib2, bs4 and csv. Urllib2, як зрозуміло з назви, містить функції для відкриття URL. Працюючи над цим проектом, я знайшов сайт, де була така сторінка: на ній містилася інформація з зразковими продажами випусків за кожен місяць аж до початку 90-х. Щоб витягти дані за кожний місяць, не оновлюючи вручну жорстко закодовані URL знову і знову, я написав скрипт, який брав в якості аргументів MONTH та YEAR —
month_sales_scraper.py




Відгук на виклик функції
urlopen(url) 
містив повний HTML-код в такому вигляді, як він відображається в браузері. Такий формат був для мене практично марний, тому довелося задіяти парсер, щоб витягти дані з HTML. Парсер в даному випадку – це програма, яка зчитує документ в конкретному форматі, розбиває його на складові, не порушуючи при цьому тих взаємозв'язків, що існували між цими складовими; нарешті, парсер дозволяє вибірково звертатися до вищезазначених складових. Отже, HTML-парсер відкривав мені легкий доступ до всіх тегам стовпців в конкретній таблиці всередині HTML-документа. Я скористався програмою BeautifulSoup, вона ж bs4.

У BeautifulSoup є пошукові функції, що дозволяють знайти конкретну HTML-таблиці з даними про продажі і циклічно перебрати всі її рядки, заповнюючи при цьому об'єкт Python значеннями стовпців таблиці.



Цей об'єкт Python під назвою data містить поля, заповнені даними з різних джерел. Інформація про рік і місяці заповнюється на основі аргументів, переданих модулю. Поле формату встановлюється динамічно, виходячи з логіки, за якою будуються рейтинги, а інші поля заповнюються залежно від того, де саме їх джерела розташовані в HTML таблиці. Як бачите, тут багато жорстко закодованої логіки, яку довелося б оновлювати вручну, якби змінився формат того сайту, з якого ми отримуємо дані. Проте, поки ми справляємося із завданням, користуючись описаної тут логікою.
Останній етап вирішення завдання – записати ці об'єкти Python в CSV-файл. У модулі Python CSV є функція
writerow()
, яка приймає як параметр масив і записує всі елементи масиву як стовпці у форматі CSV.



При першому прогоні програма видала виняток, оскільки в полі title містилися символи unicode, які не міг опрацювати механізм запису в CSV.



Щоб справитися з цим, довелося додати перевірку на unicode і закодувати весь вміст як UTF-8. Unicode та UTF-8 – це символ кодування, тобто, вони служать словниками, за допомогою яких комп'ютери ідентифікують символи. У кодуваннях містяться алфавітні і логографические символи з різних мов, а також інші поширені символи, наприклад,.

Крім того, потрібно переформатувати значення деяких числових полях (зокрема, видалити звідти символи $ і коми), щоб пізніше над цими значеннями можна було виконувати математичні дії. В іншому завантаження даних пройшла цілком гладко. Для кожного місяця був згенерований файл під назвою (MONTH)_(YEAR).CSV. Кожен з цих файлів виглядав так:



Хоча в результаті були згенеровані десятки тисяч рядків з даними про продажі коміксів, мені цього було недостатньо. Тобто, у мене був потрібний обсяг інформації, але вона вийшла недостатньо широкою. Щоб робити точні прогнози, мені потрібно було заповнити моделі та інші змінні, а не тільки назву коміксу, номер випуску і ціну. Видавництво не мало значення, так як я вирішив повправлятися лише з коміксами Marvel, а передача приблизних даних по продажах була б шахрайством, оскільки рейтинг залежить від продажів. Отже, щоб поліпшити моє безліч даних, я витягнув метадані про кожному випуску з «хмари», скориставшись Marvel's Developer API. На щастя, оскільки цей API є веб-сервісом, вдалося обійтися без скрепинга екрану.

Витягувати і об'єднувати ці дані було не так просто як може здатися. Основна проблема полягала в тому, що назви випусків, отримані шляхом скрепинга, не повністю збігалися з назвами, збереженими в базі даних Marvel. Наприклад, у масиві даних, отриманих шляхом скрепинга, фігурує назва 'All New All Different Avengers". Скориставшись API для пошуку по базі даних Marvel я нічого такого не знайшов. В результаті вдалося вручну відшукати у них в базі даних запис «All-New All Different Avengers». В інших випадках траплялися зайві слова, порівняйте: «The Superior Foes of Spider-Man» і «Superior Foes of Spider-Man». Отже, щоб виконати пошук за назвою, я повинен був знати, в якому вигляді назва може згадуватися в базі даних Marvel. Для цього я вирішив зробити список назв всіх тих серій, чиї метадані були змінені протягом проміжків часу, з яким у мене були дані про продажі. І знову я зіткнувся з перешкодою. API Marvel дозволяв отримувати не більше 100 результатів за одним запитом, а Marvel опублікували тисячі коміксів. Щоб обійти цю проблему, довелося витягувати дані инкрементно, сегментуємо їх за алфавітом.



Навіть тут виникли невеликі проблеми, оскільки на деякі букви, наприклад, 'S', потрапляло понад 100 назв. Щоб вирішити їх, мені довелося витягати всі назви 'S' спочатку в алфавітному, а потім у зворотному алфавітному порядку, потім комбінувати ці результати і позбуватися від всіх дублів. Тому раджу уважно розбиратися з усіма обмеженнями того API, який ви збираєтеся використовувати. Можливо, якісь обмеження виявляться непереборні, але, можливо, і вдасться обійти, винахідливо формулюючи запити.



На даному етапі у мене вже був список назв серій Marvel, збережених в декількох CSV-файли, які я в підсумку склав в один файл MarvelSeriesList.csv, щоб було простіше працювати. Але у мене було і ще дещо. Витягуючи назви серій, я також зберігав ID кожної серії і рейтинг її репрезентативності. Пошук по ID набагато більш точний, ніж по імені, а рейтинг репрезентативності може стати в нагоді при побудові прогностичної моделі. Далі потрібно перебрати всі рядки CSV-файлів, створених на основі даних про продажі, знайти відповідності з ID файлу MarvelSeriesList.csv і використовувати цей ID для вилучення відповідних метаданих через API.

Як ви пам'ятаєте, останній етап знадобився тому, що заголовки, збережені у файлах з даними про продажі, не збіглися з заголовками в API, і мені потрібно було якимось чином об'єднати ці два джерела. Я не став писати кейси для обробки кожного сценарію (наприклад, розбіжність пунктуації, зайві слова), я знайшов бібліотеки Python для пошуку нечітких відповідностей. Мені попалася виключно корисна бібліотека Fuzzy Wuzzy.Fuzzy Wuzzy, в якій була функція
extractOne()
. Ця функція дозволяє передати термін і порівняти його з масивом значень. Потім функція
extractOne()
повертає знайдений у масиві термін, максимально відповідний запит. Крім того, можна вказати нижню межу прийнятності відповідності (тобто повертати лише ті результати, ступінь відповідності яких >= 90%).

Знову ж таки, довелося трохи повозитися, щоб така конфігурація працювала ефективно. На перший раз лише для 65% назв в списках продажів вдалося знайти відповідності. На мій погляд, скрипт відсіював занадто багато даних, тому довелося придивитися до винятків і з'ясувати, які у відповідності вислизають від мене. Так, виявилася наступна проблема: заголовки, прив'язані в базі даних Marvel до конкретного році, наприклад, «All-New X-Men (2012)», мали рейтинг відповідності понад 80 при порівнянні з такими заголовками, як «All New X-Men». Ця проблема зустрічалася постійно, тому я вирішив не занижувати відсоток відповідності (в такому випадку, скрипт міг не помітити деяких реальних розбіжностей), а при незбігу відсікати рік і перевіряти відповідність знову. Майже вийшло. Була ще така проблема: бібліотека This was a pretty consistent issue, so rather than lowering the match percentage, which Fuzzy Wuzzy погано порівнювала акроніми та акровірші. Так, при порівнянні 'S. H. E. I. L. D.' і 'SHIELD' виходила ступінь відповідності близько 50. Справа в тому, що в другому варіанті не вистачало половини символів (точок). Оскільки в цьому випадку піднімалось всього дві назви, я написав пошуковий словник зі спеціальними випадками, які потрібно «перекладати». В описуваному вправі можна було пропустити цей крок, оскільки у мене і так виходило пристойне відповідність, але для стовідсотково точного пошуку збігів він необхідний. Як тільки функція пошуку відповідностей запрацювала, я підключив urllib2 і витягнув всі метадані за випусками, які зміг дістати.

У готових файли містилися не тільки дані про продажі (назва, номер випуску, місячний рейтинг, зразкові продажу), але і інформація про авторів, самих випусках, персонажів, дати виходу і відповідних сюжетних лініях.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.