Giter Club home page Giter Club logo

graduate-2022-rec-sys's Introduction

hero3-wide

Рекомендательная система для онлайн-гипермаркета Instacart

Ссылки

  1. Видео-презентация (10 минут) + оценка эксперта
  2. Презентация в Google Slides
  3. Датасет на kaggle: Instacart dataset
  4. Описание задачи
  5. Итоги

Материалы проекта

  1. https://github.com/d-01/instacartlib
    • Разработанная, согласно техническому заданию, программная библиотека-обертка для ML модели.
  2. https://colab.research.google.com/drive/1aFc-e_u5W-BrA7cdp6E2qZsgZtiJCGC8
    • Демонстрация программной библиотеки в Google Colab
  3. https://github.com/d-01/graduate-2022-rec-sys/blob/main/submission.csv
    • Файл для оценки модели на платформе kaggle
    • Private / public score: 0.31839 / 0.32082

Описание задачи

Техническое задание

  1. Алгоритм предсказания покупок пользователя:
    1. Точность по метрике MAP@10 > 0.25
    2. Время обучения: не более 5 часов.
    3. Прогнозирование на всех покупателей: не более 15 минут.
  2. Программная библиотека со следующими функциями:
    1. Выдать K релевантных товаров по ID одного или нескольких пользователей.
    2. Добавить новые данные о транзакциях и о товарах.
    3. Обучить рекомендательную систему заново.

Описание датасета

Датасет состоит из 2 файлов:

  • transactions.csv (26M, 9) - транзакции покупателей
  • products.csv (50k, 6) - товары, отделы, категории

transactions.csv

image-20220707185326355

  • order_id - уникальный идентификатор заказа
  • user_id - уникальный идентификатор пользователя (100 000)
  • order_number - порядковый номер заказа в истории покупок пользователя (1-99)
  • order_dow - день недели формирования заказа (0-6)
  • order_hour_of_day - час дня формирования заказа (0-23)
  • days_since_prior_order - число дней с совершения предыдущего заказа данным пользователем
  • product_id - уникальный идентификатор товара (49 465)
  • add_to_cart_order - порядковый номер товара при добавлении в корзину
  • reordered - был ли товар "перезаказан"

products.csv

image-20220707185633829

  • product_id - уникальный идентификатор товара
  • product_name - название товара
  • aisle_id - уникальный идентификатор категории (134)
  • department_id - уникальный идентификатор отдела (21)
  • aisle - название категории
  • department - название отдела

Задача предсказания целевой корзины

Next Basket Prediction / Предсказание целевой корзины

Next Basket -- целевая корзина, следующая после последней:

User orders:
1   | 32792 47766 20574 12000 48110 22474 16589 35917 27344 30489 27966 13176
2   | 16797 47526  8479 47766 19051  8138
... | 47766 32792 20574  7781 28874
N   | 49451 32792 32139 34688 36735 37646 22829 24852 47209 33276 45613  9681
N+1 |     ?     ?     ?     ?     ?     ?     ?     ?     ?     ?

Метрика Mean Average Precision

Mean Average Precision at 10 / MAP@10

Если обычная точность (precision) оценивает все релевантные элементы равноценно, то AP@K снижает оценку если релевантный элемент, стоит в списке ниже нерелевантного.

image-20220712092123287

От чего зависит оценка (score) по метрике MAP@K:

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

От чего не зависит оценка:

  1. Если поменять местами два релевантных или два нерелевантных элемента оценка не изменится.
  2. Предсказания ниже первых k, не учитываются.

Исследование моделей

Протестированные модели и оценки на kaggle

Генеративная нейросеть

  • Предсказание вектора заказа с помощью нейросети (TCN 14k).

Коллаборативная фильтрация

  • SVD, 100 (implicit), последние 10 заказов, только из купленных ранее.

TIFU-KNN

  • Предсказание с помощью модели TIFU-KNN (k=300, a=0.9): 90% индивидуальный компонент + 10% коллаборативный компонент.

Частотная модель

  • Индивидуальный топ 10 из последних 8 заказов + сортировка по AvgCartPos

Классификатор

  • Задача бинарной классификации: пара пользователь-товар относится к классу 1 если пользователь добавит товар в целевую корзину, 0 -- если нет.
  • CatBoostClassifier (21 признак) + популярные товары

Все эксперименты и kaggle private score

МЕТОД MAP@10
Топ 10 популярных 0.04858
Предсказание вектора корзины с помощью нейросети (TCN 14k) 0.04938
SVD, 128 (surpriselib) 0.08375
SVD, 100 (implicit) 0.12076
SVD, 100 (implicit), последние 10 заказов 0.13518
SVD, 100 (implicit), только из купленных ранее 0.14569
SVD, 100 (implicit), последние 10 заказов, только из купленных ранее 0.16385
Зачет 0.25000
Индивидуальный топ 10 из последних 10 заказов + SVD для сортировки 0.25777
Индивидуальный топ 10 0.27455
Индивидуальный топ 10 из последних 5 заказов 0.29526
Предсказание с помощью TIFU-KNN (a=1.0) 0.29716
Предсказание с помощью TIFU-KNN (k=300, a=0.9) 0.30036
Индивидуальный топ 10 из последних 11 заказов 0.30108
Индивидуальный топ 10 из последних 10 заказов 0.30199
Индивидуальный топ 10 из последних 9 заказов 0.30275
Индивидуальный топ 10 из последних 7 заказов 0.30291
Индивидуальный топ 10 из последних 8 заказов 0.30326
Индивидуальный топ 10 из последних 10 заказов + сортировка по AvgCartPos 0.30381
Линейный классификатор (12 фичей) 0.30572
Индивидуальный топ 10 из последних 8 заказов + сортировка по AvgCartPos 0.30716
Линейный классификатор (13 фичей) 0.30856
Линейный классификатор (13 фичей) + коллаб. фильтр. по схожим товарам 0.30868
Линейный классификатор (13 фичей) + коллаб. фильтр. по схожим пользователям 0.30876
Линейный классификатор (16 фичей) 0.31347
Линейный классификатор (17 фичей) 0.31431
GBC (21 фича) 0.31785
GBC (21 фича) + популярные товары 0.31803
CatBoostClassifier (21 фича) + популярные товары 0.31839

В следующих разделах дано более подробное описание 5-и моделей.

Частотная модель

Простая частотная модель или индивидуальный топ-10 основана на подсчете покупок каждого товара индивидуально для каждого пользователя.

image-20220713104136544

Для каждого пользователя:

  1. Считаем частоту покупок каждого товара.
  2. Сортируем купленные товары по частоте и используем топ-10 в качестве предсказаний.

Улучшения:

  1. Используем не более 8 последних заказов каждого пользователя.
  2. Дополнительно сортируем товары по средней позиции в корзине пользователя (AvgCartPos).

Результаты

plot-model-freq

Предсказание вектора корзины с помощью нейросети

Участник, занявший 3е место в соревновании Instacart Market Basket Analysis, использовал чисто нейросетевой подход (без фичей, созданных вручную / hand-crafted features) [подробнее].

Второе место занял участник использовав hand-crafted фичи [подробнее].

История пользователя состоит из последовательности заказов. Каждый заказ (корзина) представляется m-hot вектором из 0 и 1. Длина вектора равна числу всех возможных товаров.

Тогда задача сводится к NLP задаче генерации последовательности:

  • Слово (embedding) = корзина (m-hot вектор)
  • Предложение (sequence) = история заказов (history)

Обучающий датасет: n - 1 заказ (кроме последнего), последний заказ используется как метка.

image-20220706174526356

Тестовый датасет: n - 1 заказ (включая последний), предсказывается следующий (целевой).

image-20220712121126891

Архитектура нейросети: Temporal Convolutional Network (TCN, Bai et al., 2018).

image-20220706174457556

Результаты

plot-model-nn

Выводы

  1. Нейросеть работает лучше случайной модели, но точность равносильна примитивной модели, усредняющей входные вектора.
    • Нейросеть схлопнулась (collapsed) к константному решению: для любого пользователя один и тот же ответ, состоящий из самых популярных товаров, поэтому точность такая же как "Топ 10 популярных".
  2. Недостаточный размер нейросети для данный задачи.
    • Стоило увеличить число параметров до 14M (увеличить в 1000 раз, 14k * 1000 = 14M).
  3. Проблемы совместимости нестандартных тензоров (sparse / ragged) и некоторых слоев (Conv1D) в Keras.
  4. Трудности при выборе функции потерь для задачи multilabel classification:
    • Классификация на бесконечное число классов.
    • Низкая эффективность обучения, низкая точность.

Коллаборативная фильтрация

Long format (26M rows):

image-20220706185808283

Wide format (sparse, 5000M):

image-20220706192624774

  • Density: 0.2% (0.001912)
  • Sparsity: 99.8% (0.998087)

SVD разложение:

image-20220706193049199

Результаты

plot-model-svd

Выводы

  1. Точность возрастает, если не учитывать покупки сделанные давно.
    • Не смотря на то, что плотность матрицы рейтингов снижается, т.е.:
      1. Для всех заказов density=0.19% 9459065 / (100000 * 49465) = 0.001912
      2. Для последних 10 density=0.11% 5470374 / (100000 * 48297) = 0.001133
  2. Область применения коллаборативной фильтрации:
    1. Предсказание рейтингов (explicit feedback).
    2. Предсказание однократных покупок (implicit feedback).
    3. Поиск похожих товаров (item similarity).
    4. Поиск похожих пользователей (user similarity).
  3. Не подходит для периодических покупок, уступает простой частотной модели.

TIFU-KNN

Temporal-Item-Frequency-based User-KNN (TIFU-KNN) -- рекомендации для каждого пользователя основаны на сумме двух векторов: индивидуальный компонент (PIF) и коллаборативный компонент.

Способ описанный в научной работе от 31 мая 2020 года (https://arxiv.org/abs/2006.00556):

image-20220707193345544

Рекомендации Основанные на Персонализированной Частотности Товаров

Personalized Item Frequency (PIF) -- векторное представление истории покупок пользователя. Длина вектора равна количеству всевозможных товаров.

Анализ датасета Instacart:

  • В среднем 3 из 10 товаров в корзине пользователя заказаны впервые.
  • 3 из 4 товаров можно найти в заказах похожих пользователей.
  • 11% товаров в корзине не предсказуемы этой моделью, т.к. ни сам пользователь ни похожие пользователи ранее не покупали этот товар.

PIF

PIF / Personalized Item Frequency

Каждый заказ (корзина) представляется бинарным m-hot вектором, где 1 стоит напротив добавленных в корзину товаров.

Векторное представление истории покупок пользователя или, просто, вектор пользователя рассчитывается как взвешенная сумма векторов корзин с иерархическим (group + basket) затухающим коэффициентом.

image-20220708053212674

Роль затухающего коэффициента

Без затухания:

order | 0    1    2    3    4    5    6    7    8    9   | sum
----- | ------------------------------------------------ | ---
prod1 | 1.0  1.0  1.0  1.0  1.0  0.0  0.0  0.0  0.0  0.0 | 5.0
prod2 | 0.0  0.0  0.0  0.0  0.0  1.0  1.0  1.0  1.0  1.0 | 5.0
  • Одинаковое значение для товара купленного давно (prod1=5.0) и для товара купленного недавно (prod2=5.0).

С затуханием:

order | 0    1    2    3    4    5    6    7    8    9   | sum
----- | ------------------------------------------------ | ---
prod1 | 0.1  0.2  0.3  0.4  0.5  0.0  0.0  0.0  0.0  0.0 | 1.5
prod2 | 0.0  0.0  0.0  0.0  0.0  0.6  0.7  0.8  0.9  1.0 | 4.0
  • Для товара купленного давно значение меньше (prod1=1.5), чем для товара купленного недавно (prod2=4.0).

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

$$ P = \alpha \cdot u_t + (1-\alpha) \cdot u_n $$

Предсказание состоит из двух компонентов:

  1. Индивидуальный компонент (repeated purchase component) представлен как вектор пользователя $u_t$ (PIF).
  2. Коллаборативный компонент представлен как усредненный PIF вектор похожих пользователей (ближайших соседей): $u_n = \text{avg}(U_{neighbor})$, где $U_{neighbor}$ вектора ближайших $k$ соседей ($k=300$).

Расчет коллаборативного компонента

Задача:

  • Для расчета коллаборативного компонента для каждого из 100 000 пользователей необходимо найти 300 ближайших соседей используя PIF вектора пользователей.

Доп. условия:

  1. Разряженные вектора (CSR format)
  2. Длина вектора = 49 465

Время работы примитивной реализации KNN: ≈6 часов.

Для уменьшения времени необходимо использовать алгоритм ANN (Approximate Nearest Neighbour). В этой работе я использовал его открытую реализацию facebookresearch/pysparnn, т.к. она поддерживает разряженные вектора.

Время работы ANN составило 5 минут.

Результаты

plot-model-tifu

  1. При $\alpha=1.0$, 100% информации берется из истории пользователя.
  2. При $\alpha=0.9$, 90% информации берется из истории пользователя (индивидуальный компонент), а 10% из истории 300 похожих пользователей (коллаборативный компонент).

Выводы

  1. 70% товаров пользователь заказывал ранее.
  2. До 20% товаров, заказанных пользователем впервые, можно найти в истории похожих пользователей.
  3. 10% товаров, заказанных впервые, не встречаются в истории пользователя, ни похожих пользователей.
  4. Затухающий коэффициент дает прирост в оценке. При $\alpha=1.0$ без затухающего коэффициента модель равносильна простой частотной модели:
    1. Без коэффициента затухания: 0.27455
    2. С коэффициентом затухания: 0.29716

Классификатор

Задача сводится к задаче бинарной классификации: для каждой пары пользователь-товар (user-item) модель предсказывает значение 1, если товар будет перезаказан (есть в целевой корзине), 0 если нет.

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

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

Тренировочный датасет

image-20220719181442714

  • В тренировочном датасете используются 5 предпоследних заказов, а последний в качестве целевого.

На основании таблицы транзакций рассчитываются признаки (features / фичи). Целевой признак: in_target = 1 если товар есть в целевой корзине, 0 если нет.

Тестовый датасет

image-20220720043230744

  • В тестовом датасете используются 5 последних заказов, а следующий (целевой) предсказывается с помощью обученной модели классификатора.

Для каждой пары пользователь-товар выходной вектор содержит значение в интервале [0, 1] выражающее вероятность перезаказа товара Б пользователем А. Для финального предсказания товары сортируются по убыванию вероятности.

Feature engineering

Конструирование фичей

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

Признаки 3-х типов:

  1. Признаки пользователя (u_ user):
    • Например: число транзакций, число уникальных товаров в истории, число заказов, средний размер заказа, среднее время между заказами и т.д.
  2. Признаки товара (i_ item):
    • Например: сколько пользователей купили этот товар, сколько раз в среднем пользователь покупает этот товар, среднее время между заказами этого товара и т.д.
  3. Признаки взаимодействия пользователя с конкретным товаром (ui_ user-item):
    • Например: сколько раз пользователь купил определенный товар, дней с момента последнего заказа этого товара, средняя позиция товара в корзине и т.д.

feature-importance-03_catboost

  • i_days_delay_global_mid -- средний интервал между заказами товара Б среди всех пользователей;

    • Как часто пользователи в среднем перезаказывают товар Б.
  • ui_total_buy_ratio -- доля заказов пользователя А, в которых есть товар Б;

    • = 1.0 если товар Б есть в 10 из 10 заказов пользователя А.
    • = 0.2 если товар Б есть в 2 из 10 заказов пользователя А.
  • u_n_transactions -- число транзакций пользователя А;

    • Отражает активность пользователя.
  • u_unique_items -- число уникальных товаров в истории покупок пользователя А;

    • Отражает его склонность пробовать новые товары.
  • ui_total_buy -- число заказов пользователя А, в которых есть товар Б;

  • ui_readyness_mid_abs -- отклонение пользователя А от привычного интервала перезаказа товара Б.

Показатель readyness (готовность):

  • Например в среднем между заказами товара Б проходит 10 дней, т.е. ui_days_delay_mid = 10.
  • Последний раз пользователь купил товар Б 12 дней назад, т.е. ui_days_passed = 12.
  • Тогда ui_readyness_mid = (ui_days_passed - ui_days_delay_mid) = 2
    • Отрицательные значения = с последнего заказа прошло слишком мало времени, пользователь не готов купить товар.
    • Большое значение = прошло больше времени чем обычно, пользователь готов купить товар.

Полное описание всех признаков в приложении.

Результаты

plot-model-clf

Выводы

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

Итоги

plot-model-all5

  1. Одни и те же методы не подходят для разных типов товаров:

    1. Фильмы, музыка, книжный магазин

      • Однократные покупки
      • Задача: коллаборативная фильтрация
      • Рекомендации на основе похожих пользователей или товаров
    2. Гипермаркеты (еда, продовольственные товары)

      • Периодические покупки

      • Задача: генерация следующего набора элементов (NBR / NBP / Sets2sets)

      • Простые модели: подсчет покупок, средний интервал между заказами

    3. Техника, услуги, одежда

      • Смешанные покупки: однократные + периодические

      • Задача: генерация следующего элемента последовательности (Seq2seq)

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

Библиотека instacartlib

Установка:

pip install https://github.com/d-01/instacartlib/archive/main.zip

Загрузка датасета:

from instacartlib import InstacartDataset

InstacartDataset(verbose=1).download(to_dir='instacart_data')

Инициализация API и загрузка предобученной модели:

from instacartlib import NextBasketPrediction

nbp = NextBasketPrediction(verbose=1)
nbp.add_data('instacart_data')
nbp.load_model('catboost')  # or 'gbc'

Получить предсказания / рекомендации для указанных пользователей:

nbp.get_predictions(user_ids=[20001, 85768])

image-20220713125203808

nbp.predictions_to_csv('submission.csv')

submission.csv:

user_id,product_id
1,196 12427 25133 10258 46149 39657 38928 35951 13032 49235
2,47209 19156 1559 18523 33754 16589 21709 24852 39928 22825
3,39190 47766 43961 21903 17668 18599 16797 48523 32402 22035
...

Обучение новой модели:

nbp_new = NextBasketPrediction(verbose=1)
nbp_new.add_data('instacart_data')
nbp_new.train_model()
nbp_new.save_model('model_nbp.dump')

Время обучения модели (Google Colab):

  • ≈25 мин. (GBC)
  • ≈17 мин. (CatBoost)
  • ≈1 мин. (CatBoost, GPU)

Время генерации предсказаний: ≈2 сек.

Приложение

Описание признаков

(п. А = пользователь А; товар Б)

  • u_n_orders: число заказов п. А.
  • ui_n_chances: число заказов п. А после первой покупки товара Б (включительно).
  • ui_total_buy: число заказов товара Б пользователем А.
  • ui_total_buy_ratio: = ui_total_buy / u_n_orders доля заказов п. А в которых есть товар Б.
  • ui_chance_buy_ratio: = ui_total_buy / u_n_chances доля заказов п. А, в которых есть товар Б, после первой покупки товара Б.
  • u_n_transactions: число транзакций пользователя А.
  • u_unique_items: число уникальных товаров в истории п. А.
  • u_order_size_mid: средний размер корзины п. А.
  • i_n_popularity: число пользователей, купивших товар Б хотя бы раз.
  • i_n_orders_mid: среднее число заказов товара Б среди пользователей, купивших его хотя бы раз.
  • ui_avg_cart_pos: средняя позиция товара Б. в корзине пользователя А.
  • ui_days_delay_max: максимальная задержка (дней) между покупками товара Б пользователем А.
  • ui_days_delay_mid: медианная задержка (дней) между покупками товара Б пользователем А.
  • i_days_delay_global_mid: медианная задержка (дней) между покупками товара Б среди всех пользователей.
  • ui_days_passed: прогнозируемое число дней до следующего заказа пользователя А.
  • ui_readyness_max: дней до следующего заказа п. А сверх своей максимальной задержки между покупками товара Б. Пример:
    • readyness=10: пользователь А не покупал товар Б больше 10 дней сверх своей обычной задержки.
    • readyness=-10: пользователь А может "обойтись" без товара Б еще 10 дней.
  • ui_readyness_max_abs: модуль значения readyness. Большое значение сигнализирует о нетипичном отклонении в поведении пользователя относительно своего обычного поведения.
  • ui_readyness_mid: аналогично ui_readyness_max для медианы.
  • ui_readyness_mid_abs: аналогично ui_readyness_max_abs для медианы.
  • ui_readyness_global_mid: задержка п. А в покупке товара Б сверх общей задержки среди всех пользователей покупающих этот товар.
  • ui_readyness_global_mid_abs: модуль значения ui_readyness_global_mid. Большое значение сигнализирует о нетипичном отклонении в поведении пользователя относительно других пользователей.

Features description

  • u_n_orders: total number of orders made by user.
  • ui_n_chances: number of orders in which user A had a chance to buy item B. That is a number of orders following the order in which user A bought item B, including the order itself.
  • ui_total_buy: times user A bought item B.
  • ui_total_buy_ratio: = ui_total_buy / u_n_orders
  • ui_chance_buy_ratio: = ui_total_buy / u_n_chances
  • u_n_transactions: number of user's A transactions.
  • u_unique_items: number of unique items in user's A history.
  • u_order_size_mid: user's A average order size.
  • i_n_popularity: number of users who purchaised this item at least once.
  • i_n_orders_mid: number of purchaises on average across all users who purchaised this item at least once.
  • avg_cart_pos: position of item B in user's A cart on average.
  • ui_days_delay_max: the longest (in days) user A gone without buying item B.
  • ui_days_delay_mid: median number of days user A gone without buying item B.
  • i_days_delay_global_mid: median number of days any user gone without buying item B.
  • ui_days_passed: days passed since last order (prediction based on medium delay between user's orders).
  • ui_readyness_max: days passed exceeds max delay for particular item. Example:
    • readyness=10: means user hasn't bought the item more then 10 days above his longest delay.
    • readyness=-10: 10 days left until delay exceeds maximum delay, i.e. user probably can go longer without buying this item.
  • ui_readyness_max_abs: absolute value of readyness. The bigger the value the more unusual is delay.
  • ui_readyness_mid: days passed exceeds median delay for particular item.
  • ui_readyness_mid_abs: absolute value of ui_readyness_mid.
  • ui_readyness_global_mid: user readyness relative to global delay for particular item.
  • ui_readyness_global_mid_abs: absolute value of ui_readyness_global_mid.

graduate-2022-rec-sys's People

Contributors

d-01 avatar lifefornerzhul avatar

Stargazers

 avatar  avatar  avatar  avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.