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

Графики

Gnuplot и с чем его едят

02.09.2020 12:08:46 | Автор: admin

Наверняка многие из вас листая западные научные издания видели красивые и простые графики. Возможно некоторые из вас задумывались в чём же эти учёные мужи визуализируют свою данные. И вот есть шикарный и очень простой инструмент для построения графиков, который есть практически везде: Windows, linux, android, и прочих, уверен даже есть под ДОС. Он надёжен, прост и позволяет представить в виде красивых графиков любые текстовые-табличные данные.

Почему именно gnuplot?


Многие из вас, кто читают мои статьи видели красивые графики, которые привожу в них: Одновременный speedtest на нескольких LTE-модемах, Гармонические колебания, Создаём аппаратный генератор случайных чисел.


График из поста про генератор случайных чисел


Картинка из поста про speetest модемов

Графики простые и классные. А главное для их построения вам нужен только текстовый файл с исходными данными, gnuplot на вашей любимой ОС (хоть OpenWRT) и любимый тестовый редактор vim.

На первый взгляд может показаться, что gnuplot сложнее в использовании для построения графиков чем MS Exel. Но это только так кажется, порог вхождения чуть выше (это вам не мышкой наклацать, тут надо документацию читать), но на практике выходит намного проще и удобнее. Один раз написал скрипт и используешь его всю жизнь. Мне реально намного сложнее построить график в Exel, где всё не логично, нежели в gnuplot. А главное преимущество gnuplot, то что его можно встраивать в свои программы и на ходу визуализировать данные. Так же gnuplot мне без особых проблем строил график с 30-ти гигабайтового файла статистических данных, тогда как Exel просто падал и не мог его открыть.

К плюсам gnuplot можно отнести что он легко интегрируется в языки программирования. Есть готовые библиотеки для многих языков, лично я сталкивался с php и python. Таким образом можно генерировать графики прямо из своей программы.

Для примера скажу, что моя хорошая подруга, когда писала диссертацию освоила gnuplot (с моей подачи). Она ни разу не технарь, но разобралась за один вечер. После чего строила графики только там, и уровень её работы сразу выгодно отличался от других, кто использует Exel. Лично для меня показатель качества научной работы, когда графики строятся в специализированных программах.

Таким образом, это просто, доступно и красиво. Едем дальше.

Gnuplot применение


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

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

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

gnuplot> plot sin(x)



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

set xlabel "X"

Задает подпись для оси абсцисс.

set ylabel "Y"

Задает подпись для оси ординат.

Добавим сетку, чтобы было видно где построен график.

set grid

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

set yrange [-1.1:1.1] 

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

set xrange[-pi:pi]

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

set title "Gnuplot for habr" font "Helvetica Bold, 20"

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

Ну и наконец, давайте кроме синуса на графике ещё нарисуем и косинус, да ещё и зададим тип линии и её цвет. А так же добавим легенды, что же мы чертим.

plot sin(x) title "sinux" lc rgb "red", cos(x)  title "cosinus" lc rgb "green"

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

resetset xlabel "X" set ylabel "Y"set gridset yrange [-1.1:1.1]set xrange[-pi:pi]set title "Gnuplot for habr" font "Helvetica Bold, 20"plot sin(x) title "sinux"  lc rgb "red", cos(x)  title "cosinus" lc rgb "green"

В результате получаем вот такую красоту.


График уже не стыдно опубликовать в научный журнал.

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

vim testsin.gpi

#! /usr/bin/gnuplot -persistset xlabel "X" set ylabel "Y"set gridset yrange [-1.1:1.1]set xrange[-pi:pi]set title "Gnuplot for habr" font "Helvetica Bold, 20"plot sin(x) title "sinux"  lc rgb "red", cos(x)  title "cosinus" lc rgb "green"

Делаем его исполняемым и запускаем.

chmod +x testsin.gpi./testsin.gpi

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

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

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

set terminal png size 800, 600set output "result.png"

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



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

set terminal postscript eps enhanced color solidset output "result.ps"

Реальные данные


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

Operator; #Test; Date; Time; Coordinates; Download Mb/s; Upload Mb/s; ping; Testserver
Rostelecom;0;21.05.2020;09:56:00;NA, NA;3.7877656948451692;5.231226008184113;132.227;MaximaTelecom (Moscow) [0.12 km]: 132.227 ms
Rostelecom;1;21.05.2020;10:01:02;NA, NA;5.274994541394363;5.1088572634075815;127.52;MaximaTelecom (Moscow) [0.12 km]: 127.52 ms
Rostelecom;2;21.05.2020;10:04:35;NA, NA;3.61044819424076;4.624132180211938;135.456;MaximaTelecom (Moscow) [0.12 km]: 135.456 ms


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

#! /usr/bin/gnuplot -persistset terminal postscript eps enhanced color solidset output "Rostelecom.ps"#set terminal png size 1024, 768#set output "Rostelecom.png" set datafile separator ';'set grid xtics yticsset xdata timeset timefmt '%d.%m.%Y;%H:%M:%S'set ylabel "Speed Mb/s"set xlabel 'Time'set title "Rostelecom Speed"plot "Rostelecom.csv" using 3:6 with lines title "Download", '' using 3:7 with lines title "Upload" set title "Rostelecom 2 Ping"set ylabel "Ping ms"plot "Rostelecom.csv" using 3:8 with lines title "Ping"

В начале файла мы задаём выходной файл postscript (либо png, если будет нужен).
set datafile separator ';' мы задаём символ разделитель. По умолчанию столбцы разделяет пробел, но csv-файл предлагает множество вариантов разделителей, и нужно уметь использовать все.
set grid xtics ytics устанавливаем сетку (можно сетку установить только по одной оси).
set xdata time это важный момент, мы говорим о том, что по оси X формат данных будет время.
set timefmt '%d.%m.%Y;%H:%M:%S' задаём формат данных времени. Обратите внимание, что не смотря на то что разделитель у нас идёт ";", она входит в строку дата-время. Хотя столбец будет другой.

Задаём подписи осей и графика. После чего строим график.

plot Rostelecom.csv using 3:6 with lines title Download, '' using 3:7 with lines title Upload на одном графике мы строим как скорость скачивания, так и скорость отдачи. Using 3:6 это номер столбца в нашем файле исходных данных, что от чего строим (X:Y).
Далее так же точно строим график для пинга. Результирующий график будет выглядеть следующим образом.



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

А 3D???


Вы хотите 3D? Их есть у меня!
Долго думал, какой же пример трёхмерного графика привести, и не придумал ничего лучше, чем визуализировать картинку. Ведь по сути картинка это трёхмерный график, где яркость пикселя это координата по z. Поэтому давайте немного похулиганим.
Возьмём самое знаменитое фото Эйнштейна.



И сделаем из него график. Для этого конвертнём его в формат pgm ASCII и выкинем все пробелы, заменив переводом строки, такой простой командой.

convert Einstein.jpg  -compress none pgm:- | tr -s '\012\015' ' '  | tr -s ' ' '\012'> outfile.pgm 


Кто не понял, что здесь происходит, поясняю: мы конвертируем с помощью imagemagic картинку в формат pgm, а потом с помощью tr заменяем перевод каретки с пеносом на новую строку на пробел, а потом все пробелы на перенос каретки и сохраняем это всё в outfile.pgm. Кому это сложно, могут открыть файл в gimp и экспортировать его как pgm-ASCII.

После чего открываем получившийся файл нашим любимым редактором vim и удаляем у него заголовок. В моём случае это первые три строки. Из заголовка не забываем узнать разрешение файла, в данном случае было 325х408 пикселей. Всё, мы получаем текстовый файл координат Z! Теперь наша задача добавить координаты X и Y, для этого прогоним всё это питоновским скриптом в координаты.

f = open('outfile.pgm')for x in range(408):for y in range(325):line = f.readline()print ('%d %d %s' % (x, y, line)),f.close()

Сохраняем его как convert.py и запускаем:

python convert.py > res.txt

Всё, у нас теперь res.txt содержит координаты Эйнштейна Хм, ну точнее сказать координаты его изображения. Ну в общем, вы поняли :).


406 317 60
406 318 54
406 319 30
406 320 41
406 321 84
406 322 101
406 323 112
406 324 119
407 0 128
407 1 53
407 2 89
407 3 95
407 4 87
...

Пример файла.

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

#! /usr/bin/gnuplot -persist#set terminal png size 1024, 768#set output "result.png"#set grid xtics ytics#set terminal postscript eps enhanced color solid#set output "result.ps"set title "Albert Einstein"set palette grayset hidden3dset pm3d at bsset dgrid3d 100,100 qnorm 2set xlabel "X" font "Helvetica Bold,18"set ylabel "Y" rotate by 90 font "Helvetica Bold,18"set zlabel "Z" font "Helvetica Bold,18"set xrange [0:408]set yrange [0:325]set zrange [-256:256]unset key splot "./res.txt" with l 

Прежде чем мы пойдём разбирать скрипт, если вы будете это повторять, то настоятельно рекомендую выполнить строки скрипта в командном режиме, чтобы можно было мышкой вращать график (разумеется не указывая set terminal). Это очень круто!

Вначале мы устанавливаем тип выходных данных, а так же границы данных. Границы выставлены по размерам картинки, и плюс от низа я отступил по оси Z на 256 символов, чтобы была видна проекция картинки. Дальше мы озаглавливаем график, подписываем оси. Командой unset key я отключаю легенды (она не нужна на графике). А вот далее идёт настоящая магия!

set palette gray мы задаём палитру. Если оставить по умолчанию, то график будет цветным, как тепловизоре. Чем выше, тем более жёлтое пятно, чем ниже тем темнее красный цвет.

set hidden3d как бы натягивает изогнутую поверхность (удаляет линии), таким образом формируется красивая изгнутая повехность.

set pm3d at bs включаем стиль рисования трёхмерный данных, который рисует данные с координатой сеткой и цветом. Более детально читайте в документации, более детальное описание выходит за рамки статьи.

set dgrid3d 100,100 qnorm 2 устанавливаем размер ячеек сетки 100х100, и сглаживание между ячейками. Значение 100х100 и так очень большое, и программа сильно тормозит. qnorm 2 это сглаживание (интерполяция данных между ячейками).

splot "./res.txt" with l рисуем получившийся график. With l, потому что точки видны на графике (можно задать маленькие точки).

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


Изображение в командном режиме, после того как повращали.

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

Применение gnuplot в своих программах


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

Пример кода на Си использующего gnuplot
#include <stdio.h>#include <stdlib.h>#include <unistd.h>float s=10.;float r=28.;float b=8.0/3.0;/* Definimos las funciones */float f(float x,float y,float z){    return s*(y-x);}float g(float x,float y,float z){    return x*(r-z)-y;}float h(float x,float y,float z){    return x*y-b*z;}FILE *output;FILE *gp;int main(){    gp = popen("gnuplot -","w");    output = fopen("lorenzgplot.dat","w");    float t=0.;     float dt=0.01;    float tf=30;    float x=3.;    float y=2.;    float z=0.;    float k1x,k1y,k1z, k2x,k2y,k2z,k3x,k3y,k3z,k4x,k4y,k4z;    fprintf(output,"%f %f %f \n",x,y,z);    fprintf(gp, "splot './lorenzgplot.dat' with lines \n");/* Ahora Runge Kutta de orden 4 */      while(t<tf){        /* RK4 paso 1 */        k1x = f(x,y,z)*dt;        k1y = g(x,y,z)*dt;        k1z = h(x,y,z)*dt;        /* RK4 paso 2 */        k2x = f(x+0.5*k1x,y+0.5*k1y,z+0.5*k1z)*dt;        k2y = g(x+0.5*k1x,y+0.5*k1y,z+0.5*k1z)*dt;        k2z = h(x+0.5*k1x,y+0.5*k1y,z+0.5*k1z)*dt;        /* RK4 paso 3 */        k3x = f(x+0.5*k2x,y+0.5*k2y,z+0.5*k2z)*dt;        k3y = g(x+0.5*k2x,y+0.5*k2y,z+0.5*k2z)*dt;        k3z = h(x+0.5*k2x,y+0.5*k2y,z+0.5*k2z)*dt;        /* RK4 paso 4 */        k4x = f(x+k3x,y+k3y,z+k3z)*dt;        k4y = g(x+k3x,y+k3y,z+k3z)*dt;        k4z = h(x+k3x,y+k3y,z+k3z)*dt;        /* Actualizamos las variables y el tiempo */        x += (k1x/6.0 + k2x/3.0 + k3x/3.0 + k4x/6.0);        y += (k1y/6.0 + k2y/3.0 + k3y/3.0 + k4y/6.0);        z += (k1z/6.0 + k2z/3.0 + k3z/3.0 + k4z/6.0);        /* finalmente escribimos sobre el archivo */        fprintf(output,"%f %f %f \n",x,y,z);        fflush(output);         usleep(10000);        fprintf(gp, "replot \n");        fflush(gp);        t += dt;    }    fclose(gp);    fclose(output);    return 0;}


Код работает очень просто, мы открываем pipe:

gp = popen("gnuplot -","w");

Это аналогично вертикальной черте в bash, когда мы за одной командой пишем другую. Только в условиях программы. Пишем данные в файл lorenzgplot.dat. Один раз вызываем в gnuplot команду splot:

fprintf(gp, "splot './lorenzgplot.dat' with lines \n");

И далее при добавлении новой точки, мы перестраиваем график.

fprintf(gp, "replot \n");

В результате получаем очень красивое медленное построение Аттрактора Лоренца. Ниже видео, снятое почти десять лет назад, на старенький фотоаппарат, поэтому не ругайтесь сильно. Важно в видео другое, что всё это прекрасно работает на таком старом железе, как Nokia N800. Смотреть это желательно без звука.



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

Заключение


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



В этой статье не смог рассказать и тысячной доли возможностей данного графопостроителя, разве что немного ознакомил читателя с данной программой. Далее вам следует самостоятельно искать примеры, читать документацию на официальном сайте gnuplot.sourceforge.net либо www.gnuplot.info. Обязательно загляните в примеры, там очень много интересного и полезного.

Для старта так же могу порекомендовать Краткое введение в gnuplot (рус). Искренне удивлён, что такая замечательная программа не изучается во всех технических ВУЗах наравне с Latex. У нас зачем-то учили MS Exel и Word.

Изучить gnuplot не сложно, я потратил буквально несколько дней в попытке разобраться с нуля. Но с данной статьёй, верю, что у вас всё будет быстрее. Теперь я забыл о всяких Exel/Calc в качестве графопостроителей, использую только гнуплот. Тем более, что я даже не знаю и десятой доли всех возможностей построения графиков.

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

Подробнее..

Сравнение ассортимента блюд трёх ресторанов Санкт-Петербурга

07.04.2021 20:14:16 | Автор: admin

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

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

Сайты для сбора данных были подобраны по принципу нет блокировщика парсеров и из анализа этих данных может выйти что-то интересное. Поэтому выбор пал на ассортимент блюд на доставку трёх ресторанов Санкт-Петербурга - Токио City, Евразия и 2 Берега. У них приблизительно одна направленность кухни и похожий ассортимент, поэтому явно найдется, что сравнить.

Поделюсь самим парсером для одного из ресторанов.

import requestsfrom bs4 import BeautifulSoupimport pandas as pdimport datetimeprint("Начало парсинга Токио Сити: " + str(datetime.datetime.now()))#все страницы с информацией о менюurllist = ['https://www.tokyo-city.ru/spisok-product/goryachie-blyuda1.html',           'https://www.tokyo-city.ru/spisok-product/sushi.html',           'https://www.tokyo-city.ru/spisok-product/rolly.html',           'https://www.tokyo-city.ru/spisok-product/nabory.html',           'https://www.tokyo-city.ru/spisok-product/new_lunches.html',           'https://www.tokyo-city.ru/spisok-product/pitctca.html',           'https://www.tokyo-city.ru/spisok-product/salaty.html',           'https://www.tokyo-city.ru/spisok-product/-supy-.html',           'https://www.tokyo-city.ru/spisok-product/goryachie-zakuski1.html',           'https://www.tokyo-city.ru/spisok-product/wok.html',           'https://www.tokyo-city.ru/spisok-product/pasta.html',           'https://www.tokyo-city.ru/spisok-product/gamburgery-i-shaverma.html',           'https://www.tokyo-city.ru/spisok-product/Tokio-FIT.html',           'https://www.tokyo-city.ru/spisok-product/deserty.html',           'https://www.tokyo-city.ru/spisok-product/childrensmenu.html',           'https://www.tokyo-city.ru/spisok-product/napitki1.html',           'https://www.tokyo-city.ru/new/',           'https://www.tokyo-city.ru/spisok-product/postnoe-menyu.html',           'https://www.tokyo-city.ru/hit/',           'https://www.tokyo-city.ru/vegetarian/',           'https://www.tokyo-city.ru/hot/',           'https://www.tokyo-city.ru/offers/',           'https://www.tokyo-city.ru/spisok-product/sauces.html',           'https://www.tokyo-city.ru/spisok-product/Pirogi-torty.html']#создаем пустые списки для записи всех данныхnames_all = []descriptions_all = []prices_all = []categories_all = []url_all = []weight_all = []nutr_all = []#собираем данныеfor url in urllist:    response = requests.get(url).text    soup = BeautifulSoup(response, features="html.parser")    items = soup.find_all('a', class_='item__name')    itemsURL = []    n = 0    for n, i in enumerate(items, start=n):        itemnotfullURL = i.get('href')        itemURL = 'https://www.tokyo-city.ru' + itemnotfullURL        itemsURL.extend({itemURL})        m = 0        namesList = []        descriptionsList = []        pricesList = []        weightList = []        nutrList = []        itemResponse = requests.get(itemURL).text        itemsSoup = BeautifulSoup(itemResponse, features="html.parser")        itemsInfo = itemsSoup.find_all('div', class_='item__full-info')        for m, u in enumerate(itemsInfo, start=m):            if (u.find('h1', class_='item__name') == None):                itemName = 'No data'            else:                itemName = u.find('h1', class_='item__name').text.strip()            if (u.find('p', class_='item__desc') == None):                itemDescription = 'No data'            else:                itemDescription = u.find('p', class_='item__desc').text.strip()            if (u.find('span', class_='item__price-value') == None):                itemPrice = '0'            else:                itemPrice = u.find('span', class_='item__price-value').text            if (u.find('div', class_='nutr-value') == None):                itemNutr = 'No data'            else:                itemNutr = u.find('div', class_='nutr-value').text.strip()            if (u.find('div', class_='item__weight') == None):                itemWeight = '0'            else:                itemWeight = u.find('div', class_='item__weight').text.strip()            namesList.extend({itemName})            descriptionsList.extend({itemDescription})            pricesList.extend({itemPrice})            weightList.extend({itemWeight})            nutrList.extend({itemNutr})        df = pd.DataFrame((            {'Name': namesList,             'Description': descriptionsList,             'Price': pricesList,             'Weight': weightList,             'NutrInfo': nutrList             }))        names_all.extend(df['Name'])        descriptions_all.extend(df['Description'])        prices_all.extend(df['Price'])        weight_all.extend(df['Weight'])        nutr_all.extend(df['NutrInfo'])        df['Category'] = soup.find('div', class_='title__container').text.strip()        categories_all.extend(df['Category'])result = pd.DataFrame((    {'Name': names_all,     'Description': descriptions_all,     'Price': prices_all,     'Category': categories_all,     'NutrInfo': nutr_all,     'Weight': weight_all,     }))print("Парсинг Токио Сити окончен: " + str(datetime.datetime.now()))

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

А теперь к самому интересному - анализу полученной информации.

Начальные данные:

Наименование каждого блюда, его состав, цена, вес, калорийность, БЖУ и категория, к которой это блюдо относится.

Кусочек готовой к анализу таблицы с ассортиментом:

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

  • Токио City

Меню ресторана Токио City представлено 19 уникальными категориями и 5 дублирующимися, куда попадают блюда из других категорий, соответствующие определённому признаку (например, акционные блюда или подходящие вегетарианцам). Общее количество уникальных блюд - 351.

  • Евразия

Ассортимент блюд в Евразии несколько меньше - 13 категорий, 301 уникальное блюдо. Несмотря на то, что само название Токио City намекает на большое разнообразие японских блюд, этот ресторан предлагает почти на 40% меньше суши и роллов, чем, казалось бы, более универсальная кухня Евразии.

  • 2 Берега

Этот ресторан имеет самый маленький ассортимент из анализируемых - 241 уникальное блюдо в 15 категориях.

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

Вопрос 1: какую долю занимает фастфуд от всего меню уникальных блюд каждого ресторана?

К фастфуду относятся бургеры, пицца и разного рода стритфуд вроде шавермы.

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

Итог:

Выходит, 2 Берега - 1 по разнообразию пиццы в ассортименте. Это подтверждается, даже если просто сравнить количество блюд в категории Пицца во всех ресторанах (Токио City - 20, Евразия - 17 и 2 Берега - 51).

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

Вопрос 2: в каком из трёх ресторанов самые выгодные и сытные порции?

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

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

У 2 Берега нет такой категории, как Горячие блюда. Есть ВОКи и паста, но традиционных горячих блюд вида гарнир + мясо нет. Поэтому в категории Горячие блюда сравниваются только Токио City и Евразия.

По всем категориям Токио City является безусловным лидером по соотношению цены и веса блюда. 2 Берега занимает почётное 2 место. Евразия оказывается в хвосте рейтинга. Даже если вычесть из средней цены за 100 грамм блюда в Евразии 30% (это максимальная скидка, которую предоставляет ресторан по картам лояльности), ресторан все равно ни в одной категории не сможет обогнать Токио City по выгоде.

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

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

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

А Токио City предлагает отличные порции горячих блюд.

Вопрос 3: какова средняя калорийность блюда в каждом из ресторанов?

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

Калорийность половины блюд в Токио City не превышает 205 калорий в 100 граммах, поэтому присуждаем ресторану одного толстого кота из трёх. Это достаточно позитивный показатель для тех, кто следит за своим весом. А вот у блюд ресторана 2 Берега этот показатель на 35% выше, поэтому он получает максимальное количество толстых котов. Впрочем, в этом нет ничего удивительного, если вспомнить, какую долю от всего меню этого ресторана занимает пицца.

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

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

Несмотря на самую высокую калорийность на 100 грамм и большое количество фастфуда 2 Берега предлагает достаточно сбалансированное меню, тогда как у того же Токио City можно заметить явный перекос в сторону углеводов.

БЖУ Евразии какое-то слишком равномерное, практически без выбросов, поэтому вызывает подозрения.

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


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

Подробнее..

Перфоманс фронтенда как современное искусство графики, код, кулстори

17.09.2020 12:16:31 | Автор: admin

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


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


  • Перфоманс приложения
  • Инфраструктура: сборка, тесты, пайплайны, раскатка на продакшене, инструменты для разработчика (например бабель-плагины, кастомные eslint правила)
  • Дизайн-система (UIKit)
  • Переезд на новые технологии

Если покопаться, можно найти много интересного.


Поэтому, давайте поговорим о перфомансе. Команда фронтенд архитектуры ответственна как за клиентскую часть, так и серверную (SSR).


Я предлагаю посмотреть на метрики и разобраться, как мы реагируем на различные триггеры. Статья будет разбита на 2 составляющие. Серверную и клиентскую. Графики, код и кулстори прилагаются.



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


Клиент


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


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


FMP (first meaningful paint)



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


Тот же график, но с отображением только одной страницы:


Для нас FMP наиболее важный график и вот почему:


  • Сайт hh.ru с точки зрения соискателя текстовый. Открыть поиск, кликнуть на вакансию, прочитать, решить ок или нет, откликнуться.
  • С точки зрения работодателя сайт частично текстовый. Открыть поиск или разобрать отклики это работа с резюме кандидата.

Вопрос правильного измерения FMP один из наиболее важных для нас. Что мы понимаем под FMP? Это количество и время загрузки критических для рендера ресурсов.


Кажется, что FMP может считаться как-то так:


requestAnimationFrame(function() {   // Перед первым рендером взяли время когда renderTree было сформировано  var renderTreeFormed = performance.now();  requestAnimationFrame(function() {    // Здесь данные отрендерены пользователю    var fmp = performance.now();    // Сохраняем для дальнейшей отправки на сервер    window.globalVars.performance.fmp.push({      renderTreeFormed: renderTreeFormed,      fmp: fmp    })  });});

Здесь есть несколько интересных моментов:


  1. Если вставить этот код после меню и перед закрытием body, то получаемые данные могут и будут отличаться (при условии, что у вас вся страница не умещается в 1 экран). Дело в том, что браузеры будут пытаться оптимизировать рендер.
  2. Это решение не работает ()/


Дело в том, что браузер не будет вызывать raf и будет сильно замедлять вызовы setTimeout\interval когда вкладка не является активной. Поэтому мы получим некорректные данные.


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


window.globalVars = window.globalVars || {};window.globalVars.performance = window.globalVars.performance || {};// Помечаем, была ли страница активна в момент загрузкиwindow.globalVars.performance.pageWasActive = document.visibilityState === "visible";document.addEventListener("visibilitychange", function(e) {    // Если что-то изменилось  реагируем    if (document.visibilityState !== "visible") {        window.globalVars.performance.pageWasActive = false;    }});

Используем полученные знания в FMP:


requestAnimationFrame(function() {   // Перед первым рендером взяли время когда renderTree было сформировано  var renderTreeFormed = performance.now();  requestAnimationFrame(function() {    // Здесь данные отрендерены пользователю    var fmp = performance.now();    // Сохраняем для дальнейшей отправки на сервер,     // только в случае, если страница была все время активной    if (window.globalVars.performance.pageWasActive) {        window.globalVars.performance.fmp.push({          renderTreeFormed: renderTreeFormed,          fmp: fmp        });        }  });});

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


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


Поэтому мы решили задачу изящнее. В нужные места мы стали вставлять вот такие метки (и fmp_menu для меню):


<script>window.performance.mark('fmp_body')</script>

На их основе мы и строим графики:


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


Несколько интересностей:


  1. На FMP у нас настроен триггер. Чтобы реагировать на массовые проблемы, он настроен на 3 минуты бесперебойных проблем. Поэтому "одиночные" выбросы просто игнорирует.
  2. Критический FMP: 10 секунд. В эти моменты мы смотрим на проблемные урлы и на выдаваемые нами данные.
  3. У нас было несколько интересных историй, когда FMP начинал зашкаливать. Часто эта метрика может коррелировать с массовыми проблемами с сетью у пользователей, а также с проблемами на наших бекендах. Метрика получилась очень чувствительной
  4. Если брать статистику, то мобильные телефоны получаются производительней настольных машин! Вот пример, на котором я взял время с большой нагрузкой в рабочий день и построил графики по одному url-у. Слева мобильники, справа десктопы, 95 перцентиль:

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


Вторая метрика TTI


В целом, для работы с TTI более чем достаточно взять готовый код у гугла


У нас для вычисления TTI свой велосипед. Он нам нужен, потому что мы завязываемся внутри на Page Visibility API, о котором я писал выше. К сожалению, TTI полностью завязан на longtask и у нас нет опции "посчитать его как-нибудь по-другому", поэтому мы вырезаем пласт метрик, когда пользователь уходит со страницы.


Посмотреть код TTI
function timeToInteractive() {    // Ожидаемое время TTI    const LONG_TASK_TIME = 2000;    // Максимально ожидаемое время TTI, если не произошло лонгтасок    const MAX_LONG_TASK_TIME = 30000;    const metrics = {        report: 'TTI_WITH_VISIBILITY_API',        mobile: Supports.mobile(),    };    if ('PerformanceObserver' in window && 'PerformanceLongTaskTiming' in window) {        let timeoutIdCheckTTI;        const longTask = [];        const observer = new window.PerformanceObserver((list) => {            for (const entry of list.getEntries()) {                longTask.push(Math.round(entry.startTime + entry.duration));            }        });        observer.observe({ entryTypes: ['longtask'] });        const checkTTI = () => {            if (longTask.length === 0 && performance.now() > MAX_LONG_TASK_TIME) {                clearTimeout(timeoutIdCheckTTI);            }            const eventTime = longTask[longTask.length - 1];            if (eventTime && performance.now() - eventTime >= LONG_TASK_TIME) {                if (window.globalVars?.performance?.pageWasActive) {                    StatsSender.sendMetrics({ ...metrics, tti: eventTime });                }            } else {                timeoutIdCheckTTI = setTimeout(checkTTI, LONG_TASK_TIME);            }        };        checkTTI();    }}export default timeToInteractive;

Выглядит TTI вот так (95", TOP тяжелых урлов):


Может появиться вопрос: почему TTI такой большой? Дело в:


  1. Рекламе, которая грузится по requestIdleCallback
  2. Аналитике
  3. 3d party скриптах

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


Время инита приложения без гидрейта (рендера)


95" TOP тяжеленьких:


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


Но самым показательным для нас на клиенте в плане JS рантайма является


Гидрейт


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



Если совместить график инита и гидрейта, можем сделать несколько выводов:


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

Чем помогают эти графики?


  1. Как только видим всплеск в FMP, идем в остальные графики клиента и смотрим, увеличилось ли время гидрейта или инита. Они дают понять, была ли проблема с клиентом или нужно смотреть на сеть, SSR и бэкенды.
  2. Триггеры позволяют понимать, когда были проблемы во время релизов. На моей памяти это было лишь однажды. Статика крайне редко ломает релизы настолько сильно.

LongTasks


PerformanceObserver позволяет трекать тяжелые таски у пользователей:


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


Весна, поют птички, приходят разработчики в офис (да, это не 2020!). Прилетает сообщение от техподдержки: сайт не работает! Разработчики быстро просыпаются и пытаются воспроизвести проблему. Количество обращений растет.


Выясняется довольно занятная штука: поставщик рекламы добавил новый баннер с кормом для собак, где блокирующий js постоянно вызывал reflow, который надежно убивал event loop ровно на 30 секунд.


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


Из этой истории мы вынесли 2 урока:


  1. Решить, что сделать с рекламой
  2. Трекать Longtasks. Сказано сделано.

Что еще?


Это не все графики, которые мы строим для клиента. У нас есть время инициализации не реакт-компонентов, на старых страницах, rate ошибок в сентри + триггер, чтобы быстро реагировать при проблемах, FID. Но они практически не использовались нами в аналитике.


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


Сервер и кулстори


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


Что самое важное для серверов? Нагрузка, время ответа и понимание на что это время потратили.


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


График запросов и ошибок


График времени ответа http клиента


Каждая линия отдельный урл, здесь TOP наиболее проблемных урлов. Все триггеры настроены на 95 перцентиль. На графике мы видим, что был некий всплеск в 12:10 и затем одному урлу стало не очень хорошо в 12:40. На этих графиках "криминала нет", но как только потолок в 400мс пробивается, в это время зажигается триггер и один человек из команды бодро марширует во внутренние сервисы с логами, кибану и разбирает "что это было". Также локализовать проблему помогают дополнительные графики:


Время рендера и парcинг



Здесь уже видно, что первая проблема коррелирует с увеличением parse time.


Копаем дальше и видим график утилизации CPU. Здесь дискотека:



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


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


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



Одному из урлов надоело жить.


В то же время render и parse time чувствуют себя отлично:



На графике видно, что количество ошибок увеличивается:



Грепаем логи, из них извлекаем ошибку


TypeError: Cannot read property 'map' of undefined    at Social (at path/to/module)

Кажется, сервер стал асоциальным.


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



И еще один пример, когда parse time имеет значение:



Видим постепенно растущий график времени ответа сервиса. Но время рендера совсем не растет. А время парсинга наоборот крайне подозрительно коррелирует с временем ответа:



У нас SSR работает as a service. То есть у нас BFF, которая ходит в наш node.js сервис, для рендера данных. Сама BFF написана на питоне.


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


Сама протечка получилась небольшой и на графиках используемой памяти в BFF это было практически незаметно. А вот на времени ответов \ парсинге это сказалось отрицательно.


Мораль


Сей басни такова:


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


Чем больше информации вы трекаете, тем проще понимать что происходит.


С первого взгляда, на клиенте должно работать тоже самое. Но там все сильно сложнее. Чего стоит только пример с FMP. Кроме этого почти все клиенские графики, имеют выбросы, достаточно сложно поддаются аналитике, мы не знаем всех условий, в котором находится наш пользователь. Однако аналитика по-прежнему возможна. Мы можем локализовать проблему: загрузка, инит до гидрейта, рендер, реклама и\или аналитика, лонгтаски.


Все это позволяет нам обрести глаза и своевременно реагировать на проблемы.

Подробнее..

Финансовые графики для вашего приложения

18.09.2020 20:23:44 | Автор: admin


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


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


Привет, Хабр!


Было необходимо подключить графики к приложению. По итогу пользуюсь 3 вариантами, о которых мы поговорим в статье. Все они бесплатные. Два из них open source lightweight-charts, trading-vue-js (на Vuejs) и один проприетарный сharting_library.


Введение


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


Библиотеки


lightweight-charts


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


Тот, кто знаком с графиками платформы TradingView увидит лайт-версию классических графиков.



Из "коробки" доступны графики: line, area, бары, свечи и гистограмма. Графики можно комбинировать. Есть возможность выводить сделки, выставлять ордера и много дополнительных настроек. Есть CDN версия и пакет для Nodejs.


Пример подключения:


$ npm install lightweight-charts

import { createChart } from 'lightweight-charts';const chart = createChart(document.body, { width: 400, height: 300 });const lineSeries = chart.addLineSeries();lineSeries.setData([    { time: '2019-04-11', value: 80.01 },    { time: '2019-04-12', value: 96.63 },    { time: '2019-04-13', value: 76.64 },    { time: '2019-04-14', value: 81.89 },]);

Демо: https://ru.tradingview.com/lightweight-charts/
Документация: https://github.com/tradingview/lightweight-charts/blob/master/docs/README.md
Лицензия: Apache License, Version 2.0
Версия на момент статьи: 3.1.5
Комьюнити: https://discord.com/invite/E6UthXZ


сharting_library


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



Библиотека закрытая. Получение доступа проходит в несколько этапов:


  1. Заполнение заявки на сайте и ожидание ответа (от двух недель)
  2. Подписание договора
  3. Получение доступа к репозиторию на GitHub

При желании, можно подключить виджет с ограниченными настройками.


Главным минусом является ограниченность API, нельзя масштабировать. Например, добавить кастомные индикаторы или использовать скриптовый язык PineScript, он недоступен. Возможности сказываются на весе, более 6МБ. Скорость загрузки данных можно увеличить за счет кеширования запросов.


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


сharting_library я использую в своем приложении для торгового терминала. Выглядит "дорого-богато" и знакомо пользователям.


Демо: https://charting-library.tradingview.com/
Документация: https://github.com/tradingview/charting_library/wiki (если нет доступа будет 404 ошибка)
Комьюнити: https://discord.com/invite/E6UthXZ


TradingVue


Достаточно молодой проект, на котором делают действительно крутые графики. Визуально они похожи на классический TradingView, с отличиями в лицензии (MIT), полной кастомизацией и простым API. На этих графиках можно рисовать все, что захотите. Высокая скорость обработки данных 20ms для 1000 свечей. Доступен скриптовый язык JavaScript, есть песочница. Библиотека написана на Vuejs, поэтому тем, кто знаком с фреймворком, все будет понятно.


Цитата разработчика:


Если вы создаете софт для торговли эта библиотека для вас. Если вы хотите создавать собственные индикаторы и мыслите шире эта библиотека для вас. И если вам не хватает юзабилити TradingView.com в других библиотеках с открытым исходным кодом вы определенно попали в нужное место!


Минусом является небольшое количество плагинов для расширения функционала. В рамках библиотеки они называются overlays. Доступные расширения tvjs-overlays.


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



Пример подключения:


npm i trading-vue-js

<template><trading-vue :data="this.$data"></trading-vue></template><script>import TradingVue from 'trading-vue-js'export default {    name: 'app',    components: { TradingVue },    data() {        return {            ohlcv: [                [ 1551128400000, 33,  37.1, 14,  14,  196 ], // [timestamp, open, high, low, close, volume],                [ 1551132000000, 13.7, 30, 6.6,  30,  206 ],                [ 1551135600000, 29.9, 33, 21.3, 21.8, 74 ],                [ 1551139200000, 21.7, 25.9, 18, 24,  140 ],                [ 1551142800000, 24.1, 24.1, 24, 24.1, 29 ],            ]        }    }}</script>

Демо и playground: https://tvjs.io/play/
Документация: https://github.com/tvjsx/trading-vue-js
Лицензия: MIT
Комьюнити: https://discord.gg/PKD4PUy


Заключение


Если вам интересно разобрать техническую сторону и "подводные камни" для charting_library и TradingVue поддержите статью и/или отпишитесь в комментариях.


Спасибо за внимание!

Подробнее..

Визуализация сложных данных с использованием D3 и React

15.10.2020 16:12:34 | Автор: admin

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


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




Посмотреть на результат (спойлер)

Сложные данные


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


Пару примеров эффективного восприятия информации:


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

Что из себя представляет D3


D3.js это javaScript библиотека для обработки и визуализации данных. Она включает в себя функции для масштабирования, утилиты для манипуляции с данными и DOM-узлами.


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


1. Абстрагирование от физических размеров


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


getY(`значение`); \\ возвращает координату по оси y в пикселяхgetX(`название категории`); \\ возвращает координату по оси x в пикселях

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


К счастью в D3 это сделать очень просто.


Получение координат по оси Y (ось значения)


На изображении показано положение точек из массива [4, 15, 28, 35, 40] в контейнере выстой 300px:



Теперь посмотрите как с помощью D3 создать функцию для получения физических координат для отрисовки этих точек:


const getY = d3.scaleLinear()  .domain([0, 40])  .range([300, 0]);

Мы создаем функцию getY с помощью D3 функции scaleLinear(). В метод domain передаем область данных, а в range передаем физические размеры от 300px до 0px. Так как в svg отчет начинается с левого верхнего угла, то нужно именно в таком порядке передавать аргументы в range сначала 300, потом 0.


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


Пример применения функции getY:


getY(4);  // 270getY(15); // 187.5getY(28); // 90getY(35); // 37.5getY(40); // 0

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


Получение координат по оси X (ось категории)


Аналогичная ситуация по оси X. Мы хотим один раз подвязаться к категориям, а дальше передавать название категории и получать ее координаты.


На изображении мы видим контейнер шириной 600px и 5 месяцев. Месяца будут служить подписями по оси X:



Создадим такую функцию:


const getX = d3.scaleBand()  .domain(['Jan', 'Feb', 'Mar', 'Apr', 'May'])  .range([0, 600]);

Мы используем функцию scaleBand из D3. В domain мы передаем все возможные категории в нужном порядке, а в range область, выделенную под график.


Смотрим пример применения нашей функции getX:


getX('Jan'); // 0getX('Feb'); // 120getX('Mar'); // 240getX('Apr'); // 360getX('May'); // 480

В качестве аргумента мы передаем название категории, а на выходе получаем координату по оси X (отступ слева).


2. Отрисовка простых фигур


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


  • rect прямоугольник;
  • circle круг;
  • line линия;
  • text обычный блок текста.

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


Точки


Для примера попробуем нарисовать точки с использованием svg-фигуры circle:


const data = [  { name: 'Jan', value: 40 },  { name: 'Feb', value: 35 },  { name: 'Mar', value: 4 },  { name: 'Apr', value: 28 },  { name: 'May', value: 15 },];return (  <svg width={600} height={300}>    {data.map((item, index) => {      return (        <circle          key={index}          cx={getX(item.name) + getX.bandwidth() / 2}          cy={getY(item.value)}          r={4}          fill="#7cb5ec"        />      );    })}  </svg>);

Фигура circle абсолютно примитивна. В данном случае она принимает координаты центра cx, cy, радиус r и цвет заливки fill.


Здесь мы использовали новый метод bandwidth:


getX.bandwidth()

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


getX(item.name) + getX.bandwidth() / 2

Вот, что у нас получится в результате:



Подписи


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


Подпишем значения на наших точках:


return (  <svg ...>    {data.map((item, index) => {      return (        <g key={index}>          <circle ... />          <text            fill="#666"            x={getX(item.name) + getX.bandwidth() / 2}            y={getY(item.value) - 10}            textAnchor="middle"          >            {item.value}          </text>        </g>      );    })}  </svg>);

Что здесь нового? Мы обернули наш круг и текст элементом g. Элемент g один из самых распространенных в svg, обычно он просто группирует элементы и двигает их вместе при необходимости через свойство transform.


Вот как выглядят наши подписи к точкам:



3. Оси


Для осей существуют готовые элементы в D3.


const getYAxis = ref => {  const yAxis = d3.axisLeft(getY);  d3.select(ref).call(yAxis);};const getXAxis = ref => {  const xAxis = d3.axisBottom(getX);  d3.select(ref).call(xAxis);};return (  <svg ...>    <g ref={getYAxis} />    <g      ref={getXAxis}      transform={`translate(0,${getY(0)})`} // нужно сдвинуть ось в самый низ svg    />    ...  </svg>);

Вот что получается, если ничего не менять и не настраивать:



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


const getYAxis = ref => {  const yAxis = d3.axisLeft(getY)    .tickSize(-600) // ширина горизонтальных линий на графике    .tickPadding(7); // отступ значений от самого графика  d3.select(ref).call(yAxis);};const getXAxis = ref => {  const xAxis = d3.axisBottom(getX);  d3.select(ref).call(xAxis);};return (  <svg  ...>    <g className="axis" ref={getYAxis} />    <g      className="axis xAxis"      ref={getXAxis}      transform={`translate(0,${getY(0)})`}    />    ...  </svg>);

И немного стилей:


.axis {  color: #ccd6eb;  & text {    color: #666;  }  & .domain {    display: none;  }}.xAxis {  & line {    display: none;  }}

Посмотрим как сейчас выглядит наш пример:



4. Отрисовка сложных фигур


У svg нет каких-то встроенных простых методов для построения кривых по точкам, секций круга и так далее. Это достаточно сложный процесс на низком уровне. D3 предоставляет методы для построения таких сложных фигур.


Кривые линии


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


const linePath = d3  .line()  .x(d => getX(d.name) + getX.bandwidth() / 2)  .y(d => getY(d.value))  .curve(d3.curveMonotoneX)(data);// M60,0C100,6.25,140,12.5,180,37.5C220,62.5,260,270,300,270C340,270,380,90,420,90C460,90,500,138.75,540,187.5

В качестве аргумента line() мы передаем наш массив с данными data, а D3 уже под капотом проходится по этому массиву и вызывает функции для поиска координат, которые мы передали в методы x и y. В curve мы передаем тип линии, в данном случае это curveNatural (таких типов достаточно много).


Теперь немного разберем полученную строку. Команда M используется в строки для указания точки, откуда нужно начать рисовать. Команда С это кубическая кривая Безье, которая принимает три набора координат, по которым строит кривую. Подробнее можно почитать здесь https://developer.mozilla.org/ru/docs/Web/SVG/Tutorial/Paths.


Теперь просто вставляем полученную строку в качестве атрибута d для элемента path:


return (  <svg  ...>        <path      strokeWidth={3}      fill="none"      stroke="#7cb5ec"      d={linePath}    />    </svg>);

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


Смотрим на результат:



Замкнутые области


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


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


const areaPath = d3.area()  .x(d => getX(d.name) + getX.bandwidth() / 2)  .y0(d => getY(d.value))  .y1(() => getY(0))  .curve(d3.curveMonotoneX)(data);// M60,300C100,300,140,300,180,300C220,300,260,300,300,300C340,300,380,300,420,300C460,300,500,300,540,300L540,187.5C500,138.75,460,90,420,90C380,90,340,270,300,270C260,270,220,62.5,180,37.5C140,12.5,100,6.25,60,0Z

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


Добавляем полученную строку в path:


return (  <svg  ...>        <path      fill="#7cb5ec"      d={areaPath}      opacity={0.2}    />      </svg>);

Смотрим на нашу красоту:



5. События


Мы игнорируем все методы для навешивания событий из D3. Эту задачу мы также перекладываем на React и вешаем все события прям в разметке JSX. А для хранения состояний используем знакомый всем хук useState.


Эффект наведения


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


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


Но для начало заведем состояние активной категории:


// null  если ничего не активно (по умолчанию)const [activeIndex, setActiveIndex] = useState(null);

После этого пишем наш обработчик:


const handleMouseMove = (e) => {  const x = e.nativeEvent.offsetX; // количество пикселей от левого края svg  const index = Math.floor(x / getX.step()); // делим количество пикселей на ширину одной колонки и получаем индекс  setActiveIndex(index); // обновляем наше состояние};return (  <svg        onMouseMove={handleMouseMove}  ></svg>)

И добавим событие, которое будет сбрасывать активный индекс, когда мы убираем мышку с svg:


const handleMouseMove = (e) => {  };const handleMouseLeave = () => {  setActiveIndex(null);};return (  <svg        onMouseMove={handleMouseMove}    onMouseLeave={handleMouseLeave}  ></svg>)

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


data.map((item, index) => {  return (    <g key={index}>      <circle        cx={getX(item.name) + getX.bandwidth() / 2}        cy={getY(item.value)}        r={index === activeIndex ? 6 : 4} // при наведении просто немного увеличиваем круг        fill="#7cb5ec"        strokeWidth={index === activeIndex ? 2 : 0} // обводка появляется только при наведении        stroke="#fff" // добавили белый цвет для обводки        style={{ transition: `ease-out .1s` }}      />          </g>  );})

И теперь смотрим на результат:



Итог


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



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


Мы выкидываем из D3 все устаревшие методы для прямой манипуляции элементами DOMа и делам это как знали и умели до этого.


В интернете будет много примеров, которые будут сбивать вас с толку и заставлять писать в стиле jQuery, будьте внимательны. Надеюсь эта статья вам поможет сделать всё красиво!

Подробнее..

Из песочницы Пользовательский интерфейс масштабирования временных рядов на графиках

19.11.2020 02:23:00 | Автор: admin
Задача построения временных рядов на графиках решалась человеком уже в средневековье. Разработчики современных программных систем визуализации данных уделяют ей довольно много внимания. Сегодня для конкретного практического случая обработки временных рядов можно выбрать из десятков подходящих инструментов наиболее подходящий. Тем не менее, остаются случаи, для которых в наиболее популярных продуктах не хватает некоторых возможностей.

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


Необходимо исследовать многомерный временной ряд при следующих условиях:

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

Особенности работы с точки зрения среды визуализации данных


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

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

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

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

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

Существующие системы


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



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



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



Пример записан в приложении Advanced Grapher, но аналогичную опцию поддерживают многие другие системы, например, библиотека MetricsGraphics.js.

Выигрыш в скорости работы по сравнению с вариантом MS Excel здесь очевиден. Вся задача масштабирования решается в один клик:

  • левая кнопка мыши нажата в точке, соответствующей углу новой прямоугольной области;
  • курсор перемещен в противоположный угол новой области;
  • левая кнопка мыши отпущена.



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

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



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

Доработка пользовательского интерфейса


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

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

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



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

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



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

Ограничение применимости


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

Моя музыка 2020 года в картинках и графиках

26.12.2020 16:10:02 | Автор: admin

Иногда я пишу научные статьи. Но перед Новым годом хочется немного отвлечься от звериной серьёзности науки. Значит, можно написать несерьёзную и не совсем научную! Но основанную на данных и их статистическом анализе, с графиками и воспроизводимыми результатами.

Введение

В конце года Яндекс-музыка подводит итоги: самые популярные песни, альбомы и исполнители. Я открыл итоги за 2020-й и понял, что у меня специфические вкусы: на 100 треков одно совпадение. Впрочем, чему я удивляюсь? И так понятно, что мой любимый жанр A, мои любимые группы B и C, а вот эти все D, E, F, G, H я как-то не очень принимаю. Но хорошо ли я разбираюсь в своих музыкальных вкусах? Не помню даже, что я делал в начале года, а уж какая музыка мне тогда нравилась и подавно К счастью, есть количественные методы, которые помогут пролить свет на загадки моих предпочтений в музыке.

Яндекс-музыка сделала не только общие, но и личные итоги. У меня появился плейлист Мой 2020. В нём 50 треков. Можно проанализировать его статистически и посмотреть, каких исполнителей и жанров там больше всего, когда вышли песни, на каком языке написаны тексты и какие слова в них наиболее часто встречаются.

Данные и методы

Сначала нужно собрать данные. Я перенёс в таблицу порядковые номера в плейлисте, названия треков и исполнителей, потом зашёл на страницу каждого трека и выписал жанр. С годом выхода пришлось повозиться: некоторые старые песни идут в переизданиях, поэтому уточнял по Википедии. С языком, наоборот, всё сразу понятно. Ещё я сделал колонку с текстом: его тоже можно проанализировать. Для трёх песен на финском текста не нашёл оставил поля пустыми. Если бы песен было не 50, а 500, то сбор данных пришлось бы автоматизировать, а так справился вручную. Таблица в итоге выглядит примерно так.

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

Теперь берём эту таблицу и анализируем. Вот только как лучше всего это сделать?

Самый простой вариант посчитать как есть. Надо сказать, такой способ тоже даёт некоторые результаты. Например, в плейлисте 27 исполнителей в среднем получается 1,9 треков на каждого. Что ж, относительное разнообразие. При этом на исполнителя приходится либо одна песня (таких 15), либо три (таких 11), и только у одной группы в плейлисте две песни. Кроме того, можно сделать вывод, что больше всего песен на английском 32. На немецком 8, на русском 7, и ещё есть 3 песни на финском.

Но есть тонкость. Мы считаем песни так, как будто они равнозначны. А на самом деле у нас плейлист, в котором песни идут в определённом порядке. Я не знаю, почему они расположены именно так. Легче всего предположить, будто это топ-50 самых прослушиваемых треков за год. Но вряд ли: в плейлисте есть песни, которые я включал раз-другой, но нет тех, которые играли каждый день в течение месяца. Похоже, что Яндекс собирал плейлист по хитрому алгоритму, где порядковый номер песни зависит не только от количества прослушиваний. А может быть, количество прослушиваний вовсе не имеет значения. Но так или иначе, я полагаю, что песни в плейлисте идут от самых важных к менее важным, пусть даже эта важность определяется непонятно как. Следовательно, песни не равнозначны, и неравнозначность нужно учитывать.

Как сделать поправку на то, что одни песни важнее других? Добавить веса. Проще говоря, написать рядом с каждой песней число, которое характеризует её важность. Например, если есть две песни с весами 2 и 3 и одна песня с весом 7, то одна песня перевесит две остальные, потому что её вес (7) больше, чем суммарный вес двух других (5).

Подобрать веса можно по-разному. Я перепробовал кучу вариантов.

В итоге остановился на таком.

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

Теперь, когда мы расставили веса, переходим к нормальному анализу.

Результаты

Начнём с исполнителей. Если помните, у первых трёх песен в таблице с данными исполнители шли в таком порядке: Lindemann, Lordi, Imagine Dragons. Как думаете, кто три первые исполнителя по суммарному весу песен?

Правильный ответ: Lindemann, Lordi и Imagine Dragons. Ещё Rammstein очень близко, но всё же на четвёртом месте. Sampsa Astala, который на пятом месте, выглядит как мой личный прорыв года, потому что у него всего-то 16 песен вообще, и первую из них я послушал полтора месяца назад а он за эти полтора месяца забрался аж на пятое место.

Переходим к жанрам. Тут Industrial & Rock Hallelujah, а возможно, и Hard Rock Hallelujah.

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

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

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

Наконец, нарисуем график с годами выхода песен.

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

Схема волны цунамиСхема волны цунами

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

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

Я не анализировал тексты на русском и немецком, потому что их мало. Ну ладно, если совсем честно, то я их проанализировал и понял, что результат искажённый. По-русски самое частое слово внутри, потому что Distemper поёт: Ты настоящий лишь внутри, внутри, внутри. В немецких текстах на первом месте Moskau благодаря Чингисхану, а на втором allesfresser из одноимённой песни Lindemann. Песен на английском 32, поэтому результаты должны меньше зависеть от слов, которые повторяются в отдельных песнях.

Отмечу две технические подробности анализа. Во-первых, слова приводятся к начальной форме. Это нужно, чтобы посчитать listen, listens, listened и listening как одно слово listen. Во-вторых, из текстов удаляются стоп-слова те, которые встречаются чаще всего и не несут большого смысла, например: I, he, is, a, the.

Я считал по-разному: сначала просто количество слов, потом tf-idf. Tf-idf значит term frequency / inversed document frequency. Это тоже способ расставить веса, только не песням, а словам. Некоторые слова часто встречаются не только в тексте данной песни они вообще часто используются. А некоторые слова встречаются редко, но и сами по себе редко используются. Tf-idf позволяет придать больший вес тем словам, которые редко используются, но важны для конкретного текста. Ещё, пожалуй, можно было бы использовать веса песен: у слова из песни, которая на втором месте, вес больше, чем у слова из пятнадцатой песни. Однако, по-моему, так слишком легко запутаться. И вообще не факт, что важность слова зависит от важности песни: некоторые ценные фразы есть в песнях из последней десятки.

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

Результат анализа текстов оказался несколько неожиданным для меня. Ладно Lindemann, индастриал и английский язык, но я ни за что бы не подумал, что слова heart, take и feel будут самыми частыми. Может, надо было всё-таки с tf-idf считать? Там head и heart окажутся в начале. А потом языковая экзотика в виде слова countdown из The Final Countdown, а ещё zombie из одноимённой песни The Cranberries В общем, лучше без tf-idf.

Выводы

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

  2. Группа года Lindemann, жанр года индастриал, песня года Allesfresser.

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

  4. Более новые песни для меня в целом важнее, чем вышедшие давно, но и в истории музыки для меня есть несколько значимых периодов.

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

Цифр по ВКонтакте у меня нет, но навскидку там должно быть много Lordi и Sampsa Astala, а Lindemann, наоборот, меньше. Разрыв между Lindemann и Lordi небольшой, и к тому же Lindemann это в некотором смысле продолжение Rammstein, а их я начал слушать в 2019, а Lordi находка исключительно 2020-го, потому что до середины 2020-го я был уверен, что кого-кого, а этих не буду слушать никогда. На второе место Lordi вышли всего-то за полгода, а если экстраполировать на год Словом, выводы нужно скорректировать: группой 2020 года у меня становятся вот эти натуральные чудовища.

Группа Lordi в текущем составеГруппа Lordi в текущем составе

Ну ладно не такие уж и чудовища на самом деле.

Если что, это старый состав группы. Сампса Астала (Кита), Нико Хурме (Калма) и Леэна Пейса (Ава) уже не в группе, поэтому им можно показывать лица, а самому Mr. Lordi нельзяЕсли что, это старый состав группы. Сампса Астала (Кита), Нико Хурме (Калма) и Леэна Пейса (Ава) уже не в группе, поэтому им можно показывать лица, а самому Mr. Lordi нельзя

Будет интересно сравнить, когда Яндекс-музыка пришлёт письмо с итогами 2020 года: они там обычно пишут, какая у тебя любимая группа и песня, а ещё сколько ты всего за год музыки слушал.

Направления дальнейших исследований

  1. Использовать больше методов анализа текста. Облако слов неплохо выглядит, но есть куда более продвинутые приёмы: тематическое моделирование и анализ тональности. Первый расскажет, о чём песни, а второй об их настроении. К сожалению, я пока этими методами не владею. А ещё боюсь, что они не справятся с переносным смыслом и отправят Fish On в охоту и рыболовство, а AUSLNDER в туризм и путешествия.

  2. Сравнить себя с друзьями.

  3. Уговорить ВК и Яндекс сделать общедоступный АПИ, чтобы выкачать всю свою статистику и провести исследование на полном наборе данных. Вряд ли получится, к сожалению.

  4. Зарегистрироваться на Last.fm, чтобы не изобретать велосипед.

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

Подробнее..

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

02.01.2021 20:04:09 | Автор: admin

Возьмём гиперболу вида:

f(x)=n/x

Здесь n - число, делители которого должны быть найдены. Умножим f(x) на cos[f(x)] (прим. - скобки ( ) и [ ] равнозначны и не вносят дополнительных смыслов). И возьмём модуль полученной функции g(x):

|g(x)|=|f(x)cos[f(x)]|

Графики f(x) и |g(x)| показаны на рис. 1. n при этом взято равным 15. И это один из главных недостатков метода, при больших значениях n аргумент косинуса меняется с очень высокой частотой.

Рисунок 1 - График функций f(x)=35/x и |g(x)|=|f(x)cos[f(x)]|Рисунок 1 - График функций f(x)=35/x и |g(x)|=|f(x)cos[f(x)]|

Если возвести в четную степень косинус, получим график, изображённый на рисунке 2 красным.

Рисунок 2 - График функции f(x)cos[f(x)]^10Рисунок 2 - График функции f(x)cos[f(x)]^10

На последнем шаге "профильтруем" (см. рис. 3) наш косинус (т.е. умножим g(x)) функцией вида [sin(x/20)sin(3x/20)sin(5x/20)sin(7x/20)]^20.

На графике будут видны все возможные делители числа n. В нашем случае это 1, 3, 5, 15.

Рисунок 3 - Фильтрация f(x)cos[f(x)]^10 с помощью sin(nx/2) Рисунок 3 - Фильтрация f(x)cos[f(x)]^10 с помощью sin(nx/2)

Если взять n=105, на рисунках 4, 5 можно увидеть возможные делители 1, 3, 5, 7, 15, 21, 35. 105 не показано.

Рисунок 4 - Гипербола f(x)=105/x и возможные делителиРисунок 4 - Гипербола f(x)=105/x и возможные делителиРисунок 5 - Гипербола f(x)=105/x и возможные делители (продолжение)Рисунок 5 - Гипербола f(x)=105/x и возможные делители (продолжение)

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

Т.к. гиперболой описывается изотермический процесс, позаимствовав из термодинамики p-V-T диаграмму, изложенное выше можно представить и в трёхмерном виде. Для красоты на рис. 6 все множители нормированы по величине 10.

Рисунок 6 - Множители чисел 21, 77, 187, 323, 437 в 3D.Рисунок 6 - Множители чисел 21, 77, 187, 323, 437 в 3D.

Некоторые справочные данные функции (-cos[f(x)]) :

  1. Количество периодов на отрезке от 1 до n равно Nn=(n-1)/2

  2. Номер периода N для координаты x можно вычислить по формуле Nx=n(x-1)/2x

  3. Координата х N-го периода вычисляется по формуле xN=n/(n-2N)

  4. Отношение значения координаты xN+1 к xN: xN+1/xN=1+2/(n-2N)

  5. Если представить число достаточно большое n как произведение П(1+2/(n-2N)) от 1 до Nn, первые 63,2% членов при произведении дадут число е.

Подробнее..

Перевод Визуализация пересечений и перекрытий с помощью Python

06.01.2021 12:20:45 | Автор: admin

Изучение вариантов решения одной из самых сложных задач визуализации данных


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

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


Диаграмма Венна



В следующих примерах я воспользуюсь датасетом из переписи общества визуализации данных 2020 года.

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


Источник Datavisualizationsurvey Git

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

Например, если бы у нас было 100 респондентов и три возможных ответа A, B и C.

У нас может быть что-то вроде этого:
50 ответов A и B;
25 ответов А и С;
25 ответов А.


Гистограмма

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

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

Диаграммы Венна


Давайте начнём с простого и очень знакомого решения диаграмм Венна. Я использую Matplotlib-Venn для этой задачи.

import pandas as pdimport numpy as npimport matplotlib.pyplot as pltfrom matplotlib_venn import venn3, venn3_circlesfrom matplotlib_venn import venn2, venn2_circles

Теперь загрузим набор данных и подготовим данные, которые хотим проанализировать.

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

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

df = pd.read_csv('data/2020/DataVizCensus2020-AnonymizedResponses.csv')nm = 'Which of these best describes your role as a data visualizer in the past year?'d1 = df[~df[nm].isnull()].index.tolist() # independentd2 = df[~df[nm+'_1'].isnull()].index.tolist() # organizationd3 = df[~df[nm+'_2'].isnull()].index.tolist() # hobbyd4 = df[~df[nm+'_3'].isnull()].index.tolist() # studentd5 = df[~df[nm+'_4'].isnull()].index.tolist() # teacherd6 = df[~df[nm+'_5'].isnull()].index.tolist() # passive income

Диаграммы Венна просты в понимании и применении.

Нам нужно передать наборы с ключами/предложениями, которые мы будем анализировать. Если это пересечение двух наборов, воспользуемся Venn2; если это три набора, тогда используем Venn3.

venn2([set(d1), set(d2)])plt.show()


Диаграмма Венна

Здорово! С помощью диаграмм Венна мы можем чётко показать, что 201 респондент выбрал А и не выбрал B, 974 респондента выбрали B и не выбрали A, а 157 респондентов выбрали A и B.

Можно даже настроить некоторые аспекты графика.

venn2([set(d1), set(d2)],       set_colors=('#3E64AF', '#3EAF5D'),       set_labels = ('Freelance\nConsultant\nIndependent contractor',                     'Position in an organization\nwith some dataviz job responsibilities'),      alpha=0.75)venn2_circles([set(d1), set(d2)], lw=0.7)plt.show()



venn3([set(d1), set(d2), set(d5)],      set_colors=('#3E64AF', '#3EAF5D', '#D74E3B'),       set_labels = ('Freelance\nConsultant\nIndependent contractor',                     'Position in an organization\nwith some data viz job responsibilities',                    'Academic\nTeacher'),      alpha=0.75)venn3_circles([set(d1), set(d2), set(d5)], lw=0.7) plt.show()



Это здорово, но что, если мы захотим отобразить перекрытия более трёх наборов? Здесь есть пара возможностей. Например, мы могли бы использовать несколько диаграмм.

labels = ['Freelance\nConsultant\nIndependent contractor',          'Position in an organization\nwith some data viz\njob responsibilities',           'Non-compensated\ndata visualization hobbyist',          'Student',          'Academic/Teacher',          'Passive income from\ndata visualization\nrelated products']c = ('#3E64AF', '#3EAF5D')# subplot indexestxt_indexes = [1, 7, 13, 19, 25]title_indexes = [2, 9, 16, 23, 30]plot_indexes = [8, 14, 20, 26, 15, 21, 27, 22, 28, 29]# combinations of setstitle_sets = [[set(d1), set(d2)], [set(d2), set(d3)],               [set(d3), set(d4)], [set(d4), set(d5)],               [set(d5), set(d6)]]plot_sets = [[set(d1), set(d3)], [set(d1), set(d4)],              [set(d1), set(d5)], [set(d1), set(d6)],             [set(d2), set(d4)], [set(d2), set(d5)],             [set(d2), set(d6)], [set(d3), set(d5)],             [set(d3), set(d6)], [set(d4), set(d6)]]fig, ax = plt.subplots(1, figsize=(16,16))# plot textsfor idx, txt_idx in enumerate(txt_indexes):    plt.subplot(6, 6, txt_idx)    plt.text(0.5,0.5,             labels[idx+1],              ha='center', va='center', color='#1F764B')    plt.axis('off')# plot top plots (the ones with a title)for idx, title_idx in enumerate(title_indexes):    plt.subplot(6, 6, title_idx)    venn2(title_sets[idx], set_colors=c, set_labels = (' ', ' '))    plt.title(labels[idx], fontsize=10, color='#1F4576')# plot the rest of the diagramsfor idx, plot_idx in enumerate(plot_indexes):    plt.subplot(6, 6, plot_idx)    venn2(plot_sets[idx], set_colors=c, set_labels = (' ', ' '))plt.savefig('venn_matrix.png')


Матрица диаграммы Венна

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



Здесь всё начинает усложняться. На изображении выше нет пересечения только синего и зелёного. Чтобы решить эту проблему, вместо кругов мы можем использовать эллипсы.

В двух следующих примерах применяется PyVenn.

from venn import vennsets = {    labels[0]: set(d1),    labels[1]: set(d2),    labels[2]: set(d3),    labels[3]: set(d4)}fig, ax = plt.subplots(1, figsize=(16,12))venn(sets, ax=ax)plt.legend(labels[:-2], ncol=6)



Вот оно!

Но мы потеряли размер критически важную для диаграммы информацию. Синий (807) меньше жёлтого (62), что не очень помогает в визуализации. Чтобы понять, что есть что, мы можем использовать легенду и метки, но таблица была бы яснее.

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

График UpSet


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

upset_df = pd.DataFrame()col_names = ['Independent', 'Work for Org', 'Hobby', 'Student', 'Academic', 'Passive Income']nm = 'Which of these best describes your role as a data visualizer in the past year?'for idx, col in enumerate(df[[nm, nm+'_1', nm+'_2', nm+'_3', nm+'_4', nm+'_5']]):    temp = []    for i in df[col]:        if str(i) != 'nan':            temp.append(True)        else:            temp.append(False)    upset_df[col_names[idx]] = temp    upset_df['c'] = 1example = upset_df.groupby(col_names).count().sort_values('c')example



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

upsetplot.plot(example['c'], sort_by="cardinality")plt.title('Which of these best describes your role as a data visualizer in the past year?', loc='left')plt.show()


График UpSet

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

Это большое количество информации, но хорошо организованный макет позволяет легко извлекать её.

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

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

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

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



Визуализация трех наборов



Визуализация шести наборов

image



Подробнее..

Как настроить мониторинг любых бизнес-процессов, в БД Oracle построение графиков, используя бесплатную версию Grafana

10.01.2021 14:07:00 | Автор: admin

Вводные. Зачем мне это было нужно

Лично мне нужно было организовать мониторинг домашней солнечной электростанции.

Кратко о матчасти (хотя этот пост не про неё):

  • Инвертор МАП Энергия и 3 солнечных контроллера того же производителя.

  • Внутри инвертора установлен микрокомпьютер (производитель его называет "Малина"), который кое-что умеет в плане мониторинга, но не всё что мне нужно, и не очень удобно. Ценность микрокомпьютера в том, что он снимает данные с com-портов инвертора и контроллеров и публикует их насвоём http-сервере в виде Json. Данные веб-сервисов обновляются примерно каждую секунду. Также есть веб-сервисы для управления встроенными в контроллеры и инвертор реле

  • Парочка Ethernet-устройств SR-201 это такие платы с релюхами, используются для управления нагрузкой и кое-чем еще, управляются по протоколу tcp и udp.

  • Домашний сервер под управлением Centos-8, на нём установлен Oracle (разумеется Express Edition со всеми своими ограничениями, но для домашнего сервера достаточно)

  • В оракле крутятся 2 JOBa (на самом деле это persistent процессы, которые крутят бесконечный цикл и перезапускаются примерно раз в полчаса):

    1. Раз в секуну снимает данные с вебсервисов "Малины", текущее состояние реле устройств SR-201 и пишет это всё в БД Oracle. С Малины снимает с помощью несложных функций на основе utl_http, с реюх - через utl_tcp. Собственно это и есть статистика, которую будем мониторить

    2. Постоянно пересчитывает статистику за некоторый промежуток времени, и на основе полученных результатов, управляет нагрузкой и еще кое-чем через SR-201 и встроенные реле инвертора и контроллеров.

Вот это всё хозяйство мне нужно мониторить. Причем мониторить не события (событиями занимаетс Job2), а строить графики на основе накопленной статистической информации, визуализировать их на компе и мобилке. Сама "Малина" кое-что умеет, но во-первых не всё (про мои SR-201 она точно ничего не знает), во-вторых неудобный интерфейс - нельзя всё посмотреть на одном экране в удомном мне виде, а в третьих - в некоторых местах кривовато.

Вопросы: Почему Oracle а не Postgres например? Ну просто лень, хотелось сделать из того что умею... :-)

Выбор пал на Grafana https://grafana.com - довольно мощное средство визуализации статистики и прочей ерунды. Легко настраивается, удобно использовать. Работает с многими БД...

Собственно описание проекта

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

Итак:

Устанавливаем grafana

$ sudo nano /etc/yum.repos.d/grafana.repo[grafana]name=grafanabaseurl=https://packages.grafana.com/oss/rpmrepo_gpgcheck=1enabled=1gpgcheck=1gpgkey=https://packages.grafana.com/gpg.keysslverify=1sslcacert=/etc/pki/tls/certs/ca-bundle.crt
dnf updatednf install grafanasystemctl daemon-reloadsystemctl enable --now grafana-serversystemctl status grafana-server

Selinux у меня отключен, файрвол тоже, так что в эти нюансы вдаваться не буду

Далее одна проблемка: Grafana конечно с Oracle работать умеет, но данная опция (плагин) предоставляется только в Enterprise версии, которая начинается от 24к$ и это в мои планы не входит. Устанавливаем плагин grafana-simple-json-datasource

grafana-cli plugins install grafana-simple-json-datasourcesystemctl restart grafana-server

То есть графана у нас в оракл ходить не будет. Она будет брать данные из вебсервиса, теперь дело за малым - вебсервис написать.

Вебсервис будем делать на apache + php

Для этого потребуется установить и настроить:

httpd, php и php-fpm (у меня php 7.2) установлен и сконфигрирован вместе с freepbx которая живёт на том же сервере :-)

Для php нужно подключить библиотеку oci8 - тут есть сложность в том, что для php 7.2 не получится поставить oci8 командой pecl.

В общем путь такой:

Подключаем репозиторий remi, и оттуда:

dnf install php-pecl-oci8

Подключаем oci8 к php

/etc/hp.d/20-oci8.ini

В принципе достаточно раскомментировать 1 строку

extension=oci8.so

Далее этот oci8 не очень хочет запускаться, тут помогут примерно такие строки в

/etc/php-fpm.d/www.conf

env[ORACLE_HOSTNAME] = myserver.localdomainenv[ORACLE_UNQNAME] = mydbenv[ORACLE_BASE] = /u01/app/oracleenv[ORACLE_HOME] = /u01/app/oracle/product/18.4.0/dbhome_1env[ORA_INVENTORY] = /u01/app/oraInventoryenv[ORACLE_SID] = mydbenv[LD_LIBRARY_PATH] = /u01/app/oracle/product/18.4.0/dbhome_1/lib:/lib:/usr/libenv[NLS_LANG] = AMERICAN_CIS.UTF8

Теперь при исполнении php-скрипта на вебсервере, oci8 прекрасно запускается

Выкладываем скрипт на вебсервер

/var/www/html/gr/gr.php

<?phpheader("Content-Type: application/json;");$conn = oci_pconnect('www', 'www$password', 'mydb', 'AL32UTF8');if (!$conn) {    $e = oci_error();    trigger_error(htmlentities($e['message'], ENT_QUOTES), E_USER_ERROR);}// Подготовка выражения$stid = oci_parse($conn, 'begin  LGRAFANA.GetJson(:vPath, :vInp, :vOut); end;');if (!$stid) {    $e = oci_error($conn);    trigger_error(htmlentities($e['message'], ENT_QUOTES), E_USER_ERROR);}// Создадим дескрипторы$vInp = oci_new_descriptor($conn, OCI_DTYPE_LOB);$vOut = oci_new_descriptor($conn, OCI_DTYPE_LOB);// Привяжем переменные$vPath = $_SERVER["PATH_INFO"];$postdata = file_get_contents("php://input");$vInp->writeTemporary($postdata, OCI_TEMP_BLOB);oci_bind_by_name($stid, ":vPath", $vPath);oci_bind_by_name($stid, ":vInp", $vInp, -1, OCI_B_BLOB);oci_bind_by_name($stid, ":vOut", $vOut, -1, OCI_B_BLOB);// Выполним логику запроса$r = oci_execute($stid);if (!$r) {    $e = oci_error($stid);    trigger_error(htmlentities($e['message'], ENT_QUOTES), E_USER_ERROR);}echo $vOut->load(); $vInp ->close();$vOut ->close();oci_free_statement($stid);oci_commit($conn);oci_close($conn);?>

Вебсервис готов.

В нашей БД есть пакет LGRAFANA, из которого наружу торчит только одна процедура

procedure GetJson(pPathInfo in varchar2, pInpPost in blob, pOutPost out blob);

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

https://grafana.com/grafana/plugins/grafana-simple-json-datasource

Теперь настройка в самой графане:

Configuration - Data Sources - Add DataSource - Simple JSON

Дальше можно идти добавлять DashBoard и накидывать туда панели с нужными графиками

... Если у Вас уже есть реализация пакета LGRAFANA разумеется.

Да кстати про пакет. Он у меня написан не совсем на PL/SQL, но в целом Вы сможете это использовать для того чтобы понять, что надо написать в своём пакете. Это легко переводится на pl/sql.

Вкратце так:

  1. Реализуем метод, который реагирует на pahinfo=/search и отдаёт массив имён метрик которые мы умеем считать

  2. Реализуем метод /query который формирует массив данных по нужным метрикам

Полный текст пакета
pragma include([DEBUG_TRIGGER]::[MACRO_LIB]);CPALL const varchar2(30) := 'Мощность нагр.';CPNET const varchar2(30) := 'Мощность сеть';CPACB const varchar2(30) := 'Мощность АКБ';CPI2C const varchar2(30) := 'Мощность I2C';CPADD const varchar2(30) := 'Доп. Нагрузка';CPMP1 const varchar2(30) := 'Мощность MPPT1';CPMP2 const varchar2(30) := 'Мощность MPPT2';CPMP3 const varchar2(30) := 'Мощность MPPT3';CEDAY const varchar2(30) := 'Выработка за день';CEMP1 const varchar2(30) := 'Выработка MPPT1';CEMP2 const varchar2(30) := 'Выработка MPPT2';CEMP3 const varchar2(30) := 'Выработка MPPT3';CETOB const varchar2(30) := 'На заряд батареи';CEFRB const varchar2(30) := 'Взято от батареи';CEFRN const varchar2(30) := 'Взято от сети';CUNET const varchar2(30) := 'Напряжение сети';CUOUT const varchar2(30) := 'Напряжение выход';CUACB const varchar2(30) := 'Напряжение АКБ';public function TsToUTs(v_Ts in timestamp) return number isv_Dt date;beginv_Dt := v_ts;return trunc((v_Dt - to_date('01.01.1970','DD.MM.YYYY')) -- Кол-во дней с 1 янв 1970 * (24 * 60 * 60)) -- Теперь это кол-во секунд * 1000 -- Теперь миллисекунд + to_number(to_char(v_ts,'FF3')); -- Добавили миллисекундыend;procedure get_query(pInp in out nocopy JSON_OBJECT_T, pOut in out nocopy JSON_ARRAY_T) istype rtflag is record ( fTp varchar2(30),fOb json_object_t,fAr json_array_t);type ttflag is table of rtflag index by string;tflag ttflag;vTmpOb json_object_t;vTmpAr json_array_t;vTmpId varchar2(30);vDBeg timestamp;vDEnd timestamp;vDDBeg date;vDDEnd date;num_tz number;curts number;function GetFlag(pFlagName in varchar2) return boolean isbeginif tflag.exists(pFlagName) thenreturn true;elsereturn false;end if;end;--function GetFlagType(pFlagName in varchar2) return varchar2 is--begin--if tflag.exists(pFlagName) then--return tflag(pFlagName).fTp;--else--pragma error('Нет значения ['||pFlagName||'] в мвссиве tflag');--end if;--end;procedure AddTrgData(pTrgName in varchar2, pStamp in number, pValue in number) isbeginvTmpAr := Json_Array_t;vTmpAr.append(pValue);vTmpAr.append(pStamp);tFlag(pTrgName).fAr.append(vTmpAr);end;begin&debug('pInp='||pInp.to_string())vTmpOb := pInp.get_Object('range');num_tz := to_number(::[GA_MAP_STAT].[LIB].GetSetting('MALINA_TIME_ZONE'));vDBeg := vTmpOb.get_Timestamp('from') + numtodsinterval(num_tz,'hour');vDEnd := vTmpOb.get_Timestamp('to')   + numtodsinterval(num_tz,'hour');vDDBeg := to_date(to_char(vDBeg,'dd.mm.yyyy hh24:mi:ss'),'dd.mm.yyyy hh24:mi:ss');vDDEnd := to_date(to_char(vDEnd,'dd.mm.yyyy hh24:mi:ss'),'dd.mm.yyyy hh24:mi:ss');&debug('vDBeg='||to_char(vDBeg,'dd.mm.yyyy hh24:mi:ss:ff'))&debug('vDEnd='||to_char(vDEnd,'dd.mm.yyyy hh24:mi:ss:ff'))vTmpAr := pInp.get_Array('targets');for i in 0 .. vTmpAr.get_size - 1 loopvTmpOb := JSON_OBJECT_T(vTmpAr.get(i));vTmpId := vTmpOb.get_string('target');tflag(vTmpId).fTp := vTmpOb.get_string('type');tflag(vTmpId).fOb := Json_object_t;tflag(vTmpId).fAr := Json_array_t;tflag(vTmpId).fOb.put('target',vTmpId);end loop;-- Взять значения мощностей из статистики МАПif GetFlag(CPALL) or GetFlag(CPNET) or GetFlag(CPACB) or GetFlag(CPI2C) or GetFlag(CUNET) or GetFlag(CUOUT) or GetFlag(CUACB) thenfor (select x(x.[QTIME]:qtime, x.[F__PNET_CALC]:pnet -- Мощность сеть, - x.[F__PLOAD_CALC] + x.[F__PNET_CALC]:pall -- Мощность нагр., - x.[F__PLOAD_CALC]:pacb -- Мощность АКБ, x.[F__P_MPPT_AVG]:pi2c -- Мощность I2C, x.[F__UNET]:unet, x.[F__UOUTMED]:uout, x.[F__UACC]:uacb) in ::[GA_MAP_STAT] allwhere x.[QTIME] >= vDBeg and x.[QTIME] <= vDEndorder by x.[QTIME]) loopcurts := TsToUTs(x.qtime - numtodsinterval(num_tz,'hour'));vTmpId := tflag.first;while vTmpId is not null loopcase vTmpId of:CPALL: AddTrgData(vTmpId,curts,x.pall);:CPNET: AddTrgData(vTmpId,curts,x.pnet);:CPACB: AddTrgData(vTmpId,curts,x.pacb);:CPI2C: AddTrgData(vTmpId,curts,x.pi2c);:CUNET: AddTrgData(vTmpId,curts,x.unet);:CUOUT: AddTrgData(vTmpId,curts,x.uout);:CUACB: AddTrgData(vTmpId,curts,x.uacb);end;vTmpId := tflag.next(vTmpId);end loop;end loop;end if;-- Взять статистику панелейif GetFlag(CPMP1) or GetFlag(CPMP2) or GetFlag(CPMP3) thenfor (select x(x.[QTIME]:qtime,x.[F_UID]:fuid,x.[F_P_CURR]:fpower -- Мощность заряда) in ::[GA_MPPT_STAT] allwhere x.[QTIME] >= vDBeg and x.[QTIME] <= vDEndorder by x.[QTIME], x.[F_UID]) loopcurts := TsToUTs(x.qtime - numtodsinterval(num_tz,'hour'));case x.fuid of:1: if GetFlag(CPMP1) then AddTrgData(CPMP1,curts,x.fpower); end if;:2: if GetFlag(CPMP2) then AddTrgData(CPMP2,curts,x.fpower); end if;:3: if GetFlag(CPMP3) then AddTrgData(CPMP3,curts,x.fpower); end if;end;end loop;end if;-- Взять значения мощностей из статистики допнагрузкиif GetFlag(CPADD) thendeclaretqend timestamp;paend number;beginfor (select x(  x.[QTIME]:qtime, x.[FPOWER]:padd -- Доп. Нагрузка) in ::[GA_LOAD_H] allwhere x.[QTIME] >= (select x(nvl(max(x.[QTIME]),to_timestamp('01.01.1970','dd.mm.yyyy')))in ::[GA_LOAD_H] allwhere x.[QTIME] < vDBeg)and x.[QTIME] < vDEndorder by x.[QTIME]) loopcurts := TsToUTs(x.qtime - numtodsinterval(num_tz,'hour'));tqend := x.qtime;paend := x.padd;AddTrgData(CPADD,curts,x.padd);end loop;curts := TsToUTs(vDEnd - numtodsinterval(num_tz,'hour'));AddTrgData(CPADD,curts,paend);end;end if;-- Взять значения выработки по датамif GetFlag(CEDAY) or GetFlag(CEMP1) or GetFlag(CEMP2) or GetFlag(CEMP3) or GetFlag(CEFRN) thendeclarevDEBeg date;vDEEnd date;vDECur date;curEn number;prven number;vTSCur timestamp;curEnToBat number;curEnFromBat number;procedure GetCeMp(vCeMp in varchar2, vMpUID in number) isbeginvDECur := vDEBeg;while vDECur <= vDEEnd loopvTSCur := to_timestamp(to_char(vDECur,'dd.mm.yyyy'),'dd.mm.yyyy');select x(nvl(max(x.[F_PWR_KW]),0)*1000) in ::[GA_MPPT_STAT] allwhere x.[qtime] >= vTSCurand x.[qtime] < (vTSCur + numtodsinterval(1,'day'))and x.[F_TIMESTAMP] >= vDECurand x.[F_TIMESTAMP] < (vDECur+1)and x.[F_UID] = vMpUIDinto curEn;curts := TsToUTs(vTsCur - numtodsinterval(num_tz,'hour'));AddTrgData(vCeMp,curts,curen);vDECur := vDECur + 1;end loop;end;beginvDEBeg := trunc(vDDBeg);vDEEnd := trunc(vDDEnd);if GetFlag(CEDAY) or GetFlag(CETOB) or GetFlag(CEFRB) thenvDECur := vDEBeg;while vDECur <= vDEEnd loopvTSCur := to_timestamp(to_char(vDECur,'dd.mm.yyyy'),'dd.mm.yyyy');select x( nvl(max(x.[S1].[F_MPPT_DAY_E]),0),nvl(max(x.[S1].[F_ESUM_TO_BAT]),0),nvl(max(x.[S1].[F_ESUM_FROM_BAT]),0)) in ::[GA_BAT_STAT] allwhere x.[qtime] >= vTSCurand x.[qtime] < (vTSCur + numtodsinterval(1,'day'))and x.[S1].[F_TIMESTAMP] >= vDECurand x.[S1].[F_TIMESTAMP] < (vDECur+1)into curEn,curEnToBat,curEnFromBat;curts := TsToUTs(vTsCur - numtodsinterval(num_tz,'hour'));if GetFlag(CEDAY) then AddTrgData(CEDAY,curts,curen); end if;if GetFlag(CETOB) then AddTrgData(CETOB,curts,curenToBat); end if;if GetFlag(CEFRB) then AddTrgData(CEFRB,curts,curenFromBat); end if;vDECur := vDECur + 1;end loop;end if;if GetFlag(CEMP1) thenGetCeMp(CEMP1,1);end if;if GetFlag(CEMP2) thenGetCeMp(CEMP2,2);end if;if GetFlag(CEMP3) thenGetCeMp(CEMP3,3);end if;-- Посчитать сколько взято от сетиif GetFlag(CEFRN) thenvDECur := vDEBeg-1;prven := null;while vDECur <= vDEEnd loopvTSCur := to_timestamp(to_char(vDECur,'dd.mm.yyyy'),'dd.mm.yyyy');curen := 0;for (select x(x.[F__E_NET_B]*10:enet)in ::[GA_MAP_STAT] allwhere x.[qtime] >= vTSCurand x.[qtime] < (vTSCur + numtodsinterval(1,'day'))and x.[F_TIMESTAMP] >= vDECurand x.[F_TIMESTAMP] < (vDECur+1)order by x.[qtime] desc) loopcuren := x.enet;exit;end loop;if curen = 0 and prven != 0 thencuren := prven;end if;if prven is null thenprven := curen;elseif prven = 0 thenprven := curen;end if;curts := TsToUTs(vTsCur - numtodsinterval(num_tz,'hour'));&debug('1. dcur = '||to_char(vDECur,'dd.mm.yyyy')||' prven = '||prven||' curen ='||curen||' diff='||to_char(curen - prven))AddTrgData(CEFRN,curts,curen - prven);prven := curen;end if;vDECur := vDECur + 1;end loop;end if;end;end if;-- Выгрузить собранные массивы  ответvTmpId := tflag.first;while vTmpId is not null looptflag(vTmpId).fOb.put('datapoints',tflag(vTmpId).fAr);tflag(vTmpId).fAr := null;pOut.append(tflag(vTmpId).fOb);tflag(vTmpId).fOb := null;vTmpId := tflag.next(vTmpId);end loop;end;procedure get_search(pInp in out nocopy JSON_OBJECT_T, pOut in out nocopy JSON_ARRAY_T) isvTarget varchar2(100);begin&debug('pInp='||pInp.to_string())vTarget := trim(pInp.get_String('target'));if vTarget is null thenpOut.Append(CPALL);pOut.Append(CPNET);pOut.Append(CPACB);pOut.Append(CPI2C);pOut.Append(CPADD);pOut.Append(CPMP1);pOut.Append(CPMP2);pOut.Append(CPMP3);pOut.Append(CEDAY);pOut.Append(CEMP1);pOut.Append(CEMP2);pOut.Append(CEMP3);pOut.Append(CETOB);pOut.Append(CEFRB);pOut.Append(CEFRN);pOut.Append(CUNET);pOut.Append(CUOUT);pOut.Append(CUACB);end if;end;public procedure GetJson(pPathInfo in varchar2, pInpPost in blob, pOutPost out blob) isvInp JSON_OBJECT_T;vOut JSON_ARRAY_T;beginvInp := JSON_OBJECT_T(pInpPost);vOut := JSON_ARRAY_T();&debug('pPathInfo='||pPathInfo)-- Маршрутизация запроса в зависимости от pPathInfoif pPathInfo = '/search' thenget_search(vInp, vOut);elsif pPathInfo = '/query' thenget_query(vInp, vOut);end if;pOutPost := vOut.to_Blob;end;

Возможно это кому-то окажется полезным :-)

Вот такие результаты:

Подробнее..

Всего 38 заказов в интернете оплачивают заранее. У курьеров выкупают реже

05.02.2021 12:22:10 | Автор: admin

Покупатели стали делать заказы чаще, но оплачивать их не научились. С 2019 года процент предоплаченные заказов застрял где-то у отметки в 38%. То есть больше половины посылок клиенты готовы оплатить только при получении, если вообще готовы их получить. Недавно две крупные компании - PIM Solutions и Data Insight (одна агрегирует логистические возможности, другая - занимается крупными исследованиями) опубликовали большие отчеты о рынке российской логистики в 2020 году. Из него и стало ясно, что далеко не все готовы выкупать свои посылки после заказа. Что только не влияет на этот факт: категория товара, регион проживания покупателя и даже курьеры. Мы просмотрели все графики по 4 раза и готовы поделиться краткой сводкой. Спойлер: продавцам интернет-магазинов отчёт реально будет полезен.

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

Россияне не любят платить заранее

Те самые 38% из первого абзаца - доля предоплаченных заказов. Этот показатель почти не меняется уже несколько лет. Да, в Москве он подрос на пару процентов, но это лишь исключение из правил - сами понимаете, пандемия. Но даже среди 38% покупателей, которые оплачивают заказы сразу, есть те, кто не забирают свои посылки и возвращают деньги назад. Для e-commerce это звучит печально.

Данные отчетов компании Data InsightДанные отчетов компании Data Insight

Курьерам не доверяют, либо не могут доверить деньги

Продавцы, наверное, часто видят сны в которых идеальные покупатели выкупают 100% заказов и никогда не пишут гневных отзывов. В нашем неидеальном мире эти значения чуть выше 90%. Что приятно, в 2020 году этот показатель реально вырос. И тут важное открытие: доля выкупа заказов из ПВЗ выше, чем у курьеров.

Это значит, что в 2020 году покупатели чаще приходили за заказами в пункты выдачи и постаматы и реже встречали курьера у двери. Речь именно о получении заказа (не важно оплачен он заранее или нет). Пока показатели выкупа в ПВЗ в 2020 году подрастали с 91,03% до 92,87%, выкуп заказов у курьеров ни разу не превысил 90%. По факту этот параметр стабилен и не сильно меняется в последние годы.

 Данные отчета компании PIM Solutions Данные отчета компании PIM Solutions

Дойти до двери проще, чем стоять у двери

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

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

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

А еще доставка до ПВЗ - более экономичный способ доставки. Очевидно, что делая заказ в интернет-магазине и следя за тем, как меняется итоговая цена при выборе способа доставки, покупатель часто делает выбор в пользу экономии.

Есть и более явные причины. Из-за пандемии число отправлений выросло в разы, а многим ПВЗ пришлось сократить время хранения заказов. Иначе хранить было бы негде. Это, безусловно, стимулировало покупателей быстрее решать: бежать за посылкой или оказаться в числе 10%, которые бросают свои заказы и обрекают их на страшные муки возвратной логистики.

Котики делают продавцов счастливее

Эти бесконечные графики демонстрируют и еще одну зависимость. Процент выкупа заказов также зависит от категории товара. Импульсные покупки забирают реже, товары первой необходимости - чаще. Судя по графику, ребенок вполне может обойтись без новой игрушки, а вот кошка без пакета свежего корма - нет. Про детей мы готовы поспорить - просто попробуйте отказать малышу в новой машинке. Но по данным Pim, товары для животных в 2020 выкупили 91,4% покупателей, детские товары - только 82,7%

 Данные отчета компании PIM Solutions Данные отчета компании PIM Solutions

Есть категория в которой все гораздо сложнее. Одежду в 2020 выкупили только в 78,5% случаев. При этом аналитики признают, что даже эти значения завышены, потому что частично выкупленные заказы тоже легли в основу этой цифры.

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

 Данные отчета компании PIM Solutions Данные отчета компании PIM Solutions

Покупатель всегда прав, только не все его слушают

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

По данным Data Insigh, сегодня 97% всех магазинов предлагают клиентам услугу доставки до двери, а доставку до ПВЗ - лишь 94% больших магазинов и 90% всех остальных. С доставкой до постаматов предриниматели, и вовсе, не разобрались. Она подключена только у 64% крупнейших магазинов и у 24% остальных. То есть большая магазинов использует курьеров как основной канал доставки в то время, когда покупатели старательно уклоняются от встречи с посыльными.

Данные отчета Data InsightДанные отчета Data Insight

А может продавать только в Москве?

Можно. В Москве и еще паре регионов России. Срез на графике - направления доставки, который наглядно показывает, что в каждом регионе свои значения выкупа и объемы отправлений. На картах отмечено куда направляются заказы чаще и жители каких городов их выкупают.

Данные отчета Pim SolutionsДанные отчета Pim SolutionsДанные отчета Pim SolutionsДанные отчета Pim Solutions

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

Как стать котиком для покупателей?

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

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

С помощью сервиса аналитикиSellerFoxмы посмотрели как обстоят дела на маркетплейсах в категории "Товары для животных" в январе 2020 года. Данные рассмотрели на примере Wildberries.

Данные сервиса аналитики SellerFoxДанные сервиса аналитики SellerFox

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

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

Данные сервиса аналитики SellerFoxДанные сервиса аналитики SellerFox

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

Данные сервиса аналитики SellerFoxДанные сервиса аналитики SellerFox

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

Данные сервиса аналитики SellerFoxДанные сервиса аналитики SellerFox

Теперь строим гипотезу.

У нас с вами есть данные о хорошем спросе на некий noname-товар. Мы знаем, что покупатели охотнее выкупают заказы из ПВЗ и очень любят выбирать в качестве способа доставки постаматы. Еще мы видели на графиках, что в Нижегородской области отличные показатели выкупа и именно сюда доставляется огромный объем посылок.

Теперь продумываем логистику и продвижение. На какие склады маркетплейсов стоит отгрузиться, чтобы оказаться рядом с нашими гипотетическими "выкупателями"? Какой рекламный канал выбрать? Возможно контекстную рекламу стоит настроить с упором на географию этих пользователей? Можно построить массу теорий и провести сотню тестов. Если решитесь - расскажите, помогли ли вам данные исследований. А вдруг статистика не врет?

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    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