This section documents everything done in Python before moving the cleaned data into SQL Server and then Power BI.
In Python, the goal is to take the raw Spaceship Titanic dataset and produce a clean, analysis-ready CSV with:
- Missing values handled consistently
- Duplicates removed
- Useful engineered features added (spending + cabin/group features)
- Correct data types prepared for SQL storage
Tools
- Jupyter Notebook (or VS Code notebooks)
Libraries used
pandas(data loading + cleaning)matplotlib/seaborn(EDA visuals)
Input: spaceship_titanic_dataset.csv
Steps:
- Read the CSV into a DataFrame.
- Preview rows to confirm the dataset loaded correctly.
What to check immediately:
- Column names
- Data types
- Quick sample of values
The notebook runs the basic “health check” steps:
-
df.info()→ data types + non-null counts -
df.describe()→ statistics for numeric columns -
Missing values count per column:
df.isna().sum()
Also checks if there are completely empty rows:
-
Remove rows where all columns are null:
df.dropna(how="all")
Why this matters: it prevents “fake rows” from breaking summaries, joins, and counts later.
A clear rule was applied per column type:
Columns:
HomePlanet,Cabin,Destination,VIP
Rule:
- Replace missing with
mode()[0]
Why: mode keeps the most common category and avoids creating a new “Unknown” bucket for core analysis columns.
Namemissing →"UnKnown"
Why: Name isn’t used for analysis, but you still want a non-null value for completeness.
Columns:
RoomService,FoodCourt,ShoppingMall,Spa,VRDeck
Rule:
- Missing spending =
0
Why: In this dataset, spending columns behave like “amount spent”. If it’s missing, treating it as 0 is a reasonable default for analysis and for building TotalSpending.
Perfect — then we must correct the README to reflect what you actually implemented, not what was originally suggested.
Since you filled remaining CryoSleep nulls with:
df["CryoSleep"].fillna(True, inplace=True)CryoSleep was handled using a structured two-step approach:
A passenger in CryoSleep should not generate any spending.
Therefore:
- If a passenger has any spending > 0
- AND
CryoSleepis missing → setCryoSleep = False
This ensures internal consistency between spending behavior and sleep status.
After applying the logical rule:
- All remaining missing values in
CryoSleepwere filled withTrue.
df["CryoSleep"].fillna(True, inplace=True)- Passengers without spending are logically more likely to be in CryoSleep.
- The dataset contains many zero-spending passengers.
- This assumption preserves logical coherence with the business rule.
- It avoids introducing statistical noise through random imputation.
These plots were created to make sure the cleaning choices make sense:
- Histograms for each spending column
- X-axis capped at the 99th percentile to avoid extreme outliers hiding the shape
Why: spending columns are heavily skewed; the cap helps you “see” the distribution.
Why: validates the median-imputation choice and checks if ages look realistic.
Why: quickly highlights outliers and spread across all spending columns.
df.drop_duplicates(inplace=True)- Then checks duplicates count again.
Why: duplicates can break counts, group sizes, and also create problems when building keys/constraints in SQL.
These are the engineered columns added to support analysis + modeling:
TotalSpending = sum(RoomService, FoodCourt, ShoppingMall, Spa, VRDeck)
Why: a single KPI that’s easy to analyze in SQL/Power BI and useful for segmentation.
From Cabin formatted like Deck/Number/Side:
DeckCabinNumSide
Why: cabin location is more useful when split into structured attributes.
From PassengerId formatted like Group_PassengerNumber:
- Extract
Group - Extract
Group_Number
Then:
Group_Size = count passengers per GroupIs_Solo = (Group_Size == 1)
Why: group behavior is a strong driver in this dataset (solo vs with family/friends).
Boolean columns converted to integers (0/1) for easier storage and querying in SQL Server:
CryoSleep,Transported,Is_Solo,VIP→astype(int)
Why: makes BULK INSERT + constraints + aggregations simpler in SQL.
Output file created:
spaceship_cleaned_dataset.csv
Why: this is the dataset that should be used for:
- SQL table insertion (BULK INSERT)
- Power BI modeling via SQL
This phase takes the cleaned CSV from Python and builds a structured SQL Server database that supports clean querying and Power BI modeling.
- Store the cleaned dataset in a normalized relational design
- Enforce data integrity (keys, uniqueness, foreign keys)
- Prepare the data for fast analysis and Power BI reporting
Input: spaceship_cleaned_dataset.csv (from Phase 1)
A dedicated database was created to isolate the project:
CREATE DATABASE SpaceShipUSE SpaceShip
Why: keeps the project clean, avoids mixing with other practice databases.
Instead of keeping everything in one wide table, the data was split into logical entities:
Stores passenger-level attributes:
PassengerId(unique business identifier)HomePlanet,Age,VIP- Group attributes:
GroupNo,Group_Size
Primary key:
id INT IDENTITY(1,1) PRIMARY KEY
Why:
- Identity
idis a stable surrogate key for joins. PassengerIdis enforced asUNIQUEto prevent duplicates.
Cabin values were separated into their own table to avoid repeating cabin text fields for every row.
Stores:
Cabin(full string)Deck,CabinNumber,Side
Key rule that fixed the earlier FK issue:
CONSTRAINT UQ_Cabin_Cabin UNIQUE (Cabin)
Why this matters:
- If
Cabinrepeats and you try to reference it like a dimension, you must ensure the referenced field is unique. - The unique constraint guarantees one cabin row per cabin code.
This holds the “fact-like” trip record:
-
Foreign keys:
PassengerID→Passenger(id)CabinID→Cabin(id)
-
Outcome:
Transported
-
Behavior flags:
IsSolo,CryoSleep
-
Spending:
RoomService,FoodCourt,ShoppingMall,Spa,VRDeckTotalSpending
Why:
- This structure makes it easy to analyze behavior + outcome while keeping passenger and cabin clean and reusable.
Because the cleaned data is in a single CSV, the common professional approach is:
Create a StagingSpaceship table matching the CSV columns exactly (all as nullable initially).
Then:
BULK INSERTinto staging
Why staging:
- It’s safer and avoids breaking constraints during initial load.
- Insert distinct passengers into
Passenger - Insert distinct cabins into
Cabin(ensuring uniqueness)
Join staging to Passenger + Cabin to get:
Passenger(id)andCabin(id)Then insert the fact rows.
These checks confirm the SQL database matches the cleaned file logically.
- Total rows in staging vs final fact table
- Should match (or match after intentional filtering)
Examples:
SELECT COUNT(*) FROM StagingSpaceship;SELECT COUNT(*) FROM SpaceShip;
Ensure business keys aren’t duplicated:
-
PassengerId duplicates:
SELECT PassengerId, COUNT(*) FROM Passenger GROUP BY PassengerId HAVING COUNT(*) > 1;
-
Cabin duplicates (should be blocked by constraint):
SELECT Cabin, COUNT(*) FROM Cabin GROUP BY Cabin HAVING COUNT(*) > 1;
Confirm there are no orphan facts:
-
SpaceShiprows with missing passenger link:SELECT * FROM SpaceShip WHERE PassengerID IS NULL;
-
SpaceShiprows with missing cabin link:SELECT * FROM SpaceShip WHERE CabinID IS NULL;
-
Negative spending should not exist:
SELECT * FROM SpaceShip WHERE RoomService < 0 OR FoodCourt < 0 OR ShoppingMall < 0 OR Spa < 0 OR VRDeck < 0;
-
Total spending consistency:
SELECT * FROM SpaceShip WHERE TotalSpending <> (RoomService + FoodCourt + ShoppingMall + Spa + VRDeck);
-
Clean relationships (Passenger ↔ SpaceShip ↔ Cabin)
-
Less duplication → smaller model size
-
Faster slicing by:
- Deck / Side (Cabin)
- HomePlanet / VIP / Age groups (Passenger)
- Spending + Transported (SpaceShip)
The Power BI model follows a Star Schema architecture:
-
Fact Table (Center):
Spaceship -
Dimension Tables:
PassengerCabin
This structure ensures:
- Clean filtering behavior
- No circular relationships
- Optimized performance
- Clear separation between descriptive attributes and measurable facts
| From Table | To Table | Relationship | Type |
|---|---|---|---|
| Passenger | Spaceship | Passenger.id → PassengerID | 1 → * |
| Cabin | Spaceship | Cabin.id → CabinID | 1 → * |
✔ Spaceship contains the foreign keys:
PassengerID(FK → Passenger.id)CabinID(FK → Cabin.id)
These same foreign keys were enforced in SQL Server using FOREIGN KEY constraints.
Power BI relationships mirror the database integrity rules.
This confirms:
- One-to-many relationships
- Fact table in the center
- Dimension tables on the sides
- Correct filter direction flowing from dimensions to fact
Chart Type: Stacked Bar Axis: VIP (0 / 1) Values:
- Average of ShoppingMall
- Average of RoomService
- Average of Spa
- Average of VRDeck
- Average of FoodCourt
Business Insight:
- VIP passengers spend significantly more across all categories.
- FoodCourt and VRDeck dominate VIP spending.
Chart Type: Pie Chart Legend: Destination Values: Count of Passengers
Business Insight:
- TRAPPIST-1e dominates passenger distribution.
- 55 Cancri e and PSO J318.5-22 represent much smaller proportions.
Slicers Added:
- HomePlanet
- CryoSleep (0 / 1)
- VIP (0 / 1)
Purpose:
- Allow dynamic segmentation.
- Enable interactive filtering across all report visuals.
- Provide business-style filtering scenarios.
Chart Type: Pie Chart Values:
- Sum of FoodCourt
- Sum of RoomService
- Sum of ShoppingMall
- Sum of Spa
- Sum of VRDeck
Business Insight:
- FoodCourt contributes the highest share of revenue.
- Spa and VRDeck are also major contributors.
- ShoppingMall generates the lowest total spending.
Chart Type: Line Chart Axis: Group_Size Values: Survival Rate % (Transported ratio)
Business Insight:
- Survival probability peaks around medium group sizes.
- Very small and very large groups show lower survival rates.
- Confirms that engineered feature
Group_Sizeprovides analytical value.
Chart Type: Dual Axis Line Chart Axis: Age Values:
- Sum of TotalSpending
- Sum of Is_Solo
Business Insight:
- Spending peaks around mid-age passengers.
- Solo passengers are concentrated within specific age ranges.
- Demonstrates interaction between demographic and behavioral features.






