Uczestników e-mailowej edycji „Poradnika” pytałem, czego najbardziej nie lubią w pracy z Excelem. Wśród odpowiedzi bardzo często pojawiały się daty i obliczenia kalendarzowe. Oto jeden z typowych cytatów: „Praca z datami i timestampami. To jest koszmar i jedna wielka patologia, szczególnie w połączeniu z wykresami i to jeszcze przeklejanymi do prezentacji. Jeśli miałbym wskazać jedną, absolutnie najgorszą rzecz, to to by było to. Aż się zdenerwowałem na samą myśl.”
Trzeba przyznać, że Excel zasłużył sobie na krytykę – jego nadgorliwość w konwersji wszystkiego na daty jest co najmniej irytująca. Ważniejsze problemy leżą jednak głębiej, w sposobie reprezentacji dat i godzin oraz operowania na nich.
Excel zapamiętuje daty jako liczbę dni od pierwszego stycznia roku 1900. Jako ciekawostkę można wskazać, że choć rok 1900 nie był przestępny, to Excel zakłada istnienie dnia 29 lutego 1900 roku. Historyczną przyczyną jest wsteczna kompatybilność z arkuszem kalkulacyjnym Lotus 1-2-3, wydanym w roku 1983 i zapomnianym kilkanaście lat później. Ktoś kiedyś uznał, że lepiej jest replikować błąd, niż przeprowadzać prawidłowe obliczenia. Po przeszło 40 latach nadal żyjemy z tą usterką.
Do rzeczy jednak. Reprezentowanie daty liczbą dni od „chwili zero” nie jest zbyt dużym problemem. Schody zaczynają się, gdy spojrzymy na reprezentację godzin, minut i sekund – są to bowiem ułamki dziesiętne odpowiadające części pełnego dnia. Przykład: doba ma 1440 minut. Jedna minuta to 0,00069(4) część doby, przy czym czwórka występuje w rozwinięciu dziesiętnym okresowo, w nieskończoność.
Spójrzmy teraz, jak data i data z godziną wyglądają po sformatowaniu jako liczba. Dwudziestego szóstego marca zmienialiśmy czas z zimowego na letni, o drugiej w nocy popchnęliśmy zegary o godzinę. Ów dzień miał 23 godziny czyli 1380 minut, ale Excel nic o tym nie wie – dla niego minuta tamtego dnia to nadal taki sam ułamek doby, jak choćby dziś.
Jest jeszcze gorzej! Excel nie tylko nie zna koncepcji czasu zimowego i letniego, nie wie też niczego o strefach czasowych! Z doświadczenia (również programistycznego) wiem, jak trudne są obliczenia dotyczące daty i czasu nawet wówczas, gdy algorytmy i struktury danych oferują pomoc. Sytuacja, gdy użytkownik miałby samodzielnie implementować przeliczenia stref czasowych, szybko przekształca się w dramat.
Problemy czekają wszystkich, którzy będą chcieli przygotować w Excelu tabele określające czas podróży lub frachtu lotniczego – starty i lądowania podaje się bowiem wg czasu miejscowego. Na żmudne dodawanie i odejmowanie ułamków doby skazani będą wszyscy, którzy sporządzają raporty godzinowe na podstawie danych z różnych części świata. Nie pomogą w tym takie językowe potworki, jak funkcja NR.SER.OST.DN.MIES
(w wersji angielskiej: EOMONTH
).
Biblioteka Lubridate
Opisane wyżej problemy złagodzimy w języku R przy użyciu biblioteki Lubridate. Data i czas są tu zawsze zapamiętywane wraz ze strefą czasową i uwzględnieniem zmian czasu. Spójrzmy tylko:
W pierwszym wierszu tworzymy zmienną x1
, przechowującą informację o chwili, która miała miejsce 26 marca 2023 o godzinie 1:30 w Polsce. Próba utworzenia zmiennej x2
przechowującej chwilę z godziny 2:30 tego dnia nie udaje się – takiej godziny bowiem nie było, nasze zegarki przeskoczyły z godziny 1:59:59 na 3:00:00.
Poprawnie utworzoną zmienną x1
możemy za to przedstawić jako godzinę z dowolnej innej strefy czasowej:
Jeśli wczytujemy cząstkowe raporty z różnych miejsc świata, nawet podawane wg czasu lokalnego, możemy jednym poleceniem uzupełnić je o właściwą strefę czasową – wówczas agregacja zdarzeń czy obliczanie różnicy czasu między dwoma zdarzeniami będą banalnie proste.
Datę i czas, rozumiane jako konkretny moment wskazany przez kalendarz, zegar i strefę czasową, przechowamy w obiektach klasy POSIXct
. Jest ona częścią języka R, jednak sam język nie daje nam zbyt wygodnych narzędzi do operowania na datach.
Odstępy w czasie
Co robić, jeśli nasze obliczenia mają dotyczyć dat urzędowych albo okresów wynikających z dokumentów bankowych? Gdy mowa o określeniach typu „z końcem miesiąca” albo „w ciągu dwóch tygodni”, mniej obchodzą nas strefy czasowe. Chcemy raczej określić, kiedy upłynie „jeden miesiąc po 31 stycznia”.
Także tu pomoże nam biblioteka Lubridate, która definiuje kilka różnych bytów określających (szeroko rozumiane) rozpiętości czasowe. Przyjrzyjmy im się po kolei.
Duration (przedział czasu)
Duration to przedział, który mierzymy stoperem. Możemy o nim myśleć, jak o konkretnej liczbie sekund, albo raczej pikosekund, bo taką precyzję oferują R i Lubridate. Gdy wyrażamy duration w godzinach, dniach czy tygodniach, zawsze mowa tu o wielokrotności „typowej” liczby sekund, minut czy godzin (godzina = 3600 sekund, doba = 86400 sekund itp).
Gdy do chwili określającej północ dodamy przedział (duration) trwający 24h, w dniach zmiany czasu otrzymamy wynik wskazujący godzinę 23 tego samego dnia (gdy cofaliśmy zegar a doba miała 25h) albo godzinę 1 dnia następnego (gdy popychaliśmy zegar o godzinę).
Period (okres)
Period to okres dzielący dwa wydarzenia określone względem siebie w sposób „urzędowy” czy „zdroworozsądkowy”. Zostanie on zinterpretowany jako określona liczba sekund dopiero wtedy, gdy znany będzie kontekst jego użycia.
Jeśli do styczniowej daty określonego roku dodamy period/okres wynoszący „1 rok”, liczba dni w tym okresie będzie zależna od tego, czy okres obejmuje dzień przestępny. Podobnie będzie z dniami zmiany daty – gdy do chwili określającej północ dodamy okres wynoszący dobę, wynik zawsze będzie wskazywał na północ kolejnego dnia (okres „1 doba” przyjmie skonkretyzowaną wartość z przedziału 23-25 godzin).
Z tego właśnie powodu zmiennej przechowującej period (np. „1 doba”) nie da się przełożyć na duration ani odwrotnie.
Uwaga! Przed użyciem okresów zapoznaj się dokładnie z arytmetyką, wg której działają obliczenia z nich korzystające! Oczekujemy, że „31 stycznia 2023 plus miesiąc” da nam „28 lutego 2023”. Niekoniecznie jednak spodziewamy się, że „31 stycznia 2023 plus miesiąc minus miesiąc” to nadal „31 stycznia 2023”.
Interval (interwał)
Interwał jest definiowany zawsze jako czas między dwoma wskazanymi datami.
To oznacza, że jesteśmy w stanie skonwertować go zarówno do obiektu duration (bo znamy dokładną liczbę sekund między datami) jak i period (bo możemy określić, ile lat, miesięcy, dni, godzin itd upływa między tymi datami).
Arytmetyka na interwałach jest niemożliwa, ale można używać ich np. do sprawdzania, czy jakaś data zawiera się we wskazanym interwale.
Użycie dat we własnych obliczeniach
Daty i godziny wczytywane ze źródeł zewnętrznych są zazwyczaj ładowane do ramki danych jako tekst. Polecam najpierw wczytać źródłową ramkę danych a w kolejnych krokach ustanawiać docelowe typy kolumn – to pozwala skorzystać z wielu wygodnych funkcji pomocniczych Lubridate.
Przykład: funkcje ymd()
/dmy()
przekształcające tekst na datę automatycznie radzą sobie z takimi napisami:
ymd("2021-01-02");
ymd("2021.01.02");
ymd("20210102");
ymd("2021 Styczeń 02");
dmy("02 styczeń 2021");
dmy("02Jan2021")
Data z godziną? Spójrzmy na dmy_hms()
dmy_hms("10 03 2023, 19:42:23");
dmy_hms("10-03-2023T19:42:23");
dmy_hms("10_Mar_2023 19_42_23");
dmy_hms("10 Marzec 2023 19 42 23");
Funkcje automatyczne dostępne są też w wariantach mdy()
, dym()
i innych. Jeśli jakieś formatowanie będzie zbyt wymyślne, można użyć funkcji as_datetime
pozwalającej na przekazanie własnej specyfikacji formatowania.
Uwaga – Lubridate nie rozumie polskiej odmiany nazw miesięcy, data „02 stycznia 2021” nie zostanie poprawnie zinterpretowana. W takiej sytuacji pomoże zamiana nazwy miesiąca na trzy pierwsze litery jego nazwy.
Podsumowanie
Jeśli nasze obliczenia na datach zamykają się w obrębie jednej strefy czasowej, biblioteka Lubridate i typy duration / period / interval będą wielką pomocą. Jeśli to tylko możliwe, pozbywaj się stref czasowych na rzecz czasu uniwersalnego UTC. Gdy np. sprzedajesz czasowy dostęp do swojego serwisu, lepiej dodać kilka darmowych godzin do miesięcznego abonamentu i resetować dostępy o północy UTC, niż zastanawiać się, jaką strefę czasową przypisać klientowi z językiem norweskim w przeglądarce, australijską kartą kredytową i numerem IP z Brazylii.
Gdy jednak musisz brać pod uwagę różne strefy czasowe w danych wejściowych i w warstwie prezentacji, zmierzysz się z wyzwaniami nowej kategorii – np. problemem, co dla ludzi z różnych części świata znaczyć będzie słowo „dzisiaj”. To wszystko jest zwyczajnie trudne a niektóre przypadki brzegowe zamanifestują swoją obecność raz na cztery lata.
Pozwólcie, że dzisiejszy odcinek zakończymy obrazkiem:
O autorze: zawodowy programista od 2003 roku, pasjonat bezpieczeństwa informatycznego. Rozwijał systemy finansowe dla NBP, tworzył i weryfikował zabezpieczenia bankowych aplikacji mobilnych, brał udział w pracach nad grą Angry Birds i wyszukiwarką internetową Microsoft Bing.
4 odpowiedzi na “Analiza danych w języku R – odcinek 5”
Chyba jestem trochę doświadczonym programistą, bo:
1) jestem świadom że nie wszędzie zmiana czasu jest 2->3
2) nie wszędzie rok 2500 jest w przyszłości
3) jakbym miał sprzedawać dostęp miesięczny to dodawałbym co najmniej 12h, a pewnie 23h;
4) mam świadomość że powyższe wcale nie zabezpiecza przed wszystkimi przypadkami
5) powyższe przetrenowałem w praktyce 😀
Niby taka świetna ta biblioteka, ale ze zmianą w drugą stronę, tj. czasu letniego na zimowy już sobie nie radzi 😉
x3 <- ymd_hms("2023-10-29 02:30:00", tz = "Europe/Warsaw")
x4 [1] „2023-10-29 02:30:00 CET”
> [1] „2023-10-29 02:30:00 CET”
https://isap.sejm.gov.pl/isap.nsf/download.xsp/WDU20220000539/O/D20220539.pdf
Celne spostrzeżenie! Ciekawe, skąd pochodzi nieaktualna informacja o zmianie czasu – z biblioteki Lubridate? Dystrybucji R? Systemu operacyjnego? Ciekawy temat do researchu
Mój komentarz ucieło 🙂
ymd_hms(„2023-10-29 02:30:00”, tz = „Europe/Warsaw”)
ymd_hms(„2023-10-29 02a:30:00”, tz = „Europe/Warsaw”)
https://pastebin.com/T3dBCugP
W sumie to chodziło mi o obsługę godziny „2a”, która, co prawda występuje tylko raz w roku, ale zgodnie z przytoczonym rozporządzeniem jest to poprawna godzina (i wyobrażam sobie sytuacje w których odróżnienie godziny 2 od 2a będzie krytyczne).
Generalnie żaden znany mi system komputerowy / biblioteka nie obsługuje poprawnie zmiany czasu „w tył”, czyli z 3 na 2.