# Cattle and sheep, department by department

**Answer.** Only **Salto and Artigas** still have more sheep than cattle by heads (53.5% and 51.9% sheep). The national flock fell **11.5%** in one year, from 5,420,661 to 4,799,066: 621,595 fewer sheep.

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

## Findings

- Only **Salto and Artigas** still have more sheep than cattle by heads (53.5% and 51.9% sheep). The national flock fell **11.5%** in one year, from 5,420,661 to 4,799,066: 621,595 fewer sheep.
- Every one of the 19 departments declared fewer sheep in 2025. The steepest fall was in **Río Negro** (-18.3%); in absolute heads, Salto lost the most (118,943).

## The notebook

Uruguay is a cattle country, but the north is still sheep country: on the
basalt of Salto and Artigas there are more sheep than cattle. Wool built the
old estancias; the national flock has been shrinking for decades. This
notebook asks how the two herds split across the 19 departments, and what one
year (2024 → 2025) did to the sheep.

A note on comparing a sheep with a cow: we compare **heads**, the unit
DICOSE counts. In livestock units a sheep weighs far less (DICOSE's own
codelist weights a breeding ewe at 0.17 of a unit, a breeding cow at 1.03),
so a department that is "half sheep" by heads is still mostly cattle by grass
eaten.

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

import cowtools_client as cw
import cowtools_charts as cc
```

```python
# The query: cattle (beef + dairy) and sheep per department, both years.
q = cw.sparql("""
SELECT ?department ?year ?kind (SUM(?n) AS ?animals)
WHERE {
  GRAPH ?g {
    ?obs a cow:AnimalFlowObservation ;
         cow:year ?year ;
         cow:department ?department ;
         cow:species ?species ;
         cow:animalsCount ?n .
    VALUES (?species ?kind) {
      (<urn:sophia:cow:species/1>  "cattle")
      (<urn:sophia:cow:species/11> "cattle")
      (<urn:sophia:cow:species/2>  "sheep")
    }
  }
}
GROUP BY ?department ?year ?kind
""")
q["code"] = q["department"].map(cw.dept_code)
t = q.pivot_table(index="code", columns=["kind", "year"], values="animals", aggfunc="sum")
t.columns = [f"{k}_{y}" for k, y in t.columns]
t = t.reset_index().merge(cw.departments()[["code", "name"]], on="code")
for y in (2024, 2025):
    t[f"sheep_share_{y}"] = t[f"sheep_{y}"] / (t[f"sheep_{y}"] + t[f"cattle_{y}"])
t["sheep_change"] = t["sheep_2025"] / t["sheep_2024"] - 1
t["cattle_change"] = t["cattle_2025"] / t["cattle_2024"] - 1
t = t.sort_values("sheep_share_2025", ascending=False)
cc.show_table(t[["name", "cattle_2025", "sheep_2025", "sheep_share_2025", "sheep_change"]]
              .rename(columns={"sheep_share_2025": "sheep share of heads", "sheep_change": "sheep 2024→25"}),
              floatfmt=".3f", name="by-department")
```

| name           | cattle_2025   | sheep_2025   |   sheep share of heads |   sheep 2024→25 |
|:---------------|:--------------|:-------------|-----------------------:|----------------:|
| Salto          | 898,899       | 1,032,181    |                  0.535 |          -0.103 |
| Artigas        | 742,198       | 801,403      |                  0.519 |          -0.098 |
| Montevideo     | 2,736         | 2,369        |                  0.464 |          -0.004 |
| Paysandú       | 801,512       | 555,079      |                  0.409 |          -0.149 |
| Maldonado      | 234,191       | 101,340      |                  0.302 |          -0.061 |
| Lavalleja      | 654,451       | 261,086      |                  0.285 |          -0.099 |
| Tacuarembó     | 1,061,136     | 398,742      |                  0.273 |          -0.122 |
| Cerro Largo    | 950,412       | 303,495      |                  0.242 |          -0.129 |
| Durazno        | 952,171       | 290,920      |                  0.234 |          -0.131 |
| Treinta y Tres | 584,734       | 178,572      |                  0.234 |          -0.142 |
| Rivera         | 639,917       | 182,585      |                  0.222 |          -0.094 |
| Rocha          | 682,644       | 154,813      |                  0.185 |          -0.088 |
| Flores         | 409,429       | 83,001       |                  0.169 |          -0.13  |
| Florida        | 1,117,097     | 202,434      |                  0.153 |          -0.114 |
| Canelones      | 338,601       | 50,487       |                  0.13  |          -0.041 |
| Río Negro      | 549,369       | 69,144       |                  0.112 |          -0.183 |
| Soriano        | 567,452       | 55,293       |                  0.089 |          -0.097 |
| Colonia        | 537,066       | 38,341       |                  0.067 |          -0.158 |
| San José       | 541,254       | 37,781       |                  0.065 |          -0.152 |

### Cross-check against the source

Re-derive the 2025 department totals from `datosanimales.csv` and compare
every department, both species, to the head. Then compare three shares with
the figures the July 2026 expedition published (Salto 53.5% sheep, Artigas
51.9%, San José 6.5%).

```python
csv = cw.source_csv(2025, "datosanimales.csv")
csv["n"] = cw.num(csv["AnimalesPorEspeciePD_AD"])
csv["kind"] = csv["EspecieCodigo"].map({"1": "cattle", "11": "cattle", "2": "sheep"})
src = csv.dropna(subset=["kind"]).groupby(["DepartamentoCodigo", "kind"])["n"].sum().unstack()
mismatches = 0
for _, row in t.iterrows():
    for kind in ("cattle", "sheep"):
        if int(row[f"{kind}_2025"]) != int(src.loc[row["code"], kind]):
            mismatches += 1
cw.check("2025 cattle and sheep, all 19 departments: cube = CSV (mismatching cells)", mismatches, 0,
         source="datosanimales.csv 2025, AnimalesPorEspeciePD_AD")
share = t.set_index("name")["sheep_share_2025"]
cw.check("Salto sheep share of heads", round(share["Salto"], 3), 0.535, tol=0.0005, source="expedition 2026-07-05")
cw.check("Artigas sheep share of heads", round(share["Artigas"], 3), 0.519, tol=0.0005, source="expedition 2026-07-05")
cw.check("San José sheep share of heads", round(share["San José"], 3), 0.065, tol=0.0005, source="expedition 2026-07-05")
```

```text
PASS  2025 cattle and sheep, all 19 departments: cube = CSV (mismatching cells): 0 vs 0 (exactly)  — datosanimales.csv 2025, AnimalesPorEspeciePD_AD
PASS  Salto sheep share of heads: 0.535 vs 0.535 (within ±0.0005)  — expedition 2026-07-05
PASS  Artigas sheep share of heads: 0.519 vs 0.519 (within ±0.0005)  — expedition 2026-07-05
PASS  San José sheep share of heads: 0.065 vs 0.065 (within ±0.0005)  — expedition 2026-07-05
```

```python
sheep_n = {y: int(t[f"sheep_{y}"].sum()) for y in (2024, 2025)}
fall = sheep_n[2025] / sheep_n[2024] - 1
cw.check("national sheep 2024 → 2025", round(fall, 4), -0.1147, tol=0.00005, source="expedition 2026-07-05: −11.47%")
majority = t.query("sheep_share_2025 > 0.5")["name"].tolist()
cw.finding("headline",
    f"Only **{' and '.join(majority)}** still have more sheep than cattle by heads "
    f"({share['Salto']:.1%} and {share['Artigas']:.1%} sheep). The national flock fell "
    f"**{-fall:.1%}** in one year, from {sheep_n[2024]:,} to {sheep_n[2025]:,}: "
    f"{sheep_n[2024]-sheep_n[2025]:,} fewer sheep.")
```

```text
PASS  national sheep 2024 → 2025: -0.1147 vs -0.1147 (within ±5e-05)  — expedition 2026-07-05: −11.47%
```

> **Finding.** Only **Salto and Artigas** still have more sheep than cattle by heads (53.5% and 51.9% sheep). The national flock fell **11.5%** in one year, from 5,420,661 to 4,799,066: 621,595 fewer sheep.

## The map: where the sheep are

Share of heads that are sheep, 2025. The north-west basalt (Artigas, Salto,
Paysandú) is the old sheep country; the dairy south-west is nearly free of
them.

```python
cc.show(cc.choropleth(t, "sheep_share_2025", title="Sheep as a share of cattle + sheep, 2025",
                      subtitle="heads on the holding (PD_AD)", legend="sheep share", fmt=".0%",
                      domain=[0, 0.6]),
        "sheep-share-map-2025", thumbnail=True,
        alt_text="Map: sheep are over half the heads in Salto and Artigas; Montevideo (46%, a few thousand animals) and Paysandú (41%) follow; lowest in San José and Colonia.")
print(cc.map_credit())
```

![Map: sheep are over half the heads in Salto and Artigas; Montevideo (46%, a few thousand animals) and Paysandú (41%) follow; lowest in San José and Colonia.](charts/sheep-share-map-2025.svg)

Chart data and encoding: [charts/sheep-share-map-2025.vl.json](charts/sheep-share-map-2025.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.
```

## The decline, department by department

Every department lost sheep between the two declarations. The map is
coloured on a diverging scale centred on zero, so a department that had
gained would show blue; none did.

```python
lost_all = bool((t["sheep_change"] < 0).all())
worst = t.sort_values("sheep_change").iloc[0]
cw.check("every department lost sheep", lost_all, True, source="the table above")
cc.show(cc.choropleth(t, "sheep_change", title="Change in sheep, 2024 → 2025",
                      subtitle="per department; brown is a fall, blue a rise", legend="change", fmt="+.0%",
                      diverging=True),
        "sheep-change-map",
        alt_text="Diverging map: every department is brown, a fall in sheep everywhere; the steepest is Río Negro.")
```

```text
PASS  every department lost sheep: True vs True (exactly)  — the table above
```

![Diverging map: every department is brown, a fall in sheep everywhere; the steepest is Río Negro.](charts/sheep-change-map.svg)

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

```python
cw.finding("decline",
    f"Every one of the 19 departments declared fewer sheep in 2025. The steepest fall was in "
    f"**{worst['name']}** ({worst['sheep_change']:+.1%}); in absolute heads, "
    f"{t.assign(d=t.sheep_2024 - t.sheep_2025).sort_values('d').iloc[-1]['name']} lost the most "
    f"({int(t.assign(d=t.sheep_2024 - t.sheep_2025)['d'].max()):,}).")
```

> **Finding.** Every one of the 19 departments declared fewer sheep in 2025. The steepest fall was in **Río Negro** (-18.3%); in absolute heads, Salto lost the most (118,943).

## Small multiples: both herds, both years

One panel per department, cattle and sheep side by side, 2024 and 2025. The
panels share a scale, so the size of each department's herd is comparable.

```python
long = pd.concat([
    t[["name", f"{k}_{y}"]].rename(columns={f"{k}_{y}": "animals"}).assign(kind=k, year=str(y))
    for k in ("cattle", "sheep") for y in (2024, 2025)
])
order = t.sort_values("cattle_2025", ascending=False)["name"].tolist()
sm = (
    alt.Chart(long)
    .mark_bar()
    .encode(
        x=alt.X("year:N", title=None, axis=alt.Axis(labelAngle=0)),
        xOffset="kind:N",
        y=alt.Y("animals:Q", title=None, axis=alt.Axis(format="~s", tickCount=3)),
        color=alt.Color("kind:N", scale=alt.Scale(domain=["cattle", "sheep"], range=[cc.CATTLE, cc.SHEEP]),
                        title=None),
        tooltip=["name:N", "kind:N", "year:N", alt.Tooltip("animals:Q", format=",")],
    )
    .properties(width=95, height=80)
    .facet(facet=alt.Facet("name:N", sort=order, title=None), columns=5)
    .properties(title=alt.Title("Cattle and sheep by department, 2024 and 2025",
                                subtitle="heads on the holding; panels ordered by cattle, largest first"))
)
cc.show(sm, "small-multiples",
        alt_text="Nineteen small panels, one per department, each with cattle and sheep bars for 2024 and 2025; sheep bars shrink everywhere.")
```

![Nineteen small panels, one per department, each with cattle and sheep bars for 2024 and 2025; sheep bars shrink everywhere.](charts/small-multiples.svg)

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

## 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                                          |
|:--------------------------------------------------------------------------|--------:|-----------:|:---------------|:------------------------------------------------|
| 2025 cattle and sheep, all 19 departments: cube = CSV (mismatching cells) |  0      |     0      | exactly        | datosanimales.csv 2025, AnimalesPorEspeciePD_AD |
| Salto sheep share of heads                                                |  0.535  |     0.535  | within ±0.0005 | expedition 2026-07-05                           |
| Artigas sheep share of heads                                              |  0.519  |     0.519  | within ±0.0005 | expedition 2026-07-05                           |
| San José sheep share of heads                                             |  0.065  |     0.065  | within ±0.0005 | expedition 2026-07-05                           |
| national sheep 2024 → 2025                                                | -0.1147 |    -0.1147 | within ±5e-05  | expedition 2026-07-05: −11.47%                  |
| every department lost sheep                                               |  1      |     1      | exactly        | the table above                                 |

```text
     kind        target  rows  \
0  sparql  uruguay-cows    76   

                                          first line  
0  SELECT ?department ?year ?kind (SUM(?n) AS ?an...
```

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