{ "cells": [ { "cell_type": "markdown", "id": "5da16986-b620-4d9e-8d0d-d669d7c8ccf5", "metadata": {}, "source": [ "# AGC Tools 2022: Columnar Analysis with Coffea" ] }, { "cell_type": "markdown", "id": "d61ced43-27c1-42a1-9f1e-7aeecf73c406", "metadata": {}, "source": [ "
keys()
:"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "64305403-ccb2-47cd-9a08-4a48a0523926",
"metadata": {
"tags": []
},
"outputs": [
{
"data": {
"text/plain": [
"['numberelectron',\n",
" 'nelectron_e',\n",
" 'electron_e',\n",
" 'nelectron_pt',\n",
" 'electron_pt',\n",
" 'nelectron_px',\n",
" 'electron_px',\n",
" 'nelectron_py',\n",
" 'electron_py',\n",
" 'nelectron_pz',\n",
" 'electron_pz',\n",
" 'nelectron_eta',\n",
" 'electron_eta',\n",
" 'nelectron_phi',\n",
" 'electron_phi',\n",
" 'nelectron_ch',\n",
" 'electron_ch',\n",
" 'nelectron_iso',\n",
" 'electron_iso',\n",
" 'nelectron_isLoose',\n",
" 'electron_isLoose',\n",
" 'nelectron_isMedium',\n",
" 'electron_isMedium',\n",
" 'nelectron_isTight',\n",
" 'electron_isTight',\n",
" 'nelectron_dxy',\n",
" 'electron_dxy',\n",
" 'nelectron_dz',\n",
" 'electron_dz',\n",
" 'nelectron_dxyError',\n",
" 'electron_dxyError',\n",
" 'nelectron_dzError',\n",
" 'electron_dzError',\n",
" 'numGenPart',\n",
" 'nGenPart_pt',\n",
" 'GenPart_pt',\n",
" 'nGenPart_eta',\n",
" 'GenPart_eta',\n",
" 'nGenPart_mass',\n",
" 'GenPart_mass',\n",
" 'nGenPart_pdgId',\n",
" 'GenPart_pdgId',\n",
" 'nGenPart_phi',\n",
" 'GenPart_phi',\n",
" 'nGenPart_px',\n",
" 'GenPart_px',\n",
" 'nGenPart_py',\n",
" 'GenPart_py',\n",
" 'nGenPart_pz',\n",
" 'GenPart_pz',\n",
" 'nGenPart_status',\n",
" 'GenPart_status',\n",
" 'numberjet',\n",
" 'njet_e',\n",
" 'jet_e',\n",
" 'njet_pt',\n",
" 'jet_pt',\n",
" 'njet_px',\n",
" 'jet_px',\n",
" 'njet_py',\n",
" 'jet_py',\n",
" 'njet_pz',\n",
" 'jet_pz',\n",
" 'njet_eta',\n",
" 'jet_eta',\n",
" 'njet_phi',\n",
" 'jet_phi',\n",
" 'njet_ch',\n",
" 'jet_ch',\n",
" 'njet_mass',\n",
" 'jet_mass',\n",
" 'njet_btag',\n",
" 'jet_btag',\n",
" 'njet_pt_uncorr',\n",
" 'jet_pt_uncorr',\n",
" 'met_e',\n",
" 'met_pt',\n",
" 'met_px',\n",
" 'met_py',\n",
" 'met_phi',\n",
" 'met_significance',\n",
" 'met_rawpt',\n",
" 'met_rawphi',\n",
" 'met_rawe',\n",
" 'numbermuon',\n",
" 'nmuon_e',\n",
" 'muon_e',\n",
" 'nmuon_pt',\n",
" 'muon_pt',\n",
" 'nmuon_px',\n",
" 'muon_px',\n",
" 'nmuon_py',\n",
" 'muon_py',\n",
" 'nmuon_pz',\n",
" 'muon_pz',\n",
" 'nmuon_eta',\n",
" 'muon_eta',\n",
" 'nmuon_phi',\n",
" 'muon_phi',\n",
" 'nmuon_ch',\n",
" 'muon_ch',\n",
" 'nmuon_isSoft',\n",
" 'muon_isSoft',\n",
" 'nmuon_isTight',\n",
" 'muon_isTight',\n",
" 'nmuon_dxy',\n",
" 'muon_dxy',\n",
" 'nmuon_dz',\n",
" 'muon_dz',\n",
" 'nmuon_dxyError',\n",
" 'muon_dxyError',\n",
" 'nmuon_dzError',\n",
" 'muon_dzError',\n",
" 'nmuon_pfreliso03all',\n",
" 'muon_pfreliso03all',\n",
" 'nmuon_pfreliso04all',\n",
" 'muon_pfreliso04all',\n",
" 'nmuon_jetidx',\n",
" 'muon_jetidx',\n",
" 'nmuon_genpartidx',\n",
" 'muon_genpartidx',\n",
" 'numberphoton',\n",
" 'nphoton_e',\n",
" 'photon_e',\n",
" 'nphoton_pt',\n",
" 'photon_pt',\n",
" 'nphoton_px',\n",
" 'photon_px',\n",
" 'nphoton_py',\n",
" 'photon_py',\n",
" 'nphoton_pz',\n",
" 'photon_pz',\n",
" 'nphoton_eta',\n",
" 'photon_eta',\n",
" 'nphoton_phi',\n",
" 'photon_phi',\n",
" 'nphoton_ch',\n",
" 'photon_ch',\n",
" 'nphoton_chIso',\n",
" 'photon_chIso',\n",
" 'nphoton_nhIso',\n",
" 'photon_nhIso',\n",
" 'nphoton_phIso',\n",
" 'photon_phIso',\n",
" 'nphoton_isLoose',\n",
" 'photon_isLoose',\n",
" 'nphoton_isMedium',\n",
" 'photon_isMedium',\n",
" 'nphoton_isTight',\n",
" 'photon_isTight',\n",
" 'nPV_chi2',\n",
" 'PV_chi2',\n",
" 'nPV_ndof',\n",
" 'PV_ndof',\n",
" 'PV_npvs',\n",
" 'PV_npvsGood',\n",
" 'nPV_x',\n",
" 'PV_x',\n",
" 'nPV_y',\n",
" 'PV_y',\n",
" 'nPV_z',\n",
" 'PV_z',\n",
" 'numbertau',\n",
" 'ntau_e',\n",
" 'tau_e',\n",
" 'ntau_pt',\n",
" 'tau_pt',\n",
" 'ntau_px',\n",
" 'tau_px',\n",
" 'ntau_py',\n",
" 'tau_py',\n",
" 'ntau_pz',\n",
" 'tau_pz',\n",
" 'ntau_eta',\n",
" 'tau_eta',\n",
" 'ntau_phi',\n",
" 'tau_phi',\n",
" 'ntau_ch',\n",
" 'tau_ch',\n",
" 'ntau_mass',\n",
" 'tau_mass',\n",
" 'ntau_decaymode',\n",
" 'tau_decaymode',\n",
" 'ntau_iddecaymode',\n",
" 'tau_iddecaymode',\n",
" 'ntau_idisoraw',\n",
" 'tau_idisoraw',\n",
" 'ntau_idisovloose',\n",
" 'tau_idisovloose',\n",
" 'ntau_idisoloose',\n",
" 'tau_idisoloose',\n",
" 'ntau_idisomedium',\n",
" 'tau_idisomedium',\n",
" 'ntau_idisotight',\n",
" 'tau_idisotight',\n",
" 'ntau_idantieletight',\n",
" 'tau_idantieletight',\n",
" 'ntau_idantimutight',\n",
" 'tau_idantimutight',\n",
" 'numbertrigobj',\n",
" 'ntrigobj_e',\n",
" 'trigobj_e',\n",
" 'ntrigobj_pt',\n",
" 'trigobj_pt',\n",
" 'ntrigobj_px',\n",
" 'trigobj_px',\n",
" 'ntrigobj_py',\n",
" 'trigobj_py',\n",
" 'ntrigobj_pz',\n",
" 'trigobj_pz',\n",
" 'ntrigobj_eta',\n",
" 'trigobj_eta',\n",
" 'ntrigobj_phi',\n",
" 'trigobj_phi']"
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"events.keys()"
]
},
{
"cell_type": "markdown",
"id": "8e4d5996-3ab1-465a-a533-202d39947b8a",
"metadata": {},
"source": [
"Each of these branches can be interpreted as an awkward array. Let's examine their contents. Since this is a mumu dataset, it seems natural to look at muons."
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "2e628ae2-a818-416d-bfa9-adf2b222eefd",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[[51, 40.7, 3.24], [48.3, 40.3], [34.6, ... [29.1], [55.2, 34, 3.13, 1.53, 1.08]]\n"
]
}
],
"source": [
"muon_pt = events['muon_pt'].array()\n",
"print(muon_pt)"
]
},
{
"cell_type": "markdown",
"id": "4d6782cc-1ae5-4eac-bf99-3bd3ded81f07",
"metadata": {},
"source": [
"It's instructive to take a closer look at the structure of this array. First, we see that it is an array of subarrays. Each subarray represents one event, and each element of a subarray represents one muon. It is now clear what we mean by \"non-rectangular data.\" There are a variable amount of muons, so the subarrays are not of equal size.\n",
"\n",
"If we look at another (non-muon) branch, we would expect it to have the same amount of subarrays, but not necessarily the same amount of elements in each subarray. For comparison, let's now look at electrons:"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "0f0a0f4c-c5d1-4ece-8e9b-e509c89d4964",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[[], [], [], [], [], [], [], [8.26], [], ... [], [], [], [], [], [], [], [], []]\n"
]
}
],
"source": [
"electron_pt = events['electron_pt'].array()\n",
"print(electron_pt)"
]
},
{
"cell_type": "markdown",
"id": "03e6eea7-cd4e-43fc-b041-4f11b4b7615b",
"metadata": {},
"source": [
"In which case it is clear that there are not the same amount of elements in each subarray (most are empty!)\n",
"\n",
"To drive the point home, we can also prove that there is an equal amount of subarrays:"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "fd8fc00a-02bd-4418-9fbe-b4a572fbe159",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"(43962, 43962)"
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"ak.num(electron_pt, axis=0), ak.num(muon_pt, axis=0)"
]
},
{
"cell_type": "markdown",
"id": "be9678e7-d52d-47f3-91db-83196371641d",
"metadata": {},
"source": [
"A quick note about axes in awkward: 0 is always the shallowest, while -1 is the deepest. In other words, axis=0
would tell us the number of subarrays (events), while axis=-1
would tell us the number of muons within each subarray:"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "de72dea0-6a8b-4fbf-8ab7-73afcb1505a4",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"(data[mask]
. This will pick out only the elements of our data which correspond to a True
. \n",
"\n",
"Let's pause and consider the details of this methodology. Our mask in this case must have the same shape as our muons branch, and this is guaranteed to be the case since it is generated from the data in that branch. When we apply this mask, the output should have the same amount of events, but it should down-select muons - muons which correspond to False should be dropped. Let's compare to check:"
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "672f9542-4002-4776-a571-e1d4c83228c1",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Input: [[51, 40.7, 3.24], [48.3, 40.3], [34.6, ... [29.1], [55.2, 34, 3.13, 1.53, 1.08]]\n",
"Output: [[51, 40.7], [48.3, 40.3], [34.6], [28.4, ... [43.6, 37.2], [29.1], [55.2, 34]]\n"
]
}
],
"source": [
"print('Input:', muon_pt)\n",
"print('Output:', muon_pt[muon_pt > 10])"
]
},
{
"cell_type": "markdown",
"id": "a36a805e-8b0a-4d60-8901-bc76dbb4aab7",
"metadata": {},
"source": [
"We can also confirm we have fewer muons now, but the same amount of events:"
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "4f43d9a0-d242-4702-acb7-90f9a0b561ea",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Input Counts: 92259\n",
"Output Counts: 57514\n",
"Input Size: 43962\n",
"Output Size: 43962\n"
]
}
],
"source": [
"print('Input Counts:', ak.sum(ak.num(muon_pt, axis=1)))\n",
"print('Output Counts:', ak.sum(ak.num(muon_pt[muon_pt > 10], axis=1)))\n",
"\n",
"print('Input Size:', ak.num(muon_pt, axis=0))\n",
"print('Output Size:', ak.num(muon_pt[ak.num(muon_pt)], axis=0))"
]
},
{
"cell_type": "markdown",
"id": "c2c4cd0e-834a-44b8-b2b8-fd0d956ddd54",
"metadata": {},
"source": [
"What if we wanted to do a selection on our events, rather than on our muons? Then we'd just need a mask with the same length as our events array, with a True
or False
entry in place of the subarray instead of the element of the subarray. For example, we might want to select events which have at least one electron. Then our mask would be:"
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "e11a2b07-dfc7-43b5-abc2-850145a23ee2",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"np.where()
(ak.where()
). Other resources (listed in the appendix) cover it more extensively and do it better justice.\n",
"\n",
"Nonetheless, there are a couple of things which aren't exactly pretty. For example, what if we want LorentzVector operations? The nicest way to handle this is with coffea schemas."
]
},
{
"cell_type": "markdown",
"id": "0eb7edc6-cbeb-465b-a3f7-af0d6c62fd85",
"metadata": {},
"source": [
"## **NanoEvents:** Making Data Physics-Friendly"
]
},
{
"cell_type": "markdown",
"id": "dea69bdd-7e37-41b2-a5a7-4884af78ac37",
"metadata": {},
"source": [
"Before we can dive into our Z-peak analysis, we need to spruce up our data a bit.\n",
"\n",
"Let's turn our attention to NanoEvents and schemas. Schemas let us better organize our file and impose physics methods onto our data. There exist schemas for some standard file formats, most prominently NanoAOD, and there is a BaseSchema
which operates much like uproot. The coffea development team welcomes community development of other schemas and it is not so difficult to do so.\n",
"\n",
"For the purposes of this tutorial, I have already made a schema. We won't go into the details, but we will show off its features and the general structure. First, let's take a look at our data structure again. Because there's a lot of branches, we'll zoom in on the muon-related ones here:"
]
},
{
"cell_type": "code",
"execution_count": 16,
"id": "9343cc44-7ef9-46b0-a982-d548a77fe250",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"numbermuon\n",
"nmuon_e\n",
"muon_e\n",
"nmuon_pt\n",
"muon_pt\n",
"nmuon_px\n",
"muon_px\n",
"nmuon_py\n",
"muon_py\n",
"nmuon_pz\n",
"muon_pz\n",
"nmuon_eta\n",
"muon_eta\n",
"nmuon_phi\n",
"muon_phi\n",
"nmuon_ch\n",
"muon_ch\n",
"nmuon_isSoft\n",
"muon_isSoft\n",
"nmuon_isTight\n",
"muon_isTight\n",
"nmuon_dxy\n",
"muon_dxy\n",
"nmuon_dz\n",
"muon_dz\n",
"nmuon_dxyError\n",
"muon_dxyError\n",
"nmuon_dzError\n",
"muon_dzError\n",
"nmuon_pfreliso03all\n",
"muon_pfreliso03all\n",
"nmuon_pfreliso04all\n",
"muon_pfreliso04all\n",
"nmuon_jetidx\n",
"muon_jetidx\n",
"nmuon_genpartidx\n",
"muon_genpartidx\n"
]
}
],
"source": [
"branches = uproot.open(\"https://xrootd-local.unl.edu:1094//store/user/AGC/zmumu/RunIIFall15MiniAODv2/ZToMuMu_NNPDF30_13TeV-powheg_M_50_120/MINIAODSIM/PU25nsData2015v1_76X_mcRun2_asymptotic_v12-v1/20000/022FAAEA-1BB9-E511-A6DF-44A842CFD5D8.root\")['events']\n",
"\n",
"for branch in branches.keys():\n",
" if 'muon' in branch:\n",
" print(branch)"
]
},
{
"cell_type": "markdown",
"id": "11330d3d-7206-4a7c-8d96-47abd00b3e35",
"metadata": {},
"source": [
"By default, uproot (and BaseSchema
) treats all of the muon branches as distinct branches with distinct data. This is not ideal, as some of our data is redundant, e.g., all of the nmuon_*
branches better have the same counts. Further, we'd expect all the muon_*
branches to have the same shape, as each muon should have an entry in each branch.\n",
"\n",
"The first benefit of instating a schema, then, is a standardization of our fields. It would be more succinct to create a general muon
collection under which all of these branches (in NanoEvents, fields) with identical size can be housed, and to scrap the redundant ones. We can use numbermuon
to figure out how many muons should be in each subarray (the counts, or offsets), and then fill the contents with each muon_*
field. We can repeat this for the other branches."
]
},
{
"cell_type": "code",
"execution_count": 17,
"id": "05706bd9-dc43-4272-b492-254edb354c6c",
"metadata": {},
"outputs": [],
"source": [
"from agc_schema import AGCSchema\n",
"agc_events = NanoEventsFactory.from_root('https://xrootd-local.unl.edu:1094//store/user/AGC/zmumu/RunIIFall15MiniAODv2/ZToMuMu_NNPDF30_13TeV-powheg_M_50_120/MINIAODSIM/PU25nsData2015v1_76X_mcRun2_asymptotic_v12-v1/20000/022FAAEA-1BB9-E511-A6DF-44A842CFD5D8.root', schemaclass=AGCSchema, treepath='events').events()"
]
},
{
"cell_type": "markdown",
"id": "0128655f-4749-40bd-813b-1e7acb12f452",
"metadata": {},
"source": [
"For NanoEvents, there is a slightly different syntax to access our data. Instead of querying keys()
to find our fields we query fields
. We can still access specific fields as we would navigate a dictionary (collection[field]
) or we can navigate them in a new way: collection.field
.\n",
"\n",
"Let's take a look at our fields now:"
]
},
{
"cell_type": "code",
"execution_count": 18,
"id": "c3b83bd4-d51f-4381-a847-6ba9059b2de3",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"['jet', 'photon', 'trigobj', 'tau', 'muon', 'electron', 'met', 'GenPart', 'PV']"
]
},
"execution_count": 18,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"agc_events.fields"
]
},
{
"cell_type": "markdown",
"id": "048de18a-fbb3-4900-9179-e694d06fa32e",
"metadata": {},
"source": [
"We can confirm that no information has been lost by querying the fields
of our event fields:"
]
},
{
"cell_type": "code",
"execution_count": 19,
"id": "5700692e-81ba-41ff-a180-913620367272",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"['pt',\n",
" 'px',\n",
" 'py',\n",
" 'pz',\n",
" 'eta',\n",
" 'phi',\n",
" 'ch',\n",
" 'isSoft',\n",
" 'isTight',\n",
" 'dxy',\n",
" 'dz',\n",
" 'dxyError',\n",
" 'dzError',\n",
" 'pfreliso03all',\n",
" 'pfreliso04all',\n",
" 'jetidx',\n",
" 'genpartidx',\n",
" 'energy']"
]
},
"execution_count": 19,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"agc_events.muon.fields"
]
},
{
"cell_type": "markdown",
"id": "6ccb9901-3669-4c3e-ac8b-325022adf0a4",
"metadata": {},
"source": [
"All of the branches above are still here, just as fields of our new electron collection. We can confirm this matches our original counts from uproot:"
]
},
{
"cell_type": "code",
"execution_count": 20,
"id": "ce23e19d-af49-4131-9191-69b5aff69f1a",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"(number{name}
field except for GenPart, which has num{name}
. This discrepancy is irrelevant after the application of the schema, so we don't have to worry about it.\n",
"\n",
"There are also other benefits to this structure: as we now have a collection object (agc_events.muon
), there is a natural place to impose physics methods. By default, this collection object does nothing - it's just a category. But we're physicists, and we often want to deal with Lorentz vectors. Why not treat these objects as such?\n",
"\n",
"This behavior can be built fairly simply into a schema simply by specifying that it is a PtEtaPhiELorentzVector
and having the appropriate fields present in each collection (in this case, pt, eta, phi and e). This makes mathematical operations on our muon
s well-defined:"
]
},
{
"cell_type": "code",
"execution_count": 21,
"id": "4d6b3b26-6f7c-40fb-9f99-37a1a1b84b21",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"dimuons
array should now contain only opposite-charge muon pairs and our dielectrons
opposite-charge electron pairs. Let's check!"
]
},
{
"cell_type": "code",
"execution_count": 26,
"id": "89c8dae6-9adb-4bc3-9f5d-96936c4e6ab5",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"(dimuons[:, 0]
will select the first muon in every dimuon
event. Recall that NanoEvents allows us to treat mathematical operations on the muon collection level as LorentzVector objects. The same goes for our electrons collection, of course. That makes our life easy:"
]
},
{
"cell_type": "code",
"execution_count": 27,
"id": "ea1eb5b0-7d6e-4c80-aa7c-986265117200",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"(Regular
axis in the Hist package.\n",
"\n",
"In our case, let's plot 50 bins between values of 0 and 150. This merely cuts off a shrinking tail on the higher end. Because a histogram can contain an arbitrary amount of axes, we also need to give our axis a name (which becomes its reference in our code) and a label (which is the label on the axis that users see when the histogram is plotted)."
]
},
{
"cell_type": "code",
"execution_count": 28,
"id": "d9bf9bb7-db03-475a-bb5f-484003bfae0d",
"metadata": {},
"outputs": [],
"source": [
"ll_bin = hist.axis.Regular(label=\"Dilepton Mass\", name=\"dilep_mass\", bins=50, start=0, stop=150)"
]
},
{
"cell_type": "markdown",
"id": "33b35d1b-ff9e-4f73-9851-977130248f84",
"metadata": {},
"source": [
"We are still not *yet* ready to plot. We have two masses we'd like to plot, and it doesn't make much sense to throw ee masses into the same bins as $\\mu\\mu$ masses. We want to keep these separate. We do so by introducing a Categorical
axis. Another example of when we might use a categorical axis is to keep data from different datasets separate, though in our case we're only working with a single dataset.\n",
"\n",
"A Categorical
axis takes a name, a label, and a pre-defined list of categories."
]
},
{
"cell_type": "code",
"execution_count": 29,
"id": "7b06e44e-87cf-4042-a9b7-8b805a6adfb0",
"metadata": {},
"outputs": [],
"source": [
"ll_cat = hist.axis.StrCategory(label='Leptons', name='lepton', categories=[\"ee\", \"$\\mu\\mu$\"])"
]
},
{
"cell_type": "markdown",
"id": "5db5a5fb-d8a8-4532-87e8-bdd6a6e3532a",
"metadata": {},
"source": [
"We finally have all of the ingredients needed for a histogram! All that remains is to put them together:"
]
},
{
"cell_type": "code",
"execution_count": 30,
"id": "a8395f72-65ad-4414-9f32-0a4ef5984e36",
"metadata": {},
"outputs": [],
"source": [
"ll_hist = Hist(ll_bin, ll_cat)"
]
},
{
"cell_type": "markdown",
"id": "92e3322d-a601-43a5-9d80-bed800c8f480",
"metadata": {},
"source": [
"And to fill it with our data:"
]
},
{
"cell_type": "code",
"execution_count": 31,
"id": "80e4096d-e519-4787-a92e-a098bac08ae5",
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"\n",
"