Reading in Data
This module serves as the data entry point and engineering foundation for the MooVision project. It handles the parsing, structural validation, asset tracking, and label synchronization required to transform raw, manually maintained clip indices and unstructured CVAT annotation exports into clean datasets for model training.
Summary
By matching .zip annotations to clip names we build a data index housing the neccessary information on videos and label folder required for all downstream pipeline functionality. Moreover, through strict data schemas and filtering methods, it catches formatting errors, missing videos, or duplicate labels early, allowing all neccessary data to reach downstream machine learning workflows while preventing corrupted data from moving beyond the intial boundary point.
| Property | Detail |
|---|---|
| Primary Script | read_data_from_index_file.py |
| Supporting Modules | matching.py (String Matcher), schema.py (Pandera Validation) |
| Libraries Used | Pandas + Pandera |
| Input | Index sheet (CSV/Excel/etc.) + Unlabelled clips folder + CVAT .zip annotations |
| Output | Validated raw index copy + Processed model-ready index CSV |
| CLI Support | Yes |
Note: For explicit column definitions and data type constraints governing these dataframes, refer directly to
Data Schemabelow.
Input
- Clips Index File: A tabular spreadsheet (
.csv,.xlsx, etc.) containing clip metadata, animal identifiers, pen numbers, and video attributes. - Unlabelled Clips Directory (
clips_dir): The folder containing sliced, short-form.mp4video clips. - Labelled Clips Directory (
labels_dir): The folder containing bounding box annotation.zipbundles exported from CVAT. - Source Videos Directory (
source_dir): Root storage hosting the original, continuous un-segmented baseline footage.
Output
The script saves processed index states directly back to disk and returns None. The resulting outputs are organized into raw and processed subdirectories:
ROOT_DIR/ # Data root directory configured in config.py
└── data/
├── raw/
│ └── all_clips_index_raw.csv # Validated copy of raw ingestion data before asset filtering
└── processed/
└── processed_clips_index.csv # Synchronized index with verified path mappings and paired label references
How it works
- The reader automatically detects the index file extension and applies the correct parsing engine (supporting
.csv,.xlsx,.parquet,.json, and.tsv). - The input dataset is validated against the raw schema rules. If columns are missing or data types are incorrect, execution halts immediately.
- The pipeline checks the physical presence of every clip file listed in the index. Rows pointing to missing assets are dropped from the final index, and a warning is logged.
- A regex engine standardizes annotation filename variations down to unique numeric ID and part ID tokens to reliably link raw clips with hand-named CVAT exports (
.zipannotation folders). Relative paths to these folders are then added to the data index under the columnlabelled_clip_relative_path - Once label paths are resolved and appended, the dataframe undergoes a final validation pass for type stability and chronological logic before being saved to disk.
Core Pipeline Concepts
Flexible Format Resolution
To maximize workflow flexibility, the ingestion framework identifies file suffixes at runtime and dynamically maps them to their respective reading lambdas. This allows the pipeline to accept human-readable spreadsheets (.xlsx), flat-files (.csv, .tsv), or column-compressed stores (.parquet) without requiring manual code changes.
Strict Structural Alignment
Dataframe validation blocks default to strict=True and coerce=False layouts. Downstream modeling components depend on predictable datatypes; introducing unauthorized metadata columns or passing mismatched format types yields immediate terminal faults rather than letting bad data pass through silently.
Chronological Validation
The pipeline evaluates temporal sequences to ensure absolute logical continuity across all recorded windows. Pandera mathematical checks verify that clip start-times consistently precede their respective end-times across all tracking boundaries:
interval_end_obs_sec > interval_start_obs_sec part_end_obs_sec > part_start_obs_sec source_segment_obs_end_sec > source_segment_obs_start_sec clip_end_in_source_sec > clip_start_in_source_sec
Unification & Regex Matching
Because filenames vary, the matching module automatically standardizes naming conventions to link CVAT annotation folders directly to their corresponding video clips.
-
Real-World Clip Name Parsing: Currently checks for
CS_{numeric_ID}...format near the filename beginning to capture the numeric ID, and checks for..._part01.mp4or..._part02.mp4to capture the part ID. If no part id is found it is labelled as None. -
Real-World Variant Handling: The module parses and resolves explicit part strings, whitespace discrepancies, and common human typos (e.g.,
0003 - p2.zip,0004_part02zip.zip,0101_part01zip.zip) matching annotation labels back to the original cross-sucking clips.
Note: For more information see Data Requirements.
Conflict Mitigation & Ambiguity Faults - Fixed Clips
When multiple annotation archives match a single clip index record, the engine checks file locations. If a naming conflict occurs between a file inside the fixed_clips directory and a base clip option, the script defaults to the base file and logs a UserWarning. However, if multiple conflicting files emerge without a clear fallback rule, a ValueError is triggered to protect training set integrity.
Usage
Command Line Execution
uv run scripts/read_data/read_all_clips_index.py \
--index_path "data/cross_sucking_clips/all_clips_index.csv" \
--clips_dir "data/unlabelled_clips" \
--labels_dir "data/labelled_clips" \
--force
Function Reference
Module to read all_clips_index.csv.
Filters for videos existing in file path, and appends corresponding labelled video paths.
Notes
Naming Conventions: Convention: CS_{clip_number){Weaning_period}_d{day_number}_p{pen_number}_cow{cow_identifier}{ddmmyyyy}{source_video_base_name}{clip_start_time_s}_{clip_end_time_s}.mp4 Example: CS_0001_POSTWEAN_d1_p2_cow6_02112025_ch02-20251102075200_684_702.mp4
add_label_paths(df, label_paths)
This is an internal function meant to be used in read_data_from_index_file.
Takes in a df and a list of relative paths to annotation data folders, and adds the list as a new column to the data frame. This adds corresponding annotation data paths for each cross-sucking clip to the data frame.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
DataFrame
|
A pandas DataFrame |
required |
label_paths
|
list[str | None]
|
A list of relative paths to annotation data folder matched to cross-sucking clips, or None if a clip does not have a match. |
required |
Returns:
| Type | Description |
|---|---|
DataFrame
|
A pandas DataFrame representing the filtered index file with an extra column added for relative paths to annotated data labels. |
Raises:
| Type | Description |
|---|---|
TypeError
|
If df is not a pandas DataFrame. |
ValueError
|
If either df, or labels_paths is empty. |
Notes
This function requires data validation and should only be called after calling validate_data(). This output should also be validated using the processed_schema DataFrameSchema in schema.py.
Examples:
Source code in scripts/read_data/read_all_clips_index.py
515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 | |
filter_existing_clips(df, clips_dir)
This is an internal function meant to be used in read_data_from_index_file.
Takes in a validated dataframe, the root directory for cross-sucking
clips, and filters the data frame for rows which have a
"clip_relative_path" that exists in the clip_dir.
This function raises a warning if the filtered df is empty. It also returns a warning stating the number of rows (videos) dropped.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
DataFrame
|
A pandas dataframe meant to represent the validated dataframe output from validate_data() |
required |
clips_dir
|
Path
|
The path to the root directory holding all cross-sucking clips. |
required |
Returns:
| Type | Description |
|---|---|
DataFrame
|
The validated dataframe filtered for rows which have cross-sucking clips in the file path. |
Raises:
| Type | Description |
|---|---|
TypeError
|
If df is not a pandas DataFrame. |
ValueError
|
If df is an empty dataframe. |
FileNotFoundError
|
If clips_dir does not exist in the file path. |
Notes
This function requires the "clip_relative_path" column in the data frame and relies on the data validation to ensure this column exists. This is an internal function that should only ever be called after validate_data().
Examples:
>>> import pandas as pd
>>> from pathlib import Path
>>> df = pd.DataFrame({
... "clip_relative_path": ["clips/a.mp4", "clips/b.mp4", "clips/c.mp4"],
... "label": ["cat", "dog", "cat"],
... })
>>> clips_dir = Path("/data/moovision/clips")
>>> filtered_df = filter_existing_clips(df, clips_dir)
>>> len(filtered_df)
Source code in scripts/read_data/read_all_clips_index.py
221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 | |
filter_label_paths(df)
This is an internal function meant to be used in read_data_from_index_file.
Filter the data file for cross-sucking clips which have an associated annotation file present in the data. Takes in a data file which has had relative paths to annotation files added in a "labelled_clip_relative_path" and filters for rows where this columns is not None. The column "labelled_clip_relative_path" is the output of match_label_paths, and has None values where matches were not found in the data. Filtering out None values removes these rows from training sets as both cross-sucking clips and annotation labels are required for model training.
This function requires the data is validated for the correct columns. It is meant to be called after add_label_paths, and after processed data has been validated.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
DataFrame
|
A pandas data frame representing the output of validate_data once the relative paths to annotation labels have been added via add_labels. |
required |
Returns:
| Type | Description |
|---|---|
DataFrame
|
A pandas data frame containing only cross-sucking clips that have a matching annotation labels folder in the data. |
Raises:
| Type | Description |
|---|---|
TypeError
|
If df is not a pandas DataFrame |
ValueError
|
If df is empty |
Notes
This function will raise warnings for the number of dropped cross-sucking clips, and if the returned df is empty.
Examples:
Source code in scripts/read_data/read_all_clips_index.py
571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 | |
get_label_paths(labels_dir)
This is an internal function meant to be used in read_data_from_index_file.
Looks for all folders containing bounding box annotations in the labelled clips directory, and creates a list of tuples with names and relative paths to the annotation folders.
Assumes all .zip files in the labels_dir directory are annotations for CS
events. This function will read in all zip files in the labels_dir which
may cause errors down the line.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
labels_dir
|
Path
|
The path to the root directory containing bounding box annotations for the cross-sucking clips. |
required |
Returns:
| Type | Description |
|---|---|
list[tuple[str, str]]
|
A list of tuples containing the .zip file names and relative paths within the annotated labels directory. |
Raises:
| Type | Description |
|---|---|
FileNotFoundError
|
If labels_dir does not exist, or if no .zip files are found in the specified directory. |
Examples:
>>> from pathlib import Path
>>> labels_dir = Path("/data/moovision/labels")
>>> label_paths = get_label_paths(labels_dir)
>>> label_paths
[("annotations_batch1.zip", "labels/annotations_batch1.zip"),
("annotations_batch2.zip", "labels/annotations_batch2.zip")]
If no .zip files are found or the directory does not exist, an error is raised:
>>> get_label_paths(Path("/data/moovision/empty_dir"))
FileNotFoundError: No .zip files found in /data/moovision/empty_dir
Source code in scripts/read_data/read_all_clips_index.py
319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 | |
match_label_paths(df, label_paths)
This is an internal function meant to be used in read_data_from_index_file.
Takes in a df and a list of zip folder names and relative paths to annotated cross-sucking data folders output from get_label_paths(). For each cross-sucking clip in the df, match_label_paths searches over all names/paths in label_paths to find a matching annotaion folder. The function returns a list of relative paths to these matching annotation folders, or None if a matching folder cannot be found. This is meant to be passed on and later added to the index data frame as a column of relative paths to data annotation folders.
These names use the is_match() function from matching.py to match cross-sucking clip names to annotation folder names usign regex captures to parse and compare numeric ID's and part ID's from each name. See the matching.py documentation for more detail. is_match() also assumes the correct naming conventions for cross-sucking clips and annotaion folders. This will raise an error if the function encounters an unkown naming format.
If there are multiple/duplicate matches the function raises an error as it does not know which folder contains the correct annotations. If there are duplicate matches between normal video paths and video paths to fixed_clips folder, the normal video paths are used, and a warning is raised.
A warning is also raised if no matches are found for any clip.
Parameter
df : pd.DataFrame A pandas DataFrame label_paths : list[tuple[str, str]] a list of annotation folder names and relative paths inside the labelled data directory.
Returns:
| Type | Description |
|---|---|
list[str | None]
|
A list of relative paths to annotaion folders, or None values if a cross-sucking clip name does not match to any annotation folder names. |
Raises:
| Type | Description |
|---|---|
TypeError
|
If df is not a Pandas DataFrame |
ValueError
|
If df, or label_paths is empty. If there are more than 2 matches for a cross-sucking clip, or if there are exactly 2 matches, but neither are a fixed_path file. |
Notes
This requires the dataset be previously validated and should only be called after validate_data().
Examples:
Source code in scripts/read_data/read_all_clips_index.py
381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 | |
read_data(path)
Read data from path and return it as a pandas dataframe. This is an
internal function meant for use in read_data_from_index_file()
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
Path
|
Path to data file to read in |
required |
Returns:
| Type | Description |
|---|---|
DataFrame
|
A pandas dataframe of the existing file. |
Raises:
| Type | Description |
|---|---|
FileNotFoundError
|
If the file path to the data frame does not exist. |
Notes
Handles multiple file types: .csv, .xlsx, .parquet, .json, .tsv.
Examples:
.. code-block:: python
from config import ROOT_DIR
INDEX_PATH = ROOT_DIR / "cross_sucking_clips" / "all_clips_index.csv"
df = read_data(INDEX_PATH)
Source code in scripts/read_data/read_all_clips_index.py
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | |
read_data_from_index_file(index_path, clips_dir, labels_dir, source_dir, raw_output, processed_output, force=False)
Reads in data from index.csv file, adds labelled cross sucking clip (CVAT Output) paths to matching unlabelled cross sucking clips. Filters for rows which have a source video, unlabelled cross sucking clip, and labelled cross sucking output whose paths exists within the given directories.
Saves a raw index.csv to the directory specified at out_index_dir, and an output index (filtered csv file) to directory specified at output_index_dir.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
index_path
|
Path
|
Path to index.csv file. |
required |
clips_dir
|
Path
|
Path to folder with unlabelled cross sucking clips. |
required |
labels_dir
|
Path
|
Path to folder with labelled cross sucking clips (CVAT Output). |
required |
source_dir
|
Path
|
Path to raw source videos. |
required |
raw_output
|
Path
|
Path to output raw index file |
required |
processed_output
|
Path
|
Path to output processed index file |
required |
FORCE
|
bool
|
Force rewriting of indices if they already exist |
required |
Returns:
| Type | Description |
|---|---|
None
|
The function writes to disk and does not return a value. |
Raises:
| Type | Description |
|---|---|
FileNotFoundError
|
If the path to index.csv does not exist does not exist. |
Notes
This function ensures that the index file contains relative paths to the source files and cross sucking clips, as well as other specific column formats. See documentation for more details.
This is an internal function, input paths should be called via config.py.
Examples:
>>> from config import INDEX_PATH
>>> read_data_from_index_file(INDEX_PATH)
Source code in scripts/read_data/read_all_clips_index.py
644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 | |
save_data(df, path)
This is an internal function meant for use in read_data_from_index_file()
Takes in a dataframe and a path, and saves to dataframe to the location specified from the path. This is to be used to save the raw and processed data files to data/raw/ and data/processed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
DataFrame
|
The dataframe to save |
required |
path
|
Path
|
The path to save the data frame to. |
required |
Returns:
| Type | Description |
|---|---|
None
|
This function reads to disk and does not return anything. |
Raises:
| Type | Description |
|---|---|
TypeError
|
If df input is not a pandas DataFrame. |
ValueError
|
If df is empty or if path does not end in |
Notes
This function assumes the output is a .csv file. To update this,
you can update the format check in the function to replace ".csv" with
whichever output extension you would like.
Examples:
>>> import pandas as pd
>>> df = pd.DataFrame({"age": [25, 30], "name": ["Alice", "Bob"]})
>>> save_data(df, Path("data/processed/processed.csv))
Saved file to ~/.../data/processed/processed.csv
Source code in scripts/read_data/read_all_clips_index.py
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 | |
validate_data(df, df_schema)
This is an internal function meant for use in read_data_from_index_file()
Validate a pandas dataframe against a data schema using pandera. Takes in a data frame and a dataframe schema and validates the data frame against the schema. This is deisgned to enforce data strucutres to ensure the pipeline runs as intended through downstream usage.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
DataFrame
|
The dataframe to validate. |
required |
df_schema
|
DataFrameSchema
|
The pandera dataframe schema to validata against. |
required |
Returns:
| Type | Description |
|---|---|
DataFrame
|
The validated dataframe. |
Raises:
| Type | Description |
|---|---|
TypeError
|
If inputs are not a pandas DataFrame or a pandera DataFrameSchema |
ValueError
|
If the dataframe is empty, or if there are any validation errors when calling df_schema.validate(...) |
Notes
There are two data schemas for the MooVision project, held in schema.py. These are for validating the raw data file and the processed data frame.
Examples:
>>> import pandas as pd
>>> import pandera as pa
>>> schema = pa.DataFrameSchema({
... "age": pa.Column(int, pa.Check.ge(0)),
... "name": pa.Column(str),
... })
>>> df = pd.DataFrame({"age": [25, 30], "name": ["Alice", "Bob"]})
>>> validate_data(df, schema)
age name
0 25 Alice
1 30 Bob
>>> # Using the project schemas from schema.py
>>> from schema import raw_schema
>>> validated_df = validate_data(raw_df, raw_schema)
Source code in scripts/read_data/read_all_clips_index.py
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 | |
options: show_source: false show_root_heading: true
WIP
- Source Video Verification Integration: Hook the
source_dirpath variable into an active file-checking sequence to purge row entries from the index if the underlying continuous source videos are missing. - Part Index Boundary Constraints: Reactivate the disabled internal validation check to programmatically enforce part count parameters:
part_index < part_count
- Regex Alignment Update: Update
parse_unlabelled_nameto handle alternative separator variations. The pattern currently processes standard part strings (e.g.,_part01), but it chokes on double underscores (e.g.,_part_01). Updating this pattern will better capture common human-edited export names.