English · Português

cgelab

Você fornece uma matriz de contabilidade social (MCS). A partir dela, o pacote calibra um EGC padrão com funções aninhadas, resolve cada período, simula uma trajetória recursivo-dinâmica e mede o bem-estar. As verificações contábeis que dão credibilidade a tudo isso já vêm embutidas, em vez de ficarem a cargo do usuário.

github.com/GustavoHM/cgelab

Python 3.11+ numpy, scipy, pandas Walras como verificação 5 fechamentos CET, IED, vários capitais confere com o JuMP até 1,2e-11 MIT · v0.6.0

Três objetos, mantidos separados de propósito

ObjetoO que registra
SAMcomo era o ano-base
ModelSpecas hipóteses sobre substituição e forma funcional
Closurequais saldos são fixados e quem se ajusta

Os resultados podem depender de forma decisiva dos dois últimos. Por isso, eles acompanham cada resultado, em vez de ficarem enterrados num desvio condicional dentro do solucionador.

O mercado do fator numerário fica fora do sistema resolvido. Ele é fechado pela lei de Walras, e assim o seu resíduo serve como verificação independente da contabilidade, e não como uma identidade que o solucionador foi instruído a satisfazer. O resíduo é registrado em todos os períodos.

Instalação

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

Para desenvolvimento:

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

Exemplos

Todos rodam sem alteração logo após o pip install. Usam a economia didática incluída no pacote, escrita entrada por entrada para que o ano-base possa ser conferido no papel, e são os mesmos exemplos executados pela suíte de testes.

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 de cada 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. Uma cunha sobre o preço ao consumidor, e para quem vai 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 conforme a 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 das duas

capital.hh_income[0] > percap.hh_income[0]           # o domicílio com mais 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

A escolha não é detalhe de implementação. A propriedade do capital se concentra no topo da distribuição. Com "capital", os pobres pagam a cunha nas compras e os ricos a recebem como renda, de modo que parte de qualquer regressividade medida decorre dessa hipótese. A combinação convexa permite tratar o achado como questão de grau, e não de sim ou não: em geral existe um peso em que o gradiente distributivo passa por zero, e é a distância até esse peso que indica a robustez do resultado.

A receita reciclada forma uma série infinita: creditada à renda, ela aumenta o gasto, que aumenta a arrecadação da cunha, que volta a aumentar a renda. Como a demanda é afim no gasto, a série tem soma em forma fechada. Iterá-la também converge, mas introduz descontinuidades que estragam 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 primeiro número a conferir
ref.capital_stock         # estoque obtido 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 denuncia um coeficiente que entrou no bloco de preços mas não no de quantidades. Num período isolado, o problema passa despercebido; num horizonte de vinte e cinco anos, salta aos olhos.

5. Bem-estar sem confundir mudança de composição com ganho

ev = cge.equivalent_variation(ref, alt)
ultimo = ev[ev.year == 2045].set_index("account")
ultimo.ev              # avaliada aos preços do cenário de referência
ultimo.ev_base         # avaliada 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 de uma conta para outra, a medida por conta registra a migração como se fosse variação de bem-estar. A economia didática tem uma variante com duas contas gêmeas, de mesmas preferências e mesma taxa de poupança, feita justamente para mostrar isso:

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

migrado = econ2.hh_facshare.copy()           # transfere 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%: mudança de composição registrada 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 valor correto
ev2.ev_pct_account.abs().max()
20
cge.ev_by_group(ev2, grupos).ev_pct.abs().max()
2.22e-14

ev_by_group soma as rendas reais dentro de cada grupo antes de dividir. Assim, o conjunto de domicílios é o mesmo nos dois cenários, e a razão não depende de quantos migraram. Todas as contas precisam constar do mapeamento: se uma fosse descartada sem aviso, o grupo sairia subestimado e o erro passaria por 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 resto do mundo paga 20% a mais pelo bem industrial

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 era 0,100
depois.pe[j] / depois.pd[j]        # 1,09: exportar passa a render mais que vender no mercado interno
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

Com export_rule="cet", a produção se divide entre mercado interno e exportação ao longo de uma fronteira de transformação, e o preço doméstico se ajusta até que a receita nessa fronteira iguale o custo unitário. Com participações fixas, que são o padrão, o pacote rejeita export_price com um erro, em vez de ignorá-lo sem aviso.

_, 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: para vender mais, o preço desce pela curva de demanda do comprador
big.exports[j] / econ_big.exp0[j]    # 1,14: as exportações sobem 14%, contra 22% para um tomador de preço
big.world_export_price[j]
1.124
big.exports[j] / econ_big.exp0[j]
1.14

Uma região que responde por boa parte das vendas mundiais de um produto não é tomadora de preço. ModelSpec.export_demand coloca as exportações dela sobre uma curva de demanda, E / E0 = (pWe / export_price)^(-η), na forma usada no lrgcge de Hosoe; import_supply faz o mesmo para 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 do imposto direto de cada domicílio, lida da MCS
econ_i.fac_row_share    # parcela de cada fator que pertence ao exterior
econ_i.foreign_sav      # 18: o saldo em 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 vão para o exterior
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 admite duas leituras. Lançado como fdi, ele financia investimento e se torna capital de propriedade estrangeira, cuja remuneração sai pela conta corrente. Lançado como simples poupança externa, o capital fica com os residentes, e todo o rendimento que ele gera é creditado a eles. A primeira leitura mede o efeito do investimento estrangeiro; a segunda o superestima.

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 específico

g = cge.Growth(capital_factor=("f_cap", "f_kind"),
               depreciation={"f_cap": 0.05, "f_kind": 0.08},
               allocation=2.0)                     # o investimento segue a remuneração relativa

def economia(ano, t):                             # a indústria usa 2% menos insumo agrícola a cada 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 para cada fator de capital
traj.worst_walras          # continua na precisão de máquina
traj.capital_stock[-1]
[359.1 379.2]
traj.worst_walras
3.359e-15

Mudanças que não são de preço, de tributo nem de dotação, como a adoção de uma tecnologia ou um plano de financiamento, entram pelo argumento economy, que devolve a economia de cada ano. É disso 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 enxergando a mesma tecnologia.

9. Com a sua própria MCS

A economia didática serve para que os exemplos rodem sem dados externos. Com dados próprios, mudam apenas 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()                            # bloqueante: nada roda com a 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 deste fator fica a cargo da 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": as tabelas de insumo-produto não fornecem a elasticidade CET
    household_demand="cobb_douglas",  # ou "les", com o parâmetro de Frisch
    negative_value_added="leontief",  # excedente operacional negativo é um dado, não um erro
)
econ = cge.calibrate(sam, spec, population={"hh_d1": 8.3e6, "hh_d2": 8.3e6})

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

As contas são identificadas por prefixo ou por listas explícitas. Numa MCS de nomenclatura irregular, as listas são a opção mais segura: uma regra de prefixo que deixa escapar uma conta sem aviso produz um modelo que fecha e, ainda assim, está errado.

10. Multiplicadores a preços fixos

sam_toy, _ = toy.build()
m = cge.accounting_multipliers(sam_toy, spec=toy.spec())  # governo, investimento e resto do mundo exógenos

m.M.loc["hh_2", "c_agr"]         # 0.546: quanto uma unidade de demanda por produto agrícola acrescenta à 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    via salários, incluindo a realimentação 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

Aqui a mesma MCS é lida como modelo de preços fixos: as quantidades respondem a uma injeção nas proporções do ano-base. leontief_multipliers torna endógena apenas a produção; fixed_price_multipliers usa propensões marginais para as famílias; e linkages monta a tabela de setores-chave, com encadeamentos para trás e para a frente. Somadas sobre todos os caminhos elementares, as influências totais reproduzem o multiplicador.

11. Contas nacionais

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: as duas óticas do PIB coincidem em qualquer equilíbrio
pi = cge.price_indices(taxed)
pi.loc["gdp", ["fisher", "quantity_fisher"]]  # 1.040, 0.9999: preços 4% mais altos, volume estável
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 pacote calcula o PIB a preços correntes pelas óticas 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. Essa conta é registrada pelo solucionador junto com a solução, de modo que a poupança do governo que aparece nela é a mesma 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

Em cada ponto, o modelo é recalibrado e resolvido para o ano-base e para o cenário. Com 2n soluções, a regra de Stroud dá a média exata para funções de até grau três nos parâmetros; o Monte Carlo acrescenta quantis. Valores da elasticidade de Armington próximos de 1 ficam de fora de propósito: com participações de exportação fixas, o câmbio é indeterminado em 1 e mal condicionado nas proximidades.

13. Quanto da variação cabe a cada choque

_, 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 parcelas somam a variação total
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 avançam juntos ao longo do segmento que liga a referência à alternativa, e o termo de cada grupo é integrado separadamente; assim, as parcelas somam o total sem depender da ordem dos choques.

14. De onde vem 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 alocativo de zerar a tarifa, e o que um país grande perde nos termos de troca
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, adaptada aos instrumentos deste modelo: dotações, tecnologia, um termo alocativo para cada distorção, termos de troca e conta externa, integrados ao longo da trajetória. A medida decomposta é a absorção real, incluindo governo e investimento; apenas a parcela das famílias é comparável a uma VE.

15. Precificação de carbono com reciclagem da receita

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, contra 22 no ano-base
r.tax_scale                       # 0.26: a receita do carbono financia um corte de 74% nas alíquotas sobre produtos
cge.emissions(r)["total"]
21.79
r.tax_scale
0.2648

As emissões formam uma conta satélite, calibrada por fonte a partir dos totais do ano-base. Nas atividades, o preço do carbono incide como imposto por unidade produzida, proporcional à intensidade de emissão; nas famílias, como imposto específico sobre o combustível. Com meta de receita, outro imposto se ajusta para manter a arrecadação, que neste exemplo é o imposto sobre produtos.

16. Uso de 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, contra 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 pode substituir valor adicionado. O carbono de um combustível entra no preço que a atividade paga por ele, e assim o mesmo preço de carbono altera não só quanto se produz, mas também como. Com as duas elasticidades iguais a zero, o ninho reproduz o modelo Leontief na 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.

26. O mesmo choque sob quatro fechamentos, e em moeda

_, simples = toy.build()
choque = cge.Scenario(world_price=np.array([1.0, 1.2]))
pib_pct = lambda base, alt: {"pib_pct": 100 * (cge.real_gdp(alt)["expenditure"]
                                               / cge.real_gdp(base)["expenditure"] - 1)}
grade = cge.closure_grid(investment=["savings_driven", "fixed_real"],
                         foreign=["fixed_saving", "fixed_rate"])
varredura = cge.closure_sweep(simples, grade, scenario=choque, measure=pib_pct)
varredura.table
varredura.range()[["min", "max", "same_sign"]]
base, alt = cge.solve_period(simples), cge.solve_period(simples, scenario=choque)
cge.in_money(base, alt).loc["final_demand", ["change", "change_base_prices"]]
varredura.table
                                           pib_pct
closure
default                                    -0.1454
foreign=fixed_rate                         -0.4845
investment=fixed_real                      -0.1458
investment=fixed_real, foreign=fixed_rate  -0.4978
varredura.range()[["min", "max", "same_sign"]]
            min     max  same_sign
pib_pct -0.4978 -0.1454       True
cge.in_money(base, alt).loc["final_demand", ["change", "change_base_prices"]]
              change  change_base_prices
item
consumption  -0.1189              0.3996
government  -0.00778                   0
investment  -0.07754             -0.2573
exports     -0.01755             0.05611
imports     -0.01755              0.4819
gdp          -0.2042             -0.2835

O fechamento que um resultado supõe é uma escolha, e dois fechamentos defensáveis podem discordar. A varredura resolve a referência e o cenário sob cada um, diz quais combinações o pacote recusa e por quê, e dá a amplitude. in_money põe a variação em níveis, a preços correntes e do ano-base, na unidade da MCS vezes unit: uma MCS em milhões e unit=1e-3 dão bilhões.

27. De onde vem a variância

res = cge.sobol(run, params, n=256, bootstrap=100)     # o run e os parâmetros do exemplo 12
res.indices()
res.indices()
                              S1  S1_conf    ST   ST_conf
output  parameter
gdp_pct sigma_va        0.001761 0.009473 0.003 0.0007993
        sigma_armington    1.013   0.1872 1.012    0.1222

Os índices de Sobol repartem a variância de um resultado entre os parâmetros: S1 é a parcela que um parâmetro explica sozinho, ST soma as suas interações. O desenho é o de Saltelli, com n (k + 2) soluções, e o SALib dá os mesmos índices no mesmo desenho. Aqui a elasticidade de Armington carrega quase toda a variância do PIB real. Uma estimativa pode passar de um por erro de amostragem; as semiamplitudes dizem de quanto, e mais pontos de base as reduzem.

28. A MCS também é uma estimativa

sorteios = cge.sam_draws(sam_toy,
    blocks={"intermediario": (["c_agr", "c_ind"], ["a_agr", "a_ind"])},
    concentration=80.0, draws=8, seed=7,
    balance_kwargs={"rel_tol": 1e-12, "abs_tol": 1e-10})
incerteza = cge.sam_sweep(sorteios, toy.spec(), measure=pib_pct, scenario=choque)
incerteza.points[["draw", "sam_distance", "pib_pct"]]
incerteza.summary()
incerteza.points[["draw", "sam_distance", "pib_pct"]]
   draw  sam_distance  pib_pct
0     0     9.736e-05  -0.1447
1     1     7.543e-05   -0.146
2     2     0.0004869  -0.1437
3     3      0.001046  -0.1446
4     4     0.0007494  -0.1434
5     5     0.0004054  -0.1466
6     6      0.001005  -0.1448
7     7     4.359e-05  -0.1451
incerteza.summary()
           mean       sd   q0.05    q0.5   q0.95     min     max
pib_pct -0.1449 0.001002 -0.1464 -0.1447 -0.1435 -0.1466 -0.1434

O choque Dirichlet direto move o bloco intermediário; depois o RAS com preservação de sinal restaura o equilíbrio. Cada MCS é recalibrada e resolvida, e os pontos conservam seu peso e sua distância KL da original. O resumo é a distribuição do resultado sobre os sorteios da MCS, não apenas sobre as elasticidades.

29. O relatório de robustez completo

rel = cge.robustness_report(sam_toy, toy.spec(), scenario=choque, measure=pib_pct,
    sam_blocks={"intermediario": (["c_agr", "c_ind"], ["a_agr", "a_ind"])},
    draws=4, seed=7,
    sam_kwargs={"balance_kwargs": {"rel_tol": 1e-12, "abs_tol": 1e-10}})
rel.tables["monte_carlo"]
rel.tables["oat"][["low", "central", "high"]]
rel.tables["monte_carlo"]
           mean      sd   q0.05    q0.5    q0.95     min      max
pib_pct -0.1395 0.04613 -0.1833 -0.1523 -0.07789 -0.1845 -0.06909
rel.tables["oat"][["low", "central", "high"]]
point                      low  central    high
output  parameter
pib_pct sigma_va        -0.145  -0.1454 -0.1455
        sigma_armington    NaN  -0.1454  -0.224

Uma chamada reúne faixas um de cada vez, quadratura, Monte Carlo, quatro fechamentos, sorteios da MCS e tabelas em moeda. Os resultados completos ficam em parts. Aqui o ponto inferior de Armington é NaN porque vale exatamente um, que este fechamento recusa; oat_failed registra o motivo. Figuras de tornado e distribuição estão disponíveis com o extra opcional plot.

30. Um fechamento histórico limitado

import dataclasses
orcamento = simples.hh_budget.copy()
pesos = orcamento[0] * np.array([1.2, 1.0])
orcamento[0] = pesos / pesos.sum()
economia_observada = dataclasses.replace(simples, hh_budget=orcamento)
observado = cge.solve_period(economia_observada,
    scenario=cge.Scenario(productivity=np.full(simples.n_act, 1.05)))
hist = cge.calibrate_historical(simples, years=[2021],
    targets={"pib": (pd.Series({2021: cge.real_gdp(observado)["expenditure"]}),
                     lambda r: cge.real_gdp(r)["expenditure"]),
             "alimento": (pd.Series({2021: observado.hh_quantity[0, 0]}),
                          lambda r: r.hh_quantity[0, 0])},
    shifters={"pib": ("productivity",),
              "alimento": ("preference", "hh_1", "c_agr")},
    preference_reference={"hh_1": "c_ind"})
hist.achieved
hist.multiplier
hist.preferences["hh_1"]
hist.achieved
       pib  alimento
2021 210.8     34.42
hist.multiplier
      a_agr  a_ind
2021   1.05   1.05
hist.preferences["hh_1"]
      c_agr  c_ind
2021    1.2      1

O PIB e a demanda alimentar observados são os alvos; a produtividade e a preferência relativa por alimento são as incógnitas, resolvidas em conjunto em log. A outra mercadoria fixa a escala das preferências. Depois, hist.run(policy) mantém esses deslocadores e deixa os alvos se moverem. calibrate_baseline usa as mesmas rotinas de raiz.

As três verificações

Não são formalidade. Um modelo que não passa na 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)
VerificaçãoO que detecta
Reprodução do ano-baseSem choque, o modelo precisa reproduzir a MCS: todos os preços iguais a um e todas as quantidades nos valores do ano-base. É essa verificação que flagra erros de unidade na calibração, como um preço de comprador tratado como preço básico ou um tributo aplicado sobre a base errada, antes que eles se diluam num resultado.
Lei de WalrasUm resíduo diferente de zero indica renda perdida ou criada em algum ponto do modelo, e um vazamento pequeno pode causar um erro grande.
HomogeneidadeMultiplicar o numerário por um escalar não pode alterar nenhuma variável real. Se alterar, alguma grandeza nominal expressa em unidades do numerário ficou fixa enquanto o nível de preços mudou: a poupança externa, uma meta de balança comercial, um tributo específico que entra de forma aditiva. Cada uma delas distorce alguma alocação real, e nenhuma aparece na saída.

O que o pacote oferece

MCS

Leitura em formato longo ou quadrado, verificação de balanceamento que impede a execução quando falha e RAS com preservação de sinais, para as células negativas comuns em MCS reais.

Calibração

Coeficientes a 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 o agregado intermediário e o 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 é o saldo em conta corrente.

Dinâmica

Recursiva, com um ou vários estoques de capital, investimento alocado segundo o retorno, IED que gera capital de propriedade estrangeira e economia que pode mudar a cada 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 a mudanças de composição e as duas bases de preço explícitas.

Réplica em JuMP

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

Uma cautela embutida no padrão

A elasticidade de transformação (CET) é o único parâmetro de um EGC padrão que as tabelas de insumo-produto não fornecem. Com participações de exportação fixas, esse parâmetro fica totalmente fora do cenário-base, e nenhum resultado principal depende de um valor arbitrado. Quando o exercício exige que as exportações respondam a preço, export_rule="cet" resolve a fronteira de transformação completa. É uma posição metodológica, e o padrão do pacote a adota.

Validação

Testes com MCS escritas à mão

São oito economias didáticas com cada entrada escrita no código, de modo que os testes comparam o resultado com a aritmética, e não com uma saída anterior:

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

Os testes cobrem a reprodução do ano-base na precisão de máquina, a homogeneidade e a lei de Walras sob choque, em todas as combinações de fechamento e em cada ano de uma trajetória com crescimento. Cobrem também a identidade de receita da CET e a propriedade estrangeira do capital adquirido com IED. Todos os exemplos executáveis desta página também rodam na suíte.

Uma réplica independente em JuMP

to_jump escreve o mesmo equilíbrio como um modelo JuMP sem nenhum aninhamento: o ponto fixo de preços, a série da reciclagem e a relação entre poupança e arrecadação viram restrições comuns, que o Ipopt resolve a partir de um ponto inicial deslocado. Nos 50 casos testados (margens, curvas de demanda de exportação e de oferta de importação, tarifa e imposto sobre o produto incluídos nos 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 solucionadores diferem em no máximo 1.2e-11. Quando os dados exportados são perturbados, a diferença sobe para valores entre 1e-3 e 3e-1, o que mostra que a comparação detecta erros.

Comparação com o que já existe

A diferença, aqui, não é de opinião, e sim de escala.

CapacidadecgelabpycgePSLmodels/CGEpython-cge
Escala demonstradaMCS aplicada
ainda não pública
didáticaMCS 16×18
4 setores
2 bens
OrigemMCS aplicadaadaptação do splcge.gamsHosoe et al. (2010)Hosoe et al., p. 72
Bloco de margens de comércio e transportesim?——
Tratamento de valor adicionado negativosim?——
Atividades de conta própriasim———
Fechamentos alternativos5 combinações?——
Walras como verificação, não como 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———
Bem-estar (VE, VC, por grupo)sim———

Quem trabalha com EGC aplicado usa GAMS ou GEMPACK, e com razão: solucionadores dedicados, uma comunidade estabelecida, bases de dados como a GTAP. Os três pacotes de EGC em Python que encontrei implementam o modelo do livro-texto de Hosoe, Gasawa e Hashimoto (2010) ou o splcge da biblioteca do GAMS, com dois a quatro setores; o README do PSLmodels/CGE descreve o projeto como "muito preliminar" e pede que ele não seja citado.

O que se pode afirmar sobre o cgelab é algo restrito: para um EGC de escala aplicada em Python, sem licença comercial e com as verificações contábeis embutidas, não encontrei alternativa. Isso está longe de cobrir o que o GAMS cobre: não há modelos multirregionais, nem estimação de elasticidades, nem trinta anos de uso acumulado. A tabela completa, que distingue o que foi verificado no código do que só foi lido na documentação, está em docs/comparison.pt-br.md.

Como citar

Se o pacote contribuiu para um resultado publicado, ele deve constar das referências. Software é um insumo metodológico como qualquer outro, e a versão importa: um número que só se reproduz com uma versão específica não pode ser reproduzido sem a indicação dela.

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.6.0},
  license = {MIT},
  doi     = {10.5281/zenodo.23614565},
  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.6.0. 2026. Software em Python. Zenodo. DOI: 10.5281/zenodo.23614565. Disponível em:
https://doi.org/10.5281/zenodo.23614565. Acesso em: [data].

No corpo do texto

Informe a versão junto com o resultado, e não apenas o nome do pacote:

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

Cite a versão arquivada. Uma URL de repositório não serve como referência permanente: o conteúdo muda e o link pode deixar de funcionar. 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.6.0 é 10.5281/zenodo.23614565, e o DOI conceitual 10.5281/zenodo.23458851 aponta sempre para a versão mais recente. O arquivo CITATION.cff, na raiz do pacote, traz o ORCID do autor e o DOI conceitual no formato que o GitHub lê, e o botão "Cite this repository" gera a entrada a partir dele.

As ferramentas em que o pacote se apoia

NumPy e SciPy pedem citação explicitamente e estão na base de tudo o que o pacote faz; o pandas lê e armazena a MCS. Se o resultado foi conferido com a réplica, cite 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

Todas as funções, campos e formatos de array.

Notas de projeto

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

Migrar um pipeline

Correspondência entre os ajustes de um solucionador existente e Scenario e Closure.

Comparação

Como o pacote se situa diante dos outros EGC em Python e do GAMS, e onde fica atrás.

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 reconstruídos a partir das próprias MCS, e os pontos em que um resultado ainda não pode ser dado como replicado.