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 fn upsample<I: IntoVec<PlSmallStr>>(
49 &self,
50 by: I,
51 time_column: &str,
52 every: Duration,
53 ) -> PolarsResult<DataFrame>;
54
55 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 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}