Matplotlib: Как создать нелинейную ось X?

Что такое нелинейная ось и зачем она нужна?

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

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

Необходимость в нелинейных осях возникает, когда:

Диапазон данных слишком широк для адекватного отображения на линейной шкале (например, данные от 1 до 1 000 000).

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

Требуется линеаризовать нелинейную зависимость для более простого анализа (например, степенная или экспоненциальная зависимость).

Данные содержат выбросы, которые искажают вид графика на линейной шкале.

Примеры использования нелинейных осей

Нелинейные оси активно применяются в различных областях:

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

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

Обработка сигналов: Отображение амплитудно-частотных характеристик в децибелах (логарифмическая шкала).

Экономика: Анализ распределения доходов или населения.

В Matplotlib есть встроенные нелинейные шкалы, такие как логарифмическая ('log'), симметричная логарифмическая ('symlog') и логистическая ('logit'). Однако иногда возникает потребность в создании совершенно пользовательской нелинейной шкалы, соответствующей специфической математической функции или требующей особого форматирования делений.

Создание пользовательской шкалы для оси X

Создание полностью пользовательской нелинейной шкалы в Matplotlib требует определения двух основных компонентов: шкалы (Scale) и трансформации (Transform). Шкала определяет логику размещения делений и их форматирования, тогда как трансформация выполняет математическое преобразование координат.

Определение пользовательского класса шкалы

Чтобы создать пользовательскую шкалу, необходимо наследовать класс от matplotlib.scale.ScaleBase. Этот базовый класс предоставляет структуру для определения поведения шкалы. Ваш класс должен, как минимум, определить статический атрибут name (уникальное строковое имя для регистрации шкалы) и реализовать методы get_transform и set_default_locators_and_formatters.

import matplotlib.scale as mscale
import matplotlib.transforms as mtransforms
import matplotlib.ticker as mticker

class CustomXScale(mscale.ScaleBase):
    """
    Пользовательская шкала для оси X.
    Пример демонстрирует создание шкалы, которая использует определенную трансформацию.
    """
    # Уникальное имя для регистрации шкалы в Matplotlib
    name: str = 'custom_x'

    def get_transform(self) -> mtransforms.Transform:
        """
        Возвращает объект трансформации, связанный с этой шкалой.
        """
        # Здесь должна быть определена или импортирована ваша пользовательская трансформация
        # В этом примере предполагается, что есть класс CustomXTransform
        return CustomXTransform()

    def set_default_locators_and_formatters(self, axis: mticker.Axis) -> None:
        """
        Устанавливает локаторы (для определения позиций делений) и форматтеры
        (для форматирования текста делений) по умолчанию для оси.
        
        Args:
            axis: Объект оси (X или Y), к которому применяется шкала.
        """
        # Для пользовательской шкалы часто требуются пользовательские локаторы и форматтеры,
        # так как стандартные могут не соответствовать нелинейному распределению.
        # В простейшем случае можно использовать стандартные, но это может привести к неоптимальному виду.
        axis.set_major_locator(mticker.AutoLocator())
        axis.set_major_formatter(mticker.ScalarFormatter())
        axis.set_minor_locator(mticker.AutoMinorLocator())
        # axis.set_minor_formatter(mticker.NullFormatter()) # Или другой форматтер для мелких делений

Метод get_transform является ключевым, так как он связывает вашу шкалу с логикой преобразования данных. Метод set_default_locators_and_formatters позволяет определить, как Matplotlib будет автоматически расставлять и подписывать деления на оси, если пользователь не задал их явно. Для нелинейных шкал часто приходится создавать пользовательские классы Locator и Formatter.

Реализация методов `get_transform` и `set_default_locators_and_formatters`

Как показано выше, get_transform должен вернуть экземпляр класса, унаследованного от matplotlib.transforms.Transform. Этот класс будет выполнять фактическое преобразование координат данных в координаты отображения. Описание его реализации будет дано в следующем разделе.

set_default_locators_and_formatters принимает объект axis и позволяет вызвать его методы set_major_locator, set_minor_locator, set_major_formatter, set_minor_formatter. Если стандартные локаторы и форматтеры (AutoLocator, ScalarFormatter и т.п.) не дают приемлемого результата для вашей нелинейной шкалы, вам потребуется создать пользовательские классы, унаследованные от matplotlib.ticker.Locator и matplotlib.ticker.Formatter соответственно.

Например, логарифмическая шкала использует специальные локаторы (LogLocator) и форматтеры (LogFormatter) для правильного размещения и подписи делений на степенях числа 10 (или другого основания).

Трансформация данных для нелинейной оси X

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

Создание класса трансформации

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

import numpy as np
import matplotlib.transforms as mtransforms

class CustomXTransform(mtransforms.Transform):
    """
    Пользовательская трансформация для оси X.
    Пример: трансформация x -> x^2 (и обратная sqrt(x)).
    Данная трансформация подходит только для положительных данных.
    """
    # Определяет, является ли трансформация аффинной (линейной).
    # Нелинейные трансформации должны установить это в False.
    input_dims: int = 1
    output_dims: int = 1
    is_separable: bool = True # Указывает, может ли трансформация применяться к каждому измерению независимо

    def transform_non_affine(self, values: np.ndarray) -> np.ndarray:
        """
        Применяет прямое неаффинное преобразование к массиву значений.
        Преобразует из Data Coordinates в Display Coordinates (перед аффинной частью).
        
        Args:
            values: Массив значений данных для преобразования.
            
        Returns:
            Преобразованный массив значений.
        """
        # Пример: преобразование x в x^2. Требует, чтобы x >= 0.
        # В реальной логарифмической шкале здесь было бы log(x).
        transformed_values = np.power(values, 2.0)
        # Обработка возможных недействительных значений (например, отрицательные числа для sqrt) - опционально,
        # зависит от характера трансформации и ожидаемых данных.
        # Для x^2 из положительных чисел это не проблема.
        return transformed_values

    def inverted(self) -> 'CustomXTransform':
        """
        Возвращает инвертированную трансформацию.
        Инвертированная трансформация преобразует из Display Coordinates обратно в Data Coordinates.
        
        Returns:
            Объект инвертированной трансформации.
        """
        # Возвращаем экземпляр класса, который выполняет обратное преобразование.
        # В данном случае, это будет трансформация sqrt(x).
        # Часто удобнее создать отдельный класс для инвертированной трансформации
        # или использовать флаг внутри одного класса.
        
        # Простейший подход: создать новый класс для инверсии.
        # Или, как показано ниже, использовать общий класс с флагом.
        return InvertedCustomXTransform()

class InvertedCustomXTransform(mtransforms.Transform):
    """
    Инвертированная пользовательская трансформация (sqrt).
    """
    input_dims: int = 1
    output_dims: int = 1
    is_separable: bool = True

    def transform_non_affine(self, values: np.ndarray) -> np.ndarray:
        """
        Применяет инвертированное преобразование (sqrt).
        
        Args:
            values: Массив значений для преобразования (из Display Coordinates).
            
        Returns:
            Преобразованный массив значений (обратно в Data Coordinates).
        """
        # Пример: преобразование y в sqrt(y). Требует, чтобы y >= 0.
        # В реальной обратной логарифмической шкале здесь было бы 10**y.
        # Важно: могут появиться отрицательные значения из-за смещения или масштабирования
        # на уровне аффинной части или Display Coordinates. Нужна аккуратная обработка.
        valid_values = np.maximum(0, values) # Обрабатываем возможные отрицательные значения
        inverted_values = np.sqrt(valid_values)
        return inverted_values

    def inverted(self) -> 'InvertedCustomXTransform':
        """
        Инверсия инвертированной трансформации возвращает исходную трансформацию.
        """
        return CustomXTransform()
Реклама

Реализация методов `transform_non_affine` и `inverted`

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

inverted(self): Этот метод должен вернуть экземпляр обратной трансформации. Обратная трансформация выполняет преобразование из пространства отображения обратно в пространство данных. Matplotlib использует ее, например, при интерактивном перемещении курсора по графику для отображения координат данных. Реализация обратной трансформации так же критична для корректной работы оси.

В примере выше показана трансформация x^2 и ее инверсия sqrt(x). Для логарифмической шкалы прямая трансформация была бы log(x), а обратная — exp(x) (или 10^x в зависимости от основания логарифма).

Интеграция пользовательской шкалы и трансформации в Matplotlib

После определения классов ScaleBase и Transform необходимо зарегистрировать вашу шкалу в Matplotlib и затем применить ее к нужной оси.

Регистрация пользовательской шкалы

Чтобы Matplotlib "знал" о вашей новой шкале и мог использовать ее по имени (например, в ax.set_xscale()), ее необходимо зарегистрировать с помощью функции matplotlib.scale.register_scale().

import matplotlib.pyplot as plt
import matplotlib.scale as mscale
# Предполагается, что классы CustomXScale, CustomXTransform,
# InvertedCustomXTransform уже определены как выше.

# Регистрация пользовательской шкалы
if CustomXScale not in mscale._scale_registry:
    mscale.register_scale(CustomXScale)

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

Применение шкалы к оси X на графике

После регистрации шкалы ее можно применить к оси X (или Y) объекта Axes с помощью метода set_xscale() (или set_yscale()), передав имя вашей шкалы в качестве аргумента.

fig, ax = plt.subplots()

# Пример данных (только положительные для нашей x^2 шкалы)
# Для логарифмической шкалы также нужны только положительные данные (или >0).
# Для демонстрации "логарифмической" шкалы вручную, используем данные > 0.
x_data = np.linspace(0.1, 100, 200)
y_data = np.sin(x_data / 10) * np.exp(x_data / 50) + np.random.normal(0, 0.5, x_data.shape)

ax.plot(x_data, y_data)

# Применение пользовательской шкалы к оси X
ax.set_xscale('custom_x') # Используем имя, под которым зарегистрирована шкала

ax.set_title('График с пользовательской шкалой X (похожей на логарифмическую)')
ax.set_xlabel('Значения по оси X (логарифмическая шкала)')
ax.set_ylabel('Значения по оси Y')
ax.grid(True, which="both", linestyle='--', linewidth=0.5)

plt.show()

Пример: Логарифмическая ось, реализованная вручную

Давайте соберем все вместе и покажем, как можно создать шкалу, имитирующую логарифмическую, используя пользовательские классы шкалы и трансформации. Вместо x^2 и sqrt, будем использовать log10(x) и 10^x.

import matplotlib.pyplot as plt
import matplotlib.scale as mscale
import matplotlib.transforms as mtransforms
import matplotlib.ticker as mticker
import numpy as np

# 1. Определяем класс трансформации (log10)
class Log10Transform(mtransforms.Transform):
    input_dims: int = 1
    output_dims: int = 1
    is_separable: bool = True

    def transform_non_affine(self, values: np.ndarray) -> np.ndarray:
        # Применяем log10. Обрабатываем значения  0
        transformed_values[positive_mask] = np.log10(values[positive_mask])
        return transformed_values

    def inverted(self) -> 'Pow10Transform':
        return Pow10Transform()

# 2. Определяем класс обратной трансформации (10^x)
class Pow10Transform(mtransforms.Transform):
    input_dims: int = 1
    output_dims: int = 1
    is_separable: bool = True

    def transform_non_affine(self, values: np.ndarray) -> np.ndarray:
        # Применяем 10^x.
        inverted_values = np.power(10.0, values)
        return inverted_values

    def inverted(self) -> 'Log10Transform':
        return Log10Transform()

# 3. Определяем класс шкалы
class ManualLog10Scale(mscale.ScaleBase):
    name: str = 'manual_log10'

    def get_transform(self) -> mtransforms.Transform:
        return Log10Transform()

    def set_default_locators_and_formatters(self, axis: mticker.Axis) -> None:
        # Для логарифмической шкалы нужны специальные локаторы и форматтеры.
        # Используем встроенные логарифмические, чтобы получить правильные деления (1, 10, 100...) и подписи.
        axis.set_major_locator(mticker.LogLocator(base=10.0))
        axis.set_major_formatter(mticker.LogFormatterSciNotation(base=10.0))
        axis.set_minor_locator(mticker.LogLocator(base=10.0, subs=np.arange(2, 10)))
        axis.set_minor_formatter(mticker.NullFormatter())

# 4. Регистрируем пользовательскую шкалу
if ManualLog10Scale not in mscale._scale_registry:
     mscale.register_scale(ManualLog10Scale)

# 5. Создаем график и применяем шкалу
fig, ax = plt.subplots(figsize=(8, 4))

# Данные для логарифмической шкалы должны быть > 0.
# Используем широкий диапазон значений.
x_data_log = np.logspace(-1, 3, 100) # От 0.1 до 1000
y_data_log = np.sin(np.log10(x_data_log) * np.pi/2) + np.random.normal(0, 0.1, x_data_log.shape)

ax.plot(x_data_log, y_data_log)

# Применяем нашу вручную созданную логарифмическую шкалу
ax.set_xscale('manual_log10')

ax.set_title('График с вручную реализованной логарифмической шкалой X')
ax.set_xlabel('Значения по оси X (manual_log10)')
ax.set_ylabel('Значения по оси Y')
ax.grid(True, which="both", linestyle='--', linewidth=0.5)

plt.show()

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

Альтернативные подходы и готовые решения

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

Использование `matplotlib.scale.FuncScale` для простых трансформаций

Если ваша нелинейная шкала может быть описана парой функций (прямая и обратная трансформации), и вам не требуются особые локаторы или форматтеры (или вас устраивает использование стандартных), можно воспользоваться классом matplotlib.scale.FuncScale.

FuncScale принимает в конструктор два аргумента: основание оси (Axes base) и кортеж из двух функций (прямая и обратная трансформации).

import matplotlib.pyplot as plt
import matplotlib.scale as mscale
import numpy as np

# Функции для трансформации x -> x^3 и обратной y -> y^(1/3)
def forward_cube(x: np.ndarray) -> np.ndarray:
    """Прямая трансформация: x в x^3."""
    return np.power(x, 3.0)

def inverse_cube(y: np.ndarray) -> np.ndarray:
    """Обратная трансформация: y в y^(1/3)."""
    # Необходимо обрабатывать отрицательные числа для нечетных степеней
    return np.sign(y) * np.power(np.abs(y), 1.0/3.0)

# Регистрация шкалы на основе функций
# FuncScale сам создает необходимый класс ScaleBase и регистрирует его.
# Указываем имя 'cube_scale' и кортеж с функциями.
mscale.FuncScale.register('cube_scale', forward_cube, inverse_cube)

# Создаем график с данными (теперь можно использовать и отрицательные)
x_data_cube = np.linspace(-5, 5, 100)
y_data_cube = np.cos(x_data_cube) + np.random.normal(0, 0.1, x_data_cube.shape)

fig, ax = plt.subplots(figsize=(6, 4))
ax.plot(x_data_cube, y_data_cube)

# Применяем шкалу, зарегистрированную через FuncScale
ax.set_xscale('cube_scale')

ax.set_title('График с шкалой X: x -> x^3 (используя FuncScale)')
ax.set_xlabel('Значения по оси X')
ax.set_ylabel('Значения по оси Y')
ax.grid(True, which="both", linestyle='--', linewidth=0.5)

plt.show()

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

Применение существующих нелинейных шкал (например, логарифмической, симметричной логарифмической)

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

Основные встроенные нелинейные шкалы:

'log': Логарифмическая шкала. Требует положительных данных (или данных >0). Используется для данных, охватывающих много порядков или изменяющихся экспоненциально.

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

'logit': Логистическая шкала. Применяется для данных в диапазоне от 0 до 1 (например, вероятности). Преобразует данные p в log(p / (1-p)).

Использование этих шкал максимально просто:

import matplotlib.pyplot as plt
import numpy as np

# Пример данных для логарифмической шкалы
x_data_builtin_log = np.logspace(1, 5, 100) # От 10 до 100000
y_data_builtin_log = np.random.rand(100)

fig, ax = plt.subplots(figsize=(6, 4))
ax.plot(x_data_builtin_log, y_data_builtin_log)

# Применение встроенной логарифмической шкалы
ax.set_xscale('log')

ax.set_title('График со встроенной логарифмической шкалой X')
ax.set_xlabel('Значения по оси X (log)')
ax.set_ylabel('Значения по оси Y')
ax.grid(True, which="both", linestyle='--', linewidth=0.5)

plt.show()

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


Добавить комментарий