Линейные графики и área

Линия — это история во времени. Наклон рассказывает о росте, падении и переломах.

«Линейный график подразумевает непрерывность. Используйте его только там, где между точками есть осмысленный переход».

Линейный график показывает, как величина меняется вдоль упорядоченной оси — чаще всего времени. Он идеален, когда точек много (десятки и сотни) и важен тренд, а не отдельные значения. Несколько линий на одной фигуре позволяют сравнить динамику серий.

В реальной аналитике линия — рабочая лошадка дашбордов: выручка по дням, посещаемость по неделям, температура сервера по минутам, цена акции по тикам. Сила формы в том, что человеческий глаз мгновенно считывает наклон как скорость изменения, а перегиб — как событие: запуск кампании, релиз, авария. Именно поэтому линию ставят туда, где важен ответ «куда движется метрика и когда что-то сломалось», а не «сколько именно было в конкретный вторник». Если же стоит вопрос точного значения отдельной точки, линию дополняют маркерами или вообще переходят к таблице.

Важно различать непрерывную и дискретную ось. Линия честна, когда между соседними точками есть осмысленный переход: температура между двумя замерами действительно где-то была, выручка перетекала изо дня в день. Если же по оси X стоят несвязанные сущности — города, отделы, продукты — соединять их линией нельзя: вы рисуете «переход», которого не существует. Это первый признак того, что линию надо заменить на bar. Ещё одна тонкость — неравномерный шаг по времени: если точки сняты в произвольные моменты, обязательно используйте datetime-ось, а не порядковый индекс, иначе три дня простоя сожмутся до одного сегмента и тренд исказится.

fig, ax = plt.subplots(figsize=(9, 4))

ax.plot(days, users_a, label="Продукт A")
ax.plot(days, users_b, label="Продукт B", linestyle="--")

# заполненная область (area) для накопления/объёма
ax.fill_between(days, users_a, alpha=0.2)

ax.set_xlabel("День")
ax.set_ylabel("Активные пользователи")
ax.legend()
ax.grid(True, alpha=0.3)

Как работает под капотом

Линия — это последовательность сегментов между соседними точками. Matplotlib просто соединяет (x[i], y[i]) с (x[i+1], y[i+1]). Поэтому порядок точек критичен: если перемешать данные, линия превратится в «зигзаг-спагетти». Area (fill_between) заливает площадь между линией и базой — она хороша для накопленных величин, но плоха для сравнения нескольких перекрывающихся серий.

Под капотом ax.plot создаёт объект Line2D, который хранит массивы x и y и стиль (цвет, толщина, тип штриха, маркеры). Matplotlib не «понимает» смысл данных — он буквально протягивает прямые отрезки между заданными координатами, а кривизны добивается лишь за счёт большого числа точек. Отсюда вытекает важное следствие: пропуски в данных лучше кодировать значением NaN (или None), а не выкидывать строку — тогда Matplotlib честно оставит разрыв в линии вместо того, чтобы провести через пропуск прямую и нарисовать тренд, которого не было. Параметр linestyle и маркеры стоит использовать как второй канал кодирования: когда график печатают в чёрно-белом виде, серии должны различаться не только цветом, иначе они сольются.

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

Шумные ряды часто сглаживают скользящим средним. Посчитаем его вручную.

# Скользящее среднее (moving average) — сглаживание шумного ряда
series = [10, 14, 9, 16, 22, 18, 25, 19, 30, 28, 35, 33]
window = 3

ma = []
for i in range(len(series)):
    if i < window - 1:
        ma.append(None)            # окно ещё не заполнено
    else:
        chunk = series[i - window + 1:i + 1]
        ma.append(sum(chunk) / window)

for i, (raw, m) in enumerate(zip(series, ma)):
    label = "{:.1f}".format(m) if m is not None else "  -"
    print("t={:>2}  raw={:>3}  MA={}".format(i, raw, label))

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

Частые ошибки

Линия по категориям без порядка — ложный тренд. Слишком много линий («спагетти-график») — выделите 1–2 цветом, остальные приглушите. Несортированная ось X — зигзаги. Площадь (area) для нескольких перекрывающихся серий — нижние не видны. Разрывы в данных, нарисованные как сплошная линия.

Ещё несколько типичных промахов. Двойная ось Y (twinx) с двумя линиями в разных масштабах — частый источник манипуляций: подбором диапазонов можно заставить любые две метрики выглядеть синхронными, поэтому к двойной оси относятся с подозрением и подписывают обе шкалы. Неравномерные интервалы по времени, отрисованные как равные сегменты, сжимают и растягивают тренд — пользуйтесь datetime-осью. Интерполяция между редкими замерами создаёт иллюзию знания: если вы измеряли раз в месяц, не делайте вид, будто между точками шло плавное движение. Наконец, слишком плотная сетка и яркий фон отвлекают от самих линий — данные должны быть самым контрастным элементом фигуры, а вспомогательная разметка — приглушённой.

Best practices

  • Ось X — строго упорядочена (время или последовательность).
  • Не более 3–4 линий; выделяйте главную, приглушайте фон.
  • Для динамики ось Y не обязана начинаться с нуля (в отличие от bar), но подпишите масштаб.
  • Разрывы данных показывайте как разрывы, не достраивайте.

Итог: линия — про тренды. Теперь перейдём к scatter — графику связи двух переменных.

Проверьте себя
1. Когда линейный график уместен?
AДля сравнения несвязанных категорий
BДля упорядоченной оси, например времени
CДля долей целого
DВсегда вместо bar
2. Чем плох «спагетти-график» из множества линий?
AОн точнее любого другого
BЛинии сливаются и сравнение становится невозможным
CMatplotlib его не строит
DОн всегда требует area