-
Notifications
You must be signed in to change notification settings - Fork 108
Expand file tree
/
Copy pathanalysis_02_timing.py
More file actions
178 lines (151 loc) · 6.56 KB
/
Copy pathanalysis_02_timing.py
File metadata and controls
178 lines (151 loc) · 6.56 KB
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
#!/usr/bin/env python3
"""
川普密碼 分析 #2 — 發文時間規律
什麼時候發?頻率突變前後發生了什麼?
"""
import json
from collections import Counter, defaultdict
from datetime import datetime, timezone
from pathlib import Path
from statistics import median
from utils import est_hour
BASE = Path(__file__).parent
DATA = BASE / "data"
def main():
with open(BASE / "clean_president.json", 'r', encoding='utf-8') as f:
posts = json.load(f)
originals = [p for p in posts if p['has_text'] and not p['is_retweet']]
print("=" * 70)
print("⏰ 分析 #2: 發文時間規律")
print(f" 分析對象: 就任後原創貼文 {len(originals)} 篇")
print("=" * 70)
# --- 1. 每小時分布(UTC → EST 轉換,Trump 在東岸) ---
hour_dist = Counter()
hour_by_month = defaultdict(Counter)
for p in originals:
est_h, _ = est_hour(p['created_at'])
hour_dist[est_h] += 1
month = p['created_at'][:7]
hour_by_month[month][est_h] += 1
print(f"\n🕐 發文時段分布 (美東時間 EST):")
print("-" * 60)
max_count = max(hour_dist.values())
for h in range(24):
count = hour_dist.get(h, 0)
bar = '█' * int(count / max_count * 40) if max_count > 0 else ''
period = "🌙" if h < 6 else ("☀️" if h < 12 else ("🌅" if h < 18 else "🌙"))
print(f" {h:02d}:00 {period} {count:4d} {bar}")
# 深夜推文 (12am-5am EST)
night_posts = [p for p in originals if est_hour(p['created_at'])[0] < 5]
print(f"\n🌙 深夜推文 (12am-5am EST): {len(night_posts)} 篇 ({len(night_posts)/len(originals)*100:.1f}%)")
if night_posts:
print(" 最近 5 篇深夜推文:")
for p in night_posts[:5]:
est_h, est_m = est_hour(p['created_at'])
print(f" {p['created_at'][:16]} (EST {est_h}:{est_m:02d}) | {p['content'][:80]}...")
# --- 2. 每日發文量 ---
print(f"\n📅 每日發文量分布:")
print("-" * 60)
daily = Counter()
for p in originals:
daily[p['created_at'][:10]] += 1
counts = sorted(daily.values())
avg_daily = sum(counts) / len(counts)
# P4-1: 空值防護 + P3-8: 中位數改用 statistics.median
median_count = median(counts) if counts else 0
print(f" 平均每天: {avg_daily:.1f} 篇")
if counts:
print(f" 最少: {counts[0]} 篇")
print(f" 最多: {counts[-1]} 篇")
print(f" 中位數: {median_count} 篇")
# Top 10 最多發文的日子
print(f"\n🔥 Top 10 發文最多的日子:")
for date, count in daily.most_common(10):
# 那天的第一篇看看主題
day_posts = [p for p in originals if p['created_at'][:10] == date]
topic = day_posts[0]['content'][:60] if day_posts else ''
bar = '█' * count
print(f" {date} | {count:3d}篇 | {bar} | {topic}...")
# 沉默日(0 篇或極少篇)
print(f"\n🤫 沉默日(≤2 篇):")
all_dates = sorted(daily.keys())
quiet_days = [(d, daily[d]) for d in all_dates if daily[d] <= 2]
print(f" 總計 {len(quiet_days)} 天")
for d, c in quiet_days[-10:]:
print(f" {d} | {c} 篇")
# --- 3. 星期分布 ---
print(f"\n📊 星期分布:")
print("-" * 60)
weekday_dist = Counter()
weekday_names = ['一', '二', '三', '四', '五', '六', '日']
for p in originals:
dt = datetime.fromisoformat(p['created_at'].replace('Z', '+00:00'))
weekday_dist[dt.weekday()] += 1
for wd in range(7):
count = weekday_dist.get(wd, 0)
bar = '█' * int(count / max(weekday_dist.values()) * 30)
print(f" 週{weekday_names[wd]} | {count:4d} {bar}")
# --- 4. 發文間隔分析 ---
print(f"\n⏱️ 發文間隔分析:")
print("-" * 60)
intervals = []
sorted_posts = sorted(originals, key=lambda p: p['created_at'])
for i in range(1, len(sorted_posts)):
dt1 = datetime.fromisoformat(sorted_posts[i-1]['created_at'].replace('Z', '+00:00'))
dt2 = datetime.fromisoformat(sorted_posts[i]['created_at'].replace('Z', '+00:00'))
diff_minutes = (dt2 - dt1).total_seconds() / 60
intervals.append({
'minutes': diff_minutes,
'date': sorted_posts[i]['created_at'][:16],
'content': sorted_posts[i]['content'][:60]
})
intervals_min = sorted([i['minutes'] for i in intervals])
# P4-1: 空值防護
if not intervals_min:
print(" 無間隔資料")
return
print(f" 最短間隔: {intervals_min[0]:.0f} 分鐘")
print(f" 最長間隔: {intervals_min[-1]:.0f} 分鐘 ({intervals_min[-1]/60:.1f} 小時)")
print(f" 平均間隔: {sum(intervals_min)/len(intervals_min):.0f} 分鐘")
# P3-8: 用 statistics.median
print(f" 中位數間隔: {median(intervals_min):.0f} 分鐘")
# 連續轟炸(5分鐘內連發多篇)
bursts = [i for i in intervals if i['minutes'] < 5]
print(f"\n🔥 連續轟炸(5分鐘內連發): {len(bursts)} 次")
# 長沉默後的第一篇(沉默 > 12小時)
long_silence = [i for i in intervals if i['minutes'] > 720]
print(f"\n😶 長時間沉默(>12小時)後的第一篇: {len(long_silence)} 次")
for item in long_silence[:10]:
hours = item['minutes'] / 60
print(f" 沉默 {hours:.1f}h → {item['date']} | {item['content']}...")
# --- 5. 發文量趨勢(每週) ---
print(f"\n📈 每週發文量趨勢:")
print("-" * 60)
weekly = defaultdict(int)
for p in originals:
dt = datetime.fromisoformat(p['created_at'].replace('Z', '+00:00'))
# ISO week
year, week, _ = dt.isocalendar()
key = f"{year}-W{week:02d}"
weekly[key] += 1
weeks = sorted(weekly.keys())
for w in weeks[-16:]: # 最近 16 週
count = weekly[w]
bar = '█' * (count // 2)
print(f" {w} | {count:3d} {bar}")
# 存結果
results = {
'hourly_distribution_est': dict(hour_dist),
'daily_counts': dict(daily.most_common()),
'weekday_distribution': {weekday_names[k]: v for k, v in weekday_dist.items()},
'night_posts_count': len(night_posts),
'burst_count': len(bursts),
'long_silence_count': len(long_silence),
'avg_daily': round(avg_daily, 1),
'avg_interval_minutes': round(sum(intervals_min)/len(intervals_min), 0),
}
with open(DATA / 'results_02_timing.json', 'w', encoding='utf-8') as f:
json.dump(results, f, ensure_ascii=False, indent=2)
print(f"\n💾 詳細結果存入 results_02_timing.json")
if __name__ == '__main__':
main()