Обработка и визуализация данных о продажах пиццы с помощью Julia DataFrames.jl и Plots.jl

В этой статье мы рассмотрим данные о продажах пиццы, которые находятся здесь:

https://vincentarelbundock.github.io/Rdatasets/csv/gt/pizzaplace.csv

Этим типом данных можно управлять в приложении для работы с электронными таблицами, таком как Excel, и с использованием фреймов данных, популярных в таких языках, как R, Python (Pandas) и Julia (DataFrames.jl).

Загрузка данных

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

using DataFrames, CSV

url = "https://vincentarelbundock.github.io/Rdatasets/csv/gt/pizzaplace.csv"
filename = download(url)
all_pizzas = CSV.read(filename, DataFrame)

# Get rid of column with row numbers
all_pizzas = all_pizzas[:, 2:end]

# Pick most interesting columns
pz = select(all_pizzas, :id, :name, :size, :price)

Мы можем взглянуть на первые строки представления, чтобы увидеть, как это выглядит в Julia REPL (цикл Read Evaluate Program):

julia> first(pz, 4)
4×4 DataFrame
│ Row │ id          │ name        │ size   │ price   │
│     │ String      │ String      │ String │ Float64 │
├─────┼─────────────┼─────────────┼────────┼─────────┤
│ 1   │ 2015-000001 │ hawaiian    │ M      │ 13.25   │
│ 2   │ 2015-000002 │ classic_dlx │ M      │ 16.0    │
│ 3   │ 2015-000002 │ mexicana    │ M      │ 16.0    │
│ 4   │ 2015-000002 │ thai_ckn    │ L      │ 20.75   │

julia> nrow(pz)
49574

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

Данные выборки

Мы собираемся выбрать случайную выборку из 16 строк из 49 574 строк, которые мы загрузили. Для этого мы случайным образом перемешаем индексы строк от 1 до 49 574.

julia> using Random

julia> rows = shuffle(1:nrow(pz))

Затем мы можем выбрать первые 16 строк из этих перемешанных строк, чтобы получить 16 случайных строк из наших исходных данных:

julia> sample = pz[rows[1:16], :]
16×4 DataFrame
│ Row │ id          │ name         │ size   │ price   │
│     │ String      │ String       │ String │ Float64 │
├─────┼─────────────┼──────────────┼────────┼─────────┤
│ 1   │ 2015-000348 │ thai_ckn     │ S      │ 12.75   │
│ 2   │ 2015-007731 │ green_garden │ S      │ 12.0    │
│ 3   │ 2015-014409 │ hawaiian     │ S      │ 10.5    │
│ 4   │ 2015-017919 │ sicilian     │ S      │ 12.25   │
│ 5   │ 2015-015337 │ prsc_argla   │ M      │ 16.5    │
│ 6   │ 2015-006190 │ ital_veggie  │ S      │ 12.75   │
│ 7   │ 2015-015481 │ spin_pesto   │ S      │ 12.5    │
│ 8   │ 2015-007865 │ hawaiian     │ L      │ 16.5    │
│ 9   │ 2015-001928 │ bbq_ckn      │ L      │ 20.75   │
│ 10  │ 2015-017298 │ cali_ckn     │ S      │ 12.75   │
│ 11  │ 2015-018872 │ four_cheese  │ L      │ 17.95   │
│ 12  │ 2015-018036 │ four_cheese  │ L      │ 17.95   │
│ 13  │ 2015-011238 │ classic_dlx  │ L      │ 20.5    │
│ 14  │ 2015-013366 │ classic_dlx  │ M      │ 16.0    │
│ 15  │ 2015-014380 │ bbq_ckn      │ M      │ 16.75   │
│ 16  │ 2015-020245 │ ital_cpcllo  │ S      │ 12.0    │

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

Перенести продажи пиццы в электронную таблицу

Однако мы не можем легко скопировать и вставить данные в этом «красивом» формате в приложение для работы с электронными таблицами, такое как Excel. Мы хотим, чтобы это было в формате CSV. К счастью, система отображения Julia позволяет нам отображать одни и те же данные во многих различных форматах.

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

julia> display(sample)
16×4 DataFrame
│ Row │ id          │ name         │ size   │ price   │
│     │ String      │ String       │ String │ Float64 │
├─────┼─────────────┼──────────────┼────────┼─────────┤
│ 1   │ 2015-000348 │ thai_ckn     │ S      │ 12.75   │
│ 2   │ 2015-007731 │ green_garden │ S      │ 12.0    │
│ 3   │ 2015-014409 │ hawaiian     │ S      │ 10.5    │
│ 4   │ 2015-017919 │ sicilian     │ S      │ 12.25   │
│ 5   │ 2015-015337 │ prsc_argla   │ M      │ 16.5    │
│ 6   │ 2015-006190 │ ital_veggie  │ S      │ 12.75   │
│ 7   │ 2015-015481 │ spin_pesto   │ S      │ 12.5    │
│ 8   │ 2015-007865 │ hawaiian     │ L      │ 16.5    │
│ 9   │ 2015-001928 │ bbq_ckn      │ L      │ 20.75   │
│ 10  │ 2015-017298 │ cali_ckn     │ S      │ 12.75   │
│ 11  │ 2015-018872 │ four_cheese  │ L      │ 17.95   │
│ 12  │ 2015-018036 │ four_cheese  │ L      │ 17.95   │
│ 13  │ 2015-011238 │ classic_dlx  │ L      │ 20.5    │
│ 14  │ 2015-013366 │ classic_dlx  │ M      │ 16.0    │
│ 15  │ 2015-014380 │ bbq_ckn      │ M      │ 16.75   │
│ 16  │ 2015-020245 │ ital_cpcllo  │ S      │ 12.0    │

Однако эта функция также может принимать в качестве аргумента тип MIME, который используется, например, когда Джулия используется в записной книжке, которая поддерживает более широкие графические возможности. В записной книжке мы предоставляем тип text/html MIME, но в этом случае нам нужны наши данные в формате CSV:

julia> display("text/csv", sample)
"id","name","size","price"
"2015-000348","thai_ckn","S",12.75
"2015-007731","green_garden","S",12.0
"2015-014409","hawaiian","S",10.5
"2015-017919","sicilian","S",12.25
"2015-015337","prsc_argla","M",16.5
"2015-006190","ital_veggie","S",12.75
"2015-015481","spin_pesto","S",12.5
"2015-007865","hawaiian","L",16.5
"2015-001928","bbq_ckn","L",20.75
"2015-017298","cali_ckn","S",12.75
"2015-018872","four_cheese","L",17.95
"2015-018036","four_cheese","L",17.95
"2015-011238","classic_dlx","L",20.5
"2015-013366","classic_dlx","M",16.0
"2015-014380","bbq_ckn","M",16.75
"2015-020245","ital_cpcllo","S",12.0

Вы можете просто скопировать и вставить это в приложение для работы с электронными таблицами. Но есть много способов сделать это. Мы могли записывать в файл с помощью пакета CSV:

julia> CSV.write("pizza-sales.csv", sample)
"pizza-sales.csv"

Но вы можете добиться этого, даже используя типы MIME с show:

julia> open("pizza-sales.csv", "w") do io
           show(io, MIME("text/csv"), sample)
       end

Или как насчет того, чтобы быть немного сумасшедшим и использовать IOBuffer с clipboard:

julia> buf = IOBuffer();
julia> CSV.write(buf, sample);
julia> clipboard(String(take!(buf)))

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

Вместо take! мы могли бы использовать seekstart, чтобы перейти к началу IOBuffer и нормально читать из него. Однако преимущество использования take! заключается в том, что вы очищаете буфер, чтобы он больше не занимал память для хранения данных CSV.

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

Получить то же самое в Юлии несложно. Нам просто нужно импортировать пакет Statistics.

julia> using Statistics

julia> sum(sample.price)
240.39999999999998

julia> mean(sample.price)
15.024999999999999

julia> minimum(sample.price)
10.5

julia> maximum(sample.price)
20.75

julia> nrow(sample)
16

Построение гистограмм

При выполнении Исследовательского анализа данных (EDA) одна из первых полезных вещей, которые вы можете сделать, чтобы получить представление о данных, - это отобразить интересные данные в виде гистограмм.

julia> histogram(sample.price, bins = 4)

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

julia> histogram(pz.price, bins = 6)

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

julia> p1 = histogram(sample.price,
               bins  = 4,
               xlims = (0, 40),
               xaxis = "price",
               yaxis = "count",
               legend=nothing)
               
julia> p2 = histogram(pz.price,
               bins  = 4,
               xlims = (0, 40),
               xaxis = "price",
               yaxis = "count",
               legend=nothing)
               
julia> p = plot(p1, p2)

Если вы хотите включить свои графики в отчеты и документы, вы можете сохранить их на диск с помощью функции savefig. Он выберет желаемый формат для использования на основе расширения вашего имени файла. Поддерживаются .png, .pdf, .svg и несколько других форматов:

julia> savefig(p, "pizza-price-hist.png")

Интересные вопросы о наших данных

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

  • Какие пиццы продаются больше всего?
  • Пицца какого размера: маленькая, средняя, ​​большая и т. Д. Продается больше всего?
  • Какая пицца приносит больше всего дохода?
  • Сколько обычно тратится на каждый заказ пиццы?

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

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

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

Нарезка и нарезание фреймов данных

Работая с Data Frames, мы получили несколько ключевых функций, которые помогают нам манипулировать данными:

  • select Выбирает подмножество столбцов и, возможно, переименовывает или трансформирует значения в столбце.
  • transform Аналогично select, за исключением того, что мы не удаляем столбцы. Мы просто переименовываем и трансформируем выбранные столбцы.
  • groupby Превращает таблицу в несколько таблиц. Разделение выполняется путем создания таблицы для каждого уникального значения определенного выбранного столбца.
  • combine Берет несколько таблиц и снова превращает их в одну. Позволяет свернуть все строки в каждой таблице в одну строку.
  • join Позволяет сопоставить столбец в двух разных таблицах, чтобы объединить их в одну таблицу.

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

julia> tiny = first(sample, 3)
3×4 DataFrame
│ Row │ id          │ name         │ size   │ price   │
│     │ String      │ String       │ String │ Float64 │
├─────┼─────────────┼──────────────┼────────┼─────────┤
│ 1   │ 2015-000348 │ thai_ckn     │ S      │ 12.75   │
│ 2   │ 2015-007731 │ green_garden │ S      │ 12.0    │
│ 3   │ 2015-014409 │ hawaiian     │ S      │ 10.5    │

Мы можем выбрать конкретный столбец и ничего с ним не делать:

julia> select(tiny, :price)
3×1 DataFrame
│ Row │ price   │
│     │ Float64 │
├─────┼─────────┤
│ 1   │ 12.75   │
│ 2   │ 12.0    │
│ 3   │ 10.5    │

julia> transform(tiny, :price)
3×4 DataFrame
│ Row │ id          │ name         │ size   │ price   │
│     │ String      │ String       │ String │ Float64 │
├─────┼─────────────┼──────────────┼────────┼─────────┤
│ 1   │ 2015-000348 │ thai_ckn     │ S      │ 12.75   │
│ 2   │ 2015-007731 │ green_garden │ S      │ 12.0    │
│ 3   │ 2015-014409 │ hawaiian     │ S      │ 10.5    │

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

julia> select(tiny, :price => :cost)
3×1 DataFrame
│ Row │ cost    │
│     │ Float64 │
├─────┼─────────┤
│ 1   │ 12.75   │
│ 2   │ 12.0    │
│ 3   │ 10.5    │

julia> transform(tiny, :price => :cost)
3×5 DataFrame
│ Row │ id          │ name         │ size   │ price   │ cost    │
│     │ String      │ String       │ String │ Float64 │ Float64 │
├─────┼─────────────┼──────────────┼────────┼─────────┼─────────┤
│ 1   │ 2015-000348 │ thai_ckn     │ S      │ 12.75   │ 12.75   │
│ 2   │ 2015-007731 │ green_garden │ S      │ 12.0    │ 12.0    │
│ 3   │ 2015-014409 │ hawaiian     │ S      │ 10.5    │ 10.5    │

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

julia> select(tiny, :price => mean)
3×1 DataFrame
│ Row │ price_mean │
│     │ Float64    │
├─────┼────────────┤
│ 1   │ 11.75      │
│ 2   │ 11.75      │
│ 3   │ 11.75      │

julia> transform(tiny, :price => mean)
3×5 DataFrame
│ Row │ id          │ name         │ size   │ price   │ price_mean │
│     │ String      │ String       │ String │ Float64 │ Float64    │
├─────┼─────────────┼──────────────┼────────┼─────────┼────────────┤
│ 1   │ 2015-000348 │ thai_ckn     │ S      │ 12.75   │ 11.75      │
│ 2   │ 2015-007731 │ green_garden │ S      │ 12.0    │ 11.75      │
│ 3   │ 2015-014409 │ hawaiian     │ S      │ 10.5    │ 11.75      │

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

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

julia> round(3.4)
3.0

julia> rounder = ByRow(round);

julia> rounder([1.2, 4.8, 8.3])
3-element Array{Float64,1}:
 1.0
 5.0
 8.0

Таким образом ByRow дает нам простой способ создания функций, которые могут работать с каждой строкой:

julia> select(tiny, :price => ByRow(round))
3×1 DataFrame
│ Row │ price_round │
│     │ Float64     │
├─────┼─────────────┤
│ 1   │ 13.0        │
│ 2   │ 12.0        │
│ 3   │ 10.0        │

julia> transform(tiny, :price => ByRow(round))
3×5 DataFrame
│ Row │ id          │ name         │ size   │ price   │ price_round │
│     │ String      │ String       │ String │ Float64 │ Float64     │
├─────┼─────────────┼──────────────┼────────┼─────────┼─────────────┤
│ 1   │ 2015-000348 │ thai_ckn     │ S      │ 12.75   │ 13.0        │
│ 2   │ 2015-007731 │ green_garden │ S      │ 12.0    │ 12.0        │
│ 3   │ 2015-014409 │ hawaiian     │ S      │ 10.5    │ 10.0        │

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

julia> transform(tiny, :price => ByRow(round) => :rounded)
3×5 DataFrame
│ Row │ id          │ name         │ size   │ price   │ rounded │
│     │ String      │ String       │ String │ Float64 │ Float64 │
├─────┼─────────────┼──────────────┼────────┼─────────┼─────────┤
│ 1   │ 2015-000348 │ thai_ckn     │ S      │ 12.75   │ 13.0    │
│ 2   │ 2015-007731 │ green_garden │ S      │ 12.0    │ 12.0    │
│ 3   │ 2015-014409 │ hawaiian     │ S      │ 10.5    │ 10.0    │

Но означает ли это, что использование функций без ByRow бессмысленно? Нет, на самом деле они очень полезны при использовании с combine, потому что он работает аналогично select, за исключением случаев, когда каждый выбранный столбец преобразуется функцией, которая принимает весь столбец и возвращает одно значение, например mean, вы получите одно ряд.

julia> combine(tiny, :price => mean)
1×1 DataFrame
│ Row │ price_mean │
│     │ Float64    │
├─────┼────────────┤
│ 1   │ 11.75      │

Но если вы этого не сделаете, combine будет вести себя так же, как select:

julia> combine(tiny, :name, :price => mean)
3×2 DataFrame
│ Row │ name         │ price_mean │
│     │ String       │ Float64    │
├─────┼──────────────┼────────────┤
│ 1   │ thai_ckn     │ 11.75      │
│ 2   │ green_garden │ 11.75      │
│ 3   │ hawaiian     │ 11.75      │

Разделение и рекомбинирование таблиц

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

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

  • length Подсчитайте количество элементов. Например. сколько там средних пицц.
  • sum Сложите значения в столбце.
  • mean Найдите среднее арифметическое (среднее).

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

julia> sizes = groupby(sample, :size)
GroupedDataFrame with 3 groups based on key: size
Group 1 (7 rows): size = "M"
│ Row │ id          │ name         │ size   │ price   │
│     │ String      │ String       │ String │ Float64 │
├─────┼─────────────┼──────────────┼────────┼─────────┤
│ 1   │ 2015-018649 │ pepperoni    │ M      │ 12.5    │
│ 2   │ 2015-011258 │ hawaiian     │ M      │ 13.25   │
│ 3   │ 2015-009299 │ veggie_veg   │ M      │ 16.0    │
│ 4   │ 2015-010260 │ peppr_salami │ M      │ 16.5    │
│ 5   │ 2015-017124 │ hawaiian     │ M      │ 13.25   │
│ 6   │ 2015-011800 │ thai_ckn     │ M      │ 16.75   │
│ 7   │ 2015-008107 │ ckn_alfredo  │ M      │ 16.75   │
Group 2 (5 rows): size = "L"
│ Row │ id          │ name        │ size   │ price   │
│     │ String      │ String      │ String │ Float64 │
├─────┼─────────────┼─────────────┼────────┼─────────┤
│ 1   │ 2015-000629 │ spin_pesto  │ L      │ 20.75   │
│ 2   │ 2015-011532 │ spinach_fet │ L      │ 20.25   │
│ 3   │ 2015-019947 │ pepperoni   │ L      │ 15.25   │
│ 4   │ 2015-002630 │ thai_ckn    │ L      │ 20.75   │
│ 5   │ 2015-018629 │ cali_ckn    │ L      │ 20.75   │
Group 3 (4 rows): size = "S"
│ Row │ id          │ name         │ size   │ price   │
│     │ String      │ String       │ String │ Float64 │
├─────┼─────────────┼──────────────┼────────┼─────────┤
│ 1   │ 2015-017814 │ green_garden │ S      │ 12.0    │
│ 2   │ 2015-012022 │ veggie_veg   │ S      │ 12.0    │
│ 3   │ 2015-010260 │ southw_ckn   │ S      │ 12.75   │
│ 4   │ 2015-010846 │ big_meat     │ S      │ 12.0    │

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

julia> combine(sizes, :size => length)
3×2 DataFrame
│ Row │ size   │ size_length │
│     │ String │ Int64       │
├─────┼────────┼─────────────┤
│ 1   │ M      │ 7           │
│ 2   │ L      │ 5           │
│ 3   │ S      │ 4           │

Мы могли бы переименовать столбец для более красивой таблицы:

julia> combine(sizes, :size => length => :amount)
3×2 DataFrame
│ Row │ size   │ amount │
│     │ String │ Int64  │
├─────┼────────┼────────┤
│ 1   │ M      │ 7      │
│ 2   │ L      │ 5      │
│ 3   │ S      │ 4      │

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

julia> pz_sizes = combine(groupby(pz, :size), :size => length => :amount)

julia> bar(pz_sizes.size, pz_sizes.amount, bins=5, legend=nothing)

Одна проблема с этим графиком, которую вы видите, заключается в том, что размеры не отсортированы в порядке S, M, L, XL, XXL, что сбивает с толку. Как мы можем это решить?

Объединение таблиц

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

Стандартные диаметры пиццы:

  • Маленький - 26 см
  • Средний - 31 см
  • Большой - 36 см.
  • X-Large - 41 см.
  • Джамбо - 46 см.

Мы будем использовать размер Jumbo для XXL. Сделаем таблицу, в которой размеры пиццы сопоставляются с диаметрами:

julia> diameters = DataFrame(size = ["S", "M", "L", "XL", "XXL"],
                             diameter = [26, 31, 36, 41, 46])
5×2 DataFrame
│ Row │ size   │ diameter │
│     │ String │ Int64    │
├─────┼────────┼──────────┤
│ 1   │ S      │ 26       │
│ 2   │ M      │ 31       │
│ 3   │ L      │ 36       │
│ 4   │ XL     │ 41       │
│ 5   │ XXL    │ 46       │

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

julia> pz = join(pz, diameters, on=:size);

julia> select(pz, :name, :size, :diameter, :price)
49574×4 DataFrame
│ Row   │ name         │ size   │ diameter │ price   │
│       │ String       │ String │ Int64    │ Float64 │
├───────┼──────────────┼────────┼──────────┼─────────┤
│ 1     │ spin_pesto   │ L      │ 36       │ 20.75   │
│ 2     │ pepperoni    │ M      │ 31       │ 12.5    │
│ 3     │ hawaiian     │ M      │ 31       │ 13.25   │
│ 4     │ spinach_fet  │ L      │ 36       │ 20.25   │
│ 5     │ veggie_veg   │ M      │ 31       │ 16.0    │

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

julia> pz_sizes = combine(groupby(pz, :size), :size => length => :amount)

julia> pz_sizes = join(pz_sizes, diameters, on=:size)
5×3 DataFrame
│ Row │ size   │ amount │ diameter │
│     │ String │ Int64  │ Int64    │
├─────┼────────┼────────┼──────────┤
│ 1   │ M      │ 15635  │ 31       │
│ 2   │ L      │ 18956  │ 36       │
│ 3   │ S      │ 14403  │ 26       │
│ 4   │ XL     │ 552    │ 41       │
│ 5   │ XXL    │ 28     │ 46       │

И рассортируем по диаметру:

julia> sort!(pz_sizes, :diameter)
5×3 DataFrame
│ Row │ size   │ amount │ diameter │
│     │ String │ Int64  │ Int64    │
├─────┼────────┼────────┼──────────┤
│ 1   │ S      │ 14403  │ 26       │
│ 2   │ M      │ 15635  │ 31       │
│ 3   │ L      │ 18956  │ 36       │
│ 4   │ XL     │ 552    │ 41       │
│ 5   │ XXL    │ 28     │ 46       │

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

bar(pz_sizes.size, pz_sizes.amount, bins=5, legend=nothing)

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

pie(pz_sizes.size, pz_sizes.amount, title="Most sold pizza sizes")

Какой размер пиццы самый популярный?

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

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

julia> area(r) = π*r^2
julia> pz_sizes.area = area.(pz_sizes.diameter ./ 2);

Чтобы было удобнее отображать, я просто округлю расчетные площади:

julia> pz_sizes.area = round.(Int, pz_sizes.area)
julia> pz_sizes
5×4 DataFrame
│ Row │ size   │ amount │ diameter │ area  │
│     │ String │ Int64  │ Int64    │ Int64 │
├─────┼────────┼────────┼──────────┼───────┤
│ 1   │ S      │ 14403  │ 26       │ 531   │
│ 2   │ M      │ 15635  │ 31       │ 755   │
│ 3   │ L      │ 18956  │ 36       │ 1018  │
│ 4   │ XL     │ 552    │ 41       │ 1320  │
│ 5   │ XXL    │ 28     │ 46       │ 1662  │

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

julia> pzs = pz_sizes
julia> pzs.total_area = pzs.area .* pzs.amount
julia> pzs
5×5 DataFrame
│ Row │ size   │ amount │ diameter │ area  │ total_area │
│     │ String │ Int64  │ Int64    │ Int64 │ Int64      │
├─────┼────────┼────────┼──────────┼───────┼────────────┤
│ 1   │ S      │ 14403  │ 26       │ 531   │ 7647993    │
│ 2   │ M      │ 15635  │ 31       │ 755   │ 11804425   │
│ 3   │ L      │ 18956  │ 36       │ 1018  │ 19297208   │
│ 4   │ XL     │ 552    │ 41       │ 1320  │ 728640     │
│ 5   │ XXL    │ 28     │ 46       │ 1662  │ 46536      │

И давайте сделаем его более интересным, построив график, сколько пиццы было куплено, и сравним его с площадью потребленной пиццы каждого типа:

p1 = pie(pzs.size, pzs.amount, title="Pizzas by Numbers");
p2 = pie(pzs.size, pzs.total_area, title="Pizzas by Area");
p  = plot(p1, p2)

Какая пицца приносит больше всего дохода?

Начнем с создания подгруппы для каждой пиццы.

groupby(pz, :name)

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

combine(groupby(pz, :name), :price => sum => :revenue)

Но мы хотим, чтобы это было отсортировано, чтобы мы могли найти 10 самых продаваемых пицц.

julia> pizzas = groupby(pz, :name);
julia> pizzas = combine(pizzas, :price => sum => :revenue);

julia> top10 = first(sort(pizzas, :revenue, rev=true), 10)
10×2 DataFrame
│ Row │ name        │ revenue │
│     │ String      │ Float64 │
├─────┼─────────────┼─────────┤
│ 1   │ thai_ckn    │ 43434.2 │
│ 2   │ bbq_ckn     │ 42768.0 │
│ 3   │ cali_ckn    │ 41409.5 │
│ 4   │ classic_dlx │ 38180.5 │
│ 5   │ spicy_ital  │ 34831.2 │
│ 6   │ southw_ckn  │ 34705.8 │
│ 7   │ ital_supr   │ 33476.8 │
│ 8   │ hawaiian    │ 32273.2 │
│ 9   │ four_cheese │ 32265.7 │
│ 10  │ sicilian    │ 30940.5 │

Мы можем визуализировать это с помощью гистограммы:

bar(top10.name, top10.revenue, label="pizza revenue")

Сколько обычно тратится на каждый заказ пиццы?

Чтобы узнать, сколько тратится на каждый заказ пиццы, мы создаем подгруппу фрейма данных для каждого уникального идентификатора заказа:

julia> groupby(pz, :id)
GroupedDataFrame with 21350 groups based on key: id
First Group (1 row): id = "2015-000001"
│ Row │ id          │ name     │ size   │ price   │ diameter │ area    │
│     │ String      │ String   │ String │ Float64 │ Float64  │ Float64 │
├─────┼─────────────┼──────────┼────────┼─────────┼──────────┼─────────┤
│ 1   │ 2015-000001 │ hawaiian │ M      │ 13.25   │ 0.31     │ 0.0755  │
⋮
Last Group (1 row): id = "2015-021350"
│ Row │ id          │ name    │ size   │ price   │ diameter │ area    │
│     │ String      │ String  │ String │ Float64 │ Float64  │ Float64 │
├─────┼─────────────┼─────────┼────────┼─────────┼──────────┼─────────┤
│ 1   │ 2015-021350 │ bbq_ckn │ S      │ 12.75   │ 0.26     │ 0.0531  │

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

julia> orders = combine(groupby(pz, :id), :price => sum => :sum)
21350×2 DataFrame
│ Row   │ id          │ sum     │
│       │ String      │ Float64 │
├───────┼─────────────┼─────────┤
│ 1     │ 2015-000001 │ 13.25   │
│ 2     │ 2015-000002 │ 92.0    │
│ 3     │ 2015-000003 │ 37.25   │
│ 4     │ 2015-000004 │ 16.5    │

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

julia> minimum(orders.sum)
9.75

julia> maximum(orders.sum)
444.2

julia> mean(orders.sum)
38.30726229508197

julia> median(orders.sum)
32.5

Существует сокращение для получения всех этих данных одним махом:

julia> describe(orders, :min, :max, :mean, :median)
2×5 DataFrame
│ Row │ variable │ min         │ max         │ mean    │ median │
│     │ Symbol   │ Any         │ Any         │ Union…  │ Union… │
├─────┼──────────┼─────────────┼─────────────┼─────────┼────────┤
│ 1   │ id       │ 2015-000001 │ 2015-021350 │         │        │
│ 2   │ sum      │ 9.75        │ 444.2       │ 38.3073 │ 32.5   │

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

julia> histogram(
           orders.sum,
           xaxis="order sum",
           yaxis="frequency",
           xticks=0:50:400,
           legend=nothing)

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

Заключительные слова

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

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