Fairmas

Матеріал з expertsolution
Перейти до навігації Перейти до пошуку
На цій сторінці було здійснено зміни, які не відмічені для перекладу.

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» тримається саме собою.