Функциональное программирование и c++ на практике

Функциональное программирование (далее ФП) нынче в моде. Статьи, книги, блоги. На каждой конференции обязательно присутствуют выступления, где люди рассказывают о красоте и удобстве функционального подхода. Я долгое время смотрел в его сторону издалека, но пришла пора попробовать применить его на практике. Прочитав достаточное количество теории и сложив в голове общую картину я решил написать небольшое приложение в функциональном стиле. Так как в данный момент я c++ программист, то буду использовать этот замечательный язык. За основу я возьму код из моей предыдущей статьи, т.е. моим примером будет упрощенная 2Д симуляция физических тел.

Заявление

Я ни в коем случае ни эксперт. Моей целью была попытка понять ФП и область его применения. В статье я шаг за шагом опишу как я превращал ООП код в некое подобие функционального с использованием c++. Из функциональных языков программирования я имел опыт только с Erlang. Другими словами, здесь я опишу процесс своего обучения — возможно, кому-то это поможет. И конечно же я приветствую конструктивную критику и замечания. Даже настаиваю, чтобы вы оставляли комментарии — что я делал неправильно, что можно улучшить.

Введение

В статье я не буду рассказывать теорию ФП — в сети великое множество материала, в том числе и на хабре. Хоть я и старался приблизить программу к чистому ФП, на 100% мне это сделать не удалось. В некоторых случаях из-за нецелесообразности, в некоторых — из-за отсутствия опыта. Так, например, рендер выполнен в привычном ООП стиле. Почему? Потому что одним из принципов ФП является неизменность данных (immutable data) и отсутствие состояния. Но для DirectX (API, который я использую) необходимо хранить буферы, текстуры, устройства. Конечно возможно создавать все заново каждый фрейм, но это будет чертовски долго (о производительности мы поговорим в конце статьи). Еще пример — в ФП часто применяются ленивые вычисления (lazy evaluation). Но я не нашел в программе места для их использования, поэтому их вы не встретите.

Main

Исходный код находится в этом коммите.

Все начинается в функции main() — здесь мы создаем фигуры (struct Shape) и запускаем бесконечный цикл, где будем обновлять симуляцию. Сразу стоит обратить внимание на оформление кода — я пишу функцию в отдельном cpp файле и в месте использования объявляю ее как extern — таким образом мне не нужно создавать отдельный заголовочный файл или даже отдельный тип, что положительно сказывается на времени компиляции и в целом делает код более читабельным: одна функция — один файл.

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

Update Simulation

Это сердце нашей программы и именно та часть, к которой применялось ФП. Сигнатура выглядит следующим образом:

vector<Shape> updateSimulation(float const dt, vector<Shape> const shapes, float const width, float const height);

Мы принимаем копию исходных данных и возвращаем новый вектор с измененными данными. Но почему копию, а не константную ссылку? Ведь выше я писал, что одним из принципов ФП является неизменность данных и const reference гарантирует это. Это так, но следующим важнейшим принципом является чистота функций (pure function) — т.е. отсутствие побочных эффектов и гарантия того, что функция будет возвращать одинаковые значения при одинаковых входных данных. Но, получая ссылку, мы этого гарантировать не можем. Разберем на примере. Допустим мы имеем некоторую функцию, принимающую константную ссылку:

int foo(vector<int> const & data) {     return accumulate(data.begin(), data.end(), 0); }

И вызываем так:

vector<int> data{1, 1, 1}; int result{foo(data)};

Хотя foo() и принимает const &, сами данные не являются константными, а это значит, что они могут быть изменены до и в момент вызова accumulate(), например, другим потоком. Именно поэтому все данные должны приходить в виде копии.

Кроме того, чтобы поддержать принцип неизменности данных все поля всех пользовательских типов должны быть константами. Так выглядит, например, класс вектора:

struct Vec2 { 	float const x; 	float const y;  	Vec2(float const x = 0.0f, float const y = 0.0f) : x{ x }, y{ y } 	{}  	// member functions }

Как видите, состояние задается при создании объекта и не меняется никогда! Т.е. даже состоянием назвать это можно с натяжкой — просто набор данных.

Вернемся к нашей функции updateSimulation(). Вызывается она следующим образом:

shapes = updateSimulation(dtStep, move(shapes), wSize.x, wSize.y);

Здесь используется семантика перемещения (std::move()) — это позволяет избавиться от лишних копий. В нашем случае, однако, это не имеет никакого эффекта, т.к. мы оперируем примитивными типами и перемещение эквивалентно копированию.

Есть еще интересный момент — наша функция возвращает новый набор набор данных, который мы присваиваем старой переменной shapes, что, по сути, является нарушением принципа отсутствия состояния. Однако я считаю, что локальную переменную мы можем менять безбоязненно — на результат функции это не повлияет, т.к. это изменение остается инкапсулированным внутри этой функции.

Тело функции выглядит так:

updateSimulation

vector<Shape> updateSimulation(float const dt, vector<Shape> const shapes, float const width, float const height) { 	// step 1 - update calculate current positions 	vector<Shape> const updatedShapes1{ calculatePositionsAndBounds(dt, move(shapes)) };  	// step 2 - for each shape calculate cells it fits in 	uint32_t rows; 	uint32_t columns; 	tie(rows, columns) = getNumberOfCells(width, height); // auto [rows, columns] = getNumberOfCells(width, height); - c++17 structured bindings - not supported in vs2017 at the moment of writing  	vector<Shape> const updatedShapes2{ calculateCellsRanges(width, height, rows, columns, move(updatedShapes1)) };  	// step 3 - put shapes in corresponding cells 	vector<vector<Shape>> const cellsWithShapes{ fillGrid(width, height, rows, columns, updatedShapes2) };  	// step 4 - calculate collisions 	vector<VelocityAfterImpact> const velocityAfterImpact{ solveCollisions(move(cellsWithShapes), columns) };  	// step 5- apply velocities 	vector<Shape> const updatedShapes3{ applyVelocities(move(updatedShapes2), velocityAfterImpact) };  	return updatedShapes3; }

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

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

Сalculate Positions And Bounds

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

calculatePositionsAndBounds

vector<Shape> calculatePositionsAndBounds(float const dt, vector<Shape> const shapes) { 	vector<Shape> updatedShapes; 	updatedShapes.reserve(shapes.size());  	for_each(shapes.begin(), shapes.end(), [dt, &updatedShapes](Shape const shape) 	{ 		Shape const newShape{ shape.id, shape.vertices, calculatePosition(shape, dt), shape.velocity, shape.bounds, shape.cellsRange, shape.color, shape.massInverse }; 		updatedShapes.emplace_back(newShape.id, newShape.vertices, newShape.position, newShape.velocity, calculateBounds(newShape), newShape.cellsRange, newShape.color, newShape.massInverse); 	});  	return updatedShapes; }

Стандартная библиотека уже много лет поддерживает ФП. Алгоритм for_each — это функция высшего порядка, т.е. функция, принимающая другие функции. Вообще stl очень богат на алгоритмы, поэтому знание библиотеки очень важно, если вы пишите в функциональном стиле.

В приведенном коде есть пара интересных моментов. Первый — это ссылка на вектор в списке захвата лямбды. Да, я старался обойтись без ссылок совсем, но в данном месте это просто необходимо. И, как я писал выше, это не должно нарушать принципов, т.к. ссылка берется на локальный вектор, т.е. закрытый для внешнего мира. Здесь можно было бы обойтись без нее, используя цикл for, но я пошел в сторону наглядности и читабельности.

Второй момент связан с самим циклом. Опять же, так как не должно быть состояний, то и циклов быть не должно — ведь счетчик цикла и есть состояние. В чистом ФП циклов нет, их заменяет рекурсия. Давайте попробуем переписать функцию с ее использованием:

calculatePositionsAndBounds

vector<Shape> updateOne(float const dt, vector<Shape> shapes, vector<Shape> updatedShapes) { 	if (shapes.size() > 0) 	{ 		Shape shape{ shapes.back() }; 		shapes.pop_back();  		Shape const newShape{ shape.id, shape.vertices, calculatePosition(shape, dt), shape.velocity, shape.bounds, shape.cellsRange, shape.color, shape.massInverse }; 		updatedShapes.emplace_back(newShape.id, newShape.vertices, newShape.position, newShape.velocity, calculateBounds(newShape), newShape.cellsRange, newShape.color, newShape.massInverse); 	} 	else 	{ 		return updatedShapes; 	}  	return updateOne(dt, move(shapes), move(updatedShapes)); }  vector<Shape> calculatePositionsAndBounds(float const dt, vector<Shape> const shapes) { 	return updateOne(dt, move(shapes), {}); }

Мы избавились от ссылок! Но вместо одной функции получилось две. И, что самое главное, ухудшилась читабельность (по крайней мере, для меня — человека выросшего на традиционном ООП). Интересный момент — здесь используется так называемая хвостовая рекурсия (tail recursion) — в теории в этом случае стэк должен очищаться. Однако, я не нашел в стандарте c++ записей о таком поведении, поэтому я не могу гарантировать отсутствие переполнения стэка (stack overflow). Учитывая все вышесказанное я решил остановиться на циклах и больше рекурсию в этой статье вы не увидите.

Calculate Cells Ranges и Fill Grid

Для ускорения расчетов я использую 2Д сетку, разделенную на ячейки. Находясь в этой сетке, объект может занимать несколько ячеек, как показано на картинке:

image

Функция calculateCellsRanges() рассчитывает ячейки, занимаемые фигурой, и возвращает измененные данные.

В функции fillGrid() мы наполняем каждую ячейку (в нашем примере ячейка — это просто std::vector) соответствующими фигурами. Т.е. если ячейка не содержит ничего, вернется пустой вектор. Позже в коде мы будем пробегать по каждой ячейке, и проверять внутри нее каждую фигуру с каждой другой на пересечение. Но на рисунке можно увидеть, что фигура a и фигура b находятся (помимо других ячеек) как в ячейке 2, так и в ячейке 5. Это значит, что проверка будет осуществляться дважды. Поэтому мы добавим логику, которая скажет — нужна ли проверка. Зная строки и столбцы сделать это тривиально.

Solve Collisions

В моей предыдущей статье я использовал следующую технику — если оказалось, что объекты накладывались, мы отодвигали их друг от друга.

image

Т.е. мы делали так, чтобы объекты a и b переставали соприкасаться. Это добавляло много сложностей — нужно было заново рассчитывать bounding box каждый раз когда мы двигали объект. Чтобы избежать многократной перестановки мы ввели специальный аккумулятор, в который складывали все перестановки и позднее применяли этот аккумулятор только один раз. Так или иначе, пришлось ввести мьютексы для синхронизации, код был сложен и в таком виде не годился для функционального подхода. В новой попытке мы не будем перемещать объекты совсем, кроме того, мы будем производить рассчеты, только если они действительно необходимы. На картинке, например, рассчеты не нужны, т.к. фигура b движется быстрее фигуры a, т.е. они отдаляются друг от друга, и рано или поздно они перестанут соприкасаться без нашего участия. Конечно же это физически неправдоподобно, но если скорости невелики и/или используется маленький шаг симуляции, то выглядит вполне нормально. Если же рассчеты нужны, мы считаем изменения в скоростях, которые произошли при столкновении и возвращаем эти скорости вместе с идентификатором фигуры.

Apply Velocities

Имея на руках изменения скоростей, функция applyVelocities() просто суммирует их и применяет к объекту. Снова о правдоподобности речь не идет и, вполне возможно, при некоторых условиях появятся артефакты, но на моих тестовых данных я не заметил проблем с данным подходом. Да и целью эксперимента была вовсе не правдоподобность.

Результат

После этих нехитрых шагов мы будем иметь новые данные, которые мы передадим отрисовщику. Потом все сначала, и так до бесконечности. В качестве доказательства, что все это работает вот короткое видео:

Видео

Код — в этом коммите.

Заключение

ФП требует перестройки мышления. Но стоит ли оно того? Вот мои за и против.

За:

  • Читаемость кода. Вместе с SRP код очень легок в понимании.
  • Тестируемость. Так как результат функции не зависит от окружения мы уже имеем все необходимое для тестирования.
  • На мой взгляд — самый важный пункт. Великолепная возможность распараллеливания. Каждая (да-да — каждая!) функция в нашем примере может быть вызвана несколькими потоками безопасно! Без средств синхронизации!

Против:

  • Только одна ложка дегтя — даже не ложка, половник. Производительность. Вспомните, в прошлой статье в одном потоке с 2Д сеткой мы могли симулировать 8000 фигур. Сейчас всего 330. Триста тридцать, Карл!

Я десять лет проработал в геймдеве, стараясь выжимать максимум из каждой строчки. Для 3Д движка функциональный подход в том виде, в котором был представлен сегодня, несомненно, самоубийство. Однако, c++ — это не только геймдев. Не могу сказать точно, но интуиция подсказывает, что для большинства приложения ФП окажется вполне конкурентоспособной техникой.

Имея на руках несколько техник, почему бы не попробовать совместить их? В моей следующей статье я попробую скрестить ООП, DOD и ФП. Я не знаю результат, но уже вижу места, где можно значительно увеличить производительность. Поэтому оставайтесь на связи — должно быть интересно.
ссылка на оригинал статьи https://habrahabr.ru/post/324518/

Инструкции FMA3 в Ryzen намертво вешают операционную систему

Как выяснилось, выполнение некоторых специфичных инструкций FMA3 на процессоре AMD Ryzen приводит к критическому сбою ОС.

Инструкции типа FMA3 (Fused-Multiply-Add) поддерживаются и Intel (в Haswell), и AMD. Это инструкции типа d = round(a × b + c), где d должна быть в том же регистре, что и a, b или c. Для сравнения, инструкции FMA4 поддерживает только AMD (в процессорах Buldozer и более поздних). Там a, b, c и d могут быть в разных регистрах.

Баг в процессоре обнаружен во Flops version2 — простой и малоизвестной утилите для тестирования ЦП. Следует заметить, что разработчик этой утилиты Александр "Mystical" Йи (Alexander «Mystical» Yee) позиционирует её как специфическую утилиту тестирования, которая чувствительна к микроархитектуре процессоров. В других бенчмарках баг так и не проявился.

С утилитой Flops version2 поставляются специфические бинарники для всех основных архитектур x64 (Core2, Bulldozer, Sandy Bridge, Piledriver, Haswell, Skylake). Но на данный момент ни среди бинарных сборок под Windows, ни под Linux нет версии для тестирования Zen. Поэтому сейчас для тестирования Ryzen применяли бинарники других архитектур, а именно наиболее близкой Haswell. Вышеупомянутая ошибка с инструкциями FMA3 обнаружена две недели назад самим автором программы Flops, когда он запустил тест со стоковым бинарником для Haswell на компьютере следующей конфигурации:

  • Ryzen 7 1800X
  • Asus Prime B350M-A (BIOS 0502)
  • 4 x 8GB Corsair CMK32GX4M4A2400C14 @ 2133 MHz
  • Windows 10 Anniversary Update

Внезапно обнаружилось, что система обычно зависает при выполнении следующей операции:

Single-Precision - 128-bit FMA3 - Fused Multiply Add:

Иногда тест проходит эту операцию успешно, но всё равно зависает на какой-нибудь другой операции в дальнейшем.

Разработчик объясняет, что его тест — с открытым исходным кодом, и если вы не доверяете результатам, то можете сами взять и скомпилировать бинарник в Visual Studio, и перепроверить результаты.

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

Оставались какие-то вероятности, что причина сбоя может быть всё-таки не в процессоре, а в чём-то другом. Например, в специфической материнской плате, в специфическом BIOS, в специфической операционной системе… Что ещё может быть?

Разработчик поделился результатами с коллегами, чтобы они проверили другие версии Zen на своих компьютерах. Сбои подтвердились и для других процессоров, на разных материнских платах, под разными версиями Windows и под Linux.

В первые дни после призыва Алекса тесты запустили пять владельцев процессоров Ryzen. Вот какие получены результаты:

Подтверждённые сбои:

  • 1800X + Asus Prime B350M-A (BIOS 0502)
  • 1700 + Asus Prime B350M-A (BIOS ???)
  • 1700 + Asus Crosshair VI Hero
  • 1700 + Asus Crosshair VI Hero (BIOS 5803) (два банка памяти G.Skill + Kingston)
  • 1800X + Asus Crosshair VI Hero (Windows 7) — один раз прошёл тест, многократно не прошёл.

Подтверждённая бессбойная работа:

  • Пока нет

Разработчик бенчмарка проверил все варианты FMA (128 бит, 256 бит, одинарная точность, двойная точность чисел). Во всех случаях компьютер намертво зависал.

Ему не давала покоя только одна деталь: хотя тест написан правильно, но почему-то зависания не присходило в других бенчмарках, таких как prime95 и y-cruncher, хотя они тоже используют FMA в тестировании.

Так что некоторая неопределённость оставалась.

В конце концов, 16 марта было получено официальное сообщение от представителя AMD, что баг будет исправлен в новом коде AGESA (AMD Generic Encapsulated Software Architecture) — протокола, который используется в том числе для инициализации ядер процессоров AMD. Другими словами, специалисты компании проверили и подтвердили баг. Позже представители AMD официально подтвердили баг в комментариях для СМИ.

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

Плохая новость в том, что его могут использовать злоумышленники для DoS-атак. То есть главным образом эта ошибка — проблема информационной безопасности. Ведь обычная пользовательская программа, работающая в user mode, а не на уровне ядра ОС, никак не должна намертво вешать систему. Но это происходит.

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

Опасность уязвимости в безопасности усугубляется тем, что вредоносный код можно запустить даже из-под виртуальной машины, он всё равно подвесит всю систему. Компьютер с новым процессором Ryzen может подвесить любая вредоносная программа. Возможно, даже через браузер.

Как уже было сказано, AMD работает над обновлением протокола AGESA. После этого патчи будут выпущены для всех версий BIOS во всех материнских платах.
ссылка на оригинал статьи https://geektimes.ru/post/287206/

Зашифрованные почтовые сервисы: что выбрать?

Константин Докучаев, автора блога All-in-One Person и телеграм-канала @themarfa, рассказал специально для «Нетологии» о двух почтовых сервисах: Tutanota и ProtonMail и объяснил, какой из них выбрать и почему.

image

Сегодня уже не так часто услышишь о важности частной переписки, о методах её защиты и шифровании переписки. Но я всё равно решил взглянуть на два популярных почтовых сервиса с end-to-end шифрованием: Tutanota и ProtonMail. Они предлагают безопасную переписку с шифрованием всех писем. Давайте разберём подробно, что дают оба сервиса, и стоит ли прятать свою переписку от ФСБ или других спецслужб и конкурентов.

Tutanota

Tutanota — бесплатный почтовый сервис от немцев, который предоставляет шифрование почтовой переписки для своих клиентов.

Плюсы:

  • Русскоязычный интерфейс.
  • Простая регистрация.
  • Бесплатный тариф.
  • Веб-версия, iOS и Android.
  • Возможность развернуть сервер на своём домене.

Минусы:

  • В бесплатном аккаунте только 1 Гб хранилища.
  • Нет поддержки облачных хранилищ.
  • Нет двухфакторной аутентификации.
  • Нет возможности получения почты по IMAP сторонними клиентами.

Регистрация в сервисе гораздо проще, чем в обычных почтовых провайдерах. От вас потребуется лишь выбрать имя почтового ящика и указать пароль, после чего сразу же можно начать пользоваться своей новой защищённой почтой.

Как видно из скриншота ниже, Tutanota не обладает красочным интерфейсом. Но не это здесь главное. Как и в любом почтовом сервисе, здесь есть стандартное распределение писем по папкам: Входящие, Черновики, Отправленные, Корзина, Архив и Спам. При создании нового письма или ответе на полученное, вы также найдёте все стандартные функции: пересылку, скрытых адресатов и прочее. Ещё можно прикреплять файлы к письмам.

image

Для входящих писем можете настроить правила фильтрации. Из интересных вещей в Tutanota стоить отметить возможность прикрепления нескольких псевдонимов к одному почтовому ящику. Правда, такая возможность есть только в платной версии сервиса. Максимальное ограничение на письмо с учётом вложений составляет 25 Мб.

О безопасности

Как и большинство сервисов, борющихся за безопасность, Tutanota выложили свой исходный код на Github. Поэтому сообщество разработчиков может самостоятельно проверить код сервиса на «закладки» и прочие небезопасные штуки.

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

Вся переписка шифруется end-to-end и не передаётся никаким третьим лицам. Шифрованию подвергаются сами письма: тема, содержимое, вложения и список контактов. Tutanota имеют доступ лишь к метаданным письма, таким как отправитель, получатель и дата письма. Что, в принципе, понятно, но разработчики обещают в будущем полное шифрование писем.

Шифрование писем при отправке между пользователями Tutanota происходит при помощи стандартизированных алгоритмов AES с ключом шифрования 128 бит и RSA с 2048 бит. Письма в сторонние сервисы шифруются при помощи AES 128 бит. Алгоритм шифрования наглядно показан на картинке ниже, где отображена отправка и получение писем внутри и вне сервиса.

image

Отправить письмо в другой почтовый сервис можно двумя способами: защищённым и нет. Поговорим о защищенном способе. Для отправки таких писем вам нужно обменяться с получателем уникальным паролем, которым будет зашифрована вся ваша переписка. Это можно сделать через любой сторонний сервис или устно. После первой отправки письма и ввода пароля получателем, ключ шифрования сохраняется в вашей адресной книге и про него можно забыть. Вся почта автоматически будет шифроваться.

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

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

Анонимность сервиса заметна уже на этапе регистрации, где от вас не требуется никаких личных данных. IP-адреса не хранятся сервисом и обрезаются при отправке писем. Таким образом, ваше местоположение постоянно скрыто. За премиум-возможности можно заплатить анонимной валютой Bitcoin. Конечно, сервис ведёт технические логи для обработки ошибок. Но они хранятся 14 дней и не содержат никакой личной информации о пользователе.

ProtonMail

Теперь поговорим о более известном сервисе для защищённого обмена почтой ProtonMail.

Плюсы:

  • Веб-интерфейс и мобильные приложения.
  • Двухфакторная аутентификация.
  • Тонкие настройки внешнего вида.
  • Настройки безопасности.
  • Шифрование при помощи PGP.

Минусы:

  • Нет русского языка.
  • В бесплатной версии доступно лишь 150 сообщений в день.
  • В бесплатной версии 500 Мб хранилища.
  • Ограничения расширяются, но остаются даже в платной версии (есть тарифный план без ограничений).

В ProtonMail при регистрации не запрашиваются никакие личные данные. От вас требуется выбрать имя для почты и указать пароль, при помощи которого будут шифроваться письма. Необязательным полем является дополнительный адрес почты, на который можно будет восстановить пароль. Во время процесса регистрации генерируются ключи шифрования, а по окончании появляется капча, чтобы убедиться в вашей человечности.

Интерфейс ProtonMail менее аскетичный, чем у своего собрата. Здесь, кроме стандартных почтовых функций, можно найти и такие уже привычные вещи, как звёздочки для избранных писем и ярлыки. Интерфейс можно настроить под себя и изменить отображение писем с горизонтального на вертикальное. Письма можно сортировать по различным параметрам. Например, по дате или объёму. Кроме этого, разработчики предусмотрели поиск по почте.

В целом ProtonMail больше похож на привычные нам почтовые сервисы, и по своим функциям он не уступает конкурентам. Перемещение писем, просмотр «тела» письма, удобное форматирование и многое другое. В настройках сервиса вы можете включить двухфакторную аутентификацию, отключить возможность восстановления пароля и настроить уровень логирования.

image

О безопасности

Все данные, передаваемые через сервис, защищены шифрованием. «Тело» и вложения письма зашифрованы end-to-end, но тема письма не защищена. Это сделано из-за того, что разработчики используют PGP-алгоритм, который зависит от стандартов передачи данных по протоколу SMTP. Разработчики пошли на эту уступку, чтобы не ограничивать шифрование писем только между клиентами сервиса. PGP-алгоритм позволяет пользоваться перепиской независимо от используемого почтового клиента.

image

Для отправки писем вне сервиса вы можете использовать защищённый метод и незащищённый. В первом случае ваши письма остаются зашифрованными end-to-end. Во втором для отправки писем будет использоваться метод шифрования TLS, который поддерживает большинство популярных почтовых сервисов. Однако в этом случае у третьих лиц появляется возможность получения доступа к вашей переписке. При этом вся почта внутри ProtonMail недоступна третьим лицам независимо от метода отправки писем.

Сервера ProtonMail находятся в Швейцарии и разработчик подчиняется законам этой страны. При законном запросе от суда разработчики в состоянии предоставить тему всех писем.

Так как вся инфраструктура сервиса базируется на работе с алгоритмом PGP, на сайте разработчика не описаны конкретные характеристики шифрования. Но Википедия всё знает:

«Шифрование PGP осуществляется последовательно хешированием, сжатием данных, шифрованием с симметричным ключом, и, наконец, шифрованием с открытым ключом, причём каждый этап может осуществляться одним из нескольких поддерживаемых алгоритмов. Симметричное шифрование производится с использованием одного из семи симметричных алгоритмов (AES, CAST5, 3DES, IDEA, Twofish, Blowfish, Camellia) на сеансовом ключе. Сеансовый ключ генерируется с использованием криптографически стойкого генератора псевдослучайных чисел. Сеансовый ключ зашифровывается открытым ключом получателя с использованием алгоритмов RSA или Elgamal (в зависимости от типа ключа получателя). Каждый открытый ключ соответствует имени пользователя или адресу электронной почты. Первая версия системы называлась Сеть Доверия и противопоставлялась системе X.509, которая использовала иерархический подход и была основана на удостоверяющих центрах, добавленный в PGP позже. Современные версии PGP включают оба способа»

Какой сервис выбрать?

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

Более дешёвый вариант — Tutanota. Но есть несколько главных минусов. Первый: вы не сможете пользоваться сторонними почтовыми клиентами. Второй: получатели ваших писем в сторонних сервисах будут вынуждены читать переписку в браузере с вводом пароля.

ProtonMail — своего рода шифрованная почта для «домохозяек». Минус: цена. Скорее всего, вам придётся оплатить подписку на сервис. С другой стороны, вы получите возможность «бесшовной» переписки со всем миром независимо от почтового провайдера или клиента.

От редакции

21 апреля в «Нетологии» запускается курс «Big Data: основы работы с большими массивами данных». На нем мы расскажем о том, что же это такое, какие есть методы анализа, на чем строятся и как работают системы и научимся реальной работе с массивами больших данных. Работая с Big Data, можно повысить свою квалификацию, научиться применять в жизни и работе данные, и понять, зачем шифровать или не шифровать свои переписки.
ссылка на оригинал статьи https://habrahabr.ru/post/324618/

Решение задачи кредитного скоринга методом логистической регрессии

Отучившись на нескольких онлайн-курсах, попробовал занять позицию, связанную с Machine Learning — на входе получил тестовое задание о кредитном скоринге. Свое решение которой здесь и привожу:

Задание

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

Тренировочная выборка содержится в файле train.csv, тестовая — test.csv.

Информация о значениях признаков содержится в файле feature_descr.xlsx.

Целевой признак — loan_status (бинарный). 1 означает что кредит успешно вернули.

В рамках тестового задания вам предлагается:

  • Обучить модель на предоставленных данных, найти качество полученной модели.
  • Записать предсказания (вероятности) для тестового набора в файл results.csv
  • Продемонстрировать результаты анализа в графическом виде (ROC-curve)

Тщательный выбор фич и подбор гиперпараметров можно не проводить.

Решение

import pandas as pd

import sys print "Python version: {}".format(sys.version) print "Pandas version: {}".format(pd.__version__)

Python version: 2.7.13 |Anaconda 4.3.1 (64-bit)| (default, Dec 19 2016, 13:29:36) [MSC v.1500 64 bit (AMD64)] Pandas version: 0.19.2

Тут стоит заметить что использовался Python 2.7

pd.set_option('display.max_columns', 500) pd.set_option('display.width', 1000)

Читаем данные из train.csv, используя record_id как индекс:

train = pd.read_csv('train.csv', index_col='record_id') train.head()

loan_amnt term int_rate installment grade sub_grade emp_title emp_length home_ownership annual_inc verification_status issue_d loan_status pymnt_plan purpose zip_code addr_state dti delinq_2yrs earliest_cr_line inq_last_6mths mths_since_last_delinq open_acc pub_rec revol_bal revol_util total_acc initial_list_status collections_12_mths_ex_med policy_code application_type acc_now_delinq tot_coll_amt tot_cur_bal total_rev_hi_lim
record_id
453246940 15000.0 36 months 11.99 498.15 B B3 Quality Assurance Specialist 4 years MORTGAGE 70000.0 Verified Oct-2013 1 n debt_consolidation 156xx PA 13.85 0.0 Dec-1991 1.0 NaN 17.0 3.0 12540.0 61.2 32.0 f 0.0 1.0 INDIVIDUAL 0.0 0.0 295215.0 20500.0
453313687 3725.0 36 months 6.03 113.38 A A1 NaN n/a MORTGAGE 52260.0 Source Verified Oct-2012 1 n credit_card 339xx FL 19.43 0.0 Oct-2000 0.0 NaN 7.0 0.0 3730.0 26.3 9.0 f 0.0 1.0 INDIVIDUAL 0.0 0.0 25130.0 14200.0
453283543 16000.0 36 months 11.14 524.89 B B2 KIPP NYC 3 years RENT 67500.0 Source Verified Apr-2013 1 n debt_consolidation 104xx NY 14.77 0.0 Jul-2001 0.0 NaN 9.0 0.0 11769.0 60.5 22.0 f 0.0 1.0 INDIVIDUAL 0.0 193.0 41737.0 19448.0
453447199 4200.0 36 months 13.33 142.19 C C3 Receptionist < 1 year MORTGAGE 21600.0 Not Verified Mar-2015 0 n major_purchase 982xx WA 39.00 0.0 May-2003 0.0 47.0 9.0 0.0 6797.0 46.9 19.0 w 0.0 1.0 INDIVIDUAL 0.0 165.0 28187.0 14500.0
453350283 6500.0 36 months 12.69 218.05 B B5 Medtox Laboratories 10+ years RENT 41000.0 Not Verified Jan-2012 1 n credit_card 551xx MN 18.35 0.0 Sep-1990 0.0 NaN 8.0 0.0 14674.0 82.4 12.0 f 0.0 1.0 INDIVIDUAL 0.0 NaN NaN NaN
train.shape

(200189, 35)

Смотрим, какие типы данных в столбцах:

train.dtypes

loan_amnt                     float64 term                           object int_rate                      float64 installment                   float64 grade                          object sub_grade                      object emp_title                      object emp_length                     object home_ownership                 object annual_inc                    float64 verification_status            object issue_d                        object loan_status                     int64 pymnt_plan                     object purpose                        object zip_code                       object addr_state                     object dti                           float64 delinq_2yrs                   float64 earliest_cr_line               object inq_last_6mths                float64 mths_since_last_delinq        float64 open_acc                      float64 pub_rec                       float64 revol_bal                     float64 revol_util                    float64 total_acc                     float64 initial_list_status            object collections_12_mths_ex_med    float64 policy_code                   float64 application_type               object acc_now_delinq                float64 tot_coll_amt                  float64 tot_cur_bal                   float64 total_rev_hi_lim              float64 dtype: object

Исследуем поля на наличие пропусков:

train.isnull().sum()

loan_amnt                          0 term                               0 int_rate                           0 installment                        0 grade                              0 sub_grade                          0 emp_title                      11124 emp_length                         0 home_ownership                     0 annual_inc                         0 verification_status                0 issue_d                            0 loan_status                        0 pymnt_plan                         0 purpose                            0 zip_code                           0 addr_state                         0 dti                                0 delinq_2yrs                        0 earliest_cr_line                   0 inq_last_6mths                     0 mths_since_last_delinq        110568 open_acc                           0 pub_rec                            0 revol_bal                          0 revol_util                       154 total_acc                          0 initial_list_status                0 collections_12_mths_ex_med        44 policy_code                        0 application_type                   0 acc_now_delinq                     0 tot_coll_amt                   47957 tot_cur_bal                    47957 total_rev_hi_lim               47957 dtype: int64

Следующие столбцы имеют пропуски:

emp_title — The job title supplied by the Borrower when applying for the loan

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

train['is_title_known'] = train['emp_title'].map(lambda x: 0 if x == 'n/a' else 1) train.drop('emp_title', axis=1, inplace=True)

mths_since_last_delinq — The number of months since the borrower’s last delinquency

— кол-во месяцев с момента последнего неосуществления платежа в установленный срок. Заменить пропуски на 0 будет неправильно, заменим их на max в этом столбце и добавим столбец is_delinq_occurs с флагом, был ли факт неплатежа ранее

import numpy as np import math train['is_delinq_occurs'] = train['mths_since_last_delinq'].map(lambda x: 0 if math.isnan(x) else 1)  max_mths_since_last_delinq = np.nanmax(train.mths_since_last_delinq.values) train['mths_since_last_delinq'].fillna(max_mths_since_last_delinq, inplace=True)

revol_util — Revolving line utilization rate, or the amount of credit the borrower is using relative to all available revolving credit

collections_12_mths_ex_med — Number of collections in 12 months excluding medical collections

tot_coll_amt — Total collection amounts ever owed

tot_cur_bal — Total current balance of all accounts

total_rev_hi_lim — Total revolving high credit/credit limit

Мне неизвестны тонкости этого бизнес-домена, поэтому заменяем пропуски на нули с помощью функции fillna():

train.fillna(0, inplace=True) train.isnull().sum()

loan_amnt                     0 term                          0 int_rate                      0 installment                   0 grade                         0 sub_grade                     0 emp_length                    0 home_ownership                0 annual_inc                    0 verification_status           0 issue_d                       0 loan_status                   0 pymnt_plan                    0 purpose                       0 zip_code                      0 addr_state                    0 dti                           0 delinq_2yrs                   0 earliest_cr_line              0 inq_last_6mths                0 mths_since_last_delinq        0 open_acc                      0 pub_rec                       0 revol_bal                     0 revol_util                    0 total_acc                     0 initial_list_status           0 collections_12_mths_ex_med    0 policy_code                   0 application_type              0 acc_now_delinq                0 tot_coll_amt                  0 tot_cur_bal                   0 total_rev_hi_lim              0 is_title_known                0 is_delinq_occurs              0 dtype: int64

— Пропусков теперь нет

Разбиваем данные на X и Y:

def extract_XY(data):     X = data.drop(['loan_status'], axis=1)     Y = data['loan_status']     return X, Y  X, Y = extract_XY(train)  print X.shape, Y.shape

(200189, 35) (200189L,)

Предварительно надо разобраться с нечисловыми столбцами

Добавим методы для LabelEncoder и OneHotEncoder:

from sklearn.preprocessing import LabelEncoder from sklearn.preprocessing import OneHotEncoder  # Добавляет в DataFrame df новый столбец с именем column_name+'_le', содержащий номера категорий,  # соответствующие столбцу column_name. Исходный столбец column_name удаляется # def encode_with_LabelEncoder(df, column_name):     label_encoder = LabelEncoder()     label_encoder.fit(df[column_name])     df[column_name+'_le'] = label_encoder.transform(df[column_name])     df.drop([column_name], axis=1, inplace=True)     return label_encoder  # Кодирование с использованием ранее созданного LabelEncoder # def encode_with_existing_LabelEncoder(df, column_name, label_encoder):     df[column_name+'_le'] = label_encoder.transform(df[column_name])     df.drop([column_name], axis=1, inplace=True)  # Вначале кодирует столбец column_name при помощи LabelEncoder, потом добавляет в DataFrame df новые столбцы  # с именами column_name=<категория_i>. Столбцы column_name и column_name+'_le' удаляются # Usage: df, label_encoder = encode_with_OneHotEncoder_and_delete_column(df, column_name) # def encode_with_OneHotEncoder_and_delete_column(df, column_name):     le_encoder = encode_with_LabelEncoder(df, column_name)     return perform_dummy_coding_and_delete_column(df, column_name, le_encoder), le_encoder  # То же, что предыдущий метод, но при помощи уже существующего LabelEncoder # def encode_with_OneHotEncoder_using_existing_LabelEncoder_and_delete_column(df, column_name, le_encoder):     encode_with_existing_LabelEncoder(df, column_name, le_encoder)     return perform_dummy_coding_and_delete_column(df, column_name, le_encoder)  # Реализует Dummy-кодирование # def perform_dummy_coding_and_delete_column(df, column_name, le_encoder):     oh_encoder = OneHotEncoder(sparse=False)     oh_features = oh_encoder.fit_transform(df[column_name+'_le'].values.reshape(-1,1))     ohe_columns=[column_name + '=' + le_encoder.classes_[i] for i in range(oh_features.shape[1])]      df.drop([column_name+'_le'], axis=1, inplace=True)      df_with_features = pd.DataFrame(oh_features, columns=ohe_columns)     df_with_features.index = df.index     return pd.concat([df, df_with_features], axis=1)

term — The number of payments on the loan. Values are in months and can be either 36 or 60.

— Кол-во платежей, стоит оставить

import numpy as np print np.unique(X['term'])

[' 36 months' ' 60 months']

term_le_encoder = encode_with_LabelEncoder(X,'term')

grade, sub_grade — loan grade & subgrade

— Некие "класс и подкласс заема", grade соответствует риску займа (с нарастанием риска от A к G), sub_grade — то же, только c больше детализацией, — стоит оставить

print np.unique(X['grade']) print np.unique(X['sub_grade'])

['A' 'B' 'C' 'D' 'E' 'F' 'G'] ['A1' 'A2' 'A3' 'A4' 'A5' 'B1' 'B2' 'B3' 'B4' 'B5' 'C1' 'C2' 'C3' 'C4' 'C5'  'D1' 'D2' 'D3' 'D4' 'D5' 'E1' 'E2' 'E3' 'E4' 'E5' 'F1' 'F2' 'F3' 'F4' 'F5'  'G1' 'G2' 'G3' 'G4' 'G5']

grade_le_encoder = encode_with_LabelEncoder(X,'grade') sub_grade_le_encoder = encode_with_LabelEncoder(X,'sub_grade')

emp_length — Employment length in years. Possible values are between 0 and 10 where 0 means less than one year and 10 means ten or more years

— Срок занятости — стоит оставить

print np.unique(X['emp_length'])

['1 year' '10+ years' '2 years' '3 years' '4 years' '5 years' '6 years'  '7 years' '8 years' '9 years' '< 1 year' 'n/a']

X, emp_length_le_encoder = encode_with_OneHotEncoder_and_delete_column(X,'emp_length')

home_ownership — The home ownership status provided by the borrower during registration. Our values are: RENT, OWN, MORTGAGE, OTHER

— Флажок, принадлежит ли владельцу его текущий дом — стоит оставить

print np.unique(X['home_ownership'])

['ANY' 'MORTGAGE' 'NONE' 'OTHER' 'OWN' 'RENT']

X, home_ownership_le_encoder = encode_with_OneHotEncoder_and_delete_column(X,'home_ownership')

verification_status

— В описании столбцов данного столбца нету, но судя из названия — это информация о том, являются ли данные о заемщике проверенными. Стоит оставить

print np.unique(X['verification_status'])

['Not Verified' 'Source Verified' 'Verified']

X, verification_status_le_encoder = encode_with_OneHotEncoder_and_delete_column(X,'verification_status')

issue_d — The month which the loan was funded

— Месяц (и год), в который предоставлялся заем (вроде как). В период спада экономики возвратность займов может падать — стоит оставить

print np.unique(X['issue_d'])

['Apr-2008' 'Apr-2009' 'Apr-2010' 'Apr-2011' 'Apr-2012' 'Apr-2013'  'Apr-2014' 'Apr-2015' 'Aug-2007' 'Aug-2008' 'Aug-2009' 'Aug-2010'  'Aug-2011' 'Aug-2012' 'Aug-2013' 'Aug-2014' 'Aug-2015' 'Dec-2007'  'Dec-2008' 'Dec-2009' 'Dec-2010' 'Dec-2011' 'Dec-2012' 'Dec-2013'  'Dec-2014' 'Dec-2015' 'Feb-2008' 'Feb-2009' 'Feb-2010' 'Feb-2011'  'Feb-2012' 'Feb-2013' 'Feb-2014' 'Feb-2015' 'Jan-2008' 'Jan-2009'  'Jan-2010' 'Jan-2011' 'Jan-2012' 'Jan-2013' 'Jan-2014' 'Jan-2015'  'Jul-2007' 'Jul-2008' 'Jul-2009' 'Jul-2010' 'Jul-2011' 'Jul-2012'  'Jul-2013' 'Jul-2014' 'Jul-2015' 'Jun-2007' 'Jun-2008' 'Jun-2009'  'Jun-2010' 'Jun-2011' 'Jun-2012' 'Jun-2013' 'Jun-2014' 'Jun-2015'  'Mar-2008' 'Mar-2009' 'Mar-2010' 'Mar-2011' 'Mar-2012' 'Mar-2013'  'Mar-2014' 'Mar-2015' 'May-2008' 'May-2009' 'May-2010' 'May-2011'  'May-2012' 'May-2013' 'May-2014' 'May-2015' 'Nov-2007' 'Nov-2008'  'Nov-2009' 'Nov-2010' 'Nov-2011' 'Nov-2012' 'Nov-2013' 'Nov-2014'  'Nov-2015' 'Oct-2007' 'Oct-2008' 'Oct-2009' 'Oct-2010' 'Oct-2011'  'Oct-2012' 'Oct-2013' 'Oct-2014' 'Oct-2015' 'Sep-2007' 'Sep-2008'  'Sep-2009' 'Sep-2010' 'Sep-2011' 'Sep-2012' 'Sep-2013' 'Sep-2014'  'Sep-2015']

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

def month_to_decimal(month):     month_dict = {'Jan':0, 'Feb':1/12., 'Mar':2/12., 'Apr':3/12., 'May':4/12., 'Jun':5/12.,       'Jul':6/12., 'Aug':7/12., 'Sep':8/12., 'Oct':9/12., 'Nov':10/12., 'Dec':11/12.}     return month_dict[month]  def convert_date(month_year):     month_and_year = month_year.split('-')     return float(month_and_year[1]) + month_to_decimal(month_and_year[0])  # Some check convert_date('Mar-2011')

2011.1666666666667

def encode_with_func(df, column_name, func_name):     df[column_name+'_le'] = df[column_name].map(func_name)     df.drop(column_name, axis=1, inplace=True)  encode_with_func(X, 'issue_d', convert_date)

pymnt_plan — Indicates if a payment plan has been put in place for the loan

— Флажок был ли план платежей, стоит оставить

print np.unique(X['pymnt_plan'])

['n' 'y']

pymnt_plan_le_encoder = encode_with_LabelEncoder(X,'pymnt_plan')

purpose — A category provided by the borrower for the loan request

— Категория, для чего брался заем, стоит оставить

print np.unique(X['purpose'])

['car' 'credit_card' 'debt_consolidation' 'educational' 'home_improvement'  'house' 'major_purchase' 'medical' 'moving' 'other' 'renewable_energy'  'small_business' 'vacation' 'wedding']

X,purpose_le_encoder = encode_with_OneHotEncoder_and_delete_column(X,'purpose')

zip_code — The first 3 numbers of the zip code provided by the borrower in the loan application

addr_state — The state provided by the borrower in the loan application

— Почтовый индекс и адрес

print len(np.unique(X['zip_code'])) print len(np.unique(X['addr_state']))

877 51

X.drop(['zip_code'], axis=1, inplace=True) addr_state_le_encoder = encode_with_LabelEncoder(X,'addr_state')

earliest_cr_line — The month the borrower’s earliest reported credit line was opened

print np.unique(X['earliest_cr_line'])

['Apr-1964' 'Apr-1965' 'Apr-1966' 'Apr-1967' 'Apr-1968' 'Apr-1969'  'Apr-1970' 'Apr-1971' 'Apr-1972' 'Apr-1973' 'Apr-1974' 'Apr-1975'  'Apr-1976' 'Apr-1977' 'Apr-1978' 'Apr-1979' 'Apr-1980' 'Apr-1981'  'Apr-1982' 'Apr-1983' 'Apr-1984' 'Apr-1985' 'Apr-1986' 'Apr-1987'  'Apr-1988' 'Apr-1989' 'Apr-1990' 'Apr-1991' 'Apr-1992' 'Apr-1993'  'Apr-1994' 'Apr-1995' 'Apr-1996' 'Apr-1997' 'Apr-1998' 'Apr-1999'  'Apr-2000' 'Apr-2001' 'Apr-2002' 'Apr-2003' 'Apr-2004' 'Apr-2005'  'Apr-2006' 'Apr-2007' 'Apr-2008' 'Apr-2009' 'Apr-2010' 'Apr-2011'  'Apr-2012' 'Aug-1959' 'Aug-1960' 'Aug-1965' 'Aug-1966' 'Aug-1967'  'Aug-1968' 'Aug-1969' 'Aug-1970' 'Aug-1971' 'Aug-1972' 'Aug-1973'  'Aug-1974' 'Aug-1975' 'Aug-1976' 'Aug-1977' 'Aug-1978' 'Aug-1979'  'Aug-1980' 'Aug-1981' 'Aug-1982' 'Aug-1983' 'Aug-1984' 'Aug-1985'  'Aug-1986' 'Aug-1987' 'Aug-1988' 'Aug-1989' 'Aug-1990' 'Aug-1991'  'Aug-1992' 'Aug-1993' 'Aug-1994' 'Aug-1995' 'Aug-1996' 'Aug-1997'  'Aug-1998' 'Aug-1999' 'Aug-2000' 'Aug-2001' 'Aug-2002' 'Aug-2003'  'Aug-2004' 'Aug-2005' 'Aug-2006' 'Aug-2007' 'Aug-2008' 'Aug-2009'  'Aug-2010' 'Aug-2011' 'Dec-1950' 'Dec-1956' 'Dec-1958' 'Dec-1960'  'Dec-1961' 'Dec-1962' 'Dec-1963' 'Dec-1964' 'Dec-1965' 'Dec-1966'  'Dec-1967' 'Dec-1968' 'Dec-1969' 'Dec-1970' 'Dec-1971' 'Dec-1972'  'Dec-1973' 'Dec-1974' 'Dec-1975' 'Dec-1976' 'Dec-1977' 'Dec-1978'  'Dec-1979' 'Dec-1980' 'Dec-1981' 'Dec-1982' 'Dec-1983' 'Dec-1984'  'Dec-1985' 'Dec-1986' 'Dec-1987' 'Dec-1988' 'Dec-1989' 'Dec-1990'  'Dec-1991' 'Dec-1992' 'Dec-1993' 'Dec-1994' 'Dec-1995' 'Dec-1996'  'Dec-1997' 'Dec-1998' 'Dec-1999' 'Dec-2000' 'Dec-2001' 'Dec-2002'  'Dec-2003' 'Dec-2004' 'Dec-2005' 'Dec-2006' 'Dec-2007' 'Dec-2008'  'Dec-2009' 'Dec-2010' 'Dec-2011' 'Feb-1957' 'Feb-1964' 'Feb-1965'  'Feb-1966' 'Feb-1967' 'Feb-1968' 'Feb-1969' 'Feb-1970' 'Feb-1971'  'Feb-1972' 'Feb-1973' 'Feb-1974' 'Feb-1975' 'Feb-1976' 'Feb-1977'  'Feb-1978' 'Feb-1979' 'Feb-1980' 'Feb-1981' 'Feb-1982' 'Feb-1983'  'Feb-1984' 'Feb-1985' 'Feb-1986' 'Feb-1987' 'Feb-1988' 'Feb-1989'  'Feb-1990' 'Feb-1991' 'Feb-1992' 'Feb-1993' 'Feb-1994' 'Feb-1995'  'Feb-1996' 'Feb-1997' 'Feb-1998' 'Feb-1999' 'Feb-2000' 'Feb-2001'  'Feb-2002' 'Feb-2003' 'Feb-2004' 'Feb-2005' 'Feb-2006' 'Feb-2007'  'Feb-2008' 'Feb-2009' 'Feb-2010' 'Feb-2011' 'Feb-2012' 'Jan-1946'  'Jan-1954' 'Jan-1955' 'Jan-1956' 'Jan-1959' 'Jan-1960' 'Jan-1961'  'Jan-1962' 'Jan-1963' 'Jan-1964' 'Jan-1965' 'Jan-1966' 'Jan-1967'  'Jan-1968' 'Jan-1969' 'Jan-1970' 'Jan-1971' 'Jan-1972' 'Jan-1973'  'Jan-1974' 'Jan-1975' 'Jan-1976' 'Jan-1977' 'Jan-1978' 'Jan-1979'  'Jan-1980' 'Jan-1981' 'Jan-1982' 'Jan-1983' 'Jan-1984' 'Jan-1985'  'Jan-1986' 'Jan-1987' 'Jan-1988' 'Jan-1989' 'Jan-1990' 'Jan-1991'  'Jan-1992' 'Jan-1993' 'Jan-1994' 'Jan-1995' 'Jan-1996' 'Jan-1997'  'Jan-1998' 'Jan-1999' 'Jan-2000' 'Jan-2001' 'Jan-2002' 'Jan-2003'  'Jan-2004' 'Jan-2005' 'Jan-2006' 'Jan-2007' 'Jan-2008' 'Jan-2009'  'Jan-2010' 'Jan-2011' 'Jan-2012' 'Jul-1958' 'Jul-1961' 'Jul-1963'  'Jul-1964' 'Jul-1965' 'Jul-1966' 'Jul-1967' 'Jul-1968' 'Jul-1969'  'Jul-1970' 'Jul-1971' 'Jul-1972' 'Jul-1973' 'Jul-1974' 'Jul-1975'  'Jul-1976' 'Jul-1977' 'Jul-1978' 'Jul-1979' 'Jul-1980' 'Jul-1981'  'Jul-1982' 'Jul-1983' 'Jul-1984' 'Jul-1985' 'Jul-1986' 'Jul-1987'  'Jul-1988' 'Jul-1989' 'Jul-1990' 'Jul-1991' 'Jul-1992' 'Jul-1993'  'Jul-1994' 'Jul-1995' 'Jul-1996' 'Jul-1997' 'Jul-1998' 'Jul-1999'  'Jul-2000' 'Jul-2001' 'Jul-2002' 'Jul-2003' 'Jul-2004' 'Jul-2005'  'Jul-2006' 'Jul-2007' 'Jul-2008' 'Jul-2009' 'Jul-2010' 'Jul-2011'  'Jul-2012' 'Jun-1957' 'Jun-1959' 'Jun-1963' 'Jun-1964' 'Jun-1965'  'Jun-1966' 'Jun-1967' 'Jun-1968' 'Jun-1969' 'Jun-1970' 'Jun-1971'  'Jun-1972' 'Jun-1973' 'Jun-1974' 'Jun-1975' 'Jun-1976' 'Jun-1977'  'Jun-1978' 'Jun-1979' 'Jun-1980' 'Jun-1981' 'Jun-1982' 'Jun-1983'  'Jun-1984' 'Jun-1985' 'Jun-1986' 'Jun-1987' 'Jun-1988' 'Jun-1989'  'Jun-1990' 'Jun-1991' 'Jun-1992' 'Jun-1993' 'Jun-1994' 'Jun-1995'  'Jun-1996' 'Jun-1997' 'Jun-1998' 'Jun-1999' 'Jun-2000' 'Jun-2001'  'Jun-2002' 'Jun-2003' 'Jun-2004' 'Jun-2005' 'Jun-2006' 'Jun-2007'  'Jun-2008' 'Jun-2009' 'Jun-2010' 'Jun-2011' 'Jun-2012' 'Mar-1960'  'Mar-1961' 'Mar-1963' 'Mar-1964' 'Mar-1965' 'Mar-1966' 'Mar-1967'  'Mar-1968' 'Mar-1969' 'Mar-1970' 'Mar-1971' 'Mar-1972' 'Mar-1973'  'Mar-1974' 'Mar-1975' 'Mar-1976' 'Mar-1977' 'Mar-1978' 'Mar-1979'  'Mar-1980' 'Mar-1981' 'Mar-1982' 'Mar-1983' 'Mar-1984' 'Mar-1985'  'Mar-1986' 'Mar-1987' 'Mar-1988' 'Mar-1989' 'Mar-1990' 'Mar-1991'  'Mar-1992' 'Mar-1993' 'Mar-1994' 'Mar-1995' 'Mar-1996' 'Mar-1997'  'Mar-1998' 'Mar-1999' 'Mar-2000' 'Mar-2001' 'Mar-2002' 'Mar-2003'  'Mar-2004' 'Mar-2005' 'Mar-2006' 'Mar-2007' 'Mar-2008' 'Mar-2009'  'Mar-2010' 'Mar-2011' 'Mar-2012' 'May-1959' 'May-1960' 'May-1962'  'May-1963' 'May-1964' 'May-1965' 'May-1966' 'May-1967' 'May-1968'  'May-1969' 'May-1970' 'May-1971' 'May-1972' 'May-1973' 'May-1974'  'May-1975' 'May-1976' 'May-1977' 'May-1978' 'May-1979' 'May-1980'  'May-1981' 'May-1982' 'May-1983' 'May-1984' 'May-1985' 'May-1986'  'May-1987' 'May-1988' 'May-1989' 'May-1990' 'May-1991' 'May-1992'  'May-1993' 'May-1994' 'May-1995' 'May-1996' 'May-1997' 'May-1998'  'May-1999' 'May-2000' 'May-2001' 'May-2002' 'May-2003' 'May-2004'  'May-2005' 'May-2006' 'May-2007' 'May-2008' 'May-2009' 'May-2010'  'May-2011' 'May-2012' 'Nov-1954' 'Nov-1955' 'Nov-1956' 'Nov-1958'  'Nov-1959' 'Nov-1960' 'Nov-1961' 'Nov-1962' 'Nov-1964' 'Nov-1965'  'Nov-1966' 'Nov-1967' 'Nov-1968' 'Nov-1969' 'Nov-1970' 'Nov-1971'  'Nov-1972' 'Nov-1973' 'Nov-1974' 'Nov-1975' 'Nov-1976' 'Nov-1977'  'Nov-1978' 'Nov-1979' 'Nov-1980' 'Nov-1981' 'Nov-1982' 'Nov-1983'  'Nov-1984' 'Nov-1985' 'Nov-1986' 'Nov-1987' 'Nov-1988' 'Nov-1989'  'Nov-1990' 'Nov-1991' 'Nov-1992' 'Nov-1993' 'Nov-1994' 'Nov-1995'  'Nov-1996' 'Nov-1997' 'Nov-1998' 'Nov-1999' 'Nov-2000' 'Nov-2001'  'Nov-2002' 'Nov-2003' 'Nov-2004' 'Nov-2005' 'Nov-2006' 'Nov-2007'  'Nov-2008' 'Nov-2009' 'Nov-2010' 'Nov-2011' 'Oct-1954' 'Oct-1958'  'Oct-1959' 'Oct-1960' 'Oct-1961' 'Oct-1962' 'Oct-1963' 'Oct-1964'  'Oct-1965' 'Oct-1966' 'Oct-1967' 'Oct-1968' 'Oct-1969' 'Oct-1970'  'Oct-1971' 'Oct-1972' 'Oct-1973' 'Oct-1974' 'Oct-1975' 'Oct-1976'  'Oct-1977' 'Oct-1978' 'Oct-1979' 'Oct-1980' 'Oct-1981' 'Oct-1982'  'Oct-1983' 'Oct-1984' 'Oct-1985' 'Oct-1986' 'Oct-1987' 'Oct-1988'  'Oct-1989' 'Oct-1990' 'Oct-1991' 'Oct-1992' 'Oct-1993' 'Oct-1994'  'Oct-1995' 'Oct-1996' 'Oct-1997' 'Oct-1998' 'Oct-1999' 'Oct-2000'  'Oct-2001' 'Oct-2002' 'Oct-2003' 'Oct-2004' 'Oct-2005' 'Oct-2006'  'Oct-2007' 'Oct-2008' 'Oct-2009' 'Oct-2010' 'Oct-2011' 'Oct-2012'  'Sep-1956' 'Sep-1959' 'Sep-1960' 'Sep-1962' 'Sep-1963' 'Sep-1964'  'Sep-1965' 'Sep-1966' 'Sep-1967' 'Sep-1968' 'Sep-1969' 'Sep-1970'  'Sep-1971' 'Sep-1972' 'Sep-1973' 'Sep-1974' 'Sep-1975' 'Sep-1976'  'Sep-1977' 'Sep-1978' 'Sep-1979' 'Sep-1980' 'Sep-1981' 'Sep-1982'  'Sep-1983' 'Sep-1984' 'Sep-1985' 'Sep-1986' 'Sep-1987' 'Sep-1988'  'Sep-1989' 'Sep-1990' 'Sep-1991' 'Sep-1992' 'Sep-1993' 'Sep-1994'  'Sep-1995' 'Sep-1996' 'Sep-1997' 'Sep-1998' 'Sep-1999' 'Sep-2000'  'Sep-2001' 'Sep-2002' 'Sep-2003' 'Sep-2004' 'Sep-2005' 'Sep-2006'  'Sep-2007' 'Sep-2008' 'Sep-2009' 'Sep-2010' 'Sep-2011']

Формат аналогичен столбцу issue_d, кодируем той же функцией convert_date:

encode_with_func(X, 'earliest_cr_line', convert_date)

initial_list_status — The initial listing status of the loan. Possible values are – W, F

— Некий параметр заема, оставляем

print np.unique(X['initial_list_status'])

['f' 'w']

initial_list_status_le_encoder = encode_with_LabelEncoder(X,'initial_list_status')

application_type — Indicates whether the loan is an individual application or a joint application with two co-borrowers

— Индикатор, заем брался одним человеком или в группе с кем-то. Стоит оставить

print np.unique(X['application_type'])

['INDIVIDUAL' 'JOINT']

application_type_le_encoder = encode_with_LabelEncoder(X,'application_type')

Теперь все признаки должны быть числовыми:

X.dtypes

loan_amnt                              float64 int_rate                               float64 installment                            float64 annual_inc                             float64 dti                                    float64 delinq_2yrs                            float64 inq_last_6mths                         float64 mths_since_last_delinq                 float64 open_acc                               float64 pub_rec                                float64 revol_bal                              float64 revol_util                             float64 total_acc                              float64 collections_12_mths_ex_med             float64 policy_code                            float64 acc_now_delinq                         float64 tot_coll_amt                           float64 tot_cur_bal                            float64 total_rev_hi_lim                       float64 is_title_known                           int64 is_delinq_occurs                         int64 term_le                                  int64 grade_le                                 int64 sub_grade_le                             int64 emp_length=1 year                      float64 emp_length=10+ years                   float64 emp_length=2 years                     float64 emp_length=3 years                     float64 emp_length=4 years                     float64 emp_length=5 years                     float64                                         ...    emp_length=n/a                         float64 home_ownership=ANY                     float64 home_ownership=MORTGAGE                float64 home_ownership=NONE                    float64 home_ownership=OTHER                   float64 home_ownership=OWN                     float64 home_ownership=RENT                    float64 verification_status=Not Verified       float64 verification_status=Source Verified    float64 verification_status=Verified           float64 issue_d_le                             float64 pymnt_plan_le                            int64 purpose=car                            float64 purpose=credit_card                    float64 purpose=debt_consolidation             float64 purpose=educational                    float64 purpose=home_improvement               float64 purpose=house                          float64 purpose=major_purchase                 float64 purpose=medical                        float64 purpose=moving                         float64 purpose=other                          float64 purpose=renewable_energy               float64 purpose=small_business                 float64 purpose=vacation                       float64 purpose=wedding                        float64 addr_state_le                            int64 earliest_cr_line_le                    float64 initial_list_status_le                   int64 application_type_le                      int64 dtype: object

X.shape

(200189, 65)

Признаки подготовлены

Для решения выбрал логистическую регрессию — она дает ответ в виде набора вероятностей и работает достаточно быстро

При решении применяем кросс-валидацию по 5 блокам с перемешиванием

Качеством считаем площадь под ROC-кривой — AUC-ROC величину

from sklearn.cross_validation import KFold from sklearn.metrics import roc_auc_score from sklearn.cross_validation import cross_val_score from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LogisticRegression  records_count = Y.count() kf = KFold(n=records_count, n_folds=5, shuffle=True)  def my_scorer(estimator, testX, testY):     predicted_testY = estimator.predict_proba(testX)[:, 1]     return roc_auc_score(testY, predicted_testY)  scaler = StandardScaler() scaledX = scaler.fit_transform(X)  def LogR_teach(C_value):     clf = LogisticRegression(penalty='l2', C=C_value)     return cross_val_score(clf, scaledX, Y, cv=kf, scoring=my_scorer).mean()  def check_quality_for_different_C():     for power in range(-4, 2):         C = math.pow(10, power)         quality = LogR_teach(C)         print 'C=', C, ', quality=', quality

check_quality_for_different_C()

C= 0.0001 , quality= 0.707991113828 C= 0.001 , quality= 0.709654648448 C= 0.01 , quality= 0.709779198127 C= 0.1 , quality= 0.709776206257 C= 1.0 , quality= 0.709775629602 C= 10.0 , quality= 0.709775731556

Лучшее качество 0.71 достигается при С=0.1

from sklearn.model_selection import train_test_split  X_train, X_test, y_train, y_test = train_test_split(scaledX, Y, test_size=.2, random_state=0) clf = LogisticRegression(penalty='l2', C=0.1) clf.fit(X_train, y_train) y_score = clf.predict_proba(X_test)[:, 1]

Чертим ROC-кривую:

import matplotlib.pyplot as plt from sklearn.metrics import roc_curve  plt.figure() line_width = 2 fpr, tpr, thresholds = roc_curve(y_test, y_score) plt.plot(fpr, tpr, color='darkorange', lw=line_width, label='LogRegression, C=0.1') plt.plot([0, 1], [0, 1], color='navy', lw=line_width, linestyle='--') plt.xlim([0.0, 1.0]) plt.ylim([0.0, 1.0]) plt.xlabel('False Positive Rate') plt.ylabel('True Positive Rate') plt.title('Receiver operating characteristic') plt.legend(loc="lower right") plt.show()

image

Вычислим предсказания для тестового набора

Загружаем данные:

test = pd.read_csv('test.csv', index_col='record_id')

Делаем действия аналогичные проделанным с train набором:

test['is_title_known'] = test['emp_title'].map(lambda x: 0 if x == 'n/a' else 1) test.drop('emp_title', axis=1, inplace=True)  test['is_delinq_occurs'] = test['mths_since_last_delinq'].map(lambda x: 0 if math.isnan(x) else 1) max_mths_since_last_delinq = np.nanmax(test.mths_since_last_delinq.values) test['mths_since_last_delinq'].fillna(max_mths_since_last_delinq, inplace=True)  test.fillna(0, inplace=True) test.isnull().sum()

loan_amnt                     0 term                          0 int_rate                      0 installment                   0 grade                         0 sub_grade                     0 emp_length                    0 home_ownership                0 annual_inc                    0 verification_status           0 issue_d                       0 loan_status                   0 pymnt_plan                    0 purpose                       0 zip_code                      0 addr_state                    0 dti                           0 delinq_2yrs                   0 earliest_cr_line              0 inq_last_6mths                0 mths_since_last_delinq        0 open_acc                      0 pub_rec                       0 revol_bal                     0 revol_util                    0 total_acc                     0 initial_list_status           0 collections_12_mths_ex_med    0 policy_code                   0 application_type              0 acc_now_delinq                0 tot_coll_amt                  0 tot_cur_bal                   0 total_rev_hi_lim              0 is_title_known                0 is_delinq_occurs              0 dtype: int64

— Пропусков нету, как и ожидаем

print test.shape

(66730, 36)

Подготовим нечисловые столбцы:

encode_with_existing_LabelEncoder(test, 'term', term_le_encoder) encode_with_existing_LabelEncoder(test, 'grade', grade_le_encoder) encode_with_existing_LabelEncoder(test, 'sub_grade', sub_grade_le_encoder)  test = encode_with_OneHotEncoder_using_existing_LabelEncoder_and_delete_column(test, 'emp_length', emp_length_le_encoder) test = encode_with_OneHotEncoder_using_existing_LabelEncoder_and_delete_column(test, 'home_ownership', home_ownership_le_encoder) test = encode_with_OneHotEncoder_using_existing_LabelEncoder_and_delete_column(test, 'verification_status', verification_status_le_encoder)  encode_with_func(test, 'issue_d', convert_date) encode_with_existing_LabelEncoder(test, 'pymnt_plan', pymnt_plan_le_encoder)  test = encode_with_OneHotEncoder_using_existing_LabelEncoder_and_delete_column(test, 'purpose', purpose_le_encoder)  test.drop(['zip_code'], axis=1, inplace=True) encode_with_existing_LabelEncoder(test, 'addr_state', addr_state_le_encoder) encode_with_func(test, 'earliest_cr_line', convert_date) encode_with_existing_LabelEncoder(test, 'initial_list_status', initial_list_status_le_encoder) encode_with_existing_LabelEncoder(test, 'application_type', application_type_le_encoder)  X.dtypes

loan_amnt                              float64 int_rate                               float64 installment                            float64 annual_inc                             float64 dti                                    float64 delinq_2yrs                            float64 inq_last_6mths                         float64 mths_since_last_delinq                 float64 open_acc                               float64 pub_rec                                float64 revol_bal                              float64 revol_util                             float64 total_acc                              float64 collections_12_mths_ex_med             float64 policy_code                            float64 acc_now_delinq                         float64 tot_coll_amt                           float64 tot_cur_bal                            float64 total_rev_hi_lim                       float64 is_title_known                           int64 is_delinq_occurs                         int64 term_le                                  int64 grade_le                                 int64 sub_grade_le                             int64 emp_length=1 year                      float64 emp_length=10+ years                   float64 emp_length=2 years                     float64 emp_length=3 years                     float64 emp_length=4 years                     float64 emp_length=5 years                     float64                                         ...    emp_length=n/a                         float64 home_ownership=ANY                     float64 home_ownership=MORTGAGE                float64 home_ownership=NONE                    float64 home_ownership=OTHER                   float64 home_ownership=OWN                     float64 home_ownership=RENT                    float64 verification_status=Not Verified       float64 verification_status=Source Verified    float64 verification_status=Verified           float64 issue_d_le                             float64 pymnt_plan_le                            int64 purpose=car                            float64 purpose=credit_card                    float64 purpose=debt_consolidation             float64 purpose=educational                    float64 purpose=home_improvement               float64 purpose=house                          float64 purpose=major_purchase                 float64 purpose=medical                        float64 purpose=moving                         float64 purpose=other                          float64 purpose=renewable_energy               float64 purpose=small_business                 float64 purpose=vacation                       float64 purpose=wedding                        float64 addr_state_le                            int64 earliest_cr_line_le                    float64 initial_list_status_le                   int64 application_type_le                      int64 dtype: object

print test.shape

(66730, 65)

scaled_test = scaler.transform(test)

clf = LogisticRegression(penalty='l2', C=0.1) clf.fit(scaledX, Y)

LogisticRegression(C=0.1, class_weight=None, dual=False, fit_intercept=True,           intercept_scaling=1, max_iter=100, multi_class='ovr', n_jobs=1,           penalty='l2', random_state=None, solver='liblinear', tol=0.0001,           verbose=0, warm_start=False)

prediction = clf.predict_proba(scaled_test)[:, 1]

Убедимся, что предсказанные вероятности находятся на отрезке [0,1] и не совпадают между собой (т.е. что модель не получилась константной):

print min(prediction), max(prediction)

0.0 0.999999996487

Преобразуем prediction в DataFrame:

result = pd.DataFrame(np.array(prediction), columns=['prob1'], index=test.index) print result[result['prob1']>0.5].count() print result.count()

prob1    711 dtype: int64 prob1    66730 dtype: int64

Немного странно конечно, что из 67К записей только у 0.7К вероятность больше 0.5

Сохраняем результаты:

result.to_csv('result.csv', encoding='utf8')

Приложение

Код здесь: https://bitbucket.org/andrei_punko/credit-scoring-test-task

Судя по информации в Интернете — качество 70-80% для скоринга считается хорошим, но все же есть сомнения, поэтому выслушаю предложения как можно было его улучшить

ссылка на оригинал статьи https://habrahabr.ru/post/324614/

О том, как мы начинали разрабатывать собственную систему управления проектами и что из этого получилось

… На дворе стояла середина жаркого лета 2013-го. В компанию Х устроился молодой и слегка зеленый сисадмин, с базовым пониманием об администрировании и еще более базовыми знаниями php и сопричастными mysql, html, css, js.

Компания та была пропитана модными веяниями и на понятие «ИСУП» (Информационная Система Управления Проектами), разве что не молились, полагая что с введением оной, польются молочные реки и по нажатию 1 кнопки любой заказ будет выполнен четко, качественно и полностью автоматически.

Но, в связи с некоторыми особенностями работы компании Х, «стандартные» системы из коробки, к с частью или к сожалению, не подходили и именно с этого момента началась эта история…

Как это было до

История

Я не знаю, когда и как было принято решение руководством компании о том, что заказ = проект, и работаем по принципам «проектного управления», но к тому моменту, когда я стал сисадмином, существовал уже целый свод условных правил, по которому работала компания. Самой примечательной особенностью сей работы было полное отсутствие общей, хм, назовем это «базы данных проектов (заказов)» и состояния по оным.

Когда приходил новый заказ, менеджер лез в гуглдокс в определенную табличку, в ней были номера заказов, методом «ручного авто инкремента» рождался новый номер проекта, который так же заносился в табличку, дабы в следующий раз не задвоить проект. Далее печаталась форма, с названием проекта, его номером… в общем, с необходимыми данными для опознания. После «стадии регистрации», проекту выдавалась персональная папка и он отправлялся дальше по отделам, кол-во распечатанных листочков с информацией о комплектации, работах, сроках, поставщиках постоянно заставляло прибавлять в весе и объеме папку проекта. После завершения работ по проекту или отказе клиента, папка, временами очень упитанная, отправлялась в архив, которым служил обычный шкаф.

Руководство ставило себе 2 стратегически важных задачи:

  1. Найти подходящую систему для управления проектами
  2. Получить прозрачность и четкое понимание в каком состоянии находится тот или иной проект и почему

Анализ ситуации и первые «hello world’ы»

Руководство четко понимало, что система бумажного обмена имела ряд неудобств: от банальной нехватки места под хранение, до злободневной потери листков (папок) проектов, что влекло за собой просрочки в проекте или вообще — отказы. С увеличением заказов, данные неудобства превращались в критические проблемы, требующие решения «еще вчера».

В результате, было принято решение о создании некоторого исполина, под названием «ИСУП» на базе MS Project. Была написана достаточно объемная (порядка 150-200 страниц) документация с описанием бизнес-процессов, что должны были бы происходить с проектом в процессе его «жизненного цикла».

Ценник данного внедрения был в районе шестизначной суммы: сервер + софт. Все лицензированное. Так как сумма была несколько не маленькая и покупка всего — задача не на 1 день, было решено хоть как-то облегчить текущую ситуацию. Поэтому, была реализована следующая идея: была заведена электронная таблица, в которой указывалось название проекта, стадии, что он прошел, ответственные за стадии и дата завершения/отказа. Таблицу актуализировал отдельный человек, собирая информацию с подобных табличек, что находились в каждом отделе. Благодаря данному «решению» ситуация более-менее стабилизировалось, количество «косяков» уменьшилось, но вместе с «решением» пришли и «бонусы» в виде не всегда актуальной информации в таблицах отделов по причине обоснованных или придуманных оправданий, но, как говорится, на безрыбье и рак — рыба.

Время шло, но внедрение project’а по разным причинам тормозилось, а «бонусы» от «решения» становились все более и более значимыми и болезненными. И тут появилась «светлая идея» в моей голове — что если сделать небольшую табличку в MySql:

image

Приделать к ней некоторый интерфейс, например, на php, и получим вполне не плохой список проектов, а главное — он будет доступен всем и в одно и тоже время, и даже если каждый будет изменять данные, за которые он ответственен, не будет ругани от электронной таблицы, что сейчас уже кто-то открыл таблицу и нельзя ничего сохранить. Дешево и сердито! После некоторых консольтаций со своим руководителем отдела, идея стала известна «верхам». Сказано, согласовано — сделано.

Оставался открытым вопрос по поводу доступов и кто, что должен был делать и где сейчас «находится» проект? В результате, «список» оброс функционалом.

Парочка снимков экрана из актуальной на тот момент, инструкции

image
image

Начиная с этого момента, руководство начало задумываться на тем, а нужно ли тратить шестизначную сумму на кота в мешке, если есть руководитель IT отдела и его подчиненный сисадмин, которые, буквально, на коленке и за пару дней сделали «уже столько».

В общем, с того момента был дан карт-бланш на разработку собственной системы управления проектами на основе бизнес-процессов, что были разработаны под «ИСУП» на project.

Как это работало и работает

Под собственную систему был выделен отдельный компьютер, на которым был поднят apache + php + mysql.

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

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

Однако, о реализации проекта, что на гите — расскажу.

База системы работает на mysql.

Вся система идеологически «вертится» вокруг проекта. Проект имеет уникальный номер, но, что если проект может повторится, то есть, клиент закажет еще раз ту же услугу? Для этого номера вынесены в отдельную таблицу tbl_project_num, в которой номер и количество повторений

image

Также в проекте есть тот, кто его создал, в настройках «по default» — менеджер. Менеджер — пользователь, у которого есть определенная роль и он состоит в определенной группе. Роли собираются в группы:

image

По факту, роль — специальность, а группа — отдел.

image

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

image

Итого получаем: проект со стадиями, и задачами, которые назначаются на основании стадии проекта.

Отдельно хочется упомянуть о планировании проекта:

image

Можно запланировать весь проект, потом его запустить и в полуавтоматическом режиме — ну, нажимать на кнопку «передать проект дальше» придется, а еще придется писать причину, если просрочена стадия. Сразу хочется вспомнить пожелание руководства «чтобы по 1 кнопке все было», хотя, на самом деле, оно так не работает в 99% случаев — всегда есть какое-то «но». Хотя, в нашем случае, это из-за особенностей бизнес-процессов фирмы/типа проектов, тут ничего пока не сделать…

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

image

Принцип работы достаточно прост: в базе «вешаются» триггеры на создание/изменение записи в задачах, проектах, комментариях к задачам, переписке в проектах, в общем, везде, откуда нужно получить оповещение. При срабатывании триггера в базу данных в определенную таблицу, пишется запись: что случилось, куда идти, кому показать:

image

А дальше это можно отобразить в журнале, а можно и по почте послать (работу с почтой в реализации на гите пока не реализовывал, там будет использование крона) и даже больше — настроить подписку, что слать на почту, а что вообще везде игнорировать (в реализации на гите пока не реализовывал).

На самом деле, тут можно развернуть целый цикл статей, как и почему реализовывался каждый отдельно взятый модуль. Упомянуть об интерфейсе и почему он выполнен именно так, хотя, я ни разу не дизайнер и мое мнение весьма субъективно. Сейчас бы мне хотелось чуть-чуть упомянуть еще о реализации на php.

Вся система написана на php с применением MVC (а куда без него?). Кто-то пилил свой первый форум, кто-то — гостевую книгу, а я вот — небольшой и легкий движок все пилю. «Киллер фитч», по отношению к другим движкам — нет. Особенности, как и везде: создание модуля, возможность создавать плагины, возможность писать модули без использования MVC — обычным скриптом, правда, с рядом оговорок — использование классов все равно присутствует: шаблонизатор, работа с бд, конфиги и т.д. Можно создавать и использовать отдельные билды: по сути, готовые сайты, переключая в настройках параметры и/или изменяя оные в сессии к сайту. Есть возможность работы с кроном прямо из коробки, но это, действительно, другая история.

Суммарный итог

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

Системные требования:

  1. apache/nginx/вебсервер, что работает с php
  2. PHP 7
  3. MySQL 5.х + / MariaDB 10.х +
  4. ~ 10 МБ свободного места на диске (без учета хранения файлов)

Примерную реализацию можно посмотреть/скачать по ссылке в самом конце статьи.
Я не могу назвать получившуюся систему (что боевую, что аналог, что выложен на гите) лучшей, но согласитесь, системные требования подкупают, да и бесплатность с открытым кодом — тоже.
На данный момент, поделка на гитхабе — хобби и отработка некоторого полученного опыта, понятно, что 1 человек по определению гору вряд ли сдвинет, но если не поможет кому-то в подобной ситуации, то, хотя бы даст направление.

Очень буду рад, если оказался полезен. Одновременно, прошу прощения за:

  • Скорее художественный, чем технический
  • Сумбурное изложение
  • Отсутствие, как такового, листинга с реализацией
  • за найденные ошибки

Буду очень рад обоснованной критике.

Сторонние ресурсы

P.S.: Если статья пройдет модерацию, очень хочу передать привет и сказать спасибо своему бывшему руководителю отдела — он точно меня узнает по скринам с инструкции и тексту. Без него я бы не научился тому, чему он, собственно, меня и научил, хотя или нехотя.

ссылка на оригинал статьи https://habrahabr.ru/post/324612/