Русский
Русский
English
Статистика
Реклама

Data science

Перевод Прогнозирование временных рядов на JS анализ данных для самых маленьких фронтендеров

10.06.2021 16:07:13 | Автор: admin

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

В чем суть?

На своей работе в качестве React Front-end разработчика я обычно работаю с дашбордами и различными видами данных. В какой-то момент нам понадобилось добавить предсказания по метрикам, а в команде не было специалистов по анализу данных, которые могли бы этим заняться.

Наш стек - это React + Java.

Проблема 1

Очень большой объем данных для предсказания и малое количество записей - тысячи возможных срезов данных, но малое количество исторических данных.

Проблема 2

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

Поэтому мы решили сделать предсказание рядов на стороне клиента - в браузере. Мы ж фронтендеры!

Проверим, что ряды вообще можно предсказать

Для этого загоним данные в эксель и посмотрим на результаты функции FORECAST.ETS(). Наши сезонные прогнозы выглядят правдоподобно. Мы проверили, что на наших данных реально получить что-то адекватное, поэтому можно теперь искать JS-либы для предсказаний!

Прогнозы рядов на JS

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

Я экспериментировал с моделью Tensorflow.js RNN из этой статьи, но она требует много времени для обучения на заданном наборе данных, сам набор данных должен быть достаточно большим, предсказание тоже не быстрое. Короче, нам она не подошла: у нас 1000+ рядов из 40-50 записей в каждом.

Быстро найти норм реализацию ARIMA в JS не удалось, зато нашли либу Nostradamus, где реализован алгоритм экспоненциациального сглаживания Холта-Уинтерса.

Найденная либа работает достаточно удобно:

predict = (    data,    a = 0.95,    b = 0.4,    g = 0.2,    p = this.PERIODS_TO_PREDICT,  ) => {    const alpha = a;    const beta = b;    const gamma = g;    const predictions = forecast(data, alpha, beta, gamma, this.OBSERVATIONS_PER_SEASON, p);    return predictions;  };

Функция Forecast возвращает массив элементов, где последние p элементов являются предсказанными значениями. Чисто и просто.

Но это не конец

Было бы как-то очень слабо закончить статью на этом месте. Добавлю подводные камни, которые замедлили интеграцию client-side предсказаний в проект:

  1. У этого алгоритма есть ограничение, которое может оказаться довольно весомым: мы не можем прогнозировать дальше, чем на количество элементов в одном сезоне. То есть если, к примеру, мы прогнозируем продажи книг по месяцам год к году, то в таком случае мы не можем предсказывать дальше, чем на 12 месяцев.

  2. Помимо пункта 1, у нас есть еще один лимит - у нас должно быть по меньшей мере 2 полных сезона с данными. Если взять тот же пример с книгами, то мы должны знать количество проданных книг в месяц за последние 24 месяца (2 года).

  3. Иногда бывает так, что в рамках проекта мы предсказываем разные метрики, которые, очевидно, друг с другом не связаны. А это значит, что коэффиценты (альфа/гамма/бета) от одной метрики не подойдут к другой и нам надо вычислять их динамически. В этом случае мы вычисляем значение ошибки для разных показателей и в конце выбираем набор с наименьшей ошибкой (сниппет с примером такого вычисления будет как бонус в конце статьи). Очевидно, что это влияет на производительность, но в нашем случае это было незначительно.

  4. Нам нужно такое количество записей, чтобы оно нацело делилось на размер сезона. Если сезон - это год, и в нем 12 записей (месяцев), то для прогноза нужно брать, например, 24/36/48 записей.

И еще одно: я не понял, в чем дело, но имея один набор исторических данных и разное количество записей, которые мы собираемся предсказать (например, есть история за 2 года, а предсказать хотим то на 3 месяца вперед, то на 12), мы получим разные прогнозы. Нам нужно было считать на 3 месяца вперед, поэтому я сделал еще один лайфхак - считал ошибку для обоих случаев и выбирал тот, в котором ошибка меньше.

Синяя линия - текущий гоод. Фиолетовая - предыдущий. Пунктир - прогноз.Синяя линия - текущий гоод. Фиолетовая - предыдущий. Пунктир - прогноз.Прогнозирование по трем независимым метрикам (пунктирные линии). Синяя линия - текущий год.Прогнозирование по трем независимым метрикам (пунктирные линии). Синяя линия - текущий год.

Бонус - код для расчета ошибок и подгона параметров

const adjustParams = (period) => {      const iter = 10;      const incr = 1 / iter;      let bestAlpha = 0.0;      let bestError = -1;      let alpha = bestAlpha;      let bestGamma = 0.0;      let gamma = bestGamma;      let bestDelta = 0.0;      let delta = bestDelta;      while (alpha < 1) {        while (gamma < 1) {          while (delta < 1) {            const pred = this.predict(data, alpha, delta, gamma, period);            const error = this.computeMeanSquaredError(data, pred);            if (error < bestError || bestError === -1) {              bestAlpha = alpha;              bestGamma = gamma;              bestDelta = delta;              bestError = error;            }            delta += incr;          }          delta = 0;          gamma += incr;        }        gamma = 0;        alpha += incr;      }      alpha = bestAlpha;      gamma = bestGamma;      delta = bestDelta;      return {        alpha,        gamma,        delta,        bestError,      };    };

Бонус 2

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


А какие вы задачи решали на стороне клиента? Напишите свою историю в комментариях!
Перевел: Даниил Охлопков.

Подробнее..

Как мы построили Computer Vision из подручных материалов, чтобы сделать гифки

17.06.2021 14:15:17 | Автор: admin

Меня зовут Денис Власов, я Data Scientist в Учи.ру. С помощью моделей машинного обучения из записей онлайн-уроков мы сделали гифки последовательность из нескольких кадров с наиболее яркими эмоциями учеников. Эти гифки получили их родители в e-mail-рассылке. Вместе с Data Scientist @DariaV Дашей Васюковой расскажем, как без экспертизы в Computer Vision, а только с помощью открытых библиотек и готовых моделей сделать MVP, в основе которого лежат low-res видео. В конце бонус виджет для быстрой разметки кадров.

Откуда у нас вообще возникла мысль распознавать эмоции? Дело в том, что мы в Учи.ру развиваем онлайн-школу Учи.Дома сервис персональных видео-уроков для школьников. Но поскольку такой урок это чисто человеческое взаимодействие, возникла идея прикрутить к нему немного аналитики. Такие данные могут помочь повысить конверсию, отследить эффективность уроков, замерить вовлеченность учеников и многое другое.

Если у вас, как и у нас, не стоит задача RealTime определения эмоций, можно пойти простым способом: анализировать записи уроков.

Маркеры начала и конца урока

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

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

Разбили видео на кадры

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

Научились детектировать детские улыбки (и не только)

На каждом кадре необходимо обнаружить лицо. Если оно там есть, проверить, родитель это или ученик, а также оценить эмоции на лице. И тут возникло несколько нюансов, которые пришлось учитывать.

Проблема 1. Распознавать лица на картинках низкого качества сложнее

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

Стандартный детектор DNN Face Detector из библиотеки OpenCV, который мы сначала взяли за основу, на наших данных давал неточные результаты. Оказалось, что алгоритм недостаточно хорошо справляется с реальными кадрами из видеочатов: иногда пропускает лица, которые явно есть в кадре, из двух лиц находил только одно или определял лица там, где их нет.

Стандартный детектор DNN Face Detector мог определить как лицо узор на занавеске, игрушечного медведя или даже композицию из картин на стене и стулаСтандартный детектор DNN Face Detector мог определить как лицо узор на занавеске, игрушечного медведя или даже композицию из картин на стене и стула

Поэтому мы решили попробовать обучить свой детектор. Для этого взяли реализацию RetinaNet-модели на PyTorch. В качестве данных для обучения подали результаты работы стандартного детектора и убедились, что новая модель учится находить лица. Затем подготовили обучающую и валидационную выборку, просматривая и при необходимости исправляя результаты работы детектора на новых кадрах: исправлять разметку работающей модели получается быстрее, чем отмечать лица на кадре с нуля.

Размечали итеративно: после добавления новой порции размеченных кадров мы заново обучали модель. А после проверки ее работы сохраняли разметку для новых кадров, наращивая обучающую выборку. Всего мы разметили 2624 кадра из 388 видеозаписей, на которых в сумме было 3325 лиц.

Таким образом удалось обучить более чувствительный в наших условиях детектор. В валидационной выборке из 140 кадров старый детектор нашел 150 лиц, а пропустил 38. Новый же пропустил только 5, а 183 обнаружил верно.

Проблема 2. В кадре присутствует не только ребенок

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

Мы обучили две отдельные модели. На момент эксперимента не было нужных публичных датасетов, поэтому данные для обучения мы разметили сами.

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

  • возраст людей на кадре с низким разрешением становится неочевидным;

  • дети присутствуют в кадре практически весь урок, а взрослые минуты.

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

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

Вторая модель должна была находить именно такие родительские плечи. Очевидно, что в этой задаче детектор лиц не применим, поэтому надо обучаться на кадрах целиком. Конечно, таких датасетов мы не нашли в публичном доступе и разметили около 250 000 кадров, на которых есть часть родителя, и кадры без них. Разметки на порядок больше, чем в других задачах, потому что размечать гораздо легче: можно смотреть не отдельные кадры, а отрезки видео и в несколько кликов отмечать, например, что вот эти 15 минут (900 кадров!) родитель присутствовал.

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

На верхнем графике вероятность присутствия родителя хотя бы плечом, а на нижнем вероятность того, что родитель смотрит в камеру, например, общается с преподавателемНа верхнем графике вероятность присутствия родителя хотя бы плечом, а на нижнем вероятность того, что родитель смотрит в камеру, например, общается с преподавателем

Проблема 3. Дети улыбаются по-разному

На практике оказалось, что не так уж просто понять, улыбается ребенок или нет. И если с улыбчивыми ребятами проблем нет, то детекция сдержанных улыбок оказывается нетривиальной задачей даже для человека.

За основу классификатора настроения мы взяли предобученную модель ResNet34 из библиотеки fast.ai. Эту же библиотеку использовали для дообучения модели в два этапа: сначала на публичных датасетах facial_expressions и SMILEsmileD с веселыми и нейтральными лицами, потом на нашем размеченном вручную датасете с кадрами с камер учеников. Публичные датасеты решили включить, чтобы расширить размер выборки и помочь модели более качественными изображениями, чем кадры видео с планшетов и веб-камер наших учеников.

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

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

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

  3. Аугментация. Позволяет в разы увеличить эффективный размер выборки и учесть особенности данных.

  4. Нормализация цветов с помощью CLAHE normalizer из библиотеки OpenCV. По ощущениям, такая нормализация лучше других вытягивает контраст на пересвеченных или темных изображениях.

Дообучаем модель для распознавания улыбок

1. Аугментации

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

  • Отражали изображение по горизонтали.

  • Поворачивали на случайную величину.

  • Применяли три разных искажения для изменения контраста и яркости.

  • Брали не всю картинку, а квадрат, составляющий не менее 60% от площади исходного изображения.

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

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

Пример аугментаций на одном изображении. Для наглядности аугментации сделаны до масштабирования к разрешению 64х64Пример аугментаций на одном изображении. Для наглядности аугментации сделаны до масштабирования к разрешению 64х64Код для аугментаций
# ! pip freeze | grep fastai# fastai==1.0.44import fastaiimport matplotlib.pyplot as pltfrom matplotlib import cmfrom matplotlib import colorsimport seaborn as sns%matplotlib inlinefrom pylab import rcParamsplt.style.use('seaborn-talk')rcParams['figure.figsize'] = 12, 6path = 'facial_expressions/images/'def _side_cutoff(    x,    cutoff_prob=0.25,    cutoff_intensity=(0.1, 0.25)):    if np.random.uniform() > cutoff_prob:        return x    # height and width    h, w = x.shape[1:]    h_cutoff = np.random.randint(        int(cutoff_intensity[0]*h), int(cutoff_intensity[1]*h)    )    w_cutoff = np.random.randint(        int(cutoff_intensity[0]*w), int(cutoff_intensity[1]*w)    )        cutoff_side = np.random.choice(        range(4),        p=[.34, .34, .16, .16]    ) # top, bottom, left, right.    if cutoff_side == 0:        x[:, :h_cutoff, :] = 0    elif cutoff_side == 1:        x[:, h-h_cutoff:, :] = 0    elif cutoff_side == 2:        x[:, :, :w_cutoff] = 0    elif cutoff_side == 3:        x[:, :, w-w_cutoff:] = 0    return x# side cutoff goes frist.side_cutoff = fastai.vision.TfmPixel(_side_cutoff, order=99)augmentations = fastai.vision.get_transforms(    do_flip=True,    flip_vert=False,    max_rotate=25.0,    max_zoom=1.25,    max_lighting=0.5,    max_warp=0.0,    p_affine=0.5,    p_lighting=0.5,        xtra_tfms = [side_cutoff()])def get_example():    return fastai.vision.open_image(        path+'George_W_Bush_0016.jpg',    )def plots_f(rows, cols, width, height, **kwargs):    [        get_example()        .apply_tfms(            augmentations[0], **kwargs        ).show(ax=ax)        for i,ax in enumerate(            plt.subplots(                rows,                cols,                figsize=(width,height)            )[1].flatten())    ]plots_f(3, 5, 15, 9, size=size)

2. Нормализация цвета

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

Пример нормализации цвета на изображениях из публичного датасетаПример нормализации цвета на изображениях из публичного датасетаКод для нормализации цвета
# pip freeze | grep opencv# > opencv-python==4.5.2.52import cv2import matplotlib.pyplot as pltfrom matplotlib import cmfrom matplotlib import colorsimport seaborn as sns%matplotlib inlinefrom pylab import rcParamsplt.style.use('seaborn-talk')rcParams['figure.figsize'] = 12, 6path = 'facial_expressions/images/'imgs = [    'Guillermo_Coria_0021.jpg',    'Roger_Federer_0012.jpg',]imgs = list(    map(        lambda x: path+x, imgs    ))clahe = cv2.createCLAHE(    clipLimit=2.0,    tileGridSize=(4, 4))rows_cnt = len(imgs)cols_cnt = 4imsize = 3fig, ax = plt.subplots(    rows_cnt, cols_cnt,    figsize=(cols_cnt*imsize, rows_cnt*imsize))for row_num, f in enumerate(imgs):    img = cv2.imread(f)    col_num = 0        img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)    ax[row_num, col_num].imshow(img, cmap='gray')    ax[row_num, col_num].set_title('bw', fontsize=14)    col_num += 1    img_normed = cv2.normalize(        img,        None,        alpha=0,        beta=1,        norm_type=cv2.NORM_MINMAX,        dtype=cv2.CV_32F    )    ax[row_num, col_num].imshow(img_normed, cmap='gray')    ax[row_num, col_num].set_title('bw normalize', fontsize=14)    col_num += 1        img_hist_normed = cv2.equalizeHist(img)    ax[row_num, col_num].imshow(img_hist_normed, cmap='gray')    ax[row_num, col_num].set_title('bw equalizeHist', fontsize=14)    col_num += 1        img_clahe = clahe.apply(img)    ax[row_num, col_num].imshow(img_clahe, cmap='gray')    ax[row_num, col_num].set_title('bw clahe_norm', fontsize=14)    col_num += 1        for col in ax[row_num]:        col.set_xticks([])        col.set_yticks([])plt.show()

В итоге мы получили модель, способную отличить улыбку от нейтрального выражения лица в с качеством 0.93 по метрике ROC AUC. Иными словами, если взять из выборки по случайному кадру с улыбкой и без, то с вероятностью 93% модель присвоит большую вероятность улыбки кадру с улыбающимся лицом. Этот показатель мы использовали для сравнения разных вариантов дообучения и пайплайнов. Но интуитивно кажется, что это достаточно высокий уровень точности: даже человек не всегда может определить эмоцию на лице другого человека. К тому же в реальности существует гораздо больше выражений лиц кроме однозначной радости и однозначной печали.

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

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

3. Увеличение объема выборки

На этом этапе у нас было около 5400 размеченных кадров, нам хотелось понять: достаточно ли такого объема для обучения. Мы разделили кадры на две подвыборки: половина для оценки качества (валидация), другая для обучения (трейн). Каждая группа состояла из кадров с разными учениками: если бы одни и те же лица попали в разные выборки, результаты оценки качества были бы завышены.

Мы несколько раз дообучили модель на публичных датасетах и подвыборках разного размера из трейна и проверили качество на валидационной выборке. На графике видно, что качество растет по мере увеличения объема подвыборки, поэтому мы разметили дополнительно еще 3 тыс. кадров: финальную модель обучали на выборке из 8500 кадров. Скорее всего и при таком объеме данных качество еще не начало выходить на плато, но перепроверять это мы не стали.

Такое упражнение называется построением learning curve, и оно в очередной раз подтверждает тезис: объем данных самое важное в модели машинного обучения.

Качество на отложенной выборке растет по мере увеличения выборки для дообученияКачество на отложенной выборке растет по мере увеличения выборки для дообучения

4. Картинки Google для обогащения выборки

Мы пробовали спарсить первые 1000 результатов картинок по запросам в духе happy, unhappy, smiling, neutral и т. д. Не ожидали получить данные высокого качества, поэтому планировали потом просмотреть их глазами и удалить совсем неподходящие. В итоге мы быстро поняли, что никакая фильтрация эти картинки не спасет, поэтому отказались от этой идеи совсем.

Примеры изображений по запросам happy и unhappyПримеры изображений по запросам happy и unhappy

В итоге мы получили четыре модели, которые с высокой точностью могли показать:

  • есть ли в кадре лицо;

  • с какой вероятностью этот человек улыбается;

  • ребенок это или взрослый;

  • есть ли в кадре взрослый, даже если мы не нашли лица.

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

Собрали гифку

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

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

Примеры итоговых GIF с улыбками нашей коллеги и ее детейПримеры итоговых GIF с улыбками нашей коллеги и ее детей

Что мы в итоге получили?

Исследование и эксперимент показали, что можно быстро и без глубокой экспертизы в Computer Vision научиться различать пользователей и их эмоции по видео (даже если оно плохого качества) на основе открытых библиотек и моделей.

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

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

Статистика дисконнектов. В этом уроке был единственный дисконнект на стороне ученикаСтатистика дисконнектов. В этом уроке был единственный дисконнект на стороне ученика

Другой пример трекинг настроения ученика на протяжении урока. Он позволяет проанализировать ход занятия и понять, нужно ли что-то менять в его структуре.

Виджеты

Все данные мы размечали сами и делали это довольно быстро (примерно 100 кадров в минуту). В этом нам помогали самописные виджеты:

  1. Виджет для разметки кадров с улыбками.

  2. Виджет для разметки кадров с детьми и взрослыми.

  3. Виджет для разметки кадров с плечом или локтем родителя.

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

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

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

Видео работы виджета

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

Код виджета
import pandas as pdimport numpy as npimport datetimeimport randomimport osimport ipywidgets as widgetsfrom IPython.display import displayfrom pathlib import Pathclass BulkLabeler():    def __init__(self, frames_path, annotations_path,                 labels = ['0', '1'],                 predict_fn = None,                 frame_width=120,                 num_frames = 27,                 face_width = 120,                 num_faces = 27,                 myname = '?',                 ):        self.predict_fn = predict_fn        self.labels = labels        self.frames_path = frames_path        self.frame_width = frame_width        self.num_frames = num_frames        self.face_width = face_width        self.num_faces = num_faces        self.myname = myname        self.faces_batch = []                # get annotations        self.annotations_path = annotations_path        processed_videos = []        if annotations_path.exists():            annotations = pd.read_csv(annotations_path)            processed_videos = annotations.file.str.split('/').str[-3].unique()        else:            with open(self.annotations_path, 'w') as f:                f.write('file,label,by,created_at\n')                # get list of videos        self.video_ids = [x for x in os.listdir(frames_path)                           if x not in processed_videos]        random.shuffle(self.video_ids)        self.video_ind = -1                self._make_video_widgets_row()        self._make_frames_row()        self._make_range_slider()        self._make_buttons_row()        self._make_faces_row()        self._make_video_stats_row()                display(widgets.VBox([self.w_video_row,                              self.w_frames_row,                              self.w_slider_row,                              self.w_buttons_row,                              self.w_faces_row,                              self.w_faces_label,                              self.w_video_stats]))        self._on_next_video_click(0)            ### Video name and next video button        def _make_video_widgets_row(self):        # widgets for current video name and "Next video" button        self.w_current_video = widgets.Text(            value='',            description='Current video:',            disabled=False,            layout = widgets.Layout(width='500px')            )                self.w_next_video_button = widgets.Button(            description='Next video',            button_style='info', # 'success', 'info', 'warning', 'danger' or ''            tooltip='Go to the next video',            icon='right-arrow'        )                self.w_video_row = widgets.HBox([self.w_current_video, self.w_next_video_button])                self.w_current_video.observe(self._on_video_change, names='value')        self.w_next_video_button.on_click(self._on_next_video_click)                        def _on_next_video_click(self, _):        while True:            self.video_ind += 1            current_video = self.video_ids[self.video_ind]            if next(os.scandir(self.frames_path/current_video/'student_faces'), None) is not None:                break        self.w_current_video.value = current_video                    def _on_video_change(self, change):        self.video_id = change['new']        self.frame_nums_all = sorted(int(f.replace('.jpg',''))                                      for f in os.listdir(self.frames_path/self.video_id/'student_src'))        start, stop = min(self.frame_nums_all), max(self.frame_nums_all)        self.w_range_slider.min = start        self.w_range_slider.max = stop        step = self.frame_nums_all[1] - self.frame_nums_all[0] if len(self.frame_nums_all)>1 else 1        self.w_range_start.step = step        self.w_range_stop.step = step        # change to slider value will cause frames to be redrawn        self.w_range_slider.value = [start, stop]               # reset faces        self.faces_df = None        self._reset_faces_row()        self.w_video_stats.value = f'Video {self.video_id}  no annotations yet.'                def _close_video_widgets_row(self):        self.w_current_video.close()        self.w_next_video_button.close()        self.w_video_row.close()        ### Video frames box        def _make_frames_row(self):        frame_boxes = []        self.w_back_buttons = {}        self.w_forward_buttons = {}        for i in range(self.num_frames):            back_button = widgets.Button(description='<',layout=widgets.Layout(width='20px',height='20px'))            self.w_back_buttons[back_button] = i            back_button.on_click(self._on_frames_back_click)            label = widgets.Label(str(i+1), layout = widgets.Layout(width=f'{self.frame_width-50}px'))            forward_button = widgets.Button(description='>',layout=widgets.Layout(width='20px',height='20px'))            self.w_forward_buttons[forward_button] = i            forward_button.on_click(self._on_frames_forward_click)            image = widgets.Image(width=f'{self.frame_width}px')            frame_boxes.append(widgets.VBox([widgets.HBox([back_button, label, forward_button]),                                              image]))                    self.w_frames_row = widgets.GridBox(frame_boxes,                                             layout = widgets.Layout(width='100%',                                                                     display='flex',                                                                     flex_flow='row wrap'))            def _on_frames_back_click(self, button):        frame_ind = self.w_back_buttons[button]        frame = int(self.w_frames_row.children[frame_ind].children[0].children[1].value)        start, stop = self.w_range_slider.value        self.w_range_slider.value = [frame, stop]            def _on_frames_forward_click(self, button):        frame_ind = self.w_forward_buttons[button]        frame = int(self.w_frames_row.children[frame_ind].children[0].children[1].value)        start, stop = self.w_range_slider.value        self.w_range_slider.value = [start, frame]            def _close_frames_row(self):        for box in self.w_frames_row.children:            label_row, image = box.children            back, label, forward = label_row.children            image.close()            back.close()            label.close()            forward.close()            box.close()        self.w_frames_row.close()                    ### Frames range slider                        def _make_range_slider(self):        self.w_range_start = widgets.BoundedIntText(                                        value=0,                                        min=0,                                        max=30000,                                        step=1,                                        description='Frames from:',                                        disabled=False,                                        layout = widgets.Layout(width='240px')                                    )        self.w_range_stop = widgets.BoundedIntText(                                        value=30000,                                        min=0,                                        max=30000,                                        step=1,                                        description='to:',                                        disabled=False,                                        layout = widgets.Layout(width='240px')                                    )        self.w_range_slider = widgets.IntRangeSlider(            value=[0, 30000],            min=0,            max=30000,            step=1,            description='',            disabled=False,            continuous_update=False,            orientation='horizontal',            readout=True,            readout_format='d',            layout=widgets.Layout(width='500px')        )        self.w_range_flip = widgets.Button(description='Flip range',            button_style='', # 'success', 'info', 'warning', 'danger' or ''            tooltip='Invert frames selection',            layout = widgets.Layout(width=f'{self.frame_width}px'),            icon='retweet'                                          )                self.w_range_slider.observe(self._on_slider_change, names='value')        self.w_range_start.observe(self._on_range_start_change, names='value')        self.w_range_stop.observe(self._on_range_stop_change, names='value')        self.w_range_flip.on_click(self._on_range_flip)        self.w_slider_row = widgets.HBox([self.w_range_start,                                          self.w_range_slider,                                          self.w_range_stop,                                          self.w_range_flip])        def _close_range_slider(self):        self.w_range_start.close()        self.w_range_stop.close()        self.w_range_slider.close()        self.w_range_flip.close()        self.w_slider_row.close()            def _on_range_flip(self, _):        start, stop = self.w_range_slider.value        left, right = self.w_range_slider.min, self.w_range_slider.max        if start==left and right==stop:            pass        elif start - left > right - stop:            self.w_range_slider.value=[left, start]        else:            self.w_range_slider.value=[stop, right]                                                   def _on_range_start_change(self, change):        new_start = change['new']        start, stop = self.w_range_slider.value        self.w_range_slider.value = [new_start, stop]                    def _on_range_stop_change(self, change):        new_stop = change['new']        start, stop = self.w_range_slider.value        self.w_range_slider.value = [start, new_stop]                    def _on_slider_change(self, change):        start, stop = change['new']        # update text controls        self.w_range_start.max = stop        self.w_range_start.value = start        self.w_range_stop.min = start        self.w_range_stop.max = self.w_range_slider.max        self.w_range_stop.value = stop        # show frames that fit current selection        frame_nums = [i for i in self.frame_nums_all if i>=start and i<=stop]        N = len(frame_nums)        n = self.num_frames        inds = [int(((N-1)/(n-1))*i) for i in range(n)]        # load new images into image widgets        for ind, box in zip(inds, self.w_frames_row.children):            frame_num = frame_nums[ind]            filename = self.frames_path/self.video_id/'student_src'/f'{frame_num}.jpg'            with open(filename, "rb") as image:                f = image.read()            label, image = box.children            label.children[1].value = str(frame_num)            image.value = f            ### Buttons row        def _make_buttons_row(self):        labels = list(self.labels)        if self.predict_fn is not None:            labels.append('model')        self.w_default_label = widgets.ToggleButtons(options=labels,                                                      value=self.labels[0],                                                      description='Default label:')                self.w_next_batch_button = widgets.Button(description='New batch',            button_style='info', # 'success', 'info', 'warning', 'danger' or ''            tooltip='Show next batch of faces from current frame range',            icon='arrow-right'        )        self.w_save_button = widgets.Button(description='Save labels',            button_style='success', # 'success', 'info', 'warning', 'danger' or ''            tooltip='Save current labels',            icon='check'        )        self.w_buttons_row = widgets.HBox([self.w_default_label, self.w_next_batch_button, self.w_save_button])        self.w_next_batch_button.on_click(self._on_next_batch_click)        self.w_save_button.on_click(self._on_save_labels_click)                def _close_buttons_row(self):        self.w_default_label.close()        self.w_next_batch_button.close()        self.w_save_button.close()        self.w_buttons_row.close()                def _on_next_batch_click(self, _):        if self.faces_df is None:             self._create_faces_df()        # select a sample from faces_df        start, stop = self.w_range_slider.value        subdf = self.faces_df.loc[lambda df: df.frame_num.ge(start)&                                             df.frame_num.le(stop)&                                             df.label.eq('')]        num_faces = min(len(subdf), self.num_faces)                if num_faces == 0:            self.faces_batch = []            self.w_faces_label.value = 'No more unlabeled images in this frames range'            self.w_faces_label.layout.visibility = 'visible'            for box in self.w_faces_row.children:                box.layout.visibility = 'hidden'        else:            self.w_faces_label.layout.visibility = 'hidden'            self.faces_batch = subdf.sample(num_faces).index            # if we have a model then we use it to sort images            if self.predict_fn is not None:                probs, labels = self._predict()                # sort faces according to probability                ind = sorted(range(len(probs)), key=probs.__getitem__)                self.faces_batch = [self.faces_batch[i] for i in ind]                labels = [labels[i] for i in ind]            # create labels for each face            if self.w_default_label.value != 'model':                labels = [self.w_default_label.value]*len(self.faces_batch)            # update faces UI            for facefile, label, box in zip(self.faces_batch, labels, self.w_faces_row.children):                image, buttons = box.children                with open(self.frames_path/facefile, "rb") as im:                    image.value = im.read()                buttons.value = label                box.layout.visibility = 'visible'            if len(self.faces_batch) < len(self.w_faces_row.children):                for box in self.w_faces_row.children[len(self.faces_batch):]:                    box.layout.visibility = 'hidden'        def _predict(self):        probs = []        labels = []        for facefile in self.faces_batch:            prob, label = self.predict_fn(self.frames_path/facefile)            probs.append(prob)            labels.append(label)        self.faces_df.loc[self.faces_batch, 'prob'] = probs        return probs, labels                    def _on_save_labels_click(self, _):        self.w_save_button.description='Saving...'                        with open(self.annotations_path, 'a') as f:            for file, box in zip(self.faces_batch, self.w_faces_row.children):                label = box.children[1].value                self.faces_df.at[file,'label'] = label                print(file, label, self.myname, str(datetime.datetime.now()),sep=',', file=f)                # update current video statistics        stats = self.faces_df.loc[self.faces_df.label.ne(''),'label'].value_counts().sort_index()        stats_str = ', '.join(f'{label}: {count}' for label, count in stats.items())        self.w_video_stats.value = f'Video {self.video_id}  {stats_str}.'                self.w_save_button.description = 'Save labels'        # ask for next batch        self._on_next_batch_click(0)            ### Faces row        def _make_faces_row(self):        face_boxes = []        for i in range(self.num_faces):            image = widgets.Image(width=f'{self.face_width}px')            n = len(self.labels)            toggle_buttons_width = int(((self.face_width-5*(n-1))/n))            toggle_buttons = widgets.ToggleButtons(options=self.labels,                                                    value=self.w_default_label.value,                                                    style=widgets.ToggleButtonsStyle(button_width=f'{toggle_buttons_width}px'))            face_boxes.append(widgets.VBox([image, toggle_buttons]))                    self.w_faces_row = widgets.GridBox(face_boxes,                                            layout = widgets.Layout(width='100%',                                                                    display='flex',                                                                    flex_flow='row wrap'))        self.w_faces_label = widgets.Label()        self._reset_faces_row()            def _close_faces_row(self):        for box in self.w_faces_row.children:            image, buttons = box.children            for w in [image, buttons, box]:                w.close()        self.w_faces_row.close()        self.w_faces_label.close()            def _reset_faces_row(self):        for box in self.w_faces_row.children:            box.layout.visibility = 'hidden'        self.w_faces_label.layout.visibility = 'visible'        self.w_faces_label.value = 'Press "New batch" button to see a new batch of faces'        self.faces_batch = []            ### Video statistics row        def _make_video_stats_row(self):        self.w_video_stats = widgets.Label('No video currently selected')        def _close_video_stats_row(self):        self.w_video_stats.close()                def _create_faces_df(self):        folder = Path(self.video_id,'student_faces')        df = pd.DataFrame({'file':[folder/f for f in os.listdir(self.frames_path/folder)]})        df['frame_num'] = df.file.apply(lambda x: int(x.stem.split('_')[0]))        df['label'] = '' #TODO maybe existing annotations?        df['prob'] = np.nan        df = df.sort_values(by='frame_num').set_index('file')        self.faces_df = df                    def close(self):        self._close_video_widgets_row()        self._close_frames_row()        self._close_range_slider()        self._close_buttons_row()        self._close_faces_row()        self._close_video_stats_row()
Подробнее..

5 условий зарождения искуственного интеллекта в индустрии

28.05.2021 16:10:14 | Автор: admin


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

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

В своей публикации я хотел бы поделиться приобретённым опытом индустриализации проекта на базе ИИ и сформулировать пять, на мой взгляд, неотъемлемых, жизненно важных условий его успешной реализации.

1. Единая команда с общим мышлением





На первых этапах проекта, когда происходит исследовательская работа, изучение доступных данных, знакомство с технологическим процессом, генерация идей и гипотез, представители бизнеса и разработчики должны стать единой командой с общим пониманием целей и решаемых задач. Важно именно на первых стадиях проекта, чтобы он стал своим для объединенной команды, чтобы каждый его участник в полной мере осознавал и разделял ответственность за свои действия и общий результат.
Как показывает практика реализации проектов в металлургической отрасли, наиболее продуктивными оказываются команды, в которые включены специалисты из смежных подразделений предприятия: производственники, технологи, экономисты, специалисты по ИТ и работе с данными. Абсолютно точно разработка проекта невозможна без сотрудников, которые непосредственно будут пользоваться цифровым сервисом: они впоследствии и должны стать владельцами продукта.

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

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

2. Переход к новой культуре технологических и бизнес-процессов





В ходе ряда исследований последних лет учёные выяснили, что при совершении одной и той же ошибки в прогнозах люди скорее перестают доверять алгоритму, чем человеку [1].
Да, люди склонны больше доверять себе подобным, потому что знают, как мы устроены, потому что примерно понимают логику поведения друг друга и легко могут представить себя на месте другого человека, спроецировать ситуацию.
Когда менеджеров первой линейки и среднего звена спросили, что побудило бы их доверять советам системы, 60 процентов выбрали вариант Чёткое понимание того, как работает система и как она генерирует совет, 55 процентов Система с проверенной репутацией, и 49 Система, которая объясняет свою логику [2].
Перед компаниями, которые берут курс на цифровизацию и переход на новый уровень построения технологических и бизнес-процессов за счёт внедрения систем ИИ, стоит сложная лидерская задача сформировать корпоративную культуру, способствующую пониманию целей, этапов, способов их проектирования и внедрения. Достичь этой цели непросто, поскольку многие люди, особенно те, кому непосредственно придётся взаимодействовать с ИИ, часто обеспокоены, что в конечном счёте машины могут занять их место, а они останутся ненужными и без собственного ремесла.
В рабочей среде необходимо сформировать понимание, что искусственный интеллект позволит не отвлекаться на отдельные задачи и направлен не на замену сотрудников, а на расширение их возможностей, перевод функционала на новый уровень, облегчение их работы и возможность сосредоточиться не на рутинных процедурах, а на вещах, по-настоящему нуждающихся в человеческом интеллекте.
Команда разработки, со своей стороны, должна освоить язык индустрии, максимально глубоко погрузиться в производственные и технологические процессы.
Крайне важно, чтобы люди, которые будут непосредственно пользоваться ИИ, понимали основные принципы его устройства и поведения, могли вносить коррективы в результаты его работы и чувствовали себя активными участниками разработки, чтобы у них было ощущение прозрачности и контроля системы. В идеале, конечно, системы ИИ необходимо проектировать так, чтобы они объясняли свои решения и помогали людям сохранять определенную автономию в принятии решения.

3. Экспериментирование с ИИ





Несколько раз в нашей практике бывало такое, что производственные бригады, которые работали с нашим сервисом, не выполняли его рекомендации или пытались его обмануть, потому что боялись получить нагоняй от своих начальников за возможное снижение показателей эффективности производства и повышенные производственные затраты (например, повышенный расход электроэнергии).
На этапах горячего тестирования системы ИИ важно создать максимально доверительную обстановку внутри объединённой команды, важно дать понять экспериментаторам, что отрицательный результат это тоже результат и порой он бывает даже более ценным, чем положительный. Тут необходимо быть максимально честными и не утаивать истинное положение дел. Где-то это сравнимо с приёмом у врача. У пациента не всегда бывает желание рассказывать обо всех своих симптомах и отклонениях по здоровью, он утаивает некоторые, а впоследствии лечение становится гораздо более длительным, дорогостоящим и сложным.
Соль в том, чтобы стать немножко стартапом и научиться быстро экспериментировать с цифровизацией в стиле стартапов. Их обычное правило: если получается, идём вперёд, если нет, пробуем новую идею. Каждый такой стартап это многоступенчатый процесс проработки и развития гипотезы от рождения, через проверку и превращение в рабочее решение, до получения бизнес-эффекта. Причем сотрудники, которые занимаются одной гипотезой, должны сопровождать ее от начала до конца [2].
Основной метрикой развития гипотезы должен стать бизнес-эффект, для которого важно построить модель расчета в самом начале проекта, при этом на каждом шаге данная модель актуализируется. Очевидные вначале источники эффекта для гипотезы могут оказаться бесперспективными, но по ходу реализации могут появиться новые идеи, и результат будет достигнут за счет них.

4. Важность налаженной и полной поставки данных





Прежде всего следует отметить, что качество данных играет важнейшую роль в системах искусственного интеллекта. По существу, данные это топливо для искусственного интеллекта. Больше всего времени и усилий уходит на сбор и изучение адекватного массива данных для формирования и апробации гипотез. Опыт говорит, что примерно 80 процентов времени реализации ИИ-инициатив занимает извлечение, доставка и предобработка данных, конструирование признаков, а не составление самих алгоритмов. Нужно объяснять бизнес-заказчику, насколько важно собрать качественный датасет и почему столько времени уходит на его создание.
По нашему опыту, на начальных стадиях проекта данных не будет совсем или их объем будет ограничен как с точки зрения глубины хранения, так и со стороны дискретности, они будут с большим количеством пропусков и общим низким уровнем качества. И с этим придется работать: придумывать, как строить модели, оперирующие неполными данными, как оптимально интерпретировать некачественные.
В нашей практике также встречались случаи, когда технологи или производственники убеждали нас в бесполезности целого ряда технологических данных, которые, по их мнению и опыту, никоим образом не могут влиять на формирование оценки или рекомендации системы ИИ. Однако на практике оказывалось, что именно эти, на первый взгляд, нецелевые данные, помогали ИИ улавливать самую тонкую зависимость технологического процесса, и прогностическая сила модели увеличивалась именно за счёт них.
Поэтому важно с самого начала проекта готовить полноценную площадку для беспрепятственной и бесперебойной доставки данных, они должны быть обширными, разнообразными, качественными и полезными. Не выстраивать цепочки для доставки отдельных данных для проверки конкретной гипотезы, а наладить процесс, который поможет быстро конфигурировать требуемый набор данных и будет иметь достаточный потенциал для наращивания объёма доставляемой информации.

5. Забег на длинную дистанцию





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

Заключение


Сегодня искусственный интеллект быстро проникает во большинство сфер индустрии и бизнеса. Новая реальность взаимодействия человека и машины требует переосмысления устоявшихся ранее технологических и бизнес-процессов. Цифровой взрыв уже произошёл и сейчас происходит расширение цифровой вселенной. Те, кто смогут понять физические законы нового мира, следовать периферии идущего цифрового фронта, не будут бояться экспериментировать и внедрять новые технологии, перейдут на новый качественный уровень эффективности бизнес-процессов в своей сфере деятельности.

Литература



  1. Человек+машина. Новые принципы работы в эпоху искусственного интеллекта / Пол Доэрти, Джеймс Уилсон; пер.с англ. Олега Сивченко, Натальи Яцюк; [науч. ред. М. Григорьева, А. Кучма, А. Епишев, Е. Кученева]. М.: Манн, Иванов и Фербер, 2019. 304 с.
  2. Индустрия Х.0. Преимущества цифровых технологий для производства / Эрик Шеффер: Пер. с англ. М.: Издательская группа Точка, 2019.-320 с.
Подробнее..

Перевод Многоразовый шаблон логирования на Python для всех ваших приложений в Data Science

14.05.2021 18:17:37 | Автор: admin

Идеальный способ отлаживать и отслеживать приложения хорошо определённые, информативные и удобно структурированные логи. Они являются необходимым компонентом любого малого, среднего или крупного проекта на любом языке программирования, не только на Python. Не используйте print() или корневой логгер по умолчанию, вместо этого настройте логирование на уровне проекта. К старту нового потока курса по Data Science, мы перевели статью, автор которой решил поделиться своим шаблоном для логирования. Не лишним будет сказать, что этот шаблон пришёлся по душе многим специалистам от дата-сайентистов профессионалов и до разработчиков ПО разного уровня.


Я стал работать с модулем логирования Python пару лет назад и с тех пор изучил бесчисленное количество руководств и статей в Интернете о том, как работать с ним эффективно, с наилучшей настройкой для своих проектов.

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

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

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

Приступим к делу!

Создадим простой проект на Python

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

Создайте папку под названием 'MyAwesomeProject'. Внутри неё создайте новый файл Python с именем app.py. Этот файл будет точкой старта приложения. Я буду использовать этот проект для создания простого рабочего примера шаблона, о котором говорю.

Откройте свой проект в VSCode (или в предпочитаемом редакторе). Теперь создадим новый модуль для настройки логирования на уровне приложения. Назовем его logger. С этой частью мы закончили.

Создаём логгер уровня приложения

Это основная часть нашего шаблона. Создадим новый файл logger.py. Определим корневой логгер и воспользуемся им для инициализации логгера уровня приложения. Настало время немного покодить. Несколько импортов и название нашего приложения:

qimport loggingimport sysAPP_LOGGER_NAME = 'MyAwesomeApp'

Функция, которую мы будем вызывать в нашем app.py:

def setup_applevel_logger(logger_name = APP_LOGGER_NAME, file_name=None):     logger = logging.getLogger(logger_name)    logger.setLevel(logging.DEBUG)    formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")    sh = logging.StreamHandler(sys.stdout)    sh.setFormatter(formatter)    logger.handlers.clear()    logger.addHandler(sh)    if file_name:        fh = logging.FileHandler(file_name)        fh.setFormatter(formatter)        logger.addHandler(fh)    return logger

Мы определим наш логгер с уровнем по умолчанию DEBUG и, чтобы структурировать сообщения логгера, воспользуемся Formatter. Затем присвоим его нашему обработчику, чтобы писать сообщения в консоль. Далее мы также обязательно включаем файл, в котором можем дополнительно хранить все наши сообщения лога. Это делается через логирование FileHandler. Наконец, мы возвращаем логгер.

Необходима ещё одна функция, которая гарантирует, что наши модули могут вызывать логгер, когда необходимо. Определите функцию get_logger.

def get_logger(module_name):       return logging.getLogger(APP_LOGGER_NAME).getChild(module_name)

Также, чтобы работать с модулем как с пакетом, по желанию мы можем создать папку logger и поместить в нее этот файл. Если мы сделаем это, нам также нужно будет включить в папку файл _init.py и написать такую строку:

from .logger import *

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

Устанавливаем логгер модульного уровня

Для лучшего понимания шаблона можно сделать простой модуль, чтобы протестировать логгер. Давайте определим простой module.py.

import loggerlog = logger.get_logger(__name__)def multiply(num1, num2): # just multiply two numbers    log.debug("Executing multiply function.")    return num1 * num2

Теперь этот модуль имеет доступ к логгеру и должен вывести сообщение с соответствующим именем модуля. Давайте проверим его.

Запустите сценарий и протестируйте логгер

Соорудим app.py.

import loggerlog = logger.setup_applevel_logger(file_name = 'app_debug.log')import mymodulelog.debug('Calling module function.')mymodule.multiply(5, 2)log.debug('Finished.')

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

Папка проектаПапка проекта

Наконец, запустите скрипт этой командой:

python3 app.py

Вы получите вывод вроде такого:

Структура каталогов также должна измениться: в ней должен появиться новый файл log. Проверим его содержимое.

Файл логовФайл логов

Заключение

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

А если вам интересна сфера Data Science и вы думаете поучиться по ссылке можете ознакомиться с программой курса и специализациями, которыми можно овладеть на нём.

Узнайте, как прокачаться и в других специальностях или освоить их с нуля:

Другие профессии и курсы
Подробнее..

Python, корреляция и регрессия часть 1

18.05.2021 14:13:42 | Автор: admin

Чем больше я узнаю людей, тем больше мне нравится моя собака.

Марк Твен

В предыдущих сериях постов из ремикса книги Генри Гарнера Clojure для исследования данных (Clojure for Data Science) на языке Python мы рассмотрели методы описания выборок с точки зрения сводных статистик и методов статистического вывода из них параметров популяции. Такой анализ сообщает нам нечто о популяции в целом и о выборке в частности, но он не позволяет нам делать очень точные утверждения об их отдельных элементах. Это связано с тем, что в результате сведения данных всего к двум статистикам - среднему значению и стандартному отклонению - теряется огромный объем информации.

Нам часто требуется пойти дальше и установить связь между двумя или несколькими переменными либо предсказать одну переменную при наличии другой. И это подводит нас к теме данной серии из 5 постов - исследованию корреляции и регрессии. Корреляция имеет дело с силой и направленностью связи между двумя или более переменными. Регрессия определяет природу этой связи и позволяет делать предсказания на ее основе.

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

О данных

В этой серии постов используются данные, любезно предоставленные компанией Guardian News and Media Ltd., о спортсменах, принимавших участие в Олимпийских Играх 2012 г. в Лондоне. Эти данные изначально были взяты из блога газеты Гардиан.

Обследование данных

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

Файл all-london-2012-athletes.tsv достаточно небольшой. Мы можем обследовать данные при помощи pandas, как мы делали в первой серии постов Python, исследование данных и выборы, воспользовавшись функцией read_csv:

def load_data(): return pd.read_csv('data/ch03/all-london-2012-athletes-ru.tsv', '\t') def ex_3_1(): '''Загрузка данных об участниках  олимпийских игр в Лондоне 2012 г.''' return load_data()

Если выполнить этот пример в консоли интерпретатора Python либо в блокноте Jupyter, то вы должны увидеть следующий ниже результат:

Столбцы данных (нам повезло, что они ясно озаглавлены) содержат следующую информацию:

  • ФИО атлета

  • страна, за которую он выступает

  • возраст, лет

  • рост, см.

  • вес, кг.

  • пол "М" или "Ж"

  • дата рождения в виде строки

  • место рождения в виде строки (со страной)

  • число выигранных золотых медалей

  • число выигранных серебряных медалей

  • число выигранных бронзовых медалей

  • всего выигранных золотых, серебряных и бронзовых медалей

  • вид спорта, в котором он соревновался

  • состязание в виде списка, разделенного запятыми

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

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

В первую очередь мы рассмотрим разброс роста спортсменов на Олимпийских играх 2012 г. в Лондоне. Изобразим эти значения роста в виде гистограммы, чтобы увидеть характер распределения данных, не забыв сначала отфильтровать пропущенные значения:

def ex_3_2(): '''Визуализация разброса значений  роста спортсменов на гистограмме''' df = load_data() df['Рост, см'].hist(bins=20) plt.xlabel('Рост, см.') plt.ylabel('Частота') plt.show()

Этот пример сгенерирует следующую ниже гистограмму:

Как мы и ожидали, данные приближенно нормально распределены. Средний рост спортсменов составляет примерно 177 см. Теперь посмотрим на распределение веса олимпийских спортсменов:

def ex_3_3(): '''Визуализация разброса значений веса спортсменов''' df = load_data() df['Вес'].hist(bins=20) plt.xlabel('Вес') plt.ylabel('Частота') plt.show()

Приведенный выше пример сгенерирует следующую ниже гистограмму:

Данные показывают четко выраженную асимметрию. Хвост с правой стороны намного длиннее, чем с левой, и поэтому мы говорим, что асимметрия - положительная. Мы можем оценить асимметрию данных количественно при помощи функции библиотеки pandas skew:

def ex_3_4(): '''Вычисление асимметрии веса спортсменов''' df = load_data() swimmers = df[ df['Вид спорта'] == 'Swimming'] return swimmers['Вес'].skew()
0.23441459903001483

К счастью, эта асимметрия может быть эффективным образом смягчена путем взятия логарифма веса при помощи функции библиотеки numpy np.log:

def ex_3_5(): '''Визуализация разброса значений веса спортсменов на полулогарифмической гистограмме с целью удаления  асимметрии''' df = load_data() df['Вес'].apply(np.log).hist(bins=20) plt.xlabel('Логарифмический вес') plt.ylabel('Частота') plt.show()

Этот пример сгенерирует следующую ниже гистограмму:

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

Логнормальное распределение

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

Логарифм показывает степень, в которую должно быть возведено фиксированное число (основание) для получения данного числа. Изобразив логарифмы на графике в виде гистограммы, мы показали, что эти степени приближенно нормально распределены. Логарифмы обычно берутся по основанию 10 или основанию e, трансцендентному числу, приближенно равному 2.718. В функции библиотеки numpy np.log и ее инверсии np.exp используется основание e. Выражение loge также называется натуральным логарифмом, или ln, из-за свойств, делающих его особенно удобным в исчислении.

Логнормальное распределение обычно имеет место в процессах роста, где темп роста не зависит от размера. Этот феномен известен как закон Джибрэта, который был cформулирован в 1931 г. Робертом Джибрэтом, заметившим, что он применим к росту фирм. Поскольку темп роста пропорционален размеру, более крупные фирмы демонстрируют тенденцию расти быстрее, чем фирмы меньшего размера.

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

С тех пор выяснилось, что закон Джибрэта применим к большому числу ситуаций, включая размеры городов и, согласно обширному математическому ресурсу Wolfram MathWorld, к количеству слов в предложениях шотландского писателя Джорджа Бернарда Шоу.

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

Визуализация корреляции

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

def swimmer_data(): '''Загрузка данных роста и веса только олимпийских пловцов''' df = load_data() return df[df['Вид спорта'] == 'Swimming'].dropna()def ex_3_6(): '''Визуализация корреляции между ростом и весом''' df = swimmer_data() xs = df['Рост, см'] ys = df['Вес'].apply( np.log ) pd.DataFrame(np.array([xs,ys]).T).plot.scatter(0, 1, s=12, grid=True) plt.xlabel('Рост, см.') plt.ylabel('Логарифмический вес') plt.show()

Этот пример сгенерирует следующий ниже график:

Результат ясно показывает, что между этими двумя переменными имеется связь. График имеет характерно смещенную эллиптическую форму двух коррелируемых, нормально распределенных переменных с центром вокруг среднего значения. Следующая ниже диаграмма сравнивает график рассеяния с распределениями вероятностей роста и логарифма веса:

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

Генерирование джиттера

Поскольку каждое значение округлено до ближайшего сантиметра или килограмма, то значение, записанное как 180 см, на самом деле может быть каким угодно между 179.5 и 180.5 см, тогда как значение 80 кг на самом деле может быть каким угодно между 79.5 и 80.5 кг. Для создания случайных искажений, мы можем добавить случайные помехи в каждую точку данных роста в диапазоне между -0.5 и 0.5 и в том же самом диапазоне проделать с точками данных веса (разумеется, это нужно cделать до того, как мы возьмем логарифм значений веса):

def jitter(limit): '''Генератор джиттера (произвольного сдвига точек данных)''' return lambda x: random.uniform(-limit, limit) + xdef ex_3_7(): '''Визуализация корреляции между ростом и весом с джиттером''' df = swimmer_data() xs = df['Рост, см'].apply(jitter(0.5)) ys = df['Вес'].apply(jitter(0.5)).apply(np.log) pd.DataFrame(np.array([xs,ys]).T).plot.scatter(0, 1, s=12, grid=True) plt.xlabel('Рост, см.') plt.ylabel('Логарифмический вес') plt.show()

График с джиттером выглядит следующим образом:

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

Ковариация

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

Если у нас имеется два ряда чисел, X и Y, то их отклонения от среднего значения составляют:

dx_i= x_i-x dy_i=y_i-y

Здесь xi это значение X с индексом i, yi значение Y с индексом i, x среднее значение X, и y среднее значение Y. Если X и Y проявляют тенденцию изменяться вместе, то их отклонения от среднего будет иметь одинаковый знак: отрицательный, если они меньше среднего, положительный, если они больше среднего. Если мы их перемножим, то произведение будет положительным, когда у них одинаковый знак, и отрицательным, когда у них разные знаки. Сложение произведений дает меру тенденции этих двух переменных отклоняться от среднего значения в одинаковом направлении для каждой заданной выборки.

Ковариация определяется как среднее этих произведений:

На чистом Python ковариация вычисляется следующим образом:

def covariance(xs, ys): '''Вычисление ковариации (несмещенная, т.е. n-1)''' dx = xs - xs.mean()  dy = ys - ys.mean() return (dx * dy).sum() / (dx.count() - 1)

В качестве альтернативы, мы можем воспользоваться функцией pandas cov:

df['Рост, см'].cov(df['Вес'])
1.3559273321696459

Ковариация роста и логарифма веса для наших олимпийских пловцов равна 1.356, однако это число сложно интерпретировать. Единицы измерения здесь представлены произведением единиц на входе.

По этой причине о ковариации редко сообщают как об отдельной сводной статистике. Сделать число более понятным можно, разделив отклонения на произведение стандартных отклонений. Это позволяет трансформировать единицы измерения в стандартные оценки и ограничить выход числом в диапазоне между -1 и +1. Этот результат называется корреляцией Пирсона.

Стандартная оценка, англ. standard score, также z-оценка это относительное число стандартных отклонений, на которые значение переменной отстоит от среднего значения. Положительная оценка показывает, что переменная находится выше среднего, отрицательная ниже среднего. Это безразмерная величина, получаемая при вычитании популяционного среднего из индивидуальных значений и деления разности на популяционное стандартное отклонение.

Корреляция Пирсона

Корреляция Пирсона часто обозначается переменной rи вычисляется следующим образом, где отклонения от среднего dxiи dyiвычисляются как и прежде:

Поскольку для переменных X и Y стандартные отклонения являются константными, уравнение может быть упрощено до следующего, где xи y это стандартные отклонения соответственно X и Y:

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

Ранее мы уже написали функции для вычисления стандартного отклонения. В сочетании с нашей функцией с вычислением ковариации получится следующая реализация корреляции Пирсона:

def variance(xs): '''Вычисление корреляции, несмещенная дисперсия при n <= 30''' x_hat = xs.mean() n = xs.count() n = n - 1 if n in range( 1, 30 ) else n  return sum((xs - x_hat) ** 2) / ndef standard_deviation(xs): '''Вычисление стандартного отклонения''' return np.sqrt(variance(xs))def correlation(xs, ys):  '''Вычисление корреляции''' return covariance(xs, ys) / (standard_deviation(xs) *  standard_deviation(ys))

В качестве альтернативы мы можем воспользоваться функцией pandas corr:

df['Рост, см'].corr(df['Вес'])

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

Правда, если r = 0, то с необходимостью вовсе не следует, что переменные не коррелируют. Корреляция Пирсона измеряет лишь линейные связи. Как продемонстрировано на следующих графиках, между переменными может существовать еще некая нелинейная связь, которую rне объясняет:

Отметим, что корреляция центрального примера не определена, потому что стандартное отклонение y = 0. Поскольку наше уравнение для rсодержало бы деление ковариации на 0, то результат получается бессмысленным. В этом случае между переменными не может быть никакой корреляции; yвсегда будет иметь среднее значение. Простое обследование стандартных отклонений это подтвердит.

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

def ex_3_8(): '''Вычисление корреляции средствами pandas на примере данных роста и веса''' df = swimmer_data() return df['Рост, см'].corr( df['Вес'].apply(np.log))
0.86748249283924894

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

Выборочный rи популяционный

Аналогично среднему значению и стандартному отклонению, коэффициент корреляции является сводной статистикой. Он описывает выборку; в данном случае, выборку спаренных значений: роста и веса. Коэффициент корреляции известной выборки обозначается буквой r, тогда как коэффициент корреляции неизвестной популяции обозначается греческой буквой (рхо).

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

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

  • Размера выборки

  • Величины r

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

Проверка статистических гипотез

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

В первую очередь, мы должны сформулировать две гипотезы, нулевую гипотезу и альтернативную:

H_0=0H_1\ne 0

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

H1 - это альтернативная возможность, что корреляция в популяции не нулевая. Отметим, что мы не определяем направление корреляции, а только что она существует. Это означает, что мы выполняем двустороннюю проверку.

Стандартная ошибка коэффициента корреляции rпо выборке задается следующей формулой:

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

Мы можем снова воспользоваться t-распределением и вычислить t-статистику:

В приведенной формуле df это степень свободы наших данных. Для проверки корреляции степень свободы равна n - 2, где n это размер выборки. Подставив это значение в формулу, получим:

В итоге получим t-значение 102.21. В целях его преобразования в p-значение мы должны обратиться к t-распределению. Библиотека scipy предоставляет интегральную функцию распределения (ИФР) для t-распределения в виде функции stats.t.cdf, и комплементарной ей (1-cdf) функции выживания stats.t.sf. Значение функции выживания соответствует p-значению для односторонней проверки. Мы умножаем его на 2, потому что выполняем двустороннюю проверку:

def t_statistic(xs, ys): '''Вычисление t-статистики''' r = xs.corr(ys) # как вариант, correlation(xs, ys) df = xs.count() - 2 return r * np.sqrt(df / 1 - r ** 2)def ex_3_9(): '''Выполнение двухстороннего t-теста''' df = swimmer_data() xs = df['Рост, см'] ys = df['Вес'].apply(np.log) t_value = t_statistic(xs, ys) df = xs.count() - 2  p = 2 * stats.t.sf(t_value, df) # функция выживания  return {'t-значение':t_value, 'p-значение':p}
{'p-значение': 1.8980236317815443e-106, 't-значение': 25.384018200627057}

P-значение настолько мало, что в сущности равно 0, означая, что шанс, что нулевая гипотеза является истинной, фактически не существует. Мы вынуждены принять альтернативную гипотезу о существовании корреляции.

Интервалы уверенности

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

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

Приведенный выше график показывает отрицательно скошенное распределение r-выборок для параметра , равного 0.6.

К счастью, трансформация под названием z-преобразование Фишера стабилизирует дисперсию r по своему диапазону. Она аналогична тому, как наши данные о весе спортсменов стали нормально распределенными, когда мы взяли их логарифм.

Уравнение для z-преобразования следующее:

Стандартная ошибка z равна:

Таким образом, процедура вычисления интервалов уверенности состоит в преобразовании rв z с использованием z-преобразования, вычислении интервала уверенности в терминах стандартной ошибки SEzи затем преобразовании интервала уверенности в r.

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

Мы можем убедиться в этом, воспользовавшись функцией scipy stats.norm.ppf. Она вернет стандартную оценку, связанную с заданной интегральной вероятностью в условиях односторонней проверки.

Однако, как показано на приведенном выше графике, мы хотели бы вычесть ту же самую величину, т.е. 2.5%, из каждого хвоста с тем, чтобы 95%-й интервал уверенности был центрирован на нуле. Для этого при выполнении двусторонней проверки нужно просто уменьшить разность наполовину и вычесть результат из 100%. Так что, требуемый уровень доверия в 95% означает, что мы обращаемся к критическому значению 97.5%:

def critical_value(confidence, ntails): # ДИ и число хвостов '''Расчет критического значения путем вычисления квантиля и получения  для него нормального значения''' lookup = 1 - ((1 - confidence) / ntails)  return stats.norm.ppf(lookup, 0, 1) # mu=0, sigma=1critical_value(0.95, 2)
1.959963984540054

Поэтому наш 95%-й интервал уверенности в z-пространстве для задается следующей формулой:

Подставив в нашу формулу zrи SEz, получим:

Для r=0.867и n=859она даст нижнюю и верхнюю границу соответственно 1.137 и 1.722. В целях их преобразования из z-оценок в r-значения, мы используем следующее обратное уравнение z-преобразования:

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

def z_to_r(z): '''Преобразование z-оценки обратно в r-значение''' return (np.exp(z*2) - 1) / (np.exp(z*2) + 1)def r_confidence_interval(crit, xs, ys):  '''Расчет интервала уверенности для критического значения и данных''' r = xs.corr(ys) n = xs.count() zr = 0.5 * np.log((1 + r) / (1 - r))  sez = 1 / np.sqrt(n - 3) return (z_to_r(zr - (crit * sez))), (z_to_r(zr + (crit * sez)))def ex_3_10(): '''Расчет интервала уверенности на примере данных роста и веса''' df = swimmer_data() X = df['Рост, см'] y = df['Вес'].apply(np.log) interval = r_confidence_interval(1.96, X, y)  print('Интервал уверенности (95%):', interval)
Интервал уверенности (95%): (0.8499088588880347, 0.8831284878884087)

В результате получаем 95%-й интервал уверенности для , расположенный между 0.850 и 0.883. Мы можем быть абсолютно уверены в том, что в более широкой популяции олимпийских пловцов существует сильная положительная корреляция между ростом и весом.

В следующем посте, посте 2, будет рассмотрена сама тема серии постов - регрессия и приемы оценивания ее качества.

Подробнее..

Python, корреляция и регрессия часть 4

19.05.2021 12:19:31 | Автор: admin

Предыдущий пост см. здесь.

Предсказание

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

9-кратный олимпийский чемпион по плаванию Марк Шпитц завоевал 7 золотых медалей на Олимпийских играх 1972 г. Он родился в 1950 г. и, согласно веб-страницы Википедии, имеет рост 183 см. и вес 73 кг. Посмотрим, что наша модель предсказывает в отношении его веса.

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

Матрица содержит коэффициенты для каждого из этих признаков:

Предсказанием модели будет сумма произведений коэффициентов и признаков xв каждой строке:

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

Напомним, что размерность итоговой матрицы будет числом строк из первой матрицы и числом столбцов из второй матрицы:

Здесь Tx это произведение матрицы размера 1 nи матрицы размера n 1. Результатом является матрица размера 1 1:

Исходный код вычислений очень прост:

def predict(coefs, x):     '''функция предсказания'''    return np.matmul(coefs, x.values) 
def ex_3_29():    '''Вычисление ожидаемого веса спортсмена'''    df = swimmer_data()    df['бин_Пол'] = df['Пол'].map({'М': 1, 'Ж': 0}).astype(int)     df['Год рождения'] = df['Дата рождения'].map(str_to_year)    X = df[['Рост, см', 'бин_Пол', 'Год рождения']]     X.insert(0, 'константа', 1.0)    y = df['Вес'].apply(np.log)     beta = linear_model(X, y)    xspitz = pd.Series([1.0, 183, 1, 1950]) # параметры Марка Шпитца    return np.exp( predict(beta, xspitz) )  
84.20713139038605

Этот пример вернет число 84.21, которое соответствует ожидаемому весу 84.21 кг. Это намного тяжелее зарегистрированного веса Марка Шпитца 73 кг. Наша модель, похоже, не сработала как надо.

Интервал уверенности для конкретного предсказания

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

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

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

Здесь yp это предсказание, плюс или минус интервал. Мы пользуемся t-распределением, где степень свободы равна n - p, т.е. размер выборки минус число параметров. Это та же самая формула, которая ранее применялась при вычислении F-тестов. Хотя указанная формула, возможно, пугает своей сложностью, она относительно прямолинейно транслируется в исходный код, показанный в следующем ниже примере, который вычисляет 95%-ый интервал предсказания.

def prediction_interval(x, y, xp):    '''Вычисление интервала предсказания'''    xtx    = np.matmul(x.T, np.asarray(x))    xtxi   = np.linalg.inv(xtx)      xty    = np.matmul(x.T, np.asarray(y))     coefs  = linear_model(x, y)     fitted = np.matmul(x, coefs)    resid  = y - fitted    rss    = resid.dot(resid)      n      = y.shape[0]  # строки    p      = x.shape[1]  # столбцы    dfe    = n - p     mse    = rss / dfe    se_y   = np.matmul(np.matmul(xp.T, xtxi), xp)    t_stat = np.sqrt(mse * (1 + se_y))         # t-статистика    intl   = stats.t.ppf(0.975, dfe) * t_stat       yp     = np.matmul(coefs.T, xp)    return np.array([yp - intl, yp + intl])

Поскольку t-статистика параметризуется степенью свободы ошибки, она принимает в расчет присутствующую в модели неопределенность.

Если вместо интервала предсказания потребуется рассчитать интервал уверенности для среднего значения, мы попросту можем опустить прибавление единицы к se_y при вычислении t-статистики t_stat.

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

В приведенном выше графике модель тренируется на выборке из 5 элементов и показывает, как 95%-й интервал предсказания увеличивается по мере нашего движения дальше от среднего роста. Применив приведенную выше формулу к Марку Шпитцу, в результате получим следующее:

def ex_3_30():    '''Интервал предсказания       применительно к данным о Марке Шпитце'''    df = swimmer_data()    df['бин_Пол'] = df['Пол'].map({'М': 1, 'Ж': 0}).astype(int)     df['Год рождения'] = df['Дата рождения'].map(str_to_year)    X = df[['Рост, см', 'бин_Пол', 'Год рождения']]     X.insert(0, 'константа', 1.0)    y = df['Вес'].apply(np.log)     xspitz = pd.Series([1.0, 183, 1, 1950])  # данные М.Шпитца    return np.exp( prediction_interval(X, y, xspitz) )
array([72.74964444, 97.46908087])

Этот пример возвращает диапазон между 72.7 и 97.4 кг., который как раз включает в себя вес Марка 73 кг., поэтому наше предсказание находится в пределах 95%-ого интервала предсказания. Правда оно лежит неудобно близко к границам диапазона.

Границы действия модели

Марк Шпитц родился в 1950 г., за несколько десятилетий до рождения самых возрастных пловцов на Олимпийских играх 2012 г. Пытаясь предсказать вес Марка, используя его год рождения, мы виноваты в том, что пытаемся экстраполировать слишком далеко за пределы наших тренировочных данных. Мы превысили границы действия нашей модели.

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

Согласно данным, в 1972 г. 22-летний Марк Шпитц имел рост 185 см. и весил 79 кг.

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

Следует отбирать признаки не только, опираясь на их предсказательную силу, но и на их актуальность для моделируемой области.

Окончательная модель

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

Модель произведет приблизительно с такими значениями:

Наши признаки для Марка на играх 1972 г. таковы:

Эти признаки можно использовать для предсказания его соревновательного веса при помощи приведенного ниже примера:

def ex_3_32():    '''Окончательная модель для предсказания        соревновательного веса'''    df = swimmer_data()    df['бин_Пол'] = df['Пол'].map({'М': 1, 'Ж': 0}).astype(int)     X = df[['Рост, см', 'бин_Пол', 'Возраст']]     X.insert(0, 'константа', 1.0)    y = df['Вес'].apply(np.log)     beta = linear_model(X, y)    # предсказать вес Марка Шпитца    xspitz = pd.Series([1.0, 185, 1, 22])     return np.exp( predict(beta, xspitz) )
78.46882772630318

Пример возвращает число 78.47, т.е. предсказывает вес 78.47 кг. Теперь результат находится очень близко к истинному соревновательному весу Марка, равному 79 кг.

Примеры исходного кода для этого поста находятся в моемрепона Github. Все исходные данные взяты врепозиторииавтора книги.

Резюме

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

Определив корреляцию между переменными, мы смогли построить предсказательную модель с использованием регрессии методом обычных наименьших квадратов и простых функций Python. Затем мы обобщили наш подход, воспользовавшись для этого матричной функциональностью библиотек pandas и numpy и нормальным уравнением. Эта простая модель продемонстрировала принципы машинного усвоения закономерностей путем определения модельных параметров , извлеченных из выборочных данных, которые могут использоваться для выполнения предсказаний. Наша модель смогла предсказать ожидаемый вес нового атлета, который полностью попадал в пределы интервала предсказания для истинного значения.

Подробнее..

Recovery mode Создаём компанию мечты нет хайпу

01.06.2021 00:14:15 | Автор: admin
Наверняка в вашей компании уже не раз появлялись ребята в дорогих костюмах и с хорошо подвешенным языком, увлекательно рассказывающие, что без современных айти-штучек компания не проживет и несколько лет!

Все эти data lake (болото данных), КХД (корпоративное кладбище данных), data mining (смотри, не подорвись), data governance (стань рабом своих данных) и им подобные не исчезают из их рассказов, периодически сменяя друг друга. Срок жизни очередного хайпа редко превышает год-два, но при желании для вас с большим удовольствием откопают любую почти забытую технологию.

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

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

image


Статья продолжает цикл:
Создаём компанию мечты: мастер-данные и интеграция
Создаём компанию мечты: управление качеством данных

Содержание


1. Big data: постановка проблемы
2. Мастер-данные: бессмертная классика
3. Как хранить данные: нужны ли КХД
4. Нормализация, или зачем вам болота данных
5. Почему дата-сайентист получает больше аналитика, а делает меньше?
6. Шина данных vs микросервисы
7. Как вообще не попасть на хайп?

1. Big data: постановка проблемы


Роль big data в развитии современной цивилизации впечатляющая. Но не по той причине, которая вам кажется.

Если интернет в каждой деревне и каждом телефоне появился благодаря порно и соцсетям (мессенджерам), то big data подарила триллионы долларов производителям жёстких дисков и оперативной памяти.

Проблема в том, что реальная польза от современной big data (в широком смысле слова) для всего человечества близка к пользе от порнографии, т.е. за несколькими исключениями нулевая!

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

Проблема, если быть точнее, в повторяемости результатов. Скажу по секрету, что скамейка запасных у продажников big data короткая. Если вы попросите их привести ещё несколько примеров, список закончится на втором десятке. Уверен, что мессенджеров и порносайтов они смогут назвать куда больше :) потому что их просто физически больше.

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

А General Electric сформировала своё конкурентное преимущество основываясь на методах математического анализа и статистики, которые можно найти в любом курсе математики для университета. Понятия big data тогда ещё не было.

Но на матанализе хайп не сделаешь, и слушать про двухсотлетние методы Фурье и Коши большие менеджеры вряд ли будут. Ведь там всё скучно, занудно, нужно много думать, и точно нет серебряной пули и волшебной таблетки.

Что же делать? Работать! Долго, скучно и уныло, стараясь создать такую атмосферу, которая поощряла бы активное думание. Как в канонических примерах от Bell Labs или той же GE. Это вполне возможно, более того, на это способны самые обычные люди, как вы и я, если их нужным образом замотивировать.

А начать нужно с

2. Мастер-данные: бессмертная классика


Мастер-данные это такой подход в структурировании информации, которая есть в компании. Если вы в какой-то момент обнаруживаете, что та или иная сущность используется одновременно в двух или большем количестве систем в вашей компании (например, список сотрудников на внутреннем сайте, в базе 1С-Бухгалтерии или CRM-системе), нужно выносить его в отдельную систему мастер-данных (MDM) и заставлять все системы пользоваться только этим справочником. По ходу понадобиться всем участникам договориться о нужных полях и атрибутах, а также придумать множество правил для контроля качества этих данных.

В среде дата-сайентистов младше 30 лет встречается убеждение, что окно для внедрения систем MDM началось примерно в 2008 и закончилось в районе 2012-15 годов. Что после этого появилось так много новых инструментов (всякие hadoop и spark), что заморачиваться с мастер-данными уже не нужно, не нужно ходить и договариваться с владельцами всех систем, думать о последствиях выбора архитектуры MDM и каждого конкретного реквизита в каждом справочнике.

К сожалению для них и к счастью для вас, это окно не закрылось. MDM-системы всё так же актуальны, как и системы бухучета или взаимодействия с клиентами. И думать и договариваться всё ещё нужно.

3. Как хранить данные: нужны ли КХД


Нет, корпоративные кладбища данных вам не нужны.

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

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

4. Нормализация, или зачем вам болота данных


Это раздел про data lake, или болото данных. Легенды гласят, что можно сваливать все данные без разбору в одну большую кучу. Не нужно приводить все данные к одному формату, не нужно нормализовать и вычищать их!

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

На практике самый ценный вывод, который вы сможете сделать из data lake что ваша компания почти не работает на январских каникулах.

И главный вопрос как какие-то жулики сумели убедить хоть кого-то в работоспособности этого подхода. Я склоняюсь к гипнозу :)

5. Почему дата-сайентист получает больше аналитика, а делает меньше?


Маркетинг, грамотная самопрезентация, максимум уверенности в себе. Не исключаю также гипноз :)

6. Шина данных vs микросервисы


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

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

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

Как показывает практика, любые две системы прекрасно интегрируются через микросервисы. Любые три хорошо. Любые пять терпимо, если очень тщательно всё задокументировать и обвешать автотестами.

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

image

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

7. Как вообще не попасть на хайп?


Вы познакомились с несколькими примерами хайпа, когда какой-то подход или технология могут оказаться бесполезными. И это с учётом того, что, по мировой статистике, доля успешно выполненных проектов по разработке и внедрению в IT редко превышает 40%.

Послевкусие от проваленных или бесполезных проектов может оказаться таким, что компания на время вообще откажется от IT-инициатив до тех пор, пока очередной влиятельный менеджер не оседлает очередной хайп.

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

у технологии есть большая скамейка запасных. Количество приводимых примеров успешного применения должно превышать пару десятков, и от них не должно возникать ощущения, что тут происходит какая-то магия;
технология должна проходить тест на бабушку (объяснение сути должны быть настолько понятным, что его осилит даже ваша бабушка повторю, никакой магии);
у технологии должен быть конкретный, оцифрованный список ачивок, которые в результате получит ваша компания. Внедренцы MDM, CRM или той же 1С-бухгалтерии могут часами рассказывать о пользе их решения на примере ваших конкретных задач. Внедренцы Big data вообще начинают рассказывать, что сначала соберём кучу данных, а потом посмотрим, что с ней делать;
и, наконец, технология должна быть фальсифицируемой (в смысле критерия Поппера), т.е. внедренец должен чётко понимать область её применения и актуальности и суметь привести доводы против (!) внедрения. Не нужно забивать гвозди микроскопом, и вообще, например, если у вас мало клиентов, нужна ли вам супер-пупер CRM?

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

Можете предложить ещё какие-нибудь критерии?
Приглашаю к обсуждению!
Подробнее..

Оценка кредитного портфеля на R

19.05.2021 18:19:31 | Автор: admin

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


Ниже приведен код на R с прототипом подхода. Не более одного экрана кода на прототип и никаких циклов (закладные для производительности и читаемости).


Декомпозиция


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


  1. Формирование тестовых данных.
  2. Расчет даты погашения каждого займа.
  3. Расчет и визуализация динамики для заданного временнОго окна.

Допущения и положения для прототипа:


  1. Гранулярность до даты. В одну дату только одна транзакция. Если в один день будет несколько транзакций, то надо будет их порядок устанавливать (для соблюдения принципа FIFO). Можно использовать доп. индексы, можно использовать unixtimestamp, можно еще что-либо придумывать. Для прототипа это несущественно.
  2. Явных циклов for быть не должно. Лишних копирований быть не должно. Фокус на минимальное потребление памяти и максимальную производительность.
  3. Будем рассматривать следующие группы задержек: "< 0", "0-30", "31-60", "61-90", "90+".

Шаг 1.


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


Генерация датасета
library(tidyverse)library(lubridate)library(magrittr)library(tictoc)library(data.table)total_users <- 100events_dt <- tibble(  date = sample(    seq.Date(as.Date("2021-01-01"), as.Date("2021-04-30"), by = "1 day"),    total_users * 10,    replace = TRUE)  ) %>%  # сделаем суммы кратными 50 р.  mutate(amount = (runif(n(), -2000, 1000)) %/% 50 * 50) %>%  # нашпигуем идентификаторами пользователей  mutate(user_id = sample(!!total_users, n(), replace = TRUE)) %>%  setDT(key = "date") %>%  # первая запись должна быть займом  .[.[, .I[1L], by = user_id]$V1, amount := abs(amount)] %>%  # для простоты оставим только одну операцию в день,   # иначе нельзя порядок определить и гранулярность до секунд надо спускать  # либо вводить порядковый номер займа и погашения  unique(by = c("user_id", "date"))

Шаг 2. Расчитываем даты погашения каждого займа


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


Расчет даты погашения
# инициализируем аккумуляторaccu_dt <- events_dt[amount < 0, .(accu = cumsum(amount), date), by = user_id]ff <- function(dt){  # на вход получаем матрицу пользователей и их платежей на заданную дату  # затягиваем суммы займов  accu_dt[dt, amount := i.amount, on = "user_id"]  accu_dt[is.na(amount) == FALSE, accu := accu + amount][accu > 0, accu := NA, by = user_id]  calc_dt <- accu_dt[!is.na(accu), head(date, 1), by = user_id]  # нанизываем обратно на входной data.frame, сохраняя порядок следования  calc_dt[dt, on = "user_id"]$V1}repay_dt <- events_dt[amount > 0] %>%  .[, repayment_date := ff(.SD), by = date] %>%  .[order(user_id, date)]

Шаг 3. Считаем динамику задолженности за период


Расчет динамики
calcDebt <- function(report_date){  as_tibble(repay_dt) %>%    # выкидываем все, что уже погашено на дату отчета    filter(is.na(repayment_date) | repayment_date > !! report_date) %>%    mutate(delay = as.numeric(!!report_date - date)) %>%    # размечаем просрочки    mutate(tag = santoku::chop(delay, breaks = c(0, 31, 61, 90),                               labels = c("< 0", "0-30", "31-60", "61-90", "90+"),                               extend = TRUE, drop = FALSE)) %>%    # делаем сводку    group_by(tag) %>%    summarise(amount = sum(amount)) %>%    mutate_at("tag", as.character)}# Устанавливаем окно наблюденияdf <- seq.Date(as.Date("2021-04-01"), as.Date("2021-04-30"), by = "1 day") %>%  tibble(date = ., tbl = purrr::map(., calcDebt)) %>%  unnest(tbl)# строим графикggplot(df, aes(date, amount, colour = tag)) +  geom_point(alpha = 0.5, size = 3) +  geom_line() +  ggthemes::scale_colour_tableau("Tableau 10") +  theme_minimal()

Можем получить примерно такую картинку.


Один экран кода, как и требовалось.


Предыдущая публикация Storytelling R отчет против BI, прагматичный подход.

Подробнее..

Запросить 100 серверов нельзя оптимизировать код. Ставим запятую

15.06.2021 20:16:32 | Автор: admin

Можно выделить ряд алгоритмов, которые являются базовыми и лежат в основе практически каждой строчки программ, написанных на языках высокого уровня. Хорошо иметь под руками классический многотомный труд Дональда Кнута "The Art of Computer Programming", там детально разобраны многие базовые алгоритмы. Но прочесть и усвоить все задача, требующая много усилий и времени, которая должна как-то быть мотивирована.


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


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


Является продолжением серии предыдущих публикаций.


Введение


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


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


  • case_id уникальный идентификатор кейса/прецедента;
  • record журнальная запись события в кейсе;
  • start время регистрации.

Используемые библиотеки


library(tidyverse)library(data.table)library(rTRNG)

Наиболее интересной задачей является генерация временных меток. События должны идти последовательно во времени для каждого кейса в отдельности. Сначала подготовим простую "рыбу". В частном случае мы возьмем для демонстрации малое число кейсов. В продуктиве их может быть 10^5-10^n, что определяется задачами.


Пример кода
# определим число кейсовnn <- 100# создаем первичный набор кейсовrecords <- c("first", "one and a half", "second", "third", "fourth",              "fifth", "sixth")# готовим два варианта для экспериментовdf <- tibble(case_id = 1:nn, recs = list(records)) %>%  unnest(recs)dt <- as.data.table(df)[, case_id := as.numeric(case_id)]# указание ключа приводит к физической сортировке данныхsetkey(dt, case_id)head(df, 10)

  # A tibble: 10 x 2     case_id recs                 <int> <chr>            1       1 first            2       1 one and a half   3       1 second           4       1 third            5       1 fourth           6       1 fifth            7       1 sixth            8       2 first            9       2 one and a half  10       2 second  

Теперь приступим к интересному блоку генерации временных меток. Для простоты задачи сведем ее к распределению долей в интервале [0; 1] в рамках каждого кейса. Перевод в реальный unixtimestamp оставим за пределами, это неинтересно. Варианты с явными циклами также за пределами. Времена исполнения приведены на условном компьютере, главное, что выполняется все на одном.


Создание одной временнОй метки


Вариант 1. Прямолинейный


Этот вариант предлагается в большинстве случаев. А что, все просто и понятно.


Пример кода
f1 <- function(df) {  df %>%    group_by(case_id) %>%    mutate(t_idx = sort(runif(n(), 0, 1))) %>%    ungroup()}

Получаем такие условные показатели. Наверное, неплохо. Но не забываем, что тут всего 100 кейсов.


  median `itr/sec` mem_alloc 15.38ms      63.2   284.9KB

Подумаем, что можно улучшить?


Вариант 1+1/2. Прямолинейный + быстрый генератор чисел


Есть хорошая библиотека rTRNG. На больших объемах она дает существенное ускорение, в том числе, за счет параллельной генерации. Просто проверим:


Пример кода
f1_5 <- function(df) {  df %>%    group_by(case_id) %>%    mutate(t_idx = sort(runif_trng(n(), 0, 1))) %>%    ungroup()}

  median `itr/sec` mem_alloc 29.34ms      29.5   284.9KB

На малых объемах не получили никакого выигрыша. Это все? Конечно же нет. Мы знаем, что tidyverse медленнее data.table, попробуем применить его. Но здесь мы попробуем применить первую хитрость отсортировать вектор времен по индексам, а потом его переприсвоить.


Вариант 2. Однопроходный, через индексы data.table


Пример кода
f2 <- function(dt) {  # здесь полагаемся на то, что мы заранее отсортировали уже по `case_id``  # формируем случайные числа и сортируем их по кейсам  vec <- dt[, t_idx := runif_trng(.N, 0, 1)][order(case_id, t_idx), t_idx]  # возвращаем сортированный   dt[, t_idx := vec]}

Получается вполне неплохо, ускорение раз в 15-20 и памяти требуется почти в три раза меньше.


  median `itr/sec` mem_alloc   1.69ms     554.      109KB 

Останавливаемся? А почему да?


Вариант 3. Однопроходный, через композитный индекс


На самом деле, как только мы сваливаемся в цикл, явный, или через by, мы резко просаживаемся в производительности. Попробуем сделать все за один проход. Идея следующая сделать композитный индекс, который позволил бы нам отсортировать все события одним махом. Используем трюк. Поскольку у нас внутри кейса все временные метки будут в диапазоне [0; 1], то мы можем разделить индекс на две части. Целочисленная часть будет содержать case_id, дробная часть временнУю долю. Однократная сортировка одного такого индекса сохранит принадлежность строчек case_id, при этом мы одним махом отсортируем значения внутри каждого кейса


Пример кода
f3 <- function(dt) {  # делаем трюк, формируем композитный индекс из case_id, который является монотонным, и смещением по времени  # поскольку случайные числа генерятся в диапазоне [0, 1], мы их утаскиваем в дробную часть (за запятую)  # сначала просто генерируем случайные числа от 0 до 1 для каждой записи отдельно   # и масштабируем одним вектором  dt[, t_idx := sort(case_id + runif_trng(.N, 0, 1, parallelGrain = 10000L)) - case_id]}

Запускаем и получаем выигрыш еще в 2 раза против предыдущего варианта, как по времени, так и по памяти.


  median `itr/sec` mem_alloc 826.7us    1013.     54.3KB

Вариант 3+1/2. Однопроходный, через композитный индекс, используем set


Останавливаемся? Можно и остановиться, хотя поле для сжатия еще есть. Дело в том, что при таких малых временах исполнения накладные расходы на NSE становятся весьма ощутимыми. Если использовать прямые функции, то можно получить куда лучший результат.


Пример кода
f3_5 <- function(dt) {  set(dt, j = "t_idx",       value = sort(dt$case_id + runif(nrow(dt), 0, 1)) - dt$case_id)}

Ускорение еще в 5 раз, памяти потребляем в 4 раза меньше


  median `itr/sec` mem_alloc 161.5us    5519.     16.3KB

Промежуточное подведение итогов


Соберем все вместе.


Тестируем
bench::mark(  f1(df),  f1_5(df),  f2(dt),  f3(dt),  f3_5(dt),  check = FALSE)

  expression      min   median `itr/sec` mem_alloc  <bch:expr> <bch:tm> <bch:tm>     <dbl> <bch:byt>1 f1(df)       14.3ms  15.38ms      63.2   284.9KB2 f1_5(df)    24.43ms  29.34ms      29.5   284.9KB3 f2(dt)       1.55ms   1.69ms     554.      109KB4 f3(dt)        722us  826.7us    1013.     54.3KB5 f3_5(dt)    142.5us  161.5us    5519.     16.3KB

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


Создание временнОй метки начала записи и окончания


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


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


Вариант 1. Прямолинейный


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


Пример кода
# Cоздание ЧЕТРЕХ колонок -- case_id, record, start, finish# Все как в предыдущем, только для каждого записи finish > start # и для двух последовательных записей 1, 2 в одном кейсе start_2 > finish_1 dt[, t_idx := NULL] # очистим хвосты предыдущего упражненияf1 <- function(df) {  df %>%    group_by(case_id) %>%    mutate(ts_idx = sort(runif(n(), 0, 1))) %>%    ungroup() %>%    # еще раз пройдемся генератором, используя время начала следующей записи как границу    # чтобы избежать NaN при переходе между кейсами (в случае max < min),     # принудительно выставим порог 1 в таких переходах, NA в последней строке тоже заменим на 1    mutate(tf_idx = {lead(ts_idx, default = 1) %>% if_else(. > ts_idx, ., 1)}) %>%    mutate(tf_idx = map2_dbl(ts_idx, tf_idx, ~runif(1, .x, .y)))}

В целом меньше секунды, но, очевидно, что это ОЧЕНЬ далеко от оптимальности.


  median `itr/sec` mem_alloc  28.16ms      30.7    2.06MB 

Вариант 2. Однопроходный, через композитный индекс и матрицы


Идея следующая. Нам нужно получить 2 колонки, в которых значения будут отсортированы строчка за строчкой и значение первой колонки не превышает значение второй колонки для каждой строки. Но это же матрица! Матрица это единый кусок в памяти, который сопровождается атрибутами сегментирования. Работа с ними куда быстрее классических датафреймов.


Пример кода
f2 <- function(dt){  dt[, c("ts_idx", "tf_idx") := {    # используем принцип vector recycling    x <- case_id + runif(2 * .N, 0, 1);    m <- matrix(sort(x), ncol = 2, byrow = TRUE) - case_id;    list(m[, 1], m[, 2])  }]}

В легкую сократили время и память почти в 30 раз! Да и код стал существенно проще и прямолинейнее.


  median `itr/sec` mem_alloc   1.04ms     733.    74.38KB 

Вариант 2+1/2. Однопроходный, через композитный индекс, матрицы и set


Пример кода
f2_5 <- function(dt){  x <- dt$case_id + runif(2 * nrow(dt), 0, 1)  m <- matrix(sort(x), ncol = 2, byrow = TRUE) - dt$case_id  set(dt, j = "ts_idx", value = m[, 1])  set(dt, j = "tf_idx", value = m[, 2])}

Перфекционизм в действии. Еще в 4 раза ускорили.


  median `itr/sec` mem_alloc  278.1us    2781.    57.55KB 

Промежуточное подведение итогов


Соберем все вместе.


Тестируем
bench::mark(  f1(df),  f2(dt),  f2_5(dt),  check = FALSE)

  median `itr/sec` mem_alloc  28.16ms      30.7    2.06MB   1.04ms     733.    74.38KB  278.1us    2781.    57.55KB 

Первичный наивный вариант также удалось бесплатно улучшить по производительности в 90 раз, а потребление памяти сократить в 35 раз.


Заключение


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


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


Предыдущая публикация Оценка структуры кредитного портфеля с помощью R.

Подробнее..

Сам себе Гутенберг. Делаем параллельные книги

16.05.2021 22:09:51 | Автор: admin

Lingtrain parallel books


Если вам нравится изучать языки (или вы их преподаете), то вы наверняка сталкивались с таким способом освоения языка как параллельное чтение. Он помогает погрузиться в контекст, увеличивает лексикон и позволяет получить удовольствие от обучения. Читать тексты в оригинале параллельно с русскоязычными, на мой взгляд, стоит, когда уже освоены азы грамматики и фонетики, так что учебники и преподавателей никто не отменял. Но когда дело все же доходит до чтения, то хочется подобрать что-то по своему вкусу, либо что-то уже знакомое или любимое, а это часто невозможно, потому что такого варианта параллельной книги никто не выпускал. А если вы учите не английский язык, а условный японский или венгерский, то трудно найти вообще хоть какой-то интересный материал с параллельным переводом.


Сегодня мы сделаем решительный шаг в сторону исправления этой ситуации.


Из чего делаем


На входе у нас будут два текстовых файла с оригинальным текстом и его переводом. Для примера возьмем книгу "Убить пересмешника" Харпер Ли на русском и английском языках.


Начало документов выглядит так (отрывки приведены в таком виде, в котором они были найдены в сети):


TO KILL A MOCKINGBIRDby Harper LeeDEDICATIONfor Mr. Lee and Alicein consideration of Love & AffectionLawyers, I suppose, were children once.Charles LambPART ONE1When he was nearly thirteen, my brother Jem got his arm badlybroken at the elbow. When it healed, and Jems fears of never beingable to play football were assuaged, he was seldom self-conscious abouthis injury. His left arm was somewhat shorter than his right; when hestood or walked, the back of his hand was at right angles to his body,his thumb parallel to his thigh. He couldnt have cared less, so long ashe could pass and punt.



Харпер ЛиУбить пересмешникаЮристы, наверно, тоже когда-то были детьми.Чарлз ЛэмЧАСТЬ ПЕРВАЯ1Незадолго до того, как моему брату Джиму исполнилось тринадцать, у него была сломана рука. Когда рука зажила и Джим перестал бояться, что не сможет играть в футбол, он ее почти не стеснялся. Левая рука стала немного короче правой; когда Джим стоял или ходил, ладонь была повернута к боку ребром. Но ему это было все равно - лишь бы не мешало бегать и гонять мяч.

Как делаем


Задача объемная, поэтому разобьем ее на три части:


  1. Подготовка текстов
  2. Выравнивание двух текстов по предложениям
  3. Создание книги

Начнем с выравнивания двух текстов, так как эта часть является центральной и крайние пункты опираются на нее.


Получение параллельного корпуса


Строго говоря, нам нужно получить параллельный корпус из двух текстов. Задача не так проста как кажется по ряду причин:


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

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


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


При подаче в программу текстов, произойдет следующее:


  • Текст сливается в одну строку.
  • Строка подчищается в зависимости от языка текста.
  • Строка разбивается по предложениям при помощи библиотеки razdel или регулярок.
  • Из каждой строки достается метаинформация, на основе специальных меток.

Метки нам понадобятся для того, чтобы при составлении книги из корпуса восстановить деление по частям и главам.
Давайте подготовим тексты для подачи в программу.


Подготовка текстов


Язык разметки


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


Метка Значение Установка
%%%%%title. Название произведения Вручную
%%%%%author. Автор Вручную
%%%%%h1. %%%%%h2. %%%%%h3. %%%%%h4. %%%%%h5. Заголовки Вручную
%%%%%divider. Разделитель Вручную
%%%%%. Новый абзац Автоматически

Метки абзацев


Метки абзацев будут проставлены автоматически по следующему правилу: если строка кончается на символ [.,:,!?] и перенос строки, то считаем такую строку концом абзаца.


Правила подготовки текста


  1. Удалить заведомо лишние строки (информацию об издателе, посвящение, номера страниц, примечания).
  2. Проставить метки для автора и названия.
  3. Проставить метки для заголовков (H1 самый большой, H5 самый маленький). Если заголовки не нужны, то просто удалите их.
  4. Убедиться, что в тексте нет строк, которые кончаются точкой и при этом не являются концом абзаца (иначе целый абзац разобьется в этом месте на два).

Расставьте метки руками в соответствии с правилами, пустые строки в данном случае роли не играют. Должны получиться документы, похожие на такие:


TO KILL A MOCKINGBIRD%%%%%title.by Harper Lee%%%%%author.%%%%%divider.PART ONE%%%%%h1.1%%%%%h2.When he was nearly thirteen, my brother Jem got his arm badlybroken at the elbow. When it healed, and Jems fears of never beingable to play football were assuaged, he was seldom self-conscious abouthis injury. His left arm was somewhat shorter than his right; when hestood or walked, the back of his hand was at right angles to his body,his thumb parallel to his thigh. He couldnt have cared less, so long ashe could pass and punt....



Харпер Ли%%%%%author.Убить пересмешника%%%%%title.%%%%%divider.ЧАСТЬ ПЕРВАЯ%%%%%h1.1%%%%%h2.Незадолго до того, как моему брату Джиму исполнилось тринадцать,у него была сломана рука. Когда рука зажила и Джим перестал бояться,что не сможет играть в футбол, он ее почти не стеснялся. Левая рука сталанемного короче правой; когда Джим стоял или ходил, ладонь была повернута кбоку ребром. Но ему это было все равно - лишь бы не мешало бегать игонять мяч....

Здесь и дальше все "главные" заголовки ("ЧАСТЬ ПЕРВАЯ", "ЧАСТЬ ВТОРАЯ" и т.д.) помечены меткой h1, номера глав помечены метками h2. Перед выравниваем все метки будут удалены из текста и будут использованы при создании книги.


Выравнивание


Colab блокнот


Чтобы выровнять свои тексты используйте вот этот Colab блокнот. Это интерактивный блокнот на питоне, в который вы можете вносить изменения и запускать в браузере. В нем есть инструкции и некоторые комментарии к процессу. В конце можно будет скачать получившуюся книгу как html страничку.


Скрипты


Здесь же давайте напишем небольшой скрипт, обсудим сложности и ограничения нашего подхода.
Установим библиотеку следующей командой:


pip install lingtrain-aligner

Импортируем необходимые компоненты:


from lingtrain_aligner import preprocessor, splitter, aligner, resolver, reader, vis_helper

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


text1_input = "harper_lee_ru.txt"text2_input = "harper_lee_en.txt"with open(text1_input, "r", encoding="utf8") as input1:  text1 = input1.readlines()with open(text2_input, "r", encoding="utf8") as input2:  text2 = input2.readlines()

Определим также путь до SQLite базы данных (это хранилище со всей необходимой для выравнивания информацией) и параметрами языка lang_from и lang_to. Эти параметры очень важны, так как они влияют на правила разбиения строк на предложения:


db_path = "db/book.db"lang_from = "ru"lang_to = "en"models = ["sentence_transformer_multilingual", "sentence_transformer_multilingual_labse"]model_name = models[0]

Получить список всех доступных языков можно следующей командой:


splitter.get_supported_languages()

Если нужного языка в списке пока нет, но он поддерживаются моделями, то используйте код xx, тогда к тексту будут применены стандартные правила фильтрации и разбиения на предложения. Модель sentence_transformer_multilingual работает быстрее и поддерживает 50+ языков, sentence_transformer_multilingual_labse поддерживает 100+ языков.


Добавим к текстам метки абзацев:


text1_prepared = preprocessor.mark_paragraphs(text1)text2_prepared = preprocessor.mark_paragraphs(text2)

Разобьем документы на строки:


splitted_from = splitter.split_by_sentences_wrapper(text1_prepared , lang_from, leave_marks=True)splitted_to = splitter.split_by_sentences_wrapper(text2_prepared , lang_to, leave_marks=True)

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


aligner.fill_db(db_path, splitted_from, splitted_to)

Теперь можно выровнять документы. Процесс выравнивания идет кусками с размером batch_size, вокруг каждого куска берется дополнительное количество строк размера window, чтобы гарантированно захватить необходимые строки. Модель берет заданное количество строк первого текста и подбирает в соответствующем фрагменте второго текста лучшие соответствия, используя векторные представления. На этом основывается первоначальное выравнивание. Для того, чтобы выровнять первые четыреста строк, выполним следующую команду.


batch_ids = [0,1,2,3]aligner.align_db(db_path, \                model_name, \                batch_size=100, \                window=30, \                batch_ids=batch_ids, \                save_pic=False,                embed_batch_size=50, \                normalize_embeddings=True, \                show_progress_bar=True                )

Результат выравнивания


Теперь можно посмотреть на результат первичного выравнивания! Это возможно благодаря тому, что в базе мы храним изначальные номера строк для выровненного корпуса. Воспользуемся модулем vis_helper. Так как строк у нас 400, то нарисуем все на одной картинке, задав параметр batch_size=400. Если указать, например, batch_size=50, то получим 4 картинки по-меньше.


vis_helper.visualize_alignment_by_db(db_path, output_path="alignment_vis.png", \                                    lang_name_from=lang_from, \                                    lang_name_to=lang_to, \                                    batch_size=400, \                                    size=(800,800), \                                    plt_show=True)

Первичное выравнивание


Посмотрим на картинку. Выравнивание предсказуемо идет от начала к концу, но есть конфликты. Основных причин две:


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

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


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


Меня зовут Уинстон Вульф. Я решаю проблемы.


Давайте теперь разбираться с шероховатостями. Глядя на картинку, мы видим, что есть непрерывные цепочки, есть разрывы и есть выбросы. Например, для предложений 10,11,12 модель подобрала предложения 15,16,17 из второго текста. Эта цепочка хорошая. Все, что находится между цепочками назовем конфликтом. При таком определении конфликта можно измерить его размер и подобрать стратегию разрешения. Логика по решению проблем находится в модуле resolver.


Для начала давайте посмотрим на все найденные конфликты:


conflicts_to_solve, rest = resolver.get_all_conflicts(db_path, min_chain_length=2, max_conflicts_len=6)

conflicts to solve: 46total conflicts: 47

При этом в переменную conflicts_to_solve попадут конфликты, которые соответствуют заданным параметрам поиска, а в переменную rest остальные.


Выведем на экран статистику:


resolver.get_statistics(conflicts_to_solve)resolver.get_statistics(rest)

('2:3', 11)('3:2', 10)('3:3', 8)('2:1', 5)('4:3', 3)('3:5', 2)('6:4', 2)('5:4', 1)('5:3', 1)('2:4', 1)('5:6', 1)('4:5', 1)('8:7', 1)

Видим, что чаще всего попадаются конфликты размера 2:3 и 3:2, это означает, что одно из предложений было переведено как два, либо два предложения были слиты в одно.


Посмотреть на конфликт можно следующей командой:


resolver.show_conflict(db_path, conflicts_to_solve[10])

124 Дом Рэдли стоял в том месте, где улица к югу от нас описывает крутую дугу.125 Если идти в ту сторону, кажется, вотвот упрешься в их крыльцо.126 Но тут тротуар поворачивает и огибает их участок.122 The Radley Place jutted into a sharp curve beyond our house.123 Walking south, one faced its porch; the sidewalk turned and ran beside the lot.

Видим, что строки 125 и 126 нужно бы сложить в одну, тогда правильное сопоставление выглядело бы как [124]-[122] и [125,126]-[123]. Как же научить этому программу? Так как она уже умеет выбирать лучший из предоставленных вариантов, то давайте ей их и предоставим. Конфликты у нас не очень большие, поэтому мы будем брать все возможные варианты разрешения конфликта, считать для них коэффициент похожести, суммировать и брать лучший. В данном случае это будет два варианта:


  1. [124,125]-[122] // [126]-[123]
  2. [124]-[122] // [125,126]-[123]

Что до стратегии выравнивания, то на данный момент лучше всего себя проявила такая, сначала ищем конфликты при минимальной длинне хорошей цепочки 2 (при таком параметре конфликтов найдется больше всего) и максимальной длинной конфликта не больше 6. Разрешаем все найденные конфликты, при этом большие конфликты становятся меньше так мы их частично разрешили. Затем увеличиваем оба параметра, ищем и снова разрешаем, добивая остатки.


Выглядит это так:


steps = 3batch_id = -1 #выровнять все доступные батчиfor i in range(steps):    conflicts, rest = resolver.get_all_conflicts(db_path, min_chain_length=2+i, max_conflicts_len=6*(i+1), batch_id=batch_id)    resolver.resolve_all_conflicts(db_path, conflicts, model_name, show_logs=False)    vis_helper.visualize_alignment_by_db(db_path, output_path="img_test1.png", batch_size=400, size=(800,800), plt_show=True)    if len(rest) == 0:        break

Результат после первого шага:


Разрешение конфликтов. Шаг 1


И после второго:


Разрешение конфликтов. Шаг 2


На выходе мы имеем файл book.db. Теперь мы можем перейти к созданию книги.


Конфликты на концах интервала


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


resolver.fix_start(db_path, model_name, max_conflicts_len=20)

и


resolver.fix_end(db_path, model_name, max_conflicts_len=20)

Книги и стили


За создание книжки отвечает модуль reader.


from lingtrain_aligner import reader

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


paragraphs_from, paragraphs_to, meta = reader.get_paragraphs(db_path, direction="from")

Параметр direction ["from", "to"] показывает на основе какого текста делить выравнивание на абзацы. Это дает нам возможность лучше подредактировать только один текст (например, русский) и на его основе сформировать книгу.


Теперь передадим данные в метод create_book():


reader.create_book(paragraphs_from, paragraphs_to, meta, output_path = f"lingtrain.html")

Получим вот такую книгу:



Это обыкновенная html страничка со встроенными стилями. В стили я добавил модификаторы, поэтому ее можно распечатать или сохранить как pdf, при этом шрифт с полями станут меньше.


Стилизация


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


reader.create_book(paragraphs_from, paragraphs_to, meta, output_path = f"lingtrain.html", template="pastel_fill")


reader.create_book(paragraphs_from, paragraphs_to, meta, output_path = f"lingtrain.html", template="pastel_start")


Шаблонов стилей пока немного, предлагайте свои кастомные стили, добавим их в библиотеку.


Кастомные стили


Зададим параметр template="custom" и передадим объект styles. Этот объект представляет из себя массив CSS стилей, которые будут применены к предложениям каждого абзаца циклически.


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


my_style = [    '{}',    '{"background": "#fafad2"}',    ]reader.create_book(paragraphs_from, paragraphs_to, meta, output_path = f"lingtrain.html", template="custom", styles=my_style)


Задавать можно любые применимые к span'ам стили:


my_style = [    '{"background": "linear-gradient(90deg, #FDEB71 0px, #fff 150px)", "border-radius": "15px"}',    '{"background": "linear-gradient(90deg, #ABDCFF 0px, #fff 150px)", "border-radius": "15px"}',    '{"background": "linear-gradient(90deg, #FEB692 0px, #fff 150px)", "border-radius": "15px"}',    '{"background": "linear-gradient(90deg, #CE9FFC 0px, #fff 150px)", "border-radius": "15px"}',    '{"background": "linear-gradient(90deg, #81FBB8 0px, #fff 150px)", "border-radius": "15px"}'    ]reader.create_book(paragraphs_from, paragraphs_to, meta, output_path = f"lingtrain.html", template="custom", styles=my_style)


Заключение


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


Поддержать проект можно на Patreon'e.


Ссылки


[1] Код lingtrain-aligner на github.


[2] Выровнять тексты в Google Colab.


[3] Sentence Transformers модели.


[4] Making Monolingual Sentence Embeddings Multilingual using Knowledge Distillation


[5] Language Agnostic BERT Sentence Encoder.

Подробнее..

Перевод Топ 6 библиотек Python для визуализации какую и когда лучше использовать?

20.05.2021 20:12:40 | Автор: admin

Перевод подготовлен в рамках курса "Machine Learning. Basic".

Всех желающих приглашаем на открытый онлайн-интенсив Data Science это проще, чем кажется. Поговорим об истории и основных вехах в развитии ИИ, вы узнаете, какие задачи решает DS и чем занимается ML. И уже на первом занятии вы сможете научить компьютер определять, что изображено на картинке. А именно, вы попробуете обучить свою первую модель машинного обучения для решения задачи классификации изображений. Поверьте, это проще, чем кажется!


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

Это руководство было дополнено несколькими подробными примерами. Вы также можете отслеживать актуальные версии этой статьи здесь.

Мотивация

Если вы только собираетесь начать работу с визуализацией в Python, количество библиотек и решений вас определенно поразит:

  • Matplotlib

  • Seaborn

  • Plotly

  • Bokeh

  • Altair

  • Folium

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

Мы будем использовать один и тот же набор данных, на примере которого будем рассматривать каждую библиотеку, уделяя особое внимание нескольким показателям:

Интерактивность

Хотите ли вы, чтобы ваша визуализация была интерактивной?

Визуализация в некоторых библиотеках, таких как Matplotlib, является простым статичным изображением, что хорошо подходит для объяснения концепций (в документе, на слайдах или в презентации).

Другие библиотеки, такие как Altair, Bokeh и Plotly, позволяют создавать интерактивные графики, которые пользователи могут изучать, взаимодействуя с ними.

Синтаксис и гибкость

Чем отличается синтаксис каждой библиотеки? Библиотеки низкого уровня, такие как Matplotlib, позволяют делать все, что вы захотите, но за счет более сложного API. Некоторые библиотеки, такие как Altair, очень декларативны, что упрощает построение графиков по вашим данным.

Тип данных и визуализации

Приходилось ли вам сталкиваться в работе с нестандартными юзкейсами, например, с географическим графиком, включающим большой набор данных или с типом графика, который поддерживается только определенной библиотекой?

Данные

Чтобы было проще сравнивать библиотеки, здесь представлены реальные данные с Github из этой статьи:

I Scraped more than 1k Top Machine Learning Github Profiles and this is what I Found

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

Вы можете скачать файл csv здесь, либо получите данные напрямую из Datapane Blob.

import datapane as dpdp.Blob.get(name='github_data', owner='khuyentran1401').download_df()

Не забудьте залогиниться со своим токеном авторизации в Datapane, если вы хотите использовать Blob. Это займет менее минуты.

Matplotlib

Matplotlib, вероятно, является самой популярной библиотекой Python для визуализации данных. Все, кто интересуется data science, наверняка хоть раз сталкивались с Matplotlib.

Плюсы

  1. Четко отображены свойства данных

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

Например, если я хочу быстро посмотреть распределение топ 100 пользователей с наибольшим количеством подписчиков, обычно Matplotlib мне будет вполне достаточно:

import matplotlib.pyplot as plttop_followers = new_profile.sort_values(by='followers', axis=0, ascending=False)[:100]fig = plt.figure()plt.bar(top_followers.user_name,       top_followers.followers)

Даже что-то вроде этого:

fig = plt.figure()plt.text(0.6, 0.7, "learning", size=40, rotation=20.,         ha="center", va="center",         bbox=dict(boxstyle="round",                   ec=(1., 0.5, 0.5),                   fc=(1., 0.8, 0.8),                   )         )plt.text(0.55, 0.6, "machine", size=40, rotation=-25.,         ha="right", va="top",         bbox=dict(boxstyle="square",                   ec=(1., 0.5, 0.5),                   fc=(1., 0.8, 0.8),                   )         )plt.show()

Минусы

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

Несмотря на то, что график достаточно хорошо подходит для визуализации распределений, если вы хотите презентовать его публике, вам нужно будет откорректировать оси X и Y, что потребует больших усилий, потому что Matplotlib имеет чрезвычайно низкоуровневый интерфейс.

correlation = new_profile.corr()fig, ax = plt.subplots()im = plt.imshow(correlation)ax.set_xticklabels(correlation.columns)ax.set_yticklabels(correlation.columns)plt.setp(ax.get_xticklabels(), rotation=45, ha="right",         rotation_mode="anchor")

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

Seaborn

Seaborn - это библиотека Python для визуализации данных, построенная на базе Matplotlib. Она более высокоуровневая, что упрощает ее использование.

Плюсы

  1. Меньше кода

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

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

correlation = new_profile.corr()sns.heatmap(correlation, annot=True)

Мы получаем лучший график пользовательской активности без возни x и y!

2. Делает стандартные графики красивее

Многие люди выбирают seaborn для создания широко используемых графиков, таких как столбчатые и прямоугольные диаграммы, расчетные графики, гистограммы и т. д., но не только потому, что это потребует меньше кода, они еще и визуально приятнее. Как видно на примере выше, цвета выглядят лучше, чем цвета по умолчанию в Matplotlib.

sns.set(style="darkgrid")titanic = sns.load_dataset("titanic")ax = sns.countplot(x="class", data=titanic)

Минусы

Seaborn более ограничен и не имеет такой широкой коллекции графиков, как matplotlib.

Вывод: Seaborn это версия Matplotlib более высокого уровня. Несмотря на то, что коллекция графиков не настолько большая, как в Matplotlib, созданные с помощью seaborn широко используемые графики (например, столбчатая диаграмма, прямоугольная диаграмма, график пользовательской активности и т. д.), при меньшем количестве кода будет выглядеть визуально приятнее.

Plotly

Python библиотека Plotly упрощает создание интерактивных графиков типографского качества. Он также может создавать диаграммы, аналогичные Matplotlib и seaborn, такие как линейные графики, точечные диаграммы, диаграммы с областями, столбчатые диаграммы и т. д.

Плюсы

  1. Похож на R

Если вы поклонник графиков в R и вам не хватает его функционала при переходе на Python, Plotly даст вам такое же качество графиков с использованием Python!

Мой любимая версия - Plotly Express, потому что с ней можно легко и быстро создавать отличные графики одной строчкой в Python.

import plotly.express as pxfig = px.scatter(new_profile[:100],          x='followers',          y='total_stars',          color='forks',          size='contribution')fig.show()

2. Простота создания интерактивных графиков

Plotly также упрощает создание интерактивных графиков. Интерактивные графики не только красиво выглядят, но и позволяют публике более внимательно изучить каждую точку на графике.

Помните столбчатую диаграмму, которую мы показывали ранее в matplotlib? Давайте посмотрим, как она получится с помощью Plotly

import plotly.express as pxtop_followers = new_profile.sort_values(by='followers', axis=0, ascending=False)[:100]fig = px.bar(top_followers,              x='user_name',              y='followers',            )fig.show()

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

3. Легко делать сложные графики

С помощью Plotly достаточно легко создавать сложные графики.

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

import plotly.express as pximport datapane as dplocation_df = dp.Blob.get(name='location_df', owner='khuyentran1401').download_df()m = px.scatter_geo(location_df, lat='latitude', lon='longitude',                 color='total_stars', size='forks',                 hover_data=['user_name','followers'],                 title='Locations of Top Users')m.show()

И, написав всего несколько строк кода, местоположения всех пользователей красиво представлены на карте. Цвет окружностей представляет количество форков, а размер - общее количество звезд.

Вывод: Plotly отлично подходит для создания интерактивных и качественных графиков при помощи всего нескольких строк кода.

Altair

Altair - это библиотека Python декларативной статистической визуализации, которая основана на vega-lite, что идеально подходит для графиков, требующих большого количества статистических преобразований.

Плюсы

1. Простая грамматика визуализации

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

Например, для данных о Титанике выше мы хотели бы подсчитать количество людей в каждом классе. Все, что нам нужно, это использовать count() в y_axis

import seaborn as snsimport altair as alt titanic = sns.load_dataset("titanic")alt.Chart(titanic).mark_bar().encode(    alt.X('class'),    y='count()')

2. Простота преобразования данных

Altair также упрощает преобразование данных при создании диаграммы.

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

hireable = alt.Chart(titanic).mark_bar().encode(    x='sex:N',    y='mean_age:Q').transform_aggregate(    mean_age='mean(age)',    groupby=['sex'])hireable

Логика здесь состоит в том, чтобы использовать transform_aggregate() для взятия среднего значения возраста (mean(age)) каждого пола (groupby=['sex']) и сохранить его в переменной mean_age). За ось Y мы берем переменную.

Мы также можем убедиться, что класс - это номинальные данные (категорийные данные в произвольном порядке), используя :N, или что mean_age - это количественные данные (меры значений, такие как числа), используя :Q.

Полный список преобразований данных можно найти здесь.

3. Связывание нескольких графиков

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

Например, мы хотим визуализировать количество людей из каждого класса в пределах значений, ограниченных выделенным интервалом в точечной диаграмме по возрасту и плате за проезд. Тогда нам нужно написать что-то вроде этого:

brush = alt.selection(type='interval')points = alt.Chart(titanic).mark_point().encode(    x='age:Q',    y='fare:Q',    color=alt.condition(brush, 'class:N', alt.value('lightgray'))).add_selection(    brush)bars = alt.Chart(titanic).mark_bar().encode(    y='class:N',    color='class:N',    x = 'count(class):Q').transform_filter(    brush)points & bars

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

Минусы

Если вы не задаете пользовательский стиль, простые диаграммы, такие как, например, столбчатые, не будут оформлены стилистически так же хорошо, как в seaborn или Plotly. Altair также не рекомендует использовать наборы данных с более чем 5000 экземплярами и рекомендует вместо этого агрегировать данные перед визуализацией.

Вывод: Altair идеально подходит для создания сложных графиков для отображения статистики. Altair не может обрабатывать данные, превышающие 5000 экземпляров, и некоторые простые диаграммы в нем уступают по стилю Plotly или Seaborn.

Bokeh

Bokeh - это интерактивная библиотека для визуализации, предназначенная для презентации данных в браузерах.

Плюсы

  1. Интерактивная версия Matplotlib

Если мы будем будем составлять топы интерактивных библиотек для визуализации, Bokeh, вероятно, займет первое место в категории сходства с Matplotlib.

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

Например, круговой график Matplotlib,

import matplotlib.pyplot as pltfig, ax = plt.subplots()x = [1, 2, 3, 4, 5]y = [2, 5, 8, 2, 7]for x,y in zip(x,y):     ax.add_patch(plt.Circle((x, y), 0.5, edgecolor = "#f03b20",facecolor='#9ebcda', alpha=0.8))#Use adjustable='box-forced' to make the plot area square-shaped as well.ax.set_aspect('equal', adjustable='datalim')ax.set_xbound(3, 4)ax.plot()   #Causes an autoscale update.plt.show()

который, в Bokeh, может быть создан с лучшим разрешением и функциональностью:

from bokeh.io import output_file, showfrom bokeh.models import Circlefrom bokeh.plotting import figurereset_output()output_notebook()plot = figure(plot_width=400, plot_height=400, tools="tap", title="Select a circle")renderer = plot.circle([1, 2, 3, 4, 5], [2, 5, 8, 2, 7], size=50)selected_circle = Circle(fill_alpha=1, fill_color="firebrick", line_color=None)nonselected_circle = Circle(fill_alpha=0.2, fill_color="blue", line_color="firebrick")renderer.selection_glyph = selected_circlerenderer.nonselection_glyph = nonselected_circleshow(plot)

2. Связь между графиками

В Bokeh также можно достаточно просто связывать графики. Изменение, примененное к одному графику, будет применено к другому графику с этой же переменной.

Например, если мы создаем 3 графика рядом и хотим наблюдать их взаимосвязь, мы можем связанное закрашивание

from bokeh.layouts import gridplot, rowfrom bokeh.models import ColumnDataSourcereset_output()output_notebook()source = ColumnDataSource(new_profile)TOOLS = "box_select,lasso_select,help"TOOLTIPS = [('user', '@user_name'),            ('followers', '@followers'),            ('following', '@following'),            ('forks', '@forks'),             ('contribution', '@contribution')]s1 = figure(tooltips=TOOLTIPS, plot_width=300, plot_height=300, title=None, tools=TOOLS)s1.circle(x='followers', y='following', source=source)s2 = figure(tooltips=TOOLTIPS, plot_width=300, plot_height=300, title=None, tools=TOOLS)s2.circle(x='followers', y='forks', source=source)s3 = figure(tooltips=TOOLTIPS, plot_width=300, plot_height=300, title=None, tools=TOOLS)s3.circle(x='followers', y='contribution', source=source)p = gridplot([[s1,s2,s3]])show(p)

Минусы

Поскольку Bokeh - это библиотека, которая имеет интерфейс среднего уровня, она часто требует меньше кода, чем Matplotlib, но требует больше кода для создания того же графика, чем Seaborn, Altair или Plotly.

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

Если мы не добавим ширину столбцов графика, то он будет выглядеть так:

from bokeh.transform import factor_cmapfrom bokeh.palettes import Spectral6p = figure(x_range=list(titanic_groupby['class']))p.vbar(x='class', top='survived', source = titanic_groupby,      fill_color=factor_cmap('class', palette=Spectral6, factors=list(titanic_groupby['class'])      ))show(p)

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

from bokeh.transform import factor_cmapfrom bokeh.palettes import Spectral6p = figure(x_range=list(titanic_groupby['class']))p.vbar(x='class', top='survived', width=0.9, source = titanic_groupby,      fill_color=factor_cmap('class', palette=Spectral6, factors=list(titanic_groupby['class'])      ))show(p)

Если вы хотите создать красивую столбчатую диаграмму, используя меньшее количеством кода, то для вас это может быть недостатком Bokeh по сравнению с другими библиотеками

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

Folium

Folium позволяет легко визуализировать данные на интерактивной встраиваемой карте. В библиотеке есть несколько встроенных тайлсетов из OpenStreetMap, Mapbox и Stamen

Плюсы

  1. Очень легко создавать карты с маркерами

Несмотря на то, что Plotly, Altair и Bokeh также позволяют нам создавать карты, Folium использует открытую уличную карту, что-то близкое к Google Map, с помощью минимального количества кода

Помните, как мы создавали карту для визуализации местоположения пользователей Github с помощью Plotly? Мы могли бы сделать карту еще лучше с помощью Folium:

import folium# Load datalocation_df = dp.Blob.get(name='location_df', owner='khuyentran1401').download_df() # Save latitudes, longitudes, and locations' names in a listlats = location_df['latitude']lons = location_df['longitude']names = location_df['location']# Create a map with an initial locationm = folium.Map(location=[lats[0], lons[0]])for lat, lon, name in zip(lats, lons, names):      # Create marker with other locations    folium.Marker(location=[lat, lon],                  popup= name,                  icon=folium.Icon(color='green')).add_to(m)    m

Живой вариант карты можно посмотреть в оригинале: https://towardsdatascience.com/top-6-python-libraries-for-visualization-which-one-to-use-fe43381cd658

2. Добавление потенциального местоположения

Если мы хотим добавить потенциальные местоположения других пользователей, Folium упрощает это, позволяя пользователям добавлять маркеры:

# Code to generate map here#....# Enable adding more locations in the mapm = m.add_child(folium.ClickForMarker(popup='Potential Location'))

Живой вариант карты можно посмотреть в оригинале: https://towardsdatascience.com/top-6-python-libraries-for-visualization-which-one-to-use-fe43381cd658

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

3. Плагины

У Folium есть ряд плагинов, которые вы можете использовать со своей картой, в том числе плагин для Altair. Что, если мы хотим увидеть карту пользовательской активности общего количества звездных пользователей Github в мире, чтобы определить, где находится большое количество пользователей Github с большим количеством звезд? Карта пользовательской активности в плагинах Folium позволяет вам это сделать:

from folium.plugins import HeatMapm = folium.Map(location=[lats[0], lons[0]])HeatMap(data=location_df[['latitude', 'longitude', 'total_stars']]).add_to(m)

Живой вариант карты можно посмотреть в оригинале: https://towardsdatascience.com/top-6-python-libraries-for-visualization-which-one-to-use-fe43381cd658

Уменьшите масштаб, чтобы увидеть полное отображение пользовательской активности на карте.

Вывод: Folium позволяет создавать интерактивную карту в несколько строк кода. Он дает вам ощущения близкие к использованию Google Map.

Заключение

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

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

Не стесняйтесь форкать и использовать код для этой статьи из этого репозитория на Github.

Мне нравится писать об основных концепциях data science и пробовать различные алгоритмы и инструменты анализа данных. Вы можете связаться со мной в LinkedIn и Twitter.

Отметьте этот репозиторий, если хотите изучить код всех статей, которые я писал. Следите за мной на Medium, чтобы быть в курсе моих последних статей по data science.


Узнать подробнее о курсе "Machine Learning. Basic"

Смотреть онлайн-интенсив Data Science это проще, чем кажется

Подробнее..

Как я предсказал LGD на хакатоне и устроился на работу

11.06.2021 14:22:51 | Автор: admin

Всем привет! Меня зовут Андрей, недавно яприсоединился ккоманде VSRobotics изанимаюсь проектом автопостроителя сценариев диалогов робота-оператора. Вэтом посте хочу поделиться историей своего трудоустройства ирешением задачи LGD prediction, которое мне вэтом очень помогло. Несекрет, что начинающим DS-специалистам приходится преодолевать серьезные трудности, чтобы получить начальную позицию. Мнеже повезло получить офер, поучаствовав всоревновании иминовав изнурительные интервью имуки сомнений всобственной компетенции. Надеюсь, мой рассказ будет полезен иобратит внимание новичков нахакатоны иконференции как наотличные инструменты для активного поиска работы.

Вступление прошлая жизнь ипервые шаги вData Science

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

Впериод начала пандемии уменя освободилось время засчет отсутствия поездок доофиса, которое удалось использовать для изучения основ языка Python. Постигать азы начал спрочтения первого тома Лутца Изучаем Python. Летом 2020я попрощался сработой, чтобы перезарядить батарейки иуйти вперестройку. Выбрал онлайн курс поData Science иначал учиться.

Для себя ясформулировал, что хочу работать вкрупной компании, чтобы быть частью большого сообщества после учебы поставил себе цель найти подходящую позицию вСбере. Яотслеживал информацию оконференциях имероприятиях, вкоторых компания участвует. Благодаря странице https://ict2go.ru/companies/19/, яузнал, что Сбер участвует вконференции ScoringDay Весна 2021 икэтой конференции приурочен хакатон наплощадке dsbattle.com под названием LGD Prediction. Призеры соревнования (топ-3) получают бесплатный билет наконференцию ивозможность присоединиться ккоманде блока Риски. Нучтож, вызов принят!

Тяжело вучении, легко вбою!

0. Дрожащими руками, терзаемый сомнениями смогули я?, открыл ссылку сbaseline-решением наколабе. Посмотрел. Смогу. Визуальное знакомство сданными показало, что вцелом такие задачки решать яумею. Обычные табличные данные, задача регрессии, призовем CatBoost. Отмечу, что натот момент опыта участия всоревнованиях, кроме как вТитанике наKaggle, уменя небыло.

Ниже разберу основные шаги решения, которые позволили занять заветную вторую строчку иотправиться вМоскву наконференцию. Также прикладываю ссылку наgithub иcolab.

Целью задания было построить модель машинного обучения, предсказывающую LGD (Loss Given Default), другими словами, тудолю отвыданного кредита, которую банк потеряет вслучае дефолта заемщика. Для оценки качества модели использовалась метрика MAE mean absolute error, средний модуль отклонений.

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

1. После знакомства сдатасетом яразбил 35признаков для обучения на2группы: первая группа 24признака это финансовые показатели (выручка, чистая прибыль, совокупные активы ит.п.), вторая группа 11признаков различные прочие характеристики предприятия (срок ведения бизнеса, величина уставного капитала, объект взалоге).

Далее важно было взглянуть нараспределение целевого признака LGD чтобы понять, счем мывообще имеем дело.

Распределение целевого признака имеет U-образную форму, тоесть, наиболее вероятные сценарии развития событий для банка что онлибо вернет всю сумму, либо неполучит ничего.

Наэтапе предобработки данных важно проверить данные напропуски. Вданном случае это был ключевой момент, так как вобучающей выборке только38% объектов имели значения всех признаков, ау60% вообще отсутствовали данные изгруппы признаков финансовых показателей, только прочие характеристики. Для тестовой выборки картина была схожей.

Кроме того, япроверял данные надубликаты построкам (ихнебыло) ипостолбцам (были).

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

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

Помог опыт участия вТитанике наKaggle создание хороших категориальных признаков помогло мне улучшить результат, поэтому ярешил, что этот трюк пройдет издесь.

Как уже можно было догадаться, наиболее очевидным вариантом разделения на2категории стало наличие/отсутствие финансовой отчетности. Разбив объекты, ясоставил сводную таблицу, чтобы убедиться всущественном различии целевого признака погруппам.

Заметно, что средний имедианный LGD укомпаний сфинансовыми данными существенно ниже, чем уоставшихся компаний.

Далее ярешил перейти кеще одному потенциально плодовитому наулучшение результата столбцу это объект взалоге. Данный признак содержал относительно неоднородные данные, там можно было увидеть квартира, Дом жилой, жилой дом, Скотомогильник..., автомобиль легковой иЛегковой автомобиль. Здесь напомощь пришел учебный опыт впервом моем проекте было задание выделить группы залога. Визуальный анализ значений позволял выделить три категории жилое здание, нежилое здание, автомобиль или поручительство. Поиском подстроки встроке эти категории яивыделил. Автомобиль ипоручительство пришлось объединить водну категорию, поскольку статистика вэтих группах отличалась несильно. Сводная таблица показала следующее:

Все пропуски вфинансовых данных язаполнил нулями.

Затем обратился кисследованию признаков выручка ивеличина уставного капитала. Пообоим столбцам япопытался выделить крупные компании вотдельную категорию. Исследовав медиану исредние при различных вариантах, остановился награнице в50млн руб. для выручки (больше категория corporation) и100тыс. руб. для уставного капитала (больше категория big).

Изсводных таблиц выше видно, что увыделенных категорий LGD существенно различаются.

Далее ярешил построить pairplot для признаков, которые есть увсех объектов это срок ведения бизнеса, срок смомента регистрации ОГРН, сгруппировав объекты покатегории залога.

Этот график позволил сделать следующие выводы:

  • срок ведения бизнеса достаточно тесно коррелирует с целевым признаком;

  • срок ведения бизнеса исрок смомента регистрации ОГРН доопределенного момента имеют тесную связь, нозначение второго ограничено сверху (поскольку пофизическому смыслу они выражают примерно одно итоже то, сколько компания существует, было принято решение остановиться наменее зашумленном признаке срок ведения бизнеса);

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

Здесь видно, что примерно после значения науровне 70срока ведения бизнеса LGD укредиторов, отдавших взалог жилое здание, падал донуля. Было только 4исключения. Ярешил ими пренебречь ивыделить такую группу вотдельную категорию безопасный заемщик.

Получилось 100 таких компаний вобучающей выборке.

3. Потом ярешил посоздавать количественные признаки наоснове данных финансовой отчетности. Нопоскольку таких объектов было меньшинство исами финансовые данные были зашумлены (совокупные активы небыли равны совокупным обязательствам икапиталу, например), результативность таких действий была низкой. Однако мне удалось выделить два признака, повысивших качество модели.

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

Так моя обучающая выборка содержала всего 9признаков: 4количественных и5категориальных. Ярешил использовать признак выручка (ar_revenue), так как онвзначительной степени характеризует величину компании иявляется определяющим для получения денежных потоков.

4. Затем яначал обучать модели. Перед градиентными алгоритмами япробовал ислучайный лес, ноего качество было ощутимо хуже. CatBoost показал себя лучше других, поэтому тюнил дальше его. Изначально обучал его изкоробки состратегией кросс-валидации на9разбиениях.

Среднее качество модели, рассчитанное таким образом, составило 0.086. Затем яобучил модель навсей обучающей выборке иполучил качество 0.066.

Ниже график ошибок фактический LGD предсказание для обучающей выборки.

Заметно, что самые серьезные ошибки возникали тогда, когда модель выдавала LGD = 1, когда это небыло нужно, инепредсказывала0, когда это было нужно.

Затем ясделал предсказания для тестовой выборки, исистема показала, что ихкачество около 0.087: налицо было переобучение, нопоиграв сколичеством итераций вCatBoost ирегуляризацией, ядобился лишь небольшого улучшения 0.086. Так или иначе, оценка качества модели, сделанной пометоду берем только необходимое, оказалась выше, чем модель отбрасываем ненужное.

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

Наиболее значимым является срок ведения бизнеса, который, как мыпомним, тесно коррелировал сlgd. Второе итретье места заняли созданные категории наличия отчетности ивида залога. Также сам параметр выручки (ar_revenue) оказался важен. Прочие созданные мной признаки были существенно менее важны, нобез них качество наоткрытой части тестовой выборки было ниже.

Тут надо отметить, что, являясь новичком всоревнованиях, ятолько впроцессе решения понял, что имеют ввиду организаторы, когда говорят, что итоговый результат будет оцениваться поприватной части выборки, аразмер еенеизвестен. Поэтому напишу важное наблюдение для такихже новичков: вомногих соревнованиях конечный результат определяется позакрытой части выборки! Участник делает предсказания для всей тестовой выборки, новоткрытом лидерборде его оценка рассчитывается только покакой-то доле отнее. Очевидно, делается это для того, чтобы человек несделал миллион вариантов предсказаний инеполучил простым перебором идеальный результат.

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

После открытия итогового лидерборда оценки ожидаемо снизились, астатус-кво сохранился яостался навторой строчке. Отлично, серебро тоже замечательный результат!

Большой итог

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

После всех докладов наконференции инаграждения (где мне также вручили приз засоответствие модели регуляторным требованиям), мне удалось приятно пообщаться спобедителем Артемом ируководителем направления вVSRobotics Александром. Выяснилось, что вкомпанию, посчастливому для меня стечению обстоятельств, нужны люди! После вопросов отом, знакомыли мне основные понятия изобласти обработки естественного языка ивообще интересноли мне это, мыобменялись контактами, ияотправил свое резюме ипортфолио нарассмотрение. Спустя некоторое время мне перезвонили, сделали предложение, ияприсоединился ккоманде VSRobotics!

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

  1. Считаю, что мне вомногом повезло ссоревнованием вчастности, там было мало участников только у45человек результат оказался выше baseline. Правда, наверное, главное все-таки неколичество, акачество.

  2. Повезло также ивтом, что соревнование соприкасалось сдоменной областью, которая была мне знакома. Знание небольших тонкостей финансового анализа позволило улучшить качество модели. Поэтому всем начинающим свой путь data scientistам, которые меняют профессию, рекомендовалбы смело проявлять свои сильные стороны, заключающиеся вхорошем знании какой-либо области, иискать близкие имподуху соревнования ипроекты.

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

  4. Лучше мало хороших признаков, чем много плохих.

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

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

Спасибо, что дочитали доконца! Буду рад выслушать конструктивную критику решения иответить навопросы!

Подробнее..

Перевод Путеводитель по базам данных в 2021г

04.06.2021 20:14:20 | Автор: admin

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

Основные понятия баз данных

Что такое данные?

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

Что такое база данных?

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

Зачем нужна база данных?

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

Система управления базами данных (СУБД)

Итак, мы уже знаем, что данные и базы данных важны, но как осуществляется работа с базами данных в компьютерных системах? Вот тут на сцену и выходит СУБД. СУБД это программное обеспечение, предоставляющее нам способ взаимодействия с базами данных на компьютере для выполнения различных операций, таких как создание, редактирование, вставка данных и т.д. Для этого СУБД предоставляет нам соответствующие API. Редко какие программы не используют СУБД для работы с данными, хранящимися на диске.Помимо операций с данными СУБД также берет на себя резервное копирование, проверку допуска, проверку состояния базы данных и т.д. Поэтому рекомендуется всегда использовать СУБД при работе с базами данных.

Пространственные данные и база данных

Особое внимание мы уделим обработке пространственных данных, поэтому я хотел бы обсудить здесь этот тип данных. Пространственные данные несколько отличаются от остальных. Координаты необходимо сохранять в особом формате, который обычно указан в документации на веб-сайте о базе данных. Этот формат позволяет базе считывать и правильно воспринимать координаты. Если обычно для поиска данных мы используем запросы типа Получить все результаты, где возраст>15, то пространственный запрос выглядит как-то так: Получить все результаты в радиусе 10км от определенной точки.Поэтому пространственные данные необходимо хранить в надлежащем формате.

Типы баз данных

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

Реляционные базы данных и РСУБД

Допустим, ваш начальник просит вас создать электронную таблицу с важной информацией, включающей имена, местонахождения, адреса электронной почты, номера телефонов и должности всех сотрудников. Вы сразу же откроете таблицу Excel или Google Spreadsheets, напишете все эти названия столбцов и начнете собирать информацию.

Образец таблицы с информациейОбразец таблицы с информацией

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

Связь между двумя столбцамиСвязь между двумя столбцами

Взаимосвязи в реляционных базах данных мы подробно рассмотрим позже.

По сравнению с базами данных NoSQL, недостатком реляционных баз данных является относительно медленное получение результатов, когда количество данных стремительно увеличивается (по мнению автора статьи прим. пер.). Еще один недостаток заключается в том, что при добавлении каждой записи нужно следовать определенным правилам (типы столбцов, количество столбцов и т.д.), мы не можем просто добавить отдельный столбец только для одной записи.В реляционных базах данных используется SQL(Structured Query Language язык структурированных запросов), с помощью которого пользователи могут взаимодействовать с данными, хранящимися в таблицах. SQL стал одним из наиболее широко используемых языков для этой цели. Мы подробнее поговорим об SQL чуть позже.Вот примеры некоторых известных и часто используемых реляционных баз данных: PostgreSQL, MySQL, MSSQL и т.д. У каждой крупной компании, занимающейся реляционными базами данных, есть собственная версия SQL. В большинстве аспектов они выглядят одинаково, но иногда требуется немного изменить какой-нибудь запрос, чтобы получить те же результаты в другой базе данных (например, при переходе из PostgreSQL в MySQL).

Нереляционные базы данных (NoSQL)

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

  1. Пара ключ-значение

  2. Формат JSON, XML

  3. Графовый формат

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

База данных NoSQL реального времени в Google FirebaseБаза данных NoSQL реального времени в Google Firebase

База данных NoSQL реального времени в Google Firebase

При использовании баз данных NoSQL пользователям иногда приходится прописывать собственную логику, чтобы добавить уникальный ключ к каждой записи и тем самым обеспечить доступ к записям. В большинстве стандартных баз данных NoSQL, таких как Firebase и MongoDB, для хранения данных используется формат JSON. Благодаря этому очень легко и удобно выполнять операции с данными из веб-приложений, используя JavaScript, Python, Ruby и т.д.

Рекомендации по выбору типа базы для хранения пространственных данных

Очевидно, что нам хотелось бы сохранить точку, линию, многоугольник, растры и т.д. так, чтобы это имело смысл, вместо того чтобы сохранять просто координаты. Нам нужна СУБД, которая позволяет не только сохранять данные, но и запрашивать их пространственными методами (буфер, пересечение, вычисление расстояния и т.д.). На сегодняшний день для этого лучше всего подходят реляционные базы данных, поскольку в SQL есть функции, помогающие выполнять подобные операции. Использование таких дополнительных средств, как PostGIS для PostgreSQL, открывает разработчикам возможности для написания сложных пространственных запросов. С другой стороны, NoSQL тоже работает в области геопространственных технологий: например, MongoDB предоставляет кое-какие функции для выполнения геопространственных операций. Однако реляционные базы данных все же лидируют на рынке с большим отрывом.

Работа с РСУБД

Основное внимание мы уделим РСУБД, так как именно эти системы в большинстве случаев мы будем использовать для хранения пространственных данных и работы с ними. В качестве примера мы будем использовать PostgreSQL, поскольку это самая перспективная реляционная база данных с открытым исходным кодом, а ее расширение PostGIS позволяет работать и с пространственными данными. Вы можете установить PostgreSQL, следуя инструкциям из документации. Помимо PostgreSQL рекомендуется также загрузить и установить pgAdmin. Платформа pgAdmin предоставляет веб-интерфейс для взаимодействия с базой данных. Также для этого можно загрузить и установить какое-либо другое совместимое ПО или использовать командную строку.

pgAdmin4 на MacpgAdmin4 на Mac

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

Создание базы данных. Нам нужно создать базу данных (в идеале должно быть по одной базе данных для каждого проекта).

Создание новой базы данных для проектаСоздание новой базы данных для проекта

В инструменте запросов (Query Tool) база данных создается следующим образом:

CREATE DATABASE <database_name>

Создание таблиц. Создание таблицы требует некоторых дополнительных соображений, поскольку именно здесь нам нужно определить все столбцы и типы данных в них. Все типы данных, которые можно использовать в PostgreSQL, вы найдете здесь.

pgAdmin позволяет нам выбрать в таблице различные ключи и ограничения, например Not Null (запрет на отсутствующие значения), Primary Key (первичный ключ) и т.д. Обсудим это подробнее чуть позже.

Создание таблицы пользователейСоздание таблицы пользователей

Заметьте, что мы не добавляли столбец первичного идентификатора в список столбцов, поскольку PostgreSQL делает это автоматически. Мы можем создать сколько угодно таблиц в одной базе данных. После того как таблицы созданы, мы можем установить связи между разными таблицами, используя определенные столбцы (обычно столбцы с идентификаторами).В инструменте запросов таблица создается следующим образом:

CREATE TABLE <table_name> (<column_1> <datatype>,<column_2> <datatype>,.....<column_n> <datatype>PRIMARY KEY (<column>));

CRUD-операции с данными в таблицах

CRUD-операции (создание, чтение, обновление и удаление Create, Retrieve, Update, Delete) это своего рода hello world в мире СУБД. Поскольку эти операции используются наиболее часто, команды для их выполнения одинаковы во всех РСУБД. Мы будем писать и выполнять запросы в инструменте запросов в pgAdmin, который вызывается следующим образом:

Инструмент запросов (Query Tool) в pgAdminИнструмент запросов (Query Tool) в pgAdmin

1. Создание новой записи

Для добавления новой записи в таблицу используйте следующую команду:

INSERT INTO <tablename> (column1, column2, column3,...) VALUES (value1, value2, value3,...);

INSERT, INTO, VALUE являются ключевыми словами в SQL, поэтому их нельзя использовать в качестве переменных, значений и т.д. Чтобы добавить новую запись в нашу таблицу пользователей, мы напишем в инструменте запросов следующий запрос:

INSERT INTO users(name, employed, address) VALUES ('Sheldon Cooper', true, 'Pasadena');

Обратите внимание: строки всегда следует заключать в'' (одинарные кавычки), а не в"" (двойные кавычки).

2. Получение записей (всех или нескольких)

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

select <column1, column2 ,...> from <tablename> 

Этот код извлекает весь набор данных. Если вы хотите получить только 20записей, напишите:

select <column1, column2 ,...> from <tablename> limit 20

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

select * from <tablename>

Если вы хотите получить результат с определенным условием, используйте ключевое слово WHERE, как показано ниже:

select * from <tablename> where <key> = <value>

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

--Retrieving Specific columns for all usersselect name,employed from users--Retrieving all columns for all usersselect * from users--Retrieving all columns for first 3 usersselect * from users limit 3--Retrieving all columns for all users where employed = trueselect * from users where employed = true

3. Обновление записей (всех или нескольких)РСУБД позволяет нам обновить все или только некоторые записи данных, указав новые значения для столбцов.

UPDATE <tablename> SET <column1> = <value1>, <column2> = <value2> 

Если вы хотите обновить определенные строки, добавьте условия с использованием ключевого слова WHERE:

UPDATE <tablename> SET <column1> = <value1>, <column2> = <value2>WHERE <column> = <value> 

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

-- Make all rows as  employed = trueupdate users set employed = true-- change employed = false for entries with address = 'nebraska'update users set employed = false where address = 'nebraska'
Обновление записейОбновление записей

4. Удаление записей (всех или нескольких)Удалять записи в SQL легко. Пользователь может удалить либо все строки, либо только определенные строки, добавив условие WHERE.

-- Deleting all entries Delete from <tablename> -- Deleting entries based on conditionsDelete from <tablename> where <column> = <value> 
-- Deleting all entries Delete from users-- Deleting entries based on conditionsDelete from users where employed = false
Удаление записей из таблицыУдаление записей из таблицы

CRUD-операции используются очень часто, поскольку выполняют основные функции в базе данных.


Перевод подготовлен в рамках курса Базы данных. Все желающих приглашаем на бесплатный двухдневный онлайн-интенсив Бэкапы и репликация PostgreSQL. Практика применения. Цели занятия: настроить бэкапы; восстановить информацию после сбоя. Регистрация здесь.

Подробнее..

Перевод Линейная алгебра для исследователей данных

15.06.2021 14:10:21 | Автор: admin
Иллюстрация: UCIИллюстрация: UCI

Наша [Ирвинга Капланского и Пола Халмоша] общая философия в отношении линейной алгебры такова: мы думаем в безбазисных терминах, пишем в безбазисных терминах, но когда доходит до серьезного дела, мы запираемся в офисе и вовсю считаем с помощью матриц.

Ирвинг Капланский

Для многих начинающих исследователей данных линейная алгебра становится камнем преткновения на пути к достижению мастерства в выбранной ими профессии.

kdnuggetskdnuggets

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

Произведения векторов

Для двух векторов x, y их скалярным или внутренним произведением xy

называется следующее вещественное число:

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

.

x^Ty = y^Tx

Для двух векторов x , y (не обязательно одной размерности) также можно определить внешнее произведение xy . Это матрица, значения элементов которой определяются следующим образом: (xy) = xy, то есть

След

Следом квадратной матрицы A , обозначаемым tr(A) (или просто trA), называют сумму элементов на ее главной диагонали:

След обладает следующими свойствами:

  • Для любой матрицы A : trA = trA.

  • Для любых матриц A,B : tr(A + B) = trA + trB.

  • Для любой матрицы A и любого числа t : tr(tA) = t trA.

  • Для любых матриц A,B, таких, что их произведение AB является квадратной матрицей: trAB = trBA.

  • Для любых матриц A,B,C, таких, что их произведение ABC является квадратной матрицей: trABC = trBCA = trCAB (и так далее данное свойство справедливо для любого числа матриц).

TimoElliottTimoElliott

Нормы

Норму x вектора x можно неформально определить как меру длины вектора. Например, часто используется евклидова норма, или норма l:

Заметим, что x=xx.

Более формальное определение таково: нормой называется любая функция f : n , удовлетворяющая четырем условиям:

  1. Для всех векторов x : f(x) 0 (неотрицательность).

  2. f(x) = 0 тогда и только тогда, когда x = 0 (положительная определенность).

  3. Для любых вектора x и числа t : f(tx) = |t|f(x) (однородность).

  4. Для любых векторов x, y : f(x + y) f(x) + f(y) (неравенство треугольника)

Другими примерами норм являются норма l

и норма l

Все три представленные выше нормы являются примерами норм семейства lp, параметризуемых вещественным числом p 1 и определяемых как

Нормы также могут быть определены для матриц, например норма Фробениуса:

Линейная независимость и ранг

Множество векторов {x,x,...,x} называют линейно независимым, если никакой из этих векторов не может быть представлен в виде линейной комбинации других векторов этого множества. Если же такое представление какого-либо из векторов множества возможно, эти векторы называют линейно зависимыми. То есть, если выполняется равенство

для некоторых скалярных значений ,, - , то мы говорим, что векторы x,...,x линейно зависимы; в противном случае они линейно независимы. Например, векторы

линейно зависимы, так как x = 2x + x.

Столбцовым рангом матрицы A называют число элементов в максимальном подмножестве ее столбцов, являющемся линейно независимым. Упрощая, говорят, что столбцовый ранг это число линейно независимых столбцов A. Аналогично строчным рангом матрицы является число ее строк, составляющих максимальное линейно независимое множество.

Оказывается (здесь мы не будем это доказывать), что для любой матрицы A столбцовый ранг равен строчному, поэтому оба этих числа называют просто рангом A и обозначают rank(A) или rk(A); встречаются также обозначения rang(A), rg(A) и просто r(A). Вот некоторые основные свойства ранга:

  • Для любой матрицы A : rank(A) min(m,n). Если rank(A) = min(m,n), то A называют матрицей полного ранга.

  • Для любой матрицы A : rank(A) = rank(A).

  • Для любых матриц A , B np: rank(AB) min(rank(A),rank(B)).

  • Для любых матриц A,B : rank(A + B) rank(A) + rank(B).

Ортогональные матрицы

Два вектора x, y называются ортогональными, если xy = 0. Вектор x называется нормированным, если ||x|| = 1. Квадратная м

атрица U называется ортогональной, если все ее столбцы ортогональны друг другу и нормированы (в этом случае столбцы называют ортонормированными). Заметим, что понятие ортогональности имеет разный смысл для векторов и матриц.

Непосредственно из определений ортогональности и нормированности следует, что

Другими словами, результатом транспонирования ортогональной матрицы является матрица, обратная исходной. Заметим, что если U не является квадратной матрицей (U , n < m), но ее столбцы являются ортонормированными, то UU = I, но UU I. Поэтому, говоря об ортогональных матрицах, мы будем по умолчанию подразумевать квадратные матрицы.

Еще одно удобное свойство ортогональных матриц состоит в том, что умножение вектора на ортогональную матрицу не меняет его евклидову норму, то есть

для любых вектора x и ортогональной матрицы U .

TimoElliottTimoElliott

Область значений и нуль-пространство матрицы

Линейной оболочкой множества векторов {x,x,...,x} является множество всех векторов, которые могут быть представлены в виде линейной комбинации векторов {x,...,x}, то есть

Областью значений R(A) (или пространством столбцов) матрицы A называется линейная оболочка ее столбцов. Другими словами,

Нуль-пространством, или ядром матрицы A (обозначаемым N(A) или ker A), называют множество всех векторов, которые при умножении на A обращаются в нуль, то есть

Квадратичные формы и положительно полуопределенные матрицы

Для квадратной матрицы A и вектора x квадратичной формой называется скалярное значение x Ax. Распишем это выражение подробно:

Заметим, что

  • Симметричная матрица A называется положительно определенной, если для всех ненулевых векторов x справедливо неравенство xAx > 0. Обычно это обозначается как

    (или просто A > 0), а множество всех положительно определенных матриц часто обозначают

    .

  • Симметричная матрица A называется положительно полуопределенной, если для всех векторов справедливо неравенство x Ax 0. Это записывается как

    (или просто A 0), а множество всех положительно полуопределенных матриц часто обозначают

    .

  • Аналогично симметричная матрица A называется отрицательно определенной

  • , если для всех ненулевых векторов x справедливо неравенство xAx < 0.

  • Далее, симметричная матрица A называется отрицательно полуопределенной (

    ), если для всех ненулевых векторов x справедливо неравенство xAx 0.

  • Наконец, симметричная матрица A называется неопределенной, если она не является ни положительно полуопределенной, ни отрицательно полуопределенной, то есть если существуют векторы x, x такие, что

    и

    .

Собственные значения и собственные векторы

Для квадратной матрицы A комплексное значение и вектор x будут соответственно являться собственным значением и собственным вектором, если выполняется равенство

На интуитивном уровне это определение означает, что при умножении на матрицу A вектор x сохраняет направление, но масштабируется с коэффициентом . Заметим, что для любого собственного вектора x и скалярного значения с справедливо равенство A(cx) = cAx = cx = (cx). Таким образом, cx тоже является собственным вектором. Поэтому, говоря о собственном векторе, соответствующем собственному значению , мы обычно имеем в виду нормализованный вектор с длиной 1 (при таком определении все равно сохраняется некоторая неоднозначность, так как собственными векторами будут как x, так и x, но тут уж ничего не поделаешь).


Перевод статьи был подготовлен в преддверии старта курса "Математика для Data Science". Также приглашаем всех желающих посетить бесплатный демоурок, в рамках которого рассмотрим понятие линейного пространства на примерах, поговорим о линейных отображениях, их роли в анализе данных и порешаем задачи.


Подробнее..

Перевод 5 вещей о наблюдаемости данных, которые должен знать каждый дата-инженер

26.05.2021 14:12:47 | Автор: admin

Как быть уверенным в своих рабочих процессах, конвейер за конвейером

В преддверии старта онлайн-курса "Data Engineer" подготовили перевод материала.


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

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

Джесси Андерсон, управляющий директор Big Data Institute и автор книги Команды инженерии данных: создание успешных Big Data команд и продуктов, и Барр Мозес, соучредитель и генеральный директор Monte Carlo, делятся всем, что вам нужно знать, чтобы начать работу на этом новом уровне стека данных.

Инжиниринг данных (Data Engineering) часто называют водопроводом data science - обычно, имея в виду способ, которым инженеры по обработке данных обеспечивают правильное функционирование всех конвейеров и рабочих процессов, а также правильные данные, поступающие в нужных направлениях к нужным заинтересованным сторонам. Но большинство дата-инженеров, с которыми я разговариваю, имеют одно вполне конкретное мнение о водопроводчиках: вы звоните им только тогда, когда что-то идет не так.

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

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

Slack в середине встречи от лида по маркетингу: рентабельность инвестиций в мою кампанию в этом месяце невысока. Я думаю, что что-то не так с данными атрибуции.

Сообщение, которое вы никогда не получите: данные в этом отчете идеальны. Так держать!

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

Один из способов выбраться из это порочного круга ночных писем - наблюдаемость данных (Data Observability).

#1. Что такое наблюдаемость данных и почему это важно

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

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

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

#2. Как DevOps заложил наблюдаемость данных

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

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

Следуя структуре наблюдаемости в разработке приложений, наблюдаемость данных разбита на пять столпов: свежесть, распределение, объем, схема и происхождение.

  • Свежесть (Freshness) показывает, насколько актуальны ваши таблицы данных.

  • Распределение (Distribution) сообщает вам, попадают ли ваши данные в ожидаемый диапазон.

  • Объем (Volume ) предполагает понимание полноты ваших таблиц данных и состояния ваших источников данных.

  • Схема (Schema) позволяет понять, кто и когда вносит изменения в таблицы данных.

  • Происхождение (Lineage) сопоставляет вышестоящие источники и нижележащие приемники ваших данных, помогая определить, где произошли ошибки или сбои.

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

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

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

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

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

#4. Наблюдаемость данных - это больше, чем просто тщательное тестирование и мониторинг

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

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

Наблюдаемость данных также обеспечивает происхождение (lineage), которое мы определили ранее как сопоставление вышестоящих источников и нижележащих приемников ваших данных. Lineage действительно дает вам представление о ваших данных с высоты птичьего полета, понимание того, откуда они взялись, кто с ними взаимодействовал, какие изменения были внесены и где в конечном итоге они были доставлены конечным потребителям.

Эта видимость позволяет обнаруживать данные, которые мы описываем как следующее поколение каталогов данных, обеспечивая динамическое понимание ваших данных на основе их происхождения. Автоматическое, масштабируемое и распределенное обнаружение данных позволяет вам отвечать на вопросы о текущем состоянии ваших данных в каждом домене: когда эта таблица обновлялась в последний раз? У кого есть к ней доступ? Когда последний раз использовался этот информационный актив? Каково качество продукции?

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

#5. Когда дело доходит до ваших данных, иметь в основном плохие данные хуже, чем их вообще не иметь

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

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

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


- Узнать подробнее о курсе "Data Engineer"

Подробнее..

Чтобы потолка не стало, а крышу не снесло о чем новый подкаст ВТБ

08.06.2021 22:04:34 | Автор: admin

Привет, Хабр! Команда ВТБ запустила серию подкастов о передовых решениях финтеха Деньги любят техно. Журналист, технологический обозреватель Марина Эфендиева будет обсуждать с экспертами банка, рынка, учеными и бизнесменами перспективы и сложности финтеха: внедрения технологий на основе Big Data, машинного обучения, искусственного интеллекта, вопросы кибербезопасности и защиты данных, перспективные технологические специальности, голосовых помощников и многое другое.

В первом выпускезаместитель президента-председателя правления ВТБ Вадим Кулик и директор Физтех-школы прикладной математики и информатики д.ф.-м.н. Андрей Райгородский обсуждают, почему банки вРоссии так любятData science, можно ли стать дата-сайнтистом за три месяцаигде учиться, чтобысоздатьуспешную карьеру. Под катом основные темы этой беседы и ссылка на сам подкаст.

Откуда взялся банковскийData Science

Тривиальный, но важный вопрос: почему именно банковский Data Science сегодня занимает передовые позиции?

По словам Вадима Кулика, сегодняшние тренды это новый этап в решении вопросов, которые стояли перед банковским сектором еще в 90-х. Тогда жесткая нехватка какой-либо информации о клиентах усложняла процесс выдачи потребительских кредитов и выход на B2C-рынок в целом.

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

Поэтому ВТБ уделяет такое внимание подходу Data Fusion, который предполагает объединение, обезличивание данных из разных источников и их обработку. По этому поводу недавно прошлабольшая конференция.

Хорошей иллюстрацией применения данного подхода может служить СП ВТБ и РостелекомаПлатформа больших данных, которое уже предоставляет рынку продукты на основе Big Data для увеличения эффективности и развития бизнеса.

Data Science за 3 месяца без SMS и регистрации

Андрей Райгородский ответил на ещё один очень важный вопрос: можно ли стать дата сайентистом за 3 месяца, как сейчас предлагают многие онлайн-платформы. Он также рассказал о том, какова сейчас потребность в этих специалистах.

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

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

МФТИ (Московский физико-технический институт) лидер этого направления в России фокусируется на фундаментальном обучении и готовит кадры для будущего. При этом есть и специальные нишевые программы например,Школа глубокого обучения, которая заработала в онлайн-формате ещё до того, когда это стало ковидным мейнстримом.

Главной особенностью МФТИ можно считать взаимодействие прикладного и фундаментального. В наши дни это связка между коммерческой индустрией, которая формирует запрос, и академической наукой, которая даёт фундаментальные математические решения. Отличный пример такого симбиоза созданная в начале 2021 года лаборатория ВТБ при МФТИ.

Резюме

Современный мир устроен так, что во многих сферах а в финансовой в первую очередь умение собирать и анализировать данные становится главным фактором роста. Скорость этого роста такова, что не позволяет только сиюминутные задачи. Нужно уметь формировать повестку будущего. Как выразился Андрей Райгородский, нельзя упускать фундаментальное в гонке за количеством кадров: цель не в том, чтобы снесло крышу, а в том, чтобы потолка не стало. А что вы об этом думаете? Делитесь мнениями в комментариях.

А вот и сам подкаст:

Подробнее..

Перевод Полезные приемы и лучшие практики от Kaggle

27.05.2021 18:04:31 | Автор: admin

В преддверии старта курса "Machine Learning. Professional" делимся традиционным переводом полезного материала.


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

Об этом проекте

Kaggle прекрасное место. Это золотая жила для дата-сайентистов и инженеров машинного обучения. Не так много платформ, на которых вы можете найти высококачественные, эффективные, воспроизводимые, отобранные экспертами, потрясающие примеры коды в одном месте.

С момента запуска она провела более 164 соревнований. Эти соревнования привлекают на платформу экспертов и профессионалов со всего мира. В результате на каждом соревновании появляется множество высококачественных блокнотов и скриптов, а также огромное количество опенсорсных наборов данных, которые предоставляет Kaggle.

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

Но теперь я обнаружил, что провожу много времени за чтением чужих блокнотов и отправкой заявок на соревнования. Иногда там есть вещи, на которые стоит потратить все выходные. А иногда я нахожу простые, но невероятно эффективные приемы и передовой опыт, которые можно изучить, только наблюдая за другими профессионалами.

А в остальном дело за малым, мое ОКР практически вынуждает меня выкладывать все имеющиеся у меня знания в области науки о данных. Таким образом я представляю вам первый выпуск моего еженедельника Полезные приемы и лучшие практики от Kaggle. На протяжении всей серии я буду писать обо всем, что может быть полезно во время типичного рабочего процесса в области data science, включая фрагменты кода распространенных библиотек, передовые практики, которым следуют ведущие эксперты в области с Kaggle, и т. д. - все, что я узнал за прошедшую неделю. Наслаждайтесь!

1. Отображение только нижней части корреляционной матрицы

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

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

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

houses = pd.read_csv('data/melb_data.csv')# Calculate pairwise-correlationmatrix = houses.corr()# Create a maskmask = np.triu(np.ones_like(matrix, dtype=bool))# Create a custom diverging palettecmap = sns.diverging_palette(250, 15, s=75, l=40,                             n=9, center="light", as_cmap=True)plt.figure(figsize=(16, 12))sns.heatmap(matrix, mask=mask, center=0, annot=True,             fmt='.2f', square=True, cmap=cmap)plt.show();

Полученный в результате график намного легче интерпретировать, и он не так отвлекает избыточными данными. Сначала мы строим корреляционную матрицу, используя метод DataFrame .corr. Затем мы используем функцию np.ones_like с dtype, установленным в bool, чтобы создать матрицу значений True с той же формой, что и наш DataFrame:

>>> np.ones_like(matrix, dtype=bool)[:5]array([[ True, True, True, True, True, True, True, True, True, True, True, True, True], [ True, True, True, True, True, True, True, True, True, True, True, True, True], [ True, True, True, True, True, True, True, True, True, True, True, True, True], [ True, True, True, True, True, True, True, True, True, True, True, True, True], [ True, True, True, True, True, True, True, True, True, True, True, True, True]])

Затем мы передаем его в функцию Numpy .triu, которая возвращает двумерную логическую маску, которая содержит значения False для нижнего треугольника матрицы. Затем мы можем передать его функции Seaborn heatmap для построения подмножества матрицы в соответствии с этой маской:

sns.heatmap(matrix, mask=mask, center=0, annot=True,               fmt='.2f', square=True, cmap=cmap)

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

2. Добавление отсутствующих значений в value_counts

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

>>> houses.CouncilArea.value_counts(dropna=False, normalize=True).head()NaN           0.100810Moreland      0.085641Boroondara    0.085420Moonee Valley 0.073417Darebin       0.068778Name: CouncilArea, dtype: float64

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

>>> missing_props = houses.isna().sum() / len(houses)>>> missing_props[missing_props > 0].sort_values(ascending=False                                                 BuildingArea 0.474963YearBuilt    0.395803CouncilArea  0.100810Car          0.004566dtype: float64

Сначала найдите пропорции, разделив количество отсутствующих значений на длину DataFrame. Затем вы можете отфильтровать столбцы с 0%, т.е. выбирать только столбцы с пропущенными значениями.

3. Использование Pandas DataFrame Styler

Многие из нас никогда не осознают огромный неиспользованный потенциалpandas. Недооцененной и часто упускаемой из виду особенностью pandas является ее способность стилизовать свои DataFrameы. Используя атрибут .style для DataFrameов pandas, вы можете применять к ним условные конструкции и стили. В качестве первого примера давайте посмотрим, как можно изменить цвет фона в зависимости от значения каждой ячейки:

>>> diamonds = sns.load_dataset('diamonds')>>> pd.crosstab(diamonds.cut, diamonds.clarity).\                style.background_gradient(cmap='rocket_r')

Это практически тепловая карта без использования функции Seaborn heatmap. Здесь мы подсчитываем каждую комбинацию огранки и чистоты алмаза с помощью pd.crosstab. Используя .style.background_gradient с цветовой палитрой, вы можете легко определить, какие комбинации встречаются чаще всего. Только из приведенного выше DataFrame мы можем видеть, что большинство алмазов имеют идеальную огранку, а самая распространенная комбинация - с типом чистоты VS2.

Мы даже можем пойти дальше, найдя среднюю цену каждой комбинации огранки и чистоты бриллианта в перекрестной таблице:

>>> pd.crosstab(diamonds.cut, diamonds.clarity,          aggfunc=np.mean, values=diamonds.price).\          style.background_gradient(cmap='flare')

На этот раз мы агрегируем цены на бриллианты для каждой комбинации огранки и чистоты. Из стилизованного DataFrame мы видим, что самые дорогие бриллианты имеют чистоту VS2 или премиальную огранку. Но было бы лучше, если бы мы могли отображать агрегированные цены, округляя их. Мы также можем изменить это с помощью .style:

>>> agg_prices = pd.crosstab(diamonds.cut, diamonds.clarity,                         aggfunc=np.mean, values=diamonds.price).\                         style.background_gradient(cmap='flare')>>> agg_prices.format('{:.2f}')

Изменив в методе .format строку формата {:.2f} мы указываем точность в 2 числа после запятой.

С .style предел - ваше воображение. Имея базовые познания в CSS, вы можете создавать собственные функции стилизации под свои нужды. Ознакомьтесь с официальным руководством pandas для получения дополнительной информации.

4. Настройка глобальных конфигураций графиков с помощью Matplotlib

При выполнении EDA (Exploratory Data Analysis) вы обнаружите, что сохраняете некоторые настройки Matplotlib одинаковыми для всех ваших графиков. Например, вы можете захотеть применить настраиваемую палитру для всех графиков, использовать более крупные шрифты для меток, изменить расположение легенды, использовать фиксированные размеры фигур и т. д.

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

from matplotlib import rcParams

rcParams - это просто старый словарь Python, содержащий настройки по умолчанию для Matplotlib:

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

# Remove top and right spinesrcParams['axes.spines.top'] = FalsercParams['axes.spines.right'] = False# Set fixed figure sizercParams['figure.figsize'] = [12, 9]# Set dots per inch to 300, very high quality imagesrcParams['figure.dpi'] = 300# Enable autolayoutrcParams['figure.autolayout'] = True# Set global fontsizercParams['font.style'] = 16# Fontsize of ticklabelsrcParams['xtick.labelsize'] = 10rcParams['ytick.labelsize'] = 10

Вы можете избежать большого количества повторяющейся работы, установив все сразу после импорта Matplotlib. Просмотреть все другие доступные настройки можно, вызвав rcParams.keys().

5. Настройка глобальных конфигураций Pandas.

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

  • get_option() / set_option() - получить/установить значение одного параметра.

  • reset_option() - сбросить один или несколько параметров до значений по умолчанию.

  • description_option() - вывести описание одного или нескольких параметров.

  • option_context() - выполнить блок кода с набором параметров, которые после выполнения возвращаются к предыдущим настройкам.

Все параметры имеют имена без учета регистра и находятся с помощью регулярного выражения из под капота. Вы можете использовать pd.get_option, чтобы узнать, какое поведение используется по умолчанию, и изменить его по своему вкусу с помощью set_option:

>>> pd.get_option(display.max_columns)20

Например, указанный выше параметр управляет количеством столбцов, которые должны отображаться, когда в DataFrame очень много столбцов. Сегодня большинство наборов данных содержит более 20 переменных, и всякий раз, когда вы вызываете .head или другие функции отображения, pandas ставит раздражающее многоточие, чтобы обрезать результат:

>>> houses.head()

Я бы предпочел видеть все столбцы, прокручивая их. Давайте изменим это поведение:

>>> pd.set_option(display.max_columns, None)

Выше я полностью убираю ограничение:

>>> houses.head()

Вы можете вернуться к настройке по умолчанию с помощью:

pd.reset_option(display.max_columns)

Как и в столбцах, вы можете настроить количество отображаемых строк по умолчанию. Если вы установите для display.max_rows значение 5, вам не придется все время вызывать .head():

>>> pd.set_option(display.max_rows, 5)>>> houses

В настоящее время plotly становится очень популярным, поэтому было бы неплохо установить его как график по умолчанию для pandas. Поступая таким образом, вы будете получать интерактивные графические диаграммы всякий раз, когда вы вызываете .plot для DataFrameов pandas:

pd.set_option(plotting.backend, plotly)

Обратите внимание, что для этого вам необходимо установить plotly.

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

>>> df = pd.DataFrame(np.random.randn(5, 5))>>> pd.reset_option('display.max_rows')>>> with pd.option_context('float_format', '{:f}'.format):        df.describe()

Вы можете увидеть список доступных параметров в официальном руководстве пользователя pandas.


Узнать подробнее о курсе "Machine Learning. Professional"

Подробнее..

ML нечеловеческие технологии для человеческих цен

28.05.2021 14:21:34 | Автор: admin


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

80% компаний внедряют технологии машинного обучения такую цифру назвал один из руководителей Microsoft Жан-Филипп Куртуа на декабрьской конференции AI Jorney, говоря о влиянии пандемии на мировую экономику. По данным г-на Куртуа, 56% компаний планируют увеличивать свои инвестиции на внедрение машинного обучения.

В ритейле AI и ML используются уже для многих операций от планирования поставок и повышения эффективности маркетинга до расчёта рабочих графиков сотрудников розницы. М.Видео-Эльдорадо пошла дальше, чтобы предложить своим покупателям ассортимент, цены и промоакции, основанные на продвитуной аналитике. Как это работает, будем рассказывать в цикле материалов про различные ML-решения в области коммерции.

Несколько очевидных мыслей


Для начала вспомним, какие задачи позволяет решить Machine Learning и чем оно хорошо для ритейла. Если вы продаете молоко от своей коровы, вы и без компьютеров прекрасно знаете, кому из соседей оно нужно, сколько они купят и какую цену посчитают привлекательной, да и тут простые навыки учета не помешают.

Иное дело крупные торговые сети с десятками, а то и сотнями тысяч товарных позиций и миллионами покупателей. Что из этого пойдет в народ легко и быстро, а что будет стоять на полке годами? Что по-прежнему заказывать у поставщиков, а от чего следует отказаться?

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

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

К примеру, на одну и ту же аудиосистему может быть несколько ценовых предложений от 2 000 до 4 000 рублей в Москве и от 1 500 до 3 400 в областном центре с меньшим доходом на душу населения. Есть цель продать быстрее устанавливаете нижний порог.

Хотите заработать побольше поступаете ровно наоборот. А спустя определенное время инструменты ML подскажут, правильно ли вы поступали и нужно ли корректировать свои действия. Скажем, не гнаться за максимальной прибылью, а сделать ставку на скорость оборота средств.

Вирус гонит в цифру


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

Ежемесячно веб-площадки М.Видео и Эльдорадо посещают порядка 74 млн пользователей. Их профиль и история взаимодействия с компанией ложатся в основу прогнозных моделей и рекомендательных сервисов, которые ритейлер развивает в том числе в области коммерции.

Однако онлайн больше не является единственным источником информации о потребителях. М.Видео-Эльдорадо в 2020 году полностью перешла на платформу OneRetail, которая благодаря мобильным технологиям в рознице позволяет оцифровывать офлайн-опыт клиентов и анализировать эти данные. А это огромный массив 85% покупателей техники так или иначе взаимодействуют с физическими магазинами.

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

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

Внедрение Data science решений в коммерции позволит М.Видео-Эльдорадо, во-первых, глубже понять потребности клиентов и повысить точность планирования ассортимента, во-вторых, рассчитать оптимальную цену, исходя из стремления делать лучшие предложения на рынке, повышая при этом эффективность бизнеса.

Как настроить ассортимент


Например, машинное обучение помогает прогнозировать спрос не только на основе уже совершенных продаж, но также на потребительской потребности. Если представить, что на полке магазина можно разместить 12 чайников, то какие это должны быть модели, если у вас в ассортименте их 50?

Как сформировать сток небольшого регионального склада, чтобы максимальное число клиентов получали быстрые сроки доставки по своим заказам? И, наконец, как найти идеальный баланс между ростом продаж, доли рынка и эффективностью бизнеса?

Если раньше на эти вопросы экспертно отвечали коммерческие менеджеры М.Видео-Эльдорадо, то сейчас наша data-science команда разрабатывает им в помощь рекомендательные сервисы, основанные на ML.

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

Определить подходящую цену


М.Видео-Эльдорадо также тестирует алгоритмы Machine Learning для создания сценариев автоматического расчета рекомендованной цены и оценки эффективности промоакций. Цель дать коммерческим менеджерам инструмент для ежедневного управления ценой на основе как внутренних данных (объем продаж, уровень маржи, товарный запас, промо-календарь), так и внешних (цены на рынке, активность конкурентов и тд.).

Модель рассчитывает несколько сценариев в зависимости от целевых показателей и рекомендует оптимальный. Сейчас в рамках пилотов мы сравниваем степень заинтересованности потребителей в конкретном товаре в разных регионах и уровень их чувствительности к ценовым колебаниям, рассказывает Владимир Литвинюк, руководитель центра компетенций по прикладному анализу данных и машинному обучению Группы М.Видео-Эльдорадо.

Не секрет, что, выбирая флагманский смартфон или холодильник side-by-side, покупатель ищет, где выгоднее, а при покупке чайника на дачу или замене телевизора на кухне предпочтёт проверенный магазин у дома или выгодные условия программы лояльности.

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

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

Сейчас развитие Machine Learning в России сдерживают два момента: проблемы с наличием данных для обработки и недостаточное распространение ML-моделей, о чем как раз говорили на упомянутой конференции AI Jorney.

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

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

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

Вопрос и в том, какие платформы выбирать для динамического ценообразования Кто-то выбирает open source, кто-то проприетарные коробочные решения от вендоров, кто-то облачные ML фреймворки Число программных инструментов измеряется десятками, есть варианты, бесплатные для предпринимателей. Так почему же участники ИТ-конференций говорят о недостаточности распространения моделей машинного обучения?

Дело опять в том, чтобы понять собственные потребности и оценить свои возможности. И тут как никогда важен обмен опытом и лучшими практиками. Например, когда в российском интернет-магазине BABADU внедрили ML-модели и динамическое ценообразование, всего за несколько недель выручка и маржинальный доход выросли на 7%. Потребитель реагирует на честные цены тем, что несет деньги амбассадорам Machine Learning.
Подробнее..

Switchback-эксперименты в Ситимобил Часть 1. Зачем это нужно

03.06.2021 20:19:56 | Автор: admin

Содержание

  1. Введение

  2. Про эксперименты

  3. Что такое сетевой эффект?

  4. Почему switchback помогает?

  5. Зачем так сложно, может, у вас нет сетевого эффекта?

  6. Убедили, как подобрать окно переключения по расстоянию и времени?

  7. Слабые стороны Switchback

  8. О следующей статье

Введение

Сегодняс вами на связи отдел динамического ценообразования Ситимобил. И мы начинаем серию статей о том, как мы проводим и оцениваем ценовые эксперименты внутри нашего маркетплейса.

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

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

Про эксперименты

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

До середины 2019 года чаще всего мы проводили рандомизированные A/B-тесты сосплитованием по hash (id), реже W2W (week-to-week, то есть когда производится сравнение выборок за одно время и один день недели, но в разные периоды), или diff-in-diff (подробнее см. здесь) эксперименты. Но все эти подходы для наших задач имеют ряд больших недостатков.

В W2W и diff-in-diff оценках может быть сложно обеспечить сопоставимые условия в тестовой и контрольной группах. Как-то мы хотели провести W2W-оценку, а в нашем тестовом городе на второй неделе была песчаная буря.

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

С рандомизированными A/B-тестами проблемы не такие очевидные. Когда вы запускаете рандомизированный А/В-тест в двух-трёх-n-стороннем маркетплейсе, то у вас может возникнуть сетевой эффект (не бойтесь, об этом термине поговорим в следующем разделе), который ставит под вопрос валидность теста в целом.

В итоге мы пришли к использованию геохроносплитования (в переводе со сложного означает разбиение районов города на экспериментальные группы, с перемешиванием каждые n минут), а затем обнаружили, что этот подход популярен, и называется этот загадочный зверь switchback. К нему мы ещё вернёмся.

Что такое сетевой эффект?

Главное условие валидности А/В-теста stable unit treatment value assumption (SUTVA), которое говорит, что измененные условия воздействуют только на группу, к которой они были применены, и не воздействуют на пользователей из других групп.

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

Слишком сложная схема, давайте на примере:

Пусть Миша с Колей живут в соседних домах напротив друг друга и каждое утро примерно в одно время едут на такси до метро. Наша команда динамического ценообразования начала эксперимент, и так получилось, что Миша и Коля оказались в разных группах. Для Коли цена будет рассчитываться старым алгоритмом, а для Миши новым, который по каким-то причинам снизил утром на 5 % цену поездки до метро. Наступает очередное утро, и Миша с Колей заказывают такси примерно в одно время. Цена у Миши ниже обычного, и он принимает решение о заказе такси быстрее, тем самым отнимая водителя у Коли. Коля оказывается без машины, и мы вынуждены изменить для него цену, чтобы он отказался от поездки, так как свободных машин в округе больше нет. Получается, что конверсия Коли занижена из-за Миши, то есть измененные условия повлияли не только на решение Миши, но и на Колю, и SUTVA не выполняется.

Это и есть сетевой эффект. Если формулировать более научно, то:

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

Спасительный Switchback

SUTVA не выполняется, рандомизированный А/В-тест под угрозой. Как же нам теперь проводить честные эксперименты?

Здесь нам на помощь приходит тип эксперимента, который называется Switchback.

Switchback метод геохроносплитования контрольных и тестовых групп с гиперпараметрами в виде длительности применения группы на все наблюдения и площади применения группы.

Суть метода Switchback заключается в следующем:

  1. Имеющиеся районы разбивают на контрольные и экспериментальные группы. К экспериментальным применяется тестируемый алгоритм.

  2. Через короткий промежуток времени районы случайно изменяются (мы считаем районами группы гексагонов, используем гексагональную сетку от Uber; подробнее читайте здесь). Затем они снова меняются, и так далее. Процесс перестановки продолжается в течение всего эксперимента.

  3. Показатели за время, когда алгоритм действовал и бездействовал, считаются в разные корзины.

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

Теперь Миша и Коля с бОльшей вероятностью оказались бы в одной группе, так как они близко друг к другу по расстоянию и времени. Решение они принимали бы в одинаковых условиях, и SUTVA не нарушилось бы.

Почему Switchback помогает?

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

Мы даже можем оценить сокращение взаимодействия численно!

Немного математики для бесстрашных

Взаимное влияние пассажиров друг на друга

Пусть пассажир определяется вектором:

r = \begin{bmatrix} t \\ latitude \\ longitude \end{bmatrix}, \\

где

  • t время, в которое клиент зашел в приложение;

  • latitude долгота точки заказа;

  • longtitude широта точки заказа.

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

interaction = \frac{1}{\beta}, \\ \beta = \sqrt{\alpha_1^2(t_1-t_2)^2 + \alpha_2^2(\Delta d)^2} \\ \Delta d = f(lat_1, lat_2, lon_1, lon_2)Почему interaction это дробь?

Interaction обратно пропорционален расстоянию между клиентами и временем между калькуляциями, то есть чем "дальше" друг от друга клиенты по расстоянию или времени, тем меньше они влияют друг на друга.

Поэтому подходящие виды зависимостей для определения interaction могут быть следующие:

y = \frac{1}{x^{\alpha}}, \alpha \geq 1 \\ y = e^{-x}

Для определения interaction в данном примере была выбрана зависимость \frac{1}{x} так как она убывает медленнее всего, значит позволит учитывать с бОльшим весом влияние между клиентами, которые находятся друг от друга далеко по времени или расстоянию, по сравнению с другими функциями. Интуитивно, кажется, что даже "далекие" к друг другу клиенты всё равно влияют на друг друга, поэтому мы и выбрали самую медленно убывающую функцию.

Зачем нужны веса?

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

В обычных метриках, например, L_2 , мы сравниваем между собой координаты x и y , эти величины имеют одинаковый масштаб. В нашем случае мы сравниваем метры и секунды. Поэтому чтобы они вносили одинаковый вклад их необходимо привести к одному масштабу. Здесь мы поступили очень просто и посмотрели на наших реальных данных отношение среднего времени между заходами клиентов в приложение, к среднему расстоянию между ними, и получили 1:16. Это соотношение и подставим в наши \alpha_1, \alpha_2 при расчетах.

Эта метрика не существует в нуле, но на наших данных и не было таких случаев, когда числитель был нулевым. Иначе можно было бы прибавить в знаменателе 1.

Сравним, как пассажиры влияют друг на друга в рандомизированном А/В и Switchback.

Теперь поступим так же, как в примере с кругами. Если пользователи относятся к разным группам, то взаимное влияние между ними есть, и мы его считаем по формуле для interaction выше. Если к разным, то считаем, что его нет. По сути, мы проставляем веса на черные линии из картинки выше и суммируем их для некоторого промежутка времени. Стоит отметить, что также для упрощения и ускорения подсчетов мы ограничили дельту между клиентами, когда учитываем их взаимное влияние, 6 минутами и 3 км, их также получили на реальных данных.

Если такое проделать на Москве в течение одного дня и сравнить уровень взаимодействия для рандомизированного эксперимента и Switchback, то Switchback снижает сетевой эффект более чем на 70%.

Конечно, сетевой эффект можно снижать не только с помощью Switchback,нам подходит любая разбивка, которая уменьшает уровень взаимодействия между клиентами количество черных линий. Например, для социальных сетей удобно проводить А/В-тест на кластерах клиентов, но в таких случаях сложнее добиться сходимости между группами, да и не для всех бизнесов они подходят, собственно, как и Switchback.

Зачем так сложно, может, у вас нет сетевого эффекта?

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

Краткая идея статьи

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

Идея следующая:

  • запускаем эксперимент в двух сетапах параллельно, первый сетап классический, полностью рандомизированный, второй для уменьшения сетевого эффекта, например, на кластеризованных группах.

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

Убедили, как подобрать окно переключения по расстоянию и времени?

При определении длины временного промежутка и размера географической зоны основной вопрос заключается в качестве данных, которые мы соберем с выделенного кусочка. Глобально идея заключается в том, что нам необходимо найти компромисс между смещением данных (Bias) и предельной погрешностью выборки (Margin). Подробнее об этом можно почитать вот тут, но давайте кратко обсудим идею.

Сформулируем, что есть Bias, а что Margin of Error.

Разница в средних между нашими группами при семплирования назовем Bias смещение. Например, когда мы делаем АА-тест нам бы очень хотелось, чтобы разница в средних между группами А1 и А2 не было, иначе мы не сможем запустить наш тест. Имеется в виду, конечно, что не будет статистически значимой разницы.

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

Теперь опустимся на практику и поговорим про параметры сплитования, и как они связаны с Bias и Margin. Для более удобных формулировок введем понятие Unit. Unit кусочек времени и пространства, который отсекается определенным промежутком времени и геозоной.

Теперь обсудим связь размера Unit'a c Bias. Когда мы уменьшаем географическую зону и промежуток переключения групп, выборка уменьшается, и мы с большей вероятностью соберем нерепрезентативные смещенные данные. Представим ситуацию, где мы хотим протестировать два алгоритма, один из которых обрабатывает заказы по мере поступления, а другой - обрабатывает сначала короткие поездки, а уже потом все остальные. Тогда при слишком быстром переключении мы можем получить ситуацию, при которой один алгоритм будет обрабатывать только короткие поездки, а другой будет пытаться исправить ситуацию после выбора другого алгоритма. При этом сделать какие-то обобщающие выводы мы не сможем, так как в данных по поездкам будет заложено смещение, которое возникло из-за слишком частой смены групп. То есть при уменьшении размера Unit'a (уменьшаем окно сплитования, например, было 20 минут стало 10, и уменьшении геозоны стали работать с более маленькими гексагонами) растет Bias.

С Margin капельку сложнее, но мы справимся. Margin зависит от двух вещей от разброса данных, с которыми мы работаем и от их количества (снова см. подробнее тут).

 Margin \sim \sqrt{\frac{D}{n}},

где D дисперсия выборки, а n сколько Unit'ов у нас есть. При уменьшении периода переключения или работе с более мелкими геозонами растет количество Unit'ов, с которых мы собираем наблюдения. Но при этом растет и дисперсия нашей выборки маленькие Unit'ы менее похожи друг на друга и содержат больше выбросов. При увеличении сплита и, как следствие, объема данных внутри него эти выбросы сглаживаются, дисперсия снижается.

Зависимость, конечно, не однозначная, но на практике всё-таки дисперсия растёт не так быстро, как количество зон, потому что города ведут себя относительно стабильно, за исключением, каких-то особых периодов, например, дождя или вечера 31-го декабря. Поэтому в целом можно сказать, что при уменьшении размера Unit'а Margin падает. Но важно иметь ввиду, что природа этой зависимости на ваших данных может оказаться другой.

Получается вот такая сложная зависимость, с которой нам нужно как-то жить, если хотим запустить Switchback):

Как же жить с такой сложной зависимостью на практике:

Можно провести АА-тест на исторических данных и посмотреть, за какое время он сходится и какой разброс в метриках мы получаем, чтобы сделать выводы о том, насколько вас это устраивает.

CookBook для запуска первого в вашей жизни Switchback-теста такой (такие вводные работают для нас):

  • держим тест около 2 недель в зависимости от объема рынка;

  • проводим сплитование по гексагонам размером 6 (то есть по районам площадью 36 кв. км.);

  • переключение происходит раз в 20 минут.

Выглядит это примерно так:

Теперь самое время пойти и запустить с первыми вводными AA-тест в Switchback на исторических данных для своего маркетплейса!

Слабые стороны Switchback

Конечно, Switchback не безгрешен и имеет несколько особенностей, с которыми стоит быть внимательными.

Сохранение сетевого эффекта

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

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

Осторожно, вы в эксперименте

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

Бороться с этим можно такой же очисткой данных, как и в предыдущем случае.

Мощность ниже

Чистка может негативно повлиять на мощность эксперимента. Кроме этого, на мощность switchback негативно влияет и единица рандомайза пара регион+время.

Сложность экспериментов с визуальными изменениями

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

Долгосрочный эффект

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

Сходимость групп

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

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

Завести А/А-тест в дизайн вашего эксперимента можно двумя путями. Если вы уверены, что ваш маркетплейс стабилен во времени, то можно подержать А/А-тест перед А/В-тестом и убедиться, что разбиение на группы равномерное. Если же такой уверенности нет, то можно сразу провести А/А/В-тест. Но здесь хорошо бы проверить, что вам хватит данных, чтобы честно сравнить группы А1 и А2 между собой.

Если всё-таки ваш выбор пал на А/А/В-тест, то распределение по группам лучше держать 25 %/25 %/50 %, так в теории мощность вашего теста будет выше (по сравнению с менее сбалансированными группами), подробнее об этом можно почитать вот тут.

О следующей статье

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

В подготовке статьи участвовали Артём Солоухин, Ксения Мензорова, Николай Ишмаметьев. Также выражаем благодарность за помощь в подготовке статьи ребятам из expf.ru, Искандеру Мирмахмадову и Виталию Черемисинову.

Подробнее..

RamblerMeetupampUsermodel

18.06.2021 18:20:53 | Автор: admin

Мы долго шли к этому и вот наконец! Наш внутренний RamblerMeetup&Usermodel выходит в свет! Уже 30 июня эксперты поделятся своими кейсами в области ML и Big Data.

Митап пройдет в онлайн-формате, начало в 19:00. Обязательна предварительная регистрация на Timepad.

Ведущий и модератор:

Артём Выборнов, руководитель направления машинного обучения и анализа данных Rambler&Co

В программе:

Павел Ашихмин, инженер-разработчик Python, Rambler&Co

Тема: Spark Structured Streaming и распределенный джойн в реальном времени

Spark Structured Streaming фреймворк для распределенной обработки данных в режиме, близком к реальному времени. Его внушительный функционал позволяет строить сложные realtime-pipeline поставки данных для аналитики и машинного обучения. Павел расскажет про свой опыт построения realtime-контура обработки данных с использованием Spark Structured Streaming. Обсудим, с какими подводными камнями можно столкнуться, если использовать его вместе с Kafka и Clickhouse, и как увеличить свои шансы на надежную шину для передачи данных в реальном времени.

Роман Ананьев, NoSQL Engineer, Avito

Тема: Kafka в Multi DC реалиях

Есть много способов сделать Apache Kafka работающей в нескольких DC от создания единого широкого кластера до разных версий репликаций между разрозненными инсталляциями. Также возможно совмещать все вместе. Рассмотрим на реализованных проектах, какой из вариантов в каком случае подходит и каким образом их можно воплотить. Погрузимся в работу таких репликаторов, как Mirror Maker 2 и Uber Replicator. Поговорим о концепте Kafka Federation, который объединяет в себе разные варианты Kafka в Multi DC.

Александр Ошурков, руководитель центра компетенций ML, МКБ

Тема: Как стартовать ML-практику в финтехе

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

Свои вопросы докладчикам уже можно задавать в чате в Telegram.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru