> ## 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.

# Rate a study with a program

> Supply the facts your book lacks as scenarios, rate every version with one program, and see how much each assumption moves the premium.

A program rates only the facts its rater accepts. When your book lacks a fact the rater requires,
each submission the rater would accept stops as `invalid_inputs`:

```python theme={null}
from relativities import Program

program = Program.load("effective://example-ca-ppa")
book = program.example_book
thin = book.with_tables(drivers=lambda drivers: drivers.drop(columns=["first_licensed_date"]))
results = program.rate(thin)
results["status"].value_counts().to_dict()
# {'invalid_inputs': 97065, 'refused': 2935}
```

`thin` is the example book with its licence dates dropped. To rate it, write each missing fact as a scenario,
and rate the study with the program:

```mermaid theme={null}
flowchart LR
  thin["Book: missing facts"] --> baseline["baseline: as reported"]
  thin --> s1["Scenario: one assumption"]
  thin --> s2["Scenario: another assumption"]
  baseline --> rate["program.rate"]
  s1 --> rate
  s2 --> rate
  rate --> rows["One row per submission and scenario"]
```

## Find the facts the book lacks

Each `invalid_inputs` row's `reason` names the rater field it lacks, then the book column that field
comes from. Count the rows by that column:

```python theme={null}
unrated = results.query("status == 'invalid_inputs'")
unrated["reason"].iloc[0]
# 'drivers[0].years_licensed: missing required input years_licensed (book: drivers.first_licensed_date)'
unrated["reason"].str.extract(r"\(book: (.+)\)")[0].value_counts(dropna=False)
# 0
# drivers.first_licensed_date    97065
# Name: count, dtype: int64
```

`prepare` derives `years_licensed` from each driver's `first_licensed_date`, so that column is the
fact to supply. A program traces book columns from its example book. A reason that ends at the
rater field counts as `NaN`, so read the field at the start of that `reason`. A `refused` row names the carrier rule that declines it, in `reason_code`.
`program.rater.inputs` marks each field the rater requires. An optional field means what its RSL
says: a filed default, such as 12,000 annual miles, or an absent coverage.

## Write each assumption as a scenario

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

from relativities import Study, scenario


@scenario("Licensed at {age}")
def licensed_at(book, age=16):
    """Date each driver's first licence `age` years after their birth date."""

    def dated(drivers):
        return drivers.assign(first_licensed_date=drivers["birth_date"] + pd.DateOffset(years=age))

    return book.with_tables(drivers=dated)


study = Study(thin, scenarios=[licensed_at, licensed_at(age=18), licensed_at(age=21)])
study.scenarios  # ('baseline', 'Licensed at 16', 'Licensed at 18', 'Licensed at 21')
```

Each scenario holds one assumption. Several values of one assumption bracket its effect on the premium.

## Rate the study

```python theme={null}
results = program.rate(study)
results.groupby("scenario_id", sort=False)["status"].value_counts().unstack(fill_value=0)
# status          invalid_inputs  refused  succeeded
# scenario_id
# baseline                 97065     2935          0
# Licensed at 16             281     2935      96784
# Licensed at 18             281     2935      96784
# Licensed at 21             281     2935      96784
```

`baseline` is the book as reported, so it keeps its unrated rows. In each scenario, 281
submissions stay unrated: 186 have a driver with a blank birth date, so the scenario leaves
`drivers.first_licensed_date` blank, and 95 have a vehicle with a blank garaging ZIP
(`vehicles.garaging_postal_code`).

## Compare the assumptions

```python theme={null}
premiums = results.query("status == 'succeeded'").pivot(index="submission_id", columns="age", values="premium").astype(float).dropna()
premiums.mean().round(0)
# age
# 16    2996.0
# 18    3101.0
# 21    3281.0
# dtype: float64
(premiums.sum() / premiums[18].sum() - 1).round(4)
# age
# 16   -0.0338
# 18    0.0000
# 21    0.0580
# dtype: float64
```

`dropna` keeps the policies that every scenario rates, so each total compares the same policies.
`premium` is each policy's annual premium. A later licence age gives fewer years licensed, so the
premium rises. The assumption moves the book's total by 9.5% from age 16 to age 21.

## Check an assumption against reported facts

To check an assumption, rate the policies whose facts you hold, and compare the totals:

```python theme={null}
reported = program.rate(book).set_index("submission_id")["premium"].astype(float)
both = premiums.join(reported.rename("reported")).dropna()
(both.sum() / both["reported"].sum() - 1).round(4)
# 16         -0.0872
# 18         -0.0552
# 21         -0.0004
# reported    0.0000
# dtype: float64
```

On these synthetic policies, a licence at age 21 gives the reported total within 0.04%. A single
assumption hides the spread between drivers. [Create a study](/relativities/how-to/create-a-study#fill-unknown-facts-in-draws)
fills blank facts in draws instead.


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