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

# Fill unknown facts

> Fill blank facts with sampled draws, rate every draw with every carrier, and report how often the ranking holds and how wide the gap runs.

Find out whether a carrier ranking holds when the book leaves facts blank, such as annual mileage.

[Install Relativities and download the example data](/relativities/setup) before running this page.

<Accordion title="Set up the book and the two carriers">
  The book is the synthetic six-household book in `relativities-examples/ppa/book`. Carrier A
  rates liability and collision per vehicle for a six-month term. Carrier B rates an account for a
  twelve-month term and returns one premium. [Compare carriers](/relativities/how-to/compare-carriers) shows
  how to check what each carrier receives.

  ```python theme={null}
  from pathlib import Path

  import pandas as pd

  from relativities import Book, Market, Program, Rater, Study, ppa, sampler

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


  VEHICLE = ["submission_id", "vehicle_id"]


  def years(start, on):
      """Whole years from each start date to each on date."""
      before = on.dt.strftime("%m%d") < start.dt.strftime("%m%d")
      return on.dt.year - start.dt.year - before.astype(int)


  def on_effective_date(book):
      """Each driver with their submission's effective date."""
      return book.drivers.merge(book.submissions[["submission_id", "effective_date"]])


  def nest(book, **lists):
      """Each submission's input: one list of records per keyword, in book order."""
      grouped = {
          name: {sid: rows.drop(columns="submission_id").to_dict("records") for sid, rows in frame.groupby("submission_id", sort=False)}
          for name, frame in lists.items()
      }
      return {sid: {name: grouped[name].get(sid, []) for name in lists} for sid in book.submissions["submission_id"]}


  def carrier_a(book):
      drivers = on_effective_date(book)
      drivers["age"] = years(drivers["birth_date"], drivers["effective_date"])
      coverages = book.vehicle_coverages
      collision = coverages[coverages["coverage_id"] == "collision"][[*VEHICLE, "deductible"]]
      vehicles = book.vehicles.merge(collision, on=VEHICLE, how="left")
      vehicles["collision_deductible"] = vehicles["deductible"].fillna(0).astype(int)
      return nest(
          book,
          drivers=drivers[["submission_id", "age"]],
          vehicles=vehicles[["submission_id", "annual_mileage", "collision_deductible"]],
      )


  def carrier_b(book):
      drivers = on_effective_date(book)
      blank = drivers.loc[drivers["first_licensed_date"].isna(), "driver_id"]
      if len(blank):
          raise ValueError(f"driver {blank.iloc[0]} has no first_licensed_date")
      drivers["years_licensed"] = years(drivers["first_licensed_date"], drivers["effective_date"])
      coverages = book.vehicle_coverages
      comprehensive = coverages[coverages["coverage_id"] == "comprehensive"][VEHICLE]
      autos = book.vehicles.merge(comprehensive.assign(comprehensive=True), on=VEHICLE, how="left")
      autos["comprehensive"] = autos["comprehensive"].eq(True)
      autos = autos.rename(columns={"annual_mileage": "miles"})
      return nest(
          book,
          operators=drivers[["submission_id", "years_licensed"]],
          autos=autos[["submission_id", "miles", "comprehensive"]],
      )


  market = Market(
      {
          "a": Program(Rater.load(Path("relativities-examples/ppa/carrier-a.zip")), carrier_a, schema=ppa.SCHEMA),
          "b": Program(Rater.load(Path("relativities-examples/ppa/carrier-b.zip")), carrier_b, schema=ppa.SCHEMA),
      }
  )
  ```
</Accordion>

```mermaid theme={null}
flowchart LR
  book["Book: blanks stay blank"] --> draw0["Draw 0: as reported"]
  book --> sample["sampler with a seeded generator"]
  sample --> draws["Draws 1 to 100: modeled values"]
  draw0 --> rate["market.rate"]
  draws --> rate
  rate --> summary["Share of draws and gap range"]
```

## Mark the unknown facts

Set each unknown fact to a blank in the book. Keep liability alone, so both carriers price the
same coverage.

```python theme={null}
vehicles = book.vehicles
vehicles.loc[vehicles["vehicle_id"].isin(["V3", "V6"]), "annual_mileage"] = pd.NA
liability = book.vehicle_coverages.query("coverage_id == 'liability'")
reported = book.with_tables(vehicles=vehicles, vehicle_coverages=liability)
reported.vehicles.query("submission_id in ['S2', 'S5']")[["submission_id", "vehicle_id", "annual_mileage"]]
#   submission_id vehicle_id  annual_mileage
# 1            S2         V2            6000
# 2            S2         V3            <NA>
# 5            S5         V6            <NA>
```

V3 and V6 now have a blank annual mileage. S5's driver D6 already has a blank first licensed
date. [Books and studies](/relativities/explanation/books-and-studies) describes why the book keeps facts as
reported.

## Write a sampler

Write a function that receives one scenario as a book and a seeded NumPy generator. Make it return
the same rows, in order, with the blanks filled. Name each column it fills in `fills`.

```python theme={null}
@sampler(fills=["vehicles.annual_mileage", "drivers.first_licensed_date"])
def fill_unknowns(book, rng):
    vehicles = book.vehicles
    blank = vehicles["annual_mileage"].isna()
    vehicles.loc[blank, "annual_mileage"] = rng.integers(4_000, 16_001, size=int(blank.sum()))
    drivers = book.drivers
    blank = drivers["first_licensed_date"].isna()
    births = pd.to_datetime(drivers.loc[blank, "birth_date"])
    ages = rng.integers(16, 34, size=int(blank.sum()))
    drivers.loc[blank, "first_licensed_date"] = [
        birth + pd.DateOffset(years=int(age)) for birth, age in zip(births, ages, strict=True)
    ]
    return book.with_tables(vehicles=vehicles, drivers=drivers)
```

This function draws mileage uniformly from 4,000 to 16,000 miles a year. It draws a licensing age
from 16 to 33. Put your own model in its place.

## Make the draws

Give the [`Study`](/relativities/reference/relativities#study) the sampler, the number of draws and a seed.
A study is a recipe: `study.book(scenario, draw)` builds one version of the book.

```python theme={null}
study = Study(reported, sampler=fill_unknowns, draws=100, seed=7)
study  # Study(book in memory: 1 scenario, 100 draws, 101 books)
v6 = [study.book("baseline", draw).vehicles.set_index("vehicle_id").loc["V6"] for draw in range(3)]
pd.DataFrame(v6, index=range(3))[["annual_mileage", "annual_mileage_source"]]
#   annual_mileage annual_mileage_source
# 0           <NA>              reported
# 1          13242               modeled
# 2           7335               modeled
```

Draw 0 is the book as reported, and draws 1 to 100 are sampled. Each column in `fills` gets a
`_source` column with `reported` or `modeled`. The same seed gives the same draws.

## Rate every draw

Rate the study with the market. Then put both premiums on a twelve-month term: double carrier A's
six-month premium.

```python theme={null}
rated = market.rate(study)
rated["annual"] = rated["premium"].mul(2).where(rated["program"] == "a", rated["annual_premium"])
rated.query("draw_id == 0 and status != 'succeeded'")[["submission_id", "program", "status", "reason_code"]]
#   submission_id program          status       reason_code
# 2            S2       a  invalid_inputs        INPUT-TYPE
# 3            S2       b  invalid_inputs        INPUT-TYPE
# 5            S3       b         refused  MILES_OVER_LIMIT
# 6            S4       a  invalid_inputs       INPUT-RANGE
# 7            S4       b         refused  MILES_OVER_LIMIT
# 8            S5       a  invalid_inputs        INPUT-TYPE
# 9            S5       b  mapping_failed    PREPARE_FAILED
```

In draw 0, the blanks stay blank, so S2 and S5 stay unrated. Carrier B's `prepare` raises for the
blank date, so S5 has the status `mapping_failed`.

## Count how often the ranking holds

Compare the two annual premiums in each sampled draw that both carriers rate.

```python theme={null}
annual = rated.query("draw_id > 0").pivot(index=["submission_id", "draw_id"], columns="program", values="annual")
both = annual.dropna()
gap = (both["a"] - both["b"]).groupby("submission_id")
summary = pd.DataFrame(
    {
        "draws": gap.size(),
        "a_cheaper": (both["a"] < both["b"]).groupby("submission_id").mean(),
        "gap_min": gap.min(),
        "gap_median": gap.median(),
        "gap_max": gap.max(),
    }
)
summary
#                draws  a_cheaper  gap_min  gap_median  gap_max
# submission_id
# S1               100       0.00    120.0       120.0    120.0
# S2               100       0.00     24.0        90.0    174.0
# S5               100       0.12   -108.0        60.0    180.0
# S6               100       0.00    300.0       300.0    300.0
```

The gap is carrier A's annual premium less carrier B's. A positive gap means carrier B has the
lower premium.

* **S1 and S6** have complete facts, so every draw gives the same gap.
* **S2**: carrier B is cheaper in every draw. The gap runs from $24 to $174 a year with V3's mileage.
* **S5**: carrier B is cheaper in 88 of 100 draws. Report this ranking as a share of draws.

## Count the draws each carrier rates

```python theme={null}
annual.notna().groupby("submission_id").sum()  # draws each carrier rated
# program          a    b
# submission_id
# S1             100  100
# S2             100  100
# S3             100    0
# S4               0    0
# S5             100  100
# S6             100  100
```

Carrier B refuses S3 and S4 in every draw, and carrier A refuses S4. List these submissions with
their reasons from `rated`.

<Note>
  These ranges come from your sampling model. They show how much the answer depends on the blanks,
  under your assumptions. A different model gives different ranges. Treat the ranking frequency as a
  modeled share of draws.
</Note>

## Close the raters

```python theme={null}
for program in market.programs.values():
    program.close()
```


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