lactationcurve.fitting

Fitting lactation curves to data.

 1"""Fitting lactation curves to data."""
 2
 3from .lactation_curve_fitting import (
 4    ali_schaeffer_model,
 5    bayesian_fit_milkbot_single_lactation,
 6    brody_model,
 7    dhanoa_model,
 8    dijkstra_model,
 9    emmans_model,
10    fischer_model,
11    fit_lactation_curve,
12    get_chen_priors,
13    get_lc_parameters,
14    get_lc_parameters_least_squares,
15    hayashi_model,
16    milkbot_model,
17    nelder_model,
18    prasad_model,
19    rook_model,
20    sikka_model,
21    wilmink_model,
22    wood_model,
23)
24
25__all__ = [
26    "ali_schaeffer_model",
27    "bayesian_fit_milkbot_single_lactation",
28    "brody_model",
29    "dhanoa_model",
30    "dijkstra_model",
31    "emmans_model",
32    "fischer_model",
33    "fit_lactation_curve",
34    "get_chen_priors",
35    "get_lc_parameters",
36    "get_lc_parameters_least_squares",
37    "hayashi_model",
38    "milkbot_model",
39    "nelder_model",
40    "prasad_model",
41    "rook_model",
42    "sikka_model",
43    "wilmink_model",
44    "wood_model",
45]
def ali_schaeffer_model(t, a, b, c, d, k):
133def ali_schaeffer_model(t, a, b, c, d, k):
134    """Ali & Schaeffer lactation curve model.
135
136    Args:
137        t: Time since calving in days (DIM). Use `t >= 1` to avoid `log(0)`.
138        a: Intercept-like parameter (numerical).
139        b: Linear coefficient on scaled time `t/305` (numerical).
140        c: Quadratic coefficient on scaled time `t/305` (numerical).
141        d: Coefficient on `log(305/t)` (numerical).
142        k: Coefficient on `[log(305/t)]^2` (numerical).
143
144    Returns:
145        Predicted milk yield at `t`.
146
147    Notes:
148        Uses `t_scaled = t / 305` and `log_term = ln(305 / t)`.
149    """
150    t_scaled = t / 305
151    log_term = np.log(305 / t)
152    return a + b * t_scaled + c * (t_scaled**2) + d * log_term + k * (log_term**2)

Ali & Schaeffer lactation curve model.

Arguments:
  • t: Time since calving in days (DIM). Use t >= 1 to avoid log(0).
  • a: Intercept-like parameter (numerical).
  • b: Linear coefficient on scaled time t/305 (numerical).
  • c: Quadratic coefficient on scaled time t/305 (numerical).
  • d: Coefficient on log(305/t) (numerical).
  • k: Coefficient on [log(305/t)]^2 (numerical).
Returns:

Predicted milk yield at t.

Notes:

Uses t_scaled = t / 305 and log_term = ln(305 / t).

def bayesian_fit_milkbot_single_lactation( dim, milkrecordings, key: str, parity=3, breed='H', continent='USA') -> dict:
711def bayesian_fit_milkbot_single_lactation(
712    dim, milkrecordings, key: str, parity=3, breed="H", continent="USA"
713) -> dict:
714    """
715    Fit a single lactation using the MilkBot API.
716
717    Args:
718        dim: List/array of DIM values.
719        milkrecordings: List/array of milk recordings (kg).
720        key: API key for MilkBot.
721        parity: Lactation number; values >= 3 are treated as one group in priors.
722        breed: "H" (Holstein) or "J" (Jersey).
723        continent: Prior source:
724            - "USA"   → MilkBot USA priors
725            - "EU"    → MilkBot EU priors
726            - "CHEN"  → Chen et al. published priors
727
728    Returns:
729        Dictionary with fitted parameters and metadata:
730            {
731                "scale": float,
732                "ramp": float,
733                "decay": float,
734                "offset": float,
735                "nPoints": int
736            }
737
738    Raises:
739        requests.HTTPError: For unsuccessful HTTP response codes.
740        RuntimeError: If the response format is unexpected.
741
742    Notes:
743        - When `continent == "CHEN"`, Chen et al. priors are included in the request payload.
744        - EU calls use the GCP EU endpoint; others use `milkbot.com`.
745    """
746    # check and prepare input
747    inputs = validate_and_prepare_inputs(
748        dim, milkrecordings, breed=breed, parity=parity, continent=continent
749    )
750
751    dim = inputs.dim
752    milkrecordings = inputs.milkrecordings
753    breed = inputs.breed
754    parity = inputs.parity
755    continent = inputs.continent
756
757    # -----------------------------
758    # Select server (USA vs EU)
759    # -----------------------------
760    if continent == "EU":
761        base_url = "https://europe-west1-numeric-analogy-337601.cloudfunctions.net/milkBot-fitter"
762    else:
763        base_url = "https://milkbot.com"
764
765    # -----------------------------
766    # Prepare headers
767    # -----------------------------
768    headers = {"Content-Type": "application/json", "X-API-KEY": key}
769
770    # -----------------------------
771    # Prepare milk points
772    # -----------------------------
773    points = sorted(
774        ({"dim": int(d), "milk": float(m)} for d, m in zip(dim, milkrecordings)),
775        key=lambda p: p["dim"],
776    )
777
778    # -----------------------------
779    # Lactation metadata
780    # -----------------------------
781    payload = {
782        "lactation": {
783            "lacKey": "single_lactation_fit",
784            "breed": breed,
785            "parity": parity,
786            "points": points,
787        },
788        "options": {
789            "returnInputData": False,
790            "returnPath": False,
791            "returnDiscriminatorPath": False,
792            # "fitEngine": "AnnealingFitter@2.0", #comment out to use the default fitter
793            # "fitObjective": "MB2@2.0",
794            "preferredMilkUnit": "kg",
795        },
796    }
797
798    # -----------------------------
799    # Add priors only if Chen
800    # -----------------------------
801    if continent == "CHEN":
802        assert parity is not None, "parity is required for Chen priors"
803        payload["priors"] = get_chen_priors(parity)
804
805    # -----------------------------
806    # Call API
807    # -----------------------------
808    response = requests.post(f"{base_url}/fitLactation", headers=headers, json=payload)
809    response.raise_for_status()
810    res = response.json()
811
812    # -----------------------------
813    # Normalize response (USA vs EU)
814    # -----------------------------
815    if "fittedParams" in res:
816        # USA-style response
817        fitted = res["fittedParams"]
818    elif "params" in res:
819        fitted = res["params"]
820    else:
821        raise RuntimeError(f"Unexpected MilkBot response format: {res}")
822
823    # -----------------------------
824    # Parse result
825    # -----------------------------
826
827    return {
828        "scale": fitted["scale"],
829        "ramp": fitted["ramp"],
830        "decay": fitted["decay"],
831        "offset": fitted["offset"],
832        "nPoints": len(points),
833    }

Fit a single lactation using the MilkBot API.

Arguments:
  • dim: List/array of DIM values.
  • milkrecordings: List/array of milk recordings (kg).
  • key: API key for MilkBot.
  • parity: Lactation number; values >= 3 are treated as one group in priors.
  • breed: "H" (Holstein) or "J" (Jersey).
  • continent: Prior source:
    • "USA" → MilkBot USA priors
    • "EU" → MilkBot EU priors
    • "CHEN" → Chen et al. published priors
Returns:

Dictionary with fitted parameters and metadata: { "scale": float, "ramp": float, "decay": float, "offset": float, "nPoints": int }

Raises:
  • requests.HTTPError: For unsuccessful HTTP response codes.
  • RuntimeError: If the response format is unexpected.
Notes:
  • When continent == "CHEN", Chen et al. priors are included in the request payload.
  • EU calls use the GCP EU endpoint; others use milkbot.com.
def brody_model(t, a, k):
170def brody_model(t, a, k):
171    """Brody lactation curve model.
172
173    Args:
174        t: Time since calving in days (DIM).
175        a: Scale parameter (numerical).
176        k: Decay parameter (numerical).
177
178    Returns:
179        Predicted milk yield at `t`.
180    """
181    return a * np.exp(-k * t)

Brody lactation curve model.

Arguments:
  • t: Time since calving in days (DIM).
  • a: Scale parameter (numerical).
  • k: Decay parameter (numerical).
Returns:

Predicted milk yield at t.

def dhanoa_model(t, a, b, c):
217def dhanoa_model(t, a, b, c):
218    """Dhanoa lactation curve model.
219
220    Args:
221        t: Time since calving in days (DIM).
222        a: Scale parameter (numerical).
223        b: Shape parameter (numerical).
224        c: Decay parameter (numerical).
225
226    Returns:
227        Predicted milk yield at `t`.
228
229    Notes:
230        Formula: `y(t) = a * t ** (b * c) * exp(-c * t)`.
231    """
232    return a * t ** (b * c) * np.exp(-c * t)

Dhanoa lactation curve model.

Arguments:
  • t: Time since calving in days (DIM).
  • a: Scale parameter (numerical).
  • b: Shape parameter (numerical).
  • c: Decay parameter (numerical).
Returns:

Predicted milk yield at t.

Notes:

Formula: y(t) = a * t ** (b * c) * exp(-c * t).

def dijkstra_model(t, a, b, c, d):
292def dijkstra_model(t, a, b, c, d):
293    """Dijkstra lactation curve model.
294
295    Args:
296        t: Time since calving in days (DIM).
297        a: Scale parameter (numerical).
298        b: Growth parameter (numerical).
299        c: Saturation rate parameter (numerical).
300        d: Decay parameter (numerical).
301
302    Returns:
303        Predicted milk yield at `t`.
304
305    Notes:
306        Formula: `y(t) = a * exp((b * (1 - exp(-c * t)) / c) - d * t)`.
307    """
308    return a * np.exp((b * (1 - np.exp(-c * t)) / c) - d * t)

Dijkstra lactation curve model.

Arguments:
  • t: Time since calving in days (DIM).
  • a: Scale parameter (numerical).
  • b: Growth parameter (numerical).
  • c: Saturation rate parameter (numerical).
  • d: Decay parameter (numerical).
Returns:

Predicted milk yield at t.

Notes:

Formula: y(t) = a * exp((b * (1 - exp(-c * t)) / c) - d * t).

def emmans_model(t, a, b, c, d):
235def emmans_model(t, a, b, c, d):
236    """Emmans lactation curve model.
237
238    Args:
239        t: Time since calving in days (DIM).
240        a: Scale parameter (numerical).
241        b: Growth parameter (numerical).
242        c: Decay parameter (numerical).
243        d: Location parameter in nested exponential (numerical).
244
245    Returns:
246        Predicted milk yield at `t`.
247
248    Notes:
249        Formula: `y(t) = a * exp(-exp(d - b*t)) * exp(-c*t)`.
250    """
251    return a * np.exp(-np.exp(d - b * t)) * np.exp(-c * t)

Emmans lactation curve model.

Arguments:
  • t: Time since calving in days (DIM).
  • a: Scale parameter (numerical).
  • b: Growth parameter (numerical).
  • c: Decay parameter (numerical).
  • d: Location parameter in nested exponential (numerical).
Returns:

Predicted milk yield at t.

Notes:

Formula: y(t) = a * exp(-exp(d - b*t)) * exp(-c*t).

def fischer_model(t, a, b, c):
155def fischer_model(t, a, b, c):
156    """Fischer lactation curve model.
157
158    Args:
159        t: Time since calving in days (DIM).
160        a: Scale parameter (numerical).
161        b: Linear decline parameter (numerical).
162        c: Exponential decay parameter (numerical).
163
164    Returns:
165        Predicted milk yield at `t`.
166    """
167    return a - b * t - a * np.exp(-c * t)

Fischer lactation curve model.

Arguments:
  • t: Time since calving in days (DIM).
  • a: Scale parameter (numerical).
  • b: Linear decline parameter (numerical).
  • c: Exponential decay parameter (numerical).
Returns:

Predicted milk yield at t.

def fit_lactation_curve( dim, milkrecordings, model='wood', fitting='frequentist', breed='H', parity=3, continent='USA', key=None):
373def fit_lactation_curve(
374    dim,
375    milkrecordings,
376    model="wood",
377    fitting="frequentist",
378    breed="H",
379    parity=3,
380    continent="USA",
381    key=None,
382):
383    """Fit lactation data to a lactation curve model and return predictions.
384
385    Depending on `fitting`:
386    - **frequentist**: Fits parameters using `minimize` and/or `curve_fit`
387      for the specified `model`, then predicts over DIM 1–305 (or up to `max(dim)` if greater).
388    - **bayesian**: (MilkBot only) Calls the MilkBot Bayesian fitting API and
389      returns predictions using the fitted parameters.
390
391    Args:
392        dim (Int): List/array of days in milk (DIM).
393        milkrecordings (Float): List/array of milk recordings (kg).
394        model (Str): Model name (lowercase), default "wood".
395            Supported for frequentist: "wood", "wilmink", "ali_schaeffer", "fischer", "milkbot".
396        fitting (Str): "frequentist" (default) or "bayesian".
397            Bayesian fitting is currently implemented only for "milkbot".
398        breed (Str): "H" (Holstein, default) or "J" (Jersey). Only used for Bayesian.
399        parity (Int): Lactation number; all parities >= 3 considered one group in priors (Bayesian).
400        continent (Str): Prior source for Bayesian, "USA" (default), "EU", or "CHEN".
401        key (Str | None): API key for MilkBot (required when `fitting == "bayesian"`).
402
403    Returns:
404        List/array of predicted milk yield for DIM 1–305 (or up to the maximum DIM if > 305).
405
406    Raises:
407        Exception: If an unknown model is requested (frequentist),
408            or Bayesian is requested for a non-MilkBot model,
409            or `key` is missing when Bayesian fitting is requested.
410
411    Notes:
412        Uses `validate_and_prepare_inputs` for input checking and normalization.
413    """
414    # check and prepare input
415    inputs = validate_and_prepare_inputs(
416        dim,
417        milkrecordings,
418        model=model,
419        fitting=fitting,
420        breed=breed,
421        parity=parity,
422        continent=continent,
423    )
424
425    dim = inputs.dim
426    milkrecordings = inputs.milkrecordings
427    model = inputs.model
428    fitting = inputs.fitting
429    breed = inputs.breed
430    parity = inputs.parity
431    continent = inputs.continent
432
433    if fitting == "frequentist":
434        if model == "wood":
435            params = get_lc_parameters(dim, milkrecordings, model)
436            assert params is not None, "Failed to fit Wood model parameters"
437            a_w, b_w, c_w = params[0], params[1], params[2]
438            if max(dim) > 305:
439                t_range = np.arange(1, (max(dim) + 1))
440                y_w = wood_model(t_range, a_w, b_w, c_w)
441            else:
442                t_range = np.arange(1, 306)
443                y_w = wood_model(t_range, a_w, b_w, c_w)
444            return y_w
445
446        elif model == "wilmink":
447            params = get_lc_parameters(dim, milkrecordings, model)
448            assert params is not None, "Failed to fit Wilmink model parameters"
449            a_wil, b_wil, c_wil, k_wil = (params[0], params[1], params[2], params[3])
450            if max(dim) > 305:
451                t_range = np.arange(1, (max(dim) + 1))
452                y_wil = wilmink_model(t_range, a_wil, b_wil, c_wil, k_wil)
453            else:
454                t_range = np.arange(1, 306)
455                y_wil = wilmink_model(t_range, a_wil, b_wil, c_wil, k_wil)
456            return y_wil
457
458        elif model == "ali_schaeffer":
459            params = get_lc_parameters(dim, milkrecordings, model)
460            assert params is not None, "Failed to fit Ali & Schaeffer model parameters"
461            a_as, b_as, c_as, d_as, k_as = (params[0], params[1], params[2], params[3], params[4])
462            if max(dim) > 305:
463                t_range = np.arange(1, (max(dim) + 1))
464                y_as = ali_schaeffer_model(t_range, a_as, b_as, c_as, d_as, k_as)
465            else:
466                t_range = np.arange(1, 306)
467                y_as = ali_schaeffer_model(t_range, a_as, b_as, c_as, d_as, k_as)
468            return y_as
469
470        elif model == "fischer":
471            params = get_lc_parameters(dim, milkrecordings, model)
472            assert params is not None, "Failed to fit Fischer model parameters"
473            a_f, b_f, c_f = params[0], params[1], params[2]
474            if max(dim) > 305:
475                t_range = np.arange(1, (max(dim) + 1))
476                y_f = fischer_model(t_range, a_f, b_f, c_f)
477            else:
478                t_range = np.arange(1, 306)
479                y_f = fischer_model(t_range, a_f, b_f, c_f)
480            return y_f
481
482        elif model == "milkbot":
483            params = get_lc_parameters(dim, milkrecordings, model)
484            assert params is not None, "Failed to fit MilkBot model parameters"
485            a_mb, b_mb, c_mb, d_mb = (params[0], params[1], params[2], params[3])
486            if max(dim) > 305:
487                t_range = np.arange(1, (max(dim) + 1))
488            else:
489                t_range = np.arange(1, 306)
490
491            y_mb = milkbot_model(t_range, a_mb, b_mb, c_mb, d_mb)
492            return y_mb
493
494        else:
495            raise Exception("Unknown model")
496    else:
497        if model == "milkbot":
498            if key is None:
499                raise Exception("Key needed to use Bayesian fitting engine milkbot")
500            else:
501                assert parity is not None, "parity is required for Bayesian fitting"
502                assert breed is not None, "breed is required for Bayesian fitting"
503                assert continent is not None, "continent is required for Bayesian fitting"
504                parameters = bayesian_fit_milkbot_single_lactation(
505                    dim, milkrecordings, key, parity, breed, continent
506                )
507                if max(dim) > 305:
508                    t_range = np.arange(1, (max(dim) + 1))
509                    y_mb_bay = milkbot_model(
510                        t_range,
511                        parameters["scale"],
512                        parameters["ramp"],
513                        parameters["offset"],
514                        parameters["decay"],
515                    )
516                else:
517                    t_range = np.arange(1, 306)
518                    y_mb_bay = milkbot_model(
519                        t_range,
520                        parameters["scale"],
521                        parameters["ramp"],
522                        parameters["offset"],
523                        parameters["decay"],
524                    )
525                return y_mb_bay
526        else:
527            raise Exception("Bayesian fitting is currently only implemented for milkbot models")

Fit lactation data to a lactation curve model and return predictions.

Depending on fitting:

  • frequentist: Fits parameters using minimize and/or curve_fit for the specified model, then predicts over DIM 1–305 (or up to max(dim) if greater).
  • bayesian: (MilkBot only) Calls the MilkBot Bayesian fitting API and returns predictions using the fitted parameters.
Arguments:
  • dim (Int): List/array of days in milk (DIM).
  • milkrecordings (Float): List/array of milk recordings (kg).
  • model (Str): Model name (lowercase), default "wood". Supported for frequentist: "wood", "wilmink", "ali_schaeffer", "fischer", "milkbot".
  • fitting (Str): "frequentist" (default) or "bayesian". Bayesian fitting is currently implemented only for "milkbot".
  • breed (Str): "H" (Holstein, default) or "J" (Jersey). Only used for Bayesian.
  • parity (Int): Lactation number; all parities >= 3 considered one group in priors (Bayesian).
  • continent (Str): Prior source for Bayesian, "USA" (default), "EU", or "CHEN".
  • key (Str | None): API key for MilkBot (required when fitting == "bayesian").
Returns:

List/array of predicted milk yield for DIM 1–305 (or up to the maximum DIM if > 305).

Raises:
  • Exception: If an unknown model is requested (frequentist), or Bayesian is requested for a non-MilkBot model, or key is missing when Bayesian fitting is requested.
Notes:

Uses validate_and_prepare_inputs for input checking and normalization.

def get_chen_priors(parity: int) -> dict:
664def get_chen_priors(parity: int) -> dict:
665    """
666    Return Chen et al. priors in MilkBot format.
667
668    Args:
669        parity: Lactation number (1, 2, or >= 3).
670
671    Returns:
672        Dictionary with parameter priors:
673        - "scale": {"mean", "sd"}
674        - "ramp": {"mean", "sd"}
675        - "decay": {"mean", "sd"}
676        - "offset": {"mean", "sd"}
677        - "seMilk": Standard error of milk measurement.
678        - "milkUnit": Unit string (e.g., "kg").
679    """
680    if parity == 1:
681        return {
682            "scale": {"mean": 34.11, "sd": 7},
683            "ramp": {"mean": 29.96, "sd": 3},
684            "decay": {"mean": 0.001835, "sd": 0.000738},
685            "offset": {"mean": -0.5, "sd": 0.02},
686            "seMilk": 4,
687            "milkUnit": "kg",
688        }
689
690    if parity == 2:
691        return {
692            "scale": {"mean": 44.26, "sd": 9.57},
693            "ramp": {"mean": 22.52, "sd": 3},
694            "decay": {"mean": 0.002745, "sd": 0.000979},
695            "offset": {"mean": -0.78, "sd": 0.07},
696            "seMilk": 4,
697            "milkUnit": "kg",
698        }
699
700    # parity >= 3
701    return {
702        "scale": {"mean": 48.41, "sd": 10.66},
703        "ramp": {"mean": 22.54, "sd": 8.724},
704        "decay": {"mean": 0.002997, "sd": 0.000972},
705        "offset": {"mean": 0.0, "sd": 0.03},
706        "seMilk": 4,
707        "milkUnit": "kg",
708    }

Return Chen et al. priors in MilkBot format.

Arguments:
  • parity: Lactation number (1, 2, or >= 3).
Returns:

Dictionary with parameter priors:

  • "scale": {"mean", "sd"}
  • "ramp": {"mean", "sd"}
  • "decay": {"mean", "sd"}
  • "offset": {"mean", "sd"}
  • "seMilk": Standard error of milk measurement.
  • "milkUnit": Unit string (e.g., "kg").
def get_lc_parameters(dim, milkrecordings, model='wood') -> tuple[float, ...]:
591def get_lc_parameters(dim, milkrecordings, model="wood") -> tuple[float, ...]:
592    """Fit lactation data to a model and return fitted parameters (frequentist).
593
594    Depending on `model`, this uses `scipy.optimize.minimize` and/or
595    `scipy.optimize.curve_fit` with model-specific starting values and bounds.
596
597    Args:
598        dim (int): List/array of DIM values.
599        milkrecordings (float): List/array of milk recordings (kg).
600        model (str): One of "wood", "wilmink", "ali_schaeffer", "fischer", "milkbot".
601
602    Returns:
603        Fitted parameters as floats, in alphabetical order by parameter name:
604            - wood: (a, b, c)
605            - wilmink: (a, b, c, k) with k fixed at -0.05
606            - ali_schaeffer: (a, b, c, d, k)
607            - fischer: (a, b, c)
608            - milkbot: (a, b, c, d)
609    """
610    # check and prepare input
611    inputs = validate_and_prepare_inputs(dim, milkrecordings, model=model)
612
613    dim = inputs.dim
614    milkrecordings = inputs.milkrecordings
615    model = inputs.model
616
617    if model == "wood":
618        wood_guess = [30, 0.2, 0.01]
619        wood_bounds = [(1, 100), (0.01, 1.5), (0.0001, 0.1)]
620        wood_res = minimize(
621            wood_objective, wood_guess, args=(dim, milkrecordings), bounds=wood_bounds
622        )
623        a_w, b_w, c_w = wood_res.x
624        return a_w, b_w, c_w
625
626    elif model == "wilmink":
627        wil_guess = [10, 0.1, 30]
628        wil_params, _ = curve_fit(wilmink_model, dim, milkrecordings, p0=wil_guess)
629        a_wil, b_wil, c_wil = wil_params
630        k_wil = -0.05  # set fixed
631        return a_wil, b_wil, c_wil, k_wil
632
633    elif model == "ali_schaeffer":
634        ali_schaeffer_guess = [10, 10, -5, 1, 1]
635        ali_schaeffer_params, _ = curve_fit(
636            ali_schaeffer_model, dim, milkrecordings, p0=ali_schaeffer_guess
637        )
638        a_as, b_as, c_as, d_as, k_as = ali_schaeffer_params
639        return a_as, b_as, c_as, d_as, k_as
640
641    elif model == "fischer":
642        fischer_guess = [max(milkrecordings), 0.01, 0.01]
643        fischer_bounds = [(0, 100), (0, 1), (0.0001, 1)]
644        fischer_params, _ = curve_fit(
645            fischer_model,
646            dim,
647            milkrecordings,
648            p0=fischer_guess,
649            bounds=np.transpose(fischer_bounds),
650        )
651        a_f, b_f, c_f = fischer_params
652        return a_f, b_f, c_f
653
654    elif model == "milkbot":
655        mb_guess = [max(milkrecordings), 20.0, -0.7, 0.022]
656        mb_bounds = [(1, 100), (1, 100), (-600, 300), (0.0001, 0.1)]
657        mb_res = minimize(milkbot_objective, mb_guess, args=(dim, milkrecordings), bounds=mb_bounds)
658        a_mb, b_mb, c_mb, d_mb = mb_res.x
659        return a_mb, b_mb, c_mb, d_mb
660
661    raise ValueError(f"Unknown model: {model}")

Fit lactation data to a model and return fitted parameters (frequentist).

Depending on model, this uses scipy.optimize.minimize and/or scipy.optimize.curve_fit with model-specific starting values and bounds.

Arguments:
  • dim (int): List/array of DIM values.
  • milkrecordings (float): List/array of milk recordings (kg).
  • model (str): One of "wood", "wilmink", "ali_schaeffer", "fischer", "milkbot".
Returns:

Fitted parameters as floats, in alphabetical order by parameter name: - wood: (a, b, c) - wilmink: (a, b, c, k) with k fixed at -0.05 - ali_schaeffer: (a, b, c, d, k) - fischer: (a, b, c) - milkbot: (a, b, c, d)

def get_lc_parameters_least_squares(dim, milkrecordings, model='milkbot'):
530def get_lc_parameters_least_squares(dim, milkrecordings, model="milkbot"):
531    """Fit lactation data and return model parameters (least squares; frequentist).
532
533    This helper uses `scipy.optimize.least_squares` to fit the MilkBot model with bounds,
534    and returns the fitted parameters.
535    Currently implemented only for the MilkBot model, as it is
536    more complex and benefits from the robust optimization approach.
537    Other models can be fitted using `get_lc_parameters` with
538    numerical optimisation, which is generally faster for simpler
539    models.
540
541    Args:
542        dim (int): List/array of DIM values.
543        milkrecordings (float): List/array of milk recordings (kg).
544        model (str): Pre-defined model name; currently used with "milkbot".
545
546    Returns:
547        Parameters `(a, b, c, d)` as `np.float` in alphabetic order.
548
549    """
550    # check and prepare input
551    inputs = validate_and_prepare_inputs(dim, milkrecordings, model=model)
552
553    dim = inputs.dim
554    milkrecordings = inputs.milkrecordings
555    model = inputs.model
556
557    # ------------------------------
558    # Initial guess
559    # ------------------------------
560    a0 = np.max(milkrecordings)
561    b0 = 50.0
562    c0 = 30.0
563    d0 = 0.01
564    p0 = [a0, b0, c0, d0]
565
566    # ------------------------------
567    # Parameter bounds
568    # ------------------------------
569    lower = [np.max(milkrecordings) * 0.5, 1.0, -300.0, 1e-6]
570    upper = [np.max(milkrecordings) * 8.0, 400.0, 300.0, 1.0]
571
572    # ------------------------------
573    # Fit using least-squares
574    # ------------------------------
575    res = least_squares(
576        residuals_milkbot,
577        p0,
578        args=(dim, milkrecordings),
579        bounds=(lower, upper),
580        method="trf",  # trust region reflective, works well with bounds
581    )
582
583    # ------------------------------
584    # Extract parameters
585    # ------------------------------
586    a_mb, b_mb, c_mb, d_mb = res.x
587
588    return a_mb, b_mb, c_mb, d_mb

Fit lactation data and return model parameters (least squares; frequentist).

This helper uses scipy.optimize.least_squares to fit the MilkBot model with bounds, and returns the fitted parameters. Currently implemented only for the MilkBot model, as it is more complex and benefits from the robust optimization approach. Other models can be fitted using get_lc_parameters with numerical optimisation, which is generally faster for simpler models.

Arguments:
  • dim (int): List/array of DIM values.
  • milkrecordings (float): List/array of milk recordings (kg).
  • model (str): Pre-defined model name; currently used with "milkbot".
Returns:

Parameters (a, b, c, d) as np.float in alphabetic order.

def hayashi_model(t, a, b, c, d):
254def hayashi_model(t, a, b, c, d):
255    """Hayashi lactation curve model.
256
257    Args:
258        t: Time since calving in days (DIM).
259        a: Ratio parameter (> 0) (numerical).
260        b: Scale parameter (numerical).
261        c: Time constant for the first exponential term (numerical).
262        d: Parameter retained for compatibility with literature (unused in this expression).
263
264    Returns:
265        Predicted milk yield at `t`.
266
267    Notes:
268        Formula: `y(t) = b * (exp(-t / c) - exp(-t / (a * c)))`.
269    """
270    return b * (np.exp(-t / c) - np.exp(-t / (a * c)))

Hayashi lactation curve model.

Arguments:
  • t: Time since calving in days (DIM).
  • a: Ratio parameter (> 0) (numerical).
  • b: Scale parameter (numerical).
  • c: Time constant for the first exponential term (numerical).
  • d: Parameter retained for compatibility with literature (unused in this expression).
Returns:

Predicted milk yield at t.

Notes:

Formula: y(t) = b * (exp(-t / c) - exp(-t / (a * c))).

def milkbot_model(t, a, b, c, d):
76def milkbot_model(t, a, b, c, d):
77    """MilkBot lactation curve model.
78
79    Args:
80        t: Time since calving in days (DIM), scalar or array-like.
81        a: Scale; overall level of milk production.
82        b: Ramp; governs the rate of rise in early lactation.
83        c: Offset; small (usually minor) correction around the theoretical start of lactation.
84        d: Decay; exponential decline rate, evident in late lactation.
85
86    Returns:
87        Predicted milk yield at `t` (same shape as `t`).
88
89    Notes:
90        Formula: `y(t) = a * (1 - exp((c - t) / b) / 2) * exp(-d * t)`.
91    """
92    return a * (1 - np.exp((c - t) / b) / 2) * np.exp(-d * t)

MilkBot lactation curve model.

Arguments:
  • t: Time since calving in days (DIM), scalar or array-like.
  • a: Scale; overall level of milk production.
  • b: Ramp; governs the rate of rise in early lactation.
  • c: Offset; small (usually minor) correction around the theoretical start of lactation.
  • d: Decay; exponential decline rate, evident in late lactation.
Returns:

Predicted milk yield at t (same shape as t).

Notes:

Formula: y(t) = a * (1 - exp((c - t) / b) / 2) * exp(-d * t).

def nelder_model(t, a, b, c):
199def nelder_model(t, a, b, c):
200    """Nelder lactation curve model.
201
202    Args:
203        t: Time since calving in days (DIM).
204        a: Denominator intercept (numerical).
205        b: Denominator linear coefficient (numerical).
206        c: Denominator quadratic coefficient (numerical).
207
208    Returns:
209        Predicted milk yield at `t`.
210
211    Notes:
212        Formula: `y(t) = t / (a + b*t + c*t**2)`.
213    """
214    return t / (a + b * t + c * t**2)

Nelder lactation curve model.

Arguments:
  • t: Time since calving in days (DIM).
  • a: Denominator intercept (numerical).
  • b: Denominator linear coefficient (numerical).
  • c: Denominator quadratic coefficient (numerical).
Returns:

Predicted milk yield at t.

Notes:

Formula: y(t) = t / (a + b*t + c*t**2).

def prasad_model(t, a, b, c, d):
311def prasad_model(t, a, b, c, d):
312    """Prasad lactation curve model.
313
314    Args:
315        t: Time since calving in days (DIM).
316        a: Intercept-like parameter (numerical).
317        b: Linear coefficient (numerical).
318        c: Quadratic coefficient (numerical).
319        d: Inverse-time coefficient (numerical).
320
321    Returns:
322        Predicted milk yield at `t`.
323
324    Notes:
325        Formula: `y(t) = a + b*t + c*t**2 + d/t`.
326    """
327    return a + b * t + c * t**2 + d / t

Prasad lactation curve model.

Arguments:
  • t: Time since calving in days (DIM).
  • a: Intercept-like parameter (numerical).
  • b: Linear coefficient (numerical).
  • c: Quadratic coefficient (numerical).
  • d: Inverse-time coefficient (numerical).
Returns:

Predicted milk yield at t.

Notes:

Formula: y(t) = a + b*t + c*t**2 + d/t.

def rook_model(t, a, b, c, d):
273def rook_model(t, a, b, c, d):
274    """Rook lactation curve model.
275
276    Args:
277        t: Time since calving in days (DIM).
278        a: Scale parameter (numerical).
279        b: Shape parameter in rational term (numerical).
280        c: Offset parameter in rational term (numerical).
281        d: Exponential decay parameter (numerical).
282
283    Returns:
284        Predicted milk yield at `t`.
285
286    Notes:
287        Formula: `y(t) = a * (1 / (1 + b / (c + t))) * exp(-d * t)`.
288    """
289    return a * (1 / (1 + b / (c + t))) * np.exp(-d * t)

Rook lactation curve model.

Arguments:
  • t: Time since calving in days (DIM).
  • a: Scale parameter (numerical).
  • b: Shape parameter in rational term (numerical).
  • c: Offset parameter in rational term (numerical).
  • d: Exponential decay parameter (numerical).
Returns:

Predicted milk yield at t.

Notes:

Formula: y(t) = a * (1 / (1 + b / (c + t))) * exp(-d * t).

def sikka_model(t, a, b, c):
184def sikka_model(t, a, b, c):
185    """Sikka lactation curve model.
186
187    Args:
188        t: Time since calving in days (DIM).
189        a: Scale parameter (numerical).
190        b: Growth parameter (numerical).
191        c: Quadratic decay parameter (numerical).
192
193    Returns:
194        Predicted milk yield at `t`.
195    """
196    return a * np.exp(b * t - c * t**2)

Sikka lactation curve model.

Arguments:
  • t: Time since calving in days (DIM).
  • a: Scale parameter (numerical).
  • b: Growth parameter (numerical).
  • c: Quadratic decay parameter (numerical).
Returns:

Predicted milk yield at t.

def wilmink_model(t, a, b, c, k=-0.05):
113def wilmink_model(t, a, b, c, k=-0.05):
114    """Wilmink lactation curve model.
115
116    Args:
117        t: Time since calving in days (DIM), scalar or array-like.
118        a: Intercept-like parameter (numerical).
119        b: Linear trend coefficient (numerical).
120        c: Exponential-term scale (numerical).
121        k: Fixed exponential rate (numerical), default -0.05.
122
123    Returns:
124        Predicted milk yield at `t`.
125
126    Notes:
127        Formula: `y(t) = a + b * t + c * exp(k * t)`.
128    """
129    t = np.asarray(t)
130    return a + b * t + c * np.exp(k * t)

Wilmink lactation curve model.

Arguments:
  • t: Time since calving in days (DIM), scalar or array-like.
  • a: Intercept-like parameter (numerical).
  • b: Linear trend coefficient (numerical).
  • c: Exponential-term scale (numerical).
  • k: Fixed exponential rate (numerical), default -0.05.
Returns:

Predicted milk yield at t.

Notes:

Formula: y(t) = a + b * t + c * exp(k * t).

def wood_model(t, a, b, c):
 95def wood_model(t, a, b, c):
 96    """Wood lactation curve model.
 97
 98    Args:
 99        t: Time since calving in days (DIM), scalar or array-like.
100        a: Scale parameter (numerical).
101        b: Shape parameter controlling rise (numerical).
102        c: Decay parameter (numerical).
103
104    Returns:
105        Predicted milk yield at `t`.
106
107    Notes:
108        Formula: `y(t) = a * t**b * exp(-c * t)`.
109    """
110    return a * (t**b) * np.exp(-c * t)

Wood lactation curve model.

Arguments:
  • t: Time since calving in days (DIM), scalar or array-like.
  • a: Scale parameter (numerical).
  • b: Shape parameter controlling rise (numerical).
  • c: Decay parameter (numerical).
Returns:

Predicted milk yield at t.

Notes:

Formula: y(t) = a * t**b * exp(-c * t).