Честни графики
Накратко: Подвеждащата графика показва вярна таблица с грешно впечатление (отрязана ос, две оси, 3D); честната започва стълбовете от нула и казва извода в заглавието.
Проблем
„Септември се срина“
Колега праща графика за срещата с управителя: оборотът за последните четири месеца. Септемврийският стълб почти го няма. Изглежда като катастрофа.
Септември наистина е по-слаб: 7 128,80 € срещу 8 812,70 € през август. Защо, ще разберем в проекта в края на курса. Тук въпросът е друг: колко по-слаб изглежда на графиката и колко е всъщност?
Идеята
Стълбовете започват от нула
Окото чете стълба по дължината му: два пъти по-дълъг стълб значи два пъти повече. Затова дължината трябва да е самото число. Графиката на колегата започва от 7 000 (set_ylim(7000, 10000)), а така се казва отрязана ос. Ето същите числа с ос от 7 000 и с ос от 0:
Истинският спад от август към септември е 19,1%. Вляво септемврийският стълб е с 92,9% по-къс от августовския: спадът изглежда 4,9 пъти по-голям. Отрязаната ос маха долните 7 000 €, които двата месеца имат общо, и оставя само разликата.
matplotlib сам започва стълбовете от 0. Отрязаната ос идва от set_ylim или от инструменти, които сами „приближават“ оста. Ако разликата между стълбовете не се вижда от 0, тя е малка. Това също е отговор.
По-точно, за любопитните
Линията е друго: тя се чете по наклона и по това кога се качва и слиза, не по дължина. Оста на линия може да не започва от 0, ако е надписана. Линия, която се мени с няколко процента, на ос от 0 е почти права и промяната не се вижда.
Пробвай
Поправи графиката
Някой е начертал оборота по месеци с ос от 7 000. Поправи графиката: 12 стълба с оборота на всеки месец (monthly, без отказаните поръчки), а оста y да започва от 0 и да стига над декември.
Решение
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
orders = pd.read_csv('year_orders.csv', parse_dates=['ordered_at'])
items = pd.read_csv('year_items.csv')
items['revenue'] = items['quantity'] * items['unit_price']
per_order = items.groupby('order_id', as_index=False)['revenue'].sum()
orders = orders.merge(per_order, left_on='id', right_on='order_id', how='left', validate='one_to_one')
# Оборотът е без отказаните поръчки.
sold = orders[orders['status'] != 'cancelled']
monthly = sold.set_index('ordered_at')['revenue'].resample('MS').sum()
months = monthly.index.strftime('%Y-%m')
fig, ax = plt.subplots(figsize=(9, 4))
ax.bar(months, monthly)
ax.set_ylim(0, 17000)
ax.set_title('Оборот по месеци, €')
ax.tick_params(axis='x', rotation=90)
print(monthly.min().round(2), monthly.max().round(2))ax.bar(months, monthly) с ax.set_ylim(0, 17000) показва най-слабия месец (7 128,80 €) малко под половината от най-силния (15 915,90 €), какъвто е. С ос от 7 000 слабите месеци почти изчезват. Ако рисуваш оборота от orders, в стълбовете влизат и отказаните поръчки.
Идеята
Две оси y
Оборотът е в хиляди евро, а поръчките са стотици. За да ги сложат на една графика, често дават на втората линия своя ос вдясно: ax.twinx() прави нова ос y, която дели оста x с първата. Мащабът на всяка ос обаче се избира отделно и всяка линия се разтяга да запълни своята. Така две линии могат да изглеждат слети или разделени, както поиска авторът.
Честната алтернатива е индекс: дели всеки месец на първия и умножи по 100. by_month / by_month.iloc[0] * 100 го прави за двете колони наведнъж (broadcasting по колоните). Тогава и двете линии са на една ос и започват от 100:
Вляво линиите почти съвпадат и изглежда, че оборотът и поръчките вървят заедно. Вдясно се вижда разликата: през декември оборотът е 171,2, а поръчките 136,7. Оборотът е растял много повече от поръчките, защото декемврийските поръчки са и по-големи. Видя го и в кутиите с мустаци в предишния урок.
Друг честен начин са две графики една под друга, всяка със своята ос и надпис: plt.subplots(2, 1, sharex=True).
Идеята
3D, кръгови диаграми и картинки
Три неща, които изглеждат впечатляващо и подвеждат:
- 3D стълбове и 3D кръгова диаграма. Перспективата прави близкото по-голямо от далечното. Третото измерение не носи никакви данни, само изкривява.
- Кръгова диаграма. Окото сравнява ъгли и площи много по-лошо от дължини. Видя го в урока за избора на графика: подредени стълбове казват същото по-ясно.
- Стойност в размера на картинка. Ако картинката расте и по височина, и по ширина, площта ѝ расте с квадрата: два пъти повече изглежда четири пъти повече.
Идеята
Цвят за отговора
Цветът не е украса: той казва къде да гледаш. Един цвят за важното и сиво за останалото е най-ясното. Ето оборота по месеци два пъти: вляво всички стълбове са еднакви, вдясно декември е оцветен:
np.where(monthly.index.month == 12, '#E69F00', 'lightgray') дава по един цвят за всеки месец: оранжево за декември, сиво за другите. color=colors ги слага на стълбовете.
Заглавието вдясно също работи: не „Оборот по месеци“, а изводът: „Декември носи 14% от оборота на годината“ (точно 14,3%). Читателят разбира, преди да е погледнал осите.
Оранжевото #E69F00 е от палитрата Okabe-Ito: осем цвята, които се различават и от хора, които не виждат някои цветове. Много хора не различават червено от зелено. Затова никога не разделяй две неща само с червено и зелено. Ако ти трябват повече цветове, палитрата tab10 на matplotlib (цветовете по подразбиране, C0 до C9) също е добро начало.
Идеята
Надписи вместо легенда
Шест линии в шест цвята и легенда в ъгъла: окото скача между линията и легендата, за да разбере коя коя е. Надписът до самата линия спестява това. Ето оборота по категории, вляво с легенда, вдясно с надписи:
ax.text(x, y, 'Кухня') пише текст в точката (x, y) от данните. Месеците са текст и matplotlib ги слага на позиции 0, 1, 2…, затова len(table) - 0.8 е точно вдясно от последния месец, а 1.3 е до декември. Подписът на кухнята е в синия цвят на линията ѝ, а кухнята е начертана последна и по-дебела, за да е отгоре.
През декември кухнята продава за 3 240,60 €, 4,4 пъти повече от средния си месец през останалата година. Продават се най-вече тигани и ножове, вероятно за подарък. На графиката вляво това е една от шест линии, а вдясно е първото, което виждаш. Подаръчната карта няма категория, затова я няма тук.
Пробвай
Цветове
Упражнение
Графиката за годишния отчет
Управителят иска една графика за годишния отчет: оборотът по месеци (monthly), декември в цвят, другите месеци в сиво, оста от 0 и заглавие, което казва колко от оборота на годината носи декември. Запиши дела в share, а цветовете в colors.
Решение
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
orders = pd.read_csv('year_orders.csv', parse_dates=['ordered_at'])
items = pd.read_csv('year_items.csv')
items['revenue'] = items['quantity'] * items['unit_price']
per_order = items.groupby('order_id', as_index=False)['revenue'].sum()
orders = orders.merge(per_order, left_on='id', right_on='order_id', how='left', validate='one_to_one')
# Оборотът е без отказаните поръчки.
sold = orders[orders['status'] != 'cancelled']
monthly = sold.set_index('ordered_at')['revenue'].resample('MS').sum()
months = monthly.index.strftime('%Y-%m')
share = monthly['2025-12-01'] / monthly.sum() * 100
colors = np.where(monthly.index.month == 12, '#E69F00', 'lightgray')
fig, ax = plt.subplots(figsize=(9, 4))
ax.bar(months, monthly, color=colors)
ax.set_ylim(0, 17000)
ax.set_title(f'Декември носи {share:.0f}% от оборота на годината')
ax.tick_params(axis='x', rotation=90)Декември носи 14,3% от оборота на годината. Ако всеки месец беше еднакъв, щеше да е 1/12, около 8,3%. np.where(monthly.index.month == 12, "#E69F00", "lightgray") оцветява само декември, а f"Декември носи {share:.0f}% от оборота на годината" слага отговора в заглавието. Заглавие „Оборот по месеци“ казва само какво има на осите, а еднаквите стълбове оставят читателя сам да търси.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Широко и дълго: pivot_table и melt · Дати и времеви редове · Първата графика · Коя графика за какъв въпрос
- Продължава в: Защо оборотът падна?
- Задачи по темата: Графики