python pandas - 航班到达和起飞时间在单独的行 - 匹配并加入同一行然后绘制甘特图
python pandas - flight arrival and departure time on separate row - to be matched and joined to same row then draw gantt chart
假设我有一个这样的数据框(笔记本文本版本如下图):
A为Arrival Flight(着陆),D为Departure flight(起飞)。
Carrier 和 FltReg 一起是一架飞机......到达和离开机场,它将再次返回同一机场......几小时或几天后。
Acft是飞机的类型。
到达和离开需要匹配,以便生成的数据框可用于计算和绘制甘特图(开始时间即到达时间和结束时间即出发时间......航班在地面上的时间.)
数据通常会持续 7 天的航班时刻表和更多航空公司。7 天大约 3000 行...来自 sql 服务器数据库
from io import StringIO
import pandas as pd
dfstr = StringIO(u"""
ID;Car;FltNo;Acft;FltReg;E_FltType;Rtg;STADDtTm;ArrDep
0;EK;376;77W;A6ECI;T/A;DXB-BKK-DXB;03/05/2017 12:50;A
1;EK;377;77W;A6ECI;T/A;DXB-BKK-DXB;03/05/2017 15:40;D
2;EK;384;380;A6EDL;T/S;DXB-BKK-HKG;02/05/2017 12:15;A
3;EK;384;380;A6EDL;T/S;DXB-BKK-HKG;02/05/2017 14:00;D
4;EK;385;380;A6EDL;T/A;HKG-BKK-DXB;02/05/2017 23:45;A
5;EK;385;380;A6EDL;T/A;HKG-BKK-DXB;03/05/2017 01:15;D
54;VZ;920;320;HSVKA;DEP ONLY;BKK-HPH;01/05/2017 11:15;D
55;VZ;921;320;HSVKA;ARR ONLY;HPH-BKK;01/05/2017 15:25;A
56;VZ;602;320;HSVKA;DEP ONLY;BKK-CNX;01/05/2017 16:35;D
57;VZ;603;320;HSVKA;ARR ONLY;CNX-BKK;01/05/2017 19:45;A
58;VZ;602;320;HSVKA;DEP ONLY;BKK-CNX;02/05/2017 11:15;D
59;VZ;603;320;HSVKA;ARR ONLY;CNX-BKK;02/05/2017 14:25;A
60;VZ;820;320;HSVKA;DEP ONLY;BKK-HKT;03/05/2017 07:05;D
61;VZ;821;320;HSVKA;ARR ONLY;HKT-BKK;03/05/2017 15:45;A
62;VZ;828;320;HSVKA;DEP ONLY;BKK-HKT;03/05/2017 18:20;D
63;VZ;829;320;HSVKA;ARR ONLY;HKT-BKK;03/05/2017 21:50;A
64;VZ;600;320;HSVKB;DEP ONLY;BKK-CNX;01/05/2017 06:10;D
65;VZ;601;320;HSVKB;ARR ONLY;CNX-BKK;01/05/2017 09:20;A
66;VZ;606;320;HSVKB;DEP ONLY;BKK-CNX;01/05/2017 09:50;D
67;VZ;607;320;HSVKB;ARR ONLY;CNX-BKK;01/05/2017 13:00;A
""")
df = pd.read_csv(dfstr, sep=";", index_col='ID')
df
问题1:如何将上面的dataframe转换成下面的。
如果 Car 和 FltReg 相同,我希望将其转换为相同的行。例如ID 0,EK376 A6ECI 5 月 3 日抵达 12:50 并作为 ID 1,EK377 A6ECI 于 5 月 3 日 15:40 离开......同样对于 ID2 和 3,ID4 和 5......这些是突出显示的 3 架不同的飞机以粗体显示。中间还有许多其他航班……然后接下来是 ID54,它是一架带有 HSKVA 飞机的 VZ 承运人……它首先起飞,所以它应该在自己的行上……然后它到达 ID55 并以 ID56 离开,以 ID57 再次到达,以 ID58 离开。
结果数据框应该是这样的:
from io import StringIO
import pandas as pd
dfstr = StringIO(u"""
IDArr;Car;FltNo;Acft;FltReg;E_FltType;Rtg;STADDtTm;ArrDep;IDDep;Car;FltNo;Acft;FltReg;E_FltType;Rtg;STADDtTm;ArrDep
0;EK;376;77W;A6ECI;T/A;DXB-BKK-DXB;03/05/2017 12:50;A;1;EK;377;77W;A6ECI;T/A;DXB-BKK-DXB;03/05/2017 15:40;D
2;EK;384;380;A6EDL;T/S;DXB-BKK-HKG;02/05/2017 12:15;A;3;EK;384;380;A6EDL;T/S;DXB-BKK-HKG;02/05/2017 14:00;D
4;EK;385;380;A6EDL;T/A;HKG-BKK-DXB;02/05/2017 23:45;A;5;EK;385;380;A6EDL;T/A;HKG-BKK-DXB;03/05/2017 01:15;D
;;;;;;;;;54;VZ;920;320;HSVKA;DEP ONLY;BKK-HPH;01/05/2017 11:15;D
55;VZ;921;320;HSVKA;ARR ONLY;HPH-BKK;01/05/2017 15:25;A;56;VZ;602;320;HSVKA;DEP ONLY;BKK-CNX;01/05/2017 16:35;D
57;VZ;603;320;HSVKA;ARR ONLY;CNX-BKK;01/05/2017 19:45;A;58;VZ;602;320;HSVKA;DEP ONLY;BKK-CNX;02/05/2017 11:15;D
59;VZ;603;320;HSVKA;ARR ONLY;CNX-BKK;02/05/2017 14:25;A;60;VZ;820;320;HSVKA;DEP ONLY;BKK-HKT;03/05/2017 07:05;D
61;VZ;821;320;HSVKA;ARR ONLY;HKT-BKK;03/05/2017 15:45;A;62;VZ;828;320;HSVKA;DEP ONLY;BKK-HKT;03/05/2017 18:20;D
63;VZ;829;320;HSVKA;ARR ONLY;HKT-BKK;03/05/2017 21:50;A;;;;;;;;;
;;;;;;;;;64;VZ;600;320;HSVKB;DEP ONLY;BKK-CNX;01/05/2017 06:10;D
65;VZ;601;320;HSVKB;ARR ONLY;CNX-BKK;01/05/2017 09:20;A;66;VZ;606;320;HSVKB;DEP ONLY;BKK-CNX;01/05/2017 09:50;D
67;VZ;607;320;HSVKB;ARR ONLY;CNX-BKK;01/05/2017 13:00;A;;;;;;;;;
""")
df2 = pd.read_csv(dfstr, sep=";")
df2
如您所见...我们可以看到 ID0 和 ID1 匹配在同一行...因此更容易看到航班在地面(即在机场)的时间...从 12:50 到 15:40(2 小时 50 分钟)...其余航班依此类推。
问题 2:使用上述结果数据框制作甘特图
生成的数据框将用于生成甘特图。
这是示例飞机:HSKVA(VZ 航班)将有自己的行...首先是 11:15 起飞(甘特图从 10:15 绘制(起飞前 1 小时,因为没有到达) 11:15。然后在同一行绘制甘特图 15:25 到 16:35,19:45 到 11:15 第二天,14:25 到 07:05, 15:45 to 18:20, 21:50 to 22:50 (航班到达后一小时没有起飞). matplotlib的broken_barh来了记住
HSKVB 将有自己的甘特图行...等等。
每个 Carrier/Aircraft 都在自己的视觉行上注册。
问题一
对您的设置的一个快速更改是我没有将 ID
设置为 index_col
,因为我想在 groupby().shift
中快速使用它的值。所以从修改后的 read_csv
:
开始
df = pd.read_csv(dfstr, sep=";")
cols = df.columns.values.tolist()
解决方案的很大一部分是确保 df 按 Car
、FltReg
和 STADDtTm
排序(因为前两个是唯一标识符,最后一个是主要排序值)。
sort_cols = ['Car', 'FltReg', 'STADDtTm']
df.sort_values(by=sort_cols, inplace=True)
现在我们进入了逻辑的主要部分。我要将 df 分为到达和离开,两者的连接方式是通过 shifted ID。也就是说,对于任何 (Car
, FltReg
) 分区,我知道将给定的 'A' 行与紧随其后的 'D' 行配对。同样,这就是我们需要排序(和完整)数据的原因。
让我们生成转换后的 ID:
# sort_cols[:2] is `Car` and `FltReg` together
df['NextID'] = df.groupby(sort_cols[:2])['ID'].shift(1)
现在使用 'A' 过滤后的 df 和 'D' 过滤后的 df,我将把它们全外连接在一起。到达(左数据集)以原始 ID
为键,出发(右数据集)以我们刚刚制作的 NextID
为键。
df_display = df[df['ArrDep'] == 'A'] \
.merge(df[df['ArrDep'] == 'D'],
how='outer',
left_on='ID',
right_on='NextID',
suffixes=('1', '2'))
请注意,这些列现在将以 1
(左)和 2
(右)作为后缀。
此时,这个新数据框 df_display
具有它需要的所有行,但在您的最终显示中它没有很好的排序。为此,您再次需要 sort_cols
列表,但每个列的 coalesced 版本将各自的左右版本放在一起。例如,Car1
和 Car2
必须合并在一起,以便您可以按合并版本对 所有行 进行排序。
pandas' combine_first
就像合并。
# purely for sorting the final display
for c in sort_cols:
df_display['sort_' + c] = df_display[c + '1'] \
.combine_first(df_display[c + '2'])
# for example, Car1 and Car2 have now been coalesced into sort_Car
df_display.sort_values(by=['sort_{}'.format(c) for c in sort_cols], inplace=True)
我们快完成了。现在 df_display
有我们不需要的无关列。我们可以 select 只有我们想要的列——基本上,原始列列表的两个副本 cols
.
df_display = df_display[['{}1'.format(c) for c in cols] + ['{}2'.format(c) for c in cols]]
df_display.to_csv('output.csv', index=None)
我检查了(在 csv 导出中以便我们可以看到广泛的数据集)这与您的示例匹配。
问题二
好的,所以如果您尝试一下 https://matplotlib.org/examples/pylab_examples/broken_barh.html 中的代码,您就会看到 broken_barh
是如何运行的。这很重要,因为我们必须使数据适合这种结构才能使用它。 broken_barh
的第一个参数是要绘制的元组列表,每个元组是一个(开始时间,持续时间)。
对于 matplotlib,开始时间必须采用其特殊的日期格式。所以我们必须使用 matplotlib.dates.date2num
转换 pandas 日期时间。最后,持续时间似乎以天为单位。
因此,如果 HSVKA 到达 2017-05-01 15:25:00 并在地面上停留 70 分钟,则 broken_barh
需要绘制元组 (mdates.date2num(Timestamp('2017-05-03 15:25:00')), 70 minutes in day units or 0.04861)
.
所以第一步是以这种格式从问题 1 中得到 df_display
。我们现在只需要关注四列'Car1', 'FltReg1', 'STADDtTm1', 'STADDtTm2'
。
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import seaborn # optional ... I like the look
print(df_display[['Car1', 'FltReg1', 'STADDtTm1', 'STADDtTm2']])
看起来像
Car1 FltReg1 STADDtTm1 STADDtTm2
0 EK A6ECI 03/05/2017 12:50 03/05/2017 15:40
1 EK A6EDL 02/05/2017 12:15 02/05/2017 14:00
2 EK A6EDL 02/05/2017 23:45 03/05/2017 01:15
10 NaN NaN NaN 01/05/2017 11:15
3 VZ HSVKA 01/05/2017 15:25 01/05/2017 16:35
4 VZ HSVKA 01/05/2017 19:45 02/05/2017 11:15
5 VZ HSVKA 02/05/2017 14:25 03/05/2017 07:05
6 VZ HSVKA 03/05/2017 15:45 03/05/2017 18:20
7 VZ HSVKA 03/05/2017 21:50 NaN
11 NaN NaN NaN 01/05/2017 06:10
8 VZ HSVKB 01/05/2017 09:20 01/05/2017 09:50
9 VZ HSVKB 01/05/2017 13:00 NaN
到达或离开时有 NaN
秒。估算这些是相当简单的。我在您的文章中注意到,当缺少某些内容时,您希望两侧各有一个小时的缓冲时间。所以这就是所有直接的争论:
df_gantt = df_display.copy()
# Convert to pandas timestamps for date arithmetic
df_gantt['STADDtTm1'] = pd.to_datetime(df_gantt['STADDtTm1'],
format='%d/%m/%Y %H:%M')
df_gantt['STADDtTm2'] = pd.to_datetime(df_gantt['STADDtTm2'],
format='%d/%m/%Y %H:%M')
# Impute identifiers
df_gantt['Car'] = df_gantt['Car1'].combine_first(df_gantt['Car2'])
df_gantt['FltReg'] = df_gantt['FltReg1'].combine_first(df_gantt['FltReg2'])
# Also just gonna combine Car and FltReg
# into a single column for simplicty
df_gantt['Car_FltReg'] = df_gantt['Car'] + ': ' + df_gantt['FltReg']
# Impute hour gaps
df_gantt['STADDtTm1'] = df_gantt['STADDtTm1'] \
.fillna(df_gantt['STADDtTm2'] - pd.Timedelta('1 hour'))
df_gantt['STADDtTm2'] = df_gantt['STADDtTm2'] \
.fillna(df_gantt['STADDtTm1'] + pd.Timedelta('1 hour'))
# Date diff in day units
df_gantt['DayDiff'] = (df_gantt['STADDtTm2'] - df_gantt['STADDtTm1']).dt.seconds \
/ 60 / 60 / 24
# matplotlib numeric date format
df_gantt['STADDtTm1'] = df_gantt['STADDtTm1'].apply(mdates.date2num)
df_gantt['STADDtTm2'] = df_gantt['STADDtTm2'].apply(mdates.date2num)
df_gantt = df_gantt[['Car_FltReg', 'STADDtTm1', 'STADDtTm2', 'DayDiff']]
print(df_gantt)
现在看起来像
Car_FltReg STADDtTm1 STADDtTm2 DayDiff
0 EK: A6ECI 736452.534722 736452.652778 0.118056
1 EK: A6EDL 736451.510417 736451.583333 0.072917
2 EK: A6EDL 736451.989583 736452.052083 0.062500
10 VZ: HSVKA 736450.427083 736450.468750 0.041667
3 VZ: HSVKA 736450.642361 736450.690972 0.048611
4 VZ: HSVKA 736450.822917 736451.468750 0.645833
5 VZ: HSVKA 736451.600694 736452.295139 0.694444
6 VZ: HSVKA 736452.656250 736452.763889 0.107639
7 VZ: HSVKA 736452.909722 736452.951389 0.041667
11 VZ: HSVKB 736450.215278 736450.256944 0.041667
8 VZ: HSVKB 736450.388889 736450.409722 0.020833
9 VZ: HSVKB 736450.541667 736450.583333 0.041667
现在创建一个字典,其中每个键都是唯一的 Car_FltReg
,每个值都是一个元组列表(如前所述),可以输入 broken_barh
.
dict_gantt = df_gantt.groupby('Car_FltReg')['STADDtTm1', 'DayDiff'] \
.apply(lambda x: list(zip(x['STADDtTm1'].tolist(),
x['DayDiff'].tolist()))) \
.to_dict()
所以dict_gantt
看起来像
{'EK: A6ECI': [(736452.5347222222, 0.11805555555555557)],
'EK: A6EDL': [(736451.5104166666, 0.07291666666666667),
(736451.9895833334, 0.0625)],
'VZ: HSVKA': [(736450.4270833334, 0.041666666666666664),
(736450.6423611111, 0.04861111111111111),
(736450.8229166666, 0.6458333333333334),
(736451.6006944445, 0.6944444444444445),
(736452.65625, 0.1076388888888889),
(736452.9097222222, 0.041666666666666664)],
'VZ: HSVKB': [(736450.2152777778, 0.041666666666666664),
(736450.3888888889, 0.020833333333333332),
(736450.5416666666, 0.041666666666666664)]}
非常适合 broken_barh
。而现在这一切都是 matplotlib 的疯狂。在准备 broken_barh
东西的核心逻辑之后,其他一切都只是艰苦的刻度格式等。如果你在 matplotlib 中定制过一些东西,这些东西应该很熟悉——我不会解释太多。
FltReg_list = sorted(dict_gantt, reverse=True)
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
start_datetime = df_gantt['STADDtTm1'].min()
end_datetime = df_gantt['STADDtTm2'].max()
# parameters for yticks, etc.
# you might have to play around
# with the different parts to modify
n = len(FltReg_list)
bar_size = 9
for i, bar in enumerate(FltReg_list):
ax.broken_barh(dict_gantt[bar], # data
(10 * (i + 1), bar_size), # (y position, bar size)
alpha=0.75,
edgecolor='k',
linewidth=1.2)
# I got date formatting ideas from
# https://matplotlib.org/examples/pylab_examples/finance_demo.html
ax.set_xlim(start_datetime, end_datetime)
ax.xaxis.set_major_locator(mdates.HourLocator(byhour=range(0, 24, 6)))
ax.xaxis.set_major_formatter(mdates.DateFormatter('%m-%d %H:%M'))
ax.xaxis.set_minor_locator(mdates.HourLocator(byhour=range(0, 24, 1)))
# omitting minor labels ...
plt.grid(b=True, which='minor', color='w', linestyle='dotted')
ax.set_yticks([5 + 10 * n for n in range(1, n + 1)])
ax.set_ylim(5, 5 + 10 * (n + 1))
ax.set_yticklabels(FltReg_list)
ax.set_title('Time on Ground')
ax.set_ylabel('Carrier: Registration')
plt.setp(plt.gca().get_xticklabels(), rotation=30, horizontalalignment='right')
plt.tight_layout()
fig.savefig('gantt.png', dpi=200)
这是最终输出。
假设我有一个这样的数据框(笔记本文本版本如下图):
A为Arrival Flight(着陆),D为Departure flight(起飞)。 Carrier 和 FltReg 一起是一架飞机......到达和离开机场,它将再次返回同一机场......几小时或几天后。 Acft是飞机的类型。
到达和离开需要匹配,以便生成的数据框可用于计算和绘制甘特图(开始时间即到达时间和结束时间即出发时间......航班在地面上的时间.)
数据通常会持续 7 天的航班时刻表和更多航空公司。7 天大约 3000 行...来自 sql 服务器数据库
from io import StringIO
import pandas as pd
dfstr = StringIO(u"""
ID;Car;FltNo;Acft;FltReg;E_FltType;Rtg;STADDtTm;ArrDep
0;EK;376;77W;A6ECI;T/A;DXB-BKK-DXB;03/05/2017 12:50;A
1;EK;377;77W;A6ECI;T/A;DXB-BKK-DXB;03/05/2017 15:40;D
2;EK;384;380;A6EDL;T/S;DXB-BKK-HKG;02/05/2017 12:15;A
3;EK;384;380;A6EDL;T/S;DXB-BKK-HKG;02/05/2017 14:00;D
4;EK;385;380;A6EDL;T/A;HKG-BKK-DXB;02/05/2017 23:45;A
5;EK;385;380;A6EDL;T/A;HKG-BKK-DXB;03/05/2017 01:15;D
54;VZ;920;320;HSVKA;DEP ONLY;BKK-HPH;01/05/2017 11:15;D
55;VZ;921;320;HSVKA;ARR ONLY;HPH-BKK;01/05/2017 15:25;A
56;VZ;602;320;HSVKA;DEP ONLY;BKK-CNX;01/05/2017 16:35;D
57;VZ;603;320;HSVKA;ARR ONLY;CNX-BKK;01/05/2017 19:45;A
58;VZ;602;320;HSVKA;DEP ONLY;BKK-CNX;02/05/2017 11:15;D
59;VZ;603;320;HSVKA;ARR ONLY;CNX-BKK;02/05/2017 14:25;A
60;VZ;820;320;HSVKA;DEP ONLY;BKK-HKT;03/05/2017 07:05;D
61;VZ;821;320;HSVKA;ARR ONLY;HKT-BKK;03/05/2017 15:45;A
62;VZ;828;320;HSVKA;DEP ONLY;BKK-HKT;03/05/2017 18:20;D
63;VZ;829;320;HSVKA;ARR ONLY;HKT-BKK;03/05/2017 21:50;A
64;VZ;600;320;HSVKB;DEP ONLY;BKK-CNX;01/05/2017 06:10;D
65;VZ;601;320;HSVKB;ARR ONLY;CNX-BKK;01/05/2017 09:20;A
66;VZ;606;320;HSVKB;DEP ONLY;BKK-CNX;01/05/2017 09:50;D
67;VZ;607;320;HSVKB;ARR ONLY;CNX-BKK;01/05/2017 13:00;A
""")
df = pd.read_csv(dfstr, sep=";", index_col='ID')
df
问题1:如何将上面的dataframe转换成下面的。
如果 Car 和 FltReg 相同,我希望将其转换为相同的行。例如ID 0,EK376 A6ECI 5 月 3 日抵达 12:50 并作为 ID 1,EK377 A6ECI 于 5 月 3 日 15:40 离开......同样对于 ID2 和 3,ID4 和 5......这些是突出显示的 3 架不同的飞机以粗体显示。中间还有许多其他航班……然后接下来是 ID54,它是一架带有 HSKVA 飞机的 VZ 承运人……它首先起飞,所以它应该在自己的行上……然后它到达 ID55 并以 ID56 离开,以 ID57 再次到达,以 ID58 离开。
结果数据框应该是这样的:
from io import StringIO
import pandas as pd
dfstr = StringIO(u"""
IDArr;Car;FltNo;Acft;FltReg;E_FltType;Rtg;STADDtTm;ArrDep;IDDep;Car;FltNo;Acft;FltReg;E_FltType;Rtg;STADDtTm;ArrDep
0;EK;376;77W;A6ECI;T/A;DXB-BKK-DXB;03/05/2017 12:50;A;1;EK;377;77W;A6ECI;T/A;DXB-BKK-DXB;03/05/2017 15:40;D
2;EK;384;380;A6EDL;T/S;DXB-BKK-HKG;02/05/2017 12:15;A;3;EK;384;380;A6EDL;T/S;DXB-BKK-HKG;02/05/2017 14:00;D
4;EK;385;380;A6EDL;T/A;HKG-BKK-DXB;02/05/2017 23:45;A;5;EK;385;380;A6EDL;T/A;HKG-BKK-DXB;03/05/2017 01:15;D
;;;;;;;;;54;VZ;920;320;HSVKA;DEP ONLY;BKK-HPH;01/05/2017 11:15;D
55;VZ;921;320;HSVKA;ARR ONLY;HPH-BKK;01/05/2017 15:25;A;56;VZ;602;320;HSVKA;DEP ONLY;BKK-CNX;01/05/2017 16:35;D
57;VZ;603;320;HSVKA;ARR ONLY;CNX-BKK;01/05/2017 19:45;A;58;VZ;602;320;HSVKA;DEP ONLY;BKK-CNX;02/05/2017 11:15;D
59;VZ;603;320;HSVKA;ARR ONLY;CNX-BKK;02/05/2017 14:25;A;60;VZ;820;320;HSVKA;DEP ONLY;BKK-HKT;03/05/2017 07:05;D
61;VZ;821;320;HSVKA;ARR ONLY;HKT-BKK;03/05/2017 15:45;A;62;VZ;828;320;HSVKA;DEP ONLY;BKK-HKT;03/05/2017 18:20;D
63;VZ;829;320;HSVKA;ARR ONLY;HKT-BKK;03/05/2017 21:50;A;;;;;;;;;
;;;;;;;;;64;VZ;600;320;HSVKB;DEP ONLY;BKK-CNX;01/05/2017 06:10;D
65;VZ;601;320;HSVKB;ARR ONLY;CNX-BKK;01/05/2017 09:20;A;66;VZ;606;320;HSVKB;DEP ONLY;BKK-CNX;01/05/2017 09:50;D
67;VZ;607;320;HSVKB;ARR ONLY;CNX-BKK;01/05/2017 13:00;A;;;;;;;;;
""")
df2 = pd.read_csv(dfstr, sep=";")
df2
如您所见...我们可以看到 ID0 和 ID1 匹配在同一行...因此更容易看到航班在地面(即在机场)的时间...从 12:50 到 15:40(2 小时 50 分钟)...其余航班依此类推。
问题 2:使用上述结果数据框制作甘特图
生成的数据框将用于生成甘特图。
这是示例飞机:HSKVA(VZ 航班)将有自己的行...首先是 11:15 起飞(甘特图从 10:15 绘制(起飞前 1 小时,因为没有到达) 11:15。然后在同一行绘制甘特图 15:25 到 16:35,19:45 到 11:15 第二天,14:25 到 07:05, 15:45 to 18:20, 21:50 to 22:50 (航班到达后一小时没有起飞). matplotlib的broken_barh来了记住
HSKVB 将有自己的甘特图行...等等。
每个 Carrier/Aircraft 都在自己的视觉行上注册。
问题一
对您的设置的一个快速更改是我没有将 ID
设置为 index_col
,因为我想在 groupby().shift
中快速使用它的值。所以从修改后的 read_csv
:
df = pd.read_csv(dfstr, sep=";")
cols = df.columns.values.tolist()
解决方案的很大一部分是确保 df 按 Car
、FltReg
和 STADDtTm
排序(因为前两个是唯一标识符,最后一个是主要排序值)。
sort_cols = ['Car', 'FltReg', 'STADDtTm']
df.sort_values(by=sort_cols, inplace=True)
现在我们进入了逻辑的主要部分。我要将 df 分为到达和离开,两者的连接方式是通过 shifted ID。也就是说,对于任何 (Car
, FltReg
) 分区,我知道将给定的 'A' 行与紧随其后的 'D' 行配对。同样,这就是我们需要排序(和完整)数据的原因。
让我们生成转换后的 ID:
# sort_cols[:2] is `Car` and `FltReg` together
df['NextID'] = df.groupby(sort_cols[:2])['ID'].shift(1)
现在使用 'A' 过滤后的 df 和 'D' 过滤后的 df,我将把它们全外连接在一起。到达(左数据集)以原始 ID
为键,出发(右数据集)以我们刚刚制作的 NextID
为键。
df_display = df[df['ArrDep'] == 'A'] \
.merge(df[df['ArrDep'] == 'D'],
how='outer',
left_on='ID',
right_on='NextID',
suffixes=('1', '2'))
请注意,这些列现在将以 1
(左)和 2
(右)作为后缀。
此时,这个新数据框 df_display
具有它需要的所有行,但在您的最终显示中它没有很好的排序。为此,您再次需要 sort_cols
列表,但每个列的 coalesced 版本将各自的左右版本放在一起。例如,Car1
和 Car2
必须合并在一起,以便您可以按合并版本对 所有行 进行排序。
pandas' combine_first
就像合并。
# purely for sorting the final display
for c in sort_cols:
df_display['sort_' + c] = df_display[c + '1'] \
.combine_first(df_display[c + '2'])
# for example, Car1 and Car2 have now been coalesced into sort_Car
df_display.sort_values(by=['sort_{}'.format(c) for c in sort_cols], inplace=True)
我们快完成了。现在 df_display
有我们不需要的无关列。我们可以 select 只有我们想要的列——基本上,原始列列表的两个副本 cols
.
df_display = df_display[['{}1'.format(c) for c in cols] + ['{}2'.format(c) for c in cols]]
df_display.to_csv('output.csv', index=None)
我检查了(在 csv 导出中以便我们可以看到广泛的数据集)这与您的示例匹配。
问题二
好的,所以如果您尝试一下 https://matplotlib.org/examples/pylab_examples/broken_barh.html 中的代码,您就会看到 broken_barh
是如何运行的。这很重要,因为我们必须使数据适合这种结构才能使用它。 broken_barh
的第一个参数是要绘制的元组列表,每个元组是一个(开始时间,持续时间)。
对于 matplotlib,开始时间必须采用其特殊的日期格式。所以我们必须使用 matplotlib.dates.date2num
转换 pandas 日期时间。最后,持续时间似乎以天为单位。
因此,如果 HSVKA 到达 2017-05-01 15:25:00 并在地面上停留 70 分钟,则 broken_barh
需要绘制元组 (mdates.date2num(Timestamp('2017-05-03 15:25:00')), 70 minutes in day units or 0.04861)
.
所以第一步是以这种格式从问题 1 中得到 df_display
。我们现在只需要关注四列'Car1', 'FltReg1', 'STADDtTm1', 'STADDtTm2'
。
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import seaborn # optional ... I like the look
print(df_display[['Car1', 'FltReg1', 'STADDtTm1', 'STADDtTm2']])
看起来像
Car1 FltReg1 STADDtTm1 STADDtTm2
0 EK A6ECI 03/05/2017 12:50 03/05/2017 15:40
1 EK A6EDL 02/05/2017 12:15 02/05/2017 14:00
2 EK A6EDL 02/05/2017 23:45 03/05/2017 01:15
10 NaN NaN NaN 01/05/2017 11:15
3 VZ HSVKA 01/05/2017 15:25 01/05/2017 16:35
4 VZ HSVKA 01/05/2017 19:45 02/05/2017 11:15
5 VZ HSVKA 02/05/2017 14:25 03/05/2017 07:05
6 VZ HSVKA 03/05/2017 15:45 03/05/2017 18:20
7 VZ HSVKA 03/05/2017 21:50 NaN
11 NaN NaN NaN 01/05/2017 06:10
8 VZ HSVKB 01/05/2017 09:20 01/05/2017 09:50
9 VZ HSVKB 01/05/2017 13:00 NaN
到达或离开时有 NaN
秒。估算这些是相当简单的。我在您的文章中注意到,当缺少某些内容时,您希望两侧各有一个小时的缓冲时间。所以这就是所有直接的争论:
df_gantt = df_display.copy()
# Convert to pandas timestamps for date arithmetic
df_gantt['STADDtTm1'] = pd.to_datetime(df_gantt['STADDtTm1'],
format='%d/%m/%Y %H:%M')
df_gantt['STADDtTm2'] = pd.to_datetime(df_gantt['STADDtTm2'],
format='%d/%m/%Y %H:%M')
# Impute identifiers
df_gantt['Car'] = df_gantt['Car1'].combine_first(df_gantt['Car2'])
df_gantt['FltReg'] = df_gantt['FltReg1'].combine_first(df_gantt['FltReg2'])
# Also just gonna combine Car and FltReg
# into a single column for simplicty
df_gantt['Car_FltReg'] = df_gantt['Car'] + ': ' + df_gantt['FltReg']
# Impute hour gaps
df_gantt['STADDtTm1'] = df_gantt['STADDtTm1'] \
.fillna(df_gantt['STADDtTm2'] - pd.Timedelta('1 hour'))
df_gantt['STADDtTm2'] = df_gantt['STADDtTm2'] \
.fillna(df_gantt['STADDtTm1'] + pd.Timedelta('1 hour'))
# Date diff in day units
df_gantt['DayDiff'] = (df_gantt['STADDtTm2'] - df_gantt['STADDtTm1']).dt.seconds \
/ 60 / 60 / 24
# matplotlib numeric date format
df_gantt['STADDtTm1'] = df_gantt['STADDtTm1'].apply(mdates.date2num)
df_gantt['STADDtTm2'] = df_gantt['STADDtTm2'].apply(mdates.date2num)
df_gantt = df_gantt[['Car_FltReg', 'STADDtTm1', 'STADDtTm2', 'DayDiff']]
print(df_gantt)
现在看起来像
Car_FltReg STADDtTm1 STADDtTm2 DayDiff
0 EK: A6ECI 736452.534722 736452.652778 0.118056
1 EK: A6EDL 736451.510417 736451.583333 0.072917
2 EK: A6EDL 736451.989583 736452.052083 0.062500
10 VZ: HSVKA 736450.427083 736450.468750 0.041667
3 VZ: HSVKA 736450.642361 736450.690972 0.048611
4 VZ: HSVKA 736450.822917 736451.468750 0.645833
5 VZ: HSVKA 736451.600694 736452.295139 0.694444
6 VZ: HSVKA 736452.656250 736452.763889 0.107639
7 VZ: HSVKA 736452.909722 736452.951389 0.041667
11 VZ: HSVKB 736450.215278 736450.256944 0.041667
8 VZ: HSVKB 736450.388889 736450.409722 0.020833
9 VZ: HSVKB 736450.541667 736450.583333 0.041667
现在创建一个字典,其中每个键都是唯一的 Car_FltReg
,每个值都是一个元组列表(如前所述),可以输入 broken_barh
.
dict_gantt = df_gantt.groupby('Car_FltReg')['STADDtTm1', 'DayDiff'] \
.apply(lambda x: list(zip(x['STADDtTm1'].tolist(),
x['DayDiff'].tolist()))) \
.to_dict()
所以dict_gantt
看起来像
{'EK: A6ECI': [(736452.5347222222, 0.11805555555555557)],
'EK: A6EDL': [(736451.5104166666, 0.07291666666666667),
(736451.9895833334, 0.0625)],
'VZ: HSVKA': [(736450.4270833334, 0.041666666666666664),
(736450.6423611111, 0.04861111111111111),
(736450.8229166666, 0.6458333333333334),
(736451.6006944445, 0.6944444444444445),
(736452.65625, 0.1076388888888889),
(736452.9097222222, 0.041666666666666664)],
'VZ: HSVKB': [(736450.2152777778, 0.041666666666666664),
(736450.3888888889, 0.020833333333333332),
(736450.5416666666, 0.041666666666666664)]}
非常适合 broken_barh
。而现在这一切都是 matplotlib 的疯狂。在准备 broken_barh
东西的核心逻辑之后,其他一切都只是艰苦的刻度格式等。如果你在 matplotlib 中定制过一些东西,这些东西应该很熟悉——我不会解释太多。
FltReg_list = sorted(dict_gantt, reverse=True)
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
start_datetime = df_gantt['STADDtTm1'].min()
end_datetime = df_gantt['STADDtTm2'].max()
# parameters for yticks, etc.
# you might have to play around
# with the different parts to modify
n = len(FltReg_list)
bar_size = 9
for i, bar in enumerate(FltReg_list):
ax.broken_barh(dict_gantt[bar], # data
(10 * (i + 1), bar_size), # (y position, bar size)
alpha=0.75,
edgecolor='k',
linewidth=1.2)
# I got date formatting ideas from
# https://matplotlib.org/examples/pylab_examples/finance_demo.html
ax.set_xlim(start_datetime, end_datetime)
ax.xaxis.set_major_locator(mdates.HourLocator(byhour=range(0, 24, 6)))
ax.xaxis.set_major_formatter(mdates.DateFormatter('%m-%d %H:%M'))
ax.xaxis.set_minor_locator(mdates.HourLocator(byhour=range(0, 24, 1)))
# omitting minor labels ...
plt.grid(b=True, which='minor', color='w', linestyle='dotted')
ax.set_yticks([5 + 10 * n for n in range(1, n + 1)])
ax.set_ylim(5, 5 + 10 * (n + 1))
ax.set_yticklabels(FltReg_list)
ax.set_title('Time on Ground')
ax.set_ylabel('Carrier: Registration')
plt.setp(plt.gca().get_xticklabels(), rotation=30, horizontalalignment='right')
plt.tight_layout()
fig.savefig('gantt.png', dpi=200)
这是最终输出。