An interpreted language for applied econometrics. Named after Fumio Hayashi.
Stata-like syntax, modern language features, zero cost. Built in Rust on top of Greeners.
git clone https://github.com/sheep-farm/hayashi.git
cd hayashi
cargo build --release
# Binary at target/release/hay
# Optional: ODBC support (requires unixodbc)
cargo build --release --features odbcPre-built binaries are generated daily from the dev branch for Linux, macOS, and Windows:
hay dist-update --nightlyThis downloads and replaces your current hay binary with the latest nightly build. Nightly builds are pre-releases and may be unstable — they include features not yet in the stable release (e.g. tidy(), glance(), names(), model serialization for plugins).
To check for stable updates without installing:
hay dist-update --checkTo install the latest stable release:
hay dist-updatehay # interactive REPL (tab completion, syntax highlighting)
hay script.hay # run a script
hay - # read from stdin
hay --help # list commandsREPL features: tab completion for keywords + variables, syntax highlighting (keywords blue, strings green, numbers yellow), history hints (fish-style).
The VS Code extension supports breakpoints, stepping, and variable inspection via the Debug Adapter Protocol (hay dap). Model objects expand in the Variables panel with a concise summary and structured children:
result: OLS(k=2, n=10000), R2=1.0000
coefficients DataFrame(2 rows, 7 cols)
fit Dict(13 entries)
params Series(params: 2 values)
...
See the debugging guide and the VS Code extension README for details.
load "data.dta" as df
let m1 = reg(Y ~ X1 + X2, df, cluster=firm)
let m2 = reg(Y ~ X1 + X2 + X3, df, cluster=firm)
esttab(m1, m2)
esttab(m1, m2, fmt=latex, path="table.tex")
export(df, "csv", "data.csv")
export(df, "xlsx", "data.xlsx")
export(df, "parquet", "data.parquet")
export(m1, "latex", "table.tex")
Hayashi is a dynamically-typed, block-scoped interpreted language. It combines Stata-like econometrics syntax with modern programming constructs.
| Type | Literal | Notes |
|---|---|---|
int |
42 |
64-bit signed integer |
float |
3.14 |
64-bit IEEE 754 |
bool |
true / false |
|
string |
"hello" |
UTF-8, immutable |
list |
[1, 2, 3] |
Heterogeneous; push/pop mutate, rest COW |
dict |
{"key": value} |
String keys, any values, immutable (COW) |
nil |
Absence of value | |
dataframe |
via load / input |
Tabular data, copy-on-write (Rc) |
function |
fn name(x) { } or |x| expr |
Named or anonymous (closure) |
Explicit conversions: int(x), float(x), str(x), bool(x).
Introspection: type(x) returns the type name as a string.
let x = 10 // mutable — can be reassigned
const PI = 3.14159 // immutable — error on reassign or redeclare
x = 20 // assign without let — searches outer scopes
No variable shadowing: let x over an existing const x is an error, even in an inner scope. This prevents subtle bugs common in C/C++.
Block-scoped with deterministic destruction. No garbage collector.
let x = 10
if true {
let temp = 42 // lives only in this block
x = x + temp // modifies outer x
}
// temp is gone, x = 52
Function parameters are const by default — data enters immutable, result exits via return. DataFrames use Rc<DataFrame> for zero-copy passing and copy-on-write mutation.
| Construct | Syntax | Returns value? |
|---|---|---|
| If statement | if cond { } else if { } else { } |
No |
| If expression | if cond { a } else { b } |
Yes |
| Match | match expr { pat => result, _ => default } |
Yes |
| Block expression | { stmt; ...; expr } |
Yes |
| For loop | for i in 1..10 { } / for v in list { } |
No |
| Parallel for | let r = parallel for v in list, threads=N { } |
No |
| While loop | while cond { } |
No |
| Try/catch | try { } catch e { } |
No |
| Break/continue | break / continue |
— |
| Return | return expr |
— |
A block { stmt; ...; expr } evaluates a sequence of statements and returns the value of the last expression. Variables declared inside the block are local to it.
let df = {
let raw = load("data.csv")
generate raw y = log(x)
keep(raw, ["date", "y"])
raw
}
// df is available; raw and the temporary columns are gone
quietly on suppresses automatic output from statements and estimators. print(...) still appears; display ... also appears but is deprecated. quietly off restores normal output. The flag is scope-aware: a toggle inside a block reverts when the block ends.
Note:
displayis deprecated. Prefer
quietly on
let df = {
let a = load("a.csv")
let b = load("b.csv")
let m = merge(a, b, key=id, type=inner)
generate m z = x - y
m
}
quietly off
ols(z ~ x, df)
print("done")
Both quietly on and quietly off share the same suppression mechanism, so new commands need no special handling — they just use the internal output channel. The quietly(expr) function form is deprecated and will be removed in a future release; use quietly on / quietly off instead.
// Named function — parameters are const
fn add(a, b) {
let result = a + b
return result
}
// Closure — anonymous, captures outer scope
let double = |x| x * 2
let big = filter(list, |x| x > 10)
| Category | Operators |
|---|---|
| Arithmetic | + - * / ^ ** % |
| Assignment | = += -= *= /= %= |
| Comparison | == != > < >= <= |
| Logical | && (or &) || ! |
| Membership | in — works with list, dict (key), string (substring) |
| Pipe | |> — passes left side as first argument (or replacing _ placeholder) |
| Index | list[i] dict["key"] |
| String | + for concatenation |
let msg = f"mean = {mu:.2f}, n = {n}, p-value = {p:.4e}"
F-strings support any expression inside {} and format specifiers: .Nf (decimal places), .Ne (scientific notation). Escape braces with {{ and }}.
push(list, item) and pop(list) mutate in-place (like Python/JS). Other list operations return new lists, leaving the original unchanged.
List operations: push pop insert remove clear reverse index slice join map filter unique flatten sort range len
Dict operations: keys values has_key dict_set dict_remove dict_merge len
Pipe chaining:
[5, 3, 1, 4, 2]
|> filter(|x| x > 2)
|> sort
|> map(|x| x * 10)
|> reverse
value |> |x| x * 3 // pipe with inline closure
exper |> dobro // pipe with user function
df |> ols(lw ~ yos, _) // pipe using '_' as placeholder for specific argument positions
// Load — 8 formats + URL
load "file.csv" as df
load "file.tsv" as df
load "file.json" as df
load "file.dta" as df // Stata
load "file.xlsx" as df, sheet=Plan1 // Excel (xlsx/xls/ods)
load "file.parquet" as df // Apache Parquet
load "file.db" as df, table=prices // SQLite
load "file.db" as df, query="SELECT * FROM prices WHERE year > 2020"
load "odbc://DSN=mydb" as df, query="SELECT * FROM t" // ODBC (feature flag)
load "https://...data.csv" as df // URL (auto-download)
load "data.csv" as df, sep=";" // custom delimiter
load "panel.parquet" as df, columns=[ticker, date, close], where="ticker == \"AAPL\"" // projection + filter pushdown
// Export — 8 formats
export(df, "csv", "out.csv")
export(df, "json", "out.json")
export(df, "tsv", "out.tsv")
export(df, "xlsx", "out.xlsx")
export(df, "parquet", "out.parquet")
export(df, "sqlite", "out.db")
export(m, "latex", "table.tex")
export(m, "html", "table.html")
query= is raw SQL executed by SQLite or the configured ODBC database. columns= and where= push column projection and row filtering down to the data source (Parquet uses Arrow ProjectionMask + RowFilter; SQLite/ODBC rewrites the SELECT/WHERE; CSV/TSV/DTA/Excel filter row-by-row), avoiding loading the full dataset into RAM — useful for large files. query= cannot be combined with columns= or where=. Remote load downloads untrusted input even with URL validation and size/time limits. ODBC support is optional and requires system ODBC drivers. See the Trust Model.
| Category | Commands |
|---|---|
| Linear | ols reg iv wls glsar |
| Panel | fe re feiv ab sysgmm pcse xtgls |
| Binary | logit probit clogit |
| Count | poisson nbreg zip zinb |
| Ordinal | ologit oprobit mlogit cmnlogit |
| GMM | gmm |
| Censored | tobit heckman |
| Survival | cox km |
| Quantile | qreg |
| Regularization | lasso ridge elasticnet |
| Time series | arima sarima autoreg ardl kalman garch egarch gjrgarch |
| VAR | var vecm varma svar irf fevd |
| Causal | did rd fuzzy_rd synth psm |
| Finance | fmb portsort doublesort |
| Robust / flexible | rlm gee glm betareg mixed lowess gam isotonic |
| Systems / factors | sur three_sls pca factor dfm manova cancorr |
Hayashi also exposes a growing set of ML, clustering, spatial, and Bayesian estimators:
| Category | Commands |
|---|---|
| Machine learning | rf gbm qrf xgboost mlp lstm transformer bart gp |
| Causal ML | causalforest grf dr_learner tmle orf bsc dml_crossfit |
| Clustering / unsupervised | kmeans dbscan hclust tsne umap spectral gmm_clust biplot kde |
| Spatial | spatial_sar spatial_sem spatial_durbin spatial_panel_sar spatial_panel_sem spatial_durbin_error |
| Bayesian / frontier | bayes_lm bayes_sfa_production bayes_sfa_cost sfa_production sfa_cost |
Common options include if= for subsamples and cov=/cluster=/nw= where supported. Core regression estimators auto-detect and drop perfectly collinear variables (Stata-style (omitted) display).
test(m, "X1", "X2") // joint F-test
test(m, "X1 = X2") // linear restriction
test(m, "white") // White heteroskedasticity
test(m, "bp") // Breusch-Pagan
test(m, "dw") // Durbin-Watson
nlcom(m, X1 / X2) // nonlinear combination (delta method)
margins(m) // AME with SEs, z-values, p-values
coefplot(m) // ASCII coefficient plot with 95% CI
estat(m1, m2, m3) // AIC/BIC comparison
hausman(m_fe, m_re) // Hausman test
predict df yhat = m // fitted values
predict df e = m, "residuals" // residuals
bootstrap(ols, Y ~ X, df, n=1000)
influence(m) // DFFITS, Cook's D, leverage
vif(m) // variance inflation factors
// Tidy/glance: extract model data as DataFrames
let t = tidy(m) // variable, coef, std_err, t, p_value, conf_low, conf_high
let g = glance(m) // r2, adj_r2, n, f_stat, prob_f, aic, bic, log_lik, sigma
// Column names of a DataFrame
let cols = names(df) // ["price", "mpg", "weight", ...]
// Store and compare models
eststo(m1)
eststo(m2)
esttab() // model comparison table
estclear() // clear stored models
// Joint F-test
testparm(m, ["X1", "X2"]) // H0: selected coefficients = 0
// Generate (statement — modifies in-place)
generate df lnY = log(Y)
generate df D = (X == 1)
generate df row = _n
// Mutate (function — multi-column, pipe-friendly)
let df2 = mutate(df, z = x^2, w = ln(y), ratio = x / y)
let df2 = df |> mutate(z = x * 2) |> filter(z > 5) |> sort(z)
// Pipe semantics: standalone modifies source, captured preserves it
df |> mutate(z = x^2) // modifies df
let result = df |> mutate(z = x^2) // df unchanged, result has z
// Selection and filtering
select(df, col1, col2) // alias for keep
drop(df, col)
filter(df, mpg > 25 & foreign == 1)
sort(df, price)
// Aggregation
group_by(df, setor, mean, ret, vol) // pipe-friendly
collapse(df, mean, price, mpg, by=foreign)
// Reshape
pivot_longer(df, stubs=["gdp"], i=country, j=year)
pivot_wider(df, i=id, j=year, values=gdp)
// Other
replace df Y = 0 if X > 10
merge(df1, df2, key=id, type=left)
append(df1, df2)
encode(df, region) // string -> numeric
decode(df, region_num, labels=["north", "south", "east", "west"])
winsor(df, Y, p=0.01)
dropna(df, price, mpg)
ffill(df) // forward-fill NaN em colunas float
rename(df, old, new)
label(df, Y, "GDP per capita")
duplicates(df, id, action=drop)
drop_collinear(df) // remove perfectly collinear columns
preserve(df) / restore(df)
// Time-series declaration (required for L.x, F.x, D.x operators)
tsset df year
xtset(df, firm, year) // panel structure
// Parsing
let t = date("2024-06-15") // -> Unix timestamp
let dt = datetime("2024-06-15 14:30:00")
// Extraction in generate
generate df Y = year(date_col)
generate df M = month(date_col)
generate df D = day(date_col)
generate df H = hour(date_col)
generate df W = dow(date_col) // 0=Monday
// Filtering with scalar variables
let cutoff = date("2020-01-01")
let sub = filter(df, ts >= cutoff)
// summarize returns dict when captured, prints when standalone
let s = summarize(df, price, detail=true)
display s["mean"]
// All accept bare, string, variable, or list for column names
let cols = ["price", "mpg"]
summarize(df, cols)
// Descriptive commands
codebook(df) // detailed variable description
tabulate(df, group)
tabulate(df, row, col, chi2=true)
correlate(df, X1, X2, X3)
pwcorr(df, X1, X2, X3)
ttest(df, Y, by=group)
ci(df, Y, level=0.99)
centile(df, Y, percentiles=[10, 50, 90])
describe(df)
// Panel summary
xtsum(df, wage, hours, id=firm) // within/between decomposition
// Normality tests
swilk(df, Y) // Shapiro-Wilk
sfrancia(df, Y) // Shapiro-Francia
sktest(df, Y) // Skewness/Kurtosis (JB + D'Agostino)
Hayashi includes a reproducible, automated empirical validation programme in
validation/. It compares Hayashi output against reference implementations
(R and Python/statsmodels) on real datasets and on simulated DGPs taken from
the Hayashi book chapters:
python -m venv validation/.venv
validation/.venv/bin/pip install -r validation/requirements.txt
Rscript -e 'install.packages(c("wooldridge", "jsonlite"))'
hay validateSee validation/README.md for the full protocol and validation/MATRIX.md for
the current status of every case.
The matrix now covers 215 cases across 115 estimators, with 199 passing against R and Python reference implementations.
// SVG (publishable)
graph_scatter(df, X, Y, path="fig.svg")
graph_line(df, X, Y, path="fig.svg")
graph_hist(df, Y, path="fig.svg", bins=30)
graph_coef(m, path="fig.svg")
// ASCII (terminal)
scatter(df, X, Y)
histogram(df, Y)
coefplot(m)
boxplot(df, Y)
kdensity(df, Y)
acfplot(df, Y, lags=20)
qqplot(df, Y)
corrplot(df, X1, X2, X3)
// Scalars
let x = 42 // int
let pi = 3.14 // float
let name = "Hayashi" // string
let ok = true // bool
// Constants (immutable)
const N = 1000
const TAX = 0.15
// Lists (push/pop mutate in-place, other ops return new list)
let nums = [1, 2, 3]
let doubled = nums |> map(|x| x * 2) // [2, 4, 6]
push(nums, 4) pop(nums) insert(nums, 0, 99) remove(nums, 1)
sort(nums) reverse(nums) unique(nums) flatten(nested)
slice(nums, 1, 3) index(nums, 2) join(nums, ", ") len(nums)
// Dicts (immutable — operations return new dict)
let d = {"name": "Alice", "age": 30}
display d["name"]
keys(d) values(d) has_key(d, "name")
dict_set(d, "city", "SP") dict_remove(d, "age") dict_merge(d1, d2)
// Build DataFrame from dict of lists
let df = dataframe({"x": [1, 2, 3], "y": [4, 5, 6]})
// Type predicates
is_int(42) // true
is_str("hello") // true
is_df(df) // true
// Type conversions
int(3.9) float(42) str(true) bool(0) type(x)
// Median
median([1, 3, 2])
median(df, price)
// If statement
if x > 0 {
display "positive"
} else if x == 0 {
display "zero"
} else {
display "negative"
}
// If expression (ternary — returns value)
let label = if x > 0 { "positive" } else { "negative" }
// Match (pattern matching — returns value)
let name = match code {
1 => "one",
2 => "two",
_ => "other"
}
// Loops
for i in 1..10 { display i }
for v in ["X1", "X2"] { eststo(ols("Y ~ " + v, df)) }
let r = parallel for v in list, threads=4 { eststo(ols("Y ~ " + v, df)) }
while cond { ... }
// Try/catch
try {
load "data.csv" as df
} catch e {
display f"Error: {e}"
}
// Named functions (parameters are const by default)
fn square(x) { return x * x }
fn add(a, b) {
let result = a + b
return result
}
// Closures (anonymous, capture outer scope)
let doubled = map([1, 2, 3], |x| x * 2)
let big = filter(nums, |x| x > 10)
let add = |a, b| a + b
// Module-based namespacing
import("finance") // finance::sharpe(), finance::sortino()
import("finance", as=fin) // fin::sharpe()
import("finance", only=["sharpe"]) // sharpe() directly
// Qualified calls
let ratio = finance::sharpe(ret, rf)
// String interpolation
let msg = f"mean = {mu:.2f}, n = {n}, p = {p:.4e}"
// Template strings (t-strings) re-evaluate the generated text as code
let n = 1
let x1 = 42
let y = t"x{n}" // y == 42, as if you had written x1
let gen_y = "x1 + x2 + x3"
generate df y = t"{gen_y}" // creates column y from x1 + x2 + x3
// Pipe operator (|>)
[5, 3, 1, 4, 2] |> sort |> reverse |> map(|x| x * 10)
df |> ols(lw ~ yos, _) // passes df to the '_' placeholder position
// In operator (membership test)
if 3 in [1, 2, 3] { ... }
if "key" in dict { ... }
if "lo" in "hello" { ... }
// Substring / membership
contains("hello", "ell") // true
// Regex
regexm(s, "[0-9]+") // match → bool
regexr(s, "[0-9]+", "NUM") // replace first
regexra(s, "[0-9]+", "NUM") // replace all
regexs(s, "([0-9]+\\.[0-9]+)") // extract capture
generate df is_dr = regexm(name, "^Dr")
// Panel
xtset(df, firm, year)
let m_fe = fe(Y ~ X1 + X2, df)
let m_re = re(Y ~ X1 + X2, df)
hausman(m_fe, m_re)
// Finance
fmb(ret ~ beta + size + bm, df, time=month, nw=4)
portsort(df, ret, beta, n=5)
doublesort(df, ret, size, bm, n1=5, n2=5)
let x = 10 // mutable
const PI = 3.14 // immutable — error on reassign
// Block scoping — variables die at }
if true {
let temp = 42 // dies here
x = x + temp // modifies outer x (assign without let)
}
// Function parameters are const (immutable input, result out)
fn f(n) {
// n = 99 // ERROR: cannot reassign const 'n'
let result = n + 1
return result
}
// DataFrames use copy-on-write (Rc) — zero-copy in functions
quietly on // suppress automatic output from here
quietly off // restore automatic output
quietly(ols(Y ~ X, df)) // (deprecated) suppress one expression
capture(ols(Y ~ X, df)) // ignore errors
assert(n > 0, "empty data")
timer(ols(Y ~ X, df)) // time execution
set_seed(42) // reproducibility
source("other_script.hay") // run another script
help(ols) // help() has ~210 topics with examples
help(about) // project info (version, license, author)
print("x =", x, "y =", y) // multi-arg with sep= and end=
file_exists("cache/data.csv") // bool
ensure_dir("cache") // create directory if missing
write("text", "note.txt") // write string to file
print("a", "b", sep=", ") // a, b
// Native & Script plugins — installed to ~/.hay/packages/
import("finance") // finance::sharpe(), finance::sortino()
import("finance", as=fin) // fin::sharpe()
// Install script or native plugin from GitHub (-y to bypass overwrite prompt)
// $ hay install user/repo [version] [-y]
install("user/repo") // callable from inside a Hayashi script
install("user/repo", version="v1.2.3")
install("user/repo", force=true) // overwrite if already installed
// Uninstall a package (successfully deletes native plugin files, dirs, and metadata)
// $ hay remove user/repo
// List installed packages
// $ hay list
// Check integrity/version of installed packages with remote GitHub repository
// $ hay check-plugin [user/repo]
// Update one or all packages to their latest versions (-y to bypass prompt)
// $ hay update [user/repo] [-y]
// Plugin search paths
plugin_path("/shared/plugins", "/team/lib")
Packages, imports, and auto-loaded plugins execute Hayashi/native code in your session. Install and import only code you trust; see the Trust Model.
Native plugins (.so/.dll/.dylib / .wasm) are fully supported, enabling third parties to ship optimized estimators, spatial packages, and data connectors via Hayashi's namespace system (using the hayashi-plugin-sdk). Closed-source proprietary plugins are legally permitted through Hayashi's GPL-3.0 Linking Exception.
Available plugins:
| Plugin | Description |
|---|---|
| hayahoo | Yahoo Finance data (quotes, history, search, info) |
| hayfred | FRED (Federal Reserve Economic Data) connector |
| hayplot | Plotting (scatter, line, bar, histogram, box, area) via Arrow FFI |
| haytex | LaTeX snippet generator (regression tables, equations, summary stats, correlation matrices) |
cargo build --release # optimized binary -> target/release/hay
cargo test # 428 tests, <1s60 example scripts in examples/, all passing.
Hayashi provides rich error diagnostics:
error: line 3: undefined variable 'preco_total'
3 │ display preco_total
│ ^^^^^^^^^^^^^^^^^^^
error: line 6: undefined function 'sumarize' — did you mean 'summarize'?
6 │ sumarize(df)
│ ^^^^^^^^^^^^
error: line 2: undefined variable 'factor'
Stack trace:
in calculate() at line 2
in process() at line 5
error: line 1: expected DataFrame, got Int
1 │ summarize(42)
│ ^^^^^^^^^^^^^
Flávio de Vasconcellos Corrêa — @sheep-farm — flavio.vcorrea@ufpel.edu.br
GPL-3.0 with Plugin Exception — see LICENSE.
This exception explicitly allows linking and loading proprietary/closed-source plugins developed using hayashi-plugin-sdk into Hayashi without triggering copyleft requirements.