lactationcurve.characteristics.method_test_interval

ICAR Test Interval Method for accumulated lactation yield.

Purpose

This module implements the ICAR Test Interval Method (Procedure 2, Section 2) to compute cumulative milk yield from test-day records.

Method Summary

Calculate total milk yield over a lactation by summing three parts:
  • First test-day milk yield multiplied by the number of days between calving and the first test day.
  • Trapezoidal integration between consecutive test days (the average of two consecutive yields multiplied by the interval length).
  • Last test-day milk yield multiplied by the number of days from the last test day to the end of the calculation window.
Formula:

MY = I0M1 + I1(M1 + M2)/2 + I2*(M2 + M3)/2 + ... + I(n-1)(M(n-1) + Mn)/2 + InMn

Where:

  • MY: total milk yield over the lactation window.
  • M1, M2, ..., Mn: milk yield measured in the 24 hours of each test day.
  • I1, I2, ..., I(n-1): interval lengths (days) between consecutive test days.
  • I0: days from lactation start (calving) to first test day.
  • In: days from last test day to the end of the calculation window (e.g., DIM 305).

Key Entry Points

  • test_interval_method: Computes cumulative lactation milk yield per TestId using start, interval, and end segments.

Column Flexibility

The function accepts several case-insensitive column name aliases and can create a default TestId if one is missing. Recognized aliases:

  • Days in Milk: ["daysinmilk", "dim", "testday"]
  • Milk Yield: ["milkingyield", "testdaymilkyield", "milkyield", "yield"]
  • Test Id: ["animalid", "testid", "id"]

It is also possible to provide your own column names so the function can be applied to dataframes with different column naming conventions.

Returns a DataFrame with columns: ["TestId", "LactationMilkYield"].

Notes

  • Units: DIM (days in milk) is measured in days, and milk yield can be in kg or lb. The output stays in the same unit as the input.
  • Records with DIM > max_dim are excluded before computation.
  • This method's main strength is its ease of use and simplicity.
  • Its main disadvantage is that it does not account for the shape of the lactation curve, which can lead to underestimation of total yield, especially for lactations with few test days or irregular patterns. Outliers in test-day records can also have a large influence on the final result.
  • When a full lactation is known (i.e., test days up to DIM 305), this method will give a higher cumulative milk yield then the sum off all test days, because of the way the start and end contributions are calculated.
  • This method gives the same result as the Fleischmann method, which is formulated differently but mathematically equivalent.

Reference

Sargent, F. D., V. H. Lyton, and 0. G. Wall, J r . 1968. Test interval method of calculating Dairy Herd Improvement Association records. J. Dairy Sci. 51:170.

Author: Meike van Leerdam Date: 07-31-2025 Last update: 22-May-2026

  1"""
  2ICAR Test Interval Method for accumulated lactation yield.
  3
  4Purpose
  5-------
  6This module implements the ICAR Test Interval Method (Procedure 2,
  7Section 2) to compute cumulative milk yield from test-day records.
  8
  9Method Summary
 10--------------
 11Calculate total milk yield over a lactation by summing three parts:
 12        - First test-day milk yield multiplied by the number of days between
 13            calving and the first test day.
 14        - Trapezoidal integration between consecutive test days (the average of
 15            two consecutive yields multiplied by the interval length).
 16        - Last test-day milk yield multiplied by the number of days from the last
 17            test day to the end of the calculation window.
 18
 19Formula:
 20
 21    MY = I0*M1
 22         + I1*(M1 + M2)/2
 23         + I2*(M2 + M3)/2
 24         + ...
 25         + I(n-1)*(M(n-1) + Mn)/2
 26         + In*Mn
 27
 28Where:
 29- ``MY``: total milk yield over the lactation window.
 30- ``M1, M2, ..., Mn``: milk yield measured in the 24 hours of each test day.
 31- ``I1, I2, ..., I(n-1)``: interval lengths (days) between consecutive test days.
 32- ``I0``: days from lactation start (calving) to first test day.
 33- ``In``: days from last test day to the end of the calculation window (e.g., DIM 305).
 34
 35
 36Key Entry Points
 37----------------
 38- ``test_interval_method``: Computes cumulative lactation milk yield per
 39    ``TestId`` using start, interval, and end segments.
 40
 41
 42Column Flexibility
 43------------------
 44The function accepts several case-insensitive column name aliases and can
 45create a default ``TestId`` if one is missing. Recognized aliases:
 46
 47- Days in Milk: `["daysinmilk", "dim", "testday"]`
 48- Milk Yield: `["milkingyield", "testdaymilkyield", "milkyield", "yield"]`
 49- Test Id: `["animalid", "testid", "id"]`
 50
 51It is also possible to provide your own column names so the function
 52can be applied to dataframes with different column naming conventions.
 53
 54Returns a DataFrame with columns: ``["TestId", "LactationMilkYield"]``.
 55
 56Notes
 57-----
 58- Units: DIM (days in milk) is measured in days,
 59    and milk yield can be in kg or lb. The
 60    output stays in the same unit as the input.
 61- Records with ``DIM > max_dim`` are excluded before computation.
 62- This method's main strength is its ease of use and simplicity.
 63- Its main disadvantage is that it does not account for the shape of the
 64    lactation curve, which can lead to underestimation of total yield,
 65    especially for lactations with few test days or irregular patterns.
 66    Outliers in test-day records can also have a large influence on the final
 67    result.
 68- When a full lactation is known (i.e., test days up to DIM 305),
 69this method will give a higher cumulative milk yield then the sum off all test days,
 70because of the way the start and end contributions are calculated.
 71- This method gives the same result as the Fleischmann method,
 72which is formulated differently but mathematically equivalent.
 73
 74Reference
 75---------
 76Sargent, F. D., V. H. Lyton, and 0. G. Wall, J r . 1968. Test interval method of
 77calculating Dairy Herd Improvement Association records. J. Dairy Sci. 51:170.
 78
 79Author: Meike van Leerdam
 80Date: 07-31-2025
 81Last update: 22-May-2026
 82"""
 83
 84import pandas as pd
 85
 86from lactationcurve.preprocessing import standardize_lactation_columns
 87
 88
 89def test_interval_method(
 90    df: pd.DataFrame,
 91    days_in_milk_col: str | None = None,
 92    milking_yield_col: str | None = None,
 93    test_id_col: str | None = None,
 94    default_test_id: int = 0,
 95    max_dim: int = 305,
 96) -> pd.DataFrame:
 97    """Compute total lactation milk yield using the ICAR Test Interval Method.
 98
 99    The method applies:
100    - First test day milk yield from calving to the first test day,
101    - Trapezoidal integration between consecutive test days,
102    - Last test day milk yield from the last test day to DIM = max_dim (default = 305).
103
104    Args:
105        df (pd.DataFrame): Input DataFrame with at least DaysInMilk and
106            MilkingYield columns, plus an optional TestId column. Column names
107            can be provided explicitly or matched via known aliases.
108        days_in_milk_col (str | None): Optional column name override for
109            DaysInMilk.
110        milking_yield_col (str | None): Optional column name override for
111            MilkingYield.
112        test_id_col (str | None): Optional column name override for TestId.
113        default_test_id (int): Value used to create a default TestId column if
114            one is missing.
115        max_dim (int): Lactation length used to calculate cumulative
116            production. The default is 305 days.
117            Records with DIM > max_dim are excluded.
118
119    Returns:
120        pd.DataFrame: Two-column DataFrame with
121            - "TestId": identifier per lactation,
122            - "LactationMilkYield": computed total milk yield over the
123              specified window.
124
125    Raises:
126        ValueError: If required columns (DaysInMilk or MilkingYield) cannot be found.
127
128    Notes:
129        - Records with DIM > max_dim are dropped before computation.
130        - At least two data points per TestId are required for trapezoidal integration;
131          otherwise the lactation is skipped.
132    """
133
134    # Standardize columns and filter DIM <= max_dim
135    df = standardize_lactation_columns(
136        df,
137        days_in_milk_col=days_in_milk_col,
138        milking_yield_col=milking_yield_col,
139        test_id_col=test_id_col,
140        default_test_id=default_test_id,
141        max_dim=max_dim,
142    )
143
144    result = []
145
146    # Iterate over each lactation
147    for lactation in df["TestId"].unique():
148        lactation_df = pd.DataFrame(df[df["TestId"] == lactation])
149
150        # Sort by DaysInMilk ascending
151        lactation_df.sort_values(by="DaysInMilk", ascending=True, inplace=True)
152
153        if len(lactation_df) < 2:
154            print(f"Skipping TestId {lactation}: not enough data points for interpolation.")
155            continue
156
157        # Start and end points
158        start = lactation_df.iloc[0]
159        end = lactation_df.iloc[-1]
160
161        # Start contribution
162        MY0 = start["DaysInMilk"] * start["MilkingYield"]
163
164        # End contribution
165        MYend = (max_dim + 1 - end["DaysInMilk"]) * end["MilkingYield"]
166
167        # Intermediate trapezoidal contributions
168        lactation_df["width"] = lactation_df["DaysInMilk"].diff().shift(-1)
169        lactation_df["avg_yield"] = (
170            lactation_df["MilkingYield"] + lactation_df["MilkingYield"].shift(-1)
171        ) / 2
172        lactation_df["trapezoid_area"] = lactation_df["width"] * lactation_df["avg_yield"]
173
174        total_intermediate = lactation_df["trapezoid_area"].sum()
175
176        total_yield = MY0 + total_intermediate + MYend
177        result.append((lactation, total_yield))
178
179    return pd.DataFrame(result, columns=pd.Index(["TestId", "LactationMilkYield"]))
180
181
182# to prevent pytest from trying to collect this function as a test
183test_interval_method.__test__ = False
def test_interval_method( df: pandas.DataFrame, days_in_milk_col: str | None = None, milking_yield_col: str | None = None, test_id_col: str | None = None, default_test_id: int = 0, max_dim: int = 305) -> pandas.DataFrame:
 90def test_interval_method(
 91    df: pd.DataFrame,
 92    days_in_milk_col: str | None = None,
 93    milking_yield_col: str | None = None,
 94    test_id_col: str | None = None,
 95    default_test_id: int = 0,
 96    max_dim: int = 305,
 97) -> pd.DataFrame:
 98    """Compute total lactation milk yield using the ICAR Test Interval Method.
 99
100    The method applies:
101    - First test day milk yield from calving to the first test day,
102    - Trapezoidal integration between consecutive test days,
103    - Last test day milk yield from the last test day to DIM = max_dim (default = 305).
104
105    Args:
106        df (pd.DataFrame): Input DataFrame with at least DaysInMilk and
107            MilkingYield columns, plus an optional TestId column. Column names
108            can be provided explicitly or matched via known aliases.
109        days_in_milk_col (str | None): Optional column name override for
110            DaysInMilk.
111        milking_yield_col (str | None): Optional column name override for
112            MilkingYield.
113        test_id_col (str | None): Optional column name override for TestId.
114        default_test_id (int): Value used to create a default TestId column if
115            one is missing.
116        max_dim (int): Lactation length used to calculate cumulative
117            production. The default is 305 days.
118            Records with DIM > max_dim are excluded.
119
120    Returns:
121        pd.DataFrame: Two-column DataFrame with
122            - "TestId": identifier per lactation,
123            - "LactationMilkYield": computed total milk yield over the
124              specified window.
125
126    Raises:
127        ValueError: If required columns (DaysInMilk or MilkingYield) cannot be found.
128
129    Notes:
130        - Records with DIM > max_dim are dropped before computation.
131        - At least two data points per TestId are required for trapezoidal integration;
132          otherwise the lactation is skipped.
133    """
134
135    # Standardize columns and filter DIM <= max_dim
136    df = standardize_lactation_columns(
137        df,
138        days_in_milk_col=days_in_milk_col,
139        milking_yield_col=milking_yield_col,
140        test_id_col=test_id_col,
141        default_test_id=default_test_id,
142        max_dim=max_dim,
143    )
144
145    result = []
146
147    # Iterate over each lactation
148    for lactation in df["TestId"].unique():
149        lactation_df = pd.DataFrame(df[df["TestId"] == lactation])
150
151        # Sort by DaysInMilk ascending
152        lactation_df.sort_values(by="DaysInMilk", ascending=True, inplace=True)
153
154        if len(lactation_df) < 2:
155            print(f"Skipping TestId {lactation}: not enough data points for interpolation.")
156            continue
157
158        # Start and end points
159        start = lactation_df.iloc[0]
160        end = lactation_df.iloc[-1]
161
162        # Start contribution
163        MY0 = start["DaysInMilk"] * start["MilkingYield"]
164
165        # End contribution
166        MYend = (max_dim + 1 - end["DaysInMilk"]) * end["MilkingYield"]
167
168        # Intermediate trapezoidal contributions
169        lactation_df["width"] = lactation_df["DaysInMilk"].diff().shift(-1)
170        lactation_df["avg_yield"] = (
171            lactation_df["MilkingYield"] + lactation_df["MilkingYield"].shift(-1)
172        ) / 2
173        lactation_df["trapezoid_area"] = lactation_df["width"] * lactation_df["avg_yield"]
174
175        total_intermediate = lactation_df["trapezoid_area"].sum()
176
177        total_yield = MY0 + total_intermediate + MYend
178        result.append((lactation, total_yield))
179
180    return pd.DataFrame(result, columns=pd.Index(["TestId", "LactationMilkYield"]))

Compute total lactation milk yield using the ICAR Test Interval Method.

The method applies:

  • First test day milk yield from calving to the first test day,
  • Trapezoidal integration between consecutive test days,
  • Last test day milk yield from the last test day to DIM = max_dim (default = 305).
Arguments:
  • df (pd.DataFrame): Input DataFrame with at least DaysInMilk and MilkingYield columns, plus an optional TestId column. Column names can be provided explicitly or matched via known aliases.
  • days_in_milk_col (str | None): Optional column name override for DaysInMilk.
  • milking_yield_col (str | None): Optional column name override for MilkingYield.
  • test_id_col (str | None): Optional column name override for TestId.
  • default_test_id (int): Value used to create a default TestId column if one is missing.
  • max_dim (int): Lactation length used to calculate cumulative production. The default is 305 days. Records with DIM > max_dim are excluded.
Returns:

pd.DataFrame: Two-column DataFrame with - "TestId": identifier per lactation, - "LactationMilkYield": computed total milk yield over the specified window.

Raises:
  • ValueError: If required columns (DaysInMilk or MilkingYield) cannot be found.
Notes:
  • Records with DIM > max_dim are dropped before computation.
  • At least two data points per TestId are required for trapezoidal integration; otherwise the lactation is skipped.