AI-220 · Lesson 5 of 12 · 29 min

Text, time series, location and image data

The cleaning and feature methods of Lesson 4 assume a table of numbers and categories. Much of the data AI systems use is not like that: customer messages and documents are text, sensor and sales records are time series, deliveries and stores have locations, and inspection systems produce images. Each kind has its own structure, its own ways to go wrong, and standard ways to turn it into features a model can use. This lesson introduces the essentials of each, with an emphasis on the preparation steps that most often cause errors.

Text

Raw text is messy: different encodings, accents, capitalization, punctuation, typos, emoji, markup and boilerplate. Preparation usually involves:

  • Decoding correctly: read files as UTF-8 (or the declared encoding) and check for replacement characters, which signal a wrong guess.
  • Unicode normalization: the same visible character can be stored in different ways (an accented é as one code point, or as e plus a combining accent). Normalizing (NFC or NFKD) makes them identical.
  • Case folding and punctuation handling, if the task does not depend on them.
  • Tokenization: splitting text into units. Classic methods split on words; modern language models use subword tokenizers that split rare words into pieces, so any word can be represented.
  • Removing boilerplate: signatures, quoted replies, legal footers and templates that repeat across documents and drown the meaningful content.

For classical models, text becomes a vector through a bag of words: one column per term, holding its count or a weight. TF-IDF (term frequency times inverse document frequency) weights a term by how often it appears in a document and down-weights terms that appear in many documents, so distinctive words count most.

\[\mathrm{tfidf}(t,d) = \mathrm{tf}(t,d) \times \log\frac{1+N}{1+\mathrm{df}(t)} + \mathrm{tf}(t,d)\]

The form above, with smoothing, is the one scikit-learn uses by default before normalizing each document vector to unit length.

Normalizing accents, case and punctuation, then building TF-IDF vectors: the two complaints about the café become nearly identical, and share nothing with the compliments.
import re
import unicodedata
from sklearn.feature_extraction.text import TfidfVectorizer

docs = [
    "Café closed early!! Refund please.",
    "cafe CLOSED early, want my refund",
    "Great coffee and friendly staff.",
    "Coffee was cold; staff were friendly though.",
]

def normalize(s):
    s = unicodedata.normalize("NFKD", s)
    s = "".join(ch for ch in s if not unicodedata.combining(ch))   # strip accents
    s = s.lower()
    s = re.sub(r"[^a-z0-9 ]+", " ", s)
    return " ".join(s.split())

clean = [normalize(d) for d in docs]
for c in clean:
    print(c)

vec = TfidfVectorizer(stop_words="english")
X = vec.fit_transform(clean)
print("vocabulary size:", len(vec.vocabulary_))
terms = vec.get_feature_names_out()
for i, row in enumerate(X.toarray()):
    top = sorted(zip(row, terms), reverse=True)[:3]
    print(i, [(t, float(round(w, 2))) for w, t in top])

sim = (X @ X.T).toarray().round(2)
print("cosine similarity of doc 0 with others:", sim[0])
Output
cafe closed early refund please
cafe closed early want my refund
great coffee and friendly staff
coffee was cold staff were friendly though
vocabulary size: 10
0 [('refund', 0.5), ('early', 0.5), ('closed', 0.5)]
1 [('want', 0.54), ('refund', 0.42), ('early', 0.42)]
2 [('great', 0.59), ('staff', 0.47), ('friendly', 0.47)]
3 [('cold', 0.59), ('staff', 0.47), ('friendly', 0.47)]
cosine similarity of doc 0 with others: [1.   0.84 0.   0.  ]

Normalization made "Café closed early!!" and "cafe CLOSED early" share their terms, so their cosine similarity is 0.84. Without it, they would be treated as having different words. Modern systems more often use embeddings from a pretrained language model (AI-340), which capture meaning beyond shared words, but the same cleaning and de-duplication steps come first.

Text-specific pitfalls

  • Near-duplicates: the same message forwarded, templated or scraped twice ends up in both training and test sets, inflating results. De-duplicate on normalized text, or with similarity thresholds.
  • Leakage through text: a support ticket's text may include the resolution ("refund issued"), which reveals the label.
  • Personal information: names, emails, phone numbers and addresses in free text must be detected and removed or masked when data is shared (Lesson 7).
  • Language and dialect: a model trained on one language or register may fail on others; record the language of each document.

Time series

A time series is a sequence of measurements in time order: hourly electricity demand, daily sales, a machine's vibration every second. Its defining property is that order matters and neighbouring values are related. Preparation steps include:

  • Regularizing: resampling irregular events onto a fixed interval, choosing how to aggregate (sum for counts, mean for levels, last value for states) and how to fill gaps.
  • Handling time zones and daylight saving (Lesson 3).
  • Detecting gaps, duplicates and stuck sensors (a value that stays exactly constant).
  • Understanding patterns: trend, daily and weekly seasonality, holidays, and one-off events.

Features that respect time

The core time-series features are lags (the value some steps ago), rolling statistics (mean, max, standard deviation over a past window) and calendar features (hour, day of week, holiday). The rule is that every feature for time t may use only values from before t. In pandas, this means shifting before rolling: df["demand"].shift(1).rolling(24).mean() uses the 24 hours before t; df["demand"].rolling(24).mean() includes t itself, leaking the value to be predicted.

Lag and rolling features built only from past values, a time-ordered train/test split, and simple baselines: the same hour last week is the strongest.
import numpy as np
import pandas as pd

# Hourly demand with a daily cycle, a weekly cycle and noise
rng = np.random.default_rng(0)
idx = pd.date_range("2026-01-05", periods=24 * 7 * 6, freq="h")
hour, dow = idx.hour.to_numpy(), idx.dayofweek.to_numpy()
demand = 100 + 30 * np.sin(2 * np.pi * (hour - 6) / 24) - 15 * (dow >= 5) + rng.normal(0, 5, len(idx))
df = pd.DataFrame({"demand": demand}, index=idx)

# Features that use only the past: shift BEFORE rolling
df["lag_1h"] = df["demand"].shift(1)
df["lag_24h"] = df["demand"].shift(24)
df["lag_168h"] = df["demand"].shift(168)
df["mean_24h"] = df["demand"].shift(1).rolling(24).mean()
df = df.dropna()

# Time-ordered split: train on the first 4 weeks, test on the rest
cut = df.index[0] + pd.Timedelta(weeks=4)
train, test = df[df.index < cut], df[df.index >= cut]
print("train", train.index.min().date(), "to", train.index.max().date(), len(train), "rows")
print("test ", test.index.min().date(), "to", test.index.max().date(), len(test), "rows")

def mae(pred):
    return float(np.mean(np.abs(test["demand"] - pred)))

print("naive last hour     MAE", round(mae(test["lag_1h"]), 2))
print("same hour yesterday MAE", round(mae(test["lag_24h"]), 2))
print("same hour last week MAE", round(mae(test["lag_168h"]), 2))
print("trailing 24h mean   MAE", round(mae(test["mean_24h"]), 2))
Output
train 2026-01-12 to 2026-02-08 672 rows
test  2026-02-09 to 2026-02-15 168 rows
naive last hour     MAE 6.73
same hour yesterday MAE 8.28
same hour last week MAE 4.94
trailing 24h mean   MAE 19.4

Two habits are visible here. The split is by time, never random, because a random split lets the model learn from hours surrounding each test hour. And simple baselines are computed before any model: here, "same hour last week" beats the last hour and yesterday because demand has a weekly pattern. Any model must beat the best baseline to be worth deploying.

Worked problem

Leakage through a centred window

An analyst smooths a sensor signal with a centred 5-point rolling mean (two points before, the point itself and two after) and then uses the smoothed value as a feature to predict a failure at time t. What is wrong?

Show solution
  1. A centred window at t uses values at t + 1 and t + 2.
  2. Those values are in the future at prediction time, and near a failure they may already show it.

Answer: The feature leaks future information. Use trailing windows ending at t − 1 (shift, then roll) so that every feature could have been computed at prediction time.

Location data

Latitude and longitude describe points on a sphere (approximately), so ordinary Euclidean distance on degrees is wrong: a degree of longitude shrinks from 111 km at the equator to zero at the poles. Distances between points should use the great-circle (haversine) formula, or a projection suited to the region.

\[d = 2r \arcsin\sqrt{\sin^{2}\frac{\Delta\varphi}{2} + \cos\varphi_{1}\cos\varphi_{2}\sin^{2}\frac{\Delta\lambda}{2}}\]
Great-circle distances from customers to stores with NumPy broadcasting, the nearest store for each, and how the length of a degree of longitude shrinks with latitude.
import numpy as np
import pandas as pd

stores = pd.DataFrame({
    "store": ["Toronto", "Ottawa", "Montreal", "Calgary", "Vancouver"],
    "lat": [43.6532, 45.4215, 45.5019, 51.0447, 49.2827],
    "lon": [-79.3832, -75.6972, -73.5674, -114.0719, -123.1207],
})
customers = pd.DataFrame({
    "cust": [1, 2, 3],
    "lat": [43.2557, 45.3311, 49.8880],     # Hamilton, Kanata, Kelowna
    "lon": [-79.8711, -75.9039, -119.4960],
})

def haversine_km(lat1, lon1, lat2, lon2):
    r = 6371.0
    p1, p2 = np.radians(lat1), np.radians(lat2)
    dphi = p2 - p1
    dlmb = np.radians(lon2 - lon1)
    a = np.sin(dphi / 2) ** 2 + np.cos(p1) * np.cos(p2) * np.sin(dlmb / 2) ** 2
    return 2 * r * np.arcsin(np.sqrt(a))

# Distance from every customer to every store (broadcasting), then the nearest
d = haversine_km(customers["lat"].to_numpy()[:, None], customers["lon"].to_numpy()[:, None],
                 stores["lat"].to_numpy()[None, :], stores["lon"].to_numpy()[None, :])
customers["nearest"] = stores["store"].to_numpy()[d.argmin(axis=1)]
customers["km"] = d.min(axis=1).round(1)
print(customers.to_string(index=False))

# Why not plain degrees? One degree of longitude shrinks with latitude.
for lat in (0, 45, 60):
    print(f"1 degree of longitude at {lat} N = {haversine_km(lat, 0, lat, 1):.1f} km")
Output
 cust     lat       lon   nearest    km
    1 43.2557  -79.8711   Toronto  59.2
    2 45.3311  -75.9039    Ottawa  19.0
    3 49.8880 -119.4960 Vancouver 269.8
1 degree of longitude at 0 N = 111.2 km
1 degree of longitude at 45 N = 78.6 km
1 degree of longitude at 60 N = 55.6 km

Common location features are distances to points of interest, counts of nearby facilities, and the region a point falls in (postal area, census tract, grid cell). Care points:

  • Coordinate order and sign: longitude is negative west of Greenwich, and some systems store (lon, lat), others (lat, lon).
  • Precision and privacy: a home location at five decimal places (about 1 m) identifies a household. Rounding or aggregating to areas reduces risk (Lesson 7).
  • Spatial leakage: nearby locations are similar, so random splits can place a test point next to a training point that is nearly identical. Spatial or grouped splits (by region) give honest estimates for new areas.
  • Travel distance differs from straight-line distance, especially across water, mountains and road networks; routing services give realistic travel times.

Image data

An image is an array of pixel values: height × width × channels (three for red, green and blue), usually stored as 8-bit integers from 0 to 255, and compressed on disk in formats such as JPEG and PNG. Preparing images for a model involves:

  • Decoding and checking: corrupt files, unexpected sizes, grayscale or four-channel images in a colour data set, and EXIF orientation tags that rotate phone photos.
  • Resizing to the model's input size, keeping or padding the aspect ratio.
  • Scaling and normalization: dividing by 255 and standardizing each channel using statistics from the training set (or those the pretrained model expects).
  • Augmentation during training: random flips, crops, rotations and colour changes that create plausible variations, improving robustness. Augmentations must not change the label: a horizontal flip of a handwritten digit or a road sign may.
An image as an array: its shape and size, per-channel scaling and standardization, a flip and a crop, and how quickly uncompressed image data sets grow.
import numpy as np

# A tiny synthetic RGB image: height x width x channels, 8-bit
rng = np.random.default_rng(1)
img = np.zeros((6, 8, 3), dtype=np.uint8)
img[:, :4] = [200, 40, 40]          # left half reddish
img[:, 4:] = [30, 90, 210]          # right half bluish
img = np.clip(img + rng.integers(-10, 11, img.shape), 0, 255).astype(np.uint8)
print("shape", img.shape, "dtype", img.dtype, "bytes", img.nbytes)

# Scale to 0..1 and standardize per channel with statistics from training images
x = img.astype(np.float32) / 255.0
mean, std = x.mean(axis=(0, 1)), x.std(axis=(0, 1))
z = (x - mean) / std
print("channel means", mean.round(3), "after standardizing", z.mean(axis=(0, 1)).round(3) + 0.0)

# Simple augmentations: horizontal flip and a crop
flip = img[:, ::-1]
crop = img[1:5, 2:6]
print("left column before flip", img[0, 0], "after flip", flip[0, 0], "crop", crop.shape)

# Storage: why image data sets get large
n, h, w = 1_000_000, 224, 224
print(f"{n:,} images at {h}x{w}x3 uint8 = {n * h * w * 3 / 1e9:.0f} GB uncompressed")
Output
shape (6, 8, 3) dtype uint8 bytes 144
channel means [0.449 0.253 0.499] after standardizing [0. 0. 0.]
left column before flip [199  40  45] after flip [ 35  97 211] crop (4, 4, 3)
1,000,000 images at 224x224x3 uint8 = 151 GB uncompressed

Image data sets are large, so they are usually stored compressed in object storage and decoded on the fly while training, with sharded files to make reading efficient (Lesson 9). Metadata, such as the camera, location, time and labeller, is kept in a table alongside, and is essential for finding problems such as all positive examples coming from one camera.

Worked problem

Spurious cues

A model to detect a skin condition from photos achieves 97% accuracy. Analysis of metadata shows 90% of positive images came from one clinic, whose photos include a ruler for scale. What might the model have learned?

Show solution
  1. The ruler, or the clinic's camera and lighting, is strongly associated with positive labels.
  2. The model may detect the ruler rather than the condition.

Answer: A spurious cue. Check by testing on images from other clinics, cropping out rulers, and balancing sources. Metadata about how each image was collected is what makes this check possible.

Common mistakes

  • MistakeComparing text without Unicode and case normalization.

    InsteadNormalize before matching, counting or de-duplicating.

  • MistakeUsing rolling windows that include the current or future values as features.

    InsteadShift before rolling so features use only the past.

  • MistakeSplitting time series or spatial data at random.

    InsteadSplit by time, or by region, to estimate performance on the future or new places.

  • MistakeMeasuring distance in degrees.

    InsteadUse great-circle distance or a suitable projection.

  • MistakeApplying augmentations that change the label.

    InsteadChoose augmentations that preserve the meaning, such as flips only where symmetry holds.

Summary and key terms

  • Text needs decoding, normalization, tokenization and boilerplate removal; TF-IDF weights distinctive terms.
  • Watch for near-duplicates, label leakage and personal information in text.
  • Time series features must use only past values; split by time and compare with baselines.
  • Location distances need the haversine formula; beware coordinate order, privacy and spatial leakage.
  • Images are arrays that are decoded, resized, normalized and augmented.
  • Metadata about collection reveals spurious cues in any data type.
Unicode normalization
Converting equivalent character sequences to one standard form.
Tokenization
Splitting text into units such as words or subwords.
TF-IDF
A term weight combining frequency in a document with rarity across documents.
Lag feature
A feature equal to the value some steps earlier.
Rolling window
A moving set of consecutive observations used for statistics.
Seasonality
A repeating pattern tied to the calendar or clock.
Haversine formula
The great-circle distance between two points on a sphere.
Spatial leakage
Optimistic results from testing near training locations.
Augmentation
Creating label-preserving variations of training examples.
Spurious cue
A feature correlated with the label by accident of collection.

Practice problems

Work each problem on paper first, then reveal the answer to check.

  1. Why normalize "Café" and "cafe" before counting words?

    Show answer

    So the same word is counted as one term.

  2. Which terms get high TF-IDF weight?

    Show answer

    Frequent in the document but rare across documents.

  3. Name a way text can leak the label.

    Show answer

    It mentions the outcome, such as "refund issued".

  4. Correct pandas expression for a trailing 7-step mean feature?

    Show answer

    s.shift(1).rolling(7).mean()

  5. Why split time series by time?

    Show answer

    Random splits let the model learn from neighbouring times of test points.

  6. Length of 1° of longitude at 60° N?

    Show answer

    About 55.6 km.

  7. About how precise are 5 decimal places of latitude?

    Show answer

    About 1 m.

  8. Size of one 224×224 RGB uint8 image?

    Show answer

    150,528 bytes.

  9. Give an augmentation that changes the label for some tasks.

    Show answer

    A horizontal flip of digits, letters or road signs.

  10. What metadata check reveals spurious cues?

    Show answer

    Comparing labels by source, camera, site or time.

Lesson quiz · 5 questions

Score 60% or more to complete this lesson.

Take the quiz