Lesson 6 - Guided Project: Predicting IPO Listing Gains with TensorFlow

Welcome to Your TensorFlow Guided Project

This lesson is a guided, end-to-end project. Instead of introducing a new concept, you will pull together everything from this module and build a real deep learning classifier from scratch in TensorFlow and Keras. You will load an actual financial dataset, explore it, prepare it, define a Keras Sequential model, train it, and evaluate it honestly, including the uncomfortable but important finding that a neural network does not always win.

By the end of this lesson, you will be able to:

  • Load and explore a real tabular dataset and turn a continuous column into a binary classification target
  • Normalize features and create a train/test split for honest evaluation
  • Build and compile a Keras Sequential model for binary classification
  • Train the model and evaluate it with accuracy, AUC, and a confusion matrix
  • Explain why deep learning can fail to beat a simple baseline, and what would actually help

This project assumes you have completed the earlier lessons in this module and are comfortable with TensorFlow tensors, the Keras Sequential API, activation functions, and the compile/fit/evaluate loop. Let’s begin.


The Problem: Will an IPO List at a Profit?

Imagine you work as a data scientist at an investment firm that is interested in the Indian Initial Public Offering (IPO) market. When a private company first sells its shares to the public, it sets an issue price. On the first day of trading, the share often opens at a different price. The percentage difference between the listing-day price and the issue price is called the listing gain.

Your firm wants a model that answers a simple yes/no question for each upcoming IPO: will it list at a profit? That is a binary classification problem, exactly the kind of task you have been building toward in this module.

This project is the TensorFlow counterpart to the PyTorch IPO project from the deep learning with PyTorch module. The dataset, the target, and the question are identical. Working the same problem in both frameworks is one of the best ways to internalize what is framework-specific (the API calls) versus what is universal (the workflow, the data, and the honest evaluation of results).

Why repeat the same project in two frameworks

Frameworks come and go, but the modeling workflow stays the same. When you build the same model in PyTorch and again in TensorFlow, the parts that change are just syntax. The parts that stay the same, exploring data, choosing a target, normalizing, splitting, training, and evaluating, are the real skills. That is the whole point of seeing this project twice.

The Dataset

You will use the Indian IPO dataset, a record of past IPOs in the Indian market. Each row is one IPO. The columns describe how heavily the offering was subscribed by different investor categories, the size and price of the issue, and the listing-day outcome.

ColumnTypeMeaning
DatedateDate the IPO was listed
IPONametextName of the company
Issue_SizefloatSize of the issue, in INR crores
Subscription_QIBfloatTimes subscribed by Qualified Institutional Buyers
Subscription_HNIfloatTimes subscribed by High Net-worth Individuals
Subscription_RIIfloatTimes subscribed by Retail Individual Investors
Subscription_TotalfloatTotal times the IPO was subscribed
Issue_PricefloatThe price in INR at which shares were issued
Listing_Gains_PercentfloatPercentage gain of the listing price over the issue price

The last column, Listing_Gains_Percent, is continuous, but our question is binary. The first real task of the project is to turn that continuous number into a clean yes/no target.


Step 1: Loading and Exploring the Data

Start by importing the libraries you will use throughout the project and loading the CSV with pandas.

import numpy as np
import pandas as pd
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
from sklearn.model_selection import train_test_split

# download: https://datatweets.com/datasets/indian_ipo.csv
df = pd.read_csv("indian_ipo.csv")

print("Shape:", df.shape)
# Output: Shape: (319, 10)

You have 319 IPOs and 10 columns. That is a small dataset by deep learning standards, and you should already be raising an eyebrow. Neural networks are hungry for data; 319 rows is barely a snack. Keep that fact in your back pocket, because it will explain a lot when you reach the evaluation step.

Take a quick look at what the rows contain.

print(df.head())
print(df.describe()["Listing_Gains_Percent"])

The summary statistics for Listing_Gains_Percent show a wide spread: some IPOs lost value on listing day (negative gains) while others jumped sharply. The mean sits modestly above zero, which already hints that a typical IPO lists at a small profit, but with a lot of variance.


Step 2: Creating the Target Variable

The model needs a binary label, so convert the continuous Listing_Gains_Percent into a 0/1 column. The rule is intuitive: if the listing gain is positive, the IPO listed at a profit (1); otherwise it did not (0).

df["Listing_Gains_Profit"] = (df["Listing_Gains_Percent"] > 0).astype(int)

print(df["Listing_Gains_Profit"].value_counts())
# Output:
# Listing_Gains_Profit
# 1    174
# 0    145
# Name: count, dtype: int64

print("gain rate:", round(df["Listing_Gains_Profit"].mean(), 3))
# Output: gain rate: 0.545

Out of 319 IPOs, 174 listed at a profit and 145 did not. The gain rate is 0.545, meaning about 54.5 percent of IPOs in this dataset listed above their issue price.

That number is more important than it looks. It is your baseline. A model that ignores every input and always predicts “profit” would be correct 54.5 percent of the time on this data. Any model worth deploying must clear that bar comfortably. Write that number on a sticky note; you will compare your neural network against it at the end.

Bar chart showing 174 profitable IPOs versus 145 unprofitable IPOs
The target is close to balanced, so a naive always-profit guess already scores about 54.5 percent.

Always know your baseline

Before you build anything fancy, ask: what is the dumbest possible model, and how well does it do? On a nearly balanced target like this, “always predict the majority class” scores about 54.5 percent. If your deep network lands near that number, it has learned essentially nothing useful, no matter how sophisticated the architecture looks.


Step 3: Selecting Features and Preparing the Data

Not every column is useful for modeling. Date and IPOName are identifiers, not predictors. Listing_Gains_Percent is leakage, it is the answer in continuous form, so it cannot be an input. That leaves the subscription and issue columns as your predictors.

feature_cols = [
    "Issue_Size",
    "Subscription_QIB",
    "Subscription_HNI",
    "Subscription_RII",
    "Subscription_Total",
    "Issue_Price",
]

X = df[feature_cols].values.astype("float32")
y = df["Listing_Gains_Profit"].values.astype("float32")

print("X shape:", X.shape)
print("y shape:", y.shape)
# Output:
# X shape: (319, 6)
# y shape: (319,)

Normalizing the Features

These features live on wildly different scales. Issue_Price might be a few hundred rupees, while Subscription_Total can be a small decimal or a large multiple. Neural networks train far more stably when every input occupies a similar range, so you will normalize each feature to the interval [0,1] [0, 1] using min-max scaling.

The transform applied to each value x x in a column is:

xscaled=xxminxmaxxmin x_{\text{scaled}} = \frac{x - x_{\min}}{x_{\max} - x_{\min}}

where xmin x_{\min} and xmax x_{\max} are the column’s minimum and maximum. You compute those bounds on the training data only, then apply the same transform to the test data, so that no information about the test set leaks into training.

from sklearn.preprocessing import MinMaxScaler

X_train, X_test, y_train, y_test = train_test_split(
    X, y,
    test_size=0.25,      # hold out 25% for honest evaluation
    random_state=42,     # reproducible split
    stratify=y,          # keep the profit/no-profit ratio in both sets
)

scaler = MinMaxScaler()
X_train = scaler.fit_transform(X_train)  # learn min/max on TRAIN
X_test = scaler.transform(X_test)        # apply the SAME transform to TEST

print("Training observations:", X_train.shape[0])
print("Test observations:    ", X_test.shape[0])
# Output:
# Training observations: 239
# Test observations:     80

You now have 239 training rows and 80 test rows. Stratifying on y keeps roughly the same 54/46 split in each set, which matters for a fair comparison against your baseline.

Min-max vs. standardization

Earlier lessons used StandardScaler, which centers each feature at mean 0 and scales to unit variance. Here we use MinMaxScaler, which squeezes everything into [0,1] [0, 1] . Both are valid for neural networks; the choice rarely makes or breaks a model. What matters far more is that you scale at all, and that you fit the scaler on the training set only.


Step 4: Building the Keras Model

With the data ready, you can define the network. You will use the Keras Sequential API, stacking dense (fully connected) layers one after another. For a binary classification problem, the architecture follows a familiar shape: a couple of hidden layers with relu activations to learn nonlinear patterns, then a single output node with a sigmoid activation that squashes its result into a probability between 0 and 1.

tf.random.set_seed(42)

model = keras.Sequential([
    layers.Input(shape=(X_train.shape[1],)),   # 6 input features
    layers.Dense(32, activation="relu"),        # first hidden layer
    layers.Dense(16, activation="relu"),        # second hidden layer
    layers.Dense(1, activation="sigmoid"),      # output: probability of profit
])

model.summary()

The output layer has exactly one node because this is binary classification: the single number it produces is the model’s estimated probability that the IPO will list at a profit. The sigmoid activation guarantees that number lands in [0,1] [0, 1] .

Diagram of a Keras Sequential network: six inputs feeding two hidden ReLU layers and a single sigmoid output
A small Sequential network: six inputs, two hidden ReLU layers, and a single sigmoid output for the profit probability.

Compiling the Model

Before training, you compile the model, which tells Keras three things: which optimizer to use, which loss function to minimize, and which metrics to report.

model.compile(
    optimizer=keras.optimizers.Adam(learning_rate=0.001),
    loss="binary_crossentropy",
    metrics=["accuracy", keras.metrics.AUC(name="auc")],
)

The choices here are the standard recipe for binary classification:

  • Adam is a reliable, adaptive optimizer that works well out of the box.
  • Binary cross-entropy is the natural loss for a single sigmoid output; it penalizes confident wrong predictions heavily.
  • Accuracy is intuitive, and AUC (area under the ROC curve) tells you how well the model ranks profitable IPOs above unprofitable ones, independent of any single decision threshold.

Step 5: Training the Model

Now fit the model to the training data. You hold out a slice of the training set as a validation set so you can watch for overfitting as the epochs go by.

history = model.fit(
    X_train, y_train,
    validation_split=0.2,
    epochs=100,
    batch_size=16,
    verbose=0,
)

print("final train loss:", round(history.history["loss"][-1], 4))
# Output: final train loss: 0.2529

By the final epoch, the training loss has dropped to about 0.2529. On the surface that looks encouraging, the model is clearly fitting the training data. But a low training loss only tells you the network memorized the patterns it was shown. The real question is whether those patterns hold up on IPOs it has never seen.

Training and validation loss curves over epochs, with validation loss flattening while training loss keeps dropping
Training loss keeps falling while validation loss flattens out, a classic sign the model is starting to memorize rather than generalize.

The gap between the two curves is the tell. When training loss keeps dropping but validation loss stalls or rises, the model is fitting noise in the training data that does not generalize. On 239 rows, that happens fast.


Step 6: Evaluating the Model Honestly

This is the moment of truth. Evaluate the trained model on the 80 test IPOs it has never seen.

test_loss, test_acc, test_auc = model.evaluate(X_test, y_test, verbose=0)

print(f"Test accuracy: {test_acc:.3f}")
print(f"Test AUC:      {test_auc:.3f}")
# Output:
# Test accuracy: 0.537
# Test AUC:      0.591

Here are the numbers, and they are sobering. The model reaches a test accuracy of 0.537 and a test AUC of 0.591.

Compare that accuracy to your sticky note. The naive baseline, always predicting “profit,” scores about 0.545. Your carefully constructed two-layer neural network scores 0.537, which is slightly worse than guessing the majority class every time. The AUC of 0.591 is only modestly above 0.5 (pure chance), confirming the model has very little genuine ability to separate profitable from unprofitable IPOs.

This is not a bug in your code. It is an honest, real-world result, and learning to recognize it is one of the most valuable skills in this entire module.

The Confusion Matrix

A single accuracy number hides how the model is wrong. The confusion matrix breaks the 80 test predictions into four buckets, letting you see the pattern of errors.

from sklearn.metrics import confusion_matrix

probs = model.predict(X_test, verbose=0).ravel()
preds = (probs >= 0.5).astype(int)

cm = confusion_matrix(y_test, preds)
print(cm)
# Output:
# [[17 19]
#  [18 26]]

Reading the matrix against the figure below:

  • True Negatives (TN) = 17: unprofitable IPOs correctly flagged as unprofitable.
  • False Positives (FP) = 19: unprofitable IPOs the model wrongly called profitable.
  • False Negatives (FN) = 18: profitable IPOs the model missed.
  • True Positives (TP) = 26: profitable IPOs correctly identified.
Confusion matrix heatmap with TN 17, FP 19, FN 18, TP 26
The test-set confusion matrix: errors are spread almost evenly across all four cells, a hallmark of a model with little predictive signal.

Notice how the errors (19 + 18 = 37) are nearly as numerous as the correct predictions (17 + 26 = 43), and they spread evenly across both classes. A model with real predictive power concentrates its correct answers on the diagonal. This one is scattered, the visual signature of a classifier that is mostly guessing.

You can confirm the accuracy by hand from the matrix: (17+26)/80=43/80=0.5375 (17 + 26) / 80 = 43 / 80 = 0.5375 , which rounds to the 0.537 you saw above.

A low score is a finding, not a failure

It is tempting to keep tweaking layers and epochs until the number creeps up. Resist that urge. When a model barely beats chance on a small, noisy dataset, the honest conclusion is usually that the signal is not there, or there is not enough data to extract it. Reporting that clearly is far more professional than chasing a fragile 0.01 improvement that will not survive contact with new IPOs.


Why Deep Learning Did Not Win Here

It is worth slowing down to understand why a perfectly correct deep learning pipeline produced a near-useless model. There are three intertwined reasons.

The dataset is tiny. Deep networks have many parameters and learn best from thousands or millions of examples. With only 239 training rows, the model has more than enough capacity to memorize the training set (hence the low 0.2529 training loss) but nowhere near enough data to learn patterns that generalize. This is overfitting in its purest form.

The signal may genuinely be weak. IPO listing-day returns are driven by market sentiment, news, and timing, factors that are largely absent from these six columns. If the predictive signal is not present in the features, no architecture can conjure it. A model can only learn what the data contains.

Deep learning is not always the right tool. For small, tabular datasets like this one, simpler methods such as logistic regression or gradient-boosted trees often match or beat a neural network, while being faster to train and easier to interpret. The lesson here is not that TensorFlow failed; it is that matching the tool to the problem matters more than reaching for the most powerful tool available.

What Would Actually Help

If you genuinely needed to push this model further, the highest-leverage moves are about the data, not the architecture:

  • More data. Hundreds more IPOs would give the network something to actually learn from.
  • Better features. Market-level indicators, sector information, sentiment from news around the listing date, or macroeconomic context could add real signal.
  • A simpler model. Try logistic regression or a gradient-boosted tree as a strong, honest baseline before assuming a neural network is the answer.
  • Regularization. Dropout and weight decay would curb the overfitting you saw in the loss curves, though they cannot create signal that is not in the data.

Tuning the learning rate, adding hidden layers, or training for more epochs, the usual first instincts, would do little here, because none of them address the real bottlenecks: too little data and weak features.


Practice Exercises

Now it is your turn. Try these before checking the hints.

Exercise 1: Establish the Baseline in Code

You compared the model against a 0.545 “always predict profit” baseline. Compute that baseline accuracy on the test set directly in code, so you can see exactly what bar the model needs to clear.

import numpy as np
# y_test is the array of true 0/1 labels from the lesson

# Your code here: what accuracy does "always predict 1" get on y_test?

Hint

A model that always predicts 1 is correct exactly when the true label is 1. So its accuracy on the test set is simply np.mean(y_test == 1), the fraction of profitable IPOs in the test set. Compare that to the model’s 0.537 test accuracy.

Exercise 2: Add Dropout to Fight Overfitting

The training curve showed clear overfitting. Add a Dropout layer after each hidden layer to randomly zero out some activations during training, then retrain and re-evaluate. Does the gap between training and test performance shrink?

from tensorflow.keras import layers

# Your code here: rebuild the Sequential model with Dropout(0.3) after each Dense hidden layer

Hint

Insert layers.Dropout(0.3) immediately after each layers.Dense(..., activation="relu") line. Keep the input layer and the final sigmoid output unchanged, then recompile and refit. Dropout often narrows the train/test gap, but on a dataset this small do not expect a dramatic jump in test accuracy, the data ceiling is still the binding constraint.

Exercise 3: Compare Against Logistic Regression

Train a plain LogisticRegression on the same scaled features and compare its test accuracy to your neural network. On small tabular data, a simple linear model is a fair, strong baseline.

from sklearn.linear_model import LogisticRegression

# Your code here: fit LogisticRegression on (X_train, y_train), score on (X_test, y_test)

Hint

Instantiate LogisticRegression(max_iter=1000, random_state=42), call .fit(X_train, y_train), then .score(X_test, y_test). Do not be surprised if it lands in the same low range as the neural network; when the signal is weak, both models hit the same ceiling, which is itself a useful finding.


Summary

Congratulations! You have completed a full deep learning project in TensorFlow, from raw CSV to an honestly evaluated model. Let’s review what you learned.

Key Concepts

The Project Workflow

  • A deep learning project follows the same loop as any ML project: load, explore, prepare, build, train, evaluate
  • Turning a continuous column (Listing_Gains_Percent) into a binary target (Listing_Gains_Profit) is a common first step for classification
  • Always compute a baseline (here, the 0.545 gain rate) before judging a model

Data Preparation in TensorFlow

  • Drop identifier and leakage columns; keep genuine predictors
  • Normalize features to [0,1] [0, 1] with MinMaxScaler, fitting on the training set only
  • Use train_test_split with stratify and a fixed random_state for an honest, reproducible split

Building and Training with Keras

  • The Sequential API stacks Dense layers; relu for hidden layers, a single sigmoid node for binary output
  • compile wires up the optimizer (Adam), loss (binary_crossentropy), and metrics (accuracy, AUC)
  • fit trains the model; a validation_split lets you watch for overfitting

Honest Evaluation

  • The model reached a test accuracy of 0.537 and an AUC of 0.591, barely matching the 0.545 baseline
  • The confusion matrix (TN=17, FP=19, FN=18, TP=26) showed errors spread evenly, the signature of a near-guessing model
  • A low score on a small, noisy dataset is a legitimate finding, not a coding mistake

Why This Matters

The most important takeaway from this project is counterintuitive: building the model correctly is not the same as building a useful model. Your TensorFlow pipeline was sound at every step, yet the result barely beat a coin flip, because the dataset was tiny and the signal was weak.

Real data science is full of moments like this. The professional skill is not making every model succeed, it is recognizing when a model has failed, diagnosing why (here: too little data, weak features, the wrong tool for small tabular data), and communicating that honestly instead of torturing the numbers. You also saw firsthand that this is the same project you could build in PyTorch; the framework was never the deciding factor. Master the workflow and the judgment, and the framework becomes a detail.


Next Steps

You have now built complete deep learning models in TensorFlow, defined, compiled, trained, and evaluated, and you have practiced the harder skill of judging results honestly. The next module shifts from tabular data to images, where deep learning truly shines and where the data scale finally matches the power of the models.

Continue to the Next Module - Computer Vision with CNNs

Move from tabular data to images and learn convolutional neural networks, where deep learning's strengths really show.

Back to Module Overview

Return to the Deep Learning with TensorFlow module overview.


Keep Building Your Skills

You have done something that many tutorials skip: you took a model all the way to an honest evaluation and accepted what the data told you. That discipline, knowing your baseline, watching for overfitting, reading a confusion matrix, and recognizing when deep learning is the wrong tool, will serve you on every project, long after the specific Keras syntax fades from memory. Carry it forward into computer vision, where the data is plentiful and the models finally get to stretch their legs.