EDA and Feature Engineeringpandas 3.0 · scikit-learn 1.9 · seaborn 0.13 · Python 3.12+
Dashboard
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
72 small wins to finish your pathNext lesson →

Random sample imputation

Random sample imputation fills each missing value with a value drawn at random from the same column's recorded values, so the filled column keeps the shape and the spread the original had.

Last updated: 12 Oct, 2026 · pandas 3.0

Mean and median imputation filled 177 ages with one number and the standard deviation fell from 14.526497 to 13.019697. Drawing 177 different numbers from the column itself is the fix, and it takes three lines, one of which is the trick.

The three steps of random sample imputation. First, sample as many values as there are gaps from the recorded part of the column, which gives a small Series carrying the index positions it was drawn from. Second, replace that Series' index with the index of the rows that are missing, because pandas aligns on the index when it assigns. Third, assign the Series into the missing rows with df.loc, so each gap receives one drawn value.

Syntax: Series.dropna().sample(n, random_state=0) draws n recorded values, sample.index = ... relabels them with the rows that need filling, and df.loc[mask, 'col'] = sample writes them in. random_state is what makes the result the same on every run.

python
missing = df['Age'].isnull().sum()               # how many gaps to fill
pool = df['Age'].dropna()                        # draw only from real values
sample = pool.sample(missing, random_state=0)    # a Series of 177 ages
sample.index = df[df['Age'].isnull()].index      # relabel: pandas aligns on index
df.loc[df['Age'].isnull(), 'Age'] = sample       # one drawn value per gap

Loading the Titanic data

The Titanic table is 891 passengers. Age is missing for 177 of them, Cabin for 687 and Embarked for 2, which makes it the right table to learn these techniques on. It loads from its GitHub URL, so there is nothing to download. Run this block first, then the example under it.

python
import pandas as pd
import matplotlib
matplotlib.use('svg')
import matplotlib.pyplot as plt

url = ("https://raw.githubusercontent.com/krishnaik06/"
       "Feature-Engineering-Live-sessions/master/titanic.csv")
df = pd.read_csv(url, usecols=['Survived', 'Age', 'Fare'])

Why the index has to be replaced

sample() returns the drawn rows with their own index, the positions it took them from. Assigning that Series into other rows would align on those labels and fill almost nothing. Overwriting the index with the labels of the rows that are missing is what lines the two up, and it is the step that is easy to leave out.

ExampleThe same four values, relabelled onto the rows that need them
missing = int(df['Age'].isnull().sum())
sample = df['Age'].dropna().sample(missing, random_state=0)
print("values drawn:", len(sample))
print(sample.head(4).to_string())
print("rows that need one:", df[df['Age'].isnull()].index[:4].tolist())

sample.index = df[df['Age'].isnull()].index
print("after relabelling:")
print(sample.head(4).to_string())
  • 177 values are drawn, one per gap. dropna() first means the pool holds no NaN, so a gap can never be filled with another gap.
  • The drawn values carry labels 423, 177, 305 and 292. Those are the passengers the values came from, not the passengers that need filling.
  • After the relabel the same values carry 5, 17, 19 and 26, which are the first four rows with no age. The values did not change; only their labels did.
  • Without the relabel the fill silently misses. Only the few rows whose labels appear in both would receive a value.

Keeping the shape the column had

Drawing from the column's own values means the filled rows follow the same distribution as the recorded ones. The standard deviation barely moves, and a density curve of the result sits on top of the original.

ExampleThe two techniques against the original
df['Age_median'] = df['Age'].fillna(df['Age'].median())
sample = df['Age'].dropna().sample(int(df['Age'].isnull().sum()), random_state=0)
sample.index = df[df['Age'].isnull()].index
df['Age_random'] = df['Age']
df.loc[df['Age'].isnull(), 'Age_random'] = sample
print(df[['Age', 'Age_median', 'Age_random']].std().round(6).to_string())

fig, ax = plt.subplots(figsize=(7, 4))
df['Age'].plot(kind='kde', ax=ax, label='Age, the 714 recorded')
df['Age_median'].plot(kind='kde', ax=ax, color='red', label='median imputation')
df['Age_random'].plot(kind='kde', ax=ax, color='green',
                      label='random sample imputation')
ax.set_xlabel('Age')
ax.legend()
ax.set_title('Three curves: original, median filled, random sample filled')
fig.savefig('chart.svg')
Three density curves of passenger age. The blue original and the green random sample imputation lie almost on top of each other across the whole range, while the red median imputation curve rises into a tall narrow spike at 28.

Reading the three standard deviations

  • 14.526497 before, 14.563654 after random sample imputation. The spread is kept; the small rise is the randomness of a 177 value draw.
  • 13.019697 after median imputation. Filling with one number cost about 10 percent of the spread, and this technique costs none of it.
  • The green curve hides the blue one. Nothing downstream that reads the shape of this column can tell that 177 values were invented.
  • Which is also the weakness. Nothing records which rows were filled, so a model cannot learn from the fact that an age was absent.

Random sample vs median imputation

Median imputationRandom sample imputation
Fill valueone number for every gapa different drawn value per gap
Spread after the fillshrinkskept
Shape after the filla spike at the fill valueunchanged
Reproducibleyes, alwaysonly with random_state
Needs the column to haveenough values for a medianenough values to draw from
Keeps the missingness signalnono, unless you add an indicator

Where random sample imputation is worth the extra lines

  • A column whose distribution matters downstream. Anything that will be transformed, binned or used to fit a distribution.
  • A column with a large share missing where the shape must survive. One fill value over 20 percent of a column leaves a spike that no later step can undo.
  • Alongside an indicator column. The pair keeps both the distribution and the fact that a value was absent, which Capturing NaN with an indicator column covers.
Watch out. Without random_state every run fills different values, so a model trained today cannot be reproduced tomorrow, and a difference in its score could be the fill rather than the change you made. Pass the seed, and never rely on a global np.random.seed, which any extra cell can disturb.
Try it yourself
  • Change random_state=0 to random_state=1 and read how far the standard deviation moves.
  • Remove the sample.index = ... line and count how many nulls are left after the assignment.
  • Drop the dropna() from the pool and check whether any gap is filled with a NaN.

Every expert started right here.