Как с помощью Matplotlib рисовать линии уровня

Оглавление

Введение

Линия уровня функции - это кривая, в каждой точке которой функция принимает одно и то же фиксированное значение.

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

  1. В виде трех двумерных матриц, описывающих двумерную сетку и значения функции в узлах этой сетки. В этом случае используются функции contour и contourf.
  2. В виде трех одномерных массивов, описывающих произвольно расположенные точки на плоскости и значения функции в этих точках. В этом случае используются функции tricontour и tricontourf.

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

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

# -*- coding: utf-8 -*-

import matplotlib.pyplot as plt

import numpy as np


def make_data():
    x = np.arange(-10, 10, 0.05)
    y = np.arange(-10, 10, 0.05)
    xgrid, ygrid = np.meshgrid(x, y)

    z = (np.sin(xgrid * 0.3) * np.cos(ygrid * 0.75) /
         (1 + np.abs(xgrid * ygrid) * 0.05))
    return xgrid, ygrid, z


if __name__ == '__main__':
    x, y, z = make_data()

    fig = plt.figure()
    axes = fig.add_subplot(projection='3d')

    axes.plot_surface(x, y, z, edgecolor='k', linewidth=0.3)
    plt.show()

Этот пример основан на тех функциях, методах и их параметрах, которые были подробно описаны в статье про рисование трехмерных графиков. В том числе там были описаны функция numpy.meshgrid() и метод Axes3d.plot_surface(). Поэтому, если этот пример у вас вызывает какие-то вопросы, то лучше сначала прочитайте вышеупомянутую статью.

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

Рисование линий уровня

Чтобы построить линии уровня по данным, которые заданы в виде трех двумерных матриц (по данным, расположенным в узлах прямоугольной сетки), можно воспользоваться функциями countour() или contourf() из модуля matplotlib.pyplot. Отличие этих двух функций состоит в том, что contourf(), в отличие от contour(), закрашивает области между линиями уровня однотонным цветом. Эти функции имеют некоторое количество параметров для настройки внешнего вида графика, но для начала ими можно пренебречь и передать в функцию только данные, по которым нужно построить линии уровня. Например:

# -*- coding: utf-8 -*-

import matplotlib.pyplot as plt

import numpy as np


def make_data():
    x = np.arange(-10, 10, 0.05)
    y = np.arange(-10, 10, 0.05)
    xgrid, ygrid = np.meshgrid(x, y)

    z = (np.sin(xgrid * 0.3) * np.cos(ygrid * 0.75) /
         (1 + np.abs(xgrid * ygrid) * 0.05))
    return xgrid, ygrid, z


if __name__ == '__main__':
    x, y, z = make_data()

    # !!!
    plt.contour(x, y, z)
    plt.show()

Результат будет выглядеть следующим образом:

Аналогичного результата можно добиться, если использовать одноименный метод contour() из класса осей, как показано в следующем примере:

# -*- coding: utf-8 -*-

import matplotlib.pyplot as plt

import numpy as np


def make_data():
    x = np.arange(-10, 10, 0.05)
    y = np.arange(-10, 10, 0.05)
    xgrid, ygrid = np.meshgrid(x, y)

    z = (np.sin(xgrid * 0.3) * np.cos(ygrid * 0.75) /
         (1 + np.abs(xgrid * ygrid) * 0.05))
    return xgrid, ygrid, z


if __name__ == '__main__':
    x, y, z = make_data()

    fig = plt.figure()
    axes = fig.add_subplot()

    # !!!
    axes.contour(x, y, z)
    plt.show()

Для того, чтобы линии уровня были залиты цветом, нужно заменить функцию contour() на contourf():

# -*- coding: utf-8 -*-

import matplotlib.pyplot
import numpy


def makeData():
    x = numpy.arange(-10, 10, 0.05)
    y = numpy.arange(-10, 10, 0.05)
    xgrid, ygrid = numpy.meshgrid(x, y)

    zgrid = (numpy.sin(xgrid * 0.3) * numpy.cos(ygrid * 0.75) /
             (1 + numpy.abs(xgrid * ygrid) * 0.05))
    return xgrid, ygrid, zgrid


if __name__ == '__main__':
    x, y, z = makeData()
    matplotlib.pyplot.contourf(x, y, z)

    matplotlib.pyplot.show()

В результате будет показано окно с таким графиком:

Добавление подписей к линиям уровня

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

Функции contour() / contourf() возвращают объект типа QuadContourSet (это производный класс от более общего класса ContourSet), который описывает наш график. Чтобы добавить на график подписи для линий уровня, нужно вызвать функцию clabel() из модуля matplotlib.pyplot или одноименный метод из экземпляра класса осей. Эта функция в качестве первого параметра ожидает экземпляр класса ContourSet. Таким образом, в функцию clabel() в качестве первого параметра можно передавать экземпляры класса, которые были возвращены функциями contour() / contourf().

# -*- coding: utf-8 -*-

import matplotlib.pyplot as plt

import numpy as np


def make_data():
    x = np.arange(-10, 10, 0.05)
    y = np.arange(-10, 10, 0.05)
    xgrid, ygrid = np.meshgrid(x, y)

    z = (np.sin(xgrid * 0.3) * np.cos(ygrid * 0.75) /
         (1 + np.abs(xgrid * ygrid) * 0.05))
    return xgrid, ygrid, z


if __name__ == '__main__':
    x, y, z = make_data()

    cs = plt.contour(x, y, z)
    plt.clabel(cs)
    plt.show()

Результат выглядит следующим образом:

Такого же результата можно добиться, если использовать не функцию clabel() из matplotlib.pyplot, а одноименный метод из класса QuadContourSet, то есть в предыдущем примере заменить строку plt.clabel(cs) на cs.clabel():

# -*- coding: utf-8 -*-

import matplotlib.pyplot as plt

import numpy as np


def make_data():
    x = np.arange(-10, 10, 0.05)
    y = np.arange(-10, 10, 0.05)
    xgrid, ygrid = np.meshgrid(x, y)

    z = (np.sin(xgrid * 0.3) * np.cos(ygrid * 0.75) /
         (1 + np.abs(xgrid * ygrid) * 0.05))
    return xgrid, ygrid, z


if __name__ == '__main__':
    x, y, z = make_data()

    cs = plt.contour(x, y, z)

    # !!!
    cs.clabel()
    plt.show()

Строго говоря, метод clabel() класса QuadContourSet наследует от своего прародителя - класса ContourLabeler, а общая иерархия классов выглядит следующим образом:

Полный список параметров функции clabel вы можете найти в документации, некоторые из них мы рассмотрим ниже в этой статье.

Задание значений отображаемых линий уровня

В предыдущих примерах мы не задавали явно значения, которые мы хотели бы отметить с помощью линий уровня, а понадеялись на то, что библиотека Matplotlib выберет за нас эти значения таким образом, чтобы адекватно представить отображаемую функцию. Однако в реальности скорее всего мы захотим явно задавать значения, по которым должны проходить линии уровня. Для этого в функции contour() / contourf() нужно передать дополнительный параметр (с именем levels), который может быть либо целым числом, задающим количество отображаемых линий уровня (тогда разность значений между соседними линиями уровня будут равные), либо массив, явно задающий значения линий уровня, которые мы хотим отобразить. В последнем случае массив обязательно должен быть отсортирован по возрастанию значений.

В следующем примере мы задаем количество линий уровня, равное 20, а учитывая, что значения нашей функции лежит в интервале от -1 до 1, то расстояние между линиями уровня будет равно 0.1.

# -*- coding: utf-8 -*-

import matplotlib.pyplot as plt

import numpy as np


def make_data():
    x = np.arange(-10, 10, 0.05)
    y = np.arange(-10, 10, 0.05)
    xgrid, ygrid = np.meshgrid(x, y)

    z = (np.sin(xgrid * 0.3) * np.cos(ygrid * 0.75) /
         (1 + np.abs(xgrid * ygrid) * 0.05))
    return xgrid, ygrid, z


if __name__ == '__main__':
    x, y, z = make_data()

    cs = plt.contour(x, y, z, levels=20)
    cs.clabel()
    plt.show()

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

# -*- coding: utf-8 -*-

import matplotlib.pyplot as plt

import numpy as np


def make_data():
    x = np.arange(-10, 10, 0.05)
    y = np.arange(-10, 10, 0.05)
    xgrid, ygrid = np.meshgrid(x, y)

    z = (np.sin(xgrid * 0.3) * np.cos(ygrid * 0.75) /
         (1 + np.abs(xgrid * ygrid) * 0.05))
    return xgrid, ygrid, z


if __name__ == '__main__':
    x, y, z = make_data()

    levels = [-0.99, -0.7, -0.5, -0.3, -0.1, 0.0, 0.1, 0.3, 0.5, 0.7, 0.99]
    cs = plt.contour(x, y, z, levels=levels)
    cs.clabel()
    plt.show()

Задание цвета и толщины линий уровня

По умолчанию цвета линий уровня выбираются, исходя из установленной в данный момент цветовой карты (Colormap, cmap в терминах Matplotlib), но мы можем явно указать, каким цветом отображать линии. Для этого в функцию contour() / contourf() можно передать именованный параметр colors, который может принимать значение None, что является значением по умолчанию, строку, описывающую цвет (в этом случае все линии уровня будут рисоваться этим цветом) или кортеж элементов, описывающих цвета (про то, каким образом можно указывать цвета в Matplotlib вы можете прочитать в статье Способы задания цвета в Matplotlib), в этом случае цвета будут последовательно перебираться среди элементов этого кортежа.

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

# -*- coding: utf-8 -*-

import matplotlib.pyplot as plt

import numpy as np


def make_data():
    x = np.arange(-10, 10, 0.05)
    y = np.arange(-10, 10, 0.05)
    xgrid, ygrid = np.meshgrid(x, y)

    z = (np.sin(xgrid * 0.3) * np.cos(ygrid * 0.75) /
         (1 + np.abs(xgrid * ygrid) * 0.05))
    return xgrid, ygrid, z


if __name__ == '__main__':
    x, y, z = make_data()

    cs = plt.contour(x, y, z, colors='black')
    cs.clabel()
    plt.show()

Также можно задавать цвета с помощью цветовой карты. Для выбора цветовой карты предназначен параметр cmap, он может принимать значение строки с именем цветовой карты или экземпляр класса Colormap. Следующий пример показывает, как установить цветовую карту с именем "jet". Имена встроенных цветовых карт можно найти в документации.

# -*- coding: utf-8 -*-

import matplotlib.pyplot as plt

import numpy as np


def make_data():
    x = np.arange(-10, 10, 0.05)
    y = np.arange(-10, 10, 0.05)
    xgrid, ygrid = np.meshgrid(x, y)

    z = (np.sin(xgrid * 0.3) * np.cos(ygrid * 0.75) /
         (1 + np.abs(xgrid * ygrid) * 0.05))
    return xgrid, ygrid, z


if __name__ == '__main__':
    x, y, z = make_data()

    cs = plt.contour(x, y, z, cmap='jet')
    cs.clabel()
    plt.show()

Если на графике мы используем только один цвет, то отрицательные значения будут отображаться штриховыми линиями. Если вы хотите, чтобы и отрицательные значения отображались сплошной линией, то для этого нужно в словаре настроек matplotlib.pyplot.rcParams (или, что то же самое, matplotlib.rcParams) по ключу "contour.negative_linestyle" установить значение "solid". Более подробно про изменение настроек в Matplotlib вы можете прочитать в статье Как изменять настройки по умолчанию).

# -*- coding: utf-8 -*-

import matplotlib.pyplot as plt

import numpy as np


def make_data():
    x = np.arange(-10, 10, 0.05)
    y = np.arange(-10, 10, 0.05)
    xgrid, ygrid = np.meshgrid(x, y)

    z = (np.sin(xgrid * 0.3) * np.cos(ygrid * 0.75) /
         (1 + np.abs(xgrid * ygrid) * 0.05))
    return xgrid, ygrid, z


if __name__ == '__main__':
    x, y, z = make_data()

    plt.rcParams['contour.negative_linestyle'] = 'solid'
    cs = plt.contour(x, y, z, colors='black')
    cs.clabel()
    plt.show()

Результат будет выглядеть следующим образом:

Также можно задать толщину линий уровня, для этого предназначен параметр linewidths. Этот параметр может принимать либо значение числа с плавающей точкой (в этом случае все линии уровня будут нарисованы одной толщиной), либо список чисел с плавающей точкой (тогда толщины для линий уровня будут последовательно циклически перебираться из этого списка). В следующем примере показано, как сделать линии толщиной в два раза меньше, чем значение по умолчанию:

# -*- coding: utf-8 -*-

import matplotlib.pyplot as plt

import numpy as np


def make_data():
    x = np.arange(-10, 10, 0.05)
    y = np.arange(-10, 10, 0.05)
    xgrid, ygrid = np.meshgrid(x, y)

    z = (np.sin(xgrid * 0.3) * np.cos(ygrid * 0.75) /
         (1 + np.abs(xgrid * ygrid) * 0.05))
    return xgrid, ygrid, z


if __name__ == '__main__':
    x, y, z = make_data()

    cs = plt.contour(x, y, z, colors='black', linewidths=0.5)
    cs.clabel()
    plt.show()

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

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

# -*- coding: utf-8 -*-

import matplotlib.pyplot as plt

import numpy as np


def make_data():
    x = np.arange(-10, 10, 0.05)
    y = np.arange(-10, 10, 0.05)
    xgrid, ygrid = np.meshgrid(x, y)

    z = (np.sin(xgrid * 0.3) * np.cos(ygrid * 0.75) /
         (1 + np.abs(xgrid * ygrid) * 0.05))
    return xgrid, ygrid, z


if __name__ == '__main__':
    x, y, z = make_data()

    plt.rcParams['contour.linewidth'] = 0.5
    cs = plt.contour(x, y, z, colors='black')
    cs.clabel()
    plt.show()

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

Оформление надписей на линиях уровня

Перейдем теперь к настройке внешнего вида подписей, создаваемых с помощью функции или метода clabel().

Пожалуй, наиболее полезный из дополнительных параметров функции clabel() - это именованный параметр colors, который задает цвета для подписей линий уровня. Этот параметр работает аналогично параметру colors для функций contour() / contourf(), который был описан в предыдущем разделе. Параметр colors может быть равен None (значение по умолчанию), в этом случае метки линий уровня будут рисоваться теми же цветами, что и соответствующая линия. Параметр colors может быть равен строке, задающий цвет, в этом случае все метки будут отображаться одинаковым цветом. Также параметр colors может быть равен кортежу аргументов, описывающих цвета, в этом случае метки будут рисоваться цветами из кортежа, а цвета будут циклически сменять друг друга по очереди.

Воспользуемся параметром color и сделаем так, чтобы все метки были нарисованы черным цветом:

# -*- coding: utf-8 -*-

import matplotlib.pyplot as plt

import numpy as np


def make_data():
    x = np.arange(-10, 10, 0.05)
    y = np.arange(-10, 10, 0.05)
    xgrid, ygrid = np.meshgrid(x, y)

    z = (np.sin(xgrid * 0.3) * np.cos(ygrid * 0.75) /
         (1 + np.abs(xgrid * ygrid) * 0.05))
    return xgrid, ygrid, z


if __name__ == '__main__':
    x, y, z = make_data()

    cs = plt.contour(x, y, z)
    cs.clabel(colors='black')
    plt.show()

Того же эффекта мы бы добились, если передали параметру colors значения #000000 или просто k.

Если вам не нравится, что метки прерывают собой линии уровня, то в функцию clabel можно передать именованный параметр inline, равный False как показано в следующем примере. Теперь текст будет нанесен непосредственно поверх линий уровня.

# -*- coding: utf-8 -*-

import matplotlib.pyplot as plt

import numpy as np


def make_data():
    x = np.arange(-10, 10, 0.05)
    y = np.arange(-10, 10, 0.05)
    xgrid, ygrid = np.meshgrid(x, y)

    z = (np.sin(xgrid * 0.3) * np.cos(ygrid * 0.75) /
         (1 + np.abs(xgrid * ygrid) * 0.05))
    return xgrid, ygrid, z


if __name__ == '__main__':
    x, y, z = make_data()

    cs = plt.contour(x, y, z)
    cs.clabel(colors='black', inline=False)
    plt.show()

Результат теперь выглядит следующим образом:

Еще один полезный параметр функции clabel - это именованный параметр fmt, который задает формат чисел на графике. В простейшем случае параметр fmt может быть строкой, задающий формат вывода чисел. По умолчанию установлен вывод числа с тремя знаками после запятой, что соответствует строке "%1.3f".

Зададим параметр fmt, таким образом чтобы перед значениями было написано "z=", а числа выводились с двумя знаками после запятой.

# -*- coding: utf-8 -*-

import matplotlib.pyplot as plt

import numpy as np


def make_data():
    x = np.arange(-10, 10, 0.05)
    y = np.arange(-10, 10, 0.05)
    xgrid, ygrid = np.meshgrid(x, y)

    z = (np.sin(xgrid * 0.3) * np.cos(ygrid * 0.75) /
         (1 + np.abs(xgrid * ygrid) * 0.05))
    return xgrid, ygrid, z


if __name__ == '__main__':
    x, y, z = make_data()

    cs = plt.contour(x, y, z)
    cs.clabel(colors="black", fmt='z=%.2f')
    plt.show()

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

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

В следующем примере мы с помощью параметра fmt скрываем надпись для линии уровня, проходящей по нулю, а для других значений явно задаем метки, особо отмечая значения ±0.25 и ±0.75.

# -*- coding: utf-8 -*-

import matplotlib.pyplot as plt

import numpy as np


def make_data():
    x = np.arange(-10, 10, 0.05)
    y = np.arange(-10, 10, 0.05)
    xgrid, ygrid = np.meshgrid(x, y)

    z = (np.sin(xgrid * 0.3) * np.cos(ygrid * 0.75) /
         (1 + np.abs(xgrid * ygrid) * 0.05))
    return xgrid, ygrid, z


if __name__ == '__main__':
    x, y, z = make_data()

    fmt_dict = {0.0: '',
                -0.25: '(-0.25)',
                0.25: '(0.25)',
                -0.5: '-0.5',
                0.5: '0.5',
                -0.75: '-0.75 (!)',
                0.75: '0.75 (!)',
                }

    levels = sorted(fmt_dict.keys())
    cs = plt.contour(x, y, z, levels=levels)
    cs.clabel(colors="black", fmt=fmt_dict)
    plt.show()

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

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

Рисование линий уровня по произвольно расположенным точкам

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

def makeData():
    x = numpy.random.rand(2000) * 20.0 - 10.0
    y = numpy.random.rand(len(x)) * 20.0 - 10.0
    z = (numpy.sin(x * 0.3) * numpy.cos(y * 0.75) /
         (1 + numpy.abs(x * y) * 0.05))
    return x, y, z

В этом случае мы уже не сможем воспользоваться функцией contour(), нам понадобится другая функция - tricontour(). Самое главное, что функция tricontour() возвращает класс matplotlib.tri.tricontour.TriContourSet, который тоже является производным от класса ContourSet, а это значит, что для оформления меток мы можем использовать уже рассмотренную выше функцию clabel(), а оформление самих линий уровня подобно оформлению линий уровня для функции contour(). Работа с функцией tricontour() с настройками внешнего вида показана в следующем примере:

# -*- coding: utf-8 -*-

import matplotlib.pyplot as plt

import numpy as np


def make_data():
    x = np.random.rand(2000) * 20.0 - 10.0
    y = np.random.rand(len(x)) * 20.0 - 10.0
    z = (np.sin(x * 0.3) * np.cos(y * 0.75) /
         (1 + np.abs(x * y) * 0.05))
    return x, y, z


if __name__ == '__main__':
    x, y, z = make_data()

    fmt_dict = {0.0: '',
                -0.25: '(-0.25)',
                0.25: '(0.25)',
                -0.5: '-0.5',
                0.5: '0.5',
                -0.75: '-0.75 (!)',
                0.75: '0.75 (!)',
                }

    levels = sorted(fmt_dict.keys())
    cs = plt.tricontour(x, y, z, colors='black', levels=levels)
    cs.clabel(colors="black", fmt=fmt_dict)
    plt.show()

Рисунок выглядит несколько искаженным из-за небольшого количества точек. Если увеличить количество точек, по которым рассчитывается график в функции make_data(), с 2000 до 5000, то результат будет более сглаженным:

Заключение

Мы рассмотрели основные функции для рисования линий уровня с помощью библиотеки Matplotlib, а также научились настраивать внешний вид полученного графика. В этой статье мы рассмотрели лишь наиболее часто используемые параметры, но кроме них функции contour(), contourf(), tricontour() и clabel() имеют и другие параметры, о которых вы можете прочитать в справке.

На этом все, жду ваших комментариев.

Другие статьи про Matplotlib

Вы можете подписаться на новости сайта через RSS, Группу Вконтакте или Канал в Telegram.
5 stars

Рейтинг 4.8/5. Всего 8 голос(а, ов)




Подписаться на комментарии
Автор:
Тема:
 Ваш комментарий
 
 
Введите код 161