Três objetos separáveis, e mantê-los assim é o ponto
| Objeto | Diz |
|---|---|
SAM | o que o ano-base foi |
ModelSpec | o que se supõe sobre substituição e forma funcional |
Closure | quais 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)
| Portaria | O que pega |
|---|---|
| Reprodução do ano-base | Sem 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 Walras | Resí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. |
| Homogeneidade | Escalar 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.
| Capacidade | cgelab | pycge | PSLmodels/CGE | python-cge |
|---|---|---|---|---|
| Escala demonstrada | MCS aplicada ainda não pública | didática | MCS 16×18 4 setores | 2 bens |
| Origem | MCS aplicada | porte do splcge.gams | Hosoe et al. (2010) | Hosoe et al., p. 72 |
| Bloco de margens de comércio e transporte | sim | ? | — | — |
| Valor adicionado negativo tratado | sim | ? | — | — |
| Atividades de conta própria | sim | — | — | — |
| Fechamentos alternativos | 5 combinações | ? | — | — |
| Walras como verificação, não equação | sim | ? | — | — |
| Reprodução do ano-base e homogeneidade automáticas | sim | parcial | — | — |
| CET resolvida, com preço mundial de exportação | sim | ? | sim | — |
| Impostos diretos, transferências, capital estrangeiro e IED | sim | ? | ? | ? |
| Dinâmica recursiva | vá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.