Spaces:
Sleeping
Sleeping
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 |