Portable pandas and Python idioms
Analysis code travels between machines. The syntax that older interpreters reject, the pandas calls whose behaviour has moved between releases, the forms that behave the same everywhere, and how to check what an environment has before trusting an answer found online.
A notebook is written on one machine and run on another: a shared server, a colleague’s laptop, a container built two years ago. The cheapest way to make it survive that trip is to write to the subset of Python and pandas that behaves the same across releases, and to avoid the newest syntax and the calls whose behaviour has moved. Every snippet on this site is written that way and executed in a pinned environment before it is published; nothing here needs anything beyond pandas, numpy, scikit-learn and matplotlib.
Syntax that older interpreters reject
| Newer form | Portable form |
|---|---|
match / case | if / elif |
list[int], dict[str, float] as annotations | from typing import List, Dict |
X | None in annotations | Optional[X] from typing |
dict1 | dict2 | {**dict1, **dict2} |
str.removeprefix / removesuffix | slicing with a check |
Parenthesised context managers with (a as x, b as y): | one with per line, or nested |
zip(..., strict=True) | check lengths first |
Safe everywhere this site cares about: f-strings, the walrus operator :=, positional-only parameters, functools.cached_property, math.prod.
# newer syntax: fails to parse on an older interpreter
def summarise(xs: list[float]) -> dict[str, float] | None: ...
merged = defaults | overrides
match kind:
case "daily": ...
from typing import List, Dict, Optional
def summarise(xs: List[float]) -> Optional[Dict[str, float]]:
if not xs:
return None
return {"n": float(len(xs)), "mean": sum(xs) / len(xs)}
defaults = {"window": 20, "min_periods": 10}
overrides = {"window": 60}
merged = {**defaults, **overrides}
print(summarise([1.0, 2.0, 3.0]), merged)
pandas calls whose behaviour has moved
| Avoid | Because | Write instead |
|---|---|---|
groupby(...).apply(f, include_groups=False) | the argument exists only in recent releases | select columns before apply: g[["a", "b"]].apply(f) |
DataFrame.map(f) | recent addition; older releases have only applymap | DataFrame.applymap(f), or df[col].map(f) per column |
groupby.rolling(...) assigned back to the frame | the result’s index shape has changed between releases | g[col].transform(lambda x: x.rolling(n).mean()) |
Series.rolling(step=...) | recent addition | slice after rolling: .rolling(n).mean()[::step] |
pd.NA-typed nullable columns as the default | behaviour still shifting | stay with float64 and np.nan for numeric work |
df.groupby(...).apply(f) where f uses the grouping column | deprecated, later an error | pass only the needed columns, or use .transform |
read_csv(..., dtype_backend="pyarrow") | recent, and needs pyarrow | the default numpy backend |
OneHotEncoder(sparse_output=...) in scikit-learn | the argument was renamed between releases | pd.get_dummies on the training frame, then reindex the test frame to the training columns |
The transform-with-rolling pattern is the one that matters most and it is used on every page here; see rolling windows per group. The get_dummies + reindex pattern is the portable way to one-hot encode with unseen categories handled explicitly; see categorical features and unseen levels.
Check what you have
import sys, pandas as pd, numpy as np
print(sys.version.split()[0], "| pandas", pd.__version__, "| numpy", np.__version__)
This line goes at the top of the notebook. When an answer found online looks version-specific, compare it against this line before trusting it; see looking things up.
Adding a package
Install only what the analysis needs, and pin to a release that supports the interpreter in front of you, because pip will otherwise pick the newest wheel and fail to build it. pip index versions <package> lists what is installable. Write the base-stack version of the analysis first, so that if the install fails nothing is lost; the optional-library version is a bonus, labelled as such. See OLS from first principles for the pattern.
Charts
plt.savefig("fig.png", dpi=120) then insert the PNG into the slide. Plain matplotlib does everything on this site. See charts as PNGs for slides.