Skip to main content

polars_time/
upsample.rs

1#[cfg(feature = "timezones")]
2use polars_core::datatypes::time_zone::parse_time_zone;
3use polars_core::prelude::*;
4use polars_core::utils::accumulate_dataframes_vertical_unchecked;
5#[cfg(any(feature = "dtype-date", feature = "dtype-datetime"))]
6use polars_defs::join::{JoinArgs, JoinType};
7use polars_defs::time::duration::{Duration, ensure_duration_matches_dtype};
8#[cfg(any(feature = "dtype-date", feature = "dtype-datetime"))]
9use polars_defs::time::group_by::ClosedWindow;
10use polars_ops::prelude::*;
11use polars_ops::series::SeriesMethods;
12
13#[cfg(any(feature = "dtype-date", feature = "dtype-datetime"))]
14use crate::prelude::*;
15
16pub trait PolarsUpsample {
17    /// Upsample a [`DataFrame`] at a regular frequency.
18    ///
19    /// # Arguments
20    /// * `by` - First group by these columns and then upsample for every group
21    /// * `time_column` - Will be used to determine a date_range.
22    ///   Note that this column has to be sorted for the output to make sense.
23    /// * `every` - interval will start 'every' duration
24    /// * `offset` - change the start of the date_range by this offset.
25    ///
26    /// The `every` and `offset` arguments are created with
27    /// the following string language:
28    /// - 1ns   (1 nanosecond)
29    /// - 1us   (1 microsecond)
30    /// - 1ms   (1 millisecond)
31    /// - 1s    (1 second)
32    /// - 1m    (1 minute)
33    /// - 1h    (1 hour)
34    /// - 1d    (1 calendar day)
35    /// - 1w    (1 calendar week)
36    /// - 1mo   (1 calendar month)
37    /// - 1q    (1 calendar quarter)
38    /// - 1y    (1 calendar year)
39    /// - 1i    (1 index count)
40    ///
41    /// Or combine them:
42    /// "3d12h4m25s" # 3 days, 12 hours, 4 minutes, and 25 seconds
43    ///
44    /// By "calendar day", we mean the corresponding time on the next
45    /// day (which may not be 24 hours, depending on daylight savings).
46    /// Similarly for "calendar week", "calendar month", "calendar quarter",
47    /// and "calendar year".
48    fn upsample<I: IntoVec<PlSmallStr>>(
49        &self,
50        by: I,
51        time_column: &str,
52        every: Duration,
53    ) -> PolarsResult<DataFrame>;
54
55    /// Upsample a [`DataFrame`] at a regular frequency.
56    ///
57    /// Similar to [`upsample`][PolarsUpsample::upsample], but order of the
58    /// DataFrame is maintained when `by` is specified.
59    ///
60    /// # Arguments
61    /// * `by` - First group by these columns and then upsample for every group
62    /// * `time_column` - Will be used to determine a date_range.
63    ///   Note that this column has to be sorted for the output to make sense.
64    /// * `every` - interval will start 'every' duration
65    /// * `offset` - change the start of the date_range by this offset.
66    ///
67    /// The `every` and `offset` arguments are created with
68    /// the following string language:
69    /// - 1ns   (1 nanosecond)
70    /// - 1us   (1 microsecond)
71    /// - 1ms   (1 millisecond)
72    /// - 1s    (1 second)
73    /// - 1m    (1 minute)
74    /// - 1h    (1 hour)
75    /// - 1d    (1 calendar day)
76    /// - 1w    (1 calendar week)
77    /// - 1mo   (1 calendar month)
78    /// - 1q    (1 calendar quarter)
79    /// - 1y    (1 calendar year)
80    /// - 1i    (1 index count)
81    ///
82    /// Or combine them:
83    /// "3d12h4m25s" # 3 days, 12 hours, 4 minutes, and 25 seconds
84    ///
85    /// By "calendar day", we mean the corresponding time on the next
86    /// day (which may not be 24 hours, depending on daylight savings).
87    /// Similarly for "calendar week", "calendar month", "calendar quarter",
88    /// and "calendar year".
89    fn upsample_stable<I: IntoVec<PlSmallStr>>(
90        &self,
91        by: I,
92        time_column: &str,
93        every: Duration,
94    ) -> PolarsResult<DataFrame>;
95}
96
97impl PolarsUpsample for DataFrame {
98    fn upsample<I: IntoVec<PlSmallStr>>(
99        &self,
100        by: I,
101        time_column: &str,
102        every: Duration,
103    ) -> PolarsResult<DataFrame> {
104        let by = by.into_vec();
105        let time_type = self.column(time_column)?.dtype();
106        ensure_duration_matches_dtype(every, time_type, "every")?;
107        upsample_impl(self, by, time_column, every, false)
108    }
109
110    fn upsample_stable<I: IntoVec<PlSmallStr>>(
111        &self,
112        by: I,
113        time_column: &str,
114        every: Duration,
115    ) -> PolarsResult<DataFrame> {
116        let by = by.into_vec();
117        let time_type = self.column(time_column)?.dtype();
118        ensure_duration_matches_dtype(every, time_type, "every")?;
119        upsample_impl(self, by, time_column, every, true)
120    }
121}
122
123fn upsample_impl(
124    source: &DataFrame,
125    by: Vec<PlSmallStr>,
126    index_column: &str,
127    every: Duration,
128    stable: bool,
129) -> PolarsResult<DataFrame> {
130    let s = source.column(index_column)?;
131    let original_type = s.dtype();
132
133    let needs_cast = matches!(
134        original_type,
135        DataType::Date | DataType::UInt32 | DataType::UInt64 | DataType::Int32 | DataType::Int64
136    );
137
138    let mut df = source.clone();
139
140    if needs_cast {
141        df.try_apply(index_column, |s| match s.dtype() {
142            #[cfg(feature = "dtype-date")]
143            DataType::Date => s.cast(&DataType::Datetime(TimeUnit::Microseconds, None)),
144            DataType::UInt32 | DataType::UInt64 | DataType::Int32 => s
145                .cast(&DataType::Int64)?
146                .cast(&DataType::Datetime(TimeUnit::Nanoseconds, None)),
147            DataType::Int64 => s.cast(&DataType::Datetime(TimeUnit::Nanoseconds, None)),
148            _ => Ok(s.clone()),
149        })?;
150    }
151
152    let mut out = upsample_core(&df, by, index_column, every, stable)?;
153
154    if needs_cast {
155        out.try_apply(index_column, |s| s.cast(original_type))?;
156    }
157
158    Ok(out)
159}
160
161fn upsample_core(
162    source: &DataFrame,
163    by: Vec<PlSmallStr>,
164    index_column: &str,
165    every: Duration,
166    stable: bool,
167) -> PolarsResult<DataFrame> {
168    if by.is_empty() {
169        let index_column = source.column(index_column)?;
170        return upsample_single_impl(source, index_column.as_materialized_series(), every);
171    }
172
173    if source.height() == 0 {
174        polars_bail!(
175            ComputeError: "cannot determine upsample boundaries: all elements are null"
176        );
177    }
178
179    let source_schema = source.schema();
180
181    let group_keys_df = source.select(by)?;
182    let group_keys_schema = group_keys_df.schema();
183
184    let groups = if stable {
185        group_keys_df.group_by_stable(group_keys_schema.iter_names_cloned())
186    } else {
187        group_keys_df.group_by(group_keys_schema.iter_names_cloned())
188    }?
189    .into_groups();
190
191    let non_group_keys_df = unsafe {
192        source.select_unchecked(
193            source_schema
194                .iter_names()
195                .filter(|name| !group_keys_schema.contains(name.as_str())),
196        )?
197    };
198
199    let upsample_index_col_idx: Option<usize> = non_group_keys_df.schema().index_of(index_column);
200
201    // don't parallelize this, this may SO on large data.
202    let dfs: Vec<DataFrame> = groups
203        .iter()
204        .map(|g| {
205            let first_idx = g.first();
206
207            let mut non_group_keys_df = unsafe { non_group_keys_df.gather_group_unchecked(&g) };
208
209            if let Some(i) = upsample_index_col_idx {
210                non_group_keys_df = upsample_single_impl(
211                    &non_group_keys_df,
212                    non_group_keys_df.columns()[i].as_materialized_series(),
213                    every,
214                )?
215            }
216
217            let mut out = non_group_keys_df;
218
219            let group_keys_df = group_keys_df.new_from_index(first_idx as usize, out.height());
220
221            let out_cols = unsafe { out.columns_mut() };
222
223            out_cols.reserve(group_keys_df.width());
224            out_cols.extend(group_keys_df.into_columns());
225
226            Ok(out)
227        })
228        .collect::<PolarsResult<_>>()?;
229
230    Ok(unsafe {
231        accumulate_dataframes_vertical_unchecked(dfs)
232            .select_unchecked(source_schema.iter_names())?
233            .with_schema(source_schema.clone())
234    })
235}
236
237fn upsample_single_impl(
238    source: &DataFrame,
239    index_column: &Series,
240    every: Duration,
241) -> PolarsResult<DataFrame> {
242    index_column.ensure_sorted_arg("upsample")?;
243    let index_col_name = index_column.name();
244
245    use DataType::*;
246    match index_column.dtype() {
247        #[cfg(any(feature = "dtype-date", feature = "dtype-datetime"))]
248        Datetime(tu, tz) => {
249            let s = index_column.cast(&Int64).unwrap();
250            let ca = s.i64().unwrap();
251            let first = ca.iter().flatten().next();
252            let last = ca.iter().flatten().next_back();
253            match (first, last) {
254                (Some(first), Some(last)) => {
255                    let tz = match tz {
256                        #[cfg(feature = "timezones")]
257                        Some(tz) => Some(parse_time_zone(tz)?),
258                        _ => None,
259                    };
260                    let range = datetime_range_impl(
261                        index_col_name.clone(),
262                        first,
263                        last,
264                        every,
265                        ClosedWindow::Both,
266                        *tu,
267                        tz.as_ref(),
268                    )?
269                    .into_series()
270                    .into_frame();
271                    range.join(
272                        source,
273                        [index_col_name.clone()],
274                        [index_col_name.clone()],
275                        JoinArgs::new(JoinType::Left),
276                        None,
277                    )
278                },
279                _ => polars_bail!(
280                    ComputeError: "cannot determine upsample boundaries: all elements are null"
281                ),
282            }
283        },
284        dt => polars_bail!(
285            ComputeError: "upsample not allowed for index column of dtype {}", dt,
286        ),
287    }
288}