---
title: "Time Zones, Missing Hours and Daily Data"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Time Zones, Missing Hours and Daily Data}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include = FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
```

```{r library}
library(rmet)
x <- rmet_example
```

Hourly INMET data look simple, but three conventions decide whether daily
values computed from them are right: how the hours are stamped, which day an
hour belongs to, and what to do with missing hours. This vignette explains each
one and how `inmet_daily()` handles it.

## 1. Timestamps are UTC and mark the end of the hour

The historical files record time in UTC. `inmet_read()` keeps it that way by
default, so `datetime` is unambiguous for every station in the country:

```{r utc}
x$datetime[1:3]
attr(x$datetime, "tzone")
```

Many columns describe the hour that *ends* at the timestamp: precipitation is
the total of the previous hour, and `temp_max_prev_c` and `temp_min_prev_c` are
the highest and lowest air temperature during that hour. The record stamped
`03:00 UTC` therefore covers 02:00 to 03:00 UTC, which is 23:00 to midnight in
Porto Alegre.

Displaying the same instants in local time only changes how they print:

```{r local}
local <- x$datetime[1:3]
attr(local, "tzone") <- "America/Sao_Paulo"
local
```

## 2. Which day does an hour belong to?

A daily maximum in Brazil should be the maximum of a *local* day. Grouping by
`as.Date(datetime)` uses UTC days instead, which in Porto Alegre run from
21:00 to 21:00 local time (22:00 to 22:00 during daylight saving time, which
Brazil observed until 2019). An evening shower or a hot late afternoon can then
land on the wrong day.

`inmet_daily()` assigns each record to the local day containing the middle of
its hour (`datetime` minus 30 minutes). The two choices give different days:

```{r compare-days}
utc_day <- as.Date(x$datetime)
local_day <- as.Date(x$datetime - 1800, tz = "America/Sao_Paulo")
mean(utc_day != local_day)
```

One hour in six (the records stamped 00:00 to 03:00 UTC) is placed on a
different day, and the differences matter for daily totals and extremes:

```{r compare-values}
wrong <- tapply(x$precip_mm, utc_day, sum)
right <- inmet_daily(x, min_hours = 1)
right <- setNames(right$precip_mm, right$date)
common <- intersect(names(wrong), names(right))
summary(abs(wrong[common] - right[common]))
```

Brazil spans four time zones, and some states observed daylight saving time
in some years and others not. By default (`tz = "auto"`), `inmet_daily()`
uses each station's own zone, derived from its state and longitude by
`inmet_timezone()`, and reports it in the `time_zone` column:

```{r zones}
inmet_timezone(c("RS", "MT", "AC", "AM"), c(-51.2, -56.1, -67.8, -69.9))
```

Pass a fixed zone, such as `tz = "America/Sao_Paulo"`, to put all stations
on the same clock, or `tz = "UTC"` for UTC days.

## 3. Missing hours

INMET files contain rows for every hour of the year, including hours when a
station was not reporting. By default `inmet_read()` drops rows in which *every*
measurement is missing (`drop_empty = FALSE` keeps them). Individual variables
can still be missing in the rows that remain.

A daily total over a day with half its hours missing is not a daily total.
`inmet_daily()` therefore computes a variable only when it has at least
`min_hours` valid hourly values that day (20 by default), and reports the
number of hourly records in `n_hours`:

```{r completeness}
daily <- inmet_daily(x)
table(cut(daily$n_hours, c(0, 12, 19, 23, 25), right = TRUE))
daily[daily$n_hours < 20, c("date", "n_hours", "temp_max_c", "precip_mm")]
```

To see completeness before computing anything, `inmet_completeness()` gives
the percentage of valid hours per station and month (or year):

```{r completeness-table}
head(inmet_completeness(x, variables = c("temp_dry_c", "precip_mm", "wind_speed_ms")))
```

Choose `min_hours` to suit the variable and the analysis: precipitation totals
are sensitive to every missing hour, while a daily maximum of temperature is
usually reached in the afternoon. Radiation is an exception: it is missing at
night at most stations, so `radiation_mj_m2` sums whatever daytime values are
present (see `?inmet_daily`).

## 4. Which temperature columns?

The hourly files contain both the air temperature at the top of the hour
(`temp_dry_c`) and its extremes during the previous hour
(`temp_max_prev_c`, `temp_min_prev_c`). The daily maximum and minimum should
come from the extremes; the instantaneous values miss the peaks between
readings:

```{r extremes}
from_extremes <- inmet_daily(x)$temp_max_c
from_hourly <- inmet_daily(x[, setdiff(names(x), "temp_max_prev_c")])$temp_max_c
summary(from_extremes - from_hourly)
```

At Porto Alegre in 2023 the hourly readings underestimate the daily maximum by
about half a degree on average, and by more than two degrees on some days.

`inmet_daily()` uses the extremes when they are present and falls back to
`temp_dry_c` otherwise.

## 5. Quality control

INMET labels automatic-station observations as raw data. `inmet_qc()` flags
the most common problems: values outside physical limits, internally
inconsistent records (such as an hourly maximum below the minimum), isolated
spikes, and sensors stuck on the same value. It adds a `<variable>_flag`
column naming the failed checks, and `inmet_daily()` and
`inmet_completeness()` then ignore the flagged values:

```{r qc}
checked <- inmet_qc(x)
```

Station A801 passed every check in 2023. To see what a flag looks like, plant
a sensor spike of 15 degC in one hour:

```{r qc-spike}
y <- x
y$temp_dry_c[1000] <- y$temp_dry_c[1000] + 15
flagged <- inmet_qc(y, quiet = TRUE)
flagged[998:1002, c("datetime", "temp_dry_c", "temp_max_prev_c", "temp_dry_c_flag")]
```

The reading is both a spike and inconsistent with the hour's maximum.
`inmet_daily(flagged)` leaves it out of the daily statistics, and
`attr(flagged, "qc_summary")` counts the flags per variable and check. Across all 545 stations
in the 2024 archive the same checks flag, for example, a stuck wind vane that
reported 236 degrees for months and temperature readings that drop by 9 degC
for one hour and return. Use `action = "remove"` to set flagged values to `NA`
directly.

These checks catch gross errors only. Comparing with neighbouring stations
(`inmet_nearest()`) remains the most effective way to find subtler problems.
