cassionData Analysis

Back to the lessonLesson 5 of 8Working the table

Selecting and filtering, and the warning that became an error

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 / 35

    What this lesson covers

    • Three ways to select, and when each is right
    • Boolean masks
    • The warning that became an error
    • Adding and changing columns
    • Sorting, and why it is not a ranking
    • What comes next
    Speaker notes
    loc, iloc and boolean masks used deliberately — plus what SettingWithCopyWarning was warning about, why pandas 3 replaced it with ChainedAssignmentError, and what that changes for code you already have.
  2. Slide 2 / 35

    Three ways to select, and when each is right — In Python

    import pandas as pd
    
    muac = read_register(RAW / "muac-screening-artibonite-2024.v1.csv")
  3. Slide 3 / 35

    Three ways to select, and when each is right

    • By label, with loc — Rows by index value, columns by name
    Speaker notes
    By label, with loc. Rows by index value, columns by name. This is the one to reach for by default, because it says what it means.
  4. Slide 4 / 35

    Three ways to select, and when each is right — In Python

    muac.loc[muac["commune"] == "Gonaives", ["child_id", "muac_mm", "outcome"]]
  5. Slide 5 / 35

    Three ways to select, and when each is right

    • By position, with iloc — Rows and columns by integer position
    Speaker notes
    By position, with iloc. Rows and columns by integer position.
  6. Slide 6 / 35

    Three ways to select, and when each is right — In Python

    muac.iloc[0]           # first row
    muac.iloc[:5, :3]      # first five rows, first three columns
  7. Slide 7 / 35

    Three ways to select, and when each is right

    • A single column — is a Series; a list of columns is a DataFrame:
    Speaker notes
    iloc is for looking, not for logic. Position depends on the sort order of the file, and a script that says iloc[:, 3] breaks silently the day the export gains a column. Reserve it for inspection and for genuinely positional work. A single column is a Series; a list of columns is a DataFrame:
  8. Slide 8 / 35

    Three ways to select, and when each is right — In Python

    muac["muac_mm"]              # Series
    muac[["muac_mm"]]            # DataFrame with one column
    muac[["commune", "muac_mm"]] # DataFrame with two
    Speaker notes
    The double bracket is not a stylistic choice. Some functions need a DataFrame, and a Series will fail at a distance from where the mistake was made.
  9. Slide 9 / 35

    Boolean masks — In Python

    severe = muac["muac_mm"] < 115
    print(severe.sum())              # how many
    print(severe.mean())             # what share
    muac.loc[severe]
    Speaker notes
    A mask is a Series of True/False the same length as the frame.
  10. Slide 10 / 35

    Boolean masks

    • Combining conditions
    Speaker notes
    .sum() and .mean() on a boolean mask are the count and the proportion. That is the fastest way to answer "how many" and "what share" without a groupby.
  11. Slide 11 / 35

    Boolean masks — In Python

    # Parentheses are mandatory: & binds tighter than <
    under_five = (muac["age_months"] < 60) & (muac["muac_mm"] < 125)
    
    # Use & | ~ — not and / or / not, which cannot operate elementwise
    either = (muac["muac_mm"] < 115) | (muac["oedema"] == True)
  12. Slide 12 / 35

    Boolean masks

    • Missing values are not False
    Speaker notes
    Writing and here raises ValueError: The truth value of a Series is ambiguous. The message is opaque the first time and always means the same thing: you used a scalar operator on a vector. This is the trap that changes a caseload.
  13. Slide 13 / 35

    Boolean masks — In Python

    print(len(muac))                                   # 4218
    print((muac["muac_mm"] < 125).sum())               # counts only measured children
    print(muac["muac_mm"].isna().sum())                # the ones excluded silently
  14. Slide 14 / 35

    Boolean masks — In Python

    measured = muac["muac_mm"].notna()
    gam = measured & (muac["muac_mm"] < 125)
    
    print(f"assessed: {measured.sum()}, GAM cases: {gam.sum()}")
    print(f"GAM rate: {gam.sum() / measured.sum():.3f}")
    Speaker notes
    A comparison against NA yields NA, and NA is not selected. So a filter quietly drops every unmeasured child — which is correct if you meant "children measured below 125 mm" and wrong if you meant "children not known to be above 125 mm". Be explicit about which you meant:
  15. Slide 15 / 35

    Boolean masks

    • The denominator is the point — Dividing by len(muac) instead of measured.sum() reports a lower prevalence for a…
    • isin, between, query
    Speaker notes
    The denominator is the point. Dividing by len(muac) instead of measured.sum() reports a lower prevalence for a reason that has nothing to do with nutrition.
  16. Slide 16 / 35

    Boolean masks — In Python

    north = muac["commune"].isin(["Gonaives", "Terre-Neuve", "Anse-Rouge"])
    plausible = muac["muac_mm"].between(80, 220)
  17. Slide 17 / 35

    Boolean masks — In Python

    impossible = muac["muac_mm"].notna() & ~muac["muac_mm"].between(80, 220)
    Speaker notes
    between is inclusive on both ends by default, and — the important part — it returns False for NA. So ~muac["muac_mm"].between(80, 220) counts a missing measurement as an impossible one. Those are different failures and only one of them is a data-entry error:
  18. Slide 18 / 35

    Boolean masks — In Python

    muac.query("age_months < 60 and muac_mm < 125")
    Speaker notes
    query reads well for long conditions and is worth knowing: It is slower and it hides typos in a string, so prefer masks in a script that must fail loudly.
  19. Slide 19 / 35

    The warning that became an error — Example

    SettingWithCopyWarning:
    A value is trying to be set on a copy of a slice from a DataFrame.
    Try using .loc[row_indexer, col_indexer] = value instead
    Speaker notes
    Older pandas produced a warning that generations of analysts learned to silence:
  20. Slide 20 / 35

    The warning that became an error — In Python

    muac[muac["commune"] == "Gonaives"]["muac_mm"] = 0
    Speaker notes
    It was warning about chained assignment — selecting, then assigning to the selection:
  21. Slide 21 / 35

    The warning that became an error

    • What pandas 3 does instead
    • The behaviour is now defined — A selection never writes back to its parent — not sometimes, never
    • Chained assignment tells you it did nothing
    Speaker notes
    The first bracket produces a new object. Whether the assignment reached the original frame depended on the memory layout, which meant the same line could work on Monday and not on Tuesday. The warning existed because pandas could not tell you which had happened. Copy-on-Write is always on and cannot be disabled. Two things follow. The behaviour is now defined. A selection never writes back to its parent — not sometimes, never. SettingWithCopyWarning no longer exists; pd.errors.SettingWithCopyWarning raises AttributeError. Chained assignment tells you it did nothing.
  22. Slide 22 / 35

    The warning that became an error — In Python

    import pandas as pd
    
    df = pd.DataFrame({"a": [1, 2, 3], "b": [10, 20, 30]})
    df[df["a"] > 1]["b"] = 0
  23. Slide 23 / 35

    The warning that became an error — Example

    ChainedAssignmentError: A value is being set on a copy of a
    DataFrame or Series through chained assignment. Such chained
    assignment never works ...
  24. Slide 24 / 35

    The warning that became an error — In Python

    import warnings
    
    warnings.simplefilter("error", pd.errors.ChainedAssignmentError)
    Speaker notes
    ChainedAssignmentError is a Warning subclass, so by default the line prints and continues. In a script whose numbers matter, promote it:
  25. Slide 25 / 35

    The warning that became an error

    • The correct form, and what changed for existing code
    Speaker notes
    Now the script stops instead of reporting a figure computed from an edit that never landed.
  26. Slide 26 / 35

    The warning that became an error — In Python

    # One .loc, both axes, one operation.
    muac.loc[muac["commune"] == "Gonaives", "muac_mm"] = pd.NA
  27. Slide 27 / 35

    The warning that became an error

    • .copy() and when you still want it
    Speaker notes
    If you are maintaining code written for pandas 1 or 2, the migration risk runs in one direction only: a chained assignment that used to modify the frame now does not. The number changes and nothing crashes. Grep for ][" and for .copy() calls added to silence the old warning — the copies are now unnecessary, and each one you remove is a real saving on a large frame. Under Copy-on-Write, df[mask] is already independent, so a defensive .copy() buys nothing. It is still worth writing when it documents intent:
  28. Slide 28 / 35

    The warning that became an error — In Python

    # This subset is going to be modified and is not a view of anything.
    gonaives = muac.loc[muac["commune"] == "Gonaives"].copy()
    gonaives["flag"] = gonaives["muac_mm"] < 115
    Speaker notes
    Adding a column to a selection you did not copy works and is the common case; the .copy() above is a note to the reader, not a requirement.
  29. Slide 29 / 35

    Adding and changing columns — In Python

    summary = (
        muac
        .loc[muac["muac_mm"].notna()]
        .assign(
            gam=lambda d: d["muac_mm"] < 125,
            sam=lambda d: d["muac_mm"] < 115,
        )
    )
    Speaker notes
    assign returns a new frame and chains, which keeps a pipeline readable:
  30. Slide 30 / 35

    Adding and changing columns — In Python

    import numpy as np
    
    muac["band"] = np.select(
        [muac["muac_mm"] < 115, muac["muac_mm"] < 125],
        ["severe", "moderate"],
        default="normal",
    )
    Speaker notes
    The lambda d: matters: it refers to the frame at that point in the chain, so sam can be defined against columns assign created a line earlier. Referring to muac directly inside the chain would use the pre-filter frame and misalign. For conditions with more than two branches, np.select beats nested where:
  31. Slide 31 / 35

    Adding and changing columns — In Python

    muac.loc[muac["muac_mm"].isna(), "band"] = pd.NA
    Speaker notes
    Conditions are evaluated in order and the first match wins, so < 115 must come first. Note that default also catches missing values, which is almost never what you want — set them back explicitly:
  32. Slide 32 / 35

    Sorting, and why it is not a ranking — In Python

    muac.sort_values(["commune", "screening_date"], ascending=[True, False])
  33. Slide 33 / 35

    Sorting, and why it is not a ranking — In Python

    muac.groupby("commune")["muac_mm"].mean().nsmallest(5)
    Speaker notes
    Sorting arranges rows. It says nothing about whether the differences between adjacent rows are real — a point the Nutrition programme dashboard project makes at length, where nine of twelve communes have overlapping confidence intervals and the sort order is not information. nlargest is the readable form of "top N" and is faster than sorting the whole frame:
  34. Slide 34 / 35

    What comes next

    • You can select a subset and change it without wondering whether the change landed.
    Speaker notes
    You can select a subset and change it without wondering whether the change landed. The next lesson aggregates: groupby, named aggregation, and getting a numerator and a denominator out of the same operation so they cannot drift apart.
  35. Slide 35 / 35

    Where this goes next

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