|
31 | 31 |
|
32 | 32 | np.random.seed(0) |
33 | 33 |
|
34 | | -PROJECT_NAME = "default" |
35 | | - |
36 | | - |
37 | | -def stage_driver_hourly_stats_parquet_source(directory, df): |
38 | | - # Write to disk |
39 | | - driver_stats_path = os.path.join(directory, "driver_stats.parquet") |
40 | | - df.to_parquet(path=driver_stats_path, allow_truncated_timestamps=True) |
41 | | - return FileSource( |
42 | | - path=driver_stats_path, |
43 | | - event_timestamp_column="event_timestamp", |
44 | | - created_timestamp_column="", |
45 | | - ) |
46 | | - |
47 | | - |
48 | | -def stage_driver_hourly_stats_bigquery_source(df, table_id): |
49 | | - client = bigquery.Client() |
50 | | - df.reset_index(drop=True, inplace=True) |
51 | | - job = _write_df_to_bq(client, df, table_id) |
52 | | - job.result() |
53 | | - |
54 | | - |
55 | | -def create_driver_hourly_stats_feature_view(source): |
56 | | - driver_stats_feature_view = FeatureView( |
57 | | - name="driver_stats", |
58 | | - entities=["driver"], |
59 | | - features=[ |
60 | | - Feature(name="conv_rate", dtype=ValueType.FLOAT), |
61 | | - Feature(name="acc_rate", dtype=ValueType.FLOAT), |
62 | | - Feature(name="avg_daily_trips", dtype=ValueType.INT32), |
63 | | - ], |
64 | | - batch_source=source, |
65 | | - ttl=timedelta(hours=2), |
66 | | - ) |
67 | | - return driver_stats_feature_view |
68 | | - |
69 | | - |
70 | | -def stage_customer_daily_profile_parquet_source(directory, df): |
71 | | - customer_profile_path = os.path.join(directory, "customer_profile.parquet") |
72 | | - df.to_parquet(path=customer_profile_path, allow_truncated_timestamps=True) |
73 | | - return FileSource( |
74 | | - path=customer_profile_path, |
75 | | - event_timestamp_column="event_timestamp", |
76 | | - created_timestamp_column="created", |
77 | | - ) |
78 | | - |
79 | | - |
80 | | -def feature_service(name: str, views) -> FeatureService: |
81 | | - return FeatureService(name, views) |
82 | | - |
83 | | - |
84 | | -def stage_customer_daily_profile_bigquery_source(df, table_id): |
85 | | - client = bigquery.Client() |
86 | | - df.reset_index(drop=True, inplace=True) |
87 | | - job = _write_df_to_bq(client, df, table_id) |
88 | | - job.result() |
89 | | - |
90 | | - |
91 | | -def create_customer_daily_profile_feature_view(source): |
92 | | - customer_profile_feature_view = FeatureView( |
93 | | - name="customer_profile", |
94 | | - entities=["customer_id"], |
95 | | - features=[ |
96 | | - Feature(name="current_balance", dtype=ValueType.FLOAT), |
97 | | - Feature(name="avg_passenger_count", dtype=ValueType.FLOAT), |
98 | | - Feature(name="lifetime_trip_count", dtype=ValueType.INT32), |
99 | | - Feature(name="avg_daily_trips", dtype=ValueType.INT32), |
100 | | - ], |
101 | | - batch_source=source, |
102 | | - ttl=timedelta(days=2), |
103 | | - ) |
104 | | - return customer_profile_feature_view |
105 | | - |
106 | | - |
107 | | -# Converts the given column of the pandas records to UTC timestamps |
108 | | -def convert_timestamp_records_to_utc(records, column): |
109 | | - for record in records: |
110 | | - record[column] = utils.make_tzaware(record[column]).astimezone(utc) |
111 | | - return records |
112 | | - |
113 | | - |
114 | | -# Find the latest record in the given time range and filter |
115 | | -def find_asof_record(records, ts_key, ts_start, ts_end, filter_key, filter_value): |
116 | | - found_record = {} |
117 | | - for record in records: |
118 | | - if record[filter_key] == filter_value and ts_start <= record[ts_key] <= ts_end: |
119 | | - if not found_record or found_record[ts_key] < record[ts_key]: |
120 | | - found_record = record |
121 | | - return found_record |
122 | | - |
123 | | - |
124 | | -def get_expected_training_df( |
125 | | - customer_df: pd.DataFrame, |
126 | | - customer_fv: FeatureView, |
127 | | - driver_df: pd.DataFrame, |
128 | | - driver_fv: FeatureView, |
129 | | - orders_df: pd.DataFrame, |
130 | | - event_timestamp: str, |
131 | | - full_feature_names: bool = False, |
132 | | -): |
133 | | - # Convert all pandas dataframes into records with UTC timestamps |
134 | | - order_records = convert_timestamp_records_to_utc( |
135 | | - orders_df.to_dict("records"), event_timestamp |
136 | | - ) |
137 | | - driver_records = convert_timestamp_records_to_utc( |
138 | | - driver_df.to_dict("records"), driver_fv.batch_source.event_timestamp_column |
139 | | - ) |
140 | | - customer_records = convert_timestamp_records_to_utc( |
141 | | - customer_df.to_dict("records"), customer_fv.batch_source.event_timestamp_column |
142 | | - ) |
143 | | - |
144 | | - # Manually do point-in-time join of orders to drivers and customers records |
145 | | - for order_record in order_records: |
146 | | - driver_record = find_asof_record( |
147 | | - driver_records, |
148 | | - ts_key=driver_fv.batch_source.event_timestamp_column, |
149 | | - ts_start=order_record[event_timestamp] - driver_fv.ttl, |
150 | | - ts_end=order_record[event_timestamp], |
151 | | - filter_key="driver_id", |
152 | | - filter_value=order_record["driver_id"], |
153 | | - ) |
154 | | - customer_record = find_asof_record( |
155 | | - customer_records, |
156 | | - ts_key=customer_fv.batch_source.event_timestamp_column, |
157 | | - ts_start=order_record[event_timestamp] - customer_fv.ttl, |
158 | | - ts_end=order_record[event_timestamp], |
159 | | - filter_key="customer_id", |
160 | | - filter_value=order_record["customer_id"], |
161 | | - ) |
162 | | - |
163 | | - order_record.update( |
164 | | - { |
165 | | - (f"driver_stats__{k}" if full_feature_names else k): driver_record.get( |
166 | | - k, None |
167 | | - ) |
168 | | - for k in ("conv_rate", "avg_daily_trips") |
169 | | - } |
170 | | - ) |
171 | | - |
172 | | - order_record.update( |
173 | | - { |
174 | | - ( |
175 | | - f"customer_profile__{k}" if full_feature_names else k |
176 | | - ): customer_record.get(k, None) |
177 | | - for k in ( |
178 | | - "current_balance", |
179 | | - "avg_passenger_count", |
180 | | - "lifetime_trip_count", |
181 | | - ) |
182 | | - } |
183 | | - ) |
184 | | - |
185 | | - # Convert records back to pandas dataframe |
186 | | - expected_df = pd.DataFrame(order_records) |
187 | | - |
188 | | - # Move "event_timestamp" column to front |
189 | | - current_cols = expected_df.columns.tolist() |
190 | | - current_cols.remove(event_timestamp) |
191 | | - expected_df = expected_df[[event_timestamp] + current_cols] |
192 | | - |
193 | | - # Cast some columns to expected types, since we lose information when converting pandas DFs into Python objects. |
194 | | - if full_feature_names: |
195 | | - expected_column_types = { |
196 | | - "order_is_success": "int32", |
197 | | - "driver_stats__conv_rate": "float32", |
198 | | - "customer_profile__current_balance": "float32", |
199 | | - "customer_profile__avg_passenger_count": "float32", |
200 | | - } |
201 | | - else: |
202 | | - expected_column_types = { |
203 | | - "order_is_success": "int32", |
204 | | - "conv_rate": "float32", |
205 | | - "current_balance": "float32", |
206 | | - "avg_passenger_count": "float32", |
207 | | - } |
208 | | - |
209 | | - for col, typ in expected_column_types.items(): |
210 | | - expected_df[col] = expected_df[col].astype(typ) |
211 | | - |
212 | | - return expected_df |
213 | | - |
214 | | - |
215 | | -def stage_orders_bigquery(df, table_id): |
216 | | - client = bigquery.Client() |
217 | | - df.reset_index(drop=True, inplace=True) |
218 | | - job = _write_df_to_bq(client, df, table_id) |
219 | | - job.result() |
220 | | - |
221 | | - |
222 | | -class BigQueryDataSet: |
223 | | - def __init__(self, dataset_name): |
224 | | - self.name = dataset_name |
225 | | - |
226 | | - def __enter__(self): |
227 | | - client = bigquery.Client() |
228 | | - dataset = bigquery.Dataset(f"{client.project}.{self.name}") |
229 | | - dataset.location = "US" |
230 | | - print(f"Creating dataset: {dataset}") |
231 | | - dataset = client.create_dataset(dataset, exists_ok=True) |
232 | | - return dataset |
233 | | - |
234 | | - def __exit__(self, exc_type, exc_value, exc_traceback): |
235 | | - print("Tearing down BigQuery dataset") |
236 | | - client = bigquery.Client() |
237 | | - dataset_id = f"{client.project}.{self.name}" |
238 | | - |
239 | | - client.delete_dataset(dataset_id, delete_contents=True, not_found_ok=True) |
240 | | - print(f"Deleted dataset '{dataset_id}'") |
241 | | - if exc_type: |
242 | | - print( |
243 | | - "***Logging exception {}***".format( |
244 | | - (exc_type, exc_value, exc_traceback) |
245 | | - ) |
246 | | - ) |
247 | | - |
248 | | - |
249 | 34 | def test_feature_name_collision_on_historical_retrieval(): |
250 | 35 |
|
251 | 36 | # _validate_feature_refs is the function that checks for colliding feature names |
@@ -292,137 +77,3 @@ def test_feature_name_collision_on_historical_retrieval(): |
292 | 77 | "have different names." |
293 | 78 | ) |
294 | 79 | assert str(error.value) == expected_error_message |
295 | | - |
296 | | - |
297 | | -@pytest.mark.integration |
298 | | -def test_historical_features_from_bigquery_sources_containing_backfills(capsys): |
299 | | - now = datetime.now().replace(microsecond=0, second=0, minute=0) |
300 | | - tomorrow = now + timedelta(days=1) |
301 | | - |
302 | | - entity_dataframe = pd.DataFrame( |
303 | | - data=[ |
304 | | - {"driver_id": 1001, "event_timestamp": now + timedelta(days=2)}, |
305 | | - {"driver_id": 1002, "event_timestamp": now + timedelta(days=2)}, |
306 | | - ] |
307 | | - ) |
308 | | - |
309 | | - driver_stats_df = pd.DataFrame( |
310 | | - data=[ |
311 | | - # Duplicated rows simple case |
312 | | - { |
313 | | - "driver_id": 1001, |
314 | | - "avg_daily_trips": 10, |
315 | | - "event_timestamp": now, |
316 | | - "created": tomorrow, |
317 | | - }, |
318 | | - { |
319 | | - "driver_id": 1001, |
320 | | - "avg_daily_trips": 20, |
321 | | - "event_timestamp": tomorrow, |
322 | | - "created": tomorrow, |
323 | | - }, |
324 | | - # Duplicated rows after a backfill |
325 | | - { |
326 | | - "driver_id": 1002, |
327 | | - "avg_daily_trips": 30, |
328 | | - "event_timestamp": now, |
329 | | - "created": tomorrow, |
330 | | - }, |
331 | | - { |
332 | | - "driver_id": 1002, |
333 | | - "avg_daily_trips": 40, |
334 | | - "event_timestamp": tomorrow, |
335 | | - "created": now, |
336 | | - }, |
337 | | - ] |
338 | | - ) |
339 | | - |
340 | | - expected_df = pd.DataFrame( |
341 | | - data=[ |
342 | | - { |
343 | | - "driver_id": 1001, |
344 | | - "event_timestamp": now + timedelta(days=2), |
345 | | - "avg_daily_trips": 20, |
346 | | - }, |
347 | | - { |
348 | | - "driver_id": 1002, |
349 | | - "event_timestamp": now + timedelta(days=2), |
350 | | - "avg_daily_trips": 40, |
351 | | - }, |
352 | | - ] |
353 | | - ) |
354 | | - |
355 | | - bigquery_dataset = ( |
356 | | - f"test_hist_retrieval_{int(time.time_ns())}_{random.randint(1000, 9999)}" |
357 | | - ) |
358 | | - |
359 | | - with BigQueryDataSet(bigquery_dataset), TemporaryDirectory() as temp_dir: |
360 | | - gcp_project = bigquery.Client().project |
361 | | - |
362 | | - # Entity Dataframe SQL query |
363 | | - table_id = f"{bigquery_dataset}.orders" |
364 | | - stage_orders_bigquery(entity_dataframe, table_id) |
365 | | - entity_df_query = f"SELECT * FROM {gcp_project}.{table_id}" |
366 | | - |
367 | | - # Driver Feature View |
368 | | - driver_table_id = f"{gcp_project}.{bigquery_dataset}.driver_hourly" |
369 | | - stage_driver_hourly_stats_bigquery_source(driver_stats_df, driver_table_id) |
370 | | - |
371 | | - store = FeatureStore( |
372 | | - config=RepoConfig( |
373 | | - registry=os.path.join(temp_dir, "registry.db"), |
374 | | - project="".join( |
375 | | - random.choices(string.ascii_uppercase + string.digits, k=10) |
376 | | - ), |
377 | | - provider="gcp", |
378 | | - offline_store=BigQueryOfflineStoreConfig( |
379 | | - type="bigquery", dataset=bigquery_dataset |
380 | | - ), |
381 | | - ) |
382 | | - ) |
383 | | - |
384 | | - driver = Entity(name="driver", join_key="driver_id", value_type=ValueType.INT64) |
385 | | - driver_fv = FeatureView( |
386 | | - name="driver_stats", |
387 | | - entities=["driver"], |
388 | | - features=[Feature(name="avg_daily_trips", dtype=ValueType.INT32)], |
389 | | - batch_source=BigQuerySource( |
390 | | - table_ref=driver_table_id, |
391 | | - event_timestamp_column="event_timestamp", |
392 | | - created_timestamp_column="created", |
393 | | - ), |
394 | | - ttl=None, |
395 | | - ) |
396 | | - |
397 | | - store.apply([driver, driver_fv]) |
398 | | - |
399 | | - try: |
400 | | - job_from_sql = store.get_historical_features( |
401 | | - entity_df=entity_df_query, |
402 | | - features=["driver_stats:avg_daily_trips"], |
403 | | - full_feature_names=False, |
404 | | - ) |
405 | | - |
406 | | - start_time = datetime.utcnow() |
407 | | - actual_df_from_sql_entities = job_from_sql.to_df() |
408 | | - end_time = datetime.utcnow() |
409 | | - with capsys.disabled(): |
410 | | - print( |
411 | | - str( |
412 | | - f"\nTime to execute job_from_sql.to_df() = '{(end_time - start_time)}'" |
413 | | - ) |
414 | | - ) |
415 | | - |
416 | | - assert sorted(expected_df.columns) == sorted( |
417 | | - actual_df_from_sql_entities.columns |
418 | | - ) |
419 | | - assert_frame_equal( |
420 | | - expected_df.sort_values(by=["driver_id"]).reset_index(drop=True), |
421 | | - actual_df_from_sql_entities[expected_df.columns] |
422 | | - .sort_values(by=["driver_id"]) |
423 | | - .reset_index(drop=True), |
424 | | - check_dtype=False, |
425 | | - ) |
426 | | - |
427 | | - finally: |
428 | | - store.teardown() |
0 commit comments