Metadata-Version: 2.4
Name: nini-datagen
Version: 0.2.0
Summary: Dados sintéticos realistas do Brasil em escala: kernels C++ que escrevem direto em Polars.
Keywords: fake data,synthetic data,dados sintéticos,polars,cpf,cnpj,brasil,testing
Author: Nini (blzrosa)
License-Expression: MIT
License-File: DATA_LICENSES.md
License-File: LICENSE
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Natural Language :: Portuguese (Brazilian)
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: C++
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Testing
Classifier: Typing :: Typed
Project-URL: Documentation, https://github.com/blzrosa/nini-datagen/blob/main/docs/guia.md
Project-URL: Repository, https://github.com/blzrosa/nini-datagen
Project-URL: Issues, https://github.com/blzrosa/nini-datagen/issues
Project-URL: Changelog, https://github.com/blzrosa/nini-datagen/blob/main/CHANGELOG.md
Requires-Python: >=3.11
Requires-Dist: numpy>=1.24
Requires-Dist: polars>=1.30
Requires-Dist: pydantic>=2.7
Requires-Dist: platformdirs>=3
Requires-Dist: pyarrow>=19
Description-Content-Type: text/markdown

# nini-datagen

[![CI](https://github.com/blzrosa/nini-datagen/actions/workflows/ci.yml/badge.svg)](https://github.com/blzrosa/nini-datagen/actions/workflows/ci.yml)
[![wheels](https://github.com/blzrosa/nini-datagen/actions/workflows/wheels.yml/badge.svg)](https://github.com/blzrosa/nini-datagen/actions/workflows/wheels.yml)
[![PyPI](https://img.shields.io/pypi/v/nini-datagen)](https://pypi.org/project/nini-datagen/)
[![Python](https://img.shields.io/pypi/pyversions/nini-datagen)](https://pypi.org/project/nini-datagen/)
[![Polars](https://img.shields.io/badge/polars-%E2%89%A5%201.30-0075ff)](https://pola.rs)
[![Licença: MIT](https://img.shields.io/badge/licen%C3%A7a-MIT-green)](LICENSE)
[![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
[![pyright](https://microsoft.github.io/pyright/img/pyright_badge.svg)](https://microsoft.github.io/pyright/)

Dados sintéticos realistas do Brasil, em escala: CPF, CNPJ (inclusive alfanumérico), nomes com a distribuição real do
IBGE, endereços do CNEFE, telefones, e-mails, textos e mais. Os geradores são kernels C++ que escrevem direto na
memória do Polars (Arrow `string_view`), sem passar string por string pelo Python.

Guia completo em [docs/guia.md](docs/guia.md), números em [docs/desempenho.md](docs/desempenho.md), exemplos em
[examples/](examples/) e mudanças no [CHANGELOG](CHANGELOG.md).

## Uso

```python
import nini_datagen as dg

dg.first_name(5, seed=42)  # pl.Series String com nomes na frequência do Censo
dg.integer(1_000_000, low=1, high=6)  # sem seed: semente nova a cada chamada

g = dg.DataGen(seed=42)  # semente, locale e threads fixos
sexo = g.sex(1_000_000)  # "F"/"M", 51,5% de mulheres
nome = g.first_name(1_000_000, sex=sexo)  # nomes coerentes com o sexo de cada linha
idade = g.age(1_000_000)  # pirâmide etária aproximada do Brasil
plano = g.choice(1_000_000, ["bronze", "prata", "ouro"], weights=[6, 3, 1], null_fraction=0.05)
```

Toda configuração é um modelo Pydantic imutável. Dá para passar o objeto, um dict ou kwargs; nome de parâmetro
errado ou valor inválido vira `ValidationError`:

```python
cfg = dg.IntegerConfig(low=18, high=90, null_fraction=0.1)
g.integer(1000, cfg)
g.integer(1000, cfg, high=65)  # kwargs sobrescrevem campos
g.integer(1000, hihg=65)  # ValidationError: Extra inputs are not permitted
```

| gerador | configuração | tipo |
|---|---|---|
| `integer` | `IntegerConfig(low, high, dtype)` | Int32 / Int64 |
| `uniform` | `UniformConfig(low, high, dtype)` | Float64 / Float32 |
| `normal`, `lognormal` | `NormalConfig(mean, std)`, `LognormalConfig(mean, sigma)` | Float64 |
| `boolean` | `BooleanConfig(p)` | Boolean |
| `date`, `datetime` | `DateConfig(start, end)`, `DatetimeConfig(start, end, unit)` | Date / Datetime |
| `choice` | `ChoiceConfig(values, weights)` | String, Int64, Float64 ou Boolean |
| `sex` | `SexConfig(p_female, labels)` | String |
| `age` | `AgeConfig(low, high, weights)` | Int32 |
| `first_name`, `last_name` | `FirstNameConfig(sex)`, `LastNameConfig()` | String |
| `full_name` | `FullNameConfig(sex, compound, two_surnames)` | String |
| `cpf` | `CPFConfig(formatted, unique)` | String |
| `cnpj` | `CNPJConfig(alphanumeric, branch, formatted, unique)` | String |
| `pis`, `voter_id`, `rg` | `PISConfig(formatted)`, `VoterIDConfig(formatted)`, `RGConfig(formatted)` | String |
| `cnh`, `iban`, `ean13` | `CNHConfig()`, `IBANConfig()`, `EAN13Config()` | String |
| `license_plate` | `LicensePlateConfig(style)` (`"mercosul"` ou `"old"`) | String |
| `credit_card` | `CreditCardConfig(brand, formatted)` | String |
| `uuid`, `ipv6`, `mac`, `url` | `UUIDConfig()`, `IPv6Config()`, `MACConfig()`, `URLConfig()` | String |
| `ipv4` | `IPv4Config(private)` | String |
| `phone` | `PhoneConfig(style, mobile, uf)` | String |
| `email`, `username` | `EmailConfig(domains, domain_weights, style_weights)`, `UsernameConfig(style_weights)` | String |
| `company`, `occupation` | `CompanyConfig()`, `OccupationConfig(zipf)` | String |
| `vehicle` | `VehicleConfig(year_low, year_high)` | Struct `{brand, model, year}` |
| `cep` | `CepConfig(formatted, uf, source)` | String |
| `address` | `AddressConfig(uf, complement, phone, coords, full, source)` | Struct `{street, number, complement, neighborhood, city, state, cep, ...}` |
| `city`, `state` | `CityConfig(uf, with_state)`, `StateConfig(weighted)` | String |
| `sentence`, `paragraph` | `SentenceConfig(theme, templates, slots)`, `ParagraphConfig(..., sentences)` | String |
| `birthdate` | `BirthdateConfig(on)` + coluna `age` | Date |
| `correlated` | `CorrelatedConfig(rho, mean, std, lognormal, center, scale)` + coluna `other` | Float64 |
| `choice_by` | `ChoiceByConfig(options, default)` + coluna `by` | String |

Todos aceitam `name` (nome da coluna) e `null_fraction` (fração de nulos).

Os documentos saem com dígitos verificadores corretos (não são documentos reais de ninguém), e `dg.validators` confere
textos de qualquer origem:

```python
g.cpf(1_000_000, unique=True)  # sem repetição (até 10^9)
g.cnpj(10, alphanumeric=True, formatted=True)  # "HI.EJT.HBE/0001-69", o formato novo da Receita
dg.validators.cpf("529.982.247-25")  # True
```

Frases saem de uma gramática com concordância (temas `atendimento`, `saude`, `entrega`, `avaliacao`, `cadastro` ou
`geral`), ou de modelos próprios:

```python
g.sentence(3, theme="saude")  # "Reembolso de R$ 964,22 liberado para pagamento."
g.paragraph(3, sentences=(2, 4))
g.sentence(3, templates=["Pedido {cod} enviado {prazo}.", ("Sem observações.", 3)], slots={"cod": ["{d}{d}{d}"]})
g.vehicle(5).struct.unnest()  # colunas brand, model, year
```

Endereços são reais, do CNEFE do IBGE (Censo 2022): rua, bairro, cidade, UF e CEP existem juntos, sorteados pelo
número de endereços. O pacote traz uma base leve (1% dos endereços de cada município, todos os 5.570 municípios); a
completa (~55 MB, com a faixa real de números de cada rua) é baixada uma vez e fica em cache:

```python
g.address(5, full=True).struct.unnest()  # street, number, complement, neighborhood, city, state, cep, full
g.cep(5, uf="SP")
dg.datasets.download("cnefe")  # depois disso, source="auto" usa a base completa
```

## Tabelas e LazyFrame

Um `Schema` descreve a tabela: cada coluna é um gerador com parâmetros, uma referência a outra coluna (`dg.ref`) para
os geradores que dependem de dados, ou uma expressão do Polars. Ele é validado e compilado uma vez:

```python
import polars as pl

schema = dg.Schema(
    {
        "id": ("cpf", {"unique": True, "formatted": True}),
        "sexo": "sex",
        "nome": ("full_name", {"sex": dg.ref("sexo")}),  # nome coerente com o sexo da linha
        "idade": ("age", {"low": 18, "high": 90}),
        "nascimento": ("birthdate", {"age": dg.ref("idade"), "on": "2026-01-01"}),
        "renda": ("correlated", {"other": dg.ref("idade"), "rho": 0.4, "mean": 8, "std": 0.6, "lognormal": True}),
        "area": (
            "choice_by",
            {"by": dg.ref("sexo"), "options": {"F": ["Saúde", "Educação"], "M": ["Engenharia", "Comércio"]}},
        ),
        "cliente": dg.CPFConfig(formatted=True),  # também vale o objeto de configuração
        "meses": pl.col("idade") * 12,
    }
)

df = g.table(1_000_000, schema)  # pl.DataFrame
lf = g.scan(10**9, schema)  # pl.LazyFrame: nada é gerado até o collect
lf.filter(pl.col("idade") >= 60).select("nome", "renda").head(10).collect()  # gera só o necessário
```

`table` preenche as colunas bloco a bloco (65.536 linhas), todas as colunas de um bloco de uma vez, com os blocos em
paralelo: até colunas baratas, como inteiros e booleanos, entram no paralelismo. `scan` registra o schema como fonte
do Polars: ele recebe as colunas pedidas, o filtro e o limite, e gera em lotes só o que foi pedido. Cada coluna sai igual
a `g.generate(tipo, n, name=coluna, ...)`, e `scan(n, schema).collect()` é igual a `table(n, schema)`. No `scan`, as
expressões são avaliadas lote a lote: use expressões linha a linha (sem `mean()`, `rank()` etc.).

### Dados sujos, duplicatas e arquivos grandes

```python
schema = dg.Schema(
    {
        "cpf": ("cpf", {"formatted": True, "dirty": {"unmask": 0.3, "whitespace": 0.02}}),
        "nome": ("full_name", {"dirty": {"typo": 0.02, "case": 0.01, "accents": 0.05}}),
        "valor": ("lognormal", {"mean": 5, "sigma": 1, "dirty": {"outliers": 0.001, "sentinel": [(-1, 0.01)]}}),
        "uf": "state",
    }
)
df = g.table(100_000, schema)
com_dups = g.add_duplicates(df, 0.05, dirty={"nome": {"typo": 0.5}})  # row_id e dup_of são o gabarito
g.write_parquet_dataset("saida/", 50_000_000, schema, partition_by="uf", rows_per_file=1_000_000)
```

Os defeitos seguem as mesmas regras de reprodutibilidade dos dados: cada um tem seu fluxo, e a coluna suja sai igual
em `table`, `scan` e `write_parquet_dataset`. Os arquivos gravados juntos são exatamente `table(n, schema)`, para
qualquer `batch_rows`.

## Desempenho

Os geradores são kernels C++ que escrevem direto na memória do Polars, em blocos de 65.536 linhas, em paralelo. Na
máquina de desenvolvimento (Windows, 16 threads), com 1 thread: CPF ~10 ns/linha, nome ~5, e-mail ~30, frase ~60,
endereço completo do CNEFE ~100; a tabela de 20 colunas do benchmark sai a ~300 ns/linha com 1 thread e ~110 com 16
(1 milhão de linhas em ~0,1 s). Detalhes, método e comparação com Faker, Mimesis e dataforge em
[docs/desempenho.md](docs/desempenho.md).

## Reprodutibilidade

A partir da 0.2.0, a mesma `seed` dá sempre a mesma saída, em qualquer sistema e com qualquer número de threads, e
`n=1_000` dá exatamente as primeiras linhas de `n=1_000_000`. Cada coluna tem seu próprio fluxo de números aleatórios,
identificado pelo nome (`name`, que por padrão é o tipo do gerador): colunas com nomes diferentes não saem
correlacionadas, e a mesma coluna sai sempre igual (`g.integer(10)` duas vezes dá o mesmo resultado). Os nulos têm
fluxo próprio: mudar `null_fraction` não muda os valores das linhas que continuam preenchidas.

## Instalação

```bash
pip install nini-datagen      # ou: uv add nini-datagen
```

Os wheels já trazem a biblioteca C++ compilada (Linux, Windows e macOS; x86-64 e ARM64) e funcionam com qualquer
Python ≥ 3.11.

## Desenvolvimento

Precisa de [uv](https://docs.astral.sh/uv/) e de um compilador C++17 (GCC, Clang ou MSVC). CMake e Ninja vêm sozinhos
pelo scikit-build-core.

```bash
uv sync                                   # cria o ambiente e compila o C++
uv run pytest                             # testes
uv run pytest -m bench -s                 # benchmarks contra o MVP (fora da suíte padrão)
uv run python tools/bench.py              # tabela completa de ns/linha
uv run python tools/samples.py            # amostras de cada gerador
uv run python tools/profile_table.py      # onde vai o tempo da tabela e dos endereços
uv run --isolated --python 3.14 --group bench python tools/compare.py   # contra Faker, Mimesis e dataforge
uv run ruff check . && uv run pyright     # qualidade
uv run python tools/gen_bindings.py       # depois de mudar a assinatura de uma função DG_EXPORT
```

`uv sync` / `uv run` recompilam sozinhos quando um arquivo de `cpp/` muda (ou as variáveis abaixo).

- `DG_CLANGCL=1` (Windows): compila com clang-cl em vez do MSVC, como os wheels publicados; bem mais rápido nos
  kernels. Precisa dos componentes "C++ Clang Compiler for Windows" e "MSBuild support for LLVM (clang-cl) toolset"
  do Visual Studio Installer.
- `DG_NATIVE=ON`: compila para as instruções da sua CPU (`-march=native`), só para medir; os wheels usam x86-64-v2.
  O resultado não muda: o código não depende de FMA nem da biblioteca matemática do sistema.

### Estrutura

```
cpp/                     kernels C++ (ABI em C, prefixo dg_), um arquivo por domínio
src/nini_datagen/
  __init__.py, api.py    funções soltas (dg.integer, dg.table...) e exportações
  generator.py           DataGen: semente, locale e threads; um método por gerador
  registry.py            registro dos geradores por nome e montagem das configurações
  config/                modelos Pydantic (um por gerador, mais DirtyConfig)
  providers/             um gerador por tipo: plan() aloca, Job.fill() preenche cada bloco, Job.finish() entrega
  schema/                Schema (colunas, ref, expressões, níveis), table e scan (fonte do Polars)
  transforms/            dados sujos e duplicatas
  io/                    write_parquet_dataset
  datasets/              download com cache (CNEFE completo)
  locales/pt_BR/         dados e regras do Brasil (data/)
  core/                  sementes e fluxos, blocos e threads, views do Arrow, tabelas alias e vocabulários
  _native.py             carrega a biblioteca C++ e confere a versão da ABI
tests/                   pytest; tests/golden/ guarda os gabaritos da v0.2.0
tools/                   bindings, benchmarks, diagnóstico, comparação, amostras, base do CNEFE
docs/, examples/         guia, desempenho e exemplos
```

Gerador novo: uma configuração em `config/`, um provider com `@registry.register` em `providers/` e um atalho em
`generator.py`/`api.py`; o resto (blocos, threads, nulos, semente) vem do roteiro comum em `providers/base.py`.

## Licença

Código: MIT ([LICENSE](LICENSE)). Dados: fontes abertas do IBGE e do TSE, com créditos em
[DATA_LICENSES.md](DATA_LICENSES.md).
