File size: 11,947 Bytes
51fcbfd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
#!/usr/bin/env python3
import json
import logging
from typing import Dict, Any
from pathlib import Path

# Configure logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger("DataFormatUpdater")

# Standard attributes to add to all files
STANDARD_ADDITIONS = {
    "secondary_data": [],
    "variables": {
      "width": 600,
      "height": 600,
      "has_rounded_corners": False,
      "has_shadow": False,
      "has_spacing": False,
      "has_gradient": False,
      "has_stroke": False
    },
    "typography": {
      "title": {
        "font_family": "Arial",
        "font_size": "28px",
        "font_weight": 700
      },
      "description": {
        "font_family": "Arial",
        "font_size": "16px",
        "font_weight": 500
      },
      "label": {
        "font_family": "Arial",
        "font_size": "16px",
        "font_weight": 500
      },
      "annotation": {
        "font_family": "Arial",
        "font_size": "12px",
        "font_weight": 400
      }
    }
}
from typing import Dict, List, Tuple
import re
from datetime import datetime
import logging

logger = logging.getLogger(__name__)

def process_temporal_data(data: Dict) -> None:
    """处理时间类型的数据"""
    for column in data["data"]["columns"]:
        if column["data_type"] == "temporal":
            has_valid_temporal = False
            for row in data["data"]["data"]:
                value = str(row.get(column["name"], ""))
                
                try:
                    if value.isdigit():
                        if len(value) == 4:
                            has_valid_temporal = True
                            continue
                        else:
                            has_valid_temporal = False
                            break
                    
                    if "." in value:
                        parts = value.split(".")
                        if len(parts) == 2 and parts[0].isdigit() and parts[1].isdigit():
                            year, month = parts
                            month = month.zfill(2)
                            row[column["name"]] = f"{year}-{month}"
                            has_valid_temporal = True
                        elif len(parts) == 3 and all(part.isdigit() for part in parts):
                            year, month, day = parts
                            month = month.zfill(2)
                            day = day.zfill(2)
                            row[column["name"]] = f"{year}-{month}-{day}"
                            has_valid_temporal = True
                        else:
                            continue
                        continue
                    
                    if " " in value:
                        try:
                            # 尝试解析完整的月份名称
                            date_obj = datetime.strptime(value, "%B %Y")
                        except ValueError:
                            try:
                                # 尝试解析缩写的月份名称
                                date_obj = datetime.strptime(value, "%b %Y")
                            except ValueError:
                                # 尝试其他常见格式
                                try:
                                    # 处理 "YYYY-MM" 或 "YYYY/MM" 格式
                                    if "-" in value or "/" in value:
                                        separator = "-" if "-" in value else "/"
                                        parts = value.split(separator)
                                        if len(parts) == 2 and parts[0].isdigit() and parts[1].isdigit():
                                            year = parts[0]
                                            month = parts[1].zfill(2)
                                            row[column["name"]] = f"{year}-{month}"
                                            has_valid_temporal = True
                                            continue
                                except Exception:
                                    continue
                                continue
                        
                        # 转换为 "YYYY-MM" 格式
                        row[column["name"]] = date_obj.strftime("%Y-%m")
                        has_valid_temporal = True
                        continue
                        
                except Exception as e:
                    logger.warning(f"Failed to parse temporal value '{value}': {str(e)}")
                    continue
            
            # 如果没有找到任何有效的时间数据,将类型改为categorical
            if not has_valid_temporal:
                column["data_type"] = "categorical"
                data["data"]["type_combination"] = " + ".join([col["data_type"] for col in data["data"]["columns"]])
                logger.info(f"Changed column '{column['name']}' from temporal to categorical due to invalid temporal data")

def process_numerical_data(data: Dict) -> None:
    """处理数值类型的数据"""
    for column in data["data"]["columns"]:
        if column["data_type"] == "numerical":
            for row in data["data"]["data"]:
                value = row.get(column["name"])
                
                # 处理 null 或 None
                if value is None or value == "null" or value == "":
                    row[column["name"]] = 0
                    continue
                
                # 转换为字符串以进行处理
                value_str = str(value)
                
                # 提取数字(包括负号和小数点)
                numeric_chars = re.findall(r'-?\d*\.?\d+', value_str)
                if numeric_chars:
                    # 使用第一个匹配的数字
                    try:
                        row[column["name"]] = float(numeric_chars[0])
                    except ValueError:
                        row[column["name"]] = 0
                else:
                    row[column["name"]] = 0 

def deduplicate_combinations(data: Dict) -> None:
    """检查并去重temporal和categorical属性的组合
    
    Args:
        data: 包含数据的字典,格式为 {"data": {"columns": [...], "data": [...]}}
    """
    # 找出所有temporal和categorical列
    temporal_categorical_cols = [
        col["name"] for col in data["data"]["columns"]
        if col["data_type"] in ["temporal", "categorical"]
    ]
    
    if not temporal_categorical_cols:
        return
    
    # 用于存储已见过的组合
    seen_combinations = set()
    # 用于存储要保留的行索引
    rows_to_keep = []
    
    # 检查每一行
    for idx, row in enumerate(data["data"]["data"]):
        # 获取当前行的temporal和categorical值组合
        combination = tuple(str(row.get(col, "")) for col in temporal_categorical_cols)
        
        # 如果这个组合还没见过,就保留这行
        if combination not in seen_combinations:
            seen_combinations.add(combination)
            rows_to_keep.append(idx)
    
    # 只保留不重复的行
    data["data"]["data"] = [data["data"]["data"][i] for i in rows_to_keep]
    
    # 记录去重信息
    removed_count = len(data["data"]["data"]) - len(rows_to_keep)
    #if removed_count > 0:
    #    logger.info(f"Removed {removed_count} duplicate combinations of temporal/categorical attributes") 
def remove_unnecessary_fields(data: Any) -> Any:
    """
    Recursively remove unnecessary fields from any level of the data structure
    """
    unnecessary_fields = {
        "discarded_data_points",
        "missing_percentage",
        "zero_percentage",
        "transformed_columns"
    }
    
    if isinstance(data, dict):
        return {
            k: remove_unnecessary_fields(v)
            for k, v in data.items()
            if k not in unnecessary_fields
        }
    elif isinstance(data, list):
        return [remove_unnecessary_fields(item) for item in data]
    else:
        return data

def update_data_format(data: Dict[str, Any]) -> Dict[str, Any]:
    """
    Update the data format to match the new requirements
    """
    # First, remove unnecessary fields at all levels
    updated_data = remove_unnecessary_fields(data.copy())
    
    # Extract columns and data from the nested structure
    if "data" in updated_data and "data" in updated_data["data"] and "columns" in updated_data["data"]:
        pass
    else:
        columns = updated_data["columns"]
        data = updated_data["data"]
        updated_data["data"] = {
            "data": data,
            "columns": columns
        }
        del updated_data["columns"]

    try:
        if "title" in updated_data and "description" in updated_data and "main_insight" in updated_data:
            title = updated_data["title"]
            description = updated_data["description"]
            main_insight = updated_data["main_insight"]
            updated_data["metadata"] = {
                "title": title,
                "description": description,
                "main_insight": main_insight
            }
        elif "description" in updated_data and "titles" in updated_data and "main_title" in updated_data["titles"]:
            description = updated_data["description"]
            main_title = updated_data["titles"]["main_title"]
            main_insight = updated_data["metadata"]["main_insight"]
            datafact = updated_data["metadata"]["datafact"]
            updated_data["metadata"] = {
                "title": main_title,
                "description": description,
                "main_insight": main_insight,
                "datafact": datafact
            }
    except Exception as e:
        pass
    
    if "data" in updated_data and "type_combinations" in updated_data["data"]:
        updated_data["data"]["type_combination"] = updated_data["data"]["type_combinations"]
        del updated_data["data"]["type_combinations"]
    # Add standard attributes
    for key, value in STANDARD_ADDITIONS.items():
        if key not in updated_data:
            updated_data[key] = value
    
    return updated_data

def process(input: str, output: str = None) -> None:
    """
    Pipeline入口函数,处理单个文件的数据预处理
    
    Args:
        input (str): 输入JSON文件路径
        output (str): 输出JSON文件路径,如果为None则原地修改输入文件
    """
    try:
        # 如果没有指定输出路径,则原地修改
        if output is None:
            output = input
            
        logger.info(f"处理文件: {input}")
        
        # 检查是否需要处理
        if Path(output).exists():
            with open(output) as f:
                data = json.load(f)
                #if "metadata" in data and "data" in data and "variables" in data and "processed" in data:
                #    logger.info(f"跳过处理: {output} 已包含必要字段")
                #    return
        
        # 读取输入数据
        with open(input, 'r', encoding='utf-8') as f:
            data = json.load(f)
        
        # 更新数据格式
        updated_data = update_data_format(data)
        
        # 处理时间类型数据
        process_temporal_data(updated_data)
        
        # 处理数值类型数据
        process_numerical_data(updated_data)
        
        # 去重temporal和categorical属性的组合
        deduplicate_combinations(updated_data)
        updated_data["processed"] = True
        
        # 保存更新后的数据
        with open(output, 'w', encoding='utf-8') as f:
            json.dump(updated_data, f, indent=2, ensure_ascii=False)
            
        logger.info(f"处理完成: {output}")
            
    except Exception as e:
        logger.error(f"处理失败: {str(e)}")
        raise