eagleplot 3.0.0__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.
- eagleplot/Home.py +362 -0
- eagleplot/__init__.py +19 -0
- eagleplot/annotations.py +215 -0
- eagleplot/assets/logo.ico +0 -0
- eagleplot/assets/logo.png +0 -0
- eagleplot/assets/nccu-horiz-logo.png +0 -0
- eagleplot/assets/nccu-wings.png +0 -0
- eagleplot/batch.py +446 -0
- eagleplot/cli.py +719 -0
- eagleplot/data_tools.py +849 -0
- eagleplot/example-data/Artificial/bimodal_distribution.csv +501 -0
- eagleplot/example-data/Artificial/correlation_matrix.npy +0 -0
- eagleplot/example-data/Artificial/gaussian_surface.npy +0 -0
- eagleplot/example-data/Artificial/grouped_categorical.csv +241 -0
- eagleplot/example-data/Artificial/ir_spectrum.csv +1802 -0
- eagleplot/example-data/Artificial/molecular_properties.csv +121 -0
- eagleplot/example-data/Artificial/normal_distribution.csv +401 -0
- eagleplot/example-data/Artificial/reaction_kinetics.csv +121 -0
- eagleplot/example-data/Artificial/scatter_correlation.csv +201 -0
- eagleplot/example-data/Artificial/sine_cosine_waves.csv +301 -0
- eagleplot/gallery/box_plot.png +0 -0
- eagleplot/gallery/contour_plot.png +0 -0
- eagleplot/gallery/distribution_plot.png +0 -0
- eagleplot/gallery/heatmap.png +0 -0
- eagleplot/gallery/histogram.png +0 -0
- eagleplot/gallery/interactive_3d_surface.png +0 -0
- eagleplot/gallery/interactive_heatmap.png +0 -0
- eagleplot/gallery/interactive_histogram.png +0 -0
- eagleplot/gallery/interactive_line.png +0 -0
- eagleplot/gallery/interactive_scatter.png +0 -0
- eagleplot/gallery/line_plot.png +0 -0
- eagleplot/gallery/manifest.json +196 -0
- eagleplot/gallery/regression_plot.png +0 -0
- eagleplot/gallery/scatter_plot.png +0 -0
- eagleplot/io.py +1008 -0
- eagleplot/math.py +1210 -0
- eagleplot/molecular/__init__.py +21 -0
- eagleplot/molecular/atom_data.py +287 -0
- eagleplot/molecular/parsers.py +747 -0
- eagleplot/nist.py +241 -0
- eagleplot/pages/10_Molecular_Viz.py +225 -0
- eagleplot/pages/11_Batch_Analysis.py +325 -0
- eagleplot/pages/12_Help.py +199 -0
- eagleplot/pages/13_Settings.py +388 -0
- eagleplot/pages/1_Data_Upload.py +521 -0
- eagleplot/pages/2_Quick_Plot.py +1693 -0
- eagleplot/pages/3_Analysis_Tools.py +2510 -0
- eagleplot/pages/4_Multi_Plot_Dashboard.py +791 -0
- eagleplot/pages/5_Advanced_Plotting.py +691 -0
- eagleplot/pages/6_Export_Results.py +442 -0
- eagleplot/pages/7_Gallery.py +201 -0
- eagleplot/pages/8_Data_Tools.py +434 -0
- eagleplot/pages/9_Spectroscopy.py +821 -0
- eagleplot/pages/__init__.py +8 -0
- eagleplot/peaks.py +513 -0
- eagleplot/plotting.py +2630 -0
- eagleplot/plugin_loader.py +208 -0
- eagleplot/report.py +448 -0
- eagleplot/signal.py +494 -0
- eagleplot/spectroscopy.py +830 -0
- eagleplot/utils/__init__.py +76 -0
- eagleplot/utils/access.py +77 -0
- eagleplot/utils/data_preview.py +352 -0
- eagleplot/utils/plot_config.py +2721 -0
- eagleplot/utils/session_state.py +535 -0
- eagleplot/utils/theming.py +312 -0
- eagleplot/utils/user_settings.py +294 -0
- eagleplot-3.0.0.dist-info/METADATA +304 -0
- eagleplot-3.0.0.dist-info/RECORD +73 -0
- eagleplot-3.0.0.dist-info/WHEEL +5 -0
- eagleplot-3.0.0.dist-info/entry_points.txt +2 -0
- eagleplot-3.0.0.dist-info/licenses/LICENSE +21 -0
- eagleplot-3.0.0.dist-info/top_level.txt +1 -0
eagleplot/io.py
ADDED
|
@@ -0,0 +1,1008 @@
|
|
|
1
|
+
"""Input/Output Module.
|
|
2
|
+
|
|
3
|
+
This module provides utilities for reading and writing scientific data
|
|
4
|
+
in various formats commonly used in computational chemistry.
|
|
5
|
+
|
|
6
|
+
Supported Formats
|
|
7
|
+
-----------------
|
|
8
|
+
- Pickle (.pkl): Python object serialization
|
|
9
|
+
- NumPy arrays (.npy, .npz): Numerical array data
|
|
10
|
+
- Pandas DataFrames (.csv, .xlsx, .tsv, .json, .parquet): Tabular data
|
|
11
|
+
- JCAMP-DX (.jdx, .dx): Spectroscopy data (IR, Raman, NMR, MS) — requires ``jcamp``
|
|
12
|
+
- HDF5 (.h5, .hdf5): Hierarchical data arrays — requires ``h5py``
|
|
13
|
+
- NetCDF (.nc, .cdf): Array/climate data — requires ``xarray``
|
|
14
|
+
- SPC (.spc): Thermo Fisher spectroscopy — requires ``spc``
|
|
15
|
+
- ASC (.asc): Generic instrument text export (no extra dependency)
|
|
16
|
+
|
|
17
|
+
Examples
|
|
18
|
+
--------
|
|
19
|
+
>>> from eagleplot.io import load_data, save_data
|
|
20
|
+
>>> data = load_data('experiment.pkl')
|
|
21
|
+
>>> save_data(processed_data, 'results.pkl')
|
|
22
|
+
>>> spectrum = load_data('spectrum.jdx') # returns pd.DataFrame
|
|
23
|
+
>>> arrays = load_data('simulation.h5') # returns dict of np.ndarray
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import pickle
|
|
27
|
+
import numpy as np
|
|
28
|
+
import pandas as pd
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Any, Union
|
|
31
|
+
|
|
32
|
+
# ============================================================================
|
|
33
|
+
# Pickle Operations
|
|
34
|
+
# ============================================================================
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def load_pickle(filepath: Union[str, Path]) -> Any:
|
|
38
|
+
"""Load data from a pickle file.
|
|
39
|
+
|
|
40
|
+
Parameters
|
|
41
|
+
----------
|
|
42
|
+
filepath : str or Path
|
|
43
|
+
Path to the pickle file (.pkl)
|
|
44
|
+
|
|
45
|
+
Returns
|
|
46
|
+
-------
|
|
47
|
+
data : Any
|
|
48
|
+
The deserialized Python object
|
|
49
|
+
|
|
50
|
+
Raises
|
|
51
|
+
------
|
|
52
|
+
FileNotFoundError
|
|
53
|
+
If the file does not exist
|
|
54
|
+
pickle.UnpicklingError
|
|
55
|
+
If the file is corrupted or not a valid pickle file
|
|
56
|
+
|
|
57
|
+
Examples
|
|
58
|
+
--------
|
|
59
|
+
>>> data = load_pickle('experiment.pkl')
|
|
60
|
+
>>> print(type(data))
|
|
61
|
+
<class 'dict'>
|
|
62
|
+
"""
|
|
63
|
+
filepath = Path(filepath)
|
|
64
|
+
|
|
65
|
+
if not filepath.exists():
|
|
66
|
+
raise FileNotFoundError(f"File not found: {filepath}")
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
with open(filepath, "rb") as f:
|
|
70
|
+
data = pickle.load(f)
|
|
71
|
+
return data
|
|
72
|
+
except pickle.UnpicklingError as e:
|
|
73
|
+
raise pickle.UnpicklingError(
|
|
74
|
+
f"Failed to unpickle file {filepath}. "
|
|
75
|
+
f"The file may be corrupted or not a valid pickle file."
|
|
76
|
+
) from e
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def save_pickle(data: Any, filepath: Union[str, Path]) -> None:
|
|
80
|
+
"""Save data to a pickle file.
|
|
81
|
+
|
|
82
|
+
Parameters
|
|
83
|
+
----------
|
|
84
|
+
data : Any
|
|
85
|
+
Python object to serialize
|
|
86
|
+
filepath : str or Path
|
|
87
|
+
Output file path (.pkl)
|
|
88
|
+
|
|
89
|
+
Raises
|
|
90
|
+
------
|
|
91
|
+
PermissionError
|
|
92
|
+
If there are insufficient permissions to write to the file
|
|
93
|
+
pickle.PicklingError
|
|
94
|
+
If the object cannot be pickled
|
|
95
|
+
|
|
96
|
+
Examples
|
|
97
|
+
--------
|
|
98
|
+
>>> data = {'temperature': [300, 310, 320], 'pressure': [1.0, 1.1, 1.2]}
|
|
99
|
+
>>> save_pickle(data, 'results.pkl')
|
|
100
|
+
"""
|
|
101
|
+
filepath = Path(filepath)
|
|
102
|
+
|
|
103
|
+
# Create parent directories if they don't exist
|
|
104
|
+
filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
with open(filepath, "wb") as f:
|
|
108
|
+
pickle.dump(data, f)
|
|
109
|
+
except pickle.PicklingError as e:
|
|
110
|
+
raise pickle.PicklingError(
|
|
111
|
+
f"Failed to pickle object of type {type(data).__name__}. "
|
|
112
|
+
f"Ensure the object is serializable."
|
|
113
|
+
) from e
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# ============================================================================
|
|
117
|
+
# NumPy Array Operations
|
|
118
|
+
# ============================================================================
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def load_numpy(filepath: Union[str, Path]) -> Union[np.ndarray, dict]:
|
|
122
|
+
"""Load NumPy array(s) from file.
|
|
123
|
+
|
|
124
|
+
Supports both .npy (single array) and .npz (multiple arrays) formats.
|
|
125
|
+
|
|
126
|
+
Parameters
|
|
127
|
+
----------
|
|
128
|
+
filepath : str or Path
|
|
129
|
+
Path to NumPy file (.npy or .npz)
|
|
130
|
+
|
|
131
|
+
Returns
|
|
132
|
+
-------
|
|
133
|
+
data : np.ndarray or dict
|
|
134
|
+
For .npy files: returns a single NumPy array
|
|
135
|
+
For .npz files: returns a dict-like object with array names as keys
|
|
136
|
+
|
|
137
|
+
Raises
|
|
138
|
+
------
|
|
139
|
+
FileNotFoundError
|
|
140
|
+
If the file does not exist
|
|
141
|
+
ValueError
|
|
142
|
+
If the file format is not supported
|
|
143
|
+
|
|
144
|
+
Examples
|
|
145
|
+
--------
|
|
146
|
+
>>> array = load_numpy('data.npy')
|
|
147
|
+
>>> print(array.shape)
|
|
148
|
+
(100, 3)
|
|
149
|
+
|
|
150
|
+
>>> arrays = load_numpy('multiple.npz')
|
|
151
|
+
>>> print(arrays['x'].shape)
|
|
152
|
+
(100,)
|
|
153
|
+
"""
|
|
154
|
+
filepath = Path(filepath)
|
|
155
|
+
|
|
156
|
+
if not filepath.exists():
|
|
157
|
+
raise FileNotFoundError(f"File not found: {filepath}")
|
|
158
|
+
|
|
159
|
+
suffix = filepath.suffix.lower()
|
|
160
|
+
|
|
161
|
+
if suffix == ".npy":
|
|
162
|
+
return np.load(filepath)
|
|
163
|
+
elif suffix == ".npz":
|
|
164
|
+
return np.load(filepath)
|
|
165
|
+
else:
|
|
166
|
+
raise ValueError(f"Unsupported file format: {suffix}. Expected .npy or .npz")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def save_numpy(
|
|
170
|
+
data: Union[np.ndarray, dict], filepath: Union[str, Path], compressed: bool = False
|
|
171
|
+
) -> None:
|
|
172
|
+
"""Save NumPy array(s) to file.
|
|
173
|
+
|
|
174
|
+
Parameters
|
|
175
|
+
----------
|
|
176
|
+
data : np.ndarray or dict
|
|
177
|
+
For .npy: single NumPy array
|
|
178
|
+
For .npz: dict of arrays with names as keys
|
|
179
|
+
filepath : str or Path
|
|
180
|
+
Output file path (.npy or .npz)
|
|
181
|
+
compressed : bool, default False
|
|
182
|
+
If True and saving .npz, use compression
|
|
183
|
+
|
|
184
|
+
Raises
|
|
185
|
+
------
|
|
186
|
+
ValueError
|
|
187
|
+
If data type doesn't match file format
|
|
188
|
+
TypeError
|
|
189
|
+
If data is not a NumPy array or dict of arrays
|
|
190
|
+
|
|
191
|
+
Examples
|
|
192
|
+
--------
|
|
193
|
+
>>> array = np.random.rand(100, 3)
|
|
194
|
+
>>> save_numpy(array, 'data.npy')
|
|
195
|
+
|
|
196
|
+
>>> arrays = {'x': np.arange(10), 'y': np.arange(10)**2}
|
|
197
|
+
>>> save_numpy(arrays, 'multiple.npz')
|
|
198
|
+
"""
|
|
199
|
+
filepath = Path(filepath)
|
|
200
|
+
filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
201
|
+
|
|
202
|
+
suffix = filepath.suffix.lower()
|
|
203
|
+
|
|
204
|
+
if suffix == ".npy":
|
|
205
|
+
if not isinstance(data, np.ndarray):
|
|
206
|
+
raise TypeError(
|
|
207
|
+
f"For .npy format, data must be a NumPy array, got {type(data).__name__}"
|
|
208
|
+
)
|
|
209
|
+
np.save(filepath, data)
|
|
210
|
+
|
|
211
|
+
elif suffix == ".npz":
|
|
212
|
+
if isinstance(data, dict):
|
|
213
|
+
if compressed:
|
|
214
|
+
np.savez_compressed(filepath, **data)
|
|
215
|
+
else:
|
|
216
|
+
np.savez(filepath, **data)
|
|
217
|
+
elif isinstance(data, np.ndarray):
|
|
218
|
+
# Save single array with default name
|
|
219
|
+
if compressed:
|
|
220
|
+
np.savez_compressed(filepath, arr_0=data)
|
|
221
|
+
else:
|
|
222
|
+
np.savez(filepath, arr_0=data)
|
|
223
|
+
else:
|
|
224
|
+
raise TypeError(
|
|
225
|
+
f"For .npz format, data must be a NumPy array or dict of arrays, "
|
|
226
|
+
f"got {type(data).__name__}"
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
else:
|
|
230
|
+
raise ValueError(f"Unsupported file format: {suffix}. Expected .npy or .npz")
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# ============================================================================
|
|
234
|
+
# Pandas DataFrame Operations
|
|
235
|
+
# ============================================================================
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def load_dataframe(filepath: Union[str, Path], **kwargs) -> pd.DataFrame:
|
|
239
|
+
"""Load tabular data into a Pandas DataFrame.
|
|
240
|
+
|
|
241
|
+
Automatically detects format from file extension.
|
|
242
|
+
Supports CSV, Excel, and other Pandas-compatible formats.
|
|
243
|
+
|
|
244
|
+
Parameters
|
|
245
|
+
----------
|
|
246
|
+
filepath : str or Path
|
|
247
|
+
Path to data file (.csv, .xlsx, .xls, etc.)
|
|
248
|
+
kwargs : dict
|
|
249
|
+
Additional keyword arguments passed to pandas read function
|
|
250
|
+
|
|
251
|
+
Returns
|
|
252
|
+
-------
|
|
253
|
+
df : pd.DataFrame
|
|
254
|
+
Loaded DataFrame
|
|
255
|
+
|
|
256
|
+
Raises
|
|
257
|
+
------
|
|
258
|
+
FileNotFoundError
|
|
259
|
+
If the file does not exist
|
|
260
|
+
ValueError
|
|
261
|
+
If the file format is not supported
|
|
262
|
+
|
|
263
|
+
Examples
|
|
264
|
+
--------
|
|
265
|
+
>>> df = load_dataframe('data.csv')
|
|
266
|
+
>>> print(df.head())
|
|
267
|
+
|
|
268
|
+
>>> df = load_dataframe('data.xlsx', sheet_name='Sheet1')
|
|
269
|
+
"""
|
|
270
|
+
filepath = Path(filepath)
|
|
271
|
+
|
|
272
|
+
if not filepath.exists():
|
|
273
|
+
raise FileNotFoundError(f"File not found: {filepath}")
|
|
274
|
+
|
|
275
|
+
suffix = filepath.suffix.lower()
|
|
276
|
+
|
|
277
|
+
if suffix == ".csv":
|
|
278
|
+
return pd.read_csv(filepath, **kwargs)
|
|
279
|
+
elif suffix in [".xlsx", ".xls"]:
|
|
280
|
+
return pd.read_excel(filepath, **kwargs)
|
|
281
|
+
elif suffix == ".tsv":
|
|
282
|
+
return pd.read_csv(filepath, sep="\t", **kwargs)
|
|
283
|
+
elif suffix == ".json":
|
|
284
|
+
return pd.read_json(filepath, **kwargs)
|
|
285
|
+
elif suffix == ".parquet":
|
|
286
|
+
return pd.read_parquet(filepath, **kwargs)
|
|
287
|
+
else:
|
|
288
|
+
raise ValueError(
|
|
289
|
+
f"Unsupported file format: {suffix}. "
|
|
290
|
+
f"Supported formats: .csv, .xlsx, .xls, .tsv, .json, .parquet"
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def save_dataframe(
|
|
295
|
+
df: pd.DataFrame, filepath: Union[str, Path], index: bool = False, **kwargs
|
|
296
|
+
) -> None:
|
|
297
|
+
"""Save Pandas DataFrame to file.
|
|
298
|
+
|
|
299
|
+
Automatically selects format based on file extension.
|
|
300
|
+
|
|
301
|
+
Parameters
|
|
302
|
+
----------
|
|
303
|
+
df : pd.DataFrame
|
|
304
|
+
DataFrame to save
|
|
305
|
+
filepath : str or Path
|
|
306
|
+
Output file path
|
|
307
|
+
index : bool, default False
|
|
308
|
+
Whether to include the index in the output
|
|
309
|
+
kwargs : dict
|
|
310
|
+
Additional keyword arguments passed to pandas write function
|
|
311
|
+
|
|
312
|
+
Raises
|
|
313
|
+
------
|
|
314
|
+
ValueError
|
|
315
|
+
If the file format is not supported
|
|
316
|
+
TypeError
|
|
317
|
+
If df is not a Pandas DataFrame
|
|
318
|
+
|
|
319
|
+
Examples
|
|
320
|
+
--------
|
|
321
|
+
>>> df = pd.DataFrame({'x': [1, 2, 3], 'y': [4, 5, 6]})
|
|
322
|
+
>>> save_dataframe(df, 'output.csv')
|
|
323
|
+
|
|
324
|
+
>>> save_dataframe(df, 'output.xlsx', sheet_name='Results')
|
|
325
|
+
"""
|
|
326
|
+
if not isinstance(df, pd.DataFrame):
|
|
327
|
+
raise TypeError(f"Expected pandas DataFrame, got {type(df).__name__}")
|
|
328
|
+
|
|
329
|
+
filepath = Path(filepath)
|
|
330
|
+
filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
331
|
+
|
|
332
|
+
suffix = filepath.suffix.lower()
|
|
333
|
+
|
|
334
|
+
if suffix == ".csv":
|
|
335
|
+
df.to_csv(filepath, index=index, **kwargs)
|
|
336
|
+
elif suffix in [".xlsx", ".xls"]:
|
|
337
|
+
df.to_excel(filepath, index=index, **kwargs)
|
|
338
|
+
elif suffix == ".tsv":
|
|
339
|
+
df.to_csv(filepath, sep="\t", index=index, **kwargs)
|
|
340
|
+
elif suffix == ".json":
|
|
341
|
+
df.to_json(filepath, **kwargs)
|
|
342
|
+
elif suffix == ".parquet":
|
|
343
|
+
df.to_parquet(filepath, index=index, **kwargs)
|
|
344
|
+
else:
|
|
345
|
+
raise ValueError(
|
|
346
|
+
f"Unsupported file format: {suffix}. "
|
|
347
|
+
f"Supported formats: .csv, .xlsx, .xls, .tsv, .json, .parquet"
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
# ============================================================================
|
|
352
|
+
# Instrument File Formats (M17)
|
|
353
|
+
# ============================================================================
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def load_jcamp(filepath: Union[str, Path]) -> pd.DataFrame:
|
|
357
|
+
"""Load a JCAMP-DX spectroscopy file (.jdx or .dx).
|
|
358
|
+
|
|
359
|
+
Requires the ``jcamp`` package: ``pip install jcamp``
|
|
360
|
+
|
|
361
|
+
Returns a :class:`pandas.DataFrame` with columns ``x`` and ``y``.
|
|
362
|
+
Spectral metadata (title, data type, units) is stored in ``df.attrs``.
|
|
363
|
+
|
|
364
|
+
Parameters
|
|
365
|
+
----------
|
|
366
|
+
filepath : str or Path
|
|
367
|
+
Path to the JCAMP-DX file (.jdx or .dx)
|
|
368
|
+
|
|
369
|
+
Returns
|
|
370
|
+
-------
|
|
371
|
+
df : pd.DataFrame
|
|
372
|
+
Two-column DataFrame: ``x`` (frequency/wavenumber axis) and
|
|
373
|
+
``y`` (intensity/absorbance/transmittance). Metadata available
|
|
374
|
+
via ``df.attrs``.
|
|
375
|
+
|
|
376
|
+
Raises
|
|
377
|
+
------
|
|
378
|
+
ImportError
|
|
379
|
+
If the ``jcamp`` package is not installed
|
|
380
|
+
FileNotFoundError
|
|
381
|
+
If the file does not exist
|
|
382
|
+
|
|
383
|
+
Examples
|
|
384
|
+
--------
|
|
385
|
+
>>> df = load_jcamp('spectrum.jdx')
|
|
386
|
+
>>> print(df.columns.tolist())
|
|
387
|
+
['x', 'y']
|
|
388
|
+
>>> print(df.attrs.get('xunits'))
|
|
389
|
+
1/CM
|
|
390
|
+
"""
|
|
391
|
+
try:
|
|
392
|
+
from jcamp import JCAMP_reader as _jcamp_read
|
|
393
|
+
except ImportError:
|
|
394
|
+
try:
|
|
395
|
+
from jcamp import jcamp_read as _jcamp_read # type: ignore[no-redef]
|
|
396
|
+
except ImportError:
|
|
397
|
+
raise ImportError(
|
|
398
|
+
"The `jcamp` package is required to load JCAMP-DX files. "
|
|
399
|
+
"Install it with: pip install jcamp"
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
filepath = Path(filepath)
|
|
403
|
+
if not filepath.exists():
|
|
404
|
+
raise FileNotFoundError(f"File not found: {filepath}")
|
|
405
|
+
|
|
406
|
+
data = _jcamp_read(str(filepath))
|
|
407
|
+
|
|
408
|
+
x = np.asarray(data.get("x", []), dtype=float)
|
|
409
|
+
y = np.asarray(data.get("y", []), dtype=float)
|
|
410
|
+
|
|
411
|
+
df = pd.DataFrame({"x": x, "y": y})
|
|
412
|
+
|
|
413
|
+
for key in ("title", "data type", "xunits", "yunits", "origin", "owner"):
|
|
414
|
+
if key in data:
|
|
415
|
+
df.attrs[key] = data[key]
|
|
416
|
+
|
|
417
|
+
return df
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def load_hdf5(
|
|
421
|
+
filepath: Union[str, Path], dataset: Union[str, None] = None
|
|
422
|
+
) -> Union[dict, np.ndarray]:
|
|
423
|
+
"""Load an HDF5 file (.h5 or .hdf5).
|
|
424
|
+
|
|
425
|
+
Requires the ``h5py`` package: ``pip install h5py``
|
|
426
|
+
|
|
427
|
+
By default returns a flat ``dict`` mapping every dataset path to a
|
|
428
|
+
:class:`numpy.ndarray`. If *dataset* is given, returns just that
|
|
429
|
+
single array.
|
|
430
|
+
|
|
431
|
+
Parameters
|
|
432
|
+
----------
|
|
433
|
+
filepath : str or Path
|
|
434
|
+
Path to the HDF5 file
|
|
435
|
+
dataset : str, optional
|
|
436
|
+
Slash-separated path to a specific dataset inside the file
|
|
437
|
+
(e.g. ``"group/signal"``). When omitted all datasets are
|
|
438
|
+
returned.
|
|
439
|
+
|
|
440
|
+
Returns
|
|
441
|
+
-------
|
|
442
|
+
data : dict[str, np.ndarray] or np.ndarray
|
|
443
|
+
All datasets as a flat dict, or the requested single array.
|
|
444
|
+
|
|
445
|
+
Raises
|
|
446
|
+
------
|
|
447
|
+
ImportError
|
|
448
|
+
If ``h5py`` is not installed
|
|
449
|
+
FileNotFoundError
|
|
450
|
+
If the file does not exist
|
|
451
|
+
KeyError
|
|
452
|
+
If the requested *dataset* path does not exist in the file
|
|
453
|
+
|
|
454
|
+
Examples
|
|
455
|
+
--------
|
|
456
|
+
>>> arrays = load_hdf5('simulation.h5')
|
|
457
|
+
>>> print(list(arrays.keys()))
|
|
458
|
+
['time', 'temperature', 'pressure']
|
|
459
|
+
|
|
460
|
+
>>> arr = load_hdf5('simulation.h5', dataset='temperature')
|
|
461
|
+
>>> print(arr.shape)
|
|
462
|
+
(1000,)
|
|
463
|
+
"""
|
|
464
|
+
try:
|
|
465
|
+
import h5py
|
|
466
|
+
except ImportError:
|
|
467
|
+
raise ImportError(
|
|
468
|
+
"The `h5py` package is required to load HDF5 files. Install it with: pip install h5py"
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
filepath = Path(filepath)
|
|
472
|
+
if not filepath.exists():
|
|
473
|
+
raise FileNotFoundError(f"File not found: {filepath}")
|
|
474
|
+
|
|
475
|
+
def _flatten(group: "h5py.Group", prefix: str = "") -> dict:
|
|
476
|
+
result: dict = {}
|
|
477
|
+
for key, item in group.items():
|
|
478
|
+
full = f"{prefix}/{key}" if prefix else key
|
|
479
|
+
if isinstance(item, h5py.Dataset):
|
|
480
|
+
result[full] = np.array(item)
|
|
481
|
+
elif isinstance(item, h5py.Group):
|
|
482
|
+
result.update(_flatten(item, full))
|
|
483
|
+
return result
|
|
484
|
+
|
|
485
|
+
with h5py.File(filepath, "r") as f:
|
|
486
|
+
if dataset is not None:
|
|
487
|
+
if dataset not in f:
|
|
488
|
+
raise KeyError(f"Dataset '{dataset}' not found in {filepath}")
|
|
489
|
+
return np.array(f[dataset])
|
|
490
|
+
return _flatten(f)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def load_netcdf(filepath: Union[str, Path]) -> dict:
|
|
494
|
+
"""Load a NetCDF file (.nc or .cdf).
|
|
495
|
+
|
|
496
|
+
Requires the ``xarray`` package: ``pip install xarray netcdf4``
|
|
497
|
+
|
|
498
|
+
Returns a ``dict`` mapping variable names to :class:`numpy.ndarray`
|
|
499
|
+
values. Coordinate arrays are included alongside data variables.
|
|
500
|
+
|
|
501
|
+
Parameters
|
|
502
|
+
----------
|
|
503
|
+
filepath : str or Path
|
|
504
|
+
Path to the NetCDF file
|
|
505
|
+
|
|
506
|
+
Returns
|
|
507
|
+
-------
|
|
508
|
+
data : dict[str, np.ndarray]
|
|
509
|
+
All data variables and coordinates as numpy arrays.
|
|
510
|
+
|
|
511
|
+
Raises
|
|
512
|
+
------
|
|
513
|
+
ImportError
|
|
514
|
+
If ``xarray`` (or its NetCDF engine) is not installed
|
|
515
|
+
FileNotFoundError
|
|
516
|
+
If the file does not exist
|
|
517
|
+
|
|
518
|
+
Examples
|
|
519
|
+
--------
|
|
520
|
+
>>> data = load_netcdf('climate.nc')
|
|
521
|
+
>>> print(list(data.keys()))
|
|
522
|
+
['time', 'lat', 'lon', 'temperature']
|
|
523
|
+
"""
|
|
524
|
+
try:
|
|
525
|
+
import xarray as xr
|
|
526
|
+
except ImportError:
|
|
527
|
+
raise ImportError(
|
|
528
|
+
"The `xarray` package is required to load NetCDF files. "
|
|
529
|
+
"Install it with: pip install xarray netcdf4"
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
filepath = Path(filepath)
|
|
533
|
+
if not filepath.exists():
|
|
534
|
+
raise FileNotFoundError(f"File not found: {filepath}")
|
|
535
|
+
|
|
536
|
+
ds = xr.open_dataset(str(filepath))
|
|
537
|
+
result: dict = {}
|
|
538
|
+
for name, var in ds.data_vars.items():
|
|
539
|
+
result[str(name)] = np.array(var.values)
|
|
540
|
+
for name, coord in ds.coords.items():
|
|
541
|
+
if str(name) not in result:
|
|
542
|
+
result[str(name)] = np.array(coord.values)
|
|
543
|
+
ds.close()
|
|
544
|
+
return result
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def load_spc(filepath: Union[str, Path]) -> pd.DataFrame:
|
|
548
|
+
"""Load a Thermo Fisher SPC spectroscopy file (.spc).
|
|
549
|
+
|
|
550
|
+
Requires the ``spc`` package: ``pip install spc``
|
|
551
|
+
|
|
552
|
+
Returns a :class:`pandas.DataFrame` with a ``wavenumber`` column
|
|
553
|
+
and one ``intensity`` column per trace (``intensity_0``,
|
|
554
|
+
``intensity_1``, … for multi-trace files).
|
|
555
|
+
|
|
556
|
+
Parameters
|
|
557
|
+
----------
|
|
558
|
+
filepath : str or Path
|
|
559
|
+
Path to the SPC file
|
|
560
|
+
|
|
561
|
+
Returns
|
|
562
|
+
-------
|
|
563
|
+
df : pd.DataFrame
|
|
564
|
+
Spectral data. Single-trace files have columns
|
|
565
|
+
``['wavenumber', 'intensity']``; multi-trace files add a
|
|
566
|
+
numeric suffix to each intensity column.
|
|
567
|
+
|
|
568
|
+
Raises
|
|
569
|
+
------
|
|
570
|
+
ImportError
|
|
571
|
+
If the ``spc`` package is not installed
|
|
572
|
+
FileNotFoundError
|
|
573
|
+
If the file does not exist
|
|
574
|
+
|
|
575
|
+
Examples
|
|
576
|
+
--------
|
|
577
|
+
>>> df = load_spc('raman.spc')
|
|
578
|
+
>>> print(df.columns.tolist())
|
|
579
|
+
['wavenumber', 'intensity']
|
|
580
|
+
"""
|
|
581
|
+
try:
|
|
582
|
+
import spc # type: ignore[import-untyped]
|
|
583
|
+
except ImportError:
|
|
584
|
+
raise ImportError(
|
|
585
|
+
"The `spc` package is required to load SPC files. Install it with: pip install spc"
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
filepath = Path(filepath)
|
|
589
|
+
if not filepath.exists():
|
|
590
|
+
raise FileNotFoundError(f"File not found: {filepath}")
|
|
591
|
+
|
|
592
|
+
f = spc.File(str(filepath))
|
|
593
|
+
|
|
594
|
+
# Shared x-axis (may live on the file or on each sub-file)
|
|
595
|
+
shared_x = np.asarray(f.x) if hasattr(f, "x") and f.x is not None else None
|
|
596
|
+
|
|
597
|
+
columns: dict = {}
|
|
598
|
+
for i, sub in enumerate(f.sub):
|
|
599
|
+
x_data = np.asarray(sub.x) if (hasattr(sub, "x") and sub.x is not None) else shared_x
|
|
600
|
+
y_data = np.asarray(sub.y)
|
|
601
|
+
|
|
602
|
+
if i == 0 and x_data is not None:
|
|
603
|
+
columns["wavenumber"] = x_data
|
|
604
|
+
|
|
605
|
+
suffix = f"_{i}" if len(f.sub) > 1 else ""
|
|
606
|
+
columns[f"intensity{suffix}"] = y_data
|
|
607
|
+
|
|
608
|
+
return pd.DataFrame(columns)
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def load_asc(filepath: Union[str, Path]) -> pd.DataFrame:
|
|
612
|
+
"""Load a generic instrument ASC / plain-text export file (.asc).
|
|
613
|
+
|
|
614
|
+
Handles whitespace-, comma-, and tab-delimited files. Lines
|
|
615
|
+
starting with ``#`` are treated as comments. If the first
|
|
616
|
+
non-comment line contains only numeric tokens the file is assumed
|
|
617
|
+
to have no header row and columns are named ``col_0``, ``col_1``,
|
|
618
|
+
etc. Otherwise the first line is used as a header.
|
|
619
|
+
|
|
620
|
+
No extra package is required beyond Pandas.
|
|
621
|
+
|
|
622
|
+
Parameters
|
|
623
|
+
----------
|
|
624
|
+
filepath : str or Path
|
|
625
|
+
Path to the ASC or plain-text file
|
|
626
|
+
|
|
627
|
+
Returns
|
|
628
|
+
-------
|
|
629
|
+
df : pd.DataFrame
|
|
630
|
+
Parsed tabular data.
|
|
631
|
+
|
|
632
|
+
Raises
|
|
633
|
+
------
|
|
634
|
+
FileNotFoundError
|
|
635
|
+
If the file does not exist
|
|
636
|
+
ValueError
|
|
637
|
+
If the file cannot be parsed as delimited text
|
|
638
|
+
|
|
639
|
+
Examples
|
|
640
|
+
--------
|
|
641
|
+
>>> df = load_asc('spectrum.asc')
|
|
642
|
+
>>> print(df.shape)
|
|
643
|
+
(512, 2)
|
|
644
|
+
"""
|
|
645
|
+
filepath = Path(filepath)
|
|
646
|
+
if not filepath.exists():
|
|
647
|
+
raise FileNotFoundError(f"File not found: {filepath}")
|
|
648
|
+
|
|
649
|
+
# Detect whether the first non-comment line is a header
|
|
650
|
+
first_data_line = ""
|
|
651
|
+
with open(filepath, encoding="utf-8", errors="replace") as fh:
|
|
652
|
+
for line in fh:
|
|
653
|
+
stripped = line.strip()
|
|
654
|
+
if stripped and not stripped.startswith("#"):
|
|
655
|
+
first_data_line = stripped
|
|
656
|
+
break
|
|
657
|
+
|
|
658
|
+
parts = first_data_line.replace(",", " ").replace("\t", " ").split()
|
|
659
|
+
try:
|
|
660
|
+
[float(p) for p in parts if p]
|
|
661
|
+
header_row: Union[int, None] = None # all numeric → no header
|
|
662
|
+
except ValueError:
|
|
663
|
+
header_row = 0 # first line is a header
|
|
664
|
+
|
|
665
|
+
try:
|
|
666
|
+
df = pd.read_csv(
|
|
667
|
+
filepath,
|
|
668
|
+
sep=None,
|
|
669
|
+
comment="#",
|
|
670
|
+
header=header_row,
|
|
671
|
+
engine="python",
|
|
672
|
+
skip_blank_lines=True,
|
|
673
|
+
)
|
|
674
|
+
except Exception as exc:
|
|
675
|
+
raise ValueError(f"Could not parse {filepath} as delimited text: {exc}") from exc
|
|
676
|
+
|
|
677
|
+
if header_row is None:
|
|
678
|
+
df.columns = [f"col_{i}" for i in range(df.shape[1])]
|
|
679
|
+
|
|
680
|
+
return df
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
def load_mzml(filepath: Union[str, Path]) -> "pd.DataFrame":
|
|
684
|
+
"""Load a mass spectrometry mzML or mzXML file.
|
|
685
|
+
|
|
686
|
+
Requires the ``pymzml`` package (``pip install pymzml``).
|
|
687
|
+
Returns a summary DataFrame with one row per scan.
|
|
688
|
+
|
|
689
|
+
Parameters
|
|
690
|
+
----------
|
|
691
|
+
filepath : str
|
|
692
|
+
Path to a ``.mzML`` or ``.mzXML`` file.
|
|
693
|
+
|
|
694
|
+
Returns
|
|
695
|
+
-------
|
|
696
|
+
pd.DataFrame
|
|
697
|
+
Columns: ``scan``, ``rt`` (s), ``ms_level``, ``tic``,
|
|
698
|
+
``base_peak_mz``, ``base_peak_intensity``.
|
|
699
|
+
Raw (mz, intensity) arrays are available via
|
|
700
|
+
``df.attrs["spectra"]`` — a list of dicts with keys
|
|
701
|
+
``scan``, ``mz``, ``intensity``.
|
|
702
|
+
"""
|
|
703
|
+
try:
|
|
704
|
+
import pymzml
|
|
705
|
+
except ImportError as exc:
|
|
706
|
+
raise ImportError(
|
|
707
|
+
"pymzml is required for mzML/mzXML files. Install it with: pip install pymzml"
|
|
708
|
+
) from exc
|
|
709
|
+
|
|
710
|
+
rows = []
|
|
711
|
+
spectra = []
|
|
712
|
+
run = pymzml.run.Reader(str(filepath))
|
|
713
|
+
for scan_idx, spectrum in enumerate(run, start=1):
|
|
714
|
+
mz_arr = np.asarray(spectrum.mz, dtype=float)
|
|
715
|
+
int_arr = np.asarray(spectrum.i, dtype=float)
|
|
716
|
+
if hasattr(spectrum, "scan_time_in_minutes"):
|
|
717
|
+
rt = float(spectrum.scan_time_in_minutes() * 60.0)
|
|
718
|
+
else:
|
|
719
|
+
rt = float(spectrum.get("MS:1000016", 0.0))
|
|
720
|
+
ms_level = int(spectrum.ms_level) if spectrum.ms_level else 1
|
|
721
|
+
tic = float(int_arr.sum()) if len(int_arr) else 0.0
|
|
722
|
+
if len(int_arr) > 0:
|
|
723
|
+
bp_idx = int(np.argmax(int_arr))
|
|
724
|
+
bp_mz = float(mz_arr[bp_idx])
|
|
725
|
+
bp_int = float(int_arr[bp_idx])
|
|
726
|
+
else:
|
|
727
|
+
bp_mz, bp_int = float("nan"), 0.0
|
|
728
|
+
rows.append(
|
|
729
|
+
{
|
|
730
|
+
"scan": scan_idx,
|
|
731
|
+
"rt": rt,
|
|
732
|
+
"ms_level": ms_level,
|
|
733
|
+
"tic": tic,
|
|
734
|
+
"base_peak_mz": bp_mz,
|
|
735
|
+
"base_peak_intensity": bp_int,
|
|
736
|
+
}
|
|
737
|
+
)
|
|
738
|
+
spectra.append({"scan": scan_idx, "mz": mz_arr, "intensity": int_arr})
|
|
739
|
+
|
|
740
|
+
df = pd.DataFrame(rows)
|
|
741
|
+
df.attrs["spectra"] = spectra
|
|
742
|
+
df.attrs["source_file"] = str(filepath)
|
|
743
|
+
return df
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
# ============================================================================
|
|
747
|
+
# Universal Data Loader/Saver
|
|
748
|
+
# ============================================================================
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
def load_data(filepath: Union[str, Path], **kwargs) -> Any:
|
|
752
|
+
"""Universal data loader with automatic format detection.
|
|
753
|
+
|
|
754
|
+
Automatically detects the file format from extension and uses
|
|
755
|
+
the appropriate loading function.
|
|
756
|
+
|
|
757
|
+
Supported formats:
|
|
758
|
+
- .pkl: Pickle files
|
|
759
|
+
- .npy, .npz: NumPy arrays
|
|
760
|
+
- .csv, .xlsx, .xls, .tsv, .json, .parquet: Tabular data (Pandas)
|
|
761
|
+
- .jdx, .dx: JCAMP-DX spectroscopy (requires ``jcamp``)
|
|
762
|
+
- .h5, .hdf5: HDF5 arrays (requires ``h5py``)
|
|
763
|
+
- .nc, .cdf: NetCDF (requires ``xarray``)
|
|
764
|
+
- .spc: Thermo Fisher SPC (requires ``spc``)
|
|
765
|
+
- .asc: Generic instrument text export
|
|
766
|
+
|
|
767
|
+
Parameters
|
|
768
|
+
----------
|
|
769
|
+
filepath : str or Path
|
|
770
|
+
Path to data file
|
|
771
|
+
kwargs : dict
|
|
772
|
+
Additional arguments passed to format-specific loader
|
|
773
|
+
|
|
774
|
+
Returns
|
|
775
|
+
-------
|
|
776
|
+
data : Any
|
|
777
|
+
Loaded data in appropriate format
|
|
778
|
+
|
|
779
|
+
Raises
|
|
780
|
+
------
|
|
781
|
+
FileNotFoundError
|
|
782
|
+
If the file does not exist
|
|
783
|
+
ValueError
|
|
784
|
+
If the file format is not supported
|
|
785
|
+
|
|
786
|
+
Examples
|
|
787
|
+
--------
|
|
788
|
+
>>> data = load_data('experiment.pkl')
|
|
789
|
+
>>> array = load_data('data.npy')
|
|
790
|
+
>>> df = load_data('results.csv')
|
|
791
|
+
>>> df = load_data('spectrum.jdx')
|
|
792
|
+
>>> arrays = load_data('simulation.h5')
|
|
793
|
+
"""
|
|
794
|
+
filepath = Path(filepath)
|
|
795
|
+
|
|
796
|
+
if not filepath.exists():
|
|
797
|
+
raise FileNotFoundError(f"File not found: {filepath}")
|
|
798
|
+
|
|
799
|
+
suffix = filepath.suffix.lower()
|
|
800
|
+
|
|
801
|
+
# Pickle files
|
|
802
|
+
if suffix == ".pkl":
|
|
803
|
+
return load_pickle(filepath)
|
|
804
|
+
|
|
805
|
+
# NumPy files
|
|
806
|
+
elif suffix in [".npy", ".npz"]:
|
|
807
|
+
return load_numpy(filepath)
|
|
808
|
+
|
|
809
|
+
# Tabular data files
|
|
810
|
+
elif suffix in [".csv", ".xlsx", ".xls", ".tsv", ".json", ".parquet"]:
|
|
811
|
+
return load_dataframe(filepath, **kwargs)
|
|
812
|
+
|
|
813
|
+
# JCAMP-DX spectroscopy
|
|
814
|
+
elif suffix in [".jdx", ".dx"]:
|
|
815
|
+
return load_jcamp(filepath)
|
|
816
|
+
|
|
817
|
+
# HDF5
|
|
818
|
+
elif suffix in [".h5", ".hdf5"]:
|
|
819
|
+
return load_hdf5(filepath, **kwargs)
|
|
820
|
+
|
|
821
|
+
# NetCDF
|
|
822
|
+
elif suffix in [".nc", ".cdf"]:
|
|
823
|
+
return load_netcdf(filepath)
|
|
824
|
+
|
|
825
|
+
# Thermo SPC
|
|
826
|
+
elif suffix == ".spc":
|
|
827
|
+
return load_spc(filepath)
|
|
828
|
+
|
|
829
|
+
# Generic instrument text
|
|
830
|
+
elif suffix == ".asc":
|
|
831
|
+
return load_asc(filepath)
|
|
832
|
+
|
|
833
|
+
# mzML / mzXML mass spectrometry
|
|
834
|
+
elif suffix in [".mzml", ".mzxml"]:
|
|
835
|
+
return load_mzml(filepath)
|
|
836
|
+
|
|
837
|
+
else:
|
|
838
|
+
raise ValueError(
|
|
839
|
+
f"Unsupported file format: {suffix}. "
|
|
840
|
+
f"Supported formats: .pkl, .npy, .npz, .csv, .xlsx, .xls, "
|
|
841
|
+
f".tsv, .json, .parquet, .jdx, .dx, .h5, .hdf5, .nc, .cdf, "
|
|
842
|
+
f".spc, .asc, .mzml, .mzxml"
|
|
843
|
+
)
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
def save_data(data: Any, filepath: Union[str, Path], **kwargs) -> None:
|
|
847
|
+
"""Universal data saver with automatic format detection.
|
|
848
|
+
|
|
849
|
+
Automatically detects the target format from file extension and uses
|
|
850
|
+
the appropriate saving function.
|
|
851
|
+
|
|
852
|
+
Parameters
|
|
853
|
+
----------
|
|
854
|
+
data : Any
|
|
855
|
+
Data to save (Python object, NumPy array, or Pandas DataFrame)
|
|
856
|
+
filepath : str or Path
|
|
857
|
+
Output file path
|
|
858
|
+
kwargs : dict
|
|
859
|
+
Additional arguments passed to format-specific saver
|
|
860
|
+
|
|
861
|
+
Raises
|
|
862
|
+
------
|
|
863
|
+
ValueError
|
|
864
|
+
If the file format is not supported or data type incompatible
|
|
865
|
+
|
|
866
|
+
Examples
|
|
867
|
+
--------
|
|
868
|
+
>>> save_data({'temp': 300, 'pressure': 1.0}, 'params.pkl')
|
|
869
|
+
>>> save_data(np.array([1, 2, 3]), 'data.npy')
|
|
870
|
+
>>> save_data(df, 'results.csv')
|
|
871
|
+
"""
|
|
872
|
+
filepath = Path(filepath)
|
|
873
|
+
suffix = filepath.suffix.lower()
|
|
874
|
+
|
|
875
|
+
# Pickle files - accept any object
|
|
876
|
+
if suffix == ".pkl":
|
|
877
|
+
save_pickle(data, filepath)
|
|
878
|
+
|
|
879
|
+
# NumPy files - require NumPy array or dict of arrays
|
|
880
|
+
elif suffix in [".npy", ".npz"]:
|
|
881
|
+
if isinstance(data, (np.ndarray, dict)):
|
|
882
|
+
save_numpy(data, filepath, **kwargs)
|
|
883
|
+
else:
|
|
884
|
+
raise TypeError(
|
|
885
|
+
f"For {suffix} format, data must be NumPy array or dict of arrays, "
|
|
886
|
+
f"got {type(data).__name__}"
|
|
887
|
+
)
|
|
888
|
+
|
|
889
|
+
# Tabular data - require Pandas DataFrame
|
|
890
|
+
elif suffix in [".csv", ".xlsx", ".xls", ".tsv", ".json", ".parquet"]:
|
|
891
|
+
if isinstance(data, pd.DataFrame):
|
|
892
|
+
save_dataframe(data, filepath, **kwargs)
|
|
893
|
+
else:
|
|
894
|
+
raise TypeError(
|
|
895
|
+
f"For {suffix} format, data must be pandas DataFrame, got {type(data).__name__}"
|
|
896
|
+
)
|
|
897
|
+
|
|
898
|
+
else:
|
|
899
|
+
raise ValueError(
|
|
900
|
+
f"Unsupported file format: {suffix}. "
|
|
901
|
+
f"Supported formats: .pkl, .npy, .npz, .csv, .xlsx, .xls, "
|
|
902
|
+
f".tsv, .json, .parquet"
|
|
903
|
+
)
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
# ============================================================================
|
|
907
|
+
# Large Dataset Utilities
|
|
908
|
+
# ============================================================================
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
def downsample_for_preview(
|
|
912
|
+
data: pd.DataFrame,
|
|
913
|
+
max_rows: int = 10_000,
|
|
914
|
+
method: str = "systematic",
|
|
915
|
+
) -> pd.DataFrame:
|
|
916
|
+
"""Downsample a DataFrame to at most *max_rows* rows for GUI performance.
|
|
917
|
+
|
|
918
|
+
The original DataFrame is returned unchanged when it already fits within
|
|
919
|
+
the limit, so callers can use the result unconditionally.
|
|
920
|
+
|
|
921
|
+
Parameters
|
|
922
|
+
----------
|
|
923
|
+
data : pd.DataFrame
|
|
924
|
+
Input DataFrame.
|
|
925
|
+
max_rows : int
|
|
926
|
+
Row threshold above which downsampling is applied. Default 10 000.
|
|
927
|
+
method : {'systematic', 'random'}
|
|
928
|
+
``'systematic'`` selects every N-th row (preserves data shape).
|
|
929
|
+
``'random'`` draws a random sample (uniform coverage).
|
|
930
|
+
|
|
931
|
+
Returns
|
|
932
|
+
-------
|
|
933
|
+
pd.DataFrame
|
|
934
|
+
Downsampled (or original) DataFrame.
|
|
935
|
+
|
|
936
|
+
Examples
|
|
937
|
+
--------
|
|
938
|
+
>>> import pandas as pd, numpy as np
|
|
939
|
+
>>> large = pd.DataFrame({'x': np.arange(50_000)})
|
|
940
|
+
>>> small = downsample_for_preview(large, max_rows=1000)
|
|
941
|
+
>>> len(small) <= 1000
|
|
942
|
+
True
|
|
943
|
+
"""
|
|
944
|
+
if not isinstance(data, pd.DataFrame) or len(data) <= max_rows:
|
|
945
|
+
return data
|
|
946
|
+
|
|
947
|
+
if method == "random":
|
|
948
|
+
return data.sample(n=max_rows, random_state=42).reset_index(drop=True)
|
|
949
|
+
|
|
950
|
+
# systematic: every step-th row
|
|
951
|
+
step = max(1, len(data) // max_rows)
|
|
952
|
+
return data.iloc[::step].reset_index(drop=True)
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
def load_large_csv(
|
|
956
|
+
filepath: Union[str, Path],
|
|
957
|
+
max_rows: int = 500_000,
|
|
958
|
+
chunksize: int = 50_000,
|
|
959
|
+
**kwargs,
|
|
960
|
+
) -> pd.DataFrame:
|
|
961
|
+
"""Load a large CSV in chunks, stopping once *max_rows* rows are read.
|
|
962
|
+
|
|
963
|
+
Useful for very large files where ``pd.read_csv`` would exhaust memory.
|
|
964
|
+
For files where all rows are needed, use :func:`load_data` directly.
|
|
965
|
+
|
|
966
|
+
Parameters
|
|
967
|
+
----------
|
|
968
|
+
filepath : str or Path
|
|
969
|
+
Path to the CSV file.
|
|
970
|
+
max_rows : int
|
|
971
|
+
Maximum rows to load. Default 500 000.
|
|
972
|
+
chunksize : int
|
|
973
|
+
Rows per chunk. Default 50 000.
|
|
974
|
+
kwargs
|
|
975
|
+
Extra keyword arguments forwarded to :func:`pandas.read_csv`.
|
|
976
|
+
|
|
977
|
+
Returns
|
|
978
|
+
-------
|
|
979
|
+
pd.DataFrame
|
|
980
|
+
Concatenated DataFrame of at most *max_rows* rows.
|
|
981
|
+
|
|
982
|
+
Examples
|
|
983
|
+
--------
|
|
984
|
+
>>> df = load_large_csv('huge.csv', max_rows=100_000)
|
|
985
|
+
>>> len(df) <= 100_000
|
|
986
|
+
True
|
|
987
|
+
"""
|
|
988
|
+
filepath = Path(filepath)
|
|
989
|
+
if not filepath.exists():
|
|
990
|
+
raise FileNotFoundError(f"File not found: {filepath}")
|
|
991
|
+
|
|
992
|
+
chunks = []
|
|
993
|
+
rows_read = 0
|
|
994
|
+
reader = pd.read_csv(filepath, chunksize=chunksize, **kwargs)
|
|
995
|
+
for chunk in reader:
|
|
996
|
+
remaining = max_rows - rows_read
|
|
997
|
+
if remaining <= 0:
|
|
998
|
+
break
|
|
999
|
+
if len(chunk) > remaining:
|
|
1000
|
+
chunk = chunk.iloc[:remaining]
|
|
1001
|
+
chunks.append(chunk)
|
|
1002
|
+
rows_read += len(chunk)
|
|
1003
|
+
if rows_read >= max_rows:
|
|
1004
|
+
break
|
|
1005
|
+
|
|
1006
|
+
if not chunks:
|
|
1007
|
+
return pd.DataFrame()
|
|
1008
|
+
return pd.concat(chunks, ignore_index=True)
|