Ethscriptions commited on
Commit
cdf0803
·
verified ·
1 Parent(s): f7f0cb9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +161 -425
app.py CHANGED
@@ -1,429 +1,165 @@
1
- import pandas as pd
2
  import streamlit as st
3
- import matplotlib.pyplot as plt
4
- import matplotlib.font_manager as font_manager
5
- from matplotlib.lines import Line2D
6
- import io
7
- import base64
8
- import os
9
- from datetime import datetime, timedelta
10
- from pypinyin import lazy_pinyin, Style
11
- from matplotlib.backends.backend_pdf import PdfPages
12
- import matplotlib.gridspec as gridspec
13
- import math
14
-
15
- # --- 常量定义 ---
16
- SPLIT_TIME = "17:00"
17
- BUSINESS_START = "09:30"
18
- BUSINESS_END = "01:30"
19
- BORDER_COLOR = 'grey'
20
- DATE_COLOR = '#A9A9A9'
21
- A5_WIDTH_IN = 5.83
22
- A5_HEIGHT_IN = 8.27
23
- NUM_COLS = 3
24
-
25
- # --- 字体加载与文本处理函数 ---
26
-
27
- def get_font_regular(size=14):
28
- """加载思源黑体-常规 (SimHei.ttf)"""
29
- font_path = "SimHei.ttf"
30
- if os.path.exists(font_path):
31
- return font_manager.FontProperties(fname=font_path, size=size)
32
- else:
33
- st.warning("警告:未找到字体文件 'SimHei.ttf',LED屏排片表显示可能不正确。")
34
- return font_manager.FontProperties(family='sans-serif', size=size)
35
-
36
- def get_font_bold(size=14):
37
- """加载思源黑体-粗体 (SourceHanSansOLD-Heavy-2.otf)"""
38
- font_path = "SourceHanSansOLD-Heavy-2.otf"
39
- if os.path.exists(font_path):
40
- return font_manager.FontProperties(fname=font_path, size=size)
41
- else:
42
- st.warning("警告:未找到字体文件 'SourceHanSansOLD-Heavy-2.otf',散场时间表显示可能不正确。")
43
- return font_manager.FontProperties(family='sans-serif', size=size, weight='bold')
44
-
45
- def get_pinyin_abbr(text):
46
- """获取中文文本前两个字的拼音首字母"""
47
- if not text:
48
- return ""
49
- chars = [c for c in text if '\u4e00' <= c <= '\u9fff'][:2]
50
- if not chars:
51
- return ""
52
- pinyin_list = lazy_pinyin(chars, style=Style.FIRST_LETTER)
53
- return ''.join(pinyin_list).upper()
54
-
55
- def format_seq(n):
56
- """将数字转换为带圈序号 (①, ②, ③...)"""
57
- if not isinstance(n, int) or n <= 0:
58
- return str(n)
59
- circled_chars = "①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳" \
60
- "㉑㉒㉓㉔㉕㉖㉗㉘㉙㉚㉛㉜㉝㉞㉟" \
61
- "㊱㊲㊳㊴㊵㊶㊷㊸㊹㊺㊻㊼㊽㊾㊿"
62
- if 1 <= n <= 50:
63
- return circled_chars[n - 1]
64
- return f'({n})'
65
-
66
- # --- '放映时间核对表' 处理函数 ---
67
- def process_schedule_led(file):
68
- """处理 '放映时间核对表.xls' 文件"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  try:
70
- date_df = pd.read_excel(file, header=None, skiprows=7, nrows=1, usecols=[3])
71
- date_str = pd.to_datetime(date_df.iloc[0, 0]).strftime('%Y-%m-%d')
72
- base_date = pd.to_datetime(date_str).date()
73
- except Exception:
74
- date_str = datetime.today().strftime('%Y-%m-%d')
75
- base_date = datetime.today().date()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
- try:
78
- df = pd.read_excel(file, header=9, usecols=[1, 2, 4, 5])
79
- df.columns = ['Hall', 'StartTime', 'EndTime', 'Movie']
80
- df['Hall'] = df['Hall'].ffill()
81
- df.dropna(subset=['StartTime', 'EndTime', 'Movie'], inplace=True)
82
- df['Hall'] = df['Hall'].astype(str).str.extract(r'(\d+号)')
83
- df['StartTime_dt'] = pd.to_datetime(df['StartTime'], format='%H:%M', errors='coerce').apply(
84
- lambda t: t.replace(year=base_date.year, month=base_date.month, day=base_date.day) if pd.notnull(t) else t)
85
- df['EndTime_dt'] = pd.to_datetime(df['EndTime'], format='%H:%M', errors='coerce').apply(
86
- lambda t: t.replace(year=base_date.year, month=base_date.month, day=base_date.day) if pd.notnull(t) else t)
87
- df.loc[df['EndTime_dt'] < df['StartTime_dt'], 'EndTime_dt'] += timedelta(days=1)
88
- df = df.sort_values(['Hall', 'StartTime_dt'])
89
- merged_rows = []
90
- for _, group in df.groupby('Hall'):
91
- current = None
92
- for _, row in group.sort_values('StartTime_dt').iterrows():
93
- if current is None:
94
- current = row.copy()
95
- elif row['Movie'] == current['Movie']:
96
- current['EndTime_dt'] = row['EndTime_dt']
97
- else:
98
- merged_rows.append(current)
99
- current = row.copy()
100
- if current is not None:
101
- merged_rows.append(current)
102
- merged_df = pd.DataFrame(merged_rows)
103
- merged_df['StartTime_dt'] -= timedelta(minutes=10)
104
- merged_df['EndTime_dt'] -= timedelta(minutes=5)
105
- merged_df['Seq'] = merged_df.groupby('Hall').cumcount() + 1
106
- merged_df['StartTime_str'] = merged_df['StartTime_dt'].dt.strftime('%H:%M')
107
- merged_df['EndTime_str'] = merged_df['EndTime_dt'].dt.strftime('%H:%M')
108
- return merged_df[['Hall', 'Seq', 'Movie', 'StartTime_str', 'EndTime_str']], date_str
109
- except Exception as e:
110
- st.error(f"处理数据出错: {e}。请检查文件格式是否正确。")
111
- return None, date_str
112
-
113
- def create_print_layout_led(data, date_str):
114
- """为 '放映时间核对表' 生成打印布局 (使用 Regular 字体)"""
115
- if data is None or data.empty:
116
- return None
117
- A4_width_in, A4_height_in = 8.27, 11.69
118
- dpi = 300
119
- total_content_rows = len(data)
120
-
121
- # --- 已修改部分:如果数据条数小于25条,则按25条计算行高以防止字体过大 ---
122
- layout_rows = max(total_content_rows, 25)
123
- totalA = layout_rows + 2
124
- # --- 修改结束 ---
125
-
126
- row_height = A4_height_in / totalA
127
- data = data.reset_index(drop=True)
128
- data['hall_str'] = '$' + data['Hall'].str.replace('号', '') + '^{\#}$'
129
- data['seq_str'] = data['Seq'].apply(format_seq)
130
- data['pinyin_abbr'] = data['Movie'].apply(get_pinyin_abbr)
131
- data['time_str'] = data['StartTime_str'] + ' - ' + data['EndTime_str']
132
-
133
- temp_fig = plt.figure(figsize=(A4_width_in, A4_height_in), dpi=dpi)
134
- renderer = temp_fig.canvas.get_renderer()
135
- base_font_size_pt = (row_height * 0.9) * 72
136
- seq_font_size_pt = (row_height * 0.5) * 72
137
-
138
- def get_col_width_in(series, font_size_pt, is_math=False):
139
- if series.empty: return 0
140
- font_prop = get_font_regular(font_size_pt) # 使用 Regular 字体
141
- longest_str_idx = series.astype(str).str.len().idxmax()
142
- max_content = str(series.loc[longest_str_idx])
143
- text_width_px, _, _ = renderer.get_text_width_height_descent(max_content, font_prop, ismath=is_math)
144
- return (text_width_px / dpi) * 1.1
145
-
146
- margin_col_width = row_height
147
- hall_col_width = get_col_width_in(data['hall_str'], base_font_size_pt, is_math=True)
148
- seq_col_width = get_col_width_in(data['seq_str'], seq_font_size_pt)
149
- pinyin_col_width = get_col_width_in(data['pinyin_abbr'], base_font_size_pt)
150
- time_col_width = get_col_width_in(data['time_str'], base_font_size_pt)
151
- movie_col_width = A4_width_in - (
152
- margin_col_width * 2 + hall_col_width + seq_col_width + pinyin_col_width + time_col_width)
153
- plt.close(temp_fig)
154
-
155
- col_widths = {'hall': hall_col_width, 'seq': seq_col_width, 'movie': movie_col_width, 'pinyin': pinyin_col_width,
156
- 'time': time_col_width}
157
- col_x_starts = {}
158
- current_x = margin_col_width
159
- for col_name in ['hall', 'seq', 'movie', 'pinyin', 'time']:
160
- col_x_starts[col_name] = current_x
161
- current_x += col_widths[col_name]
162
-
163
- def draw_figure(fig, ax):
164
- renderer = fig.canvas.get_renderer()
165
- for col_name in ['hall', 'seq', 'movie', 'pinyin']:
166
- x_line = col_x_starts[col_name] + col_widths[col_name]
167
- line_top_y, line_bottom_y = A4_height_in - row_height, row_height
168
- ax.add_line(Line2D([x_line, x_line], [line_bottom_y, line_top_y], color='gray', linestyle=':', linewidth=0.5))
169
-
170
- last_hall_drawn = None
171
- for i, row in data.iterrows():
172
- y_bottom = A4_height_in - (i + 2) * row_height
173
- y_center = y_bottom + row_height / 2
174
-
175
- if row['Hall'] != last_hall_drawn:
176
- ax.text(col_x_starts['hall'] + col_widths['hall'] / 2, y_center, row['hall_str'],
177
- fontproperties=get_font_regular(base_font_size_pt), ha='center', va='center')
178
- last_hall_drawn = row['Hall']
179
-
180
- ax.text(col_x_starts['seq'] + col_widths['seq'] / 2, y_center, row['seq_str'],
181
- fontproperties=get_font_regular(seq_font_size_pt), ha='center', va='center')
182
- ax.text(col_x_starts['pinyin'] + col_widths['pinyin'] / 2, y_center, row['pinyin_abbr'],
183
- fontproperties=get_font_regular(base_font_size_pt), ha='center', va='center')
184
- ax.text(col_x_starts['time'] + col_widths['time'] / 2, y_center, row['time_str'],
185
- fontproperties=get_font_regular(base_font_size_pt), ha='center', va='center')
186
-
187
- movie_font_size = base_font_size_pt
188
- movie_font_prop = get_font_regular(movie_font_size)
189
- text_w_px, _, _ = renderer.get_text_width_height_descent(row['Movie'], movie_font_prop, ismath=False)
190
- text_w_in = text_w_px / dpi
191
- max_width_in = col_widths['movie'] * 0.9
192
-
193
- if text_w_in > max_width_in:
194
- movie_font_size *= (max_width_in / text_w_in)
195
- movie_font_prop = get_font_regular(movie_font_size)
196
-
197
- ax.text(col_x_starts['movie'] + 0.05, y_center, row['Movie'], fontproperties=movie_font_prop, ha='left', va='center')
198
-
199
- is_last_in_hall = (i == len(data) - 1) or (row['Hall'] != data.loc[i + 1, 'Hall'])
200
- line_start_x = margin_col_width
201
- line_end_x = A4_width_in - margin_col_width
202
- if is_last_in_hall:
203
- ax.add_line(Line2D([line_start_x, line_end_x], [y_bottom, y_bottom], color='black', linestyle='-', linewidth=0.8))
204
- else:
205
- ax.add_line(Line2D([line_start_x, line_end_x], [y_bottom, y_bottom], color='gray', linestyle=':', linewidth=0.5))
206
-
207
- outputs = {}
208
- for format_type in ['png', 'pdf']:
209
- fig = plt.figure(figsize=(A4_width_in, A4_height_in), dpi=dpi)
210
- ax = fig.add_axes([0, 0, 1, 1])
211
- ax.set_axis_off()
212
- ax.set_xlim(0, A4_width_in)
213
- ax.set_ylim(0, A4_height_in)
214
- ax.text(margin_col_width, A4_height_in - (row_height / 2), date_str,
215
- fontproperties=get_font_regular(10), color='#A9A9A9', ha='left', va='center')
216
-
217
- draw_figure(fig, ax)
218
-
219
- buf = io.BytesIO()
220
- fig.savefig(buf, format=format_type, dpi=dpi, bbox_inches='tight', pad_inches=0)
221
- buf.seek(0)
222
- data_uri = base64.b64encode(buf.getvalue()).decode()
223
- mime_type = 'image/png' if format_type == 'png' else 'application/pdf'
224
- outputs[format_type] = f"data:{mime_type};base64,{data_uri}"
225
- plt.close(fig)
226
-
227
- return outputs
228
-
229
- # --- '放映场次核对表' 处理函数 ---
230
- def process_schedule_times(file):
231
- """处理 '放映场次核对表.xls' 文件"""
232
- try:
233
- df = pd.read_excel(file, skiprows=8)
234
- df = df.iloc[:, [6, 7, 9]]
235
- df.columns = ['Hall', 'StartTime', 'EndTime']
236
- df = df.dropna(subset=['Hall', 'StartTime', 'EndTime'])
237
- df['Hall'] = df['Hall'].str.extract(r'(\d+)号').astype(str) + ' '
238
- base_date = datetime.today().date()
239
- df['StartTime'] = pd.to_datetime(df['StartTime'])
240
- df['EndTime'] = pd.to_datetime(df['EndTime'])
241
-
242
- business_start = datetime.strptime(f"{base_date} {BUSINESS_START}", "%Y-%m-%d %H:%M")
243
- business_end = datetime.strptime(f"{base_date} {BUSINESS_END}", "%Y-%m-%d %H:%M")
244
- if business_end < business_start:
245
- business_end += timedelta(days=1)
246
-
247
- for idx, row in df.iterrows():
248
- end_time = row['EndTime']
249
- if end_time.hour < 9:
250
- df.at[idx, 'EndTime'] = end_time + timedelta(days=1)
251
- if row['StartTime'].hour >= 21 and end_time.hour < 9:
252
- df.at[idx, 'EndTime'] = end_time + timedelta(days=1)
253
-
254
- df['time_for_comparison'] = df['EndTime'].apply(lambda x: datetime.combine(base_date, x.time()))
255
- df.loc[df['time_for_comparison'].dt.hour < 9, 'time_for_comparison'] += timedelta(days=1)
256
-
257
- valid_times = (((df['time_for_comparison'] >= datetime.combine(base_date, business_start.time())) &
258
- (df['time_for_comparison'] <= datetime.combine(base_date + timedelta(days=1), business_end.time()))))
259
- df = df[valid_times]
260
-
261
- df = df.sort_values('EndTime')
262
- split_time = datetime.strptime(f"{base_date} {SPLIT_TIME}", "%Y-%m-%d %H:%M")
263
- split_time_for_comparison = df['time_for_comparison'].apply(lambda x: datetime.combine(base_date, split_time.time()))
264
-
265
- part1 = df[df['time_for_comparison'] <= split_time_for_comparison].copy()
266
- part2 = df[df['time_for_comparison'] > split_time_for_comparison].copy()
267
-
268
- for part in [part1, part2]:
269
- part['EndTime'] = part['EndTime'].dt.strftime('%-H:%M')
270
-
271
- date_df = pd.read_excel(file, skiprows=5, nrows=1, usecols=[2], header=None)
272
- date_cell = date_df.iloc[0, 0]
273
- try:
274
- if isinstance(date_cell, str):
275
- date_str = datetime.strptime(date_cell, '%Y-%m-%d').strftime('%Y-%m-%d')
276
- else:
277
- date_str = pd.to_datetime(date_cell).strftime('%Y-%m-%d')
278
- except:
279
- date_str = datetime.today().strftime('%Y-%m-%d')
280
-
281
- return part1[['Hall', 'EndTime']], part2[['Hall', 'EndTime']], date_str
282
  except Exception as e:
283
- st.error(f"处理文件出错: {str(e)}")
284
- return None, None, None
285
-
286
- def create_print_layout_times(data, title, date_str):
287
- """为 '放映场次核对表' 生成打印布局 (使用 Bold 字体)"""
288
- if data.empty:
289
- return None
290
-
291
- def generate_figure():
292
- total_items = len(data)
293
- num_rows = math.ceil(total_items / NUM_COLS) if total_items > 0 else 1
294
- date_header_height_in = 0.3
295
- data_area_height_in = A5_HEIGHT_IN - date_header_height_in
296
- cell_width_in = A5_WIDTH_IN / NUM_COLS
297
- cell_height_in = data_area_height_in / num_rows
298
- cell_width_pt = cell_width_in * 72
299
- cell_height_pt = cell_height_in * 72
300
- target_text_width_pt = cell_width_pt * 0.9
301
- fontsize_from_width = target_text_width_pt / (8 * 0.6)
302
- fontsize_from_height = cell_height_pt * 0.8
303
- base_fontsize = min(fontsize_from_width, fontsize_from_height)
304
-
305
- fig = plt.figure(figsize=(A5_WIDTH_IN, A5_HEIGHT_IN), dpi=300)
306
- fig.subplots_adjust(left=0, right=1, top=1, bottom=0)
307
-
308
- gs = gridspec.GridSpec(
309
- num_rows + 1, NUM_COLS,
310
- hspace=0, wspace=0,
311
- height_ratios=[date_header_height_in] + [cell_height_in] * num_rows,
312
- figure=fig)
313
-
314
- data_values = data.values.tolist()
315
- while len(data_values) % NUM_COLS != 0:
316
- data_values.append(['', ''])
317
-
318
- rows_per_col_layout = math.ceil(len(data_values) / NUM_COLS)
319
- sorted_data = [['', '']] * len(data_values)
320
- for i, item in enumerate(data_values):
321
- if item[0] and item[1]:
322
- row_in_col = i % rows_per_col_layout
323
- col_idx = i // rows_per_col_layout
324
- new_index = row_in_col * NUM_COLS + col_idx
325
- if new_index < len(sorted_data):
326
- sorted_data[new_index] = item
327
-
328
- for idx, (hall, end_time) in enumerate(sorted_data):
329
- if hall and end_time:
330
- row_grid = idx // NUM_COLS + 1
331
- col_grid = idx % NUM_COLS
332
- ax = fig.add_subplot(gs[row_grid, col_grid])
333
- for spine in ax.spines.values():
334
- spine.set_visible(True)
335
- spine.set_linestyle((0, (1, 2)))
336
- spine.set_color(BORDER_COLOR)
337
- spine.set_linewidth(0.75)
338
-
339
- display_text = f"{hall}{end_time}"
340
- ax.text(0.5, 0.5, display_text,
341
- fontproperties=get_font_bold(base_fontsize), # 使用 Bold 字体
342
- ha='center', va='center',
343
- transform=ax.transAxes)
344
- ax.set_xticks([])
345
- ax.set_yticks([])
346
- ax.set_facecolor('none')
347
-
348
- ax_date = fig.add_subplot(gs[0, :])
349
- ax_date.text(0.01, 0.5, f"{date_str} {title}",
350
- fontproperties=get_font_bold(base_fontsize * 0.5), # 使用 Bold 字体
351
- color=DATE_COLOR,
352
- ha='left', va='center',
353
- transform=ax_date.transAxes)
354
- ax_date.set_axis_off()
355
- ax_date.set_facecolor('none')
356
-
357
- return fig
358
-
359
- fig_for_output = generate_figure()
360
- png_buffer = io.BytesIO()
361
- fig_for_output.savefig(png_buffer, format='png')
362
- png_buffer.seek(0)
363
- png_base64 = base64.b64encode(png_buffer.getvalue()).decode()
364
-
365
- pdf_buffer = io.BytesIO()
366
- with PdfPages(pdf_buffer) as pdf:
367
- pdf.savefig(fig_for_output)
368
- pdf_buffer.seek(0)
369
- pdf_base64 = base64.b64encode(pdf_buffer.getvalue()).decode()
370
-
371
- plt.close(fig_for_output)
372
- return {'png': f'data:image/png;base64,{png_base64}', 'pdf': f'data:application/pdf;base64,{pdf_base64}'}
373
-
374
- def display_pdf(base64_pdf):
375
- """在Streamlit中嵌入显示PDF"""
376
- return f'<iframe src="{base64_pdf}" width="100%" height="800" type="application/pdf"></iframe>'
377
-
378
- # --- Streamlit 主程序 ---
379
- st.set_page_config(page_title="影院排期打印工具", layout="wide")
380
- st.title("影院排期打印工具")
381
-
382
- uploaded_file = st.file_uploader("请上传 '放映时间核对表.xls' 或 '放映场次核对表.xls'",
383
- type=["xls", "xlsx"])
384
-
385
- if uploaded_file:
386
- # 根据文件名中的关键字判断使用哪个处理流程
387
- if "时间" in uploaded_file.name:
388
- st.header("LED屏排片表")
389
- with st.spinner("正在处理文件,请稍候..."):
390
- schedule, date_str = process_schedule_led(uploaded_file)
391
- if schedule is not None and not schedule.empty:
392
- output = create_print_layout_led(schedule, date_str)
393
- tab1, tab2 = st.tabs(["PDF 预览", "图片预览 (PNG)"])
394
- with tab1:
395
- st.markdown(display_pdf(output['pdf']), unsafe_allow_html=True)
396
- with tab2:
397
- st.image(output['png'], use_container_width=True)
398
- else:
399
- st.error("无法处理文件。请检查文件内容和格式是否正确。")
400
-
401
- elif "场次" in uploaded_file.name:
402
- st.header("散场时间快捷打印")
403
- part1, part2, date_str = process_schedule_times(uploaded_file)
404
- if part1 is not None and part2 is not None:
405
- part1_output = create_print_layout_times(part1, "A", date_str)
406
- part2_output = create_print_layout_times(part2, "C", date_str)
407
-
408
- col1, col2 = st.columns(2)
409
- with col1:
410
- st.subheader("白班 (散场时间 ≤ 17:00)")
411
- if part1_output:
412
- tab1_1, tab1_2 = st.tabs(["PDF 预览 ", "图片预览 (PNG) "])
413
- with tab1_1:
414
- st.markdown(display_pdf(part1_output['pdf']), unsafe_allow_html=True)
415
- with tab1_2:
416
- st.image(part1_output['png'])
417
- else:
418
- st.info("白班没有排期数据。")
419
-
420
- with col2:
421
- st.subheader("晚班 (散场时间 > 17:00)")
422
- if part2_output:
423
- tab2_1, tab2_2 = st.tabs(["PDF 预览 ", "图片预览 (PNG) "])
424
- with tab2_1:
425
- st.markdown(display_pdf(part2_output['pdf']), unsafe_allow_html=True)
426
- with tab2_2:
427
- st.image(part2_output['png'])
428
- else:
429
- st.info("晚班没有排期数据。")
 
 
1
  import streamlit as st
2
+ import pandas as pd
3
+ import numpy as np
4
+
5
+ # 设置页面布局为宽屏模式,并设置页面标题
6
+ st.set_page_config(layout="wide", page_title="影城效率分析 - 最终版")
7
+
8
+
9
+ def clean_movie_title(title):
10
+ """
11
+ 清理并规范化电影标题,移除版本、语言等标识以便合并统计。
12
+ """
13
+ if not isinstance(title, str):
14
+ return title
15
+ suffixes_to_remove = [
16
+ '2D', '3D', '4D', '4K', 'IMAX', 'CINITY', '杜比', '巨幕',
17
+ '国语', '英语', '粤语', '日语', '原版', '修复版',
18
+ '(国)', '(英)', '(粤)'
19
+ ]
20
+ parts = title.split()
21
+ cleaned_parts = [p for p in parts if p.upper() not in [s.upper() for s in suffixes_to_remove]]
22
+ if not cleaned_parts:
23
+ return title
24
+ return ' '.join(cleaned_parts).strip()
25
+
26
+
27
+ def style_efficiency(row):
28
+ """
29
+ 根据效率值高亮特定行。
30
+ 如果座次效率或场次效率 < 0.5 或 > 1.5,则高亮为淡黄色。
31
+ """
32
+ highlight = 'background-color: #FFFFE0;'
33
+ default = ''
34
+ seat_efficiency = row.get('座次效率', 0)
35
+ session_efficiency = row.get('场次效率', 0)
36
+ if (seat_efficiency < 0.5 or seat_efficiency > 1.5 or
37
+ session_efficiency < 0.5 or session_efficiency > 1.5):
38
+ return [highlight] * len(row)
39
+ return [default] * len(row)
40
+
41
+
42
+ def process_and_analyze_data(df):
43
+ """
44
+ 核心数据处理与分析函数。
45
+ """
46
+ if df.empty:
47
+ return pd.DataFrame()
48
+
49
+ analysis_df = df.groupby('影片名称_清理后').agg(
50
+ 座位数=('座位数', 'sum'),
51
+ 场次=('影片名称_清理后', 'size'),
52
+ 票房=('总收入', 'sum'),
53
+ 人次=('总人次', 'sum')
54
+ ).reset_index()
55
+ analysis_df.rename(columns={'影片名称_清理后': '影片'}, inplace=True)
56
+
57
+ analysis_df = analysis_df.sort_values(by='票房', ascending=False).reset_index(drop=True)
58
+
59
+ total_seats = analysis_df['座位数'].sum()
60
+ total_sessions = analysis_df['场次'].sum()
61
+ total_revenue = analysis_df['票房'].sum()
62
+
63
+ analysis_df['均价'] = np.divide(analysis_df['票房'], analysis_df['人次']).fillna(0)
64
+ analysis_df['座次比'] = np.divide(analysis_df['座位数'], total_seats).fillna(0)
65
+ analysis_df['场次比'] = np.divide(analysis_df['场次'], total_sessions).fillna(0)
66
+ analysis_df['票房比'] = np.divide(analysis_df['票房'], total_revenue).fillna(0)
67
+ analysis_df['座次效率'] = np.divide(analysis_df['票房比'], analysis_df['座次比']).fillna(0)
68
+ analysis_df['场次效率'] = np.divide(analysis_df['票房比'], analysis_df['场次比']).fillna(0)
69
+
70
+ # **优化1:移除“序号”列的定义**
71
+ final_columns = [
72
+ '影片', '座位数', '场次', '票房', '人次', '均价',
73
+ '座次比', '场次比', '票房比', '座次效率', '场次效率'
74
+ ]
75
+ analysis_df = analysis_df[final_columns]
76
+
77
+ return analysis_df
78
+
79
+
80
+ # --- Streamlit 用户界面 ---
81
+
82
+ st.title('排片效率分析')
83
+ st.write("上传 `影片映出日累计报表.xlsx` 文件。")
84
+
85
+ uploaded_file = st.file_uploader("请在此处上传 Excel 文件", type=['xlsx', 'xls', 'csv'])
86
+
87
+ if uploaded_file is not None:
88
  try:
89
+ df = pd.read_excel(uploaded_file, skiprows=3, header=None)
90
+
91
+ df.rename(columns={
92
+ 0: '影片名称', 2: '放映时间', 5: '总人次', 6: '总收入', 7: '座位数'
93
+ }, inplace=True)
94
+
95
+ required_cols = ['影片名称', '放映时间', '座位数', '总收入', '总人次']
96
+ df = df[required_cols]
97
+ df.dropna(subset=['影片名称', '放映时间'], inplace=True)
98
+
99
+ for col in ['座位数', '总收入', '总人次']:
100
+ df[col] = pd.to_numeric(df[col], errors='coerce').fillna(0)
101
+
102
+ df['放映时间'] = pd.to_datetime(df['放映时间'], format='%H:%M:%S', errors='coerce').dt.time
103
+ df.dropna(subset=['放映时间'], inplace=True)
104
+
105
+ df['影片名称_清理后'] = df['影片名称'].apply(clean_movie_title)
106
+
107
+ st.toast("文件上传成功,数据已按规则处理!", icon="🎉")
108
+
109
+ format_config = {
110
+ '座位数': '{:,.0f}', '场次': '{:,.0f}', '人次': '{:,.0f}',
111
+ '票房': '{:,.2f}', '均价': '{:.2f}', '座次比': '{:.2%}', '场次比': '{:.2%}',
112
+ '票房比': '{:.2%}', '座次效率': '{:.2f}', '场次效率': '{:.2f}',
113
+ }
114
+
115
+ # --- 1. 全天数据分析 ---
116
+ st.header("全天排片效率分析")
117
+
118
+ full_day_analysis = process_and_analyze_data(df.copy())
119
+
120
+ if not full_day_analysis.empty:
121
+ table_height = (len(full_day_analysis) + 1) * 35 + 3
122
+ # **优化2:使用 .hide(axis="index") 隐藏默认序号列**
123
+ st.dataframe(
124
+ full_day_analysis.style.format(format_config).apply(style_efficiency, axis=1).hide(axis="index"),
125
+ height=table_height,
126
+ use_container_width=True
127
+ )
128
+ else:
129
+ st.warning("全天数据不足,无法生成分析报告。")
130
+
131
+ # --- 2. 黄金时段数据分析 ---
132
+ st.header("黄金时段 (14:00 - 21:00) 排片效率分析")
133
+
134
+ start_time = pd.to_datetime('14:00:00').time()
135
+ end_time = pd.to_datetime('21:00:00').time()
136
+ prime_time_df = df[df['放映时间'].between(start_time, end_time)]
137
+
138
+ prime_time_analysis = process_and_analyze_data(prime_time_df.copy())
139
+
140
+ if not prime_time_analysis.empty:
141
+ table_height_prime = (len(prime_time_analysis) + 1) * 35 + 3
142
+ # **优化2:同样隐藏黄金时段表格的默认序号**
143
+ st.dataframe(
144
+ prime_time_analysis.style.format(format_config).apply(style_efficiency, axis=1).hide(axis="index"),
145
+ height=table_height_prime,
146
+ use_container_width=True
147
+ )
148
+ else:
149
+ st.warning("黄金时段内没有有效场次数据,无法生成分析报告。")
150
+
151
+ # --- 3. 一键复制影片列表 ---
152
+ if not full_day_analysis.empty:
153
+ st.header("复制当日影片列表")
154
+
155
+ movie_titles = full_day_analysis['影片'].tolist()
156
+ formatted_titles = ''.join([f'《{title}》' for title in movie_titles])
157
+
158
+ st.code(formatted_titles, language='text')
159
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  except Exception as e:
161
+ st.error(f"处理文件时出错: {e}")
162
+ st.warning("请确保上传的文件是'影片映出日累计报表.xlsx',并且格式正确。")
163
+
164
+ else:
165
+ st.info("请上传文件以开始分析。")