lactationcurve.characteristics.centering_date_method

Centering date method for accumulated lactation yield.

Purpose

This module implements the centering date method to compute cumulative milk yield from test-day records. This method is also known as the McDowell mehod for centering the testing day. This approach was the standard method used by U.S. DHIA before 1969.

Method Summary

The centering date method (CDM) estimates lactation yield by assuming that each test-day milk yield is representative for the calender month it was taken in. It estimates total milk yield over a lactation by taking a milk yield measurement each month and multiplying it by the number of days in that month. If a cow calved before the start of the month, the extra days are multiplied by the first test-day milk yield. If a cow goes dry after the end of the month, the extra days are multiplied by the last test-day milk yield. So each testing interval has the same amount of days as days in that month, whereby the testing day is considered as the middle of the testing period. The total lactation yield is then calculated by multiplying each test-day yield by the length of its corresponding centered interval and summing these contributions across the lactation.

Formula:

MY_total = (d₁ + ℓ₁/2) · y₁ + Σ ℓᵢ · yᵢ (i = 2 … n−1) + (L − dₙ + ℓₙ/2) · yₙ

Where:

  • d: days in milk of the test day
  • i: index of the test day in the lactation
  • yᵢ: milk yield measured in the 24 hours of the test day i
  • ℓ: number of days in the month that the measurement was taken
  • L: lactation length (e.g., 305 days or the days in milk that the cow went dry)
  • n: number of test days in the lactation
  • MY_total: total cumulative milk yield over the lactation

Key Entry Points

  • centering_date_method: Computes cumulative lactation milk yield for a group of lactations, each identified by a unique TestId and returns the results as a DataFrame with a TestId and LactationMilkYield column.
  • centering_date_method_single_lactation: Computes cumulative lactation milk yield for a single lactation.

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.

The main function 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 > lactation_length are excluded before computation.
  • This method's main strength is its ease of use and simplicity.
  • Its main disadvantages are that it does not account for the shape of the lactation curve, which can lead to overestimation or 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.
  • This deviation is most prevalent at the beginning and end of the lactation as it ignores the increase in milk yield at the start of lactation and the decrease at the end of lactation. Everett et al. (1968) has proposed correction factors to account for this, but they are not implemented in this method. The ICAR guideline allows for adjustment of the first recorded value when computing the accumulated record. If the first recording occurs within 14 days from calving, then no adjustment is required to the first recorded value when computing the accumulated record. If the first recording occurs 15 to 95 days from calving, then an adjustment procedure may be applied.
  • Months are of inequal length so the cummulative milk yield is biased by when the cow freshens, e.g. a cow that has her peak yield in January will likely have a higher cumulative milk yield. according to this method than a cow that has her peak yield in February, even if they have the same total milk yield over the lactation.
  • Because the method assumes a once per month testing scheme, you cannot use it with a testing scheme that has multiple tests per month.
  • This method works best with a very consistent once per month testing scheme. For instance always take the first day of the month so that the interval length is always the same.
  • If a month is missing, the milk yield for that month is estimated as the average of the nearest test before and the nearest test after.
  • If the first test day is missing, the second test day is used to estimate the milk yield for the first month.
  • This method can also be applied to bimonthly or trimonthly testing schemes, but the accuracy rapidly decreases as the number of test days per lactation decreases. This has been shown by Everett et al. (1968) reporting errors up to 28% and is also true for the test interval method. -The code takes into account leap years if dates include years. The milk yield of February will than be multiplied by 29 instead of 28.
  • This method is different from the test interval method only because it uses the number of days in the month as the interval length instead of the actual number of days between test days. Otherwise they mathematically yield the same result.

Reference

U.S. Department of Agriculture, Bureau of Dairying. (1925). Cow Testing Association Letter No. 1. Washington, DC.

R.W. Everett, B.T. McDaniel, H.W. Carter, Accuracy of Monthly, Bimonthly, and Trimonthly Dairy Herd Improvement Association Records, Journal of Dairy Science, Volume 51, Issue 7, 1968, Pages 1051-1058, ISSN 0022-0302, https://doi.org/10.3168/jds.S0022-0302(68)87123-1.


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

  1"""
  2Centering date method for accumulated lactation yield.
  3
  4Purpose
  5-------
  6This module implements the centering date method to compute cumulative milk yield
  7from test-day records.
  8This method is also known as the McDowell mehod for centering the testing day.
  9This approach was the standard method used by U.S. DHIA before 1969.
 10
 11Method Summary
 12--------------
 13The centering date method (CDM) estimates lactation yield by assuming that each
 14test-day milk yield is representative for the calender month it was taken in.
 15It estimates total milk yield over a lactation by taking a milk yield measurement
 16each month and multiplying it by the number of days in that month.
 17If a cow calved before the start of the month,
 18the extra days are multiplied by the first test-day milk yield. If a cow goes
 19dry after the end of the month, the extra days are multiplied by the last
 20test-day milk yield.
 21So each testing interval has the same amount of days as days in that month,
 22whereby the testing day is considered as the middle of the testing period.
 23The total lactation yield is then calculated by multiplying each test-day yield
 24by the length of its corresponding centered interval and summing these
 25contributions across the lactation.
 26
 27
 28Formula:
 29
 30MY_total = (d₁ + ℓ₁/2) · y₁ + Σ ℓᵢ · yᵢ (i = 2 … n−1) + (L − dₙ + ℓₙ/2) · yₙ
 31
 32Where:
 33- ``d``: days in milk of the test day
 34- ``i``: index of the test day in the lactation
 35- ``yᵢ``: milk yield measured in the 24 hours of the test day i
 36- ``ℓ``: number of days in the month that the measurement was taken
 37- ``L``: lactation length (e.g., 305 days or the days in milk that the cow went dry)
 38- ``n``: number of test days in the lactation
 39- ``MY_total``: total cumulative milk yield over the lactation
 40
 41
 42
 43Key Entry Points
 44----------------
 45- ``centering_date_method``: Computes cumulative lactation milk yield for
 46    a group of lactations, each identified by a unique ``TestId``
 47    and returns the results as a DataFrame with a ``TestId`` and ``LactationMilkYield`` column.
 48- ``centering_date_method_single_lactation``: Computes cumulative lactation
 49    milk yield for a single lactation.
 50
 51
 52Column Flexibility
 53------------------
 54The function accepts several case-insensitive column name aliases and can
 55create a default ``TestId`` if one is missing. Recognized aliases:
 56
 57- Days in Milk: `["daysinmilk", "dim", "testday"]`
 58- Milk Yield: `["milkingyield", "testdaymilkyield", "milkyield", "yield"]`
 59- Test Id: `["animalid", "testid", "id"]`
 60
 61It is also possible to provide your own column names so the function
 62can be applied to dataframes with different column naming conventions.
 63
 64The main function returns a DataFrame with columns: ``["TestId", "LactationMilkYield"]``.
 65
 66Notes
 67-----
 68- Units: DIM (days in milk) is measured in days,
 69    and milk yield can be in kg or lb. The
 70    output stays in the same unit as the input.
 71- Records with ``DIM > lactation_length`` are excluded before computation.
 72- This method's main strength is its ease of use and simplicity.
 73- Its main disadvantages are that it does not account for the shape of the
 74    lactation curve, which can lead to overestimation or underestimation
 75    of total yield,
 76    especially for lactations with few test days or irregular patterns.
 77    Outliers in test-day records can also have a large influence on the final
 78    result.
 79- This deviation is most prevalent at the beginning and end of the lactation
 80    as it ignores the increase in milk yield at the start of lactation
 81    and the decrease at the end of lactation.
 82    Everett et al. (1968) has proposed correction factors to account for this,
 83    but they are not implemented in this method.
 84    The ICAR guideline allows for adjustment of the first recorded value when
 85    computing the accumulated record.
 86    If the first recording occurs within 14 days from calving,
 87    then no adjustment is required to the first recorded value when computing
 88    the accumulated record. If the first recording occurs 15 to 95 days from
 89    calving, then an adjustment procedure may be applied.
 90- Months are of inequal length so the cummulative milk yield is biased
 91    by when the cow freshens, e.g. a cow that has her peak yield in January
 92    will likely have a higher cumulative milk yield.
 93    according to this method than a cow that has her peak yield in February,
 94    even if they have the same total milk yield over the lactation.
 95- Because the method assumes a once per month testing scheme,
 96    you cannot use it with a testing scheme that has multiple tests per month.
 97- This method works best with a very consistent once per month testing scheme.
 98    For instance always take the first day of the month so that the interval
 99    length is always the same.
100- If a month is missing, the milk yield for that month is estimated as the average
101    of the nearest test before and the nearest test after.
102- If the first test day is missing, the second test day is used to estimate
103the milk yield for the first month.
104- This method can also be applied to bimonthly or trimonthly testing schemes,
105    but the accuracy rapidly decreases as the number of test days per lactation
106    decreases.
107    This has been shown by Everett et al. (1968) reporting errors up to 28%
108    and is also true for the test interval method.
109-The code takes into account leap years if dates include years.
110    The milk yield of February will than be multiplied by 29 instead of 28.
111- This method is different from the test interval method **only**
112because it uses the number of days in the month as the interval length
113instead of the actual number of days between test days.
114Otherwise they mathematically yield the same result.
115
116
117
118Reference
119---------
120U.S. Department of Agriculture, Bureau of Dairying. (1925).
121Cow Testing Association Letter No. 1. Washington, DC.
122
123R.W. Everett, B.T. McDaniel, H.W. Carter,
124Accuracy of Monthly, Bimonthly, and Trimonthly Dairy Herd Improvement Association Records,
125Journal of Dairy Science,
126Volume 51, Issue 7,
1271968,
128Pages 1051-1058,
129ISSN 0022-0302,
130https://doi.org/10.3168/jds.S0022-0302(68)87123-1.
131
132
133---------
134Author: Meike van Leerdam
135Date: 07-31-2025
136Last update: 22-May-2026
137"""
138
139from typing import cast
140
141import numpy as np
142import pandas as pd
143
144from lactationcurve.preprocessing import standardize_lactation_columns
145
146
147def centering_date_method_single_lactation(
148    lactation_df: pd.DataFrame,
149    lactation_length: int = 305,
150    lactation_start_grace_period: int = 0,
151) -> float:
152    """Compute total lactation milk yield using the centering date method.
153
154    Args:
155        lactation_df (pd.DataFrame): Input DataFrame that contains all milk
156            recordings of a single lactation
157            with at least TestCalendarDate,DaysInMilk and MilkingYield columns.
158            Column names can be provided explicitly or matched via known aliases.
159        lactation_length (int): Lactation length used to calculate cumulative
160            production. The default is 305 days.
161            Records with DIM > lactation_length are excluded.
162            If dry dates are known it is recommended to use the actual lactation
163            length instead of the default 305 days.
164        lactation_start_grace_period (int): Number of days after calving that
165            are not considered for cumulative yield becuase the colostrum is
166            not representative for the lactation. The default is 0 days.
167
168    Returns:
169        float: The computed total milk yield over the lactation for a single cow.
170
171    """
172    df = lactation_df.sort_values("TestCalendarDate")
173
174    # One yield per calendar month (if a month has several tests, take their mean)
175    month = df["TestCalendarDate"].dt.to_period("M")
176    y = cast(pd.Series, df.groupby(month)["MilkingYield"].mean())
177
178    # Full monthly range from first to last test; missing months become NaN
179    full_range = pd.period_range(y.index.min(), y.index.max(), freq="M")
180    y = y.reindex(full_range)
181
182    # Missing month = average of the nearest test before and the nearest test after
183    y = y.fillna(y.ffill().add(y.bfill()).div(2))
184
185    # Weights: days in each month, with the first and last month adjusted
186    w = np.array([period.days_in_month for period in full_range], dtype=float)
187    d_first = df["DaysInMilk"].iloc[0] - lactation_start_grace_period
188    d_last = df["DaysInMilk"].iloc[-1]
189    w[0] = d_first + w[0] / 2
190    w[-1] = lactation_length - d_last + w[-1] / 2
191
192    total_milk_yield = (w * y).sum()
193    return total_milk_yield
194
195
196def centering_date_method(
197    df: pd.DataFrame,
198    days_in_milk_col: str | None = None,
199    milking_yield_col: str | None = None,
200    test_id_col: str | None = None,
201    test_calender_date_col: str | None = None,
202    default_test_id: int = 0,
203    lactation_length: int = 305,
204    lactation_start_grace_period: int = 0,
205) -> pd.DataFrame:
206    """Compute total lactation milk yield using the centering date method for each test id.
207
208    The method applies:
209
210
211    Args:
212        df (pd.DataFrame): Input DataFrame with at least DaysInMilk and
213            MilkingYield columns, plus an optional TestId column. Column names
214            can be provided explicitly or matched via known aliases.
215        days_in_milk_col (str | None): Optional column name override for
216            DaysInMilk.
217        milking_yield_col (str | None): Optional column name override for
218            MilkingYield.
219        test_id_col (str | None): Optional column name override for TestId.
220        test_calender_date_col (str | None): Optional column name override for
221        TestCalendarDate.
222        default_test_id (int): Value used to create a default TestId column if
223            one is missing.
224        lactation_length (int): Lactation length used to calculate cumulative
225            production. The default is 305 days.
226            Records with DIM > lactation_length are excluded.
227        lactation_start_grace_period (int): Number of days after calving that
228            are not considered for cumulative yield becuase the colostrum is
229            not representative for the lactation. The default is 0 days.
230
231    Returns:
232        pd.DataFrame: Two-column DataFrame with
233            - "TestId": identifier per lactation,
234            - "LactationMilkYield": computed total milk yield over the
235              specified window.
236
237    Raises:
238        ValueError: If required columns (DaysInMilk or MilkingYield) cannot be found.
239        ValueError: If there are multiple measurements in the same calendar month.
240        ValueError: If there are fewer than two data points per TestId.
241
242    """
243
244    # Standardize columns and filter DIM <= lactation_length
245    df = standardize_lactation_columns(
246        df,
247        days_in_milk_col=days_in_milk_col,
248        milking_yield_col=milking_yield_col,
249        test_id_col=test_id_col,
250        test_calender_date_col=test_calender_date_col,
251        default_test_id=default_test_id,
252        max_dim=lactation_length,
253        lactation_start_grace_period=lactation_start_grace_period,
254    )
255
256    result = []
257
258    # Iterate over each lactation
259    for lactation in df["TestId"].unique():
260        lactation_df = pd.DataFrame(df[df["TestId"] == lactation])
261
262        if len(lactation_df) < 2:
263            print(f"Skipping TestId {lactation}: not enough data points for interpolation.")
264            continue
265
266        # Check that the date column is datetime and convert it if needed.
267        if not pd.api.types.is_datetime64_any_dtype(lactation_df["TestCalendarDate"]):
268            lactation_df["TestCalendarDate"] = pd.to_datetime(lactation_df["TestCalendarDate"])
269
270        # Check that there is no more than one test per calendar month
271        month_counts = lactation_df["TestCalendarDate"].dt.to_period("M").value_counts()
272
273        if (month_counts > 1).any():
274            duplicated_months = month_counts[month_counts > 1].index.tolist()
275
276            raise ValueError(
277                f"TestId {lactation}: multiple measurements found in "
278                f"calendar month(s) {duplicated_months}. "
279                "The McDowell centering date method requires maximum one test per calendar month."
280            )
281
282        # Calculate total milk yield for this lactation
283        total_milk_yield = centering_date_method_single_lactation(
284            lactation_df,
285            lactation_start_grace_period=lactation_start_grace_period,
286            lactation_length=lactation_length,
287        )
288
289        result.append({"TestId": lactation, "LactationMilkYield": total_milk_yield})
290
291    return pd.DataFrame(result, columns=pd.Index(["TestId", "LactationMilkYield"]))
def centering_date_method_single_lactation( lactation_df: pandas.DataFrame, lactation_length: int = 305, lactation_start_grace_period: int = 0) -> float:
148def centering_date_method_single_lactation(
149    lactation_df: pd.DataFrame,
150    lactation_length: int = 305,
151    lactation_start_grace_period: int = 0,
152) -> float:
153    """Compute total lactation milk yield using the centering date method.
154
155    Args:
156        lactation_df (pd.DataFrame): Input DataFrame that contains all milk
157            recordings of a single lactation
158            with at least TestCalendarDate,DaysInMilk and MilkingYield columns.
159            Column names can be provided explicitly or matched via known aliases.
160        lactation_length (int): Lactation length used to calculate cumulative
161            production. The default is 305 days.
162            Records with DIM > lactation_length are excluded.
163            If dry dates are known it is recommended to use the actual lactation
164            length instead of the default 305 days.
165        lactation_start_grace_period (int): Number of days after calving that
166            are not considered for cumulative yield becuase the colostrum is
167            not representative for the lactation. The default is 0 days.
168
169    Returns:
170        float: The computed total milk yield over the lactation for a single cow.
171
172    """
173    df = lactation_df.sort_values("TestCalendarDate")
174
175    # One yield per calendar month (if a month has several tests, take their mean)
176    month = df["TestCalendarDate"].dt.to_period("M")
177    y = cast(pd.Series, df.groupby(month)["MilkingYield"].mean())
178
179    # Full monthly range from first to last test; missing months become NaN
180    full_range = pd.period_range(y.index.min(), y.index.max(), freq="M")
181    y = y.reindex(full_range)
182
183    # Missing month = average of the nearest test before and the nearest test after
184    y = y.fillna(y.ffill().add(y.bfill()).div(2))
185
186    # Weights: days in each month, with the first and last month adjusted
187    w = np.array([period.days_in_month for period in full_range], dtype=float)
188    d_first = df["DaysInMilk"].iloc[0] - lactation_start_grace_period
189    d_last = df["DaysInMilk"].iloc[-1]
190    w[0] = d_first + w[0] / 2
191    w[-1] = lactation_length - d_last + w[-1] / 2
192
193    total_milk_yield = (w * y).sum()
194    return total_milk_yield

Compute total lactation milk yield using the centering date method.

Arguments:
  • lactation_df (pd.DataFrame): Input DataFrame that contains all milk recordings of a single lactation with at least TestCalendarDate,DaysInMilk and MilkingYield columns. Column names can be provided explicitly or matched via known aliases.
  • lactation_length (int): Lactation length used to calculate cumulative production. The default is 305 days. Records with DIM > lactation_length are excluded. If dry dates are known it is recommended to use the actual lactation length instead of the default 305 days.
  • lactation_start_grace_period (int): Number of days after calving that are not considered for cumulative yield becuase the colostrum is not representative for the lactation. The default is 0 days.
Returns:

float: The computed total milk yield over the lactation for a single cow.

def centering_date_method( df: pandas.DataFrame, days_in_milk_col: str | None = None, milking_yield_col: str | None = None, test_id_col: str | None = None, test_calender_date_col: str | None = None, default_test_id: int = 0, lactation_length: int = 305, lactation_start_grace_period: int = 0) -> pandas.DataFrame:
197def centering_date_method(
198    df: pd.DataFrame,
199    days_in_milk_col: str | None = None,
200    milking_yield_col: str | None = None,
201    test_id_col: str | None = None,
202    test_calender_date_col: str | None = None,
203    default_test_id: int = 0,
204    lactation_length: int = 305,
205    lactation_start_grace_period: int = 0,
206) -> pd.DataFrame:
207    """Compute total lactation milk yield using the centering date method for each test id.
208
209    The method applies:
210
211
212    Args:
213        df (pd.DataFrame): Input DataFrame with at least DaysInMilk and
214            MilkingYield columns, plus an optional TestId column. Column names
215            can be provided explicitly or matched via known aliases.
216        days_in_milk_col (str | None): Optional column name override for
217            DaysInMilk.
218        milking_yield_col (str | None): Optional column name override for
219            MilkingYield.
220        test_id_col (str | None): Optional column name override for TestId.
221        test_calender_date_col (str | None): Optional column name override for
222        TestCalendarDate.
223        default_test_id (int): Value used to create a default TestId column if
224            one is missing.
225        lactation_length (int): Lactation length used to calculate cumulative
226            production. The default is 305 days.
227            Records with DIM > lactation_length are excluded.
228        lactation_start_grace_period (int): Number of days after calving that
229            are not considered for cumulative yield becuase the colostrum is
230            not representative for the lactation. The default is 0 days.
231
232    Returns:
233        pd.DataFrame: Two-column DataFrame with
234            - "TestId": identifier per lactation,
235            - "LactationMilkYield": computed total milk yield over the
236              specified window.
237
238    Raises:
239        ValueError: If required columns (DaysInMilk or MilkingYield) cannot be found.
240        ValueError: If there are multiple measurements in the same calendar month.
241        ValueError: If there are fewer than two data points per TestId.
242
243    """
244
245    # Standardize columns and filter DIM <= lactation_length
246    df = standardize_lactation_columns(
247        df,
248        days_in_milk_col=days_in_milk_col,
249        milking_yield_col=milking_yield_col,
250        test_id_col=test_id_col,
251        test_calender_date_col=test_calender_date_col,
252        default_test_id=default_test_id,
253        max_dim=lactation_length,
254        lactation_start_grace_period=lactation_start_grace_period,
255    )
256
257    result = []
258
259    # Iterate over each lactation
260    for lactation in df["TestId"].unique():
261        lactation_df = pd.DataFrame(df[df["TestId"] == lactation])
262
263        if len(lactation_df) < 2:
264            print(f"Skipping TestId {lactation}: not enough data points for interpolation.")
265            continue
266
267        # Check that the date column is datetime and convert it if needed.
268        if not pd.api.types.is_datetime64_any_dtype(lactation_df["TestCalendarDate"]):
269            lactation_df["TestCalendarDate"] = pd.to_datetime(lactation_df["TestCalendarDate"])
270
271        # Check that there is no more than one test per calendar month
272        month_counts = lactation_df["TestCalendarDate"].dt.to_period("M").value_counts()
273
274        if (month_counts > 1).any():
275            duplicated_months = month_counts[month_counts > 1].index.tolist()
276
277            raise ValueError(
278                f"TestId {lactation}: multiple measurements found in "
279                f"calendar month(s) {duplicated_months}. "
280                "The McDowell centering date method requires maximum one test per calendar month."
281            )
282
283        # Calculate total milk yield for this lactation
284        total_milk_yield = centering_date_method_single_lactation(
285            lactation_df,
286            lactation_start_grace_period=lactation_start_grace_period,
287            lactation_length=lactation_length,
288        )
289
290        result.append({"TestId": lactation, "LactationMilkYield": total_milk_yield})
291
292    return pd.DataFrame(result, columns=pd.Index(["TestId", "LactationMilkYield"]))

Compute total lactation milk yield using the centering date method for each test id.

The method applies:
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.
  • test_calender_date_col (str | None): Optional column name override for
  • TestCalendarDate.
  • default_test_id (int): Value used to create a default TestId column if one is missing.
  • lactation_length (int): Lactation length used to calculate cumulative production. The default is 305 days. Records with DIM > lactation_length are excluded.
  • lactation_start_grace_period (int): Number of days after calving that are not considered for cumulative yield becuase the colostrum is not representative for the lactation. The default is 0 days.
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.
  • ValueError: If there are multiple measurements in the same calendar month.
  • ValueError: If there are fewer than two data points per TestId.