> ## Documentation Index
> Fetch the complete documentation index at: https://docs.effectiveai.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Books and studies

> Why a book holds facts that belong to every carrier, and why a study is the one place where versions of a book exist.

A book holds what was reported about each household. A study holds each version of the book that
you want to price. This split keeps reported facts, assumptions and sampled values apart, so each
premium traces back to its source.

```mermaid theme={null}
flowchart LR
  reported["Reported facts"] --> book["Book"]
  assumed["Assumptions you choose"] --> scenario["Scenario"]
  unknown["Unknown facts"] --> draw["Draw, marked modeled"]
  book --> study["Study"]
  scenario --> study
  draw --> study
```

## A book belongs to every carrier

Each carrier asks for the same household in its own shape. One carrier wants an age, and another
wants years licensed. The book stores the fact behind both: a birth date and a first licensed
date. Each program derives what its carrier asks for. One book then serves each carrier, and a
new carrier needs a new `prepare` only.

For the same reason, a core column holds a fact with one meaning, such as `birth_date`. A value
that a rater derives, such as an age or a violation class, belongs to the program that derives it.
Keep additional facts in columns or tables of your own. The programs that need those facts read them there.

## A line is a schema

A line of business is a schema object. `Book(tables, schema=ppa.SCHEMA)`
checks the tables against the PPA schema: the required tables, the keys and the core columns.
The examples use the included personal auto schema, `ppa.SCHEMA`. Extend it with typed columns and tables for your own programs.
A plain book, `Book(tables)`, keeps arbitrary tables and infers keys from `_id` columns.
Studies and the rating examples require a line schema, supplied explicitly when you construct or load the book.

`print(book.schema)` shows the line's tables and columns, then your own. Each column's `origin` is
`line`, `declared` or `observed`. An observed column has the type its values have. The
[PPA book schema](/relativities/reference/book-schema) lists each table, key and core column of `ppa.SCHEMA`.

## Declare your own columns

A column that only your programs read can still have a type. Declare it on the schema, and each
book built with that schema types and checks it like a core column. The result is the same line,
so studies and programs take the book.

```python theme={null}
from relativities import Book, BookError, Column, Table, ppa

schema = ppa.SCHEMA.with_columns(
    drivers={"credit_score": Column("int", required=True, meaning="The driver's insurance score.")}
).with_tables(
    vehicle_symbols=Table(("submission_id", "vehicle_id"), {"symbol": Column("text", required=True)})
)
```

A book reports each problem at once, in one `BookError`. Each problem names its table, column and
rule, counts the rows that break it, and shows up to five of their keys. `error.to_pandas()` holds
the same problems as rows.

```text theme={null}
The book has 2 problems:
- drivers.credit_score must be a whole number ('high' is not a number): submission_id='S1', driver_id='D1' and 6 more rows.
- vehicle_symbols.symbol is required: submission_id='S1', vehicle_id='V1'.
```

Declared columns receive the same typing and validation as core columns.
Pandas stores whole numbers as `Int64`, numbers as `Float64`, dates as `datetime64`, and booleans as `boolean`.
Use a text column for codes whose spelling matters, including alphanumeric vehicle symbols and codes with leading zeros.

## A book knows where it came from

`book.source` is the directory `Book.read_csv` read, a URI you pass as `source=`, or `None` for a
book built in code. A changed book, such as one from `with_tables`, has the source `None`,
because the source holds the earlier book. `book.hash` is a SHA-256 hash of the tables as `to_csv` writes them, so
the same content has the same hash wherever it came from. A result or a study can then name the
exact book it priced.

## A book stays the same

A book is checked once, when you build it, and stays the same after that. Each table property
returns a copy. To change a book, you change a copy in pandas and build a new book with
`with_tables`. Each result then refers to exactly one set of facts, and an earlier book stays valid
beside a later one.

## A study is a recipe

A study stores how to build each version of the book, and builds one version at a time. Memory
holds one version, the base book and the current scenario, however large the grid.

```text theme={null}
base book ── steps ──► base ──┬── baseline ─────► draw 0, 1 .. n
(source, hash)                ├── scenario "a" ──► draw 0, 1 .. n
                              └── scenario "b" ──► draw 0, 1 .. n
```

| Part | What it is | What it may change |
| - | - | - |
| Book | The book's source, hash and schema | - |
| Steps | Functions that run first, in order, such as a resample of submissions | Any row |
| Scenarios | Named functions from the base book to a changed book | Rows inside each submission |
| Draws | One sampler, a count and a seed; draw 0 is each scenario as reported | Values in the sampler fills |

A study refers to each step by its import path and its parameters, such as
`{"fn": "scenarios:age_drivers", "params": {"years": 1}}`. Define each step at the top level of a
module. `study.to_json()` writes the recipe, and `Study.from_json` reads it back. The same JSON
and the same code build books with the same hashes.

```python theme={null}
import pandas as pd

from relativities import Step, Study, sampler

book = Book.read_csv("relativities-examples/ppa/book", schema=ppa.SCHEMA)


def raise_deductible(book, before, after):
    coverages = book.vehicle_coverages
    chosen = coverages["coverage_id"].eq("collision") & coverages["deductible"].eq(before)
    coverages.loc[chosen, "deductible"] = after
    return book.with_tables(vehicle_coverages=coverages)


@sampler(fills=["drivers.first_licensed_date"])
def first_licensed(book, rng):
    drivers = book.drivers
    blank = drivers["first_licensed_date"].isna()
    years_ago = rng.integers(1, 20, size=len(drivers))
    licensed = pd.to_datetime([f"{2026 - int(years)}-01-15" for years in years_ago])
    drivers.loc[blank, "first_licensed_date"] = licensed[blank.to_numpy()]
    return book.with_tables(drivers=drivers)


study = Study(
    book,
    scenarios={"higher_deductible": Step(raise_deductible, before=500, after=1_000)},
    sampler=first_licensed,
    draws=2,
    seed=7,
)
study.grid  # (('baseline', 0), ('baseline', 1), ('baseline', 2), ('higher_deductible', 0), ...)
again = Study.from_json(study.to_json())
again.book("higher_deductible", 2).hash == study.book("higher_deductible", 2).hash  # True
```

A study checks each version as it builds it:

* A scenario returns a book of the same schema, with the same submissions in the same order.
* A draw keeps each row in its place and changes only the columns its sampler `fills`.
* A draw keeps each reported value present. It may change a reported value, which is then
  `modeled`.
* `Study.from_json` checks the book's hash, so a study reads only the data it was written for.

## Scenarios and draws answer different questions

| Version | What it answers | Example |
| - | - | - |
| Scenario | What happens under an assumption you choose | A \$1,000 collision deductible on each vehicle |
| Draw | How much the answer depends on facts the book leaves empty | Annual mileage sampled for each blank mileage |

A sampler names the columns it fills. Each of these columns gets a `<column>_source` column,
`reported` or `modeled`, in each version, so a modeled value stays visible as modeled.

Draw `k` uses the same random stream in each scenario. This is the method of common random
numbers. A fact that a scenario leaves unchanged gets the same modeled value in each scenario, which reduces sampling noise in paired scenario comparisons.
Keep the sampling procedure and row order consistent across scenarios to preserve that pairing.

## Where a value goes

| Kind of value | Where it goes | Example |
| - | - | - |
| What was reported | The book | A driver's birth date from the application |
| An assumption you choose | A scenario | A proxy credit score for each driver, or an added driver |
| An unknown you sample | A draw | Annual mileage left blank on the application |
| One carrier's scale or rule | That program's `prepare` | The carrier's vehicle symbol or credit tier |

## A study works for each line

A study keeps the schema of the book you give it. The scenario and draw checks read the book's
tables, so a book for another line of business follows the same rules.

## A sampler draws one stream per draw

Draw `k` gives the sampler the same generator in each scenario. A sampler that draws one value per
row, in row order, gives an unchanged row the same value in each scenario. A scenario that adds a
row moves later values in the stream, so draw by row key when a scenario adds rows.

## The book defines what a comparison represents

A market basket represents the profiles and weights you selected. A portfolio extract represents its observed mix, subject to the retained rating population.
Carry your own segment dimensions through the book and join them to results by key.
Report matched counts and rating exceptions beside premium statistics so each result has a clear population.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.