Source code for biogeme.model_elements.database_adapter
"""
Database adapters (strategy objects) for ModelElements.
This module provides a small adapter interface that encapsulates the
preparation and exposure of the working database used to evaluate model
expressions, together with a factory for the corresponding
:class:`~biogeme.expressions_registry.ExpressionRegistry`.
Two concrete adapters are provided:
* :class:`RegularAdapter` – uses the database as-is (no transformation).
* :class:`FlatPanelAdapter` – flattens a panel database and updates
expressions with the maximum number of observations per individual.
These adapters allow the :class:`ModelElements` container to remain generic
and free of conditional logic about database variants.
Michel Bierlaire
Tue Nov 11 2025, 17:40:56
"""
from __future__ import annotations
from typing import Protocol
from biogeme.database import Database, PanelDatabase
from biogeme.default_parameters import MISSING_VALUE
from biogeme.exceptions import BiogemeError
from biogeme.expressions import Expression
from biogeme.expressions_registry import ExpressionRegistry
[docs]
class ModelElementsAdapter(Protocol):
"""
Strategy interface for providing a working database and registry.
Any implementation must be able to prepare its internal state given
the model expressions, expose the database on which expressions will
be evaluated, and build an :class:`ExpressionRegistry` bound to that
database.
Methods
-------
prepare(expressions)
Perform any one-time preparation needed before evaluation
(e.g., flatten a panel DB, set expression metadata).
database
The :class:`~biogeme.database.Database` against which expressions
are to be evaluated.
build_registry(expressions)
Construct an :class:`~biogeme.expressions_registry.ExpressionRegistry`
connected to the working database.
sample_size
Number of data rows in the working database.
number_of_observations
Number of original observations in the source database
(this may differ from ``sample_size`` for flattened panel data).
:param expressions: Mapping of expression names to :class:`~biogeme.expressions.Expression`
instances. Implementations may use this to attach metadata.
"""
[docs]
def prepare(self, expressions: dict[str, Expression]) -> None: ...
@property
def database(self) -> Database: ...
self, expressions: dict[str, Expression]
) -> ExpressionRegistry: ...
@property
def sample_size(self) -> int: ...
@property
def number_of_observations(self) -> int: ...
[docs]
class RegularAdapter:
"""
Adapter for a regular (non-panel or non-flattened) database.
This adapter is a thin wrapper around a :class:`~biogeme.database.Database`
instance and performs no transformation.
:note:
* ``sample_size`` equals ``number_of_observations``.
* :meth:`prepare` is a no-op.
"""
def __init__(self, database: Database | None):
"""Ctor
:param database: Input database. If ``None``, a dummy database is created.
"""
self._db: Database = database or Database.dummy_database()
[docs]
def prepare(self, expressions: dict[str, Expression]) -> None:
"""
No-op preparation for regular databases.
:param expressions: Mapping of expression names to expressions (unused).
"""
return
@property
def database(self) -> Database:
"""Working database (the input database)."""
return self._db
[docs]
def build_registry(self, expressions: dict[str, Expression]) -> ExpressionRegistry:
"""
Build an :class:`ExpressionRegistry` bound to the working database.
:param expressions: Mapping of expression names to expressions.
:return: Registry bound to :pyattr:`database`.
:rtype: ExpressionRegistry
"""
return ExpressionRegistry(expressions.values(), self._db)
@property
def sample_size(self) -> int:
"""Number of rows in the working database."""
return self._db.num_rows()
@property
def number_of_observations(self) -> int:
"""Number of observations in the source database."""
return self._db.num_rows()
[docs]
class FlatPanelAdapter:
"""
Adapter that flattens a panel database for expression evaluation.
This adapter converts an input panel database into a flat database
using :class:`~biogeme.database.PanelDatabase`. It also updates each
expression with the maximum number of observations per individual.
:note:
* After :meth:`prepare`, :pyattr:`database` points to the flat database.
* ``sample_size`` is the number of rows in the flat database.
``number_of_observations`` is the number of rows in the original database.
"""
def __init__(self, database: Database | None):
"""Ctor
:param database: Input *panel* database to be flattened. If ``None``, a dummy database
is created. A :class:`BiogemeError` is raised if the database is not
marked as panel.
:ivar _orig: Original database passed at construction.
:ivar _panel: :class:`PanelDatabase` used to perform the flattening.
:ivar _flat: Resulting flat :class:`Database` after :meth:`prepare` is called.
:ivar _max_obs: Maximum number of observations per individual computed during
flattening.
:raises BiogemeError: If the supplied database is not a panel database.
"""
self._orig: Database = database or Database.dummy_database()
if not self._orig.is_panel():
raise BiogemeError("FlatPanelAdapter requires a panel database.")
self._panel = PanelDatabase(
database=self._orig, panel_column=self._orig.panel_column
)
self._flat: Database | None = None
self._max_obs: int | None = None
[docs]
def prepare(self, expressions: dict[str, Expression]) -> None:
"""
Flatten the panel database and update expressions.
:param expressions: Mapping of expression names to expressions. Each expression is
informed of the maximum number of observations per individual
via ``set_maximum_number_of_observations_per_individual``.
"""
flat_df, max_obs = self._panel.flatten_database(missing_data=MISSING_VALUE)
self._flat = Database(name=f"flat {self._orig.name}", dataframe=flat_df)
self._max_obs = max_obs
for expr in expressions.values():
expr.set_maximum_number_of_observations_per_individual(max_number=max_obs)
@property
def database(self) -> Database:
"""
Working database, i.e., the flattened database.
:return: The flat database created in :meth:`prepare`.
:rtype: Database
:raises BiogemeError: If :meth:`prepare` has not been called yet.
"""
if self._flat is None:
raise BiogemeError("Flat database not prepared. Call 'prepare' first.")
return self._flat
[docs]
def build_registry(self, expressions: dict[str, Expression]) -> ExpressionRegistry:
"""
Build an :class:`ExpressionRegistry` bound to the flat database.
:param expressions: Mapping of expression names to expressions.
:return: Registry bound to :pyattr:`database`.
:rtype: ExpressionRegistry
"""
return ExpressionRegistry(expressions.values(), self.database)
@property
def sample_size(self) -> int:
"""
Number of rows in the working (flat) database.
:return: Number of rows.
:rtype: int
"""
return self.database.num_rows()
@property
def number_of_observations(self) -> int:
"""
Number of observations in the original (panel) database.
:return: Number of observations.
:rtype: int
"""
return self._orig.num_rows()