Fairmas: відмінності між версіями
(Створена сторінка: FAIRMAS REPORTS Обидва файли рахуються одним методом `BuildFairmasReport` — логіка підрахунку ідентична, відрізняються лише період і набір даних. Наслідок: будь-яка помилка в метриці автоматично сидить в обох файлах. Джерела даних: Guests : У звіт беруться лише статуси Boo...) |
Немає опису редагування |
||
| (Не показано одну проміжну версію цього користувача) | |||
| Рядок 1: | Рядок 1: | ||
<languages/> | |||
<translate> | |||
== Fairmas звіти == <!--T:1--> | |||
</translate> | |||
<languages/> | |||
<translate> | |||
4 кроки для кожного дня: | <!--T:2--> | ||
1 | Обидва файли Fairmas-звіту рахуються одним методом <code>BuildFairmasReport</code> — логіка підрахунку ідентична, відрізняються лише період і набір даних. Наслідок: будь-яка помилка в метриці автоматично присутня в обох файлах. | ||
броней); | |||
Платить гість | === Джерела даних === <!--T:3--> | ||
рядків-сегментів. | <!--T:4--> | ||
4 | * '''Guests''' — у звіт беруться лише статуси Booking / Living / Departured; | ||
v_CoefficientGuestRoomsOccupiedEarlyArrival) по записах із заїздом | * '''Payments''' — беруться записи з <code>!p.v_IsCanceled</code>; | ||
(CoefficientGuestRoomsOccupiedArrival вже містить | * '''St.Rooms''' — зріз стану номерів на конкретну дату, агрегат по типах номерів. Дає місткість (скільки всього / на ремонті / у продажу); | ||
CoefficientGuestRoomsOccupiedEarlyArrival); | * '''St.RoomsGuests''' — деталізація до рівня «конкретна бронь — номер — ніч». Наразі місткість для RoomSold та RoomArrival береться саме з St.RoomsGuests. | ||
Уся арифметика номерів стоїть на трьох коефіцієнтах із | === Алгоритм розрахунку (4 кроки для кожного дня) === <!--T:5--> | ||
<!--T:6--> | |||
'''Крок 1. Місткість.''' Складаємо по всіх типах номерів цієї дати: | |||
* <code>TotalRooms</code> = сума <code>RoomsInExploitation</code> (весь фонд в експлуатації, не залежить від броней); | |||
* <code>OOO</code> = сума <code>RoomsOnRepairNotSale</code> (на ремонті, продати не можна); | |||
* <code>In-sale (TotalRoomsAvailable)</code> = TotalRooms − OOO; | |||
* <code>OOS</code> = 0 (хардкод). | |||
<!--T:7--> | |||
'''Крок 2. Розкладка оплат.''' Проходимо всі оплати дня. Платить гість — використовується його сегмент; платить не гість — <code>retail_direct</code>: | |||
* dwelling → Logis (проживання); | |||
* eat → F&B (харчування); | |||
* citytax → міський податок → викидається повністю (не є виручкою готелю); | |||
* решта → Other. | |||
У CSV колонки виручки вже за вирахуванням податків. | |||
<!--T:8--> | |||
'''Крок 3. Режими сегментації.''' Є два режими: | |||
* <code>retail_direct</code> / <code>retail_indirect</code>; | |||
* <code>BySource</code> — скільки різних джерел фігурувало (Online reservation, YandexTravel…), стільки й рядків-сегментів. | |||
<!--T:9--> | |||
'''Крок 4. Номерні показники.''' | |||
* <code>RoomsSold</code> = округл(Σ <code>CoefficientGuestRoomsOccupied</code>) по записах гостей сегмента; | |||
* <code>RoomsArrival</code> = округл(Σ <code>v_CoefficientGuestRoomsOccupiedArrival</code> + <code>v_CoefficientGuestRoomsOccupiedEarlyArrival</code>) по записах із заїздом — '''баг''' (CoefficientGuestRoomsOccupiedArrival вже містить CoefficientGuestRoomsOccupiedEarlyArrival); | |||
* <code>Guests</code> = Σ (<code>Client_Count + Child_Count</code>) по всіх записах сегмента; | |||
* <code>GuestsArrival</code> = те саме, але лише по записах із заїздом. | |||
Округлення використовує ToEven (1.5 → 2, 4.5 → 4). | |||
=== Коефіцієнти st_RoomsGuests === <!--T:10--> | |||
<!--T:11--> | |||
Уся арифметика номерів стоїть на трьох коефіцієнтах із <code>st_RoomsGuests</code>: | |||
* '''Occupied''' — вага зайнятості номеро-доби (гість 6 = 1.0 — повна ніч; гість 40 = 1.5 — ніч + ранній заїзд); | |||
* '''Arrival''' — вага заїзду; вже містить ранній заїзд (у гостя 40 = 1.5); | |||
* '''EarlyArrival''' — ранній заїзд окремим полем. | |||
<!--T:12--> | |||
Ключове: у St.Rooms заїзд рахується цілими штуками, у St.RoomsGuests — дробом. | |||
<!--T:13--> | |||
Приклад коефіцієнтів (на даних 09.08): | |||
<!--T:14--> | |||
{| class="wikitable" | |||
! GuestID !! RoomID !! Occupied !! Arrival !! EarlyArrival | |||
|- | |||
| 40 || 96 || 1.5000 || 1.5000 || 0.5000 | |||
|- | |||
| 6 || 156 || 1.0000 || 1.0000 || 0.0000 | |||
|} | |||
=== Баг №1 — задвоєння заїзду === <!--T:15--> | |||
<!--T:16--> | |||
Формула в коді: <code>Sum(Arrival + EarlyArrival)</code>. Але EarlyArrival уже входить в Arrival, тож ранній заїзд рахується двічі: | |||
* гість 40: 1.5 + 0.5 = 2.0; | |||
* гість 6: 1.0 + 0.0 = 1.0; | |||
* разом 3.0 → 3 (невірно). | |||
Правильно — лише Arrival: 1.5 + 1.0 = 2.5 → 2. | |||
<!--T:17--> | |||
Проявляється на бронях із раннім заїздом. | |||
<!--T:18--> | |||
Прибрати <code>+ EarlyArrival</code> закриває тикет (09.08 → 2), але не лікує корінь: ми й далі складаємо дроби й округлюємо. Два ранні заїзди по 1.5 дадуть 3 замість 2 номерів. | |||
=== Баг №2 — порожні рядки за джерелом === <!--T:19--> | |||
<!--T:20--> | |||
Механізм: збіг двох незалежних фактів. | |||
<!--T:21--> | |||
'''Факт 1.''' Виїжджаючий гість потрапляє у вибірку дня свого виїзду. Дати броні (First_Date, Last_Date) зберігаються з часом (заїзд о 14:00, виїзд о 12:00), але код порівнює їх через <code>.Date</code>, тобто час відкидається. Гість, що виїжджає 09.08 о 12:00, має Last_Date.Date = 09.08. Фільтр «чи живе він 09.08» (<code>First_Date.Date <= day && Last_Date.Date >= day</code>) дає true. Таким чином гість вважається присутнім у день свого виїзду, хоча фактично провів лише ранок. | |||
<!--T:22--> | |||
'''Факт 2.''' Зайнятості в день виїзду немає. Ніч не зайнята, тому запису в st_RoomsGuests на цю дату не існує (записів із нульовим коефіцієнтом зайнятості в таблиці немає взагалі — це перевірено на даних). Сума коефіцієнтів зайнятості по порожньому набору дорівнює нулю. | |||
<!--T:23--> | |||
'''Результат.''' Гість потрапляє у список гостей дня, робить свій сегмент непорожнім, і його джерело потрапляє у набір сегментів режиму BySource. Для сегмента заводиться рядок, але зайнятість і заїзд у ньому дорівнюють нулю. Отримуємо рядок, у якому назва джерела є, а всі номерні показники нульові. | |||
<!--T:24--> | |||
'''Чому саме режим BySource це виявив.''' У старому режимі DirectIndirect сегментів лише два, і «зайвий» виїжджаючий гість вливається у retail_direct або retail_indirect та губиться у загальній масі. Режим BySource виносить кожне джерело окремим рядком, тому порожній хвіст стає видимим окремою нульовою стрічкою. | |||
<!--T:25--> | |||
'''Де саме дірка в коді.''' Перед записом рядка сегмента стоїть відсічка: | |||
<!--T:26--> | |||
<syntaxhighlight lang="csharp"> | |||
if (!segmentGuests.Any() && segmentRevs.Logis == 0 | |||
&& segmentRevs.FaB == 0 && segmentRevs.Other == 0) | |||
continue; | |||
</syntaxhighlight> | |||
<!--T:27--> | |||
Вона пропускає рядок лише тоді, коли одночасно немає гостей і вся виручка дорівнює нулю. Проблема в тому, що умова <code>segmentGuests.Any()</code> перевіряє наявність гостей у списку, а перевіряти треба реальну зайнятість (<code>roomsOccupied > 0</code>). Але <code>roomsOccupied</code> обчислюється нижче за кодом, вже після <code>continue</code>, тому відсічка фізично не може його врахувати. | |||
=== Спільний корінь обох багів === <!--T:28--> | |||
<!--T:29--> | |||
Баги різні за природою: баг №1 — це складання дробових коефіцієнтів, баг №2 — це відсічка за наявністю гостей до фактичного розрахунку зайнятості. Але обидва виростають з однієї хиби методу: звіт рахує номери сумою дробових ваг і покладається на присутність гостя у списку, а не на реальну зайнятість чи заїзд у штуках. | |||
<!--T:30--> | |||
'''Правильний напрям''' — рахувати кількість номерів як подій, тобто скільки окремих записів заїзду або зайнятості має сегмент за день, а не суму коефіцієнтів. Тоді округлення зникає (результат одразу цілий), а співвідношення «RoomsArrival не більше за RoomsSold» тримається саме собою. | |||
</translate> | |||
Поточна версія на 14:49, 18 серпня 2026
Fairmas звіти
Обидва файли Fairmas-звіту рахуються одним методом BuildFairmasReport — логіка підрахунку ідентична, відрізняються лише період і набір даних. Наслідок: будь-яка помилка в метриці автоматично присутня в обох файлах.
Джерела даних
- Guests — у звіт беруться лише статуси Booking / Living / Departured;
- Payments — беруться записи з
!p.v_IsCanceled; - St.Rooms — зріз стану номерів на конкретну дату, агрегат по типах номерів. Дає місткість (скільки всього / на ремонті / у продажу);
- St.RoomsGuests — деталізація до рівня «конкретна бронь — номер — ніч». Наразі місткість для RoomSold та RoomArrival береться саме з St.RoomsGuests.
Алгоритм розрахунку (4 кроки для кожного дня)
Крок 1. Місткість. Складаємо по всіх типах номерів цієї дати:
TotalRooms= сумаRoomsInExploitation(весь фонд в експлуатації, не залежить від броней);OOO= сумаRoomsOnRepairNotSale(на ремонті, продати не можна);In-sale (TotalRoomsAvailable)= TotalRooms − OOO;OOS= 0 (хардкод).
Крок 2. Розкладка оплат. Проходимо всі оплати дня. Платить гість — використовується його сегмент; платить не гість — retail_direct:
- dwelling → Logis (проживання);
- eat → F&B (харчування);
- citytax → міський податок → викидається повністю (не є виручкою готелю);
- решта → Other.
У CSV колонки виручки вже за вирахуванням податків.
Крок 3. Режими сегментації. Є два режими:
retail_direct/retail_indirect;BySource— скільки різних джерел фігурувало (Online reservation, YandexTravel…), стільки й рядків-сегментів.
Крок 4. Номерні показники.
RoomsSold= округл(ΣCoefficientGuestRoomsOccupied) по записах гостей сегмента;RoomsArrival= округл(Σv_CoefficientGuestRoomsOccupiedArrival+v_CoefficientGuestRoomsOccupiedEarlyArrival) по записах із заїздом — баг (CoefficientGuestRoomsOccupiedArrival вже містить CoefficientGuestRoomsOccupiedEarlyArrival);Guests= Σ (Client_Count + Child_Count) по всіх записах сегмента;GuestsArrival= те саме, але лише по записах із заїздом.
Округлення використовує ToEven (1.5 → 2, 4.5 → 4).
Коефіцієнти st_RoomsGuests
Уся арифметика номерів стоїть на трьох коефіцієнтах із st_RoomsGuests:
- Occupied — вага зайнятості номеро-доби (гість 6 = 1.0 — повна ніч; гість 40 = 1.5 — ніч + ранній заїзд);
- Arrival — вага заїзду; вже містить ранній заїзд (у гостя 40 = 1.5);
- EarlyArrival — ранній заїзд окремим полем.
Ключове: у St.Rooms заїзд рахується цілими штуками, у St.RoomsGuests — дробом.
Приклад коефіцієнтів (на даних 09.08):
| GuestID | RoomID | Occupied | Arrival | EarlyArrival |
|---|---|---|---|---|
| 40 | 96 | 1.5000 | 1.5000 | 0.5000 |
| 6 | 156 | 1.0000 | 1.0000 | 0.0000 |
Баг №1 — задвоєння заїзду
Формула в коді: Sum(Arrival + EarlyArrival). Але EarlyArrival уже входить в Arrival, тож ранній заїзд рахується двічі:
- гість 40: 1.5 + 0.5 = 2.0;
- гість 6: 1.0 + 0.0 = 1.0;
- разом 3.0 → 3 (невірно).
Правильно — лише Arrival: 1.5 + 1.0 = 2.5 → 2.
Проявляється на бронях із раннім заїздом.
Прибрати + EarlyArrival закриває тикет (09.08 → 2), але не лікує корінь: ми й далі складаємо дроби й округлюємо. Два ранні заїзди по 1.5 дадуть 3 замість 2 номерів.
Баг №2 — порожні рядки за джерелом
Механізм: збіг двох незалежних фактів.
Факт 1. Виїжджаючий гість потрапляє у вибірку дня свого виїзду. Дати броні (First_Date, Last_Date) зберігаються з часом (заїзд о 14:00, виїзд о 12:00), але код порівнює їх через .Date, тобто час відкидається. Гість, що виїжджає 09.08 о 12:00, має Last_Date.Date = 09.08. Фільтр «чи живе він 09.08» (First_Date.Date <= day && Last_Date.Date >= day) дає true. Таким чином гість вважається присутнім у день свого виїзду, хоча фактично провів лише ранок.
Факт 2. Зайнятості в день виїзду немає. Ніч не зайнята, тому запису в st_RoomsGuests на цю дату не існує (записів із нульовим коефіцієнтом зайнятості в таблиці немає взагалі — це перевірено на даних). Сума коефіцієнтів зайнятості по порожньому набору дорівнює нулю.
Результат. Гість потрапляє у список гостей дня, робить свій сегмент непорожнім, і його джерело потрапляє у набір сегментів режиму BySource. Для сегмента заводиться рядок, але зайнятість і заїзд у ньому дорівнюють нулю. Отримуємо рядок, у якому назва джерела є, а всі номерні показники нульові.
Чому саме режим BySource це виявив. У старому режимі DirectIndirect сегментів лише два, і «зайвий» виїжджаючий гість вливається у retail_direct або retail_indirect та губиться у загальній масі. Режим BySource виносить кожне джерело окремим рядком, тому порожній хвіст стає видимим окремою нульовою стрічкою.
Де саме дірка в коді. Перед записом рядка сегмента стоїть відсічка:
if (!segmentGuests.Any() && segmentRevs.Logis == 0
&& segmentRevs.FaB == 0 && segmentRevs.Other == 0)
continue;
Вона пропускає рядок лише тоді, коли одночасно немає гостей і вся виручка дорівнює нулю. Проблема в тому, що умова segmentGuests.Any() перевіряє наявність гостей у списку, а перевіряти треба реальну зайнятість (roomsOccupied > 0). Але roomsOccupied обчислюється нижче за кодом, вже після continue, тому відсічка фізично не може його врахувати.
Спільний корінь обох багів
Баги різні за природою: баг №1 — це складання дробових коефіцієнтів, баг №2 — це відсічка за наявністю гостей до фактичного розрахунку зайнятості. Але обидва виростають з однієї хиби методу: звіт рахує номери сумою дробових ваг і покладається на присутність гостя у списку, а не на реальну зайнятість чи заїзд у штуках.
Правильний напрям — рахувати кількість номерів як подій, тобто скільки окремих записів заїзду або зайнятості має сегмент за день, а не суму коефіцієнтів. Тоді округлення зникає (результат одразу цілий), а співвідношення «RoomsArrival не більше за RoomsSold» тримається саме собою.