jlprep 0.1.0__py3-none-any.whl
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.
- jlprep/__init__.py +36 -0
- jlprep/cleaning.py +209 -0
- jlprep/comparison.py +55 -0
- jlprep/diagnostics.py +129 -0
- jlprep/encoding.py +116 -0
- jlprep/exploration.py +254 -0
- jlprep/quality.py +122 -0
- jlprep/reporting.py +288 -0
- jlprep-0.1.0.dist-info/METADATA +160 -0
- jlprep-0.1.0.dist-info/RECORD +12 -0
- jlprep-0.1.0.dist-info/WHEEL +5 -0
- jlprep-0.1.0.dist-info/top_level.txt +1 -0
jlprep/__init__.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# jlprep/__init__.py
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
# Importando funções diretamente para facilitar o uso
|
|
6
|
+
from .cleaning import remove_duplicates, remove_outliers, clean_missing
|
|
7
|
+
from .encoding import encode_label, encode_onehot
|
|
8
|
+
from .exploration import summarize_data, value_counts_plot, check_data_quality
|
|
9
|
+
from .quality import check_class_balance
|
|
10
|
+
from .diagnostics import analyze_dataset
|
|
11
|
+
from .comparison import compare_datasets
|
|
12
|
+
from .reporting import generate_report
|
|
13
|
+
|
|
14
|
+
__all__ = ["remove_duplicates",
|
|
15
|
+
"remove_outliers",
|
|
16
|
+
"clean_missing",
|
|
17
|
+
"encode_label",
|
|
18
|
+
"encode_onehot",
|
|
19
|
+
"summarize_data",
|
|
20
|
+
"value_counts_plot",
|
|
21
|
+
"check_class_balance",
|
|
22
|
+
"check_data_quality",
|
|
23
|
+
"analyze_dataset",
|
|
24
|
+
"compare_datasets",
|
|
25
|
+
"generate_report"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
"""
|
|
29
|
+
JlPrep - Biblioteca de pré-processamento de dados para Machine Learning
|
|
30
|
+
|
|
31
|
+
Desenvolvida por João Lucas para facilitar tarefas como:
|
|
32
|
+
- Limpeza de dados
|
|
33
|
+
- Tratamento de valores ausentes
|
|
34
|
+
- Codificação de variáveis categóricas
|
|
35
|
+
- Análise exploratória (em futuras versões)
|
|
36
|
+
"""
|
jlprep/cleaning.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
__all__ = ["remove_duplicates",
|
|
2
|
+
"remove_outliers",
|
|
3
|
+
"clean_missing"]
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
def remove_duplicates(data: pd.DataFrame,
|
|
8
|
+
visualization: bool = False,
|
|
9
|
+
remove: bool = False) -> pd.DataFrame:
|
|
10
|
+
"""
|
|
11
|
+
Remove ou visualiza duplicatas de um dataframe.
|
|
12
|
+
|
|
13
|
+
:param data: Dataframe com os dados a serem analisados.
|
|
14
|
+
:param visualization: Se True, visualiza as duplicatas no Dataframe.
|
|
15
|
+
:param remove: Se True, remove as duplicatas no Dataframe.
|
|
16
|
+
:return: Dataframe com ou sem duplicatas, dependendo do parâmetro 'remove'
|
|
17
|
+
|
|
18
|
+
"""
|
|
19
|
+
#Verificando o comportamento
|
|
20
|
+
if visualization and remove:
|
|
21
|
+
print("Exibindo duplicatas")
|
|
22
|
+
print(data[data.duplicated()])
|
|
23
|
+
print("\nRemovendo duplicatas...")
|
|
24
|
+
return data.drop_duplicates()
|
|
25
|
+
|
|
26
|
+
elif visualization:
|
|
27
|
+
|
|
28
|
+
print("Exibindo duplicatas:")
|
|
29
|
+
return data[data.duplicated()]
|
|
30
|
+
|
|
31
|
+
elif remove:
|
|
32
|
+
|
|
33
|
+
print("Removendo duplicatas...")
|
|
34
|
+
return data.drop_duplicates()
|
|
35
|
+
|
|
36
|
+
else:
|
|
37
|
+
|
|
38
|
+
print("Nenhuma ação foi realizada. Use 'visualization' para visualizar ou 'remove' para remover duplicatas.")
|
|
39
|
+
return data
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def remove_outliers(data: pd.DataFrame,
|
|
44
|
+
method:str = "iqr",
|
|
45
|
+
threshold: float = 1.5,
|
|
46
|
+
columns: list[str] | None = None) -> pd.DataFrame:
|
|
47
|
+
|
|
48
|
+
"""
|
|
49
|
+
Remove outliers de um DataFrame com base em métodos estatísticos.
|
|
50
|
+
|
|
51
|
+
Parâmetros
|
|
52
|
+
----------
|
|
53
|
+
data : pandas.DataFrame
|
|
54
|
+
Conjunto de dados a ser processado.
|
|
55
|
+
method : str, opcional (default='iqr')
|
|
56
|
+
Método usado para detecção de outliers. Pode ser:
|
|
57
|
+
- 'iqr': usa o intervalo interquartil (Q1 - 1.5*IQR, Q3 + 1.5*IQR)
|
|
58
|
+
- 'zscore': usa o desvio padrão em relação à média.
|
|
59
|
+
threshold : float, opcional (default=1.5)
|
|
60
|
+
Fator usado para definir o limite dos outliers.
|
|
61
|
+
- Para 'iqr': multiplicador do IQR.
|
|
62
|
+
- Para 'zscore': valor absoluto do Z-score máximo permitido.
|
|
63
|
+
columns : list ou None, opcional
|
|
64
|
+
Lista de colunas numéricas a serem analisadas. Se None, aplica em todas as colunas numéricas.
|
|
65
|
+
|
|
66
|
+
Retorna
|
|
67
|
+
-------
|
|
68
|
+
pandas.DataFrame
|
|
69
|
+
DataFrame sem os outliers detectados.
|
|
70
|
+
|
|
71
|
+
Lança
|
|
72
|
+
-----
|
|
73
|
+
ValueError
|
|
74
|
+
Se o método informado não for reconhecido ou se o threshold for menor ou igual a zero.
|
|
75
|
+
|
|
76
|
+
KeyError
|
|
77
|
+
Se alguma coluna informada não existir no DataFrame.
|
|
78
|
+
|
|
79
|
+
TypeError
|
|
80
|
+
Se alguma coluna informada não for numérica.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
df = data.copy()
|
|
84
|
+
|
|
85
|
+
# Seleciona as colunas numéricas
|
|
86
|
+
if columns is None:
|
|
87
|
+
columns = df.select_dtypes(include=["number"]).columns.tolist()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# Valida as colunas
|
|
91
|
+
for col in columns:
|
|
92
|
+
if col not in df.columns:
|
|
93
|
+
raise KeyError(f"A coluna '{col}' não existe no DataFrame.")
|
|
94
|
+
|
|
95
|
+
# Verifica se as colunas são numéricas
|
|
96
|
+
if not pd.api.types.is_numeric_dtype(df[col]):
|
|
97
|
+
raise TypeError(f"A coluna '{col}' deve ser numérica")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
if threshold <= 0:
|
|
101
|
+
raise ValueError("O threshold deve ser maior que zero.")
|
|
102
|
+
|
|
103
|
+
# Verifica o método escolhido
|
|
104
|
+
match method:
|
|
105
|
+
case "iqr":
|
|
106
|
+
|
|
107
|
+
for col in columns:
|
|
108
|
+
Q1 = df[col].quantile(0.25)
|
|
109
|
+
Q3 = df[col].quantile(0.75)
|
|
110
|
+
IQR = Q3 - Q1
|
|
111
|
+
|
|
112
|
+
lower_limit = Q1 - threshold * IQR
|
|
113
|
+
upper_limit = Q3 + threshold * IQR
|
|
114
|
+
|
|
115
|
+
df = df[(df[col] >= lower_limit) & (df[col] <= upper_limit)]
|
|
116
|
+
|
|
117
|
+
return df
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
case "zscore":
|
|
121
|
+
|
|
122
|
+
for col in columns:
|
|
123
|
+
mean = df[col].mean()
|
|
124
|
+
std = df[col].std()
|
|
125
|
+
|
|
126
|
+
z_scores = (df[col] - mean) / std
|
|
127
|
+
|
|
128
|
+
df = df[z_scores.abs() <= threshold]
|
|
129
|
+
|
|
130
|
+
return df
|
|
131
|
+
|
|
132
|
+
case _:
|
|
133
|
+
raise ValueError(f"Método '{method}' não reconhecido. Use 'iqr' ou 'zscore'.")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def clean_missing(
|
|
139
|
+
data: pd.DataFrame,
|
|
140
|
+
strategy: str = "drop",
|
|
141
|
+
columns: list[str] | None = None,
|
|
142
|
+
value: object | None = None,
|
|
143
|
+
threshold: float | None = None,
|
|
144
|
+
visualize: bool = False
|
|
145
|
+
) -> pd.DataFrame:
|
|
146
|
+
|
|
147
|
+
"""
|
|
148
|
+
Trata valores ausentes em um DataFrame com diferentes estratégias.
|
|
149
|
+
|
|
150
|
+
Parâmetros:
|
|
151
|
+
- data: DataFrame do pandas.
|
|
152
|
+
- strategy: Estratégia de tratamento. Pode ser 'drop', 'mean', 'median', 'mode', 'value' ou 'interpolate'.
|
|
153
|
+
- columns: Lista de colunas a serem tratadas. Se None, serão escolhidas com base na estratégia.
|
|
154
|
+
- value: Valor específico para preenchimento (usado apenas se strategy='value').
|
|
155
|
+
- threshold: Proporção de valores ausentes para remover colunas (usado apenas com strategy='drop').
|
|
156
|
+
- visualize: Se True, exibe o resumo de valores ausentes antes do tratamento.
|
|
157
|
+
|
|
158
|
+
Retorna:
|
|
159
|
+
- DataFrame com valores ausentes tratados.
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
df = data.copy()
|
|
163
|
+
|
|
164
|
+
if columns is None:
|
|
165
|
+
if strategy in ["mean", "median", "zscore"]:
|
|
166
|
+
columns = df.select_dtypes(include=["number"]).columns
|
|
167
|
+
elif strategy in ["mode", "value", "drop", "interpolate"]:
|
|
168
|
+
columns = df.columns
|
|
169
|
+
else:
|
|
170
|
+
raise ValueError(f"Estratégia '{strategy}' não reconhecida.")
|
|
171
|
+
|
|
172
|
+
match strategy:
|
|
173
|
+
case "drop":
|
|
174
|
+
if threshold is not None:
|
|
175
|
+
|
|
176
|
+
df = df.loc[:, df.isnull().mean() < threshold]
|
|
177
|
+
else:
|
|
178
|
+
|
|
179
|
+
df = df.dropna(subset=columns)
|
|
180
|
+
|
|
181
|
+
case "mean":
|
|
182
|
+
for col in columns:
|
|
183
|
+
df[col] = df[col].fillna(df[col].mean())
|
|
184
|
+
|
|
185
|
+
case "median":
|
|
186
|
+
for col in columns:
|
|
187
|
+
df[col] = df[col].fillna(df[col].median())
|
|
188
|
+
|
|
189
|
+
case "mode":
|
|
190
|
+
for col in columns:
|
|
191
|
+
mode = df[col].mode()
|
|
192
|
+
if not mode.empty:
|
|
193
|
+
df[col] = df[col].fillna(mode[0])
|
|
194
|
+
|
|
195
|
+
case "value":
|
|
196
|
+
if value is None:
|
|
197
|
+
raise ValueError("Para a estratégia 'value', o parâmetro 'value' deve ser fornecido.")
|
|
198
|
+
for col in columns:
|
|
199
|
+
df[col] = df[col].fillna(value)
|
|
200
|
+
|
|
201
|
+
case "interpolate":
|
|
202
|
+
df[columns] = df[columns].interpolate()
|
|
203
|
+
|
|
204
|
+
case _:
|
|
205
|
+
raise ValueError(f"Estratégia '{strategy}' não reconhecida.")
|
|
206
|
+
|
|
207
|
+
return df
|
|
208
|
+
|
|
209
|
+
|
jlprep/comparison.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
|
|
2
|
+
__all__ = ["comparison"]
|
|
3
|
+
|
|
4
|
+
import pandas as pd
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def compare_datasets(
|
|
8
|
+
original: pd.DataFrame,
|
|
9
|
+
processed: pd.DataFrame
|
|
10
|
+
) -> dict:
|
|
11
|
+
"""
|
|
12
|
+
Compara indicadores de dois DataFrames antes e depois
|
|
13
|
+
de um procedimento de pré-processamento.
|
|
14
|
+
"""
|
|
15
|
+
if not isinstance(original, pd.DataFrame):
|
|
16
|
+
raise TypeError("original deve ser um DataFrame.")
|
|
17
|
+
|
|
18
|
+
if not isinstance(processed, pd.DataFrame):
|
|
19
|
+
raise TypeError("processed deve ser um DataFrame.")
|
|
20
|
+
|
|
21
|
+
def metrics(df):
|
|
22
|
+
total_cells = df.shape[0] * df.shape[1]
|
|
23
|
+
missing = int(df.isna().sum().sum())
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
"rows": len(df),
|
|
27
|
+
"columns": len(df.columns),
|
|
28
|
+
"missing_values": missing,
|
|
29
|
+
"missing_percentage": round(
|
|
30
|
+
missing / total_cells * 100, 2
|
|
31
|
+
) if total_cells else 0.0,
|
|
32
|
+
"duplicates": int(df.duplicated().sum()),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
before = metrics(original)
|
|
36
|
+
after = metrics(processed)
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
"before": before,
|
|
40
|
+
"after": after,
|
|
41
|
+
"changes": {
|
|
42
|
+
"row_difference": after["rows"] - before["rows"],
|
|
43
|
+
"column_difference": (
|
|
44
|
+
after["columns"] - before["columns"]
|
|
45
|
+
),
|
|
46
|
+
"missing_difference": (
|
|
47
|
+
after["missing_values"]
|
|
48
|
+
- before["missing_values"]
|
|
49
|
+
),
|
|
50
|
+
"duplicate_difference": (
|
|
51
|
+
after["duplicates"] - before["duplicates"]
|
|
52
|
+
),
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
|
jlprep/diagnostics.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
__all__ = ["diagnostics"]
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def analyze_dataset(df: pd.DataFrame, target: str | None = None) -> dict:
|
|
7
|
+
"""
|
|
8
|
+
Analisa um DataFrame e gera um diagnóstico com recomendações.
|
|
9
|
+
|
|
10
|
+
A análise não modifica os dados originais.
|
|
11
|
+
"""
|
|
12
|
+
if not isinstance(df, pd.DataFrame):
|
|
13
|
+
raise TypeError("df deve ser um DataFrame do Pandas.")
|
|
14
|
+
|
|
15
|
+
if target is not None and target not in df.columns:
|
|
16
|
+
raise KeyError(f"Coluna-alvo '{target}' não encontrada.")
|
|
17
|
+
|
|
18
|
+
alerts = []
|
|
19
|
+
|
|
20
|
+
# 1. Valores ausentes
|
|
21
|
+
for column in df.columns:
|
|
22
|
+
missing_count = int(df[column].isna().sum())
|
|
23
|
+
|
|
24
|
+
if missing_count == 0:
|
|
25
|
+
continue
|
|
26
|
+
|
|
27
|
+
missing_pct = (
|
|
28
|
+
missing_count / len(df) * 100 if len(df) else 0.0
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
if pd.api.types.is_numeric_dtype(df[column]):
|
|
32
|
+
suggestion = (
|
|
33
|
+
"Investigar a distribuição da variável e avaliar "
|
|
34
|
+
"imputação pela mediana ou outra estratégia adequada."
|
|
35
|
+
)
|
|
36
|
+
else:
|
|
37
|
+
suggestion = (
|
|
38
|
+
"Investigar os valores ausentes e avaliar "
|
|
39
|
+
"imputação pela moda ou criação de uma categoria."
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
alerts.append({
|
|
43
|
+
"type": "missing_values",
|
|
44
|
+
"column": column,
|
|
45
|
+
"count": missing_count,
|
|
46
|
+
"percentage": round(missing_pct, 2),
|
|
47
|
+
"recommendation": suggestion,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
# 2. Registros duplicados
|
|
51
|
+
duplicates = int(df.duplicated().sum())
|
|
52
|
+
|
|
53
|
+
if duplicates:
|
|
54
|
+
alerts.append({
|
|
55
|
+
"type": "duplicates",
|
|
56
|
+
"count": duplicates,
|
|
57
|
+
"recommendation": (
|
|
58
|
+
"Verificar se os registros repetidos são erros "
|
|
59
|
+
"antes de removê-los."
|
|
60
|
+
),
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
# 3. Outliers pelo método IQR
|
|
64
|
+
numeric_columns = df.select_dtypes(include="number").columns
|
|
65
|
+
|
|
66
|
+
for column in numeric_columns:
|
|
67
|
+
if column == target:
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
values = df[column].dropna()
|
|
71
|
+
|
|
72
|
+
if len(values) < 4:
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
q1 = values.quantile(0.25)
|
|
76
|
+
q3 = values.quantile(0.75)
|
|
77
|
+
iqr = q3 - q1
|
|
78
|
+
|
|
79
|
+
if iqr == 0:
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
lower = q1 - 1.5 * iqr
|
|
83
|
+
upper = q3 + 1.5 * iqr
|
|
84
|
+
|
|
85
|
+
outlier_count = int(
|
|
86
|
+
((values < lower) | (values > upper)).sum()
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if outlier_count:
|
|
90
|
+
alerts.append({
|
|
91
|
+
"type": "outliers",
|
|
92
|
+
"column": column,
|
|
93
|
+
"count": outlier_count,
|
|
94
|
+
"method": "IQR",
|
|
95
|
+
"recommendation": (
|
|
96
|
+
"Investigar os valores extremos antes de "
|
|
97
|
+
"decidir pela remoção ou transformação."
|
|
98
|
+
),
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
# 4. Balanceamento das classes
|
|
102
|
+
if target is not None:
|
|
103
|
+
counts = df[target].value_counts(dropna=True)
|
|
104
|
+
|
|
105
|
+
if len(counts) >= 2:
|
|
106
|
+
majority = int(counts.max())
|
|
107
|
+
minority = int(counts.min())
|
|
108
|
+
ratio = majority / minority
|
|
109
|
+
|
|
110
|
+
if ratio >= 3:
|
|
111
|
+
alerts.append({
|
|
112
|
+
"type": "class_imbalance",
|
|
113
|
+
"column": target,
|
|
114
|
+
"ratio": round(ratio, 2),
|
|
115
|
+
"recommendation": (
|
|
116
|
+
"Investigar o impacto do desbalanceamento "
|
|
117
|
+
"e avaliar métricas por classe e "
|
|
118
|
+
"estratégias de amostragem."
|
|
119
|
+
),
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
"rows": len(df),
|
|
124
|
+
"columns": len(df.columns),
|
|
125
|
+
"target": target,
|
|
126
|
+
"total_alerts": len(alerts),
|
|
127
|
+
"alerts": alerts,
|
|
128
|
+
}
|
|
129
|
+
|
jlprep/encoding.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
__all__ = ["encode_label",
|
|
2
|
+
"encode_onehot"]
|
|
3
|
+
|
|
4
|
+
import pandas as pd
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def encode_label(data: pd.DataFrame,
|
|
8
|
+
columns: list[str] | None = None,
|
|
9
|
+
return_mapping: bool = False) -> pd.DataFrame:
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
Aplica codificação label encoding em colunas categóricas de um Dataframe.
|
|
15
|
+
|
|
16
|
+
Cada valor único de texto é substituído por um número inteiro, facilitando o uso
|
|
17
|
+
de dados categóricos em algoritmos de Machine Learning.
|
|
18
|
+
|
|
19
|
+
Parâmetros:
|
|
20
|
+
----------
|
|
21
|
+
data: pandas.Dataframe
|
|
22
|
+
O DataFrame de entrada que contém os dados a serem codificados
|
|
23
|
+
|
|
24
|
+
columns: list (opcional)
|
|
25
|
+
Lista de colunas a serem codificadas. Se "None", todas as colunas do tipo 'object' serão usadas.
|
|
26
|
+
|
|
27
|
+
return_mapping : bool, opcional
|
|
28
|
+
Se True, retorna também um dicionário com os mapeamentos de rótulo para número inteiro
|
|
29
|
+
para cada coluna codificada.
|
|
30
|
+
|
|
31
|
+
Retorna:
|
|
32
|
+
-------
|
|
33
|
+
pandas.DataFrame
|
|
34
|
+
O DataFrame com as colunas especificadas codificadas.
|
|
35
|
+
|
|
36
|
+
dict (opcional)
|
|
37
|
+
Um dicionário contendo os mapeamentos usados para cada coluna, se return_mapping=True.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
if columns is None:
|
|
41
|
+
columns = data.select_dtypes(include=["object", "string", "category"]).columns
|
|
42
|
+
|
|
43
|
+
if return_mapping:
|
|
44
|
+
mapping_dict = {}
|
|
45
|
+
|
|
46
|
+
for col in columns:
|
|
47
|
+
unique_values = data[col].unique()
|
|
48
|
+
mapping = {label: idx for idx, label in enumerate(unique_values)}
|
|
49
|
+
data[col] = data[col].map(mapping)
|
|
50
|
+
|
|
51
|
+
if return_mapping:
|
|
52
|
+
mapping_dict[col] = mapping
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
if return_mapping:
|
|
56
|
+
return data, mapping_dict
|
|
57
|
+
return data
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def encode_onehot(
|
|
62
|
+
data: pd.DataFrame,
|
|
63
|
+
columns: list[str] | None = None,
|
|
64
|
+
drop_first: bool = False
|
|
65
|
+
) -> pd.DataFrame:
|
|
66
|
+
"""
|
|
67
|
+
Codifica variáveis categóricas usando One-Hot Encoding.
|
|
68
|
+
|
|
69
|
+
Parâmetros
|
|
70
|
+
----------
|
|
71
|
+
data : pandas.DataFrame
|
|
72
|
+
DataFrame contendo os dados.
|
|
73
|
+
|
|
74
|
+
columns : list[str] | None, opcional
|
|
75
|
+
Lista das colunas categóricas que serão codificadas.
|
|
76
|
+
Se None, todas as colunas dos tipos 'object', 'string' e 'category'
|
|
77
|
+
serão utilizadas.
|
|
78
|
+
|
|
79
|
+
drop_first : bool, opcional
|
|
80
|
+
Se True, remove a primeira categoria de cada coluna codificada,
|
|
81
|
+
evitando multicolinearidade.
|
|
82
|
+
|
|
83
|
+
Retorna
|
|
84
|
+
-------
|
|
85
|
+
pandas.DataFrame
|
|
86
|
+
Novo DataFrame contendo as colunas categóricas codificadas.
|
|
87
|
+
|
|
88
|
+
Levanta
|
|
89
|
+
-------
|
|
90
|
+
KeyError
|
|
91
|
+
Se alguma coluna informada não existir no DataFrame.
|
|
92
|
+
|
|
93
|
+
TypeError
|
|
94
|
+
Se alguma coluna informada não for categórica.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
if columns is None:
|
|
98
|
+
columns = data.select_dtypes(include=["object", "string", "category"]).columns.tolist()
|
|
99
|
+
|
|
100
|
+
for col in columns:
|
|
101
|
+
if col not in data.columns:
|
|
102
|
+
raise KeyError(f"A coluna '{col}' não existe no DataFrame.")
|
|
103
|
+
|
|
104
|
+
if not (
|
|
105
|
+
pd.api.types.is_object_dtype(data[col])
|
|
106
|
+
or pd.api.types.is_string_dtype(data[col])
|
|
107
|
+
or pd.api.types.is_categorical_dtype(data[col])
|
|
108
|
+
):
|
|
109
|
+
raise TypeError(f"A coluna '{col}' deve ser categórica.")
|
|
110
|
+
|
|
111
|
+
return pd.get_dummies(
|
|
112
|
+
data,
|
|
113
|
+
columns=columns,
|
|
114
|
+
drop_first=drop_first,
|
|
115
|
+
dtype=int
|
|
116
|
+
)
|
jlprep/exploration.py
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# preprocess/exploration.py
|
|
2
|
+
from pandas.api.types import (
|
|
3
|
+
is_numeric_dtype,
|
|
4
|
+
is_categorical_dtype,
|
|
5
|
+
is_object_dtype,
|
|
6
|
+
is_bool_dtype,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
import pandas as pd
|
|
10
|
+
|
|
11
|
+
__all__ = ["summarize_data",
|
|
12
|
+
"value_counts_plot",
|
|
13
|
+
"check_data_quality"]
|
|
14
|
+
|
|
15
|
+
def summarize_data(data: pd.DataFrame):
|
|
16
|
+
"""
|
|
17
|
+
Gera um resumo exploratório do DataFrame:
|
|
18
|
+
- Tamanho (linhas/colunas)
|
|
19
|
+
- Tipos de dados por coluna
|
|
20
|
+
- Valores ausentes (contagem e %)
|
|
21
|
+
- Estatísticas por coluna:
|
|
22
|
+
* Numéricas: média, mediana, moda, min, max
|
|
23
|
+
* Categóricas (object/category/bool): moda, nº de categorias únicas, freq. da moda
|
|
24
|
+
Retorna:
|
|
25
|
+
dict: {nome_da_coluna: {estatísticas...}}
|
|
26
|
+
"""
|
|
27
|
+
# 1) Tamanho
|
|
28
|
+
print("Número de linhas e colunas:", len(data), "linhas,", len(data.columns), "colunas")
|
|
29
|
+
|
|
30
|
+
# 2) Tipos
|
|
31
|
+
print("\nTipos de dados por coluna:")
|
|
32
|
+
print(data.dtypes)
|
|
33
|
+
|
|
34
|
+
# 3) Ausentes (contagem e %)
|
|
35
|
+
print("\nValores ausentes por coluna (ordem decrescente):")
|
|
36
|
+
print(data.isnull().sum().sort_values(ascending=False))
|
|
37
|
+
|
|
38
|
+
print("\nPorcentagem de valores ausentes por coluna:")
|
|
39
|
+
print(((data.isnull().sum() / len(data)) * 100).round(2))
|
|
40
|
+
|
|
41
|
+
# 4) Amostra
|
|
42
|
+
print("\n5 primeiras linhas do DataFrame:")
|
|
43
|
+
print(data.head(5))
|
|
44
|
+
|
|
45
|
+
# 5) Estatísticas por coluna
|
|
46
|
+
resumo = {}
|
|
47
|
+
for col in data.columns:
|
|
48
|
+
if is_numeric_dtype(data[col]):
|
|
49
|
+
s = data[col].dropna()
|
|
50
|
+
moda_series = s.mode()
|
|
51
|
+
moda = moda_series.iloc[0] if not moda_series.empty else None
|
|
52
|
+
resumo[col] = {
|
|
53
|
+
"média": s.mean() if not s.empty else None,
|
|
54
|
+
"mediana": s.median() if not s.empty else None,
|
|
55
|
+
"moda": moda,
|
|
56
|
+
"mínimo": s.min() if not s.empty else None,
|
|
57
|
+
"máximo": s.max() if not s.empty else None,
|
|
58
|
+
}
|
|
59
|
+
elif is_categorical_dtype(data[col]) or is_object_dtype(data[col]) or is_bool_dtype(data[col]):
|
|
60
|
+
s = data[col].dropna()
|
|
61
|
+
moda_series = s.mode()
|
|
62
|
+
moda = moda_series.iloc[0] if not moda_series.empty else None
|
|
63
|
+
freq_moda = s.value_counts().iloc[0] if not s.value_counts().empty else 0
|
|
64
|
+
resumo[col] = {
|
|
65
|
+
"moda": moda,
|
|
66
|
+
"n_categorias": s.nunique(),
|
|
67
|
+
"freq_moda": int(freq_moda),
|
|
68
|
+
}
|
|
69
|
+
else:
|
|
70
|
+
# Para tipos como datetime, etc., um resumo simples:
|
|
71
|
+
s = data[col].dropna()
|
|
72
|
+
resumo[col] = {
|
|
73
|
+
"tipo": str(data[col].dtype),
|
|
74
|
+
"n_únicos": s.nunique(),
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# Imprime o resumo
|
|
78
|
+
for col, stats in resumo.items():
|
|
79
|
+
print(f"\nColuna: {col}")
|
|
80
|
+
for k, v in stats.items():
|
|
81
|
+
print(f" {k}: {v}")
|
|
82
|
+
|
|
83
|
+
return resumo
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def value_counts_plot(
|
|
87
|
+
data: pd.DataFrame,
|
|
88
|
+
column: str,
|
|
89
|
+
normalize: bool = False,
|
|
90
|
+
top_n: int | None = None
|
|
91
|
+
) -> None:
|
|
92
|
+
|
|
93
|
+
import matplotlib.pyplot as plt
|
|
94
|
+
|
|
95
|
+
"""
|
|
96
|
+
Gera um gráfico de barras com a contagem (ou proporção) dos valores únicos
|
|
97
|
+
em uma coluna categórica.
|
|
98
|
+
|
|
99
|
+
Parâmetros
|
|
100
|
+
----------
|
|
101
|
+
data : pandas.DataFrame
|
|
102
|
+
O DataFrame com os dados.
|
|
103
|
+
column : str
|
|
104
|
+
Nome da coluna categórica a ser analisada.
|
|
105
|
+
normalize : bool, opcional (default=False)
|
|
106
|
+
Se True, mostra proporções (%) em vez de contagens absolutas.
|
|
107
|
+
top_n : int, opcional
|
|
108
|
+
Se definido, mostra apenas as top N categorias mais frequentes.
|
|
109
|
+
|
|
110
|
+
Retorno
|
|
111
|
+
-------
|
|
112
|
+
None
|
|
113
|
+
Exibe o gráfico.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
#Obtendo frequências
|
|
117
|
+
counts = data[column].value_counts(normalize=normalize)
|
|
118
|
+
|
|
119
|
+
if top_n is not None:
|
|
120
|
+
counts = counts.head(top_n)
|
|
121
|
+
|
|
122
|
+
counts.plot(kind="bar")
|
|
123
|
+
plt.title(f"Distribuição de valores - {column}")
|
|
124
|
+
plt.xlabel("Categorias")
|
|
125
|
+
plt.ylabel("Frequências (%)" if normalize else "Frequências")
|
|
126
|
+
plt.show()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def check_data_quality(data: pd.DataFrame):
|
|
131
|
+
"""
|
|
132
|
+
Analisa a qualidade dos dados de um DataFrame.
|
|
133
|
+
|
|
134
|
+
Funcionalidades:
|
|
135
|
+
- Valores ausentes (%)
|
|
136
|
+
- Duplicatas por coluna (%)
|
|
137
|
+
- Outliers (IQR) para colunas numéricas (quantidade)
|
|
138
|
+
- Cardinalidade de colunas categóricas (com gráfico)
|
|
139
|
+
|
|
140
|
+
Parâmetros
|
|
141
|
+
----------
|
|
142
|
+
data : pandas.DataFrame
|
|
143
|
+
O DataFrame a ser analisado.
|
|
144
|
+
|
|
145
|
+
Retornos
|
|
146
|
+
-------
|
|
147
|
+
dict
|
|
148
|
+
Dicionários com informações sobre valores ausentes, duplicatas,
|
|
149
|
+
outliers e cardinalidade.
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
import numpy as np
|
|
153
|
+
import matplotlib.pyplot as plt
|
|
154
|
+
from pandas.api.types import is_numeric_dtype, is_object_dtype, is_categorical_dtype
|
|
155
|
+
|
|
156
|
+
dict_missing_col = {}
|
|
157
|
+
dict_duplicated_col = {}
|
|
158
|
+
dict_outliers_col = {}
|
|
159
|
+
dict_cardinality = {}
|
|
160
|
+
|
|
161
|
+
total_rows = len(data)
|
|
162
|
+
|
|
163
|
+
# Valores ausentes
|
|
164
|
+
for col in data.columns:
|
|
165
|
+
percentual_missing = (data[col].isnull().sum() / total_rows) * 100
|
|
166
|
+
dict_missing_col[col] = round(float(percentual_missing), 2)
|
|
167
|
+
|
|
168
|
+
# Duplicatas
|
|
169
|
+
for col in data.columns:
|
|
170
|
+
duplicated_count = total_rows - data[col].nunique()
|
|
171
|
+
duplicated_percent = (duplicated_count / total_rows) * 100
|
|
172
|
+
dict_duplicated_col[col] = {
|
|
173
|
+
"quantidade": int(duplicated_count),
|
|
174
|
+
"percentual": round(float(duplicated_percent), 2)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
# Outliers (IQR)
|
|
178
|
+
for col in data.columns:
|
|
179
|
+
if is_numeric_dtype(data[col]):
|
|
180
|
+
col_clean = data[col].dropna()
|
|
181
|
+
|
|
182
|
+
Q1 = np.percentile(col_clean, 25)
|
|
183
|
+
Q3 = np.percentile(col_clean, 75)
|
|
184
|
+
IQR = Q3 - Q1
|
|
185
|
+
|
|
186
|
+
lower_bound = Q1 - 1.5 * IQR
|
|
187
|
+
upper_bound = Q3 + 1.5 * IQR
|
|
188
|
+
|
|
189
|
+
outliers_count = ((col_clean < lower_bound) | (col_clean > upper_bound)).sum()
|
|
190
|
+
dict_outliers_col[col] = int(outliers_count)
|
|
191
|
+
|
|
192
|
+
# Cardinalidade (object + categorical)
|
|
193
|
+
for col in data.columns:
|
|
194
|
+
if is_object_dtype(data[col]) or is_categorical_dtype(data[col]):
|
|
195
|
+
unique_values = data[col].nunique()
|
|
196
|
+
proportion = (unique_values / total_rows) * 100
|
|
197
|
+
|
|
198
|
+
dict_cardinality[col] = {
|
|
199
|
+
"unicos": int(unique_values),
|
|
200
|
+
"percentual": round(float(proportion), 2)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
# PRINT ORGANIZADO
|
|
204
|
+
print("\n=== QUALIDADE DOS DADOS ===\n")
|
|
205
|
+
|
|
206
|
+
print("🔹 Valores ausentes:")
|
|
207
|
+
for col, perc in dict_missing_col.items():
|
|
208
|
+
if perc > 0:
|
|
209
|
+
print(f"- {col}: {perc}%")
|
|
210
|
+
|
|
211
|
+
print("\n🔹 Duplicatas:")
|
|
212
|
+
for col, info in dict_duplicated_col.items():
|
|
213
|
+
if info["quantidade"] > 0:
|
|
214
|
+
print(f"- {col}: {info['quantidade']} ({info['percentual']}%)")
|
|
215
|
+
|
|
216
|
+
print("\n🔹 Outliers:")
|
|
217
|
+
has_outliers = False
|
|
218
|
+
for col, count in dict_outliers_col.items():
|
|
219
|
+
if count > 0:
|
|
220
|
+
print(f"- {col}: {count} outliers")
|
|
221
|
+
has_outliers = True
|
|
222
|
+
if not has_outliers:
|
|
223
|
+
print("- Nenhum outlier detectado")
|
|
224
|
+
|
|
225
|
+
print("\n🔹 Cardinalidade:")
|
|
226
|
+
if dict_cardinality:
|
|
227
|
+
for col, info in dict_cardinality.items():
|
|
228
|
+
print(f"- {col}: {info['unicos']} valores únicos ({info['percentual']}%)")
|
|
229
|
+
|
|
230
|
+
if info["percentual"] > 50:
|
|
231
|
+
print(f" ⚠️ Alta cardinalidade, considerar tratamento")
|
|
232
|
+
else:
|
|
233
|
+
print("- Nenhuma coluna categórica detectada")
|
|
234
|
+
|
|
235
|
+
# Gráfico de cardinalidade
|
|
236
|
+
if dict_cardinality:
|
|
237
|
+
cols = list(dict_cardinality.keys())
|
|
238
|
+
percents = [info["percentual"] for info in dict_cardinality.values()]
|
|
239
|
+
|
|
240
|
+
plt.figure(figsize=(10, 6))
|
|
241
|
+
plt.bar(cols, percents)
|
|
242
|
+
plt.xticks(rotation=45, ha="right")
|
|
243
|
+
plt.ylabel("Percentual de cardinalidade")
|
|
244
|
+
plt.title("Cardinalidade das colunas categóricas")
|
|
245
|
+
plt.show()
|
|
246
|
+
|
|
247
|
+
# Retorno estruturado
|
|
248
|
+
return {
|
|
249
|
+
"missing": dict_missing_col,
|
|
250
|
+
"duplicated": dict_duplicated_col,
|
|
251
|
+
"outliers": dict_outliers_col,
|
|
252
|
+
"cardinality": dict_cardinality
|
|
253
|
+
}
|
|
254
|
+
|
jlprep/quality.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# preprocess/exploration.py
|
|
2
|
+
from pandas.api.types import (
|
|
3
|
+
is_numeric_dtype,
|
|
4
|
+
is_categorical_dtype,
|
|
5
|
+
is_object_dtype,
|
|
6
|
+
is_bool_dtype,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
import pandas as pd
|
|
10
|
+
import matplotlib.pyplot as plt
|
|
11
|
+
|
|
12
|
+
__all__ = ["check_class_balance"]
|
|
13
|
+
|
|
14
|
+
def check_class_balance(
|
|
15
|
+
data: pd.DataFrame,
|
|
16
|
+
target: str,
|
|
17
|
+
plot: bool = False
|
|
18
|
+
) -> dict:
|
|
19
|
+
"""
|
|
20
|
+
Analisa o balanceamento das classes em uma variável alvo de um dataset.
|
|
21
|
+
|
|
22
|
+
Esta função calcula a distribuição das classes, identifica o tipo de problema
|
|
23
|
+
(classificação binária ou multiclasse), mede o nível de desbalanceamento por meio
|
|
24
|
+
da razão entre a classe majoritária e minoritária (imbalance ratio) e fornece
|
|
25
|
+
um diagnóstico com sugestões de tratamento.
|
|
26
|
+
|
|
27
|
+
Parâmetros
|
|
28
|
+
----------
|
|
29
|
+
data : pandas.DataFrame
|
|
30
|
+
Dataset contendo a variável alvo.
|
|
31
|
+
|
|
32
|
+
target : str
|
|
33
|
+
Nome da coluna que representa a variável alvo.
|
|
34
|
+
|
|
35
|
+
plot : bool, opcional (default=False)
|
|
36
|
+
Se True, exibe um gráfico de barras com a distribuição das classes.
|
|
37
|
+
|
|
38
|
+
Retorna
|
|
39
|
+
-------
|
|
40
|
+
dict
|
|
41
|
+
Um dicionário contendo:
|
|
42
|
+
- n_classes
|
|
43
|
+
- problem_type
|
|
44
|
+
- imbalance_ratio
|
|
45
|
+
- status
|
|
46
|
+
- suggestion
|
|
47
|
+
|
|
48
|
+
Levanta
|
|
49
|
+
-------
|
|
50
|
+
KeyError
|
|
51
|
+
Se a coluna especificada não existir.
|
|
52
|
+
|
|
53
|
+
TypeError
|
|
54
|
+
Se o parâmetro target não for uma string.
|
|
55
|
+
|
|
56
|
+
ValueError
|
|
57
|
+
Se a variável alvo possuir menos de duas classes.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
if not isinstance(target, str):
|
|
61
|
+
raise TypeError("O parâmetro 'target' deve ser uma string.")
|
|
62
|
+
|
|
63
|
+
if target not in data.columns:
|
|
64
|
+
raise KeyError(f"A coluna '{target}' não existe no DataFrame.")
|
|
65
|
+
|
|
66
|
+
col = data[target].dropna()
|
|
67
|
+
|
|
68
|
+
class_count = col.value_counts()
|
|
69
|
+
class_proportion = col.value_counts(normalize=True) * 100
|
|
70
|
+
|
|
71
|
+
n_classes = len(class_count)
|
|
72
|
+
|
|
73
|
+
if n_classes == 2:
|
|
74
|
+
problem = "Binary classification"
|
|
75
|
+
elif n_classes > 2:
|
|
76
|
+
problem = "Multiclass classification"
|
|
77
|
+
else:
|
|
78
|
+
raise ValueError("O dataset deve possuir pelo menos duas classes.")
|
|
79
|
+
|
|
80
|
+
max_class = class_count.max()
|
|
81
|
+
min_class = class_count.min()
|
|
82
|
+
|
|
83
|
+
imbalance_ratio = max_class / min_class
|
|
84
|
+
|
|
85
|
+
if imbalance_ratio <= 1.5:
|
|
86
|
+
status = "Dataset balanceado"
|
|
87
|
+
suggestion = "Nenhuma ação necessária."
|
|
88
|
+
|
|
89
|
+
elif imbalance_ratio <= 3:
|
|
90
|
+
status = "Dataset levemente desbalanceado"
|
|
91
|
+
suggestion = "Monitorar o desempenho do modelo."
|
|
92
|
+
|
|
93
|
+
elif imbalance_ratio <= 10:
|
|
94
|
+
status = "Dataset moderadamente desbalanceado"
|
|
95
|
+
suggestion = "Considerar técnicas de balanceamento."
|
|
96
|
+
|
|
97
|
+
else:
|
|
98
|
+
status = "Dataset altamente desbalanceado"
|
|
99
|
+
suggestion = "Aplicar técnicas de balanceamento (SMOTE, Oversampling, etc.)."
|
|
100
|
+
|
|
101
|
+
print("Distribuição das classes:\n")
|
|
102
|
+
|
|
103
|
+
for classe in class_count.index:
|
|
104
|
+
print(f"{classe}: {class_count[classe]} ({class_proportion[classe]:.2f}%)")
|
|
105
|
+
|
|
106
|
+
print(f"\nNúmero de classes: {n_classes}")
|
|
107
|
+
print(f"Tipo de problema: {problem}")
|
|
108
|
+
print(f"\nImbalance ratio: {imbalance_ratio:.2f}")
|
|
109
|
+
print(f"Status: {status}")
|
|
110
|
+
print(f"Sugestão: {suggestion}")
|
|
111
|
+
|
|
112
|
+
if plot:
|
|
113
|
+
class_count.plot(kind="bar", title="Distribuição das classes")
|
|
114
|
+
plt.show()
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
"n_classes": n_classes,
|
|
118
|
+
"problem_type": problem,
|
|
119
|
+
"imbalance_ratio": imbalance_ratio,
|
|
120
|
+
"status": status,
|
|
121
|
+
"suggestion": suggestion
|
|
122
|
+
}
|
jlprep/reporting.py
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""Geração de relatórios HTML a partir de analyze_dataset e compare_datasets."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
__all__ = ["reporting"]
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from html import escape
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
import webbrowser
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _text(value: Any) -> str:
|
|
15
|
+
return escape(str(value), quote=True)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _number(value: Any) -> str:
|
|
19
|
+
return _text(value if value is not None else "—")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def generate_report(
|
|
23
|
+
diagnosis: dict,
|
|
24
|
+
comparison: dict | None = None,
|
|
25
|
+
output_path: str | Path = "jlprep_report.html",
|
|
26
|
+
title: str = "Relatório de qualidade dos dados — JlPrep",
|
|
27
|
+
open_browser: bool = False,
|
|
28
|
+
) -> Path:
|
|
29
|
+
"""Gera um relatório HTML local com diagnóstico e comparação."""
|
|
30
|
+
|
|
31
|
+
if not isinstance(diagnosis, dict) or not isinstance(
|
|
32
|
+
diagnosis.get("alerts"), list
|
|
33
|
+
):
|
|
34
|
+
raise TypeError(
|
|
35
|
+
"diagnosis deve ser o retorno de analyze_dataset()."
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
if comparison is not None and not isinstance(comparison, dict):
|
|
39
|
+
raise TypeError(
|
|
40
|
+
"comparison deve ser o retorno de compare_datasets()."
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
path = Path(output_path).expanduser().resolve()
|
|
44
|
+
|
|
45
|
+
if path.suffix.lower() != ".html":
|
|
46
|
+
raise ValueError("O arquivo deve terminar em .html")
|
|
47
|
+
|
|
48
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
49
|
+
|
|
50
|
+
labels = {
|
|
51
|
+
"missing_values": "Valores ausentes",
|
|
52
|
+
"duplicates": "Registros duplicados",
|
|
53
|
+
"outliers": "Valores extremos (IQR)",
|
|
54
|
+
"class_imbalance": "Desbalanceamento de classes",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
metric_labels = {
|
|
58
|
+
"rows": "Registros",
|
|
59
|
+
"columns": "Colunas",
|
|
60
|
+
"missing_values": "Células ausentes",
|
|
61
|
+
"missing_percentage": "Ausentes (%)",
|
|
62
|
+
"duplicates": "Duplicatas",
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
alerts = diagnosis["alerts"]
|
|
66
|
+
alert_html = []
|
|
67
|
+
|
|
68
|
+
for alert in alerts:
|
|
69
|
+
if not isinstance(alert, dict):
|
|
70
|
+
continue
|
|
71
|
+
|
|
72
|
+
details = []
|
|
73
|
+
|
|
74
|
+
fields = [
|
|
75
|
+
("column", "Coluna"),
|
|
76
|
+
("count", "Ocorrências"),
|
|
77
|
+
("percentage", "Percentual (%)"),
|
|
78
|
+
("ratio", "Razão"),
|
|
79
|
+
("method", "Método"),
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
for key, label in fields:
|
|
83
|
+
if key in alert:
|
|
84
|
+
details.append(
|
|
85
|
+
f'<span class="chip">'
|
|
86
|
+
f'{label}: {_text(alert[key])}'
|
|
87
|
+
f'</span>'
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
recommendation = alert.get(
|
|
91
|
+
"recommendation",
|
|
92
|
+
"Investigar o indicador antes de decidir o tratamento.",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
alert_html.append(
|
|
96
|
+
'<article class="alert">'
|
|
97
|
+
f'<h3>{_text(labels.get(alert.get("type"), alert.get("type", "Alerta")))}</h3>'
|
|
98
|
+
f'<div class="chips">{"".join(details)}</div>'
|
|
99
|
+
f'<p><strong>Recomendação:</strong> '
|
|
100
|
+
f'{_text(recommendation)}</p>'
|
|
101
|
+
'</article>'
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if not alert_html:
|
|
105
|
+
alert_html = [
|
|
106
|
+
'<p class="empty">Nenhum alerta encontrado pelas regras '
|
|
107
|
+
'implementadas. Isso não garante ausência de problemas.</p>'
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
comparison_html = ""
|
|
111
|
+
|
|
112
|
+
if comparison is not None:
|
|
113
|
+
before = comparison.get("before", {})
|
|
114
|
+
after = comparison.get("after", {})
|
|
115
|
+
|
|
116
|
+
if not isinstance(before, dict) or not isinstance(after, dict):
|
|
117
|
+
raise ValueError(
|
|
118
|
+
"comparison precisa conter 'before' e 'after'."
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
rows = []
|
|
122
|
+
|
|
123
|
+
for key, label in metric_labels.items():
|
|
124
|
+
if key in before or key in after:
|
|
125
|
+
rows.append(
|
|
126
|
+
f'<tr><th scope="row">{_text(label)}</th>'
|
|
127
|
+
f'<td>{_number(before.get(key))}</td>'
|
|
128
|
+
f'<td>{_number(after.get(key))}</td></tr>'
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
comparison_html = (
|
|
132
|
+
'<section class="panel">'
|
|
133
|
+
'<h2>Comparação antes e depois</h2>'
|
|
134
|
+
'<p class="muted">Indicadores agregados; '
|
|
135
|
+
'não identifica alterações individuais de registros.</p>'
|
|
136
|
+
'<div class="table-wrap"><table>'
|
|
137
|
+
'<thead><tr><th>Indicador</th>'
|
|
138
|
+
'<th>Antes</th><th>Depois</th></tr></thead>'
|
|
139
|
+
f'<tbody>{"".join(rows)}</tbody>'
|
|
140
|
+
'</table></div></section>'
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
generated = datetime.now().strftime("%d/%m/%Y %H:%M")
|
|
144
|
+
|
|
145
|
+
html = f"""<!doctype html>
|
|
146
|
+
<html lang="pt-BR">
|
|
147
|
+
<head>
|
|
148
|
+
<meta charset="utf-8">
|
|
149
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
150
|
+
<title>{_text(title)}</title>
|
|
151
|
+
<style>
|
|
152
|
+
:root {{
|
|
153
|
+
--bg: #f4f7fb;
|
|
154
|
+
--ink: #17243a;
|
|
155
|
+
--muted: #596b82;
|
|
156
|
+
--line: #dce5ef;
|
|
157
|
+
--brand: #2357a4;
|
|
158
|
+
--paper: #fff;
|
|
159
|
+
}}
|
|
160
|
+
* {{ box-sizing: border-box; }}
|
|
161
|
+
body {{
|
|
162
|
+
margin: 0;
|
|
163
|
+
background: var(--bg);
|
|
164
|
+
font: 16px/1.55 system-ui, sans-serif;
|
|
165
|
+
color: var(--ink);
|
|
166
|
+
}}
|
|
167
|
+
header {{
|
|
168
|
+
background: linear-gradient(110deg, #17396d, #2862b1);
|
|
169
|
+
color: white;
|
|
170
|
+
padding: 38px max(24px, calc((100vw - 1040px)/2));
|
|
171
|
+
}}
|
|
172
|
+
header p {{ opacity: .88; margin: 6px 0 0; }}
|
|
173
|
+
main {{
|
|
174
|
+
max-width: 1040px;
|
|
175
|
+
margin: 30px auto 70px;
|
|
176
|
+
padding: 0 20px;
|
|
177
|
+
}}
|
|
178
|
+
h1 {{ font-size: 2rem; margin: 0; }}
|
|
179
|
+
h2 {{ margin: 0 0 14px; font-size: 1.25rem; }}
|
|
180
|
+
h3 {{ margin: 0 0 8px; font-size: 1.03rem; }}
|
|
181
|
+
.cards {{
|
|
182
|
+
display: grid;
|
|
183
|
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
184
|
+
gap: 14px;
|
|
185
|
+
margin-bottom: 24px;
|
|
186
|
+
}}
|
|
187
|
+
.card, .panel {{
|
|
188
|
+
background: var(--paper);
|
|
189
|
+
border: 1px solid var(--line);
|
|
190
|
+
border-radius: 14px;
|
|
191
|
+
box-shadow: 0 4px 16px #17243a0a;
|
|
192
|
+
}}
|
|
193
|
+
.card {{ padding: 20px; }}
|
|
194
|
+
.card .value {{
|
|
195
|
+
font-size: 2rem;
|
|
196
|
+
font-weight: 750;
|
|
197
|
+
color: var(--brand);
|
|
198
|
+
}}
|
|
199
|
+
.card .label {{
|
|
200
|
+
color: var(--muted);
|
|
201
|
+
font-size: .9rem;
|
|
202
|
+
}}
|
|
203
|
+
.panel {{ padding: 24px; margin-bottom: 22px; }}
|
|
204
|
+
.alert {{
|
|
205
|
+
border-left: 4px solid #e6a43d;
|
|
206
|
+
background: #fffbf3;
|
|
207
|
+
padding: 16px 18px;
|
|
208
|
+
margin: 12px 0;
|
|
209
|
+
border-radius: 7px;
|
|
210
|
+
}}
|
|
211
|
+
.alert p {{ margin: 10px 0 0; }}
|
|
212
|
+
.chips {{ display: flex; gap: 8px; flex-wrap: wrap; }}
|
|
213
|
+
.chip {{
|
|
214
|
+
background: #e8eef7;
|
|
215
|
+
border-radius: 999px;
|
|
216
|
+
padding: 3px 10px;
|
|
217
|
+
font-size: .85rem;
|
|
218
|
+
}}
|
|
219
|
+
.muted, .empty {{ color: var(--muted); }}
|
|
220
|
+
.table-wrap {{ overflow-x: auto; }}
|
|
221
|
+
table {{ width: 100%; border-collapse: collapse; text-align: left; }}
|
|
222
|
+
th, td {{ padding: 12px 14px; border-bottom: 1px solid var(--line); }}
|
|
223
|
+
thead {{ background: #eaf1fa; }}
|
|
224
|
+
footer {{
|
|
225
|
+
text-align: center;
|
|
226
|
+
color: var(--muted);
|
|
227
|
+
font-size: .85rem;
|
|
228
|
+
margin-top: 25px;
|
|
229
|
+
}}
|
|
230
|
+
@media print {{
|
|
231
|
+
body {{ background: white; }}
|
|
232
|
+
.panel, .card {{ box-shadow: none; break-inside: avoid; }}
|
|
233
|
+
}}
|
|
234
|
+
</style>
|
|
235
|
+
</head>
|
|
236
|
+
<body>
|
|
237
|
+
<header>
|
|
238
|
+
<h1>{_text(title)}</h1>
|
|
239
|
+
<p>Gerado em {generated} · Diagnóstico automatizado</p>
|
|
240
|
+
</header>
|
|
241
|
+
<main>
|
|
242
|
+
<section class="cards">
|
|
243
|
+
<div class="card">
|
|
244
|
+
<div class="label">Registros</div>
|
|
245
|
+
<div class="value">{_number(diagnosis.get("rows"))}</div>
|
|
246
|
+
</div>
|
|
247
|
+
<div class="card">
|
|
248
|
+
<div class="label">Colunas</div>
|
|
249
|
+
<div class="value">{_number(diagnosis.get("columns"))}</div>
|
|
250
|
+
</div>
|
|
251
|
+
<div class="card">
|
|
252
|
+
<div class="label">Alertas</div>
|
|
253
|
+
<div class="value">{len(alerts)}</div>
|
|
254
|
+
</div>
|
|
255
|
+
<div class="card">
|
|
256
|
+
<div class="label">Variável-alvo</div>
|
|
257
|
+
<div class="value" style="font-size:1.3rem;overflow-wrap:anywhere">
|
|
258
|
+
{_text(diagnosis.get("target") or "Não definida")}
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
</section>
|
|
262
|
+
|
|
263
|
+
<section class="panel">
|
|
264
|
+
<h2>Diagnóstico e recomendações</h2>
|
|
265
|
+
{"".join(alert_html)}
|
|
266
|
+
</section>
|
|
267
|
+
|
|
268
|
+
{comparison_html}
|
|
269
|
+
|
|
270
|
+
<section class="panel">
|
|
271
|
+
<h2>Como interpretar</h2>
|
|
272
|
+
<p>Os alertas indicam situações que merecem investigação.
|
|
273
|
+
Valores extremos não são necessariamente erros, e nem toda
|
|
274
|
+
ausência ou duplicata deve ser removida. As recomendações
|
|
275
|
+
não substituem a análise do contexto dos dados.</p>
|
|
276
|
+
</section>
|
|
277
|
+
|
|
278
|
+
<footer>JlPrep · Relatório gerado localmente.</footer>
|
|
279
|
+
</main>
|
|
280
|
+
</body>
|
|
281
|
+
</html>"""
|
|
282
|
+
|
|
283
|
+
path.write_text(html, encoding="utf-8")
|
|
284
|
+
|
|
285
|
+
if open_browser:
|
|
286
|
+
webbrowser.open(path.as_uri())
|
|
287
|
+
|
|
288
|
+
return path
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jlprep
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Biblioteca Python para pré-processamento e análise da qualidade de dados
|
|
5
|
+
Author: Joao Lucas Moreira de Sousa
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Classifier: Intended Audience :: Science/Research
|
|
10
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: pandas
|
|
14
|
+
Requires-Dist: numpy
|
|
15
|
+
Requires-Dist: matplotlib
|
|
16
|
+
|
|
17
|
+
# jlprep
|
|
18
|
+
|
|
19
|
+
Biblioteca em Python para **análise e pré-processamento de dados** voltada para aplicações de Machine Learning.
|
|
20
|
+
|
|
21
|
+
A `jlprep` foi desenvolvida como parte de um **Trabalho de Conclusão de Curso (TCC)**, com foco na análise e melhoria da qualidade de dados utilizados em modelos de Machine Learning.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Objetivo
|
|
26
|
+
|
|
27
|
+
A biblioteca busca centralizar etapas comuns do pré-processamento de dados, como:
|
|
28
|
+
|
|
29
|
+
* Limpeza de dados
|
|
30
|
+
* Codificação de variáveis categóricas
|
|
31
|
+
* Análise exploratória
|
|
32
|
+
* Avaliação da qualidade dos dados
|
|
33
|
+
|
|
34
|
+
Além disso, fornece **diagnósticos interpretáveis**, auxiliando na tomada de decisão antes do treinamento de modelos de Machine Learning.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Contexto do TCC
|
|
39
|
+
|
|
40
|
+
Este projeto foi desenvolvido no contexto do tema:
|
|
41
|
+
|
|
42
|
+
> **Qualidade dos dados em Machine Learning (dados faltantes, desbalanceados e ruidosos)**
|
|
43
|
+
|
|
44
|
+
A proposta da `jlprep` é oferecer uma abordagem prática para identificação e tratamento desses problemas, reunindo funcionalidades essenciais em uma única biblioteca.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Funcionalidades
|
|
49
|
+
|
|
50
|
+
### Limpeza de dados
|
|
51
|
+
|
|
52
|
+
* Remoção de valores duplicados
|
|
53
|
+
* Tratamento de valores faltantes
|
|
54
|
+
* Remoção de outliers
|
|
55
|
+
|
|
56
|
+
### Transformação
|
|
57
|
+
|
|
58
|
+
* Label Encoding
|
|
59
|
+
* One-Hot Encoding
|
|
60
|
+
|
|
61
|
+
### Exploração de dados
|
|
62
|
+
|
|
63
|
+
* Resumo estatístico do dataset
|
|
64
|
+
* Visualização da distribuição de valores
|
|
65
|
+
|
|
66
|
+
### Qualidade dos dados
|
|
67
|
+
|
|
68
|
+
* Verificação de inconsistências
|
|
69
|
+
* Análise de desbalanceamento de classes
|
|
70
|
+
* Sugestões de pré-processamento
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Instalação
|
|
75
|
+
|
|
76
|
+
Clone o repositório e acesse a pasta do projeto.
|
|
77
|
+
|
|
78
|
+
Em seguida, instale as dependências necessárias utilizando o arquivo `requirements.txt`:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
python -m pip install -r requirements.txt
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
As principais dependências utilizadas pelo projeto são:
|
|
85
|
+
|
|
86
|
+
* `pandas`
|
|
87
|
+
* `matplotlib`
|
|
88
|
+
* `pytest`
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Testes
|
|
93
|
+
|
|
94
|
+
Os testes automatizados estão localizados na pasta `tests/`.
|
|
95
|
+
|
|
96
|
+
Para executar todos os testes, utilize:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
python -m pytest
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Para executar um teste específico:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
python -m pytest tests/test_encode_onehot.py
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Exemplo de uso
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
import pandas as pd
|
|
114
|
+
|
|
115
|
+
from jlprep import check_class_balance
|
|
116
|
+
|
|
117
|
+
df = pd.DataFrame({
|
|
118
|
+
"classe": ["A", "A", "B", "B", "A"]
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
resultado = check_class_balance(
|
|
122
|
+
df,
|
|
123
|
+
target="classe"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
print(resultado)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
A função retorna um dicionário contendo informações sobre a quantidade de classes, o tipo de classificação, o nível de desbalanceamento e uma sugestão de tratamento.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Estrutura do projeto
|
|
134
|
+
|
|
135
|
+
```text
|
|
136
|
+
JlPrep/
|
|
137
|
+
├── jlprep/
|
|
138
|
+
│ ├── __init__.py
|
|
139
|
+
│ ├── cleaning.py
|
|
140
|
+
│ ├── encoding.py
|
|
141
|
+
│ ├── exploration.py
|
|
142
|
+
│ └── quality.py
|
|
143
|
+
│
|
|
144
|
+
├── tests/
|
|
145
|
+
│ ├── test_clean_missing.py
|
|
146
|
+
│ ├── test_encode_label.py
|
|
147
|
+
│ ├── test_encode_onehot.py
|
|
148
|
+
│ ├── test_remove_outliers.py
|
|
149
|
+
│ └── test_check_class_balance.py
|
|
150
|
+
│
|
|
151
|
+
├── examples/
|
|
152
|
+
├── requirements.txt
|
|
153
|
+
└── README.md
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Projeto acadêmico
|
|
159
|
+
|
|
160
|
+
A `jlprep` foi desenvolvida como parte de um Trabalho de Conclusão de Curso, com o objetivo de aplicar conceitos de programação em Python, pré-processamento de dados, qualidade de dados e testes automatizados em uma biblioteca voltada para aplicações de Machine Learning.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
jlprep/__init__.py,sha256=G7F1Pm1CqC9h-0Yz-eDDd1z19iMhWULBtkO-EcUz2MM,1149
|
|
2
|
+
jlprep/cleaning.py,sha256=VH5AwLuJIqOJtKFIZqM1Q0vGNoDxdNv1SKoovcnyJAk,6600
|
|
3
|
+
jlprep/comparison.py,sha256=jFu23h_BCYRQstaFE68nG-dtdcIIb9mJgv0huQV_Vxc,1537
|
|
4
|
+
jlprep/diagnostics.py,sha256=d3qH4YgVd7LKRUerrPTTYBlI-kgC_rYUhxgwfDFON4w,3760
|
|
5
|
+
jlprep/encoding.py,sha256=-cmUGERgsxU-ulSFpZRXilCZqUaGMfml3uv8-Wu_nNI,3347
|
|
6
|
+
jlprep/exploration.py,sha256=4A3sU3XUWDi5P7OMxkjMprNxfbUE8So0gU-LAt52Oc8,8136
|
|
7
|
+
jlprep/quality.py,sha256=XnlzjUZ3uZdCUE1lc44uXYOO8s5zARchY2IeNQNJ0Pc,3517
|
|
8
|
+
jlprep/reporting.py,sha256=l68p7gcX5FdrAszVB89mWu14J_xBFezrWTzwT6rLxtE,8266
|
|
9
|
+
jlprep-0.1.0.dist-info/METADATA,sha256=obtheANuJN54oV-mqL_drF8Q87m0vLxmGXy8DlNLYBc,3962
|
|
10
|
+
jlprep-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
11
|
+
jlprep-0.1.0.dist-info/top_level.txt,sha256=GZfxq8otRhKKyaFna4n68IIDh1P-ISwvT6Eh1jZlfrc,7
|
|
12
|
+
jlprep-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
jlprep
|