Git Product home page Git Product logo

sorobn's Introduction

sorobn โ€” Bayesian networks in Python


DALLยทE 2023-03-17 09 21 56 - An oil painting by Matisse of a Bayesian network  Each node in the network is an abacus with red balls and a wooden frame

This is an unambitious Python library for working with Bayesian networks. For serious usage, you should probably be using a more established project, such as pomegranate, pgmpy, bnlearn (which is built on the latter), or even PyMC. There's also the well-documented bnlearn package in R. Hey, you could even go medieval and use something like Netica โ€” I'm just jesting, they actually have a nice tutorial on Bayesian networks. By the way, if you're not familiar with Bayesian networks, then I highly recommend Patrick Winston's MIT courses on probabilistic inference (part 1, part 2).

The main goal of this project is to be used for educational purposes. As such, more emphasis is put on tidyness and conciseness than on performance. I find libraries such as pomegranate are wonderful. But, they literally contain several thousand lines of non-obvious code, at the detriment of simplicity and ease of comprehension. I've also put some effort into designing a slick API that makes full use of pandas. Although performance is not the main focus of this library, it is reasonably efficient and should be able to satisfy most use cases in a timely manner.

Table of contents

Installation

You should be able to install and use this library with any Python version above 3.9:

pip install sorobn

Note that under the hood, sorobn uses vose for random sampling, which is written in Cython.

Usage

โœ๏ธ Manual structures

The central construct in sorobn is the BayesNet class. A Bayesian network's structure can be manually defined by instantiating a BayesNet. As an example, let's use Judea Pearl's famous alarm network:

>>> import sorobn as hh

>>> bn = hh.BayesNet(
...     ('Burglary', 'Alarm'),
...     ('Earthquake', 'Alarm'),
...     ('Alarm', 'John calls'),
...     ('Alarm', 'Mary calls'),
...     seed=42,
... )

You may also use the following notation, which is slightly more terse:

>>> import sorobn as hh

>>> bn = hh.BayesNet(
...     (['Burglary', 'Earthquake'], 'Alarm'),
...     ('Alarm', ['John calls', 'Mary calls']),
...     seed=42
... )

In Judea Pearl's example, the conditional probability tables are given. Therefore, we can define them manually by setting the values of the P attribute:

>>> import pandas as pd

# P(Burglary)
>>> bn.P['Burglary'] = pd.Series({False: .999, True: .001})

# P(Earthquake)
>>> bn.P['Earthquake'] = pd.Series({False: .998, True: .002})

# P(Alarm | Burglary, Earthquake)
>>> bn.P['Alarm'] = pd.Series({
...     (True, True, True): .95,
...     (True, True, False): .05,
...
...     (True, False, True): .94,
...     (True, False, False): .06,
...
...     (False, True, True): .29,
...     (False, True, False): .71,
...
...     (False, False, True): .001,
...     (False, False, False): .999
... })

# P(John calls | Alarm)
>>> bn.P['John calls'] = pd.Series({
...     (True, True): .9,
...     (True, False): .1,
...     (False, True): .05,
...     (False, False): .95
... })

# P(Mary calls | Alarm)
>>> bn.P['Mary calls'] = pd.Series({
...     (True, True): .7,
...     (True, False): .3,
...     (False, True): .01,
...     (False, False): .99
... })

The prepare method has to be called whenever the structure and/or the P are manually specified. This will do some house-keeping and make sure everything is sound. It is not compulsory but highly recommended, just like brushing your teeth.

>>> bn.prepare()

Note that you are allowed to specify variables that have no dependencies with any other variable:

>>> _ = hh.BayesNet(
...     ('Cloud', 'Rain'),
...     (['Rain', 'Cold'], 'Snow'),
...     'Wind speed'  # has no dependencies
... )

๐ŸŽฒ Random sampling

You can use a Bayesian network to generate random samples. The samples will follow the distribution induced by the network's structure and its conditional probability tables.

>>> from pprint import pprint

>>> pprint(bn.sample())
{'Alarm': False,
 'Burglary': False,
 'Earthquake': False,
 'John calls': False,
 'Mary calls': False}

>>> bn.sample(5)  # doctest: +SKIP
    Alarm  Burglary  Earthquake  John calls  Mary calls
0  False     False       False       False       False
1  False     False       False       False       False
2  False     False       False       False       False
3  False     False       False       False       False
4  False     False       False        True       False

You can also specify starting values for a subset of the variables.

>>> pprint(bn.sample(init={'Alarm': True, 'Burglary': True}))
{'Alarm': True,
 'Burglary': True,
 'Earthquake': False,
 'John calls': True,
 'Mary calls': False}

The supported inference methods are:

Note that randomness is controlled via the seed parameter, when BayesNet is initialized.

๐Ÿ”ฎ Probabilistic inference

A Bayesian network is a generative model. Therefore, it can be used for many purposes. For instance, it can answer probabilistic queries, such as:

What is the likelihood of there being a burglary if both John and Mary call?

This question can be answered by using the query method, which returns the probability distribution for the possible outcomes. Said otherwise, the query method can be used to look at a query variable's distribution conditioned on a given event. This can be denoted as P(query | event).

>>> bn.query('Burglary', event={'Mary calls': True, 'John calls': True})
Burglary
False    0.715828
True     0.284172
Name: P(Burglary), dtype: float64

We can also answer questions that involve multiple query variables, for instance:

What are the chances that John and Mary call if an earthquake happens?

>>> bn.query('John calls', 'Mary calls', event={'Earthquake': True})
John calls  Mary calls
False       False         0.675854
            True          0.027085
True        False         0.113591
            True          0.183470
Name: P(John calls, Mary calls), dtype: float64

By default, the answer is found via an exact inference procedure. For small networks this isn't very expensive to perform. However, for larger networks, you might want to prefer using approximate inference. The latter is a class of methods that randomly sample the network and return an estimate of the answer. The quality of the estimate increases with the number of iterations that are performed. For instance, you can use Gibbs sampling:

>>> bn.query(
...     'Burglary',
...     event={'Mary calls': True, 'John calls': True},
...     algorithm='gibbs',
...     n_iterations=1000
... )  # doctest: +SKIP
Burglary
False    0.706
True     0.294
Name: P(Burglary), dtype: float64

The supported inference methods are:

As with random sampling, randomness is controlled during BayesNet initialization, via the seed parameter.

โ“ Missing value imputation

A use case for probabilistic inference is to impute missing values. The impute method fills the missing values with the most likely replacements, given the present information. This is usually more accurate than simply replacing by the mean or the most common value. Additionally, such an approach can be much more efficient than model-based iterative imputation.

>>> sample = {
...     'Alarm': True,
...     'Burglary': True,
...     'Earthquake': False,
...     'John calls': None,  # missing
...     'Mary calls': None   # missing
... }

>>> sample = bn.impute(sample)
>>> pprint(sample)
{'Alarm': True,
 'Burglary': True,
 'Earthquake': False,
 'John calls': True,
 'Mary calls': True}

Note that the impute method can be seen as the equivalent of pomegranate's predict method.

๐Ÿคท Likelihood estimation

You can estimate the likelihood of an event with the predict_proba method:

>>> event = {
...     'Alarm': False,
...     'Burglary': False,
...     'Earthquake': False,
...     'John calls': False,
...     'Mary calls': False
... }

>>> bn.predict_proba(event)
0.936742...

In other words, predict_proba computes P(event), whereas the query method computes P(query | event). You may also estimate the likelihood for a partial event. The probabilities for the unobserved variables will be summed out.

>>> event = {'Alarm': True, 'Burglary': False}
>>> bn.predict_proba(event)
0.001576...

This also works for an event with a single variable:

>>> event = {'Alarm': False}
>>> bn.predict_proba(event)
0.997483...

Note that you can also pass a bunch of events to predict_proba, as so:

>>> events = pd.DataFrame([
...     {'Alarm': False, 'Burglary': False, 'Earthquake': False,
...      'John calls': False, 'Mary calls': False},
...
...     {'Alarm': False, 'Burglary': False, 'Earthquake': False,
...      'John calls': True, 'Mary calls': False},
...
...     {'Alarm': True, 'Burglary': True, 'Earthquake': True,
...      'John calls': True, 'Mary calls': True}
... ])

>>> bn.predict_proba(events)
Alarm  Burglary  Earthquake  John calls  Mary calls
False  False     False       False       False         0.936743
                             True        False         0.049302
True   True      True        True        True          0.000001
Name: P(Alarm, Burglary, Earthquake, John calls, Mary calls), dtype: float64

๐Ÿงฎ Parameter estimation

You can determine the values of the P from a dataset. This is a straightforward procedure, as it only requires performing a groupby followed by a value_counts for each CPT.

>>> samples = bn.sample(1000)
>>> bn = bn.fit(samples)

Note that in this case you do not have to call the prepare method because it is done for you implicitly.

If you want to update an already existing Bayesian networks with new observations, then you can use partial_fit:

>>> bn = bn.partial_fit(samples[:500])
>>> bn = bn.partial_fit(samples[500:])

The same result will be obtained whether you use fit once or partial_fit multiple times in succession.

๐Ÿงฑ Structure learning

๐ŸŒณ Chow-Liu trees

A Chow-Liu tree is a tree structure that represents a factorised distribution with maximal likelihood. It's essentially the best tree structure that can be found.

>>> samples = hh.examples.asia().sample(300)
>>> structure = hh.structure.chow_liu(samples)
>>> bn = hh.BayesNet(*structure)

๐Ÿ‘€ Visualization

You can use the graphviz method to obtain a graphviz.Digraph representation.

>>> bn = hh.examples.asia()
>>> dot = bn.graphviz()
>>> path = dot.render('asia', directory='figures', format='svg', cleanup=True)


Note that the graphviz library is not installed by default because it requires a platform dependent binary. Therefore, you have to install it by yourself.

๐Ÿ‘๏ธ Graphical user interface

A side-goal of this project is to provide a user interface to play around with a given user interface. Fortunately, we live in wonderful times where many powerful and opensource tools are available. At the moment, I have a preference for streamlit.

You can install the GUI dependencies by running the following command:

$ pip install git+https://github.com/MaxHalford/sorobn --install-option="--extras-require=gui"

You can then launch a demo by running the sorobn command:

$ sorobn

This will launch a streamlit interface where you can play around with the examples that sorobn provides. You can see a running instance of it in this Streamlit app.

An obvious next step would be to allow users to run this with their own Bayesian networks. Then again, using streamlit is so easy that you might as well do this yourself.

๐Ÿ”ข Support for continuous variables

Bayesian networks that handle both discrete and continuous are said to be hybrid. There are two approaches to deal with continuous variables. The first approach is to use parametric distributions within nodes that pertain to a continuous variable. This has two disadvantages. First, it is complex because there are different cases to handle: a discrete variable conditioned by a continuous one, a continuous variable conditioned by a discrete one, or combinations of the former with the latter. Secondly, such an approach requires having to pick a parametric distribution for each variable. Although there are methods to automate this choice for you, they are expensive and are far from being foolproof.

The second approach is to simply discretize the continuous variables. Although this might seem naive, it is generally a good enough approach and definitely makes things simpler implementation-wise. There are many ways to go about discretising a continuous attribute. For instance, you can apply a quantile-based discretization function. You could also round each number to its closest integer. In some cases you might be able to apply a manual rule. For instance, you can convert a numeric temperature to "cold", "mild", and "hot".

To summarize, we prefer to give the user the flexibility to discretize the variables by herself. Indeed, most of the time the best procedure depends on the problem at hand and cannot be automated adequately.

Toy networks

Several toy networks are available to fool around with in the examples submodule:

Here is some example usage:

>>> bn = hh.examples.sprinkler()

>>> bn.nodes
['Cloudy', 'Rain', 'Sprinkler', 'Wet grass']

>>> pprint(bn.parents)
{'Rain': ['Cloudy'],
 'Sprinkler': ['Cloudy'],
 'Wet grass': ['Rain', 'Sprinkler']}

>>> pprint(bn.children)
{'Cloudy': ['Rain', 'Sprinkler'],
 'Rain': ['Wet grass'],
 'Sprinkler': ['Wet grass']}

Development

# Download and navigate to the source code
git clone https://github.com/MaxHalford/sorobn
cd sorobn

# Install poetry
curl -sSL https://install.python-poetry.org | python3 -

# Install in development mode
poetry install

# Run tests
poetry shell
pytest

License

This project is free and open-source software licensed under the MIT license.

sorobn's People

Contributors

dependabot[bot] avatar maxhalford avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

sorobn's Issues

Example with continuous data

It would be to add an example with continuous to the documentation. Also, the example should contain show how to perform range queries.

Differences in posterior probabilities for `Asia` example network

Hi Max,

thanks so much for setting up such a great library with a very clean and concise API!

I have a quick question regarding the posterior probabilities of the Asia network example:

If I select Dispnea as query variable and no event variables in your demo app, I get the following posterior probabilities:

Dispnea P(Dispnea)
0       0.5640
1       0.4360

These are also the values that you see in other sources using the Asia example (e.g., Bayes Server or Netica)

If I run the following code in Python (3.9.2), I get the following posterior probabilities for the Dispnea node:

>>> import hedgehog as hh
>>> bn = hh.examples.asia()
>>> bn.query("Dispnea", event={})

Dispnea
False    0.602547
True     0.397453
Name: P(Dispnea), dtype: float64

Do you know what is causing these differences, or am I using the API incorrectly?

Thanks a lot!
Daniel

Support for missing data

Ideally, it should be possible to allow for missing data for the following functionalities:

  • Structure learning
  • Parameter estimation
  • Anything else?

Clarify node order in README

sorobn/sorobn/bayes_net.py

Lines 320 to 321 in 5e14193

# The nodes are sorted in topological order. Nodes of the same level are sorted in
# lexicographic order.

Can we have a clear mention of this in the README? I was wondering about the difference in node ordering -- thought it was in order of model definition, but couldn't find this out until I dug into the code.

CPT's require parent nodes in alphabetic order

Hi Max,

thanks again for putting together hedgehog - great library with a great API.

I think there might be a small hiccup (or design choice?) in BayesNet.prepare(). Consider the following example:

import hedgehog as hh
import pandas as pd

edges = pd.DataFrame(
    {
        "Parent Node": ["Input A", "Input B"],
        "Child Node": ["Output", "Output"],
    }
)

bn = hh.BayesNet(*edges.itertuples(index=False, name=None))

bn.P['Input A'] = pd.Series({True: 0.7, False: 0.3})
bn.P['Input B'] = pd.Series({True: 0.4, False: 0.6})

# Manual input of a CPT with columns NOT ordered alphabetically
output_cpt = pd.DataFrame(
    {
        "Input B": [True, True, True, True, False, False, False, False],
        "Input A": [True, True, False, False, True, True, False, False],
        "Output": [
            True,
            False,
            True,
            False,
            True,
            False,
            True,
            False,
        ],
        "Prob": [1, 0, 0, 1, 0.5, 0.5, 0.001, 0.999],
    }
)
bn.P["Output"] = output_cpt.set_index(["Input B", "Input A", "Output"])["Prob"]

If we take a look at bn.P["Output"], we see that the probabilities are integrated correctly.

Input B  Input A  Output
True     True     True      1.000
                  False     0.000
         False    True      0.000
                  False     1.000
False    True     True      0.500
                  False     0.500
         False    True      0.001
                  False     0.999
Name: Prob, dtype: float64

Next, we call prepare():

bn.prepare()

After calling prepare(), we see that the index is sorted (which is completely fine) and the column names are mixed up during renaming, since the parent node names are stored alphabetically as attributes during __init__().

Input A  Input B  Output
False    False    False     0.999
                  True      0.001
         True     False     0.500
                  True      0.500
True     False    False     1.000
                  True      0.000
         True     False     0.000
                  True      1.000
Name: P(Output | Input A, Input B), dtype: float64

A quick workaround for this behaviour is to sort the parent nodes in the input CPT alphabetically. Nevertheless, this is not perfectly intuitive, and I think a better design choice would be to require a series with index names, so an explicit reference can be made. Unfortunately, I am also not able to use the query() method with several nodes specified in event without running prepare() beforehand (e.g., bn.query("Output", event={"Input A": True, "Input B": False}) will give ValueError: cannot join with no overlapping index names before running bn.prepare()).

Happy to get your feedback on this!

Best,
Daniel

Installation failing on Windows due to encoding issue

Hi Max,

sweet library! What a breeze it is to use pandas with a Bayesian network!

I stumbled upon an installation issue, that's easy to fix:

Problem

When installing hedgehog on Windows via
$ pip install git+https://github.com/MaxHalford/hedgehog
this error occurs:

Collecting git+https://github.com/MaxHalford/hedgehog
  Cloning https://github.com/MaxHalford/hedgehog to c:\users\u870378\appdata\local\temp\pip-req-build-1zcx1moe
  Running command git clone -q https://github.com/MaxHalford/hedgehog 'C:\Users\u870378\AppData\Local\Temp\pip-req-build-1zcx1moe'
    ERROR: Command errored out with exit status 1:
     command: 'C:\Users\u870378\AppData\Local\pypoetry\Cache\virtualenvs\ki-servicekompass-5UQ9rpz6-py3.9\Scripts\python.exe' -c 'import sys, setuptools, tokenize; sys.argv[0] = '"'"'C:\\Users\\u870378\\AppData\\Local\\Tem
p\\pip-req-build-1zcx1moe\\setup.py'"'"'; __file__='"'"'C:\\Users\\u870378\\AppData\\Local\\Temp\\pip-req-build-1zcx1moe\\setup.py'"'"';f=getattr(tokenize, '"'"'open'"'"', open)(__file__);code=f.read().replace('"'"'\r\n'"'
"', '"'"'\n'"'"');f.close();exec(compile(code, __file__, '"'"'exec'"'"'))' egg_info --egg-base 'C:\Users\u870378\AppData\Local\Temp\pip-pip-egg-info-a3nzzdf8'
         cwd: C:\Users\u870378\AppData\Local\Temp\pip-req-build-1zcx1moe\
    Complete output (9 lines):
    Traceback (most recent call last):
      File "<string>", line 1, in <module>
      File "C:\Users\u870378\AppData\Local\Temp\pip-req-build-1zcx1moe\setup.py", line 16, in <module>
        long_description=read('README.md'),
      File "C:\Users\u870378\AppData\Local\Temp\pip-req-build-1zcx1moe\setup.py", line 6, in read
        return open(os.path.join(os.path.dirname(__file__), fname)).read()
      File "C:\Program Files\Python39\lib\encodings\cp1252.py", line 23, in decode
        return codecs.charmap_decode(input,self.errors,decoding_table)[0]
    UnicodeDecodeError: 'charmap' codec can't decode byte 0x8d in position 1856: character maps to <undefined>
    ----------------------------------------
WARNING: Discarding git+https://github.com/MaxHalford/hedgehog. Command errored out with exit status 1: python setup.py egg_info Check the logs for full command output.
ERROR: Command errored out with exit status 1: python setup.py egg_info Check the logs for full command output.

Explanation / Solution

This error is caused by Windows' default encoding cp1252. For using utf-8 across platforms, setup.py can be adjusted in line 6:

    return open(os.path.join(os.path.dirname(__file__), fname), encoding="utf-8").read()

Solution was tested on Windows. No change should be noticed on unix systems.

Source: https://stackoverflow.com/questions/49640513/unicodedecodeerror-charmap-codec-cant-decode-byte-0x9d-in-position-x-charac/49642852

Cheers

Python 3.9 requirement

Hi, recently you changed the python requirement to 3.9, which makes it pain to use in Colab (which uses python 3.7). Is it really necessary?

Support more operators in query

Currently, it's only possible to specify single values in the event parameter of the query method. It would nice to be able to support more parameters, such as <, in, >. In fact I think the way to go is to support each operator defined in Python's operator module.

how to interpolate smooth distributions?

Hi there,
I guess I may have already hit a limitation with the library.
Any help would be great, maybe I have to move to a more complex solution.
Anyway here's my issue:

def example_learning():

    import pandas as pd

    samples = pd.DataFrame({"Host":["carl","ermano","jon"],
                       "Detection":["PsExec","PsExec","PsExec"],
                        "Outcome":["TP","FP"],
                       "HourOfDay":[5,10,13]})
    print(samples)

    structure = hh.structure.chow_liu(samples)

    bn = hh.BayesNet(*structure)
    bn = bn.fit(samples)
    bn.prepare()
    '''
    dot = bn.graphviz()

    path = dot.render('asia', directory='figures', format='svg', cleanup=True)
    '''
    print("Probability of detection")
    print(bn.P["Detection"])

    print("Probability of outcome")
    print(bn.P["Outcome"])
    print("Probability of FP at 5 am")
    event = {"Host":"carl","Detection":"PsExec","HourOfDay":5}
    bn.predict_proba(event)
    print("Probability of FP at 6 am")
    # this will fail because is unseen: how do we generalize?
    event = {"Host":"carl","Detection":"PsExec","HourOfDay":6}
    bn.predict_proba(event)

I want to predict the probably of a false positive at 6 am which was not observed in the training set.
I am not sure what is the correct approach here is there a way to assign a smooth distribution across the 24hours so that it will assign a tiny probability that is unobserved?

How other libraries like Pomegrenade handle this kind of situations?
Cheers!

Circular import error?

Hey Max,

Thanks for writing a great library. I recently installed it on my machine and I received this error while trying to implement it in a Jupyter notebook:

import hedgehog as hh

ImportError                               Traceback (most recent call last)
<ipython-input-2-c23a444b1dc9> in <module>
----> 1 import hedgehog as hh

/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/hedgehog/__init__.py in <module>
      2 
      3 from . import examples
----> 4 from . import structure
      5 from .bayes_net import BayesNet
      6 

ImportError: cannot import name 'structure' from partially initialized module 'hedgehog' (most likely due to a circular import) (/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/hedgehog/__init__.py)

Now, this could totally be my fault, but I thought I would call it to your attention.

Order of the variables in an event

Dear Max,
Thank you for the Hedgehog implementation, we are currently using it for some exercise classes at the university of Linz.
We discovered a strange behavior in the "bn.predict_proba(event)" method. More precisely the calculated probabilities change if we sort the random variables in the event differently: the correct probability is given as an output only if the random variables in the event are sorted in an alphabetic order.
This issue is not present in the "bn.query()" method which works great!

Do you have already noticed this issue?
Thanks for the support!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.