heisenberg_data_processing/processed/
mod.rs1use std::path::Path;
2
3use polars::prelude::*;
4use tracing::{info, info_span};
5
6use super::error::Result;
7use crate::raw::fetch::TempData;
8
9pub mod create_admin_search;
10pub mod create_place_search;
11
12pub fn generate_processed_data(temp_data: TempData) -> Result<(DataFrame, DataFrame)> {
14 let mut dfs = {
17 let _span = info_span!("Transform Raw Data").entered();
18
19 let admin_search_lf = create_admin_search::get_admin_search_lf(&temp_data);
20
21 let place_search_lf =
22 create_place_search::get_place_search_lf(&temp_data, admin_search_lf.clone());
23 info!("Collecting transformed data");
24 let transform_time = std::time::Instant::now();
25 let dfs = collect_all([admin_search_lf, place_search_lf])?;
26 info!(
27 transform_time = ?transform_time.elapsed()
28 , "Transforming data took"
29 );
30 dfs
31 };
32
33 let place_search_df = dfs.pop().expect("Place search should be last");
34 let admin_search_df = dfs.pop().expect("Admin search should be first");
35
36 drop(temp_data); Ok((admin_search_df, place_search_df))
39}
40
41pub fn save_processed_data_to_parquet(df: DataFrame, path: &Path) -> Result<()> {
42 let sink_time = std::time::Instant::now();
43
44 let mut df = df
45 .lazy()
46 .drop_nulls(col("geonameId").into_selector())
47 .sort(["geonameId"], SortMultipleOptions::default())
48 .collect()?;
49 let mut file = std::fs::File::create(path)?;
50 ParquetWriter::new(&mut file).finish(&mut df)?;
51
52 info!(
53 path = ?path.file_stem(),
54 sink_time = ?sink_time.elapsed(),
55 "Saved to parquet file"
56 );
57 Ok(())
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63 use crate::tests_utils::*;
64
65 #[test]
66 fn test_create_admin_search_actual_transformation() {
67 let temp_data = crate::raw::test_data::create_test_data();
69
70 let result = create_admin_search::get_admin_search_lf(&temp_data);
72 let df = result.collect().unwrap();
73
74 assert!(df.height() > 0, "Admin search should have results");
76
77 assert_has_columns(&df, &["admin_level"]);
79 assert_column_type(&df, "admin_level", &DataType::UInt8);
80
81 let admin_levels: Vec<Option<u8>> = df
83 .column("admin_level")
84 .unwrap()
85 .u8()
86 .unwrap()
87 .into_iter()
88 .collect();
89 for level in admin_levels.iter().flatten() {
90 assert!(*level < 5, "All admin levels should be < 5, found: {level}");
91 }
92
93 let feature_codes: Vec<Option<&str>> = df
95 .column("feature_code")
96 .unwrap()
97 .str()
98 .unwrap()
99 .into_iter()
100 .collect();
101 assert!(
102 feature_codes.contains(&Some("PCLI")),
103 "Should contain country-level entity"
104 );
105 assert!(
106 feature_codes.contains(&Some("ADM1")),
107 "Should contain state-level entity"
108 );
109 }
110
111 #[test]
112 fn test_create_place_search_actual_transformation() {
113 let temp_data = crate::raw::test_data::create_test_data();
115
116 let all_countries_df = temp_data.places.as_lazy_frame().collect().unwrap();
118 println!(
119 "All countries data (before anti-join): {} rows",
120 all_countries_df.height()
121 );
122
123 let admin_search_lf = create_admin_search::get_admin_search_lf(&temp_data);
125 let admin_search_df = admin_search_lf.clone().collect().unwrap();
126 println!("Admin search data: {} rows", admin_search_df.height());
127
128 let after_anti_join = temp_data
130 .places
131 .as_lazy_frame()
132 .join(
133 admin_search_lf.clone(),
134 [col("geonameId")],
135 [col("geonameId")],
136 JoinArgs {
137 how: JoinType::Anti,
138 ..Default::default()
139 },
140 )
141 .collect()
142 .unwrap();
143 println!("After anti-join: {} rows", after_anti_join.height());
144
145 let after_filter = temp_data
147 .places
148 .as_lazy_frame()
149 .join(
150 admin_search_lf.clone(),
151 [col("geonameId")],
152 [col("geonameId")],
153 JoinArgs {
154 how: JoinType::Anti,
155 ..Default::default()
156 },
157 )
158 .filter(
159 (col("admin0_code").is_not_null()).and(
160 col("feature_class").is_in(
161 lit(Series::new(
162 "feature_class_to_keep".into(),
163 ["P", "S", "T", "H", "L", "V", "R"],
164 ))
165 .implode(),
166 false,
167 ),
168 ),
169 )
170 .collect()
171 .unwrap();
172 println!("After filter: {} rows", after_filter.height());
173 println!("After filter data: {after_filter:?}");
174
175 let feature_codes_df = temp_data.feature_codes.as_lazy_frame().collect().unwrap();
177 println!("Feature codes data: {} rows", feature_codes_df.height());
178 println!("Feature codes: {feature_codes_df:?}");
179
180 let result = create_place_search::get_place_search_lf(&temp_data, admin_search_lf);
182
183 let df = result.collect().unwrap();
184 println!("Final result: {} rows", df.height());
185 if df.height() > 0 {
186 println!("Final data: {df:?}");
187 }
188
189 assert!(df.height() > 0, "Place search should have results");
191
192 assert_has_columns(&df, &["importance_score", "importance_tier"]);
194 assert_column_type(&df, "importance_score", &DataType::Float64);
195 assert_column_type(&df, "importance_tier", &DataType::UInt8);
196
197 assert_column_range(&df, "importance_score", 0.0f64, 1.0f64);
199 assert_column_range(&df, "importance_tier", 1u8, 5u8);
200
201 let feature_classes: Vec<Option<&str>> = df
203 .column("feature_class")
204 .unwrap()
205 .str()
206 .unwrap()
207 .into_iter()
208 .collect();
209 assert!(
210 feature_classes.contains(&Some("P")),
211 "Should contain places"
212 );
213 assert!(
214 !feature_classes.contains(&Some("A")),
215 "Should not contain admin entities after anti-join"
216 );
217 }
218}