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 →

Turning a duration string into minutes

A duration written as 2h 50m becomes one number, 170 minutes, by pulling the hours and the minutes out of the text and adding them together.

Last updated: 12 Oct, 2026 · pandas 3.0 · scikit-learn 1.9 · openpyxl 3.1

Turning a time into hours and minutes split a value with one fixed shape. Duration has two shapes, 2h 50m and 19h, and one row that is only 5m. Each of those breaks a different part of the obvious solution, which makes it the best column in the dataset to learn on.

Parsing a duration, and the cast that fails · from the EDA and Feature Engineering video · 2:30:00 to 2:35:20

A Series is not a string

The first instinct is to call .split() on the column. A Series is a container of values, not one string, so it has no such method. .str is the accessor that applies a string method to every value and hands back a Series of results, and every string step in the chain needs its own .str.

python
import pandas as pd

BASE = ("https://raw.githubusercontent.com/krishnaik06/5-Days-Live-EDA-and-Featur"
        "e-Engineering/main/Flight%20Prediction/")
train = pd.read_excel(BASE + "Data_Train.xlsx")   # 10683 rows, Price included
test = pd.read_excel(BASE + "Test_set.xlsx")      # 2671 rows, no Price
flights = pd.concat([train, test], ignore_index=True)
ExampleWhat a string method does to a Series
print(flights['Duration'].head(4).tolist())
flights['Duration'].split(' ')
A Series drawn as a box holding four duration strings: calling split on it directly gives an AttributeError saying a Series has no attribute split, while calling .str.split gives a Series of lists, and a further .str index takes the first piece of each.

With the accessor, the same call works value by value. .str.split(' ').str[0] keeps the piece before the space, which is 2h, and a second .str.split('h').str[0] cuts the h off it.

python
hours_text = flights['Duration'].str.split(' ').str[0].str.split('h').str[0]
# '2h 50m' -> '2h' -> '2'   and   '19h' -> '19h' -> '19'

The cast that fails on one value

Casting that column to a number is the next obvious step, and it raises. The message names the value that stopped it, which is the fastest way to find the row.

ExampleThe cast, on pandas 3.0.6
hours_text = flights['Duration'].str.split(' ').str[0].str.split('h').str[0]
print("looks fine:", hours_text.head(4).tolist())
hours_text.astype(int)
ExampleFinding the rows with no hour part
odd = flights[~flights['Duration'].str.contains('h')]

print(odd[['Airline', 'Source', 'Destination', 'Duration']].to_string())
  • Two rows hold 5m as the whole value. Both are Mumbai to Hyderabad, where the usual duration in this data is 1h 30m. They are bad data, not a shape the parser has to support.
  • The chain produced '5m' for them, because there was no h to split on, and int('5m') is what raised.
  • Dropping two rows out of 13354 is the right call here, and it is a decision to record rather than a fix to hide.

Pulling both parts out with one pattern

Splitting twice handles the hours and loses the minutes. .str.extract takes a regular expression with named groups and returns one column per group, filling a group that did not match with NaN. One pattern then covers both shapes at once.

Optional digits before an h, optional digits before an m
python
pattern = r'(?:(?P<hours>\d+)h)?\s*(?:(?P<minutes>\d+)m)?'
parts = flights['Duration'].str.extract(pattern)
# '2h 50m' -> hours '2', minutes '50'
# '19h'    -> hours '19', minutes NaN
# '5m'     -> hours NaN,  minutes '5'

The two optional groups are what make it work. (?:...) groups without capturing, the trailing ? makes that group optional, and (?P<hours>\d+) captures one or more digits under the name hours.

python
parts = parts.astype(float).fillna(0)                  # a missing part is zero
flights['Duration_mins'] = parts['hours'] * 60 + parts['minutes']
flights = flights.drop(columns='Duration')

Every duration as one number

ExampleThe pattern applied to all 13354 rows
import matplotlib.pyplot as plt

pattern = r'(?:(?P<hours>\d+)h)?\s*(?:(?P<minutes>\d+)m)?'
parts = flights['Duration'].str.extract(pattern).astype(float).fillna(0)
flights['Duration_mins'] = parts['hours'] * 60 + parts['minutes']
flights = flights.drop(columns=['Duration'])

print(parts.head(4).to_string())
print("\nminutes:", flights["Duration_mins"].head(4).tolist())
print("shortest:", flights["Duration_mins"].min(), " longest:", flights["Duration_mins"].max())
print("median  :", flights["Duration_mins"].median())

fig, ax = plt.subplots(figsize=(7, 3.4))
ax.hist(flights['Duration_mins'], bins=60, range=(0, 1800), color='#3a6fd8')
ax.set_xlabel('duration in minutes'); ax.set_ylabel('flights')
ax.set_title('Flight duration once the text is a number')
fig.tight_layout()
A histogram of flight duration in minutes with a single tall spike of about 2400 flights near 170 minutes, then a long low spread of bars running past 1700 minutes.

Reading the parsed durations

  • The extract gives two columns, hours and minutes, with NaN where a part was absent. fillna(0) is correct here because a missing part means zero of it.
  • The shortest is 5 minutes and the longest is 2860, a little under two days. Both ends deserve a look before they are trusted.
  • The median is 520 minutes, which is why the histogram is crowded at the left: most flights are short and a tail of connecting itineraries runs on for days.
  • The column is now one number per row, so it can be scaled, transformed or split into bands. As text it could do none of those.

Splitting compared with extracting

.str.split chain.str.extract with a pattern
Covers two shapesno, one chain per shapeyes, optional groups
Missing partsilently gives the wrong pieceNaN, which you decide what to do with
Outputone Series per callone DataFrame, one column per group
Readableyes, for a simple separatorneeds a regular expression
Use it fora fixed separator and a fixed shapeoptional or repeated pieces

Where duration parsing is used

  • Travel and logistics data, where journey and delivery times arrive as text in whatever shape the source system prints.
  • Media and session lengths, written as 1h 32m or 00:45:10.
  • Support and incident logs, where time to resolution is the thing being predicted and arrives as a human readable string.
Watch out. .str.extract with a pattern whose groups are all optional matches an empty string, so a row that fits nothing comes back as NaN in every column and fillna(0) turns it into a duration of zero. Count the zeros after parsing. Here there are none, because every row has at least an hour part or a minute part.
Try it yourself
  • Count (flights['Duration_mins'] == 0).sum() after parsing. It is zero here; add a row of 'unknown' to the column and watch it become one.
  • Keep hours and minutes as two separate features instead of adding them, and compare how many distinct values each version has.
  • Change fillna(0) to fillna(-1) and look at the minimum. The parse is the same; only the decision about a missing part changed.

Every expert started right here.