Гистограмма с наклонными линиями вместо горизонтальных линий

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

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

Пример 1

Пример 2

Код:

    import pandas as pd
    from pandas import Timestamp
    import datetime
    import matplotlib.pyplot as plt
    import numpy as np  # np.nan

    dd = {'Name': {0: 'A', 1: 'B', 2: 'C'}, 'Start': {0: Timestamp('1800-01-01 00:00:00'), 1: Timestamp('1850-01-01 00:00:00'), 2: Timestamp('1950-01-01 00:00:00')}, 'End': {0: Timestamp('1849-12-31 00:00:00'), 1: Timestamp('1949-12-31 00:00:00'), 2: Timestamp('1979-12-31 00:00:00')}, 'Team': {0: 'Red', 1: 'Blue', 2: 'Red'}, 'Duration': {0: 50*365-1, 1: 100*365-1, 2: 30*365-1}, 'First': {0: 5, 1: 10, 2: 8}, 'Last': {0: 10, 1: 8, 2: 12}}
    d = pd.DataFrame.from_dict(dd)
    d.dtypes
    d

    # set up colors for team
    colors = {'Red': '#E81B23', 'Blue': '#00AEF3'}

    # reshape data to get a single Date | is there a better way?
    def reshape(data):
            d1 = data[['Start', 'Name', 'Team', 'Duration', 'First']].rename(columns = {'Start': 'Date', 'First': 'value'})
            d2 = data[['End', 'Name', 'Team', 'Duration', 'Last']].rename(columns = {'End': 'Date', 'Last': 'value'})
            return pd.concat([d1, d2]).sort_values(by='Date').reset_index(drop=True)
    df = reshape(d)
    df.dtypes
    df

    plt.plot(df['Date'], df['value'], color='black')
    plt.bar(d['Start'], height=d['Last'], align='edge', 
            width=list(+d['Duration']), 
            edgecolor='white', linewidth=2,
            color=[colors[key] for key in d['Team']])
    plt.show()

    plt.plot(df['Date'], df['value'], color='black')
    plt.bar(d['End'], height=d['First'], align='edge', 
            width=list(-d['Duration']), 
            edgecolor='white', linewidth=2,
            color=[colors[key] for key in d['Team']])
    plt.show()
            

Похоже, вам просто нужен линейный график с закрашенным пространством под ним? stackoverflow.com/questions/16917919/…

Grismar 03.09.2024 04:09

Да, это звучит правильно! Но с раскраской, основанной на переменной «Команда». Позвольте мне прочитать сообщение, на которое вы ссылаетесь.

PatrickT 03.09.2024 04:14

Не по теме, но я очень рад видеть сегодня хорошо написанный вопрос по SO.

alko 03.09.2024 15:12

Спасибо @alko. Подавляющее большинство вопросов, которые я планирую опубликовать на SO, решаются, когда я готовлю минимальный пример, поэтому дополнительные усилия определенно стоят того! :-)

PatrickT 03.09.2024 17:50
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
1
4
74
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Я бы использовал следующий подход:

  1. Создайте обычный столбчатый график, используя для каждого столбца максимальную протяженность двух значений высоты (это позволит автоматически установить правильные пределы оси Y).
  2. Преобразуйте все столбцы гистограммы, которые являются экземплярами matplotlib.patches.Rectangle, в экземпляры matplotlib.patches.Polygon, отрегулируйте необходимые углы и скопируйте все остальные атрибуты (цвет, ширину линии и т. д.).
  3. На графике замените прямоугольные полосы многоугольными.

Функция polybar() в следующем коде достигает этого (она также позволяет передавать **kwargs в plt.bar():

from matplotlib.patches import Polygon
import matplotlib.pyplot as plt
import numpy as np

def polybar(x, y_left, y_right, **kwargs):
    
    def poly_from(rect, yl, yr):
        (x, y), w = rect.get_xy(), rect.get_width()
        p = Polygon([(x, y), (x + w, y), (x + w, yr), (x, yl)], closed=True)
        p.update_from(rect)  # Copy over properties from rectangle
        return p
    
    ax = plt.gca()
    # Create regular bar plot with maximum y extent
    height = np.where(np.abs(y_left) > np.abs(y_right), y_left, y_right)
    bars = ax.bar(x, height, **kwargs)
    ylim = ax.get_ylim()
    # Convert rectangle bars to polygon bars
    polys = [poly_from(*blr) for blr in zip(bars, y_left, y_right)]
    # Replace rectangle bars with polygon bars
    for bar in bars:
        bar.remove()
    for poly in polys:
        ax.add_patch(poly)
    ax.set_ylim(ylim)

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

# TODO: Prepend imports and `polybar()` from above here

import pandas as pd
from pandas import Timestamp

dd = {'Name': {0: 'A', 1: 'B', 2: 'C'}, 'Start': {0: Timestamp('1800-01-01 00:00:00'), 1: Timestamp('1850-01-01 00:00:00'), 2: Timestamp('1950-01-01 00:00:00')}, 'End': {0: Timestamp('1849-12-31 00:00:00'), 1: Timestamp('1949-12-31 00:00:00'), 2: Timestamp('1979-12-31 00:00:00')}, 'Team': {0: 'Red', 1: 'Blue', 2: 'Red'}, 'Duration': {0: 50*365-1, 1: 100*365-1, 2: 30*365-1}, 'First': {0: 5, 1: 10, 2: 8}, 'Last': {0: 10, 1: 8, 2: 12}}
d = pd.DataFrame.from_dict(dd)
colors = {'Red': '#E81B23', 'Blue': '#00AEF3'}
def reshape(data):
        d1 = data[['Start', 'Name', 'Team', 'Duration', 'First']].rename(columns = {'Start': 'Date', 'First': 'value'})
        d2 = data[['End', 'Name', 'Team', 'Duration', 'Last']].rename(columns = {'End': 'Date', 'Last': 'value'})
        return pd.concat([d1, d2]).sort_values(by='Date').reset_index(drop=True)
df = reshape(d)

polybar(d['Start'], d['First'], d['Last'], align='edge', 
        width=list(+d['Duration']), 
        edgecolor='white', linewidth=2,
        color=[colors[key] for key in d['Team']])
plt.show()

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

Некоторые крайние случаи в настоящее время не рассматриваются. Это те, о которых я знаю:

  • Преобразование предполагает, что все значения bottom гистограммы равны нулям. Для обработки других значений необходимо настроить преобразование прямоугольника в многоугольник.
  • Установка пределов y некорректна в том случае, если для одного и того же бара y_left является общим наибольшим отрицательным значением, а y_right является общим наибольшим положительным значением (или наоборот).
  • Я не проверял, работает ли код с единицами.

Спасибо @simon, это выглядит великолепно! Я ценю подробные объяснения. Прежде чем выбрать ответ, я немного поэкспериментирую с вашим кодом.

PatrickT 03.09.2024 17:34

@PatrickT Не беспокойся! Выбирайте тот ответ, который вам больше всего подходит :)

simon 03.09.2024 17:46
Ответ принят как подходящий

Вы можете использовать Matplotlibs Axes.fill_between для создания таких типов диаграмм. Важно отметить, что это будет точно отражать разрыв между вашими рядами там, где они существуют, тогда как подход с полосами будет сделайте этот разрыв шире, чем он есть на самом деле, если вы не установите edgewidth баров до 0.

Кроме того, для преобразования данных это pandas.lreshape что аналогично выполнению нескольких операций плавления одновременно.

import pandas as pd
from pandas import Timestamp
import matplotlib.pyplot as plt

dd = pd.DataFrame({
    'Name':     ['A', 'B', 'C'],
    'Start':    pd.to_datetime(['1800-01-01', '1850-01-01', '1950-01-01']),
    'End':      pd.to_datetime(['1849-12-31', '1949-12-31', '1979-12-31']),
    'Team':     ['Red', 'Blue', 'Red'],
    'Duration': [50*365-1, 100*365-1, 30*365-1],
    'First':    [5, 10, 8],
    'Last':     [10, 8, 12]
})
df = (
    pd.lreshape(dd, groups = {'Date': ['Start', 'End'], 'Value': ['First', 'Last']})
    .sort_values('Date')
)
colors = {'Red': '#E81B23', 'Blue': '#00AEF3'}


fig, ax = plt.subplots()
for team in df['Team'].unique():
    ax.fill_between(
        df['Date'],
        df['Value'],
        where=(df['Team'] == team),
        color=colors[team],
        linewidth=0,
    )
ax.set_ylim(bottom=0)

plt.show()

Отличный ответ, спасибо Кэмерон. Позвольте мне поиграть с кодом, прежде чем выбирать ответ! Я люблю pd.lreshape, я не знал об этом (я знаю только pd.merge, pd.join, pd.concat и pd.groupby().agg(), так приятно это знать! Я добавил .reindex(['Date', 'Name', 'Team', 'Value'], axis=1), чтобы получить конкретный заказ, и удалил столбец Duration, так как он больше не нужен. Быстрый вопрос: я знаю df.to_dict() копировать фреймы данных, а также df.to_clipboard() печатать, но я вижу, что у вас есть df=pd.DataFrame({...}), вы делали это «вручную» или знаете удобный способ? Еще раз спасибо!

PatrickT 03.09.2024 18:08

Рада, что pd.lreshape для вас новинка и пригодится! Для «совместного использования» копируемых/вставляемых версий DataFrame вы можете сделать df.to_dict('list'), чтобы получить представление, подобное тому, которое я использовал в качестве входных данных. Затем используйте pd.DataFrame, чтобы снова прочитать эти значения. Существует множество других форматов, которые вы можете получить с помощью метода to_dict: pandas.pydata.org/docs/reference/api/…

Cameron Riddell 03.09.2024 19:17

В итоге я сделал это: [1]: i.sstatic.net/M6zgKawp.png [2]: i.sstatic.net/cwGRTUgY.png

PatrickT 04.09.2024 03:12

Мне пришлось столкнуться с парой незначительных неприятностей: (1) в моих данных последовательные даты начала и окончания были одинаковыми, что испортило индекс при использовании pd.lreshape; Вероятно, это можно было исправить, разобравшись с секундами, но я перешел в режим кувалды с d['End'] = d['End'] - pd.Timedelta(1, unit='D'); (2) когда дни начала/окончания не совпадали с фактическими данными, я выполнил интерполяцию, в результате чего произошел небольшой скачок в дате перехода, вызванный использованием первого дня месяца по сравнению с последним днем ​​месяца; исправил это с помощью уродливого хака d.loc[d.index > 0, 'First'] = d1[col].shift(+1).

PatrickT 04.09.2024 03:21

В любом случае, пишу это здесь больше всего для себя в будущем! Еще раз спасибо Кэмерон. Отличный ответ.

PatrickT 04.09.2024 03:21

Другие вопросы по теме