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 →

Arbitrary value imputation

Arbitrary value imputation fills the gaps with a number you pick yourself, such as 0 or 100, chosen to sit outside the column's ordinary range so that the filled rows stay recognisable.

Last updated: 12 Oct, 2026 · pandas 3.0

End of distribution imputation computed its fill value from the column. This one is chosen by hand, which makes it the simplest technique here and the one with the most ways to go wrong.

Syntax: Series.fillna(value) with a literal. The whole technique is the choice of that literal, and there are three rules for it: the value must not occur in the data, it should sit at one end of the range, and it is chosen per column, not shared.

python
df['Age_zero'] = df['Age'].fillna(0)        # below every real age
df['Age_hundred'] = df['Age'].fillna(100)   # above every real age
df['Score'] = df['Score'].fillna(-999)      # a value no score can take

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'])

Filling with 0 and with 100

Age runs from 0.42 to 80.0, so 0 sits below everything and 100 above everything. Both are outside the data and neither occurs in it, which is what the technique asks for. describe() on the three versions shows what each choice costs.

ExampleOne column, two arbitrary fill values
print("observed range:", df['Age'].min(), "to", df['Age'].max())
df['Age_zero'] = df['Age'].fillna(0)
df['Age_hundred'] = df['Age'].fillna(100)
print(df[['Age', 'Age_zero', 'Age_hundred']].describe().round(4).to_string())
  • The mean moves in opposite directions. 29.6991 becomes 23.7993 with zeros and 43.6646 with hundreds, from the same data.
  • The standard deviation rises either way. 14.5265 becomes 17.5961 and 30.9304, because both fills push 177 rows away from the centre.
  • The quartiles move too. The median falls to 24.0 with zeros and rises to 32.0 with hundreds, so every summary of this column now depends on a number somebody chose.
  • Neither version is wrong. They answer different questions, and the model only knows what it is given.

Seeing both spikes

The histograms make the choice concrete: the same hump with a bar of 177 rows bolted on at one end or the other.

ExampleThe same 714 ages with 177 rows added at 0 and at 100
df['Age_zero'] = df['Age'].fillna(0)
df['Age_hundred'] = df['Age'].fillna(100)
print("rows sitting at the fill value, zero:", int((df['Age_zero'] == 0).sum()))
print("rows sitting at the fill value, hundred:",
      int((df['Age_hundred'] == 100).sum()))

fig, axes = plt.subplots(1, 3, figsize=(13, 3.6), sharey=True)
for ax, col, colour in zip(axes, ['Age', 'Age_zero', 'Age_hundred'],
                           ['#3a6fd8', '#d64541', '#e08a1e']):
    df[col].hist(bins=50, ax=ax, color=colour)
    ax.set_title(col)
    ax.set_xlabel('years')
fig.savefig('chart.svg')
Three histograms sharing a vertical scale. The first, of the recorded ages, is a hump between 0 and 80. The second has the same hump with a red bar of 177 rows at 0. The third has the same hump with an orange bar of 177 rows at 100, standing clear of the data on the right.

Choosing the value

  • It must not occur in the data. Filling an age with 0 is safe here because no passenger is recorded as 0; the youngest is 0.42. On a column of counts, 0 is a real value and the fill would be invisible.
  • It should sit at one end of the range. A fill in the middle hides among real values and nothing downstream can separate it.
  • -999 is the common choice for a column that is never negative, for the same reason, and it is as arbitrary as any other.
  • Pick it per column. 100 is far out for an age and ordinary for a fare.

Arbitrary vs end of distribution imputation

Arbitrary valueEnd of distribution
Where the value comes fromyou choose itmean plus three standard deviations
Depends on the datanoyes, it is recomputed per column
Works on a train and test splitthe same literal both sidesthe value must be learned on train and reused on test
Riskpicking a value that occurs in the dataa fence that the data never reaches
Readable afterwardsyes, 0 and -999 are obviousless so, 73.2786 looks like data

Where an arbitrary value is used

  • Competition style tabular work. Fast, explicit, and a tree model separates the fill value in one split.
  • Codes that already have a sentinel. Some sources write -1 or 9999 for unknown, and matching their convention keeps the column consistent.
  • A column you plan to bin. The fill value becomes its own bin, which is the categorical answer in Missing as its own category.
Watch out. An arbitrary fill value is a real number to everything that reads the column afterwards. A mean, a correlation, a scaler and a linear model all take -999 literally, and the damage grows with the share of rows filled. Trees handle it; anything that computes with the value does not.
Try it yourself
  • Fill with 29.0, a value that does occur in the data, and try to find the filled rows afterwards.
  • Compare df['Age_zero'].corr(df['Survived']) with the correlation of the median filled column.
  • Fill Fare with -999 and read what it does to describe().

Slow is fine. Stopping is the only problem.