Stop screenshotting df.head() — sample() gives you a random, honest slice of the data, reproducible with random_state.
Every screenshot of df.head() on this planet is the same five rows — and pandas has had a random, honest alternative since 2015.
sample() draws random items from an axis: df.sample(n=3) for an exact row count, df.sample(frac=0.25) for a fraction, and replace=True when you need more draws than rows exist (bootstrap-style). weights= biases the draw — give rare categories a bigger multiplier to oversample them — and random_state= an integer pins the draw, so the same call returns the same rows every time. It lives on NDFrame, so DataFrames and Series share the API; rows are the default axis, pass axis=1 for columns.
Most reporting is deterministic, but the QA loop around it is not: "is this row broken?", "show me some examples of the bad segment", "give me a demo dataset for the screenshot". head() answers none of those honestly — it always shows the first rows, which are rarely representative and often one sorted cluster. sample() gives an honest random slice in one call, and because random_state pins it, the demo still runs tomorrow: same rows, same screenshot, same bug. It also replaces the manual shuffle step before a hand-rolled train/holdout split, and since pandas 1.4 you can sample per group: df.groupby('store').sample(n=1) pulls one row per store — stratified sampling without sklearn.
tickets = pd.DataFrame({
"ticket_id": ["TK-2201", "TK-2202", "TK-2203", "TK-2204", "TK-2205", "TK-2206", "TK-2207"],
"city": ["Rotterdam", "Utrecht", "Eindhoven", "Groningen", "Arnhem", "Maastricht", "Utrecht"],
"issue": ["login", "billing", "login", "sync", "billing", "login", "sync"],
"agent": ["Femke", "Jonas", "Femke", "Priya", "Jonas", "Priya", "Femke"],
"minutes": [12, 25, 9, 41, 33, 18, 27],
})
tickets.sample(n=3, random_state=42)ticket_id city issue agent minutes 0 TK-2201 Rotterdam login Femke 12 1 TK-2202 Utrecht billing Jonas 25 5 TK-2206 Maastricht login Priya 18
The daily QA move: three random tickets instead of the same first three rows. random_state=42 makes the draw reproducible — rerun tomorrow, same tickets.
tickets.sample(frac=0.25, random_state=7)
ticket_id city issue agent minutes 2 TK-2203 Eindhoven login Femke 9 5 TK-2206 Maastricht login Priya 18
frac= picks a fraction, not a count: 25% of 7 rows = 1.75, which pandas rounds to 2 rows. Size must come from exactly one of n or frac — passing both raises ValueError.
w = tickets["issue"].map({"login": 3.0, "billing": 1.0, "sync": 1.0})
tickets.sample(n=4, weights=w, random_state=99)ticket_id city issue agent minutes 4 TK-2205 Arnhem billing Jonas 33 2 TK-2203 Eindhoven login Femke 9 5 TK-2206 Maastricht login Priya 18 0 TK-2201 Rotterdam login Femke 12
Weighted sampling: mapping 'login' to 3.0 oversamples the problem segment — three of the four draws are login tickets. Weights are normalized automatically; NaN weights count as zero.
tickets.sample(n=5, replace=True, random_state=11)
ticket_id city issue agent minutes 1 TK-2202 Utrecht billing Jonas 25 0 TK-2201 Rotterdam login Femke 12 3 TK-2204 Groningen sync Priya 41 1 TK-2202 Utrecht billing Jonas 25 5 TK-2206 Maastricht login Priya 18
replace=True samples with replacement: TK-2202 appears twice. The bootstrap move — and the only way to draw more rows than the frame has.
train = tickets.sample(frac=0.8, random_state=42) test = tickets.drop(train.index) len(train), len(test)
(6, 1)
The train/holdout idiom: sample the split, then drop(train.index) for the complement. Pinned random_state means the split is identical on every machine.
tickets["city"].sample(n=3, random_state=0)
6 Utrecht 2 Eindhoven 1 Utrecht Name: city, dtype: str
Series have sample() too — it draws values, not rows. Same name, same parameters, same random_state semantics.
| Flag | Meaning |
|---|---|
df.sample(n=3) | exact row count; pass n or frac, never both (ValueError otherwise) |
df.sample(frac=0.25) | fraction of the axis; 0.5 on an even-length frame is the shuffle-and-halve move |
replace=True | sample with replacement — repeated rows allowed, and draws can exceed len(df) |
weights=df["col"] | per-row draw probability: a column name (DataFrame) or a Series/list; normalized automatically, NaN = zero chance |
random_state=42 | an int seed pins the draw — same call, same rows, every machine, every run |
axis=1 | sample columns instead of rows — handy for wide frames; axis also accepts 'columns' |
ignore_index=True | reset the result index to 0..n-1 instead of keeping the source row labels |
The method was requested by Wes McKinney himself in December 2012 (issue GH 2419, 'Series/DataFrame sample method with/without replacement') and shipped two and a half years later in 0.16.1: 'Series, DataFrames, and Panels now have a new method: sample()'. The changelog advertised exactly the four knobs it still has — count or fraction, with or without replacement, weights, and seed values 'to facilitate replication'.
sample() is defined once on NDFrame, the common base of DataFrame and Series — which is why both share identical semantics, including the 'stat axis' default (rows for a DataFrame). It never supported Panels' decline quietly: when Panels were removed in 0.25 (2019), sample() simply carried on with two-dimensional objects. The big ergonomics bump came in pandas 1.4.0 (January 2022), which added GroupBy.sample() for per-group draws and ignore_index= for clean output labels.
sample() is a thin, honest wrapper. It resolves n from frac (n = round(ax_length * frac)), builds a RandomState from your seed — an int seeds numpy's legacy Mersenne-Twister RandomState, which is why random_state=42 is stable across pandas versions and machines — and then calls rs.choice(axis_length, size=n, replace=replace) (weights route through choice's probability vector, normalized, NaN treated as zero). The returned integer positions are selected with iloc-style takes, so output order is draw order, not source order. That's the entire trick: the reproducibility contract comes from numpy's RandomState stream, not from pandas itself — which is also why the same seed gives different draws on a numpy 1.x vs 2.x default_rng-based path if you pass a Generator instead of an int.