Я хочу отобразить гистограмму на холсте временного ряда, где ширина полос соответствует продолжительности, а края соединяют первое значение с последним значением. Другими словами, как я мог наклонить столбцы вверху, чтобы они соответствовали данным?
Я знаю, как создавать гистограммы, используя либо последнее значение (пример 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()
Да, это звучит правильно! Но с раскраской, основанной на переменной «Команда». Позвольте мне прочитать сообщение, на которое вы ссылаетесь.
Не по теме, но я очень рад видеть сегодня хорошо написанный вопрос по SO.
Спасибо @alko. Подавляющее большинство вопросов, которые я планирую опубликовать на SO, решаются, когда я готовлю минимальный пример, поэтому дополнительные усилия определенно стоят того! :-)
Я бы использовал следующий подход:
matplotlib.patches.Rectangle
, в экземпляры matplotlib.patches.Polygon
, отрегулируйте необходимые углы и скопируйте все остальные атрибуты (цвет, ширину линии и т. д.).Функция 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_left
является общим наибольшим отрицательным значением, а y_right
является общим наибольшим положительным значением (или наоборот).Спасибо @simon, это выглядит великолепно! Я ценю подробные объяснения. Прежде чем выбрать ответ, я немного поэкспериментирую с вашим кодом.
@PatrickT Не беспокойся! Выбирайте тот ответ, который вам больше всего подходит :)
Вы можете использовать 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({...})
, вы делали это «вручную» или знаете удобный способ? Еще раз спасибо!
Рада, что pd.lreshape
для вас новинка и пригодится! Для «совместного использования» копируемых/вставляемых версий DataFrame вы можете сделать df.to_dict('list')
, чтобы получить представление, подобное тому, которое я использовал в качестве входных данных. Затем используйте pd.DataFrame
, чтобы снова прочитать эти значения. Существует множество других форматов, которые вы можете получить с помощью метода to_dict
: pandas.pydata.org/docs/reference/api/…
В итоге я сделал это: [1]: i.sstatic.net/M6zgKawp.png [2]: i.sstatic.net/cwGRTUgY.png
Мне пришлось столкнуться с парой незначительных неприятностей: (1) в моих данных последовательные даты начала и окончания были одинаковыми, что испортило индекс при использовании pd.lreshape
; Вероятно, это можно было исправить, разобравшись с секундами, но я перешел в режим кувалды с d['End'] = d['End'] - pd.Timedelta(1, unit='D')
; (2) когда дни начала/окончания не совпадали с фактическими данными, я выполнил интерполяцию, в результате чего произошел небольшой скачок в дате перехода, вызванный использованием первого дня месяца по сравнению с последним днем месяца; исправил это с помощью уродливого хака d.loc[d.index > 0, 'First'] = d1[col].shift(+1)
.
В любом случае, пишу это здесь больше всего для себя в будущем! Еще раз спасибо Кэмерон. Отличный ответ.
Похоже, вам просто нужен линейный график с закрашенным пространством под ним? stackoverflow.com/questions/16917919/…