memga 1.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
memga-1.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sérgio Augusto Faria Salles
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
memga-1.1.0/PKG-INFO ADDED
@@ -0,0 +1,180 @@
1
+ Metadata-Version: 2.4
2
+ Name: memga
3
+ Version: 1.1.0
4
+ Summary: Multiple Choice Method (MEM): select a set of alternatives that complement each other, by exhaustive search or a genetic algorithm
5
+ Author: Henrique Rego Monteiro da Hora
6
+ Author-email: Sérgio Augusto Faria Salles <safsalles@hotmail.com>
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/safsalles/portfolio-ds/tree/main/mem-ga
9
+ Project-URL: Issues, https://github.com/safsalles/portfolio-ds/issues
10
+ Keywords: multicriteria,decision-making,genetic-algorithm,selection,mem
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: numpy>=2.0
19
+ Requires-Dist: matplotlib>=3.8
20
+ Provides-Extra: test
21
+ Requires-Dist: pytest>=8.0; extra == "test"
22
+ Dynamic: license-file
23
+
24
+ # mem-ga
25
+
26
+ *Leia em: [English](README.en.md)*
27
+
28
+ O Método de Escolha Múltipla (MEM) escolhe um conjunto de alternativas que se completam, em vez de pegar as melhores uma a uma. Serve para montar equipes, carteiras, comitês, qualquer escolha em que o grupo importa mais que cada parte.
29
+
30
+ Este repositório traz o método como um pacote Python, `memga`, com:
31
+
32
+ - as duas regras de nota: a da tese que propôs o MEM (`media`) e a adaptação da minha dissertação (`minimo`), com a mesma interface;
33
+ - duas buscas: testar todas as combinações, para problemas pequenos, e um algoritmo genético (MEM-GA), para os grandes;
34
+ - vagas por grupo (por exemplo, 2 analistas e 1 gestor), complementação e pesos calculados dos dados, e um gráfico de radar.
35
+
36
+ Dois exemplos fictícios de recrutamento mostram o uso: um pequeno, para acompanhar a conta, e um grande, com 300 candidatos. A aplicação em futebol, com os dados do Brasileirão 2017, fica na pasta vizinha, [football-analytics](../football-analytics/README.md).
37
+
38
+ ## Instalação
39
+
40
+ Do PyPI:
41
+
42
+ ```bash
43
+ pip install memga
44
+ ```
45
+
46
+ Para mexer no código, na pasta `mem-ga`:
47
+
48
+ ```bash
49
+ pip install -e ".[test]"
50
+ python -m pytest
51
+ ```
52
+
53
+ ## Uso
54
+
55
+ A função `memga()` faz tudo: monta a regra, roda a busca e compara com a escolha tradicional. Os dois exemplos fictícios vêm dentro do pacote.
56
+
57
+ ```python
58
+ from memga import memga, load_example
59
+
60
+ # 2 de 8 candidatos, testando todas as duplas
61
+ print(memga(load_example("small"), size=2))
62
+
63
+ # Equipe de 4 entre 300, com vagas por grupo, pelo algoritmo genético
64
+ resultado = memga(
65
+ load_example("large"), method="ga", tau=0.54,
66
+ quotas={"analyst": 2, "engineer": 1, "manager": 1}, seed=1,
67
+ )
68
+ print(resultado.team, resultado.score)
69
+ # ('P007', 'P038', 'P192', 'P276') 0.6785714285714286
70
+ ```
71
+
72
+ Os argumentos principais são `method` (`"exhaustive"` ou `"ga"`), `rule` (`"mean"`, a da tese, ou `"minimum"`), `tau`, `quotas` e `config` (um `GAConfig`). Sem `rule`, a busca exaustiva usa `"mean"` e o algoritmo genético usa `"minimum"`. O resultado traz `team`, `score`, `ranking` (na busca exaustiva), `history` (no algoritmo genético) e `traditional`.
73
+
74
+ Para usar os módulos separadamente, a partir da pasta `mem-ga`:
75
+
76
+ ```python
77
+ from memga.busca import run_ga
78
+ from memga.problema import load_problem
79
+ from memga.regras import build_rule
80
+
81
+ problem = load_problem("exemplos/recrutamento-grande/data")
82
+ rule = build_rule(problem, "minimo", tau=0.54)
83
+ vagas = {"analista": 2, "engenheiro": 1, "gestor": 1}
84
+ result = run_ga(rule, problem.groups, vagas, size=4, seed=2026)
85
+ print(sorted(problem.alternatives[i] for i in result.team), result.score)
86
+ # ['P007', 'P038', 'P192', 'P276'] 0.6785714285714286
87
+ ```
88
+
89
+ | Módulo | O que faz |
90
+ | --- | --- |
91
+ | `memga.problema` | `Problem` (alternativas, critérios, direção, pesos, grupos), `load_problem` para ler os CSVs e `normalize` |
92
+ | `memga.pesos` | Complementação calculada dos dados (1 − \|r\|, como no CRITIC, dentro de cada grupo) e pesos por grupo |
93
+ | `memga.regras` | As duas regras, `mean_rule` e `minimum_rule`, e `build_rule` para montar uma delas a partir de um `Problem` |
94
+ | `memga.busca` | `exhaustive` (todas as combinações), `run_ga` (algoritmo genético) e `traditional_choice` (os melhores de cada grupo, para comparar) |
95
+ | `memga.radar` | Gráfico de radar com um painel por conjunto |
96
+
97
+ ## O método
98
+
99
+ 1. **Normaliza as avaliações.** Cada critério vai para a faixa de 0 a 1, com 1 para a melhor avaliação. Nos critérios em que menos é melhor, a escala se inverte.
100
+ 2. **Avalia o conjunto em grupos de critérios.** Para cada grupo de critérios, junta o desempenho do conjunto, os pesos e a complementação entre esses critérios num valor.
101
+ 3. **Aplica o tau de corte.** O grupo de critérios conta 1 se o valor alcançar o tau, e 0 se não alcançar. É esse corte que impede um resultado muito alto de pagar um resultado baixo em outro grupo: o método não é compensatório.
102
+ 4. **Dá a nota.** A nota é a fração dos grupos de critérios que alcançam o tau.
103
+
104
+ Com grupos de alternativas (cargos, posições), a complementação é calculada dentro de cada grupo, e cada grupo pesa mais os critérios em que se destaca.
105
+
106
+ ## As duas regras
107
+
108
+ As duas recebem um conjunto e devolvem `(nota, desempate)`, por isso qualquer busca funciona com qualquer regra.
109
+
110
+ | | `media` (tese) | `minimo` (adaptação) |
111
+ | --- | --- | --- |
112
+ | Critérios | Combinados n a n, com n = tamanho do conjunto | Combinados 2 a 2 |
113
+ | Dentro do grupo de critérios | Média do desempenho (Equação 3.4 da tese) | Mínimo do desempenho |
114
+ | Nota | Fração dos grupos de critérios que alcançam o tau | Fração dos pares que alcançam o tau |
115
+ | Desempate | Média de `mínimo(valor, tau)` nos grupos | Média de `mínimo(valor, tau)` nos pares |
116
+
117
+ **Por que a adaptação.** Na tese, os critérios são combinados na mesma quantidade das alternativas escolhidas. Num conjunto grande, como 10 de 18 critérios, cada combinação fica perto da média geral do conjunto, e as diferenças entre conjuntos somem. Com os critérios 2 a 2, cada par ainda mostra o que o conjunto tem e o que falta. E, com o mínimo no lugar da média, um critério forte não cobre um fraco dentro do par, então o método fica não compensatório também ali.
118
+
119
+ **O desempate da regra `media` é novo.** Na tese, ela só rodava testando todas as combinações, e o empate ficava na mesma posição. O algoritmo genético precisa de um desempate para ter direção entre conjuntos com a mesma nota, então a regra `media` usa o mesmo desempate da `minimo`: quanto falta, em média, para os grupos alcançarem o tau.
120
+
121
+ **Limite da regra `media`.** O número de grupos de critérios é C(critérios, n). Com 18 critérios e um conjunto de 10, são 43.758 grupos por avaliação, lento demais para o algoritmo genético. Acima de 10.000 grupos (`MAX_MEAN_GROUPS`), a regra recusa o problema com uma mensagem que diz os números e sugere a regra `minimo`. Testar todas as combinações também tem limite: acima de 20 milhões de avaliações, a busca exaustiva recusa o problema.
122
+
123
+ ## Exemplo pequeno: 2 de 8 candidatos
124
+
125
+ ```bash
126
+ python exemplos/recrutamento-pequeno/exemplo.py
127
+ python exemplos/recrutamento-pequeno/exemplo.py --regra minimo
128
+ ```
129
+
130
+ São 8 candidatos com notas em 6 critérios, e a busca testa todas as 28 duplas. Os candidatos A1 e A2 têm as melhores médias, mas são parecidos: fortes em planejamento, logística e qualidade, e medianos em custos e pessoas. O A3 é o contrário.
131
+
132
+ | Ordem | Média ponderada | MEM, regra `media` | % dos grupos | MEM, regra `minimo` | % dos pares |
133
+ | --- | --- | --- | --- | --- | --- |
134
+ | 1.º | A1 | A1, A3 | 66,7% | A1, A3 | 46,7% |
135
+ | 1.º | | A2, A3 | 66,7% | A2, A3 | 46,7% |
136
+ | 3.º | A2 | A1, A2 | 60,0% | A1, A2 | 33,3% |
137
+
138
+ A escolha tradicional fica com A1 e A2, os dois primeiros pela média ponderada. Nas duas regras, essa dupla fica em 3.º lugar, e as melhores juntam um deles com o A3, que cobre o que falta. Com tau 0,24, as duas regras concordam no ranking do topo. A regra `minimo` dá notas menores, porque cada par vale o critério mais fraco.
139
+
140
+ No radar, cada linha colorida é um candidato e a área cinza é a média da dupla. À esquerda, A1 e A2 quase se sobrepõem. À direita, A1 e A3 se completam.
141
+
142
+ ![Radar das duplas A1, A2 e A1, A3](images/radar_pequeno.png)
143
+
144
+ O resultado depende do tau. Com a regra `media` e tau de 0,27 ou mais, o corte fica alto a ponto de só as notas muito altas de A1 e A2 passarem, e essa dupla volta ao 1.º lugar. Por isso o programa mostra o que muda com o tau um pouco acima e um pouco abaixo. Mais detalhes em [exemplos/recrutamento-pequeno](exemplos/recrutamento-pequeno/).
145
+
146
+ ## Exemplo grande: equipe de 4 entre 300 candidatos
147
+
148
+ ```bash
149
+ python exemplos/recrutamento-grande/exemplo.py
150
+ ```
151
+
152
+ São 300 candidatos fictícios (150 analistas, 90 engenheiros e 60 gestores), com notas em 8 critérios, para uma equipe de 2 analistas, 1 engenheiro e 1 gestor. Seriam mais de 60 milhões de equipes possíveis, então a busca é feita pelo algoritmo genético. A execução leva cerca de 20 segundos.
153
+
154
+ | Equipe | Pessoas | Nota `media` | Nota `minimo` |
155
+ | --- | --- | --- | --- |
156
+ | Regra `media` | P050, P110 (analistas), P218 (engenheiro), P276 (gestor) | **50,0%** | 32,1% |
157
+ | Regra `minimo` | P007, P038 (analistas), P192 (engenheiro), P276 (gestor) | 24,3% | **67,9%** |
158
+ | Escolha tradicional | P038, P110 (analistas), P218 (engenheiro), P276 (gestor) | 48,6% | 53,6% |
159
+
160
+ A escolha tradicional pega os mais fortes de cada cargo. Cada regra acha uma equipe melhor que ela pela própria nota, e as duas regras escolhem equipes diferentes: a `media` troca um analista e fica perto da tradicional, e a `minimo` troca um analista e o engenheiro. A equipe da `minimo` perde um pouco em dados e programação e ganha em comunicação, negociação e organização: fica mais equilibrada, porque cada par de critérios vale o mais fraco dos dois. O gestor é o mesmo nas três.
161
+
162
+ ![Radar das três equipes](images/radar_grande.png)
163
+
164
+ O tau de cada regra foi escolhido para a nota não saturar: com o tau baixo demais, toda equipe passa em tudo; alto demais, ninguém passa. Testando de 0,01 a 1,00, ficou o valor em que a escolha tradicional chega mais perto de 50% (0,63 na `media` e 0,54 na `minimo`). Mais detalhes, e como os dados foram gerados, em [exemplos/recrutamento-grande](exemplos/recrutamento-grande/).
165
+
166
+ ## Formato dos dados
167
+
168
+ `load_problem` lê uma pasta com estes arquivos:
169
+
170
+ - `criteria.csv`: `criterion`, `direction` (1 quando mais é melhor e 0 quando mais é pior) e `weight`.
171
+ - `alternatives.csv`: a coluna `alternative`, uma coluna por critério e, se houver vagas por grupo, a coluna `group`.
172
+ - `complementarity.csv` (opcional): a matriz de complementação entre critérios, com a diagonal vazia. Sem ela, a complementação é calculada dos dados.
173
+
174
+ ## Conferência com a tese
175
+
176
+ A regra `media`, com a busca exaustiva, foi conferida com a aplicação numérica da tese, cujos dados não estão neste repositório. Na escolha de 2, com tau de 0,20, as 45 duplas do Quadro 3.9 bateram. Com tau de 0,19 e 0,21, 44 de 45 bateram nos Quadros 3.10 e 3.11, e a dupla diferente (A1, A8) tem um grupo de critérios a mais ou a menos. Na escolha de 3, os 40 conjuntos legíveis do Quadro 3.15 bateram. Os testes guardam o ranking do exemplo pequeno para que isso não mude sem aviso.
177
+
178
+ ## Fonte
179
+
180
+ O MEM foi proposto na tese *Método de Escolha Múltipla: uma proposta multicritério para seleção em conjunto de mais de uma alternativa*, de Henrique Rego Monteiro da Hora (Universidade Federal Fluminense, 2013). A regra `minimo` e o algoritmo genético vêm da dissertação *MEM-GA: Método de Escolha Múltipla com Algoritmo Genético aplicado à formação de equipes de futebol*, de Sérgio Augusto Faria Salles (Instituto Federal Fluminense, 2018), escrita em MATLAB e trazida aqui para Python.
memga-1.1.0/README.md ADDED
@@ -0,0 +1,157 @@
1
+ # mem-ga
2
+
3
+ *Leia em: [English](README.en.md)*
4
+
5
+ O Método de Escolha Múltipla (MEM) escolhe um conjunto de alternativas que se completam, em vez de pegar as melhores uma a uma. Serve para montar equipes, carteiras, comitês, qualquer escolha em que o grupo importa mais que cada parte.
6
+
7
+ Este repositório traz o método como um pacote Python, `memga`, com:
8
+
9
+ - as duas regras de nota: a da tese que propôs o MEM (`media`) e a adaptação da minha dissertação (`minimo`), com a mesma interface;
10
+ - duas buscas: testar todas as combinações, para problemas pequenos, e um algoritmo genético (MEM-GA), para os grandes;
11
+ - vagas por grupo (por exemplo, 2 analistas e 1 gestor), complementação e pesos calculados dos dados, e um gráfico de radar.
12
+
13
+ Dois exemplos fictícios de recrutamento mostram o uso: um pequeno, para acompanhar a conta, e um grande, com 300 candidatos. A aplicação em futebol, com os dados do Brasileirão 2017, fica na pasta vizinha, [football-analytics](../football-analytics/README.md).
14
+
15
+ ## Instalação
16
+
17
+ Do PyPI:
18
+
19
+ ```bash
20
+ pip install memga
21
+ ```
22
+
23
+ Para mexer no código, na pasta `mem-ga`:
24
+
25
+ ```bash
26
+ pip install -e ".[test]"
27
+ python -m pytest
28
+ ```
29
+
30
+ ## Uso
31
+
32
+ A função `memga()` faz tudo: monta a regra, roda a busca e compara com a escolha tradicional. Os dois exemplos fictícios vêm dentro do pacote.
33
+
34
+ ```python
35
+ from memga import memga, load_example
36
+
37
+ # 2 de 8 candidatos, testando todas as duplas
38
+ print(memga(load_example("small"), size=2))
39
+
40
+ # Equipe de 4 entre 300, com vagas por grupo, pelo algoritmo genético
41
+ resultado = memga(
42
+ load_example("large"), method="ga", tau=0.54,
43
+ quotas={"analyst": 2, "engineer": 1, "manager": 1}, seed=1,
44
+ )
45
+ print(resultado.team, resultado.score)
46
+ # ('P007', 'P038', 'P192', 'P276') 0.6785714285714286
47
+ ```
48
+
49
+ Os argumentos principais são `method` (`"exhaustive"` ou `"ga"`), `rule` (`"mean"`, a da tese, ou `"minimum"`), `tau`, `quotas` e `config` (um `GAConfig`). Sem `rule`, a busca exaustiva usa `"mean"` e o algoritmo genético usa `"minimum"`. O resultado traz `team`, `score`, `ranking` (na busca exaustiva), `history` (no algoritmo genético) e `traditional`.
50
+
51
+ Para usar os módulos separadamente, a partir da pasta `mem-ga`:
52
+
53
+ ```python
54
+ from memga.busca import run_ga
55
+ from memga.problema import load_problem
56
+ from memga.regras import build_rule
57
+
58
+ problem = load_problem("exemplos/recrutamento-grande/data")
59
+ rule = build_rule(problem, "minimo", tau=0.54)
60
+ vagas = {"analista": 2, "engenheiro": 1, "gestor": 1}
61
+ result = run_ga(rule, problem.groups, vagas, size=4, seed=2026)
62
+ print(sorted(problem.alternatives[i] for i in result.team), result.score)
63
+ # ['P007', 'P038', 'P192', 'P276'] 0.6785714285714286
64
+ ```
65
+
66
+ | Módulo | O que faz |
67
+ | --- | --- |
68
+ | `memga.problema` | `Problem` (alternativas, critérios, direção, pesos, grupos), `load_problem` para ler os CSVs e `normalize` |
69
+ | `memga.pesos` | Complementação calculada dos dados (1 − \|r\|, como no CRITIC, dentro de cada grupo) e pesos por grupo |
70
+ | `memga.regras` | As duas regras, `mean_rule` e `minimum_rule`, e `build_rule` para montar uma delas a partir de um `Problem` |
71
+ | `memga.busca` | `exhaustive` (todas as combinações), `run_ga` (algoritmo genético) e `traditional_choice` (os melhores de cada grupo, para comparar) |
72
+ | `memga.radar` | Gráfico de radar com um painel por conjunto |
73
+
74
+ ## O método
75
+
76
+ 1. **Normaliza as avaliações.** Cada critério vai para a faixa de 0 a 1, com 1 para a melhor avaliação. Nos critérios em que menos é melhor, a escala se inverte.
77
+ 2. **Avalia o conjunto em grupos de critérios.** Para cada grupo de critérios, junta o desempenho do conjunto, os pesos e a complementação entre esses critérios num valor.
78
+ 3. **Aplica o tau de corte.** O grupo de critérios conta 1 se o valor alcançar o tau, e 0 se não alcançar. É esse corte que impede um resultado muito alto de pagar um resultado baixo em outro grupo: o método não é compensatório.
79
+ 4. **Dá a nota.** A nota é a fração dos grupos de critérios que alcançam o tau.
80
+
81
+ Com grupos de alternativas (cargos, posições), a complementação é calculada dentro de cada grupo, e cada grupo pesa mais os critérios em que se destaca.
82
+
83
+ ## As duas regras
84
+
85
+ As duas recebem um conjunto e devolvem `(nota, desempate)`, por isso qualquer busca funciona com qualquer regra.
86
+
87
+ | | `media` (tese) | `minimo` (adaptação) |
88
+ | --- | --- | --- |
89
+ | Critérios | Combinados n a n, com n = tamanho do conjunto | Combinados 2 a 2 |
90
+ | Dentro do grupo de critérios | Média do desempenho (Equação 3.4 da tese) | Mínimo do desempenho |
91
+ | Nota | Fração dos grupos de critérios que alcançam o tau | Fração dos pares que alcançam o tau |
92
+ | Desempate | Média de `mínimo(valor, tau)` nos grupos | Média de `mínimo(valor, tau)` nos pares |
93
+
94
+ **Por que a adaptação.** Na tese, os critérios são combinados na mesma quantidade das alternativas escolhidas. Num conjunto grande, como 10 de 18 critérios, cada combinação fica perto da média geral do conjunto, e as diferenças entre conjuntos somem. Com os critérios 2 a 2, cada par ainda mostra o que o conjunto tem e o que falta. E, com o mínimo no lugar da média, um critério forte não cobre um fraco dentro do par, então o método fica não compensatório também ali.
95
+
96
+ **O desempate da regra `media` é novo.** Na tese, ela só rodava testando todas as combinações, e o empate ficava na mesma posição. O algoritmo genético precisa de um desempate para ter direção entre conjuntos com a mesma nota, então a regra `media` usa o mesmo desempate da `minimo`: quanto falta, em média, para os grupos alcançarem o tau.
97
+
98
+ **Limite da regra `media`.** O número de grupos de critérios é C(critérios, n). Com 18 critérios e um conjunto de 10, são 43.758 grupos por avaliação, lento demais para o algoritmo genético. Acima de 10.000 grupos (`MAX_MEAN_GROUPS`), a regra recusa o problema com uma mensagem que diz os números e sugere a regra `minimo`. Testar todas as combinações também tem limite: acima de 20 milhões de avaliações, a busca exaustiva recusa o problema.
99
+
100
+ ## Exemplo pequeno: 2 de 8 candidatos
101
+
102
+ ```bash
103
+ python exemplos/recrutamento-pequeno/exemplo.py
104
+ python exemplos/recrutamento-pequeno/exemplo.py --regra minimo
105
+ ```
106
+
107
+ São 8 candidatos com notas em 6 critérios, e a busca testa todas as 28 duplas. Os candidatos A1 e A2 têm as melhores médias, mas são parecidos: fortes em planejamento, logística e qualidade, e medianos em custos e pessoas. O A3 é o contrário.
108
+
109
+ | Ordem | Média ponderada | MEM, regra `media` | % dos grupos | MEM, regra `minimo` | % dos pares |
110
+ | --- | --- | --- | --- | --- | --- |
111
+ | 1.º | A1 | A1, A3 | 66,7% | A1, A3 | 46,7% |
112
+ | 1.º | | A2, A3 | 66,7% | A2, A3 | 46,7% |
113
+ | 3.º | A2 | A1, A2 | 60,0% | A1, A2 | 33,3% |
114
+
115
+ A escolha tradicional fica com A1 e A2, os dois primeiros pela média ponderada. Nas duas regras, essa dupla fica em 3.º lugar, e as melhores juntam um deles com o A3, que cobre o que falta. Com tau 0,24, as duas regras concordam no ranking do topo. A regra `minimo` dá notas menores, porque cada par vale o critério mais fraco.
116
+
117
+ No radar, cada linha colorida é um candidato e a área cinza é a média da dupla. À esquerda, A1 e A2 quase se sobrepõem. À direita, A1 e A3 se completam.
118
+
119
+ ![Radar das duplas A1, A2 e A1, A3](images/radar_pequeno.png)
120
+
121
+ O resultado depende do tau. Com a regra `media` e tau de 0,27 ou mais, o corte fica alto a ponto de só as notas muito altas de A1 e A2 passarem, e essa dupla volta ao 1.º lugar. Por isso o programa mostra o que muda com o tau um pouco acima e um pouco abaixo. Mais detalhes em [exemplos/recrutamento-pequeno](exemplos/recrutamento-pequeno/).
122
+
123
+ ## Exemplo grande: equipe de 4 entre 300 candidatos
124
+
125
+ ```bash
126
+ python exemplos/recrutamento-grande/exemplo.py
127
+ ```
128
+
129
+ São 300 candidatos fictícios (150 analistas, 90 engenheiros e 60 gestores), com notas em 8 critérios, para uma equipe de 2 analistas, 1 engenheiro e 1 gestor. Seriam mais de 60 milhões de equipes possíveis, então a busca é feita pelo algoritmo genético. A execução leva cerca de 20 segundos.
130
+
131
+ | Equipe | Pessoas | Nota `media` | Nota `minimo` |
132
+ | --- | --- | --- | --- |
133
+ | Regra `media` | P050, P110 (analistas), P218 (engenheiro), P276 (gestor) | **50,0%** | 32,1% |
134
+ | Regra `minimo` | P007, P038 (analistas), P192 (engenheiro), P276 (gestor) | 24,3% | **67,9%** |
135
+ | Escolha tradicional | P038, P110 (analistas), P218 (engenheiro), P276 (gestor) | 48,6% | 53,6% |
136
+
137
+ A escolha tradicional pega os mais fortes de cada cargo. Cada regra acha uma equipe melhor que ela pela própria nota, e as duas regras escolhem equipes diferentes: a `media` troca um analista e fica perto da tradicional, e a `minimo` troca um analista e o engenheiro. A equipe da `minimo` perde um pouco em dados e programação e ganha em comunicação, negociação e organização: fica mais equilibrada, porque cada par de critérios vale o mais fraco dos dois. O gestor é o mesmo nas três.
138
+
139
+ ![Radar das três equipes](images/radar_grande.png)
140
+
141
+ O tau de cada regra foi escolhido para a nota não saturar: com o tau baixo demais, toda equipe passa em tudo; alto demais, ninguém passa. Testando de 0,01 a 1,00, ficou o valor em que a escolha tradicional chega mais perto de 50% (0,63 na `media` e 0,54 na `minimo`). Mais detalhes, e como os dados foram gerados, em [exemplos/recrutamento-grande](exemplos/recrutamento-grande/).
142
+
143
+ ## Formato dos dados
144
+
145
+ `load_problem` lê uma pasta com estes arquivos:
146
+
147
+ - `criteria.csv`: `criterion`, `direction` (1 quando mais é melhor e 0 quando mais é pior) e `weight`.
148
+ - `alternatives.csv`: a coluna `alternative`, uma coluna por critério e, se houver vagas por grupo, a coluna `group`.
149
+ - `complementarity.csv` (opcional): a matriz de complementação entre critérios, com a diagonal vazia. Sem ela, a complementação é calculada dos dados.
150
+
151
+ ## Conferência com a tese
152
+
153
+ A regra `media`, com a busca exaustiva, foi conferida com a aplicação numérica da tese, cujos dados não estão neste repositório. Na escolha de 2, com tau de 0,20, as 45 duplas do Quadro 3.9 bateram. Com tau de 0,19 e 0,21, 44 de 45 bateram nos Quadros 3.10 e 3.11, e a dupla diferente (A1, A8) tem um grupo de critérios a mais ou a menos. Na escolha de 3, os 40 conjuntos legíveis do Quadro 3.15 bateram. Os testes guardam o ranking do exemplo pequeno para que isso não mude sem aviso.
154
+
155
+ ## Fonte
156
+
157
+ O MEM foi proposto na tese *Método de Escolha Múltipla: uma proposta multicritério para seleção em conjunto de mais de uma alternativa*, de Henrique Rego Monteiro da Hora (Universidade Federal Fluminense, 2013). A regra `minimo` e o algoritmo genético vêm da dissertação *MEM-GA: Método de Escolha Múltipla com Algoritmo Genético aplicado à formação de equipes de futebol*, de Sérgio Augusto Faria Salles (Instituto Federal Fluminense, 2018), escrita em MATLAB e trazida aqui para Python.
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "memga"
7
+ version = "1.1.0"
8
+ description = "Multiple Choice Method (MEM): select a set of alternatives that complement each other, by exhaustive search or a genetic algorithm"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [
14
+ { name = "Sérgio Augusto Faria Salles", email = "safsalles@hotmail.com" },
15
+ { name = "Henrique Rego Monteiro da Hora" },
16
+ ]
17
+ keywords = ["multicriteria", "decision-making", "genetic-algorithm", "selection", "mem"]
18
+ classifiers = [
19
+ "Development Status :: 4 - Beta",
20
+ "Intended Audience :: Science/Research",
21
+ "Programming Language :: Python :: 3",
22
+ "Topic :: Scientific/Engineering :: Mathematics",
23
+ ]
24
+ dependencies = ["numpy>=2.0", "matplotlib>=3.8"]
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/safsalles/portfolio-ds/tree/main/mem-ga"
28
+ Issues = "https://github.com/safsalles/portfolio-ds/issues"
29
+
30
+ [project.optional-dependencies]
31
+ test = ["pytest>=8.0"]
32
+
33
+ [tool.setuptools.packages.find]
34
+ where = ["src"]
35
+
36
+ [tool.setuptools.package-data]
37
+ memga = ["data/*/*.csv"]
38
+
39
+ [tool.pytest.ini_options]
40
+ testpaths = ["tests"]
memga-1.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,25 @@
1
+ """MEM: Método de Escolha Múltipla, com busca exaustiva ou por algoritmo genético.
2
+
3
+ Uso mais simples:
4
+
5
+ from memga import memga, load_example
6
+ print(memga(load_example("small"), size=2))
7
+ """
8
+
9
+ from memga.api import MemgaResult, Ranked, load_example, memga
10
+ from memga.busca import GAConfig, traditional_choice
11
+ from memga.problema import Problem, load_problem
12
+
13
+ __version__ = "1.1.0"
14
+
15
+ __all__ = [
16
+ "GAConfig",
17
+ "MemgaResult",
18
+ "Problem",
19
+ "Ranked",
20
+ "__version__",
21
+ "load_example",
22
+ "load_problem",
23
+ "memga",
24
+ "traditional_choice",
25
+ ]
@@ -0,0 +1,161 @@
1
+ """A função de entrada do pacote: `memga()` escolhe um conjunto de alternativas.
2
+
3
+ Ela junta o que está nos outros módulos: monta a regra de nota, roda a busca exaustiva ou
4
+ o algoritmo genético e compara o resultado com a escolha tradicional.
5
+ """
6
+
7
+ from dataclasses import dataclass, field
8
+ from pathlib import Path
9
+
10
+ import numpy as np
11
+
12
+ from memga.busca import GAConfig, exhaustive, run_ga, traditional_choice
13
+ from memga.problema import Problem, load_problem, normalize
14
+ from memga.regras import build_rule
15
+
16
+ DEFAULT_TAU = 0.24
17
+ METHODS = ("exhaustive", "ga")
18
+ RULES = ("mean", "minimum")
19
+ _RULE_NAMES = {"mean": "media", "minimum": "minimo"}
20
+ DATA_DIR = Path(__file__).resolve().parent / "data"
21
+ EXAMPLES = {"small": "recruiting-small", "large": "recruiting-large"}
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class Ranked:
26
+ position: int
27
+ alternatives: tuple[str, ...]
28
+ score: float
29
+
30
+
31
+ @dataclass
32
+ class MemgaResult:
33
+ """O resultado de `memga()`.
34
+
35
+ `team` e `score` valem para os dois métodos: com a busca exaustiva, é o primeiro
36
+ conjunto da ordem. `ranking` só existe na busca exaustiva e `history` só no algoritmo
37
+ genético. `traditional` é a escolha tradicional e a nota dela na mesma regra.
38
+ """
39
+
40
+ team: tuple[str, ...]
41
+ score: float
42
+ method: str
43
+ rule: str
44
+ tau: float
45
+ size: int
46
+ problem: Problem
47
+ traditional: tuple[tuple[str, ...], float]
48
+ quotas: dict | None = None
49
+ seed: int | None = None
50
+ ranking: list[Ranked] = field(default_factory=list)
51
+ history: list[float] = field(default_factory=list)
52
+ payment: np.ndarray | None = None
53
+
54
+ def __str__(self):
55
+ search = "exhaustive search" if self.method == "exhaustive" else "genetic algorithm"
56
+ lines = [
57
+ f"Multiple Choice Method: {search}, {self.rule} rule, tau {self.tau:g}",
58
+ f"{len(self.problem.alternatives)} alternatives, {len(self.problem.criteria)} criteria, "
59
+ f"selecting {self.size}",
60
+ "",
61
+ ]
62
+ if self.method == "exhaustive":
63
+ lines.append(f"Best sets ({len(self.ranking)} tested):")
64
+ for item in self.ranking[:10]:
65
+ lines.append(f" {item.position:>3}. {', '.join(item.alternatives):<24} {item.score:6.1%}")
66
+ if len(self.ranking) > 10:
67
+ lines.append(f" ... {len(self.ranking) - 10} more")
68
+ else:
69
+ lines.append(f"Selected: {', '.join(self.team)} ({self.score:.1%})")
70
+ if self.seed is not None:
71
+ lines.append(f"Seed: {self.seed}")
72
+ names, score = self.traditional
73
+ lines += ["", f"Traditional choice (best one by one): {', '.join(names)} ({score:.1%})"]
74
+ if self.ranking and len(names) == self.size:
75
+ place = next((r.position for r in self.ranking if set(r.alternatives) == set(names)), None)
76
+ if place:
77
+ lines.append(f"It ranks {place} in the Multiple Choice Method.")
78
+ return "\n".join(lines)
79
+
80
+
81
+ def load_example(name="small"):
82
+ """Carrega um dos exemplos que vêm com o pacote: "small" (8 candidatos) ou "large" (300)."""
83
+ if name not in EXAMPLES:
84
+ raise ValueError(f"Exemplo desconhecido: {name!r}. Use 'small' ou 'large'.")
85
+ return load_problem(DATA_DIR / EXAMPLES[name])
86
+
87
+
88
+ def _check_quotas(quotas, problem):
89
+ if quotas is None:
90
+ return None
91
+ if problem.groups is None:
92
+ raise ValueError("`quotas` precisa de `groups` no problema.")
93
+ unknown = sorted(set(quotas) - set(problem.groups.tolist()))
94
+ if unknown:
95
+ raise ValueError(f"`quotas` cita grupos que não estão no problema: {', '.join(unknown)}.")
96
+ if any(int(q) != q or q < 0 for q in quotas.values()):
97
+ raise ValueError("`quotas` precisa ter números inteiros, de zero para cima.")
98
+ quotas = {group: int(q) for group, q in quotas.items() if q > 0}
99
+ if not quotas:
100
+ raise ValueError("`quotas` precisa pedir pelo menos 1 alternativa.")
101
+ return quotas
102
+
103
+
104
+ def memga(problem, size=None, method="exhaustive", rule=None, tau=DEFAULT_TAU, quotas=None,
105
+ config=None, seed=None, on_progress=None):
106
+ """Escolhe o conjunto de alternativas que mais se complementam.
107
+
108
+ Parâmetros:
109
+ problem: um `Problem`, de `load_problem` ou `load_example`.
110
+ size: quantas alternativas escolher. Pode faltar quando há `quotas`.
111
+ method: "exhaustive" (testa todas as combinações e devolve a ordem completa) ou
112
+ "ga" (algoritmo genético, para problemas grandes demais para testar tudo).
113
+ rule: "mean" (a regra da tese) ou "minimum" (critérios 2 a 2, com o menor dos dois).
114
+ Por padrão, "mean" na busca exaustiva e "minimum" no algoritmo genético.
115
+ tau: o corte; uma combinação de critérios conta quando o valor chega nele.
116
+ quotas: vagas por grupo, como {"analyst": 2, "manager": 1}. Precisa de `groups`.
117
+ config: um `GAConfig`, para ajustar o algoritmo genético.
118
+ seed: semente, para repetir um resultado do algoritmo genético.
119
+ on_progress: função (iteração, total, melhor nota) chamada durante o algoritmo genético.
120
+ """
121
+ if method not in METHODS:
122
+ raise ValueError(f"Método desconhecido: {method!r}. Use 'exhaustive' ou 'ga'.")
123
+ if rule is None:
124
+ rule = "mean" if method == "exhaustive" else "minimum"
125
+ if rule not in RULES:
126
+ raise ValueError(f"Regra desconhecida: {rule!r}. Use 'mean' ou 'minimum'.")
127
+
128
+ quotas = _check_quotas(quotas, problem)
129
+ if size is None:
130
+ if quotas is None:
131
+ raise ValueError("Informe `size`, quantas alternativas escolher.")
132
+ size = sum(quotas.values())
133
+ elif int(size) != size or size < 1:
134
+ raise ValueError(f"`size` precisa ser um número inteiro de pelo menos 1, e veio {size}.")
135
+ elif quotas is not None and size != sum(quotas.values()):
136
+ raise ValueError(f"`size` é {size}, mas `quotas` soma {sum(quotas.values())}.")
137
+ size = int(size)
138
+ if size > len(problem.alternatives):
139
+ raise ValueError(
140
+ f"O conjunto pede {size} alternativas, mas os dados têm só {len(problem.alternatives)}."
141
+ )
142
+
143
+ fitted = build_rule(problem, _RULE_NAMES[rule], tau, size=size)
144
+ names = problem.alternatives
145
+ normalized = normalize(problem.values, problem.direction)
146
+ weights = problem.weights if problem.weights.sum() > 0 else np.ones_like(problem.weights)
147
+ chosen = traditional_choice(normalized * weights, size, problem.groups, quotas)
148
+ traditional = (tuple(names[i] for i in chosen), fitted.rate(tuple(sorted(chosen)))[0])
149
+
150
+ result = dict(method=method, rule=rule, tau=tau, size=size, problem=problem,
151
+ traditional=traditional, quotas=quotas, seed=seed, payment=fitted.payment)
152
+ if method == "exhaustive":
153
+ ranking = [
154
+ Ranked(c.position, tuple(names[i] for i in c.alternatives), c.score)
155
+ for c in exhaustive(fitted, len(names), size, problem.groups, quotas)
156
+ ]
157
+ return MemgaResult(team=ranking[0].alternatives, score=ranking[0].score, ranking=ranking, **result)
158
+
159
+ found = run_ga(fitted, problem.groups, quotas, size, config or GAConfig(), seed, on_progress)
160
+ return MemgaResult(team=tuple(names[i] for i in found.team), score=found.score,
161
+ history=found.history, **result)