U.S.-Mexico Border Truck Crossings at Laredo, TX

I wanted to explore how truck crossing and trade volumes vary over time at the Laredo, Texas port of entry, which serves as one of the busiest inland trade hubs between the United States and Mexico.

This analysis details my first data exploration project, documenting my query progression, data verification steps, and seasonal peak discoveries.


1. Data Structuring and Query Formulation

I started by looking at the data available in the SQLite database. The first table, entries, is comprised of raw records from the Border Crossing Entry Data.

The raw date column in the data source had dates formatted like 2025 Dec 01 12:00:00 AM. I needed to extract the year and month substrings and format the numeric crossings. The crossing count was originally stored in text format with commas (e.g., 2,459), so I stripped the commas and cast the numbers to integers.

The following query filters the data specifically for inbound truck crossings at the Port of Laredo in Texas:

SELECT
	SUBSTR("Date", 1, 4) AS year,
	SUBSTR("Date", 5, 5) AS month,
	REPLACE(Crossings, ',', '') AS cross,
	CAST(REPLACE(Crossings, ',', '') AS INTEGER) AS num_crossings
FROM
	entries
WHERE
	State = "Texas"
	AND "Port Name" = "Laredo"
	AND Measure = "Trucks"
ORDER BY num_crossings DESC
;

2. Verifying Data Integrity

I had access to a separate summary table named crossings that contained the total annual crossings by crossing type. However, that summary table only went up to 2024:

Description202220232024
Pedestrians2,490,6872,550,2092,954,326
Personal Vehicles4,552,9304,908,3685,088,431
Trucks2,799,6012,936,1303,026,632
Vehicle Passengers8,778,9319,512,7949,912,909

To ensure that my queries on the raw entries table were accurate and that the source data was reliable, I wrote a query utilizing a Common Table Expression (CTE) to sum the total crossings in entries per year:

WITH crossings_data AS (
SELECT
	SUBSTR("Date", 1, 4) AS year,
	SUBSTR("Date", 5, 5) AS month,
	REPLACE(Crossings, ',', '') AS cross,
	CAST(REPLACE(Crossings, ',', '') AS INTEGER) AS num_crossings
FROM
	entries
WHERE
	State = "Texas"
	AND "Port Name" = "Laredo"
	AND Measure = "Trucks"
)

SELECT
	year,
	SUM(num_crossings) as total_crossings
FROM
	crossings_data
GROUP BY year
;

This query returned the following annual totals:

YearTotal Crossings (Raw Sum)Summary Total (Expected)Verification
20222,799,6012,799,601Matches ✓
20232,936,1302,936,130Matches ✓
20243,026,6323,026,632Matches ✓

Seeing that the sums matched the official totals exactly gave me confidence that the raw source data was sound and that my query logic was correct.


Next, I wanted to map the monthly crossings over time to see the progression. I started by grouping the data by year:

SELECT
	CAST(SUBSTR("Date", 1, 4) as INTEGER) AS year,
	SUBSTR("Date", 5, 5) AS month,
	CAST(REPLACE(Crossings, ',', '') AS INTEGER) AS num_crossings
FROM
	entries
WHERE
	State = "Texas"
	AND "Port Name" = "Laredo"
	AND Measure = "Trucks"
	AND year BETWEEN 2022 AND 2026
GROUP BY year, month
;

I cast the year substring to an integer (CAST(SUBSTR("Date", 1, 4) as INTEGER)) so I could write a clean range limit (year BETWEEN 2022 AND 2026) in the WHERE clause instead of chaining multiple OR statements.

A query summing the totals by year shows the overall annual growth:

SELECT
	SUBSTR("Date", 1, 4) AS year_str,
	SUM(CAST(REPLACE(Crossings, ',', '') AS INTEGER)) AS num_crossings
FROM entries
WHERE
	State = 'Texas'
	AND "Port Name" = 'Laredo'
	AND Measure = 'Trucks'
GROUP BY year_str
ORDER BY year_str DESC;

Annual Truck Crossings (Laredo, TX)

2020
2.32M
2021
2.57M
2022
2.80M
2023
2.94M
2024
3.03M
2025
2.95M

4. Sorting and Identifying Monthly Peaks

When reviewing the raw results, I noticed they were ordered alphabetically by month name abbreviation (Apr, Aug, Dec, etc.) instead of chronologically. To sort them chronologically, I used a CASE statement inside a CTE to convert the month strings to two-digit numeric strings:

WITH staged_entries AS (
	SELECT 
		SUBSTR("Date", 1, 4) AS year_str,
		CASE TRIM(SUBSTR("Date", 5, 4))
			WHEN 'Jan' THEN '01' WHEN 'Feb' THEN '02' WHEN 'Mar' THEN '03'
			WHEN 'Apr' THEN '04' WHEN 'May' THEN '05' WHEN 'Jun' THEN '06'
			WHEN 'Jul' THEN '07' WHEN 'Aug' THEN '08' WHEN 'Sep' THEN '09'
			WHEN 'Oct' THEN '10' WHEN 'Nov' THEN '11' WHEN 'Dec' THEN '12'
		END	AS month_str,
		CAST(REPLACE(Crossings, ',', '') AS INTEGER) AS num_crossings
	FROM 
		entries
	WHERE
		State = 'Texas'
		AND "Port Name" = 'Laredo'
		AND Measure = 'Trucks'
)

With this structured timeline, I was able to run a query to check which year performed best for each month:

SELECT
	month_str,
	year_str,
	MAX(num_crossings) AS peak_volume
FROM staged_entries
GROUP BY month_str
;

This returned:

MonthYearPeak Crossings
01 (Jan)2024244,595
02 (Feb)2024251,110
03 (Mar)2026266,808
04 (Apr)2024267,884
05 (May)2024261,745
06 (Jun)2026261,224
07 (Jul)2024260,600
08 (Aug)2024263,414
09 (Sep)2023244,489
10 (Oct)2024267,428
11 (Nov)2023245,343
12 (Dec)2023223,977

This shows that 2024 holds the record for peak volume for most months, except for September, November, and December (held by 2023), and March and June (surpassed in 2026).


5. Busiest Months (Seasonality Patterns)

Finally, I wanted to find the single busiest month in each calendar year. To do this, I constructed ISO dates and used the ROW_NUMBER() window function partitioned by year to rank the monthly volumes:

WITH staged_entries AS (
	SELECT 
		SUBSTR("Date", 1, 4) AS year_str,
		CASE TRIM(SUBSTR("Date", 5, 4))
			WHEN 'Jan' THEN '01' WHEN 'Feb' THEN '02' WHEN 'Mar' THEN '03'
			WHEN 'Apr' THEN '04' WHEN 'May' THEN '05' WHEN 'Jun' THEN '06'
			WHEN 'Jul' THEN '07' WHEN 'Aug' THEN '08' WHEN 'Sep' THEN '09'
			WHEN 'Oct' THEN '10' WHEN 'Nov' THEN '11' WHEN 'Dec' THEN '12'
		END	AS month_str,
		CAST(REPLACE(Crossings, ',', '') AS INTEGER) AS num_crossings
	FROM 
		entries
	WHERE
		State = 'Texas'
		AND "Port Name" = 'Laredo'
		AND Measure = 'Trucks'
),
cleaned_crossings AS (
	SELECT
		year_str,
		month_str,
		year_str || '-' || month_str || '-01' AS month_date,
		CAST(year_str AS INTEGER) AS year_num,
		num_crossings
	FROM staged_entries	
),
ranked_months AS (
	SELECT
		year_str,
		month_str,
		month_date,
		num_crossings,
		ROW_NUMBER() OVER(
			PARTITION BY year_str
			ORDER BY num_crossings DESC
		) AS rank_in_year
	FROM cleaned_crossings
)

SELECT
	year_str AS year,
	month_str AS month,
	num_crossings AS peak_crossings
FROM ranked_months
WHERE rank_in_year = 1
  AND year_str BETWEEN '2022' AND '2026'
ORDER BY year_str ASC
;

This produced these results:

YearPeak MonthPeak Crossings
2022March (03)251,385
2023March (03)263,896
2024April (04)267,884
2025March (03)265,620
2026March (03)266,808

Peak Crossing Volumes (2022 - 2026)

2022 (Mar)
251K
2023 (Mar)
263K
2024 (Apr)
267K
2025 (Mar)
265K
2026 (Mar)
266K

This query confirmed that March is historically the busiest month for inbound truck traffic, except for 2024 which peaked in April.

When I removed the date filters and queried the historical dataset going back to 1996, the pattern expanded:

  • October and March consistently rank as the top months, with October holding the absolute majority of yearly peak volumes.
  • This suggests that the end of the first quarter (Q1 spring run) and the start of the final quarter (Q4 pre-holiday stocking run) represent the busiest logistics seasons.

Note: The dataset used measures inbound crossings only (traffic flowing from Mexico into the United States).