English · Português

cgelab

Você fornece uma matriz de contabilidade social. O pacote calibra um EGC aninhado padrão a partir dela, resolve cada período, percorre uma trajetória dinâmica recursiva e mede bem-estar — com as verificações contábeis que tornam qualquer disso crível já embutidas, em vez de deixadas ao usuário.

github.com/GustavoHM/cgelab

Python 3.11+ numpy, scipy, pandas Walras como verificação 5 fechamentos CET, IED, vários capitais conferido contra JuMP a 1.2e-11 MIT · v0.5.5

Três objetos separáveis, e mantê-los assim é o ponto

ObjetoDiz
SAMo que o ano-base foi
ModelSpeco que se supõe sobre substituição e forma funcional
Closurequais balanços são impostos e quem se ajusta

Resultados podem virar nos dois últimos, e por isso eles viajam junto com os resultados em vez de morar num ramo dentro de um solucionador.

O mercado do fator numerário não é resolvido. Fica para a lei de Walras, de sorte que o seu resíduo é uma verificação independente da contabilidade e não uma identidade que se mandou o solucionador satisfazer. É registrado em cada período.

Instalar

pip install cgelab
pip install "cgelab[parquet]"     # MCS em .parquet
pip install "cgelab[dsge]"        # investimento com expectativas, pelo dsgelab
pip install "cgelab @ git+https://github.com/GustavoHM/cgelab"   # o ramo main atual

Para desenvolver:

git clone https://github.com/GustavoHM/cgelab && cd cgelab
pip install -e ".[test]" && pytest
cgelab                       # roda a economia de brinquedo e imprime os diagnósticos
cgelab minha_mcs.parquet --sigma-armington 2.0 --sigma-va 0.5

Exemplos

Todos rodam como estão, logo depois do pip install: usam a economia de brinquedo que vem no pacote, escrita entrada por entrada para que o ano-base seja conhecido no papel. São os mesmos exemplos que a suíte de testes executa.

1. Da MCS ao equilíbrio

import cgelab as cge
from cgelab.examples import toy

sam, econ = toy.build()          # 2 atividades, 2 mercadorias, 2 fatores, 2 domicílios
print(cge.report(econ))          # ano-base, Walras, homogeneidade

base = cge.solve_period(econ)
print(base.summary())
base.pq                          # preços ao consumidor
base.X                           # nível de cada atividade
base.walras                      # o mercado que o solucionador NÃO resolveu
cgelab diagnostics -- Economy(2 activities, 2 commodities, 2 households, 2 factors)
  base-year reproduction : PASS (worst 3.55e-16)
      imports                3.55e-16
      exports                1.78e-16
      activity_output        1.42e-16
      commodity_output       1.42e-16
  Walras residual        : 0.00e+00 [f_lab]
  price homogeneity      : PASS (worst 4.44e-16)
equilibrium (baseline): numeraire=1; investment=savings_driven; government=real_fixed; foreign=fixed_saving
  residual        : 3.553e-16 (converged, cold)
  Walras residual : 0.000e+00  [f_lab, not solved for]
  price f_lab       : 1.000000
  price f_cap       : 1.000000
  exchange rate   : 1.000000
  savings         : 50.0
base.pq
[1.048 1.049]
base.X
[100 200]
base.walras
0

2. Um contrafactual

choque = cge.Scenario(name="tributo +50%", product_tax=econ.ptax * 1.5)
alt = cge.solve_period(econ, scenario=choque)

alt.pq / base.pq - 1                 # variação de preço, mercadoria a mercadoria
alt.real["hh_real_income"]           # renda real por domicílio
abs(alt.walras) < 1e-9               # a contabilidade continua fechando
alt.pq / base.pq - 1
[0.03488 0.04116]
alt.real["hh_real_income"]
[78.01 45.91]
abs(alt.walras) < 1e-9
True

3. Um gravame ao consumidor, e a quem volta a receita

W = cge.consumer_wedge(econ, {"c_ind": 0.08})      # 8% sobre uma mercadoria

capital = cge.solve_period(econ, scenario=cge.Scenario(
    consumer_wedge=W, wedge_revenue_to="capital"))   # devolve por propriedade do capital
percap  = cge.solve_period(econ, scenario=cge.Scenario(
    consumer_wedge=W, wedge_revenue_to="lumpsum"))   # devolve per capita
meio    = cge.solve_period(econ, scenario=cge.Scenario(
    consumer_wedge=W, wedge_revenue_to=0.5))         # combinação convexa

capital.hh_income[0] > percap.hh_income[0]           # o domicílio rico em capital ganha
abs(sum(capital.hh_wedge_paid) - capital.wedge_revenue) < 1e-12  # a identidade vale, a menos de arredondamento
capital.hh_income[0] > percap.hh_income[0]
True
abs(sum(capital.hh_wedge_paid) - capital.wedge_revenue) < 1e-12
True

Isso não é detalhe de implementação. A propriedade do capital é concentrada no topo: sob "capital" os pobres pagam o gravame nas suas compras e os ricos o recebem como renda, e parte de qualquer regressividade medida é fabricada por essa hipótese. A forma convexa existe para que o achado ganhe magnitude em vez de ser binário — existe em geral um peso em que o gradiente distributivo cruza o zero, e é essa fração que diz se o resultado tem folga.

A receita reciclada é uma série infinita: creditá-la à renda eleva o gasto, que eleva o gravame, que eleva a renda de novo. A demanda é afim no gasto, logo a série é somada em forma fechada — e iterá-la converge, mas deixa descontinuidades que arruínam a jacobiana do Newton externo.

4. Uma trajetória de 25 anos

crescimento = cge.Growth(labour=0.005, tfp=0.010,
                        depreciation=0.05, discount=0.05)

def politica(ano, t):
    return cge.Scenario(consumer_wedge=W) if ano >= 2026 else cge.Scenario()

ref = cge.run_path(econ, years=range(2021, 2046), growth=crescimento)
alt = cge.run_path(econ, years=range(2021, 2046), growth=crescimento, scenario=politica)

ref.aggregates()          # preços, absorção real, poupança, Walras — uma linha por ano
ref.worst_walras          # o número a olhar primeiro
ref.capital_stock         # estoque, recuperado do custo de uso e não da MCS
ref.aggregates()
      exchange_rate  savings     walras  residual  ...  inv_real  hh_sav_real  gov_sav_real  for_sav_real
year                                               ...                                                   
2021              1       50          0 3.553e-16  ...        50           35             5            10
2022          0.972    50.83 -2.442e-15 2.292e-16  ...     52.28        36.08          5.92         10.29
2023         0.9446    51.66 -1.151e-15 7.105e-16  ...     54.67         37.2         6.888         10.58
2024         0.9179    52.49   -1.4e-15 2.141e-16  ...     57.14        38.35         7.907         10.89
2025         0.8917    53.31  -1.52e-15 2.066e-16  ...     59.72        39.54         8.978          11.2
2026         0.8663    54.13 -1.008e-15 1.993e-16  ...     62.41        40.78          10.1         11.53
...             ...      ...        ...       ...  ...       ...          ...           ...           ...
2040          0.582    65.26  -1.88e-15 7.105e-16  ...     111.8        62.16         32.49         17.13
2041         0.5665    66.05  -1.52e-15 3.553e-16  ...     116.2        63.99         34.65          17.6
2042         0.5516    66.84 -2.676e-15 7.105e-16  ...     120.8        65.86         36.89         18.08
2043         0.5371    67.63 -2.778e-15 3.553e-16  ...     125.5        67.77         39.21         18.56
2044         0.5232    68.42 -1.843e-15         0  ...     130.4        69.72         41.63         19.06
2045         0.5098    69.21 -1.834e-15 7.105e-16  ...     135.4         71.7         44.14         19.56
ref.worst_walras
3.237e-15
ref.capital_stock
[600 620 641.3 663.9 687.8 713.2 739.9 768.1 797.8 829 861.7 896.1 932 969.7 1009 1050
 1093 1137 1184 1232 1282 1334 1388 1445 1503]

Um resíduo de Walras que cresce ao longo da trajetória é a assinatura de um coeficiente que o bloco de preços viu e o de quantidades não. É invisível num período isolado e inconfundível num horizonte de vinte e cinco anos.

5. Bem-estar, sem confundir composição com bem-estar

ev = cge.equivalent_variation(ref, alt)
ultimo = ev[ev.year == 2045].set_index("account")
ultimo.ev              # valorado aos preços do cenário de referência
ultimo.ev_base         # valorado aos preços do ano-base
ultimo.ev_pct_account  # por CONTA — leia a nota abaixo antes de usar
ultimo.ev
account
hh_1   -0.1148
hh_2   -0.5904
ultimo.ev_base
account
hh_1   -0.2125
hh_2    -1.091
ultimo.ev_pct_account
account
hh_1   -0.09563
hh_2     -1.004

Quando domicílios migram entre contas, a medida por conta reporta a migração como bem-estar. A economia de brinquedo tem uma variante com duas contas gêmeas — mesmas preferências, mesma taxa de poupança — justamente para mostrar isso:

sam, econ2 = toy.build_with_twin_accounts()   # hh_1, e hh_2 partido em hh_2a e hh_2b

migrado = econ2.hh_facshare.copy()           # move renda entre as gêmeas, sem mudar o total
a, b = econ2.household("hh_2a"), econ2.household("hh_2b")
for f in range(econ2.n_fac):
    tot = migrado[a, f] + migrado[b, f]
    migrado[a, f], migrado[b, f] = 0.6 * tot, 0.4 * tot

r1 = cge.run_path(econ2, years=range(2021, 2024))
r2 = cge.run_path(econ2, years=range(2021, 2024),
                  scenario=cge.Scenario(factor_ownership=migrado))

ev2 = cge.equivalent_variation(r1, r2)
ev2.ev_pct_account.abs().max()               # > 5% — composição, reportada como bem-estar

grupos = {"hh_1": "g1", "hh_2a": "g2", "hh_2b": "g2"}
cge.ev_by_group(ev2, grupos).ev_pct.abs().max()  # ~0 — o zero que de fato está lá
ev2.ev_pct_account.abs().max()
20
cge.ev_by_group(ev2, grupos).ev_pct.abs().max()
2.22e-14

ev_by_group soma rendas reais dentro do grupo antes de dividir, de modo que o conjunto de domicílios somado é o mesmo nos dois cenários e a razão não depende de quantos migraram. Toda conta precisa estar no mapeamento: descartar uma em silêncio subestimaria o grupo, e o erro pareceria um resultado.

6. Exportações que respondem ao preço mundial

import numpy as np

_, econ_cet = toy.build(model_spec=toy.spec(export_rule="cet", sigma_cet=2.0))
j = econ_cet.commodity("c_ind")
pe = np.ones(econ_cet.n_com)
pe[j] = 1.2                                        # o mundo paga 20% mais pela indústria

antes = cge.solve_period(econ_cet)
depois = cge.solve_period(econ_cet, scenario=cge.Scenario(export_price=pe))

depois.exports[j] / depois.Q[j]    # 0,119: a parcela exportada sobe de 0,100
depois.pe[j] / depois.pd[j]        # 1,09: exportar agora rende mais que vender em casa
depois.exchange_rate               # 0,89: o câmbio se valoriza e absorve parte do choque
depois.exports[j] / depois.Q[j]
0.1185
depois.pe[j] / depois.pd[j]
1.089
depois.exchange_rate
0.895

Sob export_rule="cet" o produto se reparte entre mercado interno e exportação ao longo de uma fronteira de transformação, e o preço doméstico é o que faz a receita dessa fronteira igualar o custo unitário. Com participações fixas, o padrão, export_price é recusado em vez de ignorado em silêncio.

_, econ_big = toy.build(model_spec=toy.spec(export_rule="cet", sigma_cet=2.0,
                                           export_demand={"c_ind": 2.0}))
big = cge.solve_period(econ_big, scenario=cge.Scenario(export_price=pe))

big.world_export_price[j]        # 1.12: vender mais desce pela curva de demanda do comprador
big.exports[j] / econ_big.exp0[j]    # 1.14: as exportações sobem 14%, contra 22% do tomador de preço
big.world_export_price[j]
1.124
big.exports[j] / econ_big.exp0[j]
1.14

Uma região que vende boa parte de um produto não é tomadora de preço. ModelSpec.export_demand põe as suas exportações numa curva de demanda, E / E0 = (pWe / export_price)^(-η), a forma do lrgcge de Hosoe; import_supply faz o mesmo com o preço mundial de importação. No ano-base as duas curvas passam pelo ponto calibrado.

7. Impostos, transferências e capital estrangeiro

import dataclasses

_, econ_i = toy.build_with_institutions()
econ_i.hh_dtax          # alíquota de imposto direto por domicílio, lida da MCS
econ_i.fac_row_share    # parcela de cada fator que pertence ao exterior
econ_i.foreign_sav      # 18: a conta corrente, e não só a balança comercial (10)

cresc = cge.Growth(labour=0.01, tfp=0.01)
anos = range(2021, 2031)
ied = cge.run_path(econ_i, years=anos, growth=cresc,
                   scenario=lambda ano, t: cge.Scenario(fdi={"f_cap": 3.0}))
doado = cge.run_path(econ_i, years=anos, growth=cresc,
                     economy=lambda ano, t: dataclasses.replace(
                        econ_i, foreign_sav=econ_i.foreign_sav + 3.0))

ied[2030].factor_income_abroad > doado[2030].factor_income_abroad   # os lucros saem
ied[2030].hh_income.sum() < doado[2030].hh_income.sum()             # e a renda local é menor
econ_i.hh_dtax
[0.1154 0.03226]
econ_i.fac_row_share
[0 0.1667]
econ_i.foreign_sav
18
ied[2030].factor_income_abroad > doado[2030].factor_income_abroad
True
ied[2030].hh_income.sum() < doado[2030].hh_income.sum()
True

O mesmo fluxo de capital, duas leituras. Como fdi, ele financia investimento e vira capital de propriedade estrangeira, cujo retorno sai pela conta corrente. Como mera poupança externa, o capital fica com os residentes e todo o ganho que produz é creditado a eles. É a diferença entre medir o efeito de um investimento estrangeiro e superestimá-lo.

8. Vários estoques de capital e uma tecnologia que muda com o tempo

_, econ_k = toy.build_with_specific_capital()     # a indústria usa um capital só dela

g = cge.Growth(capital_factor=("f_cap", "f_kind"),
               depreciation={"f_cap": 0.05, "f_kind": 0.08},
               allocation=2.0)                     # o investimento segue o aluguel relativo

def economia(ano, t):                             # a indústria poupa 2% de insumo agrícola ao ano
    io = econ_k.io.copy()
    io[econ_k.commodity("c_agr"), econ_k.activity("a_ind")] *= 0.98 ** t
    return dataclasses.replace(econ_k, io=io)

traj = cge.run_path(econ_k, years=range(2021, 2036), growth=g, economy=economia)
traj.capital_stock[-1]     # um estoque por fator de capital
traj.worst_walras          # continua à precisão de máquina
traj.capital_stock[-1]
[359.1 379.2]
traj.worst_walras
3.359e-15

Uma mudança que não é preço, tributo nem dotação (a adoção de uma tecnologia, um plano de financiamento) entra pelo argumento economy, que devolve a economia de cada ano. É o que um cenário de transição energética precisa: os coeficientes técnicos mudam, e os blocos de preço e de quantidade continuam vendo a mesma tecnologia.

9. Com a sua própria MCS

A economia de brinquedo existe para os exemplos rodarem. Com dados seus, o que muda são as duas primeiras linhas e o ModelSpec:

sam  = cge.SAM.read("minha_mcs.parquet")       # ou SAM.from_long(df) / SAM.from_square(df)
sam.check_balance()                            # porta dura: nada roda sobre MCS desbalanceada

spec = cge.ModelSpec(
    activity_prefix="a_", commodity_prefix="c_", household_prefix="hh_",
    factors=("f_lab", "f_cap"),
    government="gov", investment="s_i", rest_of_world="row",
    numeraire_factor="f_lab",         # o mercado dele fica para a lei de Walras
    value_added="ces", sigma_va=0.5,
    sigma_armington=2.0,              # escalar, ou um dicionário por mercadoria
    export_rule="fixed_shares",       # ou "cet": a elasticidade CET não tem fonte de insumo-produto
    household_demand="cobb_douglas",  # ou "les", com o parâmetro de Frisch
    negative_value_added="leontief",  # um excedente operacional negativo é dado, não erro
)
econ = cge.calibrate(sam, spec, population={"hh_d1": 8.3e6, "hh_d2": 8.3e6})

assert cge.base_year_reproduction(econ)["ok"]  # não prossiga sem isto
assert cge.homogeneity_test(econ)["ok"]
econ.diagnostics["unmodelled_flows"]           # células da MCS que o modelo não lê

Contas são encontradas por prefixo, ou por listas explícitas — que são a escolha mais segura numa MCS cuja nomenclatura não é sistemática, já que uma regra de prefixo que perde uma conta em silêncio produz um modelo que balanceia e está errado.

10. Multiplicadores, com todos os preços fixos

sam_toy, _ = toy.build()
m = cge.accounting_multipliers(sam_toy, spec=toy.spec())  # gov, investment, rest of world exogenous

m.M.loc["hh_2", "c_agr"]         # 0.546: o que uma unidade de demanda por agricultura soma à renda do domicílio 2
paths = cge.structural_paths(m, "c_agr", "hh_2", threshold=1e-3)
paths[["path", "total", "share"]].head(2)
# (c_agr, a_agr, f_lab, hh_2)  0.323  0.59    pelos salários, com a realimentação de fora do caminho
# (c_agr, a_agr, f_cap, hh_2)  0.131  0.24
m.M.loc["hh_2", "c_agr"]
0.5463
paths[["path", "total", "share"]].head(2)
                          path  total  share
0  (c_agr, a_agr, f_lab, hh_2) 0.3227 0.5907
1  (c_agr, a_agr, f_cap, hh_2) 0.1312 0.2401

A mesma MCS lida como modelo a preço fixo: as quantidades respondem a uma injeção nas proporções do ano-base. leontief_multipliers deixa endógena só a produção, fixed_price_multipliers põe as famílias em propensões marginais, e linkages dá a tabela de setores-chave, para trás e para frente. Somadas sobre todos os caminhos elementares, as influências totais dão o multiplicador.

11. Contas

taxed = cge.solve_period(econ, scenario=cge.Scenario(product_tax=econ.ptax * 1.5))
na = cge.national_accounts(taxed)      # o contrafactual do exemplo 2
na["gdp_expenditure"], na["gdp_income"]   # 202.78 e 202.78: o PIB pelos dois lados, em qualquer equilíbrio
pi = cge.price_indices(taxed)
pi.loc["gdp", ["fisher", "quantity_fisher"]]  # 1.040, 0.9999: preços 4% acima, volume parado
cge.government_account(taxed)["saving"]   # 12.04, contra 5 no ano-base
na["gdp_expenditure"], na["gdp_income"]
(202.8, 202.8)
pi.loc["gdp", ["fisher", "quantity_fisher"]]
fisher              1.04
quantity_fisher   0.9999
cge.government_account(taxed)["saving"]
12.04

O PIB a preços correntes pelos lados da despesa e da renda, índices de Laspeyres, Paasche e Fisher para cada componente da demanda final, para o PIB e para os termos de troca, e a conta do governo por instrumento. A conta do governo é registrada pelo solver na solução, então a poupança dela é a que financiou o investimento.

12. Quanto um resultado depende das elasticidades

run = cge.spec_sweep(sam, toy.spec(), scenario=cge.Scenario(product_tax=econ.ptax * 1.5),
                    measure=lambda base, alt: {"gdp_pct": 100 * (
                        cge.real_gdp(alt)["expenditure"] / cge.real_gdp(base)["expenditure"] - 1)})
params = {"sigma_va": cge.Uniform(0.3, 1.3), "sigma_armington": cge.Uniform(1.5, 3.5)}
cge.sensitivity(run, params).summary()       # quadratura de Stroud: 4 pontos para 2 parâmetros
cge.sensitivity(run, params, method="monte_carlo", draws=40).summary()
cge.sensitivity(run, params).summary()
             mean       sd
gdp_pct -0.008932 0.001384
cge.sensitivity(run, params, method="monte_carlo", draws=40).summary()
             mean      sd    q0.05      q0.5     q0.95    min       max
gdp_pct -0.008932 0.00138 -0.01149 -0.008389 -0.007568 -0.012 -0.007453

Cada ponto recalibra e resolve o ano-base e o cenário. A regra de Stroud dá uma média exata até o grau três nos parâmetros com 2n soluções; o Monte Carlo acrescenta quantis. A elasticidade de Armington perto de 1 fica de fora de propósito: com parcelas de exportação fixas o câmbio não é determinado em 1, e é mal condicionado perto dele.

13. Que choque fez o quê

_, econ_tt = toy.build_with_trade_taxes()
reform = cge.Scenario(tariff=np.zeros(2), world_price=np.array([1.1, 0.95]),
                      product_tax=econ_tt.ptax * 1.2)
parts = cge.subtotals(econ_tt, cge.Scenario(), reform,
                      {"tariff": ["tariff"], "world": ["world_price"], "tax": ["product_tax"]},
                      measure=lambda p: {"gdp": cge.real_gdp(p)["expenditure"]})
parts[["tariff", "world", "tax", "change", "residual"]]
# gdp  -0.723  -0.125  -0.007   -0.855   ~1e-10      as partes somam a variação resolvida
parts[["tariff", "world", "tax", "change", "residual"]]
     tariff   world       tax  change   residual
gdp -0.7235 -0.1245 -0.006899 -0.8549 -2.806e-10

A decomposição que o GEMPACK chama de subtotals. Todos os choques andam juntos pela reta da referência à alternativa, e o termo de cada grupo é integrado à parte, então as partes somam o total sem depender de ordem alguma.

14. De onde veio uma variação de bem-estar

from cgelab.examples import hosoe

_, lrg = hosoe.build(large_country=True)
w = cge.welfare_decomposition(lrg, cge.Scenario(), cge.Scenario(tariff=np.zeros(2)),
                              closure=hosoe.CLOSURE)
w["allocative_tariff"], w["terms_of_trade"]   # +0.054, -0.831: o ganho com a tarifa, e o que um país grande paga por ele lá fora
w["absorption_households"]                    # +0.506: a VE de Hosoe para o mesmo experimento é +0,508
w["residual"]                                 # ~1e-10: os termos somam a variação da absorção
w["allocative_tariff"], w["terms_of_trade"]
(0.05421, -0.8315)
w["absorption_households"]
0.506
w["residual"]
-3.304e-10

A decomposição de Huff e Hertel, escrita para os instrumentos deste modelo: dotações, tecnologia, um termo alocativo por distorção, termos de troca e conta externa, integrados ao longo da trajetória. A medida é a absorção real, com governo e investimento. A parte das famílias é a que se compara a uma VE.

15. Um preço de carbono, com reciclagem

import pandas as pd

_, inst = toy.build_with_institutions()
fuel = pd.DataFrame(0.0, index=inst.coms, columns=inst.acts)
fuel.loc["c_ind", ["a_agr", "a_ind"]] = [3.0, 12.0]          # toneladas queimadas na produção
green = cge.add_emissions(inst, intermediate=fuel, household={"c_ind": 5.0},
                          process={"a_ind": 2.0})
neutral = cge.Closure(government_target="revenue", government_adjusts="product_tax")
r = cge.solve_period(green, neutral, cge.Scenario(carbon_price=0.5))

cge.emissions(r)["total"]         # 21.79, eram 22 no ano-base
r.tax_scale                       # 0.26: a receita do carbono paga um corte de 74% nas alíquotas sobre produtos
cge.emissions(r)["total"]
21.79
r.tax_scale
0.2648

As emissões são uma conta satélite calibrada a partir dos totais do ano-base, por fonte. O preço do carbono é um imposto por unidade de produto, pela intensidade de emissão, nas atividades, e um imposto específico no combustível das famílias. Com meta de receita, outro imposto se move para manter a arrecadação; aqui, o imposto sobre produtos.

16. Combustível que responde ao preço

_, nested = toy.build_with_institutions(model_spec=toy.spec(energy=["c_ind"], sigma_vae=0.6))
nested = cge.add_emissions(nested, intermediate=fuel, household={"c_ind": 5.0},
                           process={"a_ind": 2.0})
r2 = cge.solve_period(nested, neutral, cge.Scenario(carbon_price=0.5))
cge.emissions(r2)["total"]         # 20.85, contra 21,79 com coeficientes fixos
r2.input_coefficients[1]           # combustível por unidade de produto: 0,089 e 0,276, eram 0,095 e 0,286
cge.emissions(r2)["total"]
20.85
r2.input_coefficients[1]
[0.08882 0.2762]

Com ModelSpec.energy, cada atividade compra um agregado de energia que substitui valor adicionado. O carbono de um combustível entra no preço que aquela atividade paga por ele, então o mesmo preço de carbono muda como se produz, não só quanto. Com as duas elasticidades em zero, o ninho é o modelo Leontief, à precisão de máquina.

17. Trabalho que se move só em parte

pools = pd.DataFrame([[30.0, 10.0], [10.0, 60.0]], index=["rural", "urbano"],
                     columns=["a_agr", "a_ind"])   # quem trabalhava onde no ano-base
spec_m = toy.spec(numeraire_factor="f_cap", factor_mobility={"f_lab": 2.0})
econ_m = cge.calibrate(sam_toy, spec_m, factor_pools={"f_lab": pools})
caro = cge.Scenario(world_price=np.array([1.0, 1.3]))   # manufaturas importadas 30% mais caras
r_m = cge.solve_period(econ_m, scenario=caro)
r_m.factor_prices_by_activity[0]    # o salário na agricultura e na indústria: 0,985 e 1,015
r_m.factor_pools["f_lab"]["price"]  # o que cada pool ganha: 0,993 o rural, 1,010 o urbano
r_m.factor_prices_by_activity[0]
[0.9853 1.015]
r_m.factor_pools["f_lab"]["price"]
[0.9928 1.01]

Com factor_mobility, o trabalho se reparte entre as atividades por uma CET de elasticidade 2, e cada atividade paga o seu salário. Com todos os fatores móveis, o mesmo choque eleva o salário único para 1,004. Os pools de origem, uma matriz de proximidade, mostram quem ganha: o pool urbano trabalha sobretudo na indústria, que o choque expande. Com elasticidade zero, o modelo é o trabalho partido num fator por atividade, e factor_mobility_form="logit" preserva o número de pessoas em vez da fronteira CET.

18. Um piso para o salário formal, e para onde vão os racionados

_, econ_s = toy.build_with_segmented_labour()   # trabalho formal (indústria) e informal (lavoura)
piso = cge.LabourMarket(rigid={"f_for": 1.1}, informal="f_inf")   # salário real formal 10% acima
r_s = cge.solve_period(econ_s, cge.Closure(labour_markets={"labour": piso}))
m = r_s.labour_markets["labour"]
m["employment"]               # formal 66,2 e informal 43,8, eram 70 e 40
m["wage"]                     # 1,097 o formal, 0,864 o informal
m["household_employment"][0]  # hh_1: 56,8 formal, 13,2 informal, eram 60 e 10
fila = cge.LabourMarket(rigid={"f_for": 1.1}, informal="f_inf",
                        unemployment="harris_todaro", unemployment_rate=0.08)
r_q = cge.solve_period(econ_s, cge.Closure(labour_markets={"labour": fila}))
r_q.labour_markets["labour"]["unemployment_rate"]   # 0,196, era 0,08

spec_b = toy.spec(factors=("f_cap", "f_for", "f_inf"), numeraire_factor="f_cap",
                  labour_segments={"labour": ["f_for", "f_inf"]},
                  segment_mobility={"labour": 2.0})   # uma CET entre os segmentos
_, econ_b = toy.build_with_segmented_labour(model_spec=spec_b)
fila_propria = cge.LabourMarket(rigid={"f_for": 1.1}, unemployment_rate=0.08)
r_b = cge.solve_period(econ_b, cge.Closure(labour_markets={"labour": fila_propria}))
r_b.labour_markets["labour"]["queue"]   # 18,6 na fila do emprego formal, eram 6,1
m["employment"]
[66.21 43.79]
m["wage"]
[1.097 0.8637]
m["household_employment"][0]
[56.75 13.25]
r_q.labour_markets["labour"]["unemployment_rate"]
0.1962
r_b.labour_markets["labour"]["queue"]
[18.59 0]

As duas contas de trabalho dividem uma população. No mercado dual, quem o setor formal deixa de contratar trabalha no informal, e o salário informal cai; no Harris–Todaro, as pessoas entram na fila do emprego formal até o salário informal igualar o formal esperado. Nos dois casos, os empregos que o hh_1 perde reaparecem como trabalho informal ou desemprego dele mesmo, e não como renda dos domicílios que já eram informais. Com segment_mobility, os trabalhadores se repartem entre os segmentos por uma CET, o salário informal se equilibra sozinho, e quem o setor formal não contrata fica na fila dele.

19. Pobreza e desigualdade nos domicílios de uma pesquisa

pesquisa = toy.survey()                   # doze domicílios sintéticos, escritos à mão
ref_s = cge.solve_period(econ_s)
alt_s = cge.solve_period(econ_s, cge.Closure(labour_markets={"labour": piso}))
micro = cge.microsimulate(pesquisa, ref_s, alt_s)
micro.reconciliation                      # salários formais do hh_1 escalados por 60/51 até a MCS
micro.calibration                         # pesos que movem o emprego formal como o modelo
cge.inequality(micro)                     # Gini de 0,531 para 0,556, Theil T de 0,527 para 0,577
cge.poverty(micro, 3.0)                   # proporção de pobres de 0,698 para 0,731, hiato de 0,459 para 0,490
micro.reconciliation
  group source  survey  sam  factor
0  hh_1  f_cap      32   45   1.406
1  hh_1  f_for      51   60   1.176
2  hh_1  f_inf     6.5   10   1.538
3  hh_2  f_cap    8.75   15   1.714
4  hh_2  f_for    10.5   10  0.9524
5  hh_2  f_inf    30.5   30  0.9836
micro.calibration
  group  constraint  base  target  reached  iterations
0  hh_1  households     7       7        7           5
1  hh_1       f_cap    45      45       45           5
2  hh_1       f_for    60   56.75    56.75           5
3  hh_1       f_inf    10   13.25    13.25           5
4  hh_2  households    11      11       11           4
5  hh_2       f_cap    15      15       15           4
6  hh_2       f_for    10   9.459    9.459           4
7  hh_2       f_inf    30   30.54    30.54           4
cge.inequality(micro)
                ref    alt  change
gini         0.5307 0.5558 0.02518
theil_t      0.5269 0.5765 0.04967
theil_l      0.4877 0.5436 0.05598
ge_2         0.8169 0.8888 0.07193
atkinson_0.5  0.228 0.2496 0.02156
atkinson_1   0.3859 0.4194 0.03343
atkinson_2   0.5538  0.591 0.03719
cge.poverty(micro, 3.0)
              ref    alt  change
line alpha                      
3.0  0     0.6978 0.7309 0.03307
     1     0.4588 0.4899 0.03111
     2      0.325 0.3529 0.02792

Cada domicílio da pesquisa pertence a uma conta da MCS: sua renda de cada fonte varia como a da conta, e o seu próprio orçamento define o seu índice de preços. Antes, a pesquisa é escalada à MCS, fonte por fonte, e os fatores são reportados em vez de escondidos. Quem perde o emprego formal entra pelos pesos: dentro de cada conta, eles são recalibrados para o emprego formal e informal da pesquisa variar como o do modelo, e é isso que move a proporção de pobres.

20. Pessoas que escolhem: microssimulação comportamental

pessoas = cge.microsimulate(pesquisa, ref_s, alt_s, persons=toy.persons())   # 42 pessoas sintéticas
pessoas.workers                           # hh_1: formal de 7,5 para 7,09, informal de 2,5 para 3,31
cge.inequality(pessoas).loc["gini"]       # de 0,526 para 0,542
sorteado = cge.microsimulate(pesquisa, ref_s, alt_s, persons=toy.persons(), method="draws",
                             replications=200, seed=1)
cge.inequality(sorteado).loc["gini"]      # de 0,526 para 0,544, erro-padrão 0,0008
pessoas.workers
  group      column     workers  ref   alt
0  hh_1       f_for       f_for  7.5 7.094
1  hh_1       f_inf       f_inf  2.5 3.311
2  hh_1    inactive    inactive  9.5 9.095
3  hh_1  unemployed  unemployed  2.5 2.499
4  hh_2       f_for       f_for    5  4.73
5  hh_2       f_inf       f_inf   19 19.34
6  hh_2    inactive    inactive 16.5 16.39
7  hh_2  unemployed  unemployed    7 7.035
cge.inequality(pessoas).loc["gini"]
ref       0.5263
alt       0.5421
change   0.01584
cge.inequality(sorteado).loc["gini"]
ref         0.5263
alt         0.5437
alt_se   0.0007506
change     0.01743

Uma escolha ocupacional (logit multinomial) e equações de rendimento são estimadas nas pessoas, e o modelo chega a elas por interceptos deslocados, conta por conta, até o emprego e o rendimento por trabalhador variarem como o modelo diz, como em Bourguignon, Robilliard e Robinson (2005). O padrão usa a expectativa exata dos sorteios condicionais; os próprios sorteios dão uma média de Monte Carlo e o seu erro.

21. As pesquisas do IBGE

pesquisa, pessoas = cge.pnadc("PNADC_032023.txt", "input_PNADC_trimestral.txt",
                              segments={"formal": "f_for", "informal": "f_inf"},
                              group=minhas_contas)
domicilios = cge.pof("Dados_POF/", "Leitura dos Microdados - SAS.txt",
                     commodities=correspondencia, group=minhas_contas)
tru = cge.read_tru("68_tab1_2021.xls", "68_tab2_2021.xls")   # PIB 2021: R$ 9.012.142 milhões

A PNAD Contínua e a POF são lidas com os próprios programas de leitura SAS do IBGE, então nenhuma posição de coluna é digitada à mão. Os trabalhadores são formais ou informais pela proxy do IBGE, e a despesa da POF é anualizada e deflacionada como fazem os programas de tabulação do IBGE. Os microdados são baixados do IBGE e nunca entram no repositório.

22. Uma MCS a partir das tabelas de recursos e usos

make = pd.DataFrame({"c_x": [100.0]}, index=["a_x"])
use = pd.DataFrame({"a_x": [0.0]}, index=["c_x"])
final = pd.DataFrame({"hh": [80.0], "s_i": [20.0]}, index=["c_x"])
va = pd.DataFrame({"a_x": [60.0, 40.0]}, index=["f_lab", "f_cap"])
sut = cge.SupplyUse(make=make, use=use, final=final, imports=pd.Series({"c_x": 0.0}),
                    value_added=va)
montada = cge.sut_to_sam(sut, source="tabelas escritas à mão")
montada.provenance[["source", "note", "total"]]   # poupança das famílias: resíduo, 20
cge.base_year_reproduction(cge.calibrate(montada, cge.ModelSpec(households=["hh"])))["ok"]
montada.provenance[["source", "note", "total"]]
                                     source                  note  total
0                    tabelas escritas à mão           value added    100
1                    tabelas escritas à mão  output, basic prices    100
2                    tabelas escritas à mão      final demand: hh     80
3                    tabelas escritas à mão     final demand: s_i     20
4  assumed: all factor income to households         factor income    100
5                                  residual      household saving     20
6                                 structure         empty account      0
cge.base_year_reproduction(cge.calibrate(montada, cge.ModelSpec(households=["hh"])))["ok"]
True

As tabelas de recursos e usos viram o bloco de produção e a demanda final; quem recebe a renda vem da Conta Econômica Integrada, aqui suposta, e a procedência diz isso. As poupanças são resíduos declarados, e tabelas que não fecham são recusadas onde falham.

23. Trabalho formal e informal, partido por atividade

parcelas = pd.DataFrame([[0.3, 0.8], [0.7, 0.2]], index=["f_for", "f_inf"],
                        columns=["a_agr", "a_ind"])     # de uma pesquisa: segment_shares(...)
partida = cge.split_factor(sam_toy, "f_lab", parcelas)
dois = cge.calibrate(partida, toy.spec(factors=("f_cap", "f_for", "f_inf"),
                                       numeraire_factor="f_cap",
                                       labour_segments={"labour": ["f_for", "f_inf"]}))
cge.solve_period(dois).residual                    # o ano-base, à precisão de máquina
cge.solve_period(dois).residual
3.384e-16

O trabalho é partido em formal e informal em cada atividade por parcelas de uma pesquisa, e as duas contas viram um grupo de segmentos. Com um só salário, o modelo partido tem exatamente o equilíbrio do fator único; um salário formal rígido é que faz a diferença. split_households faz o mesmo com uma conta de famílias e os grupos de uma pesquisa.

24. Uma linha de base que atinge uma trajetória do PIB

pib = pd.Series([0.046, 0.029, 0.032, 0.025], index=range(2022, 2026))   # crescimento real
base = cge.calibrate_baseline(econ, years=range(2021, 2026), gdp=pib,
                              growth=cge.Growth(labour=0.01))
base.factor                                        # a produtividade que atinge cada ano
politica = base.run(cge.Scenario(product_tax=econ.ptax * 1.2))
cge.real_gdp(politica[2025])["expenditure"] / base.achieved[2025] - 1
base.factor
2021       1
2022   1.017
2023   1.023
2024   1.031
2025   1.035
cge.real_gdp(politica[2025])["expenditure"] / base.achieved[2025] - 1
0.004055

O PIB real é dado, observado ou projetado, e a produtividade é a incógnita, resolvida ano a ano porque o capital de cada ano vem do investimento dos anteriores: um número por ano, ou um por atividade com o valor adicionado setorial. Uma política roda então com a produtividade calibrada, e o PIB se afasta da trajetória; sem política, a linha de base volta exatamente.

25. Capital putty-clay

safras = cge.split_vintages(sam_toy, 0.08)         # 8% do capital de cada atividade é novo
argila = cge.calibrate(safras, toy.spec(factors=("f_lab", "f_cap", "f_cap_new"),
                                        vintages={"f_cap": "f_cap_new"}))
trajetoria = cge.run_path(argila, years=range(2021, 2031),
                          scenario=cge.Scenario(world_price=np.array([1.0, 1.15])))
trajetoria.capital_by_pool["f_cap"].iloc[[0, -1]]   # capital instalado, por atividade
trajetoria[2021].vintage_value_added                 # os feixes velho e novo
trajetoria.capital_by_pool["f_cap"].iloc[[0, -1]]
      a_agr  a_ind
2021    230    322
2030  300.2  444.9
trajetoria[2021].vintage_value_added
[[59.8 96.6]
 [4.306 9.294]]

O capital instalado fica no seu setor e mantém a técnica com que foi construído; o capital novo vai para onde rende mais. Quando o preço da indústria sobe, só a safra nova acompanha no começo, então o produto responde menos que com capital móvel, e alcança à medida que a indústria instala mais do investimento de cada ano.

As três portarias

Não são formalidades. Um modelo que falha a primeira não está calibrado.

# cge.report(econ) em toy.build_with_margins()
cgelab diagnostics -- Economy(3 activities, 3 commodities, 2 households, 2 factors)
  base-year reproduction : PASS (worst 7.11e-16)
      imports                7.11e-16
      activity_output        4.26e-16
      commodity_output       4.26e-16
      exports                3.55e-16
  Walras residual        : -2.41e-16 [f_lab]
  price homogeneity      : PASS (worst 3.72e-16)
PortariaO que pega
Reprodução do ano-baseSem choque, o modelo tem de devolver a MCS: todo preço em um, toda quantidade no seu valor de base. É o que pega um erro de unidade na calibração — um preço de comprador tratado como preço básico, um tributo lançado sobre a base errada — antes que ele seja absorvido num resultado.
Lei de WalrasResíduo não nulo significa renda sendo perdida ou fabricada em algum lugar, e o tamanho do vazamento não limita o tamanho do erro que causa.
HomogeneidadeEscalar o numerário tem de deixar toda magnitude real intacta. Falha significa que uma magnitude nominal denominada no numerário ficou presa enquanto o nível de preços se moveu — poupança externa, meta de balança comercial, um tributo específico que entra aditivamente. Cada uma desloca uma alocação real, e nenhuma é visível na saída.

O que tem na caixa

MCS

Leitura em formato longo ou quadrado, verificação de balanço como porta dura e RAS que preserva sinais, para as células negativas que MCS reais têm.

Calibração

Coeficientes em preços básicos, bloco de margens, valor adicionado negativo, subsistência LES, atividades de conta própria, e a lista das células da MCS que o modelo não lê.

Produção

Leontief entre agregado intermediário e valor adicionado, com CES, Cobb-Douglas ou Leontief no ninho de fatores.

Comércio

Armington CES; exportações por participações fixas ou por CET com preço mundial de exportação.

Instituições

Impostos diretos, transferências do governo, remessas, renda de fatores do governo e do exterior; a poupança externa é a conta corrente.

Dinâmica

Recursiva, com um ou vários estoques de capital, investimento repartido pelo retorno, IED que cria capital estrangeiro e economia por ano.

Fechamentos

Investimento, governo e setor externo, cada um com a sua variável de ajuste. Cinco combinações testadas.

Bem-estar

VE e VC, com agregação por grupo imune à composição e as duas réguas de preço explicitadas.

Réplica em JuMP

O mesmo equilíbrio escrito como modelo JuMP sem aninhamento, para conferir a álgebra com o Ipopt.

Uma advertência que o padrão codifica

A elasticidade de transformação (CET) é o único parâmetro de um EGC padrão que as fontes de insumo-produto não fornecem. Fixar participações de exportação mantém esse parâmetro fora do cenário-base por inteiro, de modo que nenhum número de destaque repouse sobre um valor inventado. export_rule="cet" resolve a fronteira de transformação por inteiro, para quando o exercício precisa que as exportações respondam a preço. É uma posição metodológica, e o padrão a codifica.

Validação

Testes sobre MCS escritas à mão

Oito economias de brinquedo com cada entrada escrita no código, de modo que a comparação é contra aritmética e não contra saída anterior:

  • o caso simples;
  • um com bloco de margens de contabilidade nacional (serviços de margem vendidos na coluna de oferta das demais mercadorias, contra uma entrada negativa na própria diagonal);
  • um com atividade de conta própria;
  • um com contas gêmeas, para o teste de composição;
  • um com impostos diretos, transferências, remessas e capital do governo e do exterior;
  • um com capital específico de setor;
  • um com tarifa e imposto indireto sobre o produto dentro dos preços;
  • uma economia fechada de um bem com produção Cobb–Douglas, que é o modelo de crescimento estocástico contra o qual a ponte com o dsgelab é conferida.

Os testes cobrem reprodução do ano-base à precisão de máquina, homogeneidade, e Walras sob choque em todas as combinações de fechamento e em cada data de uma trajetória em crescimento. Cobrem também a identidade de receita da CET e a propriedade estrangeira do capital comprado com IED. E todo exemplo executável desta página roda na suíte.

Uma réplica independente em JuMP

to_jump escreve o mesmo equilíbrio como modelo JuMP em que nada é aninhado: o ponto fixo de preços, a série da reciclagem e a relação entre poupança e arrecadação são restrições comuns, e o Ipopt as resolve de um ponto de partida deslocado. Em 50 casos (margens, curvas de demanda de exportação e de oferta de importação, tarifa e imposto sobre o produto dentro dos preços, o fechamento de Hosoe, metas do governo, preço do carbono, dois ninhos de energia, conta própria, CET, impostos diretos, transferências, capital estrangeiro, IED, LES, mobilidade imperfeita de fatores, trabalho segmentado, safras de capital e fechamentos alternativos), os dois resolvedores concordam a no máximo 1.2e-11. Perturbar os dados exportados faz a comparação divergir de 1e-3 a 3e-1, o que mostra que ela enxerga erro.

Como se compara ao que já existe

Aqui a diferença não é de opinião: é de escala.

CapacidadecgelabpycgePSLmodels/CGEpython-cge
Escala demonstradaMCS aplicada
ainda não pública
didáticaMCS 16×18
4 setores
2 bens
OrigemMCS aplicadaporte do splcge.gamsHosoe et al. (2010)Hosoe et al., p. 72
Bloco de margens de comércio e transportesim?——
Valor adicionado negativo tratadosim?——
Atividades de conta própriasim———
Fechamentos alternativos5 combinações?——
Walras como verificação, não equaçãosim?——
Reprodução do ano-base e homogeneidade automáticassimparcial——
CET resolvida, com preço mundial de exportaçãosim?sim—
Impostos diretos, transferências, capital estrangeiro e IEDsim???
Dinâmica recursivavários capitais—sim—
Bem-estar (VE, VC, por grupo)sim———

Quem faz EGC aplicado usa GAMS ou GEMPACK, e com bons motivos: resolvedores dedicados, comunidade, bases como a GTAP. Os três pacotes de EGC em Python que encontrei são implementações do modelo-texto de Hosoe, Gasawa e Hashimoto (2010) ou do splcge da biblioteca do GAMS — dois a quatro setores; o PSLmodels/CGE diz de si mesmo, no README, que é "muito preliminar" e pede que não seja citado.

A afirmação defensável do cgelab é estreita: se você quer um EGC de escala aplicada em Python, sem licença comercial e com as portarias contábeis embutidas, não achei alternativa. Isso não é o mesmo que cobrir o que um GAMS cobre — não há múltiplas regiões, nem estimação de elasticidades, nem trinta anos de uso. A tabela completa, com o que foi verificado lendo código e o que apenas se leu na documentação, está em docs/comparison.pt-br.md.

Como citar

Se o pacote entrou num resultado publicado, ele entra nas referências. Software é um insumo metodológico como qualquer outro, e a versão importa: um número que só se reproduz numa versão específica não é reproduzível sem ela.

BibTeX

@software{cgelab_2026,
  author  = {Moreira, Gustavo Hoffmann},
  title   = {{cgelab}: a general toolkit for SAM-based computable general equilibrium models in Python},
  year    = {2026},
  version = {0.5.5},
  license = {MIT},
  doi     = {10.5281/zenodo.23458851},
  url     = {https://github.com/GustavoHM/cgelab},
  note    = {Pacote em Python}
}

ABNT

MOREIRA, Gustavo Hoffmann. cgelab: a general toolkit for SAM-based computable general equilibrium models in Python.
Versão 0.5.5. 2026. Software em Python. Zenodo. DOI: 10.5281/zenodo.23458851. Disponível em:
https://doi.org/10.5281/zenodo.23458851. Acesso em: [data].

No corpo do texto

Cite a versão junto do resultado, não só o nome:

O modelo foi calibrado e resolvido com o cgelab 0.5.5 (Moreira, 2026), em dinâmica recursiva de 2021 a 2045.

Cite a versão arquivada. Uma URL de repositório não é uma referência arquivável: o conteúdo muda e o link morre. Cada versão do cgelab é arquivada no Zenodo, e o DOI que o Zenodo atribui a ela é a referência a citar: a versão 0.5.0 é 10.5281/zenodo.23458852, e o DOI conceitual 10.5281/zenodo.23458851 aponta sempre para a versão mais recente. O arquivo CITATION.cff na raiz do pacote, que traz o ORCID do autor e o DOI conceitual, está no formato que o GitHub lê, e o botão "Cite this repository" fornece a entrada a partir dele.

E as ferramentas que este pacote usa

NumPy e SciPy pedem citação explicitamente e sustentam tudo o que está aqui; o pandas lê e guarda a MCS. Se o resultado foi conferido pela réplica, entram também o JuMP e o Ipopt.

Documentação

A documentação detalhada está em inglês, exceto a comparação.

Guia de uso

Da MCS ao bem-estar, com as armadilhas de cada etapa.

Referência da API

Toda função, todo campo, toda forma de array.

Notas de projeto

As decisões contábeis e os erros que cada uma evita.

Portar um pipeline

Tabela de tradução de um solucionador existente para Scenario e Closure.

Comparação

Onde o pacote está frente aos outros EGC em Python e ao GAMS, e onde não ganha.

Mudanças

O que mudou de uma versão para outra.

Roadmap

O que ainda falta no pacote, e por que importa.

Replicações

Modelos publicados refeitos a partir da matriz de contabilidade social deles, e onde um resultado ainda não pode ser reivindicado.