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

Самый распространенный и привычный вид шкал. Также их называют арифметическими или линейными шкалами. На такой шкале значения равноудалены друг друг от друга.
Например значения 100 и 200, и 200 и 300 отстают друг от друга на одно и тоже расстояние.
Например, на этом графике по оси Y — равномерная шкала с шагом в 20 лет средней продолжительности жизни, а по оси X — равномерная шкала с шагом 10 календарных лет.

Логарифмическая шкала

Этот вид шкал тоже используется достаточно часто, особенно когда речь идёт о научных исследованиях. Она используется для отображения широко диапазона величин, когда значения, которые попадают на график отличаются на много порядков. То есть когда мы хотим одновременно видеть и значения 0.1, 0.2 и значения 100, 200 на одном графике. Зачастую это связанно с физикой процесса. Так, например, в музыке ноты, различающиеся по частоте в два раза это ноты на октаву выше (Ля и Ля следующей октавы). Чтобы показать частоты двух нот будет удобно использовать логарифмическую шкалу.
Но бывает, что в наборе данных просто содержаться большой разброс данных. Например, как на этом графике из Beautiful Evidence Тафти, где он использует логарифмические шкалы для сравнения массы тела и мозга различных существ. Так как бывают и крошечные рыбки и огромные киты, то на таком графике удобно использовать логарифмические шкалы.

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

Степенная шкала

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

С равномерной шкалой

То есть степенные шкалы используются когда данные смещены в ту или иную сторону.
Сравнение шкал
Чтобы удобно сравнить и понять как использовать ту или иную шкалу, я сделал небольшой инструмент. На нём можно выбрать разные наборы данных и понять, как они выглядят на разных шкалах.
Интерполяция + (линейная | логарифмическая) шкала + С++
Понадобилось мне как-то сделать интерфейс для загрузки в микроконтроллер график функции «сопротивление -> температура» (график решили задавать по нескольким точкам, а потом их интерполировать). По ходу дела выяснилось, что график будет оч-чень нелинейным (180 Ом -> 100 o , 6 000 Ом -> 0 o , 30 000 Ом -> -30 o ). Поэтому пришлось мне погрузиться в тему логарифмических шкал… и сразу вынырнуть, так как я не нашел того, что мне нужно. А нужно-то мне было всего лишь понять математику (и реализацию на С++) таких дел. ЧуднО — вроде такая нужная тема, а не расписано! Ну да ладно — мозги заскрипели и вспомнили высшую математику из университета, и программа была написана. Решил описать я свои мытарства тут — может, кому пригодится.
В этой статье я распишу теорию (а также базовые виртуальные классы), в следующей возьмусь за конкретные реализации средствами Qt.
Осторожно: в тексте много графики!
Откуда растут ноги у задачи
В общем, надо мне сделать регулятор холостого хода — такая штука для автомобиля, которая при холостом ходе в зависимости от температуры двигателя должна поддерживать определенные обороты. Поддерживает она их, регулируя заслонку шаговым двигателем.
В общем, мне надо знать текущую температуру. Было решено измерять ее штатными средствами — с терморезистора. Измеряем падение напряжения на нем — получаем сопротивление. Далее из таблицы (поскольку это микроконтроллер) получаем требуемые обороты.
Таблицу эту надо задать (для этого программа пишется средствами Qt). У меня есть несколько точек «сопротивление => температура». Мне надо для каждого кода АЦП (для ряда значений сорпотивлений) получить соответствующую температуру. Поскольку у разных автомобилей эти значения могут быть разными, то надо на экране, сверившись с таблицей, задать несколько точек на кривой.
По ходу дела оказалось, что график этот будет явно в логарифмическом масштабе. Значит, надо его вывести на экран. Как это сделать — читаем дальше.
Постановка задачи
Давайте немного подробнее опишем что нам надо:
- задание функции — необходимо задать несколько точек, по которым строится график. Вобщем, вспоминаем интерполяцию;
- построение графика функции — да-да, я знаю про Qwt. Может, не очень хорошо я его знаю, т. к. я не нашел в нем следующей возможности:
- интерактивное задание функции — мне нужно двигать точки, по которым строится функция, прямо на экране, в текущих экранных координатах шкалы, которые переводятся в реальное значение;
- линейная/логарифмическая шкала — поскольку значения вот такие, как я написал, мне пришлось заложить возможность менять шкалу. Причем как одну, так и обе сразу.
Вот такое ТЗ… Ну да ничего, я справился! Давайте и вам помогу.
Да, пока не нырнули — спасибо Equation Editor-у от CodeCogs! С их помощью я лихо построил все математические формулы без всяких Microsoft Equation Editor, которые потом надо еще экспортировать в графику со вставкой сюда. Кстати, там есть и русский редактор. В общем, рекомендую!
Ну и если вместо формул вы видите пустые квадратики — это тоже «спасибо» Equation Editor-у…
Прикрепленный Excel-файл
По ходу написания статьи я все расчеты строил и проверял в таблице Excel с формулами. Оказалось очень удобно. И я решил его выложить для общественного пользования. Там внизу перечислены страницы по разделам. На каждой странице параметры, которые можно менять, отмечены как ячейки с желтым фоном. Остальные клетки лучше не трогать. Впрочем, все формулы можно смело смотреть. Скачивайте файлик и пробуйте на здоровье! Если проблемы с файлом — пишите, вышлю.
Функциональная зависимость
Итак, у нас есть некоторая зависимость — обозначим ее как
. Здесь у нас
— горизонтальная ось графика,
— вертикальная. В моем случае
было значение сопротивления,
— температура.
Почему не
? Ведь вроде бы должно быть так? Так-то оно так, но только в школе в простейшем случае.
— это координаты точки на плоскости. Для простоты определимся использовать Декартову систему координат: задает вертикальное смещение горизонтальной оси относительно нуля,
задает горизонтальное смещение вертикальной оси относительно нуля.
Все хорошо тогда, когда мы рисуем на бумаге эту самую систему координат и в ней ставим точки. Там и вправду — выбрали центр, линейкой отложили сюда, потом туда. А вот при построении графика в какой-то программе уже тонкости начинаются — что считать нулем? Что считать за «+», а что за «-«? Я рисую для этой статьи графику в CorelDRAW — там центр считается снизу слева (его можно передвинуть куда надо).
Да и в каких единицах график-то? В сантиметрах? А почему? У меня следующий этап будет реализация на С++ средствами Qt, так там я сделаю окно QWidget, у которого по умолчанию ноль — это слева сверху; единицы измерения — экранные пиксели.
Ну и не забываем о том, что это все эти красивые рассуждения справедливы пока что для линейной шкалы, а у нас маячит за горизонтом логарифмическая. Там вообще черт знает что будет!
Но это только лишь точка. А у нас будет какая-то линия, точнее — много линий. Что там будут за преобразования?
Вот именно поэтому мы с самого начала должны четко разделить функциональную зависимость и преобразования координат.
Итак, давайте договоримся о следующем: у нас есть некоторый абстрактный процесс, который описывается функциональной зависимостью
. При отображении на экран используется преобразование в координаты , где
,
. Следующие шаги — это прояснить эти самые
и
.
Но отложим пока в сторону координаты — нам надо как-то задать нашу функцию (помните ТЗ)? Причем задать в тех самых абстрактных координатах
. Этим и займемся.
Интерполяция
В моем случае был известен ряд точек
:
, Ω |
, ˚ |
|---|---|
| 180 | 100 |
| 6 000 | 0 |
| 30 000 | -30 |
Не ахти какая сложна и большая таблица, но тут явно куча пустых мест. А какое сопротивление соответствует 60˚, -40˚, . В общем, надо проставить отсутствующие точки. И в этом нам помогут интерполяция, аппроксимация и экстраполяция. Впрочем, не пугайтесь — одной интерполяции хватит за глаза.
Методов интерполяции много, все рассматривать я тут не буду. Лично мне приглянулся вначале интерполяционный многочлен Лагранжа. Он весьма прост в расчете и реализации, а также в настройке. Там предполагается, что задано множество из
точек вида (тут мы на время таки вернемся к заданию точек в виде
— так уж принято в математике).
Многочлен вычисляется как , где .
Математика испугала? Хм… Ладно, напишу на языке С++:
Как видите, все достаточно тривиально (насколько тривиальными могут быть полиномы).
Еще одно большое достоинство полиномов Лагранжа — их легко можно промоделировать в таблице Excel-я, что я и делал.
Потом, правда, все стало немного печально, т. к. у этих полиномов, как и у любых других, на графике видны вибрации. Т. е. они не могут дать прямые линии — постоянные значения. В моем случае я не смог их настроить дОлжным образом — они выгибались в явно недопустимые числа. Поэтому мне пришлось от них отказаться…
Работая в Corel, я был близко знаком с кривыми Безье — тоже достаточно удобное и простое представление табличных данных. Весьма легко реализуется в программировании. Однако это уже не интерполяция, а, скорее, аппроксимация, т. к. тут приходится подгонять кривую к нужному виду.
В итоге, внимательно присмотревшись к своей функции, я понял, что у меня вполне прокатит кусочно-линейная интерполяция — прямые отрезки между заданными линиями. Не то, чтобы совсем уж фен-шуйно, но зато легко реализуемо и удобно настраиваемо.
Говоря языком математики, мы между точками и проводим прямые линии вида .
Опять же, на языке С++ это будет выглядеть так:
Тоже ничего революционного, не так ли?
Есть одно существенное различие между полиномом Лагранжа и линейной интерполяцией: у первого нельзя явно задать значения за пределами точек — они вычисляются, у второго можно это дело контролировать. Также и поэтому я в конечном итоге остановился на линейном варианте. Более того — в логарифмическом масштабе, к которому я стремился, линейные отрезки дают более подходящий мне вариант.
Впрочем, не будем заморачиваться сейчас на методах интерполяции. Давайте лучше мы сделаем базовый класс, от которого будем наследовать реализации различных методов $#*@!поляции.
Базовый класс для задания/расчета функции
Что этот класс должен уметь делать? Мне кажется, что такой класс должен:
- давать значение функции в зависимости от аргумента — собственно, ради чего мы его и городим;
- реагировать (перемещать и пересчитывать) на изменение точек интерполяции — на вход подается факт нажатия/отпускания в некоторой координате, в результате чего производится пересчет параметров;
- различать одиночный и двойной щелчок мышки — одиночное нажатие, как по мне, указывает на передвижение точки; двойное создает новую точку;
- прорисовывать точки интерполяции как при их движении, так и без оного — т. к. у разных методов интерполяции будет разное интуитивно понятное значение точек интерполяции, то и выводить их должен производный класс (например, в интерполяции точка является частью графика; в аппроксимации точка не обязательно лежит на графике; в кривых Безье часть точек лежит на графике, часть задают форму);
- давать координаты текущей передвигаемой точки — это нужно для вывода текста координаты данной точки;
- давать сервисную информацию — например, «определена ли функция?», «сколько точек для интерполяции используется?», «получить координаты точек» и т. п. Эти данные позволят сохранить текущие настройки;
- производить настройку — «распределить столько-то точек», «задать координаты точки» — это нам позволит восстановить сохраненные настройки.
Еще есть мысли? Если будут — пишите в комментариях, добавим!
Получается такой вот класс:
(Тем, кто недоволен моим стилем и структурой — предложите объективно лучше!)
(Тем, кто найдет ошибки в коде — спасибо!)
Думаю, тут все очевидно.
Для координат используется представление точки в виде QPointF (пара чисел в виде qreal, qreal. «На всех платформах, кроме ARM, используется double» — так написано для Qt 4.8).
Нажатия на кнопки мышки реализованы функциями MouseClicked , MouseDblClicked , MouseReleased и MouseMove . Предполагается, что в конкретных реализациях будут соответствующие реакции.
Для прорисовки точек используются методы DrawPoints и DrawCurPoint . Если для всех методов, кроме этих, координаты используются абстрактные, то тут нужны самые что ни на есть экранные. Поэтому сюда передаются два объекта класса ScaleBase для преобразований. Этот класс — тоже виртуальный. Его предки реализуют преобразование из абстрактных координат в текущие экранные. Сам же этот класс будет описан ниже.
Текущее значение функции возвращает метод f (const qreal) и перегруженная операторная функция operator() (const qreal) .
Для задания структуры используются функции set_points (Num) — задание количества точек, point (Num) , set_point (Num) , get_point (Num) — задание координат конкретной точки. num_points () const — возвращает количество точек, point (Num) const , get_point (Num) const возвращают координаты точки. is_specified () const возвращает true , если структура функции задана.
В следующей статье мы распишем пару вариантов реализации этого класса.
Функция преобразования для вертикальной/горизонтальной шкалы
Есть линейные и логарифмические шкалы. Учитывая, что вертикальная шкала может быть сделана в одном формате, а горизонтальная — в другом, мы получаем четыре варианта графика:

Вариант первый — обе шкалы линейные. Вариант второй — обе логарифмические. Варианты третий и четвертый — смешанные графики. Кстати, в моем случае именно смешанный случай в итоге и подошел, т. к. по горизонтали у меня потребовался логарифмический масштаб, по вертикали — линейный.
Следовательно, задачу отображения нужно решать отдельно для обеих осей.
Напомним, что при отображении на экран используется преобразование в координаты , где
,
. Наша дальнейшая задача — построить эти функции для линейного и логарифмического случаев.
Что это за функции такие? На вход они получают координату в абстрактных (для компьютерной подпрограммы отображения на экран) координатах, на выход дают в экранных («экранные» координаты будут для разных операционных систем разными»). Для расчета им нужно знать следующее:

- пределы абстрактных координат — те предельные значения аргумента и функции, что нас интересуют. Это будут
,
для горизонтальной оси и
,
для вертикальной. Не нужно допускать классическую ошибку:
,
! В приведенном примере это проиллюстрировано; - пределы экранных координат — границы картинки, в которой рисуется график. Естественно, в текущих экранных координатах. На графике это
,
для горизонтальной оси и
,
для вертикальной;
- шаг экранной координаты — текущий шаг пикселя
,
. В простом случае это будет единица. Но в Qt вертикальный ноль — это верх окна. Следовательно,
. А еще могут быть применены всякие трансформации, и шаг уже будет отнюдь не единичным.
Обратите внимание — для задачи преобразования не важно, вертикальная это ось или нет! Она (задача) оперирует граничными значения входного, выходного, а также шагом выходного параметров. Следовательно, задачу можно обобщить: необходимо преобразовать параметр на основе его пределов , в выходную величину с учетом ее пределов , и шага . Тут намеренно введены обозначения , вместо привычных alt=»image» />, alt=»image» />, т. к. иначе будет путаница. Одно существенное дополнение: .
Базовый класс для преобразований шкал
Давайте сформулируем желаемую функциональность виртуального класса преобразований для шкалы, от которого будут унаследованы реализации шкал:
- преобразований из экранных координат в абстрактные и наоборот — логично, ради этого мы его и делаем;
- настройка преобразования — это тоже логично;
- свойства — текущие свойства преобразования (минимальные/максимальные значения, шаг для разных значений);
- информация по сетке — позиции для крупной и мелкой сетки, подписи под шкалой.
Реализация может выглядеть так:
Настройка шкалы производится функциями set_. (Val) . Пересчет необходимых значений должен производится в этих же функциях. При изменении размера окна вызывается метод Resized (Size) .
Для повышения производительности можно один раз рассчитать соответствие точки на экране и ее значение в исходных, абстрактных координатах. Этот массив возвращается методом scr_values () const . Далее, для построения крупной и мелкой сетки рассчитываются массивы (возвращают их, соответственно, функции scr_maj_grid () и scr_min_grid () ). Длина массива соответствует количеству оных линий, значение — смещению относительно начала шкалы на экране (т. е. индексу первого массива). Также заранее рассчитываются два массива — текст подписи к шкале (функция scr_text_str () ) и смещения этих подписей относительно начала (функция scr_text_pos () ).
Наконец, прямое преобразование — из абстрактных в экранные координаты — производится функцией scr (Val) , обратное — функцией val (Scr) .
Линейное преобразование
Давайте отдельно рассмотрим линейное преобразование для горизонтальной и вертикальной оси.


Мы имеем некоторую функцию — кривую в одном представлении. Для другого нам пришлось ее сузить и сместить вправо (окно на экране уменьшили и сдвинули вправо). Для другого представления нам пришлось сместить ее влево (окно сдвинули влево). Как это описывается математически? Достаточно просто: alt=»image» />. В первом случае, похоже, , во втором alt=»image» />.
В другом случае нам пришлось сузить вертикальное представление кривой и сдвинуть вверх. А потом и вообще — перевернуть. Оба этих преобразования описываются как alt=»image» />. В первом случае , . Во втором случае alt=»image» />.

Оба преобразования имеют одно и то же математическое описание: . В данном описании есть две константы, которые определяют преобразование — alt=»image» />и alt=»image» />. Первая определяет угол наклона, вторая — смещение относительно нуля.
Расчет этих констант достаточно прост — это решение системы двух уравнений:
Еще важно уметь делать обратное преобразование — скажем, координаты указателя мыши перевести в абстрактные координаты. Также ничего сложного:
Шаг в данном случае для расчета не используется, но он потом нам пригодится в реализации на С++ для расчета смещения.
Как это будет использоваться на практике? Да все просто! Горизонтальное преобразование: — граница картинки графика, соответствующая (как правило, слева), — (как правило, справа), — шаг вывода картинки по горизонтали. Вертикальное преобразование — аналогично, но по вертикали (у нас в Qt будет нижней границей картинки, — верхней, причем ).
Логарифмическое преобразование
А теперь окунемся туда, ради чего все это закрутилось:

(на графике не логарифм нарисован, а что-то похожее на него. Сделано это специально, т. к. логарифм тут будет не очень нагляден)
Если один и тот же на всей шкале, то будет везде разным! По какому закону он меняется? Правильно — по логарифмическому! Давайте первым делом научимся определять этот самый .
Всего точек у нас будет (например, , , ; тогда у нас будет 3 точки). Этому соответствует диапазон входных значений . Значению соответствует , соответствует . Последняя точка имеет индекс . Чему соответствует ?
Для линейной шкалы , где определяется диапазоном входных значений. Тут можно поступить аналогично, только вместо умножения будет возведение в степень: (помним, что ). Есть один, правда, ньюанс: при у нас , а должно бы. Это решается просто — вычтем единицу: . И тогда для нулевого случая все сходится. Как рассчитать в данном случае? Есть разные варианты. Я это предпочитаю сделать следующим образом.
Нам известно, что . В то же время мы теперь знаем, что . Получается уравнение: . Решим его относительно : . Вспомнив, чему равно и заменив корень возведением в степень, получим приемлемый для компьютера вид: (напоминаю, что ). Прекрасно, базовая величина получена!
Фактически, мы получили алгоритм преобразования из экранной координаты в абстрактную — обратная задача. Теперь режим прямую задачу — преобразование из абстрактной координаты в экранную. Задача решается несложно. Фактически, нужно найти , а для него несложно и .
Для нахождения надо решить уравнение относительно : . Ну а дальше уже из свойств логарифма получим, что . Ну и далее, рассматривая экранные координаты как линию с наклоном, получим, что (для полной красоты надо еще заменить на ).
Вроде бы базовую математику рассмотрели. Нашли ошибки или неточности — пишите в комментариях, буду благодарен!
Со временем напишу следующую статью — реализацию этой математики средствами Qt языка C++.
Какую шкалу графика выбрать
При подготовке инструкции по работе с графиками на сайте Stockcharts.com я не уточнила такой важной момент, как выбор шкалы. Уточняю. Если вы посмотрите на график движения цены, то увидите на нем две оси. По горизонтальной оси X отложено время, по вертикальной оси Y – цена.
Для оси X используется арифметическая (линейная) шкала. Для оси Y может применяться как арифметическая, так и логарифмическая шкала. В чем их отличие?
Арифметическая шкала показывает простое изменение цены в количестве пунктов или долларов. Логарифмическая шкала показывает не абсолютное изменение цены, а относительное, то есть не в количестве пунктов или долларов, а в процентном соотношении.
На арифметической шкале все единичные отрезки имеют одинаковую длину. Интервалы логарифмической шкалы не равны, так как с ростом цены процентные изменения уменьшаются и, как следствие, отрезки становятся уже, а деления чаще.
Принцип построения арифметической шкалы
Арифметическая шкала строится от нуля и с помощью сложения и вычитания. При перемещении вправо от нуля значения складываются, а при перемещении влево от нуля – вычитаются. Значение показателя по мере продвижения оси изменяется на равную величину. В результате отрезок шкалы 10-20 будет равен отрезку 100-110, несмотря на то, что в первом варианте изменение составит 100% (2 раза), а во втором – лишь 10%.
Принцип построения логарифмической шкалы
В логарифмической шкале равным отрезкам соответствует одинаковое процентное изменение показателя. Например, отрезок шкалы 10-20 будет равен отрезкам 20-40 и 40-80, т.е. изменение показателя во всех случаях составит 2 раза. Данная шкала строится от единицы с помощью умножения и деления. И для того чтобы переместиться на такое же расстояние как в арифметической шкале, например, на 10, необходимо 1 умножить на 10. В свою очередь, для перемещения на 10 влево, нужно 1 разделить на 10.
Отображение графиков с разной шкалой
При работе с графиками на коротких интервалах времени разница между арифметическим и логарифмическим способами шкалирования едва заметна. Однако при больших ценовых колебаниях и длительных периодах различия существенны. Эта хорошо видно на рисунке ниже. На нем я наложила графики индекса NYSE Composite ($NYA) за более чем 20-летний период. Менее четкая линия – это линия графика, построенного по логарифмической шкале.

График индекса NYSE Composite ($NYA), построенный по арифметической и логарифмической шкале
Живая версия графиков доступна здесь и здесь.
Обратите внимание, что диапазон колебаний на графике с арифметической шкалой выше, чем с логарифмической. Кроме этого, на графике с арифметической шкалой есть ощущение более существенного движения и изменения. Поэтому такой способ построения любят СМИ: он позволяет преувеличить значения показателей и раздуть новость. Этого не происходит, когда данные отображаются на логарифмической шкале. И именно ее я рекомендую использовать при работе с графиками, охватывающими данные за длительный период.