Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
675 changes: 675 additions & 0 deletions packages/preview/stargazing/0.1.0/LICENSE

Large diffs are not rendered by default.

84 changes: 84 additions & 0 deletions packages/preview/stargazing/0.1.0/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# stargazing

A Typst package that typesets regression tables from finished model results.

## What it does

The package does not estimate models. You run the regressions in Python, R, or Stata and save the results as JSON. The package reads the results and draws one table. The table has one column for each model. Each variable has a row for the coefficient and a row for the standard error. The package adds significance stars and a note below the table.

![Example table](examples/example.png)

## Requirements

* Typst 0.15.1 or later. The package was tested only with 0.15.1.

## Install

Import the package from Typst Universe in your document:

```typst
#import "@preview/stargazing:0.1.0": regression-table
```

Typst downloads the package when it compiles the document.

## Usage

Call `regression-table` with an array of models. Each model is a dictionary with these keys:

* `name`: the text in the column header.
* `coefficients`: a dictionary. Each key is a variable name. Each value has `coef` and `se`, and can have `p`.
* `stats`: a dictionary of numbers or text, for example `N`, `R2`, or `Fixed effects`.

Example. The file `examples/results.json` holds the data for the table above.

```typst
#import "@preview/stargazing:0.1.0": regression-table

#let data = json("results.json")

#regression-table(
data.models,
labels: data.labels,
stats: ("N", "R2", "Fixed effects"),
)
```

If a model has no `p`, the package computes the stars from the ratio `coef / se`. It uses the normal critical values 2.576, 1.960, and 1.645. A variable that a model does not contain gets an empty cell.

## Configuration

All settings are named arguments of `regression-table`.

| Argument | Default | Meaning |
|---|---|---|
| `labels` | `(:)` | Dictionary from a variable or stat key to the text shown. |
| `order` | `auto` | Variable keys to show, in order. The default is the order of first use. |
| `stats` | `auto` | Stat keys to show, in order. The default is the keys of the first model. |
| `digits` | `3` | Decimals for coefficients, standard errors, and decimal stats. |
| `levels` | `((0.01, "***"), (0.05, "**"), (0.1, "*"))` | Pairs of a limit and a mark. The strictest limit comes first. |
| `notes` | `auto` | `auto` writes the standard note. `none` removes the note. Content replaces the note. |

The keys `N`, `R2`, and `adj_R2` have the default labels "Observations", R², and "Adjusted R²". Integers print without decimals. Decimal numbers print with `digits` decimals.

The package exports two more functions: `format-number(x, digits: 3)` and `stars-for(entry, levels: ...)`.

## How it works

The file `lib.typ` holds all code. `format-number` rounds a number, pads the decimals with zeros, and writes a true minus sign. `stars-for` returns the mark for one coefficient. `regression-table` builds the cells row by row and passes them to one `table` call with horizontal rules at the top, below the header, above the statistics, and at the bottom.

To run the checks, compile the test file. The compile fails if an assertion fails:

```
typst compile --root . tests/test.typ /dev/null --format pdf
```

To rebuild the example image:

```
typst compile --root . examples/example.typ examples/example.png --ppi 150
```

## License

This project uses the GNU General Public License, version 3 or any later version. See the `LICENSE` file.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions packages/preview/stargazing/0.1.0/examples/example.typ
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#import "../lib.typ": regression-table

Check warning on line 1 in packages/preview/stargazing/0.1.0/examples/example.typ

View check run for this annotation

Typst package check / @preview/stargazing:0.1.0

packages/preview/stargazing/0.1.0/examples/example.typ#L1

This file seems to be an example, and should probably be added to `exclude` in your `typst.toml`.

Check warning on line 1 in packages/preview/stargazing/0.1.0/examples/example.typ

View check run for this annotation

Typst package check / @preview/stargazing:0.1.0

packages/preview/stargazing/0.1.0/examples/example.typ#L1

This import should use the package specification, not a relative path.

#let data = json("results.json")

#regression-table(
data.models,
labels: data.labels,
stats: ("N", "R2", "Fixed effects"),
)
30 changes: 30 additions & 0 deletions packages/preview/stargazing/0.1.0/examples/results.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"models": [
{
"name": "OLS",
"coefficients": {
"educ": {"coef": 0.092, "se": 0.007, "p": 0.0},
"exper": {"coef": 0.041, "se": 0.012, "p": 0.003},
"female": {"coef": -0.231, "se": 0.094, "p": 0.021}
},
"stats": {"N": 1203, "R2": 0.311, "Fixed effects": "No"}
},
{
"name": "FE",
"coefficients": {
"educ": {"coef": 0.081, "se": 0.009, "p": 0.0},
"exper": {"coef": 0.036, "se": 0.019, "p": 0.062}
},
"stats": {"N": 1203, "R2": 0.402, "Fixed effects": "Yes"}
},
{
"name": "IV",
"coefficients": {
"educ": {"coef": 0.119, "se": 0.031},
"exper": {"coef": 0.012, "se": 0.020}
},
"stats": {"N": 1203, "R2": 0.287, "Fixed effects": "No"}
}
],
"labels": {"educ": "Education (years)", "exper": "Experience (years)", "female": "Female"}
}
121 changes: 121 additions & 0 deletions packages/preview/stargazing/0.1.0/lib.typ
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Typeset regression tables. Statistics stay outside Typst: the caller
// passes finished results, and this package only lays them out.

// Format a number with a fixed count of decimals and a true minus sign.
#let format-number(x, digits: 3) = {
let r = calc.round(calc.abs(x), digits: digits)
let parts = str(r).split(".")
let frac = if parts.len() > 1 { parts.at(1) } else { "" }
while frac.len() < digits { frac += "0" }
let out = parts.at(0) + if digits > 0 { "." + frac }
if x < 0 and r != 0 { "−" + out } else { out }
}

// Default levels: p below the first value gets the second value.
#let default-levels = ((0.01, "***"), (0.05, "**"), (0.1, "*"))

// Return the star string for a result. Use `p` when present.
// Otherwise use the normal critical values 2.576, 1.960, and 1.645.
#let stars-for(entry, levels: default-levels) = {
let p = entry.at("p", default: none)
if p != none {
for (limit, mark) in levels {
if p < limit { return mark }
}
return ""
}
let t = calc.abs(entry.coef / entry.se)
let cut = (2.5758, 1.96, 1.6449)
for (i, c) in cut.enumerate() {
if t > c { return levels.at(i).at(1) }
}
""
}

#let default-labels = (
N: [Observations],
R2: [$R^2$],
adj_R2: [Adjusted $R^2$],
)

// Build a regression table.
//
// models: array of dictionaries. Each has `name` (header), `coefficients`
// (dictionary: variable -> (coef, se, p)), and `stats` (dictionary).
// labels: dictionary that maps a variable or stat key to display content.
// order: variable keys to show, in order. Default: order of first use.
// stats: stat keys to show, in order. Default: keys of the first model.
// digits: decimals for coefficients and stats.
// levels: significance levels as (limit, mark) pairs, strictest first.
// notes: `auto` for the standard note, `none` for no note, or content.
#let regression-table(
models,
labels: (:),
order: auto,
stats: auto,
digits: 3,
levels: default-levels,
notes: auto,
) = {
let label(key) = labels.at(key, default: default-labels.at(key, default: key))

let vars = if order != auto { order } else {
let seen = ()
for m in models {
for key in m.coefficients.keys() {
if key not in seen { seen.push(key) }
}
}
seen
}
let stat-keys = if stats != auto { stats } else { models.first().at("stats", default: (:)).keys() }

let cols = models.len() + 1
let cell(x) = if type(x) == int { str(x) } else if type(x) == float { format-number(x, digits: digits) } else { x }

let rows = ()
for v in vars {
rows.push(label(v))
for m in models {
let e = m.coefficients.at(v, default: none)
rows.push(if e == none { [] } else { [#format-number(e.coef, digits: digits)#super(stars-for(e, levels: levels))] })
}
rows.push([])
for m in models {
let e = m.coefficients.at(v, default: none)
rows.push(if e == none { [] } else { [(#format-number(e.se, digits: digits))] })
}
}

let stat-rows = ()
for k in stat-keys {
stat-rows.push(label(k))
for m in models {
let s = m.at("stats", default: (:)).at(k, default: none)
stat-rows.push(if s == none { [] } else { cell(s) })
}
}

let note = if notes == auto {
let marks = levels.map(((limit, mark)) => [#mark~$p < #limit$])
[_Notes:_ Standard errors in parentheses. #marks.join([, ])]
} else { notes }

block(breakable: false, {
table(
columns: (auto,) + (1fr,) * models.len(),
align: (left,) + (center,) * models.len(),
stroke: none,
inset: (x: 6pt, y: 3pt),
table.hline(stroke: 1pt),
[], ..models.enumerate(start: 1).map(((i, _)) => [(#i)]),
[], ..models.map(m => m.name),
table.hline(stroke: 0.5pt),
..rows,
table.hline(stroke: 0.5pt),
..stat-rows,
table.hline(stroke: 1pt),
)
if note != none { align(left, text(size: 0.85em, note)) }
})
}
23 changes: 23 additions & 0 deletions packages/preview/stargazing/0.1.0/tests/test.typ
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Compile this file. The compile fails if an assertion fails.
#import "../lib.typ": format-number, stars-for, regression-table

Check warning on line 2 in packages/preview/stargazing/0.1.0/tests/test.typ

View check run for this annotation

Typst package check / @preview/stargazing:0.1.0

packages/preview/stargazing/0.1.0/tests/test.typ#L2

This import should use the package specification, not a relative path.

#assert.eq(format-number(0.5), "0.500")
#assert.eq(format-number(-0.231), "−0.231")
#assert.eq(format-number(2, digits: 0), "2")
#assert.eq(format-number(-0.0001), "0.000")
#assert.eq(format-number(1.23456, digits: 2), "1.23")

#assert.eq(stars-for((coef: 1, se: 1, p: 0.005)), "***")
#assert.eq(stars-for((coef: 1, se: 1, p: 0.03)), "**")
#assert.eq(stars-for((coef: 1, se: 1, p: 0.08)), "*")
#assert.eq(stars-for((coef: 1, se: 1, p: 0.2)), "")
// Without p, the t value decides.
#assert.eq(stars-for((coef: 3, se: 1)), "***")
#assert.eq(stars-for((coef: 2, se: 1)), "**")
#assert.eq(stars-for((coef: 1.7, se: 1)), "*")
#assert.eq(stars-for((coef: 1, se: 1)), "")

#regression-table((
(name: "A", coefficients: (x: (coef: 1.0, se: 0.1, p: 0.001)), stats: (N: 10, R2: 0.5)),
(name: "B", coefficients: (y: (coef: -2.0, se: 0.5, p: 0.2)), stats: (N: 12, R2: 0.25)),
))
13 changes: 13 additions & 0 deletions packages/preview/stargazing/0.1.0/typst.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "stargazing"
version = "0.1.0"
entrypoint = "lib.typ"
authors = ["Orpheus-21 <@Orpheus-21>"]
license = "GPL-3.0-or-later"
description = "Typeset regression tables from model results."
repository = "https://github.com/Orpheus-21/stargazing"
keywords = ["econometrics", "regression", "table", "statistics"]
categories = ["model"]
disciplines = ["economics"]
compiler = "0.15.1"
exclude = ["examples/example.png", "tests"]
Loading