dfm-python 0.1.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dfm_python/__init__.py +60 -0
- dfm_python/config.py +482 -0
- dfm_python/data_loader.py +449 -0
- dfm_python/dfm.py +2366 -0
- dfm_python/kalman.py +455 -0
- dfm_python/news.py +732 -0
- dfm_python/utils/__init__.py +27 -0
- dfm_python/utils/aggregation.py +258 -0
- dfm_python/utils/data_utils.py +165 -0
- dfm_python-0.1.4.dist-info/METADATA +1010 -0
- dfm_python-0.1.4.dist-info/RECORD +13 -0
- dfm_python-0.1.4.dist-info/WHEEL +4 -0
- dfm_python-0.1.4.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
"""Data loading and transformation utilities for DFM estimation.
|
|
2
|
+
|
|
3
|
+
This module provides comprehensive data handling for Dynamic Factor Models:
|
|
4
|
+
- Configuration loading from YAML and CSV files
|
|
5
|
+
- Data loading from CSV with automatic date parsing
|
|
6
|
+
- Time series transformations (differences, percent changes, etc.)
|
|
7
|
+
- Data sorting and alignment with configuration
|
|
8
|
+
|
|
9
|
+
The module supports flexible configuration formats and handles common data
|
|
10
|
+
issues such as missing dates, inconsistent formats, and transformation errors.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
import pandas as pd
|
|
15
|
+
import io
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import List, Optional, Tuple, Union
|
|
18
|
+
import warnings
|
|
19
|
+
import logging
|
|
20
|
+
|
|
21
|
+
from .config import DFMConfig
|
|
22
|
+
|
|
23
|
+
# Backward compatibility alias (deprecated - use DFMConfig)
|
|
24
|
+
ModelConfig = DFMConfig
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
from omegaconf import OmegaConf
|
|
30
|
+
OMEGACONF_AVAILABLE = True
|
|
31
|
+
except ImportError:
|
|
32
|
+
OMEGACONF_AVAILABLE = False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def load_config_from_yaml(configfile: Union[str, Path]) -> DFMConfig:
|
|
36
|
+
"""Load model configuration from YAML file.
|
|
37
|
+
|
|
38
|
+
Parameters
|
|
39
|
+
----------
|
|
40
|
+
configfile : str or Path
|
|
41
|
+
Path to YAML configuration file
|
|
42
|
+
|
|
43
|
+
Returns
|
|
44
|
+
-------
|
|
45
|
+
ModelConfig
|
|
46
|
+
Model configuration (dataclass with validation)
|
|
47
|
+
|
|
48
|
+
Raises
|
|
49
|
+
------
|
|
50
|
+
FileNotFoundError
|
|
51
|
+
If configfile does not exist
|
|
52
|
+
ImportError
|
|
53
|
+
If omegaconf is not available
|
|
54
|
+
ValueError
|
|
55
|
+
If configuration is invalid
|
|
56
|
+
"""
|
|
57
|
+
if not OMEGACONF_AVAILABLE:
|
|
58
|
+
raise ImportError("omegaconf is required for YAML config loading. Install with: pip install omegaconf")
|
|
59
|
+
|
|
60
|
+
configfile = Path(configfile)
|
|
61
|
+
if not configfile.exists():
|
|
62
|
+
raise FileNotFoundError(f"Configuration file not found: {configfile}")
|
|
63
|
+
|
|
64
|
+
cfg = OmegaConf.load(configfile)
|
|
65
|
+
cfg_dict = OmegaConf.to_container(cfg, resolve=True)
|
|
66
|
+
|
|
67
|
+
# Extract model config and dfm parameters
|
|
68
|
+
model_dict = None
|
|
69
|
+
dfm_params = {}
|
|
70
|
+
|
|
71
|
+
# Handle nested structure (if config has @package model: directive)
|
|
72
|
+
if 'series' in cfg_dict and 'block_names' in cfg_dict:
|
|
73
|
+
# Direct model config structure
|
|
74
|
+
model_dict = cfg_dict
|
|
75
|
+
elif 'model' in cfg_dict:
|
|
76
|
+
# Nested under 'model' key (from @package model:)
|
|
77
|
+
model_dict = cfg_dict['model']
|
|
78
|
+
else:
|
|
79
|
+
# Try to construct from top-level keys
|
|
80
|
+
model_dict = cfg_dict
|
|
81
|
+
|
|
82
|
+
# Extract dfm parameters (estimation parameters like clock)
|
|
83
|
+
if 'dfm' in cfg_dict:
|
|
84
|
+
dfm_params = cfg_dict['dfm']
|
|
85
|
+
|
|
86
|
+
# Merge dfm parameters into model config
|
|
87
|
+
if dfm_params:
|
|
88
|
+
if isinstance(model_dict, dict):
|
|
89
|
+
model_dict = {**model_dict, **dfm_params}
|
|
90
|
+
else:
|
|
91
|
+
# If model_dict is not a dict, create new dict
|
|
92
|
+
model_dict = {'series': model_dict, **dfm_params} if model_dict else dfm_params
|
|
93
|
+
|
|
94
|
+
return DFMConfig.from_dict(model_dict)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def load_config_from_csv(configfile: Union[str, Path]) -> DFMConfig:
|
|
98
|
+
"""Load model configuration from CSV file.
|
|
99
|
+
|
|
100
|
+
CSV format should have columns:
|
|
101
|
+
- series_id (or 'id' as alias), series_name, frequency, transformation, category, units
|
|
102
|
+
- Block columns (named after block names, e.g., Global, Consumption, Investment, External, or Block_Global, Block_Consumption, etc.)
|
|
103
|
+
- Block columns contain 0 or 1 (1 = series loads on that block)
|
|
104
|
+
|
|
105
|
+
Note: API/database-specific columns (data_code, item_id, api_source, country, is_kpi, etc.)
|
|
106
|
+
are ignored by the generic DFM module. They should be handled by application-specific adapters.
|
|
107
|
+
|
|
108
|
+
Example:
|
|
109
|
+
id,series_name,frequency,transformation,category,units,Block_Global,Block_Consumption,Block_Invest,Block_Extern
|
|
110
|
+
0,Real GDP (Quarterly),q,pca,GDP,Billion Won,1,1,0,0
|
|
111
|
+
1,Nominal GDP (Quarterly),q,pca,GDP,Billion Won,1,1,0,0
|
|
112
|
+
|
|
113
|
+
Parameters
|
|
114
|
+
----------
|
|
115
|
+
configfile : str or Path
|
|
116
|
+
Path to CSV specification file
|
|
117
|
+
|
|
118
|
+
Returns
|
|
119
|
+
-------
|
|
120
|
+
ModelConfig
|
|
121
|
+
Model configuration object
|
|
122
|
+
|
|
123
|
+
Raises
|
|
124
|
+
------
|
|
125
|
+
FileNotFoundError
|
|
126
|
+
If configfile does not exist
|
|
127
|
+
ValueError
|
|
128
|
+
If required columns are missing or data is invalid
|
|
129
|
+
"""
|
|
130
|
+
configfile = Path(configfile)
|
|
131
|
+
if not configfile.exists():
|
|
132
|
+
raise FileNotFoundError(f"Configuration file not found: {configfile}")
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
df = pd.read_csv(configfile)
|
|
136
|
+
except Exception as e:
|
|
137
|
+
raise ValueError(f"Failed to read CSV file {configfile}: {e}")
|
|
138
|
+
|
|
139
|
+
return _load_config_from_dataframe(df)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _load_config_from_dataframe(df: pd.DataFrame) -> DFMConfig:
|
|
143
|
+
"""Load DFMConfig from a pandas DataFrame (shared by CSV and BytesIO loading).
|
|
144
|
+
|
|
145
|
+
Parameters
|
|
146
|
+
----------
|
|
147
|
+
df : pd.DataFrame
|
|
148
|
+
DataFrame with CSV columns
|
|
149
|
+
|
|
150
|
+
Returns
|
|
151
|
+
-------
|
|
152
|
+
DFMConfig
|
|
153
|
+
Model configuration
|
|
154
|
+
"""
|
|
155
|
+
# Handle 'id' as alias for 'series_id' (for backward compatibility)
|
|
156
|
+
# If CSV has data_code, item_id, api_source, generate series_id from them
|
|
157
|
+
# Otherwise, use 'id' as fallback
|
|
158
|
+
if 'id' in df.columns and 'series_id' not in df.columns:
|
|
159
|
+
# Try to generate series_id from data_code, item_id, api_source if available
|
|
160
|
+
if all(col in df.columns for col in ['data_code', 'item_id', 'api_source']):
|
|
161
|
+
# Generate series_id: {api_source}_{data_code}_{item_id}
|
|
162
|
+
df['series_id'] = df.apply(
|
|
163
|
+
lambda row: f"{row.get('api_source', '')}_{row.get('data_code', '')}_{row.get('item_id', '')}",
|
|
164
|
+
axis=1
|
|
165
|
+
)
|
|
166
|
+
else:
|
|
167
|
+
# Fallback: use 'id' as series_id if series_id column doesn't exist
|
|
168
|
+
df['series_id'] = df['id'].astype(str)
|
|
169
|
+
elif 'id' in df.columns and 'series_id' in df.columns:
|
|
170
|
+
# Both exist - prefer series_id, but if it's empty, use id
|
|
171
|
+
df['series_id'] = df['series_id'].fillna(df['id'].astype(str))
|
|
172
|
+
|
|
173
|
+
# Required fields (DFM-relevant only - no API/database fields)
|
|
174
|
+
required_fields = ['series_id', 'series_name', 'frequency', 'transformation', 'category', 'units']
|
|
175
|
+
optional_fields = [] # No optional fields in generic DFM module
|
|
176
|
+
|
|
177
|
+
missing = [f for f in required_fields if f not in df.columns]
|
|
178
|
+
if missing:
|
|
179
|
+
raise ValueError(f"Missing required columns: {', '.join(missing)}")
|
|
180
|
+
|
|
181
|
+
# Detect block columns (all columns that are not in required_fields or optional_fields)
|
|
182
|
+
# Also exclude 'id' (if it was used as alias, we already created series_id from it)
|
|
183
|
+
# and other non-DFM metadata columns (API/database fields are ignored by generic DFM module)
|
|
184
|
+
# Preserve original column order from DataFrame - first block should be Global
|
|
185
|
+
excluded_fields = set(required_fields) | set(optional_fields) | {
|
|
186
|
+
'id', 'country', 'data_code', 'item_id', 'api_source', 'api_code',
|
|
187
|
+
'api_group_id', 'is_kpi', 'description', 'priority', 'is_active', 'metadata'
|
|
188
|
+
}
|
|
189
|
+
# Use DataFrame column order (not sorted) to preserve CSV structure
|
|
190
|
+
block_columns = [col for col in df.columns if col not in excluded_fields]
|
|
191
|
+
|
|
192
|
+
if not block_columns:
|
|
193
|
+
raise ValueError("No block columns found. Expected columns like 'Global', 'Consumption', etc.")
|
|
194
|
+
|
|
195
|
+
# Validate block columns contain only 0 or 1
|
|
196
|
+
for block_col in block_columns:
|
|
197
|
+
if not df[block_col].isin([0, 1]).all():
|
|
198
|
+
raise ValueError(f"Block column '{block_col}' must contain only 0 or 1")
|
|
199
|
+
|
|
200
|
+
# Ensure all series load on at least one block (first block should always be 1)
|
|
201
|
+
if block_columns[0] not in df.columns:
|
|
202
|
+
raise ValueError(f"First block column '{block_columns[0]}' is required")
|
|
203
|
+
|
|
204
|
+
if not (df[block_columns[0]] == 1).all():
|
|
205
|
+
raise ValueError(f"All series must load on the first block '{block_columns[0]}' (Global)")
|
|
206
|
+
|
|
207
|
+
# Build blocks array (N x n_blocks)
|
|
208
|
+
blocks_data = df[block_columns].values.astype(int)
|
|
209
|
+
|
|
210
|
+
# Convert to ModelConfig format
|
|
211
|
+
from .config import SeriesConfig
|
|
212
|
+
|
|
213
|
+
series_list = []
|
|
214
|
+
for idx, row in df.iterrows():
|
|
215
|
+
# Build block array from block columns
|
|
216
|
+
blocks = [int(row[col]) for col in block_columns]
|
|
217
|
+
|
|
218
|
+
# Create SeriesConfig with only DFM-relevant fields
|
|
219
|
+
# API/database fields (data_code, item_id, api_source, etc.) are ignored by generic DFM module
|
|
220
|
+
series_list.append(SeriesConfig(
|
|
221
|
+
series_id=str(row['series_id']), # Ensure string type
|
|
222
|
+
series_name=row['series_name'],
|
|
223
|
+
frequency=row['frequency'],
|
|
224
|
+
transformation=row['transformation'],
|
|
225
|
+
category=row['category'],
|
|
226
|
+
units=row['units'],
|
|
227
|
+
blocks=blocks
|
|
228
|
+
))
|
|
229
|
+
|
|
230
|
+
return DFMConfig(series=series_list, block_names=block_columns)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# Note: Excel support removed - use CSV or YAML configs instead
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def load_config(configfile: Union[str, Path, io.BytesIO]) -> DFMConfig:
|
|
237
|
+
"""Load model configuration from file (auto-detects YAML or CSV).
|
|
238
|
+
|
|
239
|
+
Parameters
|
|
240
|
+
----------
|
|
241
|
+
configfile : str, Path, or BytesIO
|
|
242
|
+
Path to configuration file (.yaml, .yml, or .csv), or BytesIO object with CSV content
|
|
243
|
+
|
|
244
|
+
Returns
|
|
245
|
+
-------
|
|
246
|
+
ModelConfig
|
|
247
|
+
Model configuration (dataclass with validation)
|
|
248
|
+
|
|
249
|
+
Raises
|
|
250
|
+
------
|
|
251
|
+
FileNotFoundError
|
|
252
|
+
If configfile does not exist (for file paths)
|
|
253
|
+
ValueError
|
|
254
|
+
If file format is not supported or configuration is invalid
|
|
255
|
+
"""
|
|
256
|
+
# Handle BytesIO (for database storage downloads)
|
|
257
|
+
if isinstance(configfile, io.BytesIO):
|
|
258
|
+
# Read CSV from BytesIO
|
|
259
|
+
configfile.seek(0) # Reset to beginning
|
|
260
|
+
df = pd.read_csv(configfile)
|
|
261
|
+
# Use the same logic as load_config_from_csv but with DataFrame
|
|
262
|
+
return _load_config_from_dataframe(df)
|
|
263
|
+
|
|
264
|
+
configfile = Path(configfile)
|
|
265
|
+
if not configfile.exists():
|
|
266
|
+
raise FileNotFoundError(f"Configuration file not found: {configfile}")
|
|
267
|
+
|
|
268
|
+
suffix = configfile.suffix.lower()
|
|
269
|
+
if suffix in ['.yaml', '.yml']:
|
|
270
|
+
return load_config_from_yaml(configfile)
|
|
271
|
+
elif suffix == '.csv':
|
|
272
|
+
return load_config_from_csv(configfile)
|
|
273
|
+
else:
|
|
274
|
+
raise ValueError(f"Unsupported file format: {suffix}. Use .yaml, .yml, or .csv")
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _transform_series(Z: np.ndarray, formula: str, freq: str, step: int) -> np.ndarray:
|
|
278
|
+
"""Apply transformation to a single series."""
|
|
279
|
+
T = Z.shape[0]
|
|
280
|
+
X = np.full(T, np.nan)
|
|
281
|
+
t1 = step
|
|
282
|
+
n = step / 12
|
|
283
|
+
|
|
284
|
+
if formula == 'lin':
|
|
285
|
+
X[:] = Z
|
|
286
|
+
elif formula == 'chg':
|
|
287
|
+
idx = np.arange(t1, T, step)
|
|
288
|
+
if len(idx) > 1:
|
|
289
|
+
X[idx[0]] = np.nan
|
|
290
|
+
X[idx[1:]] = Z[idx[1:]] - Z[idx[:-1]]
|
|
291
|
+
elif formula == 'ch1':
|
|
292
|
+
idx = np.arange(12 + t1, T, step)
|
|
293
|
+
if len(idx) > 0:
|
|
294
|
+
X[idx] = Z[idx] - Z[idx - 12]
|
|
295
|
+
elif formula == 'pch':
|
|
296
|
+
idx = np.arange(t1, T, step)
|
|
297
|
+
if len(idx) > 1:
|
|
298
|
+
X[idx[0]] = np.nan
|
|
299
|
+
X[idx[1:]] = 100 * (Z[idx[1:]] / Z[idx[:-1]] - 1)
|
|
300
|
+
elif formula == 'pc1':
|
|
301
|
+
idx = np.arange(12 + t1, T, step)
|
|
302
|
+
if len(idx) > 0:
|
|
303
|
+
with np.errstate(divide='ignore', invalid='ignore'):
|
|
304
|
+
X[idx] = 100 * (Z[idx] / Z[idx - 12] - 1)
|
|
305
|
+
X[np.isinf(X)] = np.nan
|
|
306
|
+
elif formula == 'pca':
|
|
307
|
+
idx = np.arange(t1, T, step)
|
|
308
|
+
if len(idx) > 1:
|
|
309
|
+
X[idx[0]] = np.nan
|
|
310
|
+
with np.errstate(divide='ignore', invalid='ignore'):
|
|
311
|
+
X[idx[1:]] = 100 * ((Z[idx[1:]] / Z[idx[:-1]]) ** (1/n) - 1)
|
|
312
|
+
X[np.isinf(X)] = np.nan
|
|
313
|
+
elif formula == 'log':
|
|
314
|
+
with np.errstate(invalid='ignore'):
|
|
315
|
+
X[:] = np.log(Z)
|
|
316
|
+
else:
|
|
317
|
+
X[:] = Z
|
|
318
|
+
|
|
319
|
+
return X
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def transform_data(Z: np.ndarray, Time: pd.DatetimeIndex, config: DFMConfig) -> Tuple[np.ndarray, pd.DatetimeIndex, np.ndarray]:
|
|
323
|
+
"""Transform each data series based on specification.
|
|
324
|
+
|
|
325
|
+
Handles all frequencies: daily (d), weekly (w), monthly (m),
|
|
326
|
+
quarterly (q), semi-annual (sa), annual (a).
|
|
327
|
+
"""
|
|
328
|
+
T, N = Z.shape
|
|
329
|
+
X = np.full((T, N), np.nan)
|
|
330
|
+
|
|
331
|
+
# Frequency to step mapping (step = number of base periods per observation)
|
|
332
|
+
# Base frequency is monthly, so step is months per observation
|
|
333
|
+
freq_to_step = {
|
|
334
|
+
'd': 1, # Daily: 1 month per observation (aggregated to monthly)
|
|
335
|
+
'w': 1, # Weekly: 1 month per observation (aggregated to monthly)
|
|
336
|
+
'm': 1, # Monthly: 1 month per observation
|
|
337
|
+
'q': 3, # Quarterly: 3 months per observation
|
|
338
|
+
'sa': 6, # Semi-annual: 6 months per observation
|
|
339
|
+
'a': 12, # Annual: 12 months per observation
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
for i in range(N):
|
|
343
|
+
freq = config.Frequency[i]
|
|
344
|
+
step = freq_to_step.get(freq, 1) # Default to 1 if unknown frequency
|
|
345
|
+
X[:, i] = _transform_series(Z[:, i], config.Transformation[i], freq, step)
|
|
346
|
+
|
|
347
|
+
# Drop initial observations based on highest frequency in data
|
|
348
|
+
# Find maximum step (longest period) to determine drop period
|
|
349
|
+
max_step = max([freq_to_step.get(f, 1) for f in config.Frequency])
|
|
350
|
+
# Drop period should be at least max_step + 1 to handle transformations
|
|
351
|
+
drop = max(4, max_step + 1) # At least 4 for quarterly, more for annual
|
|
352
|
+
|
|
353
|
+
if T > drop:
|
|
354
|
+
return X[drop:], Time[drop:], Z[drop:]
|
|
355
|
+
return X, Time, Z
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def read_data(datafile: Union[str, Path]) -> Tuple[np.ndarray, pd.DatetimeIndex, List[str]]:
|
|
359
|
+
"""Read data from CSV file.
|
|
360
|
+
|
|
361
|
+
CSV file should have:
|
|
362
|
+
- First column: Date (YYYY-MM-DD format or pandas parseable)
|
|
363
|
+
- Subsequent columns: Series data (one column per series)
|
|
364
|
+
- Header row: Series IDs (matching config.SeriesID)
|
|
365
|
+
"""
|
|
366
|
+
datafile = Path(datafile)
|
|
367
|
+
if not datafile.exists():
|
|
368
|
+
raise FileNotFoundError(f"Data file not found: {datafile}")
|
|
369
|
+
|
|
370
|
+
# Read CSV file
|
|
371
|
+
try:
|
|
372
|
+
df = pd.read_csv(datafile, index_col=0, parse_dates=True)
|
|
373
|
+
except Exception as e:
|
|
374
|
+
raise ValueError(f"Failed to read CSV file {datafile}: {e}")
|
|
375
|
+
|
|
376
|
+
mnemonics = df.columns.tolist()
|
|
377
|
+
Time = df.index
|
|
378
|
+
Z = df.values.astype(float)
|
|
379
|
+
|
|
380
|
+
return Z, Time, mnemonics
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def sort_data(Z: np.ndarray, Mnem: List[str], config: DFMConfig) -> Tuple[np.ndarray, List[str]]:
|
|
384
|
+
"""Sort series to match configuration order."""
|
|
385
|
+
in_config = [m in config.SeriesID for m in Mnem]
|
|
386
|
+
Mnem_filt = [m for m, in_c in zip(Mnem, in_config) if in_c]
|
|
387
|
+
Z_filt = Z[:, in_config]
|
|
388
|
+
|
|
389
|
+
perm = [Mnem_filt.index(sid) for sid in config.SeriesID]
|
|
390
|
+
return Z_filt[:, perm], [Mnem_filt[i] for i in perm]
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def load_data(datafile: Union[str, Path], config: DFMConfig,
|
|
394
|
+
sample_start: Optional[Union[pd.Timestamp, str]] = None) -> Tuple[np.ndarray, pd.DatetimeIndex, np.ndarray]:
|
|
395
|
+
"""Load and transform data from CSV file.
|
|
396
|
+
|
|
397
|
+
This function reads data from a CSV file, sorts it to match the model
|
|
398
|
+
configuration (from CSV or YAML), and applies the specified transformations.
|
|
399
|
+
|
|
400
|
+
Note: For database-backed applications, create adapters that implement
|
|
401
|
+
the same interface (return X, Time, Z arrays).
|
|
402
|
+
|
|
403
|
+
Parameters
|
|
404
|
+
----------
|
|
405
|
+
datafile : str or Path
|
|
406
|
+
Path to CSV data file (.csv)
|
|
407
|
+
config : ModelConfig
|
|
408
|
+
Model configuration object (from CSV or YAML)
|
|
409
|
+
sample_start : pd.Timestamp or str, optional
|
|
410
|
+
Start date for sample (YYYY-MM-DD). If None, uses all available data.
|
|
411
|
+
Data before this date will be dropped.
|
|
412
|
+
|
|
413
|
+
Returns
|
|
414
|
+
-------
|
|
415
|
+
X : np.ndarray
|
|
416
|
+
Transformed data matrix (T x N), ready for DFM estimation
|
|
417
|
+
Time : pd.DatetimeIndex
|
|
418
|
+
Time index for the data
|
|
419
|
+
Z : np.ndarray
|
|
420
|
+
Original untransformed data (T x N), for reference
|
|
421
|
+
"""
|
|
422
|
+
print('Loading data...')
|
|
423
|
+
|
|
424
|
+
datafile = Path(datafile)
|
|
425
|
+
if datafile.suffix.lower() != '.csv':
|
|
426
|
+
raise ValueError('Only CSV files supported. Use database for production data loading.')
|
|
427
|
+
|
|
428
|
+
if not datafile.exists():
|
|
429
|
+
raise FileNotFoundError(f"Data file not found: {datafile}")
|
|
430
|
+
|
|
431
|
+
# Read data from CSV file
|
|
432
|
+
Z, Time, Mnem = read_data(datafile)
|
|
433
|
+
|
|
434
|
+
# Process data: sort to match config order, then transform
|
|
435
|
+
Z, _ = sort_data(Z, Mnem, config)
|
|
436
|
+
X, Time, Z = transform_data(Z, Time, config)
|
|
437
|
+
|
|
438
|
+
# Apply sample_start filtering
|
|
439
|
+
if sample_start is not None:
|
|
440
|
+
if isinstance(sample_start, str):
|
|
441
|
+
sample_start = pd.to_datetime(sample_start)
|
|
442
|
+
mask = Time >= sample_start
|
|
443
|
+
Time, X, Z = Time[mask], X[mask], Z[mask]
|
|
444
|
+
|
|
445
|
+
return X, Time, Z
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
# Database adapters are application-specific and should be implemented separately.
|
|
449
|
+
# This keeps the DFM module generic and database-agnostic.
|