Stata .dta dataset reader
README.md

dta #

Read Stata .dta datasets in OCaml. Most public economic and survey microdata is distributed in this format.

It reads release 117 (Stata 13) and release 118 (Stata 14 and later), the versions with XML framing, in either byte order. Older releases (115 and before) lack the framing and 119 onwards allows more variables; for those of_string returns an error instead of guessing at the layout. The whole file is decoded at once, so it has to fit in memory.

Installation #

Install with opam:

$ opam install dta

If opam cannot find the package, it is not in the public opam-repository yet. Add the overlay repository first:

$ opam repo add samoht https://tangled.org/gazagnaire.org/opam-overlay.git
$ opam update
$ opam install dta

Reading a dataset #

# let dataset = Result.get_ok (Dta.of_string (In_channel.with_open_bin "test/fixtures/labels.dta" In_channel.input_all));;
val dataset : Dta.t = <abstr>
# Fmt.pr "%a@." Dta.pp dataset;;
dta 118: 2 variables, 4 observations Sample Data
  sex                              long
  n                                long
- : unit = ()

Variable and value labels come back too:

# List.map (fun (v : Dta.variable) -> v.name, v.label) (Dta.variables dataset);;
- : (string * string) list =
[("sex", "respondent sex"); ("n", "count of things")]
# Dta.value_labels dataset;;
- : (string * (int32 * string) list) list =
[("sex", [(1l, "male"); (2l, "female")])]

Missing values #

In the file, a missing number is a value just past the top of its storage type's range. There is the plain . and 26 extended codes, .a to .z, which record why the value is missing. You always get Missing, never the big number:

# let types = Result.get_ok (Dta.of_string (In_channel.with_open_bin "test/fixtures/types.dta" In_channel.input_all));;
val types : Dta.t = <abstr>
# Option.get (Dta.column types "vbyte");;
- : Dta.value list =
[Dta.Float 1.; Dta.Float (-127.); Dta.Float 100.; Dta.Missing Dta.System]

Long strings #

A strL value lives outside its row, in a section after the data. The decoder follows the reference, so you just see a string:

# let strl = Result.get_ok (Dta.of_string (In_channel.with_open_bin "test/fixtures/strl.dta" In_channel.input_all));;
val strl : Dta.t = <abstr>
# List.map (List.map (fun v -> Fmt.to_to_string Dta.pp_value v)) (Dta.rows strl) |> List.map (List.map String.length);;
- : int list list = [[1; 3000]; [1; 5]]

Test corpus #

Stata cannot be redistributed, so test/fixtures/gen.py writes the files in test/fixtures/ with pandas. The layout constants in the tests are taken from Stata's documentation (help dta and help dta_117).

Licence #

ISC. See LICENSE.md.