cassionData Analysis

Back to the lessonLesson 5 of 8The market is an outcome too

The calendar is bigger than the programme

The same deck as the downloads, rendered as a page. Start the slideshow to present it full screen — arrow keys or a click advance one slide, Escape leaves.

Slides · PDFSlides · PowerPoint

  1. Slide 1 / 26

    What this lesson covers

    • An independent line of evidence
    • Clean three things before plotting anything
    • The seasonal cycle
    • The panel that changed composition
    • Report the series with its panel
    • What comes next
    Speaker notes
    Median maize is 75 gourdes at the December harvest and 130 at the July peak. Compare two survey rounds taken at different points in that cycle and you have measured the calendar — and one district's panel changed composition exactly when it mattered.
  2. Slide 2 / 26

    An independent line of evidence — In Python

    import pandas as pd
    
    prices = pd.read_csv("market-prices-2024.v1.csv")
    print(f"{len(prices):,} observations")
    print(f"{prices['market_id'].nunique()} markets, "
          f"{prices['commodity'].nunique()} series, "
          f"{prices['period'].nunique()} months")
    Speaker notes
    Everything in the first two units is what households said about themselves. A price series is not: nobody is recalling anything, and a market that has doubled its maize price has done so whether or not a survey was in the field.
  3. Slide 3 / 26

    An independent line of evidence — In R

    library(dplyr)
    
    prices |> summarise(n = n(), markets = n_distinct(market_id),
                        series = n_distinct(commodity), months = n_distinct(period))
    Speaker notes
    2,273 observations, twelve markets, eight series, twenty-four months. Two years is the minimum useful length, because a single year cannot distinguish a seasonal peak from a deterioration.
  4. Slide 4 / 26

    Clean three things before plotting anything — In Python

    prices["price_htg"] = pd.to_numeric(prices["price_htg"])
    
    dollars = prices["price_htg"] < 10
    marmite = (prices["commodity"] == "maize") & (prices["market_id"] == "MK005")
    thin = prices["trader_quotes"] == 1
    
    print(f"dollar entries: {dollars.sum()}")
    print(f"marmite market maize rows: {marmite.sum()}")
    print(f"single-quote observations: {thin.sum()}")
  5. Slide 5 / 26

    Clean three things before plotting anything — In R

    prices |> summarise(
      dollars = sum(price_htg < 10),
      thin = sum(trader_quotes == 1)
    )
  6. Slide 6 / 26

    Clean three things before plotting anything

    • Eight prices are in dollars — with the currency column still saying HTG
    • One market records maize by the marmite — a volume measure of roughly 2.7 kg, and files it in a column labelled per…
    Speaker notes
    Eight prices are in dollars with the currency column still saying HTG. They read as values under 10 and an outlier filter would delete them, when multiplying by about 132 recovers them. One market records maize by the marmite, a volume measure of roughly 2.7 kg, and files it in a column labelled per kilogram. Its median maize price is 263 gourdes against about 100 everywhere else.
  7. Slide 7 / 26

    Clean three things before plotting anything — In Python

    by_market = prices[prices["commodity"] == "maize"].groupby("market_id")["price_htg"].median()
    print(by_market.round(1).sort_values())
  8. Slide 8 / 26

    Clean three things before plotting anything — In R

    prices |> filter(commodity == "maize") |>
      summarise(median = median(price_htg), .by = market_id) |> arrange(median)
  9. Slide 9 / 26

    Clean three things before plotting anything

    • The unit error is invisible in one market and obvious across twelve — That is the general rule for a unit slip: it…
    • Thirteen observations rest on a single trader quote — They are not errors
    Speaker notes
    The unit error is invisible in one market and obvious across twelve. That is the general rule for a unit slip: it never looks wrong on its own row, and it always looks wrong beside its peers. Thirteen observations rest on a single trader quote. They are not errors. A median of one is a quote rather than a price, and the decision is whether to exclude them or weight them down — either is defensible, silence is not.
  10. Slide 10 / 26

    The seasonal cycle — In Python

    clean = prices[~dollars & ~marmite]
    maize = clean[clean["commodity"] == "maize"]
    series = maize.groupby("period")["price_htg"].median()
    print(series.round(0))
  11. Slide 11 / 26

    The seasonal cycle — In R

    prices |>
      filter(commodity == "maize", price_htg >= 10, market_id != "MK005") |>
      summarise(median = median(price_htg), .by = period) |> arrange(period)
  12. Slide 12 / 26

    The seasonal cycle

    Month20232024
    January6670
    April97110
    July (lean peak)130159
    October92104
    December (harvest)7581
  13. Slide 13 / 26

    The seasonal cycle

    • Maize costs 74% more at the July peak than at the December harvest in 2023, and 96% more in 2024 — That is the size of…
    • The consequence for survey design is direct — A baseline in December and an endline in July would show a catastrophic…
    Speaker notes
    Maize costs 74% more at the July peak than at the December harvest in 2023, and 96% more in 2024. That is the size of the seasonal effect, and it is larger than almost any programme effect anyone will ask you to detect. The consequence for survey design is direct. A baseline in December and an endline in July would show a catastrophic deterioration produced entirely by the calendar. Compare like with like: July against July.
  14. Slide 14 / 26

    The seasonal cycle — In Python

    peaks = series.loc[["2023-07", "2024-07"]]
    print(f"lean peak 2023: {peaks['2023-07']:.1f}")
    print(f"lean peak 2024: {peaks['2024-07']:.1f}")
    print(f"year on year: {peaks['2024-07'] / peaks['2023-07'] - 1:+.1%}")
  15. Slide 15 / 26

    The seasonal cycle — In R

    # Same point in the season, one year apart. Everything else is the calendar.
  16. Slide 16 / 26

    The seasonal cycle

    • 130.1 against 159.3 — a 22% rise at the same point in the season — That is the deterioration, and it is a quarter of…
    Speaker notes
    130.1 against 159.3 — a 22% rise at the same point in the season. That is the deterioration, and it is a quarter of the size of the seasonal swing that would have been mistaken for it.
  17. Slide 17 / 26

    The panel that changed composition — In Python

    coverage = maize.pivot_table(index="period", columns="market_id",
                                 values="price_htg", aggfunc="size")
    print(coverage.isna().sum(axis=1)[lambda s: s > 0])
  18. Slide 18 / 26

    The panel that changed composition — In R

    prices |> filter(commodity == "maize") |> count(period, market_id) |>
      count(period) |> filter(n < 11)
  19. Slide 19 / 26

    The panel that changed composition

    • MK001 stops reporting from June to September 2024 — when its road is cut
    Speaker notes
    MK001 stops reporting from June to September 2024, when its road is cut. The rows are absent rather than zero, so nothing in the file announces it. It is also the most expensive market in Nord-Ouest, and Nord-Ouest has only three markets — so losing one moves the district mean by a third of its deviation rather than a twelfth.
  20. Slide 20 / 26

    The panel that changed composition — In Python

    nord = maize[maize["district"] == "Nord-Ouest"]
    reported = nord.groupby("period")["price_htg"].mean()
    
    balanced = nord[nord["market_id"] != "MK001"].groupby("period")["price_htg"].mean()
    print(pd.DataFrame({"reported": reported, "balanced": balanced}).round(1)
          .loc["2024-04":"2024-10"])
  21. Slide 21 / 26

    The panel that changed composition — In R

    prices |>
      filter(commodity == "maize", district == "Nord-Ouest") |>
      summarise(reported = mean(price_htg), .by = period)
  22. Slide 22 / 26

    The panel that changed composition

    MonthWhoever reportedThe two that reported every month
    May 2024147.0123.2
    June 2024172.1172.1
    May to June+17%+40%
  23. Slide 23 / 26

    The panel that changed composition

    • The reported series understates the lean-season rise by more than half — and every number in it is a correct mean of…
    • Use a balanced panel — only the markets present in every month you are comparing — or chain month-on-month changes…
    Speaker notes
    The reported series understates the lean-season rise by more than half, and every number in it is a correct mean of the markets that reported. The composition changed underneath the series and the series did not say so. Use a balanced panel — only the markets present in every month you are comparing — or chain month-on-month changes computed within markets. Both are more work than a groupby and both are the difference between a 17% rise and a 40% one.
  24. Slide 24 / 26

    Report the series with its panel — Example

    Maize price, gourdes per kg, median across markets
    
      2023 harvest (Dec)        75      2024 harvest (Dec)       81
      2023 lean peak (Jul)     130      2024 lean peak (Jul)    159
      Seasonal swing          +74%      Seasonal swing         +96%
      Year on year at the lean peak: +22%
    
      Balanced panel of 10 markets throughout. MK001 (Nord-Ouest) did not
      report June to September 2024 and is excluded from all periods; MK005
      records maize by the marmite and is excluded from maize.
      8 dollar-denominated entries converted at 132 HTG; 13 single-quote
      observations retained and flagged.
    Speaker notes
    The panel stated, the exclusions counted, and the year-on-year comparison made at the same point in the season. A price table without its panel is the same defect as a coverage figure without its reporting rate, and this course has now met it three times.
  25. Slide 25 / 26

    What comes next

    • A price says what food cost.
    Speaker notes
    A price says what food cost. It does not say whether anybody could afford it, and the next lesson is the ratio that does — where a goat whose price fell 18% lost 62% of its purchasing power.
  26. Slide 26 / 26

    Where this goes next

    Read the full lesson, with runnable code Back to the lesson