Агрегатор вакансий об удалённой работе

Представляю на обозрение мой новый небольшой проект задача которого собрать в сети лучшие предложения об удалённой работе в Интернете — Remotelist.ru. Мой личный опыт удалённой работы вот вот приблизится к 4-м годам, более того, я активно поддерживаю такой вид занятости потому что у него куда больше плюсов чем минусов. Обещаю следующий мой пост посвятить лайфхакам удалённой работы.

О чём этот проект? Если вы находитесь в активном поиске  работы, то вам нередко приходится мониторить множество предложений с различных сайтов: Мой Круг, Stackoverflow, VC.ru, HH.ru, LinkedIn и многих других. Задача моего сайта собрать как можно больше предложений об удалённой работе в одном месте и оповещать вас о них. Проще говоря, Remotelist это агрегатор вакансий. В планах у меня есть мысль реализовать функцию постинга вакансии, но она появится чуть позже при условии востребованности сайта 🚀.

Сейчас помимо самого сайта, кросс-постинг вакансий в виде дайджестов в автоматическом режиме публикуется и в телеграм-канал @remotelist каждые 3 часа, при условии наличия новых предложений разумеется.

Если звёзды сойдутся благоприятно для сайта, то он будет развиваться в сторону персонализации. Что это значит? Если вы читатель моего блога, то в курсе, что я активно изучаю тему машинного обучения, и этот проект неплохая возможность потренировать свои навыки. Будет реализован личный кабинет с возможностью подключения своего github/stackoverflow/linkedin/etc аккаунта. На основе информации о специалисте, сайт будет рекомендовать наиболее подходящие вакансии, тем самым экономя время и нервы при поиске. Поживём-увидим.

Если вас заинтересовал проект, то, пожалуйста, подпишитесь на телеграм-канал @remotelist, а также добавьте мой сайт в закладки 😄

читать дальше

Designing Data-Intensive Applications

Где-то в середине 2017 года на глаза мне попалась интересная книга издательства O’Reilly под названием “Designing Data-Intensive Applications”. В то время я активно искал информацию в сети на тему Data Engineering. Как оказалось, материала по теме не так много, поэтому книга оказалась для меня открытием. Что же такого примечательного в ней?

Тема “data engineering” заслуживает отдельного поста, который появится в ближайшее время как только соберусь с мыслями. Для меня работа с большими данными это в первую очередь фундаментальные знания об устройстве распределенных систем. Книга Designing Data-Intensive Applications поможет вам окунуться в эту тему с головой и послужит хорошим стартом. Здесь Martin Kleppmann простым языком и очень подробно рассказывает как устроены современные базы данных, как работают sql/nosql/newsql хранилища, в чем разница между B-Tree+ и LSM-Tree, а также достаточно подробное описание современных форматов кодирования данных Avro, Thrift, Protobuf.

Особенно полезна книга будет тем, кто хочет связать свою карьеру с распределенными системами. Автор даёт достаточно информации по таким темам как репликация, партицирование, работа транзакций, а также раскрывает проблемы, возникающие при взаимодействии удаленных узлов. Ну и конечно же куда нам без CAP-теоремы. В книге также упоминаются современные методы обработки данных — Batch Processing и Stream Processing. А в последней главе нас ждет небольшое лирическое отступление и размышления на тему будущего больших данных.

Новость о выходе перевода этой книги на русский язык в издательстве “Питер” меня порадовала хотя я давно купил бумажный оригинал. Такой материал должен быть доступен как можно большему количеству людей. Немного смущает перевод названия книги, в русском варианте оно воспроизведено как “Высоконагруженные приложения. Программирование, масштабирование, поддержка”.

Несмотря на то, что электронный вариант оригинала без труда можно отыскать в сети, я советую купить бумажную версию.  Это фундаментальная книга и с годами она не потеряет свою ценность, уверяю! Заказать бумажный вариант книги в оригинале можно на сайте Amazon, а русский перевод на сайте Ozon.

читать дальше

Машинное обучение и Big Data

Около месяца назад начал проходить сразу 2 специализации на платформе Coursera:

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

Хочу поделиться мыслями о курсах. Начнём с первого.

Цель курса "Математика и Python для анализа данных" — обозначить необходимый набор навыков для успешного прохождения всей специализации. Здесь вы найдёте небольшое введение в язык Python и его инструменты для анализа данных: pandas, numpy, scipy, остальная же часть курса посвящена математическому аппарату, а именно темам из линейной алгебры (матрицы, векторы, векторное пространство), теории вероятностей и немного затронут математический анализ — предел и производная. Особенно понравился упор на прикладные задачи, т.е. изучая, например, матрицы или векторы, понимаешь как их применять для решения прикладных задач. Но курс всё таки требует некоторой предварительной подготовки по обозначенным мною темам.

Эти материалы возможно помогут при прохождении курса:

Big Data Essentials это первый из 5 курсов, посвященных "горячей" нынче теме про построение инфраструктуры для эффективного анализа данных. Анонс специализации я делал у себя в Telegram канале в первой половине октября. Тогда я только присматривался, в итоге решил проходить. Что из себя представляет первый курс? Это плавное введение в основные инструменты анализа больших данных — Apache Hadoop и Apache Spark. Из 6 недель курса, 2 недели исключительно практические, вообще практических заданий тут хватает. Материалы курса предполагают некоторый опыт программирования, от себя добавлю, что большим плюсом будет наличие навыка в функциональном программировании. К сожалению, есть и ряд минусов. Так как материал появился относительно недавно, в нём есть ошибки из-за которых я терял время. Во-первых, встречаются задания с ошибками в формулах и коде, частенько "валился" удалённый hadoop/spark кластер. Мой совет перед выполнением заданий — активно читайте форум, тестируйте код локально на небольших кусках данных, по возможности поднимите свой docker-контейнер с hadoop или spark (в материалах есть ссылка на контейнер, а на форуме инструкция по настройке). Ещё смущает дичайший русский акцент, иногда в речи встречаются неверно составленные предложения, но к этому можно привыкнуть.

Изучение продолжается. На очереди у меня "Обучение на размеченных данных" и "Big Data Analysis: Hive, Spark SQL, DataFrames and GraphFrames". Скучно точно не будет :)

читать дальше

Строим Data Pipeline на Python и Luigi

Data Pipeline

Введение

В эпоху data-intensive приложений рядовым разработчикам всё чаще приходится сталкиваться с задачами по обработке и анализу данных. Ещё десять лет назад данные большинства проектов могли уместиться на жестком диске одного компьютера в какой-нибудь реляционной базе данных типа MySQL. А задачи по извлечению и обработке хранящихся данных решались за счёт непростых (или простых) SQL запросов. С тех пор мир информационных технологий значительно поменялся. С приходом Internet of Things, мобильных телефонов и дешевого мобильного интернета, объем генерируемых данных вырос в десятки тысяч раз. Ежедневно в мире генерируются эксабайты данных. Анализировать такой поток информации вручную, а тем более извлекать полезные для бизнеса или науки данные, практически невозможно. Но технологии как и время не стоят на месте, появляются новые инструменты, наука двигает прогресс. Если вы хоть чуточку следите за новостями из мира высоких технологий, то фразы "биг дата", "машинное обучение", "глубокое обучение" вас не испугают. С приходом больших данных появились новые профессии и специализации такие как Data Scientist/Analyst (по-русски аналитик данных), Data Engineer. Задачи этих ребят тесно связаны с обработкой, анализом и хранением "нефти 21 века", т.е. информации. Но насколько эффективно они выполняются?

ETL

Аббревиатура ETL в последнее время часто мелькает в материалах, посвященных data-driven приложениям. Но не пугайтесь, это всего лишь набор из 3-х простых слов: Extract, Transform, Load. Ничего не напоминает? Тот, кто сталкивался с задачами по обработке данных не раз замечал паттерн в своих действиях, а именно:

  • сначала данные выгружаются (Extract) из какого-нибудь источника типа базы данных, внешнего сервиса (Facebook Ads, Google Analytics, Yandex Metrics) или, на худой конец, это могут быть логи вашего приложения (например, веб-сервера).

  • потом они преобразуются (Transform), скажем, необходимо сформировать сводную таблицу или провести сложный когортный анализ ваших пользователей.

  • и наконец загружаются (Load) для просмотра и дальнейшего анализа в базу данных или на какое-нибудь облако Amazon S3, не суть.

И как ни крути от этого не уйти. Чтобы данные проанализировать, их необходимо подготовить, иначе "мусор на входе — мусор на выходе". Процесс подготовки занимает львиную долю времени, отведенного на работу с данными. До 80% рабочего времени аналитик тратит на сбор и очистку. Поэтому от эффективности ETL-процесса зависит скорость и качество выполненной работы.

Перед тем как перейти к основной идеи этой статьи, я предлагаю кратко рассмотреть самый популярный на сегодняшний день метод построения ETL процесса в компании.

Внимание! Я запустил полноценный курс по разработке дата-пайплайнов на Luigi. Luigi сильно недооценён, и к нему стоит присмотреться поближе. В курсе я рассказываю зачем нужны пайплайны, как их сделать надёжными и отказоустойчивыми. Всё это заправляется практическими примерами! Не забыл я и про тему деплоя, где затрагивается Docker контейнеры, а также облачный деплой дата-пайпланой в AWS с использованием таких технологий как AWS Fargate, Cloud Map, Elastic Container Service и другое.

Более подробно о курсе по ссылке: Введение в Data Engineering: дата-пайплайны

Серые будни работы с данными

Сложно спорить с утверждением, что Python твердо занял позицию lingua franca в задачах по анализу данных. О его взрывной популярности свидетельствует и недавний пост от ребят из Stack Overflow. Но что же предшествует магическому процессу извлечения ценной информации (непосредственному анализу) из гигабайт структурированных и неструктурированных данных? Сбор. Задачи по анализу данных славятся своими жесткими сроками ведь на их основе часто принимают ключевые бизнес-решения. Это сильно отражается на том как мы, разработчики, подходим к процессу написания скриптов. Нам не стыдно создавать скрипты-однодневки с хрупким кодом и горой "разбитых окон". Хорошо ещё, если в этой массе кода есть хоть какая-нибудь структура или модульность для дальнейшего переиспользования в других скриптах, но обычно и этого нет. Краткосрочный выигрыш в скорости оборачивается головными болями в долгосрочной перспективе. Код обрастает "техническим долгом", и им становится сложно управлять. Ниже пример ETL-скрипта с соблюдением принципа Single Responsibility в каждой функции:

def extract_important_data():
    pass

def clean_important_data():
    pass

def transform_important_data():
    pass

def join_important_data_with_another_important_data():
    pass

def load_to_db():
    pass

if __name__ == "__main__":
    extract_important_data()
    clean_important_data()
    transform_important_data()
    join_important_data_with_another_important_data()
    load_to_db()

Всё бы хорошо, но у такого подхода есть ряд проблем:

  • отсутствие хорошего и централизованного обработчика ошибок
  • проблема при управлении зависимостями (между функциями/классами)
  • восстановление в точке остановки скрипта вследствии ошибки (например, получили 500 ошибку от одного из API сервисов)
  • удобная и быстрая работа с командной строкой (при необходимости передавать в скрипт аргументы)

Все они решаемы, но нужно ли изобретать ещё один, когда он давно изобретен и на нём успешно "катаются" специалисты в индустрии?!

Luigi

Luigi это один из немногих инструментов в экосистеме Python для построения т.н. pipeline’ов или, по-простому, выполнения пакетных задач (batch jobs). Разработан был инженерами из Spotify. Мне он понравился за свою простоту и широкий спектр возможностей, а именно:

  • управление зависимостями между задачами

  • failover recovery, т.е. если в одной из задач произошла ошибка, не нужно перезапускать цепочку снова

  • центральный планировщик задач с веб-интерфейсом, статусом выполнения задач и трекингом ошибок

  • “батарейки” для работы с HDFS, S3, MySQL, PostgreSQL, Redis, MongoDB, Redshift и т.д.

  • удобное построение CLI (Command Line Interface), в нём очень удобно построена передача параметров из командной строки

Основными строительными блоками Luigi являются 3 объекта: Task, Target и Parameter. Последний используется для взаимодействия с командной строкой и поэтому опционален.  Чтобы установить Luigi достаточно выполнить:

pip install luigi

Task

Класс Task это основной блок, где происходит выполнение конкретного таска. Чтобы определить свою собственную задачу, необходимо создать класс, унаследованный от Task, и реализовать несколько методов. Зачастую переопределять нужно только 3 метода: run(), output(), requires().

На сайте с документацией к Luigi есть хорошая иллюстрация что из себя представляет каждый метод и класс в целом:

Task.run

Здесь выполняется вся логика вашей будущей задачи, например, скачивание или парсинг данных с внешнего источника, запрос в базу данных для извлечения информации и т.д. Если задача объёмная, то лучше разбить её на функции и вызывать их внутри метода run(), это поможет избежать путанницы в будущем.

Task.requires

Помните я говорил об управлении зависимостями? В методе requires() необходимо их перечислить. Зависимостями выступают другие luigi.Task классы. Чуть позже я покажу реальный пример задачи с зависимостями.

Task.output

Этот метод должен возвращать 1 или более Target объектов. Target объектом может быть файл на диске, файл внутри HDFS, S3 или файл, лежащий на удалённом FTP сервере и т.д.. В Luigi уже встроено множество полезных Target классов, поэтому ситуация, когда вам понадобится создавать свой, маловероятна. Полный список доступных Target классов смотрите на сайте.

Task.input

Этот метод не нужно переопределять. Он выступает "оберткой" над Task.requires и возвращает Target объекты, полученные от выполнения задач, определенных в Task.requires. Таким образом строится граф зависимостей, когда одна задача зависит от результата выполнения другой. Продемонстрирую на примере кода:

import luigi

class A(luigi.Task):

    def output(self):
        return luigi.LocalTarget('result.txt')

    def run(self):
        with self.output().open('w') as f:
            f.write('Hello, Luigi!')

class B(luigi.Task):

    def requires(self):
        return A()

    def run(self):
        with self.input().open('r') as f:
            print(f.read())

if __name__ == '__main__':
    luigi.run()

Здесь таск B зависит от выполнения таска A, поэтому перед началом выполнения B выполнится A, результат которого вернётся при вызове метода B.input (объекта файла result.txt).

Target

Ранее я вкратце описал что из себя представляет объект Target и зачем он нужен. Здесь отмечу, что благодаря этому классу Luigi реализует механизм fault tolerance и свойство идемпотентности. Проще говоря, если ваш pipeline аварийно завершается где-то в середине выполнения задач, повторный запуск не приведёт к повторному запуску успешно завершившихся задач, выполнение начнется в месте аварийной остановки скрипта. Это достигается за счёт вызова метода exists() у Target класса.

Parameter

При создании ETL скриптов часто приходится писать код для работы с командной строкой, а именно уметь принимать и обрабатывать аргументы. Даже наличие в стандартной библиотеке Python модулей для работы с консолью не уменьшает количество boilerplate кода. Luigi решил эту проблему по-своему.

Чтобы принимать аргументы из командной строки достаточно присвоить переменной объект класса Parameter или его наследников на уровне класса.

class TaskA(luigi.Task):
    filename = luigi.Parameter()

    def output(self):
        return luigi.LocalTarget('{}.txt'.format(self.filename))

    def run(self):
        with self.output().open('w') as f:
            f.write('Hello, Luigi!')

Пример запуска такого скрипта:

python demo.py TaskA --filename helloworld --local-scheduler

Если в названии вашего параметра присутствует знак ‘_’, то в командной строке его необходимо заменить на ‘-’. То есть передача значения в переменную file_name из командной строки будет выглядеть как --file-name. Параметр --local-scheduler необходим для запуска Luigi без центрального планировщика, в режиме тестирования и разработки.

Luigid

Задача демона Luigi заключается в следующем:

  • Следить за выполнением задач, чтобы исключить ситуацию одновременного исполнения одного и того же таска

  • Визуализация работы скрипта: построение графа зависимостей, просмотр статусов у текущих задач, мониторинг ошибок

Ниже скриншот графа зависимостей внутри демона Luigi на примере простого скрипта о котором расскажу чуть ниже.

По умолчанию демон слушает 8082 порт и запускается командой:

luigid

Чтобы увидеть все доступные параметры запуска необходимо добавить --help.

Пример пайплайна

Человек лучше всего запоминает информацию на практических примерах, поэтому я придумал скрипт в задачу которого входит:

  • спарсить рейтинг фильмов по годам с сайта IMDB
  • сохранить результат каждого года в файл
  • объединить результаты всех лет в 1 файл и отсортировать фильмы по убыванию рейтинга и количества голосов

Вот как выглядит решение этой задачи в Luigi:

import csv

import luigi
from luigi.format import UTF8
import requests
import pandas as pd
from bs4 import BeautifulSoup

class AggregateMovieRatingTask(luigi.Task):
    years = luigi.ListParameter()

    def requires(self):
        return [GetMovieMetaDataTask(year) for year in self.years]

    def output(self):
        return luigi.LocalTarget('results.csv'.format(), format=UTF8)

    def run(self):
        data_frames = []

        for _input in self.input():
            with _input.open('r') as raw_file:
                data_frames.append(pd.read_csv(raw_file))

        df = pd.concat(data_frames)
        df = df.sort_values(['rating', 'votes'], ascending=[False, False])

        with self.output().open('w') as f:
            df[['title', 'rating', 'votes']].to_csv(f)

class GetMovieMetaDataTask(luigi.Task):
    year = luigi.Parameter()

    def get_movie_meta_data(self, film_div):
        title = film_div.h3.a.text
        rating = film_div.find('div', class_='ratings-imdb-rating')
        rating = rating.attrs['data-value'] if rating else 0
        votes = film_div.find('span', attrs={'name': 'nv'})
        votes = votes.attrs['data-value'] if votes else 0
        return {'title': title, 'rating': rating, 'votes': votes}

    def output(self):
        return luigi.LocalTarget('raw-{}.csv'.format(self.year), format=UTF8)

    def run(self):
        url = 'http://www.imdb.com/search/title?release_date={}'.format(self.year)
        response = requests.get(url, headers={
            'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.6,en;q=0.4'
        })
        response.raise_for_status()
        html = BeautifulSoup(response.text, 'html.parser')
        film_container = html.find_all('div', class_='lister-item mode-advanced')
        payload = [self.get_movie_meta_data(film) for film in film_container]

        with self.output().open('w') as csv_file:
            df = pd.DataFrame(payload)
            df.to_csv(csv_file)

if __name__ == '__main__':
    luigi.run()

Скачать скрипт можно по ссылке. Для корректной работы необходимо помимо luigi также установить requests, pandas и beautifulsoup4:

pip install requests, pandas, beautifulsoup4, luigi

Запускайте в терминале демон luigid, а сам скрипт вот таким образом:

python imdb_luigi_list_params.py AggregateMovieRatingTask --years [2013,2014,2015,2016,2017]

Отправной точкой будет класс AggregateMovieRatingTask которому передается список интересующих нас лет. В методе requires() определяется зависимость от GetMovieMetaDataTask, поэтому до тех пор пока не будет получен результат от GetMovieMetaDataTask, код в методе run() у класса AggregateMovieRatingTask не будет исполнен.

При удачном раскладе AggregateMovieRatingTask.input вернёт список, содержащий объекты LocalTarget, полученные от выполнения GetMovieMetaDataTaskпо каждому году. Дальше необходимо пробежаться по списку, сформировать DataFrame и отсортировать его по убыванию.

Полученных знаний достаточнот для построения сложных пайплайнов с зависимостями.

Ограничения Luigi

Как и у любого другого инструмента, у Luigi есть свои ограничения с которыми приходится мириться в зависимости от ситуаций.

  • Отсутствие механизма запуска задач по расписанию. Если такая потребность имеется, то можно использовать crontab.

  • Luigi не предназначен для real-time обработки, его стихия это batch processing.

  • Сложность масштабирования. Luigi не умеет распределять задачи между воркерами на разных узлах/нодах как это умеет делать Celery, используя единый брокер сообщений (например, Redis или RabbitMQ). Без серьёзного ручного вмешательства тут не обойтись.

Заключение

Моей главной задачей в статье было рассказать про основные возможности Luigi. Вероятно те из вас, кто до сих пор мучается с boilerplate кодом при написании ETL скриптов взглянут на свою работу иначе, и полученная информация сделает вашу работу эффективной и приятной. За более подробным описанием стоит сходить на сайт с документацией. Отмечу, что Luigi не единственный инструмент в своём роде, обратите внимание на продукт под названием Airflow, разработанный в стенах Airbnb и с недавних пор перешедший в "руки" Apache Foundation (на момент написания статьи проект находится в статусе "incubating").

Полезные ссылки

читать дальше

Обновляем подсистему Linux на Windows 10

Пару недель назад вышло обновление для ОС Windows 10 под кодовым названием Creators Update. Помимо ежегодных плюшек в пользовательском интерфейсе и улучшений в производительности, с этим обновлением также "прилетел" апдейт для подсистемы Linux внутри Windows 10. Год назад я уже писал о том как установить Ubuntu в Windows 10. На момент прошлой заметки, в Windows 10 была возможность включить полноценный дистрибутив Linux — Ubuntu версии 14.04 LTS. Всё бы хорошо, но 14.04 вышла 3 года назад, пора бы идти в ногу со временем и обновиться до более свежей версии (учитывая, что в 14.04 стоит Python аж версии 2.7.6). С приходом подсистемы Linux, таким разработчикам как я (любителям Windows) можно забыть про использование инструментов вроде Vagrant для унификации систем развертывания и разработки.

Итак, как же обновить Linux? Если у вас ещё не установлена подсистема Linux, но при этом обновление Creators Update уже стоит, то следуйте инструкциям из моей прошлогодней статьи, с одним лишь замечанием, что теперь Программы и компоненты спрятаны в раздел Приложения и возможности.

читать дальше

Введение в pandas: анализ данных на Python

Python pandas

pandas это высокоуровневая Python библиотека для анализа данных. Почему я её называю высокоуровневой, потому что построена она поверх более низкоуровневой библиотеки NumPy (написана на Си), что является большим плюсом в производительности. В экосистеме Python, pandas является наиболее продвинутой и быстроразвивающейся библиотекой для обработки и анализа данных. В своей работе мне приходится пользоваться ею практически каждый день, поэтому я пишу эту краткую заметку для того, чтобы в будущем ссылаться к ней, если вдруг что-то забуду. Также надеюсь, что читателям блога заметка поможет в решении их собственных задач с помощью pandas, и послужит небольшим введением в возможности этой библиотеки.

DataFrame и Series

Чтобы эффективно работать с pandas, необходимо освоить самые главные структуры данных библиотеки: DataFrame и Series. Без понимания что они из себя представляют, невозможно в дальнейшем проводить качественный анализ.

Series

Структура/объект Series представляет из себя объект, похожий на одномерный массив (питоновский список, например), но отличительной его чертой является наличие ассоциированных меток, т.н. индексов, вдоль каждого элемента из списка. Такая особенность превращает его в ассоциативный массив или словарь в Python.

>>> import pandas as pd
>>> my_series = pd.Series([5, 6, 7, 8, 9, 10])
>>> my_series
0     5
1     6
2     7
3     8
4     9
5    10
dtype: int64
>>> 

В строковом представлении объекта Series, индекс находится слева, а сам элемент справа. Если индекс явно не задан, то pandas автоматически создаёт RangeIndex от 0 до N-1, где N общее количество элементов. Также стоит обратить, что у Series есть тип хранимых элементов, в нашем случае это int64, т.к. мы передали целочисленные значения.

У объекта Series есть атрибуты через которые можно получить список элементов и индексы, это values и index соответственно.

>>> my_series.index
RangeIndex(start=0, stop=6, step=1)
>>> my_series.values
array([ 5,  6,  7,  8,  9, 10], dtype=int64) 

Доступ к элементам объекта Series возможны по их индексу (вспоминается аналогия со словарем и доступом по ключу).

>>> my_series[4]
9

Индексы можно задавать явно:

>>> my_series2 = pd.Series([5, 6, 7, 8, 9, 10], index=['a', 'b', 'c', 'd', 'e', 'f'])
>>> my_series2['f']
10

Делать выборку по нескольким индексам и осуществлять групповое присваивание:

>>> my_series2[['a', 'b', 'f']]
a     5
b     6
f    10
dtype: int64
>>> my_series2[['a', 'b', 'f']] = 0
>>> my_series2
a    0
b    0
c    7
d    8
e    9
f    0
dtype: int64

Фильтровать Series как душе заблагорассудится, а также применять математические операции и многое другое:

>>> my_series2[my_series2 > 0]
c    7
d    8
e    9
dtype: int64

>>> my_series2[my_series2 > 0] * 2
c    14
d    16
e    18
dtype: int64

Если Series напоминает нам словарь, где ключом является индекс, а значением сам элемент, то можно сделать так:

>>> my_series3 = pd.Series({'a': 5, 'b': 6, 'c': 7, 'd': 8})
>>> my_series3
a    5
b    6
c    7
d    8
dtype: int64
>>> 'd' in my_series3
True

У объекта Series и его индекса есть атрибут name, задающий имя объекту и индексу соответственно.

>>> my_series3.name = 'numbers'
>>> my_series3.index.name = 'letters'
>>> my_series3
letters
a    5
b    6
c    7
d    8
Name: numbers, dtype: int64

Индекс можно поменять "на лету", присвоив список атрибуту index объекта Series

>>> my_series3.index = ['A', 'B', 'C', 'D']
>>> my_series3
A    5
B    6
C    7
D    8
Name: numbers, dtype: int64

Имейте в виду, что список с индексами по длине должен совпадать с количеством элементов в Series.

DataFrame

Объект DataFrame лучше всего представлять себе в виде обычной таблицы и это правильно, ведь DataFrame является табличной структурой данных. В любой таблице всегда присутствуют строки и столбцы. Столбцами в объекте DataFrame выступают объекты Series, строки которых являются их непосредственными элементами.

DataFrame проще всего сконструировать на примере питоновского словаря:

>>> df = pd.DataFrame({
...     'country': ['Kazakhstan', 'Russia', 'Belarus', 'Ukraine'],
...     'population': [17.04, 143.5, 9.5, 45.5],
...     'square': [2724902, 17125191, 207600, 603628]
... })
>>> df
   country  population    square
0  Kazakhstan       17.04   2724902
1      Russia      143.50  17125191
2     Belarus        9.50    207600
3     Ukraine       45.50    603628

Чтобы убедиться, что столбец в DataFrame это Series, извлекаем любой:


>>> df['country']
0    Kazakhstan
1        Russia
2       Belarus
3       Ukraine
Name: country, dtype: object
>>> type(df['country'])
<class 'pandas.core.series.Series'>

Объект DataFrame имеет 2 индекса: по строкам и по столбцам. Если индекс по строкам явно не задан (например, колонка по которой нужно их строить), то pandas задаёт целочисленный индекс RangeIndex от 0 до N-1, где N это количество строк в таблице.

>>> df.columns
Index([u'country', u'population', u'square'], dtype='object')
>>> df.index
RangeIndex(start=0, stop=4, step=1)

В таблице у нас 4 элемента от 0 до 3. 

Доступ по индексу в DataFrame

Индекс по строкам можно задать разными способами, например, при формировании самого объекта DataFrame или "на лету":

>>> df = pd.DataFrame({
...     'country': ['Kazakhstan', 'Russia', 'Belarus', 'Ukraine'],
...     'population': [17.04, 143.5, 9.5, 45.5],
...     'square': [2724902, 17125191, 207600, 603628]
... }, index=['KZ', 'RU', 'BY', 'UA'])
>>> df
       country  population    square
KZ  Kazakhstan       17.04   2724902
RU      Russia      143.50  17125191
BY     Belarus        9.50    207600
UA     Ukraine       45.50    603628
>>> df.index = ['KZ', 'RU', 'BY', 'UA']
>>> df.index.name = 'Country Code'
>>> df
                 country  population    square
Country Code                                  
KZ            Kazakhstan       17.04   2724902
RU                Russia      143.50  17125191
BY               Belarus        9.50    207600
UA               Ukraine       45.50    603628

Как видно, индексу было задано имя - Country Code. Отмечу, что объекты Series из DataFrame будут иметь те же индексы, что и объект DataFrame:

>>> df['country']
Country Code
KZ    Kazakhstan
RU        Russia
BY       Belarus
UA       Ukraine
Name: country, dtype: object

Доступ к строкам по индексу возможен несколькими способами:

  • .loc - используется для доступа по строковой метке
  • .iloc - используется для доступа по числовому значению (начиная от 0)
>>> df.loc['KZ']
country       Kazakhstan
population         17.04
square           2724902
Name: KZ, dtype: object

>>> df.iloc[0]
country       Kazakhstan
population         17.04
square           2724902
Name: KZ, dtype: object

Можно делать выборку по индексу и интересующим колонкам:

>>> df.loc[['KZ', 'RU'], 'population']
Country Code
KZ     17.04
RU    143.50
Name: population, dtype: float64

Как можно заметить, .loc в квадратных скобках принимает 2 аргумента: интересующий индекс, в том числе поддерживается слайсинг и колонки.

>>> df.loc['KZ':'BY', :]
                 country  population    square
Country Code                                  
KZ            Kazakhstan       17.04   2724902
RU                Russia      143.50  17125191
BY               Belarus        9.50    207600

Фильтровать DataFrame с помощью т.н. булевых массивов:

>>> df[df.population > 10][['country', 'square']]
                 country    square
Country Code                      
KZ            Kazakhstan   2724902
RU                Russia  17125191
UA               Ukraine    603628

Кстати, к столбцам можно обращаться, используя атрибут или нотацию словарей Python, т.е. df.population и df['population'] это одно и то же.

Сбросить индексы можно вот так:

>>> df.reset_index()
  Country Code     country  population    square
0           KZ  Kazakhstan       17.04   2724902
1           RU      Russia      143.50  17125191
2           BY     Belarus        9.50    207600
3           UA     Ukraine       45.50    603628

pandas при операциях над DataFrame, возвращает новый объект DataFrame.

Добавим новый столбец, в котором население (в миллионах) поделим на площадь страны, получив тем самым плотность:

>>> df['density'] = df['population'] / df['square'] * 1000000
>>> df
                 country  population    square    density
Country Code                                             
KZ            Kazakhstan       17.04   2724902   6.253436
RU                Russia      143.50  17125191   8.379469
BY               Belarus        9.50    207600  45.761079
UA               Ukraine       45.50    603628  75.377550

Не нравится новый столбец? Не проблема, удалим его:

>>> df.drop(['density'], axis='columns')
                 country  population    square
Country Code                                  
KZ            Kazakhstan       17.04   2724902
RU                Russia      143.50  17125191
BY               Belarus        9.50    207600
UA               Ukraine       45.50    603628

Особо ленивые могут просто написать del df['density'].

Переименовывать столбцы нужно через метод rename:


>>> df = df.rename(columns={'Country Code': 'country_code'})
>>> df
  country_code     country  population    square
0           KZ  Kazakhstan       17.04   2724902
1           RU      Russia      143.50  17125191
2           BY     Belarus        9.50    207600
3           UA     Ukraine       45.50    603628

В этом примере перед тем как переименовать столбец Country Code, убедитесь, что с него сброшен индекс, иначе не будет никакого эффекта.

Чтение и запись данных

pandas поддерживает все самые популярные форматы хранения данных: csv, excel, sql, буфер обмена, html и многое другое:

Чаще всего приходится работать с csv-файлами. Например, чтобы сохранить наш DataFrame со странами, достаточно написать:

>>> df.to_csv('filename.csv')

Функции to_csv ещё передаются различные аргументы (например, символ разделителя между колонками) о которых подробнее можно узнать в официальной документации.

Считать данные из csv-файла и превратить в DataFrame можно функцией read_csv.

>>> df = pd.read_csv('filename.csv', sep=',')

Аргумент sep указывает разделитесь столбцов. Существует ещё масса способов сформировать DataFrame из различных источников, но наиболее часто используют CSV, Excel и SQL. Например, с помощью функции read_sql, pandas может выполнить SQL запрос и на основе ответа от базы данных сформировать необходимый DataFrame. За более подробной информацией стоит обратиться к официальной документации.

Группировка и агрегирование в pandas

Группировка данных один из самых часто используемых методов при анализе данных. В pandas за группировку отвечает метод .groupby. Я долго думал какой пример будет наиболее наглядным, чтобы продемонстрировать группировку, решил взять стандартный набор данных (dataset), использующийся во всех курсах про анализ данных — данные о пассажирах Титаника. Скачать CSV файл можно тут.

>>> titanic_df = pd.read_csv('titanic.csv')
>>> print(titanic_df.head())
   PassengerID                                           Name PClass    Age  \
0            1                   Allen, Miss Elisabeth Walton    1st  29.00   
1            2                    Allison, Miss Helen Loraine    1st   2.00   
2            3            Allison, Mr Hudson Joshua Creighton    1st  30.00   
3            4  Allison, Mrs Hudson JC (Bessie Waldo Daniels)    1st  25.00   
4            5                  Allison, Master Hudson Trevor    1st   0.92   
      Sex  Survived  SexCode  
0  female         1        1  
1  female         0        1  
2    male         0        0  
3  female         0        1  
4    male         1        0  

Необходимо подсчитать, сколько женщин и мужчин выжило, а сколько нет. В этом нам поможет метод .groupby.

>>> print(titanic_df.groupby(['Sex', 'Survived'])['PassengerID'].count())
Sex     Survived
female  0           154
        1           308
male    0           709
        1           142
Name: PassengerID, dtype: int64

А теперь проанализируем в разрезе класса кабины:

>>> print(titanic_df.groupby(['PClass', 'Survived'])['PassengerID'].count())
PClass  Survived
*       0             1
1st     0           129
        1           193
2nd     0           160
        1           119
3rd     0           573
        1           138
Name: PassengerID, dtype: int64

Сводные таблицы в pandas

Термин "сводная таблица" хорошо известен тем, кто не по наслышке знаком с инструментом Microsoft Excel или любым иным, предназначенным для обработки и анализа данных. В pandas сводные таблицы строятся через метод .pivot_table. За основу возьмём всё тот же пример с Титаником. Например, перед нами стоит задача посчитать сколько всего женщин и мужчин было в конкретном классе корабля:

>>> titanic_df = pd.read_csv('titanic.csv')
>>> pvt = titanic_df.pivot_table(index=['Sex'], columns=['PClass'], values='Name', aggfunc='count')

В качестве индекса теперь у нас будет пол человека, колонками станут значения из PClass, функцией агрегирования будет count (подсчёт количества записей) по колонке Name.

>>> print(pvt.loc['female', ['1st', '2nd', '3rd']])
PClass
1st    143.0
2nd    107.0
3rd    212.0
Name: female, dtype: float64

Всё очень просто.

Анализ временных рядов

В pandas очень удобно анализировать временные ряды. В качестве показательного примера я буду использовать цену на акции корпорации Apple за 5 лет по дням. Файл с данными можно скачать тут.

>>> import pandas as pd
>>> df = pd.read_csv('apple.csv', index_col='Date', parse_dates=True)
>>> df = df.sort_index()
>>> print(df.info())
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 1258 entries, 2017-02-22 to 2012-02-23
Data columns (total 6 columns):
Open         1258 non-null float64
High         1258 non-null float64
Low          1258 non-null float64
Close        1258 non-null float64
Volume       1258 non-null int64
Adj Close    1258 non-null float64
dtypes: float64(5), int64(1)
memory usage: 68.8 KB

Здесь мы формируем DataFrame с DatetimeIndex по колонке Date и сортируем новый индекс в правильном порядке для работы с выборками. Если колонка имеет формат даты и времени отличный от ISO8601, то для правильного перевода строки в нужный тип, можно использовать метод pandas.to_datetime.

Давайте теперь узнаем среднюю цену акции (mean) на закрытии (Close):

>>> df.loc['2012-Feb', 'Close'].mean()
528.4820021999999

А если взять промежуток с февраля 2012 по февраль 2015 и посчитать среднее:

>>> df.loc['2012-Feb':'2015-Feb', 'Close'].mean()
430.43968317018414

А что если нам нужно узнать среднюю цену закрытия по неделям?!

>>> df.resample('W')['Close'].mean()
Date
2012-02-26    519.399979
2012-03-04    538.652008
2012-03-11    536.254004
2012-03-18    576.161993
2012-03-25    600.990001
2012-04-01    609.698003
2012-04-08    626.484993
2012-04-15    623.773999
2012-04-22    591.718002
2012-04-29    590.536005
2012-05-06    579.831995
2012-05-13    568.814001
2012-05-20    543.593996
2012-05-27    563.283995
2012-06-03    572.539994
2012-06-10    570.124002
2012-06-17    573.029991
2012-06-24    583.739993
2012-07-01    574.070004
2012-07-08    601.937489
2012-07-15    606.080008
2012-07-22    607.746011
2012-07-29    587.951999
2012-08-05    607.217999
2012-08-12    621.150003
2012-08-19    635.394003
2012-08-26    663.185999
2012-09-02    670.611995
2012-09-09    675.477503
2012-09-16    673.476007
                 ...    
2016-08-07    105.934003
2016-08-14    108.258000
2016-08-21    109.304001
2016-08-28    107.980000
2016-09-04    106.676001
2016-09-11    106.177498
2016-09-18    111.129999
2016-09-25    113.606001
2016-10-02    113.029999
2016-10-09    113.303999
2016-10-16    116.860000
2016-10-23    117.160001
2016-10-30    115.938000
2016-11-06    111.057999
2016-11-13    109.714000
2016-11-20    108.563999
2016-11-27    111.637503
2016-12-04    110.587999
2016-12-11    111.231999
2016-12-18    115.094002
2016-12-25    116.691998
2017-01-01    116.642502
2017-01-08    116.672501
2017-01-15    119.228000
2017-01-22    119.942499
2017-01-29    121.164000
2017-02-05    125.867999
2017-02-12    131.679996
2017-02-19    134.978000
2017-02-26    136.904999
Freq: W-SUN, Name: Close, dtype: float64

Resampling мощный инструмент при работе с временными рядами (time series), помогающий переформировать выборку так, как удобно вам. Метод resample первым аргументом принимает строку rule. Все доступные значения можно найти в документации.

Визуализация данных в pandas

Для визуального анализа данных, pandas использует библиотеку matplotlib. Продемонстрирую простейший способ визуализации в pandas на примере с акциями Apple.

Берём цену закрытия в промежутке между 2012 и 2017.

>>> import matplotlib.pyplot as plt
>>> new_sample_df = df.loc['2012-Feb':'2017-Feb', ['Close']]
>>> new_sample_df.plot()
>>> plt.show()

И видим вот такую картину:

По оси X, если не задано явно, всегда будет индекс. По оси Y в нашем случае цена закрытия. Если внимательно посмотреть, то в 2014 году цена на акцию резко упала, это событие было связано с тем, что Apple проводила сплит 7 к 1. Так мало кода и уже более-менее наглядный анализ ;)

Эта заметка демонстрирует лишь малую часть возможностей pandas. Со своей стороны я постараюсь по мере своих сил обновлять и дополнять её.

Полезные ссылки

читать дальше

Используем KVM для создания виртуальных машин на сервере

Эту заметку я пишу для того, чтобы продемонстрировать пошаговую установку и настройку виртуальной машины в Linux на базе KVM. Ранее я уже писал про виртуализацию, где использовал замечательный инструмент Vagrant.

Сейчас передо мной встал вопрос аренды хорошего сервера с большим объёмом оперативной памяти и объёмным жестким диском. Но запускать проекты прямо на хост-машине не хочется, поэтому буду разграничивать их по отдельным небольшим виртуальным серверам с ОС Linux или docker-контейнерам (о них расскажу в другой статье).

Все современные облачные хостинги работают по такому же принципу, т.е. хостер на хорошем железе поднимает кучу виртуальных серверов, которые мы привыкли называть VPS/VDS, и раздаёт их пользователям, либо автоматизирует этот процесс (привет, DigitalOcean).читать дальше

Блог на английском языке

Английская версия блога

Начал потихоньку исполнять свой план на 2017 год с перевода статьи про работу с Celery на английский язык - Celery Best Practices: practical approach. В планах у меня начать английскую версию блога с перевода топа самых популярных статей, будут появляться по мере сил.

В последнее время стал замечать, что мой блог регулярно стали посещать пользователи из США и других англоговорящих стран, мне пока непонятно откуда они на него переходят (если кто знает как посмотреть отчёт в разрезе страна-источник в Я.Метрике, напишите, пожалуйста), но тенденция радует. Сейчас, конечно,  процент отказов среди таких пользователей высокий, но постараюсь его снизить путем интересного контента.

Сегодня английский язык это не только lingua franca для коммуникации между людьми с разных стран, но и язык науки. Язык на котором в первую очередь появляется самая актуальная информация практически во всех сферах нашей жизни, проще говоря, больше всего уникального контента создаётся именно на английском. Попробую внести вклад и в эту часть Интернета, думаю это того стоит. Помимо очевидных плюсов в дополнительной аудитории и новых знакомствах, это ещё и неплохая возможность развивать дальше навык письма на неродном тебе языке.

На английскую версию блога можно перейти из верхнего меню либо по ссылке - English version.

читать дальше

Блогу исполнился 1 год!

Ровно 1 год назад появился самый первый пост в этом блоге. Удивительно, но я всё таки не забросил его. Прошлый опыт говорил об обратном. Что произошло за это время?

За этот год:

  • Я написал 30 постов, включая этот. Изначально я планировал чаще писать, но ввиду некоторых обстоятельств и лени этого добиться не удалось.
  • На момент написания этого поста блог посетило 41 123 уникальных посетителя. Хороший показатель для узкоспециализированного блога с небольшим количеством контента.
  • Ежедневная аудитория увеличилась с 0 до 350 уникальных посетителей.

  • Более 60% аудитории проживает в России

Топ-5 самых популярных постов

Суммарно эти 5 статей принесли блогу 57% всех уникальных посетителей. Помните правило 80/20 ? :)

Более 85% пользователей читают статьи, используя ПК, 11% заходят через смартфон и только 3% через планшет. Это логично, так как программисты в своей работе привыкли пользоваться компьютером :)

А вот статистика по операционным системам используемых устройств:

Windows рулит :)

А вот стата по браузерам среди разработчиков:

Безоговорочный лидер Google Chrome.

Благодаря блогу в моём Telegram канале для разработчиков уже 252 подписчика! Если вы до сих пор не подписаны, то смело делайте это.

Планы на следующий год

Как ни странно, но блог я начал вести накануне Нового Года, а это время для постановки новых целей, выполнения новых задач и воплощения идей в реальность. Не буду нарушать традицию и прикину примерные цели, намеченные на 2017 год:

  • Создать английскую версию блога. В англоязычном сегменте Интернета куда больше трафика и людей, ищущих статьи на те или иные темы. Также это неплохая возможность и дальше практиковать письменный английский, знакомиться с новыми и интересными людьми.
  • Чаще писать статьи на актуальные темы для разработчиков. Тут у меня уклон больше в сторону Python. Сейчас в работе я применяю инструменты анализа данных и машинного обучения, поэтому хотелось бы раскрыть эти темы более подробно. В планах также углубление в сторону сложных интерфейсов в веб-приложениях, об этом я уже писал в заметке про progressive web apps. Серия статей на эту тему не заставит себя долго ждать :)
  • Больше контента, а значит и больше трафика. Цель на 2017 год - 2 000 уникальных посетителей в сутки.
читать дальше

Теория разбитых окон в программировании

Теория разбитых окон в программировании

В криминалистике существует интересная теория под названием "Теория разбитых окон" (ТРО). Суть её в том, что разбитое окно, при несвоевременной замене, влечёт за собой целую серию разбитых окон. Более того, серия разбитых окон может быть индикатором повышающегося уровня преступности в заданном регионе. На ум сразу приходит известная всем фраза "Чисто не там, где убирают, а там где не сорят". Согласно этой теории, чисто именно там, где убирают, стимулируя тем самым людей не сорить в будущем. Стоит заметить, что это применимо не только к окнам :) Автолюбителям наверняка знакома эта теория на дорогах, хотя они могут и не догадываться о её существовании. Я не раз замечал большое скопление автомобилей, припаркованных под знаком "Остановка запрещена". Стоит лишь одному остановиться под ним, остальные водители не заставят себя долго ждать. Удивительно как мало внимания дорожная полиция уделяет этому факту. Правонарушения должны своевременно пресекаться.

Но вернёмся всё же в мир разработки программного обеспечения. Удивительно, но и здесь ТРО находит свой отклик. Современный процесс создания ПО находится под жестким прессингом сроков. Бизнесу очень важно как можно раньше поставить продукт на рынок по ряду причин. Отсюда рождаются различные методологии управления вроде Agile, Lean, формируются концепции MVP (Minimum Viable Product). Как следствие, страдает качество кода, он начинает "протухать". С "вонючим кодом" можно жить, более того, практически всегда он есть в той или иной степени, это нормально. Но его нарастающая доля служит одним из первых индикаторов того, что пора "бить во все колокола". Почему? Основываясь на собственном опыте скажу, что программист охотнее "говнокодит" там, где этого "говнокода" предостаточно. И наоборот, человек несколько раз подумает, прежде чем отправлять свой шедевр на код ревью, если в проекте стараются соблюдать чистоту кода. Помимо прочего, "разбитое окно" в коде создаёт ощущение наплевательского отношения к проекту, тем самым порождая чувство безразличности к нему. Зачем пытаться что-то изменить, если всем наплевать?

Чините "разбитые окна" в коде как можно чаще.

Вам может быть интересно почитать также:

читать дальше