Data AnalyticsIndustry analytics playbooks

Dairy milk procurement: fat, SNF and price per litre by centre

PK
Pankit Kumar
Sr. Data Scientist at Parexel (a Goldman Sachs–backed company) · 27 September 2026 · 5 min read
In this article (8 sections)

Related programme: the Data Analytics with Generative AI course (3–4 months; ₹90,000 (inclusive of GST) · EMI from ₹4,000/month).

The short answer

To compare milk collection centres, weight fat and SNF by the quantity of milk poured, not by the number of farmers, and compare procurement cost per kilogram of milk solids as well as per litre. Price per litre mostly reflects how much buffalo milk a centre collects; price per kg of fat plus SNF shows whether the centre is paying more for the same solids. In the synthetic shift below, one centre pays ₹57.85 a litre and the other ₹41.08, but the gap per kg of solids is far smaller.

A procurement head's actual worry

A dairy's procurement manager sees a report that says Centre A's milk costs ₹16 a litre more than Centre B's. The instinct is to suspect the rate chart, the testing machine or the centre operator. Sometimes that is right. More often the centres simply buy different milk: buffalo milk carries more fat and solids and is priced higher under almost any fat-and-SNF based chart. The analytical job is to separate mix from price, and only then look for testing or payment anomalies.

Pouring records and what each field means

A typical automatic milk collection unit, or the software that consolidates it, gives one row per farmer per shift:

ColumnMeaningCaution
centre, routeVillage collection point and the route that tankers itCentres change route; keep the mapping by date
farmer_id, shiftMember code, morning or eveningEvening milk often tests differently
milk_typecow, buffalo, mixedSometimes inferred from fat level instead of declared
litres or kgQuantity measured at the centreKnow which one your unit records
fat_pct, snf_pctTest results from the analyserDefault values when the analyser was down
rate, amount_paidFrom the price chart in force for that dateChart versions change during the year

The price chart itself is set by each dairy or cooperative and varies across organisations and seasons. Treat it as an input table with an effective date; do not hardcode it in your analysis.

From percentages to kilograms

Fat and SNF percentages only aggregate correctly as kilograms:

  • kg milk = litres × density (use the conversion your plant applies; the code treats it as a parameter)
  • kg fat = kg milk × fat% ÷ 100, and likewise kg SNF
  • Weighted fat % for a centre = Σ kg fat ÷ Σ kg milk × 100
  • Cost per litre = Σ amount paid ÷ Σ litres
  • Cost per kg solids = Σ amount paid ÷ (Σ kg fat + Σ kg SNF)

The simple mean of farmers' fat percentages is the most common error in centre reports. It gives a farmer pouring four litres the same weight as one pouring forty.

Synthetic example: two centres, one morning

python
import pandas as pd

# Synthetic milk pouring records from two collection centres, one morning shift
pour = pd.DataFrame({
    "centre":  ["Khurja"] * 4 + ["Sikandrabad"] * 4,
    "farmer_id": ["F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8"],
    "milk_type": ["buffalo", "cow", "cow", "buffalo", "cow", "cow", "cow", "buffalo"],
    "litres":  [40.0, 8.0, 6.0, 12.0, 30.0, 25.0, 5.0, 4.0],
    "fat_pct": [6.8, 4.1, 3.9, 7.0, 3.6, 3.8, 4.4, 6.5],
    "snf_pct": [9.0, 8.4, 8.2, 9.1, 8.1, 8.3, 8.6, 9.0],
    "amount_paid": [2480.0, 336.0, 246.0, 756.0, 1170.0, 1000.0, 215.0, 244.0],
})

DENSITY_KG_PER_L = 1.03   # assumed conversion; replace with your plant's figure

pour["kg_milk"] = pour["litres"] * DENSITY_KG_PER_L
pour["kg_fat"] = pour["kg_milk"] * pour["fat_pct"] / 100
pour["kg_snf"] = pour["kg_milk"] * pour["snf_pct"] / 100

centre = pour.groupby("centre").agg(
    litres=("litres", "sum"), kg_milk=("kg_milk", "sum"),
    kg_fat=("kg_fat", "sum"), kg_snf=("kg_snf", "sum"),
    amount=("amount_paid", "sum"), simple_avg_fat=("fat_pct", "mean"))
centre["weighted_fat_pct"] = 100 * centre["kg_fat"] / centre["kg_milk"]
centre["weighted_snf_pct"] = 100 * centre["kg_snf"] / centre["kg_milk"]
centre["rs_per_litre"] = centre["amount"] / centre["litres"]
centre["rs_per_kg_solids"] = centre["amount"] / (centre["kg_fat"] + centre["kg_snf"])
print(centre[["litres", "simple_avg_fat", "weighted_fat_pct", "weighted_snf_pct",
              "rs_per_litre", "rs_per_kg_solids"]].round(2).to_string())

by_type = pour.groupby(["centre", "milk_type"])["litres"].sum().unstack(fill_value=0)
print((100 * by_type.div(by_type.sum(axis=1), axis=0)).round(0).to_string())

Output:

text
             litres  simple_avg_fat  weighted_fat_pct  weighted_snf_pct  rs_per_litre  rs_per_kg_solids
centre
Khurja         66.0            5.45              6.25              8.87         57.85            371.50
Sikandrabad    64.0            4.58              3.92              8.27         41.08            327.02
milk_type    buffalo   cow
centre
Khurja          79.0  21.0
Sikandrabad      6.0  94.0

Reading the result like a procurement manager

Khurja's milk is 79% buffalo by volume against Sikandrabad's 6%. That alone explains most of the ₹16.77 per-litre gap. Per kg of solids, the gap narrows to about ₹44, and that residual is the part worth investigating: is Khurja's chart more generous, are some pours priced above the chart, or does the mix of fat and SNF within those solids differ?

Notice also that the simple average understates Khurja's fat (5.45% against a weighted 6.25%) because its biggest pourer has high-fat milk, and overstates Sikandrabad's (4.58% against 3.92%) because its small pourers test higher. A report built on simple means misstates both centres, in opposite directions, and nobody notices because the numbers look plausible.

Checks before you blame the centre

  1. 1Reprice every pour from the chart. Join each row to the chart version in force on its date and compare the expected amount with amount_paid. Differences point to manual overrides or outdated charts on the unit.
  2. 2Look for default test values. A run of identical fat and SNF readings for many farmers usually means the analyser was bypassed. Flag shifts where the share of repeated values is unusually high.
  3. 3Reconcile with the chilling centre or plant. Solids bought at village centres should roughly match solids received at the plant after the tanker's own test. A persistent shortfall on one route is a handling or adulteration question, and it needs the plant's lab data, not just centre records.
  4. 4Separate morning and evening shifts. Their composition often differs; mixing them in one average hides shift-specific problems.
  5. 5Keep seasons apart. Flush and lean seasons shift both volumes and composition. Compare a centre with its own same month last year before comparing it with a neighbour.

A one-page brief

For the procurement head, one table per route is enough: litres, buffalo share, weighted fat and SNF, cost per litre, cost per kg solids, and the chart-repricing difference in rupees. Add a single sentence at the top that states the conclusion in business terms, for example: "Most of the per-litre gap between the two centres is milk mix; about ₹44 per kg of solids remains unexplained and the repricing check is the next step."

Resist the urge to rank farmers. Their data belongs to a member-owned relationship in many dairies, and the decision the manager needs is about centres, charts and testing equipment.

Weighted averages that go wrong when you aggregate percentages show up across operations analytics, including warehouse pick rates. Aggregation discipline in pandas is a core part of the Data Analyst programme. If you are deciding between an analytics and a modelling path, data analytics vs data science sets out the difference.

PK
Pankit Kumar
Lead Instructor, NeuraPath Academy

Pankit Kumar has 10 years in Data Science & AI, building and shipping production systems in regulated pharma and clinical environments. He is a freelance trainer at Boston Institute of Analytics, AnalytixLabs and Scaler, and has taught this material to thousands of working professionals.

This article is part of our Data Analytics with Generative AI programme — 3–4 months. The full analyst stack — Excel, SQL, Power BI and Python pipelines — then a generative-AI layer you can prove is right.

Explore Data Analytics with Generative AI
Counselling is free · no obligation

Not sure which programme fits?

Tell us your background and we will map it to the right entry point — including saying so when a cheaper programme is the better fit. A counsellor replies within one working day.