Concepts / Balancing and Preprocessing the Dataset

Balancing and Preprocessing the Dataset

Undersampling balances a dataset by reducing the majority class until it matches the minority class, using random selection.

  • Programming

Why Imbalance Misleads a Classifier

Before a model can classify anything, the data it trains on has to be prepared. This article follows one preparation pipeline from start to finish: download a spam dataset, load it, balance it, convert its labels to numbers, split it three ways, and save the parts to files. The goal is simple, to end with CSV files for training, validation, and testing, but each stage has to happen in a particular order, and each stage can introduce errors that carry forward.

The problem starts with the class distribution. A classification dataset assigns each row to a class. In a spam filter, every message is labeled either spam or ham, where ham means non-spam. If one class has far more rows than the other, the dataset is imbalanced. The larger group is called the majority class and the smaller one is the minority class.

Imbalance matters because the distribution of examples shapes what a model learns. When the majority class dominates the training data, the model sees the majority pattern far more often than the minority pattern, so its behavior can become biased toward the majority class. Dataset balancing is the process of adjusting the class distribution so that this bias is reduced.

What do you think happens?

A training set has 950 ham rows and 50 spam rows. If a model is trained on this data without any balancing, which class is it likely to see most often, and what could go wrong?

  • It sees spam most often, so it learns to flag everything as spam

  • It sees ham most often, so its predictions can lean toward ham

  • Both classes are seen equally often, so there is no bias

Reveal answer
Answer:

It sees ham most often, so its predictions can lean toward ham.

The majority class dominates the training data, so the model sees the majority pattern far more often than the minority pattern, and its behavior can become biased toward that majority class.

Tracing the Spam and Ham Counts

The balancing logic is packaged as a single function named create_balanced_dataset. It takes a DataFrame called df whose Label column holds the values 'spam' or 'ham', and it returns a new DataFrame called balanced_df. The function is easiest to understand as four decisions made in sequence. First, count how many spam rows exist. Second, randomly select that many ham rows. Third, combine the selected ham rows with all of the spam rows. Fourth, return the combined result.

  1. 1
    Count the spam rows in df, using the Label column. This count is stored and reused.
  2. 2
    Randomly select that many ham rows from df. The ham rows are sampled, not copied in full.
  3. 3
    Combine the selected ham rows with all of the spam rows into one DataFrame.
  4. 4
    Return the combined DataFrame as balanced_df.

Notice what the function does not do. It does not create new spam rows, and it does not generate new ham rows. The balancing operation is called undersampling. You reduce the number of instances in the majority class until it matches the minority class. Undersampling keeps every minority example and keeps only a random subset of the majority examples. It adds no new data.

Ham rowsmajority classSampled ham rowssame count as spamSpam rowsminority classSpam rowsall keptrandom sampleall kept
How does the dataset look before and after undersampling, and which rows are dropped versus kept so both classes end up the same size?

The diagram shows that the balanced dataset is not produced by one operation. It is assembled from two separate filters on the original DataFrame, joined by a sampling step and a concatenation step. The spam count does double duty. It sets how many ham rows are sampled, and every spam row is also carried into the result. Because all spam rows are kept and the ham rows are sampled to the same size, the final label counts match.

Worked example

Tracing a hypothetical balancing run

Suppose df holds 1000 rows, of which 100 are spam and 900 are ham. Trace what create_balanced_dataset returns. These numbers are invented for illustration; the source does not give specific counts.

  1. 1
    Count spam

    The spam count is 100. This is the only number the function needs from the spam side.

  2. 2
    Sample ham

    Randomly select 100 of the 900 ham rows. The other 800 ham rows are not used in balanced_df.

  3. 3
    Keep spam

    All 100 spam rows are kept, not sampled.

  4. 4
    Combine

    Concatenate the 100 sampled ham rows with the 100 spam rows, giving 200 rows.

Result

balanced_df has 200 rows, 100 ham and 100 spam. It is smaller than the original 1000 rows, and 800 ham rows of information are no longer in the data.

Random Sampling and random_state

The step most likely to surprise you is the random sample. The count of ham rows is fixed by the spam count, so the sizes always match. But which ham rows are chosen is random. Without a fixed seed, each run could select a different subset of ham rows. The class counts would be identical from run to run, yet the actual rows inside balanced_df would differ. This is the point where a balanced dataset can diverge from what you expected: the proportions are right, but the specific contents may not be the same.

The sampling call in the listing passes random_state=123. Setting random_state makes the random selection repeatable, so the same input produces the same balanced dataset each time the code runs. This matters when you debug or compare results. If the output changes, you can trust that the cause is a change in your code rather than a different random draw.

SituationSame class counts?Same ham rows chosen?Good for
random_state fixed (123)YesYes, on every runDebugging, comparing models, reproducing a result
random_state not setYesNot guaranteed; may change between runsQuick experiments where the exact rows do not matter

What a fixed random_state does and does not guarantee

From URL to DataFrame

Balancing needs data in memory, so the listing starts by getting the data. The dataset is the SMS spam collection, stored at a fixed URL on the UCI archive. The learner does not search for the file by hand. The URL is stored in a variable, and that variable is passed into a helper function that handles the download. The helper comes from the chapter 6 module, so only its calls appear here, not its internal code.

Fixed URLUCI archiveDownloaded zip filesaved to a pathExtracted data filetab-separatedDataFrameread with sep, header,namescreate_balanced_datasetbalanced_dfLabels mapped to 0and 1ham 0, spam 1random_split0.7 and 0.1Three CSV filestrain, validation, testdownloadunziploadbalancemap labelssplitto_csv
What is the sequence of steps that moves the data from a URL to a downloaded zip file, then to an extracted file, and finally into a DataFrame, and where does balancing fit in?

The helper takes four arguments: the URL, the path where the zip file will be saved, the folder where its contents are extracted, and the path of the data file inside that folder. The last argument is built from the folder path, so the file location is stated once and reused. The helper must run before pandas reads anything, because pandas can only read a file that already exists on disk.

Mapping Labels and Splitting the Data

After the DataFrame is loaded, the listing calls create_balanced_dataset on it, and every later step works from the balanced version. The order is deliberate. Balancing comes first, so that the split receives data that already has the intended class proportions. The source states the balancing and text-label mapping both happen before the split.

Next, the map call replaces each text label with an integer. Ham messages become 0 and spam messages become 1. The label column is the target a classifier learns to predict, so it must be numeric before a model can use it. The map happens after balancing and before splitting, so all three output files inherit the integer labels.

Text labelInteger labelMeaning
ham0Non-spam message
spam1Spam message

Label mapping applied to the Label column before the split

The balanced data is then divided by random_split into three sets: training, validation, and test. The call receives the balanced DataFrame and two fractions, 0.7 and 0.1. The first fraction sets the training share and the second sets the validation share. The test set is whatever remains, which is 0.2, or the remaining 20 percent.

Balanced datasetbalanced_dfTraining set70 percentValidation set10 percentTest setremaining 20 percentfraction 0.7fraction 0.1remainder
How do the rows of the balanced dataset get partitioned into training (70%), validation (10%), and test (remaining 20%) subsets, and how do those parts relate to the whole?

The three returned objects are assigned on one line, in the order train, validation, then test. The assignment order matters because the variable names are the only thing telling a reader which split is which. Later code must use the same order when it refers to the splits.

The last stage writes each split to its own CSV file, using to_csv on the matching DataFrame with a file name. Each call passes index=None, which keeps the DataFrame row index out of the saved file. Each CSV therefore holds only the message and label columns, and the three files store the training, validation, and test data separately.

Worked example

Applying the 0.7 and 0.1 fractions

A balanced dataset has 1000 rows. How many rows go to each of the training, validation, and test sets? The row count is hypothetical and chosen for illustration.

  1. 1
    Training share

    0.7 of 1000 is 700 rows for training.

  2. 2
    Validation share

    0.1 of 1000 is 100 rows for validation.

  3. 3
    Test share

    The remainder is 1000 minus 700 minus 100, which is 200 rows, the remaining 20 percent.

  4. 4
    Check

    700 plus 100 plus 200 equals 1000, so every row is assigned to exactly one set.

Result

Training 700 rows, validation 100 rows, test 200 rows.

Where Preprocessing Goes Wrong

Every stage feeds the next, so a mistake at one stage propagates to all later stages. The pipeline runs download, unzip, load, count, sample, combine, label mapping, and split. An error that enters early, such as a bad file path, can cause a failure that looks like it comes from a later line. Tracing each stage in order helps you find where the problem actually starts.

  • Common mistake

    Reading the file without sep, header=None, and names

    What it looks like

    Calling read on the extracted file with no arguments and expecting two named columns

    Why it fails

    Pandas guesses the layout from the first line, so the message text can end up in the header or be split apart.

    Do this instead

    Pass sep for the tab, header=None for no header row, and names for the two column labels.

  • Common mistake

    Expecting balanced_df to be the same size as the original data

    What it looks like

    Assuming 1000 rows stay 1000 rows after balancing

    Why it fails

    Undersampling discards majority-class rows, so the balanced dataset is smaller and some information is lost.

    Do this instead

    Expect a smaller dataset with equal class counts, and account for the discarded ham rows when interpreting results.

  • Common mistake

    Leaving random_state out and expecting identical rows every run

    What it looks like

    Rerunning the balancing step and comparing the ham rows to last time's output

    Why it fails

    The ham rows are chosen at random, so without a fixed seed each run can select a different subset.

    Do this instead

    Set random_state, such as 123, so the same input produces the same balanced dataset each time.

  • Common mistake

    Mapping labels after the split, or mapping the wrong column

    What it looks like

    Splitting first and then trying to convert the Label column in only one of the three sets

    Why it fails

    The map is meant to happen after balancing and before splitting, so every output file inherits integer labels. Applying it late leaves some sets with text labels.

    Do this instead

    Map the Label column to 0 and 1 on the balanced DataFrame, then split.

  • Common mistake

    Swapping the order of the split outputs

    What it looks like

    Assigning the three returned objects as test, validation, train

    Why it fails

    The variable names are the only signal of which split is which, and later code depends on the same order.

    Do this instead

    Assign the outputs in the order train, validation, test, and use the same order everywhere.

  • Common mistake

    Forgetting index=None when writing CSV files

    What it looks like

    Calling to_csv with only a file name

    Why it fails

    The row index is written as an extra column, which changes what the saved file holds.

    Do this instead

    Pass index=None so each CSV holds only the message and label columns.

Downloadwrong URL or pathLoadwrong sep or namesBalancemissing random_stateLabel mappingmapped at wrong stepSplitoutputs swappedSaved CSV filesextra index columnfile missing or wrongbad columnsaltered class countstext labelswrong contents
At which stage of the pipeline can an error enter, and how does that error propagate to later stages?

Practice: Trace the Pipeline

MEDIUM

Trace a balancing run on a dataset with 60 spam rows and 540 ham rows. Name the rows kept from each class, state the size of balanced_df, and say which stage would change if the random_state argument were removed. Explain which parts of the result would still be reproducible.

Hints
  • The spam count sets how many ham rows are sampled, and all spam rows are kept.
  • The size of balanced_df is the spam count plus the sampled ham count.
  • Removing random_state only affects the random selection, so the class counts stay the same while the chosen ham rows may change.
EASY

A balanced_df has 400 rows. Using fractions 0.7 and 0.1, how many rows land in each of the training, validation, and test sets, and which assignment order must the code use when it receives the three outputs?

Hints
  • Multiply 400 by 0.7, then by 0.1, and take the remainder for the test set.
  • The order is train, validation, then test.

Key Takeaways

  • An imbalanced dataset lets the majority class dominate training, so the model can become biased toward that class.
  • create_balanced_dataset counts the spam rows, randomly samples the same number of ham rows, combines them with all spam rows, and returns balanced_df. Undersampling reduces the majority class and adds no new data.
  • Fixing random_state makes the random ham selection repeatable, so the same input yields the same balanced dataset each time.
  • The pipeline downloads the zip from a fixed URL, unzips it, reads the tab-separated file with sep, header=None, and names, balances it, maps ham to 0 and spam to 1, splits it 0.7, 0.1, and 0.2, and saves three CSV files with index=None.
  • An error at any stage propagates forward, so tracing each stage in order is the way to locate where preprocessing goes wrong.

Related concepts

More concepts starting with B