pandas sample() — draw random rows for tests, demos and quick QA

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.

What it does

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.

Why it matters

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.

Examples

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.

Flags

FlagMeaning
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=Truesample 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=42an int seed pins the draw — same call, same rows, every machine, every run
axis=1sample columns instead of rows — handy for wide frames; axis also accepts 'columns'
ignore_index=Truereset the result index to 0..n-1 instead of keeping the source row labels

sample() arrived in pandas 0.16.1 (May 11, 2015)

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'.

Design note: one method, every axis, and a 1.4 upgrade

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.

Under the hood: one randint, a take, and your seed

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.

Fun facts

Pros

Cons

Takeaways