# Breeders in the east, finishers in the west

**Answer.** **Maldonado** is the most breeding-heavy department: 71.7% of its beef herd is breeding cows and calves. **Soriano** leans hardest to finishing: 29.8% steers and fattening cows. The east breeds; the west finishes.

Notebook: https://cow-tools.sophia-labs.com/notebooks/herd-structure/herd-structure.ipynb · Page: https://cow-tools.sophia-labs.com/notebooks/herd-structure/index.html · Data: Uruguay MGAP/SNIG DICOSE 2024–2025 (graph `uruguay-cows` at https://cow-tools.sophia-labs.com/mcp).

## The notebook

A beef herd is a pipeline: breeding cows have calves, calves become
yearlings, steers are fattened and sold. Regions specialise. Where the land
is poor, farmers keep cows and sell the calves (*cría*); where it is better,
they buy those calves and finish them (*invernada*). The declaration's nine
beef categories show which is which, department by department.

The categories (`codigoscategoria.csv`, species 1):
1 bulls, 2 breeding cows (mated), 3 cows being fattened, 4–6 steers by age,
7–8 heifers, 9 calves. We fold them into four groups so the chart stays
legible: **breeding** (2 breeding cows + 9 calves), **steers and fattening
cows** (3, 4, 5, 6), **heifers** (7, 8) and **bulls** (1).

```python
# Setup.
import altair as alt
import pandas as pd

import cowtools_client as cw
import cowtools_charts as cc

names = cc.department_names()
GROUP = {"1": "bulls", "2": "breeding", "9": "breeding", "3": "steers & fattening cows",
         "4": "steers & fattening cows", "5": "steers & fattening cows", "6": "steers & fattening cows",
         "7": "heifers", "8": "heifers"}
```

```python
# The query: cattle (beef and dairy) by category and department, 2025, with the beef category labels.
allcat = cw.sparql("""
SELECT ?department ?species ?category (SUM(?n) AS ?animals)
WHERE {
  GRAPH ?g {
    ?obs a cow:AnimalCategoryStockObservation ; cow:year "2025"^^xsd:gYear ;
         cow:department ?department ; cow:species ?species ;
         cow:animalCategory ?category ; cow:animalsCount ?n .
    VALUES ?species { <urn:sophia:cow:species/1> <urn:sophia:cow:species/11> }
  }
}
GROUP BY ?department ?species ?category
""")
cat = allcat[allcat["species"] == "urn:sophia:cow:species/1"].copy()
labels = cw.sparql("""
SELECT DISTINCT ?category ?label
WHERE { GRAPH ?g { ?category skos:prefLabel ?label }
        FILTER(STRSTARTS(STR(?category), "urn:sophia:cow:animal-category/1/")) }
""")
labels["code"] = labels["category"].map(cw.dept_code)
labels = labels.drop_duplicates("code").sort_values("code", key=lambda s: s.astype(int))
cc.show_table(labels[["code", "label"]], name="beef-categories")
```

|   code | label                                      |
|-------:|:-------------------------------------------|
|      1 | 1) TOROS                                   |
|      2 | 2) VACAS DE CRIA (ENTORADAS)               |
|      3 | 3) VACAS DE INVERNADA                      |
|      4 | 4) NOVILLOS DE MAS DE 3 AÑOS               |
|      5 | 5) NOVILLOS DE 2 A 3 AÑOS                  |
|      6 | 6) NOVILLOS DE 1 A 2 AÑOS                  |
|      7 | 7) VAQUILLONA DE MAS DE 2 AÑOS SIN ENTORAR |
|      8 | 8) VAQUILLONAS DE 1 A 2 AÑOS               |
|      9 | 9) TERNEROS/TERNERAS                       |

```python
cat["code"] = cat["department"].map(cw.dept_code)
cat["cat_code"] = cat["category"].map(cw.dept_code)
cat["group"] = cat["cat_code"].map(GROUP)
cw.check("every beef category maps to a group", int(cat["group"].isna().sum()), 0, source="the 9 beef categories")
wide = cat.pivot_table(index="code", columns="group", values="animals", aggfunc="sum").fillna(0)
share = wide.div(wide.sum(axis=1), axis=0)
share["name"] = share.index.map(names)
share = share.sort_values("breeding", ascending=False)
cc.show_table(share[["name", "breeding", "steers & fattening cows", "heifers", "bulls"]].reset_index(drop=True),
              floatfmt=".3f", name="group-shares")
```

```text
PASS  every beef category maps to a group: 0 vs 0 (exactly)  — the 9 beef categories
```

| name           |   breeding |   steers & fattening cows |   heifers |   bulls |
|:---------------|-----------:|--------------------------:|----------:|--------:|
| Maldonado      |      0.717 |                     0.132 |     0.131 |   0.021 |
| Treinta y Tres |      0.698 |                     0.153 |     0.132 |   0.017 |
| Rocha          |      0.678 |                     0.175 |     0.129 |   0.018 |
| Florida        |      0.677 |                     0.149 |     0.157 |   0.017 |
| Lavalleja      |      0.673 |                     0.178 |     0.131 |   0.018 |
| Rivera         |      0.67  |                     0.167 |     0.146 |   0.016 |
| Colonia        |      0.657 |                     0.183 |     0.149 |   0.012 |
| San José       |      0.652 |                     0.171 |     0.165 |   0.011 |
| Cerro Largo    |      0.649 |                     0.205 |     0.13  |   0.016 |
| Durazno        |      0.649 |                     0.205 |     0.129 |   0.017 |
| Salto          |      0.646 |                     0.196 |     0.14  |   0.018 |
| Artigas        |      0.644 |                     0.211 |     0.129 |   0.015 |
| Paysandú       |      0.637 |                     0.213 |     0.133 |   0.017 |
| Tacuarembó     |      0.62  |                     0.219 |     0.145 |   0.016 |
| Flores         |      0.606 |                     0.232 |     0.148 |   0.015 |
| Canelones      |      0.601 |                     0.226 |     0.159 |   0.015 |
| Río Negro      |      0.583 |                     0.262 |     0.142 |   0.013 |
| Montevideo     |      0.581 |                     0.208 |     0.193 |   0.018 |
| Soriano        |      0.527 |                     0.298 |     0.16  |   0.014 |

### Cross-check against the source

```python
csv = cw.source_csv(2025, "datosanimalesdetallados.csv")
beef = csv[csv["EspecieCodigo"] == "1"]
src = cw.num(beef["AnimalesPorCategoriaPD_AD"]).groupby([beef["DepartamentoCodigo"], beef["CategoriaCodigo"]]).sum()
cube = cat.set_index(["code", "cat_code"])["animals"]
diff = (cube - src.reindex(cube.index)).abs().max()
cw.check("2025 beef cattle by department × category: cube = CSV (largest difference)", int(diff), 0,
         source="datosanimalesdetallados.csv 2025, AnimalesPorCategoriaPD_AD")
cw.check("2025 beef categories sum to the beef total in datosanimales", int(cat["animals"].sum()), 11_583_321,
         source="notebook national-herd: beef cattle 2025")
```

```text
PASS  2025 beef cattle by department × category: cube = CSV (largest difference): 0 vs 0 (exactly)  — datosanimalesdetallados.csv 2025, AnimalesPorCategoriaPD_AD
PASS  2025 beef categories sum to the beef total in datosanimales: 11,583,321 vs 11,583,321 (exactly)  — notebook national-herd: beef cattle 2025
```

The cube also materialises a composition at ingest, the derived metric
`category_share_of_bovine`. Its denominator is a department's **whole**
cattle herd, beef and dairy together (its shares sum to 1 across both
species' categories), where ours is the beef herd alone. Put on its
denominator, our numbers must reproduce it for every department and
category, to the six decimals it keeps.

```python
derived = cw.sparql("""
SELECT ?department ?category ?value
WHERE {
  GRAPH ?g {
    ?d a cow:DerivedMetricObservation ; cow:year "2025"^^xsd:gYear ;
       cow:metric cow:metric_category_share_of_bovine ;
       cow:department ?department ; cow:animalCategory ?category ; cow:metricValue ?value .
  }
}
""")
whole = allcat.assign(code=allcat["department"].map(cw.dept_code)).groupby("code")["animals"].sum()
ours_all = allcat.assign(code=allcat["department"].map(cw.dept_code))
ours_all["share"] = ours_all["animals"] / ours_all["code"].map(whole)
mine = ours_all.set_index(["code", "category"])["share"]
theirs = derived.assign(code=derived["department"].map(cw.dept_code)).set_index(["code", "category"])["value"].astype(float)
cw.check("derived rows cover every department × category", len(theirs), len(mine), source="cube derived metric")
gap = float((theirs - mine.reindex(theirs.index)).abs().max())
cw.check("category_share_of_bovine (derived, 6 dp) = our shares on its denominator (largest gap)", round(gap, 7), 0.0,
         tol=5e-7, source="cow:metric_category_share_of_bovine, materialised at ingest")
```

```text
PASS  derived rows cover every department × category: 304 vs 304 (exactly)  — cube derived metric
PASS  category_share_of_bovine (derived, 6 dp) = our shares on its denominator (largest gap): 5e-07 vs 0 (within ±5e-07)  — cow:metric_category_share_of_bovine, materialised at ingest
```

```python
east = share.iloc[0]
west = share.sort_values("steers & fattening cows", ascending=False).iloc[0]
# The sentence below says "east" and "west": hold it to that.
EAST = {"Maldonado", "Rocha", "Treinta y Tres", "Lavalleja", "Cerro Largo"}
WEST = {"Soriano", "Río Negro", "Colonia", "Paysandú", "San José"}
cw.check("the most breeding-heavy department is in the east", east["name"] in EAST, True, source=f"{sorted(EAST)}")
cw.check("the most finishing-heavy department is in the west", west["name"] in WEST, True, source=f"{sorted(WEST)}")
cw.finding("headline",
    f"**{east['name']}** is the most breeding-heavy department: {east['breeding']:.1%} of its beef herd is "
    f"breeding cows and calves. **{west['name']}** leans hardest to finishing: "
    f"{west['steers & fattening cows']:.1%} steers and fattening cows. The east breeds; the west finishes.")
```

```text
PASS  the most breeding-heavy department is in the east: True vs True (exactly)  — ['Cerro Largo', 'Lavalleja', 'Maldonado', 'Rocha', 'Treinta y Tres']
PASS  the most finishing-heavy department is in the west: True vs True (exactly)  — ['Colonia', 'Paysandú', 'Río Negro', 'San José', 'Soriano']
```

> **Finding.** **Maldonado** is the most breeding-heavy department: 71.7% of its beef herd is breeding cows and calves. **Soriano** leans hardest to finishing: 29.8% steers and fattening cows. The east breeds; the west finishes.

```python
long = share.reset_index().melt(id_vars=["code", "name"], value_vars=["breeding", "heifers", "bulls",
                                                                    "steers & fattening cows"],
                                 var_name="group", value_name="share")
order = share["name"].tolist()
stack = alt.Chart(long, title=alt.Title("Beef herd composition by department, 2025",
                                        subtitle="share of beef cattle (PD_AD); departments ordered by breeding share")).mark_bar(height=14).encode(
    y=alt.Y("name:N", sort=order, title=None),
    x=alt.X("share:Q", stack="normalize", title=None, axis=alt.Axis(format=".0%")),
    color=alt.Color("group:N", scale=alt.Scale(domain=["breeding", "heifers", "bulls", "steers & fattening cows"],
                                               range=[cc.CATTLE, cc.SERIES[3], cc.MUTED, cc.SHEEP]), title=None),
    order=alt.Order("group_order:Q"),
    tooltip=["name:N", "group:N", alt.Tooltip("share:Q", format=".1%")],
).transform_calculate(group_order="indexof(['breeding','heifers','bulls','steers & fattening cows'], datum.group)").properties(width=500, height=380)
cc.show(stack, "composition",
        alt_text="Stacked bars per department: eastern departments (Maldonado, Treinta y Tres, Rocha) have the largest breeding share; Soriano and Río Negro the largest share of steers.")
```

![Stacked bars per department: eastern departments (Maldonado, Treinta y Tres, Rocha) have the largest breeding share; Soriano and Río Negro the largest share of steers.](charts/composition.svg)

Chart data and encoding: [charts/composition.vl.json](charts/composition.vl.json) (Vega-Lite).

```python
share["steers_per_breeding"] = share["steers & fattening cows"] / share["breeding"]
cc.show(cc.choropleth(share.reset_index(), "steers_per_breeding",
                      title="Steers and fattening cows per breeding animal, 2025",
                      subtitle="(categories 3–6) ÷ (breeding cows + calves); higher = more finishing",
                      legend="ratio", fmt=".2f"),
        "finishing-map", thumbnail=True,
        alt_text="Map: the finishing ratio is highest in the littoral west (Soriano, Río Negro) and lowest in Maldonado.")
print(cc.map_credit())
```

![Map: the finishing ratio is highest in the littoral west (Soriano, Río Negro) and lowest in Maldonado.](charts/finishing-map.svg)

Chart data and encoding: [charts/finishing-map.vl.json](charts/finishing-map.vl.json) (Vega-Lite).

```text
Map: Natural Earth 1:10m admin-1 (public domain, v5.1.2), joined on the DICOSE department code. Made with Natural Earth.
```

## Where every number came from

```python
cc.show_table(cw.checks()[["check", "got", "expected", "rule", "source"]], name="checks")
cw.provenance()
```

| check                                                                                  |           got |      expected | rule          | source                                                               |
|:---------------------------------------------------------------------------------------|--------------:|--------------:|:--------------|:---------------------------------------------------------------------|
| every beef category maps to a group                                                    |   0           |   0           | exactly       | the 9 beef categories                                                |
| 2025 beef cattle by department × category: cube = CSV (largest difference)             |   0           |   0           | exactly       | datosanimalesdetallados.csv 2025, AnimalesPorCategoriaPD_AD          |
| 2025 beef categories sum to the beef total in datosanimales                            |   1.15833e+07 |   1.15833e+07 | exactly       | notebook national-herd: beef cattle 2025                             |
| derived rows cover every department × category                                         | 304           | 304           | exactly       | cube derived metric                                                  |
| category_share_of_bovine (derived, 6 dp) = our shares on its denominator (largest gap) |   5e-07       |   0           | within ±5e-07 | cow:metric_category_share_of_bovine, materialised at ingest          |
| the most breeding-heavy department is in the east                                      |   1           |   1           | exactly       | ['Cerro Largo', 'Lavalleja', 'Maldonado', 'Rocha', 'Treinta y Tres'] |
| the most finishing-heavy department is in the west                                     |   1           |   1           | exactly       | ['Colonia', 'Paysandú', 'Río Negro', 'San José', 'Soriano']          |

```text
     kind        target  rows  \
0  sparql  uruguay-cows   304   
1  sparql  uruguay-cows     9   
2  sparql  uruguay-cows   304   

                                          first line  
0  SELECT ?department ?species ?category (SUM(?n)...  
1                   SELECT DISTINCT ?category ?label  
2                SELECT ?department ?category ?value
```

```python
cw.save()
```
