Практика · Графіки matplotlib¶
Лекція: lecture.html · Тест: quiz.html · ДЗ: homework.md
Наскрізний приклад той самий, що в лекції, — кавʼярня «Дві ложки»: шістдесят днів роботи й щоденний виторг. Дані тут не випадкові: вони рахуються за формулою, тому в тебе на екрані будуть точно ті самі числа, що в лекції.
Що зробимо:
- порахуємо статистики чотирьох наборів і доведемо
assert-ом, що вони збігаються до другого знака — а потім намалюємо їх і побачимо чотири різні картини; - згенеруємо дані кавʼярні й звіримо їх із числами з лекції;
- побудуємо чотири типи графіка на тих самих даних у сітці
2×2; - підпишемо осі, заголовок і легенду — і порівняємо «до» й «після»;
- подивимось, що робить
sharey=Trueі чому без нього два графіки поруч брешуть; - збережемо картинку у файл із двома різними
dpiй порівняємо розміри в байтах; - намалюємо те саме через
df.plot()і доробимо результат черезax; - зробимо пару «обрізана вісь проти чесної» й порахуємо, у скільки разів змінюється враження.
Файли зберігаємо в тимчасову теку й наприкінці прибираємо — на диску нічого не лишиться.
# %matplotlib inline вмикає малювання просто у вивід клітинки, а не в окреме вікно
%matplotlib inline
import sys
import numpy as np
import pandas as pd
import matplotlib
import matplotlib.pyplot as plt
# однаковий вигляд у всіх, хто запускає зошит: без цього розмір шрифту
# залежить від налаштувань конкретної машини
plt.rcParams["figure.figsize"] = (9, 4)
plt.rcParams["font.size"] = 11
print("Python :", sys.version.split()[0])
print("numpy :", np.__version__)
print("pandas :", pd.__version__)
print("matplotlib :", matplotlib.__version__)
print("бекенд :", matplotlib.get_backend())
Python : 3.12.2 numpy : 2.5.1 pandas : 2.3.3 matplotlib : 3.11.1 бекенд : module://matplotlib_inline.backend_inline
1 · Чотири набори з однаковими числами¶
Починаємо з того самого досліду, що й лекція. Чотири набори по одинадцять пар чисел:
x — витрати на рекламу, y — виторг. Спершу порахуємо для кожного набору звичайні
показники й не будемо дивитись на дані.
Кореляцію порахуємо двічі: спершу формулою руками, потім бібліотечною
np.corrcoef — щоб побачити, що всередині бібліотеки немає магії.
x_123 = np.array([10, 8, 13, 9, 11, 14, 6, 4, 12, 7, 5], dtype=float)
x_4 = np.array([8, 8, 8, 8, 8, 8, 8, 19, 8, 8, 8], dtype=float)
набори = {
"набір 1": (x_123, np.array([8.04, 6.95, 7.58, 8.81, 8.33, 9.96, 7.24, 4.26, 10.84, 4.82, 5.68])),
"набір 2": (x_123, np.array([9.14, 8.14, 8.74, 8.77, 9.26, 8.10, 6.13, 3.10, 9.13, 7.26, 4.74])),
"набір 3": (x_123, np.array([7.46, 6.77, 12.74, 7.11, 7.81, 8.84, 6.08, 5.39, 8.15, 6.42, 5.73])),
"набір 4": (x_4, np.array([6.58, 5.76, 7.71, 8.84, 8.47, 7.04, 5.25, 12.50, 5.56, 7.91, 6.89])),
}
def кореляція_руками(x, y):
"""Коефіцієнт Пірсона за означенням: наскільки узгоджено обидві величини
відхиляються від своїх середніх."""
відхилення_x = x - x.mean()
відхилення_y = y - y.mean()
чисельник = (відхилення_x * відхилення_y).sum()
знаменник = np.sqrt((відхилення_x ** 2).sum() * (відхилення_y ** 2).sum())
return чисельник / знаменник
рядки = []
for назва, (x, y) in набори.items():
нахил, зсув = np.polyfit(x, y, 1) # пряма, що найкраще описує звʼязок
рядки.append({
"набір": назва,
"середнє x": x.mean(),
"середнє y": y.mean(),
"відхилення y": y.std(ddof=1),
"кореляція": кореляція_руками(x, y),
"нахил": нахил,
"зсув": зсув,
})
показники = pd.DataFrame(рядки).set_index("набір").round(2)
print(показники)
середнє x середнє y відхилення y кореляція нахил зсув набір набір 1 9.0 7.5 2.03 0.82 0.5 3.0 набір 2 9.0 7.5 2.03 0.82 0.5 3.0 набір 3 9.0 7.5 2.03 0.82 0.5 3.0 набір 4 9.0 7.5 2.03 0.82 0.5 3.0
# перевірка «наша реалізація = бібліотечна»: формула руками проти np.corrcoef
for назва, (x, y) in набори.items():
наше = кореляція_руками(x, y)
бібліотечне = np.corrcoef(x, y)[0, 1]
assert np.allclose(наше, бібліотечне), f"{назва}: розрахунок розійшовся!"
print("✅ наша формула кореляції збігається з np.corrcoef для всіх чотирьох наборів")
# а тепер головне: усі чотири рядки таблиці однакові до другого знака
перший_рядок = показники.iloc[0]
for назва in показники.index[1:]:
assert (показники.loc[назва] == перший_рядок).all(), f"{назва} відрізняється від першого"
print("✅ усі чотири набори мають ОДНАКОВІ середні, відхилення, кореляцію й пряму")
print()
print("спільний рядок показників:")
print(перший_рядок)
✅ наша формула кореляції збігається з np.corrcoef для всіх чотирьох наборів ✅ усі чотири набори мають ОДНАКОВІ середні, відхилення, кореляцію й пряму спільний рядок показників: середнє x 9.00 середнє y 7.50 відхилення y 2.03 кореляція 0.82 нахил 0.50 зсув 3.00 Name: набір 1, dtype: float64
Таблиця каже: набори однакові. Тепер намалюємо їх — по одній діаграмі розсіювання на набір, з однаковим масштабом осей і однаковою прямою регресії.
fig, axes = plt.subplots(1, 4, figsize=(13, 3.4), sharey=True, constrained_layout=True)
сітка_x = np.array([2, 21]) # дві точки, щоб провести пряму через увесь графік
for ax, (назва, (x, y)) in zip(axes, набори.items()):
ax.scatter(x, y, s=42, zorder=3)
ax.plot(сітка_x, 0.5 * сітка_x + 3.0, linestyle="--", linewidth=1.4)
ax.set_title(назва)
ax.set_xlabel("витрати на рекламу")
ax.set_xlim(2, 21)
ax.set_ylim(2, 13.5)
ax.grid(True, alpha=0.3)
axes[0].set_ylabel("виторг")
fig.suptitle("Однакові числа — різні форми", fontsize=13)
plt.show()
print("Чотири однакові рядки таблиці — і чотири різні картини.")
print("Перший: шум навколо прямої. Другий: дуга. Третій: пряма з викидом.")
print("Четвертий: звʼязку немає, всю кореляцію створює єдина точка при x = 19.")
Чотири однакові рядки таблиці — і чотири різні картини. Перший: шум навколо прямої. Другий: дуга. Третій: пряма з викидом. Четвертий: звʼязку немає, всю кореляцію створює єдина точка при x = 19.
2 · Дані кавʼярні¶
Далі всюди працюємо з одним набором: щоденний виторг кавʼярні за шістдесят днів. Формула детермінована, тому числа збігаються з тими, що в лекції, — це зараз і перевіримо.
дні = np.arange(60) # 0 … 59
# псевдовипадковий, але відтворюваний шум: та сама формула, що в лекції
код = (дні * 1103515245 + 12345) % 2 ** 31
шум = код % 361 - 180 # від -180 до +180 грн
вихідний = np.isin(дні % 7, [5, 6]) # субота й неділя людніші
виторг = 3800 + 6 * дні + вихідний * 1200 + шум # +6 грн на день — повільне зростання
print("перші 10 днів :", виторг[:10])
print("усього днів :", виторг.size)
print("середній виторг:", round(виторг.mean(), 1), "грн")
print("будні / вихідні:", (~вихідний).sum(), "/", вихідний.sum())
# звірка з числами, які надруковані в лекції
assert list(виторг[:5]) == [3691, 3673, 3956, 3938, 3860], "дані розійшлися з лекцією"
assert round(виторг.mean(), 1) == 4288.7
print("✅ дані збігаються з лекцією")
перші 10 днів : [3691 3673 3956 3938 3860 5042 4964 3746 3668 4011] усього днів : 60 середній виторг: 4288.7 грн будні / вихідні: 44 / 16 ✅ дані збігаються з лекцією
# середнє по всіх днях нічого не описує, бо груп насправді дві — перевіримо це числами
середнє_буднів = виторг[~вихідний].mean()
середнє_вихідних = виторг[вихідний].mean()
print(f"середнє по всіх днях : {виторг.mean():7.1f} грн")
print(f"тільки будні : {середнє_буднів:7.1f} грн")
print(f"тільки вихідні : {середнє_вихідних:7.1f} грн")
print(f"розрив між групами : {середнє_вихідних - середнє_буднів:7.1f} грн")
# жоден день не має виторгу, близького до загального середнього ±50 грн?
близькі = np.abs(виторг - виторг.mean()) < 50
print("днів у смузі ±50 грн навколо загального середнього:", близькі.sum(), "із 60")
assert середнє_вихідних - середнє_буднів > 1000, "групи мали розійтись більш ніж на 1000 грн"
print("✅ загальне середнє лежить у порожнечі між двома групами")
середнє по всіх днях : 4288.7 грн тільки будні : 3984.8 грн тільки вихідні : 5124.4 грн розрив між групами : 1139.6 грн днів у смузі ±50 грн навколо загального середнього: 1 із 60 ✅ загальне середнє лежить у порожнечі між двома групами
3 · Чотири типи графіка на тих самих даних¶
Одні й ті самі 60 чисел, чотири виклики. Заразом це і є subplots — сітка 2×2,
де axes виявляється звичайним двовимірним масивом numpy.
fig, axes = plt.subplots(2, 2, figsize=(12, 7), constrained_layout=True)
print("тип обʼєкта axes:", type(axes).__name__, "· форма:", axes.shape)
axes[0, 0].plot(дні + 1, виторг)
axes[0, 0].set_title("ax.plot — як змінювалось у часі")
axes[0, 0].set_xlabel("день роботи")
axes[0, 0].set_ylabel("виторг, грн")
axes[0, 1].scatter(дні + 1, виторг, s=18)
axes[0, 1].set_title("ax.scatter — ті самі значення без сполучень")
axes[0, 1].set_xlabel("день роботи")
axes[0, 1].set_ylabel("виторг, грн")
axes[1, 0].bar(дні + 1, виторг)
axes[1, 0].set_title("ax.bar — 60 категорій, яких ніхто не порівнює")
axes[1, 0].set_xlabel("день роботи")
axes[1, 0].set_ylabel("виторг, грн")
кількості, межі, _ = axes[1, 1].hist(виторг, bins=12)
axes[1, 1].set_title("ax.hist — як розподілені значення")
axes[1, 1].set_xlabel("виторг, грн")
axes[1, 1].set_ylabel("днів")
for ax in axes.flat:
ax.grid(True, alpha=0.3)
plt.show()
print("скільки днів потрапило в кожну з 12 смуг:", кількості.astype(int))
тип обʼєкта axes: ndarray · форма: (2, 2)
скільки днів потрапило в кожну з 12 смуг: [ 8 10 17 9 0 0 0 0 3 10 1 2]
# гістограма показала два горби — перевіримо це числами, а не оком
порожніх_смуг = int((кількості == 0).sum())
лівий_горб = int(кількості[:6].sum())
правий_горб = int(кількості[6:].sum())
print("смуг усього :", len(кількості))
print("порожніх смуг :", порожніх_смуг)
print("днів у лівому горбі:", лівий_горб)
print("днів у правому :", правий_горб)
assert порожніх_смуг == 4, "між горбами мала бути порожнеча"
assert лівий_горб == 44 and правий_горб == 16, "горби мали вийти 44 і 16 днів"
assert лівий_горб == (~вихідний).sum(), "лівий горб — це саме будні"
print("✅ два горби — це рівно будні (44 дні) і вихідні (16 днів)")
смуг усього : 12 порожніх смуг : 4 днів у лівому горбі: 44 днів у правому : 16 ✅ два горби — це рівно будні (44 дні) і вихідні (16 днів)
4 · Підписи, заголовок, легенда¶
Той самий графік двічі: без підписів і з підписами. Порівняй, скільки часу йде на розуміння кожного.
виторг_торік = виторг - 430 # умовний ряд для порівняння — та сама форма, нижчий рівень
fig, (ax_голий, ax_підписаний) = plt.subplots(1, 2, figsize=(13, 4.2),
sharey=True, constrained_layout=True)
# ліворуч — рівно те, що виходить без жодного зайвого рядка
ax_голий.plot(дні + 1, виторг)
ax_голий.plot(дні + 1, виторг_торік)
# праворуч — ті самі два виклики плюс чотири рядки підписів
ax_підписаний.plot(дні + 1, виторг, label="2025")
ax_підписаний.plot(дні + 1, виторг_торік, label="2024")
ax_підписаний.set_xlabel("день роботи")
ax_підписаний.set_ylabel("виторг, грн")
ax_підписаний.set_title("Кавʼярня «Дві ложки»: щоденний виторг")
ax_підписаний.legend()
ax_підписаний.grid(True, alpha=0.3)
plt.show()
print("Ліворуч: дві лінії, невідомо чого і в чому. Праворуч: те саме плюс 4 рядки коду.")
print("Легенда бере назви саме з label= — без нього вона була б порожньою.")
Ліворуч: дві лінії, невідомо чого і в чому. Праворуч: те саме плюс 4 рядки коду. Легенда бере назви саме з label= — без нього вона була б порожньою.
5 · Навіщо sharey=True¶
Два графіки поруч виглядають порівнюваними навіть тоді, коли шкали в них різні. Це одна з найтихіших помилок у звітах — і найлегша для виправлення.
перша_половина = виторг[:30]
друга_половина = виторг[30:]
fig, axes = plt.subplots(2, 2, figsize=(12, 6), constrained_layout=True)
# верхній ряд: кожен графік має власну шкалу — matplotlib підбирає її сам
axes[0, 0].plot(перша_половина)
axes[0, 0].set_title("дні 1-30 · власна шкала")
axes[0, 1].plot(друга_половина)
axes[0, 1].set_title("дні 31-60 · власна шкала")
# нижній ряд: спільна шкала, задана явно однаковими межами
спільні_межі = (виторг.min() - 100, виторг.max() + 100)
axes[1, 0].plot(перша_половина)
axes[1, 0].set_title("дні 1-30 · спільна шкала")
axes[1, 0].set_ylim(спільні_межі)
axes[1, 1].plot(друга_половина)
axes[1, 1].set_title("дні 31-60 · спільна шкала")
axes[1, 1].set_ylim(спільні_межі)
for ax in axes.flat:
ax.set_ylabel("виторг, грн")
ax.grid(True, alpha=0.3)
plt.show()
межі_зверху_зліва = axes[0, 0].get_ylim()
межі_зверху_справа = axes[0, 1].get_ylim()
print("верхній ряд, межі лівого графіка :", tuple(round(m) for m in межі_зверху_зліва))
print("верхній ряд, межі правого графіка:", tuple(round(m) for m in межі_зверху_справа))
assert межі_зверху_зліва != межі_зверху_справа, "шкали мали вийти різними"
print("✅ у верхньому ряду шкали різні — однакова висота піків означає РІЗНІ гривні")
print(" у нижньому ряду шкала одна, і видно, що друга половина справді вища")
верхній ряд, межі лівого графіка : (3597, 5163) верхній ряд, межі правого графіка: (3765, 5551) ✅ у верхньому ряду шкали різні — однакова висота піків означає РІЗНІ гривні у нижньому ряду шкала одна, і видно, що друга половина справді вища
6 · Зберегти у файл: чому dpi має значення¶
Той самий аркуш зберігаємо двічі — з різною роздільністю — і дивимось на розміри в байтах. Файли кладемо у тимчасову теку, щоб нічого не лишилось на диску.
import tempfile
import pathlib
тека = pathlib.Path(tempfile.mkdtemp(prefix="dvi-lozhky-"))
print("тимчасова тека:", тека)
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(дні + 1, виторг, label="2025")
ax.set_xlabel("день роботи")
ax.set_ylabel("виторг, грн")
ax.set_title("Кавʼярня «Дві ложки»: щоденний виторг")
ax.legend()
ax.grid(True, alpha=0.3)
# ЗБЕРІГАЄМО ДО show(): у багатьох середовищах show() звільняє аркуш
for роздільність in (72, 150, 300):
ax.figure.savefig(тека / f"виторг-{роздільність}.png",
dpi=роздільність, bbox_inches="tight")
plt.show()
for файл in sorted(тека.glob("*.png")):
розмір = файл.stat().st_size
print(f"{файл.name:<18} {розмір:>8} байтів")
тимчасова тека: /tmp/dvi-lozhky-t6k7_7d4
виторг-150.png 78219 байтів виторг-300.png 173168 байтів виторг-72.png 33936 байтів
from PIL import Image # іде в комплекті з matplotlib; потрібна лише щоб зміряти пікселі
for роздільність in (72, 150, 300):
з_картинкою = Image.open(тека / f"виторг-{роздільність}.png")
ширина, висота = з_картинкою.size
очікувана_ширина = 8 * роздільність # figsize=(8, 4) помножений на dpi
print(f"dpi={роздільність:>3}: {ширина}×{висота} пікселів "
f"(figsize×dpi дало б {очікувана_ширина}; bbox_inches='tight' зрізав поля)")
# bbox_inches='tight' трохи підрізає поля, тому допускаємо відхилення
assert abs(ширина - очікувана_ширина) < 0.2 * очікувана_ширина
малий = (тека / "виторг-72.png").stat().st_size
великий = (тека / "виторг-300.png").stat().st_size
print()
print(f"файл на 300 dpi важчий за файл на 72 dpi у {великий / малий:.1f} раза")
assert великий > малий, "більша роздільність має давати більший файл"
print("✅ розмір у пікселях = figsize × dpi, і платимо ми за це байтами")
dpi= 72: 510×285 пікселів (figsize×dpi дало б 576; bbox_inches='tight' зрізав поля) dpi=150: 1065×594 пікселів (figsize×dpi дало б 1200; bbox_inches='tight' зрізав поля) dpi=300: 2127×1190 пікселів (figsize×dpi дало б 2400; bbox_inches='tight' зрізав поля) файл на 300 dpi важчий за файл на 72 dpi у 5.1 раза ✅ розмір у пікселях = figsize × dpi, і платимо ми за це байтами
7 · Те саме через df.plot()¶
pandas сам бере підписи з таблиці. Спершу найкоротший варіант, потім — той самий
виклик, але в готову область ax, яку ми доробляємо руками.
продажі = pd.DataFrame({
"день": дні + 1,
"виторг": виторг,
"вихідний": вихідний,
})
print(продажі.head())
print()
# найкоротший спосіб подивитись на дані під час роботи
осі_від_pandas = продажі.plot(x="день", y="виторг", figsize=(9, 3.6))
plt.show()
print("що повернув df.plot():", type(осі_від_pandas).__name__)
assert isinstance(осі_від_pandas, matplotlib.axes.Axes), "df.plot має повертати Axes"
print("✅ це той самий Axes — отже, усі методи ax.* до нього теж застосовні")
день виторг вихідний 0 1 3691 False 1 2 3673 False 2 3 3956 False 3 4 3938 False 4 5 3860 False
що повернув df.plot(): Axes ✅ це той самий Axes — отже, усі методи ax.* до нього теж застосовні
# групування з теми 34 плюс графік: середній виторг окремо для буднів і вихідних
середні_по_групах = продажі.groupby("вихідний")["виторг"].mean().round(1)
середні_по_групах.index = ["будній день", "вихідний"]
print(середні_по_групах)
print()
fig, ax = plt.subplots(figsize=(9, 3.8))
# pandas малює в НАШУ область — тому далі ми можемо все доналаштувати звичайними методами
продажі.plot(x="день", y="виторг", ax=ax, label="щоденний виторг")
ax.axhline(середні_по_групах["будній день"], linestyle="--", linewidth=1.2,
label=f"середнє по буднях ({середні_по_групах['будній день']:.0f} грн)")
ax.axhline(середні_по_групах["вихідний"], linestyle=":", linewidth=1.4,
label=f"середнє по вихідних ({середні_по_групах['вихідний']:.0f} грн)")
ax.set_xlabel("день роботи")
ax.set_ylabel("виторг, грн")
ax.set_title("Дві прямі, між якими не лежить майже жоден день")
ax.legend(loc="upper left", fontsize=9)
ax.grid(True, alpha=0.3)
plt.show()
print("df.plot() — для розвідки; fig, ax — коли картинку хтось побачить.")
будній день 3984.8 вихідний 5124.4 Name: виторг, dtype: float64
df.plot() — для розвідки; fig, ax — коли картинку хтось побачить.
8 · Обрізана вісь: та сама різниця, два висновки¶
Середній чек кавʼярні по кварталах — 96, 98, 101 і 104 гривні. Намалюємо ті самі чотири числа двічі й порахуємо, у скільки разів змінюється враження.
квартали = ["Q1", "Q2", "Q3", "Q4"]
чек = np.array([96, 98, 101, 104], dtype=float)
справжнє_зростання = (чек[-1] - чек[0]) / чек[0] * 100
нижня_межа = 95
висота_Q1 = чек[0] - нижня_межа # довжина стовпчика на обрізаному графіку
висота_Q4 = чек[-1] - нижня_межа
у_скільки_разів_вищий = висота_Q4 / висота_Q1
fig, (ax_обрізаний, ax_чесний) = plt.subplots(1, 2, figsize=(12, 4), constrained_layout=True)
ax_обрізаний.bar(квартали, чек)
ax_обрізаний.set_ylim(нижня_межа, 107) # ось вона, єдина відмінність
ax_обрізаний.set_title(f"вісь від {нижня_межа} — «зростання вдевʼятеро»")
ax_обрізаний.set_ylabel("середній чек, грн")
ax_чесний.bar(квартали, чек)
ax_чесний.set_ylim(0, 115) # запас угорі, щоб підпис 104 не обрізався
ax_чесний.set_title("вісь від нуля — справжні +8.3 %")
ax_чесний.set_ylabel("середній чек, грн")
for ax in (ax_обрізаний, ax_чесний):
for квартал, значення in zip(квартали, чек):
ax.text(квартал, значення, f"{значення:.0f}", ha="center", va="bottom")
plt.show()
print(f"справжнє зростання за рік : {справжнє_зростання:.1f} %")
print(f"довжини стовпчиків у одиницях осі при межі {нижня_межа}: "
f"Q1 = {висота_Q1:.0f}, Q4 = {висота_Q4:.0f}")
print(f"на екрані Q4 вищий за Q1 : у {у_скільки_разів_вищий:.1f} раза")
print(f"перебільшення : у {(висота_Q4 / висота_Q1 - 1) * 100 / справжнє_зростання:.0f} разів")
assert round(справжнє_зростання, 1) == 8.3, "зростання мало вийти 8.3 %"
assert round(у_скільки_разів_вищий, 1) == 9.0, "на обрізаній осі Q4 мав стати вдевʼятеро вищим"
print("✅ ті самі чотири числа: +8.3 % у даних і «вдевʼятеро» на екрані")
справжнє зростання за рік : 8.3 % довжини стовпчиків у одиницях осі при межі 95: Q1 = 1, Q4 = 9 на екрані Q4 вищий за Q1 : у 9.0 раза перебільшення : у 96 разів ✅ ті самі чотири числа: +8.3 % у даних і «вдевʼятеро» на екрані
9 · Прибираємо за собою¶
Тимчасова тека більше не потрібна — видаляємо її разом із картинками.
import shutil
файлів_було = len(list(тека.glob("*")))
shutil.rmtree(тека)
print("видалено файлів:", файлів_було)
print("тека існує :", тека.exists())
assert not тека.exists(), "тимчасова тека мала зникнути"
print("✅ на диску нічого не лишилось")
видалено файлів: 3 тека існує : False ✅ на диску нічого не лишилось
Завдання¶
🟢 Рівень 1 — База¶
Візьми масив виторг і побудуй один графік, у якого є все: заголовок, підпис осі x,
підпис осі y з одиницями, легенда й напівпрозора сітка. Додай другою лінією
виторг_торік — і збережи результат у файл із dpi=200.
Зроблено, якщо: файл створився, ax.get_ylabel() повертає непорожній рядок,
а len(ax.get_legend().get_texts()) == 2.
🟡 Рівень 2 — Плюс¶
Порахуй середній виторг по днях тижня (дні % 7) і покажи його стовпчиками, а
поруч, у другій області тієї самої сітки, — гістограму всього виторгу.
Зроблено, якщо: у стовпчиках видно, що два дні тижня помітно вищі за решту, і
assert підтверджує, що це саме дні з індексами 5 і 6.
🔴 Рівень 3 — Виклик¶
Знайди третій спосіб збрехати графіком, якого не було в лекції (наприклад,
подвійна вісь y з двома різними шкалами або пропущені дні на осі часу), намалюй
пару «оманливо / чесно» й порахуй числом, у скільки разів відрізняється враження.
Зроблено, якщо: обидва графіки побудовані з одного й того самого масиву даних,
а різниця вражень підтверджена assert-ом на конкретне число.
Повні умови з критеріями — у homework.md.