Blog ·
Reading launch-monitor CSV exports: what Swingman looks for
Every launch-monitor app exports a CSV, and every one is different. How Swingman recognises columns by alias, detects units, fills in missing spin numbers and maps club names, and why a real export from your device helps.
A live connection is the nice path for launch-monitor data. The realistic path, at a driving range with a phone and no laptop, is the CSV your monitor's app exports when the session is over. Swingman imports those and runs the same coach on them as on a live session; matching the rows to the swings the camera caught is the next step. This post is how the importer reads a file it has never seen, and what makes it fail.
The problem
There is no standard. The same quantity turns up as Ball Speed, ball speed (mph), BallSpeed, Speed and Ball. One app writes Launch Angle, another VLA, a third Vertical Launch Angle. Headers change with the app's language setting. Units hide in the header, in a settings screen, or nowhere. Dates are one column or two.
Writing a parser per device would mean a parser per firmware version. Instead the importer recognises fields by aliases.
Step 1: normalise the header
Each header is lower-cased, anything in brackets is removed, and everything that isn't a letter or digit is dropped. So Ball Speed (mph) becomes ballspeed, Club Head Speed becomes clubheadspeed, and Launch Angle (°) becomes launchangle. Then each normalised header is looked up in an alias table, and the first match wins. Part of the table:
| Field | Aliases recognised today |
|---|---|
| Ball speed | ballspeed, ballspeedmph, ball, speed |
| Launch angle | launchangle, vla, verticallaunch, verticallaunchangle, launch |
| Launch direction | launchdirection, hla, horizontallaunch, horizontallaunchangle, direction, sideangle, azimuth |
| Back spin | backspin, backspinrpm |
| Side spin | sidespin, sidespinrpm |
| Total spin | totalspin, spinrate, spin, spinrpm |
| Spin axis | spinaxis, axis, spinaxisdeg |
| Carry / total | carry, carrydistance, carryyds, carryyards / total, totaldistance |
| Club speed | clubspeed, clubheadspeed, swingspeed, clubspeedmph |
| Attack angle | attackangle, angleofattack, aoa |
| Face to target | facetotarget, faceangle, face, clubface |
| Club path | clubpath, swingpath, path |
| Dynamic loft | dynamicloft, loft, dynloft |
| Smash factor | smashfactor, smash |
| Face impact | impactvertical, verticalimpact, verticalfaceimpact / impacthorizontal, horizontalimpact, horizontalfaceimpact |
| Club | club, clubname, clubtype |
| When | datetime, timestamp, shottime, created, createdat; or date + time |
A new export with an unfamiliar header is a one-line change: add the alias. That is the whole design, and it is why we ask owners to send a file rather than a screenshot.
Step 2: work out the units
If any header mentions metres ((m), meters, metres) and none mentions yards, distances are metres; otherwise yards. Speeds are mph unless a header says km/h or m/s, in which case they are converted. You can override both when importing, because some apps write the unit in a settings screen and nowhere in the file.
Step 3: fill in the spin
The coach reasons about total spin and spin axis, which is how Open Connect, the live protocol, expresses spin. Many exports give back spin and side spin instead. The importer derives what is missing:
- total spin = √(back² + side²), or just back spin when side spin is absent
- spin axis = atan2(side, back), in degrees, positive tilting right
The reverse happens when a file gives total spin and axis and the coach needs the components. Nothing is invented: a file with neither total spin nor back spin gets null spin, and the rules that need spin skip that shot rather than guess.
Step 4: the two fields that must exist
A row without ball speed or launch angle is skipped and reported. Everything else can be missing: club data is optional (a ball-only device still gets slice, hook, pull, push, low-launch and strike diagnoses at lower confidence), and clubs fall back to whatever you chose for the import.
Step 5: club names
Driver, DR, 1W and D all become DR. 7 iron, 7i and 7I become 7I. 56°, 56 deg wedge become W56. Pitching, gap, sand and lob wedges map to PW, GW, SW and LW. Anything two or three characters long and alphanumeric is kept as typed, so an app's own code survives. The rest becomes "unknown club", and you pick one for the session.
Step 6: when
If there is a datetime-like column it is parsed as the shot time. If there are separate date and time columns they are joined. Exports with neither import fine; the shots just have no clock time. When matching to camera footage arrives it will work by order first, the first exported shot being the first swing the camera kept, with timestamps as anchors where they exist and your corrections where the counts differ. That is how Toptracer sessions are paired to footage today, and the CSV path will reuse it.
What the raw file gets you
The import reports which header became which field and which rows were skipped and why, so a misread is visible rather than silent. When a header is misread, the fix is an alias; when a unit is misread, the fix is a flag. Both are one-line changes.
Devices and exports we know about
- Square Golf (Home and Omni): the app exports a session as CSV, and the importer was built against its layout.
- Garmin Golf (Approach R10 and R50): exports a 31-column CSV including carry, total, deviation, spin, club path and more. The aliases are in place; the headers depend on the app's language, so exports from UK phones are especially useful.
- FlightScope, Rapsodo, Uneekor, Bushnell/Foresight, Trackman Performance Studio: all export CSV, and the alias table grows as each one is verified.
If you own any of these, one session's export (any length, any club) lets us verify the format and publish a connection guide on the works-with page. Send it over. The file is used to check the import and then kept only as long as you want it to be.
Questions or a correction? Get in touch.