Skip to content

vic3_analysis ¤

vic3_analysis package.

Provides utilities and parsers for analysing Victoria 3 game data, including buildings, goods, production methods, technologies, and economic optimisation.

Modules:

  • analysis
  • parse
  • utils

    Utility helpers for locating the Victoria 3 game installation and parsing

analysis ¤

Modules:

  • production

    Economic production analysis for Victoria 3.

production ¤

Economic production analysis for Victoria 3.

Provides :class:ProductionUnit for representing per-building-level production data, :func:production_table for building a comprehensive DataFrame of all possible building configurations, and :class:ProductionAnalyzer for filtering and linear-programming optimisation of building portfolios.

Classes:

  • OptimizeResult

    Structured result of an optimisation run, including the optimal building levels and summary statistics.

  • ProductionAnalyzer

    Wraps a production table DataFrame and provides analysis/optimisation helpers.

  • ProductionUnit

    A dict-like snapshot of one building level's production statistics.

Functions:

  • production_table

    Build a DataFrame of all possible building configurations and their stats.

OptimizeResult ¤

OptimizeResult(
    key_index: List[str],
    level: ndarray[tuple[int], dtype[float64]],
    goods_index: List[str],
    net_goods: ndarray,
    profit: float,
    employment: float,
    construction_cost: float,
)

Structured result of an optimisation run, including the optimal building levels and summary statistics.

Parameters:

  • level ¤
    (ndarray[tuple[int], dtype[float64]]) –

    Optimal building levels as a 1-D array of shape (n_buildings,).

  • net_goods ¤
    (ndarray) –

    Net goods flows for the optimal allocation.

  • profit ¤
    (float) –

    Total profit for the optimal allocation.

  • employment ¤
    (float) –

    Total employment for the optimal allocation.

  • construction_cost ¤
    (float) –

    Total construction cost for the optimal allocation.

Methods:

  • gdp_per_capita

    Calculate GDP per capita for the optimal allocation.

Source code in src/vic3_analysis/analysis/production.py
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
def __init__(
    self,
    key_index: List[str],
    level: np.ndarray[tuple[int], np.dtype[np.float64]],
    goods_index: List[str],
    net_goods: np.ndarray,
    profit: float,
    employment: float,
    construction_cost: float,
):
    """Initialise the optimisation result.

    Args:
        level: Optimal building levels as a 1-D array of shape ``(n_buildings,)``.
        net_goods: Net goods flows for the optimal allocation.
        profit: Total profit for the optimal allocation.
        employment: Total employment for the optimal allocation.
        construction_cost: Total construction cost for the optimal allocation.
    """
    self.key_index = key_index
    self.level = level
    self.goods_index = goods_index
    self.net_goods = net_goods
    self.gdp = profit * 52  # Convert weekly profit to annual GDP
    self.employment = employment
    self.construction_cost = construction_cost
gdp_per_capita ¤
gdp_per_capita() -> float

Calculate GDP per capita for the optimal allocation.

Returns:

  • float

    GDP divided by employment, or float("inf") if employment is zero.

Source code in src/vic3_analysis/analysis/production.py
238
239
240
241
242
243
244
245
246
def gdp_per_capita(self) -> float:
    """Calculate GDP per capita for the optimal allocation.

    Returns:
        GDP divided by employment, or ``float("inf")`` if employment is zero.
    """
    if self.employment == 0:
        return float("inf")
    return self.gdp / self.employment

ProductionAnalyzer ¤

ProductionAnalyzer(
    game_dir: str | None = None, df: DataFrame | None = None
)

Wraps a production table DataFrame and provides analysis/optimisation helpers.

Attributes:

  • df

    The active (potentially filtered) production table.

  • df_raw

    An unmodified copy of the original production table, used by :meth:restore to reset any applied filters.

Parameters:

  • game_dir ¤
    (str | None, default: None ) –

    Path to the Victoria 3 game directory. Ignored when df is provided. If None the directory is located automatically via :func:~vic3_analysis.utils.get_vic3_directory.

  • df ¤
    (DataFrame | None, default: None ) –

    Pre-built production table DataFrame. When provided, game_dir is not used.

Methods:

  • add_throughput_bonus

    Add a throughput bonus to all configurations of a specific building.

  • constraint_limit_building

    Build an inequality constraint that limits levels of one building type.

  • constraint_limit_construction_cost

    Build an inequality constraint that caps total construction cost.

  • constraint_limit_employment

    Build an inequality constraint that caps total employment.

  • constraint_limit_import

    Build an inequality constraint that caps net imports of each good.

  • constraint_produce

    Build an inequality constraint requiring minimum production of a good.

  • construction_cost

    Calculate total construction cost for a given building-level allocation.

  • construction_cost_vector

    Return a 1-D NumPy array of construction costs for active rows.

  • employment

    Calculate total employment for a given building-level allocation.

  • employment_vector

    Return a 1-D NumPy array of per-level employment for active rows.

  • era_vector

    Return a 1-D NumPy array of era requirements for active rows.

  • filter_by_building_group

    Remove all configurations belonging to building_group.

  • filter_by_era

    Keep only building configurations unlocked before era.

  • filter_by_production_method

    Remove configurations that include a specific production method.

  • find_same_building_group

    Return DataFrame indices of all configurations in a building group.

  • find_same_buildings

    Return DataFrame indices of all configurations for a given building.

  • goods_index

    Return the list of good-key column names in the active DataFrame.

  • goods_matrix

    Return a NumPy matrix of goods flows for all active rows.

  • key_index

    Return the list of building-configuration keys for active rows.

  • linprog

    Solve a linear programme over building levels.

  • net_goods

    Calculate net goods flows for a given building-level allocation.

  • production_index

    Return the list of production-related column names.

  • production_matrix

    Return a NumPy matrix of all production values for active rows.

  • profit

    Calculate total profit for a given building-level allocation.

  • profit_per_capita

    Calculate profit per capita for a given building-level allocation.

  • profit_vector

    Return a 1-D NumPy array of per-level profits for active rows.

  • restore

    Reset self.df to the original unfiltered production table.

Source code in src/vic3_analysis/analysis/production.py
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
def __init__(self, game_dir: str | None = None, df: pd.DataFrame | None = None):
    """Initialise the analyser.

    Args:
        game_dir: Path to the Victoria 3 ``game`` directory.  Ignored when
            *df* is provided.  If ``None`` the directory is located
            automatically via
            :func:`~vic3_analysis.utils.get_vic3_directory`.
        df: Pre-built production table DataFrame.  When provided,
            *game_dir* is not used.
    """
    if df is not None:
        self.df = df
    else:
        self.df = production_table(game_dir)
    self.df_raw = self.df.copy()  # Keep a copy of the raw DataFrame for reference
add_throughput_bonus ¤
add_throughput_bonus(
    building_key: str, bonus_multiplier: float
)

Add a throughput bonus to all configurations of a specific building.

This method modifies the active DataFrame in-place, increasing the profit and net goods of all configurations of building_key by multiplying them by bonus_multiplier.

Parameters:

  • building_key ¤
    (str) –

    The building identifier prefix to search for (e.g. "building_iron_mine").

  • bonus_multiplier ¤
    (float) –

    The factor by which to multiply the profit and net goods of the affected configurations (e.g. 1.5 for a 50% bonus).

Source code in src/vic3_analysis/analysis/production.py
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
def add_throughput_bonus(self, building_key: str, bonus_multiplier: float):
    """Add a throughput bonus to all configurations of a specific building.

    This method modifies the active DataFrame in-place, increasing the
    profit and net goods of all configurations of *building_key* by
    multiplying them by *bonus_multiplier*.

    Args:
        building_key: The building identifier prefix to search for (e.g.
            ``"building_iron_mine"``).
        bonus_multiplier: The factor by which to multiply the profit and
            net goods of the affected configurations (e.g. 1.5 for a 50%
            bonus).
    """
    indices = self.find_same_buildings(building_key)
    self.df.loc[indices, "profit"] *= bonus_multiplier
    goods_index = self.goods_index()
    self.df.loc[indices, goods_index] *= bonus_multiplier
constraint_limit_building ¤
constraint_limit_building(
    building_key: str, limit: float
) -> Tuple[ndarray, ndarray]

Build an inequality constraint that limits levels of one building type.

Returns a row-vector–scalar pair (A, b) such that A @ x <= b limits the total levels of the specified building to at most limit.

Parameters:

  • building_key ¤
    (str) –

    The building identifier prefix to restrict.

  • limit ¤
    (float) –

    Maximum combined level for all configurations of this building.

Returns:

  • ndarray

    A tuple (A, b) where A is a binary indicator vector of shape

  • ndarray

    (1, n_buildings) and b is [limit].

Source code in src/vic3_analysis/analysis/production.py
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
def constraint_limit_building(
    self, building_key: str, limit: float
) -> Tuple[np.ndarray, np.ndarray]:
    """Build an inequality constraint that limits levels of one building type.

    Returns a row-vector–scalar pair ``(A, b)`` such that ``A @ x <= b``
    limits the total levels of the specified building to at most *limit*.

    Args:
        building_key: The building identifier prefix to restrict.
        limit: Maximum combined level for all configurations of this
            building.

    Returns:
        A tuple ``(A, b)`` where *A* is a binary indicator vector of shape
        ``(1, n_buildings)`` and *b* is ``[limit]``.
    """
    indices = self.find_same_buildings(building_key)
    A = np.zeros(len(self.df))
    for idx in indices:
        A[idx] = 1
    b = np.array([limit])
    return A, b
constraint_limit_construction_cost ¤
constraint_limit_construction_cost(
    limit: float,
) -> Tuple[ndarray, ndarray]

Build an inequality constraint that caps total construction cost.

Returns a row-vector–scalar pair (A, b) such that A @ x <= b enforces that the total construction cost of all selected buildings does not exceed limit.

Parameters:

  • limit ¤
    (float) –

    Maximum total construction cost allowed.

Returns:

  • ndarray

    A tuple (A, b) where A is the transposed construction-cost

  • ndarray

    vector of shape (1, n_buildings) and b is [limit].

Source code in src/vic3_analysis/analysis/production.py
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
def constraint_limit_construction_cost(
    self, limit: float
) -> Tuple[np.ndarray, np.ndarray]:
    """Build an inequality constraint that caps total construction cost.

    Returns a row-vector–scalar pair ``(A, b)`` such that ``A @ x <= b``
    enforces that the total construction cost of all selected buildings
    does not exceed *limit*.

    Args:
        limit: Maximum total construction cost allowed.

    Returns:
        A tuple ``(A, b)`` where *A* is the transposed construction-cost
        vector of shape ``(1, n_buildings)`` and *b* is ``[limit]``.
    """
    return self.construction_cost_vector().T, np.array([limit])
constraint_limit_employment ¤
constraint_limit_employment(
    limit: float,
) -> Tuple[ndarray, ndarray]

Build an inequality constraint that caps total employment.

Returns a row-vector–scalar pair (A, b) such that A @ x <= b enforces that the dot product of the employment vector with the building-level vector does not exceed limit.

Parameters:

  • limit ¤
    (float) –

    Maximum total employment allowed.

Returns:

  • ndarray

    A tuple (A, b) where A is the transposed employment vector

  • ndarray

    of shape (1, n_buildings) and b is [limit].

Source code in src/vic3_analysis/analysis/production.py
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
def constraint_limit_employment(
    self, limit: float
) -> Tuple[np.ndarray, np.ndarray]:
    """Build an inequality constraint that caps total employment.

    Returns a row-vector–scalar pair ``(A, b)`` such that ``A @ x <= b``
    enforces that the dot product of the employment vector with the
    building-level vector does not exceed *limit*.

    Args:
        limit: Maximum total employment allowed.

    Returns:
        A tuple ``(A, b)`` where *A* is the transposed employment vector
        of shape ``(1, n_buildings)`` and *b* is ``[limit]``.
    """
    return self.employment_vector().T, np.array([limit])
constraint_limit_import ¤
constraint_limit_import(
    limit: float = 0.0,
) -> Tuple[ndarray, ndarray]

Build an inequality constraint that caps net imports of each good.

Returns a matrix–vector pair (A, b) such that A @ x <= b enforces that the net import of every good does not exceed limit building levels.

Parameters:

  • limit ¤
    (float, default: 0.0 ) –

    Maximum allowable net import per good. Defaults to 0.0 (no imports allowed).

Returns:

  • ndarray

    A tuple (A, b) where A has shape

  • ndarray

    (n_goods, n_buildings) and b is a vector of limit values

  • Tuple[ndarray, ndarray]

    of length n_goods.

Source code in src/vic3_analysis/analysis/production.py
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
def constraint_limit_import(
    self, limit: float = 0.0
) -> Tuple[np.ndarray, np.ndarray]:
    """Build an inequality constraint that caps net imports of each good.

    Returns a matrix–vector pair ``(A, b)`` such that ``A @ x <= b``
    enforces that the net import of every good does not exceed *limit*
    building levels.

    Args:
        limit: Maximum allowable net import per good.  Defaults to ``0.0``
            (no imports allowed).

    Returns:
        A tuple ``(A, b)`` where *A* has shape
        ``(n_goods, n_buildings)`` and *b* is a vector of *limit* values
        of length ``n_goods``.
    """
    A = -self.goods_matrix().T  # Negate to convert to <= 0 form
    b = (
        np.ones(self.goods_matrix().shape[1]) * limit
    )  # Vector of limits for <= constraints
    return A, b
constraint_produce ¤
constraint_produce(
    good_key: str, limit: float = 1
) -> Tuple[ndarray, ndarray]

Build an inequality constraint requiring minimum production of a good.

Returns a row-vector–scalar pair (A, b) such that A @ x <= b enforces that the net production of good_key is at least limit.

Parameters:

  • good_key ¤
    (str) –

    The good identifier that must be produced.

  • limit ¤
    (float, default: 1 ) –

    Minimum required net production of the good. Defaults to 1.

Returns:

  • ndarray

    A tuple (A, b) where A is the negated production column for

  • ndarray

    good_key of shape (1, n_buildings) and b is [-limit].

Raises:

  • ValueError

    If good_key is not present in the goods index.

Source code in src/vic3_analysis/analysis/production.py
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
def constraint_produce(
    self, good_key: str, limit: float = 1
) -> Tuple[np.ndarray, np.ndarray]:
    """Build an inequality constraint requiring minimum production of a good.

    Returns a row-vector–scalar pair ``(A, b)`` such that ``A @ x <= b``
    enforces that the net production of *good_key* is at least *limit*.

    Args:
        good_key: The good identifier that must be produced.
        limit: Minimum required net production of the good.  Defaults to
            ``1``.

    Returns:
        A tuple ``(A, b)`` where *A* is the negated production column for
        *good_key* of shape ``(1, n_buildings)`` and *b* is ``[-limit]``.

    Raises:
        ValueError: If *good_key* is not present in the goods index.
    """
    goods_index = self.goods_index()
    if good_key not in goods_index:
        raise ValueError(f"Good '{good_key}' not found in goods index.")
    idx = goods_index.index(good_key)
    A = -self.goods_matrix()[:, idx].T  # Negate to convert to >= limit form
    b = np.array([-limit])  # Vector of limits for >= constraints
    return A, b
construction_cost ¤
construction_cost(
    level: ndarray[tuple[int], dtype[float64]],
) -> float

Calculate total construction cost for a given building-level allocation.

Parameters:

  • level ¤
    (ndarray[tuple[int], dtype[float64]]) –

    1-D array of shape (n_buildings,) specifying the level of each building configuration.

Returns: Total construction cost for the given allocation.

Source code in src/vic3_analysis/analysis/production.py
619
620
621
622
623
624
625
626
627
628
629
630
def construction_cost(
    self, level: np.ndarray[tuple[int], np.dtype[np.float64]]
) -> float:
    """Calculate total construction cost for a given building-level allocation.

    Args:
        level: 1-D array of shape ``(n_buildings,)`` specifying the level
            of each building configuration.
    Returns:
        Total construction cost for the given allocation.
    """
    return float(np.dot(level, self.construction_cost_vector()))
construction_cost_vector ¤
construction_cost_vector() -> ndarray

Return a 1-D NumPy array of construction costs for active rows.

Returns:

  • ndarray

    Array of shape (n_buildings,) with the construction cost for

  • ndarray

    each building configuration.

Source code in src/vic3_analysis/analysis/production.py
391
392
393
394
395
396
397
398
def construction_cost_vector(self) -> np.ndarray:
    """Return a 1-D NumPy array of construction costs for active rows.

    Returns:
        Array of shape ``(n_buildings,)`` with the construction cost for
        each building configuration.
    """
    return self.df["construction_cost"].to_numpy()
employment ¤
employment(
    level: ndarray[tuple[int], dtype[float64]],
) -> float

Calculate total employment for a given building-level allocation.

Parameters:

  • level ¤
    (ndarray[tuple[int], dtype[float64]]) –

    1-D array of shape (n_buildings,) specifying the level of each building configuration.

Returns: Total employment for the given allocation.

Source code in src/vic3_analysis/analysis/production.py
608
609
610
611
612
613
614
615
616
617
def employment(self, level: np.ndarray[tuple[int], np.dtype[np.float64]]) -> float:
    """Calculate total employment for a given building-level allocation.

    Args:
        level: 1-D array of shape ``(n_buildings,)`` specifying the level
            of each building configuration.
    Returns:
        Total employment for the given allocation.
    """
    return float(np.dot(level, self.employment_vector()))
employment_vector ¤
employment_vector() -> ndarray

Return a 1-D NumPy array of per-level employment for active rows.

Returns:

  • ndarray

    Array of shape (n_buildings,) containing the employment count

  • ndarray

    for each building configuration.

Source code in src/vic3_analysis/analysis/production.py
382
383
384
385
386
387
388
389
def employment_vector(self) -> np.ndarray:
    """Return a 1-D NumPy array of per-level employment for active rows.

    Returns:
        Array of shape ``(n_buildings,)`` containing the employment count
        for each building configuration.
    """
    return self.df["employment"].to_numpy()
era_vector ¤
era_vector() -> ndarray

Return a 1-D NumPy array of era requirements for active rows.

Returns:

  • ndarray

    Array of shape (n_buildings,) with the minimum era required to

  • ndarray

    unlock each building configuration.

Source code in src/vic3_analysis/analysis/production.py
400
401
402
403
404
405
406
407
def era_vector(self) -> np.ndarray:
    """Return a 1-D NumPy array of era requirements for active rows.

    Returns:
        Array of shape ``(n_buildings,)`` with the minimum era required to
        unlock each building configuration.
    """
    return self.df["era"].to_numpy()
filter_by_building_group ¤
filter_by_building_group(building_group: str)

Remove all configurations belonging to building_group.

Parameters:

  • building_group ¤
    (str) –

    The building-group identifier to exclude.

Source code in src/vic3_analysis/analysis/production.py
465
466
467
468
469
470
471
def filter_by_building_group(self, building_group: str):
    """Remove all configurations belonging to *building_group*.

    Args:
        building_group: The building-group identifier to exclude.
    """
    self.df = self.df[self.df["building_group"] != building_group].copy()
filter_by_era ¤
filter_by_era(era: int)

Keep only building configurations unlocked before era.

Parameters:

  • era ¤
    (int) –

    Only rows with "era" < era are retained.

Source code in src/vic3_analysis/analysis/production.py
457
458
459
460
461
462
463
def filter_by_era(self, era: int):
    """Keep only building configurations unlocked before *era*.

    Args:
        era: Only rows with ``"era" < era`` are retained.
    """
    self.df = self.df[self.df["era"] < era].copy()
filter_by_production_method ¤
filter_by_production_method(production_method_key: str)

Remove configurations that include a specific production method.

Parameters:

  • production_method_key ¤
    (str) –

    The production-method key that should be excluded. Any configuration key matching "<building_key>(...<production_method_key>...)" is dropped.

Source code in src/vic3_analysis/analysis/production.py
473
474
475
476
477
478
479
480
481
482
def filter_by_production_method(self, production_method_key: str):
    """Remove configurations that include a specific production method.

    Args:
        production_method_key: The production-method key that should be
            excluded.  Any configuration key matching
            ``"<building_key>(...<production_method_key>...)"`` is dropped.
    """
    pattern = re.compile(rf"\b{re.escape(production_method_key)}\b")
    self.df = self.df[~self.df["key"].str.contains(pattern)].copy()
find_same_building_group ¤
find_same_building_group(building_group: str) -> List[int]

Return DataFrame indices of all configurations in a building group.

Parameters:

  • building_group ¤
    (str) –

    The building-group identifier to filter by.

Returns:

  • List[int]

    List of integer row indices whose "building_group" column

  • List[int]

    matches building_group exactly.

Source code in src/vic3_analysis/analysis/production.py
422
423
424
425
426
427
428
429
430
431
432
def find_same_building_group(self, building_group: str) -> List[int]:
    """Return DataFrame indices of all configurations in a building group.

    Args:
        building_group: The building-group identifier to filter by.

    Returns:
        List of integer row indices whose ``"building_group"`` column
        matches *building_group* exactly.
    """
    return self.df[self.df["building_group"] == building_group].index.tolist()
find_same_buildings ¤
find_same_buildings(building_key: str) -> List[int]

Return DataFrame indices of all configurations for a given building.

Parameters:

  • building_key ¤
    (str) –

    The building identifier prefix to search for (e.g. "building_iron_mine").

Returns:

  • List[int]

    List of integer row indices whose "key" column starts with

  • List[int]

    building_key.

Source code in src/vic3_analysis/analysis/production.py
409
410
411
412
413
414
415
416
417
418
419
420
def find_same_buildings(self, building_key: str) -> List[int]:
    """Return DataFrame indices of all configurations for a given building.

    Args:
        building_key: The building identifier prefix to search for (e.g.
            ``"building_iron_mine"``).

    Returns:
        List of integer row indices whose ``"key"`` column starts with
        *building_key*.
    """
    return self.df[self.df["key"].str.startswith(building_key)].index.tolist()
goods_index ¤
goods_index() -> List[str]

Return the list of good-key column names in the active DataFrame.

Returns:

  • List[str]

    Column names that represent tradeable goods (i.e. all columns

  • List[str]

    except "key", "building_group", "era",

  • List[str]

    "construction_cost", "profit", and "employment").

Source code in src/vic3_analysis/analysis/production.py
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
def goods_index(self) -> List[str]:
    """Return the list of good-key column names in the active DataFrame.

    Returns:
        Column names that represent tradeable goods (i.e. all columns
        except ``"key"``, ``"building_group"``, ``"era"``,
        ``"construction_cost"``, ``"profit"``, and ``"employment"``).
    """
    return [
        col
        for col in self.df.columns
        if col
        not in [
            "key",
            "building_group",
            "era",
            "construction_cost",
            "profit",
            "employment",
        ]
    ]
goods_matrix ¤
goods_matrix() -> ndarray

Return a NumPy matrix of goods flows for all active rows.

Returns:

  • ndarray

    A 2-D array of shape (n_buildings, n_goods) containing the net

  • ndarray

    goods amounts for every building configuration.

Source code in src/vic3_analysis/analysis/production.py
334
335
336
337
338
339
340
341
342
def goods_matrix(self) -> np.ndarray:
    """Return a NumPy matrix of goods flows for all active rows.

    Returns:
        A 2-D array of shape ``(n_buildings, n_goods)`` containing the net
        goods amounts for every building configuration.
    """
    goods_index = self.goods_index()
    return self.df[goods_index].to_numpy()
key_index ¤
key_index() -> List[str]

Return the list of building-configuration keys for active rows.

Returns:

  • List[str]

    Values from the "key" column, in DataFrame order.

Source code in src/vic3_analysis/analysis/production.py
365
366
367
368
369
370
371
def key_index(self) -> List[str]:
    """Return the list of building-configuration keys for active rows.

    Returns:
        Values from the ``"key"`` column, in DataFrame order.
    """
    return self.df["key"].tolist()
linprog ¤
linprog(
    c: ndarray[tuple[int], dtype[float64]],
    inequality_constraints: List[Tuple[ndarray, ndarray]],
    equality_constraints: List[
        Tuple[ndarray, ndarray]
    ] = [],
) -> OptimizeResult

Solve a linear programme over building levels.

Minimises c @ x subject to the given inequality and equality constraints, where x is the vector of building levels.

Parameters:

  • c ¤
    (ndarray[tuple[int], dtype[float64]]) –

    Objective coefficient vector of shape (n_buildings,). Pass the negated profit vector to maximise profit.

  • inequality_constraints ¤
    (List[Tuple[ndarray, ndarray]]) –

    List of (A, b) pairs representing A @ x <= b constraints (as produced by the constraint_* methods).

  • equality_constraints ¤
    (List[Tuple[ndarray, ndarray]], default: [] ) –

    List of (A, b) pairs representing A @ x == b constraints. Defaults to an empty list.

Returns:

Raises:

  • ValueError

    If scipy.optimize.linprog reports that the optimisation failed.

Source code in src/vic3_analysis/analysis/production.py
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
def linprog(
    self,
    c: np.ndarray[tuple[int], np.dtype[np.float64]],
    inequality_constraints: List[Tuple[np.ndarray, np.ndarray]],
    equality_constraints: List[Tuple[np.ndarray, np.ndarray]] = [],
) -> OptimizeResult:
    """Solve a linear programme over building levels.

    Minimises ``c @ x`` subject to the given inequality and equality
    constraints, where ``x`` is the vector of building levels.

    Args:
        c: Objective coefficient vector of shape ``(n_buildings,)``.
            Pass the negated profit vector to *maximise* profit.
        inequality_constraints: List of ``(A, b)`` pairs representing
            ``A @ x <= b`` constraints (as produced by the
            ``constraint_*`` methods).
        equality_constraints: List of ``(A, b)`` pairs representing
            ``A @ x == b`` constraints.  Defaults to an empty list.

    Returns:
        A ``DataFrame`` with columns ``"building_key"`` and
        ``"optimized_level"``, sorted by ``"optimized_level"`` in
        descending order.

    Raises:
        ValueError: If ``scipy.optimize.linprog`` reports that the
            optimisation failed.
    """
    A_ub = (
        np.vstack([constraint[0] for constraint in inequality_constraints])
        if inequality_constraints
        else None
    )
    b_ub = (
        np.hstack([constraint[1] for constraint in inequality_constraints])
        if inequality_constraints
        else None
    )
    A_eq = (
        np.vstack([constraint[0] for constraint in equality_constraints])
        if equality_constraints
        else None
    )
    b_eq = (
        np.hstack([constraint[1] for constraint in equality_constraints])
        if equality_constraints
        else None
    )
    res = opt.linprog(c=c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq=b_eq)
    if not res.success:
        raise ValueError(f"Optimization failed: {res.message}")
    return OptimizeResult(
        key_index=self.key_index(),
        level=res.x,
        goods_index=self.goods_index(),
        net_goods=self.net_goods(res.x),
        profit=self.profit(res.x),
        employment=self.employment(res.x),
        construction_cost=self.construction_cost(res.x),
    )
net_goods ¤
net_goods(
    level: ndarray[tuple[int], dtype[float64]],
) -> ndarray

Calculate net goods flows for a given building-level allocation.

Parameters:

  • level ¤
    (ndarray[tuple[int], dtype[float64]]) –

    1-D array of shape (n_buildings,) specifying the level of each building configuration.

Returns: Net goods flows for the given allocation.

Source code in src/vic3_analysis/analysis/production.py
632
633
634
635
636
637
638
639
640
641
642
643
def net_goods(
    self, level: np.ndarray[tuple[int], np.dtype[np.float64]]
) -> np.ndarray:
    """Calculate net goods flows for a given building-level allocation.

    Args:
        level: 1-D array of shape ``(n_buildings,)`` specifying the level
            of each building configuration.
    Returns:
        Net goods flows for the given allocation.
    """
    return np.dot(level, self.goods_matrix())
production_index ¤
production_index() -> List[str]

Return the list of production-related column names.

Returns:

  • List[str]

    Column names that represent production values (i.e. all columns

  • List[str]

    except "key", "building_group", and "era").

Source code in src/vic3_analysis/analysis/production.py
344
345
346
347
348
349
350
351
352
353
354
355
def production_index(self) -> List[str]:
    """Return the list of production-related column names.

    Returns:
        Column names that represent production values (i.e. all columns
        except ``"key"``, ``"building_group"``, and ``"era"``).
    """
    return [
        col
        for col in self.df.columns
        if col not in ["key", "building_group", "era"]
    ]
production_matrix ¤
production_matrix() -> ndarray

Return a NumPy matrix of all production values for active rows.

Returns:

  • ndarray

    A 2-D array of shape (n_buildings, n_production_cols).

Source code in src/vic3_analysis/analysis/production.py
357
358
359
360
361
362
363
def production_matrix(self) -> np.ndarray:
    """Return a NumPy matrix of all production values for active rows.

    Returns:
        A 2-D array of shape ``(n_buildings, n_production_cols)``.
    """
    return self.df[self.production_index()].to_numpy()
profit ¤
profit(level: ndarray[tuple[int], dtype[float64]]) -> float

Calculate total profit for a given building-level allocation.

Parameters:

  • level ¤
    (ndarray[tuple[int], dtype[float64]]) –

    1-D array of shape (n_buildings,) specifying the level of each building configuration.

Returns:

  • float

    Total profit for the given allocation.

Source code in src/vic3_analysis/analysis/production.py
596
597
598
599
600
601
602
603
604
605
606
def profit(self, level: np.ndarray[tuple[int], np.dtype[np.float64]]) -> float:
    """Calculate total profit for a given building-level allocation.

    Args:
        level: 1-D array of shape ``(n_buildings,)`` specifying the level
            of each building configuration.

    Returns:
        Total profit for the given allocation.
    """
    return float(np.dot(level, self.profit_vector()))
profit_per_capita ¤
profit_per_capita(
    level: ndarray[tuple[int], dtype[float64]],
) -> float

Calculate profit per capita for a given building-level allocation.

Parameters:

  • level ¤
    (ndarray[tuple[int], dtype[float64]]) –

    1-D array of shape (n_buildings,) specifying the level of each building configuration.

Returns:

  • float

    Total profit divided by total employment, or float("inf") when

  • float

    total employment is zero.

Raises:

  • ValueError

    If level is not 1-D or its length does not match the number of rows in self.df.

Source code in src/vic3_analysis/analysis/production.py
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
def profit_per_capita(
    self, level: np.ndarray[tuple[int], np.dtype[np.float64]]
) -> float:
    """Calculate profit per capita for a given building-level allocation.

    Args:
        level: 1-D array of shape ``(n_buildings,)`` specifying the level
            of each building configuration.

    Returns:
        Total profit divided by total employment, or ``float("inf")`` when
        total employment is zero.

    Raises:
        ValueError: If *level* is not 1-D or its length does not match the
            number of rows in ``self.df``.
    """
    if level.ndim != 1:
        raise ValueError("level must be a 1-D array.")
    if level.shape[0] != len(self.df):
        raise ValueError("level length must match the number of rows in self.df.")

    total_employment = self.employment(level)
    if total_employment == 0:
        return float("inf")  # Infinite profit per capita if no employment
    return self.profit(level) / total_employment
profit_vector ¤
profit_vector() -> ndarray

Return a 1-D NumPy array of per-level profits for active rows.

Returns:

  • ndarray

    Array of shape (n_buildings,) containing the net profit value

  • ndarray

    for each building configuration.

Source code in src/vic3_analysis/analysis/production.py
373
374
375
376
377
378
379
380
def profit_vector(self) -> np.ndarray:
    """Return a 1-D NumPy array of per-level profits for active rows.

    Returns:
        Array of shape ``(n_buildings,)`` containing the net profit value
        for each building configuration.
    """
    return self.df["profit"].to_numpy()
restore ¤
restore()

Reset self.df to the original unfiltered production table.

Source code in src/vic3_analysis/analysis/production.py
434
435
436
def restore(self):
    """Reset ``self.df`` to the original unfiltered production table."""
    self.df = self.df_raw.copy()

ProductionUnit ¤

ProductionUnit(
    production: dict[str, int],
    employment: int = 0,
    era: int = 0,
)

Bases: dict

A dict-like snapshot of one building level's production statistics.

Stores goods flows (positive = output, negative = input), employment, and the earliest era at which this configuration becomes available. Supports addition (+) to aggregate multiple production methods.

Parameters:

  • production ¤
    (dict[str, int]) –

    Mapping of good keys to their net amounts per building level (positive = output, negative = input).

  • employment ¤
    (int, default: 0 ) –

    Number of pops employed per building level.

  • era ¤
    (int, default: 0 ) –

    Minimum era required to unlock this production configuration.

Methods:

  • profit

    Calculate the net profit per building level.

  • profit_per_employment

    Calculate profit divided by employment per building level.

Source code in src/vic3_analysis/analysis/production.py
54
55
56
57
58
59
60
61
62
63
64
65
66
def __init__(self, production: dict[str, int], employment: int = 0, era: int = 0):
    """Initialise a :class:`ProductionUnit`.

    Args:
        production: Mapping of good keys to their net amounts per building
            level (positive = output, negative = input).
        employment: Number of pops employed per building level.
        era: Minimum era required to unlock this production configuration.
    """
    super().__init__()
    self["era"] = era
    self["employment"] = employment
    self.update(production)
profit ¤
profit(goods_cost: dict[str, int]) -> int

Calculate the net profit per building level.

Parameters:

  • goods_cost ¤
    (dict[str, int]) –

    Mapping of good keys to their base market prices.

Returns:

  • int

    The net monetary value of all goods flows (revenues from outputs

  • int

    minus costs of inputs).

Source code in src/vic3_analysis/analysis/production.py
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
def profit(self, goods_cost: dict[str, int]) -> int:
    """Calculate the net profit per building level.

    Args:
        goods_cost: Mapping of good keys to their base market prices.

    Returns:
        The net monetary value of all goods flows (revenues from outputs
        minus costs of inputs).
    """
    profit = 0
    for good, amount in self.items():
        if good in ["employment", "era"]:
            continue
        profit += goods_cost[good] * amount
    return profit
profit_per_employment ¤
profit_per_employment(goods_cost: dict[str, int]) -> float

Calculate profit divided by employment per building level.

Parameters:

  • goods_cost ¤
    (dict[str, int]) –

    Mapping of good keys to their base market prices.

Returns:

  • float

    Net profit divided by total employment, or float("inf") when

  • float

    employment is zero.

Source code in src/vic3_analysis/analysis/production.py
106
107
108
109
110
111
112
113
114
115
116
117
118
def profit_per_employment(self, goods_cost: dict[str, int]) -> float:
    """Calculate profit divided by employment per building level.

    Args:
        goods_cost: Mapping of good keys to their base market prices.

    Returns:
        Net profit divided by total employment, or ``float("inf")`` when
        employment is zero.
    """
    if self["employment"] == 0:
        return float("inf")  # Infinite profit per employment if employment is zero
    return self.profit(goods_cost) / self["employment"]

production_table ¤

production_table(game_dir: str | None = None) -> DataFrame

Build a DataFrame of all possible building configurations and their stats.

For every building that has a construction cost, enumerates every combination of production methods (one per production-method-group) and records the aggregated employment, goods flows, profit, era, and construction cost.

Parameters:

  • game_dir ¤
    (str | None, default: None ) –

    Path to the Victoria 3 game directory. If None the directory is located automatically via :func:~vic3_analysis.utils.get_vic3_directory.

Returns:

  • DataFrame

    A DataFrame where each row represents one specific building

  • DataFrame

    configuration (a unique combination of production methods). The

  • DataFrame

    "key" column encodes "<building>(<pm1>+<pm2>+...)"; other

  • DataFrame

    columns include "building_group", "era",

  • DataFrame

    "construction_cost", "profit", "employment", and one

  • DataFrame

    column per tradeable good.

Source code in src/vic3_analysis/analysis/production.py
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
def production_table(game_dir: str | None = None) -> pd.DataFrame:
    """Build a DataFrame of all possible building configurations and their stats.

    For every building that has a construction cost, enumerates every
    combination of production methods (one per production-method-group) and
    records the aggregated employment, goods flows, profit, era, and
    construction cost.

    Args:
        game_dir: Path to the Victoria 3 ``game`` directory.  If ``None`` the
            directory is located automatically via
            :func:`~vic3_analysis.utils.get_vic3_directory`.

    Returns:
        A ``DataFrame`` where each row represents one specific building
        configuration (a unique combination of production methods).  The
        ``"key"`` column encodes ``"<building>(<pm1>+<pm2>+...)"``; other
        columns include ``"building_group"``, ``"era"``,
        ``"construction_cost"``, ``"profit"``, ``"employment"``, and one
        column per tradeable good.
    """
    if game_dir is None:
        game_dir = get_vic3_directory()

    # Get goods costs
    df_goods = goods(game_dir)
    goods_dict = dict(zip(df_goods["key"], df_goods["cost"]))

    # Get technology to era mapping
    df_tech = technology(game_dir)
    tech_era_dict = dict(zip(df_tech["tech_key"], df_tech["era"]))

    # Get production method groups to production methods mapping
    pmg_pm_dict = production_method_groups(game_dir)

    buildings_tree = BuildingsParser(game_dir)
    # Get building to production method groups mapping
    building_pmg_dict = buildings_tree.production_method_groups()
    # Get building construction costs
    building_cost_dict = {}
    for building_key, building_values in buildings_tree.items():
        if "required_construction_points" in building_values.keys():
            building_cost_dict[building_key] = building_values[
                "required_construction_points"
            ]
    # Get building group information
    building_group_dict = {}
    for building_key, building_values in buildings_tree.items():
        if "building_group" in building_values.keys():
            building_group_dict[building_key] = building_values["building_group"]

    # Get production method employment and production output
    df_pm = production_method()
    pm_dict = {}
    for _, row in df_pm.iterrows():
        if row["building"] not in building_cost_dict:
            continue  # Skip if building is not in building_cost_dict
        pm_dict[row["production_method"]] = ProductionUnit(
            era=tech_era_dict.get(row["unlocking_technologies"], 0),
            employment=row["employment"],
            production={good: row[good] for good in goods_dict.keys() if good in row},
        )

    possible_buildings = []
    for building_key in building_cost_dict.keys():
        # list all possible combinations of production methods for this building
        pm_lists = []
        for pmg in building_pmg_dict[building_key]:
            pm_lists.append(pmg_pm_dict[pmg])
        # iterate through all combinations of production methods for this building
        for combo in _all_combinations(pm_lists):
            building = ProductionUnit(production={})
            key = building_key + "(" + "+".join(combo) + ")"
            for pm in combo:
                building += pm_dict[pm]
            row_dict = {"key": key}
            row_dict["building_group"] = building_group_dict[building_key]
            row_dict["era"] = building["era"]
            row_dict["construction_cost"] = building_cost_dict[building_key]
            row_dict["profit"] = building.profit(goods_dict)
            row_dict.update(building)
            possible_buildings.append(row_dict)

    result = pd.DataFrame(possible_buildings)
    return result

parse ¤

Modules:

  • buildings

    Parser for Victoria 3 building definitions.

  • buy_packages

    Parser for Victoria 3 pop buy-package definitions.

  • goods

    Parser for Victoria 3 tradeable-goods definitions.

  • production_method_groups

    Parser for Victoria 3 production-method-group definitions.

  • production_methods

    Parser for Victoria 3 production-method definitions.

  • state_regions

    Parser for Victoria 3 state region definitions.

  • technology

    Parser for Victoria 3 technology definitions.

buildings ¤

Parser for Victoria 3 building definitions.

Reads building data from the game's common/buildings directory and exposes it as a pyradox.Tree subclass with helper methods for DataFrame conversion and production-method-group look-ups.

Classes:

  • BuildingsParser

    A pyradox.Tree populated with Victoria 3 building definitions.

BuildingsParser ¤

BuildingsParser(game_dir: str | None = None)

Bases: Tree

A pyradox.Tree populated with Victoria 3 building definitions.

On construction the parser reads all building .txt files from the game directory, resolves required_construction keys to their numeric point values using the game's script_values, and stores the resolved value under the required_construction_points key for each building entry.

Attributes:

  • cost_modifiers

    Mapping of construction-cost script-value names (e.g. "construction_cost_urban") to their integer values, extracted from common/script_values.

Parameters:

  • game_dir ¤
    (str | None, default: None ) –

    Path to the Victoria 3 game directory. If None the directory is located automatically via :func:~vic3_analysis.utils.get_vic3_directory.

Methods:

  • building_groups

    Return a mapping of building group keys to their member building keys.

  • production_method_groups

    Return a mapping of building keys to their production-method-group lists.

  • to_dataframe

    Convert the buildings tree to a flat pandas.DataFrame.

Source code in src/vic3_analysis/parse/buildings.py
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
def __init__(self, game_dir: str | None = None):
    """Initialise and populate the buildings tree.

    Args:
        game_dir: Path to the Victoria 3 ``game`` directory. If ``None``
            the directory is located automatically via
            :func:`~vic3_analysis.utils.get_vic3_directory`.
    """
    super().__init__()
    if game_dir is None:
        game_dir = get_vic3_directory()

    parse_dir = os.path.join(game_dir, "common", "buildings")
    parse_tree = parse_merge(parse_dir)
    self.update(parse_tree)

    parse_dir = os.path.join(game_dir, "common", "script_values")
    parse_tree = parse_merge(parse_dir)
    self.cost_modifiers = {}
    for key, value in parse_tree.to_python().items():
        if key.startswith("construction_cost_"):
            self.cost_modifiers[key] = int(value)

    for building_key, building_values in self.items():
        if "required_construction" not in building_values.keys():
            continue
        cost_modifier = building_values["required_construction"]
        building_values.append(
            "required_construction_points", self.cost_modifiers[cost_modifier]
        )
building_groups ¤
building_groups() -> dict[str, list[str]]

Return a mapping of building group keys to their member building keys.

Returns:

  • dict[str, list[str]]

    A dict where each key is a building group identifier and each value

  • dict[str, list[str]]

    is a list of building identifiers that belong to that group.

Source code in src/vic3_analysis/parse/buildings.py
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
def building_groups(self) -> dict[str, list[str]]:
    """Return a mapping of building group keys to their member building keys.

    Returns:
        A dict where each key is a building group identifier and each value
        is a list of building identifiers that belong to that group.
    """
    result = {}
    for building_key, building_values in self.items():
        if isinstance(building_values, Tree):
            group = building_values.to_python().get("building_group")
        elif isinstance(building_values, dict):
            group = building_values.get("building_group")
        else:
            continue  # Skip non-dict entries
        if group is not None:
            if group not in result:
                result[group] = []
            result[group].append(building_key)
    return result
production_method_groups ¤
production_method_groups() -> dict[str, list[str]]

Return a mapping of building keys to their production-method-group lists.

Returns:

  • dict[str, list[str]]

    A dict where each key is a building identifier and each value is a

  • dict[str, list[str]]

    list of production-method-group keys associated with that building.

Source code in src/vic3_analysis/parse/buildings.py
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
def production_method_groups(self) -> dict[str, list[str]]:
    """Return a mapping of building keys to their production-method-group lists.

    Returns:
        A dict where each key is a building identifier and each value is a
        list of production-method-group keys associated with that building.
    """
    result = {}
    for building_key, building_values in self.items():
        if isinstance(building_values, Tree):
            pmg = building_values.to_python().get("production_method_groups")
        elif isinstance(building_values, dict):
            pmg = building_values.get("production_method_groups")
        else:
            continue  # Skip non-dict entries
        if isinstance(pmg, list):
            result[building_key] = pmg
        else:
            result[building_key] = [pmg]
    return result
to_dataframe ¤
to_dataframe() -> DataFrame

Convert the buildings tree to a flat pandas.DataFrame.

Scalar attributes of each building are preserved as columns; nested Tree, list, and dict values are omitted.

Returns:

  • DataFrame

    A DataFrame with one row per building and one column per scalar

  • DataFrame

    attribute.

Source code in src/vic3_analysis/parse/buildings.py
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
def to_dataframe(self) -> pd.DataFrame:
    """Convert the buildings tree to a flat ``pandas.DataFrame``.

    Scalar attributes of each building are preserved as columns; nested
    ``Tree``, ``list``, and ``dict`` values are omitted.

    Returns:
        A ``DataFrame`` with one row per building and one column per scalar
        attribute.
    """
    results = []
    for building_key, building_values in self.items():
        building = {"building": building_key}
        for attribute_key, attribute_value in building_values.items():
            if isinstance(attribute_value, (list, dict, Tree)):
                continue
            building[attribute_key] = attribute_value
        results.append(building)
    return pd.DataFrame(results)

buy_packages ¤

Parser for Victoria 3 pop buy-package definitions.

Reads common/buy_packages/00_buy_packages.txt and exposes each wealth level's political strength and good-consumption values as a pandas.DataFrame.

Functions:

  • buy_packages

    Parse a Victoria 3 buy-packages file into a pandas.DataFrame.

buy_packages ¤

buy_packages(file_path: str | None = None) -> DataFrame

Parse a Victoria 3 buy-packages file into a pandas.DataFrame.

Parameters:

  • file_path ¤
    (str | None, default: None ) –

    Path to the buy-packages .txt file. Defaults to the standard location inside the auto-detected Victoria 3 game directory.

Returns:

  • DataFrame

    A DataFrame with one row per wealth level and columns for

  • DataFrame

    "wealth", "political_strength", and one column per

  • DataFrame

    popneed_* good. Missing consumption values are filled with 0.

Raises:

  • FileNotFoundError

    If file_path does not point to an existing file.

Source code in src/vic3_analysis/parse/buy_packages.py
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
def buy_packages(file_path: str | None = None) -> pd.DataFrame:
    """Parse a Victoria 3 buy-packages file into a ``pandas.DataFrame``.

    Args:
        file_path: Path to the buy-packages ``.txt`` file.  Defaults to the
            standard location inside the auto-detected Victoria 3 game
            directory.

    Returns:
        A ``DataFrame`` with one row per wealth level and columns for
        ``"wealth"``, ``"political_strength"``, and one column per
        ``popneed_*`` good.  Missing consumption values are filled with ``0``.

    Raises:
        FileNotFoundError: If *file_path* does not point to an existing file.
    """
    if file_path is None:
        file_path = os.path.join(
            get_vic3_directory(), "common", "buy_packages", "00_buy_packages.txt"
        )

    if not os.path.isfile(file_path):
        raise FileNotFoundError(
            f"Could not find the file at {file_path}. Please check the path and try again."
        )

    tree = parse_file(file_path, game="HoI4", path_relative_to_game=False)
    rows, popneed_columns = _parse_rows(tree)

    fieldnames = ["wealth", "political_strength", *popneed_columns]
    normalized_rows = []
    for row in rows:
        normalized_row = {
            "wealth": row["wealth"],
            "political_strength": row["political_strength"],
        }
        for column in popneed_columns:
            normalized_row[column] = row.get(column, 0)
        normalized_rows.append(normalized_row)

    return pd.DataFrame(normalized_rows, columns=fieldnames)

goods ¤

Parser for Victoria 3 tradeable-goods definitions.

Reads all .txt files under common/goods and returns their data as a pandas.DataFrame.

Functions:

  • goods

    Parse Victoria 3 goods definitions and return them as a DataFrame.

goods ¤

goods(file_dir: str | None = None) -> DataFrame

Parse Victoria 3 goods definitions and return them as a DataFrame.

Parameters:

  • file_dir ¤
    (str | None, default: None ) –

    Path to the Victoria 3 game directory. If None the directory is located automatically via :func:~vic3_analysis.utils.get_vic3_directory.

Returns:

  • DataFrame

    A DataFrame with one row per tradeable good, where the "key"

  • DataFrame

    column holds the good's identifier and remaining columns represent its

  • DataFrame

    attributes (e.g. "cost").

Raises:

  • ValueError

    If any entry in the goods tree is not a dict.

Source code in src/vic3_analysis/parse/goods.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
def goods(file_dir: str | None = None) -> pd.DataFrame:
    """Parse Victoria 3 goods definitions and return them as a DataFrame.

    Args:
        file_dir: Path to the Victoria 3 ``game`` directory.  If ``None`` the
            directory is located automatically via
            :func:`~vic3_analysis.utils.get_vic3_directory`.

    Returns:
        A ``DataFrame`` with one row per tradeable good, where the ``"key"``
        column holds the good's identifier and remaining columns represent its
        attributes (e.g. ``"cost"``).

    Raises:
        ValueError: If any entry in the goods tree is not a ``dict``.
    """
    if file_dir is None:
        file_dir = get_vic3_directory()

    parse_dir = os.path.join(file_dir, "common", "goods")
    parse_tree = parse_merge(parse_dir)
    result = []
    for key, value in parse_tree.to_python().items():
        if not isinstance(value, dict):
            raise ValueError(f"Expected dict for {key}, got {type(value)}")
        result.append(
            {
                "key": key,
                **value,
            }
        )
    return pd.DataFrame(result)

production_method_groups ¤

Parser for Victoria 3 production-method-group definitions.

Reads all .txt files under common/production_method_groups and returns a mapping of group keys to their ordered list of production-method keys.

Functions:

production_method_groups ¤

production_method_groups(
    game_dir: str | None = None,
) -> dict[str, list[str]]

Parse Victoria 3 production-method-group data into a dict.

Parameters:

  • game_dir ¤
    (str | None, default: None ) –

    Path to the Victoria 3 game directory. If None the directory is located automatically via :func:~vic3_analysis.utils.get_vic3_directory.

Returns:

  • dict[str, list[str]]

    A dict mapping each production-method-group key to its ordered list of

  • dict[str, list[str]]

    production-method keys.

Source code in src/vic3_analysis/parse/production_method_groups.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
def production_method_groups(game_dir: str | None = None) -> dict[str, list[str]]:
    """Parse Victoria 3 production-method-group data into a dict.

    Args:
        game_dir: Path to the Victoria 3 ``game`` directory.  If ``None`` the
            directory is located automatically via
            :func:`~vic3_analysis.utils.get_vic3_directory`.

    Returns:
        A dict mapping each production-method-group key to its ordered list of
        production-method keys.
    """
    if game_dir is None:
        game_dir = get_vic3_directory()

    parse_dir = os.path.join(game_dir, "common", "production_method_groups")
    parse_tree = parse_merge(parse_dir)
    result = {}
    for key, subtree in parse_tree.items():
        if not isinstance(subtree, Tree):
            continue  # Skip non-tree entries
        production_methods = subtree.to_python().get("production_methods")
        if isinstance(production_methods, list):
            result[key] = production_methods
        else:
            result[key] = [production_methods]

    return result

production_methods ¤

Parser for Victoria 3 production-method definitions.

Reads all .txt files under common/production_methods, combines them with building and goods data, and exposes per-production-method employment and goods-flow values as a flat pandas.DataFrame.

Functions:

  • production_method

    Parse all Victoria 3 production-method data into a flat DataFrame.

production_method ¤

production_method(game_dir: str | None = None) -> DataFrame

Parse all Victoria 3 production-method data into a flat DataFrame.

Combines buildings, production-method-groups, production-methods, and goods data from the game files into a single table.

Parameters:

  • game_dir ¤
    (str | None, default: None ) –

    Path to the Victoria 3 game directory. If None the directory is located automatically via :func:~vic3_analysis.utils.get_vic3_directory.

Returns:

  • DataFrame

    A DataFrame with one row per (building, production-method-group,

  • DataFrame

    production-method) combination, containing employment numbers, goods

  • DataFrame

    flows, and unlocking-technology information.

Source code in src/vic3_analysis/parse/production_methods.py
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
def production_method(game_dir: str | None = None) -> pd.DataFrame:
    """Parse all Victoria 3 production-method data into a flat DataFrame.

    Combines buildings, production-method-groups, production-methods, and
    goods data from the game files into a single table.

    Args:
        game_dir: Path to the Victoria 3 ``game`` directory.  If ``None`` the
            directory is located automatically via
            :func:`~vic3_analysis.utils.get_vic3_directory`.

    Returns:
        A ``DataFrame`` with one row per (building, production-method-group,
        production-method) combination, containing employment numbers, goods
        flows, and unlocking-technology information.
    """
    if game_dir is None:
        game_dir = get_vic3_directory()

    df_goods = goods(game_dir)
    goods_dict = dict(zip(df_goods["key"], df_goods["cost"]))

    buildings_tree = BuildingsParser(game_dir)
    buildings_pmg_dict = buildings_tree.production_method_groups()
    # Get building technology information
    buildings_tech_dict = {}
    for building_key, building_values in buildings_tree.items():
        if "unlocking_technologies" in building_values.keys():
            buildings_tech_dict[building_key] = str(
                building_values["unlocking_technologies"]
            )
        else:
            buildings_tech_dict[building_key] = "buildings_pmg_dict"

    pmg_dict = production_method_groups(game_dir)

    pm_dict = _parse_pm(goods_dict, game_dir)

    return _to_dataframe(
        buildings_pmg_dict, pmg_dict, pm_dict, goods_dict, buildings_tech_dict
    )

state_regions ¤

Parser for Victoria 3 state region definitions.

Reads state region data from the game's map_data/state_regions directory and exposes it as a pyradox.Tree subclass with helper methods for DataFrame conversion and state region look-ups.

Classes:

  • StateRegionsParser

    A pyradox.Tree subclass for parsing Victoria 3 state region definitions.

StateRegionsParser ¤

StateRegionsParser(game_dir: str | None = None)

Bases: Tree

A pyradox.Tree subclass for parsing Victoria 3 state region definitions.

Reads all .txt files from the game's map_data/state_regions directory and stores the parsed data in a tree structure that mirrors the original file hierarchy. Provides helper methods for converting to a flat pandas.DataFrame and for looking up state region attributes & resources.

Parameters:

  • game_dir ¤
    (str | None, default: None ) –

    Path to the Victoria 3 game directory. If None the directory is located automatically via :func:~vic3_analysis.utils.get_vic3_directory.

Methods:

  • arable_resources_of

    Return a list of all arable resource keys that belong to any state region.

  • provinces_of

    Return a list of all province keys that belong to any state region.

  • to_dataframe

    Convert the state regions tree to a flat pandas.DataFrame.

  • traits_of

    Return a list of all trait keys that belong to any state region.

Source code in src/vic3_analysis/parse/state_regions.py
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
def __init__(self, game_dir: str | None = None):
    """Initialise and populate the state regions tree.

    Args:
        game_dir: Path to the Victoria 3 ``game`` directory. If ``None``
            the directory is located automatically via
            :func:`~vic3_analysis.utils.get_vic3_directory`.
    """
    super().__init__()
    if game_dir is None:
        game_dir = get_vic3_directory()

    parse_dir = os.path.join(game_dir, "map_data", "state_regions")
    parse_tree = parse_merge(parse_dir)
    self.update(parse_tree)
arable_resources_of ¤
arable_resources_of(
    state_region_key: str | None = None,
) -> list[str]

Return a list of all arable resource keys that belong to any state region.

Source code in src/vic3_analysis/parse/state_regions.py
115
116
117
118
119
120
121
122
123
124
125
126
def arable_resources_of(self, state_region_key: str | None = None) -> list[str]:
    """Return a list of all arable resource keys that belong to any state region."""
    arable_resources = []
    state_region_values = self[state_region_key]
    if not isinstance(state_region_values, Tree):
        raise ValueError(
            f"State region '{state_region_key}' does not exist or is not a valid Tree."
        )
    for attribute_key, attribute_value in state_region_values.items():
        if attribute_key == "arable_resources":
            arable_resources.append(attribute_value)
    return arable_resources
provinces_of ¤
provinces_of(
    state_region_key: str | None = None,
) -> list[str]

Return a list of all province keys that belong to any state region.

Source code in src/vic3_analysis/parse/state_regions.py
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
def provinces_of(self, state_region_key: str | None = None) -> list[str]:
    """Return a list of all province keys that belong to any state region."""
    provinces = []
    state_region_values = self[state_region_key]
    if not isinstance(state_region_values, Tree):
        raise ValueError(
            f"State region '{state_region_key}' does not exist or is not a valid Tree."
        )
    for attribute_key, attribute_value in state_region_values.items():
        if attribute_key == "provinces":
            provinces.append(attribute_value)
    return provinces
to_dataframe ¤
to_dataframe() -> DataFrame

Convert the state regions tree to a flat pandas.DataFrame.

Scalar attributes of each state region are preserved as columns; nested Tree, list, and dict values are omitted.

Returns:

  • DataFrame

    A DataFrame with one row per state region and one column per scalar

  • DataFrame

    attribute.

Source code in src/vic3_analysis/parse/state_regions.py
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
def to_dataframe(self) -> pd.DataFrame:
    """Convert the state regions tree to a flat ``pandas.DataFrame``.

    Scalar attributes of each state region are preserved as columns; nested
    ``Tree``, ``list``, and ``dict`` values are omitted.

    Returns:
        A ``DataFrame`` with one row per state region and one column per scalar
        attribute.
    """
    results = []
    for state_region_key, state_region_values in self.items():
        state_region = {"key": state_region_key}
        state_region["province_count"] = len(self.provinces_of(state_region_key))
        for attribute_key, attribute_value in state_region_values.items():
            if attribute_key == "capped_resources":
                for resource_key, resource_value in attribute_value.items():
                    state_region[f"resource_{resource_key}"] = resource_value
            elif attribute_key == "resource":
                if not isinstance(attribute_value, Tree):
                    raise ValueError(
                        f"Expected 'resource' attribute to be a Tree, got {type(attribute_value)}"
                    )
                resource_key = attribute_value["type"]
                undiscovered_amount = int(attribute_value.find("undiscovered_amount", 0)) # pyright: ignore[reportArgumentType]
                discovered_amount = int(attribute_value.find("discovered_amount", 0)) # pyright: ignore[reportArgumentType]
                state_region[f"resource_{resource_key}"] = undiscovered_amount + discovered_amount
                state_region[f"undiscovered_amount_resource_{resource_key}"] = undiscovered_amount
                state_region[f"discovered_amount_resource_{resource_key}"] = discovered_amount
            if attribute_key in _skip_keys or isinstance(attribute_value, (list, dict, Tree)):
                continue
            state_region[attribute_key] = attribute_value
        results.append(state_region)
    results = pd.DataFrame(results)
    # For every column whose name starts with "resource_" or "undiscovered_amount_resource_" or "discovered_amount_resource_",
    # convert the column to numeric, coercing errors to NaN, and then fill NaN values with 0
    for column in results.columns:
        if column.startswith("resource_") or column.startswith("undiscovered_amount_resource_") or column.startswith("discovered_amount_resource_"):
            results[column] = pd.to_numeric(results[column], errors="coerce").fillna(0).astype(int)
    return results
traits_of ¤
traits_of(state_region_key: str | None = None) -> list[str]

Return a list of all trait keys that belong to any state region.

Source code in src/vic3_analysis/parse/state_regions.py
102
103
104
105
106
107
108
109
110
111
112
113
def traits_of(self, state_region_key: str | None = None) -> list[str]:
    """Return a list of all trait keys that belong to any state region."""
    traits = []
    state_region_values = self[state_region_key]
    if not isinstance(state_region_values, Tree):
        raise ValueError(
            f"State region '{state_region_key}' does not exist or is not a valid Tree."
        )
    for attribute_key, attribute_value in state_region_values.items():
        if attribute_key == "traits":
            traits.append(attribute_value)
    return traits

technology ¤

Parser for Victoria 3 technology definitions.

Reads all .txt files under common/technology/technologies and returns each technology's key attributes (including its numeric era) as a pandas.DataFrame.

Functions:

  • technology

    Parse Victoria 3 technology definitions into a DataFrame.

technology ¤

technology(game_dir: str | None = None) -> DataFrame

Parse Victoria 3 technology definitions into a DataFrame.

Reads all .txt files from common/technology/technologies, skipping keys that are not useful for analysis (modifier, ai_weight, unlocking_technologies, on_researched), and converts era_N strings to their integer era numbers.

Parameters:

  • game_dir ¤
    (str | None, default: None ) –

    Path to the Victoria 3 game directory. If None the directory is located automatically via :func:~vic3_analysis.utils.get_vic3_directory.

Returns:

  • DataFrame

    A DataFrame with one row per technology. Always contains a

  • DataFrame

    "tech_key" column and an "era" column (integer), plus any

  • DataFrame

    additional scalar attributes defined in the game files.

Raises:

  • ValueError

    If a technology entry contains a nested Tree value for an unexpected key, or if the "era" value cannot be parsed.

Source code in src/vic3_analysis/parse/technology.py
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
def technology(game_dir: str | None = None) -> pd.DataFrame:
    """Parse Victoria 3 technology definitions into a DataFrame.

    Reads all ``.txt`` files from ``common/technology/technologies``, skipping
    keys that are not useful for analysis (``modifier``, ``ai_weight``,
    ``unlocking_technologies``, ``on_researched``), and converts ``era_N``
    strings to their integer era numbers.

    Args:
        game_dir: Path to the Victoria 3 ``game`` directory.  If ``None`` the
            directory is located automatically via
            :func:`~vic3_analysis.utils.get_vic3_directory`.

    Returns:
        A ``DataFrame`` with one row per technology.  Always contains a
        ``"tech_key"`` column and an ``"era"`` column (integer), plus any
        additional scalar attributes defined in the game files.

    Raises:
        ValueError: If a technology entry contains a nested ``Tree`` value for
            an unexpected key, or if the ``"era"`` value cannot be parsed.
    """
    if game_dir is None:
        game_dir = get_vic3_directory()

    parse_dir = os.path.join(game_dir, "common", "technology", "technologies")
    parse_tree = parse_merge(parse_dir)
    result = []
    for tech_key, subtree in parse_tree.items():
        tech_item = {"tech_key": tech_key}
        for key, value in subtree.items():
            if key in skip_keys:
                continue
            if isinstance(value, Tree):
                raise ValueError(f"Expected non-tree entry for {key}, got Tree")
            if key == "era":
                # Extract era number from string like "era_1"
                match = re.match(r"era_(\d+)", value)
                if match:
                    tech_item[key] = int(match.group(1))
                else:
                    raise ValueError(
                        f"Could not extract era number from string: {value}"
                    )
            else:
                tech_item[key] = value
        result.append(tech_item)

    return pd.DataFrame(result)

utils ¤

Utility helpers for locating the Victoria 3 game installation and parsing Paradox script files.

Functions:

  • get_vic3_directory

    Search common Steam library paths and return the Victoria 3 game directory.

  • parse_merge

    Parse all .txt files in path and merge them into a single Tree.

get_vic3_directory ¤

get_vic3_directory() -> str

Search common Steam library paths and return the Victoria 3 game directory.

Returns:

  • str

    The absolute path to the Victoria 3/game directory.

Raises:

  • FileNotFoundError

    If the Victoria 3 game directory cannot be found in any of the known Steam library locations.

Source code in src/vic3_analysis/utils.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
def get_vic3_directory() -> str:
    """Search common Steam library paths and return the Victoria 3 game directory.

    Returns:
        The absolute path to the ``Victoria 3/game`` directory.

    Raises:
        FileNotFoundError: If the Victoria 3 game directory cannot be found
            in any of the known Steam library locations.
    """
    game_suffix = "Victoria 3/game"

    for prefix in prefixes:
        pattern = os.path.join(os.path.expanduser(prefix), game_suffix)
        candidates = glob(pattern)
        if len(candidates) > 0:
            return candidates[0]
    else:
        raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), game_suffix)

parse_merge ¤

parse_merge(path: str, merge_levels: int = 0) -> Tree

Parse all .txt files in path and merge them into a single Tree.

Parameters:

  • path ¤

    (str) –

    Directory containing the Paradox script (.txt) files to parse.

  • merge_levels ¤

    (int, default: 0 ) –

    Number of levels deep to merge nested Trees. Passed directly to pyradox.Tree.merge. Defaults to 0.

Returns:

  • Tree

    A pyradox.Tree representing the merged contents of all .txt

  • Tree

    files found in path (sorted alphabetically).

Source code in src/vic3_analysis/utils.py
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
def parse_merge(path: str, merge_levels: int = 0) -> pyradox.Tree:
    """Parse all ``.txt`` files in *path* and merge them into a single Tree.

    Args:
        path: Directory containing the Paradox script (``.txt``) files to parse.
        merge_levels: Number of levels deep to merge nested Trees. Passed
            directly to ``pyradox.Tree.merge``.  Defaults to ``0``.

    Returns:
        A ``pyradox.Tree`` representing the merged contents of all ``.txt``
        files found in *path* (sorted alphabetically).
    """

    result = pyradox.Tree()
    for filename in sorted(os.listdir(path)):
        fullpath = os.path.join(path, filename)
        with open(fullpath, "r", encoding="utf-8-sig") as f:
            if filename.endswith(".md"):
                continue  # Skip markdown files
            content = f.read()
            # Replace all special strings with '=' to prevent pyradox from treating them as merge directives
            for str in replace_strings:
                content = content.replace(str, "=")
            tree = pyradox.parse(content)
            result.merge(tree, merge_levels)
    return result