Skip to content

Commit 84ffb8f

Browse files
committed
added to_csv, to_parquet, to_arrow_table to encoding result
1 parent 33a5f2d commit 84ffb8f

4 files changed

Lines changed: 221 additions & 62 deletions

File tree

src/birdnet/acoustic/inference/core/encoding/encoding_result.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
from __future__ import annotations
22

3+
import os
34
from pathlib import Path
5+
from typing import TYPE_CHECKING
46

57
import numpy as np
8+
from tqdm import tqdm
69

710
from birdnet.acoustic.inference.core.encoding.encoding_tensor import (
811
AcousticEncodingTensor,
@@ -15,10 +18,15 @@
1518
)
1619
from birdnet.utils.helper import (
1720
apply_speed_to_duration,
21+
format_input_for_csv,
1822
get_hop_duration_s,
1923
get_uint_dtype,
24+
hms_centis_fast,
2025
)
2126

27+
if TYPE_CHECKING:
28+
import pyarrow as pa
29+
2230
VAR_EMBEDDING = "embedding"
2331

2432
NP_EMB_KEY = "embeddings"
@@ -145,6 +153,116 @@ def to_structured_array(self) -> np.ndarray:
145153

146154
return structured_array
147155

156+
def to_arrow_table(self) -> pa.Table:
157+
import pyarrow as pa
158+
159+
structured = self.to_structured_array()
160+
161+
arrow_arrays: dict[str, pa.Array] = {}
162+
arrow_arrays[VAR_INPUT] = pa.array(structured[VAR_INPUT]).dictionary_encode()
163+
arrow_arrays[VAR_START_TIME] = pa.array(
164+
structured[VAR_START_TIME],
165+
type=pa.from_numpy_dtype(structured[VAR_START_TIME].dtype),
166+
)
167+
arrow_arrays[VAR_END_TIME] = pa.array(
168+
structured[VAR_END_TIME],
169+
type=pa.from_numpy_dtype(structured[VAR_END_TIME].dtype),
170+
)
171+
172+
embedding_element_type = pa.from_numpy_dtype(self._embeddings.dtype)
173+
embedding_type = pa.list_(embedding_element_type)
174+
arrow_arrays[VAR_EMBEDDING] = pa.array(
175+
structured[VAR_EMBEDDING].tolist(),
176+
type=embedding_type,
177+
)
178+
179+
fields = [
180+
pa.field(VAR_INPUT, arrow_arrays[VAR_INPUT].type, nullable=False),
181+
pa.field(VAR_START_TIME, arrow_arrays[VAR_START_TIME].type, nullable=False),
182+
pa.field(VAR_END_TIME, arrow_arrays[VAR_END_TIME].type, nullable=False),
183+
pa.field(VAR_EMBEDDING, embedding_type, nullable=False),
184+
]
185+
186+
metadata: dict[bytes | str, bytes | str] = {
187+
"segment_duration_s": str(self._segment_duration_s[0]),
188+
"overlap_duration_s": str(self._overlap_duration_s[0]),
189+
"speed": str(self._speed[0]),
190+
"n_inputs": str(self.n_inputs),
191+
"model_path": str(self._model_path[0]),
192+
"model_version": str(self._model_version[0]),
193+
"model_fmin": str(self._model_fmin[0]),
194+
"model_fmax": str(self._model_fmax[0]),
195+
"model_sr": str(self._model_sr[0]),
196+
"model_precision": str(self._model_precision[0]),
197+
"embedding_dim": str(self.emd_dim),
198+
}
199+
schema_with_metadata = pa.schema(fields, metadata=metadata)
200+
table = pa.table(arrow_arrays, schema=schema_with_metadata)
201+
return table
202+
203+
def to_csv(
204+
self,
205+
path: os.PathLike | str,
206+
*,
207+
encoding: str = "utf-8",
208+
buffer_size_kb: int = 1024,
209+
silent: bool = False,
210+
) -> None:
211+
if not silent:
212+
print("Preparing CSV export...") # noqa: T201
213+
214+
structured = self.to_structured_array()
215+
216+
buffer_bytes = buffer_size_kb * 1024
217+
output_path = Path(path)
218+
219+
if output_path.suffix != ".csv":
220+
raise ValueError("Output path must have a .csv suffix")
221+
222+
with output_path.open("w", encoding=encoding, buffering=buffer_bytes) as f:
223+
f.write(f"{VAR_INPUT},{VAR_START_TIME},{VAR_END_TIME},{VAR_EMBEDDING}\n")
224+
225+
block: list[str] = []
226+
block_size_bytes = 0
227+
total_size_bytes = 0
228+
collected_size_bytes = 0
229+
update_size_every = 1024**2 * 100
230+
231+
with tqdm(
232+
total=len(structured),
233+
desc="Writing CSV",
234+
unit="embeddings",
235+
disable=silent,
236+
) as pbar:
237+
for record in structured:
238+
line = (
239+
f"{format_input_for_csv(record[VAR_INPUT])},"
240+
f'"{hms_centis_fast(record[VAR_START_TIME])}",'
241+
f'"{hms_centis_fast(record[VAR_END_TIME])}",'
242+
f"{_format_embedding_for_csv(record[VAR_EMBEDDING])}\n"
243+
)
244+
245+
block.append(line)
246+
block_size_bytes += len(line.encode(encoding))
247+
248+
if block_size_bytes >= buffer_bytes:
249+
f.writelines(block)
250+
block.clear()
251+
collected_size_bytes += block_size_bytes
252+
block_size_bytes = 0
253+
254+
pbar.update(1)
255+
256+
if collected_size_bytes >= update_size_every or pbar.n == pbar.total:
257+
total_size_bytes += collected_size_bytes
258+
collected_size_bytes = 0
259+
260+
if not silent:
261+
pbar.set_postfix({"CSV": f"{total_size_bytes / 1024**2:.0f} MB"})
262+
263+
if block:
264+
f.writelines(block)
265+
148266
def unprocessable_inputs(self) -> np.ndarray:
149267
return self._unprocessable_inputs
150268

@@ -240,3 +358,9 @@ def __init__(
240358
model_precision=model_precision,
241359
model_version=model_version,
242360
)
361+
362+
363+
def _format_embedding_for_csv(embedding: np.ndarray, decimals: int = 6) -> str:
364+
fmt = f"{{:.{decimals}f}}"
365+
formatted = ",".join(fmt.format(value) for value in embedding)
366+
return f'"{formatted}"'

src/birdnet/acoustic/inference/core/prediction/prediction_result.py

Lines changed: 16 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import os
44
from pathlib import Path
5-
from typing import TYPE_CHECKING, Any, Literal
5+
from typing import TYPE_CHECKING, Any
66

77
import numpy as np
88
from ordered_set import OrderedSet
@@ -19,12 +19,13 @@
1919
)
2020
from birdnet.utils.helper import (
2121
apply_speed_to_duration,
22+
format_input_for_csv,
2223
get_hop_duration_s,
2324
get_uint_dtype,
25+
hms_centis_fast,
2426
)
2527

2628
if TYPE_CHECKING:
27-
import pandas as pd
2829
import pyarrow as pa
2930

3031
VAR_SPECIES_NAME = "species_name"
@@ -219,7 +220,7 @@ def to_arrow_table(self) -> pa.Table:
219220

220221
structured = self.to_structured_array()
221222

222-
arrow_arrays = {}
223+
arrow_arrays: dict[str, pa.Array] = {}
223224
arrow_arrays[VAR_INPUT] = pa.array(structured[VAR_INPUT]).dictionary_encode()
224225
arrow_arrays[VAR_START_TIME] = pa.array(
225226
structured[VAR_START_TIME],
@@ -261,9 +262,6 @@ def to_arrow_table(self) -> pa.Table:
261262
table = pa.table(arrow_arrays, schema=schema_with_metadata)
262263
return table
263264

264-
def _format_input_for_csv(self, input_value: Any) -> str: # noqa: ANN401
265-
return f'"{input_value}"'
266-
267265
def to_csv(
268266
self,
269267
path: os.PathLike | str,
@@ -273,13 +271,13 @@ def to_csv(
273271
silent: bool = False,
274272
) -> None:
275273
if not silent:
276-
print("Preparing CSV export...")
274+
print("Preparing CSV export...") # noqa: T201
277275

278276
structured = self.to_structured_array()
279277

280278
buffer_bytes = buffer_size_kb * 1024
281-
282279
output_path = Path(path)
280+
283281
if output_path.suffix != ".csv":
284282
raise ValueError("Output path must have a .csv suffix")
285283

@@ -289,7 +287,7 @@ def to_csv(
289287
f"{VAR_INPUT},{VAR_START_TIME},{VAR_END_TIME},{VAR_SPECIES_NAME},{VAR_CONFIDENCE}\n"
290288
)
291289

292-
block = []
290+
block: list[str] = []
293291
block_size_bytes = 0
294292
total_size_bytes = 0
295293
collected_size_bytes = 0
@@ -302,7 +300,13 @@ def to_csv(
302300
disable=silent,
303301
) as pbar:
304302
for record in structured:
305-
line = f'{self._format_input_for_csv(record[VAR_INPUT])},"{hms_centis_fast(record[VAR_START_TIME])}","{hms_centis_fast(record[VAR_END_TIME])}","{record[VAR_SPECIES_NAME]}",{record[VAR_CONFIDENCE]:.6f}\n'
303+
line = (
304+
f"{format_input_for_csv(record[VAR_INPUT])},"
305+
f'"{hms_centis_fast(record[VAR_START_TIME])}",'
306+
f'"{hms_centis_fast(record[VAR_END_TIME])}",'
307+
f'"{record[VAR_SPECIES_NAME]}",'
308+
f"{record[VAR_CONFIDENCE]:.6f}\n"
309+
)
306310

307311
block.append(line)
308312
block_size_bytes += len(line.encode(encoding))
@@ -319,54 +323,14 @@ def to_csv(
319323
if collected_size_bytes >= update_size_every or pbar.n == pbar.total:
320324
total_size_bytes += collected_size_bytes
321325
collected_size_bytes = 0
326+
322327
if not silent:
323328
pbar.set_postfix({"CSV": f"{total_size_bytes / 1024**2:.0f} MB"})
324329

325330
# Final flush
326331
if block:
327332
f.writelines(block)
328333

329-
def to_dataframe(self) -> pd.DataFrame:
330-
import pandas as pd
331-
332-
df = pd.DataFrame(self.to_structured_array(), copy=True)
333-
return df
334-
335-
def to_parquet(
336-
self,
337-
path: os.PathLike | str,
338-
*,
339-
compression: Literal["none", "snappy", "gzip", "brotli", "lz4", "zstd"] = "snappy",
340-
compression_level: int | None = None,
341-
silent: bool = False,
342-
) -> None:
343-
import pyarrow.parquet as pq
344-
345-
path = Path(path)
346-
if path.suffix != ".parquet":
347-
raise ValueError("Output path must have a .parquet suffix")
348-
349-
if not silent:
350-
print("Creating Arrow table...")
351-
352-
table = self.to_arrow_table()
353-
354-
if not silent:
355-
print(f"Writing Parquet to {path.absolute()} ...")
356-
357-
pq.write_table(
358-
table,
359-
path,
360-
compression=compression,
361-
compression_level=compression_level,
362-
)
363-
364-
if not silent:
365-
file_size = path.stat().st_size / 1024**2
366-
original_size = table.nbytes / 1024**2
367-
compression_ratio = original_size / file_size if file_size > 0 else 0
368-
print(f"Parquet file: {file_size:.1f} MB (compression: {compression_ratio:.1f}x)")
369-
370334

371335
class AcousticFilePredictionResult(AcousticPredictionResultBase):
372336
def __init__(
@@ -455,12 +419,5 @@ def __init__(
455419
model_version=model_version,
456420
)
457421

458-
def _format_input_for_csv(self, input_value: Any) -> str:
422+
def _format_input_for_csv(self, input_value: Any) -> str: # noqa: ANN401
459423
return f"{input_value}"
460-
461-
462-
def hms_centis_fast(v: float) -> str:
463-
h, rem = divmod(v, 3600)
464-
m, s = divmod(rem, 60)
465-
result = f"{int(h):02}:{int(m):02}:{s:05.2f}"
466-
return result

0 commit comments

Comments
 (0)